From 68b8da29968d5cff7761bd6d7e12ab955dcb80d0 Mon Sep 17 00:00:00 2001 From: Ryo Onodera Date: Wed, 3 Mar 2021 20:56:22 +0900 Subject: [PATCH] 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 --- associated-token-account/program/src/lib.rs | 26 ++- .../program/src/processor.rs | 10 +- ci/js-test-token.sh | 1 + token/js/cli/main.js | 6 + token/js/cli/store/index.js | 8 +- token/js/cli/token-test.js | 119 +++++++++-- token/js/client/token.js | 187 +++++++++++++++++- token/js/module.d.ts | 19 ++ token/js/module.flow.js | 19 ++ token/js/package.json | 2 +- token/js/test/token.test.js | 29 ++- 11 files changed, 393 insertions(+), 33 deletions(-) diff --git a/associated-token-account/program/src/lib.rs b/associated-token-account/program/src/lib.rs index 55875c2d9f1..78c1c17efd0 100644 --- a/associated-token-account/program/src/lib.rs +++ b/associated-token-account/program/src/lib.rs @@ -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: diff --git a/associated-token-account/program/src/processor.rs b/associated-token-account/program/src/processor.rs index 2bae6466d7f..98eb08bc325 100644 --- a/associated-token-account/program/src/processor.rs +++ b/associated-token-account/program/src/processor.rs @@ -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, diff --git a/ci/js-test-token.sh b/ci/js-test-token.sh index 501193655ae..cf0805365db 100755 --- a/ci/js-test-token.sh +++ b/ci/js-test-token.sh @@ -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 diff --git a/token/js/cli/main.js b/token/js/cli/main.js index 446a16e41a4..794b2ecf987 100644 --- a/token/js/cli/main.js +++ b/token/js/cli/main.js @@ -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'); diff --git a/token/js/cli/store/index.js b/token/js/cli/store/index.js index 8e2c9908d56..ad47fb1c998 100644 --- a/token/js/cli/store/index.js +++ b/token/js/cli/store/index.js @@ -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 { - 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 { await mkdirp(Store.getDir()); - const filename = path.join(Store.getDir(), uri); + const filename = Store.getFilename(uri); await fs.writeFile(filename, JSON.stringify(config), 'utf8'); } } diff --git a/token/js/cli/token-test.js b/token/js/cli/token-test.js index 577fa96faac..bb7612fed01 100644 --- a/token/js/cli/token-test.js +++ b/token/js/cli/token-test.js @@ -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 { +async function GetPrograms(connection: Connection): Promise { 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 { 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 { @@ -126,9 +150,12 @@ export async function createMint(): Promise { 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 { 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 { 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 { + 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 { @@ -219,7 +288,7 @@ export async function transferChecked(): Promise { testAccountOwner, [], 100, - 1, + testTokenDecimals - 1, ]), ); @@ -229,7 +298,7 @@ export async function transferChecked(): Promise { testAccountOwner, [], 100, - 2, + testTokenDecimals, ); const mintInfo = await testToken.getMintInfo(); @@ -242,6 +311,26 @@ export async function transferChecked(): Promise { assert(testAccountInfo.amount.toNumber() === 1800); } +export async function transferCheckedAssociated(): Promise { + 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 { const delegate = new Account().publicKey; diff --git a/token/js/client/token.js b/token/js/client/token.js index eb80256c4fe..a2656dff29b 100644 --- a/token/js/client/token.js +++ b/token/js/client/token.js @@ -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 { + const associatedAddress = await Token.getAssociatedTokenAddress( + this.associatedProgramId, + this.programId, + this.publicKey, + owner, + ); + + return this.createAssociatedTokenAccountInternal(owner, associatedAddress); + } + + async createAssociatedTokenAccountInternal( + owner: PublicKey, + associatedAddress: PublicKey, + ): Promise { + 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 { + 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 { 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 { + 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, + }); + } } diff --git a/token/js/module.d.ts b/token/js/module.d.ts index 91db98f1fbe..4643788f08f 100644 --- a/token/js/module.d.ts +++ b/token/js/module.d.ts @@ -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; + static getAssociatedTokenAddress( + associatedProgramId: PublicKey, + programId: PublicKey, + mint: PublicKey, + owner: PublicKey, + ): Promise; static createMint( connection: Connection, payer: Account, @@ -90,6 +99,7 @@ declare module '@solana/spl-token' { programId: PublicKey, ): Promise; createAccount(owner: PublicKey): Promise; + createAssociatedTokenAccount(owner: PublicKey): Promise; static createWrappedNativeAccount( connection: Connection, programId: PublicKey, @@ -100,6 +110,7 @@ declare module '@solana/spl-token' { createMultisig(m: number, signers: Array): Promise; getMintInfo(): Promise; getAccountInfo(account: PublicKey): Promise; + getOrCreateAssociatedAccountInfo(owner: PublicKey): Promise; getMultisigInfo(multisig: PublicKey): Promise; transfer( source: PublicKey, @@ -235,5 +246,13 @@ declare module '@solana/spl-token' { authority: PublicKey, multiSigners: Array, ): TransactionInstruction; + static createAssociatedTokenAccountInstruction( + associatedProgramId: PublicKey, + programId: PublicKey, + mint: PublicKey, + associatedAccount: PublicKey, + owner: PublicKey, + payer: PublicKey, + ): TransactionInstruction; } } diff --git a/token/js/module.flow.js b/token/js/module.flow.js index cede53c34d5..ac8713fbd8f 100644 --- a/token/js/module.flow.js +++ b/token/js/module.flow.js @@ -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; + static getAssociatedTokenAddress( + associatedProgramId: PublicKey, + programId: PublicKey, + mint: PublicKey, + owner: PublicKey, + ): Promise; static createMint( connection: Connection, payer: Account, @@ -92,6 +101,7 @@ declare module '@solana/spl-token' { programId: PublicKey, ): Promise; createAccount(owner: PublicKey): Promise; + createAssociatedTokenAccount(owner: PublicKey): Promise; static createWrappedNativeAccount( connection: Connection, programId: PublicKey, @@ -102,6 +112,7 @@ declare module '@solana/spl-token' { createMultisig(m: number, signers: Array): Promise; getMintInfo(): Promise; getAccountInfo(account: PublicKey): Promise; + getOrCreateAssociatedAccountInfo(owner: PublicKey): Promise; getMultisigInfo(multisig: PublicKey): Promise; transfer( source: PublicKey, @@ -237,5 +248,13 @@ declare module '@solana/spl-token' { authority: PublicKey, multiSigners: Array, ): TransactionInstruction; + static createAssociatedTokenAccountInstruction( + associatedProgramId: PublicKey, + programId: PublicKey, + mint: PublicKey, + associatedAccount: PublicKey, + owner: PublicKey, + payer: PublicKey, + ): TransactionInstruction; } } diff --git a/token/js/package.json b/token/js/package.json index 1ba657b49b3..7585792c38d 100644 --- a/token/js/package.json +++ b/token/js/package.json @@ -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 ", diff --git a/token/js/test/token.test.js b/token/js/test/token.test.js index ce7e0435147..449f419ec85 100644 --- a/token/js/test/token.test.js +++ b/token/js/test/token.test.js @@ -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); + }); });