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:
Ryo Onodera 2021-03-03 20:56:22 +09:00 committed by GitHub
parent 7d2556905c
commit 68b8da2996
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 393 additions and 33 deletions

View File

@ -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:

View File

@ -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,

View File

@ -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

View File

@ -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');

View File

@ -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');
}
}

View File

@ -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;

View File

@ -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,
});
}
}

19
token/js/module.d.ts vendored
View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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>",

View File

@ -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);
});
});