Add associated-token-account program
This commit is contained in:
parent
b68163ff23
commit
4da9cb3631
File diff suppressed because it is too large
Load Diff
|
@ -1,5 +1,6 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"associated-token-account/program",
|
||||
"memo/program",
|
||||
"shared-memory/program",
|
||||
"stake-pool/program",
|
||||
|
|
|
@ -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"]
|
|
@ -0,0 +1,2 @@
|
|||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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)
|
||||
}
|
|
@ -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![],
|
||||
}
|
||||
}
|
|
@ -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.
|
@ -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)
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue