Basic example working e2e

This commit is contained in:
armaniferrante 2021-01-01 19:07:26 -08:00
parent 7f9a521d30
commit cbe06afc99
No known key found for this signature in database
GPG Key ID: 58BEF301E91F7828
13 changed files with 132 additions and 50 deletions

View File

@ -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"

View File

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

View File

@ -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(())

View File

@ -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"

View File

@ -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
}
}

View File

@ -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,

View File

@ -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)

View File

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

View File

@ -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);

View File

@ -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 };

View File

@ -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;
}
}

View File

@ -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.

View File

@ -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();