Migrate to sighash based method dispatch (#64)

This commit is contained in:
Armani Ferrante 2021-02-06 16:28:33 +08:00 committed by GitHub
parent 170e6f18d4
commit 48b27e6943
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 344 additions and 155 deletions

View File

@ -11,6 +11,8 @@ incremented for features.
## [Unreleased]
* lang, client, ts: Migrate from rust enum based method dispatch to a variant of sighash [(#64)](https://github.com/project-serum/anchor/pull/64).
## [0.1.0] - 2021-01-31
Initial release.

3
Cargo.lock generated
View File

@ -176,12 +176,15 @@ name = "anchor-syn"
version = "0.1.0"
dependencies = [
"anyhow",
"bs58",
"heck",
"proc-macro2 1.0.24",
"quote 1.0.8",
"serde",
"serde_json",
"sha2 0.9.3",
"syn 1.0.57",
"thiserror",
]
[[package]]

View File

@ -6,12 +6,12 @@ use anchor_client::solana_sdk::sysvar;
use anchor_client::Client;
use anyhow::Result;
// The `accounts` and `instructions` modules are generated by the framework.
use basic_2::accounts::CreateAuthor;
use basic_2::instruction::Basic2Instruction;
use basic_2::Author;
use basic_2::accounts as basic_2_accounts;
use basic_2::instruction as basic_2_instruction;
use basic_2::Counter;
// The `accounts` and `instructions` modules are generated by the framework.
use composite::accounts::{Bar, CompositeUpdate, Foo, Initialize};
use composite::instruction::CompositeInstruction;
use composite::instruction as composite_instruction;
use composite::{DummyA, DummyB};
use rand::rngs::OsRng;
@ -38,7 +38,7 @@ fn main() -> Result<()> {
// Make sure to run a localnet with the program deploy to run this example.
fn composite(client: &Client) -> Result<()> {
// Deployed program to execute.
let pid = "75TykCe6b1oBa8JWVvfkXsFbZydgqi3QfRjgBEJJwy2g"
let pid = "CD4y4hpiqB9N3vo2bAmZofsZuFmCnScqDPXejZSTeCV9"
.parse()
.unwrap();
@ -73,7 +73,7 @@ fn composite(client: &Client) -> Result<()> {
dummy_b: dummy_b.pubkey(),
rent: sysvar::rent::ID,
})
.args(CompositeInstruction::Initialize)
.args(composite_instruction::Initialize)
.send()?;
// Assert the transaction worked.
@ -93,7 +93,7 @@ fn composite(client: &Client) -> Result<()> {
dummy_b: dummy_b.pubkey(),
},
})
.args(CompositeInstruction::CompositeUpdate {
.args(composite_instruction::CompositeUpdate {
dummy_a: 1234,
dummy_b: 4321,
})
@ -115,14 +115,14 @@ fn composite(client: &Client) -> Result<()> {
// Make sure to run a localnet with the program deploy to run this example.
fn basic_2(client: &Client) -> Result<()> {
// Deployed program to execute.
let program_id = "FU3yvTEGTFUdMa6qAjVyKfNcDU6hb4yXbPhz8f5iFyvE"
let program_id = "DXfgYBD7A3DvFDJoCTcS81EnyxfwXyeYadH5VdKMhVEx"
.parse()
.unwrap();
let program = client.program(program_id);
// `CreateAuthor` parameters.
let author = Keypair::generate(&mut OsRng);
// `Create` parameters.
let counter = Keypair::generate(&mut OsRng);
let authority = program.payer();
// Build and send a transaction.
@ -130,26 +130,23 @@ fn basic_2(client: &Client) -> Result<()> {
.request()
.instruction(system_instruction::create_account(
&authority,
&author.pubkey(),
&counter.pubkey(),
program.rpc().get_minimum_balance_for_rent_exemption(500)?,
500,
&program_id,
))
.signer(&author)
.accounts(CreateAuthor {
author: author.pubkey(),
.signer(&counter)
.accounts(basic_2_accounts::Create {
counter: counter.pubkey(),
rent: sysvar::rent::ID,
})
.args(Basic2Instruction::CreateAuthor {
authority,
name: "My Book Name".to_string(),
})
.args(basic_2_instruction::Create { authority })
.send()?;
let author_account: Author = program.account(author.pubkey())?;
let counter_account: Counter = program.account(counter.pubkey())?;
assert_eq!(author_account.authority, authority);
assert_eq!(author_account.name, "My Book Name".to_string());
assert_eq!(counter_account.authority, authority);
assert_eq!(counter_account.count, 0);
println!("Success!");

View File

@ -4,7 +4,7 @@
use anchor_lang::solana_program::instruction::{AccountMeta, Instruction};
use anchor_lang::solana_program::program_error::ProgramError;
use anchor_lang::solana_program::pubkey::Pubkey;
use anchor_lang::{AccountDeserialize, AnchorSerialize, ToAccountMetas};
use anchor_lang::{AccountDeserialize, InstructionData, ToAccountMetas};
use solana_client::client_error::ClientError as SolanaClientError;
use solana_client::rpc_client::RpcClient;
use solana_sdk::commitment_config::CommitmentConfig;
@ -185,9 +185,8 @@ impl<'a> RequestBuilder<'a> {
self
}
pub fn args(mut self, args: impl AnchorSerialize) -> Self {
let data = args.try_to_vec().expect("Should always serialize");
self.instruction_data = Some(data);
pub fn args(mut self, args: impl InstructionData) -> Self {
self.instruction_data = Some(args.data());
self
}

View File

@ -7,7 +7,7 @@ describe("multisig", () => {
const program = anchor.workspace.Multisig;
it("Is initialized!", async () => {
it("Tests the multisig program", async () => {
const multisig = new anchor.web3.Account();
const [
multisigSigner,
@ -58,10 +58,8 @@ describe("multisig", () => {
},
];
const newOwners = [ownerA.publicKey, ownerB.publicKey];
const data = program.coder.instruction.encode({
setOwners: {
const data = program.coder.instruction.encode('set_owners', {
owners: newOwners,
},
});
const transaction = new anchor.web3.Account();

View File

@ -159,6 +159,14 @@ pub trait AccountDeserialize: Sized {
fn try_deserialize_unchecked(buf: &mut &[u8]) -> Result<Self, ProgramError>;
}
/// Calculates the data for an instruction invocation, where the data is
/// `Sha256(<namespace>::<method_name>)[..8] || BorshSerialize(args)`.
/// `args` is a borsh serialized struct of named fields for each argument given
/// to an instruction.
pub trait InstructionData: AnchorSerialize {
fn data(&self) -> Vec<u8>;
}
/// The prelude contains all commonly used components of the crate.
/// All programs should include it via `anchor_lang::prelude::*;`.
pub mod prelude {

View File

@ -3,13 +3,19 @@ use crate::{Program, RpcArg, State};
use heck::{CamelCase, SnakeCase};
use quote::quote;
// Namespace for calculating state instruction sighash signatures.
const SIGHASH_STATE_NAMESPACE: &'static str = "state";
// Namespace for calculating instruction sighash signatures for any instruction
// not affecting program state.
const SIGHASH_GLOBAL_NAMESPACE: &'static str = "global";
pub fn generate(program: Program) -> proc_macro2::TokenStream {
let mod_name = &program.name;
let instruction_name = instruction_enum_name(&program);
let dispatch = generate_dispatch(&program);
let handlers_non_inlined = generate_non_inlined_handlers(&program);
let methods = generate_methods(&program);
let instruction = generate_instruction(&program);
let instructions = generate_instructions(&program);
let cpi = generate_cpi(&program);
let accounts = generate_accounts(&program);
@ -17,21 +23,27 @@ pub fn generate(program: Program) -> proc_macro2::TokenStream {
// TODO: remove once we allow segmented paths in `Accounts` structs.
use #mod_name::*;
#[cfg(not(feature = "no-entrypoint"))]
anchor_lang::solana_program::entrypoint!(entry);
#[cfg(not(feature = "no-entrypoint"))]
fn entry(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult {
if instruction_data.len() < 8 {
return Err(ProgramError::Custom(99));
}
let mut instruction_data: &[u8] = instruction_data;
let sighash: [u8; 8] = {
let mut sighash: [u8; 8] = [0; 8];
sighash.copy_from_slice(&instruction_data[..8]);
instruction_data = &instruction_data[8..];
sighash
};
if cfg!(not(feature = "no-idl")) {
if instruction_data.len() >= 8 {
if anchor_lang::idl::IDL_IX_TAG.to_le_bytes() == instruction_data[..8] {
return __private::__idl(program_id, accounts, &instruction_data[8..]);
}
if sighash == anchor_lang::idl::IDL_IX_TAG.to_le_bytes() {
return __private::__idl(program_id, accounts, &instruction_data[8..]);
}
}
let mut data: &[u8] = instruction_data;
let ix = instruction::#instruction_name::deserialize(&mut data)
.map_err(|_| ProgramError::Custom(1))?; // todo: error code
#dispatch
}
@ -45,7 +57,7 @@ pub fn generate(program: Program) -> proc_macro2::TokenStream {
#accounts
#instruction
#instructions
#methods
@ -57,10 +69,19 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
let ctor_state_dispatch_arm = match &program.state {
None => quote! { /* no-op */ },
Some(state) => {
let variant_arm = generate_ctor_variant(program, state);
let variant_arm = generate_ctor_variant(state);
let ctor_args = generate_ctor_args(state);
let ix_name: proc_macro2::TokenStream = generate_ctor_variant_name().parse().unwrap();
let sighash_arr = sighash_ctor();
let sighash_tts: proc_macro2::TokenStream =
format!("{:?}", sighash_arr).parse().unwrap();
quote! {
instruction::#variant_arm => __private::__ctor(program_id, accounts, #(#ctor_args),*),
#sighash_tts => {
let ix = instruction::#ix_name::deserialize(&mut instruction_data)
.map_err(|_| ProgramError::Custom(1))?; // todo: error code
let instruction::#variant_arm = ix;
__private::__ctor(program_id, accounts, #(#ctor_args),*)
}
}
}
};
@ -72,18 +93,19 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
.map(|rpc: &crate::StateRpc| {
let rpc_arg_names: Vec<&syn::Ident> =
rpc.args.iter().map(|arg| &arg.name).collect();
let variant_arm: proc_macro2::TokenStream = generate_ix_variant(
program,
rpc.raw_method.sig.ident.to_string(),
&rpc.args,
true,
);
let rpc_name: proc_macro2::TokenStream = {
let name = &rpc.raw_method.sig.ident.to_string();
format!("__{}", name).parse().unwrap()
};
let name = &rpc.raw_method.sig.ident.to_string();
let rpc_name: proc_macro2::TokenStream = { format!("__{}", name).parse().unwrap() };
let variant_arm =
generate_ix_variant(rpc.raw_method.sig.ident.to_string(), &rpc.args, true);
let ix_name = generate_ix_variant_name(rpc.raw_method.sig.ident.to_string(), true);
let sighash_arr = sighash(SIGHASH_STATE_NAMESPACE, &name);
let sighash_tts: proc_macro2::TokenStream =
format!("{:?}", sighash_arr).parse().unwrap();
quote! {
instruction::#variant_arm => {
#sighash_tts => {
let ix = instruction::#ix_name::deserialize(&mut instruction_data)
.map_err(|_| ProgramError::Custom(1))?; // todo: error code
let instruction::#variant_arm = ix;
__private::#rpc_name(program_id, accounts, #(#rpc_arg_names),*)
}
}
@ -95,15 +117,18 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
.iter()
.map(|rpc| {
let rpc_arg_names: Vec<&syn::Ident> = rpc.args.iter().map(|arg| &arg.name).collect();
let variant_arm = generate_ix_variant(
program,
rpc.raw_method.sig.ident.to_string(),
&rpc.args,
false,
);
let rpc_name = &rpc.raw_method.sig.ident;
let ix_name = generate_ix_variant_name(rpc.raw_method.sig.ident.to_string(), false);
let sighash_arr = sighash(SIGHASH_GLOBAL_NAMESPACE, &rpc_name.to_string());
let sighash_tts: proc_macro2::TokenStream =
format!("{:?}", sighash_arr).parse().unwrap();
let variant_arm =
generate_ix_variant(rpc.raw_method.sig.ident.to_string(), &rpc.args, false);
quote! {
instruction::#variant_arm => {
#sighash_tts => {
let ix = instruction::#ix_name::deserialize(&mut instruction_data)
.map_err(|_| ProgramError::Custom(1))?; // todo: error code
let instruction::#variant_arm = ix;
__private::#rpc_name(program_id, accounts, #(#rpc_arg_names),*)
}
}
@ -111,10 +136,14 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
.collect();
quote! {
match ix {
match sighash {
#ctor_state_dispatch_arm
#(#state_dispatch_arms),*
#(#dispatch_arms),*
#(#state_dispatch_arms)*
#(#dispatch_arms)*
_ => {
msg!("Fallback functions are not supported. If you have a use case, please file an issue.");
Err(ProgramError::Custom(99))
}
}
}
}
@ -426,36 +455,42 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr
}
}
pub fn generate_ctor_variant(program: &Program, state: &State) -> proc_macro2::TokenStream {
let enum_name = instruction_enum_name(program);
pub fn generate_ctor_variant(state: &State) -> proc_macro2::TokenStream {
let ctor_args = generate_ctor_args(state);
let ctor_variant_name: proc_macro2::TokenStream = generate_ctor_variant_name().parse().unwrap();
if ctor_args.len() == 0 {
quote! {
#enum_name::__Ctor
#ctor_variant_name
}
} else {
quote! {
#enum_name::__Ctor {
#ctor_variant_name {
#(#ctor_args),*
}
}
}
}
pub fn generate_ctor_typed_variant_with_comma(program: &Program) -> proc_macro2::TokenStream {
pub fn generate_ctor_variant_name() -> String {
"__Ctor".to_string()
}
pub fn generate_ctor_typed_variant_with_semi(program: &Program) -> proc_macro2::TokenStream {
match &program.state {
None => quote! {},
Some(state) => {
let ctor_args = generate_ctor_typed_args(state);
if ctor_args.len() == 0 {
quote! {
__Ctor,
#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct __Ctor;
}
} else {
quote! {
__Ctor {
#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct __Ctor {
#(#ctor_args),*
},
};
}
}
}
@ -503,12 +538,10 @@ fn generate_ctor_args(state: &State) -> Vec<Box<syn::Pat>> {
}
pub fn generate_ix_variant(
program: &Program,
name: String,
args: &[RpcArg],
underscore: bool,
) -> proc_macro2::TokenStream {
let enum_name = instruction_enum_name(program);
let rpc_arg_names: Vec<&syn::Ident> = args.iter().map(|arg| &arg.name).collect();
let rpc_name_camel: proc_macro2::TokenStream = {
let n = name.to_camel_case();
@ -521,17 +554,26 @@ pub fn generate_ix_variant(
if args.len() == 0 {
quote! {
#enum_name::#rpc_name_camel
#rpc_name_camel
}
} else {
quote! {
#enum_name::#rpc_name_camel {
#rpc_name_camel {
#(#rpc_arg_names),*
}
}
}
}
pub fn generate_ix_variant_name(name: String, underscore: bool) -> proc_macro2::TokenStream {
let n = name.to_camel_case();
if underscore {
format!("__{}", n).parse().unwrap()
} else {
n.parse().unwrap()
}
}
pub fn generate_methods(program: &Program) -> proc_macro2::TokenStream {
let program_mod = &program.program_mod;
quote! {
@ -539,9 +581,8 @@ pub fn generate_methods(program: &Program) -> proc_macro2::TokenStream {
}
}
pub fn generate_instruction(program: &Program) -> proc_macro2::TokenStream {
let enum_name = instruction_enum_name(program);
let ctor_variant = generate_ctor_typed_variant_with_comma(program);
pub fn generate_instructions(program: &Program) -> proc_macro2::TokenStream {
let ctor_variant = generate_ctor_typed_variant_with_semi(program);
let state_method_variants: Vec<proc_macro2::TokenStream> = match &program.state {
None => vec![],
Some(state) => state
@ -555,18 +596,48 @@ pub fn generate_instruction(program: &Program) -> proc_macro2::TokenStream {
);
name.parse().unwrap()
};
let raw_args: Vec<&syn::PatType> =
method.args.iter().map(|arg| &arg.raw_arg).collect();
let raw_args: Vec<proc_macro2::TokenStream> = method
.args
.iter()
.map(|arg| {
format!("pub {}", parser::tts_to_string(&arg.raw_arg))
.parse()
.unwrap()
})
.collect();
let ix_data_trait = {
let name = method.raw_method.sig.ident.to_string();
let sighash_arr = sighash(SIGHASH_GLOBAL_NAMESPACE, &name);
let sighash_tts: proc_macro2::TokenStream =
format!("{:?}", sighash_arr).parse().unwrap();
quote! {
impl anchor_lang::InstructionData for #rpc_name_camel {
fn data(&self) -> Vec<u8> {
let mut d = #sighash_tts.to_vec();
d.append(&mut self.try_to_vec().expect("Should always serialize"));
d
}
}
}
};
// If no args, output a "unit" variant instead of a struct variant.
if method.args.len() == 0 {
quote! {
#rpc_name_camel,
#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct #rpc_name_camel;
#ix_data_trait
}
} else {
quote! {
#rpc_name_camel {
#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct #rpc_name_camel {
#(#raw_args),*
},
}
#ix_data_trait
}
}
})
@ -576,21 +647,48 @@ pub fn generate_instruction(program: &Program) -> proc_macro2::TokenStream {
.rpcs
.iter()
.map(|rpc| {
let rpc_name_camel = proc_macro2::Ident::new(
&rpc.raw_method.sig.ident.to_string().to_camel_case(),
rpc.raw_method.sig.ident.span(),
);
let raw_args: Vec<&syn::PatType> = rpc.args.iter().map(|arg| &arg.raw_arg).collect();
let name = &rpc.raw_method.sig.ident.to_string();
let rpc_name_camel =
proc_macro2::Ident::new(&name.to_camel_case(), rpc.raw_method.sig.ident.span());
let raw_args: Vec<proc_macro2::TokenStream> = rpc
.args
.iter()
.map(|arg| {
format!("pub {}", parser::tts_to_string(&arg.raw_arg))
.parse()
.unwrap()
})
.collect();
let ix_data_trait = {
let sighash_arr = sighash(SIGHASH_GLOBAL_NAMESPACE, &name);
let sighash_tts: proc_macro2::TokenStream =
format!("{:?}", sighash_arr).parse().unwrap();
quote! {
impl anchor_lang::InstructionData for #rpc_name_camel {
fn data(&self) -> Vec<u8> {
let mut d = #sighash_tts.to_vec();
d.append(&mut self.try_to_vec().expect("Should always serialize"));
d
}
}
}
};
// If no args, output a "unit" variant instead of a struct variant.
if rpc.args.len() == 0 {
quote! {
#rpc_name_camel
#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct #rpc_name_camel;
#ix_data_trait
}
} else {
quote! {
#rpc_name_camel {
#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct #rpc_name_camel {
#(#raw_args),*
}
#ix_data_trait
}
}
})
@ -603,23 +701,14 @@ pub fn generate_instruction(program: &Program) -> proc_macro2::TokenStream {
/// specifying instructions on a client.
pub mod instruction {
use super::*;
#[derive(AnchorSerialize, AnchorDeserialize)]
pub enum #enum_name {
#ctor_variant
#(#state_method_variants)*
#(#variants),*
}
#ctor_variant
#(#state_method_variants)*
#(#variants)*
}
}
}
fn instruction_enum_name(program: &Program) -> proc_macro2::Ident {
proc_macro2::Ident::new(
&format!("{}Instruction", program.name.to_string().to_camel_case()),
program.name.span(),
)
}
fn generate_accounts(program: &Program) -> proc_macro2::TokenStream {
let mut accounts = std::collections::HashSet::new();
@ -678,14 +767,14 @@ fn generate_cpi(program: &Program) -> proc_macro2::TokenStream {
.map(|rpc| {
let accounts_ident = &rpc.anchor_ident;
let cpi_method = {
let ix_variant = generate_ix_variant(
program,
rpc.raw_method.sig.ident.to_string(),
&rpc.args,
false,
);
let ix_variant =
generate_ix_variant(rpc.raw_method.sig.ident.to_string(), &rpc.args, false);
let method_name = &rpc.ident;
let args: Vec<&syn::PatType> = rpc.args.iter().map(|arg| &arg.raw_arg).collect();
let name = &rpc.raw_method.sig.ident.to_string();
let sighash_arr = sighash(SIGHASH_GLOBAL_NAMESPACE, &name);
let sighash_tts: proc_macro2::TokenStream =
format!("{:?}", sighash_arr).parse().unwrap();
quote! {
pub fn #method_name<'a, 'b, 'c, 'info>(
ctx: CpiContext<'a, 'b, 'c, 'info, #accounts_ident<'info>>,
@ -693,8 +782,10 @@ fn generate_cpi(program: &Program) -> proc_macro2::TokenStream {
) -> ProgramResult {
let ix = {
let ix = instruction::#ix_variant;
let data = AnchorSerialize::try_to_vec(&ix)
let mut ix_data = AnchorSerialize::try_to_vec(&ix)
.map_err(|_| ProgramError::InvalidInstructionData)?;
let mut data = #sighash_tts.to_vec();
data.append(&mut ix_data);
let accounts = ctx.accounts.to_account_metas(None);
anchor_lang::solana_program::instruction::Instruction {
program_id: *ctx.program.key,
@ -725,3 +816,24 @@ fn generate_cpi(program: &Program) -> proc_macro2::TokenStream {
}
}
}
// We don't technically use sighash, because the input arguments aren't given.
// Rust doesn't have method overloading so no need to use the arguments.
// However, we do namespace methods in the preeimage so that we can use
// different traits with the same method name.
fn sighash(namespace: &str, name: &str) -> [u8; 8] {
let preimage = format!("{}::{}", namespace, name);
let mut sighash = [0u8; 8];
sighash.copy_from_slice(&crate::hash::hash(preimage.as_bytes()).to_bytes()[..8]);
sighash
}
fn sighash_ctor() -> [u8; 8] {
let namespace = SIGHASH_STATE_NAMESPACE;
let preimage = format!("{}::new", namespace);
let mut sighash = [0u8; 8];
sighash.copy_from_slice(&crate::hash::hash(preimage.as_bytes()).to_bytes()[..8]);
sighash
}

View File

@ -11,6 +11,8 @@ use std::collections::HashMap;
pub mod codegen;
#[cfg(feature = "hash")]
pub mod hash;
#[cfg(not(feature = "hash"))]
pub(crate) mod hash;
#[cfg(feature = "idl")]
pub mod idl;
pub mod parser;

View File

@ -32,10 +32,11 @@
"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",
"pako": "^2.0.3"
"js-sha256": "^0.9.0",
"pako": "^2.0.3",
"snake-case": "^3.0.4"
},
"devDependencies": {
"@commitlint/cli": "^11.0.0",

View File

@ -1,6 +1,7 @@
import camelCase from "camelcase";
import { snakeCase } from "snake-case";
import { Layout } from "buffer-layout";
import { sha256 } from "crypto-hash";
import * as sha256 from "js-sha256";
import * as borsh from "@project-serum/borsh";
import {
Idl,
@ -16,6 +17,15 @@ import { IdlError } from "./error";
* Number of bytes of the account discriminator.
*/
export const ACCOUNT_DISCRIMINATOR_SIZE = 8;
/**
* Namespace for state method function signatures.
*/
export const SIGHASH_STATE_NAMESPACE = "state";
/**
* Namespace for global instruction function signatures (i.e. functions
* that aren't namespaced by the state or any of its trait implementations).
*/
export const SIGHASH_GLOBAL_NAMESPACE = "global";
/**
* Coder provides a facade for encoding and decoding all IDL related objects.
@ -54,35 +64,48 @@ export default class Coder {
/**
* Encodes and decodes program instructions.
*/
class InstructionCoder<T = any> {
class InstructionCoder {
/**
* Instruction enum layout.
* Instruction args layout. Maps namespaced method
*/
private ixLayout: Layout;
private ixLayout: Map<string, Layout>;
public constructor(idl: Idl) {
this.ixLayout = InstructionCoder.parseIxLayout(idl);
}
public encode(ix: T): Buffer {
/**
* Encodes a program instruction.
*/
public encode(ixName: string, ix: any) {
return this._encode(SIGHASH_GLOBAL_NAMESPACE, ixName, ix);
}
/**
* Encodes a program state instruction.
*/
public encodeState(ixName: string, ix: any) {
return this._encode(SIGHASH_STATE_NAMESPACE, ixName, ix);
}
public _encode(nameSpace: string, ixName: string, ix: any): Buffer {
const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
const len = this.ixLayout.encode(ix, buffer);
return buffer.slice(0, len);
const methodName = camelCase(ixName);
const len = this.ixLayout.get(methodName).encode(ix, buffer);
const data = buffer.slice(0, len);
return Buffer.concat([sighash(nameSpace, ixName), data]);
}
public decode(ix: Buffer): T {
return this.ixLayout.decode(ix);
}
private static parseIxLayout(idl: Idl): Map<string, Layout> {
const stateMethods = idl.state ? idl.state.methods : [];
private static parseIxLayout(idl: Idl): Layout {
let stateMethods = idl.state ? idl.state.methods : [];
let ixLayouts = stateMethods
const ixLayouts = stateMethods
.map((m: IdlStateMethod) => {
let fieldLayouts = m.args.map((arg: IdlField) =>
IdlCoder.fieldLayout(arg, idl.types)
);
let fieldLayouts = m.args.map((arg: IdlField) => {
return IdlCoder.fieldLayout(arg, idl.types);
});
const name = camelCase(m.name);
return borsh.struct(fieldLayouts, name);
return [name, borsh.struct(fieldLayouts, name)];
})
.concat(
idl.instructions.map((ix) => {
@ -90,10 +113,11 @@ class InstructionCoder<T = any> {
IdlCoder.fieldLayout(arg, idl.types)
);
const name = camelCase(ix.name);
return borsh.struct(fieldLayouts, name);
return [name, borsh.struct(fieldLayouts, name)];
})
);
return borsh.rustEnum(ixLayouts);
// @ts-ignore
return new Map(ixLayouts);
}
}
@ -320,24 +344,14 @@ 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)
);
// @ts-ignore
return Buffer.from(sha256.digest(`account:${name}`)).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(`account:${name}`, {
outputFormat: "buffer",
})
).slice(0, 8)
);
// @ts-ignore
return Buffer.from(sha256.digest(`account:${name}`)).slice(0, 8);
}
// Returns the size of the type in bytes. For variable length types, just return
@ -424,3 +438,12 @@ export function accountSize(
.map((f) => typeSize(idl, f.type))
.reduce((a, b) => a + b);
}
// Not technically sighash, since we don't include the arguments, as Rust
// doesn't allow function overloading.
function sighash(nameSpace: string, ixName: string): Buffer {
let name = snakeCase(ixName);
let preimage = `${nameSpace}::${name}`;
// @ts-ignore
return Buffer.from(sha256.digest(preimage)).slice(0, 8);
}

View File

@ -24,6 +24,8 @@ import {
import { IdlError, ProgramError } from "./error";
import Coder, {
ACCOUNT_DISCRIMINATOR_SIZE,
SIGHASH_STATE_NAMESPACE,
SIGHASH_GLOBAL_NAMESPACE,
accountDiscriminator,
stateDiscriminator,
accountSize,
@ -229,7 +231,10 @@ export class RpcFactory {
RpcFactory.accountsArray(ctx.accounts, m.accounts)
),
programId,
data: coder.instruction.encode(toInstruction(m, ...ixArgs)),
data: coder.instruction.encodeState(
m.name,
toInstruction(m, ...ixArgs)
),
})
);
try {
@ -316,12 +321,15 @@ export class RpcFactory {
}
if (ctx.__private && ctx.__private.logAccounts) {
console.log("Outoing account metas:", keys);
console.log("Outgoing account metas:", keys);
}
return new TransactionInstruction({
keys,
programId,
data: coder.instruction.encode(toInstruction(idlIx, ...ixArgs)),
data: coder.instruction.encode(
idlIx.name,
toInstruction(idlIx, ...ixArgs)
),
});
};
@ -609,12 +617,7 @@ function toInstruction(idlIx: IdlInstruction | IdlStateMethod, ...args: any[]) {
idx += 1;
});
// JavaScript representation of the rust enum variant.
const name = camelCase(idlIx.name);
const ixVariant: { [key: string]: any } = {};
ixVariant[name] = ix;
return ixVariant;
return ix;
}
// Throws error if any account required for the `ix` is not given.

View File

@ -1687,7 +1687,7 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2:
shebang-command "^2.0.0"
which "^2.0.1"
crypto-hash@^1.2.2, crypto-hash@^1.3.0:
crypto-hash@^1.2.2:
version "1.3.0"
resolved "https://registry.yarnpkg.com/crypto-hash/-/crypto-hash-1.3.0.tgz#b402cb08f4529e9f4f09346c3e275942f845e247"
integrity sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg==
@ -1923,6 +1923,14 @@ domutils@^1.5.1:
dom-serializer "0"
domelementtype "1"
dot-case@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751"
integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==
dependencies:
no-case "^3.0.4"
tslib "^2.0.3"
dot-prop@^5.1.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88"
@ -3552,6 +3560,11 @@ jest@26.6.0:
import-local "^3.0.2"
jest-cli "^26.6.0"
js-sha256@^0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966"
integrity sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==
js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@ -3923,6 +3936,13 @@ log-update@^4.0.0:
slice-ansi "^4.0.0"
wrap-ansi "^6.2.0"
lower-case@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28"
integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==
dependencies:
tslib "^2.0.3"
lru-cache@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
@ -4142,6 +4162,14 @@ nice-try@^1.0.4:
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
no-case@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d"
integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==
dependencies:
lower-case "^2.0.2"
tslib "^2.0.3"
node-addon-api@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32"
@ -5006,6 +5034,14 @@ slice-ansi@^4.0.0:
astral-regex "^2.0.0"
is-fullwidth-code-point "^3.0.0"
snake-case@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c"
integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==
dependencies:
dot-case "^3.0.4"
tslib "^2.0.3"
snapdragon-node@^2.0.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
@ -5502,6 +5538,11 @@ tslib@^1.8.1, tslib@^1.9.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@^2.0.3:
version "2.1.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==
tsutils@^3.17.1:
version "3.17.1"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759"