Add associated-token-account program

This commit is contained in:
Michael Vines 2020-11-01 21:15:11 -08:00 committed by mergify[bot]
parent b68163ff23
commit 4da9cb3631
11 changed files with 961 additions and 93 deletions

537
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,6 @@
[workspace]
members = [
"associated-token-account/program",
"memo/program",
"shared-memory/program",
"stake-pool/program",

View File

@ -0,0 +1,23 @@
[package]
name = "spl-associated-token-account"
version = "1.0.0"
description = "SPL Associated Token Account"
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
repository = "https://github.com/solana-labs/solana-program-library"
license = "Apache-2.0"
edition = "2018"
[features]
exclude_entrypoint = []
[dependencies]
solana-program = "1.4.4"
spl-token = { path = "../../token/program", features = ["exclude_entrypoint"] }
[dev-dependencies]
solana-program-test = "1.4.4"
solana-sdk = "1.4.4"
tokio = { version = "0.3", features = ["macros"]}
[lib]
crate-type = ["cdylib", "lib"]

View File

@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []

View File

@ -0,0 +1,21 @@
use std::process::{exit, Command};
fn main() {
if std::env::var("XARGO").is_err()
&& std::env::var("RUSTC_WRAPPER").is_err()
&& std::env::var("RUSTC_WORKSPACE_WRAPPER").is_err()
{
println!(
"cargo:warning=(not a warning) Building BPF {} program",
std::env::var("CARGO_PKG_NAME").unwrap()
);
if !Command::new("cargo")
.arg("build-bpf")
.status()
.expect("Failed to build bpf")
.success()
{
exit(1);
}
}
}

View File

@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -ex
cd "$(dirname "$0")"
cargo clippy
cargo build
cargo build-bpf
if [[ $1 = -v ]]; then
export RUST_LOG=solana=debug
fi
bpf=1 cargo test
# TODO: bpf=0 not supported until native CPI rework in the monorepo completes
#bpf=0 cargo test

View File

@ -0,0 +1,16 @@
//! Program entrypoint
#![cfg(all(target_arch = "bpf", not(feature = "exclude_entrypoint")))]
use solana_program::{
account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey,
};
entrypoint!(process_instruction);
fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
crate::processor::process_instruction(program_id, accounts, instruction_data)
}

View File

@ -0,0 +1,75 @@
//! Convention for associating token accounts with a primary account (such as a user wallet)
#![deny(missing_docs)]
#![forbid(unsafe_code)]
mod entrypoint;
pub mod processor;
// Export current sdk types for downstream users building with a different sdk version
pub use solana_program;
use solana_program::{
instruction::{AccountMeta, Instruction},
program_pack::Pack,
pubkey::Pubkey,
sysvar,
};
solana_program::declare_id!("3medvrcM8s3UnkoYqqV3RAURii1ysuT5oD7t8nmfgJmj");
pub(crate) fn get_associated_token_address_and_bump_seed(
wallet_address: &Pubkey,
spl_token_mint_address: &Pubkey,
program_id: &Pubkey,
) -> (Pubkey, u8) {
Pubkey::find_program_address(
&[
&wallet_address.to_bytes(),
&spl_token::id().to_bytes(),
&spl_token_mint_address.to_bytes(),
],
program_id,
)
}
/// Derives the associated SPL token address for the given wallet address and SPL Token mint
pub fn get_associated_token_address(
wallet_address: &Pubkey,
spl_token_mint_address: &Pubkey,
) -> Pubkey {
get_associated_token_address_and_bump_seed(&wallet_address, &spl_token_mint_address, &id()).0
}
/// Create an associated token account for a wallet address
///
/// Accounts expected by this instruction:
///
/// 0. `[writeable,signer]` Funding account (must be a system account)
/// 1. `[writeable]` Associated token account address
/// 2. `[]` Wallet address for the new associated token account
/// 3. `[]` The SPL token mint for the associated token account
/// 4. `[]` System program
/// 4. `[]` SPL Token program
/// 5. `[]` Rent sysvar
///
pub fn create_associated_token_account(
funding_address: &Pubkey,
wallet_address: &Pubkey,
spl_token_mint_address: &Pubkey,
) -> Instruction {
let associated_account_address =
get_associated_token_address(wallet_address, spl_token_mint_address);
Instruction {
program_id: id(),
accounts: vec![
AccountMeta::new(*funding_address, true),
AccountMeta::new(associated_account_address, false),
AccountMeta::new_readonly(*wallet_address, false),
AccountMeta::new_readonly(*spl_token_mint_address, false),
AccountMeta::new_readonly(solana_program::system_program::id(), false),
AccountMeta::new_readonly(spl_token::id(), false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
],
data: vec![],
}
}

View File

@ -0,0 +1,113 @@
//! Program state processor
use crate::*;
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
info,
log::sol_log_compute_units,
program::{invoke, invoke_signed},
program_error::ProgramError,
pubkey::Pubkey,
rent::Rent,
system_instruction,
sysvar::Sysvar,
};
/// Instruction processor
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
_input: &[u8],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let funder_info = next_account_info(account_info_iter)?;
let associated_token_account_info = next_account_info(account_info_iter)?;
let wallet_account_info = next_account_info(account_info_iter)?;
let spl_token_mint_info = next_account_info(account_info_iter)?;
let system_program_info = next_account_info(account_info_iter)?;
let spl_token_program_info = next_account_info(account_info_iter)?;
let rent_sysvar_info = next_account_info(account_info_iter)?;
let (associated_token_address, bump_seed) = get_associated_token_address_and_bump_seed(
&wallet_account_info.key,
&spl_token_mint_info.key,
program_id,
);
if associated_token_address != *associated_token_account_info.key {
info!("Error: Associated address does not match seed derivation");
return Err(ProgramError::InvalidSeeds);
}
let associated_token_account_signer_seeds: &[&[_]] = &[
&wallet_account_info.key.to_bytes(),
&spl_token::id().to_bytes(),
&spl_token_mint_info.key.to_bytes(),
&[bump_seed],
];
sol_log_compute_units();
// Fund the associated token account with the minimum balance to be rent exempt
let rent = &Rent::from_account_info(rent_sysvar_info)?;
let required_lamports = rent
.minimum_balance(spl_token::state::Account::LEN)
.max(1)
.saturating_sub(associated_token_account_info.lamports());
if required_lamports > 0 {
invoke(
&system_instruction::transfer(
&funder_info.key,
associated_token_account_info.key,
required_lamports,
),
&[
funder_info.clone(),
associated_token_account_info.clone(),
system_program_info.clone(),
],
)?;
}
// Allocate space for the associated token account
invoke_signed(
&system_instruction::allocate(
associated_token_account_info.key,
spl_token::state::Account::LEN as u64,
),
&[
associated_token_account_info.clone(),
system_program_info.clone(),
],
&[&associated_token_account_signer_seeds],
)?;
// Assign the associated token account to the SPL Token program
invoke_signed(
&system_instruction::assign(associated_token_account_info.key, &spl_token::id()),
&[
associated_token_account_info.clone(),
system_program_info.clone(),
],
&[&associated_token_account_signer_seeds],
)?;
// Initialize the associated token account
invoke(
&spl_token::instruction::initialize_account(
&spl_token::id(),
associated_token_account_info.key,
spl_token_mint_info.key,
wallet_account_info.key,
)?,
&[
associated_token_account_info.clone(),
spl_token_mint_info.clone(),
wallet_account_info.clone(),
rent_sysvar_info.clone(),
spl_token_program_info.clone(),
],
)
}

Binary file not shown.

View File

@ -0,0 +1,251 @@
use solana_program::{
instruction::*, program_pack::Pack, pubkey::Pubkey, system_instruction, sysvar::rent::Rent,
};
use solana_program_test::*;
use solana_sdk::{
signature::Signer,
transaction::{Transaction, TransactionError},
};
use spl_associated_token_account::*;
fn program_test(token_mint_address: Pubkey) -> ProgramTest {
let mut pc = ProgramTest::new(
"spl_associated_token_account",
id(),
// TODO: BPF only until native CPI rework in the monorepo completes
None, //processor!(processor::process_instruction),
);
// Add Token program
pc.add_program(
"spl_token",
spl_token::id(),
processor!(spl_token::processor::Processor::process),
);
// Add a token mint account
//
// The account data was generated by running:
// $ solana account EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v \
// --output-file tests/fixtures/token-mint-data.bin
//
pc.add_account_with_file_data(
token_mint_address,
1461600,
spl_token::id(),
"token-mint-data.bin",
);
// Dial down the BPF compute budget to detect if the program gets bloated in the future
pc.set_bpf_compute_max_units(50_000);
pc
}
#[tokio::test]
async fn test_associated_token_address() {
let wallet_address = Pubkey::new_unique();
let token_mint_address = Pubkey::new_unique();
let associated_token_address =
get_associated_token_address(&wallet_address, &token_mint_address);
let (mut banks_client, payer, recent_blockhash) =
program_test(token_mint_address).start().await;
let rent = Rent::default(); // <-- TODO: get Rent from `ProgramTest`
let expected_token_account_balance = rent.minimum_balance(spl_token::state::Account::LEN);
// Associated account does not exist
assert_eq!(
banks_client
.get_account(associated_token_address)
.await
.expect("get_account"),
None,
);
let mut transaction = Transaction::new_with_payer(
&[create_associated_token_account(
&payer.pubkey(),
&wallet_address,
&token_mint_address,
)],
Some(&payer.pubkey()),
);
transaction.sign(&[&payer], recent_blockhash);
banks_client.process_transaction(transaction).await.unwrap();
// Associated account now exists
let associated_account = banks_client
.get_account(associated_token_address)
.await
.expect("get_account")
.expect("associated_account not none");
assert_eq!(
associated_account.data.len(),
spl_token::state::Account::LEN
);
assert_eq!(associated_account.owner, spl_token::id());
assert_eq!(associated_account.lamports, expected_token_account_balance);
}
#[tokio::test]
async fn test_create_with_a_lamport() {
let wallet_address = Pubkey::new_unique();
let token_mint_address = Pubkey::new_unique();
let associated_token_address =
get_associated_token_address(&wallet_address, &token_mint_address);
let (mut banks_client, payer, recent_blockhash) =
program_test(token_mint_address).start().await;
let rent = Rent::default(); // <-- TOOD: get Rent from `ProgramTest`
let expected_token_account_balance = rent.minimum_balance(spl_token::state::Account::LEN);
// Transfer 1 lamport into `associated_token_address` before creating it
let mut transaction = Transaction::new_with_payer(
&[system_instruction::transfer(
&payer.pubkey(),
&associated_token_address,
1,
)],
Some(&payer.pubkey()),
);
transaction.sign(&[&payer], recent_blockhash);
banks_client.process_transaction(transaction).await.unwrap();
assert_eq!(
banks_client
.get_balance(associated_token_address)
.await
.unwrap(),
1
);
// Check that the program adds the extra lamports
let mut transaction = Transaction::new_with_payer(
&[create_associated_token_account(
&payer.pubkey(),
&wallet_address,
&token_mint_address,
)],
Some(&payer.pubkey()),
);
transaction.sign(&[&payer], recent_blockhash);
banks_client.process_transaction(transaction).await.unwrap();
assert_eq!(
banks_client
.get_balance(associated_token_address)
.await
.unwrap(),
expected_token_account_balance,
);
}
#[tokio::test]
async fn test_create_with_excess_lamports() {
let wallet_address = Pubkey::new_unique();
let token_mint_address = Pubkey::new_unique();
let associated_token_address =
get_associated_token_address(&wallet_address, &token_mint_address);
let (mut banks_client, payer, recent_blockhash) =
program_test(token_mint_address).start().await;
let rent = Rent::default(); // <-- TOOD: get Rent from `ProgramTest`
let expected_token_account_balance = rent.minimum_balance(spl_token::state::Account::LEN);
// Transfer 1 lamport into `associated_token_address` before creating it
let mut transaction = Transaction::new_with_payer(
&[system_instruction::transfer(
&payer.pubkey(),
&associated_token_address,
expected_token_account_balance + 1,
)],
Some(&payer.pubkey()),
);
transaction.sign(&[&payer], recent_blockhash);
banks_client.process_transaction(transaction).await.unwrap();
assert_eq!(
banks_client
.get_balance(associated_token_address)
.await
.unwrap(),
expected_token_account_balance + 1
);
// Check that the program doesn't add any lamports
let mut transaction = Transaction::new_with_payer(
&[create_associated_token_account(
&payer.pubkey(),
&wallet_address,
&token_mint_address,
)],
Some(&payer.pubkey()),
);
transaction.sign(&[&payer], recent_blockhash);
banks_client.process_transaction(transaction).await.unwrap();
assert_eq!(
banks_client
.get_balance(associated_token_address)
.await
.unwrap(),
expected_token_account_balance + 1
);
}
#[tokio::test]
async fn test_create_account_mismatch() {
let wallet_address = Pubkey::new_unique();
let token_mint_address = Pubkey::new_unique();
let _associated_token_address =
get_associated_token_address(&wallet_address, &token_mint_address);
let (mut banks_client, payer, recent_blockhash) =
program_test(token_mint_address).start().await;
let mut instruction =
create_associated_token_account(&payer.pubkey(), &wallet_address, &token_mint_address);
instruction.accounts[1] = AccountMeta::new(Pubkey::default(), false); // <-- Invalid associated_account_address
let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey()));
transaction.sign(&[&payer], recent_blockhash);
assert_eq!(
banks_client
.process_transaction(transaction)
.await
.unwrap_err()
.unwrap(),
TransactionError::InstructionError(0, InstructionError::InvalidSeeds)
);
let mut instruction =
create_associated_token_account(&payer.pubkey(), &wallet_address, &token_mint_address);
instruction.accounts[2] = AccountMeta::new(Pubkey::default(), false); // <-- Invalid wallet_address
let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey()));
transaction.sign(&[&payer], recent_blockhash);
assert_eq!(
banks_client
.process_transaction(transaction)
.await
.unwrap_err()
.unwrap(),
TransactionError::InstructionError(0, InstructionError::InvalidSeeds)
);
let mut instruction =
create_associated_token_account(&payer.pubkey(), &wallet_address, &token_mint_address);
instruction.accounts[3] = AccountMeta::new(Pubkey::default(), false); // <-- Invalid token_mint_address
let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey()));
transaction.sign(&[&payer], recent_blockhash);
assert_eq!(
banks_client
.process_transaction(transaction)
.await
.unwrap_err()
.unwrap(),
TransactionError::InstructionError(0, InstructionError::InvalidSeeds)
);
}