Solitaire client implementation

Change-Id: I4d1f37082537cd24a4859802652a177400f6a205
This commit is contained in:
Stan Drozd 2021-06-04 13:02:35 +02:00
parent 2b473f1f12
commit c700e8847b
15 changed files with 3718 additions and 271 deletions

View File

@ -12,7 +12,7 @@ let
owner = "tilt-dev"; owner = "tilt-dev";
repo = oldAttrs.pname; repo = oldAttrs.pname;
rev = "v0.18.4"; rev = "v0.18.4";
sha256 = "sha256-xqBgbsrVSAOqtfHbEF07i6XIdiBXMYoR7H4Kc4xK7x0="; sha256 = null;
}; };
buildFlagsArray = [ "-ldflags=-X main.version=0.18.4" ]; buildFlagsArray = [ "-ldflags=-X main.version=0.18.4" ];
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,5 @@
[workspace] [workspace]
members = [ members = [
"programs/*", "programs/*",
"client/",
] ]

View File

@ -5,9 +5,12 @@ authors = ["Stan Drozd <stan@nexantic.com>"]
edition = "2018" edition = "2018"
[dependencies] [dependencies]
anchor-client = {git = "https://github.com/drozdziak1/anchor", branch = "anchor-debug-feature", features = ["anchor-debug"]}
anyhow = "1.0.40" anyhow = "1.0.40"
anchor-bridge = {path = "../programs/anchor-bridge", features = ["no-idl", "no-entrypoint", "anchor-debug"]} bridge = {path = "../programs/bridge", features = ["no-idl", "no-entrypoint"]}
clap = "3.0.0-beta.2" clap = "3.0.0-beta.2"
rand = "0.7.3" rand = "0.7.3"
shellexpand = "2.1.0" shellexpand = "2.1.0"
solana-client = "1.7.0"
solana-program = "*"
solana-sdk = "1.7.0"
solitaire = { path = "../programs/solitaire" }

View File

@ -1,30 +1,37 @@
use anchor_bridge::{ use bridge::{
accounts::Initialize, api,
instruction::state::New, client,
BridgeConfig, instruction,
InitializeData,
MAX_LEN_GUARDIAN_KEYS,
}; };
use anchor_client::{
solana_sdk::{
account_info::AccountInfo,
commitment_config::CommitmentConfig,
instruction::AccountMeta,
pubkey::Pubkey,
signature::{read_keypair_file, Keypair, Signer},
system_instruction,
system_program,
sysvar,
},
Client,
Cluster,
EventContext,
};
use anyhow::Result;
use clap::Clap; use clap::Clap;
use rand::rngs::OsRng; use solana_client::{
rpc_client::RpcClient,
rpc_config::RpcSendTransactionConfig,
};
use solana_program::{
pubkey::Pubkey,
system_instruction,
system_program,
sysvar,
};
use solana_sdk::{
commitment_config::CommitmentConfig,
signature::{
read_keypair_file,
Signer as SolSigner,
},
transaction::Transaction,
};
use solitaire::{
AccEntry,
Signer,
ToInstruction,
};
use std::time::Duration; use std::{
error,
mem::size_of,
};
#[derive(Clap)] #[derive(Clap)]
pub struct Opts { pub struct Opts {
@ -32,65 +39,95 @@ pub struct Opts {
bridge_address: Pubkey, bridge_address: Pubkey,
} }
fn main() -> Result<()> { pub type ErrBox = Box<dyn error::Error>;
fn main() -> Result<(), ErrBox> {
let opts = Opts::parse(); let opts = Opts::parse();
// Wallet and cluster params.
let payer = read_keypair_file(&*shellexpand::tilde("~/.config/solana/id.json")) let payer = read_keypair_file(&*shellexpand::tilde("~/.config/solana/id.json"))
.expect("Example requires a keypair file"); .expect("Example requires a keypair file");
let url = Cluster::Custom(
"http://localhost:8899".to_owned(),
"ws://localhost:8900".to_owned(),
);
let client = Client::new_with_options(url, payer, CommitmentConfig::processed()); // Keypair is not Clone
let payer_for_tx = read_keypair_file(&*shellexpand::tilde("~/.config/solana/id.json"))
.expect("Example requires a keypair file");
let url = "http://localhost:8899".to_owned();
initialize_bridge(&client, opts.bridge_address)?; let client = RpcClient::new(url);
Ok(())
}
fn initialize_bridge(client: &Client, bridge_address: Pubkey) -> Result<()> { let program_id = opts.bridge_address;
let program = client.program(bridge_address);
let guardian_set_key = Keypair::generate(&mut OsRng); use AccEntry::*;
let state_key = Keypair::generate(&mut OsRng); let init = api::InitializeAccounts {
bridge: Derived(program_id.clone()),
guardian_set: Derived(program_id.clone()),
payer: Signer(payer),
};
program let ix_data = vec![];
.state_request()
.instruction(system_instruction::create_account( let (ix, signers) = init.to_ix(program_id, ix_data.as_slice())?;
&program.payer(), let (recent_blockhash, _) = client.get_recent_blockhash()?;
&guardian_set_key.pubkey(),
program.rpc().get_minimum_balance_for_rent_exemption(500)?, let mut tx = Transaction::new_with_payer(&[ix], Some(&payer_for_tx.pubkey()));
500,
&program.id(), tx.sign(&signers.iter().collect::<Vec<_>>(), recent_blockhash);
))
.instruction(system_instruction::create_account( let signature = client.send_and_confirm_transaction_with_spinner_and_config(
&program.payer(), &tx,
&state_key.pubkey(), CommitmentConfig::processed(),
program.rpc().get_minimum_balance_for_rent_exemption(500)?, RpcSendTransactionConfig {
500, skip_preflight: false,
&program.id(), preflight_commitment: None,
)) encoding: None,
.signer(&guardian_set_key) },
// .signer(&state_key) )?;
.accounts(Initialize { println!("Signature: {}", signature);
payer: program.payer(),
guardian_set: guardian_set_key.pubkey(),
state: state_key.pubkey(),
system_program: system_program::id(),
clock: sysvar::clock::id(),
rent: sysvar::rent::id(),
})
.new(New {
data: InitializeData {
len_guardians: 0,
initial_guardian_keys: [[0u8; 20]; MAX_LEN_GUARDIAN_KEYS],
config: BridgeConfig {
guardian_set_expiration_time: 0u32,
},
},
})
.send()?;
Ok(()) Ok(())
} }
// fn initialize_bridge(client: &Client, bridge_address: Pubkey) -> Result<()> {
// let program = client.program(bridge_address);
// let guardian_set_key = Keypair::generate(&mut OsRng);
// let state_key = Keypair::generate(&mut OsRng);
// program
// .state_request()
// .instruction(system_instruction::create_account(
// &program.payer(),
// &guardian_set_key.pubkey(),
// program.rpc().get_minimum_balance_for_rent_exemption(500)?,
// 500,
// &program.id(),
// ))
// .instruction(system_instruction::create_account(
// &program.payer(),
// &state_key.pubkey(),
// program.rpc().get_minimum_balance_for_rent_exemption(500)?,
// 500,
// &program.id(),
// ))
// .signer(&guardian_set_key)
// // .signer(&state_key)
// .accounts(Initialize {
// payer: program.payer(),
// guardian_set: guardian_set_key.pubkey(),
// state: state_key.pubkey(),
// system_program: system_program::id(),
// clock: sysvar::clock::id(),
// rent: sysvar::rent::id(),
// })
// .new(New {
// data: InitializeData {
// len_guardians: 0,
// initial_guardian_keys: [[0u8; 20]; MAX_LEN_GUARDIAN_KEYS],
// config: BridgeConfig {
// guardian_set_expiration_time: 0u32,
// },
// },
// })
// .send()?;
// Ok(())
// }

View File

@ -20,4 +20,5 @@ byteorder = "1.4.3"
solitaire = { path = "../solitaire" } solitaire = { path = "../solitaire" }
sha3 = "0.9.1" sha3 = "0.9.1"
solana-program = "*" solana-program = "*"
primitive-types = { version = "0.9.0", default-features = false } primitive-types = { version = "0.9.0", default-features = false }
solana-sdk = "1.7.0"

View File

@ -1,8 +1,8 @@
mod governance; pub mod governance;
mod initialize; pub mod initialize;
mod post_message; pub mod post_message;
mod post_vaa; pub mod post_vaa;
mod verify_signature; pub mod verify_signature;
pub use governance::*; pub use governance::*;
pub use initialize::*; pub use initialize::*;

View File

@ -9,7 +9,7 @@ type GuardianSet<'a> =
Derive<Data<'a, GuardianSetData, { AccountState::Uninitialized }>, "GuardianSet">; Derive<Data<'a, GuardianSetData, { AccountState::Uninitialized }>, "GuardianSet">;
type Bridge<'a> = Derive<Data<'a, BridgeData, { AccountState::Uninitialized }>, "Bridge">; type Bridge<'a> = Derive<Data<'a, BridgeData, { AccountState::Uninitialized }>, "Bridge">;
#[derive(FromAccounts, ToAccounts)] #[derive(FromAccounts, ToInstruction)]
pub struct Initialize<'b> { pub struct Initialize<'b> {
pub bridge: Bridge<'b>, pub bridge: Bridge<'b>,
pub guardian_set: GuardianSet<'b>, pub guardian_set: GuardianSet<'b>,

View File

@ -20,3 +20,4 @@ byteorder = "1.4.3"
rocksalt = { path = "../../rocksalt" } rocksalt = { path = "../../rocksalt" }
sha3 = "0.9.1" sha3 = "0.9.1"
solana-program = "*" solana-program = "*"
solana-sdk = "1.7.0"

View File

@ -16,7 +16,10 @@ use solana_program::{
}, },
entrypoint, entrypoint,
entrypoint::ProgramResult, entrypoint::ProgramResult,
instruction::AccountMeta, instruction::{
AccountMeta,
Instruction,
},
program::invoke_signed, program::invoke_signed,
program_error::ProgramError, program_error::ProgramError,
program_pack::Pack, program_pack::Pack,
@ -29,6 +32,10 @@ use solana_program::{
SysvarId, SysvarId,
}, },
}; };
use solana_sdk::signature::{
Keypair,
Signer as SolSigner,
};
use std::{ use std::{
io::{ io::{
@ -74,6 +81,8 @@ pub use crate::{
types::*, types::*,
}; };
type StdResult<T, E> = std::result::Result<T, E>;
pub struct ExecutionContext<'a, 'b: 'a> { pub struct ExecutionContext<'a, 'b: 'a> {
/// A reference to the program_id of the current program. /// A reference to the program_id of the current program.
pub program_id: &'a Pubkey, pub program_id: &'a Pubkey,
@ -98,36 +107,88 @@ impl CreationLamports {
} }
} }
/// The sum type for clearly specifying the accounts required on client side.
pub enum AccEntry {
/// Least privileged account.
Unprivileged(Pubkey),
/// Accounts that need to sign a Solana call
Signer(Keypair),
SignerRO(Keypair),
/// Program addresses for privileged/unprivileged cross calls
CPIProgram(Pubkey),
CPIProgramSigner(Keypair),
/// Key decided by Wrap implementation
Sysvar,
Derived(Pubkey),
DerivedRO(Pubkey),
}
/// Types implementing Wrap are those that can be turned into a
/// partial account vector tha
/// payload.
pub trait Wrap { pub trait Wrap {
fn wrap(&self) -> Vec<AccountMeta>; fn wrap(_: &AccEntry) -> StdResult<Vec<AccountMeta>, ErrBox>;
/// If the implementor wants to sign using other AccEntry
/// variants, they should override this.
fn keypair(a: AccEntry) -> Option<Keypair> {
use AccEntry::*;
match a {
Signer(pair) => Some(pair),
SignerRO(pair) => Some(pair),
_other => None,
}
}
} }
impl<T> Wrap for T impl<'a, 'b: 'a, T> Wrap for Signer<T>
where where
T: ToAccounts, T: Keyed<'a, 'b>,
{ {
fn wrap(&self) -> Vec<AccountMeta> { fn wrap(a: &AccEntry) -> StdResult<Vec<AccountMeta>, ErrBox> {
self.to() use AccEntry::*;
match a {
Signer(pair) => Ok(vec![AccountMeta::new(pair.pubkey(), true)]),
SignerRO(pair) => Ok(vec![AccountMeta::new_readonly(pair.pubkey(), true)]),
other => Err(format!(
"{} must be passed as Signer or SignerRO",
std::any::type_name::<Self>()
)
.into()),
}
} }
} }
impl<T> Wrap for Signer<T> { impl<'a, 'b: 'a, T, const Seed: &'static str> Wrap for Derive<T, Seed> {
fn wrap(&self) -> Vec<AccountMeta> { fn wrap(a: &AccEntry) -> StdResult<Vec<AccountMeta>, ErrBox> {
todo!() match a {
} AccEntry::Derived(program_id) => {
} let (k, extra_seed) = Pubkey::find_program_address(&[Seed.as_bytes()], &program_id);
impl<T, const Seed: &'static str> Wrap for Derive<T, Seed> { Ok(vec![AccountMeta::new(k, false)])
fn wrap(&self) -> Vec<AccountMeta> { }
todo!() AccEntry::DerivedRO(program_id) => {
let (k, extra_seed) = Pubkey::find_program_address(&[Seed.as_bytes()], &program_id);
Ok(vec![AccountMeta::new_readonly(k, false)])
}
other => Err(format!(
"{} must be passed as Derived or DerivedRO",
std::any::type_name::<Self>()
)
.into()),
}
} }
} }
impl<'a, T: BorshSerialize + Owned + Default, const IsInitialized: AccountState> Wrap impl<'a, T: BorshSerialize + Owned + Default, const IsInitialized: AccountState> Wrap
for Data<'a, T, IsInitialized> for Data<'a, T, IsInitialized>
{ {
fn wrap(&self) -> Vec<AccountMeta> { fn wrap(a: &AccEntry) -> StdResult<Vec<AccountMeta>, ErrBox> {
todo!() todo!();
} }
} }
@ -141,8 +202,9 @@ pub trait InstructionContext<'a> {
} }
} }
pub trait ToAccounts { /// Trait used on client side to easily validate a program accounts + ix_data for a bare Solana call
fn to(&self) -> Vec<AccountMeta>; pub trait ToInstruction {
fn to_ix(self, program_id: Pubkey, ix_data: &[u8]) -> StdResult<(Instruction, Vec<Keypair>), ErrBox>;
} }
/// Trait definition that describes types that can be constructed from a list of solana account /// Trait definition that describes types that can be constructed from a list of solana account

View File

@ -13,7 +13,7 @@ use std::ops::{
#[macro_export] #[macro_export]
macro_rules! solitaire { macro_rules! solitaire {
{ $($row:ident($kind:ty) => $fn:ident),+ $(,)* } => { { $($row:ident($kind:ty) => $fn:ident),+ $(,)* } => {
mod instruction { pub mod instruction {
use super::*; use super::*;
use borsh::{BorshDeserialize, BorshSerialize}; use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{ use solana_program::{
@ -60,7 +60,7 @@ macro_rules! solitaire {
/// This module contains a 1-1 mapping for each function to an enum variant. The variants /// This module contains a 1-1 mapping for each function to an enum variant. The variants
/// can be matched to the Instruction found above. /// can be matched to the Instruction found above.
mod client { pub mod client {
use super::*; use super::*;
use borsh::BorshSerialize; use borsh::BorshSerialize;
use solana_program::{instruction::Instruction, pubkey::Pubkey}; use solana_program::{instruction::Instruction, pubkey::Pubkey};

View File

@ -1,8 +1,8 @@
#![allow(warnings)] #![allow(warnings)]
mod to_accounts; mod to_instruction;
use to_accounts::*; use to_instruction::*;
use solana_program::{ use solana_program::{
account_info::AccountInfo, account_info::AccountInfo,
@ -12,7 +12,7 @@ use solana_program::{
}; };
use proc_macro::TokenStream; use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2; use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{ use quote::{
quote, quote,
quote_spanned, quote_spanned,
@ -30,21 +30,41 @@ use syn::{
Index, Index,
}; };
#[proc_macro_derive(ToAccounts)] #[proc_macro_derive(ToInstruction)]
pub fn derive_to_accounts(input: TokenStream) -> TokenStream { pub fn derive_to_instruction(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput); let input = parse_macro_input!(input as DeriveInput);
let name = input.ident; let name = input.ident;
let to_method_body = generate_to_method(&name, &input.data);
let expanded = quote! { // Type params of the instruction context account
/// Macro-generated implementation of ToAccounts by Solitaire. let type_params: Vec<GenericParam> = input
impl<'a> solitaire::ToAccounts for #name<'a> { .generics
fn to(&self) -> Vec<solana_program::instruction::AccountMeta> { .type_params()
#to_method_body .map(|v| GenericParam::Type(v.clone()))
} .collect();
// Generics lifetimes of the peel type
let mut peel_g = input.generics.clone();
peel_g.params = parse_quote!('a, 'b: 'a, 'c);
let (_, peel_type_g, _) = peel_g.split_for_impl();
// Params of the instruction context
let mut type_generics = input.generics.clone();
type_generics.params = parse_quote!('b);
for x in &type_params {
type_generics.params.push(x.clone());
} }
}; let (type_impl_g, type_g, _) = type_generics.split_for_impl();
// Combined lifetimes of peel and the instruction context
let mut combined_generics = Generics::default();
combined_generics.params = peel_g.params.clone();
for x in &type_params {
combined_generics.params.push(x.clone());
}
let (combined_impl_g, _, _) = combined_generics.split_for_impl();
let from_method = generate_fields(&name, &input.data);
let expanded = generate_to_instruction(&name, &combined_impl_g, &input.data);
TokenStream::from(expanded) TokenStream::from(expanded)
} }

View File

@ -1,44 +0,0 @@
//! Derive macro logic for ToAccounts
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{
quote,
quote_spanned,
};
use syn::{
parse_macro_input,
parse_quote,
spanned::Spanned,
Data,
DataStruct,
DeriveInput,
Fields,
GenericParam,
Generics,
Index,
};
pub fn generate_to_method(name: &syn::Ident, data: &Data) -> TokenStream2 {
match *data {
Data::Struct(DataStruct {
fields: Fields::Named(ref fields),
..
}) => {
let expanded_fields = fields.named.iter().map(|field| {
let name = &field.ident;
quote! {
v.append(&mut solitaire::Wrap::wrap(&self.#name))
}
});
quote! {
let mut v = Vec::new();
#(#expanded_fields;)*
v
}
}
_ => unimplemented!(),
}
}

View File

@ -0,0 +1,102 @@
//! Derive macro logic for ToInstruction
use proc_macro::TokenStream;
use proc_macro2::{
Span,
TokenStream as TokenStream2,
};
use quote::{
quote,
quote_spanned,
};
use syn::{
parse_macro_input,
parse_quote,
spanned::Spanned,
Data,
DataStruct,
DeriveInput,
Fields,
GenericParam,
Generics,
Index,
};
pub fn generate_to_instruction(name: &syn::Ident, impl_generics: &syn::ImplGenerics, data: &Data) -> TokenStream2 {
match *data {
Data::Struct(DataStruct {
fields: Fields::Named(ref fields),
..
}) => {
let expanded_appends = fields.named.iter().map(|field| {
let name = &field.ident;
let ty = &field.ty;
quote! {
account_metas.append(&mut <#ty as solitaire::Wrap>::wrap(&self.#name)?);
if let Some(pair) = <#ty as solitaire::Wrap>::keypair(self.#name) {
signers.push(pair);
}
}
});
let client_struct_name =
syn::Ident::new(&format!("{}Accounts", name.to_string()), Span::call_site());
let client_struct_decl = generate_clientside_struct(&name, &client_struct_name, &data);
quote! {
/// Solitaire-generated client-side #name representation
#client_struct_decl
/// Solitaire-generatied ToInstruction implementation
impl #impl_generics solitaire::ToInstruction for #client_struct_name {
fn to_ix(
self,
program_id: solana_program::pubkey::Pubkey,
ix_data: &[u8]) -> std::result::Result<
(solana_program::instruction::Instruction, Vec<solana_sdk::signer::keypair::Keypair>),
solitaire::ErrBox
> {
use solana_program::{pubkey::Pubkey, instruction::Instruction};
let mut account_metas = Vec::new();
let mut signers = Vec::new();
#(#expanded_appends;)*
Ok((solana_program::instruction::Instruction::new_with_bytes(program_id,
ix_data,
account_metas), signers))
}
}
}
}
_ => unimplemented!(),
}
}
pub fn generate_clientside_struct(name: &syn::Ident, client_struct_name: &syn::Ident, data: &Data) -> TokenStream2 {
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::AccEntry
}
});
quote! {
pub struct #client_struct_name {
#(pub #expanded_fields,)*
}
}
}
_ => unimplemented!(),
}
}

View File

@ -24,6 +24,7 @@ byteorder = "1.3.4"
zerocopy = "0.3.0" zerocopy = "0.3.0"
sha3 = "0.9.1" sha3 = "0.9.1"
primitive-types = { version = "0.7.2", default-features = false } primitive-types = { version = "0.7.2", default-features = false }
solana-sdk = "1.7.0"
[dev-dependencies] [dev-dependencies]
rand = { version = "0.7.0" } rand = { version = "0.7.0" }