scg: Swap out coder for Pack trait for instructions

This commit is contained in:
armaniferrante 2020-09-25 11:41:32 -07:00 committed by Armani Ferrante
parent 0e354a5465
commit cade37335f
3 changed files with 98 additions and 148 deletions

View File

@ -16,15 +16,9 @@ use syn::parse_macro_input;
// //
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn solana_client_gen( pub fn solana_client_gen(
args: proc_macro::TokenStream, _args: proc_macro::TokenStream,
input: proc_macro::TokenStream, input: proc_macro::TokenStream,
) -> proc_macro::TokenStream { ) -> proc_macro::TokenStream {
// The one and only argument of the macro should be the Coder struct.
let coder_struct = match args.to_string().as_ref() {
"" => None,
_ => Some(parse_macro_input!(args as syn::TypePath)),
};
// Interpet token stream as the instruction `mod`. // Interpet token stream as the instruction `mod`.
let instruction_mod = parse_macro_input!(input as syn::ItemMod); let instruction_mod = parse_macro_input!(input as syn::ItemMod);
@ -58,7 +52,7 @@ pub fn solana_client_gen(
// //
// Parse the instruction enum and generate code from each enum variant. // Parse the instruction enum and generate code from each enum variant.
let (client_methods, instruction_methods, decode_and_dispatch_tree) = let (client_methods, instruction_methods, decode_and_dispatch_tree) =
enum_to_methods(&instruction_enum_item, coder_struct); enum_to_methods(&instruction_enum_item);
// Now recreate the highest level instruction `mod`, but with our new // Now recreate the highest level instruction `mod`, but with our new
// instruction_methods inside. // instruction_methods inside.
@ -85,113 +79,112 @@ pub fn solana_client_gen(
let client = quote! { let client = quote! {
use super::*; use super::*;
use solana_client_gen::solana_sdk::instruction::InstructionError;
use solana_client_gen::solana_sdk::transaction::TransactionError;
use solana_client_gen::solana_client::client_error::ClientErrorKind as RpcClientErrorKind;
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum ClientError { pub enum ClientError {
#[error("Invalid keypair filename")] #[error("Invalid keypair filename")]
InvalidKeyPairFile(String), InvalidKeyPairFile(String),
#[error("Error invoking rpc")] #[error("Error invoking rpc")]
RpcError(#[from] solana_client::client_error::ClientError), RpcError(#[from] solana_client::client_error::ClientError),
#[error("Raw error")] #[error("Raw error")]
RawError(String), RawError(String),
} }
use solana_client_gen::solana_sdk::instruction::InstructionError; impl ClientError {
use solana_client_gen::solana_sdk::transaction::TransactionError; // error_code returns Some(error_code) returned by the on chain program
use solana_client_gen::solana_client::client_error::ClientErrorKind as RpcClientErrorKind; // and none if the error resulted from somewhere else.
//
impl ClientError { // TODO: there's gotta be a cleaner way of unpacking this.
// error_code returns Some(error_code) returned by the on chain program pub fn error_code(&self) -> Option<u32> {
// and none if the error resulted from somewhere else. match self {
// ClientError::RpcError(e) => match e.kind() {
// TODO: there's gotta be a cleaner way of unpacking this. RpcClientErrorKind::TransactionError(e) => match e {
pub fn error_code(&self) -> Option<u32> { TransactionError::InstructionError(_, instr_error) => match instr_error {
match self { InstructionError::Custom(error_code) => {
ClientError::RpcError(e) => match e.kind() { Some(*error_code)
RpcClientErrorKind::TransactionError(e) => match e { }
TransactionError::InstructionError(_, instr_error) => match instr_error { _ => None,
InstructionError::Custom(error_code) => { },
Some(*error_code)
}
_ => None, _ => None,
}, },
_ => None, _ => None,
}, },
_ => None, _ => None,
}, }
_ => None,
} }
} }
}
#[derive(Debug)] #[derive(Debug)]
pub struct RequestOptions { pub struct RequestOptions {
pub commitment: CommitmentConfig, pub commitment: CommitmentConfig,
pub tx: RpcSendTransactionConfig, pub tx: RpcSendTransactionConfig,
} }
// Client is the RPC client generated to talk to a program running // Client is the RPC client generated to talk to a program running
// on a configured Solana cluster. // on a configured Solana cluster.
pub struct Client { pub struct Client {
program_id: Pubkey,
payer: Keypair,
rpc: RpcClient,
opts: RequestOptions,
}
impl Client {
pub fn new(
program_id: Pubkey, program_id: Pubkey,
payer: Keypair, payer: Keypair,
url: &str, rpc: RpcClient,
given_opts: Option<RequestOptions>, opts: RequestOptions,
) -> Self { }
let rpc = RpcClient::new(url.to_string());
let opts = match given_opts { impl Client {
Some(opts) => opts, pub fn new(
// Use these default options if None are given. program_id: Pubkey,
None => RequestOptions { payer: Keypair,
commitment: CommitmentConfig::single(), url: &str,
tx: RpcSendTransactionConfig { given_opts: Option<RequestOptions>,
skip_preflight: true, ) -> Self {
preflight_commitment: None, let rpc = RpcClient::new(url.to_string());
let opts = match given_opts {
Some(opts) => opts,
// Use these default options if None are given.
None => RequestOptions {
commitment: CommitmentConfig::single(),
tx: RpcSendTransactionConfig {
skip_preflight: true,
preflight_commitment: None,
},
}, },
}, };
}; Self {
Self { program_id,
program_id, payer,
payer, rpc,
rpc, opts,
opts, }
} }
}
pub fn from_keypair_file(program_id: Pubkey, filename: &str, url: &str) -> Result<Self, ClientError> { pub fn from_keypair_file(program_id: Pubkey, filename: &str, url: &str) -> Result<Self, ClientError> {
let kp = solana_sdk::signature::read_keypair_file(filename) let kp = solana_sdk::signature::read_keypair_file(filename)
.map_err(|_| ClientError::InvalidKeyPairFile(filename.to_string()))?; .map_err(|_| ClientError::InvalidKeyPairFile(filename.to_string()))?;
Ok(Self::new(program_id, kp, url, None)) Ok(Self::new(program_id, kp, url, None))
} }
// Builder method to set the default options for each RPC request. // Builder method to set the default options for each RPC request.
pub fn with_options(mut self, opts: RequestOptions) -> Self { pub fn with_options(mut self, opts: RequestOptions) -> Self {
self.opts = opts; self.opts = opts;
self self
} }
pub fn rpc(&self) -> &RpcClient { pub fn rpc(&self) -> &RpcClient {
&self.rpc &self.rpc
} }
pub fn payer(&self) -> &Keypair { pub fn payer(&self) -> &Keypair {
&self.payer &self.payer
} }
pub fn program(&self) -> &Pubkey { pub fn program(&self) -> &Pubkey {
&self.program_id &self.program_id
} }
#client_methods #client_methods
} }
}; };
// Generate the entire client module. // Generate the entire client module.
@ -277,25 +270,14 @@ pub fn solana_client_gen(
// * Client RPC methods for each instruction variant. // * Client RPC methods for each instruction variant.
// * Instruction methods for generating instances of solana_sdk::instruction::Instruction. // * Instruction methods for generating instances of solana_sdk::instruction::Instruction.
// * Decode and dispatch tree, i.e., the code to execute on entry to the program. // * Decode and dispatch tree, i.e., the code to execute on entry to the program.
// * Coder struct for serialization.
// //
fn enum_to_methods( fn enum_to_methods(
instruction_enum: &syn::ItemEnum, instruction_enum: &syn::ItemEnum,
coder_struct_opt: Option<syn::TypePath>,
) -> ( ) -> (
proc_macro2::TokenStream, proc_macro2::TokenStream,
proc_macro2::TokenStream, proc_macro2::TokenStream,
proc_macro2::TokenStream, proc_macro2::TokenStream,
) { ) {
let coder_struct = match &coder_struct_opt {
None => quote! {
solana_client_gen::coder::DefaultCoder
},
Some(cs) => quote! {
#cs
},
};
// When combined together, all the dispatch arms are used on // When combined together, all the dispatch arms are used on
// program entry, to define a `match` statement to interpret an // program entry, to define a `match` statement to interpret an
// instruction variant, and dispatch the request to the program's // instruction variant, and dispatch the request to the program's
@ -403,7 +385,13 @@ fn enum_to_methods(
let instruction = #instruction_enum; let instruction = #instruction_enum;
// Serialize. // Serialize.
let data = #coder_struct::to_bytes(instruction); let size = instruction
.size()
.expect("instructions must be serializable")
as usize;
let mut data = vec![0u8; size];
#instruction_enum_ident::pack(instruction, &mut data)
.expect("instruction must be serializable");
Instruction { Instruction {
program_id, program_id,
data, data,
@ -655,7 +643,7 @@ fn enum_to_methods(
let decode_and_dispatch_tree = quote! { let decode_and_dispatch_tree = quote! {
// Decode. // Decode.
let instruction: #instruction_enum_ident = Coder::from_bytes(instruction_data) let instruction = #instruction_enum_ident::unpack(instruction_data)
.map_err(|_| ProgramError::InvalidInstructionData); .map_err(|_| ProgramError::InvalidInstructionData);
// Dispatch. // Dispatch.
match instruction { match instruction {

View File

@ -1,21 +0,0 @@
use serde::{Deserialize, Serialize};
/// InstructionCoder is the trait that must be implemented to user
/// custom serialization with the main macro. If a coder is not
/// provided, the `DefaultCoder` will be used.
pub trait InstructionCoder<'a, T: ?Sized + Serialize + Deserialize<'a>> {
fn to_bytes(i: T) -> Vec<u8>;
fn from_bytes(data: &'a [u8]) -> Result<T, ()>;
}
pub struct DefaultCoder;
impl<'a, T: ?Sized + serde::Serialize + serde::Deserialize<'a>> InstructionCoder<'a, T>
for DefaultCoder
{
fn to_bytes(i: T) -> Vec<u8> {
serum_common::pack::to_bytes(&i).expect("instruction must be serializable")
}
fn from_bytes(data: &'a [u8]) -> Result<T, ()> {
serum_common::pack::from_bytes(data).map_err(|_| ())
}
}

View File

@ -129,23 +129,10 @@
//! } //! }
//! ``` //! ```
//! //!
//! # Using a custom coder. //! # Serialization
//! //!
//! It's assumed instructions implement serde's `Serialize` and `Deserialize`. //! Instructions used with this macro must implement the
//! //! `serum_common::pack::Pack` trait, where serialization should be defined.
//! By default, a default coder will be used to serialize instructions before
//! sending them to Solana. If you want to use a custom coder, inject it
//! into the macro like this
//!
//! ```
//! #[solana_client_gen(crate::mod::Coder)]
//! mod instruction {
//! ...
//! }
//! ```
//!
//! Where `Coder` is a user defined struct that implements the
//! `InstructionCoder` interface.
//! //!
//! # Limitations //! # Limitations
//! //!
@ -167,8 +154,6 @@ pub mod prelude {
pub use solana_sdk::instruction::{AccountMeta, Instruction}; pub use solana_sdk::instruction::{AccountMeta, Instruction};
pub use solana_sdk::pubkey::Pubkey; pub use solana_sdk::pubkey::Pubkey;
pub use crate::coder::InstructionCoder;
#[cfg(feature = "client")] #[cfg(feature = "client")]
pub use codegen::solana_client_gen; pub use codegen::solana_client_gen;
#[cfg(feature = "client")] #[cfg(feature = "client")]
@ -195,8 +180,6 @@ pub mod prelude {
pub use thiserror::Error; pub use thiserror::Error;
} }
pub mod coder;
// Re-export. // Re-export.
#[cfg(feature = "client")] #[cfg(feature = "client")]
pub use solana_client; pub use solana_client;