diff --git a/Cargo.lock b/Cargo.lock index 657fa0005..94af61752 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -489,9 +489,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.70" +version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" [[package]] name = "arrayref" @@ -1206,6 +1206,7 @@ name = "clockwork-thread-program" version = "2.0.17" dependencies = [ "anchor-lang", + "bincode", "chrono", "clockwork-cron", "clockwork-network-program", @@ -1276,6 +1277,7 @@ dependencies = [ "serde_json", "simple-error", "solana-account-decoder", + "solana-address-lookup-table-program", "solana-client", "solana-geyser-plugin-interface", "solana-logger", diff --git a/plugin/Cargo.toml b/plugin/Cargo.toml index 55e3dd2c5..31655f65f 100644 --- a/plugin/Cargo.toml +++ b/plugin/Cargo.toml @@ -43,6 +43,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" simple-error = "0.2.3" solana-account-decoder = "=1.14.16" +solana-address-lookup-table-program = "1.10.1" solana-client = "=1.14.16" solana-geyser-plugin-interface = "=1.14.16" solana-logger = "=1.14.16" diff --git a/plugin/src/builders/pool_rotation.rs b/plugin/src/builders/pool_rotation.rs index 85c2f504b..49309d6d8 100644 --- a/plugin/src/builders/pool_rotation.rs +++ b/plugin/src/builders/pool_rotation.rs @@ -1,13 +1,12 @@ use std::sync::Arc; -use anchor_lang::{ - solana_program::instruction::Instruction, - InstructionData, ToAccountMetas -}; +use anchor_lang::{solana_program::instruction::Instruction, InstructionData, ToAccountMetas}; use clockwork_network_program::state::{Config, Pool, Registry, Snapshot, SnapshotFrame, Worker}; use log::info; use solana_client::nonblocking::rpc_client::RpcClient; -use solana_sdk::{signature::Keypair, signer::Signer, transaction::Transaction}; +use solana_geyser_plugin_interface::geyser_plugin_interface::GeyserPluginError; +use solana_program::message::{v0, VersionedMessage}; +use solana_sdk::{signature::Keypair, signer::Signer, transaction::VersionedTransaction}; use crate::pool_position::PoolPosition; @@ -19,7 +18,7 @@ pub async fn build_pool_rotation_tx<'a>( snapshot: Snapshot, snapshot_frame: SnapshotFrame, worker_id: u64, -) -> Option { +) -> Option { info!("nonce: {:?} total_stake: {:?} current_position: {:?} stake_offset: {:?} stake_amount: {:?}", registry.nonce.checked_rem(snapshot.total_stake), snapshot.total_stake, @@ -76,12 +75,26 @@ pub async fn build_pool_rotation_tx<'a>( snapshot: snapshot_pubkey, snapshot_frame: SnapshotFrame::pubkey(snapshot_pubkey, worker_id), worker: Worker::pubkey(worker_id), - }.to_account_metas(Some(false)), + } + .to_account_metas(Some(false)), data: clockwork_network_program::instruction::PoolRotate {}.data(), }; // Build and sign tx. - let mut tx = Transaction::new_with_payer(&[ix.clone()], Some(&keypair.pubkey())); - tx.sign(&[keypair], client.get_latest_blockhash().await.unwrap()); - return Some(tx); + let blockhash = client.get_latest_blockhash().await.unwrap(); + + let tx = match v0::Message::try_compile(&keypair.pubkey(), &[ix.clone()], &[], blockhash) { + Err(_) => Err(GeyserPluginError::Custom( + format!("Failed to compile to v0 message ").into(), + )), + Ok(message) => { + match VersionedTransaction::try_new(VersionedMessage::V0(message), &[keypair]) { + Err(_) => Err(GeyserPluginError::Custom( + format!("Failed to create versioned transaction ").into(), + )), + Ok(tx) => Ok(tx), + } + } + }; + return tx.ok(); } diff --git a/plugin/src/builders/thread_exec.rs b/plugin/src/builders/thread_exec.rs index e0ac02df7..3a1015a9e 100644 --- a/plugin/src/builders/thread_exec.rs +++ b/plugin/src/builders/thread_exec.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use anchor_lang::{InstructionData, ToAccountMetas}; use clockwork_thread_program::state::{VersionedThread, Trigger}; use clockwork_network_program::state::Worker; +use bincode::serialize; use clockwork_utils::thread::PAYER_PUBKEY; use log::info; use solana_account_decoder::UiAccountEncoding; @@ -16,12 +17,16 @@ use solana_geyser_plugin_interface::geyser_plugin_interface::{ }; use solana_program::{ instruction::{AccountMeta, Instruction}, - pubkey::Pubkey, + pubkey::Pubkey, address_lookup_table_account::AddressLookupTableAccount, }; use solana_sdk::{ - account::Account, commitment_config::CommitmentConfig, - compute_budget::ComputeBudgetInstruction, signature::Keypair, signer::Signer, - transaction::Transaction, + account::Account, + commitment_config::CommitmentConfig, + compute_budget::ComputeBudgetInstruction, + message::{v0, VersionedMessage}, + signature::Keypair, + signer::Signer, + transaction::{VersionedTransaction}, }; /// Max byte size of a serialized transaction. @@ -40,7 +45,8 @@ pub async fn build_thread_exec_tx( thread: VersionedThread, thread_pubkey: Pubkey, worker_id: u64, -) -> PluginResult> { + address_lookup_tables: Vec +) -> PluginResult> { // Grab the thread and relevant data. let now = std::time::Instant::now(); let blockhash = client.get_latest_blockhash().await.unwrap(); @@ -73,11 +79,24 @@ pub async fn build_thread_exec_tx( let mut successful_ixs: Vec = vec![]; let mut units_consumed: Option = None; loop { - let mut sim_tx = Transaction::new_with_payer(&ixs, Some(&signatory_pubkey)); - sim_tx.sign(&[payer], blockhash); + let sim_tx = match v0::Message::try_compile( + &signatory_pubkey, + &ixs, + &address_lookup_tables, + blockhash, + ) { + Err(_) => Err(GeyserPluginError::Custom(format!("Failed to compile to v0 message ").into())), + Ok(message) => match VersionedTransaction::try_new( + VersionedMessage::V0(message), + &[payer] + ) { + Err(_) => Err(GeyserPluginError::Custom(format!("Failed to create versioned transaction ").into())), + Ok(tx) => Ok(tx) + } + }?; // Exit early if the transaction exceeds the size limit. - if sim_tx.message_data().len() > TRANSACTION_MESSAGE_SIZE_LIMIT { + if serialize(&sim_tx).unwrap().len() > TRANSACTION_MESSAGE_SIZE_LIMIT { break; } @@ -198,9 +217,26 @@ pub async fn build_thread_exec_tx( ); } + // let mut tx = Transaction::new_with_payer(&successful_ixs, Some(&signatory_pubkey)); + // tx.sign(&[payer], blockhash); + // Build and return the signed transaction. - let mut tx = Transaction::new_with_payer(&successful_ixs, Some(&signatory_pubkey)); - tx.sign(&[payer], blockhash); + let tx = match v0::Message::try_compile( + &signatory_pubkey, + &ixs, + &address_lookup_tables, + blockhash, + ) { + Err(_) => Err(GeyserPluginError::Custom(format!("Failed to compile to v0 message ").into())), + Ok(message) => match VersionedTransaction::try_new( + VersionedMessage::V0(message), + &[payer] + ) { + Err(_) => Err(GeyserPluginError::Custom(format!("Failed to create versioned transaction ").into())), + Ok(tx) => Ok(tx) + } + + }?; info!( "slot: {:?} thread: {:?} sim_duration: {:?} instruction_count: {:?} compute_units: {:?} tx_sig: {:?}", slot, diff --git a/plugin/src/executors/mod.rs b/plugin/src/executors/mod.rs index f47c3464a..417f335f7 100644 --- a/plugin/src/executors/mod.rs +++ b/plugin/src/executors/mod.rs @@ -11,12 +11,15 @@ use std::{ use anchor_lang::{prelude::Pubkey, AccountDeserialize}; use async_trait::async_trait; +use clockwork_thread_program::state::LookupTables; use log::info; +use solana_address_lookup_table_program::state::AddressLookupTable; use solana_client::{ client_error::{ClientError, ClientErrorKind, Result as ClientResult}, nonblocking::rpc_client::RpcClient, }; use solana_geyser_plugin_interface::geyser_plugin_interface::Result as PluginResult; +use solana_program::address_lookup_table_account::AddressLookupTableAccount; use solana_sdk::commitment_config::CommitmentConfig; use tokio::runtime::Runtime; use tx::TxExecutor; @@ -137,3 +140,57 @@ impl AccountGet for RpcClient { }) } } + +#[async_trait] +pub trait LookupTablesGet { + async fn get_lookup_tables( + &self, + pubkey: &Pubkey, + ) -> ClientResult>; +} + +#[async_trait] +impl LookupTablesGet for RpcClient { + async fn get_lookup_tables( + &self, + pubkey: &Pubkey, + ) -> ClientResult> { + let lookup_account = self + .get_account_with_commitment(pubkey, self.commitment()) // returns Ok(None) if lookup account is not initialized + .await? + .value; + match lookup_account { + // return empty vec if lookup account has not been initialized + None => Ok(vec![]), + + // get lookup tables in lookup accounts if account has been initialized + Some(lookup) => { + let lookup_keys = LookupTables::try_deserialize(&mut lookup.data.as_slice()) + .map_err(|_| { + ClientError::from(ClientErrorKind::Custom(format!( + "Failed to deserialize account data" + ))) + }) + .expect("Failed to deserialize lookup data") + .lookup_tables; + + let lookup_tables = + futures::future::join_all(lookup_keys.iter().map(|key| async move { + let raw_account = self.get_account(key).await?; + let address_lookup_table = + AddressLookupTable::deserialize(&raw_account.data).map_err(|_| { + ClientError::from(ClientErrorKind::Custom(format!( + "Could not deserialise Address Lookup Table" + ))) + })?; + Ok(AddressLookupTableAccount { + key: *key, + addresses: address_lookup_table.addresses.to_vec(), + }) + })) + .await; + lookup_tables.into_iter().collect() + } + } + } +} diff --git a/plugin/src/executors/tx.rs b/plugin/src/executors/tx.rs index 8ff4d0511..5d64e6a91 100644 --- a/plugin/src/executors/tx.rs +++ b/plugin/src/executors/tx.rs @@ -10,7 +10,7 @@ use std::{ use async_once::AsyncOnce; use bincode::serialize; use clockwork_network_program::state::{Pool, Registry, Snapshot, SnapshotFrame, Worker}; -use clockwork_thread_program::state::VersionedThread; +use clockwork_thread_program::state::{LookupTables, VersionedThread}; use lazy_static::lazy_static; use log::info; use solana_client::{ @@ -25,13 +25,13 @@ use solana_program::pubkey::Pubkey; use solana_sdk::{ commitment_config::CommitmentConfig, signature::{Keypair, Signature}, - transaction::Transaction, + transaction::{VersionedTransaction}, }; use tokio::{runtime::Runtime, sync::RwLock}; use crate::{config::PluginConfig, pool_position::PoolPosition, utils::read_or_new_keypair}; -use super::AccountGet; +use super::{AccountGet, LookupTablesGet}; /// Number of slots to wait before checking for a confirmed transaction. static TRANSACTION_CONFIRMATION_PERIOD: u64 = 24; @@ -428,7 +428,7 @@ impl TxExecutor { observed_slot: u64, due_slot: u64, thread_pubkey: Pubkey, - ) -> Option<(Pubkey, Transaction, u64)> { + ) -> Option<(Pubkey, VersionedTransaction, u64)> { let thread = match client.clone().get::(&thread_pubkey).await { Err(_err) => { self.increment_simulation_failure(thread_pubkey).await; @@ -447,6 +447,15 @@ impl TxExecutor { return None; } } + let lookup_tables_key = LookupTables::pubkey(thread.authority(), thread.pubkey()); + + let address_lookup_tables = match client.clone().get_lookup_tables(&lookup_tables_key).await + { + Err(_err) => { + return None; + } + Ok(address_lookup_tables) => address_lookup_tables, + }; if let Ok(tx) = crate::builders::build_thread_exec_tx( client.clone(), @@ -455,6 +464,7 @@ impl TxExecutor { thread, thread_pubkey, self.config.worker_id, + address_lookup_tables, ) .await { @@ -490,7 +500,7 @@ impl TxExecutor { self: Arc, slot: u64, thread_pubkey: Pubkey, - tx: &Transaction, + tx: &VersionedTransaction, ) -> PluginResult<()> { let r_transaction_history = self.transaction_history.read().await; if let Some(metadata) = r_transaction_history.get(&thread_pubkey) { @@ -502,7 +512,10 @@ impl TxExecutor { Ok(()) } - async fn simulate_tx(self: Arc, tx: &Transaction) -> PluginResult { + async fn simulate_tx( + self: Arc, + tx: &VersionedTransaction, + ) -> PluginResult { TPU_CLIENT .get() .await @@ -531,8 +544,13 @@ impl TxExecutor { })? } - async fn submit_tx(self: Arc, tx: &Transaction) -> PluginResult { - if !TPU_CLIENT.get().await.send_transaction(tx).await { + async fn submit_tx( + self: Arc, + tx: &VersionedTransaction, + ) -> PluginResult { + let serialized_tx = serialize(tx).unwrap(); + + if !TPU_CLIENT.get().await.send_wire_transaction(serialized_tx).await { return Err(GeyserPluginError::Custom( "Failed to send transaction".into(), )); diff --git a/programs/thread/Cargo.toml b/programs/thread/Cargo.toml index 26444929c..899448ef2 100644 --- a/programs/thread/Cargo.toml +++ b/programs/thread/Cargo.toml @@ -31,3 +31,4 @@ clockwork-utils = { path = "../../utils", version = "=2.0.17" } pyth-sdk-solana = "0.7.1" static-pubkey = "1.0.3" version = "3.0.0" +bincode = "1.3.3" diff --git a/programs/thread/src/errors.rs b/programs/thread/src/errors.rs index 3bd0aee73..1212f6c47 100644 --- a/programs/thread/src/errors.rs +++ b/programs/thread/src/errors.rs @@ -43,4 +43,8 @@ pub enum ClockworkError { /// Thrown if the user attempts to withdraw SOL that would put a thread below it's minimum rent threshold. #[msg("Withdrawing this amount would leave the thread with less than the minimum required SOL for rent exemption")] WithdrawalTooLarge, + + /// Thrown if the size of the instruction to be added to the thread is larger than the next instruction size + #[msg("Instruction too large for thread")] + InstructionTooLarge, } diff --git a/programs/thread/src/instructions/mod.rs b/programs/thread/src/instructions/mod.rs index 533d94503..f3b58552e 100644 --- a/programs/thread/src/instructions/mod.rs +++ b/programs/thread/src/instructions/mod.rs @@ -4,12 +4,18 @@ pub mod thread_delete; pub mod thread_exec; pub mod thread_instruction_add; pub mod thread_instruction_remove; +pub mod thread_lookup_tables_create; +pub mod thread_lookup_tables_delete; +pub mod thread_lookup_tables_add; +pub mod thread_lookup_tables_remove; pub mod thread_kickoff; pub mod thread_pause; pub mod thread_reset; pub mod thread_resume; pub mod thread_update; pub mod thread_withdraw; +pub mod thread_big_instruction_add; +pub mod thread_dummy_ix; pub use get_crate_info::*; pub use thread_create::*; @@ -17,9 +23,15 @@ pub use thread_delete::*; pub use thread_exec::*; pub use thread_instruction_add::*; pub use thread_instruction_remove::*; +pub use thread_lookup_tables_create::*; +pub use thread_lookup_tables_delete::*; +pub use thread_lookup_tables_add::*; +pub use thread_lookup_tables_remove::*; pub use thread_kickoff::*; pub use thread_pause::*; pub use thread_reset::*; pub use thread_resume::*; pub use thread_update::*; pub use thread_withdraw::*; +pub use thread_big_instruction_add::*; +pub use thread_dummy_ix::*; diff --git a/programs/thread/src/instructions/thread_big_instruction_add.rs b/programs/thread/src/instructions/thread_big_instruction_add.rs new file mode 100644 index 000000000..d2b7f6740 --- /dev/null +++ b/programs/thread/src/instructions/thread_big_instruction_add.rs @@ -0,0 +1,90 @@ +use bincode::serialize; + +use anchor_lang::{ + prelude::*, + solana_program::system_program, + system_program::{transfer, Transfer}, +}; + +use crate::{errors::ClockworkError, state::*}; + +/// Accounts required by the `thread_instruction_add` instruction. +#[derive(Accounts)] +pub struct ThreadBigInstructionAdd<'info> { + /// The authority (owner) of the thread. + #[account(mut)] + pub authority: Signer<'info>, + + /// The Solana system program + #[account(address = system_program::ID)] + pub system_program: Program<'info, System>, + + /// The thread to be paused. + #[account( + mut, + seeds = [ + SEED_THREAD, + thread.authority.as_ref(), + thread.id.as_slice(), + ], + bump = thread.bump, + has_one = authority + )] + pub thread: Account<'info, Thread>, + + /// CHECK: program_id of the instruction to build + #[account(executable)] + pub instruction_program_id: UncheckedAccount<'info>, +} + +pub fn handler( + ctx: Context, + instruction_data: Vec, +) -> Result<()> { + // Get accounts + let authority = &ctx.accounts.authority; + let thread = &mut ctx.accounts.thread; + let system_program = &ctx.accounts.system_program; + + let ix_accounts = ctx.remaining_accounts.into_iter().map(|acct_info| SerializableAccount { + is_signer: acct_info.is_signer, // false + is_writable: acct_info.is_writable, + pubkey: acct_info.key() + }).collect::>(); + + let build_ix = SerializableInstruction { + accounts: ix_accounts, + data: instruction_data, + program_id: ctx.accounts.instruction_program_id.key(), + }; + + // Check if the instruction hit next instruction size limit + let ix_size = serialize(&build_ix).unwrap().len(); + require!(ix_size <= NEXT_INSTRUCTION_SIZE, ClockworkError::InstructionTooLarge); + + // Append the instruction. + thread.instructions.push(build_ix); + + // Reallocate mem for the thread account. + thread.realloc()?; + + // If lamports are required to maintain rent-exemption, pay them. + let data_len = 8 + thread.try_to_vec()?.len(); + let minimum_rent = Rent::get().unwrap().minimum_balance(data_len); + if minimum_rent > thread.to_account_info().lamports() { + transfer( + CpiContext::new( + system_program.to_account_info(), + Transfer { + from: authority.to_account_info(), + to: thread.to_account_info(), + }, + ), + minimum_rent + .checked_sub(thread.to_account_info().lamports()) + .unwrap(), + )?; + } + + Ok(()) +} diff --git a/programs/thread/src/instructions/thread_dummy_ix.rs b/programs/thread/src/instructions/thread_dummy_ix.rs new file mode 100644 index 000000000..e555fb058 --- /dev/null +++ b/programs/thread/src/instructions/thread_dummy_ix.rs @@ -0,0 +1,13 @@ +use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct ThreadDummyIx<'info> { + /// CHECK: thread + pub thread: UncheckedAccount<'info>, +} + +pub fn handler (ctx: Context) -> Result<()> { + msg!("Hello, Clockwork Thread: {:#?}", ctx.accounts.thread.key); + Ok(()) +} + diff --git a/programs/thread/src/instructions/thread_lookup_tables_add.rs b/programs/thread/src/instructions/thread_lookup_tables_add.rs new file mode 100644 index 000000000..baf9d9ac1 --- /dev/null +++ b/programs/thread/src/instructions/thread_lookup_tables_add.rs @@ -0,0 +1,82 @@ +use anchor_lang::{ + prelude::*, + solana_program::system_program, + system_program::{transfer, Transfer}, +}; + +use crate::state::*; + +/// Accounts required by the `thread_address_lookup_tables_add` instruction. +#[derive(Accounts)] +#[instruction(address_lookup_tables: Vec)] +pub struct LookupTablesAdd<'info> { + /// The authority (owner) of the thread. + #[account(mut)] + pub authority: Signer<'info>, + + /// The Solana system program + #[account(address = system_program::ID)] + pub system_program: Program<'info, System>, + + /// The thread to be paused. + #[account( + seeds = [ + SEED_THREAD, + thread.authority.as_ref(), + thread.id.as_slice(), + ], + bump = thread.bump, + has_one = authority + )] + pub thread: Account<'info, Thread>, + + /// The lookup tables to be edited. + #[account( + mut, + seeds = [ + SEED_LOOKUP, + lookup_tables.authority.as_ref(), + lookup_tables.thread.as_ref(), + ], + bump = lookup_tables.bump, + has_one = authority, + has_one = thread, + )] + pub lookup_tables: Account<'info, LookupTables>, +} + +pub fn handler( + ctx: Context, + address_lookup_tables: Vec, +) -> Result<()> { + // Get accounts + let authority = &ctx.accounts.authority; + let lookup_tables = &mut ctx.accounts.lookup_tables; + let system_program = &ctx.accounts.system_program; + + // Append the address lookup tables. + lookup_tables.lookup_tables.extend(address_lookup_tables.iter()); + + // Reallocate mem for the thread account. + lookup_tables.realloc()?; + + // If lamports are required to maintain rent-exemption, pay them. + let data_len = 8 + lookup_tables.try_to_vec()?.len(); + let minimum_rent = Rent::get().unwrap().minimum_balance(data_len); + if minimum_rent > lookup_tables.to_account_info().lamports() { + transfer( + CpiContext::new( + system_program.to_account_info(), + Transfer { + from: authority.to_account_info(), + to: lookup_tables.to_account_info(), + }, + ), + minimum_rent + .checked_sub(lookup_tables.to_account_info().lamports()) + .unwrap(), + )?; + } + + Ok(()) +} diff --git a/programs/thread/src/instructions/thread_lookup_tables_create.rs b/programs/thread/src/instructions/thread_lookup_tables_create.rs new file mode 100644 index 000000000..1a6565347 --- /dev/null +++ b/programs/thread/src/instructions/thread_lookup_tables_create.rs @@ -0,0 +1,72 @@ +use std::mem::size_of; + +use anchor_lang::{ + prelude::*, + solana_program::system_program, +}; + +use crate::state::*; + + +/// Accounts required by the `thread_lookup_tables_create` instruction. +#[derive(Accounts)] +#[instruction(address_lookup_tables: Vec)] +pub struct LookupTablesCreate<'info> { + /// The authority (owner) of the thread. + #[account()] + pub authority: Signer<'info>, + + /// The payer for account initializations. + #[account(mut)] + pub payer: Signer<'info>, + + /// The Solana system program. + #[account(address = system_program::ID)] + pub system_program: Program<'info, System>, + + /// The thread to be paused. + #[account( + seeds = [ + SEED_THREAD, + thread.authority.as_ref(), + thread.id.as_slice(), + ], + bump = thread.bump, + has_one = authority + )] + pub thread: Account<'info, Thread>, + + /// The lookup_tables account to be created. + #[account( + init, + seeds = [ + SEED_LOOKUP, + authority.key().as_ref(), + thread.key().as_ref(), + ], + bump, + payer= payer, + space = vec![ + 8, + size_of::(), + address_lookup_tables.try_to_vec()?.len(), + ].iter().sum() + )] + pub lookup_tables: Account<'info, LookupTables>, +} + +pub fn handler(ctx: Context, address_lookup_tables: Vec) -> Result<()> { + // Get accounts + let authority = &ctx.accounts.authority; + let thread = &ctx.accounts.thread; + let lookup_tables = &mut ctx.accounts.lookup_tables; + + // Initialize the thread + let bump = *ctx.bumps.get("lookup_tables").unwrap(); + lookup_tables.authority = authority.key(); + lookup_tables.bump = bump; + lookup_tables.thread = thread.key(); + lookup_tables.lookup_tables = address_lookup_tables; + + Ok(()) +} diff --git a/programs/thread/src/instructions/thread_lookup_tables_delete.rs b/programs/thread/src/instructions/thread_lookup_tables_delete.rs new file mode 100644 index 000000000..e109b3b0b --- /dev/null +++ b/programs/thread/src/instructions/thread_lookup_tables_delete.rs @@ -0,0 +1,59 @@ +use {crate::state::*, anchor_lang::prelude::*}; + +/// Accounts required by the `thread_delete` instruction. +#[derive(Accounts)] +pub struct LookupTablesDelete<'info> { + /// The authority (owner) of the thread. + #[account( + constraint = authority.key().eq(&thread.authority) || authority.key().eq(&thread.key()) + )] + pub authority: Signer<'info>, + + /// The address to return the data rent lamports to. + #[account(mut)] + pub close_to: SystemAccount<'info>, + + /// The thread whose lookup tables is to be closed. + #[account( + seeds = [ + SEED_THREAD, + thread.authority.as_ref(), + thread.id.as_slice(), + ], + bump = thread.bump, + )] + pub thread: Account<'info, Thread>, + + /// The lookup tables to be deleted + #[account( + mut, + seeds = [ + SEED_LOOKUP, + lookup_tables.authority.as_ref(), + lookup_tables.thread.as_ref(), + ], + bump = lookup_tables.bump, + has_one = authority, + has_one = thread, + )] + pub lookup_tables: Account<'info, LookupTables>, +} + +pub fn handler(ctx: Context) -> Result<()> { + let lookup_tables = &ctx.accounts.lookup_tables; + let close_to = &ctx.accounts.close_to; + + let lookup_tables_lamports = lookup_tables.to_account_info().lamports(); + **lookup_tables.to_account_info().try_borrow_mut_lamports()? = lookup_tables + .to_account_info() + .lamports() + .checked_sub(lookup_tables_lamports) + .unwrap(); + **close_to.to_account_info().try_borrow_mut_lamports()? = close_to + .to_account_info() + .lamports() + .checked_add(lookup_tables_lamports) + .unwrap(); + + Ok(()) +} diff --git a/programs/thread/src/instructions/thread_lookup_tables_remove.rs b/programs/thread/src/instructions/thread_lookup_tables_remove.rs new file mode 100644 index 000000000..731f1ed55 --- /dev/null +++ b/programs/thread/src/instructions/thread_lookup_tables_remove.rs @@ -0,0 +1,46 @@ +use {crate::state::*, anchor_lang::prelude::*}; + +/// Accounts required by the `thread_instruction_remove` instruction. +#[derive(Accounts)] +#[instruction(index: u64)] +pub struct LookupTablesRemove<'info> { + /// The authority (owner) of the thread. + #[account()] + pub authority: Signer<'info>, + + // the thread owner of the lookup tables + #[account( + seeds = [ + SEED_THREAD, + thread.authority.as_ref(), + thread.id.as_slice(), + ], + bump = thread.bump, + has_one = authority + )] + pub thread: Account<'info, Thread>, + + /// The lookup tables to be edited. + #[account( + mut, + seeds = [ + SEED_LOOKUP, + lookup_tables.authority.as_ref(), + lookup_tables.thread.as_ref(), + ], + bump = lookup_tables.bump, + has_one = authority, + has_one = thread, + )] + pub lookup_tables: Account<'info, LookupTables>, +} + +pub fn handler(ctx: Context, index: u64) -> Result<()> { + // Get accounts + let lookup_tables = &mut ctx.accounts.lookup_tables; + + // remove the lookup table key + lookup_tables.lookup_tables.remove(index as usize); + + Ok(()) +} diff --git a/programs/thread/src/lib.rs b/programs/thread/src/lib.rs index 3eb47de99..750d31f39 100644 --- a/programs/thread/src/lib.rs +++ b/programs/thread/src/lib.rs @@ -95,4 +95,34 @@ pub mod thread_program { pub fn thread_withdraw(ctx: Context, amount: u64) -> Result<()> { thread_withdraw::handler(ctx, amount) } + + /// Allows the owner to create an account to hold lookup tables + pub fn thread_lookup_tables_create(ctx: Context, address_lookup_tables: Vec) -> Result<()> { + thread_lookup_tables_create::handler(ctx, address_lookup_tables) + } + + /// Allows the owner to add lookup tables + pub fn thread_lookup_tables_add(ctx: Context, address_lookup_tables: Vec) -> Result<()> { + thread_lookup_tables_add::handler(ctx, address_lookup_tables) + } + + /// Allows the owner to remove a lookup table + pub fn thread_lookup_tables_remove(ctx: Context, index: u64) -> Result<()> { + thread_lookup_tables_remove::handler(ctx, index) + } + + /// Allows the owner to delete the lookup account + pub fn thread_lookup_tables_delete(ctx: Context) -> Result<()> { + thread_lookup_tables_delete::handler(ctx) + } + + /// Allows the user add big instrucion to the thread + pub fn thread_big_instruction_add(ctx: Context, instruction_data: Vec) -> Result<()> { + thread_big_instruction_add::handler(ctx, instruction_data) + } + + /// Dummy instruction + pub fn thread_dummy_ix(ctx: Context) -> Result<()> { + thread_dummy_ix::handler(ctx) + } } diff --git a/programs/thread/src/state/lookup_tables.rs b/programs/thread/src/state/lookup_tables.rs new file mode 100644 index 000000000..aa0a6ae86 --- /dev/null +++ b/programs/thread/src/state/lookup_tables.rs @@ -0,0 +1,53 @@ +use anchor_lang::{prelude::*, AnchorDeserialize, AnchorSerialize}; + +pub const SEED_LOOKUP: &[u8] = b"lookup"; + +#[account] +#[derive(Debug)] +pub struct LookupTables { + pub thread: Pubkey, + pub authority: Pubkey, + pub lookup_tables: Vec, + pub bump: u8, +} + +impl LookupTables { + /// Derive the pubkey of a lookup account. + pub fn pubkey(authority: Pubkey, thread: Pubkey) -> Pubkey { + Pubkey::find_program_address( + &[SEED_LOOKUP, authority.as_ref(), thread.as_ref()], + &crate::ID, + ) + .0 + } +} + +impl PartialEq for LookupTables { + fn eq(&self, other: &Self) -> bool { + self.authority.eq(&other.authority) && self.thread.eq(&other.thread) + } +} + +impl Eq for LookupTables {} + +/// Trait for reading and writing to a lookuptables account. +pub trait LookupTablesAccount { + /// Get the pubkey of the lookuptables account. + fn pubkey(&self) -> Pubkey; + + /// Allocate more memory for the account. + fn realloc(&mut self) -> Result<()>; +} + +impl LookupTablesAccount for Account<'_, LookupTables> { + fn pubkey(&self) -> Pubkey { + LookupTables::pubkey(self.authority, self.thread) + } + + fn realloc(&mut self) -> Result<()> { + // Realloc memory for the lookuptables account + let data_len = 8 + self.try_to_vec()?.len(); + self.to_account_info().realloc(data_len, false)?; + Ok(()) + } +} \ No newline at end of file diff --git a/programs/thread/src/state/mod.rs b/programs/thread/src/state/mod.rs index 368b9b325..abdb6af05 100644 --- a/programs/thread/src/state/mod.rs +++ b/programs/thread/src/state/mod.rs @@ -2,7 +2,9 @@ mod thread; mod versioned_thread; +mod lookup_tables; pub use clockwork_utils::thread::*; pub use thread::*; pub use versioned_thread::*; +pub use lookup_tables::*; diff --git a/programs/thread/src/state/thread.rs b/programs/thread/src/state/thread.rs index d01f7b126..f463bc498 100644 --- a/programs/thread/src/state/thread.rs +++ b/programs/thread/src/state/thread.rs @@ -8,7 +8,7 @@ pub use clockwork_utils::thread::Equality; pub const SEED_THREAD: &[u8] = b"thread"; /// Static space for next_instruction field. -pub const NEXT_INSTRUCTION_SIZE: usize = 1232; +pub const NEXT_INSTRUCTION_SIZE: usize = 3200; /// Tracks the current state of a transaction thread on Solana. #[account] diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index 74e0ae8d2..77a994885 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -10,16 +10,17 @@ pub mod state { } pub mod utils { - pub use clockwork_thread_program::state::PAYER_PUBKEY; pub use clockwork_thread_program::state::Equality; + pub use clockwork_thread_program::state::PAYER_PUBKEY; } pub mod cpi { - use anchor_lang::prelude::{CpiContext, Result}; + use anchor_lang::prelude::{CpiContext, Pubkey, Result}; pub use clockwork_thread_program::cpi::accounts::{ - ThreadCreate, ThreadDelete, ThreadPause, ThreadReset, ThreadResume, ThreadUpdate, - ThreadWithdraw, + LookupTablesAdd, LookupTablesCreate, LookupTablesDelete, + LookupTablesRemove, ThreadBigInstructionAdd, ThreadCreate, ThreadDelete, ThreadDummyIx, + ThreadPause, ThreadReset, ThreadResume, ThreadUpdate, ThreadWithdraw, }; pub fn thread_create<'info>( @@ -69,4 +70,44 @@ pub mod cpi { ) -> Result<()> { clockwork_thread_program::cpi::thread_withdraw(ctx, amount) } + + pub fn thread_lookup_tables_create<'info>( + ctx: CpiContext<'_, '_, '_, 'info, LookupTablesCreate<'info>>, + address_lookup_tables: Vec, + ) -> Result<()> { + clockwork_thread_program::cpi::thread_lookup_tables_create(ctx, address_lookup_tables) + } + + pub fn thread_lookup_tables_add<'info>( + ctx: CpiContext<'_, '_, '_, 'info, LookupTablesAdd<'info>>, + address_lookup_tables: Vec, + ) -> Result<()> { + clockwork_thread_program::cpi::thread_lookup_tables_add(ctx, address_lookup_tables) + } + + pub fn thread_lookup_tables_remove<'info>( + ctx: CpiContext<'_, '_, '_, 'info, LookupTablesRemove<'info>>, + index: u64, + ) -> Result<()> { + clockwork_thread_program::cpi::thread_lookup_tables_remove(ctx, index) + } + + pub fn thread_lookup_tables_delete<'info>( + ctx: CpiContext<'_, '_, '_, 'info, LookupTablesDelete<'info>>, + ) -> Result<()> { + clockwork_thread_program::cpi::thread_lookup_tables_delete(ctx) + } + + pub fn thread_big_instruction_add<'info>( + ctx: CpiContext<'_, '_, '_, 'info, ThreadBigInstructionAdd<'info>>, + instruction_data: Vec + ) -> Result<()> { + clockwork_thread_program::cpi::thread_big_instruction_add(ctx, instruction_data) + } + + pub fn thread_dummy_ix<'info>( + ctx: CpiContext<'_, '_, '_, 'info, ThreadDummyIx<'info>>, + ) -> Result<()> { + clockwork_thread_program::cpi::thread_dummy_ix(ctx) + } }