scg: Go all in on Serde serializers
This commit is contained in:
parent
8478ac0efa
commit
d15ff58c09
|
@ -8,7 +8,7 @@ edition = "2018"
|
|||
[features]
|
||||
program = ["spl-token/program", "solana-sdk/program"]
|
||||
client = ["spl-token/default", "solana-sdk/default", "solana-client", "anyhow", "rand"]
|
||||
test = ["solana-client-gen"]
|
||||
test = []
|
||||
|
||||
[dependencies]
|
||||
spl-token = { version = "=2.0.3", default-features = false }
|
||||
|
@ -21,6 +21,3 @@ solana-sdk = { version = "=1.3.9", default-features = false }
|
|||
anyhow = { version = "1.0.32", optional = true }
|
||||
rand = { version = "0.7.3", optional = true }
|
||||
solana-client = { version = "1.3.4", optional = true }
|
||||
|
||||
# Test only.
|
||||
solana-client-gen = { path = "../solana-client-gen", optional = true }
|
|
@ -19,3 +19,4 @@ solana-sdk = { version = "=1.3.9", default-features = false }
|
|||
solana-client = { version = "1.3.4", optional = true }
|
||||
rand = { version = "0.7.3", optional = true }
|
||||
codegen = { path = "./codegen", optional = true }
|
||||
serum-common = { path = "../common" }
|
|
@ -22,7 +22,7 @@ pub fn solana_client_gen(
|
|||
// 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::Ident)),
|
||||
_ => Some(parse_macro_input!(args as syn::TypePath)),
|
||||
};
|
||||
|
||||
// Interpet token stream as the instruction `mod`.
|
||||
|
@ -57,12 +57,8 @@ pub fn solana_client_gen(
|
|||
// Second pass:
|
||||
//
|
||||
// Parse the instruction enum and generate code from each enum variant.
|
||||
let (client_methods, instruction_methods, decode_and_dispatch_tree, coder_mod) =
|
||||
enum_to_methods(
|
||||
instruction_mod.ident.clone(),
|
||||
&instruction_enum_item,
|
||||
coder_struct,
|
||||
);
|
||||
let (client_methods, instruction_methods, decode_and_dispatch_tree) =
|
||||
enum_to_methods(&instruction_enum_item, coder_struct);
|
||||
|
||||
// Now recreate the highest level instruction `mod`, but with our new
|
||||
// instruction_methods inside.
|
||||
|
@ -258,22 +254,18 @@ pub fn solana_client_gen(
|
|||
$($client_ext)*
|
||||
}
|
||||
#new_instruction_mod
|
||||
#coder_mod
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Now put it all together.
|
||||
//
|
||||
// Output the transormed AST with the new client, new instruction mod,
|
||||
// and new coder definition.
|
||||
// Output the transormed AST with the new client and new instruction mod.
|
||||
proc_macro::TokenStream::from(quote! {
|
||||
#[cfg(not(feature = "client-ext"))]
|
||||
#client_mod
|
||||
#[cfg(not(feature = "client-ext"))]
|
||||
#new_instruction_mod
|
||||
#[cfg(not(feature = "client-ext"))]
|
||||
#coder_mod
|
||||
|
||||
#extendable_client_macro
|
||||
})
|
||||
|
@ -288,18 +280,16 @@ pub fn solana_client_gen(
|
|||
// * Coder struct for serialization.
|
||||
//
|
||||
fn enum_to_methods(
|
||||
instruction_mod_ident: syn::Ident,
|
||||
instruction_enum: &syn::ItemEnum,
|
||||
coder_struct_opt: Option<syn::Ident>,
|
||||
coder_struct_opt: Option<syn::TypePath>,
|
||||
) -> (
|
||||
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
|
||||
solana_client_gen::coder::DefaultCoder
|
||||
},
|
||||
Some(cs) => quote! {
|
||||
#cs
|
||||
|
@ -673,34 +663,10 @@ fn enum_to_methods(
|
|||
}
|
||||
};
|
||||
|
||||
// Define the instruction coder to use for serialization.
|
||||
let coder_mod = match coder_struct_opt {
|
||||
// It's defined externally so do nothing.
|
||||
Some(_) => quote! {},
|
||||
// Coder not provided, so use declare and use the default one.
|
||||
None => quote! {
|
||||
pub mod solana_client_gen_coder {
|
||||
use super::*;
|
||||
pub struct _DefaultCoder;
|
||||
impl _DefaultCoder {
|
||||
pub fn to_bytes(i: #instruction_mod_ident::#instruction_enum_ident) -> Vec<u8> {
|
||||
serum_common::pack::to_bytes(&i)
|
||||
.expect("instruction must be serializable")
|
||||
}
|
||||
pub fn from_bytes(data: &[u8]) -> Result<#instruction_mod_ident::#instruction_enum_ident, ()> {
|
||||
serum_common::pack::from_bytes(data)
|
||||
.map_err(|_| ())
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
(
|
||||
client_methods,
|
||||
instruction_methods,
|
||||
decode_and_dispatch_tree,
|
||||
coder_mod,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
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(|_| ())
|
||||
}
|
||||
}
|
|
@ -1,6 +1,10 @@
|
|||
//! solana-client-gen is a convenience macro to generate rpc clients from
|
||||
//! Solana instruction definitions.
|
||||
//!
|
||||
//! Ideally, Solana would have a Rust compiler extension built into their CLI,
|
||||
//! which emitted a standard runtime IDL that could be used to generate clients.
|
||||
//! This macro is a stopgap in the mean time.
|
||||
//!
|
||||
//! # Creating an interface.
|
||||
//!
|
||||
//! To start, one should make an "interface" crate, separate from one's Solana
|
||||
|
@ -15,6 +19,7 @@
|
|||
//!
|
||||
//! #[cfg_attr(feature = "client", solana_client_gen)]
|
||||
//! pub mod instruction {
|
||||
//! #[derive(Serialize, Deserialize)]
|
||||
//! pub enum MyInstruction {
|
||||
//! Add: {
|
||||
//! a: u64,
|
||||
|
@ -56,7 +61,7 @@
|
|||
//! },
|
||||
//! });
|
||||
//!
|
||||
//! Define the accounts the program uses for each instruction.
|
||||
//! // Define the accounts the program uses for each instruction.
|
||||
//! let accounts = ...;
|
||||
//!
|
||||
//! // Invoke rpcs as desired.
|
||||
|
@ -64,11 +69,11 @@
|
|||
//! client.sub(accounts, 4, 2)
|
||||
//! ```
|
||||
//!
|
||||
//! In addition to generate an rpc client, the macro generates instruction
|
||||
//! In addition to generating an rpc client, the macro generates instruction
|
||||
//! methods to create `solana_sdk::instruction::Instruction` types. For example,
|
||||
//!
|
||||
//! ```
|
||||
//! let instruction = program_interface::instruction::add(2, 3);
|
||||
//! let instruction = my_crate::instruction::add(2, 3);
|
||||
//! ```
|
||||
//!
|
||||
//! This can be used to generate instructions to be invoked with other Solana
|
||||
|
@ -79,15 +84,18 @@
|
|||
//! It's not uncommon to want to atomically create an account and execute an
|
||||
//! instruction. For example, in the SPL token standard, one must include
|
||||
//! two instructions in the same transaction: one to create the mint account
|
||||
//! and another to initialize the mint. To do this, add the #[create_account]
|
||||
//! and another to initialize the mint. To do this, add the `#[create_account]`
|
||||
//! attribute to your instruction. For example
|
||||
//!
|
||||
//! ```
|
||||
//! #[cfg_attr(feature = "client", solana_client_gen)]
|
||||
//! pub mod instruction {
|
||||
//! #[cfg_attr(feature = "client", create_account)]
|
||||
//! Initialize {
|
||||
//! some_data: u64,
|
||||
//! #[derive(Serialize, Deserialize)]
|
||||
//! pub enum MyInstruction {
|
||||
//! #[cfg_attr(feature = "client", create_account)]
|
||||
//! Initialize {
|
||||
//! some_data: u64,
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
|
@ -97,48 +105,52 @@
|
|||
//! will always be the created account. Note: you don't have to pass the
|
||||
//! account yourself, since it will be done for you.
|
||||
//!
|
||||
//! Furthermore, continuing the above example the simpler `initialize`
|
||||
//! will also be generated.
|
||||
//! # Extending the client.
|
||||
//!
|
||||
//! If the generated client isn't enough, for example, if you want to batch
|
||||
//! multiple instructions together into the same transaction as a performance
|
||||
//! optimization, one can enable the `client-ext` feature in their crate
|
||||
//! (note: currently this feature is required to be in the crate where the
|
||||
//! instruction enum is defined).
|
||||
//!
|
||||
//! When using the `client-ext`, the proc macro won't generate a client
|
||||
//! immediately. Instead it will act as a meta-macro, generating yet another
|
||||
//! macro: `solana_client_gen_extension`, which must be used to build the client.
|
||||
//!
|
||||
//! We can extend the above client to have a `hello_world` method.
|
||||
//!
|
||||
//! ```
|
||||
//! solana_client_gen_extension! {
|
||||
//! impl Client {
|
||||
//! pub fn hello_world(&self) {
|
||||
//! println!("hello world from the generated client");
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! # Using a custom coder.
|
||||
//!
|
||||
//! It's assumed instructions implement serde's `Serialize` and `Deserialize`.
|
||||
//!
|
||||
//! 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(Coder)]
|
||||
//! #[solana_client_gen(crate::mod::Coder)]
|
||||
//! mod instruction {
|
||||
//! ...
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! Where `Coder` is a user defined struct that has two methods. For example,
|
||||
//!
|
||||
//! ```
|
||||
//! struct Coder;
|
||||
//! impl Coder {
|
||||
//! pub fn to_bytes(i: instruction::SrmSafeInstruction) -> Vec<u8> {
|
||||
//! println!("using custom coder");
|
||||
//! bincode::serialize(&(0u8, i)).expect("instruction must be serializable")
|
||||
//! }
|
||||
//! pub fn from_bytes(data: &[u8]) -> Result<instruction::SrmSafeInstruction, ()> {
|
||||
//! match data.split_first() {
|
||||
//! None => Err(()),
|
||||
//! Some((&u08, rest)) => bincode::deserialize(rest).map_err(|_| ()),
|
||||
//! Some((_, _rest)) => Err(()),
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! Note that the names must be `to_bytes` and `from_bytes` and the input/output of
|
||||
//! both methods must be the name of your instruction's `enum`.
|
||||
//! Where `Coder` is a user defined struct that implements the
|
||||
//! `InstructionCoder` interface.
|
||||
//!
|
||||
//! # Limitations
|
||||
//!
|
||||
//! Currently, only a client is generated for serializing requests to Solana clusters.
|
||||
//! This is why a conditional attribute macro is used (i.e., cfg_attr).
|
||||
//! This is why a conditional attribute macro is used via `cfg_attr`.
|
||||
//!
|
||||
//! In addition, it would be nice to generate code on the runtime as well to
|
||||
//! deserialize instructions and dispatch to the correct method. Currently, this
|
||||
|
@ -155,6 +167,8 @@ pub mod prelude {
|
|||
pub use solana_sdk::instruction::{AccountMeta, Instruction};
|
||||
pub use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
pub use crate::coder::InstructionCoder;
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
pub use codegen::solana_client_gen;
|
||||
#[cfg(feature = "client")]
|
||||
|
@ -181,6 +195,8 @@ pub mod prelude {
|
|||
pub use thiserror::Error;
|
||||
}
|
||||
|
||||
pub mod coder;
|
||||
|
||||
// Re-export.
|
||||
#[cfg(feature = "client")]
|
||||
pub use solana_client;
|
||||
|
|
Loading…
Reference in New Issue