Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit 6793256

Browse files
authored
example: Token transfer with PDA (#3965)
1 parent 64b844a commit 6793256

File tree

7 files changed

+275
-0
lines changed

7 files changed

+275
-0
lines changed

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ members = [
99
"examples/rust/logging",
1010
"examples/rust/sysvar",
1111
"examples/rust/transfer-lamports",
12+
"examples/rust/transfer-tokens",
1213
"feature-proposal/program",
1314
"feature-proposal/cli",
1415
"governance/addin-mock/program",
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[package]
2+
name = "spl-example-transfer-tokens"
3+
version = "1.0.0"
4+
description = "Solana Program Library Transfer Tokens Example"
5+
authors = ["Solana Maintainers <[email protected]>"]
6+
repository = "https://github.com/solana-labs/solana-program-library"
7+
license = "Apache-2.0"
8+
edition = "2021"
9+
10+
[features]
11+
no-entrypoint = []
12+
test-sbf = []
13+
14+
[dependencies]
15+
solana-program = "1.14.10"
16+
spl-token = { version = "3.5", path = "../../../token/program", features = [ "no-entrypoint" ] }
17+
18+
[dev-dependencies]
19+
solana-program-test = "1.14.10"
20+
solana-sdk = "1.14.10"
21+
22+
[lib]
23+
crate-type = ["cdylib", "lib"]
24+
25+
[package.metadata.docs.rs]
26+
targets = ["x86_64-unknown-linux-gnu"]
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
//! Program entrypoint
2+
3+
#![cfg(not(feature = "no-entrypoint"))]
4+
5+
use solana_program::{
6+
account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey,
7+
};
8+
9+
entrypoint!(process_instruction);
10+
fn process_instruction(
11+
program_id: &Pubkey,
12+
accounts: &[AccountInfo],
13+
instruction_data: &[u8],
14+
) -> ProgramResult {
15+
crate::processor::process_instruction(program_id, accounts, instruction_data)
16+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
//! A program demonstrating the transfer of lamports
2+
#![deny(missing_docs)]
3+
#![forbid(unsafe_code)]
4+
5+
mod entrypoint;
6+
pub mod processor;
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
//! Program instruction processor
2+
3+
use {
4+
solana_program::{
5+
account_info::{next_account_info, AccountInfo},
6+
entrypoint::ProgramResult,
7+
msg,
8+
program::invoke_signed,
9+
program_error::ProgramError,
10+
program_pack::Pack,
11+
pubkey::Pubkey,
12+
},
13+
spl_token::{
14+
instruction::transfer_checked,
15+
state::{Account, Mint},
16+
},
17+
};
18+
19+
/// Instruction processor
20+
pub fn process_instruction(
21+
program_id: &Pubkey,
22+
accounts: &[AccountInfo],
23+
_instruction_data: &[u8],
24+
) -> ProgramResult {
25+
// Create an iterator to safely reference accounts in the slice
26+
let account_info_iter = &mut accounts.iter();
27+
28+
// As part of the program specification the instruction gives:
29+
// 1. source token account
30+
// 2. mint account
31+
// 3. destination token account
32+
// 4. program-derived address that owns 1.
33+
// 5. token program
34+
let source_info = next_account_info(account_info_iter)?;
35+
let mint_info = next_account_info(account_info_iter)?;
36+
let destination_info = next_account_info(account_info_iter)?;
37+
let authority_info = next_account_info(account_info_iter)?;
38+
let token_program_info = next_account_info(account_info_iter)?;
39+
40+
// In order to transfer from the source account, owned by the program-derived
41+
// address, we must have the correct address and seeds.
42+
let (expected_authority, bump_seed) = Pubkey::find_program_address(&[b"authority"], program_id);
43+
if expected_authority != *authority_info.key {
44+
return Err(ProgramError::InvalidSeeds);
45+
}
46+
47+
// The program transfers everything out of its account, so extract that from
48+
// the account data.
49+
let source_account = Account::unpack(&source_info.try_borrow_data()?)?;
50+
let amount = source_account.amount;
51+
52+
// The program uses `transfer_checked`, which requires the number of decimals
53+
// in the mint, so extract that from the account data too.
54+
let mint = Mint::unpack(&mint_info.try_borrow_data()?)?;
55+
let decimals = mint.decimals;
56+
57+
// Invoke the transfer
58+
msg!("Attempting to transfer {} tokens", amount);
59+
invoke_signed(
60+
&transfer_checked(
61+
token_program_info.key,
62+
source_info.key,
63+
mint_info.key,
64+
destination_info.key,
65+
authority_info.key,
66+
&[], // no multisig allowed
67+
amount,
68+
decimals,
69+
)
70+
.unwrap(),
71+
&[
72+
source_info.clone(),
73+
mint_info.clone(),
74+
destination_info.clone(),
75+
authority_info.clone(),
76+
token_program_info.clone(), // not required, but better for clarity
77+
],
78+
&[&[b"authority", &[bump_seed]]],
79+
)
80+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
use {
2+
solana_program::{
3+
instruction::{AccountMeta, Instruction},
4+
program_pack::Pack,
5+
pubkey::Pubkey,
6+
rent::Rent,
7+
},
8+
solana_program_test::{processor, tokio, ProgramTest},
9+
solana_sdk::{account::Account, signature::Signer, transaction::Transaction},
10+
spl_example_transfer_tokens::processor::process_instruction,
11+
spl_token::state::{Account as TokenAccount, Mint},
12+
std::str::FromStr,
13+
};
14+
15+
#[tokio::test]
16+
async fn success() {
17+
// Setup some pubkeys for the accounts
18+
let program_id = Pubkey::from_str("TransferTokens11111111111111111111111111111").unwrap();
19+
let source_pubkey = Pubkey::new_unique();
20+
let mint_pubkey = Pubkey::new_unique();
21+
let destination_pubkey = Pubkey::new_unique();
22+
let destination_owner_pubkey = Pubkey::new_unique();
23+
let (authority_pubkey, _) = Pubkey::find_program_address(&[b"authority"], &program_id);
24+
25+
// Add the program to the test framework
26+
let rent = Rent::default();
27+
let mut program_test = ProgramTest::new(
28+
"spl_example_transfer_tokens",
29+
program_id,
30+
processor!(process_instruction),
31+
);
32+
let amount = 10_000;
33+
let decimals = 9;
34+
35+
// Setup the source account, owned by the program-derived address
36+
let mut data = vec![0; TokenAccount::LEN];
37+
TokenAccount::pack(
38+
TokenAccount {
39+
mint: mint_pubkey,
40+
owner: authority_pubkey,
41+
amount,
42+
state: spl_token::state::AccountState::Initialized,
43+
..TokenAccount::default()
44+
},
45+
&mut data,
46+
)
47+
.unwrap();
48+
program_test.add_account(
49+
source_pubkey,
50+
Account {
51+
lamports: rent.minimum_balance(TokenAccount::LEN),
52+
owner: spl_token::id(),
53+
data,
54+
..Account::default()
55+
},
56+
);
57+
58+
// Setup the mint, used in `spl_token::instruction::transfer_checked`
59+
let mut data = vec![0; Mint::LEN];
60+
Mint::pack(
61+
Mint {
62+
supply: amount,
63+
decimals,
64+
is_initialized: true,
65+
..Mint::default()
66+
},
67+
&mut data,
68+
)
69+
.unwrap();
70+
program_test.add_account(
71+
mint_pubkey,
72+
Account {
73+
lamports: rent.minimum_balance(Mint::LEN),
74+
owner: spl_token::id(),
75+
data,
76+
..Account::default()
77+
},
78+
);
79+
80+
// Setup the destination account, used to receive tokens from the account
81+
// owned by the program-derived address
82+
let mut data = vec![0; TokenAccount::LEN];
83+
TokenAccount::pack(
84+
TokenAccount {
85+
mint: mint_pubkey,
86+
owner: destination_owner_pubkey,
87+
amount: 0,
88+
state: spl_token::state::AccountState::Initialized,
89+
..TokenAccount::default()
90+
},
91+
&mut data,
92+
)
93+
.unwrap();
94+
program_test.add_account(
95+
destination_pubkey,
96+
Account {
97+
lamports: rent.minimum_balance(TokenAccount::LEN),
98+
owner: spl_token::id(),
99+
data,
100+
..Account::default()
101+
},
102+
);
103+
104+
// Start the program test
105+
let (mut banks_client, payer, recent_blockhash) = program_test.start().await;
106+
107+
// Create an instruction following the account order expected by the program
108+
let transaction = Transaction::new_signed_with_payer(
109+
&[Instruction::new_with_bincode(
110+
program_id,
111+
&(),
112+
vec![
113+
AccountMeta::new(source_pubkey, false),
114+
AccountMeta::new_readonly(mint_pubkey, false),
115+
AccountMeta::new(destination_pubkey, false),
116+
AccountMeta::new_readonly(authority_pubkey, false),
117+
AccountMeta::new_readonly(spl_token::id(), false),
118+
],
119+
)],
120+
Some(&payer.pubkey()),
121+
&[&payer],
122+
recent_blockhash,
123+
);
124+
125+
// See that the transaction processes successfully
126+
banks_client.process_transaction(transaction).await.unwrap();
127+
128+
// Check that the destination account now has `amount` tokens
129+
let account = banks_client
130+
.get_account(destination_pubkey)
131+
.await
132+
.unwrap()
133+
.unwrap();
134+
let token_account = TokenAccount::unpack(&account.data).unwrap();
135+
assert_eq!(token_account.amount, amount);
136+
}

0 commit comments

Comments
 (0)