diff --git a/CHANGELOG.md b/CHANGELOG.md index 85d3f34bd..df51ea592 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ com/project-serum/anchor/pull/1841)). * ts: Implement a coder for system program ([#1920](https://github.com/project-serum/anchor/pull/1920)). * ts: Add `program.coder.types` for encoding/decoding user-defined types ([#1931](https://github.com/project-serum/anchor/pull/1931)). * client: Add send_with_spinner_and_config function to RequestBuilder ([#1926](https://github.com/project-serum/anchor/pull/1926)). +* ts: Implement a coder for SPL associated token program ([#1939](https://github.com/project-serum/anchor/pull/1939)). ### Fixes diff --git a/tests/custom-coder/Anchor.toml b/tests/custom-coder/Anchor.toml index ec4e7c95b..1cd14e769 100644 --- a/tests/custom-coder/Anchor.toml +++ b/tests/custom-coder/Anchor.toml @@ -2,6 +2,7 @@ custom_coder = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS" spl_token = "FmpfPa1LHEYRbueNMnwNVd2JvyQ89GXGWdyZEXNNKV8w" native_system = "9NxAd91hhJ3ZBTHytYP894y4ESRKG7n8VbLgdyYGJFLB" +spl_associated_token = "4dUGnkre6uBhX1abB4ofkoecGN4aDXdiWSaWLUjVw6bh" [registry] url = "https://anchor.projectserum.com" diff --git a/tests/custom-coder/programs/spl-associated-token/Cargo.toml b/tests/custom-coder/programs/spl-associated-token/Cargo.toml new file mode 100644 index 000000000..0e1e0c5d3 --- /dev/null +++ b/tests/custom-coder/programs/spl-associated-token/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "spl-associated-token" +version = "0.1.0" +description = "Created with Anchor" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "spl_associated_token" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = [] + +[profile.release] +overflow-checks = true + +[dependencies] +anchor-lang = "0.24.2" \ No newline at end of file diff --git a/tests/custom-coder/programs/spl-associated-token/Xargo.toml b/tests/custom-coder/programs/spl-associated-token/Xargo.toml new file mode 100644 index 000000000..475fb71ed --- /dev/null +++ b/tests/custom-coder/programs/spl-associated-token/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/tests/custom-coder/programs/spl-associated-token/src/lib.rs b/tests/custom-coder/programs/spl-associated-token/src/lib.rs new file mode 100644 index 000000000..cf3540e5e --- /dev/null +++ b/tests/custom-coder/programs/spl-associated-token/src/lib.rs @@ -0,0 +1,33 @@ +// This file is autogenerated with https://github.com/acheroncrypto/native-to-anchor + +use anchor_lang::prelude::*; + +declare_id!("4dUGnkre6uBhX1abB4ofkoecGN4aDXdiWSaWLUjVw6bh"); + +#[program] +pub mod spl_associated_token { + use super::*; + + pub fn create(ctx: Context) -> Result<()> { + Ok(()) + } +} + +#[derive(Accounts)] +pub struct Create<'info> { + #[account(mut)] + authority: Signer<'info>, + #[account(mut)] + /// CHECK: + associated_account: AccountInfo<'info>, + /// CHECK: + owner: AccountInfo<'info>, + /// CHECK: + mint: AccountInfo<'info>, + /// CHECK: + system_program: AccountInfo<'info>, + /// CHECK: + token_program: AccountInfo<'info>, + /// CHECK: + rent: AccountInfo<'info>, +} diff --git a/tests/custom-coder/tests/spl-associated-token-coder.ts b/tests/custom-coder/tests/spl-associated-token-coder.ts new file mode 100644 index 000000000..140beff76 --- /dev/null +++ b/tests/custom-coder/tests/spl-associated-token-coder.ts @@ -0,0 +1,73 @@ +import * as anchor from "@project-serum/anchor"; +import { Native, Spl } from "@project-serum/anchor"; +import { Keypair, PublicKey } from "@solana/web3.js"; +import * as assert from "assert"; +import BN from "bn.js"; + +describe("spl-associated-token-coder", () => { + // Configure the client to use the local cluster. + const provider = anchor.AnchorProvider.env(); + anchor.setProvider(provider); + + // Client. + const program = Spl.associatedToken(); + const systemProgram = Native.system(); + const tokenProgram = Spl.token(); + + it("Creates an account", async () => { + // arrange + const mintKeypair = Keypair.generate(); + const mintDecimals = 6; + const mintSize = tokenProgram.coder.accounts.size( + tokenProgram.idl.accounts[0] + ); + const mintRentExemption = + await provider.connection.getMinimumBalanceForRentExemption(mintSize); + const [associatedToken] = await PublicKey.findProgramAddress( + [ + provider.publicKey.toBuffer(), + tokenProgram.programId.toBuffer(), + mintKeypair.publicKey.toBuffer(), + ], + program.programId + ); + + // act + await program.methods + .create() + .accounts({ + authority: provider.wallet.publicKey, + mint: mintKeypair.publicKey, + owner: provider.wallet.publicKey, + associatedAccount: associatedToken, + }) + .preInstructions( + await Promise.all([ + systemProgram.methods + .createAccount( + new BN(mintRentExemption), + new BN(mintSize), + tokenProgram.programId + ) + .accounts({ + from: provider.wallet.publicKey, + to: mintKeypair.publicKey, + }) + .instruction(), + tokenProgram.methods + .initializeMint(mintDecimals, provider.wallet.publicKey, null) + .accounts({ + mint: mintKeypair.publicKey, + }) + .instruction(), + ]) + ) + .signers([mintKeypair]) + .rpc(); + // assert + const tokenAccount = await tokenProgram.account.token.fetch( + associatedToken + ); + assert.ok(tokenAccount.mint.equals(mintKeypair.publicKey)); + }); +}); diff --git a/ts/src/coder/spl-associated-token/accounts.ts b/ts/src/coder/spl-associated-token/accounts.ts new file mode 100644 index 000000000..8aab9db3a --- /dev/null +++ b/ts/src/coder/spl-associated-token/accounts.ts @@ -0,0 +1,42 @@ +import { AccountsCoder } from "../index.js"; +import { Idl, IdlTypeDef } from "../../idl.js"; +import { accountSize } from "../common"; + +export class SplAssociatedTokenAccountsCoder + implements AccountsCoder +{ + constructor(private idl: Idl) {} + + public async encode(accountName: A, account: T): Promise { + switch (accountName) { + default: { + throw new Error(`Invalid account name: ${accountName}`); + } + } + } + + public decode(accountName: A, ix: Buffer): T { + return this.decodeUnchecked(accountName, ix); + } + + public decodeUnchecked(accountName: A, ix: Buffer): T { + switch (accountName) { + default: { + throw new Error(`Invalid account name: ${accountName}`); + } + } + } + + // TODO: this won't use the appendData. + public memcmp(accountName: A, _appendData?: Buffer): any { + switch (accountName) { + default: { + throw new Error(`Invalid account name: ${accountName}`); + } + } + } + + public size(idlAccount: IdlTypeDef): number { + return accountSize(this.idl, idlAccount) ?? 0; + } +} diff --git a/ts/src/coder/spl-associated-token/events.ts b/ts/src/coder/spl-associated-token/events.ts new file mode 100644 index 000000000..df70671cf --- /dev/null +++ b/ts/src/coder/spl-associated-token/events.ts @@ -0,0 +1,14 @@ +import { EventCoder } from "../index.js"; +import { Idl } from "../../idl.js"; +import { Event } from "../../program/event"; +import { IdlEvent } from "../../idl"; + +export class SplAssociatedTokenEventsCoder implements EventCoder { + constructor(_idl: Idl) {} + + decode>( + _log: string + ): Event | null { + throw new Error("SPL associated token program does not have events"); + } +} diff --git a/ts/src/coder/spl-associated-token/index.ts b/ts/src/coder/spl-associated-token/index.ts new file mode 100644 index 000000000..6a5cfb794 --- /dev/null +++ b/ts/src/coder/spl-associated-token/index.ts @@ -0,0 +1,26 @@ +import { Idl } from "../../idl.js"; +import { Coder } from "../index.js"; +import { SplAssociatedTokenInstructionCoder } from "./instruction.js"; +import { SplAssociatedTokenStateCoder } from "./state.js"; +import { SplAssociatedTokenAccountsCoder } from "./accounts.js"; +import { SplAssociatedTokenEventsCoder } from "./events.js"; +import { SplAssociatedTokenTypesCoder } from "./types.js"; + +/** + * Coder for the SPL token program. + */ +export class SplAssociatedTokenCoder implements Coder { + readonly instruction: SplAssociatedTokenInstructionCoder; + readonly accounts: SplAssociatedTokenAccountsCoder; + readonly state: SplAssociatedTokenStateCoder; + readonly events: SplAssociatedTokenEventsCoder; + readonly types: SplAssociatedTokenTypesCoder; + + constructor(idl: Idl) { + this.instruction = new SplAssociatedTokenInstructionCoder(idl); + this.accounts = new SplAssociatedTokenAccountsCoder(idl); + this.events = new SplAssociatedTokenEventsCoder(idl); + this.state = new SplAssociatedTokenStateCoder(idl); + this.types = new SplAssociatedTokenTypesCoder(idl); + } +} diff --git a/ts/src/coder/spl-associated-token/instruction.ts b/ts/src/coder/spl-associated-token/instruction.ts new file mode 100644 index 000000000..c70b62f22 --- /dev/null +++ b/ts/src/coder/spl-associated-token/instruction.ts @@ -0,0 +1,22 @@ +import camelCase from "camelcase"; +import { Idl } from "../../idl.js"; +import { InstructionCoder } from "../index.js"; + +export class SplAssociatedTokenInstructionCoder implements InstructionCoder { + constructor(_: Idl) {} + + encode(ixName: string, _: any): Buffer { + switch (camelCase(ixName)) { + case "create": { + return Buffer.alloc(0); + } + default: { + throw new Error(`Invalid instruction: ${ixName}`); + } + } + } + + encodeState(_ixName: string, _ix: any): Buffer { + throw new Error("SPL associated token does not have state"); + } +} diff --git a/ts/src/coder/spl-associated-token/state.ts b/ts/src/coder/spl-associated-token/state.ts new file mode 100644 index 000000000..c607f82c8 --- /dev/null +++ b/ts/src/coder/spl-associated-token/state.ts @@ -0,0 +1,13 @@ +import { StateCoder } from "../index.js"; +import { Idl } from "../../idl"; + +export class SplAssociatedTokenStateCoder implements StateCoder { + constructor(_idl: Idl) {} + + encode(_name: string, _account: T): Promise { + throw new Error("SPL associated token does not have state"); + } + decode(_ix: Buffer): T { + throw new Error("SPL associated token does not have state"); + } +} diff --git a/ts/src/coder/spl-associated-token/types.ts b/ts/src/coder/spl-associated-token/types.ts new file mode 100644 index 000000000..e6b2958cb --- /dev/null +++ b/ts/src/coder/spl-associated-token/types.ts @@ -0,0 +1,13 @@ +import { TypesCoder } from "../index.js"; +import { Idl } from "../../idl.js"; + +export class SplAssociatedTokenTypesCoder implements TypesCoder { + constructor(_idl: Idl) {} + + encode(_name: string, _type: T): Buffer { + throw new Error("SPL associated token does not have user-defined types"); + } + decode(_name: string, _typeData: Buffer): T { + throw new Error("SPL associated token does not have user-defined types"); + } +} diff --git a/ts/src/spl/associated-token.ts b/ts/src/spl/associated-token.ts new file mode 100644 index 000000000..ef89af387 --- /dev/null +++ b/ts/src/spl/associated-token.ts @@ -0,0 +1,120 @@ +import { PublicKey } from "@solana/web3.js"; +import { Program } from "../program/index.js"; +import Provider from "../provider.js"; +import { SplAssociatedTokenCoder } from "../coder/spl-associated-token/index.js"; + +const ASSOCIATED_TOKEN_PROGRAM_ID = new PublicKey( + "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" +); + +export function program(provider?: Provider): Program { + return new Program( + IDL, + ASSOCIATED_TOKEN_PROGRAM_ID, + provider, + coder() + ); +} + +export function coder(): SplAssociatedTokenCoder { + return new SplAssociatedTokenCoder(IDL); +} + +/** + * SplAssociatedToken IDL. + */ +export type SplAssociatedToken = { + version: "0.1.0"; + name: "spl_associated_token"; + instructions: [ + { + name: "create"; + accounts: [ + { + name: "authority"; + isMut: true; + isSigner: true; + }, + { + name: "associatedAccount"; + isMut: true; + isSigner: false; + }, + { + name: "owner"; + isMut: false; + isSigner: false; + }, + { + name: "mint"; + isMut: false; + isSigner: false; + }, + { + name: "systemProgram"; + isMut: false; + isSigner: false; + }, + { + name: "tokenProgram"; + isMut: false; + isSigner: false; + }, + { + name: "rent"; + isMut: false; + isSigner: false; + } + ]; + args: []; + } + ]; +}; + +export const IDL: SplAssociatedToken = { + version: "0.1.0", + name: "spl_associated_token", + instructions: [ + { + name: "create", + accounts: [ + { + name: "authority", + isMut: true, + isSigner: true, + }, + { + name: "associatedAccount", + isMut: true, + isSigner: false, + }, + { + name: "owner", + isMut: false, + isSigner: false, + }, + { + name: "mint", + isMut: false, + isSigner: false, + }, + { + name: "systemProgram", + isMut: false, + isSigner: false, + }, + { + name: "tokenProgram", + isMut: false, + isSigner: false, + }, + { + name: "rent", + isMut: false, + isSigner: false, + }, + ], + args: [], + }, + ], +}; diff --git a/ts/src/spl/index.ts b/ts/src/spl/index.ts index 8929f775f..ba32f0c4c 100644 --- a/ts/src/spl/index.ts +++ b/ts/src/spl/index.ts @@ -1,4 +1,8 @@ import { Program, Provider } from "../index.js"; +import { + program as associatedTokenProgram, + SplAssociatedToken, +} from "./associated-token.js"; import { program as tokenProgram, SplToken } from "./token.js"; export { SplToken } from "./token.js"; @@ -7,4 +11,10 @@ export class Spl { public static token(provider?: Provider): Program { return tokenProgram(provider); } + + public static associatedToken( + provider?: Provider + ): Program { + return associatedTokenProgram(provider); + } }