diff --git a/solana/bridge/program/src/vaa.rs b/solana/bridge/program/src/vaa.rs index 32d9a7f7..ae489349 100644 --- a/solana/bridge/program/src/vaa.rs +++ b/solana/bridge/program/src/vaa.rs @@ -17,10 +17,7 @@ use byteorder::{ BigEndian, ReadBytesExt, }; -use solana_program::{ - instruction::AccountMeta, - pubkey::Pubkey, -}; +use solana_program::{instruction::AccountMeta, pubkey::Pubkey}; use solitaire::{ processors::seeded::Seeded, trace, @@ -106,8 +103,8 @@ impl<'a, 'b: 'a, 'c, T: DeserializePayload> Peel<'a, 'b, 'c> for PayloadMessage< Data::persist(&self.0, program_id) } - fn to_partial_cpi_metas(infos: &'c mut std::slice::Iter>) -> Result> { - Data::<'b, PostedMessage, {AccountState::Initialized}>::to_partial_cpi_metas(infos) + fn to_partial_cpi_meta(&self) -> Vec { + Data::to_partial_cpi_meta(&self.0) } } diff --git a/solana/solitaire/client/src/lib.rs b/solana/solitaire/client/src/lib.rs index bb8eed9b..140e1c30 100644 --- a/solana/solitaire/client/src/lib.rs +++ b/solana/solitaire/client/src/lib.rs @@ -23,10 +23,7 @@ use borsh::BorshSerialize; use solitaire::{ AccountState, - CPICall, - FromAccounts, Info, - Mut, Sysvar, }; pub use solitaire::{ @@ -55,9 +52,9 @@ pub enum AccEntry { SignerRO(Keypair), /// Program addresses for unprivileged cross calls - CPIProgram(Pubkey, Box), + CPIProgram(Pubkey), /// Program addresses for privileged cross calls - CPIProgramSigner(Keypair, Box), + CPIProgramSigner(Keypair), /// Key decided from SPL constants Sysvar(Pubkey), @@ -74,16 +71,13 @@ pub trait Wrap { fn wrap(_: &AccEntry) -> StdResult, ErrBox>; /// If the implementor wants to sign using other AccEntry - /// variants, they should override this. Multiple keypairs may be - /// used in multi-account AccEntry variants. - fn partial_signer_keypairs(a: &AccEntry) -> Vec { + /// variants, they should override this. + fn keypair(a: AccEntry) -> Option { use AccEntry::*; match a { - // A panic on unwrap below here would imply solana keypair code does not understand its own bytes - Signer(pair) | SignerRO(pair) => { - vec![Keypair::from_bytes(pair.to_bytes().to_vec().as_slice()).unwrap()] - } - _other => vec![], + Signer(pair) => Some(pair), + SignerRO(pair) => Some(pair), + _other => None, } } } @@ -193,67 +187,11 @@ impl<'b> Wrap for Info<'b> { } } -impl<'b, T: Wrap> Wrap for Mut { - fn wrap(a: &AccEntry) -> StdResult, ErrBox> { - match a { - AccEntry::Unprivileged(_) | AccEntry::Signer(_) | AccEntry::Derived(_) => { - Ok(T::wrap(a)?) - } - _other => Err(format!( - "{} must be passed as a mutable AccEntry, such as Unprivileged, Signer or Derived", - std::any::type_name::() - ) - .into()), - } - } -} - -impl<'a, 'b: 'a, 'c, T> Wrap for CPICall { - fn wrap(a: &AccEntry) -> StdResult, ErrBox> { - match a { - AccEntry::CPIProgram(xprog_k, xprog_accs) => { - let mut v = vec![AccountMeta::new_readonly(xprog_k.clone(), false)]; - v.append(&mut xprog_accs.acc_metas(&xprog_k)?); - Ok(v) - } - AccEntry::CPIProgramSigner(xprog_pair, xprog_accs) => { - let mut v = vec![AccountMeta::new_readonly( - xprog_pair.pubkey().clone(), - false, - )]; - v.append(&mut xprog_accs.acc_metas(&xprog_pair.pubkey())?); - Ok(v) - } - _other => Err(format!( - "{} must be passed as CPIProgram or CPIProgramSigner", - std::any::type_name::() - ) - .into()), - } - } - - fn partial_signer_keypairs(a: &AccEntry) -> Vec { - match a { - AccEntry::CPIProgram(xprog_k, xprog_accs) => xprog_accs.signer_keypairs(), - AccEntry::CPIProgramSigner(xprog_pair, xprog_accs) => { - let mut v = vec![Keypair::from_bytes(&xprog_pair.to_bytes()[..]).unwrap()]; - v.append(&mut xprog_accs.signer_keypairs()); - v - } - _other => vec![], - } - } -} - -/// Trait used on client side to easily validate a program account struct + ix_data for a bare Solana call -pub trait ToInstruction: std::fmt::Debug { +/// Trait used on client side to easily validate a program accounts + ix_data for a bare Solana call +pub trait ToInstruction { fn to_ix( - &self, + self, program_id: Pubkey, ix_data: &[u8], ) -> StdResult<(Instruction, Vec), ErrBox>; - - fn acc_metas(&self, program_id: &Pubkey) -> StdResult, ErrBox>; - - fn signer_keypairs(&self) -> Vec; } diff --git a/solana/solitaire/program/src/lib.rs b/solana/solitaire/program/src/lib.rs index 380ce4f0..01a400e7 100644 --- a/solana/solitaire/program/src/lib.rs +++ b/solana/solitaire/program/src/lib.rs @@ -124,9 +124,6 @@ pub trait FromAccounts<'a, 'b: 'a, 'c> { where Self: Sized; - /// How many accounts do we expect to use for reconstructing Self? - fn size_in_accounts() -> usize; - - /// Turn a bunch of infos into relevant metas to facilitate CPI calls. - fn to_cpi_metas(infos: &'c mut Iter>) -> Result>; + /// Converts the accounts back to vector form to facilitate CPI calls. + fn to_cpi_metas(&self) -> Vec; } diff --git a/solana/solitaire/program/src/macros.rs b/solana/solitaire/program/src/macros.rs index c38eb75c..1c7e76d9 100644 --- a/solana/solitaire/program/src/macros.rs +++ b/solana/solitaire/program/src/macros.rs @@ -156,8 +156,8 @@ macro_rules! data_wrapper { Data::<'_, $embed, { $state }>::persist(self, program_id) } - fn to_partial_cpi_metas(infos: &'c mut std::slice::Iter>) -> solitaire::Result> { - solitaire::Data::<'b, $embed, {$state}>::to_partial_cpi_metas(infos) + fn to_partial_cpi_meta(&self) -> Vec { + self.0.to_partial_cpi_meta() } } diff --git a/solana/solitaire/program/src/processors/peel.rs b/solana/solitaire/program/src/processors/peel.rs index bf974e1c..9f4fbe25 100644 --- a/solana/solitaire/program/src/processors/peel.rs +++ b/solana/solitaire/program/src/processors/peel.rs @@ -7,7 +7,6 @@ use borsh::BorshDeserialize; use solana_program::{ instruction::AccountMeta, - program_error::ProgramError, pubkey::Pubkey, system_program, sysvar::{ @@ -16,10 +15,7 @@ use solana_program::{ SysvarId, }, }; -use std::{ - marker::PhantomData, - slice::Iter, -}; +use std::marker::PhantomData; use crate::{ processors::seeded::{ @@ -28,7 +24,6 @@ use crate::{ }, types::*, Context, - FromAccounts, Result, SolitaireError, }; @@ -45,15 +40,8 @@ pub trait Peel<'a, 'b: 'a, 'c> { fn persist(&self, program_id: &Pubkey) -> Result<()>; - /// How many accounts from the on-chain account iterator - /// constitute this peelable type? Important to customizefor - /// multi-account types like FromAccounts implementors. - fn partial_size_in_accounts() -> usize { - 1 - } - - /// Special method for turning an iterator into AccountMetas for CPI use cases. - fn to_partial_cpi_metas(_: &'c mut Iter>) -> Result>; + /// Special method for turning the type back into AccountMeta for CPI use cases. + fn to_partial_cpi_meta(&self) -> Vec; } /// Peel a Derived Key @@ -77,13 +65,14 @@ impl<'a, 'b: 'a, 'c, T: Peel<'a, 'b, 'c>, const Seed: &'static str> Peel<'a, 'b, T::persist(self, program_id) } - fn to_partial_cpi_metas(infos: &'c mut Iter>) -> Result> { - T::to_partial_cpi_metas(infos) + fn to_partial_cpi_meta(&self) -> Vec { + self.0.to_partial_cpi_meta() } } /// Peel a Mutable key. -impl<'a, 'b: 'a, 'c, T: Peel<'a, 'b, 'c>> Peel<'a, 'b, 'c> for Mut { +impl<'a, 'b: 'a, 'c, T: Peel<'a, 'b, 'c>> Peel<'a, 'b, 'c> for Mut +{ fn peel(mut ctx: &'c mut Context<'a, 'b, 'c, I>) -> Result { ctx.immutable = false; match ctx.info().is_writable { @@ -99,9 +88,6 @@ impl<'a, 'b: 'a, 'c, T: Peel<'a, 'b, 'c>> Peel<'a, 'b, 'c> for Mut { fn persist(&self, program_id: &Pubkey) -> Result<()> { T::persist(self, program_id) } - fn to_partial_cpi_metas(infos: &'c mut Iter>) -> Result> { - T::to_partial_cpi_metas(infos) - } } /// Peel a Signer. @@ -121,8 +107,8 @@ impl<'a, 'b: 'a, 'c, T: Peel<'a, 'b, 'c>> Peel<'a, 'b, 'c> for Signer { T::persist(self, program_id) } - fn to_partial_cpi_metas(infos: &'c mut Iter>) -> Result> { - T::to_partial_cpi_metas(infos) + fn to_partial_cpi_meta(&self) -> Vec { + self.0.to_partial_cpi_meta() } } @@ -143,8 +129,8 @@ impl<'a, 'b: 'a, 'c, T: Peel<'a, 'b, 'c>> Peel<'a, 'b, 'c> for System { T::persist(self, program_id) } - fn to_partial_cpi_metas(infos: &'c mut Iter>) -> Result> { - T::to_partial_cpi_metas(infos) + fn to_partial_cpi_meta(&self) -> Vec { + self.0.to_partial_cpi_meta() } } @@ -171,8 +157,8 @@ where Ok(()) } - fn to_partial_cpi_metas(infos: &'c mut Iter>) -> Result> { - Info::to_partial_cpi_metas(infos) + fn to_partial_cpi_meta(&self) -> Vec { + self.to_partial_cpi_meta() } } @@ -193,17 +179,14 @@ impl<'a, 'b: 'a, 'c> Peel<'a, 'b, 'c> for Info<'b> { Ok(()) } - fn to_partial_cpi_metas(infos: &'c mut Iter>) -> Result> { - let acc = infos - .next() - .ok_or_else(|| SolitaireError::ProgramError(ProgramError::AccountDataTooSmall))?; - let meta = if acc.is_writable { - AccountMeta::new(acc.key.clone(), acc.is_signer) + fn to_partial_cpi_meta(&self) -> Vec { + let meta = if self.is_writable { + AccountMeta::new(self.key.clone(), self.is_signer) } else { - AccountMeta::new_readonly(acc.key.clone(), acc.is_signer) + AccountMeta::new_readonly(self.key.clone(), self.is_signer) }; - Ok(vec![meta]) + vec![meta] } } @@ -285,51 +268,7 @@ impl< Ok(()) } - fn to_partial_cpi_metas(infos: &'c mut Iter>) -> Result> { - Info::to_partial_cpi_metas(infos) - } -} - -impl<'a, 'b: 'a, 'c, T: Peel<'a, 'b, 'c> + FromAccounts<'a, 'b, 'c>> Peel<'a, 'b, 'c> - for CPICall -{ - fn peel(ctx: &'c mut Context<'a, 'b, 'c, I>) -> Result - where - Self: Sized, - { - let xprog = ctx - .iter - .next() - .ok_or_else(|| SolitaireError::ProgramError(ProgramError::AccountDataTooSmall))?; - - let xprog_accounts = T::to_cpi_metas(ctx.iter)?; - - if xprog_accounts.len() != T::size_in_accounts() { - return Err(SolitaireError::ProgramError( - ProgramError::AccountDataTooSmall, - )); - } - Ok(Self { - xprog_id: xprog.key.clone(), - xprog_accounts, - callee_type: PhantomData, - }) - } - - fn deps() -> Vec { - todo!() - } - - fn persist(&self, program_id: &Pubkey) -> Result<()> { - // Persisting cross accounts is not our business - Ok(()) - } - - fn to_partial_cpi_metas(infos: &'c mut Iter>) -> Result> { - T::to_cpi_metas(infos) - } - - fn partial_size_in_accounts() -> usize { - T::size_in_accounts() + 1 // Nested type size + 1 for cross program ID + fn to_partial_cpi_meta(&self) -> Vec { + self.0.to_partial_cpi_meta() } } diff --git a/solana/solitaire/program/src/types/accounts.rs b/solana/solitaire/program/src/types/accounts.rs index 360cb34c..037cb12b 100644 --- a/solana/solitaire/program/src/types/accounts.rs +++ b/solana/solitaire/program/src/types/accounts.rs @@ -5,11 +5,17 @@ //! types that describe different kinds of accounts to target. use borsh::BorshSerialize; -use solana_program::{account_info::AccountInfo, instruction::AccountMeta, program::invoke_signed, pubkey::Pubkey, system_instruction, sysvar::Sysvar as SolanaSysvar}; -use std::{marker::PhantomData, ops::{ +use solana_program::{ + account_info::AccountInfo, + program::invoke_signed, + pubkey::Pubkey, + system_instruction, + sysvar::Sysvar as SolanaSysvar, +}; +use std::ops::{ Deref, DerefMut, -}}; +}; use crate::{ processors::seeded::Owned, @@ -120,11 +126,3 @@ impl invoke_signed(&ix, ctx.accounts, &[&[Seed.as_bytes(), &[bump_seed]]]).map_err(|e| e.into()) } } - -#[derive(Debug)] -pub struct CPICall { - pub xprog_id: Pubkey, - pub xprog_accounts: Vec, - /// Helps preserve information about the type of cross program's arguments - pub callee_type: PhantomData, -} diff --git a/solana/solitaire/rocksalt/src/lib.rs b/solana/solitaire/rocksalt/src/lib.rs index 509891ac..5fc26911 100644 --- a/solana/solitaire/rocksalt/src/lib.rs +++ b/solana/solitaire/rocksalt/src/lib.rs @@ -109,7 +109,6 @@ pub fn derive_from_accounts(input: TokenStream) -> TokenStream { let from_method = generate_from(&name, &input.data); let to_cpi_metas_method = generate_to_cpi_metas(&name, &input.data); - let size_in_accounts_method = generate_size_in_accounts(&name, &input.data); let persist_method = generate_persist(&name, &input.data); let deps_method = generate_deps_fields(&name, &input.data); let expanded = quote! { @@ -118,10 +117,7 @@ pub fn derive_from_accounts(input: TokenStream) -> TokenStream { fn from(pid: &'a solana_program::pubkey::Pubkey, iter: &'c mut std::slice::Iter<'a, solana_program::account_info::AccountInfo<'b>>, data: &'a DataType) -> solitaire::Result { #from_method } - fn size_in_accounts() -> usize { - #size_in_accounts_method - } - fn to_cpi_metas(infos: &'c mut std::slice::Iter>) -> solitaire::Result> { + fn to_cpi_metas(&self) -> Vec { #to_cpi_metas_method } } @@ -145,12 +141,8 @@ pub fn derive_from_accounts(input: TokenStream) -> TokenStream { solitaire::Persist::persist(self, program_id) } - fn partial_size_in_accounts() -> usize { - Self::size_in_accounts() - } - - fn to_partial_cpi_metas(infos: &'c mut std::slice::Iter>) -> solitaire::Result> { - Self::to_cpi_metas(infos) + fn to_partial_cpi_meta(&self) -> Vec { + self.to_cpi_metas() } } @@ -231,10 +223,10 @@ fn generate_to_cpi_metas(name: &syn::Ident, data: &Data) -> TokenStream2 { let v_appends = fields.named.iter().map(|f| { // Field name, to assign to. let field_name = &f.ident; - let field_ty = &f.ty; + let ty = &f.ty; quote! { - v.append(&mut <#field_ty as Peel>::to_partial_cpi_metas(infos)?); + v.append(&mut self.#field_name.to_partial_cpi_meta()); } }); @@ -246,11 +238,8 @@ fn generate_to_cpi_metas(name: &syn::Ident, data: &Data) -> TokenStream2 { quote! { let mut v = Vec::new(); #(#v_appends;)* - if v.len() != Self::size_in_accounts() { - return Err(solitaire::SolitaireError::ProgramError(solana_program::program_error::ProgramError::InvalidAccountData)); - } - Ok(v) - } + v + } } else { unimplemented!() } @@ -337,30 +326,3 @@ fn generate_persist(name: &syn::Ident, data: &Data) -> TokenStream2 { Data::Enum(_) | Data::Union(_) => unimplemented!(), } } - -fn generate_size_in_accounts(name: &syn::Ident, data: &Data) -> TokenStream2 { - if let Data::Struct(DataStruct { - fields: Fields::Named(fields), - .. - }) = data - { - // For each field, call the relevant partial_size_in_accounts method - let size_additions = fields.named.iter().map(|f| { - // Field name, to assign to. - let field_name = &f.ident; - let ty = &f.ty; - - quote! { - size += <#ty as Peel>::partial_size_in_accounts(); // #field_name - } - }); - - quote! { - let mut size = 0; - #(#size_additions;)* - size - } - } else { - unimplemented!() - } -} diff --git a/solana/solitaire/rocksalt/src/to_instruction.rs b/solana/solitaire/rocksalt/src/to_instruction.rs index 0631f1da..a332bcdd 100644 --- a/solana/solitaire/rocksalt/src/to_instruction.rs +++ b/solana/solitaire/rocksalt/src/to_instruction.rs @@ -17,14 +17,13 @@ use syn::{ DataStruct, DeriveInput, Fields, - FieldsNamed, GenericParam, Generics, Index, }; pub fn generate_to_instruction( - orig_struct_ident: &syn::Ident, + name: &syn::Ident, impl_generics: &syn::ImplGenerics, data: &Data, ) -> TokenStream2 { @@ -33,144 +32,87 @@ pub fn generate_to_instruction( fields: Fields::Named(ref fields), .. }) => { - let client_struct_ident = syn::Ident::new( - &format!("{}Accounts", orig_struct_ident.to_string()), - Span::call_site(), - ); - let client_struct_decl = - generate_clientside_struct(&orig_struct_ident, &client_struct_ident, &fields); + let expanded_appends = fields.named.iter().map(|field| { + let name = &field.ident; + let ty = &field.ty; - let acc_metas_ident = syn::Ident::new("acc_metas", Span::call_site()); - let acc_metas_appends = generate_acc_metas_appends(&acc_metas_ident, &fields); + quote! { + deps.append(&mut <#ty as solitaire::Peel>::deps()); + account_metas.append(&mut <#ty as solitaire_client::Wrap>::wrap(&self.#name)?); + if let Some(pair) = <#ty as solitaire_client::Wrap>::keypair(self.#name) { + signers.push(pair); + } + } + }); + let client_struct_name = + syn::Ident::new(&format!("{}Accounts", name.to_string()), Span::call_site()); - let signers_ident = syn::Ident::new("signers", Span::call_site()); - let signers_appends = generate_signers_appends(&signers_ident, &fields); - - let deps_ident = syn::Ident::new("deps", Span::call_site()); - let deps_appends = generate_deps_appends(&deps_ident, &fields); + let client_struct_decl = generate_clientside_struct(&name, &client_struct_name, &data); quote! { - /// Solitaire-generated client-side #orig_struct_ident representation - #[cfg(feature = "client")] - #[derive(Debug)] - #client_struct_decl + /// Solitaire-generated client-side #name representation + #[cfg(feature = "client")] + #client_struct_decl - /// Solitaire-generatied ToInstruction implementation - #[cfg(feature = "client")] - impl #impl_generics solitaire_client::ToInstruction for #client_struct_ident { + /// Solitaire-generatied ToInstruction implementation + #[cfg(feature = "client")] + impl #impl_generics solitaire_client::ToInstruction for #client_struct_name { + fn to_ix( + self, + program_id: solana_program::pubkey::Pubkey, + ix_data: &[u8]) -> std::result::Result< + (solitaire_client::Instruction, Vec), + solitaire::ErrBox + > { + use solana_program::{pubkey::Pubkey, instruction::Instruction}; + let mut account_metas = Vec::new(); + let mut signers = Vec::new(); + let mut deps = Vec::new(); - fn to_ix( - &self, - program_id: solana_program::pubkey::Pubkey, - ix_data: &[u8]) -> std::result::Result< - (solitaire_client::Instruction, Vec), - solitaire::ErrBox> { + #(#expanded_appends;)* - use solana_program::{pubkey::Pubkey, instruction::Instruction}; - let mut #acc_metas_ident = Vec::new(); - let mut #signers_ident = Vec::new(); - let mut #deps_ident = Vec::new(); + // Add dependencies + deps.dedup(); + let mut dep_ams = deps.iter().map(|v| solana_program::instruction::AccountMeta::new_readonly(*v, false)).collect(); + account_metas.append(&mut dep_ams); - #acc_metas_appends - #deps_appends - #signers_appends + Ok((solana_program::instruction::Instruction::new_with_bytes(program_id, + ix_data, + account_metas), signers)) - // Add dependencies - #deps_ident.dedup(); - let mut dep_ams = deps.iter().map(|v| solana_program::instruction::AccountMeta::new_readonly(*v, false)).collect(); - #acc_metas_ident.append(&mut dep_ams); + } - Ok((solana_program::instruction::Instruction::new_with_bytes(program_id, - ix_data, - #acc_metas_ident), #signers_ident)) - - } - - fn acc_metas(&self, aprogram_id: &solana_program::pubkey::Pubkey) -> std::result::Result , solitaire::ErrBox> { - let mut #acc_metas_ident = Vec::new(); - - #acc_metas_appends - - Ok(#acc_metas_ident) - } - - fn signer_keypairs(&self) -> Vec { - let mut #signers_ident = Vec::new(); - - #signers_appends - - #signers_ident - } - } - } + } + } } _ => unimplemented!(), } } -/// Generates Wrap::wrap() calls and appends to the account metas vec -/// for the specified vec name ident and provided field data. -pub fn generate_acc_metas_appends(vec_ident: &syn::Ident, data: &FieldsNamed) -> TokenStream2 { - let appends = data.named.iter().map(|f| { - let f_ty = &f.ty; - let f_ident = &f.ident; - quote! { - #vec_ident.append(&mut <#f_ty as solitaire_client::Wrap>::wrap(&self.#f_ident)?); - } - }); - quote! { - #(#appends;)* - } -} - -/// Generates Peel::deps() calls and appends to the dependency pubkey -/// vec for the specified vec name ident and provided field data. -pub fn generate_deps_appends(vec_ident: &syn::Ident, data: &FieldsNamed) -> TokenStream2 { - let appends = data.named.iter().map(|f| { - let f_ty = &f.ty; - quote! { - #vec_ident.append(&mut <#f_ty as solitaire::Peel>::deps()); - } - }); - quote! { - #(#appends;)* - } -} - -/// Generates Wrap::partial_signer_keypairs() calls and appends to the -/// dependency pubkey vec for the specified vec name ident and -/// provided field data. -pub fn generate_signers_appends(vec_ident: &syn::Ident, data: &FieldsNamed) -> TokenStream2 { - let appends = data.named.iter().map(|f| { - let f_ty = &f.ty; - let f_ident = &f.ident; - quote! { - #vec_ident.append(&mut <#f_ty as solitaire_client::Wrap>::partial_signer_keypairs(&self.#f_ident)); - } - }); - quote! { - #(#appends;)* - } -} - -/// Generates a client-side struct for sending relevant account pubkeys/keypairs/sysvars to RPC pub fn generate_clientside_struct( - orig_struct_ident: &syn::Ident, - client_struct_ident: &syn::Ident, - fields: &FieldsNamed, + name: &syn::Ident, + client_struct_name: &syn::Ident, + data: &Data, ) -> TokenStream2 { - let expanded_fields = fields.named.iter().map(|field| { - let field_name = &field.ident; + match *data { + Data::Struct(DataStruct { + fields: Fields::Named(ref fields), + .. + }) => { + let expanded_fields = fields.named.iter().map(|field| { + let field_name = &field.ident; - quote! { - #field_name: solitaire_client::AccEntry - } - }); + quote! { + #field_name: solitaire_client::AccEntry + } + }); - quote! { - /// This Solitaire-generated account represents #orig_struct_ident off-chain on client side. - pub struct #client_struct_ident { - #(pub #expanded_fields,)* + quote! { + pub struct #client_struct_name { + #(pub #expanded_fields,)* + } + } } + _ => unimplemented!(), } }