Add ERC20-like Token program
This commit is contained in:
parent
29a8823db1
commit
eed3b9db94
|
@ -32,6 +32,7 @@ use system_transaction::SystemTransaction;
|
|||
use tictactoe_dashboard_program::TicTacToeDashboardProgram;
|
||||
use tictactoe_program::TicTacToeProgram;
|
||||
use timing::{duration_as_us, timestamp};
|
||||
use token_program::TokenProgram;
|
||||
use transaction::Transaction;
|
||||
use window::WINDOW_SIZE;
|
||||
|
||||
|
@ -532,6 +533,11 @@ impl Bank {
|
|||
{
|
||||
return Err(BankError::ProgramRuntimeError(instruction_index as u8));
|
||||
}
|
||||
} else if TokenProgram::check_id(&tx_program_id) {
|
||||
if TokenProgram::process_transaction(&tx, instruction_index, program_accounts).is_err()
|
||||
{
|
||||
return Err(BankError::ProgramRuntimeError(instruction_index as u8));
|
||||
}
|
||||
} else if self.loaded_contract(tx_program_id, tx, instruction_index, program_accounts) {
|
||||
} else {
|
||||
return Err(BankError::UnknownContractId(instruction_index as u8));
|
||||
|
|
|
@ -68,6 +68,7 @@ pub mod thin_client;
|
|||
pub mod tictactoe_dashboard_program;
|
||||
pub mod tictactoe_program;
|
||||
pub mod timing;
|
||||
pub mod token_program;
|
||||
pub mod tpu;
|
||||
pub mod transaction;
|
||||
pub mod tvu;
|
||||
|
|
|
@ -0,0 +1,462 @@
|
|||
//! ERC20-like Token program
|
||||
|
||||
use bincode;
|
||||
|
||||
use solana_program_interface::account::Account;
|
||||
use solana_program_interface::pubkey::Pubkey;
|
||||
use std;
|
||||
use transaction::Transaction;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Error {
|
||||
InvalidArgument,
|
||||
InsufficentFunds,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "error")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||
pub struct TokenInfo {
|
||||
/**
|
||||
* Total supply of tokens
|
||||
*/
|
||||
supply: u64,
|
||||
|
||||
/**
|
||||
* Number of base 10 digits to the right of the decimal place in the total supply
|
||||
*/
|
||||
decimals: u8,
|
||||
|
||||
/**
|
||||
* Descriptive name of this token
|
||||
*/
|
||||
name: String,
|
||||
|
||||
/**
|
||||
* Symbol for this token
|
||||
*/
|
||||
symbol: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct TokenAccountInfo {
|
||||
/**
|
||||
* The kind of token this account holds
|
||||
*/
|
||||
token: Pubkey,
|
||||
|
||||
/**
|
||||
* Owner of this account
|
||||
*/
|
||||
owner: Pubkey,
|
||||
|
||||
/**
|
||||
* Amount of tokens this account holds
|
||||
*/
|
||||
amount: u64,
|
||||
|
||||
/**
|
||||
* The source account for the tokens.
|
||||
*
|
||||
* If `source` is None, `amount` belongs to this account.
|
||||
* If `source` is Option<>, `amount` represents an allowance of tokens that
|
||||
* may be transferred from the `source` account.
|
||||
*/
|
||||
source: Option<Pubkey>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
enum Command {
|
||||
NewToken(TokenInfo),
|
||||
NewTokenAccount(),
|
||||
Transfer(u64),
|
||||
Approve(u64),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub enum TokenProgram {
|
||||
Unallocated,
|
||||
Token(TokenInfo),
|
||||
Account(TokenAccountInfo),
|
||||
Invalid,
|
||||
}
|
||||
impl Default for TokenProgram {
|
||||
fn default() -> TokenProgram {
|
||||
TokenProgram::Unallocated
|
||||
}
|
||||
}
|
||||
|
||||
pub const TOKEN_PROGRAM_ID: [u8; 32] = [
|
||||
5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
];
|
||||
|
||||
impl TokenProgram {
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(needless_pass_by_value))]
|
||||
fn map_to_invalid_args(err: std::boxed::Box<bincode::ErrorKind>) -> Error {
|
||||
warn!("invalid argument: {:?}", err);
|
||||
Error::InvalidArgument
|
||||
}
|
||||
|
||||
fn deserialize(input: &[u8]) -> Result<TokenProgram> {
|
||||
if input.is_empty() {
|
||||
Err(Error::InvalidArgument)?;
|
||||
}
|
||||
match input[0] {
|
||||
0 => Ok(TokenProgram::Unallocated),
|
||||
1 => Ok(TokenProgram::Token(
|
||||
bincode::deserialize(&input[1..]).map_err(Self::map_to_invalid_args)?,
|
||||
)),
|
||||
2 => Ok(TokenProgram::Account(
|
||||
bincode::deserialize(&input[1..]).map_err(Self::map_to_invalid_args)?,
|
||||
)),
|
||||
_ => Err(Error::InvalidArgument),
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize(self: &TokenProgram, output: &mut [u8]) -> Result<()> {
|
||||
if output.is_empty() {
|
||||
warn!("serialize fail: ouput.len is 0");
|
||||
Err(Error::InvalidArgument)?;
|
||||
}
|
||||
match self {
|
||||
TokenProgram::Unallocated | TokenProgram::Invalid => Err(Error::InvalidArgument),
|
||||
TokenProgram::Token(token_info) => {
|
||||
output[0] = 1;
|
||||
let writer = std::io::BufWriter::new(&mut output[1..]);
|
||||
bincode::serialize_into(writer, &token_info).map_err(Self::map_to_invalid_args)
|
||||
}
|
||||
TokenProgram::Account(account_info) => {
|
||||
output[0] = 2;
|
||||
let writer = std::io::BufWriter::new(&mut output[1..]);
|
||||
bincode::serialize_into(writer, &account_info).map_err(Self::map_to_invalid_args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_id(program_id: &Pubkey) -> bool {
|
||||
program_id.as_ref() == TOKEN_PROGRAM_ID
|
||||
}
|
||||
|
||||
pub fn id() -> Pubkey {
|
||||
Pubkey::new(&TOKEN_PROGRAM_ID)
|
||||
}
|
||||
|
||||
pub fn process_command_newtoken(
|
||||
tx: &Transaction,
|
||||
pix: usize,
|
||||
token_info: TokenInfo,
|
||||
input_program_accounts: &[TokenProgram],
|
||||
output_program_accounts: &mut Vec<(usize, TokenProgram)>,
|
||||
) -> Result<()> {
|
||||
if input_program_accounts.len() != 2 {
|
||||
error!("Expected 2 accounts");
|
||||
Err(Error::InvalidArgument)?;
|
||||
}
|
||||
|
||||
if let TokenProgram::Account(dest_account) = &input_program_accounts[1] {
|
||||
if tx.key(pix, 0) != Some(&dest_account.token) {
|
||||
info!("account 1 token mismatch");
|
||||
Err(Error::InvalidArgument)?;
|
||||
}
|
||||
|
||||
if dest_account.source.is_some() {
|
||||
info!("account 1 is a delegate and cannot accept tokens");
|
||||
Err(Error::InvalidArgument)?;
|
||||
}
|
||||
|
||||
let mut output_dest_account = dest_account.clone();
|
||||
output_dest_account.amount = token_info.supply;
|
||||
output_program_accounts.push((1, TokenProgram::Account(output_dest_account)));
|
||||
} else {
|
||||
info!("account 1 invalid");
|
||||
Err(Error::InvalidArgument)?;
|
||||
}
|
||||
|
||||
if input_program_accounts[0] != TokenProgram::Unallocated {
|
||||
info!("account 0 not available");
|
||||
Err(Error::InvalidArgument)?;
|
||||
}
|
||||
output_program_accounts.push((0, TokenProgram::Token(token_info)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn process_command_newaccount(
|
||||
tx: &Transaction,
|
||||
pix: usize,
|
||||
input_program_accounts: &[TokenProgram],
|
||||
output_program_accounts: &mut Vec<(usize, TokenProgram)>,
|
||||
) -> Result<()> {
|
||||
// key 0 - Destination new token account
|
||||
// key 1 - Owner of the account
|
||||
// key 2 - Token this account is associated with
|
||||
// key 3 - Source account that this account is a delegate for (optional)
|
||||
if input_program_accounts.len() < 3 {
|
||||
error!("Expected 3 accounts");
|
||||
Err(Error::InvalidArgument)?;
|
||||
}
|
||||
if input_program_accounts[0] != TokenProgram::Unallocated {
|
||||
info!("account 0 is already allocated");
|
||||
Err(Error::InvalidArgument)?;
|
||||
}
|
||||
|
||||
let mut token_account_info = TokenAccountInfo {
|
||||
token: *tx.key(pix, 2).unwrap(),
|
||||
owner: *tx.key(pix, 1).unwrap(),
|
||||
amount: 0,
|
||||
source: None,
|
||||
};
|
||||
if input_program_accounts.len() >= 4 {
|
||||
token_account_info.source = Some(*tx.key(pix, 3).unwrap());
|
||||
}
|
||||
output_program_accounts.push((0, TokenProgram::Account(token_account_info)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn process_command_transfer(
|
||||
tx: &Transaction,
|
||||
pix: usize,
|
||||
amount: u64,
|
||||
input_program_accounts: &[TokenProgram],
|
||||
output_program_accounts: &mut Vec<(usize, TokenProgram)>,
|
||||
) -> Result<()> {
|
||||
if input_program_accounts.len() < 3 {
|
||||
error!("Expected 3 accounts");
|
||||
Err(Error::InvalidArgument)?;
|
||||
}
|
||||
|
||||
if let (TokenProgram::Account(source_account), TokenProgram::Account(dest_account)) =
|
||||
(&input_program_accounts[1], &input_program_accounts[2])
|
||||
{
|
||||
if source_account.token != dest_account.token {
|
||||
info!("account 1/2 token mismatch");
|
||||
Err(Error::InvalidArgument)?;
|
||||
}
|
||||
|
||||
if dest_account.source.is_some() {
|
||||
info!("account 2 is a delegate and cannot accept tokens");
|
||||
Err(Error::InvalidArgument)?;
|
||||
}
|
||||
|
||||
if Some(&source_account.owner) != tx.key(pix, 0) {
|
||||
info!("owner of account 1 not present");
|
||||
Err(Error::InvalidArgument)?;
|
||||
}
|
||||
|
||||
if source_account.amount < amount {
|
||||
Err(Error::InsufficentFunds)?;
|
||||
}
|
||||
|
||||
let mut output_source_account = source_account.clone();
|
||||
output_source_account.amount -= amount;
|
||||
output_program_accounts.push((1, TokenProgram::Account(output_source_account)));
|
||||
|
||||
if source_account.source.is_some() {
|
||||
if input_program_accounts.len() != 4 {
|
||||
error!("Expected 4 accounts");
|
||||
Err(Error::InvalidArgument)?;
|
||||
}
|
||||
|
||||
let delegate_account = source_account;
|
||||
if let TokenProgram::Account(source_account) = &input_program_accounts[3] {
|
||||
if source_account.token != delegate_account.token {
|
||||
info!("account 1/3 token mismatch");
|
||||
Err(Error::InvalidArgument)?;
|
||||
}
|
||||
if Some(&delegate_account.source.unwrap()) != tx.key(pix, 3) {
|
||||
info!("Account 1 is not a delegate of account 3");
|
||||
Err(Error::InvalidArgument)?;
|
||||
}
|
||||
|
||||
if source_account.amount < amount {
|
||||
Err(Error::InsufficentFunds)?;
|
||||
}
|
||||
|
||||
let mut output_source_account = source_account.clone();
|
||||
output_source_account.amount -= amount;
|
||||
output_program_accounts.push((3, TokenProgram::Account(output_source_account)));
|
||||
} else {
|
||||
info!("account 3 is an invalid account");
|
||||
Err(Error::InvalidArgument)?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut output_dest_account = dest_account.clone();
|
||||
output_dest_account.amount += amount;
|
||||
output_program_accounts.push((2, TokenProgram::Account(output_dest_account)));
|
||||
} else {
|
||||
info!("account 1 and/or 2 are invalid accounts");
|
||||
Err(Error::InvalidArgument)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn process_command_approve(
|
||||
tx: &Transaction,
|
||||
pix: usize,
|
||||
amount: u64,
|
||||
input_program_accounts: &[TokenProgram],
|
||||
output_program_accounts: &mut Vec<(usize, TokenProgram)>,
|
||||
) -> Result<()> {
|
||||
if input_program_accounts.len() != 3 {
|
||||
error!("Expected 3 accounts");
|
||||
Err(Error::InvalidArgument)?;
|
||||
}
|
||||
|
||||
if let (TokenProgram::Account(source_account), TokenProgram::Account(delegate_account)) =
|
||||
(&input_program_accounts[1], &input_program_accounts[2])
|
||||
{
|
||||
if source_account.token != delegate_account.token {
|
||||
info!("account 1/2 token mismatch");
|
||||
Err(Error::InvalidArgument)?;
|
||||
}
|
||||
|
||||
if Some(&source_account.owner) != tx.key(pix, 0) {
|
||||
info!("owner of account 1 not present");
|
||||
Err(Error::InvalidArgument)?;
|
||||
}
|
||||
|
||||
if source_account.source.is_some() {
|
||||
info!("account 1 is a delegate");
|
||||
Err(Error::InvalidArgument)?;
|
||||
}
|
||||
|
||||
if Some(&delegate_account.source.unwrap()) != tx.key(pix, 1) {
|
||||
info!("account 2 is not a delegate of account 1");
|
||||
Err(Error::InvalidArgument)?;
|
||||
}
|
||||
|
||||
let mut output_delegate_account = delegate_account.clone();
|
||||
output_delegate_account.amount = amount;
|
||||
output_program_accounts.push((2, TokenProgram::Account(output_delegate_account)));
|
||||
} else {
|
||||
info!("account 1 and/or 2 are invalid accounts");
|
||||
Err(Error::InvalidArgument)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn process_transaction(
|
||||
tx: &Transaction,
|
||||
pix: usize,
|
||||
accounts: &mut [&mut Account],
|
||||
) -> Result<()> {
|
||||
let command = bincode::deserialize::<Command>(&tx.userdata(pix))
|
||||
.map_err(Self::map_to_invalid_args)?;
|
||||
info!("process_transaction: command={:?}", command);
|
||||
|
||||
let input_program_accounts: Vec<TokenProgram> = accounts
|
||||
.iter()
|
||||
.map(|account| {
|
||||
if Self::check_id(&account.program_id) {
|
||||
Self::deserialize(&account.userdata)
|
||||
.map_err(|err| {
|
||||
info!("deserialize failed: {:?}", err);
|
||||
TokenProgram::Invalid
|
||||
}).unwrap()
|
||||
} else {
|
||||
TokenProgram::Invalid
|
||||
}
|
||||
}).collect();
|
||||
|
||||
let mut output_program_accounts: Vec<(_, _)> = vec![];
|
||||
|
||||
match command {
|
||||
Command::NewToken(token_info) => Self::process_command_newtoken(
|
||||
tx,
|
||||
pix,
|
||||
token_info,
|
||||
&input_program_accounts,
|
||||
&mut output_program_accounts,
|
||||
)?,
|
||||
Command::NewTokenAccount() => Self::process_command_newaccount(
|
||||
tx,
|
||||
pix,
|
||||
&input_program_accounts,
|
||||
&mut output_program_accounts,
|
||||
)?,
|
||||
|
||||
Command::Transfer(amount) => Self::process_command_transfer(
|
||||
tx,
|
||||
pix,
|
||||
amount,
|
||||
&input_program_accounts,
|
||||
&mut output_program_accounts,
|
||||
)?,
|
||||
|
||||
Command::Approve(amount) => Self::process_command_approve(
|
||||
tx,
|
||||
pix,
|
||||
amount,
|
||||
&input_program_accounts,
|
||||
&mut output_program_accounts,
|
||||
)?,
|
||||
}
|
||||
|
||||
for (index, program_account) in &output_program_accounts {
|
||||
info!(
|
||||
"output_program_account: index={} userdata={:?}",
|
||||
index, program_account
|
||||
);
|
||||
Self::serialize(program_account, &mut accounts[*index].userdata)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
#[test]
|
||||
pub fn serde() {
|
||||
assert_eq!(TokenProgram::deserialize(&[0]), Ok(TokenProgram::default()));
|
||||
|
||||
let mut userdata = vec![0; 256];
|
||||
|
||||
let account = TokenProgram::Account(TokenAccountInfo {
|
||||
token: Pubkey::new(&[1; 32]),
|
||||
owner: Pubkey::new(&[2; 32]),
|
||||
amount: 123,
|
||||
source: None,
|
||||
});
|
||||
assert!(account.serialize(&mut userdata).is_ok());
|
||||
assert_eq!(TokenProgram::deserialize(&userdata), Ok(account));
|
||||
|
||||
let account = TokenProgram::Token(TokenInfo {
|
||||
supply: 12345,
|
||||
decimals: 2,
|
||||
name: "A test token".to_string(),
|
||||
symbol: "TEST".to_string(),
|
||||
});
|
||||
assert!(account.serialize(&mut userdata).is_ok());
|
||||
assert_eq!(TokenProgram::deserialize(&userdata), Ok(account));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn serde_expect_fail() {
|
||||
let mut userdata = vec![0; 256];
|
||||
|
||||
// Certain TokenProgram's may not be serialized
|
||||
let account = TokenProgram::default();
|
||||
assert_eq!(account, TokenProgram::Unallocated);
|
||||
assert!(account.serialize(&mut userdata).is_err());
|
||||
assert!(account.serialize(&mut userdata).is_err());
|
||||
let account = TokenProgram::Invalid;
|
||||
assert!(account.serialize(&mut userdata).is_err());
|
||||
|
||||
// Bad deserialize userdata
|
||||
assert!(TokenProgram::deserialize(&[]).is_err());
|
||||
assert!(TokenProgram::deserialize(&[1]).is_err());
|
||||
assert!(TokenProgram::deserialize(&[1, 2]).is_err());
|
||||
assert!(TokenProgram::deserialize(&[2, 2]).is_err());
|
||||
assert!(TokenProgram::deserialize(&[3]).is_err());
|
||||
}
|
||||
|
||||
// Note: business logic tests are located in the @solana/web3.js test suite
|
||||
}
|
Loading…
Reference in New Issue