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

Commit 68b8da2

Browse files
authored
Support associated token for JS (Also, make the program testable) (#1364)
* Implement some js helpers for associated tokens * Create integration test and fix hard-coding in spl-associated-token * Run lint:fix and pretty:fix * Run flow as well... * More robust test fixture setup * Revert api breaking part * Fix tests... * Populate ts/flow type definitions * Improve test a bit * More consistent arg order; docs; more tests * lints and pretty * type definition updates and test tweaks * More simplification... * More cleanup * Address review comments and small cleanings * Bump the version
1 parent 7d25569 commit 68b8da2

File tree

11 files changed

+393
-33
lines changed

11 files changed

+393
-33
lines changed

associated-token-account/program/src/lib.rs

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,11 @@ pub(crate) fn get_associated_token_address_and_bump_seed(
2121
spl_token_mint_address: &Pubkey,
2222
program_id: &Pubkey,
2323
) -> (Pubkey, u8) {
24-
Pubkey::find_program_address(
25-
&[
26-
&wallet_address.to_bytes(),
27-
&spl_token::id().to_bytes(),
28-
&spl_token_mint_address.to_bytes(),
29-
],
24+
get_associated_token_address_and_bump_seed_internal(
25+
wallet_address,
26+
spl_token_mint_address,
3027
program_id,
28+
&spl_token::id(),
3129
)
3230
}
3331

@@ -39,6 +37,22 @@ pub fn get_associated_token_address(
3937
get_associated_token_address_and_bump_seed(&wallet_address, &spl_token_mint_address, &id()).0
4038
}
4139

40+
fn get_associated_token_address_and_bump_seed_internal(
41+
wallet_address: &Pubkey,
42+
spl_token_mint_address: &Pubkey,
43+
program_id: &Pubkey,
44+
token_program_id: &Pubkey,
45+
) -> (Pubkey, u8) {
46+
Pubkey::find_program_address(
47+
&[
48+
&wallet_address.to_bytes(),
49+
&token_program_id.to_bytes(),
50+
&spl_token_mint_address.to_bytes(),
51+
],
52+
program_id,
53+
)
54+
}
55+
4256
/// Create an associated token account for the given wallet address and token mint
4357
///
4458
/// Accounts expected by this instruction:

associated-token-account/program/src/processor.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@ pub fn process_instruction(
2727
let spl_token_mint_info = next_account_info(account_info_iter)?;
2828
let system_program_info = next_account_info(account_info_iter)?;
2929
let spl_token_program_info = next_account_info(account_info_iter)?;
30+
let spl_token_program_id = spl_token_program_info.key;
3031
let rent_sysvar_info = next_account_info(account_info_iter)?;
3132

32-
let (associated_token_address, bump_seed) = get_associated_token_address_and_bump_seed(
33+
let (associated_token_address, bump_seed) = get_associated_token_address_and_bump_seed_internal(
3334
&wallet_account_info.key,
3435
&spl_token_mint_info.key,
3536
program_id,
37+
&spl_token_program_id,
3638
);
3739
if associated_token_address != *associated_token_account_info.key {
3840
msg!("Error: Associated address does not match seed derivation");
@@ -41,7 +43,7 @@ pub fn process_instruction(
4143

4244
let associated_token_account_signer_seeds: &[&[_]] = &[
4345
&wallet_account_info.key.to_bytes(),
44-
&spl_token::id().to_bytes(),
46+
&spl_token_program_id.to_bytes(),
4547
&spl_token_mint_info.key.to_bytes(),
4648
&[bump_seed],
4749
];
@@ -87,7 +89,7 @@ pub fn process_instruction(
8789

8890
msg!("Assign the associated token account to the SPL Token program");
8991
invoke_signed(
90-
&system_instruction::assign(associated_token_account_info.key, &spl_token::id()),
92+
&system_instruction::assign(associated_token_account_info.key, &spl_token_program_id),
9193
&[
9294
associated_token_account_info.clone(),
9395
system_program_info.clone(),
@@ -98,7 +100,7 @@ pub fn process_instruction(
98100
msg!("Initialize the associated token account");
99101
invoke(
100102
&spl_token::instruction::initialize_account(
101-
&spl_token::id(),
103+
&spl_token_program_id,
102104
associated_token_account_info.key,
103105
spl_token_mint_info.key,
104106
wallet_account_info.key,

ci/js-test-token.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ npm install
1010
npm run lint
1111
npm run flow
1212
npm run defs
13+
npm run test
1314
npm run start-with-test-validator
1415
PROGRAM_VERSION=2.0.4 npm run start-with-test-validator

token/js/cli/main.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import {
88
loadTokenProgram,
99
createMint,
1010
createAccount,
11+
createAssociatedAccount,
1112
transfer,
1213
transferChecked,
14+
transferCheckedAssociated,
1315
approveRevoke,
1416
failOnApproveOverspend,
1517
setAuthority,
@@ -30,6 +32,8 @@ async function main() {
3032
await createMint();
3133
console.log('Run test: createAccount');
3234
await createAccount();
35+
console.log('Run test: createAssociatedAccount');
36+
await createAssociatedAccount();
3337
console.log('Run test: mintTo');
3438
await mintTo();
3539
console.log('Run test: mintToChecked');
@@ -38,6 +42,8 @@ async function main() {
3842
await transfer();
3943
console.log('Run test: transferChecked');
4044
await transferChecked();
45+
console.log('Run test: transferCheckedAssociated');
46+
await transferCheckedAssociated();
4147
console.log('Run test: approveRevoke');
4248
await approveRevoke();
4349
console.log('Run test: failOnApproveOverspend');

token/js/cli/store/index.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,20 @@ export class Store {
1313
return path.join(__dirname, 'store');
1414
}
1515

16+
static getFilename(uri: string): string {
17+
return path.join(Store.getDir(), uri);
18+
}
19+
1620
async load(uri: string): Promise<Object> {
17-
const filename = path.join(Store.getDir(), uri);
21+
const filename = Store.getFilename(uri);
1822
const data = await fs.readFile(filename, 'utf8');
1923
const config = JSON.parse(data);
2024
return config;
2125
}
2226

2327
async save(uri: string, config: Object): Promise<void> {
2428
await mkdirp(Store.getDir());
25-
const filename = path.join(Store.getDir(), uri);
29+
const filename = Store.getFilename(uri);
2630
await fs.writeFile(filename, JSON.stringify(config), 'utf8');
2731
}
2832
}

token/js/cli/token-test.js

Lines changed: 104 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,25 @@ import {
99
BPF_LOADER_PROGRAM_ID,
1010
} from '@solana/web3.js';
1111

12-
import {Token, NATIVE_MINT} from '../client/token';
12+
import {
13+
Token,
14+
TOKEN_PROGRAM_ID,
15+
ASSOCIATED_TOKEN_PROGRAM_ID,
16+
NATIVE_MINT,
17+
} from '../client/token';
1318
import {url} from '../url';
1419
import {newAccountWithLamports} from '../client/util/new-account-with-lamports';
1520
import {sleep} from '../client/util/sleep';
1621
import {Store} from './store';
1722

1823
// Loaded token program's program id
1924
let programId: PublicKey;
25+
let associatedProgramId: PublicKey;
2026

2127
// Accounts setup in createMint and used by all subsequent tests
2228
let testMintAuthority: Account;
2329
let testToken: Token;
30+
let testTokenDecimals: number = 2;
2431

2532
// Accounts setup in createAccount and used by all subsequent tests
2633
let testAccountOwner: Account;
@@ -78,43 +85,60 @@ async function loadProgram(
7885
return program_account.publicKey;
7986
}
8087

81-
async function GetPrograms(connection: Connection): Promise<PublicKey> {
88+
async function GetPrograms(connection: Connection): Promise<void> {
8289
const programVersion = process.env.PROGRAM_VERSION;
8390
if (programVersion) {
8491
switch (programVersion) {
8592
case '2.0.4':
86-
return new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
93+
programId = TOKEN_PROGRAM_ID;
94+
associatedProgramId = ASSOCIATED_TOKEN_PROGRAM_ID;
95+
return;
8796
default:
8897
throw new Error('Unknown program version');
8998
}
9099
}
91100

92101
const store = new Store();
93-
let tokenProgramId = null;
94102
try {
95103
const config = await store.load('config.json');
96-
console.log('Using pre-loaded Token program');
104+
console.log('Using pre-loaded Token programs');
97105
console.log(
98-
' Note: To reload program remove client/util/store/config.json',
106+
` Note: To reload program remove ${Store.getFilename('config.json')}`,
99107
);
100-
tokenProgramId = new PublicKey(config.tokenProgramId);
108+
programId = new PublicKey(config.tokenProgramId);
109+
associatedProgramId = new PublicKey(config.associatedTokenProgramId);
110+
let info;
111+
info = await connection.getAccountInfo(programId);
112+
assert(info != null);
113+
info = await connection.getAccountInfo(associatedProgramId);
114+
assert(info != null);
101115
} catch (err) {
102-
tokenProgramId = await loadProgram(
116+
console.log(
117+
'Checking pre-loaded Token programs failed, will load new programs:',
118+
);
119+
console.log({err});
120+
121+
programId = await loadProgram(
103122
connection,
104123
'../../target/bpfel-unknown-unknown/release/spl_token.so',
105124
);
125+
associatedProgramId = await loadProgram(
126+
connection,
127+
'../../target/bpfel-unknown-unknown/release/spl_associated_token_account.so',
128+
);
106129
await store.save('config.json', {
107-
tokenProgramId: tokenProgramId.toString(),
130+
tokenProgramId: programId.toString(),
131+
associatedTokenProgramId: associatedProgramId.toString(),
108132
});
109133
}
110-
return tokenProgramId;
111134
}
112135

113136
export async function loadTokenProgram(): Promise<void> {
114137
const connection = await getConnection();
115-
programId = await GetPrograms(connection);
138+
await GetPrograms(connection);
116139

117140
console.log('Token Program ID', programId.toString());
141+
console.log('Associated Token Program ID', associatedProgramId.toString());
118142
}
119143

120144
export async function createMint(): Promise<void> {
@@ -126,9 +150,12 @@ export async function createMint(): Promise<void> {
126150
payer,
127151
testMintAuthority.publicKey,
128152
testMintAuthority.publicKey,
129-
2,
153+
testTokenDecimals,
130154
programId,
131155
);
156+
// HACK: override hard-coded ASSOCIATED_TOKEN_PROGRAM_ID with corresponding
157+
// custom test fixture
158+
testToken.associatedProgramId = associatedProgramId;
132159

133160
const mintInfo = await testToken.getMintInfo();
134161
if (mintInfo.mintAuthority !== null) {
@@ -137,7 +164,7 @@ export async function createMint(): Promise<void> {
137164
assert(mintInfo.mintAuthority !== null);
138165
}
139166
assert(mintInfo.supply.toNumber() === 0);
140-
assert(mintInfo.decimals === 2);
167+
assert(mintInfo.decimals === testTokenDecimals);
141168
assert(mintInfo.isInitialized === true);
142169
if (mintInfo.freezeAuthority !== null) {
143170
assert(mintInfo.freezeAuthority.equals(testMintAuthority.publicKey));
@@ -160,6 +187,48 @@ export async function createAccount(): Promise<void> {
160187
assert(accountInfo.isNative === false);
161188
assert(accountInfo.rentExemptReserve === null);
162189
assert(accountInfo.closeAuthority === null);
190+
191+
// you can create as many accounts as with same owner
192+
const testAccount2 = await testToken.createAccount(
193+
testAccountOwner.publicKey,
194+
);
195+
assert(!testAccount2.equals(testAccount));
196+
}
197+
198+
export async function createAssociatedAccount(): Promise<void> {
199+
let info;
200+
const connection = await getConnection();
201+
202+
const owner = new Account();
203+
const associatedAddress = await Token.getAssociatedTokenAddress(
204+
associatedProgramId,
205+
programId,
206+
testToken.publicKey,
207+
owner.publicKey,
208+
);
209+
210+
// associated account shouldn't exist
211+
info = await connection.getAccountInfo(associatedAddress);
212+
assert(info == null);
213+
214+
const createdAddress = await testToken.createAssociatedTokenAccount(
215+
owner.publicKey,
216+
);
217+
assert(createdAddress.equals(associatedAddress));
218+
219+
// associated account should exist now
220+
info = await testToken.getAccountInfo(associatedAddress);
221+
assert(info != null);
222+
assert(info.mint.equals(testToken.publicKey));
223+
assert(info.owner.equals(owner.publicKey));
224+
assert(info.amount.toNumber() === 0);
225+
226+
// creating again should cause TX error for the associated token account
227+
assert(
228+
await didThrow(testToken, testToken.createAssociatedTokenAccount, [
229+
owner.publicKey,
230+
]),
231+
);
163232
}
164233

165234
export async function mintTo(): Promise<void> {
@@ -219,7 +288,7 @@ export async function transferChecked(): Promise<void> {
219288
testAccountOwner,
220289
[],
221290
100,
222-
1,
291+
testTokenDecimals - 1,
223292
]),
224293
);
225294

@@ -229,7 +298,7 @@ export async function transferChecked(): Promise<void> {
229298
testAccountOwner,
230299
[],
231300
100,
232-
2,
301+
testTokenDecimals,
233302
);
234303

235304
const mintInfo = await testToken.getMintInfo();
@@ -242,6 +311,26 @@ export async function transferChecked(): Promise<void> {
242311
assert(testAccountInfo.amount.toNumber() === 1800);
243312
}
244313

314+
export async function transferCheckedAssociated(): Promise<void> {
315+
const dest = new Account().publicKey;
316+
let associatedAccount;
317+
318+
associatedAccount = await testToken.getOrCreateAssociatedAccountInfo(dest);
319+
assert(associatedAccount.amount.toNumber() === 0);
320+
321+
await testToken.transferChecked(
322+
testAccount,
323+
associatedAccount.address,
324+
testAccountOwner,
325+
[],
326+
123,
327+
testTokenDecimals,
328+
);
329+
330+
associatedAccount = await testToken.getOrCreateAssociatedAccountInfo(dest);
331+
assert(associatedAccount.amount.toNumber() === 123);
332+
}
333+
245334
export async function approveRevoke(): Promise<void> {
246335
const delegate = new Account().publicKey;
247336

0 commit comments

Comments
 (0)