diff --git a/CHANGELOG.md b/CHANGELOG.md index c972c735..7d26c336 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/Cargo.lock b/Cargo.lock index 2cb0789a..fc4693f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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]] diff --git a/client/example/src/main.rs b/client/example/src/main.rs index 13e6cc32..d4ebc87e 100644 --- a/client/example/src/main.rs +++ b/client/example/src/main.rs @@ -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!"); diff --git a/client/src/lib.rs b/client/src/lib.rs index ac1a1962..20ee66f0 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -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 } diff --git a/examples/multisig/tests/multisig.js b/examples/multisig/tests/multisig.js index bf7ac32c..52eba799 100644 --- a/examples/multisig/tests/multisig.js +++ b/examples/multisig/tests/multisig.js @@ -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(); diff --git a/lang/src/lib.rs b/lang/src/lib.rs index 976d8080..d7b6d630 100644 --- a/lang/src/lib.rs +++ b/lang/src/lib.rs @@ -159,6 +159,14 @@ pub trait AccountDeserialize: Sized { fn try_deserialize_unchecked(buf: &mut &[u8]) -> Result; } +/// Calculates the data for an instruction invocation, where the data is +/// `Sha256(::)[..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; +} + /// The prelude contains all commonly used components of the crate. /// All programs should include it via `anchor_lang::prelude::*;`. pub mod prelude { diff --git a/lang/syn/src/codegen/program.rs b/lang/syn/src/codegen/program.rs index fd19af08..ce3f8a52 100644 --- a/lang/syn/src/codegen/program.rs +++ b/lang/syn/src/codegen/program.rs @@ -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> { } 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 = 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 = 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 { + 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 = 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 { + 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 +} diff --git a/lang/syn/src/lib.rs b/lang/syn/src/lib.rs index 5f59c725..6c76706e 100644 --- a/lang/syn/src/lib.rs +++ b/lang/syn/src/lib.rs @@ -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; diff --git a/ts/package.json b/ts/package.json index 3fffcada..1c377a59 100644 --- a/ts/package.json +++ b/ts/package.json @@ -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", diff --git a/ts/src/coder.ts b/ts/src/coder.ts index c318150b..b562d30d 100644 --- a/ts/src/coder.ts +++ b/ts/src/coder.ts @@ -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 { +class InstructionCoder { /** - * Instruction enum layout. + * Instruction args layout. Maps namespaced method */ - private ixLayout: Layout; + private ixLayout: Map; 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 { + 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 { 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 { - 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 { - 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); +} diff --git a/ts/src/rpc.ts b/ts/src/rpc.ts index 529c6896..c4bc06fd 100644 --- a/ts/src/rpc.ts +++ b/ts/src/rpc.ts @@ -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. diff --git a/ts/yarn.lock b/ts/yarn.lock index eacf2c61..5025bbb9 100644 --- a/ts/yarn.lock +++ b/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"