Instruction and rpc functions dynamically attached to program
This commit is contained in:
parent
840c38d8c1
commit
4d42da0146
|
@ -1,19 +1,19 @@
|
|||
{
|
||||
"version": "0.0.0",
|
||||
"name": "example",
|
||||
"methods": [
|
||||
"instructions": [
|
||||
{
|
||||
"name": "create_root",
|
||||
"accounts": [
|
||||
{
|
||||
"name": "authority",
|
||||
"is_mut": false,
|
||||
"is_signer": true
|
||||
"isMut": false,
|
||||
"isSigner": true
|
||||
},
|
||||
{
|
||||
"name": "root",
|
||||
"is_mut": true,
|
||||
"is_signer": false
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
|
@ -32,13 +32,13 @@
|
|||
"accounts": [
|
||||
{
|
||||
"name": "authority",
|
||||
"is_mut": false,
|
||||
"is_signer": true
|
||||
"isMut": false,
|
||||
"isSigner": true
|
||||
},
|
||||
{
|
||||
"name": "root",
|
||||
"is_mut": true,
|
||||
"is_signer": false
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
|
@ -53,13 +53,13 @@
|
|||
"accounts": [
|
||||
{
|
||||
"name": "root",
|
||||
"is_mut": false,
|
||||
"is_signer": false
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "leaf",
|
||||
"is_mut": true,
|
||||
"is_signer": false
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
|
@ -80,59 +80,21 @@
|
|||
"accounts": [
|
||||
{
|
||||
"name": "authority",
|
||||
"is_mut": false,
|
||||
"is_signer": true
|
||||
"isMut": false,
|
||||
"isSigner": true
|
||||
},
|
||||
{
|
||||
"name": "root",
|
||||
"is_mut": false,
|
||||
"is_signer": false
|
||||
"isMut": false,
|
||||
"isSigner": false
|
||||
},
|
||||
{
|
||||
"name": "leaf",
|
||||
"is_mut": true,
|
||||
"is_signer": false
|
||||
"isMut": true,
|
||||
"isSigner": false
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
{
|
||||
"name": "data",
|
||||
"type": "u64"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
"type": "struct",
|
||||
"name": "Root",
|
||||
"fields": [
|
||||
{
|
||||
"name": "initialized",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "authority",
|
||||
"type": "publicKey"
|
||||
},
|
||||
{
|
||||
"name": "data",
|
||||
"type": "u64"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "struct",
|
||||
"name": "Leaf",
|
||||
"fields": [
|
||||
{
|
||||
"name": "initialized",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "root",
|
||||
"type": "publicKey"
|
||||
},
|
||||
{
|
||||
"name": "data",
|
||||
"type": "u64"
|
||||
|
@ -140,26 +102,78 @@
|
|||
{
|
||||
"name": "custom",
|
||||
"type": {
|
||||
"defined": "MyCustomType"
|
||||
"option": {
|
||||
"defined": "MyCustomType"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
"name": "Root",
|
||||
"type": {
|
||||
"kind": "struct",
|
||||
"fields": [
|
||||
{
|
||||
"name": "initialized",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "authority",
|
||||
"type": "publicKey"
|
||||
},
|
||||
{
|
||||
"name": "data",
|
||||
"type": "u64"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Leaf",
|
||||
"type": {
|
||||
"kind": "struct",
|
||||
"fields": [
|
||||
{
|
||||
"name": "initialized",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "root",
|
||||
"type": "publicKey"
|
||||
},
|
||||
{
|
||||
"name": "data",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "custom",
|
||||
"type": {
|
||||
"defined": "MyCustomType"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"types": [
|
||||
{
|
||||
"type": "struct",
|
||||
"name": "MyCustomType",
|
||||
"fields": [
|
||||
{
|
||||
"name": "my_data",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "key",
|
||||
"type": "publicKey"
|
||||
}
|
||||
]
|
||||
"type": {
|
||||
"kind": "struct",
|
||||
"fields": [
|
||||
{
|
||||
"name": "my_data",
|
||||
"type": "u64"
|
||||
},
|
||||
{
|
||||
"name": "key",
|
||||
"type": "publicKey"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -30,9 +30,16 @@ mod example {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_leaf(ctx: Context<UpdateLeaf>, data: u64) -> ProgramResult {
|
||||
pub fn update_leaf(
|
||||
ctx: Context<UpdateLeaf>,
|
||||
data: u64,
|
||||
custom: Option<MyCustomType>,
|
||||
) -> ProgramResult {
|
||||
let leaf = &mut ctx.accounts.leaf;
|
||||
leaf.account.data = data;
|
||||
if let Some(custom) = custom {
|
||||
leaf.custom = custom;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
|
|||
pub struct Idl {
|
||||
pub version: String,
|
||||
pub name: String,
|
||||
pub methods: Vec<IdlMethod>,
|
||||
pub instructions: Vec<IdlInstruction>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
||||
pub accounts: Vec<IdlTypeDef>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
||||
|
@ -12,13 +12,14 @@ pub struct Idl {
|
|||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct IdlMethod {
|
||||
pub struct IdlInstruction {
|
||||
pub name: String,
|
||||
pub accounts: Vec<IdlAccount>,
|
||||
pub args: Vec<IdlField>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct IdlAccount {
|
||||
pub name: String,
|
||||
pub is_mut: bool,
|
||||
|
@ -33,16 +34,17 @@ pub struct IdlField {
|
|||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase", tag = "type")]
|
||||
pub enum IdlTypeDef {
|
||||
Struct {
|
||||
name: String,
|
||||
fields: Vec<IdlField>,
|
||||
},
|
||||
Enum {
|
||||
name: String,
|
||||
variants: Vec<EnumVariant>,
|
||||
},
|
||||
pub struct IdlTypeDef {
|
||||
pub name: String,
|
||||
#[serde(rename = "type")]
|
||||
pub ty: IdlTypeDefTy,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase", tag = "kind")]
|
||||
pub enum IdlTypeDefTy {
|
||||
Struct { fields: Vec<IdlField> },
|
||||
Enum { variants: Vec<EnumVariant> },
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
@ -75,26 +77,44 @@ pub enum IdlType {
|
|||
String,
|
||||
PublicKey,
|
||||
Defined(String),
|
||||
Option(Box<IdlType>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct IdlTypePublicKey;
|
||||
|
||||
impl std::str::FromStr for IdlType {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let r = match s {
|
||||
// Eliminate whitespace.
|
||||
let mut s = s.to_string();
|
||||
s.retain(|c| !c.is_whitespace());
|
||||
|
||||
let r = match s.as_str() {
|
||||
"bool" => IdlType::Bool,
|
||||
"u8" => IdlType::U8,
|
||||
"i8" => IdlType::I8,
|
||||
"u16" => IdlType::U16,
|
||||
"i16" => IdlType::I16,
|
||||
"u32" => IdlType::U32,
|
||||
"I32" => IdlType::I32,
|
||||
"i32" => IdlType::I32,
|
||||
"u64" => IdlType::U64,
|
||||
"i64" => IdlType::I64,
|
||||
"Vec<u8>" => IdlType::Bytes,
|
||||
"String" => IdlType::String,
|
||||
"Pubkey" => IdlType::PublicKey,
|
||||
_ => IdlType::Defined(s.to_string()),
|
||||
_ => match s.to_string().strip_prefix("Option<") {
|
||||
None => IdlType::Defined(s.to_string()),
|
||||
Some(inner) => {
|
||||
let inner_ty = Self::from_str(
|
||||
inner
|
||||
.strip_suffix(">")
|
||||
.ok_or(anyhow::anyhow!("Invalid option"))?,
|
||||
)?;
|
||||
IdlType::Option(Box::new(inner_ty))
|
||||
}
|
||||
},
|
||||
};
|
||||
Ok(r)
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ pub fn parse(filename: &str) -> Result<Idl> {
|
|||
acc_names
|
||||
};
|
||||
|
||||
let methods = p
|
||||
let instructions = p
|
||||
.rpcs
|
||||
.iter()
|
||||
.map(|rpc| {
|
||||
|
@ -63,7 +63,7 @@ pub fn parse(filename: &str) -> Result<Idl> {
|
|||
is_signer: acc.is_signer,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
IdlMethod {
|
||||
IdlInstruction {
|
||||
name: rpc.ident.to_string(),
|
||||
accounts,
|
||||
args,
|
||||
|
@ -76,11 +76,7 @@ pub fn parse(filename: &str) -> Result<Idl> {
|
|||
let mut types = vec![];
|
||||
let ty_defs = parse_ty_defs(&f)?;
|
||||
for ty_def in ty_defs {
|
||||
let name = match &ty_def {
|
||||
IdlTypeDef::Struct { name, .. } => name,
|
||||
IdlTypeDef::Enum { name, .. } => name,
|
||||
};
|
||||
if acc_names.contains(name) {
|
||||
if acc_names.contains(&ty_def.name) {
|
||||
accounts.push(ty_def);
|
||||
} else {
|
||||
types.push(ty_def);
|
||||
|
@ -90,7 +86,7 @@ pub fn parse(filename: &str) -> Result<Idl> {
|
|||
Ok(Idl {
|
||||
version: "0.0.0".to_string(),
|
||||
name: p.name.to_string(),
|
||||
methods,
|
||||
instructions,
|
||||
types,
|
||||
accounts,
|
||||
})
|
||||
|
@ -174,7 +170,10 @@ fn parse_ty_defs(f: &syn::File) -> Result<Vec<IdlTypeDef>> {
|
|||
_ => panic!("Only named structs are allowed."),
|
||||
};
|
||||
|
||||
return Some(fields.map(|fields| IdlTypeDef::Struct { name, fields }));
|
||||
return Some(fields.map(|fields| IdlTypeDef {
|
||||
name,
|
||||
ty: IdlTypeDefTy::Struct { fields },
|
||||
}));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
"scripts": {
|
||||
"build": "yarn build:node",
|
||||
"build:node": "tsc && tsc -p tsconfig.cjs.json",
|
||||
"watch": "tsc --watch",
|
||||
"watch": "tsc -p tsconfig.cjs.json --watch",
|
||||
"test:unit": "jest test/unit",
|
||||
"test:integration": "jest test/integration --detectOpenHandles",
|
||||
"coverage": "jest --coverage test/unit",
|
||||
|
@ -25,10 +25,12 @@
|
|||
"@project-serum/common": "^0.0.1-beta.0",
|
||||
"@project-serum/lockup": "^0.0.1-beta.0",
|
||||
"@solana/spl-token": "0.0.11",
|
||||
"@types/bn.js": "^4.11.6",
|
||||
"bn.js": "^5.1.2",
|
||||
"buffer-layout": "^1.2.0"
|
||||
"buffer-layout": "^1.2.0",
|
||||
"camelcase": "^5.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^8.2.0",
|
||||
"@commitlint/config-conventional": "^8.2.0",
|
||||
"@types/jest": "^26.0.15",
|
||||
|
@ -44,7 +46,7 @@
|
|||
"ts-jest": "^26.4.3",
|
||||
"ts-node": "^9.0.0",
|
||||
"typescript": "^4.0.5"
|
||||
},
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@solana/web3.js": "^0.86.1"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
import { Layout } from 'buffer-layout';
|
||||
import * as borsh from '@project-serum/borsh';
|
||||
import { Idl, IdlField, IdlTypeDef } from './idl';
|
||||
import { IdlError } from './error';
|
||||
|
||||
/**
|
||||
* Coder provides a facade for encoding and decoding all IDL related objects.
|
||||
*/
|
||||
export default class Coder {
|
||||
|
||||
/**
|
||||
* Instruction coder.
|
||||
*/
|
||||
readonly instruction: InstructionCoder;
|
||||
|
||||
/**
|
||||
* Account coder.
|
||||
*/
|
||||
readonly accounts: AccountsCoder;
|
||||
|
||||
constructor(idl: Idl) {
|
||||
this.instruction = new InstructionCoder(idl);
|
||||
this.accounts = new AccountsCoder(idl);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes and decodes program instructions.
|
||||
*/
|
||||
class InstructionCoder<T = any> {
|
||||
|
||||
/**
|
||||
* Instruction enum layout.
|
||||
*/
|
||||
private ixLayout: Layout;
|
||||
|
||||
public constructor(idl: Idl) {
|
||||
this.ixLayout = InstructionCoder.parseIxLayout(idl);
|
||||
}
|
||||
|
||||
public encode(ix: T): Buffer {
|
||||
const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
|
||||
const len = this.ixLayout.encode(ix, buffer);
|
||||
return buffer.slice(0, len);
|
||||
}
|
||||
|
||||
public decode(ix: Buffer): T {
|
||||
return this.ixLayout.decode(ix);
|
||||
}
|
||||
|
||||
private static parseIxLayout(idl: Idl): Layout {
|
||||
let ixLayouts = idl.instructions.map(ix => {
|
||||
let fieldLayouts = ix.args.map(arg => IdlCoder.fieldLayout(arg, idl.types));
|
||||
return borsh.struct(fieldLayouts, ix.name);
|
||||
});
|
||||
return borsh.rustEnum(ixLayouts);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes and decodes account objects.
|
||||
*/
|
||||
class AccountsCoder {
|
||||
|
||||
/**
|
||||
* Maps account type identifier to a layout.
|
||||
*/
|
||||
private accountLayouts: Map<string, Layout>;
|
||||
|
||||
public constructor(idl: Idl) {
|
||||
if (idl.accounts === undefined) {
|
||||
this.accountLayouts = new Map();
|
||||
return;
|
||||
}
|
||||
const layouts = 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 {
|
||||
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);
|
||||
}
|
||||
|
||||
public decode<T=any>(accountName: string, ix: Buffer): T {
|
||||
const layout = this.accountLayouts.get(accountName);
|
||||
return layout.decode(ix);
|
||||
}
|
||||
}
|
||||
|
||||
class IdlCoder {
|
||||
public static fieldLayout(field: IdlField, types?: IdlTypeDef[]): Layout {
|
||||
switch (field.type) {
|
||||
case "bool": {
|
||||
return borsh.bool(field.name);
|
||||
}
|
||||
case "u8": {
|
||||
return borsh.u8(field.name);
|
||||
}
|
||||
case "u32": {
|
||||
return borsh.u32(field.name);
|
||||
}
|
||||
case "u64": {
|
||||
return borsh.u64(field.name);
|
||||
}
|
||||
case "i64": {
|
||||
return borsh.i64(field.name);
|
||||
}
|
||||
case "bytes": {
|
||||
return borsh.vecU8(field.name);
|
||||
}
|
||||
case "string": {
|
||||
return borsh.str(field.name);
|
||||
}
|
||||
case "publicKey": {
|
||||
return borsh.publicKey(field.name);
|
||||
}
|
||||
// TODO: all the other types that need to be exported by the borsh package.
|
||||
default: {
|
||||
// @ts-ignore
|
||||
if (field.type.option) {
|
||||
return borsh.option(IdlCoder.fieldLayout({
|
||||
name: undefined,
|
||||
// @ts-ignore
|
||||
type: field.type.option,
|
||||
}, types), field.name)
|
||||
// @ts-ignore
|
||||
} else if (field.type.defined) {
|
||||
// User defined type.
|
||||
if (types === undefined) {
|
||||
throw new IdlError('User defined types not provided');
|
||||
}
|
||||
// @ts-ignore
|
||||
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);
|
||||
} else {
|
||||
throw new Error(`Not yet implemented: ${field}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static typeDefLayout(typeDef: IdlTypeDef, types: IdlTypeDef[], name?: string): Layout {
|
||||
if (typeDef.type.kind === 'struct') {
|
||||
const fieldLayouts = typeDef
|
||||
.type
|
||||
.fields
|
||||
.map(field => IdlCoder.fieldLayout(field, types));
|
||||
return borsh.struct(fieldLayouts, name);
|
||||
} else {
|
||||
// TODO: enums
|
||||
throw new Error('Enums not yet implemented');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export class IdlError extends Error {}
|
|
@ -0,0 +1,70 @@
|
|||
export type Idl = {
|
||||
version: string;
|
||||
name: string;
|
||||
instructions: IdlInstruction[];
|
||||
accounts?: IdlTypeDef[];
|
||||
types?: IdlTypeDef[];
|
||||
}
|
||||
|
||||
export type IdlInstruction = {
|
||||
name: string;
|
||||
accounts: IdlAccount[];
|
||||
args: IdlField[];
|
||||
}
|
||||
|
||||
export type IdlAccount = {
|
||||
name: string;
|
||||
isMut: boolean;
|
||||
isSigner: boolean;
|
||||
}
|
||||
|
||||
export type IdlField = {
|
||||
name: string;
|
||||
type: IdlType;
|
||||
}
|
||||
|
||||
export type IdlTypeDef = {
|
||||
name: string;
|
||||
type: IdlTypeDefTy;
|
||||
};
|
||||
|
||||
type IdlTypeDefTy = {
|
||||
kind: "struct" | "enum";
|
||||
fields?: IdlTypeDefStruct;
|
||||
variants?: IdlTypeDefEnum;
|
||||
};
|
||||
|
||||
type IdlTypeDefStruct = Array<IdlField>;
|
||||
|
||||
// TODO
|
||||
type IdlTypeDefEnum = {
|
||||
variants: IdlEnumVariant;
|
||||
};
|
||||
|
||||
type IdlType = "bool"
|
||||
| "u8"
|
||||
| "i8"
|
||||
| "u16"
|
||||
| "i16"
|
||||
| "u32"
|
||||
| "i32"
|
||||
| "u64"
|
||||
| "i64"
|
||||
| "bytes"
|
||||
| "string"
|
||||
| "publicKey"
|
||||
| IdlTypeOption
|
||||
| IdlTypeDefined;
|
||||
|
||||
export type IdlTypeOption = {
|
||||
option: IdlType;
|
||||
};
|
||||
|
||||
// User defined type.
|
||||
export type IdlTypeDefined = {
|
||||
defined: string;
|
||||
};
|
||||
|
||||
type IdlEnumVariant = {
|
||||
// todo
|
||||
};
|
|
@ -1,29 +1,20 @@
|
|||
import { PublicKey } from '@solana/web3.js';
|
||||
import { Program } from './program';
|
||||
import Coder from './coder';
|
||||
import { Provider } from '@project-serum/common';
|
||||
|
||||
export class Program {
|
||||
/**
|
||||
* Address of the program.
|
||||
*/
|
||||
public programId: PublicKey;
|
||||
let _provider: Provider | null = null;
|
||||
|
||||
/**
|
||||
* The inner variables required to implement the Program object.
|
||||
*/
|
||||
public _inner: ProgramInner;
|
||||
|
||||
public constructor(idl: Idl, programId: PublicKey, options?: ProgramOptions) {
|
||||
this.programId = programId;
|
||||
this._inner = {
|
||||
options: options === undefined ? {} : options,
|
||||
};
|
||||
console.log("building",idl);
|
||||
}
|
||||
function setProvider(provider: Provider) {
|
||||
_provider = provider;
|
||||
}
|
||||
|
||||
type Idl = any;
|
||||
|
||||
type ProgramInner = {
|
||||
options: ProgramOptions;
|
||||
function getProvider(): Provider {
|
||||
return _provider;
|
||||
}
|
||||
|
||||
type ProgramOptions = {};
|
||||
export {
|
||||
Program,
|
||||
Coder,
|
||||
setProvider,
|
||||
getProvider,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
import { PublicKey } from '@solana/web3.js';
|
||||
import { RpcFactory } from './rpc';
|
||||
import { Idl } from './idl';
|
||||
import Coder from './coder';
|
||||
import { Rpcs, Ixs } from './rpc';
|
||||
|
||||
/**
|
||||
* Program is the IDL deserialized representation of a Solana program.
|
||||
*/
|
||||
export class Program {
|
||||
/**
|
||||
* Address of the program.
|
||||
*/
|
||||
readonly programId: PublicKey;
|
||||
|
||||
/**
|
||||
* IDL describing this program's interface.
|
||||
*/
|
||||
readonly idl: Idl;
|
||||
|
||||
/**
|
||||
* Async functions to invoke instructions against a Solana priogram running
|
||||
* on a cluster.
|
||||
*/
|
||||
readonly rpc: Rpcs;
|
||||
|
||||
/**
|
||||
* Functions to build `TransactionInstruction` objects.
|
||||
*/
|
||||
readonly instruction: Ixs;
|
||||
|
||||
public constructor(idl: Idl, programId: PublicKey) {
|
||||
this.idl = idl;
|
||||
this.programId = programId;
|
||||
|
||||
// Build the serializer.
|
||||
const coder = new Coder(idl);
|
||||
|
||||
// Build the dynamic RPC functions.
|
||||
const [rpcs, ixs] = RpcFactory.build(idl, coder, programId);
|
||||
this.rpc = rpcs;
|
||||
this.instruction = ixs;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,157 @@
|
|||
import camelCase from 'camelcase';
|
||||
import { Account, PublicKey, ConfirmOptions, Transaction, TransactionSignature, TransactionInstruction } from '@solana/web3.js';
|
||||
import { Idl, IdlInstruction } from './idl';
|
||||
import { IdlError } from './error';
|
||||
import Coder from './coder';
|
||||
import { getProvider } from './';
|
||||
|
||||
/**
|
||||
* Rpcs is a dynamically generated object with rpc methods attached.
|
||||
*/
|
||||
export interface Rpcs {
|
||||
[key: string]: Rpc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ixs is a dynamically generated object with ix functions attached.
|
||||
*/
|
||||
export interface Ixs {
|
||||
[key: string]: Ix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rpc is a single rpc method.
|
||||
*/
|
||||
export type Rpc = (ctx: RpcContext, ...args: any[]) => Promise<any>;
|
||||
|
||||
/**
|
||||
* Ix is a function to create a `TransactionInstruction`.
|
||||
*/
|
||||
export type Ix = (ctx: RpcContext, ...args: any[]) => TransactionInstruction;
|
||||
|
||||
/**
|
||||
* Options for an RPC invocation.
|
||||
*/
|
||||
type RpcOptions = ConfirmOptions;
|
||||
|
||||
/**
|
||||
* RpcContext provides all arguments for an RPC/IX invocation that are not
|
||||
* covered by the instruction enum.
|
||||
*/
|
||||
type RpcContext = {
|
||||
options?: RpcOptions;
|
||||
accounts: RpcAccounts;
|
||||
// Instructions to run *before* the specified rpc instruction.
|
||||
instructions?: TransactionInstruction[];
|
||||
signers?: Array<Account>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamic object representing a set of accounts given to an rpc/ix invocation.
|
||||
* The name of each key should match the name for that account in the IDL.
|
||||
*/
|
||||
type RpcAccounts = {
|
||||
[key: string]: PublicKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* RpcFactory builds an Rpcs object for a given IDL.
|
||||
*/
|
||||
export class RpcFactory {
|
||||
|
||||
/**
|
||||
* build dynamically generates RPC methods.
|
||||
*
|
||||
* @returns an object with all the RPC methods attached.
|
||||
*/
|
||||
public static build(idl: Idl, coder: Coder, programId: PublicKey): [Rpcs, Ixs] {
|
||||
const rpcs: Rpcs = {};
|
||||
const ixFns: Ixs = {};
|
||||
idl.instructions.forEach(idlIx=> {
|
||||
// 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 name = camelCase(idlIx.name);
|
||||
rpcs[name] = rpc;
|
||||
ixFns[name] = ix;
|
||||
});
|
||||
return [rpcs, ixFns];
|
||||
}
|
||||
|
||||
private static buildIx(idlIx: IdlInstruction, coder: Coder, programId: PublicKey): Ix {
|
||||
if (idlIx.name === '_inner') {
|
||||
throw new IdlError('the _inner name is reserved');
|
||||
}
|
||||
|
||||
const ix = (ctx: RpcContext, ...args: any[]): TransactionInstruction => {
|
||||
validateAccounts(idlIx, ctx.accounts);
|
||||
validateInstruction(idlIx, args)
|
||||
|
||||
const keys = idlIx
|
||||
.accounts
|
||||
.map(acc => {
|
||||
return { pubkey: ctx.accounts[acc.name], isWritable: acc.isMut, isSigner: acc.isSigner, }
|
||||
});
|
||||
|
||||
return new TransactionInstruction({
|
||||
keys,
|
||||
programId,
|
||||
data: coder.instruction.encode(toInstruction(idlIx, args)),
|
||||
});
|
||||
};
|
||||
|
||||
return ix;
|
||||
}
|
||||
|
||||
private static buildRpc(ixFn: Ix): Rpc {
|
||||
const rpc = async (ctx: RpcContext, ...args: any[]): Promise<TransactionSignature> => {
|
||||
const tx = new Transaction();
|
||||
if (ctx.instructions !== undefined) {
|
||||
tx.add(...ctx.instructions);
|
||||
}
|
||||
tx.add(ixFn(ctx, ...args));
|
||||
const provider = getProvider();
|
||||
if (provider === null) {
|
||||
throw new Error('Provider not found');
|
||||
}
|
||||
|
||||
const txSig = await provider.send(tx, ctx.signers);
|
||||
return txSig;
|
||||
};
|
||||
|
||||
return rpc;
|
||||
}
|
||||
}
|
||||
|
||||
function toInstruction(idlIx: IdlInstruction, ...args: any[]) {
|
||||
if (idlIx.args.length != args.length) {
|
||||
throw new Error('Invalid argument length');
|
||||
}
|
||||
const ix: { [key: string]: any } = {};
|
||||
let idx = 0;
|
||||
idlIx.args.forEach(ixArg => {
|
||||
ix[ixArg.name] = args[idx];
|
||||
idx += 1;
|
||||
});
|
||||
return ix;
|
||||
}
|
||||
|
||||
// Throws error if any account required for the `ix` is not given.
|
||||
function validateAccounts(ix: IdlInstruction, accounts: RpcAccounts) {
|
||||
ix.accounts.forEach(acc => {
|
||||
if (accounts[acc.name] === undefined) {
|
||||
throw new Error(`Invalid arguments: ${acc.name} not provided.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Throws error if any argument required for the `ix` is not given.
|
||||
function validateInstruction(ix: IdlInstruction, ...args: any[]) {
|
||||
// todo
|
||||
}
|
|
@ -1,10 +1,12 @@
|
|||
const anchor = require('.');
|
||||
|
||||
console.log(anchor)
|
||||
function test() {
|
||||
const fs = require('fs');
|
||||
const idl = JSON.parse(fs.readFileSync('../examples/basic/idl.json', 'utf8'));
|
||||
const program = new anchor.Program(idl);
|
||||
|
||||
console.log('RPCS', program.rpc);
|
||||
console.log('IXS', program.instruction);
|
||||
}
|
||||
|
||||
test();
|
||||
|
|
|
@ -797,6 +797,13 @@
|
|||
dependencies:
|
||||
"@babel/types" "^7.3.0"
|
||||
|
||||
"@types/bn.js@^4.11.6":
|
||||
version "4.11.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-4.11.6.tgz#c306c70d9358aaea33cd4eda092a742b9505967c"
|
||||
integrity sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/connect@^3.4.33":
|
||||
version "3.4.34"
|
||||
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.34.tgz#170a40223a6d666006d93ca128af2beb1d9b1901"
|
||||
|
|
Loading…
Reference in New Issue