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
This commit is contained in:
parent
7d2556905c
commit
68b8da2996
|
@ -21,13 +21,11 @@ pub(crate) fn get_associated_token_address_and_bump_seed(
|
||||||
spl_token_mint_address: &Pubkey,
|
spl_token_mint_address: &Pubkey,
|
||||||
program_id: &Pubkey,
|
program_id: &Pubkey,
|
||||||
) -> (Pubkey, u8) {
|
) -> (Pubkey, u8) {
|
||||||
Pubkey::find_program_address(
|
get_associated_token_address_and_bump_seed_internal(
|
||||||
&[
|
wallet_address,
|
||||||
&wallet_address.to_bytes(),
|
spl_token_mint_address,
|
||||||
&spl_token::id().to_bytes(),
|
|
||||||
&spl_token_mint_address.to_bytes(),
|
|
||||||
],
|
|
||||||
program_id,
|
program_id,
|
||||||
|
&spl_token::id(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,6 +37,22 @@ pub fn get_associated_token_address(
|
||||||
get_associated_token_address_and_bump_seed(&wallet_address, &spl_token_mint_address, &id()).0
|
get_associated_token_address_and_bump_seed(&wallet_address, &spl_token_mint_address, &id()).0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_associated_token_address_and_bump_seed_internal(
|
||||||
|
wallet_address: &Pubkey,
|
||||||
|
spl_token_mint_address: &Pubkey,
|
||||||
|
program_id: &Pubkey,
|
||||||
|
token_program_id: &Pubkey,
|
||||||
|
) -> (Pubkey, u8) {
|
||||||
|
Pubkey::find_program_address(
|
||||||
|
&[
|
||||||
|
&wallet_address.to_bytes(),
|
||||||
|
&token_program_id.to_bytes(),
|
||||||
|
&spl_token_mint_address.to_bytes(),
|
||||||
|
],
|
||||||
|
program_id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Create an associated token account for the given wallet address and token mint
|
/// Create an associated token account for the given wallet address and token mint
|
||||||
///
|
///
|
||||||
/// Accounts expected by this instruction:
|
/// Accounts expected by this instruction:
|
||||||
|
|
|
@ -27,12 +27,14 @@ pub fn process_instruction(
|
||||||
let spl_token_mint_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 system_program_info = next_account_info(account_info_iter)?;
|
||||||
let spl_token_program_info = next_account_info(account_info_iter)?;
|
let spl_token_program_info = next_account_info(account_info_iter)?;
|
||||||
|
let spl_token_program_id = spl_token_program_info.key;
|
||||||
let rent_sysvar_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(
|
let (associated_token_address, bump_seed) = get_associated_token_address_and_bump_seed_internal(
|
||||||
&wallet_account_info.key,
|
&wallet_account_info.key,
|
||||||
&spl_token_mint_info.key,
|
&spl_token_mint_info.key,
|
||||||
program_id,
|
program_id,
|
||||||
|
&spl_token_program_id,
|
||||||
);
|
);
|
||||||
if associated_token_address != *associated_token_account_info.key {
|
if associated_token_address != *associated_token_account_info.key {
|
||||||
msg!("Error: Associated address does not match seed derivation");
|
msg!("Error: Associated address does not match seed derivation");
|
||||||
|
@ -41,7 +43,7 @@ pub fn process_instruction(
|
||||||
|
|
||||||
let associated_token_account_signer_seeds: &[&[_]] = &[
|
let associated_token_account_signer_seeds: &[&[_]] = &[
|
||||||
&wallet_account_info.key.to_bytes(),
|
&wallet_account_info.key.to_bytes(),
|
||||||
&spl_token::id().to_bytes(),
|
&spl_token_program_id.to_bytes(),
|
||||||
&spl_token_mint_info.key.to_bytes(),
|
&spl_token_mint_info.key.to_bytes(),
|
||||||
&[bump_seed],
|
&[bump_seed],
|
||||||
];
|
];
|
||||||
|
@ -87,7 +89,7 @@ pub fn process_instruction(
|
||||||
|
|
||||||
msg!("Assign the associated token account to the SPL Token program");
|
msg!("Assign the associated token account to the SPL Token program");
|
||||||
invoke_signed(
|
invoke_signed(
|
||||||
&system_instruction::assign(associated_token_account_info.key, &spl_token::id()),
|
&system_instruction::assign(associated_token_account_info.key, &spl_token_program_id),
|
||||||
&[
|
&[
|
||||||
associated_token_account_info.clone(),
|
associated_token_account_info.clone(),
|
||||||
system_program_info.clone(),
|
system_program_info.clone(),
|
||||||
|
@ -98,7 +100,7 @@ pub fn process_instruction(
|
||||||
msg!("Initialize the associated token account");
|
msg!("Initialize the associated token account");
|
||||||
invoke(
|
invoke(
|
||||||
&spl_token::instruction::initialize_account(
|
&spl_token::instruction::initialize_account(
|
||||||
&spl_token::id(),
|
&spl_token_program_id,
|
||||||
associated_token_account_info.key,
|
associated_token_account_info.key,
|
||||||
spl_token_mint_info.key,
|
spl_token_mint_info.key,
|
||||||
wallet_account_info.key,
|
wallet_account_info.key,
|
||||||
|
|
|
@ -10,5 +10,6 @@ npm install
|
||||||
npm run lint
|
npm run lint
|
||||||
npm run flow
|
npm run flow
|
||||||
npm run defs
|
npm run defs
|
||||||
|
npm run test
|
||||||
npm run start-with-test-validator
|
npm run start-with-test-validator
|
||||||
PROGRAM_VERSION=2.0.4 npm run start-with-test-validator
|
PROGRAM_VERSION=2.0.4 npm run start-with-test-validator
|
||||||
|
|
|
@ -8,8 +8,10 @@ import {
|
||||||
loadTokenProgram,
|
loadTokenProgram,
|
||||||
createMint,
|
createMint,
|
||||||
createAccount,
|
createAccount,
|
||||||
|
createAssociatedAccount,
|
||||||
transfer,
|
transfer,
|
||||||
transferChecked,
|
transferChecked,
|
||||||
|
transferCheckedAssociated,
|
||||||
approveRevoke,
|
approveRevoke,
|
||||||
failOnApproveOverspend,
|
failOnApproveOverspend,
|
||||||
setAuthority,
|
setAuthority,
|
||||||
|
@ -30,6 +32,8 @@ async function main() {
|
||||||
await createMint();
|
await createMint();
|
||||||
console.log('Run test: createAccount');
|
console.log('Run test: createAccount');
|
||||||
await createAccount();
|
await createAccount();
|
||||||
|
console.log('Run test: createAssociatedAccount');
|
||||||
|
await createAssociatedAccount();
|
||||||
console.log('Run test: mintTo');
|
console.log('Run test: mintTo');
|
||||||
await mintTo();
|
await mintTo();
|
||||||
console.log('Run test: mintToChecked');
|
console.log('Run test: mintToChecked');
|
||||||
|
@ -38,6 +42,8 @@ async function main() {
|
||||||
await transfer();
|
await transfer();
|
||||||
console.log('Run test: transferChecked');
|
console.log('Run test: transferChecked');
|
||||||
await transferChecked();
|
await transferChecked();
|
||||||
|
console.log('Run test: transferCheckedAssociated');
|
||||||
|
await transferCheckedAssociated();
|
||||||
console.log('Run test: approveRevoke');
|
console.log('Run test: approveRevoke');
|
||||||
await approveRevoke();
|
await approveRevoke();
|
||||||
console.log('Run test: failOnApproveOverspend');
|
console.log('Run test: failOnApproveOverspend');
|
||||||
|
|
|
@ -13,8 +13,12 @@ export class Store {
|
||||||
return path.join(__dirname, 'store');
|
return path.join(__dirname, 'store');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getFilename(uri: string): string {
|
||||||
|
return path.join(Store.getDir(), uri);
|
||||||
|
}
|
||||||
|
|
||||||
async load(uri: string): Promise<Object> {
|
async load(uri: string): Promise<Object> {
|
||||||
const filename = path.join(Store.getDir(), uri);
|
const filename = Store.getFilename(uri);
|
||||||
const data = await fs.readFile(filename, 'utf8');
|
const data = await fs.readFile(filename, 'utf8');
|
||||||
const config = JSON.parse(data);
|
const config = JSON.parse(data);
|
||||||
return config;
|
return config;
|
||||||
|
@ -22,7 +26,7 @@ export class Store {
|
||||||
|
|
||||||
async save(uri: string, config: Object): Promise<void> {
|
async save(uri: string, config: Object): Promise<void> {
|
||||||
await mkdirp(Store.getDir());
|
await mkdirp(Store.getDir());
|
||||||
const filename = path.join(Store.getDir(), uri);
|
const filename = Store.getFilename(uri);
|
||||||
await fs.writeFile(filename, JSON.stringify(config), 'utf8');
|
await fs.writeFile(filename, JSON.stringify(config), 'utf8');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,12 @@ import {
|
||||||
BPF_LOADER_PROGRAM_ID,
|
BPF_LOADER_PROGRAM_ID,
|
||||||
} from '@solana/web3.js';
|
} from '@solana/web3.js';
|
||||||
|
|
||||||
import {Token, NATIVE_MINT} from '../client/token';
|
import {
|
||||||
|
Token,
|
||||||
|
TOKEN_PROGRAM_ID,
|
||||||
|
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||||
|
NATIVE_MINT,
|
||||||
|
} from '../client/token';
|
||||||
import {url} from '../url';
|
import {url} from '../url';
|
||||||
import {newAccountWithLamports} from '../client/util/new-account-with-lamports';
|
import {newAccountWithLamports} from '../client/util/new-account-with-lamports';
|
||||||
import {sleep} from '../client/util/sleep';
|
import {sleep} from '../client/util/sleep';
|
||||||
|
@ -17,10 +22,12 @@ import {Store} from './store';
|
||||||
|
|
||||||
// Loaded token program's program id
|
// Loaded token program's program id
|
||||||
let programId: PublicKey;
|
let programId: PublicKey;
|
||||||
|
let associatedProgramId: PublicKey;
|
||||||
|
|
||||||
// Accounts setup in createMint and used by all subsequent tests
|
// Accounts setup in createMint and used by all subsequent tests
|
||||||
let testMintAuthority: Account;
|
let testMintAuthority: Account;
|
||||||
let testToken: Token;
|
let testToken: Token;
|
||||||
|
let testTokenDecimals: number = 2;
|
||||||
|
|
||||||
// Accounts setup in createAccount and used by all subsequent tests
|
// Accounts setup in createAccount and used by all subsequent tests
|
||||||
let testAccountOwner: Account;
|
let testAccountOwner: Account;
|
||||||
|
@ -78,43 +85,60 @@ async function loadProgram(
|
||||||
return program_account.publicKey;
|
return program_account.publicKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function GetPrograms(connection: Connection): Promise<PublicKey> {
|
async function GetPrograms(connection: Connection): Promise<void> {
|
||||||
const programVersion = process.env.PROGRAM_VERSION;
|
const programVersion = process.env.PROGRAM_VERSION;
|
||||||
if (programVersion) {
|
if (programVersion) {
|
||||||
switch (programVersion) {
|
switch (programVersion) {
|
||||||
case '2.0.4':
|
case '2.0.4':
|
||||||
return new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
|
programId = TOKEN_PROGRAM_ID;
|
||||||
|
associatedProgramId = ASSOCIATED_TOKEN_PROGRAM_ID;
|
||||||
|
return;
|
||||||
default:
|
default:
|
||||||
throw new Error('Unknown program version');
|
throw new Error('Unknown program version');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = new Store();
|
const store = new Store();
|
||||||
let tokenProgramId = null;
|
|
||||||
try {
|
try {
|
||||||
const config = await store.load('config.json');
|
const config = await store.load('config.json');
|
||||||
console.log('Using pre-loaded Token program');
|
console.log('Using pre-loaded Token programs');
|
||||||
console.log(
|
console.log(
|
||||||
' Note: To reload program remove client/util/store/config.json',
|
` Note: To reload program remove ${Store.getFilename('config.json')}`,
|
||||||
);
|
);
|
||||||
tokenProgramId = new PublicKey(config.tokenProgramId);
|
programId = new PublicKey(config.tokenProgramId);
|
||||||
|
associatedProgramId = new PublicKey(config.associatedTokenProgramId);
|
||||||
|
let info;
|
||||||
|
info = await connection.getAccountInfo(programId);
|
||||||
|
assert(info != null);
|
||||||
|
info = await connection.getAccountInfo(associatedProgramId);
|
||||||
|
assert(info != null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
tokenProgramId = await loadProgram(
|
console.log(
|
||||||
|
'Checking pre-loaded Token programs failed, will load new programs:',
|
||||||
|
);
|
||||||
|
console.log({err});
|
||||||
|
|
||||||
|
programId = await loadProgram(
|
||||||
connection,
|
connection,
|
||||||
'../../target/bpfel-unknown-unknown/release/spl_token.so',
|
'../../target/bpfel-unknown-unknown/release/spl_token.so',
|
||||||
);
|
);
|
||||||
|
associatedProgramId = await loadProgram(
|
||||||
|
connection,
|
||||||
|
'../../target/bpfel-unknown-unknown/release/spl_associated_token_account.so',
|
||||||
|
);
|
||||||
await store.save('config.json', {
|
await store.save('config.json', {
|
||||||
tokenProgramId: tokenProgramId.toString(),
|
tokenProgramId: programId.toString(),
|
||||||
|
associatedTokenProgramId: associatedProgramId.toString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return tokenProgramId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadTokenProgram(): Promise<void> {
|
export async function loadTokenProgram(): Promise<void> {
|
||||||
const connection = await getConnection();
|
const connection = await getConnection();
|
||||||
programId = await GetPrograms(connection);
|
await GetPrograms(connection);
|
||||||
|
|
||||||
console.log('Token Program ID', programId.toString());
|
console.log('Token Program ID', programId.toString());
|
||||||
|
console.log('Associated Token Program ID', associatedProgramId.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createMint(): Promise<void> {
|
export async function createMint(): Promise<void> {
|
||||||
|
@ -126,9 +150,12 @@ export async function createMint(): Promise<void> {
|
||||||
payer,
|
payer,
|
||||||
testMintAuthority.publicKey,
|
testMintAuthority.publicKey,
|
||||||
testMintAuthority.publicKey,
|
testMintAuthority.publicKey,
|
||||||
2,
|
testTokenDecimals,
|
||||||
programId,
|
programId,
|
||||||
);
|
);
|
||||||
|
// HACK: override hard-coded ASSOCIATED_TOKEN_PROGRAM_ID with corresponding
|
||||||
|
// custom test fixture
|
||||||
|
testToken.associatedProgramId = associatedProgramId;
|
||||||
|
|
||||||
const mintInfo = await testToken.getMintInfo();
|
const mintInfo = await testToken.getMintInfo();
|
||||||
if (mintInfo.mintAuthority !== null) {
|
if (mintInfo.mintAuthority !== null) {
|
||||||
|
@ -137,7 +164,7 @@ export async function createMint(): Promise<void> {
|
||||||
assert(mintInfo.mintAuthority !== null);
|
assert(mintInfo.mintAuthority !== null);
|
||||||
}
|
}
|
||||||
assert(mintInfo.supply.toNumber() === 0);
|
assert(mintInfo.supply.toNumber() === 0);
|
||||||
assert(mintInfo.decimals === 2);
|
assert(mintInfo.decimals === testTokenDecimals);
|
||||||
assert(mintInfo.isInitialized === true);
|
assert(mintInfo.isInitialized === true);
|
||||||
if (mintInfo.freezeAuthority !== null) {
|
if (mintInfo.freezeAuthority !== null) {
|
||||||
assert(mintInfo.freezeAuthority.equals(testMintAuthority.publicKey));
|
assert(mintInfo.freezeAuthority.equals(testMintAuthority.publicKey));
|
||||||
|
@ -160,6 +187,48 @@ export async function createAccount(): Promise<void> {
|
||||||
assert(accountInfo.isNative === false);
|
assert(accountInfo.isNative === false);
|
||||||
assert(accountInfo.rentExemptReserve === null);
|
assert(accountInfo.rentExemptReserve === null);
|
||||||
assert(accountInfo.closeAuthority === null);
|
assert(accountInfo.closeAuthority === null);
|
||||||
|
|
||||||
|
// you can create as many accounts as with same owner
|
||||||
|
const testAccount2 = await testToken.createAccount(
|
||||||
|
testAccountOwner.publicKey,
|
||||||
|
);
|
||||||
|
assert(!testAccount2.equals(testAccount));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAssociatedAccount(): Promise<void> {
|
||||||
|
let info;
|
||||||
|
const connection = await getConnection();
|
||||||
|
|
||||||
|
const owner = new Account();
|
||||||
|
const associatedAddress = await Token.getAssociatedTokenAddress(
|
||||||
|
associatedProgramId,
|
||||||
|
programId,
|
||||||
|
testToken.publicKey,
|
||||||
|
owner.publicKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
// associated account shouldn't exist
|
||||||
|
info = await connection.getAccountInfo(associatedAddress);
|
||||||
|
assert(info == null);
|
||||||
|
|
||||||
|
const createdAddress = await testToken.createAssociatedTokenAccount(
|
||||||
|
owner.publicKey,
|
||||||
|
);
|
||||||
|
assert(createdAddress.equals(associatedAddress));
|
||||||
|
|
||||||
|
// associated account should exist now
|
||||||
|
info = await testToken.getAccountInfo(associatedAddress);
|
||||||
|
assert(info != null);
|
||||||
|
assert(info.mint.equals(testToken.publicKey));
|
||||||
|
assert(info.owner.equals(owner.publicKey));
|
||||||
|
assert(info.amount.toNumber() === 0);
|
||||||
|
|
||||||
|
// creating again should cause TX error for the associated token account
|
||||||
|
assert(
|
||||||
|
await didThrow(testToken, testToken.createAssociatedTokenAccount, [
|
||||||
|
owner.publicKey,
|
||||||
|
]),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function mintTo(): Promise<void> {
|
export async function mintTo(): Promise<void> {
|
||||||
|
@ -219,7 +288,7 @@ export async function transferChecked(): Promise<void> {
|
||||||
testAccountOwner,
|
testAccountOwner,
|
||||||
[],
|
[],
|
||||||
100,
|
100,
|
||||||
1,
|
testTokenDecimals - 1,
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -229,7 +298,7 @@ export async function transferChecked(): Promise<void> {
|
||||||
testAccountOwner,
|
testAccountOwner,
|
||||||
[],
|
[],
|
||||||
100,
|
100,
|
||||||
2,
|
testTokenDecimals,
|
||||||
);
|
);
|
||||||
|
|
||||||
const mintInfo = await testToken.getMintInfo();
|
const mintInfo = await testToken.getMintInfo();
|
||||||
|
@ -242,6 +311,26 @@ export async function transferChecked(): Promise<void> {
|
||||||
assert(testAccountInfo.amount.toNumber() === 1800);
|
assert(testAccountInfo.amount.toNumber() === 1800);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function transferCheckedAssociated(): Promise<void> {
|
||||||
|
const dest = new Account().publicKey;
|
||||||
|
let associatedAccount;
|
||||||
|
|
||||||
|
associatedAccount = await testToken.getOrCreateAssociatedAccountInfo(dest);
|
||||||
|
assert(associatedAccount.amount.toNumber() === 0);
|
||||||
|
|
||||||
|
await testToken.transferChecked(
|
||||||
|
testAccount,
|
||||||
|
associatedAccount.address,
|
||||||
|
testAccountOwner,
|
||||||
|
[],
|
||||||
|
123,
|
||||||
|
testTokenDecimals,
|
||||||
|
);
|
||||||
|
|
||||||
|
associatedAccount = await testToken.getOrCreateAssociatedAccountInfo(dest);
|
||||||
|
assert(associatedAccount.amount.toNumber() === 123);
|
||||||
|
}
|
||||||
|
|
||||||
export async function approveRevoke(): Promise<void> {
|
export async function approveRevoke(): Promise<void> {
|
||||||
const delegate = new Account().publicKey;
|
const delegate = new Account().publicKey;
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,13 @@ export const TOKEN_PROGRAM_ID: PublicKey = new PublicKey(
|
||||||
'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
|
'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const ASSOCIATED_TOKEN_PROGRAM_ID: PublicKey = new PublicKey(
|
||||||
|
'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL',
|
||||||
|
);
|
||||||
|
|
||||||
|
const FAILED_TO_FIND_ACCOUNT = 'Failed to find account';
|
||||||
|
const INVALID_ACCOUNT_OWNER = 'Invalid account owner';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unfortunately, BufferLayout.encode uses an `instanceof` check for `Buffer`
|
* Unfortunately, BufferLayout.encode uses an `instanceof` check for `Buffer`
|
||||||
* which fails when using `publicKey.toBuffer()` directly because the bundled `Buffer`
|
* which fails when using `publicKey.toBuffer()` directly because the bundled `Buffer`
|
||||||
|
@ -139,6 +146,11 @@ export const MintLayout: typeof BufferLayout.Structure = BufferLayout.struct([
|
||||||
* Information about an account
|
* Information about an account
|
||||||
*/
|
*/
|
||||||
type AccountInfo = {|
|
type AccountInfo = {|
|
||||||
|
/**
|
||||||
|
* The address of this account
|
||||||
|
*/
|
||||||
|
address: PublicKey,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The mint associated with this account
|
* The mint associated with this account
|
||||||
*/
|
*/
|
||||||
|
@ -286,6 +298,11 @@ export class Token {
|
||||||
*/
|
*/
|
||||||
programId: PublicKey;
|
programId: PublicKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Program Identifier for the Associated Token program
|
||||||
|
*/
|
||||||
|
associatedProgramId: PublicKey;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fee payer
|
* Fee payer
|
||||||
*/
|
*/
|
||||||
|
@ -305,7 +322,14 @@ export class Token {
|
||||||
programId: PublicKey,
|
programId: PublicKey,
|
||||||
payer: Account,
|
payer: Account,
|
||||||
) {
|
) {
|
||||||
Object.assign(this, {connection, publicKey, programId, payer});
|
Object.assign(this, {
|
||||||
|
connection,
|
||||||
|
publicKey,
|
||||||
|
programId,
|
||||||
|
payer,
|
||||||
|
// Hard code is ok; Overriding is needed only for tests
|
||||||
|
associatedProgramId: ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -458,6 +482,101 @@ export class Token {
|
||||||
return newAccount.publicKey;
|
return newAccount.publicKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and initialize the associated account.
|
||||||
|
*
|
||||||
|
* This account may then be used as a `transfer()` or `approve()` destination
|
||||||
|
*
|
||||||
|
* @param owner User account that will own the new account
|
||||||
|
* @return Public key of the new associated account
|
||||||
|
*/
|
||||||
|
async createAssociatedTokenAccount(owner: PublicKey): Promise<PublicKey> {
|
||||||
|
const associatedAddress = await Token.getAssociatedTokenAddress(
|
||||||
|
this.associatedProgramId,
|
||||||
|
this.programId,
|
||||||
|
this.publicKey,
|
||||||
|
owner,
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.createAssociatedTokenAccountInternal(owner, associatedAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createAssociatedTokenAccountInternal(
|
||||||
|
owner: PublicKey,
|
||||||
|
associatedAddress: PublicKey,
|
||||||
|
): Promise<PublicKey> {
|
||||||
|
await sendAndConfirmTransaction(
|
||||||
|
'CreateAssociatedTokenAccount',
|
||||||
|
this.connection,
|
||||||
|
new Transaction().add(
|
||||||
|
Token.createAssociatedTokenAccountInstruction(
|
||||||
|
this.associatedProgramId,
|
||||||
|
this.programId,
|
||||||
|
this.publicKey,
|
||||||
|
associatedAddress,
|
||||||
|
owner,
|
||||||
|
this.payer.publicKey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
this.payer,
|
||||||
|
);
|
||||||
|
|
||||||
|
return associatedAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the associated account or create one if not found.
|
||||||
|
*
|
||||||
|
* This account may then be used as a `transfer()` or `approve()` destination
|
||||||
|
*
|
||||||
|
* @param owner User account that will own the new account
|
||||||
|
* @return The new associated account
|
||||||
|
*/
|
||||||
|
async getOrCreateAssociatedAccountInfo(
|
||||||
|
owner: PublicKey,
|
||||||
|
): Promise<AccountInfo> {
|
||||||
|
const associatedAddress = await Token.getAssociatedTokenAddress(
|
||||||
|
this.associatedProgramId,
|
||||||
|
this.programId,
|
||||||
|
this.publicKey,
|
||||||
|
owner,
|
||||||
|
);
|
||||||
|
|
||||||
|
// This is the optimum logic, considering TX fee, client-side computation,
|
||||||
|
// RPC roundtrips and guaranteed idempotent.
|
||||||
|
// Sadly we can't do this atomically;
|
||||||
|
try {
|
||||||
|
return await this.getAccountInfo(associatedAddress);
|
||||||
|
} catch (err) {
|
||||||
|
// INVALID_ACCOUNT_OWNER can be possible if the associatedAddress has
|
||||||
|
// already been received some lamports (= became system accounts).
|
||||||
|
// Assuming program derived addressing is safe, this is the only case
|
||||||
|
// for the INVALID_ACCOUNT_OWNER in this code-path
|
||||||
|
if (
|
||||||
|
err.message === FAILED_TO_FIND_ACCOUNT ||
|
||||||
|
err.message === INVALID_ACCOUNT_OWNER
|
||||||
|
) {
|
||||||
|
// as this isn't atomic, it's possible others can create associated
|
||||||
|
// accounts meanwhile
|
||||||
|
try {
|
||||||
|
await this.createAssociatedTokenAccountInternal(
|
||||||
|
owner,
|
||||||
|
associatedAddress,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
// ignore all errors; for now there is no API compatible way to
|
||||||
|
// selectively ignore the expected instruction error if the
|
||||||
|
// associated account is existing already.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now this should always succeed
|
||||||
|
return await this.getAccountInfo(associatedAddress);
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create and initialize a new account on the special native token mint.
|
* Create and initialize a new account on the special native token mint.
|
||||||
*
|
*
|
||||||
|
@ -645,10 +764,10 @@ export class Token {
|
||||||
): Promise<AccountInfo> {
|
): Promise<AccountInfo> {
|
||||||
const info = await this.connection.getAccountInfo(account, commitment);
|
const info = await this.connection.getAccountInfo(account, commitment);
|
||||||
if (info === null) {
|
if (info === null) {
|
||||||
throw new Error('Failed to find account');
|
throw new Error(FAILED_TO_FIND_ACCOUNT);
|
||||||
}
|
}
|
||||||
if (!info.owner.equals(this.programId)) {
|
if (!info.owner.equals(this.programId)) {
|
||||||
throw new Error(`Invalid account owner`);
|
throw new Error(INVALID_ACCOUNT_OWNER);
|
||||||
}
|
}
|
||||||
if (info.data.length != AccountLayout.span) {
|
if (info.data.length != AccountLayout.span) {
|
||||||
throw new Error(`Invalid account size`);
|
throw new Error(`Invalid account size`);
|
||||||
|
@ -656,6 +775,7 @@ export class Token {
|
||||||
|
|
||||||
const data = Buffer.from(info.data);
|
const data = Buffer.from(info.data);
|
||||||
const accountInfo = AccountLayout.decode(data);
|
const accountInfo = AccountLayout.decode(data);
|
||||||
|
accountInfo.address = account;
|
||||||
accountInfo.mint = new PublicKey(accountInfo.mint);
|
accountInfo.mint = new PublicKey(accountInfo.mint);
|
||||||
accountInfo.owner = new PublicKey(accountInfo.owner);
|
accountInfo.owner = new PublicKey(accountInfo.owner);
|
||||||
accountInfo.amount = u64.fromBuffer(accountInfo.amount);
|
accountInfo.amount = u64.fromBuffer(accountInfo.amount);
|
||||||
|
@ -2103,4 +2223,65 @@ export class Token {
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the address for the associated token account
|
||||||
|
*
|
||||||
|
* @param associatedProgramId SPL Associated Token program account
|
||||||
|
* @param programId SPL Token program account
|
||||||
|
* @param mint Token mint account
|
||||||
|
* @param owner Owner of the new account
|
||||||
|
* @return Public key of the associated token account
|
||||||
|
*/
|
||||||
|
static async getAssociatedTokenAddress(
|
||||||
|
associatedProgramId: PublicKey,
|
||||||
|
programId: PublicKey,
|
||||||
|
mint: PublicKey,
|
||||||
|
owner: PublicKey,
|
||||||
|
): Promise<PublicKey> {
|
||||||
|
return (
|
||||||
|
await PublicKey.findProgramAddress(
|
||||||
|
[owner.toBuffer(), programId.toBuffer(), mint.toBuffer()],
|
||||||
|
associatedProgramId,
|
||||||
|
)
|
||||||
|
)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct the AssociatedTokenProgram instruction to create the associated
|
||||||
|
* token account
|
||||||
|
*
|
||||||
|
* @param associatedProgramId SPL Associated Token program account
|
||||||
|
* @param programId SPL Token program account
|
||||||
|
* @param mint Token mint account
|
||||||
|
* @param associatedAccount New associated account
|
||||||
|
* @param owner Owner of the new account
|
||||||
|
* @param payer Payer of fees
|
||||||
|
*/
|
||||||
|
static createAssociatedTokenAccountInstruction(
|
||||||
|
associatedProgramId: PublicKey,
|
||||||
|
programId: PublicKey,
|
||||||
|
mint: PublicKey,
|
||||||
|
associatedAccount: PublicKey,
|
||||||
|
owner: PublicKey,
|
||||||
|
payer: PublicKey,
|
||||||
|
): TransactionInstruction {
|
||||||
|
const data = Buffer.alloc(0);
|
||||||
|
|
||||||
|
let keys = [
|
||||||
|
{pubkey: payer, isSigner: true, isWritable: true},
|
||||||
|
{pubkey: associatedAccount, isSigner: false, isWritable: true},
|
||||||
|
{pubkey: owner, isSigner: false, isWritable: false},
|
||||||
|
{pubkey: mint, isSigner: false, isWritable: false},
|
||||||
|
{pubkey: SystemProgram.programId, isSigner: false, isWritable: false},
|
||||||
|
{pubkey: programId, isSigner: false, isWritable: false},
|
||||||
|
{pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false},
|
||||||
|
];
|
||||||
|
|
||||||
|
return new TransactionInstruction({
|
||||||
|
keys,
|
||||||
|
programId: associatedProgramId,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ declare module '@solana/spl-token' {
|
||||||
|
|
||||||
// === client/token.js ===
|
// === client/token.js ===
|
||||||
export const TOKEN_PROGRAM_ID: PublicKey;
|
export const TOKEN_PROGRAM_ID: PublicKey;
|
||||||
|
export const ASSOCIATED_TOKEN_PROGRAM_ID: PublicKey;
|
||||||
|
|
||||||
export class u64 extends BN {
|
export class u64 extends BN {
|
||||||
toBuffer(): Buffer;
|
toBuffer(): Buffer;
|
||||||
|
@ -35,6 +36,7 @@ declare module '@solana/spl-token' {
|
||||||
|
|
||||||
export const AccountLayout: Layout;
|
export const AccountLayout: Layout;
|
||||||
export type AccountInfo = {
|
export type AccountInfo = {
|
||||||
|
address: PublicKey;
|
||||||
mint: PublicKey;
|
mint: PublicKey;
|
||||||
owner: PublicKey;
|
owner: PublicKey;
|
||||||
amount: u64;
|
amount: u64;
|
||||||
|
@ -65,6 +67,7 @@ declare module '@solana/spl-token' {
|
||||||
export class Token {
|
export class Token {
|
||||||
publicKey: PublicKey;
|
publicKey: PublicKey;
|
||||||
programId: PublicKey;
|
programId: PublicKey;
|
||||||
|
associatedProgramId: PublicKey;
|
||||||
payer: Account;
|
payer: Account;
|
||||||
constructor(
|
constructor(
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
|
@ -81,6 +84,12 @@ declare module '@solana/spl-token' {
|
||||||
static getMinBalanceRentForExemptMultisig(
|
static getMinBalanceRentForExemptMultisig(
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
): Promise<number>;
|
): Promise<number>;
|
||||||
|
static getAssociatedTokenAddress(
|
||||||
|
associatedProgramId: PublicKey,
|
||||||
|
programId: PublicKey,
|
||||||
|
mint: PublicKey,
|
||||||
|
owner: PublicKey,
|
||||||
|
): Promise<PublicKey>;
|
||||||
static createMint(
|
static createMint(
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
payer: Account,
|
payer: Account,
|
||||||
|
@ -90,6 +99,7 @@ declare module '@solana/spl-token' {
|
||||||
programId: PublicKey,
|
programId: PublicKey,
|
||||||
): Promise<Token>;
|
): Promise<Token>;
|
||||||
createAccount(owner: PublicKey): Promise<PublicKey>;
|
createAccount(owner: PublicKey): Promise<PublicKey>;
|
||||||
|
createAssociatedTokenAccount(owner: PublicKey): Promise<PublicKey>;
|
||||||
static createWrappedNativeAccount(
|
static createWrappedNativeAccount(
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
programId: PublicKey,
|
programId: PublicKey,
|
||||||
|
@ -100,6 +110,7 @@ declare module '@solana/spl-token' {
|
||||||
createMultisig(m: number, signers: Array<PublicKey>): Promise<PublicKey>;
|
createMultisig(m: number, signers: Array<PublicKey>): Promise<PublicKey>;
|
||||||
getMintInfo(): Promise<MintInfo>;
|
getMintInfo(): Promise<MintInfo>;
|
||||||
getAccountInfo(account: PublicKey): Promise<AccountInfo>;
|
getAccountInfo(account: PublicKey): Promise<AccountInfo>;
|
||||||
|
getOrCreateAssociatedAccountInfo(owner: PublicKey): Promise<AccountInfo>;
|
||||||
getMultisigInfo(multisig: PublicKey): Promise<MultisigInfo>;
|
getMultisigInfo(multisig: PublicKey): Promise<MultisigInfo>;
|
||||||
transfer(
|
transfer(
|
||||||
source: PublicKey,
|
source: PublicKey,
|
||||||
|
@ -235,5 +246,13 @@ declare module '@solana/spl-token' {
|
||||||
authority: PublicKey,
|
authority: PublicKey,
|
||||||
multiSigners: Array<Account>,
|
multiSigners: Array<Account>,
|
||||||
): TransactionInstruction;
|
): TransactionInstruction;
|
||||||
|
static createAssociatedTokenAccountInstruction(
|
||||||
|
associatedProgramId: PublicKey,
|
||||||
|
programId: PublicKey,
|
||||||
|
mint: PublicKey,
|
||||||
|
associatedAccount: PublicKey,
|
||||||
|
owner: PublicKey,
|
||||||
|
payer: PublicKey,
|
||||||
|
): TransactionInstruction;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ import type {TransactionSignature} from '@solana/web3.js';
|
||||||
|
|
||||||
declare module '@solana/spl-token' {
|
declare module '@solana/spl-token' {
|
||||||
declare export var TOKEN_PROGRAM_ID;
|
declare export var TOKEN_PROGRAM_ID;
|
||||||
|
declare export var ASSOCIATED_TOKEN_PROGRAM_ID;
|
||||||
declare export class u64 extends BN {
|
declare export class u64 extends BN {
|
||||||
toBuffer(): typeof Buffer;
|
toBuffer(): typeof Buffer;
|
||||||
static fromBuffer(buffer: typeof Buffer): u64;
|
static fromBuffer(buffer: typeof Buffer): u64;
|
||||||
|
@ -37,6 +38,7 @@ declare module '@solana/spl-token' {
|
||||||
|};
|
|};
|
||||||
declare export var AccountLayout: typeof Layout;
|
declare export var AccountLayout: typeof Layout;
|
||||||
declare export type AccountInfo = {|
|
declare export type AccountInfo = {|
|
||||||
|
address: PublicKey,
|
||||||
mint: PublicKey,
|
mint: PublicKey,
|
||||||
owner: PublicKey,
|
owner: PublicKey,
|
||||||
amount: u64,
|
amount: u64,
|
||||||
|
@ -67,6 +69,7 @@ declare module '@solana/spl-token' {
|
||||||
declare export class Token {
|
declare export class Token {
|
||||||
publicKey: PublicKey;
|
publicKey: PublicKey;
|
||||||
programId: PublicKey;
|
programId: PublicKey;
|
||||||
|
associatedProgramId: PublicKey;
|
||||||
payer: Account;
|
payer: Account;
|
||||||
constructor(
|
constructor(
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
|
@ -83,6 +86,12 @@ declare module '@solana/spl-token' {
|
||||||
static getMinBalanceRentForExemptMultisig(
|
static getMinBalanceRentForExemptMultisig(
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
): Promise<number>;
|
): Promise<number>;
|
||||||
|
static getAssociatedTokenAddress(
|
||||||
|
associatedProgramId: PublicKey,
|
||||||
|
programId: PublicKey,
|
||||||
|
mint: PublicKey,
|
||||||
|
owner: PublicKey,
|
||||||
|
): Promise<PublicKey>;
|
||||||
static createMint(
|
static createMint(
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
payer: Account,
|
payer: Account,
|
||||||
|
@ -92,6 +101,7 @@ declare module '@solana/spl-token' {
|
||||||
programId: PublicKey,
|
programId: PublicKey,
|
||||||
): Promise<Token>;
|
): Promise<Token>;
|
||||||
createAccount(owner: PublicKey): Promise<PublicKey>;
|
createAccount(owner: PublicKey): Promise<PublicKey>;
|
||||||
|
createAssociatedTokenAccount(owner: PublicKey): Promise<PublicKey>;
|
||||||
static createWrappedNativeAccount(
|
static createWrappedNativeAccount(
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
programId: PublicKey,
|
programId: PublicKey,
|
||||||
|
@ -102,6 +112,7 @@ declare module '@solana/spl-token' {
|
||||||
createMultisig(m: number, signers: Array<PublicKey>): Promise<PublicKey>;
|
createMultisig(m: number, signers: Array<PublicKey>): Promise<PublicKey>;
|
||||||
getMintInfo(): Promise<MintInfo>;
|
getMintInfo(): Promise<MintInfo>;
|
||||||
getAccountInfo(account: PublicKey): Promise<AccountInfo>;
|
getAccountInfo(account: PublicKey): Promise<AccountInfo>;
|
||||||
|
getOrCreateAssociatedAccountInfo(owner: PublicKey): Promise<AccountInfo>;
|
||||||
getMultisigInfo(multisig: PublicKey): Promise<MultisigInfo>;
|
getMultisigInfo(multisig: PublicKey): Promise<MultisigInfo>;
|
||||||
transfer(
|
transfer(
|
||||||
source: PublicKey,
|
source: PublicKey,
|
||||||
|
@ -237,5 +248,13 @@ declare module '@solana/spl-token' {
|
||||||
authority: PublicKey,
|
authority: PublicKey,
|
||||||
multiSigners: Array<Account>,
|
multiSigners: Array<Account>,
|
||||||
): TransactionInstruction;
|
): TransactionInstruction;
|
||||||
|
static createAssociatedTokenAccountInstruction(
|
||||||
|
associatedProgramId: PublicKey,
|
||||||
|
programId: PublicKey,
|
||||||
|
mint: PublicKey,
|
||||||
|
associatedAccount: PublicKey,
|
||||||
|
owner: PublicKey,
|
||||||
|
payer: PublicKey,
|
||||||
|
): TransactionInstruction;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@solana/spl-token",
|
"name": "@solana/spl-token",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"description": "SPL Token JavaScript API",
|
"description": "SPL Token JavaScript API",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "Solana Maintainers <maintainers@solana.com>",
|
"author": "Solana Maintainers <maintainers@solana.com>",
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
// @flow
|
// @flow
|
||||||
import {expect} from 'chai';
|
import {expect} from 'chai';
|
||||||
import {Account} from '@solana/web3.js';
|
import {Account, PublicKey} from '@solana/web3.js';
|
||||||
|
|
||||||
import {Token, TOKEN_PROGRAM_ID} from '../client/token';
|
import {ASSOCIATED_TOKEN_PROGRAM_ID, Token, TOKEN_PROGRAM_ID} from '../client/token';
|
||||||
|
|
||||||
describe('Token', () => {
|
describe('Token', () => {
|
||||||
it('createTransfer', () => {
|
it('createTransfer', () => {
|
||||||
|
@ -31,4 +31,29 @@ describe('Token', () => {
|
||||||
expect(ix.programId).to.eql(TOKEN_PROGRAM_ID);
|
expect(ix.programId).to.eql(TOKEN_PROGRAM_ID);
|
||||||
expect(ix.keys).to.have.length(2);
|
expect(ix.keys).to.have.length(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('getAssociatedTokenAddress', async () => {
|
||||||
|
const associatedPublicKey = await Token.getAssociatedTokenAddress(
|
||||||
|
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||||
|
TOKEN_PROGRAM_ID,
|
||||||
|
new PublicKey('7o36UsWR1JQLpZ9PE2gn9L4SQ69CNNiWAXd4Jt7rqz9Z'),
|
||||||
|
new PublicKey('B8UwBUUnKwCyKuGMbFKWaG7exYdDk2ozZrPg72NyVbfj'),
|
||||||
|
);
|
||||||
|
expect(associatedPublicKey.toString()).to.eql(
|
||||||
|
new PublicKey('DShWnroshVbeUp28oopA3Pu7oFPDBtC1DBmPECXXAQ9n').toString(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('createAssociatedTokenAccount', () => {
|
||||||
|
const ix = Token.createAssociatedTokenAccountInstruction(
|
||||||
|
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||||
|
TOKEN_PROGRAM_ID,
|
||||||
|
new Account().publicKey,
|
||||||
|
new Account().publicKey,
|
||||||
|
new Account().publicKey,
|
||||||
|
new Account().publicKey,
|
||||||
|
);
|
||||||
|
expect(ix.programId).to.eql(ASSOCIATED_TOKEN_PROGRAM_ID);
|
||||||
|
expect(ix.keys).to.have.length(7);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue