Basic example working e2e
This commit is contained in:
parent
7f9a521d30
commit
cbe06afc99
|
@ -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"
|
||||
|
|
|
@ -5,11 +5,6 @@
|
|||
{
|
||||
"name": "create_root",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "authority",
|
||||
"isMut": false,
|
||||
"isSigner": true
|
||||
},
|
||||
{
|
||||
"name": "root",
|
||||
"isMut": true,
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,4 +1,4 @@
|
|||
pub mod anchor;
|
||||
#[cfg(target_arch = "x86")]
|
||||
#[cfg(feature = "idl")]
|
||||
pub mod file;
|
||||
pub mod program;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
69
ts/test.js
69
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();
|
||||
|
|
Loading…
Reference in New Issue