Browse Source

Basic example working e2e

armaniferrante 4 years ago
parent
commit
cbe06afc99

+ 1 - 1
cli/Cargo.toml

@@ -14,6 +14,6 @@ path = "src/main.rs"
 clap = "3.0.0-beta.1"
 anyhow = "1.0.32"
 syn = { version = "1.0.54", features = ["full", "extra-traits"] }
-anchor-syn = { path = "../syn" }
+anchor-syn = { path = "../syn", features = ["idl"] }
 serde_json = "1.0"
 shellexpand = "2.1.0"

+ 0 - 5
examples/basic/idl.json

@@ -5,11 +5,6 @@
     {
       "name": "create_root",
       "accounts": [
-        {
-          "name": "authority",
-          "isMut": false,
-          "isSigner": true
-        },
         {
           "name": "root",
           "isMut": true,

+ 1 - 3
examples/basic/src/lib.rs

@@ -48,8 +48,6 @@ mod example {
 
 #[derive(Accounts)]
 pub struct CreateRoot<'info> {
-    #[account(signer)]
-    pub authority: AccountInfo<'info>,
     #[account(mut, "!root.initialized")]
     pub root: ProgramAccount<'info, Root>,
 }
@@ -108,7 +106,7 @@ pub struct MyCustomType {
 // Define any auxiliary access control checks.
 
 fn not_zero(authority: Pubkey) -> ProgramResult {
-    if authority != Pubkey::new_from_array([0; 32]) {
+    if authority == Pubkey::new_from_array([0; 32]) {
         return Err(ProgramError::InvalidInstructionData);
     }
     Ok(())

+ 3 - 1
syn/Cargo.toml

@@ -4,7 +4,9 @@ version = "0.1.0"
 authors = ["armaniferrante <armaniferrante@gmail.com>"]
 edition = "2018"
 
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+[features]
+idl = []
+default = []
 
 [dependencies]
 proc-macro2 = "1.0"

+ 1 - 1
syn/src/codegen/anchor.rs

@@ -157,7 +157,7 @@ pub fn generate_constraint_signer(f: &Field, _c: &ConstraintSigner) -> proc_macr
 pub fn generate_constraint_literal(_f: &Field, c: &ConstraintLiteral) -> proc_macro2::TokenStream {
     let tokens = &c.tokens;
     quote! {
-        if #tokens {
+        if !(#tokens) {
             return Err(ProgramError::Custom(1)); // todo: error codes
         }
     }

+ 3 - 1
syn/src/lib.rs

@@ -1,7 +1,7 @@
 //! DSL syntax tokens.
 
 pub mod codegen;
-#[cfg(target_arch = "x86")]
+#[cfg(feature = "idl")]
 pub mod idl;
 pub mod parser;
 
@@ -67,11 +67,13 @@ pub struct Field {
 }
 
 // A type of an account field.
+#[derive(PartialEq)]
 pub enum Ty {
     AccountInfo,
     ProgramAccount(ProgramAccountTy),
 }
 
+#[derive(PartialEq)]
 pub struct ProgramAccountTy {
     // The struct type of the account.
     pub account_ident: syn::Ident,

+ 8 - 6
syn/src/parser/anchor.rs

@@ -42,7 +42,7 @@ pub fn parse(strct: &syn::ItemStruct) -> AccountsStruct {
 fn parse_field(f: &syn::Field, anchor: &syn::Attribute) -> Field {
     let ident = f.ident.clone().unwrap();
     let ty = parse_ty(f);
-    let (constraints, is_mut, is_signer) = parse_constraints(anchor);
+    let (constraints, is_mut, is_signer) = parse_constraints(anchor, &ty);
     Field {
         ident,
         ty,
@@ -91,7 +91,7 @@ fn parse_program_account(path: &syn::Path) -> ProgramAccountTy {
     ProgramAccountTy { account_ident }
 }
 
-fn parse_constraints(anchor: &syn::Attribute) -> (Vec<Constraint>, bool, bool) {
+fn parse_constraints(anchor: &syn::Attribute, ty: &Ty) -> (Vec<Constraint>, bool, bool) {
     let mut tts = anchor.tokens.clone().into_iter();
     let g_stream = match tts.next().expect("Must have a token group") {
         proc_macro2::TokenTree::Group(g) => g.stream(),
@@ -153,7 +153,7 @@ fn parse_constraints(anchor: &syn::Attribute) -> (Vec<Constraint>, bool, bool) {
                 }
             },
             proc_macro2::TokenTree::Punct(punct) => {
-                if (punct.as_char() != ',') {
+                if punct.as_char() != ',' {
                     panic!("invalid syntax");
                 }
             }
@@ -168,10 +168,12 @@ fn parse_constraints(anchor: &syn::Attribute) -> (Vec<Constraint>, bool, bool) {
         }
     }
 
-    // If no owner constraint was specified, default to it being the current
-    // program.
     if !has_owner_constraint {
-        constraints.push(Constraint::Owner(ConstraintOwner::Program));
+        if ty == &Ty::AccountInfo {
+            constraints.push(Constraint::Owner(ConstraintOwner::Skip));
+        } else {
+            constraints.push(Constraint::Owner(ConstraintOwner::Program));
+        }
     }
 
     (constraints, is_mut, is_signer)

+ 1 - 1
syn/src/parser/mod.rs

@@ -1,4 +1,4 @@
 pub mod anchor;
-#[cfg(target_arch = "x86")]
+#[cfg(feature = "idl")]
 pub mod file;
 pub mod program;

+ 3 - 3
ts/src/coder.ts

@@ -1,3 +1,4 @@
+import camelCase from "camelcase";
 import { Layout } from "buffer-layout";
 import * as borsh from "@project-serum/borsh";
 import { Idl, IdlField, IdlTypeDef } from "./idl";
@@ -51,7 +52,8 @@ class InstructionCoder<T = any> {
       let fieldLayouts = ix.args.map((arg) =>
         IdlCoder.fieldLayout(arg, idl.types)
       );
-      return borsh.struct(fieldLayouts, ix.name);
+      const name = camelCase(ix.name);
+      return borsh.struct(fieldLayouts, name);
     });
     return borsh.rustEnum(ixLayouts);
   }
@@ -144,8 +146,6 @@ class IdlCoder {
           const name = field.type.defined;
           const filtered = types.filter((t) => t.name === name);
           if (filtered.length !== 1) {
-            console.log(types);
-            console.log(name);
             throw new IdlError("Type not found");
           }
           return IdlCoder.typeDefLayout(filtered[0], types, name);

+ 4 - 2
ts/src/index.ts

@@ -1,6 +1,8 @@
+import BN from "bn.js";
+import * as web3 from "@solana/web3.js";
+import { Provider } from "@project-serum/common";
 import { Program } from "./program";
 import Coder from "./coder";
-import { Provider } from "@project-serum/common";
 
 let _provider: Provider | null = null;
 
@@ -12,4 +14,4 @@ function getProvider(): Provider {
   return _provider;
 }
 
-export { Program, Coder, setProvider, getProvider, Provider };
+export { Program, Coder, setProvider, getProvider, Provider, BN, web3 };

+ 6 - 0
ts/src/program.ts

@@ -34,6 +34,11 @@ export class Program {
    */
   readonly instruction: Ixs;
 
+  /**
+   * Coder for serializing rpc requests.
+   */
+  readonly coder: Coder;
+
   public constructor(idl: Idl, programId: PublicKey) {
     this.idl = idl;
     this.programId = programId;
@@ -46,5 +51,6 @@ export class Program {
     this.rpc = rpcs;
     this.instruction = ixs;
     this.account = accounts;
+    this.coder = coder;
   }
 }

+ 41 - 17
ts/src/rpc.ts

@@ -37,12 +37,12 @@ export interface Accounts {
 /**
  * RpcFn is a single rpc method.
  */
-export type RpcFn = (ctx: RpcContext, ...args: any[]) => Promise<any>;
+export type RpcFn = (...args: any[]) => Promise<any>;
 
 /**
  * Ix is a function to create a `TransactionInstruction`.
  */
-export type IxFn = (ctx: RpcContext, ...args: any[]) => TransactionInstruction;
+export type IxFn = (...args: any[]) => TransactionInstruction;
 
 /**
  * Account is a function returning a deserialized account, given an address.
@@ -59,11 +59,14 @@ type RpcOptions = ConfirmOptions;
  * covered by the instruction enum.
  */
 type RpcContext = {
-  options?: RpcOptions;
+  // Accounts the instruction will use.
   accounts: RpcAccounts;
   // Instructions to run *before* the specified rpc instruction.
   instructions?: TransactionInstruction[];
+  // Accounts that must sign the transaction.
   signers?: Array<Account>;
+  // RpcOptions.
+  options?: RpcOptions;
 };
 
 /**
@@ -95,7 +98,7 @@ export class RpcFactory {
       // Function to create a raw `TransactionInstruction`.
       const ix = RpcFactory.buildIx(idlIx, coder, programId);
       // Function to invoke an RPC against a cluster.
-      const rpc = RpcFactory.buildRpc(ix);
+      const rpc = RpcFactory.buildRpc(idlIx, ix);
 
       const name = camelCase(idlIx.name);
       rpcs[name] = rpc;
@@ -104,7 +107,7 @@ export class RpcFactory {
 
     idl.accounts.forEach((idlAccount) => {
       // todo
-      const accountFn = async (address: PublicKey): Promise<void> => {
+      const accountFn = async (address: PublicKey): Promise<any> => {
         const provider = getProvider();
         if (provider === null) {
           throw new Error("Provider not set");
@@ -113,7 +116,7 @@ export class RpcFactory {
         if (accountInfo === null) {
           throw new Error(`Entity does not exist ${address}`);
         }
-        coder.accounts.decode(idlAccount.name, accountInfo.data);
+        return coder.accounts.decode(idlAccount.name, accountInfo.data);
       };
       const name = camelCase(idlAccount.name);
       accountFns[name] = accountFn;
@@ -131,9 +134,10 @@ export class RpcFactory {
       throw new IdlError("the _inner name is reserved");
     }
 
-    const ix = (ctx: RpcContext, ...args: any[]): TransactionInstruction => {
+    const ix = (...args: any[]): TransactionInstruction => {
+      const [ixArgs, ctx] = splitArgsAndCtx(idlIx, [...args]);
       validateAccounts(idlIx, ctx.accounts);
-      validateInstruction(idlIx, args);
+      validateInstruction(idlIx, ...args);
 
       const keys = idlIx.accounts.map((acc) => {
         return {
@@ -142,27 +146,24 @@ export class RpcFactory {
           isSigner: acc.isSigner,
         };
       });
-
       return new TransactionInstruction({
         keys,
         programId,
-        data: coder.instruction.encode(toInstruction(idlIx, args)),
+        data: coder.instruction.encode(toInstruction(idlIx, ...ixArgs)),
       });
     };
 
     return ix;
   }
 
-  private static buildRpc(ixFn: IxFn): RpcFn {
-    const rpc = async (
-      ctx: RpcContext,
-      ...args: any[]
-    ): Promise<TransactionSignature> => {
+  private static buildRpc(idlIx: IdlInstruction, ixFn: IxFn): RpcFn {
+    const rpc = async (...args: any[]): Promise<TransactionSignature> => {
+      const [_, ctx] = splitArgsAndCtx(idlIx, [...args]);
       const tx = new Transaction();
       if (ctx.instructions !== undefined) {
         tx.add(...ctx.instructions);
       }
-      tx.add(ixFn(ctx, ...args));
+      tx.add(ixFn(...args));
       const provider = getProvider();
       if (provider === null) {
         throw new Error("Provider not found");
@@ -176,6 +177,23 @@ export class RpcFactory {
   }
 }
 
+function splitArgsAndCtx(
+  idlIx: IdlInstruction,
+  args: any[]
+): [any[], RpcContext] {
+  let options = undefined;
+
+  const inputLen = idlIx.args ? idlIx.args.length : 0;
+  if (args.length > inputLen) {
+    if (args.length !== inputLen + 1) {
+      throw new Error("provided too many arguments ${args}");
+    }
+    options = args.pop();
+  }
+
+  return [args, options];
+}
+
 function toInstruction(idlIx: IdlInstruction, ...args: any[]) {
   if (idlIx.args.length != args.length) {
     throw new Error("Invalid argument length");
@@ -186,7 +204,13 @@ function toInstruction(idlIx: IdlInstruction, ...args: any[]) {
     ix[ixArg.name] = args[idx];
     idx += 1;
   });
-  return ix;
+
+  // JavaScript representation of the rust enum variant.
+  const name = camelCase(idlIx.name);
+  const ixVariant: { [key: string]: any } = {};
+  ixVariant[name] = ix;
+
+  return ixVariant;
 }
 
 // Throws error if any account required for the `ix` is not given.

+ 60 - 9
ts/test.js

@@ -1,16 +1,67 @@
-const web3 = require('@solana/web3.js');
+const assert = require('assert');
 const anchor = require('.');
-anchor.setProvider(anchor.Provider.local());
 
-const idl = JSON.parse(require('fs').readFileSync('../examples/basic/idl.json', 'utf8'));
-const pid = new web3.PublicKey('9gzNv4hUB1F3jQQNNcZxxjn1bCjgaTCrucDjFh2i8vc6');
+// Global workspace settings.
+const WORKSPACE = {
+    idl: JSON.parse(require('fs').readFileSync('../examples/basic/idl.json', 'utf8')),
+    programId: new anchor.web3.PublicKey('3bSz7zXCXFdEBw8AKEWJAa53YswM5aCoNNt5xSR42JDp'),
+    provider: anchor.Provider.local(),
+};
 
 async function test() {
-    const program = new anchor.Program(idl, pid);
-    const sig = await program.rpc.createRoot(
-      new PublicKey(''),
-      1234,
-    );
+    // Configure the local cluster.
+    anchor.setProvider(WORKSPACE.provider);
+
+    // Generate the program from IDL.
+    const program = new anchor.Program(WORKSPACE.idl, WORKSPACE.programId);
+
+    // New account to create.
+    const root = new anchor.web3.Account();
+
+    // Execute the RPC (instruction) against the cluster, passing in the arguments
+    // exactly as defined by the Solana program.
+    //
+    // The last parameter defines context for the transaction. Consisting of
+    //
+    // 1) Any additional instructions one wishes to execute *before* executing
+    //    the program.
+    // 2) Any signers (in addition to the provider).
+    // 3) Accounts for the program's instruction. Ordering does *not* matter,
+    //    only that they names are as specified in the IDL.
+    await program.rpc.createRoot(WORKSPACE.provider.wallet.publicKey, new anchor.BN(1234), {
+        accounts: {
+            root: root.publicKey,
+        },
+        signers: [root],
+        instructions: [
+            anchor.web3.SystemProgram.createAccount({
+                fromPubkey: WORKSPACE.provider.wallet.publicKey,
+                newAccountPubkey: root.publicKey,
+                space: 41,
+                lamports: await WORKSPACE.provider.connection.getMinimumBalanceForRentExemption(41),
+                programId: WORKSPACE.programId,
+            }),
+        ],
+    }
+                                );
+
+    // Read the newly created account data.
+    let account = await program.account.root(root.publicKey);
+    assert.ok(account.initialized);
+    assert.ok(account.data.eq(new anchor.BN(1234)));
+    assert.ok(account.authority.equals(WORKSPACE.provider.wallet.publicKey));
+
+    // Execute another RPC to update the data.
+    await program.rpc.updateRoot(new anchor.BN(999), {
+        accounts: {
+            root: root.publicKey,
+            authority: WORKSPACE.provider.wallet.publicKey,
+        },
+    });
+
+    // Check the update actually persisted.
+    account = await program.account.root(root.publicKey);
+    assert.ok(account.data.eq(new anchor.BN(999)));
 }
 
 test();