Przeglądaj źródła

Changes for a functioning lockup/stake UI (#46)

Armani Ferrante 4 lat temu
rodzic
commit
930aa1d9f6

+ 61 - 11
cli/src/main.rs

@@ -263,12 +263,16 @@ fn test() -> Result<()> {
     // Switch again (todo: restore cwd in `build` command).
     set_workspace_dir_or_exit();
 
-    // Bootup validator.
-    let mut validator_handle = start_test_validator()?;
-
     // Deploy all programs.
     let cfg = Config::discover()?.expect("Inside a workspace").0;
-    let programs = deploy_ws("http://localhost:8899", &cfg.wallet.to_string())?;
+
+    // Bootup validator.
+    let validator_handle = match cfg.cluster.url() {
+        "http://127.0.0.1:8899" => Some(start_test_validator()?),
+        _ => None,
+    };
+
+    let programs = deploy_ws(cfg.cluster.url(), &cfg.wallet.to_string())?;
 
     // Store deployed program addresses in IDL metadata (for consumption by
     // client + tests).
@@ -290,14 +294,19 @@ fn test() -> Result<()> {
         .arg("-t")
         .arg("10000")
         .arg("tests/")
+        .env("ANCHOR_PROVIDER_URL", cfg.cluster.url())
         .stdout(Stdio::inherit())
         .stderr(Stdio::inherit())
         .output()
     {
-        validator_handle.kill()?;
+        if let Some(mut validator_handle) = validator_handle {
+            validator_handle.kill()?;
+        }
         return Err(anyhow::format_err!("{}", e.to_string()));
     }
-    validator_handle.kill()?;
+    if let Some(mut validator_handle) = validator_handle {
+        validator_handle.kill()?;
+    }
 
     Ok(())
 }
@@ -350,18 +359,59 @@ fn start_test_validator() -> Result<Child> {
     Ok(validator_handle)
 }
 
+// TODO: Testing and deploys should use separate sections of metadata.
+//       Similarly, each network should have separate metadata.
 fn deploy(url: Option<String>, keypair: Option<String>) -> Result<()> {
-    let (cfg, ws_path, _) = Config::discover()?.ok_or(anyhow!("Not in Anchor workspace."))?;
-    std::env::set_current_dir(ws_path.parent().unwrap())?;
+    // Build all programs.
+    set_workspace_dir_or_exit();
+    build(None)?;
+    set_workspace_dir_or_exit();
 
+    // Deploy all programs.
+    let cfg = Config::discover()?.expect("Inside a workspace").0;
     let url = url.unwrap_or(cfg.cluster.url().to_string());
     let keypair = keypair.unwrap_or(cfg.wallet.to_string());
-
     let deployment = deploy_ws(&url, &keypair)?;
+
+    // Add metadata to all IDLs.
     for (program, address) in deployment {
-        println!("Deployed {} at {}", program.idl.name, address.to_string());
+        // Add metadata to the IDL.
+        let mut idl = program.idl;
+        idl.metadata = Some(serde_json::to_value(IdlTestMetadata {
+            address: address.to_string(),
+        })?);
+
+        // Persist it.
+        let idl_out = PathBuf::from("target/idl")
+            .join(&idl.name)
+            .with_extension("json");
+        write_idl(&idl, Some(&idl_out))?;
+
+        println!("Deployed {} at {}", idl.name, address.to_string());
     }
 
+    run_hosted_deploy(&url, &keypair)?;
+
+    Ok(())
+}
+
+fn run_hosted_deploy(url: &str, keypair: &str) -> Result<()> {
+    println!("Running deploy script");
+
+    let cur_dir = std::env::current_dir()?;
+    let module_path = format!("{}/migrations/deploy.js", cur_dir.display());
+    let deploy_script_str = template::deploy_script(url, &module_path);
+    std::env::set_current_dir(".anchor")?;
+
+    std::fs::write("deploy.js", deploy_script_str)?;
+    if let Err(e) = std::process::Command::new("node")
+        .arg("deploy.js")
+        .stdout(Stdio::inherit())
+        .stderr(Stdio::inherit())
+        .output()
+    {
+        std::process::exit(1);
+    }
     Ok(())
 }
 
@@ -392,7 +442,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,
 }

+ 29 - 0
cli/src/template.rs

@@ -34,6 +34,35 @@ anchor-lang = {{ git = "https://github.com/project-serum/anchor", features = ["d
     )
 }
 
+pub fn deploy_script(cluster_url: &str, script_path: &str) -> String {
+    format!(
+        r#"
+const serumCmn = require("@project-serum/common");
+const anchor = require('@project-serum/anchor');
+
+// Deploy script defined by the user.
+const userScript = require("{0}");
+
+async function main() {{
+    const url = "{1}";
+    const preflightCommitment = 'recent';
+    const connection = new anchor.web3.Connection(url, preflightCommitment);
+    const wallet = serumCmn.NodeWallet.local();
+
+    const provider = new serumCmn.Provider(connection, wallet, {{
+        preflightCommitment,
+        commitment: 'recent',
+    }});
+
+    // Run the user's deploy script.
+    userScript(provider);
+}}
+main();
+"#,
+        script_path, cluster_url,
+    )
+}
+
 pub fn xargo_toml() -> String {
     r#"[target.bpfel-unknown-unknown.dependencies.std]
 features = []"#

+ 179 - 0
examples/lockup/migrations/deploy.js

@@ -0,0 +1,179 @@
+// deploy.js is a simple deploy script to initialize a program. This is run
+// immediately after a deploy.
+
+const serumCmn = require("@project-serum/common");
+const anchor = require("@project-serum/anchor");
+const PublicKey = anchor.web3.PublicKey;
+
+module.exports = async function (provider) {
+  // Configure client to use the provider.
+  anchor.setProvider(provider);
+
+  // Setup genesis state.
+  const registrarConfigs = await genesis(provider);
+
+  // Program clients.
+  const lockup = anchor.workspace.Lockup;
+  const registry = anchor.workspace.Registry;
+
+  // Registry state constructor.
+  await registry.state.rpc.new({
+    accounts: {
+      lockupProgram: lockup.programId,
+    },
+  });
+
+  // Lockup state constructor.
+  await lockup.state.rpc.new({
+    accounts: {
+      authority: provider.wallet.publicKey,
+    },
+  });
+
+  // Delete the default whitelist entries.
+  const defaultEntry = { programId: new anchor.web3.PublicKey() };
+  await lockup.state.rpc.whitelistDelete(defaultEntry, {
+    accounts: {
+      authority: provider.wallet.publicKey,
+    },
+  });
+
+  // Whitelist the registry.
+  await lockup.state.rpc.whitelistAdd(
+    { programId: registry.programId },
+    {
+      accounts: {
+        authority: provider.wallet.publicKey,
+      },
+    }
+  );
+
+  // Initialize all registrars.
+  const cfgKeys = Object.keys(registrarConfigs);
+  for (let k = 0; k < cfgKeys.length; k += 1) {
+    let r = registrarConfigs[cfgKeys[k]];
+    const registrar = await registrarInit(
+      registry,
+      r.withdrawalTimelock,
+      r.stakeRate,
+      r.rewardQLen,
+      new anchor.web3.PublicKey(r.mint)
+    );
+    r["registrar"] = registrar.toString();
+  }
+
+  // Generate code for whitelisting on UIs.
+  const code = generateCode(registry, lockup, registrarConfigs);
+  console.log("Generated whitelisted UI addresses:", code);
+};
+
+function generateCode(registry, lockup, registrarConfigs) {
+  const registrars = Object.keys(registrarConfigs)
+    .map((cfg) => `${cfg}: new PublicKey('${registrarConfigs[cfg].registrar}')`)
+    .join(",");
+
+  const mints = Object.keys(registrarConfigs)
+    .map((cfg) => `${cfg}: new PublicKey('${registrarConfigs[cfg].mint}')`)
+    .join(",");
+
+  return `{
+registryProgramId: new PublicKey('${registry.programId}'),
+lockupProgramId: new PublicKey('${lockup.programId}'),
+registrars: { ${registrars} },
+mints: { ${mints} },
+ }`;
+}
+
+async function genesis(provider) {
+  if (
+    provider.connection._rpcEndpoint === "https://api.mainnet-beta.solana.com"
+  ) {
+    return {
+      srm: {
+        withdrawalTimelock: 60,
+        stakeRate: 1000 * 10 ** 6,
+        rewardQLen: 100,
+        mint: "SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt",
+      },
+      msrm: {
+        withdrawalTimelock: 45,
+        stakeRate: 1,
+        rewardQLen: 100,
+        mint: "MSRMcoVyrFxnSgo5uXwone5SKcGhT1KEJMFEkMEWf9L",
+      },
+    };
+  } else {
+    const [token1Mint, _god1] = await serumCmn.createMintAndVault(
+      provider,
+      new anchor.BN(10000000000000),
+      undefined,
+      6
+    );
+    const [token2Mint, _god2] = await serumCmn.createMintAndVault(
+      provider,
+      new anchor.BN(10000000000),
+      undefined,
+      0
+    );
+    return {
+      token1: {
+        withdrawalTimelock: 60,
+        stakeRate: 2 * 10 ** 6,
+        rewardQLen: 100,
+        mint: token1Mint.toString(),
+      },
+      token2: {
+        withdrawalTimelock: 45,
+        stakeRate: 1,
+        rewardQLen: 100,
+        mint: token2Mint.toString(),
+      },
+    };
+  }
+}
+
+async function registrarInit(
+  registry,
+  _withdrawalTimelock,
+  _stakeRate,
+  rewardQLen,
+  mint
+) {
+  const registrar = new anchor.web3.Account();
+  const rewardQ = new anchor.web3.Account();
+  const withdrawalTimelock = new anchor.BN(_withdrawalTimelock);
+  const stakeRate = new anchor.BN(_stakeRate);
+  const [
+    registrarSigner,
+    nonce,
+  ] = await anchor.web3.PublicKey.findProgramAddress(
+    [registrar.publicKey.toBuffer()],
+    registry.programId
+  );
+  const poolMint = await serumCmn.createMint(
+    registry.provider,
+    registrarSigner
+  );
+  await registry.rpc.initialize(
+    mint,
+    registry.provider.wallet.publicKey,
+    nonce,
+    withdrawalTimelock,
+    stakeRate,
+    rewardQLen,
+    {
+      accounts: {
+        registrar: registrar.publicKey,
+        poolMint,
+        rewardEventQ: rewardQ.publicKey,
+        rent: anchor.web3.SYSVAR_RENT_PUBKEY,
+      },
+      signers: [registrar, rewardQ],
+      instructions: [
+        await registry.account.registrar.createInstruction(registrar),
+        await registry.account.rewardQueue.createInstruction(rewardQ, 8250),
+      ],
+    }
+  );
+  return registrar.publicKey;
+}

+ 8 - 6
examples/lockup/programs/lockup/src/lib.rs

@@ -131,8 +131,8 @@ pub mod lockup {
     }
 
     // Sends funds from the lockup program to a whitelisted program.
-    pub fn whitelist_withdraw(
-        ctx: Context<WhitelistWithdraw>,
+    pub fn whitelist_withdraw<'a, 'b, 'c, 'info>(
+        ctx: Context<'a, 'b, 'c, 'info, WhitelistWithdraw<'info>>,
         instruction_data: Vec<u8>,
         amount: u64,
     ) -> Result<()> {
@@ -157,8 +157,8 @@ pub mod lockup {
     }
 
     // Sends funds from a whitelisted program back to the lockup program.
-    pub fn whitelist_deposit(
-        ctx: Context<WhitelistDeposit>,
+    pub fn whitelist_deposit<'a, 'b, 'c, 'info>(
+        ctx: Context<'a, 'b, 'c, 'info, WhitelistDeposit<'info>>,
         instruction_data: Vec<u8>,
     ) -> Result<()> {
         let before_amount = ctx.accounts.transfer.vault.amount;
@@ -393,7 +393,7 @@ impl<'a, 'b, 'c, 'info> From<&Withdraw<'info>> for CpiContext<'a, 'b, 'c, 'info,
 
 #[access_control(is_whitelisted(transfer))]
 pub fn whitelist_relay_cpi<'info>(
-    transfer: &WhitelistTransfer,
+    transfer: &WhitelistTransfer<'info>,
     remaining_accounts: &[AccountInfo<'info>],
     instruction_data: Vec<u8>,
 ) -> Result<()> {
@@ -432,7 +432,9 @@ pub fn whitelist_relay_cpi<'info>(
         &[transfer.vesting.nonce],
     ];
     let signer = &[&seeds[..]];
-    solana_program::program::invoke_signed(&relay_instruction, &transfer.to_account_infos(), signer)
+    let mut accounts = transfer.to_account_infos();
+    accounts.extend_from_slice(&remaining_accounts);
+    solana_program::program::invoke_signed(&relay_instruction, &accounts, signer)
         .map_err(Into::into)
 }
 

+ 13 - 0
examples/lockup/programs/registry/src/lib.rs

@@ -160,6 +160,10 @@ mod registry {
             token::mint_to(cpi_ctx, spt_amount)?;
         }
 
+        // Update stake timestamp.
+        let member = &mut ctx.accounts.member;
+        member.last_stake_ts = ctx.accounts.clock.unix_timestamp;
+
         Ok(())
     }
 
@@ -231,6 +235,10 @@ mod registry {
         pending_withdrawal.registrar = *ctx.accounts.registrar.to_account_info().key;
         pending_withdrawal.locked = locked;
 
+        // Update stake timestamp.
+        let member = &mut ctx.accounts.member;
+        member.last_stake_ts = ctx.accounts.clock.unix_timestamp;
+
         Ok(())
     }
 
@@ -349,12 +357,14 @@ mod registry {
         let vendor = &mut ctx.accounts.vendor;
         vendor.registrar = *ctx.accounts.registrar.to_account_info().key;
         vendor.vault = *ctx.accounts.vendor_vault.to_account_info().key;
+        vendor.mint = ctx.accounts.vendor_vault.mint;
         vendor.nonce = nonce;
         vendor.pool_token_supply = ctx.accounts.pool_mint.supply;
         vendor.reward_event_q_cursor = cursor;
         vendor.start_ts = ctx.accounts.clock.unix_timestamp;
         vendor.expiry_ts = expiry_ts;
         vendor.expiry_receiver = expiry_receiver;
+        vendor.from = *ctx.accounts.depositor_authority.key;
         vendor.total = total;
         vendor.expired = false;
         vendor.kind = kind.clone();
@@ -497,6 +507,7 @@ pub struct Initialize<'info> {
     registrar: ProgramAccount<'info, Registrar>,
     #[account(init)]
     reward_event_q: ProgramAccount<'info, RewardQueue>,
+    #[account("pool_mint.decimals == 0")]
     pool_mint: CpiAccount<'info, Mint>,
     rent: Sysvar<'info, Rent>,
 }
@@ -1096,12 +1107,14 @@ pub struct RewardEvent {
 pub struct RewardVendor {
     pub registrar: Pubkey,
     pub vault: Pubkey,
+    pub mint: Pubkey,
     pub nonce: u8,
     pub pool_token_supply: u64,
     pub reward_event_q_cursor: u32,
     pub start_ts: i64,
     pub expiry_ts: i64,
     pub expiry_receiver: Pubkey,
+    pub from: Pubkey,
     pub total: u64,
     pub expired: bool,
     pub kind: RewardVendorKind,

+ 3 - 2
examples/lockup/tests/lockup.js

@@ -5,9 +5,10 @@ const TokenInstructions = require("@project-serum/serum").TokenInstructions;
 const utils = require("./utils");
 
 describe("Lockup and Registry", () => {
-  const provider = anchor.Provider.local();
+  // Read the provider from the configured environmnet.
+  const provider = anchor.Provider.env();
 
-  // Configure the client to use the local cluster.
+  // Configure the client to use the provider.
   anchor.setProvider(provider);
 
   const lockup = anchor.workspace.Lockup;

+ 6 - 4
ts/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@project-serum/anchor",
-  "version": "0.0.0-alpha.7",
+  "version": "0.0.0-alpha.8",
   "description": "Anchor client",
   "main": "dist/cjs/index.js",
   "module": "dist/esm/index.js",
@@ -15,6 +15,7 @@
   "scripts": {
     "build": "yarn build:node",
     "build:node": "tsc && tsc -p tsconfig.cjs.json",
+    "lint:fix": "prettier src/** -w",
     "watch": "tsc -p tsconfig.cjs.json --watch",
     "test:unit": "jest test/unit",
     "test:integration": "jest test/integration --detectOpenHandles",
@@ -23,14 +24,15 @@
   },
   "dependencies": {
     "@project-serum/borsh": "^0.0.1-beta.0",
-    "@project-serum/common": "^0.0.1-beta.0",
-    "@project-serum/lockup": "^0.0.1-beta.0",
-    "@solana/spl-token": "0.0.11",
+    "@solana/web3.js": "^0.90.4",
     "@types/bn.js": "^4.11.6",
+    "@types/bs58": "^4.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"
   },
   "devDependencies": {

+ 133 - 8
ts/src/coder.ts

@@ -1,5 +1,6 @@
 import camelCase from "camelcase";
 import { Layout } from "buffer-layout";
+import { sha256 } from "crypto-hash";
 import * as borsh from "@project-serum/borsh";
 import {
   Idl,
@@ -11,6 +12,11 @@ import {
 } from "./idl";
 import { IdlError } from "./error";
 
+/**
+ * Number of bytes of the account discriminator.
+ */
+export const ACCOUNT_DISCRIMINATOR_SIZE = 8;
+
 /**
  * Coder provides a facade for encoding and decoding all IDL related objects.
  */
@@ -105,24 +111,30 @@ class AccountsCoder {
       this.accountLayouts = new Map();
       return;
     }
-    const layouts = idl.accounts.map((acc) => {
+    const layouts: [string, Layout][] = idl.accounts.map((acc) => {
       return [acc.name, IdlCoder.typeDefLayout(acc, idl.types)];
     });
 
-    // @ts-ignore
     this.accountLayouts = new Map(layouts);
   }
 
-  public encode<T = any>(accountName: string, account: T): Buffer {
+  public async encode<T = any>(
+    accountName: string,
+    account: T
+  ): Promise<Buffer> {
     const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
     const layout = this.accountLayouts.get(accountName);
     const len = layout.encode(account, buffer);
-    return buffer.slice(0, len);
+    let accountData = buffer.slice(0, len);
+    let discriminator = await accountDiscriminator(accountName);
+    return Buffer.concat([discriminator, accountData]);
   }
 
   public decode<T = any>(accountName: string, ix: Buffer): T {
+    // Chop off the discriminator before decoding.
+    const data = ix.slice(8);
     const layout = this.accountLayouts.get(accountName);
-    return layout.decode(ix);
+    return layout.decode(data);
   }
 }
 
@@ -171,14 +183,20 @@ class StateCoder {
     this.layout = IdlCoder.typeDefLayout(idl.state.struct, idl.types);
   }
 
-  public encode<T = any>(account: T): Buffer {
+  public async encode<T = any>(name: string, account: T): Promise<Buffer> {
     const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
     const len = this.layout.encode(account, buffer);
-    return buffer.slice(0, len);
+
+    const disc = await stateDiscriminator(name);
+    const accData = buffer.slice(0, len);
+
+    return Buffer.concat([disc, accData]);
   }
 
   public decode<T = any>(ix: Buffer): T {
-    return this.layout.decode(ix);
+    // Chop off discriminator.
+    const data = ix.slice(8);
+    return this.layout.decode(data);
   }
 }
 
@@ -299,3 +317,110 @@ class IdlCoder {
     }
   }
 }
+
+// Calculates unique 8 byte discriminator prepended to all anchor accounts.
+export async function accountDiscriminator(name: string): Promise<Buffer> {
+  return Buffer.from(
+    (
+      await sha256(`account:${name}`, {
+        outputFormat: "buffer",
+      })
+    ).slice(0, 8)
+  );
+}
+
+// Calculates unique 8 byte discriminator prepended to all anchor state accounts.
+export async function stateDiscriminator(name: string): Promise<Buffer> {
+  return Buffer.from(
+    (
+      await sha256(`state:${name}`, {
+        outputFormat: "buffer",
+      })
+    ).slice(0, 8)
+  );
+}
+
+// Returns the size of the type in bytes. For variable length types, just return
+// 1. Users should override this value in such cases.
+export function typeSize(idl: Idl, ty: IdlType): number {
+  switch (ty) {
+    case "bool":
+      return 1;
+    case "u8":
+      return 1;
+    case "i8":
+      return 1;
+    case "u16":
+      return 2;
+    case "u32":
+      return 4;
+    case "u64":
+      return 8;
+    case "i64":
+      return 8;
+    case "bytes":
+      return 1;
+    case "string":
+      return 1;
+    case "publicKey":
+      return 32;
+    default:
+      // @ts-ignore
+      if (ty.vec !== undefined) {
+        return 1;
+      }
+      // @ts-ignore
+      if (ty.option !== undefined) {
+        // @ts-ignore
+        return 1 + typeSize(ty.option);
+      }
+      // @ts-ignore
+      if (ty.defined !== undefined) {
+        // @ts-ignore
+        const filtered = idl.types.filter((t) => t.name === ty.defined);
+        if (filtered.length !== 1) {
+          throw new IdlError(`Type not found: ${JSON.stringify(ty)}`);
+        }
+        let typeDef = filtered[0];
+
+        return accountSize(idl, typeDef);
+      }
+      throw new Error(`Invalid type ${JSON.stringify(ty)}`);
+  }
+}
+
+export function accountSize(
+  idl: Idl,
+  idlAccount: IdlTypeDef
+): number | undefined {
+  if (idlAccount.type.kind === "enum") {
+    let variantSizes = idlAccount.type.variants.map(
+      (variant: IdlEnumVariant) => {
+        if (variant.fields === undefined) {
+          return 0;
+        }
+        // @ts-ignore
+        return (
+          variant.fields
+            // @ts-ignore
+            .map((f: IdlField | IdlType) => {
+              // @ts-ignore
+              if (f.name === undefined) {
+                throw new Error("Tuple enum variants not yet implemented.");
+              }
+              // @ts-ignore
+              return typeSize(idl, f.type);
+            })
+            .reduce((a: number, b: number) => a + b)
+        );
+      }
+    );
+    return Math.max(...variantSizes) + 1;
+  }
+  if (idlAccount.type.fields === undefined) {
+    return 0;
+  }
+  return idlAccount.type.fields
+    .map((f) => typeSize(idl, f.type))
+    .reduce((a, b) => a + b);
+}

+ 11 - 1
ts/src/index.ts

@@ -1,9 +1,12 @@
 import BN from "bn.js";
 import * as web3 from "@solana/web3.js";
-import { Provider } from "@project-serum/common";
+import Provider, { NodeWallet as Wallet } from "./provider";
 import { Program } from "./program";
 import Coder from "./coder";
+import { Idl } from "./idl";
 import workspace from "./workspace";
+import utils from "./utils";
+import { ProgramAccount } from "./rpc";
 
 let _provider: Provider | null = null;
 
@@ -12,16 +15,23 @@ function setProvider(provider: Provider) {
 }
 
 function getProvider(): Provider {
+  if (_provider === null) {
+    return Provider.local();
+  }
   return _provider;
 }
 
 export {
   workspace,
   Program,
+  ProgramAccount,
   Coder,
   setProvider,
   getProvider,
   Provider,
   BN,
   web3,
+  Idl,
+  utils,
+  Wallet,
 };

+ 11 - 2
ts/src/program.ts

@@ -1,8 +1,10 @@
 import { PublicKey } from "@solana/web3.js";
+import Provider from "./provider";
 import { RpcFactory } from "./rpc";
 import { Idl } from "./idl";
 import Coder from "./coder";
 import { Rpcs, Ixs, Txs, Accounts, State } from "./rpc";
+import { getProvider } from "./";
 
 /**
  * Program is the IDL deserialized representation of a Solana program.
@@ -49,9 +51,15 @@ export class Program {
    */
   readonly state: State;
 
-  public constructor(idl: Idl, programId: PublicKey) {
+  /**
+   * Wallet and network provider.
+   */
+  readonly provider: Provider;
+
+  public constructor(idl: Idl, programId: PublicKey, provider?: Provider) {
     this.idl = idl;
     this.programId = programId;
+    this.provider = provider ?? getProvider();
 
     // Build the serializer.
     const coder = new Coder(idl);
@@ -60,7 +68,8 @@ export class Program {
     const [rpcs, ixs, txs, accounts, state] = RpcFactory.build(
       idl,
       coder,
-      programId
+      programId,
+      this.provider
     );
     this.rpc = rpcs;
     this.instruction = ixs;

+ 184 - 0
ts/src/provider.ts

@@ -0,0 +1,184 @@
+import {
+  Connection,
+  Account,
+  PublicKey,
+  Transaction,
+  TransactionSignature,
+  ConfirmOptions,
+  sendAndConfirmRawTransaction,
+} from "@solana/web3.js";
+
+export default class Provider {
+  constructor(
+    readonly connection: Connection,
+    readonly wallet: Wallet,
+    readonly opts: ConfirmOptions
+  ) {}
+
+  static defaultOptions(): ConfirmOptions {
+    return {
+      preflightCommitment: "recent",
+      commitment: "recent",
+    };
+  }
+
+  // Node only api.
+  static local(url?: string, opts?: ConfirmOptions): Provider {
+    opts = opts || Provider.defaultOptions();
+    const connection = new Connection(
+      url || "http://localhost:8899",
+      opts.preflightCommitment
+    );
+    const wallet = NodeWallet.local();
+    return new Provider(connection, wallet, opts);
+  }
+
+  // Node only api.
+  static env(): Provider {
+    const process = require("process");
+    const url = process.env.ANCHOR_PROVIDER_URL;
+    if (url === undefined) {
+      throw new Error("ANCHOR_PROVIDER_URL is not defined");
+    }
+    const options = Provider.defaultOptions();
+    const connection = new Connection(url, options.commitment);
+    const wallet = NodeWallet.local();
+
+    return new Provider(connection, wallet, options);
+  }
+
+  async send(
+    tx: Transaction,
+    signers?: Array<Account | undefined>,
+    opts?: ConfirmOptions
+  ): Promise<TransactionSignature> {
+    if (signers === undefined) {
+      signers = [];
+    }
+    if (opts === undefined) {
+      opts = this.opts;
+    }
+
+    const signerKps = signers.filter((s) => s !== undefined) as Array<Account>;
+    const signerPubkeys = [this.wallet.publicKey].concat(
+      signerKps.map((s) => s.publicKey)
+    );
+
+    tx.setSigners(...signerPubkeys);
+    tx.recentBlockhash = (
+      await this.connection.getRecentBlockhash(opts.preflightCommitment)
+    ).blockhash;
+
+    await this.wallet.signTransaction(tx);
+    signerKps.forEach((kp) => {
+      tx.partialSign(kp);
+    });
+
+    const rawTx = tx.serialize();
+
+    const txId = await sendAndConfirmRawTransaction(
+      this.connection,
+      rawTx,
+      opts
+    );
+
+    return txId;
+  }
+
+  async sendAll(
+    reqs: Array<SendTxRequest>,
+    opts?: ConfirmOptions
+  ): Promise<Array<TransactionSignature>> {
+    if (opts === undefined) {
+      opts = this.opts;
+    }
+    const blockhash = await this.connection.getRecentBlockhash(
+      opts.preflightCommitment
+    );
+
+    let txs = reqs.map((r) => {
+      let tx = r.tx;
+      let signers = r.signers;
+
+      if (signers === undefined) {
+        signers = [];
+      }
+
+      const signerKps = signers.filter(
+        (s) => s !== undefined
+      ) as Array<Account>;
+      const signerPubkeys = [this.wallet.publicKey].concat(
+        signerKps.map((s) => s.publicKey)
+      );
+
+      tx.setSigners(...signerPubkeys);
+      tx.recentBlockhash = blockhash.blockhash;
+      signerKps.forEach((kp) => {
+        tx.partialSign(kp);
+      });
+
+      return tx;
+    });
+
+    const signedTxs = await this.wallet.signAllTransactions(txs);
+
+    const sigs = [];
+
+    for (let k = 0; k < txs.length; k += 1) {
+      const tx = signedTxs[k];
+      const rawTx = tx.serialize();
+      sigs.push(
+        await sendAndConfirmRawTransaction(this.connection, rawTx, opts)
+      );
+    }
+
+    return sigs;
+  }
+}
+
+export type SendTxRequest = {
+  tx: Transaction;
+  signers: Array<Account | undefined>;
+};
+
+export interface Wallet {
+  signTransaction(tx: Transaction): Promise<Transaction>;
+  signAllTransactions(txs: Transaction[]): Promise<Transaction[]>;
+  publicKey: PublicKey;
+}
+
+export class NodeWallet implements Wallet {
+  constructor(readonly payer: Account) {}
+
+  static local(): NodeWallet {
+    const payer = new Account(
+      Buffer.from(
+        JSON.parse(
+          require("fs").readFileSync(
+            require("os").homedir() + "/.config/solana/id.json",
+            {
+              encoding: "utf-8",
+            }
+          )
+        )
+      )
+    );
+    return new NodeWallet(payer);
+  }
+
+  async signTransaction(tx: Transaction): Promise<Transaction> {
+    tx.partialSign(this.payer);
+    return tx;
+  }
+
+  async signAllTransactions(txs: Transaction[]): Promise<Transaction[]> {
+    return txs.map((t) => {
+      t.partialSign(this.payer);
+      return t;
+    });
+  }
+
+  get publicKey(): PublicKey {
+    return this.payer.publicKey;
+  }
+}

+ 309 - 244
ts/src/rpc.ts

@@ -1,4 +1,6 @@
 import camelCase from "camelcase";
+import EventEmitter from "eventemitter3";
+import * as bs58 from "bs58";
 import {
   Account,
   AccountMeta,
@@ -9,42 +11,41 @@ import {
   TransactionSignature,
   TransactionInstruction,
   SYSVAR_RENT_PUBKEY,
+  Commitment,
 } from "@solana/web3.js";
-import { sha256 } from "crypto-hash";
+import Provider from "./provider";
 import {
   Idl,
   IdlAccount,
   IdlInstruction,
-  IdlTypeDef,
-  IdlType,
-  IdlField,
-  IdlEnumVariant,
   IdlAccountItem,
   IdlStateMethod,
 } from "./idl";
 import { IdlError, ProgramError } from "./error";
-import Coder from "./coder";
-import { getProvider } from "./";
+import Coder, {
+  ACCOUNT_DISCRIMINATOR_SIZE,
+  accountDiscriminator,
+  stateDiscriminator,
+  accountSize,
+} from "./coder";
 
 /**
- * Number of bytes of the account discriminator.
- */
-const ACCOUNT_DISCRIMINATOR_SIZE = 8;
-
-/**
- * Rpcs is a dynamically generated object with rpc methods attached.
+ * Dynamically generated rpc namespace.
  */
 export interface Rpcs {
   [key: string]: RpcFn;
 }
 
 /**
- * Ixs is a dynamically generated object with ix functions attached.
+ * Dynamically generated instruction namespace.
  */
 export interface Ixs {
   [key: string]: IxFn;
 }
 
+/**
+ * Dynamically generated transaction namespace.
+ */
 export interface Txs {
   [key: string]: TxFn;
 }
@@ -65,7 +66,11 @@ export type RpcFn = (...args: any[]) => Promise<TransactionSignature>;
 /**
  * Ix is a function to create a `TransactionInstruction` generated from an IDL.
  */
-export type IxFn = (...args: any[]) => TransactionInstruction;
+export type IxFn = IxProps & ((...args: any[]) => TransactionInstruction);
+
+type IxProps = {
+  accounts: (ctx: RpcAccounts) => any;
+};
 
 /**
  * Tx is a function to create a `Transaction` generate from an IDL.
@@ -75,7 +80,26 @@ export type TxFn = (...args: any[]) => Transaction;
 /**
  * Account is a function returning a deserialized account, given an address.
  */
-export type AccountFn<T = any> = (address: PublicKey) => T;
+export type AccountFn<T = any> = AccountProps & ((address: PublicKey) => T);
+
+/**
+ * Deserialized account owned by a program.
+ */
+export type ProgramAccount<T = any> = {
+  publicKey: PublicKey;
+  account: T;
+};
+
+/**
+ * Non function properties on the acccount namespace.
+ */
+type AccountProps = {
+  size: number;
+  all: (filter?: Buffer) => Promise<ProgramAccount<any>[]>;
+  subscribe: (address: PublicKey, commitment?: Commitment) => EventEmitter;
+  unsubscribe: (address: PublicKey) => void;
+  createInstruction: (account: Account) => Promise<TransactionInstruction>;
+};
 
 /**
  * Options for an RPC invocation.
@@ -112,6 +136,9 @@ export type State = {
   rpc: Rpcs;
 };
 
+// Tracks all subscriptions.
+const subscriptions: Map<string, Subscription> = new Map();
+
 /**
  * RpcFactory builds an Rpcs object for a given IDL.
  */
@@ -124,177 +151,150 @@ export class RpcFactory {
   public static build(
     idl: Idl,
     coder: Coder,
-    programId: PublicKey
+    programId: PublicKey,
+    provider: Provider
   ): [Rpcs, Ixs, Txs, Accounts, State] {
     const idlErrors = parseIdlErrors(idl);
 
     const rpcs: Rpcs = {};
     const ixFns: Ixs = {};
     const txFns: Txs = {};
-    const state = RpcFactory.buildState(idl, coder, programId, idlErrors);
+    const state = RpcFactory.buildState(
+      idl,
+      coder,
+      programId,
+      idlErrors,
+      provider
+    );
 
     idl.instructions.forEach((idlIx) => {
+      const name = camelCase(idlIx.name);
       // Function to create a raw `TransactionInstruction`.
       const ix = RpcFactory.buildIx(idlIx, coder, programId);
       // Ffnction to create a `Transaction`.
       const tx = RpcFactory.buildTx(idlIx, ix);
       // Function to invoke an RPC against a cluster.
-      const rpc = RpcFactory.buildRpc(idlIx, tx, idlErrors);
-
-      const name = camelCase(idlIx.name);
+      const rpc = RpcFactory.buildRpc(idlIx, tx, idlErrors, provider);
       rpcs[name] = rpc;
       ixFns[name] = ix;
       txFns[name] = tx;
     });
 
     const accountFns = idl.accounts
-      ? RpcFactory.buildAccounts(idl, coder, programId)
+      ? RpcFactory.buildAccounts(idl, coder, programId, provider)
       : {};
 
     return [rpcs, ixFns, txFns, accountFns, state];
   }
 
+  // Builds the state namespace.
   private static buildState(
     idl: Idl,
     coder: Coder,
     programId: PublicKey,
-    idlErrors: Map<number, string>
+    idlErrors: Map<number, string>,
+    provider: Provider
   ): State | undefined {
     if (idl.state === undefined) {
       return undefined;
     }
-    let address = async () => {
-      let [registrySigner, _nonce] = await PublicKey.findProgramAddress(
-        [],
-        programId
+
+    // Fetches the state object from the blockchain.
+    const state = async (): Promise<any> => {
+      const addr = await programStateAddress(programId);
+      const accountInfo = await provider.connection.getAccountInfo(addr);
+      if (accountInfo === null) {
+        throw new Error(`Account does not exist ${addr.toString()}`);
+      }
+      // Assert the account discriminator is correct.
+      const expectedDiscriminator = await stateDiscriminator(
+        idl.state.struct.name
       );
-      return PublicKey.createWithSeed(registrySigner, "unversioned", programId);
+      if (expectedDiscriminator.compare(accountInfo.data.slice(0, 8))) {
+        throw new Error("Invalid account discriminator");
+      }
+      return coder.state.decode(accountInfo.data);
     };
 
+    // Namespace with all rpc functions.
     const rpc: Rpcs = {};
     idl.state.methods.forEach((m: IdlStateMethod) => {
-      if (m.name === "new") {
-        // Ctor `new` method.
-        rpc[m.name] = async (...args: any[]): Promise<TransactionSignature> => {
-          const [ixArgs, ctx] = splitArgsAndCtx(m, [...args]);
-          const tx = new Transaction();
-          const [programSigner, _nonce] = await PublicKey.findProgramAddress(
-            [],
-            programId
-          );
-          const ix = new TransactionInstruction({
-            keys: [
-              {
-                pubkey: getProvider().wallet.publicKey,
-                isWritable: false,
-                isSigner: true,
-              },
-              { pubkey: await address(), isWritable: true, isSigner: false },
-              { pubkey: programSigner, isWritable: false, isSigner: false },
-              {
-                pubkey: SystemProgram.programId,
-                isWritable: false,
-                isSigner: false,
-              },
-
-              { pubkey: programId, isWritable: false, isSigner: false },
-              {
-                pubkey: SYSVAR_RENT_PUBKEY,
-                isWritable: false,
-                isSigner: false,
-              },
-            ].concat(RpcFactory.accountsArray(ctx.accounts, m.accounts)),
+      rpc[m.name] = async (...args: any[]): Promise<TransactionSignature> => {
+        const [ixArgs, ctx] = splitArgsAndCtx(m, [...args]);
+        const keys = await stateInstructionKeys(programId, provider, m, ctx);
+        const tx = new Transaction();
+        tx.add(
+          new TransactionInstruction({
+            keys: keys.concat(
+              RpcFactory.accountsArray(ctx.accounts, m.accounts)
+            ),
             programId,
             data: coder.instruction.encode(toInstruction(m, ...ixArgs)),
-          });
+          })
+        );
+        try {
+          const txSig = await provider.send(tx, ctx.signers, ctx.options);
+          return txSig;
+        } catch (err) {
+          let translatedErr = translateError(idlErrors, err);
+          if (translatedErr === null) {
+            throw err;
+          }
+          throw translatedErr;
+        }
+      };
+    });
+    state["rpc"] = rpc;
 
-          tx.add(ix);
+    // Calculates the address of the program's global state object account.
+    state["address"] = async (): Promise<PublicKey> =>
+      programStateAddress(programId);
 
-          const provider = getProvider();
-          if (provider === null) {
-            throw new Error("Provider not found");
-          }
-          try {
-            const txSig = await provider.send(tx, ctx.signers, ctx.options);
-            return txSig;
-          } catch (err) {
-            let translatedErr = translateError(idlErrors, err);
-            if (translatedErr === null) {
-              throw err;
-            }
-            throw translatedErr;
-          }
-        };
-      } else {
-        rpc[m.name] = async (...args: any[]): Promise<TransactionSignature> => {
-          const [ixArgs, ctx] = splitArgsAndCtx(m, [...args]);
-          validateAccounts(m.accounts, ctx.accounts);
-          const tx = new Transaction();
-
-          const keys = [
-            { pubkey: await address(), isWritable: true, isSigner: false },
-          ].concat(RpcFactory.accountsArray(ctx.accounts, m.accounts));
-
-          tx.add(
-            new TransactionInstruction({
-              keys,
-              programId,
-              data: coder.instruction.encode(toInstruction(m, ...ixArgs)),
-            })
-          );
+    // Subscription singleton.
+    let sub: null | Subscription = null;
 
-          const provider = getProvider();
-          if (provider === null) {
-            throw new Error("Provider not found");
-          }
-          try {
-            const txSig = await provider.send(tx, ctx.signers, ctx.options);
-            return txSig;
-          } catch (err) {
-            let translatedErr = translateError(idlErrors, err);
-            if (translatedErr === null) {
-              throw err;
-            }
-            throw translatedErr;
-          }
-        };
+    // Subscribe to account changes.
+    state["subscribe"] = (commitment?: Commitment): EventEmitter => {
+      if (sub !== null) {
+        return sub.ee;
       }
-    });
+      const ee = new EventEmitter();
+
+      state["address"]().then((address) => {
+        const listener = provider.connection.onAccountChange(
+          address,
+          (acc) => {
+            const account = coder.state.decode(acc.data);
+            ee.emit("change", account);
+          },
+          commitment
+        );
 
-    // Fetches the state object from the blockchain.
-    const state = async (): Promise<any> => {
-      const addr = await address();
-      const provider = getProvider();
-      if (provider === null) {
-        throw new Error("Provider not set");
-      }
-      const accountInfo = await provider.connection.getAccountInfo(addr);
-      if (accountInfo === null) {
-        throw new Error(`Entity does not exist ${address}`);
-      }
-      // Assert the account discriminator is correct.
-      const expectedDiscriminator = Buffer.from(
-        (
-          await sha256(`state:${idl.state.struct.name}`, {
-            outputFormat: "buffer",
+        sub = {
+          ee,
+          listener,
+        };
+      });
+
+      return ee;
+    };
+
+    // Unsubscribe from account changes.
+    state["unsubscribe"] = () => {
+      if (sub !== null) {
+        provider.connection
+          .removeAccountChangeListener(sub.listener)
+          .then(async () => {
+            sub = null;
           })
-        ).slice(0, 8)
-      );
-      const discriminator = accountInfo.data.slice(0, 8);
-      if (expectedDiscriminator.compare(discriminator)) {
-        throw new Error("Invalid account discriminator");
+          .catch(console.error);
       }
-      // Chop off the discriminator before decoding.
-      const data = accountInfo.data.slice(8);
-      return coder.state.decode(data);
     };
 
-    state["address"] = address;
-    state["rpc"] = rpc;
-
     return state;
   }
 
+  // Builds the instuction namespace.
   private static buildIx(
     idlIx: IdlInstruction,
     coder: Coder,
@@ -357,22 +357,21 @@ export class RpcFactory {
       .flat();
   }
 
+  // Builds the rpc namespace.
   private static buildRpc(
     idlIx: IdlInstruction,
     txFn: TxFn,
-    idlErrors: Map<number, string>
+    idlErrors: Map<number, string>,
+    provider: Provider
   ): RpcFn {
     const rpc = async (...args: any[]): Promise<TransactionSignature> => {
       const tx = txFn(...args);
       const [_, ctx] = splitArgsAndCtx(idlIx, [...args]);
-      const provider = getProvider();
-      if (provider === null) {
-        throw new Error("Provider not found");
-      }
       try {
         const txSig = await provider.send(tx, ctx.signers, ctx.options);
         return txSig;
       } catch (err) {
+        console.log("Translating error", err);
         let translatedErr = translateError(idlErrors, err);
         if (translatedErr === null) {
           throw err;
@@ -384,6 +383,7 @@ export class RpcFactory {
     return rpc;
   }
 
+  // Builds the transaction namespace.
   private static buildTx(idlIx: IdlInstruction, ixFn: IxFn): TxFn {
     const txFn = (...args: any[]): Transaction => {
       const [_, ctx] = splitArgsAndCtx(idlIx, [...args]);
@@ -398,52 +398,48 @@ export class RpcFactory {
     return txFn;
   }
 
+  // Returns the generated accounts namespace.
   private static buildAccounts(
     idl: Idl,
     coder: Coder,
-    programId: PublicKey
+    programId: PublicKey,
+    provider: Provider
   ): Accounts {
     const accountFns: Accounts = {};
+
     idl.accounts.forEach((idlAccount) => {
-      const accountFn = async (address: PublicKey): Promise<any> => {
-        const provider = getProvider();
-        if (provider === null) {
-          throw new Error("Provider not set");
-        }
+      const name = camelCase(idlAccount.name);
+
+      // Fetches the decoded account from the network.
+      const accountsNamespace = async (address: PublicKey): Promise<any> => {
         const accountInfo = await provider.connection.getAccountInfo(address);
         if (accountInfo === null) {
-          throw new Error(`Entity does not exist ${address}`);
+          throw new Error(`Account does not exist ${address.toString()}`);
         }
 
         // Assert the account discriminator is correct.
-        const expectedDiscriminator = Buffer.from(
-          (
-            await sha256(`account:${idlAccount.name}`, {
-              outputFormat: "buffer",
-            })
-          ).slice(0, 8)
-        );
-        const discriminator = accountInfo.data.slice(0, 8);
-
-        if (expectedDiscriminator.compare(discriminator)) {
+        const discriminator = await accountDiscriminator(idlAccount.name);
+        if (discriminator.compare(accountInfo.data.slice(0, 8))) {
           throw new Error("Invalid account discriminator");
         }
 
-        // Chop off the discriminator before decoding.
-        const data = accountInfo.data.slice(8);
-        return coder.accounts.decode(idlAccount.name, data);
+        return coder.accounts.decode(idlAccount.name, accountInfo.data);
       };
-      const name = camelCase(idlAccount.name);
-      accountFns[name] = accountFn;
-      const size = ACCOUNT_DISCRIMINATOR_SIZE + accountSize(idl, idlAccount);
+
+      // Returns the size of the account.
       // @ts-ignore
-      accountFns[name]["size"] = size;
+      accountsNamespace["size"] =
+        ACCOUNT_DISCRIMINATOR_SIZE + accountSize(idl, idlAccount);
+
+      // Returns an instruction for creating this account.
       // @ts-ignore
-      accountFns[name]["createInstruction"] = async (
+      accountsNamespace["createInstruction"] = async (
         account: Account,
         sizeOverride?: number
       ): Promise<TransactionInstruction> => {
-        const provider = getProvider();
+        // @ts-ignore
+        const size = accountsNamespace["size"];
+
         return SystemProgram.createAccount({
           fromPubkey: provider.wallet.publicKey,
           newAccountPubkey: account.publicKey,
@@ -454,11 +450,102 @@ export class RpcFactory {
           programId,
         });
       };
+
+      // Subscribes to all changes to this account.
+      // @ts-ignore
+      accountsNamespace["subscribe"] = (
+        address: PublicKey,
+        commitment?: Commitment
+      ): EventEmitter => {
+        if (subscriptions.get(address.toString())) {
+          return subscriptions.get(address.toString()).ee;
+        }
+        const ee = new EventEmitter();
+
+        const listener = provider.connection.onAccountChange(
+          address,
+          (acc) => {
+            const account = coder.accounts.decode(idlAccount.name, acc.data);
+            ee.emit("change", account);
+          },
+          commitment
+        );
+
+        subscriptions.set(address.toString(), {
+          ee,
+          listener,
+        });
+
+        return ee;
+      };
+
+      // Unsubscribes to account changes.
+      // @ts-ignore
+      accountsNamespace["unsubscribe"] = (address: PublicKey) => {
+        let sub = subscriptions.get(address.toString());
+        if (subscriptions) {
+          provider.connection
+            .removeAccountChangeListener(sub.listener)
+            .then(() => {
+              subscriptions.delete(address.toString());
+            })
+            .catch(console.error);
+        }
+      };
+
+      // Returns all instances of this account type for the program.
+      // @ts-ignore
+      accountsNamespace["all"] = async (
+        filter?: Buffer
+      ): Promise<ProgramAccount<any>[]> => {
+        let bytes = await accountDiscriminator(idlAccount.name);
+        if (filter !== undefined) {
+          bytes = Buffer.concat([bytes, filter]);
+        }
+        // @ts-ignore
+        let resp = await provider.connection._rpcRequest("getProgramAccounts", [
+          programId.toBase58(),
+          {
+            commitment: provider.connection.commitment,
+            filters: [
+              {
+                memcmp: {
+                  offset: 0,
+                  bytes: bs58.encode(bytes),
+                },
+              },
+            ],
+          },
+        ]);
+        if (resp.error) {
+          console.error(resp);
+          throw new Error("Failed to get accounts");
+        }
+        return (
+          resp.result
+            // @ts-ignore
+            .map(({ pubkey, account: { data } }) => {
+              data = bs58.decode(data);
+              return {
+                publicKey: new PublicKey(pubkey),
+                account: coder.accounts.decode(idlAccount.name, data),
+              };
+            })
+        );
+      };
+
+      accountFns[name] = accountsNamespace;
     });
+
     return accountFns;
   }
 }
 
+type Subscription = {
+  listener: number;
+  ee: EventEmitter;
+};
+
 function translateError(
   idlErrors: Map<number, string>,
   err: any
@@ -550,84 +637,62 @@ function validateInstruction(ix: IdlInstruction, ...args: any[]) {
   // todo
 }
 
-function accountSize(idl: Idl, idlAccount: IdlTypeDef): number | undefined {
-  if (idlAccount.type.kind === "enum") {
-    let variantSizes = idlAccount.type.variants.map(
-      (variant: IdlEnumVariant) => {
-        if (variant.fields === undefined) {
-          return 0;
-        }
-        // @ts-ignore
-        return (
-          variant.fields
-            // @ts-ignore
-            .map((f: IdlField | IdlType) => {
-              // @ts-ignore
-              if (f.name === undefined) {
-                throw new Error("Tuple enum variants not yet implemented.");
-              }
-              // @ts-ignore
-              return typeSize(idl, f.type);
-            })
-            .reduce((a: number, b: number) => a + b)
-        );
-      }
-    );
-    return Math.max(...variantSizes) + 1;
-  }
-  if (idlAccount.type.fields === undefined) {
-    return 0;
-  }
-  return idlAccount.type.fields
-    .map((f) => typeSize(idl, f.type))
-    .reduce((a, b) => a + b);
+// Calculates the deterministic address of the program's "state" account.
+async function programStateAddress(programId: PublicKey): Promise<PublicKey> {
+  let [registrySigner, _nonce] = await PublicKey.findProgramAddress(
+    [],
+    programId
+  );
+  return PublicKey.createWithSeed(registrySigner, "unversioned", programId);
 }
 
-// Returns the size of the type in bytes. For variable length types, just return
-// 1. Users should override this value in such cases.
-function typeSize(idl: Idl, ty: IdlType): number {
-  switch (ty) {
-    case "bool":
-      return 1;
-    case "u8":
-      return 1;
-    case "i8":
-      return 1;
-    case "u16":
-      return 2;
-    case "u32":
-      return 4;
-    case "u64":
-      return 8;
-    case "i64":
-      return 8;
-    case "bytes":
-      return 1;
-    case "string":
-      return 1;
-    case "publicKey":
-      return 32;
-    default:
-      // @ts-ignore
-      if (ty.vec !== undefined) {
-        return 1;
-      }
-      // @ts-ignore
-      if (ty.option !== undefined) {
-        // @ts-ignore
-        return 1 + typeSize(ty.option);
-      }
-      // @ts-ignore
-      if (ty.defined !== undefined) {
-        // @ts-ignore
-        const filtered = idl.types.filter((t) => t.name === ty.defined);
-        if (filtered.length !== 1) {
-          throw new IdlError(`Type not found: ${JSON.stringify(ty)}`);
-        }
-        let typeDef = filtered[0];
-
-        return accountSize(idl, typeDef);
-      }
-      throw new Error(`Invalid type ${JSON.stringify(ty)}`);
+// Returns the common keys that are prepended to all instructions targeting
+// the "state" of a program.
+async function stateInstructionKeys(
+  programId: PublicKey,
+  provider: Provider,
+  m: IdlStateMethod,
+  ctx: RpcContext
+) {
+  if (m.name === "new") {
+    // Ctor `new` method.
+    const [programSigner, _nonce] = await PublicKey.findProgramAddress(
+      [],
+      programId
+    );
+    return [
+      {
+        pubkey: provider.wallet.publicKey,
+        isWritable: false,
+        isSigner: true,
+      },
+      {
+        pubkey: await programStateAddress(programId),
+        isWritable: true,
+        isSigner: false,
+      },
+      { pubkey: programSigner, isWritable: false, isSigner: false },
+      {
+        pubkey: SystemProgram.programId,
+        isWritable: false,
+        isSigner: false,
+      },
+
+      { pubkey: programId, isWritable: false, isSigner: false },
+      {
+        pubkey: SYSVAR_RENT_PUBKEY,
+        isWritable: false,
+        isSigner: false,
+      },
+    ];
+  } else {
+    validateAccounts(m.accounts, ctx.accounts);
+    return [
+      {
+        pubkey: await programStateAddress(programId),
+        isWritable: true,
+        isSigner: false,
+      },
+    ];
   }
 }

+ 117 - 0
ts/src/utils.ts

@@ -0,0 +1,117 @@
+import * as bs58 from "bs58";
+import { sha256 } from "crypto-hash";
+import { struct } from "superstruct";
+import assert from "assert";
+import { PublicKey, AccountInfo, Connection } from "@solana/web3.js";
+
+export const TOKEN_PROGRAM_ID = new PublicKey(
+  "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
+);
+
+async function getMultipleAccounts(
+  connection: Connection,
+  publicKeys: PublicKey[]
+): Promise<
+  Array<null | { publicKey: PublicKey; account: AccountInfo<Buffer> }>
+> {
+  const args = [publicKeys.map((k) => k.toBase58()), { commitment: "recent" }];
+  // @ts-ignore
+  const unsafeRes = await connection._rpcRequest("getMultipleAccounts", args);
+  const res = GetMultipleAccountsAndContextRpcResult(unsafeRes);
+  if (res.error) {
+    throw new Error(
+      "failed to get info about accounts " +
+        publicKeys.map((k) => k.toBase58()).join(", ") +
+        ": " +
+        res.error.message
+    );
+  }
+  assert(typeof res.result !== "undefined");
+  const accounts: Array<null | {
+    executable: any;
+    owner: PublicKey;
+    lamports: any;
+    data: Buffer;
+  }> = [];
+  for (const account of res.result.value) {
+    let value: {
+      executable: any;
+      owner: PublicKey;
+      lamports: any;
+      data: Buffer;
+    } | null = null;
+    if (account === null) {
+      accounts.push(null);
+      continue;
+    }
+    if (res.result.value) {
+      const { executable, owner, lamports, data } = account;
+      assert(data[1] === "base64");
+      value = {
+        executable,
+        owner: new PublicKey(owner),
+        lamports,
+        data: Buffer.from(data[0], "base64"),
+      };
+    }
+    if (value === null) {
+      throw new Error("Invalid response");
+    }
+    accounts.push(value);
+  }
+  return accounts.map((account, idx) => {
+    if (account === null) {
+      return null;
+    }
+    return {
+      publicKey: publicKeys[idx],
+      account,
+    };
+  });
+}
+
+function jsonRpcResult(resultDescription: any) {
+  const jsonRpcVersion = struct.literal("2.0");
+  return struct.union([
+    struct({
+      jsonrpc: jsonRpcVersion,
+      id: "string",
+      error: "any",
+    }),
+    struct({
+      jsonrpc: jsonRpcVersion,
+      id: "string",
+      error: "null?",
+      result: resultDescription,
+    }),
+  ]);
+}
+
+function jsonRpcResultAndContext(resultDescription: any) {
+  return jsonRpcResult({
+    context: struct({
+      slot: "number",
+    }),
+    value: resultDescription,
+  });
+}
+
+const AccountInfoResult = struct({
+  executable: "boolean",
+  owner: "string",
+  lamports: "number",
+  data: "any",
+  rentEpoch: "number?",
+});
+
+const GetMultipleAccountsAndContextRpcResult = jsonRpcResultAndContext(
+  struct.array([struct.union(["null", AccountInfoResult])])
+);
+
+const utils = {
+  bs58,
+  sha256,
+  getMultipleAccounts,
+};
+
+export default utils;

+ 2 - 1
ts/src/workspace.ts

@@ -12,7 +12,8 @@ export default new Proxy({} as any, {
     const process = require("process");
 
     if (typeof window !== "undefined") {
-      throw new Error("`anchor.workspace` is not available in the browser");
+      // Workspaces aren't available in the browser, yet.
+      return undefined;
     }
 
     if (!_populatedWorkspace) {

+ 63 - 129
ts/yarn.lock

@@ -241,7 +241,7 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/runtime@^7.10.5", "@babel/runtime@^7.11.2", "@babel/runtime@^7.3.1":
+"@babel/runtime@^7.11.2", "@babel/runtime@^7.3.1":
   version "7.12.5"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e"
   integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==
@@ -665,35 +665,6 @@
     bn.js "^5.1.2"
     buffer-layout "^1.2.0"
 
-"@project-serum/common@^0.0.1-beta.0":
-  version "0.0.1-beta.0"
-  resolved "https://registry.yarnpkg.com/@project-serum/common/-/common-0.0.1-beta.0.tgz#6a2ab2a0adbd19294048e5e96ad695445a959319"
-  integrity sha512-qJVZIrWQtF24wAfyKnsYhqA698wOMCVEigv/bqpld1n2C7EV2srE7H4tLkTKJxWexeFQOzD4/VOiSfebtc72xQ==
-  dependencies:
-    "@project-serum/serum" "^0.13.17"
-    bn.js "^5.1.2"
-    superstruct "0.8.3"
-
-"@project-serum/lockup@^0.0.1-beta.0":
-  version "0.0.1-beta.0"
-  resolved "https://registry.yarnpkg.com/@project-serum/lockup/-/lockup-0.0.1-beta.0.tgz#184ff0800d980c377ad00c75a09f3f50f42c30fc"
-  integrity sha512-iSRQppQKidWax+XUJdFsY+P2hefTpwm61ucqM0yHPh4Wnm85s64jaPU531C+8m3lcSC24ynJ2MrP58VvD4Rzfg==
-  dependencies:
-    "@project-serum/borsh" "^0.0.1-beta.0"
-    "@project-serum/common" "^0.0.1-beta.0"
-    "@solana/spl-token" "0.0.11"
-    bn.js "^5.1.2"
-    buffer-layout "^1.2.0"
-
-"@project-serum/serum@^0.13.17":
-  version "0.13.17"
-  resolved "https://registry.yarnpkg.com/@project-serum/serum/-/serum-0.13.17.tgz#a2d506c87d094635ff66e4d918b8382d7e9aa78d"
-  integrity sha512-Q4LMCALOXe/izvS5gICksYWOrrOZkzg2M3sxe4jOG5lrxmcCOQzCoISJ5u0t/LrnXpveJMU4BAVRmNOUBmSbLw==
-  dependencies:
-    "@solana/web3.js" "0.86.1"
-    bn.js "^5.1.2"
-    buffer-layout "^1.2.0"
-
 "@sinonjs/commons@^1.7.0":
   version "1.8.1"
   resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.1.tgz#e7df00f98a203324f6dc7cc606cad9d4a8ab2217"
@@ -708,27 +679,15 @@
   dependencies:
     "@sinonjs/commons" "^1.7.0"
 
-"@solana/spl-token@0.0.11":
-  version "0.0.11"
-  resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.0.11.tgz#5b978d38022beccaafac8b1dd1f3f575a39ebd1b"
-  integrity sha512-Upp+x5B18UegyoDfDJFbkhtL5ynuJBe1AFCbhbSasp+fQP4xx9Lhq+w3rheZPPOnCYwgRfAodABwCO/j7ZFKPg==
-  dependencies:
-    "@babel/runtime" "^7.10.5"
-    "@solana/web3.js" "^0.78.0"
-    bn.js "^5.0.0"
-    buffer-layout "^1.2.0"
-    dotenv "8.2.0"
-    mkdirp "1.0.4"
-
-"@solana/web3.js@0.86.1":
-  version "0.86.1"
-  resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-0.86.1.tgz#034a2cef742569f74dfc9960dfbcabc92e674b08"
-  integrity sha512-9mjWs17ym7PIm7bHA37wnnYyD7rIVHwkx1RI6BzGhMO5h8E+HlZM8ISLgOx+NItg8XRCfFhlrVgJTzK4om1s0g==
+"@solana/web3.js@^0.90.4":
+  version "0.90.4"
+  resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-0.90.4.tgz#9fae2d1fcb5fb3acbcd74a4ac482f2f84b7f7056"
+  integrity sha512-3HKXIcu+XXsXEFE58Yx6MZkbvSW1Pp0g9Hcvz2o0C5mawh7if2L7abgD5WNdXfIt3M4f6jM+H8fVSuoOJOZjrg==
   dependencies:
     "@babel/runtime" "^7.3.1"
     bn.js "^5.0.0"
     bs58 "^4.0.1"
-    buffer "^5.4.3"
+    buffer "^6.0.1"
     buffer-layout "^1.2.0"
     crypto-hash "^1.2.2"
     esdoc-inject-style-plugin "^1.0.0"
@@ -743,27 +702,6 @@
     tweetnacl "^1.0.0"
     ws "^7.0.0"
 
-"@solana/web3.js@^0.78.0":
-  version "0.78.4"
-  resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-0.78.4.tgz#a942d02e353b2c3ff7ddc94cb6846b5a98887bc2"
-  integrity sha512-Zt6LN35K2sQaQfgWbNjp91qGa6dHccXTQEoojXEo0NqZ/CQqmzretgSI/3kxKUiuvLTY/1WltVM7CKRkwMNRFA==
-  dependencies:
-    "@babel/runtime" "^7.3.1"
-    bn.js "^5.0.0"
-    bs58 "^4.0.1"
-    buffer "^5.4.3"
-    buffer-layout "^1.2.0"
-    crypto-hash "^1.2.2"
-    esdoc-inject-style-plugin "^1.0.0"
-    jayson "^3.0.1"
-    mz "^2.7.0"
-    node-fetch "^2.2.0"
-    npm-run-all "^4.1.5"
-    rpc-websockets "^7.4.2"
-    superstruct "^0.8.3"
-    tweetnacl "^1.0.0"
-    ws "^7.0.0"
-
 "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7":
   version "7.1.12"
   resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.12.tgz#4d8e9e51eb265552a7e4f1ff2219ab6133bdfb2d"
@@ -804,6 +742,13 @@
   dependencies:
     "@types/node" "*"
 
+"@types/bs58@^4.0.1":
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/@types/bs58/-/bs58-4.0.1.tgz#3d51222aab067786d3bc3740a84a7f5a0effaa37"
+  integrity sha512-yfAgiWgVLjFCmRv8zAcOIHywYATEwiTVccTLnRp6UxTNavT55M9d/uhK3T03St/+8/z/wW+CRjGKUNmEqoHHCA==
+  dependencies:
+    base-x "^3.0.6"
+
 "@types/connect@^3.4.33":
   version "3.4.34"
   resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.34.tgz#170a40223a6d666006d93ca128af2beb1d9b1901"
@@ -812,9 +757,9 @@
     "@types/node" "*"
 
 "@types/express-serve-static-core@^4.17.9":
-  version "4.17.17"
-  resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.17.tgz#6ba02465165b6c9c3d8db3a28def6b16fc9b70f5"
-  integrity sha512-YYlVaCni5dnHc+bLZfY908IG1+x5xuibKZMGv8srKkvtul3wUuanYvpIj9GXXoWkQbaAdR+kgX46IETKUALWNQ==
+  version "4.17.18"
+  resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.18.tgz#8371e260f40e0e1ca0c116a9afcd9426fa094c40"
+  integrity sha512-m4JTwx5RUBNZvky/JJ8swEJPKFd8si08pPF2PfizYjGZOKr/svUWPcoUmLow6MmPzhasphB7gSTINY67xn3JNA==
   dependencies:
     "@types/node" "*"
     "@types/qs" "*"
@@ -860,9 +805,9 @@
   integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==
 
 "@types/lodash@^4.14.159":
-  version "4.14.166"
-  resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.166.tgz#07e7f2699a149219dbc3c35574f126ec8737688f"
-  integrity sha512-A3YT/c1oTlyvvW/GQqG86EyqWNrT/tisOIh2mW3YCgcx71TNjiTZA3zYZWA5BCmtsOTXjhliy4c4yEkErw6njA==
+  version "4.14.168"
+  resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.168.tgz#fe24632e79b7ade3f132891afff86caa5e5ce008"
+  integrity sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q==
 
 "@types/minimist@^1.2.0":
   version "1.2.1"
@@ -875,9 +820,9 @@
   integrity sha512-G0lD1/7qD60TJ/mZmhog76k7NcpLWkPVGgzkRy3CTlnFu4LUQh5v2Wa661z6vnXmD8EQrnALUyf0VRtrACYztw==
 
 "@types/node@^12.12.54":
-  version "12.19.11"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.11.tgz#9220ab4b20d91169eb78f456dbfcbabee89dfb50"
-  integrity sha512-bwVfNTFZOrGXyiQ6t4B9sZerMSShWNsGRw8tC5DY1qImUNczS9SjT4G6PnzjCnxsu5Ubj6xjL2lgwddkxtQl5w==
+  version "12.19.15"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.15.tgz#0de7e978fb43db62da369db18ea088a63673c182"
+  integrity sha512-lowukE3GUI+VSYSu6VcBXl14d61Rp5hA1D+61r16qnwC0lYNSqdxcvRh0pswejorHfS+HgwBasM8jLXz0/aOsw==
 
 "@types/normalize-package-data@^2.4.0":
   version "2.4.0"
@@ -1286,7 +1231,7 @@ balanced-match@^1.0.0:
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
   integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
 
-base-x@^3.0.2:
+base-x@^3.0.2, base-x@^3.0.6:
   version "3.0.8"
   resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.8.tgz#1e1106c2537f0162e8b52474a557ebb09000018d"
   integrity sha512-Rl/1AWP4J/zRrk54hhlxH4drNxPJXYUaKffODVI53/dAsV4t9fBxyxYKAVPU1XBHxYwOWP9h9H0hM2MVw4YfJA==
@@ -1405,18 +1350,18 @@ buffer-layout@^1.2.0:
   resolved "https://registry.yarnpkg.com/buffer-layout/-/buffer-layout-1.2.0.tgz#ee1f5ef05a8afd5db6b3a8fe2056c111bc69c737"
   integrity sha512-iiyRoho/ERzBUv6kFvfsrLNgTlVwOkqQcSQN7WrO3Y+c5SeuEhCn6+y1KwhM0V3ndptF5mI/RI44zkw0qcR5Jg==
 
-buffer@^5.4.3:
-  version "5.7.1"
-  resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
-  integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
+buffer@^6.0.1:
+  version "6.0.3"
+  resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
+  integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
   dependencies:
     base64-js "^1.3.1"
-    ieee754 "^1.1.13"
+    ieee754 "^1.2.1"
 
 bufferutil@^4.0.1:
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.2.tgz#79f68631910f6b993d870fc77dc0a2894eb96cd5"
-  integrity sha512-AtnG3W6M8B2n4xDQ5R+70EXvOpnXsFYg/AK2yTZd+HQ/oxAdz+GI+DvjmhBw3L0ole+LJ0ngqY4JMbDzkfNzhA==
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.3.tgz#66724b756bed23cd7c28c4d306d7994f9943cc6b"
+  integrity sha512-yEYTwGndELGvfXsImMBLop58eaGW+YdONi1fNjTINSY98tmMmFijBG6WXgdkfuLNt4imzQNtIE+eBp1PVpMCSw==
   dependencies:
     node-gyp-build "^4.2.0"
 
@@ -1435,13 +1380,13 @@ cache-base@^1.0.1:
     union-value "^1.0.0"
     unset-value "^1.0.0"
 
-call-bind@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.0.tgz#24127054bb3f9bdcb4b1fb82418186072f77b8ce"
-  integrity sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==
+call-bind@^1.0.0, call-bind@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
+  integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==
   dependencies:
     function-bind "^1.1.1"
-    get-intrinsic "^1.0.0"
+    get-intrinsic "^1.0.2"
 
 caller-callsite@^2.0.0:
   version "2.0.0"
@@ -2050,11 +1995,6 @@ dot-prop@^3.0.0:
   dependencies:
     is-obj "^1.0.0"
 
-dotenv@8.2.0:
-  version "8.2.0"
-  resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
-  integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==
-
 ecc-jsbn@~0.1.1:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
@@ -2106,9 +2046,9 @@ entities@^1.1.1, entities@~1.1.1:
   integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
 
 entities@^2.0.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5"
-  integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
+  integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
 
 error-ex@^1.3.1:
   version "1.3.2"
@@ -2118,22 +2058,24 @@ error-ex@^1.3.1:
     is-arrayish "^0.2.1"
 
 es-abstract@^1.18.0-next.1:
-  version "1.18.0-next.1"
-  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68"
-  integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==
+  version "1.18.0-next.2"
+  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.2.tgz#088101a55f0541f595e7e057199e27ddc8f3a5c2"
+  integrity sha512-Ih4ZMFHEtZupnUh6497zEL4y2+w8+1ljnCyaTa+adcoafI1GOvMwFlDjBLfWR7y9VLfrjRJe9ocuHY1PSR9jjw==
   dependencies:
+    call-bind "^1.0.2"
     es-to-primitive "^1.2.1"
     function-bind "^1.1.1"
+    get-intrinsic "^1.0.2"
     has "^1.0.3"
     has-symbols "^1.0.1"
     is-callable "^1.2.2"
-    is-negative-zero "^2.0.0"
+    is-negative-zero "^2.0.1"
     is-regex "^1.1.1"
-    object-inspect "^1.8.0"
+    object-inspect "^1.9.0"
     object-keys "^1.1.1"
-    object.assign "^4.1.1"
-    string.prototype.trimend "^1.0.1"
-    string.prototype.trimstart "^1.0.1"
+    object.assign "^4.1.2"
+    string.prototype.trimend "^1.0.3"
+    string.prototype.trimstart "^1.0.3"
 
 es-to-primitive@^1.2.1:
   version "1.2.1"
@@ -2600,10 +2542,10 @@ get-caller-file@^2.0.1:
   resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
   integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
 
-get-intrinsic@^1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.0.2.tgz#6820da226e50b24894e08859469dc68361545d49"
-  integrity sha512-aeX0vrFm21ILl3+JpFFRNe9aUvp6VFZb2/CTbgLb8j75kOhvoNYjt9d8KA/tJG4gSo8nzEDedRl0h7vDmBYRVg==
+get-intrinsic@^1.0.2:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.0.tgz#892e62931e6938c8a23ea5aaebcfb67bd97da97e"
+  integrity sha512-M11rgtQp5GZMZzDL7jLTNxbDfurpzuau5uqRWDPvlHjfvg3TdScAZo96GLvhMjImrmR8uAt0FS2RLoMrfWGKlg==
   dependencies:
     function-bind "^1.1.1"
     has "^1.0.3"
@@ -2892,7 +2834,7 @@ iconv-lite@0.4.24:
   dependencies:
     safer-buffer ">= 2.1.2 < 3"
 
-ieee754@^1.1.13:
+ieee754@^1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
   integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
@@ -3103,7 +3045,7 @@ is-glob@^4.0.0, is-glob@^4.0.1:
   dependencies:
     is-extglob "^2.1.1"
 
-is-negative-zero@^2.0.0:
+is-negative-zero@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24"
   integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==
@@ -4249,7 +4191,7 @@ mixin-deep@^1.2.0:
     for-in "^1.0.2"
     is-extendable "^1.0.1"
 
-mkdirp@1.0.4, mkdirp@1.x:
+mkdirp@1.x:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
   integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
@@ -4436,7 +4378,7 @@ object-copy@^0.1.0:
     define-property "^0.2.5"
     kind-of "^3.0.3"
 
-object-inspect@^1.8.0:
+object-inspect@^1.9.0:
   version "1.9.0"
   resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a"
   integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==
@@ -4453,7 +4395,7 @@ object-visit@^1.0.0:
   dependencies:
     isobject "^3.0.0"
 
-object.assign@^4.1.1:
+object.assign@^4.1.2:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940"
   integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==
@@ -5401,7 +5343,7 @@ string.prototype.padend@^3.0.0:
     define-properties "^1.1.3"
     es-abstract "^1.18.0-next.1"
 
-string.prototype.trimend@^1.0.1:
+string.prototype.trimend@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz#a22bd53cca5c7cf44d7c9d5c732118873d6cd18b"
   integrity sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw==
@@ -5409,7 +5351,7 @@ string.prototype.trimend@^1.0.1:
     call-bind "^1.0.0"
     define-properties "^1.1.3"
 
-string.prototype.trimstart@^1.0.1:
+string.prototype.trimstart@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz#9b4cb590e123bb36564401d59824298de50fd5aa"
   integrity sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg==
@@ -5484,14 +5426,6 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
   integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
 
-superstruct@0.8.3:
-  version "0.8.3"
-  resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.8.3.tgz#fb4d8901aca3bf9f79afab1bbab7a7f335cc4ef2"
-  integrity sha512-LbtbFpktW1FcwxVIJlxdk7bCyBq/GzOx2FSFLRLTUhWIA1gHkYPIl3aXRG5mBdGZtnPNT6t+4eEcLDCMOuBHww==
-  dependencies:
-    kind-of "^6.0.2"
-    tiny-invariant "^1.0.6"
-
 superstruct@^0.8.3:
   version "0.8.4"
   resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.8.4.tgz#478a19649f6b02c6319c02044db6a1f5863c391f"
@@ -5844,9 +5778,9 @@ use@^3.1.0:
   integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
 
 utf-8-validate@^5.0.2:
-  version "5.0.3"
-  resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.3.tgz#3b64e418ad2ff829809025fdfef595eab2f03a27"
-  integrity sha512-jtJM6fpGv8C1SoH4PtG22pGto6x+Y8uPprW0tw3//gGFhDDTiuksgradgFN6yRayDP4SyZZa6ZMGHLIa17+M8A==
+  version "5.0.4"
+  resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.4.tgz#72a1735983ddf7a05a43a9c6b67c5ce1c910f9b8"
+  integrity sha512-MEF05cPSq3AwJ2C7B7sHAA6i53vONoZbMGX8My5auEVm6W+dJ2Jd/TZPyGJ5CH42V2XtbI5FD28HeHeqlPzZ3Q==
   dependencies:
     node-gyp-build "^4.2.0"