소스 검색

Merge pull request #48 from project-serum/armani/deploy

Armani Ferrante 4 년 전
부모
커밋
6648f185ec
19개의 변경된 파일659개의 추가작업 그리고 103개의 파일을 삭제
  1. 3 0
      Cargo.lock
  2. 4 3
      cli/Cargo.toml
  3. 1 1
      cli/src/config.rs
  4. 260 35
      cli/src/main.rs
  5. 2 0
      cli/src/template.rs
  6. 10 10
      examples/lockup/migrations/deploy.js
  7. 3 3
      examples/lockup/programs/lockup/src/lib.rs
  8. 19 24
      examples/lockup/tests/lockup.js
  9. 26 1
      src/account_info.rs
  10. 14 0
      src/ctor.rs
  11. 73 0
      src/idl.rs
  12. 1 0
      src/lib.rs
  13. 10 10
      syn/src/codegen/accounts.rs
  14. 140 0
      syn/src/codegen/program.rs
  15. 14 14
      syn/src/idl.rs
  16. 3 1
      ts/package.json
  17. 35 0
      ts/src/idl.ts
  18. 31 1
      ts/src/program.ts
  19. 10 0
      ts/yarn.lock

+ 3 - 0
Cargo.lock

@@ -101,10 +101,12 @@ dependencies = [
 name = "anchor-cli"
 version = "0.1.0"
 dependencies = [
+ "anchor-lang",
  "anchor-syn",
  "anyhow",
  "clap 3.0.0-beta.2",
  "dirs",
+ "flate2",
  "heck",
  "serde",
  "serde_json",
@@ -112,6 +114,7 @@ dependencies = [
  "serum-common",
  "shellexpand",
  "solana-client",
+ "solana-program",
  "solana-sdk",
  "syn 1.0.57",
  "toml",

+ 4 - 3
cli/Cargo.toml

@@ -8,12 +8,11 @@ edition = "2018"
 name = "anchor"
 path = "src/main.rs"
 
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
-
 [dependencies]
 clap = "3.0.0-beta.1"
 anyhow = "1.0.32"
 syn = { version = "1.0.54", features = ["full", "extra-traits"] }
+anchor-lang = { path = "../" }
 anchor-syn = { path = "../syn", features = ["idl"] }
 serde_json = "1.0"
 shellexpand = "2.1.0"
@@ -21,7 +20,9 @@ serde_yaml = "0.8"
 toml = "0.5.8"
 serde = { version = "1.0", features = ["derive"] }
 solana-sdk = "1.5.0"
+solana-program = "1.5.0"
 solana-client = "1.4.4"
 serum-common = { git = "https://github.com/project-serum/serum-dex", features = ["client"] }
 dirs = "3.0"
-heck = "0.3.1"
+heck = "0.3.1"
+flate2 = "1.0.19"

+ 1 - 1
cli/src/config.rs

@@ -148,7 +148,7 @@ pub fn extract_lib_name(path: impl AsRef<Path>) -> Result<String> {
     }
 }
 
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub struct Program {
     pub lib_name: String,
     pub path: PathBuf,

+ 260 - 35
cli/src/main.rs

@@ -1,10 +1,20 @@
 use crate::config::{find_cargo_toml, read_all_programs, Config, Program};
+use anchor_lang::idl::IdlAccount;
+use anchor_lang::{AnchorDeserialize, AnchorSerialize};
 use anchor_syn::idl::Idl;
-use anyhow::Result;
+use anyhow::{anyhow, Result};
 use clap::Clap;
+use flate2::read::ZlibDecoder;
+use flate2::write::ZlibEncoder;
+use flate2::Compression;
 use serde::{Deserialize, Serialize};
 use solana_client::rpc_client::RpcClient;
+use solana_client::rpc_config::RpcSendTransactionConfig;
+use solana_program::instruction::{AccountMeta, Instruction};
+use solana_sdk::commitment_config::CommitmentConfig;
 use solana_sdk::pubkey::Pubkey;
+use solana_sdk::signature::Signer;
+use solana_sdk::transaction::Transaction;
 use std::fs::{self, File};
 use std::io::prelude::*;
 use std::path::{Path, PathBuf};
@@ -34,8 +44,39 @@ pub enum Command {
     Test,
     /// Creates a new program.
     New { name: String },
-    /// Outputs an interface definition file.
+    /// Commands for interact with interface definitions.
     Idl {
+        #[clap(subcommand)]
+        subcmd: IdlCommand,
+    },
+    /// Deploys the workspace, creates IDL accounts, and runs the migration
+    /// script.
+    Deploy {
+        #[clap(short, long)]
+        url: Option<String>,
+        #[clap(short, long)]
+        keypair: Option<String>,
+    },
+    /// Runs the deploy migration script.
+    Migrate {
+        #[clap(short, long)]
+        url: String,
+    },
+    /// Not yet implemented. Please use `solana program deploy` command to
+    /// upgrade your program.
+    Upgrade {},
+}
+
+#[derive(Debug, Clap)]
+pub enum IdlCommand {
+    /// Initializes a program's IDL account. Can only be run once.
+    Init {
+        program_id: Pubkey,
+        #[clap(short, long)]
+        filepath: String,
+    },
+    /// Parses an IDL from source.
+    Parse {
         /// Path to the program's interface definition.
         #[clap(short, long)]
         file: String,
@@ -43,12 +84,12 @@ pub enum Command {
         #[clap(short, long)]
         out: Option<String>,
     },
-    /// Deploys the workspace to the configured cluster.
-    Deploy {
-        #[clap(short, long)]
-        url: Option<String>,
+    /// Fetches an IDL for the given program from a cluster.
+    Fetch {
+        program_id: Pubkey,
+        /// Output file for the idl (stdout if not specified).
         #[clap(short, long)]
-        keypair: Option<String>,
+        out: Option<String>,
     },
 }
 
@@ -60,13 +101,13 @@ fn main() -> Result<()> {
         Command::Build { idl } => build(idl),
         Command::Test => test(),
         Command::New { name } => new(name),
-        Command::Idl { file, out } => {
-            if out.is_none() {
-                return idl(file, None);
-            }
-            idl(file, Some(&PathBuf::from(out.unwrap())))
-        }
+        Command::Idl { subcmd } => idl(subcmd),
         Command::Deploy { url, keypair } => deploy(url, keypair),
+        Command::Upgrade {} => {
+            println!("This command is not yet implemented. Please use `solana program deploy`.");
+            Ok(())
+        }
+        Command::Migrate { url } => migrate(&url),
     }
 }
 
@@ -169,7 +210,7 @@ fn build_ws(
         Some(idl) => Some(PathBuf::from(idl)),
         None => {
             let cfg_parent = match cfg_path.parent() {
-                None => return Err(anyhow::anyhow!("Invalid Anchor.toml")),
+                None => return Err(anyhow!("Invalid Anchor.toml")),
                 Some(parent) => parent,
             };
             fs::create_dir_all(cfg_parent.join("target/idl"))?;
@@ -184,10 +225,7 @@ fn build_ws(
 
 fn build_all(_cfg: Config, cfg_path: PathBuf, idl_out: Option<PathBuf>) -> Result<()> {
     match cfg_path.parent() {
-        None => Err(anyhow::anyhow!(
-            "Invalid Anchor.toml at {}",
-            cfg_path.display()
-        )),
+        None => Err(anyhow!("Invalid Anchor.toml at {}", cfg_path.display())),
         Some(parent) => {
             let files = fs::read_dir(parent.join("programs"))?;
             for f in files {
@@ -212,7 +250,7 @@ fn build_cwd(idl_out: Option<String>) -> Result<()> {
 // Runs the build command outside of a workspace.
 fn _build_cwd(cargo_toml: PathBuf, idl_out: Option<PathBuf>) -> Result<()> {
     match cargo_toml.parent() {
-        None => return Err(anyhow::anyhow!("Unable to find parent")),
+        None => return Err(anyhow!("Unable to find parent")),
         Some(p) => std::env::set_current_dir(&p)?,
     };
 
@@ -234,12 +272,29 @@ fn _build_cwd(cargo_toml: PathBuf, idl_out: Option<PathBuf>) -> Result<()> {
         Some(o) => PathBuf::from(&o.join(&idl.name).with_extension("json")),
     };
 
-    write_idl(&idl, Some(&out))
+    write_idl(&idl, OutFile::File(out))
 }
 
-fn idl(file: String, out: Option<&Path>) -> Result<()> {
-    let idl = extract_idl(&file)?;
-    write_idl(&idl, out)
+// Fetches an IDL for the given program_id.
+fn fetch_idl(program_id: Pubkey) -> Result<Idl> {
+    let cfg = Config::discover()?.expect("Inside a workspace").0;
+    let client = RpcClient::new(cfg.cluster.url().to_string());
+
+    let idl_addr = IdlAccount::address(&program_id);
+
+    let account = client
+        .get_account_with_commitment(&idl_addr, CommitmentConfig::recent())?
+        .value
+        .map_or(Err(anyhow!("Account not found")), Ok)?;
+
+    // Cut off account discriminator.
+    let mut d: &[u8] = &account.data[8..];
+    let idl_account: IdlAccount = AnchorDeserialize::deserialize(&mut d)?;
+
+    let mut z = ZlibDecoder::new(&idl_account.data[..]);
+    let mut s = Vec::new();
+    z.read_to_end(&mut s)?;
+    serde_json::from_slice(&s[..]).map_err(Into::into)
 }
 
 fn extract_idl(file: &str) -> Result<Idl> {
@@ -247,15 +302,61 @@ fn extract_idl(file: &str) -> Result<Idl> {
     anchor_syn::parser::file::parse(&*file)
 }
 
-fn write_idl(idl: &Idl, out: Option<&Path>) -> Result<()> {
+fn idl(subcmd: IdlCommand) -> Result<()> {
+    match subcmd {
+        IdlCommand::Init {
+            program_id,
+            filepath,
+        } => idl_init(program_id, filepath),
+        IdlCommand::Parse { file, out } => idl_parse(file, out),
+        IdlCommand::Fetch { program_id, out } => idl_fetch(program_id, out),
+    }
+}
+
+fn idl_init(program_id: Pubkey, idl_filepath: String) -> Result<()> {
+    let cfg = Config::discover()?.expect("Inside a workspace").0;
+    let keypair = cfg.wallet.to_string();
+
+    let bytes = std::fs::read(idl_filepath)?;
+    let idl: Idl = serde_json::from_reader(&*bytes)?;
+    let idl_address = create_idl_account(&cfg, &keypair, &program_id, &idl)?;
+
+    println!("Idl account created: {:?}", idl_address);
+    Ok(())
+}
+
+fn idl_parse(file: String, out: Option<String>) -> Result<()> {
+    let idl = extract_idl(&file)?;
+    let out = match out {
+        None => OutFile::Stdout,
+        Some(out) => OutFile::File(PathBuf::from(out)),
+    };
+    write_idl(&idl, out)
+}
+
+fn idl_fetch(program_id: Pubkey, out: Option<String>) -> Result<()> {
+    let idl = fetch_idl(program_id)?;
+    let out = match out {
+        None => OutFile::Stdout,
+        Some(out) => OutFile::File(PathBuf::from(out)),
+    };
+    write_idl(&idl, out)
+}
+
+fn write_idl(idl: &Idl, out: OutFile) -> Result<()> {
     let idl_json = serde_json::to_string_pretty(idl)?;
-    match out.as_ref() {
-        None => println!("{}", idl_json),
-        Some(out) => std::fs::write(out, idl_json)?,
+    match out {
+        OutFile::Stdout => println!("{}", idl_json),
+        OutFile::File(out) => std::fs::write(out, idl_json)?,
     };
     Ok(())
 }
 
+enum OutFile {
+    Stdout,
+    File(PathBuf),
+}
+
 // Builds, deploys, and tests all workspace programs in a single command.
 fn test() -> Result<()> {
     // Switch directories to top level workspace.
@@ -290,13 +391,13 @@ fn test() -> Result<()> {
         let idl_out = PathBuf::from("target/idl")
             .join(&idl.name)
             .with_extension("json");
-        write_idl(&idl, Some(&idl_out))?;
+        write_idl(&idl, OutFile::File(idl_out))?;
     }
 
     // Run the tests.
     if let Err(e) = std::process::Command::new("mocha")
         .arg("-t")
-        .arg("10000")
+        .arg("1000000")
         .arg("tests/")
         .env("ANCHOR_PROVIDER_URL", cfg.cluster.url())
         .stdout(Stdio::inherit())
@@ -379,6 +480,10 @@ fn deploy(url: Option<String>, keypair: Option<String>) -> Result<()> {
 
     // Add metadata to all IDLs.
     for (program, address) in deployment {
+        // Store the IDL on chain.
+        let idl_address = create_idl_account(&cfg, &keypair, &address, &program.idl)?;
+        println!("IDL account created: {}", idl_address.to_string());
+
         // Add metadata to the IDL.
         let mut idl = program.idl;
         idl.metadata = Some(serde_json::to_value(IdlTestMetadata {
@@ -389,18 +494,130 @@ fn deploy(url: Option<String>, keypair: Option<String>) -> Result<()> {
         let idl_out = PathBuf::from("target/idl")
             .join(&idl.name)
             .with_extension("json");
-        write_idl(&idl, Some(&idl_out))?;
+        write_idl(&idl, OutFile::File(idl_out))?;
 
         println!("Deployed {} at {}", idl.name, address.to_string());
     }
 
-    run_hosted_deploy(&url)?;
+    // Run migration script.
+    migrate(&url)?;
 
     Ok(())
 }
 
-fn run_hosted_deploy(url: &str) -> Result<()> {
-    println!("Running deploy script");
+fn create_idl_account(
+    cfg: &Config,
+    keypair_path: &str,
+    program_id: &Pubkey,
+    idl: &Idl,
+) -> Result<Pubkey> {
+    // Misc.
+    let idl_address = IdlAccount::address(program_id);
+    let keypair = solana_sdk::signature::read_keypair_file(keypair_path)
+        .map_err(|_| anyhow!("Unable to read keypair file"))?;
+    let client = RpcClient::new(cfg.cluster.url().to_string());
+
+    // Serialize and compress the idl.
+    let idl_data = {
+        let json_bytes = serde_json::to_vec(idl)?;
+        let mut e = ZlibEncoder::new(Vec::new(), Compression::default());
+        e.write_all(&json_bytes)?;
+        e.finish()?
+    };
+
+    // Run `Create instruction.
+    {
+        let data = serialize_idl_ix(anchor_lang::idl::IdlInstruction::Create {
+            data_len: (idl_data.len() as u64) * 2, // Double for future growth.
+        })?;
+        let program_signer = Pubkey::find_program_address(&[], program_id).0;
+        let accounts = vec![
+            AccountMeta::new_readonly(keypair.pubkey(), true),
+            AccountMeta::new(idl_address, false),
+            AccountMeta::new_readonly(program_signer, false),
+            AccountMeta::new_readonly(solana_program::system_program::ID, false),
+            AccountMeta::new_readonly(*program_id, false),
+            AccountMeta::new_readonly(solana_program::sysvar::rent::ID, false),
+        ];
+        let ix = Instruction {
+            program_id: *program_id,
+            accounts,
+            data,
+        };
+        let (recent_hash, _fee_calc) = client.get_recent_blockhash()?;
+        let tx = Transaction::new_signed_with_payer(
+            &[ix],
+            Some(&keypair.pubkey()),
+            &[&keypair],
+            recent_hash,
+        );
+        client.send_and_confirm_transaction_with_spinner_and_config(
+            &tx,
+            CommitmentConfig::single(),
+            RpcSendTransactionConfig {
+                skip_preflight: true,
+                ..RpcSendTransactionConfig::default()
+            },
+        )?;
+    }
+
+    // Write the idl to the account buffer, chopping up the IDL into pieces
+    // and sending multiple transactions in the event the IDL doesn't fit into
+    // a single transaction.
+    {
+        const MAX_WRITE_SIZE: usize = 1000;
+        let mut offset = 0;
+        while offset < idl_data.len() {
+            // Instruction data.
+            let data = {
+                let start = offset;
+                let end = std::cmp::min(offset + MAX_WRITE_SIZE, idl_data.len());
+                serialize_idl_ix(anchor_lang::idl::IdlInstruction::Write {
+                    data: idl_data[start..end].to_vec(),
+                })?
+            };
+            // Instruction accounts.
+            let accounts = vec![
+                AccountMeta::new(idl_address, false),
+                AccountMeta::new_readonly(keypair.pubkey(), true),
+            ];
+            // Instruction.
+            let ix = Instruction {
+                program_id: *program_id,
+                accounts,
+                data,
+            };
+            // Send transaction.
+            let (recent_hash, _fee_calc) = client.get_recent_blockhash()?;
+            let tx = Transaction::new_signed_with_payer(
+                &[ix],
+                Some(&keypair.pubkey()),
+                &[&keypair],
+                recent_hash,
+            );
+            client.send_and_confirm_transaction_with_spinner_and_config(
+                &tx,
+                CommitmentConfig::single(),
+                RpcSendTransactionConfig {
+                    skip_preflight: true,
+                    ..RpcSendTransactionConfig::default()
+                },
+            )?;
+            offset += MAX_WRITE_SIZE;
+        }
+    }
+
+    Ok(idl_address)
+}
+
+fn serialize_idl_ix(ix_inner: anchor_lang::idl::IdlInstruction) -> Result<Vec<u8>> {
+    let mut data = anchor_lang::idl::IDL_IX_TAG.to_le_bytes().to_vec();
+    data.append(&mut ix_inner.try_to_vec()?);
+    Ok(data)
+}
+
+fn migrate(url: &str) -> Result<()> {
+    println!("Running migration deploy script");
 
     let cur_dir = std::env::current_dir()?;
     let module_path = format!("{}/migrations/deploy.js", cur_dir.display());
@@ -424,16 +641,24 @@ fn run_hosted_deploy(url: &str) -> Result<()> {
 fn deploy_ws(url: &str, keypair: &str) -> Result<Vec<(Program, Pubkey)>> {
     let mut programs = vec![];
     println!("Deploying workspace to {}...", url);
+    println!("Upgrade authority: {}", keypair);
     for program in read_all_programs()? {
         let binary_path = format!("target/deploy/{}.so", program.lib_name);
+
+        // The Solana CLI doesn't redeploy a program if this file exists.
+        // So remove it to make deploys explicit.
+        let keypair_path = format!("target/deploy/{}-keypair.json", program.lib_name);
+        std::fs::remove_file(keypair_path)?;
+
         println!("Deploying {}...", binary_path);
         let exit = std::process::Command::new("solana")
+            .arg("program")
             .arg("deploy")
-            .arg(&binary_path)
             .arg("--url")
             .arg(url)
             .arg("--keypair")
             .arg(keypair)
+            .arg(&binary_path)
             .output()
             .expect("Must deploy");
         if !exit.status.success() {
@@ -448,7 +673,7 @@ fn deploy_ws(url: &str, keypair: &str) -> Result<Vec<(Program, Pubkey)>> {
 }
 
 #[derive(Debug, Deserialize)]
-#[serde(rename_all = "camelCase")]
+#[serde(rename_all = "PascalCase")]
 pub struct DeployStdout {
     program_id: String,
 }

+ 2 - 0
cli/src/template.rs

@@ -24,7 +24,9 @@ name = "{1}"
 
 [features]
 no-entrypoint = []
+no-idl = []
 cpi = ["no-entrypoint"]
+default = []
 
 [dependencies]
 anchor-lang = {{ git = "https://github.com/project-serum/anchor", features = ["derive"] }}

+ 10 - 10
examples/lockup/migrations/deploy.js

@@ -90,15 +90,15 @@ async function genesis(provider) {
   ) {
     return {
       srm: {
-        withdrawalTimelock: 60,
-        stakeRate: 1000 * 10 ** 6,
-        rewardQLen: 100,
+        withdrawalTimelock: 60 * 60 * 24 * 7, // 1 week.
+        stakeRate: 500 * 10 ** 6, // 500 SRM.
+        rewardQLen: 150,
         mint: "SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt",
       },
       msrm: {
-        withdrawalTimelock: 45,
+        withdrawalTimelock: 60 * 60 * 24 * 7, // 1 week.
         stakeRate: 1,
-        rewardQLen: 100,
+        rewardQLen: 150,
         mint: "MSRMcoVyrFxnSgo5uXwone5SKcGhT1KEJMFEkMEWf9L",
       },
     };
@@ -117,15 +117,15 @@ async function genesis(provider) {
     );
     return {
       token1: {
-        withdrawalTimelock: 60,
-        stakeRate: 2 * 10 ** 6,
-        rewardQLen: 100,
+        withdrawalTimelock: 60 * 60 * 24 * 7,
+        stakeRate: 1000 * 10 ** 6,
+        rewardQLen: 150,
         mint: token1Mint.toString(),
       },
       token2: {
-        withdrawalTimelock: 45,
+        withdrawalTimelock: 60 * 60 * 24 * 7,
         stakeRate: 1,
-        rewardQLen: 100,
+        rewardQLen: 150,
         mint: token2Mint.toString(),
       },
     };

+ 3 - 3
examples/lockup/programs/lockup/src/lib.rs

@@ -23,7 +23,7 @@ pub mod lockup {
     }
 
     impl Lockup {
-        pub const WHITELIST_SIZE: usize = 5;
+        pub const WHITELIST_SIZE: usize = 10;
 
         pub fn new(ctx: Context<Auth>) -> Result<Self> {
             let mut whitelist = vec![];
@@ -111,7 +111,7 @@ pub mod lockup {
                 ctx.accounts.clock.unix_timestamp,
             )
         {
-            return Err(ErrorCode::InsufficienWithdrawalBalance.into());
+            return Err(ErrorCode::InsufficientWithdrawalBalance.into());
         }
 
         // Transfer funds out.
@@ -348,7 +348,7 @@ pub enum ErrorCode {
     #[msg("Vault amount must be zero.")]
     InvalidVaultAmount,
     #[msg("Insufficient withdrawal balance.")]
-    InsufficienWithdrawalBalance,
+    InsufficientWithdrawalBalance,
     #[msg("Whitelist is full")]
     WhitelistFull,
     #[msg("Whitelist entry already exists")]

+ 19 - 24
examples/lockup/tests/lockup.js

@@ -1,5 +1,5 @@
 const assert = require("assert");
-const anchor = require('@project-serum/anchor');
+const anchor = require("@project-serum/anchor");
 const serumCmn = require("@project-serum/common");
 const TokenInstructions = require("@project-serum/serum").TokenInstructions;
 const utils = require("./utils");
@@ -15,6 +15,7 @@ describe("Lockup and Registry", () => {
   const registry = anchor.workspace.Registry;
 
   let lockupAddress = null;
+  const WHITELIST_SIZE = 10;
 
   let mint = null;
   let god = null;
@@ -39,7 +40,7 @@ describe("Lockup and Registry", () => {
     const lockupAccount = await lockup.state();
 
     assert.ok(lockupAccount.authority.equals(provider.wallet.publicKey));
-    assert.ok(lockupAccount.whitelist.length === 5);
+    assert.ok(lockupAccount.whitelist.length === WHITELIST_SIZE);
     lockupAccount.whitelist.forEach((e) => {
       assert.ok(e.programId.equals(new anchor.web3.PublicKey()));
     });
@@ -76,11 +77,7 @@ describe("Lockup and Registry", () => {
     assert.ok(lockupAccount.authority.equals(provider.wallet.publicKey));
   });
 
-  let e0 = null;
-  let e1 = null;
-  let e2 = null;
-  let e3 = null;
-  let e4 = null;
+  const entries = [];
 
   it("Adds to the whitelist", async () => {
     const generateEntry = async () => {
@@ -89,36 +86,34 @@ describe("Lockup and Registry", () => {
         programId,
       };
     };
-    e0 = await generateEntry();
-    e1 = await generateEntry();
-    e2 = await generateEntry();
-    e3 = await generateEntry();
-    e4 = await generateEntry();
-    const e5 = await generateEntry();
+
+    for (let k = 0; k < WHITELIST_SIZE; k += 1) {
+      entries.push(await generateEntry());
+    }
 
     const accounts = {
       authority: provider.wallet.publicKey,
     };
 
-    await lockup.state.rpc.whitelistAdd(e0, { accounts });
+    await lockup.state.rpc.whitelistAdd(entries[0], { accounts });
 
     let lockupAccount = await lockup.state();
 
     assert.ok(lockupAccount.whitelist.length === 1);
-    assert.deepEqual(lockupAccount.whitelist, [e0]);
+    assert.deepEqual(lockupAccount.whitelist, [entries[0]]);
 
-    await lockup.state.rpc.whitelistAdd(e1, { accounts });
-    await lockup.state.rpc.whitelistAdd(e2, { accounts });
-    await lockup.state.rpc.whitelistAdd(e3, { accounts });
-    await lockup.state.rpc.whitelistAdd(e4, { accounts });
+    for (let k = 1; k < WHITELIST_SIZE; k += 1) {
+      await lockup.state.rpc.whitelistAdd(entries[k], { accounts });
+    }
 
     lockupAccount = await lockup.state();
 
-    assert.deepEqual(lockupAccount.whitelist, [e0, e1, e2, e3, e4]);
+    assert.deepEqual(lockupAccount.whitelist, entries);
 
     await assert.rejects(
       async () => {
-        await lockup.state.rpc.whitelistAdd(e5, { accounts });
+        const e = await generateEntry();
+        await lockup.state.rpc.whitelistAdd(e, { accounts });
       },
       (err) => {
         assert.equal(err.code, 108);
@@ -129,13 +124,13 @@ describe("Lockup and Registry", () => {
   });
 
   it("Removes from the whitelist", async () => {
-    await lockup.state.rpc.whitelistDelete(e0, {
+    await lockup.state.rpc.whitelistDelete(entries[0], {
       accounts: {
         authority: provider.wallet.publicKey,
       },
     });
     let lockupAccount = await lockup.state();
-    assert.deepEqual(lockupAccount.whitelist, [e1, e2, e3, e4]);
+    assert.deepEqual(lockupAccount.whitelist, entries.slice(1));
   });
 
   const vesting = new anchor.web3.Account();
@@ -264,7 +259,7 @@ describe("Lockup and Registry", () => {
   const rewardQ = new anchor.web3.Account();
   const withdrawalTimelock = new anchor.BN(4);
   const stakeRate = new anchor.BN(2);
-  const rewardQLen = 100;
+  const rewardQLen = 170;
   let registrarAccount = null;
   let registrarSigner = null;
   let nonce = null;

+ 26 - 1
src/account_info.rs

@@ -1,4 +1,4 @@
-use crate::{Accounts, AccountsExit, ToAccountInfo, ToAccountInfos, ToAccountMetas};
+use crate::{Accounts, AccountsExit, AccountsInit, ToAccountInfo, ToAccountInfos, ToAccountMetas};
 use solana_program::account_info::AccountInfo;
 use solana_program::entrypoint::ProgramResult;
 use solana_program::instruction::AccountMeta;
@@ -19,6 +19,31 @@ impl<'info> Accounts<'info> for AccountInfo<'info> {
     }
 }
 
+impl<'info> AccountsInit<'info> for AccountInfo<'info> {
+    fn try_accounts_init(
+        _program_id: &Pubkey,
+        accounts: &mut &[AccountInfo<'info>],
+    ) -> Result<Self, ProgramError> {
+        if accounts.len() == 0 {
+            return Err(ProgramError::NotEnoughAccountKeys);
+        }
+
+        let account = &accounts[0];
+        *accounts = &accounts[1..];
+
+        // The discriminator should be zero, since we're initializing.
+        let data: &[u8] = &account.try_borrow_data()?;
+        let mut disc_bytes = [0u8; 8];
+        disc_bytes.copy_from_slice(&data[..8]);
+        let discriminator = u64::from_le_bytes(disc_bytes);
+        if discriminator != 0 {
+            return Err(ProgramError::InvalidAccountData);
+        }
+
+        Ok(account.clone())
+    }
+}
+
 impl<'info> ToAccountMetas for AccountInfo<'info> {
     fn to_account_metas(&self, is_signer: Option<bool>) -> Vec<AccountMeta> {
         let is_signer = is_signer.unwrap_or(self.is_signer);

+ 14 - 0
src/ctor.rs

@@ -5,13 +5,27 @@ use solana_program::sysvar::rent::Rent;
 // Needed for the `Accounts` macro.
 use crate as anchor_lang;
 
+// The Ctor accounts that can be used to create any account within the program
+// itself (instead of creating the account on the client).
+//
+// This is used to create accounts at deterministic addresses, as a function of
+// nothing but a program ID--for example, to create state  global program
+// structs and program IDL accounts.
 #[derive(Accounts)]
 pub struct Ctor<'info> {
+    // Payer of the transaction.
     pub from: AccountInfo<'info>,
+    // The deterministically defined "state" account being created via
+    // `create_account_with_seed`.
     #[account(mut)]
     pub to: AccountInfo<'info>,
+    // The program-derived-address signing off on the account creation.
+    // Seeds = &[] + bump seed.
     pub base: AccountInfo<'info>,
+    // The system program.
     pub system_program: AccountInfo<'info>,
+    // The program whose state is being constructed.
     pub program: AccountInfo<'info>,
+    // Rent sysvar.
     pub rent: Sysvar<'info, Rent>,
 }

+ 73 - 0
src/idl.rs

@@ -0,0 +1,73 @@
+//! idl.rs defines the instructions and account state used to store a
+//! program's IDL.
+//!
+//! Note that the transaction to store the IDL can be larger than the max
+//! transaction size. As a reuslt, the transaction must be broken up into
+//! several pieces and stored into the IDL account with multiple transactions
+//! via the `Write` instruction to continuously append to the account's IDL data
+//! buffer.
+//!
+//! To upgrade the IDL, first invoke the `Clear` instruction to reset the data.
+//! And invoke `Write` once more. To eliminate the ability to change the IDL,
+//! set the authority to a key for which you can't sign, e.g., the zero address
+//! or the system program ID, or compile the program with the "no-idl" feature
+//! and upgrade the program with the upgradeable BPF loader.
+
+use crate::prelude::*;
+use solana_program::pubkey::Pubkey;
+
+// Needed for the `Accounts` macro.
+use crate as anchor_lang;
+
+// The first 8 bytes of an instruction to create or modify the IDL account. This
+// instruction is defined outside the main program's instruction enum, so that
+// the enum variant tags can align with function source order.
+//
+// Sha256(anchor:idl)[..8];
+pub const IDL_IX_TAG: u64 = 0x0a69e9a778bcf440;
+
+#[derive(AnchorSerialize, AnchorDeserialize)]
+pub enum IdlInstruction {
+    // One time initializer for creating the program's idl account.
+    Create { data_len: u64 },
+    // Appends to the end of the idl account data.
+    Write { data: Vec<u8> },
+    // Clear's the IdlInstruction data. Used to update the IDL.
+    Clear,
+    // Sets a new authority on the IdlAccount.
+    SetAuthority { new_authority: Pubkey },
+}
+
+// Accounts for the Create instuction.
+pub type IdlCreateAccounts<'info> = crate::ctor::Ctor<'info>;
+
+// Accounts for Idl instructions.
+#[derive(Accounts)]
+pub struct IdlAccounts<'info> {
+    #[account(mut, has_one = authority)]
+    pub idl: ProgramAccount<'info, IdlAccount>,
+    #[account(signer)]
+    pub authority: AccountInfo<'info>,
+}
+
+// The account holding a program's IDL. This is stored on chain so that clients
+// can fetch it and generate a client with nothing but a program's ID.
+#[account]
+#[derive(Debug)]
+pub struct IdlAccount {
+    // Address that can modify the IDL.
+    pub authority: Pubkey,
+    // Compressed idl bytes.
+    pub data: Vec<u8>,
+}
+
+impl IdlAccount {
+    pub fn address(program_id: &Pubkey) -> Pubkey {
+        let program_signer = Pubkey::find_program_address(&[], program_id).0;
+        Pubkey::create_with_seed(&program_signer, IdlAccount::seed(), program_id)
+            .expect("Seed is always valid")
+    }
+    pub fn seed() -> &'static str {
+        "anchor:idl"
+    }
+}

+ 1 - 0
src/lib.rs

@@ -33,6 +33,7 @@ mod context;
 mod cpi_account;
 mod ctor;
 mod error;
+pub mod idl;
 mod program_account;
 mod state;
 mod sysvar;

+ 10 - 10
syn/src/codegen/accounts.rs

@@ -15,17 +15,17 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream {
                 let name = &s.ident;
                 let ty = &s.raw_field.ty;
                 quote! {
-                    let #name: #ty = Accounts::try_accounts(program_id, accounts)?;
+                    let #name: #ty = anchor_lang::Accounts::try_accounts(program_id, accounts)?;
                 }
             }
             AccountField::Field(f) => {
                 let name = f.typed_ident();
                 match f.is_init {
                     false => quote! {
-                        let #name = Accounts::try_accounts(program_id, accounts)?;
+                        let #name = anchor_lang::Accounts::try_accounts(program_id, accounts)?;
                     },
                     true => quote! {
-                        let #name = AccountsInit::try_accounts_init(program_id, accounts)?;
+                        let #name = anchor_lang::AccountsInit::try_accounts_init(program_id, accounts)?;
                     },
                 }
             }
@@ -217,7 +217,7 @@ pub fn generate_constraint_belongs_to(
     // todo: would be nice if target could be an account info object.
     quote! {
         if &#ident.#target != #target.to_account_info().key {
-            return Err(ProgramError::Custom(1)); // todo: error codes
+            return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(1)); // todo: error codes
         }
     }
 }
@@ -237,7 +237,7 @@ pub fn generate_constraint_signer(f: &Field, _c: &ConstraintSigner) -> proc_macr
         // This check will be performed on the other end of the invocation.
         if cfg!(not(feature = "cpi")) {
             if !#info.is_signer {
-                return Err(ProgramError::MissingRequiredSignature);
+                return Err(anchor_lang::solana_program::program_error::ProgramError::MissingRequiredSignature);
             }
         }
     }
@@ -247,7 +247,7 @@ pub fn generate_constraint_literal(c: &ConstraintLiteral) -> proc_macro2::TokenS
     let tokens = &c.tokens;
     quote! {
         if !(#tokens) {
-            return Err(ProgramError::Custom(1)); // todo: error codes
+            return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(1)); // todo: error codes
         }
     }
 }
@@ -263,7 +263,7 @@ pub fn generate_constraint_owner(f: &Field, c: &ConstraintOwner) -> proc_macro2:
         ConstraintOwner::Skip => quote! {},
         ConstraintOwner::Program => quote! {
             if #info.owner != program_id {
-                return Err(ProgramError::Custom(1)); // todo: error codes
+                return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(1)); // todo: error codes
             }
         },
     }
@@ -283,7 +283,7 @@ pub fn generate_constraint_rent_exempt(
         ConstraintRentExempt::Skip => quote! {},
         ConstraintRentExempt::Enforce => quote! {
             if !rent.is_exempt(#info.lamports(), #info.try_data_len()?) {
-                return Err(ProgramError::Custom(2)); // todo: error codes
+                return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(2)); // todo: error codes
             }
         },
     }
@@ -296,9 +296,9 @@ pub fn generate_constraint_seeds(f: &Field, c: &ConstraintSeeds) -> proc_macro2:
         let program_signer = Pubkey::create_program_address(
             &#seeds,
             program_id,
-        ).map_err(|_| ProgramError::Custom(1))?; // todo
+        ).map_err(|_| anchor_lang::solana_program::program_error::ProgramError::Custom(1))?; // todo
         if #name.to_account_info().key != &program_signer {
-            return Err(ProgramError::Custom(1)); // todo
+            return Err(anchor_lang::solana_program::program_error::ProgramError::Custom(1)); // todo
         }
     }
 }

+ 140 - 0
syn/src/codegen/program.rs

@@ -21,6 +21,13 @@ pub fn generate(program: Program) -> proc_macro2::TokenStream {
         anchor_lang::solana_program::entrypoint!(entry);
         #[cfg(not(feature = "no-entrypoint"))]
         fn entry(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult {
+            if cfg!(not(feature = "no-idl")) {
+                if instruction_data.len() >= 8 {
+                    if anchor_lang::idl::IDL_IX_TAG.to_le_bytes() == instruction_data[..8] {
+                        return __private::__idl(program_id, accounts, &instruction_data[8..]);
+                    }
+                }
+            }
             let mut data: &[u8] = instruction_data;
             let ix = __private::instruction::#instruction_name::deserialize(&mut data)
                 .map_err(|_| ProgramError::Custom(1))?; // todo: error code
@@ -114,6 +121,136 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
 // so.
 pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStream {
     let program_name = &program.name;
+    let non_inlined_idl: proc_macro2::TokenStream = {
+        quote! {
+            // Entry for all IDL related instructions. Use the "no-idl" feature
+            // to eliminate this code, for example, if one wants to make the
+            // IDL no longer mutable or if one doesn't want to store the IDL
+            // on chain.
+            #[inline(never)]
+            #[cfg(not(feature = "no-idl"))]
+            pub fn __idl(program_id: &Pubkey, accounts: &[AccountInfo], idl_ix_data: &[u8]) -> ProgramResult {
+                let mut accounts = accounts;
+                let mut data: &[u8] = idl_ix_data;
+
+                let ix = anchor_lang::idl::IdlInstruction::deserialize(&mut data)
+                    .map_err(|_| ProgramError::Custom(1))?; // todo
+
+                match ix {
+                    anchor_lang::idl::IdlInstruction::Create { data_len } => {
+                        let mut accounts = anchor_lang::idl::IdlCreateAccounts::try_accounts(program_id, &mut accounts)?;
+                        __idl_create_account(program_id, &mut accounts, data_len)?;
+                        accounts.exit(program_id)?;
+                    },
+                    anchor_lang::idl::IdlInstruction::Write { data } => {
+                        let mut accounts = anchor_lang::idl::IdlAccounts::try_accounts(program_id, &mut accounts)?;
+                        __idl_write(program_id, &mut accounts, data)?;
+                        accounts.exit(program_id)?;
+                    },
+                    anchor_lang::idl::IdlInstruction::Clear => {
+                        let mut accounts = anchor_lang::idl::IdlAccounts::try_accounts(program_id, &mut accounts)?;
+                        __idl_clear(program_id, &mut accounts)?;
+                        accounts.exit(program_id)?;
+                    },
+                    anchor_lang::idl::IdlInstruction::SetAuthority { new_authority } => {
+                        let mut accounts = anchor_lang::idl::IdlAccounts::try_accounts(program_id, &mut accounts)?;
+                        __idl_set_authority(program_id, &mut accounts, new_authority)?;
+                        accounts.exit(program_id)?;
+                    }
+                }
+                Ok(())
+            }
+
+            // One time IDL account initializer. Will faill on subsequent
+            // invocations.
+            #[inline(never)]
+            pub fn __idl_create_account(
+                program_id: &Pubkey,
+                accounts: &mut anchor_lang::idl::IdlCreateAccounts,
+                data_len: u64,
+            ) -> ProgramResult {
+                // Create the IDL's account.
+                let from = accounts.from.key;
+                let (base, nonce) = Pubkey::find_program_address(&[], accounts.program.key);
+                let seed = anchor_lang::idl::IdlAccount::seed();
+                let owner = accounts.program.key;
+                let to = Pubkey::create_with_seed(&base, seed, owner).unwrap();
+                // Space: account discriminator || authority pubkey || vec len || vec data
+                let space = 8 + 32 + 4 + data_len as usize;
+                let lamports = accounts.rent.minimum_balance(space);
+                let seeds = &[&[nonce][..]];
+                let ix = anchor_lang::solana_program::system_instruction::create_account_with_seed(
+                    from,
+                    &to,
+                    &base,
+                    seed,
+                    lamports,
+                    space as u64,
+                    owner,
+                );
+                anchor_lang::solana_program::program::invoke_signed(
+                    &ix,
+                    &[
+                        accounts.from.clone(),
+                        accounts.to.clone(),
+                        accounts.base.clone(),
+                        accounts.system_program.clone(),
+                    ],
+                    &[seeds],
+                )?;
+
+                // Deserialize the newly created account.
+                let mut idl_account = {
+                    let mut account_data =  accounts.to.try_borrow_data()?;
+                    let mut account_data_slice: &[u8] = &account_data;
+                    anchor_lang::idl::IdlAccount::try_deserialize_unchecked(
+                        &mut account_data_slice,
+                    )?
+                };
+
+                // Set the authority.
+                idl_account.authority = *accounts.from.key;
+
+                // Store the new account data.
+                let mut data = accounts.to.try_borrow_mut_data()?;
+                let dst: &mut [u8] = &mut data;
+                let mut cursor = std::io::Cursor::new(dst);
+                idl_account.try_serialize(&mut cursor)?;
+
+                Ok(())
+            }
+
+            #[inline(never)]
+            pub fn __idl_write(
+                program_id: &Pubkey,
+                accounts: &mut anchor_lang::idl::IdlAccounts,
+                idl_data: Vec<u8>,
+            ) -> ProgramResult {
+                let mut idl = &mut accounts.idl;
+                idl.data.extend(idl_data);
+                Ok(())
+            }
+
+            #[inline(never)]
+            pub fn __idl_clear(
+                program_id: &Pubkey,
+                accounts: &mut anchor_lang::idl::IdlAccounts,
+            ) -> ProgramResult {
+                accounts.idl.data = vec![];
+                Ok(())
+            }
+
+            #[inline(never)]
+            pub fn __idl_set_authority(
+                program_id: &Pubkey,
+                accounts: &mut anchor_lang::idl::IdlAccounts,
+                new_authority: Pubkey,
+            ) -> ProgramResult {
+                accounts.idl.authority = new_authority;
+                Ok(())
+            }
+        }
+    };
     let non_inlined_ctor: proc_macro2::TokenStream = match &program.state {
         None => quote! {},
         Some(state) => {
@@ -123,6 +260,8 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr
             let mod_name = &program.name;
             let anchor_ident = &state.ctor_anchor;
             quote! {
+                // One time state account initializer. Will faill on subsequent
+                // invocations.
                 #[inline(never)]
                 pub fn __ctor(program_id: &Pubkey, accounts: &[AccountInfo], #(#ctor_typed_args),*) -> ProgramResult {
                     let mut remaining_accounts: &[AccountInfo] = accounts;
@@ -277,6 +416,7 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr
         .collect();
 
     quote! {
+        #non_inlined_idl
         #non_inlined_ctor
         #(#non_inlined_state_handlers)*
         #(#non_inlined_handlers)*

+ 14 - 14
syn/src/idl.rs

@@ -1,6 +1,6 @@
 use serde::{Deserialize, Serialize};
 
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct Idl {
     pub version: String,
     pub name: String,
@@ -17,7 +17,7 @@ pub struct Idl {
     pub metadata: Option<serde_json::Value>,
 }
 
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct IdlState {
     #[serde(rename = "struct")]
     pub strct: IdlTypeDef,
@@ -26,7 +26,7 @@ pub struct IdlState {
 
 pub type IdlStateMethod = IdlInstruction;
 
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct IdlInstruction {
     pub name: String,
     pub accounts: Vec<IdlAccountItem>,
@@ -34,14 +34,14 @@ pub struct IdlInstruction {
 }
 
 // A single struct deriving `Accounts`.
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
 #[serde(rename_all = "camelCase")]
 pub struct IdlAccounts {
     pub name: String,
     pub accounts: Vec<IdlAccountItem>,
 }
 
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
 #[serde(untagged)]
 pub enum IdlAccountItem {
     IdlAccount(IdlAccount),
@@ -49,7 +49,7 @@ pub enum IdlAccountItem {
 }
 
 // A single field in the accounts struct.
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
 #[serde(rename_all = "camelCase")]
 pub struct IdlAccount {
     pub name: String,
@@ -57,42 +57,42 @@ pub struct IdlAccount {
     pub is_signer: bool,
 }
 
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct IdlField {
     pub name: String,
     #[serde(rename = "type")]
     pub ty: IdlType,
 }
 
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct IdlTypeDef {
     pub name: String,
     #[serde(rename = "type")]
     pub ty: IdlTypeDefTy,
 }
 
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
 #[serde(rename_all = "lowercase", tag = "kind")]
 pub enum IdlTypeDefTy {
     Struct { fields: Vec<IdlField> },
     Enum { variants: Vec<EnumVariant> },
 }
 
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct EnumVariant {
     pub name: String,
     #[serde(skip_serializing_if = "Option::is_none", default)]
     pub fields: Option<EnumFields>,
 }
 
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
 #[serde(untagged)]
 pub enum EnumFields {
     Named(Vec<IdlField>),
     Tuple(Vec<IdlType>),
 }
 
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
 #[serde(rename_all = "camelCase")]
 pub enum IdlType {
     Bool,
@@ -112,7 +112,7 @@ pub enum IdlType {
     Vec(Box<IdlType>),
 }
 
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct IdlTypePublicKey;
 
 impl std::str::FromStr for IdlType {
@@ -162,7 +162,7 @@ impl std::str::FromStr for IdlType {
     }
 }
 
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct IdlErrorCode {
     pub code: u32,
     pub name: String,

+ 3 - 1
ts/package.json

@@ -27,13 +27,15 @@
     "@solana/web3.js": "^0.90.4",
     "@types/bn.js": "^4.11.6",
     "@types/bs58": "^4.0.1",
+    "@types/pako": "^1.0.1",
     "bn.js": "^5.1.2",
     "bs58": "^4.0.1",
     "buffer-layout": "^1.2.0",
     "camelcase": "^5.3.1",
     "crypto-hash": "^1.3.0",
     "eventemitter3": "^4.0.7",
-    "find": "^0.3.0"
+    "find": "^0.3.0",
+    "pako": "^2.0.3"
   },
   "devDependencies": {
     "@commitlint/cli": "^8.2.0",

+ 35 - 0
ts/src/idl.ts

@@ -1,3 +1,6 @@
+import { PublicKey } from "@solana/web3.js";
+import * as borsh from "@project-serum/borsh";
+
 export type Idl = {
   version: string;
   name: string;
@@ -101,3 +104,35 @@ type IdlErrorCode = {
   name: string;
   msg?: string;
 };
+
+// Deterministic IDL address as a function of the program id.
+export async function idlAddress(programId: PublicKey): Promise<PublicKey> {
+  const base = (await PublicKey.findProgramAddress([], programId))[0];
+  return await PublicKey.createWithSeed(base, seed(), programId);
+}
+
+// Seed for generating the idlAddress.
+export function seed(): string {
+  return "anchor:idl";
+}
+
+// The on-chain account of the IDL.
+export interface IdlProgramAccount {
+  authority: PublicKey;
+  data: Buffer;
+}
+
+const IDL_ACCOUNT_LAYOUT: borsh.Layout<IdlProgramAccount> = borsh.struct([
+  borsh.publicKey("authority"),
+  borsh.vecU8("data"),
+]);
+
+export function decodeIdlAccount(data: Buffer): IdlProgramAccount {
+  return IDL_ACCOUNT_LAYOUT.decode(data);
+}
+
+export function encodeIdlAccount(acc: IdlProgramAccount): Buffer {
+  const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
+  const len = IDL_ACCOUNT_LAYOUT.encode(acc, buffer);
+  return buffer.slice(0, len);
+}

+ 31 - 1
ts/src/program.ts

@@ -1,7 +1,8 @@
 import { PublicKey } from "@solana/web3.js";
+import { inflate } from "pako";
 import Provider from "./provider";
 import { RpcFactory } from "./rpc";
-import { Idl } from "./idl";
+import { Idl, idlAddress, decodeIdlAccount } from "./idl";
 import Coder from "./coder";
 import { Rpcs, Ixs, Txs, Accounts, State } from "./rpc";
 import { getProvider } from "./";
@@ -78,4 +79,33 @@ export class Program {
     this.coder = coder;
     this.state = state;
   }
+
+  /**
+   * Generates a Program client by fetching the IDL from chain.
+   */
+  public static async at(programId: PublicKey, provider?: Provider) {
+    const idl = await Program.fetchIdl(programId, provider);
+    return new Program(idl, programId, provider);
+  }
+
+  /**
+   * Fetches an idl from the blockchain.
+   */
+  public static async fetchIdl(programId: PublicKey, provider?: Provider) {
+    provider = provider ?? getProvider();
+    const address = await idlAddress(programId);
+    const accountInfo = await provider.connection.getAccountInfo(address);
+    // Chop off account discriminator.
+    let idlAccount = decodeIdlAccount(accountInfo.data.slice(8));
+    const inflatedIdl = inflate(idlAccount.data);
+    return JSON.parse(decodeUtf8(inflatedIdl));
+  }
+}
+
+function decodeUtf8(array: Uint8Array): string {
+  const decoder =
+    typeof TextDecoder === "undefined"
+      ? new (require("util").TextDecoder)("utf-8") // Node.
+      : new TextDecoder("utf-8"); // Browser.
+  return decoder.decode(array);
 }

+ 10 - 0
ts/yarn.lock

@@ -829,6 +829,11 @@
   resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
   integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==
 
+"@types/pako@^1.0.1":
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/@types/pako/-/pako-1.0.1.tgz#33b237f3c9aff44d0f82fe63acffa4a365ef4a61"
+  integrity sha512-GdZbRSJ3Cv5fiwT6I0SQ3ckeN2PWNqxd26W9Z2fCK1tGrrasGy4puvNFtnddqH9UJFMQYXxEuuB7B8UK+LLwSg==
+
 "@types/parse-json@^4.0.0":
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
@@ -4510,6 +4515,11 @@ p-try@^2.0.0:
   resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
   integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
 
+pako@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/pako/-/pako-2.0.3.tgz#cdf475e31b678565251406de9e759196a0ea7a43"
+  integrity sha512-WjR1hOeg+kki3ZIOjaf4b5WVcay1jaliKSYiEaB1XzwhMQZJxRdQRv0V31EKBYlxb4T7SK3hjfc/jxyU64BoSw==
+
 parent-module@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"