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,
|
||||
program_id: &Pubkey,
|
||||
) -> (Pubkey, u8) {
|
||||
Pubkey::find_program_address(
|
||||
&[
|
||||
&wallet_address.to_bytes(),
|
||||
&spl_token::id().to_bytes(),
|
||||
&spl_token_mint_address.to_bytes(),
|
||||
],
|
||||
get_associated_token_address_and_bump_seed_internal(
|
||||
wallet_address,
|
||||
spl_token_mint_address,
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
///
|
||||
/// 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 system_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 (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,
|
||||
&spl_token_mint_info.key,
|
||||
program_id,
|
||||
&spl_token_program_id,
|
||||
);
|
||||
if associated_token_address != *associated_token_account_info.key {
|
||||
msg!("Error: Associated address does not match seed derivation");
|
||||
|
@ -41,7 +43,7 @@ pub fn process_instruction(
|
|||
|
||||
let associated_token_account_signer_seeds: &[&[_]] = &[
|
||||
&wallet_account_info.key.to_bytes(),
|
||||
&spl_token::id().to_bytes(),
|
||||
&spl_token_program_id.to_bytes(),
|
||||
&spl_token_mint_info.key.to_bytes(),
|
||||
&[bump_seed],
|
||||
];
|
||||
|
@ -87,7 +89,7 @@ pub fn process_instruction(
|
|||
|
||||
msg!("Assign the associated token account to the SPL Token program");
|
||||
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(),
|
||||
system_program_info.clone(),
|
||||
|
@ -98,7 +100,7 @@ pub fn process_instruction(
|
|||
msg!("Initialize the associated token account");
|
||||
invoke(
|
||||
&spl_token::instruction::initialize_account(
|
||||
&spl_token::id(),
|
||||
&spl_token_program_id,
|
||||
associated_token_account_info.key,
|
||||
spl_token_mint_info.key,
|
||||
wallet_account_info.key,
|
||||
|
|
|
@ -10,5 +10,6 @@ npm install
|
|||
npm run lint
|
||||
npm run flow
|
||||
npm run defs
|
||||
npm run test
|
||||
npm run start-with-test-validator
|
||||
PROGRAM_VERSION=2.0.4 npm run start-with-test-validator
|
||||
|
|
|
@ -8,8 +8,10 @@ import {
|
|||
loadTokenProgram,
|
||||
createMint,
|
||||
createAccount,
|
||||
createAssociatedAccount,
|
||||
transfer,
|
||||
transferChecked,
|
||||
transferCheckedAssociated,
|
||||
approveRevoke,
|
||||
failOnApproveOverspend,
|
||||
setAuthority,
|
||||
|
@ -30,6 +32,8 @@ async function main() {
|
|||
await createMint();
|
||||
console.log('Run test: createAccount');
|
||||
await createAccount();
|
||||
console.log('Run test: createAssociatedAccount');
|
||||
await createAssociatedAccount();
|
||||
console.log('Run test: mintTo');
|
||||
await mintTo();
|
||||
console.log('Run test: mintToChecked');
|
||||
|
@ -38,6 +42,8 @@ async function main() {
|
|||
await transfer();
|
||||
console.log('Run test: transferChecked');
|
||||
await transferChecked();
|
||||
console.log('Run test: transferCheckedAssociated');
|
||||
await transferCheckedAssociated();
|
||||
console.log('Run test: approveRevoke');
|
||||
await approveRevoke();
|
||||
console.log('Run test: failOnApproveOverspend');
|
||||
|
|
|
@ -13,8 +13,12 @@ export class Store {
|
|||
return path.join(__dirname, 'store');
|
||||
}
|
||||
|
||||
static getFilename(uri: string): string {
|
||||
return path.join(Store.getDir(), uri);
|
||||
}
|
||||
|
||||
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 config = JSON.parse(data);
|
||||
return config;
|
||||
|
@ -22,7 +26,7 @@ export class Store {
|
|||
|
||||
async save(uri: string, config: Object): Promise<void> {
|
||||
await mkdirp(Store.getDir());
|
||||
const filename = path.join(Store.getDir(), uri);
|
||||
const filename = Store.getFilename(uri);
|
||||
await fs.writeFile(filename, JSON.stringify(config), 'utf8');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,12 @@ import {
|
|||
BPF_LOADER_PROGRAM_ID,
|
||||
} 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 {newAccountWithLamports} from '../client/util/new-account-with-lamports';
|
||||
import {sleep} from '../client/util/sleep';
|
||||
|
@ -17,10 +22,12 @@ import {Store} from './store';
|
|||
|
||||
// Loaded token program's program id
|
||||
let programId: PublicKey;
|
||||
let associatedProgramId: PublicKey;
|
||||
|
||||
// Accounts setup in createMint and used by all subsequent tests
|
||||
let testMintAuthority: Account;
|
||||
let testToken: Token;
|
||||
let testTokenDecimals: number = 2;
|
||||
|
||||
// Accounts setup in createAccount and used by all subsequent tests
|
||||
let testAccountOwner: Account;
|
||||
|
@ -78,43 +85,60 @@ async function loadProgram(
|
|||
return program_account.publicKey;
|
||||
}
|
||||
|
||||
async function GetPrograms(connection: Connection): Promise<PublicKey> {
|
||||
async function GetPrograms(connection: Connection): Promise<void> {
|
||||
const programVersion = process.env.PROGRAM_VERSION;
|
||||
if (programVersion) {
|
||||
switch (programVersion) {
|
||||
case '2.0.4':
|
||||
return new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
|
||||
programId = TOKEN_PROGRAM_ID;
|
||||
associatedProgramId = ASSOCIATED_TOKEN_PROGRAM_ID;
|
||||
return;
|
||||
default:
|
||||
throw new Error('Unknown program version');
|
||||
}
|
||||
}
|
||||
|
||||
const store = new Store();
|
||||
let tokenProgramId = null;
|
||||
try {
|
||||
const config = await store.load('config.json');
|
||||
console.log('Using pre-loaded Token program');
|
||||
console.log('Using pre-loaded Token programs');
|
||||
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) {
|
||||
tokenProgramId = await loadProgram(
|
||||
console.log(
|
||||
'Checking pre-loaded Token programs failed, will load new programs:',
|
||||
);
|
||||
console.log({err});
|
||||
|
||||
programId = await loadProgram(
|
||||
connection,
|
||||
'../../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', {
|
||||
tokenProgramId: tokenProgramId.toString(),
|
||||
tokenProgramId: programId.toString(),
|
||||
associatedTokenProgramId: associatedProgramId.toString(),
|
||||
});
|
||||
}
|
||||
return tokenProgramId;
|
||||
}
|
||||
|
||||
export async function loadTokenProgram(): Promise<void> {
|
||||
const connection = await getConnection();
|
||||
programId = await GetPrograms(connection);
|
||||
await GetPrograms(connection);
|
||||
|
||||
console.log('Token Program ID', programId.toString());
|
||||
console.log('Associated Token Program ID', associatedProgramId.toString());
|
||||
}
|
||||
|
||||
export async function createMint(): Promise<void> {
|
||||
|
@ -126,9 +150,12 @@ export async function createMint(): Promise<void> {
|
|||
payer,
|
||||
testMintAuthority.publicKey,
|
||||
testMintAuthority.publicKey,
|
||||
2,
|
||||
testTokenDecimals,
|
||||
programId,
|
||||
);
|
||||
// HACK: override hard-coded ASSOCIATED_TOKEN_PROGRAM_ID with corresponding
|
||||
// custom test fixture
|
||||
testToken.associatedProgramId = associatedProgramId;
|
||||
|
||||
const mintInfo = await testToken.getMintInfo();
|
||||
if (mintInfo.mintAuthority !== null) {
|
||||
|
@ -137,7 +164,7 @@ export async function createMint(): Promise<void> {
|
|||
assert(mintInfo.mintAuthority !== null);
|
||||
}
|
||||
assert(mintInfo.supply.toNumber() === 0);
|
||||
assert(mintInfo.decimals === 2);
|
||||
assert(mintInfo.decimals === testTokenDecimals);
|
||||
assert(mintInfo.isInitialized === true);
|
||||
if (mintInfo.freezeAuthority !== null) {
|
||||
assert(mintInfo.freezeAuthority.equals(testMintAuthority.publicKey));
|
||||
|
@ -160,6 +187,48 @@ export async function createAccount(): Promise<void> {
|
|||
assert(accountInfo.isNative === false);
|
||||
assert(accountInfo.rentExemptReserve === 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> {
|
||||
|
@ -219,7 +288,7 @@ export async function transferChecked(): Promise<void> {
|
|||
testAccountOwner,
|
||||
[],
|
||||
100,
|
||||
1,
|
||||
testTokenDecimals - 1,
|
||||
]),
|
||||
);
|
||||
|
||||
|
@ -229,7 +298,7 @@ export async function transferChecked(): Promise<void> {
|
|||
testAccountOwner,
|
||||
[],
|
||||
100,
|
||||
2,
|
||||
testTokenDecimals,
|
||||
);
|
||||
|
||||
const mintInfo = await testToken.getMintInfo();
|
||||
|
@ -242,6 +311,26 @@ export async function transferChecked(): Promise<void> {
|
|||
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> {
|
||||
const delegate = new Account().publicKey;
|
||||
|
||||
|
|
|
@ -27,6 +27,13 @@ export const TOKEN_PROGRAM_ID: PublicKey = new PublicKey(
|
|||
'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`
|
||||
* 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
|
||||
*/
|
||||
type AccountInfo = {|
|
||||
/**
|
||||
* The address of this account
|
||||
*/
|
||||
address: PublicKey,
|
||||
|
||||
/**
|
||||
* The mint associated with this account
|
||||
*/
|
||||
|
@ -286,6 +298,11 @@ export class Token {
|
|||
*/
|
||||
programId: PublicKey;
|
||||
|
||||
/**
|
||||
* Program Identifier for the Associated Token program
|
||||
*/
|
||||
associatedProgramId: PublicKey;
|
||||
|
||||
/**
|
||||
* Fee payer
|
||||
*/
|
||||
|
@ -305,7 +322,14 @@ export class Token {
|
|||
programId: PublicKey,
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
@ -645,10 +764,10 @@ export class Token {
|
|||
): Promise<AccountInfo> {
|
||||
const info = await this.connection.getAccountInfo(account, commitment);
|
||||
if (info === null) {
|
||||
throw new Error('Failed to find account');
|
||||
throw new Error(FAILED_TO_FIND_ACCOUNT);
|
||||
}
|
||||
if (!info.owner.equals(this.programId)) {
|
||||
throw new Error(`Invalid account owner`);
|
||||
throw new Error(INVALID_ACCOUNT_OWNER);
|
||||
}
|
||||
if (info.data.length != AccountLayout.span) {
|
||||
throw new Error(`Invalid account size`);
|
||||
|
@ -656,6 +775,7 @@ export class Token {
|
|||
|
||||
const data = Buffer.from(info.data);
|
||||
const accountInfo = AccountLayout.decode(data);
|
||||
accountInfo.address = account;
|
||||
accountInfo.mint = new PublicKey(accountInfo.mint);
|
||||
accountInfo.owner = new PublicKey(accountInfo.owner);
|
||||
accountInfo.amount = u64.fromBuffer(accountInfo.amount);
|
||||
|
@ -2103,4 +2223,65 @@ export class Token {
|
|||
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 ===
|
||||
export const TOKEN_PROGRAM_ID: PublicKey;
|
||||
export const ASSOCIATED_TOKEN_PROGRAM_ID: PublicKey;
|
||||
|
||||
export class u64 extends BN {
|
||||
toBuffer(): Buffer;
|
||||
|
@ -35,6 +36,7 @@ declare module '@solana/spl-token' {
|
|||
|
||||
export const AccountLayout: Layout;
|
||||
export type AccountInfo = {
|
||||
address: PublicKey;
|
||||
mint: PublicKey;
|
||||
owner: PublicKey;
|
||||
amount: u64;
|
||||
|
@ -65,6 +67,7 @@ declare module '@solana/spl-token' {
|
|||
export class Token {
|
||||
publicKey: PublicKey;
|
||||
programId: PublicKey;
|
||||
associatedProgramId: PublicKey;
|
||||
payer: Account;
|
||||
constructor(
|
||||
connection: Connection,
|
||||
|
@ -81,6 +84,12 @@ declare module '@solana/spl-token' {
|
|||
static getMinBalanceRentForExemptMultisig(
|
||||
connection: Connection,
|
||||
): Promise<number>;
|
||||
static getAssociatedTokenAddress(
|
||||
associatedProgramId: PublicKey,
|
||||
programId: PublicKey,
|
||||
mint: PublicKey,
|
||||
owner: PublicKey,
|
||||
): Promise<PublicKey>;
|
||||
static createMint(
|
||||
connection: Connection,
|
||||
payer: Account,
|
||||
|
@ -90,6 +99,7 @@ declare module '@solana/spl-token' {
|
|||
programId: PublicKey,
|
||||
): Promise<Token>;
|
||||
createAccount(owner: PublicKey): Promise<PublicKey>;
|
||||
createAssociatedTokenAccount(owner: PublicKey): Promise<PublicKey>;
|
||||
static createWrappedNativeAccount(
|
||||
connection: Connection,
|
||||
programId: PublicKey,
|
||||
|
@ -100,6 +110,7 @@ declare module '@solana/spl-token' {
|
|||
createMultisig(m: number, signers: Array<PublicKey>): Promise<PublicKey>;
|
||||
getMintInfo(): Promise<MintInfo>;
|
||||
getAccountInfo(account: PublicKey): Promise<AccountInfo>;
|
||||
getOrCreateAssociatedAccountInfo(owner: PublicKey): Promise<AccountInfo>;
|
||||
getMultisigInfo(multisig: PublicKey): Promise<MultisigInfo>;
|
||||
transfer(
|
||||
source: PublicKey,
|
||||
|
@ -235,5 +246,13 @@ declare module '@solana/spl-token' {
|
|||
authority: PublicKey,
|
||||
multiSigners: Array<Account>,
|
||||
): 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 export var TOKEN_PROGRAM_ID;
|
||||
declare export var ASSOCIATED_TOKEN_PROGRAM_ID;
|
||||
declare export class u64 extends BN {
|
||||
toBuffer(): typeof Buffer;
|
||||
static fromBuffer(buffer: typeof Buffer): u64;
|
||||
|
@ -37,6 +38,7 @@ declare module '@solana/spl-token' {
|
|||
|};
|
||||
declare export var AccountLayout: typeof Layout;
|
||||
declare export type AccountInfo = {|
|
||||
address: PublicKey,
|
||||
mint: PublicKey,
|
||||
owner: PublicKey,
|
||||
amount: u64,
|
||||
|
@ -67,6 +69,7 @@ declare module '@solana/spl-token' {
|
|||
declare export class Token {
|
||||
publicKey: PublicKey;
|
||||
programId: PublicKey;
|
||||
associatedProgramId: PublicKey;
|
||||
payer: Account;
|
||||
constructor(
|
||||
connection: Connection,
|
||||
|
@ -83,6 +86,12 @@ declare module '@solana/spl-token' {
|
|||
static getMinBalanceRentForExemptMultisig(
|
||||
connection: Connection,
|
||||
): Promise<number>;
|
||||
static getAssociatedTokenAddress(
|
||||
associatedProgramId: PublicKey,
|
||||
programId: PublicKey,
|
||||
mint: PublicKey,
|
||||
owner: PublicKey,
|
||||
): Promise<PublicKey>;
|
||||
static createMint(
|
||||
connection: Connection,
|
||||
payer: Account,
|
||||
|
@ -92,6 +101,7 @@ declare module '@solana/spl-token' {
|
|||
programId: PublicKey,
|
||||
): Promise<Token>;
|
||||
createAccount(owner: PublicKey): Promise<PublicKey>;
|
||||
createAssociatedTokenAccount(owner: PublicKey): Promise<PublicKey>;
|
||||
static createWrappedNativeAccount(
|
||||
connection: Connection,
|
||||
programId: PublicKey,
|
||||
|
@ -102,6 +112,7 @@ declare module '@solana/spl-token' {
|
|||
createMultisig(m: number, signers: Array<PublicKey>): Promise<PublicKey>;
|
||||
getMintInfo(): Promise<MintInfo>;
|
||||
getAccountInfo(account: PublicKey): Promise<AccountInfo>;
|
||||
getOrCreateAssociatedAccountInfo(owner: PublicKey): Promise<AccountInfo>;
|
||||
getMultisigInfo(multisig: PublicKey): Promise<MultisigInfo>;
|
||||
transfer(
|
||||
source: PublicKey,
|
||||
|
@ -237,5 +248,13 @@ declare module '@solana/spl-token' {
|
|||
authority: PublicKey,
|
||||
multiSigners: Array<Account>,
|
||||
): TransactionInstruction;
|
||||
static createAssociatedTokenAccountInstruction(
|
||||
associatedProgramId: PublicKey,
|
||||
programId: PublicKey,
|
||||
mint: PublicKey,
|
||||
associatedAccount: PublicKey,
|
||||
owner: PublicKey,
|
||||
payer: PublicKey,
|
||||
): TransactionInstruction;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@solana/spl-token",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"description": "SPL Token JavaScript API",
|
||||
"license": "MIT",
|
||||
"author": "Solana Maintainers <maintainers@solana.com>",
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
// @flow
|
||||
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', () => {
|
||||
it('createTransfer', () => {
|
||||
|
@ -31,4 +31,29 @@ describe('Token', () => {
|
|||
expect(ix.programId).to.eql(TOKEN_PROGRAM_ID);
|
||||
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