Migrate to sighash based method dispatch (#64)
This commit is contained in:
parent
170e6f18d4
commit
48b27e6943
|
@ -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.
|
||||
|
|
|
@ -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]]
|
||||
|
|
|
@ -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!");
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
43
ts/yarn.lock
43
ts/yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue