diff --git a/token/inc/token.h b/token/inc/token.h index b9eef7e39e1..7da32665a22 100644 --- a/token/inc/token.h +++ b/token/inc/token.h @@ -44,7 +44,8 @@ typedef enum Token_TokenInstruction_Tag { */ InitializeMint, /** - * Initializes a new account to hold tokens. + * Initializes a new account to hold tokens. If this account is associated with the native mint + * then the token balance of the initialized account will be equal to the amount of SOL in the account. * * The `InitializeAccount` instruction requires no signers and MUST be included within * the same Transaction as the system program's `CreateInstruction` that creates the account @@ -75,7 +76,9 @@ typedef enum Token_TokenInstruction_Tag { */ InitializeMultisig, /** - * Transfers tokens from one account to another either directly or via a delegate. + * Transfers tokens from one account to another either directly or via a delegate. If this + * account is associated with the native mint then equal amounts of SOL and Tokens will be + * transferred to the destination account. * * Accounts expected by this instruction: * @@ -141,7 +144,7 @@ typedef enum Token_TokenInstruction_Tag { */ SetOwner, /** - * Mints new tokens to an account. + * Mints new tokens to an account. The native mint does not support minting. * * Accounts expected by this instruction: * @@ -158,7 +161,8 @@ typedef enum Token_TokenInstruction_Tag { */ MintTo, /** - * Burns tokens by removing them from an account and thus circulation. + * Burns tokens by removing them from an account. `Burn` does not support accounts + * associated with the native mint, use `CloseAccount` instead. * * Accounts expected by this instruction: * @@ -168,10 +172,28 @@ typedef enum Token_TokenInstruction_Tag { * * * Multisignature owner/delegate * 0. `[writable]` The account to burn from. - * 1. `[]` The account's multisignature owner/delegate + * 1. `[]` The account's multisignature owner/delegate. * 2. ..2+M '[signer]' M signer accounts. */ Burn, + /** + * Close an account by transferring all its SOL to the destination account. + * Non-native accounts may only be closed if its token amount is zero. + * + * Accounts expected by this instruction: + * + * * Single owner/delegate + * 0. `[writable]` The account to close. + * 1. '[writable]' The destination account. + * 2. `[signer]` The account's owner. + * + * * Multisignature owner/delegate + * 0. `[writable]` The account to close. + * 1. '[writable]' The destination account. + * 2. `[]` The account's multisignature owner. + * 3. ..3+M '[signer]' M signer accounts. + */ + CloseAccount, } Token_TokenInstruction_Tag; typedef struct Token_InitializeMint_Body { @@ -304,6 +326,10 @@ typedef struct Token_Account { * Is `true` if this structure has been initialized */ bool is_initialized; + /** + * Is this a native token + */ + bool is_native; /** * The amount delegated */ diff --git a/token/js/cli/main.js b/token/js/cli/main.js index dfb747c6e02..a886a6353f4 100644 --- a/token/js/cli/main.js +++ b/token/js/cli/main.js @@ -14,8 +14,10 @@ import { failOnApproveOverspend, setOwner, mintTo, - burn, multisig, + burn, + closeAccount, + nativeToken, } from './token-test'; async function main() { @@ -37,10 +39,14 @@ async function main() { await setOwner(); console.log('Run test: mintTo'); await mintTo(); - console.log('Run test: burn'); - await burn(); console.log('Run test: multisig'); await multisig(); + console.log('Run test: burn'); + await burn(); + console.log('Run test: closeAccount'); + await closeAccount(); + console.log('Run test: nativeToken'); + await nativeToken(); console.log('Success\n'); } diff --git a/token/js/cli/token-test.js b/token/js/cli/token-test.js index 04b7fe1fdae..5cc8baa6609 100644 --- a/token/js/cli/token-test.js +++ b/token/js/cli/token-test.js @@ -387,3 +387,75 @@ export async function multisig(): Promise { assert(accountInfo.owner.equals(newOwner)); } } + +export async function closeAccount(): Promise { + const connection = await getConnection(); + const owner = new Account(); + const close = await testToken.createAccount(owner.publicKey); + + let close_balance; + let info = await connection.getAccountInfo(close); + if (info != null) { + close_balance = info.lamports; + } else { + throw new Error('Account not found'); + } + + const balanceNeeded = + await connection.getMinimumBalanceForRentExemption(0); + const dest = await newAccountWithLamports(connection, balanceNeeded); + + info = await connection.getAccountInfo(dest.publicKey); + if (info != null) { + assert(info.lamports == balanceNeeded); + } else { + throw new Error('Account not found'); + } + + await testToken.closeAccount(close, dest.publicKey, owner, []); + info = await connection.getAccountInfo(close); + if (info != null) { + throw new Error('Account not closed'); + } + info = await connection.getAccountInfo(dest.publicKey); + if (info != null) { + assert(info.lamports == balanceNeeded + close_balance); + } else { + throw new Error('Account not found'); + } +} + +export async function nativeToken(): Promise { + const connection = await getConnection(); + + const mintPublicKey = new PublicKey('So11111111111111111111111111111111111111111'); + const payer = await newAccountWithLamports(connection, 100000000000 /* wag */); + const token = new Token(connection, mintPublicKey, programId, payer); + const owner = new Account(); + const native = await token.createAccount(owner.publicKey); + let accountInfo = await token.getAccountInfo(native); + assert(accountInfo.isNative); + let balance; + let info = await connection.getAccountInfo(native); + if (info != null) { + balance = info.lamports; + } else { + throw new Error('Account not found'); + } + + const balanceNeeded = + await connection.getMinimumBalanceForRentExemption(0); + const dest = await newAccountWithLamports(connection, balanceNeeded); + await token.closeAccount(native, dest.publicKey, owner, []); + info = await connection.getAccountInfo(native); + if (info != null) { + throw new Error('Account not burned'); + } + info = await connection.getAccountInfo(dest.publicKey); + if (info != null) { + assert(info.lamports == balanceNeeded + balance); + } else { + throw new Error('Account not found'); + } + +} diff --git a/token/js/client/token.js b/token/js/client/token.js index 238ca751642..b7896f5dc5b 100644 --- a/token/js/client/token.js +++ b/token/js/client/token.js @@ -61,14 +61,16 @@ type MintInfo = {| * Owner of the mint, given authority to mint new tokens */ owner: null | PublicKey, + /** * Number of base 10 digits to the right of the decimal place */ decimals: number, + /** * Is this mint initialized */ - initialized: Boolean, + initialized: boolean, |}; const MintLayout = BufferLayout.struct([ @@ -107,6 +109,16 @@ type AccountInfo = {| * The amount of tokens the delegate authorized to the delegate */ delegatedAmount: TokenAmount, + + /** + * Is this account initialized + */ + isInitialized: boolean, + + /** + * Is this a native token account + */ + isNative: boolean, |}; /** @@ -119,7 +131,7 @@ const AccountLayout = BufferLayout.struct([ BufferLayout.u32('option'), Layout.publicKey('delegate'), BufferLayout.u8('is_initialized'), - BufferLayout.u8('padding'), + BufferLayout.u8('is_native'), BufferLayout.u16('padding'), Layout.uint64('delegatedAmount'), ]); @@ -139,6 +151,11 @@ type MultisigInfo = {| */ n: number, + /** + * Is this mint initialized + */ + initialized: boolean, + /** * The signers */ @@ -512,6 +529,8 @@ export class Token { accountInfo.mint = new PublicKey(accountInfo.mint); accountInfo.owner = new PublicKey(accountInfo.owner); accountInfo.amount = TokenAmount.fromBuffer(accountInfo.amount); + accountInfo.isInitialized = accountInfo.isInitialized != 0; + accountInfo.isNative = accountInfo.isNative != 0; if (accountInfo.option === 0) { accountInfo.delegate = null; accountInfo.delegatedAmount = new TokenAmount(); @@ -592,7 +611,7 @@ export class Token { signers = multiSigners; } return await sendAndConfirmTransaction( - 'transfer', + 'Transfer', this.connection, new Transaction().add( this.transferInstruction( @@ -634,7 +653,7 @@ export class Token { signers = multiSigners; } await sendAndConfirmTransaction( - 'approve', + 'Approve', this.connection, new Transaction().add( this.approveInstruction(account, delegate, ownerPublicKey, multiSigners, amount), @@ -666,7 +685,7 @@ export class Token { signers = multiSigners; } await sendAndConfirmTransaction( - 'revoke', + 'Revoke', this.connection, new Transaction().add( this.revokeInstruction(account, ownerPublicKey, multiSigners), @@ -700,7 +719,7 @@ export class Token { signers = multiSigners; } await sendAndConfirmTransaction( - 'setOwneer', + 'SetOwner', this.connection, new Transaction().add( this.setOwnerInstruction(owned, newOwner, ownerPublicKey, multiSigners), @@ -735,7 +754,7 @@ export class Token { signers = multiSigners; } await sendAndConfirmTransaction( - 'mintTo', + 'MintTo', this.connection, new Transaction().add(this.mintToInstruction(dest, ownerPublicKey, multiSigners, amount)), this.payer, @@ -767,7 +786,7 @@ export class Token { signers = multiSigners; } await sendAndConfirmTransaction( - 'burn', + 'Burn', this.connection, new Transaction().add(this.burnInstruction(account, ownerPublicKey, multiSigners, amount)), this.payer, @@ -775,6 +794,37 @@ export class Token { ); } + /** + * Burn account + * + * @param account Account to burn + * @param authority account owner + * @param multiSigners Signing accounts if `owner` is a multiSig + */ + async closeAccount( + account: PublicKey, + dest: PublicKey, + owner: Account | PublicKey, + multiSigners: Array, + ): Promise { + let ownerPublicKey; + let signers; + if (owner instanceof Account) { + ownerPublicKey = owner.publicKey; + signers = [owner]; + } else { + ownerPublicKey = owner; + signers = multiSigners; + } + await sendAndConfirmTransaction( + 'CloseAccount', + this.connection, + new Transaction().add(this.closeAccountInstruction(account, dest, ownerPublicKey, multiSigners)), + this.payer, + ...signers, + ); + } + /** * Construct a Transfer instruction * @@ -1044,4 +1094,44 @@ export class Token { data, }); } + + /** + * Construct a Burn instruction + * + * @param account Account to burn tokens from + * @param owner account owner + * @param multiSigners Signing accounts if `owner` is a multiSig + */ + closeAccountInstruction( + account: PublicKey, + dest: PublicKey, + owner: Account | PublicKey, + multiSigners: Array, + ): TransactionInstruction { + const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction')]); + const data = Buffer.alloc(dataLayout.span); + dataLayout.encode( + { + instruction: 9, // CloseAccount instruction + }, + data, + ); + + let keys = [ + {pubkey: account, isSigner: false, isWritable: true}, + {pubkey: dest, isSigner: false, isWritable: true}, + ]; + if (owner instanceof Account) { + keys.push({pubkey: owner.publicKey, isSigner: true, isWritable: false}); + } else { + keys.push({pubkey: owner, isSigner: false, isWritable: false}); + multiSigners.forEach(signer => keys.push({pubkey: signer.publicKey, isSigner: true, isWritable: false})); + } + + return new TransactionInstruction({ + keys, + programId: this.programId, + data, + }); + } } diff --git a/token/src/error.rs b/token/src/error.rs index 3775f714ea9..938271c19fd 100644 --- a/token/src/error.rs +++ b/token/src/error.rs @@ -39,6 +39,15 @@ pub enum TokenError { /// State is uninitialized. #[error("State is unititialized")] UninitializedState, + /// Instruction does not support native tokens + #[error("Instruction does not support native tokens")] + NativeNotSupported, + /// Invalid instruction + #[error("Invalid instruction")] + InvalidInstruction, + /// Non-native account can only be closed if its balance is zero + #[error("Non-native account can only be closed if its balance is zero")] + NonNativeHasBalance, } impl From for ProgramError { fn from(e: TokenError) -> Self { @@ -71,6 +80,13 @@ impl PrintProgramError for TokenError { info!("Error: Invalid number of required signers") } TokenError::UninitializedState => info!("Error: State is uninitialized"), + TokenError::NativeNotSupported => { + info!("Error: Instruction does not support native tokens") + } + TokenError::InvalidInstruction => info!("Error: Invalid instruction"), + TokenError::NonNativeHasBalance => { + info!("Non-native account can only be closed if its balance is zero") + } } } } diff --git a/token/src/instruction.rs b/token/src/instruction.rs index 14fe45e85f4..64cfc3da2fa 100644 --- a/token/src/instruction.rs +++ b/token/src/instruction.rs @@ -38,7 +38,8 @@ pub enum TokenInstruction { /// Number of base 10 digits to the right of the decimal place. decimals: u8, }, - /// Initializes a new account to hold tokens. + /// Initializes a new account to hold tokens. If this account is associated with the native mint + /// then the token balance of the initialized account will be equal to the amount of SOL in the account. /// /// The `InitializeAccount` instruction requires no signers and MUST be included within /// the same Transaction as the system program's `CreateInstruction` that creates the account @@ -68,7 +69,9 @@ pub enum TokenInstruction { /// The number of signers (M) required to validate this multisignature account. m: u8, }, - /// Transfers tokens from one account to another either directly or via a delegate. + /// Transfers tokens from one account to another either directly or via a delegate. If this + /// account is associated with the native mint then equal amounts of SOL and Tokens will be + /// transferred to the destination account. /// /// Accounts expected by this instruction: /// @@ -133,7 +136,7 @@ pub enum TokenInstruction { /// 2. `[]` The mint's or account's multisignature owner. /// 3. ..3+M '[signer]' M signer accounts SetOwner, - /// Mints new tokens to an account. + /// Mints new tokens to an account. The native mint does not support minting. /// /// Accounts expected by this instruction: /// @@ -151,7 +154,8 @@ pub enum TokenInstruction { /// The amount of new tokens to mint. amount: u64, }, - /// Burns tokens by removing them from an account and thus circulation. + /// Burns tokens by removing them from an account. `Burn` does not support accounts + /// associated with the native mint, use `CloseAccount` instead. /// /// Accounts expected by this instruction: /// @@ -161,23 +165,39 @@ pub enum TokenInstruction { /// /// * Multisignature owner/delegate /// 0. `[writable]` The account to burn from. - /// 1. `[]` The account's multisignature owner/delegate + /// 1. `[]` The account's multisignature owner/delegate. /// 2. ..2+M '[signer]' M signer accounts. Burn { /// The amount of tokens to burn. amount: u64, }, + /// Close an account by transferring all its SOL to the destination account. + /// Non-native accounts may only be closed if its token amount is zero. + /// + /// Accounts expected by this instruction: + /// + /// * Single owner/delegate + /// 0. `[writable]` The account to close. + /// 1. '[writable]' The destination account. + /// 2. `[signer]` The account's owner. + /// + /// * Multisignature owner/delegate + /// 0. `[writable]` The account to close. + /// 1. '[writable]' The destination account. + /// 2. `[]` The account's multisignature owner. + /// 3. ..3+M '[signer]' M signer accounts. + CloseAccount, } impl TokenInstruction { /// Unpacks a byte buffer into a [TokenInstruction](enum.TokenInstruction.html). pub fn unpack(input: &[u8]) -> Result { if input.len() < size_of::() { - return Err(ProgramError::InvalidAccountData); + return Err(TokenError::InvalidInstruction.into()); } Ok(match input[0] { 0 => { if input.len() < size_of::() + size_of::() + size_of::() { - return Err(ProgramError::InvalidAccountData); + return Err(TokenError::InvalidInstruction.into()); } #[allow(clippy::cast_ptr_alignment)] let amount = unsafe { *(&input[size_of::()] as *const u8 as *const u64) }; @@ -188,7 +208,7 @@ impl TokenInstruction { 1 => Self::InitializeAccount, 2 => { if input.len() < size_of::() + size_of::() { - return Err(ProgramError::InvalidAccountData); + return Err(TokenError::InvalidInstruction.into()); } #[allow(clippy::cast_ptr_alignment)] let m = unsafe { *(&input[1] as *const u8) }; @@ -196,7 +216,7 @@ impl TokenInstruction { } 3 => { if input.len() < size_of::() + size_of::() { - return Err(ProgramError::InvalidAccountData); + return Err(TokenError::InvalidInstruction.into()); } #[allow(clippy::cast_ptr_alignment)] let amount = unsafe { *(&input[size_of::()] as *const u8 as *const u64) }; @@ -204,7 +224,7 @@ impl TokenInstruction { } 4 => { if input.len() < size_of::() + size_of::() { - return Err(ProgramError::InvalidAccountData); + return Err(TokenError::InvalidInstruction.into()); } #[allow(clippy::cast_ptr_alignment)] let amount = unsafe { *(&input[size_of::()] as *const u8 as *const u64) }; @@ -214,7 +234,7 @@ impl TokenInstruction { 6 => Self::SetOwner, 7 => { if input.len() < size_of::() + size_of::() { - return Err(ProgramError::InvalidAccountData); + return Err(TokenError::InvalidInstruction.into()); } #[allow(clippy::cast_ptr_alignment)] let amount = unsafe { *(&input[size_of::()] as *const u8 as *const u64) }; @@ -222,13 +242,14 @@ impl TokenInstruction { } 8 => { if input.len() < size_of::() + size_of::() { - return Err(ProgramError::InvalidAccountData); + return Err(TokenError::InvalidInstruction.into()); } #[allow(clippy::cast_ptr_alignment)] let amount = unsafe { *(&input[size_of::()] as *const u8 as *const u64) }; Self::Burn { amount } } - _ => return Err(ProgramError::InvalidAccountData), + 9 => Self::CloseAccount, + _ => return Err(TokenError::InvalidInstruction.into()), }) } @@ -278,6 +299,7 @@ impl TokenInstruction { let value = unsafe { &mut *(&mut output[size_of::()] as *mut u8 as *mut u64) }; *value = *amount; } + Self::CloseAccount => output[0] = 9, } Ok(output) } @@ -520,8 +542,36 @@ pub fn burn( ) -> Result { let data = TokenInstruction::Burn { amount }.pack()?; + let mut accounts = Vec::with_capacity(2 + signer_pubkeys.len()); + accounts.push(AccountMeta::new(*account_pubkey, false)); + accounts.push(AccountMeta::new_readonly( + *authority_pubkey, + signer_pubkeys.is_empty(), + )); + for signer_pubkey in signer_pubkeys.iter() { + accounts.push(AccountMeta::new(**signer_pubkey, true)); + } + + Ok(Instruction { + program_id: *token_program_id, + accounts, + data, + }) +} + +/// Creates an `CloseAccount` instruction. +pub fn close_account( + token_program_id: &Pubkey, + account_pubkey: &Pubkey, + dest_pubkey: &Pubkey, + authority_pubkey: &Pubkey, + signer_pubkeys: &[&Pubkey], +) -> Result { + let data = TokenInstruction::CloseAccount.pack()?; + let mut accounts = Vec::with_capacity(3 + signer_pubkeys.len()); accounts.push(AccountMeta::new(*account_pubkey, false)); + accounts.push(AccountMeta::new(*dest_pubkey, false)); accounts.push(AccountMeta::new_readonly( *authority_pubkey, signer_pubkeys.is_empty(), diff --git a/token/src/lib.rs b/token/src/lib.rs index 38a9efeb2c4..b8f752a912a 100644 --- a/token/src/lib.rs +++ b/token/src/lib.rs @@ -4,6 +4,7 @@ pub mod error; pub mod instruction; +pub mod native_mint; pub mod option; pub mod processor; pub mod state; diff --git a/token/src/native_mint.rs b/token/src/native_mint.rs new file mode 100644 index 00000000000..b7ec9a25fe0 --- /dev/null +++ b/token/src/native_mint.rs @@ -0,0 +1,4 @@ +//! The Mint that represents the native token + +// The Mint for native SOL Token accounts +solana_sdk::declare_id!("So11111111111111111111111111111111111111111"); diff --git a/token/src/state.rs b/token/src/state.rs index 8c34792544a..36a6d52c116 100644 --- a/token/src/state.rs +++ b/token/src/state.rs @@ -45,6 +45,8 @@ pub struct Account { pub delegate: COption, /// Is `true` if this structure has been initialized pub is_initialized: bool, + /// Is this a native token + pub is_native: bool, /// The amount delegated pub delegated_amount: u64, } @@ -135,10 +137,16 @@ impl State { account.mint = *mint_info.key; account.owner = *owner_info.key; - account.amount = 0; account.delegate = COption::None; account.delegated_amount = 0; account.is_initialized = true; + if *mint_info.key == crate::native_mint::id() { + account.is_native = true; + account.amount = new_account_info.lamports(); + } else { + account.is_native = false; + account.amount = 0; + }; Ok(()) } @@ -220,6 +228,11 @@ impl State { source_account.amount -= amount; dest_account.amount += amount; + if source_account.is_native { + **source_account_info.lamports.borrow_mut() -= amount; + **dest_account_info.lamports.borrow_mut() += amount; + } + Ok(()) } @@ -325,6 +338,16 @@ impl State { let dest_account_info = next_account_info(account_info_iter)?; let owner_info = next_account_info(account_info_iter)?; + let mut dest_account_data = dest_account_info.data.borrow_mut(); + let mut dest_account: &mut Account = Self::unpack(&mut dest_account_data)?; + + if dest_account.is_native { + return Err(TokenError::NativeNotSupported.into()); + } + if mint_info.key != &dest_account.mint { + return Err(TokenError::MintMismatch.into()); + } + let mut mint_info_data = mint_info.data.borrow_mut(); let mint: &mut Mint = Self::unpack(&mut mint_info_data)?; @@ -337,13 +360,6 @@ impl State { } } - let mut dest_account_data = dest_account_info.data.borrow_mut(); - let mut dest_account: &mut Account = Self::unpack(&mut dest_account_data)?; - - if mint_info.key != &dest_account.mint { - return Err(TokenError::MintMismatch.into()); - } - dest_account.amount += amount; Ok(()) @@ -362,6 +378,9 @@ impl State { let mut source_data = source_account_info.data.borrow_mut(); let source_account: &mut Account = Self::unpack(&mut source_data)?; + if source_account.is_native { + return Err(TokenError::NativeNotSupported.into()); + } if source_account.amount < amount { return Err(TokenError::InsufficientFunds.into()); } @@ -396,6 +415,34 @@ impl State { Ok(()) } + /// Processes a [CloseAccount](enum.TokenInstruction.html) instruction. + pub fn process_close_account(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + let source_account_info = next_account_info(account_info_iter)?; + let dest_account_info = next_account_info(account_info_iter)?; + let authority_info = next_account_info(account_info_iter)?; + + let mut source_data = source_account_info.data.borrow_mut(); + let source_account: &mut Account = Self::unpack(&mut source_data)?; + + if !source_account.is_native && source_account.amount != 0 { + return Err(TokenError::NonNativeHasBalance.into()); + } + + Self::validate_owner( + program_id, + &source_account.owner, + authority_info, + account_info_iter.as_slice(), + )?; + + **dest_account_info.lamports.borrow_mut() += source_account_info.lamports(); + **source_account_info.lamports.borrow_mut() = 0; + source_account.amount = 0; + + Ok(()) + } + /// Processes an [Instruction](enum.Instruction.html). pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult { let instruction = TokenInstruction::unpack(input)?; @@ -437,6 +484,10 @@ impl State { info!("Instruction: Burn"); Self::process_burn(program_id, accounts, amount) } + TokenInstruction::CloseAccount => { + info!("Instruction: CloseAccount"); + Self::process_close_account(program_id, accounts) + } } } @@ -505,8 +556,8 @@ solana_sdk_bpf_test::stubs!(); mod tests { use super::*; use crate::instruction::{ - approve, burn, initialize_account, initialize_mint, initialize_multisig, mint_to, revoke, - set_owner, transfer, + approve, burn, close_account, initialize_account, initialize_mint, initialize_multisig, + mint_to, revoke, set_owner, transfer, }; use solana_sdk::{ account::Account as SolanaAccount, account_info::create_is_signer_account_infos, @@ -1972,4 +2023,244 @@ mod tests { ); signers[5].is_signer = true; } + + #[test] + fn test_close_account() { + let program_id = pubkey_rand(); + let mint_key = pubkey_rand(); + let mut mint_account = SolanaAccount::new(0, size_of::(), &program_id); + let account_key = pubkey_rand(); + let mut account_account = SolanaAccount::new(42, size_of::(), &program_id); + let account2_key = pubkey_rand(); + let mut account2_account = SolanaAccount::new(2, size_of::(), &program_id); + let account3_key = pubkey_rand(); + let mut account3_account = SolanaAccount::new(2, size_of::(), &program_id); + let owner_key = pubkey_rand(); + let mut owner_account = SolanaAccount::default(); + let owner2_key = pubkey_rand(); + let mut owner2_account = SolanaAccount::default(); + + // uninitialized + assert_eq!( + Err(TokenError::UninitializedState.into()), + do_process_instruction( + close_account(&program_id, &account_key, &account3_key, &owner2_key, &[]).unwrap(), + vec![ + &mut account_account, + &mut account3_account, + &mut owner2_account, + ], + ) + ); + + // initialize and mint to account + do_process_instruction( + initialize_account(&program_id, &account_key, &mint_key, &owner_key).unwrap(), + vec![&mut account_account, &mut mint_account, &mut owner_account], + ) + .unwrap(); + do_process_instruction( + initialize_mint(&program_id, &mint_key, Some(&account_key), None, 42, 2).unwrap(), + vec![&mut mint_account, &mut account_account, &mut owner_account], + ) + .unwrap(); + let account: &mut Account = State::unpack(&mut account_account.data).unwrap(); + assert_eq!(account.amount, 42); + + // initialize native account + do_process_instruction( + initialize_account( + &program_id, + &account2_key, + &crate::native_mint::id(), + &owner_key, + ) + .unwrap(), + vec![&mut account2_account, &mut mint_account, &mut owner_account], + ) + .unwrap(); + let account: &mut Account = State::unpack(&mut account2_account.data).unwrap(); + assert!(account.is_native); + assert_eq!(account.amount, 2); + + // close account with balance + assert_eq!( + Err(TokenError::NonNativeHasBalance.into()), + do_process_instruction( + close_account(&program_id, &account_key, &account3_key, &owner_key, &[]).unwrap(), + vec![ + &mut account_account, + &mut account3_account, + &mut owner_account, + ], + ) + ); + + // empty account + do_process_instruction( + burn(&program_id, &account_key, &owner_key, &[], 42).unwrap(), + vec![&mut account_account, &mut owner_account], + ) + .unwrap(); + + // wrong owner + assert_eq!( + Err(TokenError::OwnerMismatch.into()), + do_process_instruction( + close_account(&program_id, &account_key, &account3_key, &owner2_key, &[]).unwrap(), + vec![ + &mut account_account, + &mut account3_account, + &mut owner2_account, + ], + ) + ); + + // close account + do_process_instruction( + close_account(&program_id, &account_key, &account3_key, &owner_key, &[]).unwrap(), + vec![ + &mut account_account, + &mut account3_account, + &mut owner_account, + ], + ) + .unwrap(); + let account: &mut Account = State::unpack_unchecked(&mut account_account.data).unwrap(); + assert_eq!(account_account.lamports, 0); + assert_eq!(account.amount, 0); + assert_eq!(account3_account.lamports, 44); + + // close native account + do_process_instruction( + close_account(&program_id, &account2_key, &account3_key, &owner_key, &[]).unwrap(), + vec![ + &mut account2_account, + &mut account3_account, + &mut owner_account, + ], + ) + .unwrap(); + let account: &mut Account = State::unpack_unchecked(&mut account2_account.data).unwrap(); + assert!(account.is_native); + assert_eq!(account_account.lamports, 0); + assert_eq!(account.amount, 0); + assert_eq!(account3_account.lamports, 46); + } + + #[test] + fn test_native_token() { + let program_id = pubkey_rand(); + let mut mint_account = SolanaAccount::new(0, size_of::(), &program_id); + let account_key = pubkey_rand(); + let mut account_account = SolanaAccount::new(42, size_of::(), &program_id); + let account2_key = pubkey_rand(); + let mut account2_account = SolanaAccount::new(2, size_of::(), &program_id); + let account3_key = pubkey_rand(); + let mut account3_account = SolanaAccount::new(2, 0, &program_id); + let owner_key = pubkey_rand(); + let mut owner_account = SolanaAccount::default(); + + // initialize native account + do_process_instruction( + initialize_account( + &program_id, + &account_key, + &crate::native_mint::id(), + &owner_key, + ) + .unwrap(), + vec![&mut account_account, &mut mint_account, &mut owner_account], + ) + .unwrap(); + let account: &mut Account = State::unpack(&mut account_account.data).unwrap(); + assert!(account.is_native); + assert_eq!(account.amount, 42); + + // initialize native account + do_process_instruction( + initialize_account( + &program_id, + &account2_key, + &crate::native_mint::id(), + &owner_key, + ) + .unwrap(), + vec![&mut account2_account, &mut mint_account, &mut owner_account], + ) + .unwrap(); + let account: &mut Account = State::unpack(&mut account2_account.data).unwrap(); + assert!(account.is_native); + assert_eq!(account.amount, 2); + + // mint_to unsupported + assert_eq!( + Err(TokenError::NativeNotSupported.into()), + do_process_instruction( + mint_to( + &program_id, + &crate::native_mint::id(), + &account_key, + &owner_key, + &[], + 42 + ) + .unwrap(), + vec![&mut mint_account, &mut account_account, &mut owner_account], + ) + ); + + // burn unsupported + assert_eq!( + Err(TokenError::NativeNotSupported.into()), + do_process_instruction( + burn(&program_id, &account_key, &owner_key, &[], 42).unwrap(), + vec![&mut account_account, &mut owner_account], + ) + ); + + // initialize native account + do_process_instruction( + transfer( + &program_id, + &account_key, + &account2_key, + &owner_key, + &[], + 40, + ) + .unwrap(), + vec![ + &mut account_account, + &mut account2_account, + &mut owner_account, + ], + ) + .unwrap(); + + let account: &mut Account = State::unpack(&mut account_account.data).unwrap(); + assert!(account.is_native); + assert_eq!(account_account.lamports, 2); + assert_eq!(account.amount, 2); + let account: &mut Account = State::unpack(&mut account2_account.data).unwrap(); + assert!(account.is_native); + assert_eq!(account2_account.lamports, 42); + assert_eq!(account.amount, 42); + + // close native account + do_process_instruction( + close_account(&program_id, &account_key, &account3_key, &owner_key, &[]).unwrap(), + vec![ + &mut account_account, + &mut account3_account, + &mut owner_account, + ], + ) + .unwrap(); + let account: &mut Account = State::unpack_unchecked(&mut account_account.data).unwrap(); + assert!(account.is_native); + assert_eq!(account_account.lamports, 0); + assert_eq!(account.amount, 0); + assert_eq!(account3_account.lamports, 4); + } }