Procházet zdrojové kódy

ts, cli: Program clients generic over IDL types (#795)

ChewingGlass před 4 roky
rodič
revize
0a660d26c4

+ 5 - 0
CHANGELOG.md

@@ -11,6 +11,11 @@ incremented for features.
 
 ## [Unreleased]
 
+### Features
+
+* cli: `target/types` directory now created on build to store a TypeScript types file for each program's IDL ([#795](https://github.com/project-serum/anchor/pull/795)).
+* ts: `Program<T>` can now be typed with an IDL type ([#795](https://github.com/project-serum/anchor/pull/795)).
+
 ## [0.17.0] - 2021-10-03
 
 ### Features

+ 46 - 20
cli/src/lib.rs

@@ -520,19 +520,18 @@ pub fn build(
     }
 
     let cfg = Config::discover(cfg_override)?.expect("Not in workspace.");
+    let cfg_parent = cfg.path().parent().expect("Invalid Anchor.toml");
+
     let cargo = Manifest::discover()?;
 
+    fs::create_dir_all(cfg_parent.join("target/idl"))?;
+    fs::create_dir_all(cfg_parent.join("target/types"))?;
+
     let idl_out = match idl {
         Some(idl) => Some(PathBuf::from(idl)),
-        None => {
-            let cfg_parent = match cfg.path().parent() {
-                None => return Err(anyhow!("Invalid Anchor.toml")),
-                Some(parent) => parent,
-            };
-            fs::create_dir_all(cfg_parent.join("target/idl"))?;
-            Some(cfg_parent.join("target/idl"))
-        }
+        None => Some(cfg_parent.join("target/idl")),
     };
+    let idl_ts_out = Some(cfg_parent.join("target/types"));
 
     let solana_version = match solana_version.is_some() {
         true => solana_version,
@@ -545,6 +544,7 @@ pub fn build(
             &cfg,
             cfg.path(),
             idl_out,
+            idl_ts_out,
             verifiable,
             solana_version,
             stdout,
@@ -556,6 +556,7 @@ pub fn build(
             &cfg,
             cfg.path(),
             idl_out,
+            idl_ts_out,
             verifiable,
             solana_version,
             stdout,
@@ -567,6 +568,7 @@ pub fn build(
             &cfg,
             cargo.path().to_path_buf(),
             idl_out,
+            idl_ts_out,
             verifiable,
             solana_version,
             stdout,
@@ -585,6 +587,7 @@ fn build_all(
     cfg: &WithPath<Config>,
     cfg_path: &Path,
     idl_out: Option<PathBuf>,
+    idl_ts_out: Option<PathBuf>,
     verifiable: bool,
     solana_version: Option<String>,
     stdout: Option<File>, // Used for the package registry server.
@@ -600,6 +603,7 @@ fn build_all(
                     cfg,
                     p.join("Cargo.toml"),
                     idl_out.clone(),
+                    idl_ts_out.clone(),
                     verifiable,
                     solana_version.clone(),
                     stdout.as_ref().map(|f| f.try_clone()).transpose()?,
@@ -620,6 +624,7 @@ fn build_cwd(
     cfg: &WithPath<Config>,
     cargo_toml: PathBuf,
     idl_out: Option<PathBuf>,
+    idl_ts_out: Option<PathBuf>,
     verifiable: bool,
     solana_version: Option<String>,
     stdout: Option<File>,
@@ -631,7 +636,7 @@ fn build_cwd(
         Some(p) => std::env::set_current_dir(&p)?,
     };
     match verifiable {
-        false => _build_cwd(idl_out, cargo_args),
+        false => _build_cwd(idl_out, idl_ts_out, cargo_args),
         true => build_cwd_verifiable(cfg, cargo_toml, solana_version, stdout, stderr),
     }
 }
@@ -696,8 +701,14 @@ fn build_cwd_verifiable(
     // Build the idl.
     if let Ok(Some(idl)) = extract_idl("src/lib.rs") {
         println!("Extracting the IDL");
+
+        // Write out the JSON file.
         let out_file = workspace_dir.join(format!("target/idl/{}.json", idl.name));
         write_idl(&idl, OutFile::File(out_file))?;
+
+        // Write out the TypeScript type.
+        let ts_file = format!("target/types/{}.ts", idl.name);
+        fs::write(&ts_file, template::idl_ts(&idl)?)?;
     }
 
     result
@@ -860,7 +871,11 @@ fn docker_build(
     Ok(())
 }
 
-fn _build_cwd(idl_out: Option<PathBuf>, cargo_args: Vec<String>) -> Result<()> {
+fn _build_cwd(
+    idl_out: Option<PathBuf>,
+    idl_ts_out: Option<PathBuf>,
+    cargo_args: Vec<String>,
+) -> Result<()> {
     let exit = std::process::Command::new("cargo")
         .arg("build-bpf")
         .args(cargo_args)
@@ -874,12 +889,22 @@ fn _build_cwd(idl_out: Option<PathBuf>, cargo_args: Vec<String>) -> Result<()> {
 
     // Always assume idl is located ar src/lib.rs.
     if let Some(idl) = extract_idl("src/lib.rs")? {
+        // JSON out path.
         let out = match idl_out {
             None => PathBuf::from(".").join(&idl.name).with_extension("json"),
             Some(o) => PathBuf::from(&o.join(&idl.name).with_extension("json")),
         };
+        // TS out path.
+        let ts_out = match idl_ts_out {
+            None => PathBuf::from(".").join(&idl.name).with_extension("ts"),
+            Some(o) => PathBuf::from(&o.join(&idl.name).with_extension("ts")),
+        };
 
+        // Write out the JSON file.
         write_idl(&idl, OutFile::File(out))?;
+
+        // Write out the TypeScript type.
+        fs::write(ts_out, template::idl_ts(&idl)?)?;
     }
 
     Ok(())
@@ -1115,7 +1140,7 @@ fn idl_init(cfg_override: &ConfigOverride, program_id: Pubkey, idl_filepath: Str
     with_workspace(cfg_override, |cfg| {
         let keypair = cfg.provider.wallet.to_string();
 
-        let bytes = std::fs::read(idl_filepath)?;
+        let bytes = fs::read(idl_filepath)?;
         let idl: Idl = serde_json::from_reader(&*bytes)?;
 
         let idl_address = create_idl_account(cfg, &keypair, &program_id, &idl)?;
@@ -1133,7 +1158,7 @@ fn idl_write_buffer(
     with_workspace(cfg_override, |cfg| {
         let keypair = cfg.provider.wallet.to_string();
 
-        let bytes = std::fs::read(idl_filepath)?;
+        let bytes = fs::read(idl_filepath)?;
         let idl: Idl = serde_json::from_reader(&*bytes)?;
 
         let idl_buffer = create_idl_buffer(cfg, &keypair, &program_id, &idl)?;
@@ -1383,8 +1408,9 @@ fn write_idl(idl: &Idl, out: OutFile) -> Result<()> {
     let idl_json = serde_json::to_string_pretty(idl)?;
     match out {
         OutFile::Stdout => println!("{}", idl_json),
-        OutFile::File(out) => std::fs::write(out, idl_json)?,
+        OutFile::File(out) => fs::write(out, idl_json)?,
     };
+
     Ok(())
 }
 
@@ -1543,7 +1569,7 @@ fn genesis_flags(cfg: &WithPath<Config>) -> Result<Vec<String>> {
 fn stream_logs(config: &WithPath<Config>) -> Result<Vec<std::process::Child>> {
     let program_logs_dir = ".anchor/program-logs";
     if Path::new(program_logs_dir).exists() {
-        std::fs::remove_dir_all(program_logs_dir)?;
+        fs::remove_dir_all(program_logs_dir)?;
     }
     fs::create_dir_all(program_logs_dir)?;
     let mut handles = vec![];
@@ -1604,10 +1630,10 @@ fn start_test_validator(
     let test_ledger_log_filename = ".anchor/test-ledger-log.txt";
 
     if Path::new(test_ledger_filename).exists() {
-        std::fs::remove_dir_all(test_ledger_filename)?;
+        fs::remove_dir_all(test_ledger_filename)?;
     }
     if Path::new(test_ledger_log_filename).exists() {
-        std::fs::remove_file(test_ledger_log_filename)?;
+        fs::remove_file(test_ledger_log_filename)?;
     }
 
     // Start a validator for testing.
@@ -1906,7 +1932,7 @@ fn migrate(cfg_override: &ConfigOverride) -> Result<()> {
             let module_path = cur_dir.join("migrations/deploy.ts");
             let deploy_script_host_str =
                 template::deploy_ts_script_host(&url, &module_path.display().to_string());
-            std::fs::write("deploy.ts", deploy_script_host_str)?;
+            fs::write("deploy.ts", deploy_script_host_str)?;
             std::process::Command::new("ts-node")
                 .arg("deploy.ts")
                 .env("ANCHOR_WALLET", cfg.provider.wallet.to_string())
@@ -1917,7 +1943,7 @@ fn migrate(cfg_override: &ConfigOverride) -> Result<()> {
             let module_path = cur_dir.join("migrations/deploy.js");
             let deploy_script_host_str =
                 template::deploy_js_script_host(&url, &module_path.display().to_string());
-            std::fs::write("deploy.js", deploy_script_host_str)?;
+            fs::write("deploy.js", deploy_script_host_str)?;
             std::process::Command::new("node")
                 .arg("deploy.js")
                 .env("ANCHOR_WALLET", cfg.provider.wallet.to_string())
@@ -2019,7 +2045,7 @@ fn shell(cfg_override: &ConfigOverride) -> Result<()> {
                     .map(|(name, pd)| {
                         if let Some(idl_fp) = &pd.idl {
                             let file_str =
-                                std::fs::read_to_string(idl_fp).expect("Unable to read IDL file");
+                                fs::read_to_string(idl_fp).expect("Unable to read IDL file");
                             let idl = serde_json::from_str(&file_str).expect("Idl not readable");
                             idls.insert(name.clone(), idl);
                         }
@@ -2188,7 +2214,7 @@ fn publish(
             // Skip target dir.
             if !path_str.contains("target/") && !path_str.contains("/target") {
                 // Only add the file if it's not empty.
-                let metadata = std::fs::File::open(&e)?.metadata()?;
+                let metadata = fs::File::open(&e)?.metadata()?;
                 if metadata.len() > 0 {
                     println!("PACKING: {}", e.display().to_string());
                     if e.is_dir() {

+ 29 - 3
cli/src/template.rs

@@ -1,7 +1,8 @@
 use crate::config::ProgramWorkspace;
 use crate::VERSION;
+use anchor_syn::idl::Idl;
 use anyhow::Result;
-use heck::{CamelCase, SnakeCase};
+use heck::{CamelCase, MixedCase, SnakeCase};
 use solana_sdk::pubkey::Pubkey;
 
 pub fn default_program_id() -> Pubkey {
@@ -27,6 +28,25 @@ token = "{}"
     )
 }
 
+pub fn idl_ts(idl: &Idl) -> Result<String> {
+    let mut idl = idl.clone();
+    idl.accounts = idl
+        .accounts
+        .into_iter()
+        .map(|acc| {
+            let mut acc = acc;
+            acc.name = acc.name.to_mixed_case();
+            acc
+        })
+        .collect();
+    let idl_json = serde_json::to_string_pretty(&idl)?;
+    Ok(format!(
+        "export type {} = {}",
+        idl.name.to_camel_case(),
+        idl_json
+    ))
+}
+
 pub fn cargo_toml(name: &str) -> String {
     format!(
         r#"[package]
@@ -229,22 +249,28 @@ pub fn ts_package_json() -> String {
 pub fn ts_mocha(name: &str) -> String {
     format!(
         r#"import * as anchor from '@project-serum/anchor';
+import {{ Program }} from '@project-serum/anchor';
+import {{ {} }} from '../target/types/{}';
 
 describe('{}', () => {{
 
   // Configure the client to use the local cluster.
   anchor.setProvider(anchor.Provider.env());
 
+  const program = anchor.workspace.{} as Program<{}>;
+
   it('Is initialized!', async () => {{
     // Add your test here.
-    const program = anchor.workspace.{};
-    const tx = await program.rpc.initialize();
+    const tx = await program.rpc.initialize({{}});
     console.log("Your transaction signature", tx);
   }});
 }});
 "#,
+        name.to_camel_case(),
+        name.to_snake_case(),
         name,
         name.to_camel_case(),
+        name.to_camel_case(),
     )
 }
 

+ 1 - 1
tests/escrow/Anchor.toml

@@ -6,4 +6,4 @@ wallet = "~/.config/solana/id.json"
 escrow = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"
 
 [scripts]
-test = "mocha -t 1000000 tests/"
+test = "ts-mocha -t 1000000 tests/*.ts"

+ 9 - 7
tests/escrow/package.json

@@ -1,11 +1,13 @@
 {
   "dependencies": {
-    "@project-serum/anchor": "^0.9.0",
-    "@project-serum/serum": "0.13.38",
-    "@solana/web3.js": "^1.18.0",
-    "@solana/spl-token": "^0.1.6"
-  },
-  "devDependencies": {
-    "ts-mocha": "^8.0.0"
+    "@project-serum/anchor": "../../ts",
+    "@project-serum/serum": "latest",
+    "@solana/spl-token": "latest",
+    "@solana/web3.js": "latest",
+    "@types/mocha": "^9.0.0",
+    "@types/node": "^14.14.37",
+    "bn.js": "^5.2.0",
+    "camelcase": "^6.2.0",
+    "chai": "^4.3.4"
   }
 }

+ 65 - 35
tests/escrow/tests/escrow.js → tests/escrow/tests/escrow.ts

@@ -1,27 +1,35 @@
-const anchor = require("@project-serum/anchor");
-const { TOKEN_PROGRAM_ID, Token } = require("@solana/spl-token");
-const assert = require("assert");
+import * as anchor from "../../../ts";
+import  { Program, BN } from "../../../ts";
+import {
+	SYSVAR_RENT_PUBKEY,
+	PublicKey,
+	Keypair,
+	SystemProgram,
+} from '@solana/web3.js';
+import { TOKEN_PROGRAM_ID, Token } from "@solana/spl-token";
+import { assert } from "chai";
+import { Escrow } from '../target/types/escrow';
 
 describe("escrow", () => {
   const provider = anchor.Provider.env();
   anchor.setProvider(provider);
 
-  const program = anchor.workspace.Escrow;
+  const program = anchor.workspace.Escrow as Program<Escrow>;
 
-  let mintA = null;
-  let mintB = null;
-  let initializerTokenAccountA = null;
-  let initializerTokenAccountB = null;
-  let takerTokenAccountA = null;
-  let takerTokenAccountB = null;
-  let pda = null;
+  let mintA: Token = null;
+  let mintB: Token = null;
+  let initializerTokenAccountA: PublicKey = null;
+  let initializerTokenAccountB: PublicKey = null;
+  let takerTokenAccountA: PublicKey = null;
+  let takerTokenAccountB: PublicKey = null;
+  let pda: PublicKey = null;
 
   const takerAmount = 1000;
   const initializerAmount = 500;
 
-  const escrowAccount = anchor.web3.Keypair.generate();
-  const payer = anchor.web3.Keypair.generate();
-  const mintAuthority = anchor.web3.Keypair.generate();
+  const escrowAccount = Keypair.generate();
+  const payer = Keypair.generate();
+  const mintAuthority = Keypair.generate();
 
   it("Initialise escrow state", async () => {
     // Airdropping tokens to a payer.
@@ -48,10 +56,14 @@ describe("escrow", () => {
       TOKEN_PROGRAM_ID
     );
 
-    initializerTokenAccountA = await mintA.createAccount(provider.wallet.publicKey);
+    initializerTokenAccountA = await mintA.createAccount(
+      provider.wallet.publicKey
+    );
     takerTokenAccountA = await mintA.createAccount(provider.wallet.publicKey);
 
-    initializerTokenAccountB = await mintB.createAccount(provider.wallet.publicKey);
+    initializerTokenAccountB = await mintB.createAccount(
+      provider.wallet.publicKey
+    );
     takerTokenAccountB = await mintB.createAccount(provider.wallet.publicKey);
 
     await mintA.mintTo(
@@ -68,7 +80,9 @@ describe("escrow", () => {
       takerAmount
     );
 
-    let _initializerTokenAccountA = await mintA.getAccountInfo(initializerTokenAccountA);
+    let _initializerTokenAccountA = await mintA.getAccountInfo(
+      initializerTokenAccountA
+    );
     let _takerTokenAccountB = await mintB.getAccountInfo(takerTokenAccountB);
 
     assert.ok(_initializerTokenAccountA.amount.toNumber() == initializerAmount);
@@ -77,31 +91,33 @@ describe("escrow", () => {
 
   it("Initialize escrow", async () => {
     await program.rpc.initializeEscrow(
-      new anchor.BN(initializerAmount),
-      new anchor.BN(takerAmount),
+      new BN(initializerAmount),
+      new BN(takerAmount),
       {
         accounts: {
           initializer: provider.wallet.publicKey,
           initializerDepositTokenAccount: initializerTokenAccountA,
           initializerReceiveTokenAccount: initializerTokenAccountB,
           escrowAccount: escrowAccount.publicKey,
-          systemProgram: anchor.web3.SystemProgram.programId,
+          systemProgram: SystemProgram.programId,
           tokenProgram: TOKEN_PROGRAM_ID,
-          rent: anchor.web3.SYSVAR_RENT_PUBKEY,
+          rent: SYSVAR_RENT_PUBKEY,
         },
         signers: [escrowAccount],
       }
     );
 
     // Get the PDA that is assigned authority to token account.
-    const [_pda, _nonce] = await anchor.web3.PublicKey.findProgramAddress(
+    const [_pda, _nonce] = await PublicKey.findProgramAddress(
       [Buffer.from(anchor.utils.bytes.utf8.encode("escrow"))],
       program.programId
     );
 
     pda = _pda;
 
-    let _initializerTokenAccountA = await mintA.getAccountInfo(initializerTokenAccountA);
+    let _initializerTokenAccountA = await mintA.getAccountInfo(
+      initializerTokenAccountA
+    );
 
     let _escrowAccount = await program.account.escrowAccount.fetch(
       escrowAccount.publicKey
@@ -115,10 +131,14 @@ describe("escrow", () => {
     assert.ok(_escrowAccount.initializerAmount.toNumber() == initializerAmount);
     assert.ok(_escrowAccount.takerAmount.toNumber() == takerAmount);
     assert.ok(
-      _escrowAccount.initializerDepositTokenAccount.equals(initializerTokenAccountA)
+      _escrowAccount.initializerDepositTokenAccount.equals(
+        initializerTokenAccountA
+      )
     );
     assert.ok(
-      _escrowAccount.initializerReceiveTokenAccount.equals(initializerTokenAccountB)
+      _escrowAccount.initializerReceiveTokenAccount.equals(
+        initializerTokenAccountB
+      )
     );
   });
 
@@ -139,8 +159,12 @@ describe("escrow", () => {
 
     let _takerTokenAccountA = await mintA.getAccountInfo(takerTokenAccountA);
     let _takerTokenAccountB = await mintB.getAccountInfo(takerTokenAccountB);
-    let _initializerTokenAccountA = await mintA.getAccountInfo(initializerTokenAccountA);
-    let _initializerTokenAccountB = await mintB.getAccountInfo(initializerTokenAccountB);
+    let _initializerTokenAccountA = await mintA.getAccountInfo(
+      initializerTokenAccountA
+    );
+    let _initializerTokenAccountB = await mintB.getAccountInfo(
+      initializerTokenAccountB
+    );
 
     // Check that the initializer gets back ownership of their token account.
     assert.ok(_takerTokenAccountA.owner.equals(provider.wallet.publicKey));
@@ -151,7 +175,7 @@ describe("escrow", () => {
     assert.ok(_takerTokenAccountB.amount.toNumber() == 0);
   });
 
-  let newEscrow = anchor.web3.Keypair.generate();
+  let newEscrow = Keypair.generate();
 
   it("Initialize escrow and cancel escrow", async () => {
     // Put back tokens into initializer token A account.
@@ -163,23 +187,25 @@ describe("escrow", () => {
     );
 
     await program.rpc.initializeEscrow(
-      new anchor.BN(initializerAmount),
-      new anchor.BN(takerAmount),
+      new BN(initializerAmount),
+      new BN(takerAmount),
       {
         accounts: {
           initializer: provider.wallet.publicKey,
           initializerDepositTokenAccount: initializerTokenAccountA,
           initializerReceiveTokenAccount: initializerTokenAccountB,
           escrowAccount: newEscrow.publicKey,
-          systemProgram: anchor.web3.SystemProgram.programId,
+          systemProgram: SystemProgram.programId,
           tokenProgram: TOKEN_PROGRAM_ID,
-          rent: anchor.web3.SYSVAR_RENT_PUBKEY,
+          rent: SYSVAR_RENT_PUBKEY,
         },
         signers: [newEscrow],
       }
     );
 
-    let _initializerTokenAccountA = await mintA.getAccountInfo(initializerTokenAccountA);
+    let _initializerTokenAccountA = await mintA.getAccountInfo(
+      initializerTokenAccountA
+    );
 
     // Check that the new owner is the PDA.
     assert.ok(_initializerTokenAccountA.owner.equals(pda));
@@ -196,8 +222,12 @@ describe("escrow", () => {
     });
 
     // Check the final owner should be the provider public key.
-    _initializerTokenAccountA = await mintA.getAccountInfo(initializerTokenAccountA);
-    assert.ok(_initializerTokenAccountA.owner.equals(provider.wallet.publicKey));
+    _initializerTokenAccountA = await mintA.getAccountInfo(
+      initializerTokenAccountA
+    );
+    assert.ok(
+      _initializerTokenAccountA.owner.equals(provider.wallet.publicKey)
+    );
 
     // Check all the funds are still there.
     assert.ok(_initializerTokenAccountA.amount.toNumber() == initializerAmount);

+ 10 - 0
tests/escrow/tsconfig.json

@@ -0,0 +1,10 @@
+{
+  "compilerOptions": {
+    "types": ["mocha", "chai", "node"],
+    "typeRoots": ["./node_modules/@types"],
+    "lib": ["es2015"],
+    "module": "commonjs",
+    "target": "es6",
+    "esModuleInterop": true
+  }
+}

+ 9 - 4
ts/src/coder/event.ts

@@ -1,8 +1,8 @@
 import * as base64 from "base64-js";
 import { Layout } from "buffer-layout";
 import { sha256 } from "js-sha256";
-import { Idl, IdlTypeDef } from "../idl";
-import { Event } from "../program/event";
+import { Idl, IdlEvent, IdlTypeDef } from "../idl";
+import { Event, EventData } from "../program/event";
 import { IdlCoder } from "./idl";
 
 export class EventCoder {
@@ -46,7 +46,9 @@ export class EventCoder {
     );
   }
 
-  public decode<T = Record<string, unknown>>(log: string): Event<T> | null {
+  public decode<E extends IdlEvent = IdlEvent, T = Record<string, never>>(
+    log: string
+  ): Event<E, T> | null {
     let logArr: Buffer;
     // This will throw if log length is not a multiple of 4.
     try {
@@ -66,7 +68,10 @@ export class EventCoder {
     if (!layout) {
       throw new Error(`Unknown event: ${eventName}`);
     }
-    const data = layout.decode(logArr.slice(8)) as T;
+    const data = layout.decode(logArr.slice(8)) as EventData<
+      E["fields"][number],
+      T
+    >;
     return { data, name: eventName };
   }
 }

+ 1 - 1
ts/src/idl.ts

@@ -122,7 +122,7 @@ type IdlEnumFieldsNamed = IdlField[];
 
 type IdlEnumFieldsTuple = IdlType[];
 
-type IdlErrorCode = {
+export type IdlErrorCode = {
   code: number;
   name: string;
   msg?: string;

+ 7 - 3
ts/src/program/context.ts

@@ -5,7 +5,7 @@ import {
   TransactionInstruction,
 } from "@solana/web3.js";
 import { Address } from "./common";
-import { IdlInstruction } from "../idl";
+import { IdlAccountItem, IdlAccounts, IdlInstruction } from "../idl";
 
 /**
  * Context provides all non-argument inputs for generating Anchor transactions.
@@ -55,10 +55,14 @@ export type Context<A extends Accounts = Accounts> = {
  * If multiple accounts are nested in the rust program, then they should be
  * nested here.
  */
-export type Accounts = {
-  [key: string]: Address | Accounts;
+export type Accounts<A extends IdlAccountItem = IdlAccountItem> = {
+  [N in A["name"]]: Account<A & { name: N }>;
 };
 
+type Account<A extends IdlAccountItem> = A extends IdlAccounts
+  ? Accounts<A["accounts"][number]>
+  : Address;
+
 export function splitArgsAndCtx(
   idlIx: IdlInstruction,
   args: any[]

+ 14 - 5
ts/src/program/event.ts

@@ -1,14 +1,23 @@
 import { PublicKey } from "@solana/web3.js";
 import * as assert from "assert";
+import { IdlEvent, IdlEventField } from "src/idl";
 import Coder from "../coder";
+import { DecodeType } from "./namespace/types";
 import Provider from "../provider";
 
 const LOG_START_INDEX = "Program log: ".length;
 
 // Deserialized event.
-export type Event<T = Record<string, unknown>> = {
-  name: string;
-  data: T;
+export type Event<
+  E extends IdlEvent = IdlEvent,
+  Defined = Record<string, never>
+> = {
+  name: E["name"];
+  data: EventData<E["fields"][number], Defined>;
+};
+
+export type EventData<T extends IdlEventField, Defined> = {
+  [N in T["name"]]: DecodeType<(T & { name: N })["type"], Defined>;
 };
 
 type EventCallback = (event: any, slot: number) => void;
@@ -82,7 +91,7 @@ export class EventManager {
       return listener;
     }
 
-    this._onLogsSubscriptionId = this._provider.connection.onLogs(
+    this._onLogsSubscriptionId = this._provider!.connection.onLogs(
       this._programId,
       (logs, ctx) => {
         if (logs.err) {
@@ -132,7 +141,7 @@ export class EventManager {
     if (this._eventCallbacks.size == 0) {
       assert.ok(this._eventListeners.size === 0);
       if (this._onLogsSubscriptionId !== undefined) {
-        await this._provider.connection.removeOnLogsListener(
+        await this._provider!.connection.removeOnLogsListener(
           this._onLogsSubscriptionId
         );
         this._onLogsSubscriptionId = undefined;

+ 36 - 23
ts/src/program/index.ts

@@ -47,7 +47,7 @@ export * from "./namespace";
  * below will refer to the two counter examples found
  * [here](https://github.com/project-serum/anchor#examples).
  */
-export class Program {
+export class Program<IDL extends Idl = Idl> {
   /**
    * Async methods to send signed transactions to *non*-state methods on the
    * program, returning a [[TransactionSignature]].
@@ -78,7 +78,7 @@ export class Program {
    * });
    * ```
    */
-  readonly rpc: RpcNamespace;
+  readonly rpc: RpcNamespace<IDL>;
 
   /**
    * The namespace provides handles to an [[AccountClient]] object for each
@@ -100,7 +100,7 @@ export class Program {
    *
    * For the full API, see the [[AccountClient]] reference.
    */
-  readonly account: AccountNamespace;
+  readonly account: AccountNamespace<IDL>;
 
   /**
    * The namespace provides functions to build [[TransactionInstruction]]
@@ -131,7 +131,7 @@ export class Program {
    * });
    * ```
    */
-  readonly instruction: InstructionNamespace;
+  readonly instruction: InstructionNamespace<IDL>;
 
   /**
    * The namespace provides functions to build [[Transaction]] objects for each
@@ -162,7 +162,7 @@ export class Program {
    * });
    * ```
    */
-  readonly transaction: TransactionNamespace;
+  readonly transaction: TransactionNamespace<IDL>;
 
   /**
    * The namespace provides functions to simulate transactions for each method
@@ -198,14 +198,14 @@ export class Program {
    * });
    * ```
    */
-  readonly simulate: SimulateNamespace;
+  readonly simulate: SimulateNamespace<IDL>;
 
   /**
    * A client for the program state. Similar to the base [[Program]] client,
    * one can use this to send transactions and read accounts for the state
    * abstraction.
    */
-  readonly state?: StateClient;
+  readonly state?: StateClient<IDL>;
 
   /**
    * Address of the program.
@@ -215,6 +215,14 @@ export class Program {
   }
   private _programId: PublicKey;
 
+  /**
+   * IDL defining the program's interface.
+   */
+  public get idl(): IDL {
+    return this._idl;
+  }
+  private _idl: IDL;
+
   /**
    * Coder for serializing requests.
    */
@@ -223,6 +231,14 @@ export class Program {
   }
   private _coder: Coder;
 
+  /**
+   * Wallet and network provider.
+   */
+  public get provider(): Provider {
+    return this._provider;
+  }
+  private _provider: Provider;
+
   /**
    * Handles event subscriptions.
    */
@@ -234,20 +250,16 @@ export class Program {
    * @param provider  The network and wallet context to use. If not provided
    *                  then uses [[getProvider]].
    */
-  public constructor(
-    /**
-     * IDL defining the program's interface.
-     */
-    public readonly idl: Idl,
-    programId: Address,
-    /**
-     * Wallet and network provider.
-     */
-    public readonly provider: Provider = getProvider()
-  ) {
+  public constructor(idl: IDL, programId: Address, provider?: Provider) {
     programId = translateAddress(programId);
 
+    if (!provider) {
+      provider = getProvider();
+    }
+
     // Fields.
+    this._idl = idl;
+    this._provider = provider;
     this._programId = programId;
     this._coder = new Coder(idl);
     this._events = new EventManager(this._programId, provider, this._coder);
@@ -278,16 +290,17 @@ export class Program {
    * @param programId The on-chain address of the program.
    * @param provider  The network and wallet context.
    */
-  public static async at(
+  public static async at<IDL extends Idl = Idl>(
     address: Address,
     provider?: Provider
-  ): Promise<Program> {
+  ): Promise<Program<IDL>> {
     const programId = translateAddress(address);
 
-    const idl = await Program.fetchIdl(programId, provider);
+    const idl = await Program.fetchIdl<IDL>(programId, provider);
     if (!idl) {
       throw new Error(`IDL not found for program: ${address.toString()}`);
     }
+
     return new Program(idl, programId, provider);
   }
 
@@ -300,10 +313,10 @@ export class Program {
    * @param programId The on-chain address of the program.
    * @param provider  The network and wallet context.
    */
-  public static async fetchIdl(
+  public static async fetchIdl<IDL extends Idl = Idl>(
     address: Address,
     provider?: Provider
-  ): Promise<Idl | null> {
+  ): Promise<IDL | null> {
     provider = provider ?? getProvider();
     const programId = translateAddress(address);
 

+ 28 - 14
ts/src/program/namespace/account.ts

@@ -18,21 +18,22 @@ import Coder, {
 } from "../../coder";
 import { Subscription, Address, translateAddress } from "../common";
 import { getProvider } from "../../";
+import { AllAccountsMap, IdlTypes, TypeDef } from "./types";
 import * as pubkeyUtil from "../../utils/pubkey";
 import * as rpcUtil from "../../utils/rpc";
 
 export default class AccountFactory {
-  public static build(
-    idl: Idl,
+  public static build<IDL extends Idl>(
+    idl: IDL,
     coder: Coder,
     programId: PublicKey,
-    provider: Provider
-  ): AccountNamespace {
+    provider?: Provider
+  ): AccountNamespace<IDL> {
     const accountFns: AccountNamespace = {};
 
     idl.accounts?.forEach((idlAccount) => {
       const name = camelCase(idlAccount.name);
-      accountFns[name] = new AccountClient(
+      accountFns[name] = new AccountClient<IDL>(
         idl,
         idlAccount,
         programId,
@@ -41,10 +42,14 @@ export default class AccountFactory {
       );
     });
 
-    return accountFns;
+    return accountFns as AccountNamespace<IDL>;
   }
 }
 
+type NullableIdlAccount<IDL extends Idl> = IDL["accounts"] extends undefined
+  ? IdlTypeDef
+  : NonNullable<IDL["accounts"]>[number];
+
 /**
  * The namespace provides handles to an [[AccountClient]] object for each
  * account in a program.
@@ -65,11 +70,17 @@ export default class AccountFactory {
  *
  * For the full API, see the [[AccountClient]] reference.
  */
-export interface AccountNamespace {
-  [key: string]: AccountClient;
-}
+export type AccountNamespace<IDL extends Idl = Idl> = {
+  [M in keyof AllAccountsMap<IDL>]: AccountClient<IDL>;
+};
 
-export class AccountClient<T = any> {
+export class AccountClient<
+  IDL extends Idl = Idl,
+  A extends NullableIdlAccount<IDL> = IDL["accounts"] extends undefined
+    ? IdlTypeDef
+    : NonNullable<IDL["accounts"]>[number],
+  T = TypeDef<A, IdlTypes<IDL>>
+> {
   /**
    * Returns the number of bytes in this account.
    */
@@ -102,11 +113,11 @@ export class AccountClient<T = any> {
   }
   private _coder: Coder;
 
-  private _idlAccount: IdlTypeDef;
+  private _idlAccount: A;
 
   constructor(
-    idl: Idl,
-    idlAccount: IdlTypeDef,
+    idl: IDL,
+    idlAccount: A,
     programId: PublicKey,
     provider?: Provider,
     coder?: Coder
@@ -140,7 +151,10 @@ export class AccountClient<T = any> {
       throw new Error("Invalid account discriminator");
     }
 
-    return this._coder.accounts.decode(this._idlAccount.name, accountInfo.data);
+    return this._coder.accounts.decode<T>(
+      this._idlAccount.name,
+      accountInfo.data
+    );
   }
 
   /**

+ 24 - 15
ts/src/program/namespace/index.ts

@@ -2,7 +2,7 @@ import camelCase from "camelcase";
 import { PublicKey } from "@solana/web3.js";
 import Coder from "../../coder";
 import Provider from "../../provider";
-import { Idl } from "../../idl";
+import { Idl, IdlInstruction } from "../../idl";
 import StateFactory, { StateClient } from "./state";
 import InstructionFactory, { InstructionNamespace } from "./instruction";
 import TransactionFactory, { TransactionNamespace } from "./transaction";
@@ -10,6 +10,7 @@ import RpcFactory, { RpcNamespace } from "./rpc";
 import AccountFactory, { AccountNamespace } from "./account";
 import SimulateFactory, { SimulateNamespace } from "./simulate";
 import { parseIdlErrors } from "../common";
+import { AllInstructions } from "./types";
 
 // Re-exports.
 export { StateClient } from "./state";
@@ -18,23 +19,24 @@ export { TransactionNamespace, TransactionFn } from "./transaction";
 export { RpcNamespace, RpcFn } from "./rpc";
 export { AccountNamespace, AccountClient, ProgramAccount } from "./account";
 export { SimulateNamespace, SimulateFn } from "./simulate";
+export { IdlAccounts, IdlTypes } from "./types";
 
 export default class NamespaceFactory {
   /**
    * Generates all namespaces for a given program.
    */
-  public static build(
-    idl: Idl,
+  public static build<IDL extends Idl>(
+    idl: IDL,
     coder: Coder,
     programId: PublicKey,
     provider: Provider
   ): [
-    RpcNamespace,
-    InstructionNamespace,
-    TransactionNamespace,
-    AccountNamespace,
-    SimulateNamespace,
-    StateClient | undefined
+    RpcNamespace<IDL>,
+    InstructionNamespace<IDL>,
+    TransactionNamespace<IDL>,
+    AccountNamespace<IDL>,
+    SimulateNamespace<IDL>,
+    StateClient<IDL> | undefined
   ] {
     const rpc: RpcNamespace = {};
     const instruction: InstructionNamespace = {};
@@ -45,10 +47,10 @@ export default class NamespaceFactory {
 
     const state = StateFactory.build(idl, coder, programId, provider);
 
-    idl.instructions.forEach((idlIx) => {
-      const ixItem = InstructionFactory.build(
+    idl.instructions.forEach(<I extends AllInstructions<IDL>>(idlIx: I) => {
+      const ixItem = InstructionFactory.build<IDL, I>(
         idlIx,
-        (ixName: string, ix: any) => coder.instruction.encode(ixName, ix),
+        (ixName, ix) => coder.instruction.encode(ixName, ix),
         programId
       );
       const txItem = TransactionFactory.build(idlIx, ixItem);
@@ -71,10 +73,17 @@ export default class NamespaceFactory {
       simulate[name] = simulateItem;
     });
 
-    const account = idl.accounts
+    const account: AccountNamespace<IDL> = idl.accounts
       ? AccountFactory.build(idl, coder, programId, provider)
-      : {};
+      : ({} as AccountNamespace<IDL>);
 
-    return [rpc, instruction, transaction, account, simulate, state];
+    return [
+      rpc as RpcNamespace<IDL>,
+      instruction as InstructionNamespace<IDL>,
+      transaction as TransactionNamespace<IDL>,
+      account,
+      simulate as SimulateNamespace<IDL>,
+      state,
+    ];
   }
 }

+ 59 - 17
ts/src/program/namespace/instruction.ts

@@ -3,7 +3,13 @@ import {
   PublicKey,
   TransactionInstruction,
 } from "@solana/web3.js";
-import { IdlAccount, IdlInstruction, IdlAccountItem } from "../../idl";
+import {
+  Idl,
+  IdlAccount,
+  IdlAccountItem,
+  IdlAccounts,
+  IdlInstruction,
+} from "../../idl";
 import { IdlError } from "../../error";
 import {
   toInstruction,
@@ -12,18 +18,27 @@ import {
   Address,
 } from "../common";
 import { Accounts, splitArgsAndCtx } from "../context";
+import {
+  AllInstructions,
+  AllInstructionsMap,
+  InstructionContextFn,
+  InstructionContextFnArgs,
+  MakeInstructionsNamespace,
+} from "./types";
 
 export default class InstructionNamespaceFactory {
-  public static build(
-    idlIx: IdlInstruction,
-    encodeFn: InstructionEncodeFn,
+  public static build<IDL extends Idl, I extends AllInstructions<IDL>>(
+    idlIx: I,
+    encodeFn: InstructionEncodeFn<I>,
     programId: PublicKey
-  ): InstructionFn {
+  ): InstructionFn<IDL, I> {
     if (idlIx.name === "_inner") {
       throw new IdlError("the _inner name is reserved");
     }
 
-    const ix = (...args: any[]): TransactionInstruction => {
+    const ix = (
+      ...args: InstructionContextFnArgs<IDL, I>
+    ): TransactionInstruction => {
       const [ixArgs, ctx] = splitArgsAndCtx(idlIx, [...args]);
       validateAccounts(idlIx.accounts, ctx.accounts);
       validateInstruction(idlIx, ...args);
@@ -45,7 +60,7 @@ export default class InstructionNamespaceFactory {
     };
 
     // Utility fn for ordering the accounts for this instruction.
-    ix["accounts"] = (accs: Accounts = {}) => {
+    ix["accounts"] = (accs: Accounts<I["accounts"][number]> | undefined) => {
       return InstructionNamespaceFactory.accountsArray(accs, idlIx.accounts);
     };
 
@@ -53,9 +68,13 @@ export default class InstructionNamespaceFactory {
   }
 
   public static accountsArray(
-    ctx: Accounts,
-    accounts: IdlAccountItem[]
+    ctx: Accounts | undefined,
+    accounts: readonly IdlAccountItem[]
   ): AccountMeta[] {
+    if (!ctx) {
+      return [];
+    }
+
     return accounts
       .map((acc: IdlAccountItem) => {
         // Nested accounts.
@@ -65,7 +84,7 @@ export default class InstructionNamespaceFactory {
           const rpcAccs = ctx[acc.name] as Accounts;
           return InstructionNamespaceFactory.accountsArray(
             rpcAccs,
-            nestedAccounts
+            (acc as IdlAccounts).accounts
           ).flat();
         } else {
           const account: IdlAccount = acc as IdlAccount;
@@ -109,21 +128,44 @@ export default class InstructionNamespaceFactory {
  * });
  * ```
  */
-export interface InstructionNamespace {
-  [key: string]: InstructionFn;
-}
+export type InstructionNamespace<
+  IDL extends Idl = Idl,
+  I extends IdlInstruction = IDL["instructions"][number]
+> = MakeInstructionsNamespace<
+  IDL,
+  I,
+  TransactionInstruction,
+  {
+    [M in keyof AllInstructionsMap<IDL>]: {
+      accounts: (
+        ctx: Accounts<AllInstructionsMap<IDL>[M]["accounts"][number]>
+      ) => unknown;
+    };
+  }
+>;
 
 /**
  * Function to create a `TransactionInstruction` generated from an IDL.
  * Additionally it provides an `accounts` utility method, returning a list
  * of ordered accounts for the instruction.
  */
-export type InstructionFn = IxProps & ((...args: any[]) => any);
-type IxProps = {
-  accounts: (ctx: Accounts) => AccountMeta[];
+export type InstructionFn<
+  IDL extends Idl = Idl,
+  I extends AllInstructions<IDL> = AllInstructions<IDL>
+> = InstructionContextFn<IDL, I, TransactionInstruction> &
+  IxProps<Accounts<I["accounts"][number]>>;
+
+type IxProps<A extends Accounts> = {
+  /**
+   * Returns an ordered list of accounts associated with the instruction.
+   */
+  accounts: (ctx: A) => AccountMeta[];
 };
 
-export type InstructionEncodeFn = (ixName: string, ix: any) => Buffer;
+export type InstructionEncodeFn<I extends IdlInstruction = IdlInstruction> = (
+  ixName: I["name"],
+  ix: any
+) => Buffer;
 
 // Throws error if any argument required for the `ix` is not given.
 function validateInstruction(ix: IdlInstruction, ...args: any[]) {

+ 19 - 10
ts/src/program/namespace/rpc.ts

@@ -1,18 +1,23 @@
 import { TransactionSignature } from "@solana/web3.js";
 import Provider from "../../provider";
-import { IdlInstruction } from "../../idl";
-import { Context, splitArgsAndCtx } from "../context";
+import { Idl } from "../../idl";
+import { splitArgsAndCtx } from "../context";
 import { TransactionFn } from "./transaction";
 import { ProgramError } from "../../error";
+import {
+  AllInstructions,
+  InstructionContextFn,
+  MakeInstructionsNamespace,
+} from "./types";
 
 export default class RpcFactory {
-  public static build(
-    idlIx: IdlInstruction,
-    txFn: TransactionFn,
+  public static build<IDL extends Idl, I extends AllInstructions<IDL>>(
+    idlIx: I,
+    txFn: TransactionFn<IDL, I>,
     idlErrors: Map<number, string>,
     provider: Provider
   ): RpcFn {
-    const rpc = async (...args: any[]): Promise<TransactionSignature> => {
+    const rpc: RpcFn<IDL, I> = async (...args) => {
       const tx = txFn(...args);
       const [, ctx] = splitArgsAndCtx(idlIx, [...args]);
       try {
@@ -66,12 +71,16 @@ export default class RpcFactory {
  * });
  * ```
  */
-export interface RpcNamespace {
-  [key: string]: RpcFn;
-}
+export type RpcNamespace<
+  IDL extends Idl = Idl,
+  I extends AllInstructions<IDL> = AllInstructions<IDL>
+> = MakeInstructionsNamespace<IDL, I, Promise<TransactionSignature>>;
 
 /**
  * RpcFn is a single RPC method generated from an IDL, sending a transaction
  * paid for and signed by the configured provider.
  */
-export type RpcFn = (...args: any[]) => Promise<TransactionSignature>;
+export type RpcFn<
+  IDL extends Idl = Idl,
+  I extends AllInstructions<IDL> = AllInstructions<IDL>
+> = InstructionContextFn<IDL, I, Promise<TransactionSignature>>;

+ 39 - 18
ts/src/program/namespace/simulate.ts

@@ -4,32 +4,37 @@ import {
   SimulatedTransactionResponse,
 } from "@solana/web3.js";
 import Provider from "../../provider";
-import { IdlInstruction } from "../../idl";
 import { splitArgsAndCtx } from "../context";
 import { TransactionFn } from "./transaction";
 import { EventParser, Event } from "../event";
 import Coder from "../../coder";
-import { Idl } from "../../idl";
+import { Idl, IdlEvent } from "../../idl";
 import { ProgramError } from "../../error";
+import {
+  AllInstructions,
+  IdlTypes,
+  InstructionContextFn,
+  MakeInstructionsNamespace,
+} from "./types";
 
 export default class SimulateFactory {
-  public static build(
-    idlIx: IdlInstruction,
-    txFn: TransactionFn,
+  public static build<IDL extends Idl, I extends AllInstructions<IDL>>(
+    idlIx: AllInstructions<IDL>,
+    txFn: TransactionFn<IDL>,
     idlErrors: Map<number, string>,
     provider: Provider,
     coder: Coder,
     programId: PublicKey,
-    idl: Idl
-  ): SimulateFn {
-    const simulate = async (...args: any[]): Promise<SimulateResponse> => {
+    idl: IDL
+  ): SimulateFn<IDL, I> {
+    const simulate: SimulateFn<IDL> = async (...args) => {
       const tx = txFn(...args);
       const [, ctx] = splitArgsAndCtx(idlIx, [...args]);
       let resp:
         | RpcResponseAndContext<SimulatedTransactionResponse>
         | undefined = undefined;
       try {
-        resp = await provider.simulate(tx, ctx.signers, ctx.options);
+        resp = await provider!.simulate(tx, ctx.signers, ctx.options);
       } catch (err) {
         console.log("Translating error", err);
         let translatedErr = ProgramError.parse(err, idlErrors);
@@ -49,7 +54,7 @@ export default class SimulateFactory {
         throw new Error("Simulated logs not found");
       }
 
-      const events: Event[] = [];
+      const events: Event<IdlEvent, IdlTypes<IDL>>[] = [];
       if (idl.events) {
         let parser = new EventParser(programId, coder);
         parser.parseLogs(logs, (event) => {
@@ -97,19 +102,35 @@ export default class SimulateFactory {
  * });
  * ```
  */
-export interface SimulateNamespace {
-  [key: string]: SimulateFn;
-}
+export type SimulateNamespace<
+  IDL extends Idl = Idl,
+  I extends AllInstructions<IDL> = AllInstructions<IDL>
+> = MakeInstructionsNamespace<
+  IDL,
+  I,
+  Promise<SimulateResponse<NullableEvents<IDL>, IdlTypes<IDL>>>
+>;
+
+type NullableEvents<IDL extends Idl> = IDL["events"] extends undefined
+  ? IdlEvent
+  : NonNullable<IDL["events"]>[number];
 
 /**
- * RpcFn is a single method generated from an IDL. It simulates a method
+ * SimulateFn is a single method generated from an IDL. It simulates a method
  * against a cluster configured by the provider, returning a list of all the
  * events and raw logs that were emitted during the execution of the
  * method.
  */
-export type SimulateFn = (...args: any[]) => Promise<SimulateResponse>;
+export type SimulateFn<
+  IDL extends Idl = Idl,
+  I extends AllInstructions<IDL> = AllInstructions<IDL>
+> = InstructionContextFn<
+  IDL,
+  I,
+  Promise<SimulateResponse<NullableEvents<IDL>, IdlTypes<IDL>>>
+>;
 
-type SimulateResponse = {
-  events: Event[];
-  raw: string[];
+type SimulateResponse<E extends IdlEvent, Defined> = {
+  events: readonly Event<E, Defined>[];
+  raw: readonly string[];
 };

+ 80 - 51
ts/src/program/namespace/state.ts

@@ -1,8 +1,19 @@
 import EventEmitter from "eventemitter3";
 import camelCase from "camelcase";
-import { PublicKey, SystemProgram, Commitment } from "@solana/web3.js";
+import {
+  PublicKey,
+  SystemProgram,
+  Commitment,
+  AccountMeta,
+} from "@solana/web3.js";
 import Provider from "../../provider";
-import { Idl, IdlStateMethod } from "../../idl";
+import {
+  Idl,
+  IdlAccountItem,
+  IdlInstruction,
+  IdlStateMethod,
+  IdlTypeDef,
+} from "../../idl";
 import Coder, { stateDiscriminator } from "../../coder";
 import { RpcNamespace, InstructionNamespace, TransactionNamespace } from "./";
 import { getProvider } from "../../";
@@ -12,14 +23,15 @@ import { Accounts } from "../context";
 import InstructionNamespaceFactory from "./instruction";
 import RpcNamespaceFactory from "./rpc";
 import TransactionNamespaceFactory from "./transaction";
+import { IdlTypes, TypeDef } from "./types";
 
 export default class StateFactory {
-  public static build(
-    idl: Idl,
+  public static build<IDL extends Idl>(
+    idl: IDL,
     coder: Coder,
     programId: PublicKey,
-    provider: Provider
-  ): StateClient | undefined {
+    provider?: Provider
+  ): StateClient<IDL> | undefined {
     if (idl.state === undefined) {
       return undefined;
     }
@@ -27,26 +39,30 @@ export default class StateFactory {
   }
 }
 
+type NullableMethods<IDL extends Idl> = IDL["state"] extends undefined
+  ? IdlInstruction[]
+  : NonNullable<IDL["state"]>["methods"];
+
 /**
  * A client for the program state. Similar to the base [[Program]] client,
  * one can use this to send transactions and read accounts for the state
  * abstraction.
  */
-export class StateClient {
+export class StateClient<IDL extends Idl> {
   /**
    * [[RpcNamespace]] for all state methods.
    */
-  readonly rpc: RpcNamespace;
+  readonly rpc: RpcNamespace<IDL, NullableMethods<IDL>[number]>;
 
   /**
    * [[InstructionNamespace]] for all state methods.
    */
-  readonly instruction: InstructionNamespace;
+  readonly instruction: InstructionNamespace<IDL, NullableMethods<IDL>[number]>;
 
   /**
    * [[TransactionNamespace]] for all state methods.
    */
-  readonly transaction: TransactionNamespace;
+  readonly transaction: TransactionNamespace<IDL, NullableMethods<IDL>[number]>;
 
   /**
    * Returns the program ID owning the state.
@@ -57,11 +73,12 @@ export class StateClient {
   private _programId: PublicKey;
 
   private _address: PublicKey;
-  private _idl: Idl;
+  private _coder: Coder;
+  private _idl: IDL;
   private _sub: Subscription | null;
 
   constructor(
-    idl: Idl,
+    idl: IDL,
     programId: PublicKey,
     /**
      * Returns the client's wallet and network provider.
@@ -79,46 +96,51 @@ export class StateClient {
 
     // Build namespaces.
     const [instruction, transaction, rpc] = ((): [
-      InstructionNamespace,
-      TransactionNamespace,
-      RpcNamespace
+      InstructionNamespace<IDL, NullableMethods<IDL>[number]>,
+      TransactionNamespace<IDL, NullableMethods<IDL>[number]>,
+      RpcNamespace<IDL, NullableMethods<IDL>[number]>
     ] => {
       let instruction: InstructionNamespace = {};
       let transaction: TransactionNamespace = {};
       let rpc: RpcNamespace = {};
 
-      idl.state?.methods.forEach((m: IdlStateMethod) => {
-        // Build instruction method.
-        const ixItem = InstructionNamespaceFactory.build(
-          m,
-          (ixName: string, ix: any) =>
-            coder.instruction.encodeState(ixName, ix),
-          programId
-        );
-        ixItem["accounts"] = (accounts: Accounts) => {
-          const keys = stateInstructionKeys(programId, provider, m, accounts);
-          return keys.concat(
-            InstructionNamespaceFactory.accountsArray(accounts, m.accounts)
+      idl.state?.methods.forEach(
+        <I extends NullableMethods<IDL>[number]>(m: I) => {
+          // Build instruction method.
+          const ixItem = InstructionNamespaceFactory.build<IDL, I>(
+            m,
+            (ixName, ix) => coder.instruction.encodeState(ixName, ix),
+            programId
+          );
+          ixItem["accounts"] = (accounts) => {
+            const keys = stateInstructionKeys(programId, provider, m, accounts);
+            return keys.concat(
+              InstructionNamespaceFactory.accountsArray(accounts, m.accounts)
+            );
+          };
+          // Build transaction method.
+          const txItem = TransactionNamespaceFactory.build(m, ixItem);
+          // Build RPC method.
+          const rpcItem = RpcNamespaceFactory.build(
+            m,
+            txItem,
+            parseIdlErrors(idl),
+            provider
           );
-        };
-        // Build transaction method.
-        const txItem = TransactionNamespaceFactory.build(m, ixItem);
-        // Build RPC method.
-        const rpcItem = RpcNamespaceFactory.build(
-          m,
-          txItem,
-          parseIdlErrors(idl),
-          provider
-        );
-
-        // Attach them all to their respective namespaces.
-        const name = camelCase(m.name);
-        instruction[name] = ixItem;
-        transaction[name] = txItem;
-        rpc[name] = rpcItem;
-      });
-
-      return [instruction, transaction, rpc];
+
+          // Attach them all to their respective namespaces.
+          const name = camelCase(m.name);
+          instruction[name] = ixItem;
+          transaction[name] = txItem;
+          rpc[name] = rpcItem;
+        }
+      );
+
+      return [
+        instruction as InstructionNamespace<IDL, NullableMethods<IDL>[number]>,
+        transaction as TransactionNamespace<IDL, NullableMethods<IDL>[number]>,
+        rpc as RpcNamespace<IDL, NullableMethods<IDL>[number]>,
+      ];
     })();
     this.instruction = instruction;
     this.transaction = transaction;
@@ -128,7 +150,14 @@ export class StateClient {
   /**
    * Returns the deserialized state account.
    */
-  async fetch(): Promise<Object> {
+  async fetch(): Promise<
+    TypeDef<
+      IDL["state"] extends undefined
+        ? IdlTypeDef
+        : NonNullable<IDL["state"]>["struct"],
+      IdlTypes<IDL>
+    >
+  > {
     const addr = this.address();
     const accountInfo = await this.provider.connection.getAccountInfo(addr);
     if (accountInfo === null) {
@@ -203,12 +232,12 @@ function programStateAddress(programId: PublicKey): PublicKey {
 
 // Returns the common keys that are prepended to all instructions targeting
 // the "state" of a program.
-function stateInstructionKeys(
+function stateInstructionKeys<M extends IdlStateMethod>(
   programId: PublicKey,
   provider: Provider,
-  m: IdlStateMethod,
-  accounts: Accounts
-) {
+  m: M,
+  accounts: Accounts<M["accounts"][number]>
+): AccountMeta[] {
   if (m.name === "new") {
     // Ctor `new` method.
     const [programSigner] = findProgramAddressSync([], programId);

+ 19 - 10
ts/src/program/namespace/transaction.ts

@@ -1,14 +1,19 @@
 import { Transaction } from "@solana/web3.js";
-import { IdlInstruction } from "../../idl";
+import { Idl, IdlInstruction } from "../../idl";
 import { splitArgsAndCtx } from "../context";
 import { InstructionFn } from "./instruction";
+import {
+  AllInstructions,
+  InstructionContextFn,
+  MakeInstructionsNamespace,
+} from "./types";
 
 export default class TransactionFactory {
-  public static build(
-    idlIx: IdlInstruction,
-    ixFn: InstructionFn
-  ): TransactionFn {
-    const txFn = (...args: any[]): Transaction => {
+  public static build<IDL extends Idl, I extends AllInstructions<IDL>>(
+    idlIx: I,
+    ixFn: InstructionFn<IDL, I>
+  ): TransactionFn<IDL, I> {
+    const txFn: TransactionFn<IDL, I> = (...args): Transaction => {
       const [, ctx] = splitArgsAndCtx(idlIx, [...args]);
       const tx = new Transaction();
       if (ctx.instructions !== undefined) {
@@ -51,11 +56,15 @@ export default class TransactionFactory {
  * });
  * ```
  */
-export interface TransactionNamespace {
-  [key: string]: TransactionFn;
-}
+export type TransactionNamespace<
+  IDL extends Idl = Idl,
+  I extends AllInstructions<IDL> = AllInstructions<IDL>
+> = MakeInstructionsNamespace<IDL, I, Transaction>;
 
 /**
  * Tx is a function to create a `Transaction` for a given program instruction.
  */
-export type TransactionFn = (...args: any[]) => Transaction;
+export type TransactionFn<
+  IDL extends Idl = Idl,
+  I extends AllInstructions<IDL> = AllInstructions<IDL>
+> = InstructionContextFn<IDL, I, Transaction>;

+ 139 - 0
ts/src/program/namespace/types.ts

@@ -0,0 +1,139 @@
+import { PublicKey } from "@solana/web3.js";
+import BN from "bn.js";
+import { Idl } from "src";
+import {
+  IdlField,
+  IdlInstruction,
+  IdlType,
+  IdlTypeDef,
+  IdlTypeDefTyEnum,
+  IdlTypeDefTyStruct,
+} from "../../idl";
+import { Accounts, Context } from "../context";
+
+/**
+ * All instructions for an IDL.
+ */
+export type AllInstructions<IDL extends Idl> = IDL["instructions"][number];
+
+/**
+ * Returns a type of instruction name to the IdlInstruction.
+ */
+export type InstructionMap<I extends IdlInstruction> = {
+  [K in I["name"]]: I & { name: K };
+};
+
+/**
+ * Returns a type of instruction name to the IdlInstruction.
+ */
+export type AllInstructionsMap<IDL extends Idl> = InstructionMap<
+  AllInstructions<IDL>
+>;
+
+/**
+ * All accounts for an IDL.
+ */
+export type AllAccounts<IDL extends Idl> = IDL["accounts"] extends undefined
+  ? IdlTypeDef
+  : NonNullable<IDL["accounts"]>[number];
+
+/**
+ * Returns a type of instruction name to the IdlInstruction.
+ */
+export type AccountMap<I extends IdlTypeDef> = {
+  [K in I["name"]]: I & { name: K };
+};
+
+/**
+ * Returns a type of instruction name to the IdlInstruction.
+ */
+export type AllAccountsMap<IDL extends Idl> = AccountMap<AllAccounts<IDL>>;
+
+export type MakeInstructionsNamespace<
+  IDL extends Idl,
+  I extends IdlInstruction,
+  Ret,
+  Mk extends { [M in keyof InstructionMap<I>]: unknown } = {
+    [M in keyof InstructionMap<I>]: unknown;
+  }
+> = {
+  [M in keyof InstructionMap<I>]: InstructionContextFn<
+    IDL,
+    InstructionMap<I>[M],
+    Ret
+  > &
+    Mk[M];
+};
+
+export type InstructionContextFn<
+  IDL extends Idl,
+  I extends AllInstructions<IDL>,
+  Ret
+> = (...args: InstructionContextFnArgs<IDL, I>) => Ret;
+
+export type InstructionContextFnArgs<
+  IDL extends Idl,
+  I extends IDL["instructions"][number]
+> = [
+  ...ArgsTuple<I["args"], IdlTypes<IDL>>,
+  Context<Accounts<I["accounts"][number]>>
+];
+
+type TypeMap = {
+  publicKey: PublicKey;
+  u64: BN;
+  i64: BN;
+} & {
+  [K in "u8" | "i8" | "u16" | "i16" | "u32" | "i32"]: number;
+};
+
+export type DecodeType<T extends IdlType, Defined> = T extends keyof TypeMap
+  ? TypeMap[T]
+  : T extends { defined: keyof Defined }
+  ? Defined[T["defined"]]
+  : T extends { option: { defined: keyof Defined } }
+  ? Defined[T["option"]["defined"]] | null
+  : T extends { vec: { defined: keyof Defined } }
+  ? Defined[T["vec"]["defined"]][]
+  : unknown;
+
+/**
+ * Tuple of arguments.
+ */
+type ArgsTuple<A extends IdlField[], Defined> = {
+  [K in keyof A]: A[K] extends IdlField
+    ? DecodeType<A[K]["type"], Defined>
+    : unknown;
+} &
+  unknown[];
+
+type FieldsOfType<I extends IdlTypeDef> = NonNullable<
+  I["type"] extends IdlTypeDefTyStruct
+    ? I["type"]["fields"]
+    : I["type"] extends IdlTypeDefTyEnum
+    ? I["type"]["variants"][number]["fields"]
+    : any[]
+>[number];
+
+export type TypeDef<I extends IdlTypeDef, Defined> = {
+  [F in FieldsOfType<I>["name"]]: DecodeType<
+    (FieldsOfType<I> & { name: F })["type"],
+    Defined
+  >;
+};
+
+type TypeDefDictionary<T extends IdlTypeDef[], Defined> = {
+  [K in T[number]["name"]]: TypeDef<T[number] & { name: K }, Defined>;
+};
+
+export type IdlTypes<T extends Idl> = TypeDefDictionary<
+  NonNullable<T["types"]>,
+  Record<string, never>
+>;
+
+export type IdlAccounts<T extends Idl> = TypeDefDictionary<
+  NonNullable<T["accounts"]>,
+  Record<string, never>
+>;
+
+export type IdlErrorInfo<IDL extends Idl> = NonNullable<IDL["errors"]>[number];

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 134 - 134
ts/yarn.lock


Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů