├── .gitignore ├── Anchor.toml ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── OLD_VERSION ├── README.md ├── VERSION ├── audits └── ottersec-2023-03-14.pdf ├── cli ├── .gitignore ├── Cargo.toml ├── README.md ├── build.rs └── src │ ├── cli.rs │ ├── client.rs │ ├── config.rs │ ├── deps.rs │ ├── errors.rs │ ├── main.rs │ ├── parser.rs │ ├── print.rs │ └── processor │ ├── config.rs │ ├── crontab.rs │ ├── delegation.rs │ ├── explorer.rs │ ├── initialize.rs │ ├── localnet.rs │ ├── mod.rs │ ├── pool.rs │ ├── registry.rs │ ├── secret.rs │ ├── snapshot.rs │ ├── thread.rs │ ├── webhook.rs │ └── worker.rs ├── cron ├── .gitignore ├── Cargo.toml ├── README.md ├── src │ ├── error.rs │ ├── lib.rs │ ├── ordinal.rs │ ├── parsing.rs │ ├── queries.rs │ ├── schedule.rs │ ├── specifier.rs │ └── time_unit │ │ ├── days_of_month.rs │ │ ├── days_of_week.rs │ │ ├── hours.rs │ │ ├── minutes.rs │ │ ├── mod.rs │ │ ├── months.rs │ │ ├── seconds.rs │ │ └── years.rs └── tests │ └── lib.rs ├── justfile ├── plugin ├── Cargo.toml ├── README.md ├── build.rs ├── config.json ├── src │ ├── builders │ │ ├── mod.rs │ │ ├── pool_rotation.rs │ │ └── thread_exec.rs │ ├── events.rs │ ├── executors │ │ ├── mod.rs │ │ ├── tx.rs │ │ └── webhook.rs │ ├── lib.rs │ ├── observers │ │ ├── mod.rs │ │ ├── thread.rs │ │ └── webhook.rs │ ├── plugin.rs │ ├── pool_position.rs │ └── utils.rs ├── utils │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── config.rs │ │ └── lib.rs └── vector.toml ├── programs ├── network │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── errors.rs │ │ ├── instructions │ │ ├── config_update.rs │ │ ├── delegation_claim.rs │ │ ├── delegation_create.rs │ │ ├── delegation_deposit.rs │ │ ├── delegation_withdraw.rs │ │ ├── fee_collect.rs │ │ ├── initialize.rs │ │ ├── mod.rs │ │ ├── penalty_claim.rs │ │ ├── pool_create.rs │ │ ├── pool_rotate.rs │ │ ├── pool_update.rs │ │ ├── registry_nonce_hash.rs │ │ ├── registry_unlock.rs │ │ ├── unstake_create.rs │ │ ├── worker_claim.rs │ │ ├── worker_create.rs │ │ └── worker_update.rs │ │ ├── jobs │ │ ├── delete_snapshot │ │ │ ├── job.rs │ │ │ ├── mod.rs │ │ │ ├── process_entry.rs │ │ │ ├── process_frame.rs │ │ │ └── process_snapshot.rs │ │ ├── distribute_fees │ │ │ ├── job.rs │ │ │ ├── mod.rs │ │ │ ├── process_entry.rs │ │ │ ├── process_frame.rs │ │ │ └── process_snapshot.rs │ │ ├── increment_epoch │ │ │ ├── job.rs │ │ │ └── mod.rs │ │ ├── mod.rs │ │ ├── process_unstakes │ │ │ ├── job.rs │ │ │ ├── mod.rs │ │ │ ├── unstake_preprocess.rs │ │ │ └── unstake_process.rs │ │ ├── stake_delegations │ │ │ ├── job.rs │ │ │ ├── mod.rs │ │ │ ├── process_delegation.rs │ │ │ └── process_worker.rs │ │ └── take_snapshot │ │ │ ├── create_entry.rs │ │ │ ├── create_frame.rs │ │ │ ├── create_snapshot.rs │ │ │ ├── job.rs │ │ │ └── mod.rs │ │ ├── lib.rs │ │ └── state │ │ ├── config.rs │ │ ├── delegation.rs │ │ ├── epoch.rs │ │ ├── fee.rs │ │ ├── mod.rs │ │ ├── penalty.rs │ │ ├── pool.rs │ │ ├── registry.rs │ │ ├── snapshot.rs │ │ ├── snapshot_entry.rs │ │ ├── snapshot_frame.rs │ │ ├── unstake.rs │ │ └── worker.rs ├── thread │ ├── Cargo.toml │ ├── README.md │ ├── src │ │ ├── errors.rs │ │ ├── instructions │ │ │ ├── get_crate_info.rs │ │ │ ├── mod.rs │ │ │ ├── thread_create.rs │ │ │ ├── thread_delete.rs │ │ │ ├── thread_exec.rs │ │ │ ├── thread_instruction_add.rs │ │ │ ├── thread_instruction_remove.rs │ │ │ ├── thread_kickoff.rs │ │ │ ├── thread_pause.rs │ │ │ ├── thread_reset.rs │ │ │ ├── thread_resume.rs │ │ │ ├── thread_update.rs │ │ │ └── thread_withdraw.rs │ │ ├── lib.rs │ │ └── state │ │ │ ├── crate_info.rs │ │ │ ├── mod.rs │ │ │ ├── thread.rs │ │ │ └── versioned_thread.rs │ └── v1 │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── idl.json │ │ └── src │ │ └── lib.rs └── webhook │ ├── Cargo.toml │ ├── README.md │ └── src │ ├── errors.rs │ ├── instructions │ ├── mod.rs │ ├── webhook_create.rs │ └── webhook_respond.rs │ ├── lib.rs │ └── state │ ├── mod.rs │ └── webhook.rs ├── relayer ├── Cargo.toml ├── README.md ├── api │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── lib.rs └── src │ └── main.rs ├── rust-toolchain.toml ├── scripts ├── build-all.sh ├── cargo-publish.sh ├── ci │ ├── bump-version.sh │ ├── create-tarball.sh │ ├── rust-version.sh │ └── solana-version.sh ├── debug_plugin.sh ├── mint-token.sh ├── refresh-program-ids.sh ├── stake-localnet.sh └── update-solana.sh ├── sdk ├── .gitignore ├── Cargo.toml ├── README.md └── src │ └── lib.rs └── utils ├── Cargo.toml ├── README.md └── src ├── explorer.rs ├── lib.rs ├── pubkey.rs └── thread.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Anchor 2 | .anchor 3 | .DS_Store 4 | target 5 | **/*.rs.bk 6 | 7 | # Clockwork 8 | bin/ 9 | lib/ 10 | .env 11 | 12 | # Docker 13 | docker-target 14 | 15 | # Solana 16 | .DS_Store 17 | **/node_modules 18 | **/dist 19 | **/build 20 | **/target 21 | **/test-ledger 22 | **/tsconfig.tsbuildinfo 23 | *.diff 24 | **/keypair.json 25 | **/*.log 26 | **/.coderrect 27 | 28 | /.idea 29 | 30 | # Disable old GitHub workflow 31 | .github* 32 | -------------------------------------------------------------------------------- /Anchor.toml: -------------------------------------------------------------------------------- 1 | [provider] 2 | cluster = "mainnet" 3 | wallet = "~/.config/solana/id.json" 4 | 5 | [programs.localnet] 6 | clockwork_network_program = "F8dKseqmBoAkHx3c58Lmb9TgJv5qeTf3BbtZZSEzYvUa" 7 | clockwork_thread_program = "CLoCKyJ6DXBJqqu2VWx9RLbgnwwR6BMHHuyasVmfMzBh" 8 | clockwork_webhook_program = "E7p5KFo8kKCDm6BUnWtnVFkQSYh6ZA6xaGAuvpv8NXTa" 9 | 10 | [programs.testnet] 11 | clockwork_network_program = "F8dKseqmBoAkHx3c58Lmb9TgJv5qeTf3BbtZZSEzYvUa" 12 | clockwork_thread_program = "CLoCKyJ6DXBJqqu2VWx9RLbgnwwR6BMHHuyasVmfMzBh" 13 | clockwork_webhook_program = "E7p5KFo8kKCDm6BUnWtnVFkQSYh6ZA6xaGAuvpv8NXTa" 14 | 15 | [programs.mainnet] 16 | clockwork_network_program = "F8dKseqmBoAkHx3c58Lmb9TgJv5qeTf3BbtZZSEzYvUa" 17 | clockwork_thread_program = "CLoCKyJ6DXBJqqu2VWx9RLbgnwwR6BMHHuyasVmfMzBh" 18 | clockwork_webhook_program = "E7p5KFo8kKCDm6BUnWtnVFkQSYh6ZA6xaGAuvpv8NXTa" 19 | 20 | [features] 21 | resolution = false 22 | seeds = false 23 | skip-lint = false 24 | 25 | [registry] 26 | url = "https://anchor.projectserum.com" 27 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "cli", 5 | "cron", 6 | "plugin", 7 | "programs/*", 8 | "relayer", 9 | "sdk", 10 | "utils" 11 | ] 12 | 13 | [profile.release] 14 | overflow-checks = true 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # This Dockerfile provides a Linux-based environment, pre-installed with Solana dev tooling 2 | # such as Rust, the Solana CLI, and the latest Soteria code scanner. 3 | # 4 | # You can pull the latest published image from Dockerhub (https://hub.docker.com/r/clockworkxyz/dev) 5 | # Or you can build an image from source using the Docker CLI: 6 | # ```sh 7 | # docker build -t clockworkxyz/solana . 8 | # ``` 9 | # 10 | # Note: When building Docker images on an M1 Mac, you should use the `--platform linux/amd64` flag. 11 | # 12 | 13 | FROM projectserum/build:v0.27.0 14 | 15 | # Set dependency versions. 16 | # ENV SOLANA_VERSION=v1.14.16 17 | 18 | # Configure path. 19 | # ENV HOME="/root" 20 | # ENV PATH="${HOME}/.cargo/bin:${PATH}" 21 | # ENV PATH="${HOME}/.local/share/solana/install/active_release/bin:${PATH}" 22 | # ENV PATH="${HOME}/soteria-linux-develop/bin/:${PATH}" 23 | 24 | # Install base utilities. 25 | # RUN mkdir -p /workdir && \ 26 | # mkdir -p /tmp && \ 27 | # apt-get update && \ 28 | # apt-get install -y build-essential git curl wget jq pkg-config libssl-dev libudev-dev 29 | 30 | # Move into root. 31 | # WORKDIR ${HOME} 32 | 33 | # Install Rust. 34 | # RUN curl "https://sh.rustup.rs" -sfo rustup.sh && \ 35 | # sh rustup.sh -y && \ 36 | # rustup component add rustfmt clippy 37 | 38 | # Install Solana. 39 | # RUN sh -c "$(curl -sSfL https://release.solana.com/${SOLANA_VERSION}/install)" 40 | 41 | # Install anchor. 42 | # RUN cargo install --git https://github.com/coral-xyz/anchor --tag ${ANCHOR_CLI} anchor-cli --locked 43 | 44 | # Build a dummy program to bootstrap the BPF SDK (doing this speeds up builds). 45 | # RUN mkdir -p /tmp && cd tmp && anchor init dummy && cd dummy && anchor build 46 | 47 | # Install Soteria. 48 | RUN sh -c "$(curl -k https://supercompiler.xyz/install)" 49 | 50 | # Set workdir. 51 | WORKDIR /workdir 52 | -------------------------------------------------------------------------------- /OLD_VERSION: -------------------------------------------------------------------------------- 1 | 2.0.17 2 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 2.0.19 2 | -------------------------------------------------------------------------------- /audits/ottersec-2023-03-14.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-clockwork/clockwork/84f200aa089a0b1ac95223cb78d96482a9127e23/audits/ottersec-2023-03-14.pdf -------------------------------------------------------------------------------- /cli/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "clockwork-cli" 3 | version = "2.0.20" 4 | description = "Command line interface for Clockwork" 5 | edition = "2021" 6 | license = "AGPL-3.0-or-later" 7 | homepage = "https://clockwork.xyz" 8 | repository = "https://github.com/clockwork-xyz/cli" 9 | documentation = "https://docs.clockwork.xyz" 10 | readme = "./README.md" 11 | keywords = ["solana"] 12 | build = "build.rs" 13 | 14 | [features] 15 | idl-build = [ 16 | "anchor-lang/idl-build", 17 | "anchor-spl/idl-build", 18 | "clockwork-network-program/idl-build", 19 | "clockwork-thread-program/idl-build", 20 | "clockwork-webhook-program/idl-build", 21 | "clockwork-utils/idl-build", 22 | ] 23 | 24 | [dependencies] 25 | anchor-lang = "0.30.0" 26 | anchor-spl = { features = ["mint", "token"], version = "0.30.0" } 27 | anyhow = "1.0.61" 28 | bincode = "1.3.3" 29 | bzip2 = "0.4" 30 | clap = { version = "3.1.2", features = ["derive"] } 31 | clockwork-cron = { path = "../cron", version = "=2.0.20" } 32 | clockwork-network-program = { path = "../programs/network", version = "=2.0.20", features = ["no-entrypoint"] } 33 | clockwork-relayer-api = { path = "../relayer/api", version = "=2.0.20" } 34 | clockwork-plugin-utils= { path = "../plugin/utils", version = "=2.0.20" } 35 | clockwork-thread-program = { path = "../programs/thread", version = "=2.0.20", features = ["no-entrypoint"] } 36 | clockwork-utils = { path = "../utils", version = "=2.0.20" } 37 | clockwork-webhook-program = { path = "../programs/webhook", version = "=2.0.20", features = ["no-entrypoint"] } 38 | chrono = { version = "0.4.19", default-features = false, features = ["alloc"] } 39 | dirs-next = "2.0.0" 40 | indicatif = "0.16" 41 | reqwest = "0.11.14" 42 | serde = { version = "1.0.136", features = ["derive"] } 43 | serde_json = "1.0.79" 44 | serde_yaml = "0.9.4" 45 | solana-account-decoder = "^1.18.14" 46 | solana-client = "^1.18.14" 47 | solana-clap-utils = "^1.18.14" 48 | solana-cli-config = "^1.18.14" 49 | solana-sdk = "^1.18.14" 50 | spl-associated-token-account = "1.1.1" 51 | spl-memo = "4.0.0" 52 | spl-token = "~4.0.0" 53 | tar = "0.4" 54 | thiserror = "1.0.30" 55 | termcolor = "1.2.0" 56 | 57 | [[bin]] 58 | name = "clockwork" 59 | path = "src/main.rs" 60 | 61 | [build-dependencies] 62 | cargo_metadata = "=0.14.0" 63 | -------------------------------------------------------------------------------- /cli/README.md: -------------------------------------------------------------------------------- 1 | # Clockwork CLI 2 | 3 | If you are on linux, you might need to run this: 4 | ```sh 5 | sudo apt-get update && sudo apt-get upgrade && sudo apt-get install -y pkg-config build-essential libudev-dev libssl-dev 6 | ``` 7 | 8 | Install with cargo 9 | ```sh 10 | cargo install -f --locked clockwork-cli 11 | ``` 12 | -------------------------------------------------------------------------------- /cli/build.rs: -------------------------------------------------------------------------------- 1 | use std::process::Command; 2 | 3 | fn main() { 4 | let output = Command::new("git") 5 | .args(&["rev-parse", "HEAD"]) 6 | .output() 7 | .expect("unable to get git commit hash"); 8 | let commit_hash = String::from_utf8(output.stdout).unwrap(); 9 | let url = format!( 10 | "https://github.com/clockwork-xyz/clockwork/tree/{}/plugin/Cargo.toml", 11 | commit_hash 12 | ); 13 | println!("cargo:rustc-env=SPEC={}", url); 14 | 15 | let metadata = cargo_metadata::MetadataCommand::new().exec().unwrap(); 16 | let geyser_interface_version = metadata 17 | .packages 18 | .iter() 19 | .find(|p| p.name == "solana-geyser-plugin-interface") 20 | .expect("Unable to parse solana-geyser-plugin-interface version using cargo metadata") 21 | .version 22 | .to_string(); 23 | println!( 24 | "cargo:rustc-env=GEYSER_INTERFACE_VERSION={}", 25 | geyser_interface_version 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /cli/src/errors.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Debug, Error)] 4 | pub enum CliError { 5 | #[error("Account not found: {0}")] 6 | AccountNotFound(String), 7 | #[error("Account data could not be parsed: {0}")] 8 | AccountDataNotParsable(String), 9 | #[error("Bad client: {0}")] 10 | BadClient(String), 11 | #[error("Bad parameter: {0}")] 12 | BadParameter(String), 13 | #[error("This codepath hasn't been implemented yet")] 14 | NotImplemented, 15 | #[error("Command not recognized: {0}")] 16 | CommandNotRecognized(String), 17 | #[error("Transaction failed with error: {0}")] 18 | FailedTransaction(String), 19 | #[error("Failed to start localnet with error: {0}")] 20 | FailedLocalnet(String), 21 | #[error("Invalid address")] 22 | InvalidAddress, 23 | #[error("Program file does not exist")] 24 | InvalidProgramFile, 25 | #[error("No default signer found in {0}, \ 26 | run `solana-keygen new`, or `solana config set —keypair `")] 27 | KeypairNotFound(String), 28 | } 29 | -------------------------------------------------------------------------------- /cli/src/main.rs: -------------------------------------------------------------------------------- 1 | mod cli; 2 | mod client; 3 | mod config; 4 | mod deps; 5 | mod errors; 6 | mod parser; 7 | mod print; 8 | mod processor; 9 | 10 | use { 11 | crate::{ 12 | config::CliConfig, 13 | print::print_style, 14 | }, 15 | cli::app, 16 | errors::CliError, 17 | processor::process, 18 | }; 19 | 20 | fn main() -> Result<(), CliError> { 21 | process(&app().get_matches()).map_err(|e| { 22 | print_error!("{}", e); 23 | e 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /cli/src/print.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use { 4 | std::{ 5 | fmt::Display, 6 | io::Write, 7 | }, 8 | termcolor::{ 9 | Color, 10 | ColorChoice, 11 | ColorSpec, 12 | StandardStream, 13 | WriteColor, 14 | }, 15 | }; 16 | 17 | #[macro_export] 18 | macro_rules! print_status { 19 | ($action:expr, $($args: tt)*) => { 20 | print_style($action, &format!($($args)*), termcolor::Color::Green, true) 21 | }; 22 | } 23 | 24 | #[macro_export] 25 | macro_rules! print_note { 26 | ($($args: tt)*) => { 27 | print_style("note", &format!($($args)*), termcolor::Color::Cyan, true) 28 | }; 29 | } 30 | 31 | #[macro_export] 32 | macro_rules! print_warn { 33 | ($($args: tt)*) => { 34 | print_style("warning", &format!($($args)*), termcolor::Color::Yellow, false) 35 | }; 36 | } 37 | 38 | #[macro_export] 39 | macro_rules! print_error { 40 | ($($args: tt)*) => { 41 | print_style("ERROR", &format!($($args)*), termcolor::Color::Red, false) 42 | }; 43 | } 44 | 45 | /// Print a message with a colored title in the style of Cargo shell messages. 46 | pub fn print_style + Display>(status: S, message: S, color: Color, justified: bool) { 47 | let mut output = StandardStream::stderr(ColorChoice::Auto); 48 | output 49 | .set_color(ColorSpec::new().set_fg(Some(color)).set_bold(true)) 50 | .unwrap(); 51 | if justified { 52 | write!(output, "{status:>12}").unwrap(); 53 | } else { 54 | write!(output, "{status}").unwrap(); 55 | output.set_color(ColorSpec::new().set_bold(true)).unwrap(); 56 | write!(output, ":").unwrap(); 57 | } 58 | output.reset().unwrap(); 59 | writeln!(output, " {message}").unwrap(); 60 | } 61 | -------------------------------------------------------------------------------- /cli/src/processor/config.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::{ 2 | solana_program::{ 3 | instruction::Instruction, pubkey::Pubkey, 4 | }, 5 | InstructionData, ToAccountMetas 6 | }; 7 | use clockwork_network_program::state::{Config, ConfigSettings}; 8 | 9 | use crate::{client::Client, errors::CliError}; 10 | 11 | pub fn get(client: &Client) -> Result<(), CliError> { 12 | let config = client 13 | .get::(&Config::pubkey()) 14 | .map_err(|_err| CliError::AccountNotFound(Config::pubkey().to_string()))?; 15 | println!("{:#?}", config); 16 | Ok(()) 17 | } 18 | 19 | pub fn set( 20 | client: &Client, 21 | admin: Option, 22 | epoch_thread: Option, 23 | hasher_thread: Option, 24 | ) -> Result<(), CliError> { 25 | // Get the current config. 26 | let config = client 27 | .get::(&Config::pubkey()) 28 | .map_err(|_err| CliError::AccountNotFound(Config::pubkey().to_string()))?; 29 | 30 | // Build new config. settings 31 | let settings = ConfigSettings { 32 | admin: admin.unwrap_or(config.admin), 33 | epoch_thread: epoch_thread.unwrap_or(config.epoch_thread), 34 | hasher_thread: hasher_thread.unwrap_or(config.hasher_thread), 35 | mint: config.mint, 36 | }; 37 | 38 | // Submit tx 39 | let ix = Instruction { 40 | program_id: clockwork_network_program::ID, 41 | accounts: clockwork_network_program::accounts::ConfigUpdate { 42 | admin: client.payer_pubkey(), 43 | config: Config::pubkey(), 44 | }.to_account_metas(Some(false)), 45 | data: clockwork_network_program::instruction::ConfigUpdate { settings }.data(), 46 | }; 47 | client.send_and_confirm(&[ix], &[client.payer()]).unwrap(); 48 | get(client)?; 49 | Ok(()) 50 | } 51 | -------------------------------------------------------------------------------- /cli/src/processor/crontab.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, NaiveDateTime, Utc}; 2 | use clockwork_cron::Schedule; 3 | use std::str::FromStr; 4 | 5 | use crate::{client::Client, errors::CliError}; 6 | 7 | pub fn get(client: &Client, schedule: String) -> Result<(), CliError> { 8 | let clock = client.get_clock().unwrap(); 9 | let schedule = Schedule::from_str(schedule.as_str()).unwrap(); 10 | 11 | let mut i = 0; 12 | for t in schedule.after(&DateTime::::from_utc( 13 | NaiveDateTime::from_timestamp_opt(clock.unix_timestamp, 0).unwrap(), 14 | Utc, 15 | )) { 16 | println!("{:#?}", t); 17 | i += 1; 18 | if i > 8 { 19 | break; 20 | } 21 | } 22 | Ok(()) 23 | } 24 | -------------------------------------------------------------------------------- /cli/src/processor/explorer.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::errors::CliError, 3 | crate::config::CliConfig, 4 | clockwork_utils::explorer::Explorer, 5 | }; 6 | 7 | pub fn thread_url(thread: T, config: CliConfig) -> Result<(), 8 | CliError> { 9 | println!("thread: {}", explorer(config).thread_url(thread, clockwork_thread_program::ID)); 10 | Ok(()) 11 | } 12 | 13 | fn explorer(config: CliConfig) -> Explorer { 14 | Explorer::from(config.json_rpc_url) 15 | } 16 | -------------------------------------------------------------------------------- /cli/src/processor/initialize.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::{ 2 | solana_program::{ 3 | instruction::Instruction, 4 | pubkey::Pubkey, 5 | system_program, 6 | }, 7 | InstructionData, ToAccountMetas, 8 | }; 9 | use clockwork_network_program::state::{Config, Pool, Registry, Snapshot}; 10 | 11 | use crate::{client::Client, errors::CliError}; 12 | 13 | pub fn initialize(client: &Client, mint: Pubkey) -> Result<(), CliError> { 14 | // Initialize the programs 15 | let admin = client.payer_pubkey(); 16 | let ix_a = Instruction { 17 | program_id: clockwork_network_program::ID, 18 | accounts: clockwork_network_program::accounts::Initialize { 19 | admin, 20 | config: Config::pubkey(), 21 | mint, 22 | registry: Registry::pubkey(), 23 | snapshot: Snapshot::pubkey(0), 24 | system_program: system_program::ID, 25 | }.to_account_metas(Some(false)), 26 | data: clockwork_network_program::instruction::Initialize {}.data(), 27 | }; 28 | let ix_b = Instruction { 29 | program_id: clockwork_network_program::ID, 30 | accounts: clockwork_network_program::accounts::PoolCreate { 31 | admin, 32 | config: Config::pubkey(), 33 | payer: admin, 34 | pool: Pool::pubkey(0), 35 | registry: Registry::pubkey(), 36 | system_program: system_program::ID, 37 | }.to_account_metas(Some(false)), 38 | data: clockwork_network_program::instruction::PoolCreate {}.data(), 39 | }; 40 | 41 | // Submit tx 42 | client 43 | .send_and_confirm(&[ix_a, ix_b], &[client.payer()]) 44 | .unwrap(); 45 | 46 | Ok(()) 47 | } 48 | -------------------------------------------------------------------------------- /cli/src/processor/pool.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::{ 2 | solana_program::{ 3 | instruction::Instruction, system_program, 4 | }, 5 | InstructionData, ToAccountMetas 6 | }; 7 | use clockwork_network_program::state::{Config, Pool, Registry, PoolSettings}; 8 | 9 | use crate::{client::Client, errors::CliError}; 10 | 11 | pub fn get(client: &Client, id: u64) -> Result<(), CliError> { 12 | let pool_pubkey = Pool::pubkey(id); 13 | let pool = client 14 | .get::(&pool_pubkey) 15 | .map_err(|_err| CliError::AccountDataNotParsable(pool_pubkey.to_string()))?; 16 | println!("{:#?}", pool); 17 | Ok(()) 18 | } 19 | 20 | pub fn list(client: &Client) -> Result<(), CliError> { 21 | let registry_pubkey = Registry::pubkey(); 22 | let registry = client 23 | .get::(®istry_pubkey) 24 | .map_err(|_err| CliError::AccountDataNotParsable(registry_pubkey.to_string()))?; 25 | 26 | for pool_id in 0..registry.total_pools { 27 | let pool_pubkey = Pool::pubkey(pool_id); 28 | let pool = client 29 | .get::(&pool_pubkey) 30 | .map_err(|_err| CliError::AccountDataNotParsable(pool_pubkey.to_string()))?; 31 | println!("{:#?}", pool); 32 | } 33 | 34 | Ok(()) 35 | } 36 | 37 | pub fn update(client: &Client, id: u64, size: usize) -> Result<(), CliError> { 38 | let pool_pubkey = Pool::pubkey(id); 39 | let settings = PoolSettings { size: size.try_into().unwrap() }; 40 | let ix = Instruction { 41 | program_id: clockwork_network_program::ID, 42 | accounts: clockwork_network_program::accounts::PoolUpdate { 43 | admin: client.payer_pubkey(), 44 | config: Config::pubkey(), 45 | payer: client.payer_pubkey(), 46 | pool: pool_pubkey, 47 | system_program: system_program::ID, 48 | }.to_account_metas(Some(false)), 49 | data: clockwork_network_program::instruction::PoolUpdate { settings }.data(), 50 | }; 51 | client.send_and_confirm(&[ix], &[client.payer()]).unwrap(); 52 | get(client, id)?; 53 | Ok(()) 54 | } 55 | -------------------------------------------------------------------------------- /cli/src/processor/registry.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::{ 2 | solana_program::instruction::Instruction, 3 | InstructionData, ToAccountMetas 4 | }; 5 | use clockwork_network_program::state::{Config, Registry, Snapshot}; 6 | 7 | use crate::{client::Client, errors::CliError}; 8 | 9 | pub fn get(client: &Client) -> Result<(), CliError> { 10 | let registry_pubkey = clockwork_network_program::state::Registry::pubkey(); 11 | let registry = client 12 | .get::(®istry_pubkey) 13 | .map_err(|_err| CliError::AccountDataNotParsable(registry_pubkey.to_string()))?; 14 | 15 | let snapshot_pubkey = Snapshot::pubkey(registry.current_epoch); 16 | let snapshot = client 17 | .get::(&snapshot_pubkey) 18 | .map_err(|_err| CliError::AccountDataNotParsable(snapshot_pubkey.to_string()))?; 19 | 20 | println!("{}\n{:#?}", registry_pubkey, registry); 21 | println!("{}\n{:#?}", snapshot_pubkey, snapshot); 22 | Ok(()) 23 | } 24 | 25 | pub fn unlock(client: &Client) -> Result<(), CliError> { 26 | let ix = Instruction { 27 | program_id: clockwork_network_program::ID, 28 | accounts: clockwork_network_program::accounts::RegistryUnlock { 29 | admin: client.payer_pubkey(), 30 | config: Config::pubkey(), 31 | registry: Registry::pubkey() 32 | }.to_account_metas(Some(false)), 33 | data: clockwork_network_program::instruction::RegistryUnlock {}.data(), 34 | }; 35 | client.send_and_confirm(&[ix], &[client.payer()]).unwrap(); 36 | get(client)?; 37 | Ok(()) 38 | } 39 | -------------------------------------------------------------------------------- /cli/src/processor/secret.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::Pubkey; 2 | use clockwork_relayer_api::{ 3 | SecretApprove, SecretCreate, SecretGet, SecretList, SecretRevoke, SignedRequest, 4 | }; 5 | use reqwest::header::CONTENT_TYPE; 6 | use solana_sdk::signer::Signer; 7 | 8 | use crate::{client::Client, errors::CliError}; 9 | 10 | pub fn get(client: &Client, name: String) -> Result<(), CliError> { 11 | let keypair = &client.payer; 12 | let msg = SecretGet { name }; 13 | let msg_bytes = bincode::serialize(&msg).unwrap(); 14 | let sig = keypair.sign_message(&msg_bytes); 15 | let req = SignedRequest { 16 | msg, 17 | signer: keypair.pubkey(), 18 | signature: sig, 19 | }; 20 | let client = reqwest::blocking::Client::new(); 21 | let res = client 22 | .post("http://0.0.0.0:8000/secret_get") 23 | .header(CONTENT_TYPE, "application/json") 24 | .json(&req) 25 | .send(); 26 | if let Ok(plaintext) = res { 27 | println!("{:?}", plaintext.text()); 28 | } 29 | Ok(()) 30 | } 31 | 32 | pub fn list(client: &Client) -> Result<(), CliError> { 33 | let keypair = &client.payer; 34 | let msg = SecretList {}; 35 | let msg_bytes = bincode::serialize(&msg).unwrap(); 36 | let sig = keypair.sign_message(&msg_bytes); 37 | let req = SignedRequest { 38 | msg, 39 | signer: keypair.pubkey(), 40 | signature: sig, 41 | }; 42 | let client = reqwest::blocking::Client::new(); 43 | let res = client 44 | .post("http://0.0.0.0:8000/secret_list") 45 | .header(CONTENT_TYPE, "application/json") 46 | .json(&req) 47 | .send(); 48 | if let Ok(plaintext) = res { 49 | println!("{:?}", plaintext.text()); 50 | } 51 | Ok(()) 52 | } 53 | 54 | pub fn create(client: &Client, name: String, word: String) -> Result<(), CliError> { 55 | let keypair = &client.payer; 56 | let msg = SecretCreate { name, word }; 57 | let msg_bytes = bincode::serialize(&msg).unwrap(); 58 | let sig = keypair.sign_message(&msg_bytes); 59 | let req = SignedRequest { 60 | msg, 61 | signer: keypair.pubkey(), 62 | signature: sig, 63 | }; 64 | let client = reqwest::blocking::Client::new(); 65 | client 66 | .post("http://0.0.0.0:8000/secret_create") 67 | .header(CONTENT_TYPE, "application/json") 68 | .json(&req) 69 | .send() 70 | .unwrap(); 71 | Ok(()) 72 | } 73 | 74 | pub fn approve(client: &Client, name: String, delegate: Pubkey) -> Result<(), CliError> { 75 | let keypair = &client.payer; 76 | let msg = SecretApprove { name, delegate }; 77 | let msg_bytes = bincode::serialize(&msg).unwrap(); 78 | let sig = keypair.sign_message(&msg_bytes); 79 | let req = SignedRequest { 80 | msg, 81 | signer: keypair.pubkey(), 82 | signature: sig, 83 | }; 84 | let client = reqwest::blocking::Client::new(); 85 | client 86 | .post("http://0.0.0.0:8000/secret_approve") 87 | .header(CONTENT_TYPE, "application/json") 88 | .json(&req) 89 | .send() 90 | .unwrap(); 91 | Ok(()) 92 | } 93 | 94 | pub fn revoke(client: &Client, name: String, delegate: Pubkey) -> Result<(), CliError> { 95 | let keypair = &client.payer; 96 | let msg = SecretRevoke { name, delegate }; 97 | let msg_bytes = bincode::serialize(&msg).unwrap(); 98 | let sig = keypair.sign_message(&msg_bytes); 99 | let req = SignedRequest { 100 | msg, 101 | signer: keypair.pubkey(), 102 | signature: sig, 103 | }; 104 | let client = reqwest::blocking::Client::new(); 105 | client 106 | .post("http://0.0.0.0:8000/secret_revoke") 107 | .header(CONTENT_TYPE, "application/json") 108 | .json(&req) 109 | .send() 110 | .unwrap(); 111 | Ok(()) 112 | } 113 | -------------------------------------------------------------------------------- /cli/src/processor/snapshot.rs: -------------------------------------------------------------------------------- 1 | use clockwork_network_program::state::{Registry, Snapshot, SnapshotEntry}; 2 | use solana_sdk::pubkey::Pubkey; 3 | 4 | use crate::{client::Client, errors::CliError}; 5 | 6 | pub fn get(client: &Client, entry_id: Option) -> Result<(), CliError> { 7 | let registry_pubkey = clockwork_client::network::objects::Registry::pubkey(); 8 | let registry = client 9 | .get::(®istry_pubkey) 10 | .map_err(|_err| CliError::AccountDataNotParsable(registry_pubkey.to_string()))?; 11 | 12 | let snapshot_pubkey = 13 | clockwork_client::network::objects::Snapshot::pubkey(registry.snapshot_count - 1); 14 | let snapshot = client 15 | .get::(&snapshot_pubkey) 16 | .map_err(|_err| CliError::AccountDataNotParsable(snapshot_pubkey.to_string()))?; 17 | 18 | println!("{:#?}", snapshot); 19 | 20 | match entry_id { 21 | None => (), 22 | Some(entry_id) => { 23 | get_snapshot_entry(client, snapshot_pubkey, entry_id).ok(); 24 | } 25 | } 26 | 27 | Ok(()) 28 | } 29 | 30 | pub fn get_snapshot_entry( 31 | client: &Client, 32 | snapshot_pubkey: Pubkey, 33 | entry_id: u64, 34 | ) -> Result<(), CliError> { 35 | let entry_pubkey = 36 | clockwork_client::network::objects::SnapshotEntry::pubkey(snapshot_pubkey, entry_id); 37 | 38 | let entry = client 39 | .get::(&entry_pubkey) 40 | .map_err(|_err| CliError::AccountDataNotParsable(snapshot_pubkey.to_string()))?; 41 | 42 | println!("{:#?}", entry); 43 | 44 | Ok(()) 45 | } 46 | -------------------------------------------------------------------------------- /cli/src/processor/webhook.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use anchor_lang::{InstructionData, ToAccountMetas}; 4 | use clockwork_webhook_program::state::{HttpMethod, Webhook}; 5 | use solana_sdk::{instruction::Instruction, system_program}; 6 | 7 | use crate::{client::Client, errors::CliError}; 8 | 9 | pub fn create( 10 | client: &Client, 11 | body: Vec, 12 | id: Vec, 13 | method: HttpMethod, 14 | url: String, 15 | ) -> Result<(), CliError> { 16 | let mut headers: HashMap = HashMap::new(); 17 | headers.insert( 18 | "X-CUSTOM-HEADER".into(), 19 | "TEST {HBUh9g46wk2X89CvaNN15UmsznP59rh6od1h8JwYAopk:hello}".into(), 20 | ); 21 | let ix = Instruction { 22 | program_id: clockwork_webhook_program::ID, 23 | accounts: clockwork_webhook_program::accounts::WebhookCreate { 24 | authority: client.payer_pubkey(), 25 | payer: client.payer_pubkey(), 26 | webhook: Webhook::pubkey(client.payer_pubkey(), id.clone()), 27 | system_program: system_program::ID, 28 | } 29 | .to_account_metas(Some(true)), 30 | data: clockwork_webhook_program::instruction::WebhookCreate { 31 | body, 32 | // headers, 33 | id: id.clone(), 34 | method, 35 | url, 36 | } 37 | .data(), 38 | }; 39 | client.send_and_confirm(&[ix], &[client.payer()]).unwrap(); 40 | get(client, id)?; 41 | 42 | Ok(()) 43 | } 44 | 45 | pub fn get(client: &Client, id: Vec) -> Result<(), CliError> { 46 | let address = Webhook::pubkey(client.payer_pubkey(), id.clone()); 47 | let webhook = client 48 | .get::(&address) 49 | .map_err(|_err| CliError::AccountDataNotParsable(address.to_string()))?; 50 | println!("Address: {}\n{:#?}", address, webhook); 51 | todo!() 52 | } 53 | -------------------------------------------------------------------------------- /cron/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | *.swp 4 | *.iml 5 | .idea 6 | **/*.rs.bk 7 | -------------------------------------------------------------------------------- /cron/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "clockwork-cron" 3 | version = "2.0.20" 4 | description = "A cron expression parser that's safe to use in the Solana runtime" 5 | edition = "2021" 6 | license = "AGPL-3.0-or-later" 7 | homepage = "https://clockwork.xyz" 8 | repository = "https://github.com/clockwork-xyz/clockwork" 9 | documentation = "https://docs.clockwork.xyz" 10 | readme = "./README.md" 11 | keywords = ["solana"] 12 | 13 | [lib] 14 | name = "clockwork_cron" 15 | 16 | [dependencies] 17 | chrono = { version = "0.4.19", default-features = false, features = ["alloc"] } 18 | nom = "~7" 19 | once_cell = "1.5.2" 20 | 21 | -------------------------------------------------------------------------------- /cron/README.md: -------------------------------------------------------------------------------- 1 | # clockwork-cron [![](https://img.shields.io/crates/v/clockwork-cron.svg)](https://crates.io/crates/clockwork-cron) [![](https://docs.rs/cron/badge.svg)](https://docs.rs/clockwork-cron) 2 | 3 | A cron expression parser that's safe to use in the Solana runtime. Works with stable Rust v1.28.0. 4 | 5 | ```rust 6 | use clockwork_cron::Schedule; 7 | use chrono::{DateTime, NaiveDateTime, Utc}; 8 | use std::str::FromStr; 9 | 10 | fn main() { 11 | // sec min hour day of month month day of week year 12 | let expression = "0 30 9,12,15 1,15 May-Aug Mon,Wed,Fri 2018/2"; 13 | let schedule = Schedule::from_str(expression).unwrap(); 14 | let ts = 1234567890; 15 | let next_ts = schedule 16 | .after(&DateTime::::from_utc( 17 | NaiveDateTime::from_timestamp(ts, 0), 18 | Utc, 19 | )) 20 | .take(1) 21 | .next() 22 | { 23 | Some(datetime) => Some(datetime.timestamp()), 24 | None => None, 25 | } 26 | } 27 | 28 | /* 29 | Upcoming fire times: 30 | -> 2018-06-01 09:30:00 UTC 31 | -> 2018-06-01 12:30:00 UTC 32 | -> 2018-06-01 15:30:00 UTC 33 | -> 2018-06-15 09:30:00 UTC 34 | -> 2018-06-15 12:30:00 UTC 35 | -> 2018-06-15 15:30:00 UTC 36 | -> 2018-08-01 09:30:00 UTC 37 | -> 2018-08-01 12:30:00 UTC 38 | -> 2018-08-01 15:30:00 UTC 39 | -> 2018-08-15 09:30:00 UTC 40 | */ 41 | ``` 42 | 43 | ## ⚠️ Syntax 44 | ```bash 45 | sec min hour day of month month day of week year 46 | ``` 47 | If you use tools such as crontab guru, note that the clockwork parser is a __7__ columns string. 48 | You probably need to add the __seconds__ _(left most column)_ and can optionally add the year _(right most column)_. 49 | e.g. the following 5 columns cron: 50 | 51 | | min | hour | day of month | month | day of week | 52 | |-----|------|--------------|-------|-------------| 53 | | 0 | 18 | * | * | FRI | 54 | 55 | becomes 56 | | sec | min | hour | day of month | month | day of week | year | 57 | |-----|-----|------|--------------|-------|-------------|------| 58 | | 0 | 0 | 18 | * | * | FRI | * | 59 | 60 | or 61 | 62 | | sec | min | hour | day of month | month | day of week | 63 | |-----|-----|------|--------------|-------|-------------| 64 | | 0 | 0 | 18 | * | * | FRI | 65 | 66 | These are also supported: 67 | ```bash 68 | "@yearly" 69 | "@weekly" 70 | "@daily" 71 | "@hourly" 72 | ``` 73 | -------------------------------------------------------------------------------- /cron/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::{error, fmt}; 2 | 3 | #[derive(Debug)] 4 | pub struct Error { 5 | kind: ErrorKind, 6 | } 7 | 8 | #[derive(Debug)] 9 | pub enum ErrorKind { 10 | Expression(String), 11 | } 12 | 13 | impl fmt::Display for Error { 14 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 15 | match self.kind { 16 | ErrorKind::Expression(ref expr) => write!(f, "Invalid expression: {}", expr), 17 | } 18 | } 19 | } 20 | 21 | impl error::Error for Error {} 22 | 23 | impl From for Error { 24 | fn from(kind: ErrorKind) -> Error { 25 | Error { kind } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /cron/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(rust_2018_idioms)] 2 | 3 | //! A cron expression parser and schedule explorer 4 | //! # Example 5 | //! ``` 6 | //! use clockwork_cron::Schedule; 7 | //! use chrono::{DateTime, NaiveDateTime, Utc}; 8 | //! use std::str::FromStr; 9 | //! 10 | //! fn main() { 11 | //! // sec min hour day of month month day of week year 12 | //! let expression = "0 30 9,12,15 1,15 May-Aug Mon,Wed,Fri 2018/2"; 13 | //! let schedule = Schedule::from_str(expression).unwrap(); 14 | //! let ts = 1234567890; 15 | //! let next_ts = match schedule 16 | //! .after(&DateTime::::from_utc( 17 | //! NaiveDateTime::from_timestamp(ts, 0), 18 | //! Utc, 19 | //! )) 20 | //! .take(1) 21 | //! .next() 22 | //! { 23 | //! Some(datetime) => Some(datetime.timestamp()), 24 | //! None => None, 25 | //! }; 26 | //! } 27 | //! 28 | //! /* 29 | //! Upcoming fire times: 30 | //! -> 2018-06-01 09:30:00 UTC 31 | //! -> 2018-06-01 12:30:00 UTC 32 | //! -> 2018-06-01 15:30:00 UTC 33 | //! -> 2018-06-15 09:30:00 UTC 34 | //! -> 2018-06-15 12:30:00 UTC 35 | //! -> 2018-06-15 15:30:00 UTC 36 | //! -> 2018-08-01 09:30:00 UTC 37 | //! -> 2018-08-01 12:30:00 UTC 38 | //! -> 2018-08-01 15:30:00 UTC 39 | //! -> 2018-08-15 09:30:00 UTC 40 | //! */ 41 | //! ``` 42 | 43 | pub mod error; 44 | mod ordinal; 45 | mod parsing; 46 | mod queries; 47 | mod schedule; 48 | mod specifier; 49 | mod time_unit; 50 | 51 | pub use crate::schedule::Schedule; 52 | pub use crate::time_unit::TimeUnitSpec; 53 | -------------------------------------------------------------------------------- /cron/src/ordinal.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeSet; 2 | 3 | pub type Ordinal = u32; 4 | // TODO: Make OrdinalSet an enum. 5 | // It should either be a BTreeSet of ordinals or an `All` option to save space. 6 | // `All` can iterate from inclusive_min to inclusive_max and answer membership 7 | // queries 8 | pub type OrdinalSet = BTreeSet; 9 | -------------------------------------------------------------------------------- /cron/src/specifier.rs: -------------------------------------------------------------------------------- 1 | use crate::ordinal::*; 2 | 3 | #[derive(Debug, PartialEq)] 4 | pub enum Specifier { 5 | All, 6 | Point(Ordinal), 7 | Range(Ordinal, Ordinal), 8 | NamedRange(String, String), 9 | } 10 | 11 | // Separating out a root specifier allows for a higher tiered specifier, allowing us to achieve 12 | // periods with base values that are more advanced than an ordinal: 13 | // - all: '*/2' 14 | // - range: '10-2/2' 15 | // - named range: 'Mon-Thurs/2' 16 | // 17 | // Without this separation we would end up with invalid combinations such as 'Mon/2' 18 | #[derive(Debug, PartialEq)] 19 | pub enum RootSpecifier { 20 | Specifier(Specifier), 21 | Period(Specifier, u32), 22 | NamedPoint(String), 23 | } 24 | 25 | impl From for RootSpecifier { 26 | fn from(specifier: Specifier) -> Self { 27 | Self::Specifier(specifier) 28 | } 29 | } -------------------------------------------------------------------------------- /cron/src/time_unit/days_of_month.rs: -------------------------------------------------------------------------------- 1 | use crate::ordinal::{Ordinal, OrdinalSet}; 2 | use crate::time_unit::TimeUnitField; 3 | use std::borrow::Cow; 4 | 5 | #[derive(Clone, Debug, Eq)] 6 | pub struct DaysOfMonth { 7 | ordinals: Option, 8 | } 9 | 10 | impl TimeUnitField for DaysOfMonth { 11 | fn from_optional_ordinal_set(ordinal_set: Option) -> Self { 12 | DaysOfMonth { 13 | ordinals: ordinal_set, 14 | } 15 | } 16 | fn name() -> Cow<'static, str> { 17 | Cow::from("Days of Month") 18 | } 19 | fn inclusive_min() -> Ordinal { 20 | 1 21 | } 22 | fn inclusive_max() -> Ordinal { 23 | 31 24 | } 25 | fn ordinals(&self) -> OrdinalSet { 26 | match self.ordinals.clone() { 27 | Some(ordinal_set) => ordinal_set, 28 | None => DaysOfMonth::supported_ordinals(), 29 | } 30 | } 31 | } 32 | 33 | impl PartialEq for DaysOfMonth { 34 | fn eq(&self, other: &DaysOfMonth) -> bool { 35 | self.ordinals() == other.ordinals() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /cron/src/time_unit/days_of_week.rs: -------------------------------------------------------------------------------- 1 | use crate::error::*; 2 | use crate::ordinal::{Ordinal, OrdinalSet}; 3 | use crate::time_unit::TimeUnitField; 4 | use std::borrow::Cow; 5 | 6 | #[derive(Clone, Debug, Eq)] 7 | pub struct DaysOfWeek { 8 | ordinals: Option, 9 | } 10 | 11 | impl TimeUnitField for DaysOfWeek { 12 | fn from_optional_ordinal_set(ordinal_set: Option) -> Self { 13 | DaysOfWeek { 14 | ordinals: ordinal_set, 15 | } 16 | } 17 | fn name() -> Cow<'static, str> { 18 | Cow::from("Days of Week") 19 | } 20 | fn inclusive_min() -> Ordinal { 21 | 1 22 | } 23 | fn inclusive_max() -> Ordinal { 24 | 7 25 | } 26 | fn ordinal_from_name(name: &str) -> Result { 27 | //TODO: Use phf crate 28 | let ordinal = match name.to_lowercase().as_ref() { 29 | "sun" | "sunday" => 1, 30 | "mon" | "monday" => 2, 31 | "tue" | "tues" | "tuesday" => 3, 32 | "wed" | "wednesday" => 4, 33 | "thu" | "thurs" | "thursday" => 5, 34 | "fri" | "friday" => 6, 35 | "sat" | "saturday" => 7, 36 | _ => { 37 | return Err(ErrorKind::Expression(format!( 38 | "'{}' is not a valid day of the week.", 39 | name 40 | )) 41 | .into()) 42 | } 43 | }; 44 | Ok(ordinal) 45 | } 46 | fn ordinals(&self) -> OrdinalSet { 47 | match self.ordinals.clone() { 48 | Some(ordinal_set) => ordinal_set, 49 | None => DaysOfWeek::supported_ordinals(), 50 | } 51 | } 52 | } 53 | 54 | impl PartialEq for DaysOfWeek { 55 | fn eq(&self, other: &DaysOfWeek) -> bool { 56 | self.ordinals() == other.ordinals() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /cron/src/time_unit/hours.rs: -------------------------------------------------------------------------------- 1 | use crate::ordinal::{Ordinal, OrdinalSet}; 2 | use crate::time_unit::TimeUnitField; 3 | use std::borrow::Cow; 4 | 5 | #[derive(Clone, Debug, Eq)] 6 | pub struct Hours { 7 | ordinals: Option, 8 | } 9 | 10 | impl TimeUnitField for Hours { 11 | fn from_optional_ordinal_set(ordinal_set: Option) -> Self { 12 | Hours { 13 | ordinals: ordinal_set, 14 | } 15 | } 16 | fn name() -> Cow<'static, str> { 17 | Cow::from("Hours") 18 | } 19 | fn inclusive_min() -> Ordinal { 20 | 0 21 | } 22 | fn inclusive_max() -> Ordinal { 23 | 23 24 | } 25 | fn ordinals(&self) -> OrdinalSet { 26 | match self.ordinals.clone() { 27 | Some(ordinal_set) => ordinal_set, 28 | None => Hours::supported_ordinals(), 29 | } 30 | } 31 | } 32 | 33 | impl PartialEq for Hours { 34 | fn eq(&self, other: &Hours) -> bool { 35 | self.ordinals() == other.ordinals() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /cron/src/time_unit/minutes.rs: -------------------------------------------------------------------------------- 1 | use crate::ordinal::{Ordinal, OrdinalSet}; 2 | use crate::time_unit::TimeUnitField; 3 | use std::borrow::Cow; 4 | 5 | #[derive(Clone, Debug, Eq)] 6 | pub struct Minutes { 7 | ordinals: Option, 8 | } 9 | 10 | impl TimeUnitField for Minutes { 11 | fn from_optional_ordinal_set(ordinal_set: Option) -> Self { 12 | Minutes { 13 | ordinals: ordinal_set, 14 | } 15 | } 16 | fn name() -> Cow<'static, str> { 17 | Cow::from("Minutes") 18 | } 19 | fn inclusive_min() -> Ordinal { 20 | 0 21 | } 22 | fn inclusive_max() -> Ordinal { 23 | 59 24 | } 25 | fn ordinals(&self) -> OrdinalSet { 26 | match self.ordinals.clone() { 27 | Some(ordinal_set) => ordinal_set, 28 | None => Minutes::supported_ordinals(), 29 | } 30 | } 31 | } 32 | 33 | impl PartialEq for Minutes { 34 | fn eq(&self, other: &Minutes) -> bool { 35 | self.ordinals() == other.ordinals() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /cron/src/time_unit/months.rs: -------------------------------------------------------------------------------- 1 | use crate::error::*; 2 | use crate::ordinal::{Ordinal, OrdinalSet}; 3 | use crate::time_unit::TimeUnitField; 4 | use std::borrow::Cow; 5 | 6 | #[derive(Clone, Debug, Eq)] 7 | pub struct Months { 8 | ordinals: Option, 9 | } 10 | 11 | impl TimeUnitField for Months { 12 | fn from_optional_ordinal_set(ordinal_set: Option) -> Self { 13 | Months { 14 | ordinals: ordinal_set, 15 | } 16 | } 17 | fn name() -> Cow<'static, str> { 18 | Cow::from("Months") 19 | } 20 | fn inclusive_min() -> Ordinal { 21 | 1 22 | } 23 | fn inclusive_max() -> Ordinal { 24 | 12 25 | } 26 | fn ordinal_from_name(name: &str) -> Result { 27 | //TODO: Use phf crate 28 | let ordinal = match name.to_lowercase().as_ref() { 29 | "jan" | "january" => 1, 30 | "feb" | "february" => 2, 31 | "mar" | "march" => 3, 32 | "apr" | "april" => 4, 33 | "may" => 5, 34 | "jun" | "june" => 6, 35 | "jul" | "july" => 7, 36 | "aug" | "august" => 8, 37 | "sep" | "september" => 9, 38 | "oct" | "october" => 10, 39 | "nov" | "november" => 11, 40 | "dec" | "december" => 12, 41 | _ => { 42 | return Err( 43 | ErrorKind::Expression(format!("'{}' is not a valid month name.", name)).into(), 44 | ) 45 | } 46 | }; 47 | Ok(ordinal) 48 | } 49 | fn ordinals(&self) -> OrdinalSet { 50 | match self.ordinals.clone() { 51 | Some(ordinal_set) => ordinal_set, 52 | None => Months::supported_ordinals(), 53 | } 54 | } 55 | } 56 | 57 | impl PartialEq for Months { 58 | fn eq(&self, other: &Months) -> bool { 59 | self.ordinals() == other.ordinals() 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /cron/src/time_unit/seconds.rs: -------------------------------------------------------------------------------- 1 | use crate::ordinal::{Ordinal, OrdinalSet}; 2 | use crate::time_unit::TimeUnitField; 3 | use std::borrow::Cow; 4 | 5 | #[derive(Clone, Debug, Eq)] 6 | pub struct Seconds { 7 | ordinals: Option, 8 | } 9 | 10 | impl TimeUnitField for Seconds { 11 | fn from_optional_ordinal_set(ordinal_set: Option) -> Self { 12 | Seconds { 13 | ordinals: ordinal_set, 14 | } 15 | } 16 | fn name() -> Cow<'static, str> { 17 | Cow::from("Seconds") 18 | } 19 | fn inclusive_min() -> Ordinal { 20 | 0 21 | } 22 | fn inclusive_max() -> Ordinal { 23 | 59 24 | } 25 | fn ordinals(&self) -> OrdinalSet { 26 | match self.ordinals.clone() { 27 | Some(ordinal_set) => ordinal_set, 28 | None => Seconds::supported_ordinals(), 29 | } 30 | } 31 | } 32 | 33 | impl PartialEq for Seconds { 34 | fn eq(&self, other: &Seconds) -> bool { 35 | self.ordinals() == other.ordinals() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /cron/src/time_unit/years.rs: -------------------------------------------------------------------------------- 1 | use crate::ordinal::{Ordinal, OrdinalSet}; 2 | use crate::time_unit::TimeUnitField; 3 | use std::borrow::Cow; 4 | 5 | #[derive(Clone, Debug, Eq)] 6 | pub struct Years { 7 | ordinals: Option, 8 | } 9 | 10 | impl TimeUnitField for Years { 11 | fn from_optional_ordinal_set(ordinal_set: Option) -> Self { 12 | Years { 13 | ordinals: ordinal_set, 14 | } 15 | } 16 | fn name() -> Cow<'static, str> { 17 | Cow::from("Years") 18 | } 19 | 20 | // TODO: Using the default impl, this will make a set w/100+ items each time "*" is used. 21 | // This is obviously suboptimal. 22 | fn inclusive_min() -> Ordinal { 23 | 1970 24 | } 25 | fn inclusive_max() -> Ordinal { 26 | 2100 27 | } 28 | fn ordinals(&self) -> OrdinalSet { 29 | match self.ordinals.clone() { 30 | Some(ordinal_set) => ordinal_set, 31 | None => Years::supported_ordinals(), 32 | } 33 | } 34 | } 35 | 36 | impl PartialEq for Years { 37 | fn eq(&self, other: &Years) -> bool { 38 | self.ordinals() == other.ordinals() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | # build stuff 2 | 3 | build: 4 | cargo build 5 | anchor build 6 | 7 | make: 8 | ./scripts/build-all.sh . 9 | 10 | tarball: 11 | ./scripts/ci/create-tarball.sh 12 | 13 | clean: 14 | cargo clean 15 | rm -rfv bin target lib clockwork-geyser-plugin-release* 16 | 17 | re: clean 18 | just make 19 | 20 | 21 | # aliases 22 | version: 23 | cat VERSION 24 | 25 | solana-version: 26 | ./scripts/ci/solana-version.sh 27 | 28 | rust-version: 29 | bash -c 'source ./scripts/ci/rust-version.sh; echo $rust_stable' 30 | 31 | kill: 32 | pkill solana-test-validator 33 | 34 | release-patch: 35 | gh workflow run bump-release.yaml -F bump=patch 36 | 37 | cli *args: 38 | cargo run --bin clockwork {{args}} 39 | 40 | localnet *args: build 41 | cargo run --bin clockwork localnet --dev {{args}} 42 | 43 | net: 44 | cargo run --bin clockwork localnet --dev 45 | 46 | logs: 47 | less test-ledger/validator.log 48 | 49 | tlg: 50 | tail -f test-ledger/validator.log 51 | 52 | watch: 53 | cargo watch -c -x "check" 54 | 55 | watch-cli: 56 | cargo watch -c -x "check --bin clockwork" 57 | 58 | 59 | # links 60 | pr: 61 | open https://github.com/clockwork-xyz/clockwork/pulls 62 | 63 | actions: 64 | open https://github.com/clockwork-xyz/clockwork/actions 65 | 66 | releases: 67 | open https://github.com/clockwork-xyz/clockwork/releases 68 | 69 | -------------------------------------------------------------------------------- /plugin/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "clockwork_plugin" 3 | version = "2.0.20" 4 | # this needs to match whatever solana uses! 5 | rust-version = "1.78.0" 6 | edition = "2021" 7 | description = "Clockwork plugin for Solana validators" 8 | license = "AGPL-3.0-or-later" 9 | homepage = "https://clockwork.xyz" 10 | repository = "https://github.com/clockwork-xyz/plugin" 11 | documentation = "https://docs.clockwork.xyz" 12 | readme = "./README.md" 13 | keywords = ["solana"] 14 | build = "build.rs" 15 | publish = false 16 | 17 | [lib] 18 | crate-type = ["cdylib", "rlib"] 19 | name = "clockwork_plugin" 20 | 21 | [features] 22 | idl-build = ["anchor-lang/idl-build"] 23 | 24 | [dependencies] 25 | anchor-lang = "0.30.0" 26 | async_once = "0.2.6" 27 | async-trait = "0.1.64" 28 | bincode = "1.3.3" 29 | bs58 = "0.4.0" 30 | bugsnag = "0.2.1" 31 | chrono = { version = "0.4.19", default-features = false, features = ["alloc"] } 32 | clockwork-cron = { path = "../cron", version = "=2.0.20" } 33 | clockwork-network-program = { path = "../programs/network", version = "=2.0.20" } 34 | clockwork-plugin-utils = { path = "utils", version = "=2.0.20" } 35 | clockwork-relayer-api = { path = "../relayer/api", version = "=2.0.20" } 36 | clockwork-thread-program = { package = "clockwork-thread-program", path = "../programs/thread", version = "=2.0.20" } 37 | clockwork-thread-program-v1 = { package = "clockwork-thread-program-v1", path = "../programs/thread/v1", version = "=1.4.4" } 38 | clockwork-webhook-program = { path = "../programs/webhook", version = "=2.0.20" } 39 | clockwork-utils = { path = "../utils", version = "=2.0.20" } 40 | lazy_static = "1.4.0" 41 | log = "0.4" 42 | prost = "0.10.0" 43 | pyth-sdk-solana = "0.10.1" 44 | reqwest = "0.11.11" 45 | serde = { version = "1.0", features = ["derive"] } 46 | serde_json = "1.0" 47 | simple-error = "0.2.3" 48 | solana-account-decoder = "^1.18.14" 49 | solana-client = "^1.18.14" 50 | solana-geyser-plugin-interface = "^1.18.14" 51 | solana-logger = "^1.18.14" 52 | solana-program = "^1.18.14" 53 | solana-sdk = "^1.18.14" 54 | solana-transaction-status = "^1.18.14" 55 | thiserror = "1.0.30" 56 | tokio = "1.18.4" 57 | futures = "0.3.26" 58 | static-pubkey = "1.0.3" 59 | solana-quic-client = "1.16.12" 60 | 61 | [build-dependencies] 62 | cargo_metadata = "=0.14.0" 63 | rustc_version = "0.4.0" 64 | 65 | [package.metadata.docs.rs] 66 | targets = ["x86_64-unknown-linux-gnu"] 67 | -------------------------------------------------------------------------------- /plugin/README.md: -------------------------------------------------------------------------------- 1 | # Clockwork Geyser Plugin 2 | -------------------------------------------------------------------------------- /plugin/build.rs: -------------------------------------------------------------------------------- 1 | use std::process::Command; 2 | 3 | fn main() { 4 | let rustc_v = rustc_version::version() 5 | .expect("Unable to get rustc version") 6 | .to_string(); 7 | let expected_v = "1.78.0".to_string(); 8 | 9 | // Check for a minimum version 10 | if rustc_v != expected_v { 11 | println!("cargo:warning=trying to compile with rustc {}, we should be using {}", 12 | rustc_v, 13 | expected_v, 14 | ); 15 | std::process::exit(1); 16 | } 17 | println!( 18 | "cargo:rustc-env=RUSTC_VERSION={:#?}", 19 | rustc_v, 20 | ); 21 | 22 | let output = Command::new("git") 23 | .args(&["rev-parse", "HEAD"]) 24 | .output() 25 | .expect("unable to get git commit hash"); 26 | let commit_hash = String::from_utf8(output.stdout).unwrap(); 27 | let url = format!( 28 | "https://github.com/clockwork-xyz/clockwork/tree/{}/cli/Cargo.toml", 29 | commit_hash 30 | ); 31 | println!("cargo:rustc-env=SPEC={}", url); 32 | 33 | let metadata = cargo_metadata::MetadataCommand::new().exec().unwrap(); 34 | let geyser_interface_version = metadata 35 | .packages 36 | .iter() 37 | .find(|p| p.name == "solana-geyser-plugin-interface") 38 | .expect("Unable to parse solana-geyser-plugin-interface version using cargo metadata") 39 | .version 40 | .to_string(); 41 | println!( 42 | "cargo:rustc-env=GEYSER_INTERFACE_VERSION={}", 43 | geyser_interface_version 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /plugin/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "libpath": "../target/debug/libclockwork_plugin.dylib", 3 | "keypath": "./test-ledger/validator-keypair.json", 4 | "slot_timeout_threshold": 150, 5 | "worker_threads": 10 6 | } -------------------------------------------------------------------------------- /plugin/src/builders/mod.rs: -------------------------------------------------------------------------------- 1 | mod pool_rotation; 2 | mod thread_exec; 3 | 4 | pub use pool_rotation::*; 5 | pub use thread_exec::*; 6 | -------------------------------------------------------------------------------- /plugin/src/builders/pool_rotation.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use anchor_lang::{ 4 | solana_program::instruction::Instruction, 5 | InstructionData, ToAccountMetas 6 | }; 7 | use clockwork_network_program::state::{Config, Pool, Registry, Snapshot, SnapshotFrame, Worker}; 8 | use log::info; 9 | use solana_client::nonblocking::rpc_client::RpcClient; 10 | use solana_sdk::{signature::Keypair, signer::Signer, transaction::Transaction}; 11 | 12 | use crate::pool_position::PoolPosition; 13 | 14 | pub async fn build_pool_rotation_tx<'a>( 15 | client: Arc, 16 | keypair: &Keypair, 17 | pool_position: PoolPosition, 18 | registry: Registry, 19 | snapshot: Snapshot, 20 | snapshot_frame: SnapshotFrame, 21 | worker_id: u64, 22 | ) -> Option { 23 | info!("nonce: {:?} total_stake: {:?} current_position: {:?} stake_offset: {:?} stake_amount: {:?}", 24 | registry.nonce.checked_rem(snapshot.total_stake), 25 | snapshot.total_stake, 26 | pool_position.current_position, 27 | snapshot_frame.stake_offset, 28 | snapshot_frame.stake_amount, 29 | ); 30 | 31 | // Exit early if the rotator is not intialized 32 | if registry.nonce == 0 { 33 | return None; 34 | } 35 | 36 | // Exit early the snapshot has no stake 37 | if snapshot.total_stake == 0 { 38 | return None; 39 | } 40 | 41 | // Exit early if the worker is already in the pool. 42 | if pool_position.current_position.is_some() { 43 | return None; 44 | } 45 | 46 | // Exit early if the snapshot frame is none or the worker has no delegated stake. 47 | if snapshot_frame.stake_amount.eq(&0) { 48 | return None; 49 | } 50 | 51 | // Check if the rotation window is open for this worker. 52 | let is_rotation_window_open = match registry.nonce.checked_rem(snapshot.total_stake) { 53 | None => false, 54 | Some(sample) => { 55 | sample >= snapshot_frame.stake_offset 56 | && sample 57 | < snapshot_frame 58 | .stake_offset 59 | .checked_add(snapshot_frame.stake_amount) 60 | .unwrap() 61 | } 62 | }; 63 | if !is_rotation_window_open { 64 | return None; 65 | } 66 | 67 | // Build rotation instruction to rotate the worker into pool 0. 68 | let snapshot_pubkey = Snapshot::pubkey(snapshot.id); 69 | let ix = Instruction { 70 | program_id: clockwork_network_program::ID, 71 | accounts: clockwork_network_program::accounts::PoolRotate { 72 | config: Config::pubkey(), 73 | pool: Pool::pubkey(0), 74 | registry: Registry::pubkey(), 75 | signatory: keypair.pubkey(), 76 | snapshot: snapshot_pubkey, 77 | snapshot_frame: SnapshotFrame::pubkey(snapshot_pubkey, worker_id), 78 | worker: Worker::pubkey(worker_id), 79 | }.to_account_metas(Some(false)), 80 | data: clockwork_network_program::instruction::PoolRotate {}.data(), 81 | }; 82 | 83 | // Build and sign tx. 84 | let mut tx = Transaction::new_with_payer(&[ix.clone()], Some(&keypair.pubkey())); 85 | tx.sign(&[keypair], client.get_latest_blockhash().await.unwrap()); 86 | return Some(tx); 87 | } 88 | -------------------------------------------------------------------------------- /plugin/src/executors/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod tx; 2 | pub mod webhook; 3 | 4 | use std::{ 5 | fmt::Debug, 6 | sync::{ 7 | atomic::{AtomicBool, Ordering}, 8 | Arc, 9 | }, 10 | }; 11 | 12 | use anchor_lang::{prelude::Pubkey, AccountDeserialize}; 13 | use async_trait::async_trait; 14 | use log::info; 15 | use solana_client::{ 16 | client_error::{ClientError, ClientErrorKind, Result as ClientResult}, 17 | nonblocking::rpc_client::RpcClient, 18 | }; 19 | use solana_geyser_plugin_interface::geyser_plugin_interface::Result as PluginResult; 20 | use solana_sdk::commitment_config::CommitmentConfig; 21 | use tokio::runtime::Runtime; 22 | use tx::TxExecutor; 23 | use webhook::WebhookExecutor; 24 | 25 | use crate::{config::PluginConfig, observers::Observers}; 26 | 27 | static LOCAL_RPC_URL: &str = "http://127.0.0.1:8899"; 28 | 29 | pub struct Executors { 30 | pub tx: Arc, 31 | pub webhook: Arc, 32 | pub client: Arc, 33 | pub lock: AtomicBool, 34 | } 35 | 36 | impl Executors { 37 | pub fn new(config: PluginConfig) -> Self { 38 | Executors { 39 | tx: Arc::new(TxExecutor::new(config.clone())), 40 | webhook: Arc::new(WebhookExecutor::new(config.clone())), 41 | client: Arc::new(RpcClient::new_with_commitment( 42 | LOCAL_RPC_URL.into(), 43 | CommitmentConfig::processed(), 44 | )), 45 | lock: AtomicBool::new(false), 46 | } 47 | } 48 | 49 | pub async fn process_slot( 50 | self: Arc, 51 | observers: Arc, 52 | slot: u64, 53 | runtime: Arc, 54 | ) -> PluginResult<()> { 55 | info!("process_slot: {}", slot,); 56 | let now = std::time::Instant::now(); 57 | 58 | // Return early if node is not healthy. 59 | if self.client.get_health().await.is_err() { 60 | info!( 61 | "processed_slot: {} duration: {:?} status: unhealthy", 62 | slot, 63 | now.elapsed() 64 | ); 65 | return Ok(()); 66 | } 67 | 68 | // Acquire lock. 69 | if self 70 | .clone() 71 | .lock 72 | .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed) 73 | .is_err() 74 | { 75 | info!( 76 | "processed_slot: {} duration: {:?} status: locked", 77 | slot, 78 | now.elapsed() 79 | ); 80 | return Ok(()); 81 | } 82 | 83 | // Process the slot on the observers. 84 | let executable_threads = observers.thread.clone().process_slot(slot).await?; 85 | 86 | // Process the slot in the transaction executor. 87 | self.tx 88 | .clone() 89 | .execute_txs( 90 | self.client.clone(), 91 | executable_threads, 92 | slot, 93 | runtime.clone(), 94 | ) 95 | .await?; 96 | 97 | // Process webhook requests. 98 | let executable_webhooks = observers.webhook.clone().process_slot(slot).await?; 99 | log::info!("Executable webhooks: {:?}", executable_webhooks); 100 | self.webhook 101 | .clone() 102 | .execute_webhooks(self.client.clone(), executable_webhooks) 103 | .await?; 104 | 105 | // Release the lock. 106 | self.clone() 107 | .lock 108 | .store(false, std::sync::atomic::Ordering::Relaxed); 109 | info!( 110 | "processed_slot: {} duration: {:?} status: processed", 111 | slot, 112 | now.elapsed() 113 | ); 114 | Ok(()) 115 | } 116 | } 117 | 118 | impl Debug for Executors { 119 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 120 | write!(f, "executors") 121 | } 122 | } 123 | 124 | #[async_trait] 125 | pub trait AccountGet { 126 | async fn get(&self, pubkey: &Pubkey) -> ClientResult; 127 | } 128 | 129 | #[async_trait] 130 | impl AccountGet for RpcClient { 131 | async fn get(&self, pubkey: &Pubkey) -> ClientResult { 132 | let data = self.get_account_data(pubkey).await?; 133 | T::try_deserialize(&mut data.as_slice()).map_err(|_| { 134 | ClientError::from(ClientErrorKind::Custom(format!( 135 | "Failed to deserialize account data" 136 | ))) 137 | }) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /plugin/src/executors/webhook.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Debug, sync::Arc}; 2 | 3 | use anchor_lang::prelude::Pubkey; 4 | use clockwork_webhook_program::state::Webhook; 5 | use clockwork_relayer_api::Relay; 6 | use log::info; 7 | use reqwest::header::CONTENT_TYPE; 8 | use solana_client::nonblocking::rpc_client::RpcClient; 9 | use solana_geyser_plugin_interface::geyser_plugin_interface::Result as PluginResult; 10 | 11 | use crate::config::PluginConfig; 12 | 13 | use super::AccountGet; 14 | 15 | pub struct WebhookExecutor { 16 | pub config: PluginConfig, 17 | } 18 | 19 | impl WebhookExecutor { 20 | pub fn new(config: PluginConfig) -> Self { 21 | Self { 22 | config: config.clone(), 23 | } 24 | } 25 | 26 | pub async fn execute_webhooks( 27 | self: Arc, 28 | client: Arc, 29 | pubkeys: Vec, 30 | ) -> PluginResult<()> { 31 | // self.spawn(|this| async move { 32 | // let url = http_request.clone().request.url; 33 | // let res = match http_request.request.method { 34 | // HttpMethod::Get => this.client.get(url), 35 | // HttpMethod::Post => this.client.post(url), 36 | // } 37 | // .header("x-caller-id", http_request.request.caller.to_string()) 38 | // .header("x-request-id", http_request.pubkey.to_string()) 39 | // .header("x-worker-id", this.worker_id.to_string()) 40 | // .send() 41 | // .await; 42 | // match res { 43 | // Ok(res) => info!("Webhook response: {:#?}", res), 44 | // Err(err) => info!("Webhook request failed with error: {}", err), 45 | // } 46 | // this.observers 47 | // .webhook 48 | // .webhook_requests 49 | // .remove(&http_request); 50 | // Ok(()) 51 | // }) 52 | // TODO Route to correct relayer 53 | for webhook_pubkey in pubkeys { 54 | let webhook = client 55 | .clone() 56 | .get::(&webhook_pubkey) 57 | .await 58 | .unwrap(); 59 | info!("webhook: {} {:?}", webhook_pubkey, webhook); 60 | let url = "http://127.0.0.1:8000/relay"; 61 | let client = reqwest::Client::new(); 62 | // for request_pubkey in requests { 63 | let _res = dbg!( 64 | client 65 | .post(url) 66 | .header(CONTENT_TYPE, "application/json") 67 | .json(&Relay { 68 | webhook: webhook_pubkey, 69 | }) 70 | .send() 71 | .await 72 | ); 73 | } 74 | // } 75 | Ok(()) 76 | } 77 | } 78 | 79 | impl Debug for WebhookExecutor { 80 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 81 | write!(f, "webhook-executor") 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /plugin/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod config { 2 | pub use clockwork_plugin_utils::PluginConfig; 3 | } 4 | 5 | use solana_geyser_plugin_interface::geyser_plugin_interface::GeyserPlugin; 6 | 7 | mod builders; 8 | 9 | mod events; 10 | 11 | mod executors; 12 | 13 | mod observers; 14 | 15 | mod plugin; 16 | 17 | mod pool_position; 18 | 19 | mod utils; 20 | 21 | pub use plugin::ClockworkPlugin; 22 | 23 | #[no_mangle] 24 | #[allow(improper_ctypes_definitions)] 25 | /// # Safety 26 | /// 27 | /// The Solana validator and this plugin must be compiled with the same Rust compiler version and Solana core version. 28 | /// Loading this plugin with mismatching versions is undefined behavior and will likely cause memory corruption. 29 | pub unsafe extern "C" fn _create_plugin() -> *mut dyn GeyserPlugin { 30 | let plugin: Box = Box::new(ClockworkPlugin::default()); 31 | Box::into_raw(plugin) 32 | } 33 | -------------------------------------------------------------------------------- /plugin/src/observers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod thread; 2 | pub mod webhook; 3 | 4 | use std::{fmt::Debug, sync::Arc}; 5 | 6 | use thread::ThreadObserver; 7 | use webhook::WebhookObserver; 8 | 9 | pub struct Observers { 10 | pub thread: Arc, 11 | pub webhook: Arc, 12 | } 13 | 14 | impl Observers { 15 | pub fn new() -> Self { 16 | Observers { 17 | thread: Arc::new(ThreadObserver::new()), 18 | webhook: Arc::new(WebhookObserver::new()), 19 | } 20 | } 21 | } 22 | 23 | impl Debug for Observers { 24 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 25 | write!(f, "observers") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /plugin/src/observers/webhook.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashSet, fmt::Debug, sync::Arc}; 2 | 3 | use clockwork_webhook_program::state::Webhook; 4 | use solana_geyser_plugin_interface::geyser_plugin_interface::Result as PluginResult; 5 | use solana_program::pubkey::Pubkey; 6 | use tokio::sync::RwLock; 7 | 8 | pub struct WebhookObserver { 9 | // The set of webhook that can be processed. 10 | pub webhooks: RwLock>, 11 | } 12 | 13 | impl WebhookObserver { 14 | pub fn new() -> Self { 15 | Self { 16 | webhooks: RwLock::new(HashSet::new()), 17 | } 18 | } 19 | 20 | pub async fn observe_webhook( 21 | self: Arc, 22 | _webhook: Webhook, 23 | webhook_pubkey: Pubkey, 24 | ) -> PluginResult<()> { 25 | let mut w_webhooks = self.webhooks.write().await; 26 | w_webhooks.insert(webhook_pubkey); 27 | Ok(()) 28 | } 29 | 30 | pub async fn process_slot(self: Arc, _slot: u64) -> PluginResult> { 31 | let mut w_webhooks = self.webhooks.write().await; 32 | let executable_webhooks = w_webhooks.clone().into_iter().collect(); 33 | w_webhooks.clear(); 34 | Ok(executable_webhooks) 35 | } 36 | } 37 | 38 | impl Debug for WebhookObserver { 39 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 40 | write!(f, "webhook-observer") 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /plugin/src/pool_position.rs: -------------------------------------------------------------------------------- 1 | use {solana_program::pubkey::Pubkey, std::fmt::Debug}; 2 | 3 | #[derive(Clone, Debug)] 4 | pub struct PoolPosition { 5 | pub current_position: Option, 6 | pub workers: Vec, 7 | } 8 | 9 | impl Default for PoolPosition { 10 | fn default() -> Self { 11 | PoolPosition { 12 | current_position: None, 13 | workers: vec![], 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /plugin/src/utils.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::signature::{read_keypair_file, Keypair}; 2 | 3 | pub fn read_or_new_keypair(keypath: Option) -> Keypair { 4 | match keypath { 5 | Some(keypath) => read_keypair_file(keypath).unwrap(), 6 | None => Keypair::new(), 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /plugin/utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "clockwork-plugin-utils" 3 | version = "2.0.20" 4 | edition = "2021" 5 | description = "Clockwork Plugin Utils" 6 | license = "AGPL-3.0-or-later" 7 | homepage = "https://clockwork.xyz" 8 | repository = "https://github.com/clockwork-xyz/plugin/utils" 9 | documentation = "https://docs.clockwork.xyz" 10 | readme = "./README.md" 11 | keywords = ["solana"] 12 | 13 | [lib] 14 | crate-type = ["cdylib", "lib"] 15 | name = "clockwork_plugin_utils" 16 | 17 | [dependencies] 18 | serde = { version = "1.0", features = ["derive"] } 19 | serde_json = "1.0" 20 | solana-geyser-plugin-interface = "^1.18.14" 21 | -------------------------------------------------------------------------------- /plugin/utils/README.md: -------------------------------------------------------------------------------- 1 | # Clockwork Geyser Plugin Utils 2 | -------------------------------------------------------------------------------- /plugin/utils/src/config.rs: -------------------------------------------------------------------------------- 1 | use { 2 | serde::{ 3 | Serialize, 4 | Deserialize 5 | }, 6 | solana_geyser_plugin_interface::geyser_plugin_interface::{ 7 | GeyserPluginError, Result as PluginResult, 8 | }, 9 | std::{fs::File, path::Path}, 10 | }; 11 | 12 | static DEFAULT_TRANSACTION_TIMEOUT_THRESHOLD: u64 = 150; 13 | static DEFAULT_THREAD_COUNT: usize = 10; 14 | 15 | /// Plugin config. 16 | #[derive(Clone, Debug, Serialize, Deserialize)] 17 | pub struct PluginConfig { 18 | pub keypath: Option, 19 | pub libpath: Option, 20 | pub thread_count: usize, 21 | pub transaction_timeout_threshold: u64, 22 | pub worker_id: u64, 23 | } 24 | 25 | impl Default for PluginConfig { 26 | fn default() -> Self { 27 | Self { 28 | keypath: None, 29 | libpath: None, 30 | transaction_timeout_threshold: DEFAULT_TRANSACTION_TIMEOUT_THRESHOLD, 31 | thread_count: DEFAULT_THREAD_COUNT, 32 | worker_id: 0, 33 | } 34 | } 35 | } 36 | 37 | impl PluginConfig { 38 | /// Read plugin from JSON file. 39 | pub fn read_from>(config_path: P) -> PluginResult { 40 | let file = File::open(config_path)?; 41 | let this: Self = serde_json::from_reader(file) 42 | .map_err(|e| GeyserPluginError::ConfigFileReadError { msg: e.to_string() })?; 43 | Ok(this) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /plugin/utils/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | 3 | pub use crate::config::PluginConfig; 4 | -------------------------------------------------------------------------------- /programs/network/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "clockwork-network-program" 3 | version = "2.0.20" 4 | description = "Clockwork networking protocol" 5 | edition = "2021" 6 | license = "AGPL-3.0-or-later" 7 | homepage = "https://clockwork.xyz" 8 | repository = "https://github.com/clockwork-xyz/clockwork" 9 | documentation = "https://docs.clockwork.xyz" 10 | readme = "./README.md" 11 | keywords = ["solana"] 12 | 13 | [lib] 14 | crate-type = ["cdylib", "lib"] 15 | name = "clockwork_network_program" 16 | 17 | [features] 18 | no-entrypoint = [] 19 | no-idl = [] 20 | no-log-ix-name = [] 21 | cpi = ["no-entrypoint"] 22 | default = [] 23 | idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build", "clockwork-utils/idl-build"] 24 | 25 | [dependencies] 26 | anchor-lang = "0.30.0" 27 | anchor-spl = { features = ["mint", "token"], version = "0.30.0" } 28 | clockwork-utils = { path = "../../utils", version = "=2.0.20" } 29 | -------------------------------------------------------------------------------- /programs/network/README.md: -------------------------------------------------------------------------------- 1 | # Clockwork Network 2 | -------------------------------------------------------------------------------- /programs/network/src/errors.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | #[error_code] 4 | pub enum ClockworkError { 5 | #[msg("The worker is already in the pool")] 6 | AlreadyInPool, 7 | 8 | #[msg("The commission rate must be an integer between 0 and 100")] 9 | InvalidCommissionRate, 10 | 11 | #[msg("You cannot request to unstake more tokens than are currently locked")] 12 | InvalidUnstakeAmount, 13 | 14 | #[msg("The penalty account has an insufficient balance for this operation")] 15 | InsufficientPenaltyBalance, 16 | 17 | #[msg("The authority address cannot be used as the worker signatory")] 18 | InvalidSignatory, 19 | 20 | #[msg("The registry is locked and may not be updated right now")] 21 | RegistryLocked, 22 | 23 | #[msg("The worker cannot rotate into the pool right now")] 24 | PoolFull, 25 | } 26 | -------------------------------------------------------------------------------- /programs/network/src/instructions/config_update.rs: -------------------------------------------------------------------------------- 1 | use {crate::state::*, anchor_lang::prelude::*}; 2 | 3 | #[derive(Accounts)] 4 | #[instruction(settings: ConfigSettings)] 5 | pub struct ConfigUpdate<'info> { 6 | #[account(mut)] 7 | pub admin: Signer<'info>, 8 | 9 | #[account( 10 | mut, 11 | seeds = [SEED_CONFIG], 12 | bump, 13 | has_one = admin 14 | )] 15 | pub config: Account<'info, Config>, 16 | } 17 | 18 | pub fn handler(ctx: Context, settings: ConfigSettings) -> Result<()> { 19 | let config = &mut ctx.accounts.config; 20 | config.update(settings) 21 | } 22 | -------------------------------------------------------------------------------- /programs/network/src/instructions/delegation_claim.rs: -------------------------------------------------------------------------------- 1 | use {crate::state::*, anchor_lang::prelude::*}; 2 | 3 | #[derive(Accounts)] 4 | #[instruction(amount: u64)] 5 | pub struct DelegationClaim<'info> { 6 | #[account()] 7 | pub authority: Signer<'info>, 8 | 9 | #[account(mut)] 10 | pub pay_to: SystemAccount<'info>, 11 | 12 | #[account( 13 | mut, 14 | seeds = [ 15 | SEED_DELEGATION, 16 | delegation.worker.as_ref(), 17 | delegation.id.to_be_bytes().as_ref(), 18 | ], 19 | bump, 20 | has_one = authority, 21 | )] 22 | pub delegation: Account<'info, Delegation>, 23 | } 24 | 25 | pub fn handler(ctx: Context, amount: u64) -> Result<()> { 26 | // Get accounts. 27 | let pay_to = &mut ctx.accounts.pay_to; 28 | let delegation = &mut ctx.accounts.delegation; 29 | 30 | // Decrement the delegation's claimable balance. 31 | delegation.yield_balance = delegation.yield_balance.checked_sub(amount).unwrap(); 32 | 33 | // Transfer commission to the worker. 34 | **delegation.to_account_info().try_borrow_mut_lamports()? = delegation 35 | .to_account_info() 36 | .lamports() 37 | .checked_sub(amount) 38 | .unwrap(); 39 | **pay_to.to_account_info().try_borrow_mut_lamports()? = pay_to 40 | .to_account_info() 41 | .lamports() 42 | .checked_add(amount) 43 | .unwrap(); 44 | 45 | Ok(()) 46 | } 47 | -------------------------------------------------------------------------------- /programs/network/src/instructions/delegation_create.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::state::*, 3 | anchor_lang::{ 4 | prelude::*, 5 | solana_program::{system_program, sysvar}, 6 | }, 7 | anchor_spl::{ 8 | associated_token::AssociatedToken, 9 | token::{Mint, Token, TokenAccount}, 10 | }, 11 | std::mem::size_of, 12 | }; 13 | 14 | #[derive(Accounts)] 15 | pub struct DelegationCreate<'info> { 16 | #[account(address = anchor_spl::associated_token::ID)] 17 | pub associated_token_program: Program<'info, AssociatedToken>, 18 | 19 | #[account(mut)] 20 | pub authority: Signer<'info>, 21 | 22 | #[account(address = Config::pubkey())] 23 | pub config: Account<'info, Config>, 24 | 25 | #[account( 26 | init, 27 | seeds = [ 28 | SEED_DELEGATION, 29 | worker.key().as_ref(), 30 | worker.total_delegations.to_be_bytes().as_ref(), 31 | ], 32 | bump, 33 | payer = authority, 34 | space = 8 + size_of::(), 35 | )] 36 | pub delegation: Account<'info, Delegation>, 37 | 38 | #[account( 39 | init, 40 | payer = authority, 41 | associated_token::authority = delegation, 42 | associated_token::mint = mint, 43 | )] 44 | pub delegation_tokens: Account<'info, TokenAccount>, 45 | 46 | #[account(address = config.mint)] 47 | pub mint: Account<'info, Mint>, 48 | 49 | #[account(address = sysvar::rent::ID)] 50 | pub rent: Sysvar<'info, Rent>, 51 | 52 | #[account(address = system_program::ID)] 53 | pub system_program: Program<'info, System>, 54 | 55 | #[account(address = anchor_spl::token::ID)] 56 | pub token_program: Program<'info, Token>, 57 | 58 | #[account( 59 | mut, 60 | seeds = [ 61 | SEED_WORKER, 62 | worker.id.to_be_bytes().as_ref(), 63 | ], 64 | bump 65 | )] 66 | pub worker: Account<'info, Worker>, 67 | } 68 | 69 | pub fn handler(ctx: Context) -> Result<()> { 70 | // Get accounts 71 | let authority = &ctx.accounts.authority; 72 | let delegation = &mut ctx.accounts.delegation; 73 | let worker = &mut ctx.accounts.worker; 74 | 75 | // Initialize the delegation account. 76 | delegation.init(authority.key(), worker.total_delegations, worker.key())?; 77 | 78 | // Increment the worker's total delegations counter. 79 | worker.total_delegations = worker.total_delegations.checked_add(1).unwrap(); 80 | 81 | Ok(()) 82 | } 83 | -------------------------------------------------------------------------------- /programs/network/src/instructions/delegation_deposit.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::state::*, 3 | anchor_lang::prelude::*, 4 | anchor_spl::token::{transfer, Token, TokenAccount, Transfer}, 5 | }; 6 | 7 | #[derive(Accounts)] 8 | #[instruction(amount: u64)] 9 | pub struct DelegationDeposit<'info> { 10 | #[account(mut)] 11 | pub authority: Signer<'info>, 12 | 13 | #[account( 14 | mut, 15 | associated_token::authority = authority, 16 | associated_token::mint = config.mint, 17 | )] 18 | pub authority_tokens: Account<'info, TokenAccount>, 19 | 20 | #[account(address = Config::pubkey())] 21 | pub config: Account<'info, Config>, 22 | 23 | #[account( 24 | mut, 25 | seeds = [ 26 | SEED_DELEGATION, 27 | delegation.worker.as_ref(), 28 | delegation.id.to_be_bytes().as_ref(), 29 | ], 30 | bump, 31 | has_one = authority, 32 | )] 33 | pub delegation: Account<'info, Delegation>, 34 | 35 | #[account( 36 | mut, 37 | associated_token::authority = delegation, 38 | associated_token::mint = config.mint, 39 | )] 40 | pub delegation_tokens: Account<'info, TokenAccount>, 41 | 42 | #[account(address = anchor_spl::token::ID)] 43 | pub token_program: Program<'info, Token>, 44 | } 45 | 46 | pub fn handler(ctx: Context, amount: u64) -> Result<()> { 47 | // Get accounts. 48 | let authority = &ctx.accounts.authority; 49 | let authority_tokens = &ctx.accounts.authority_tokens; 50 | let delegation_tokens = &ctx.accounts.delegation_tokens; 51 | let token_program = &ctx.accounts.token_program; 52 | 53 | // Transfer tokens from authority tokens to delegation 54 | transfer( 55 | CpiContext::new( 56 | token_program.to_account_info(), 57 | Transfer { 58 | from: authority_tokens.to_account_info(), 59 | to: delegation_tokens.to_account_info(), 60 | authority: authority.to_account_info(), 61 | }, 62 | ), 63 | amount, 64 | )?; 65 | 66 | Ok(()) 67 | } 68 | -------------------------------------------------------------------------------- /programs/network/src/instructions/delegation_withdraw.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::state::*, 3 | anchor_lang::prelude::*, 4 | anchor_spl::token::{transfer, Token, TokenAccount, Transfer}, 5 | }; 6 | 7 | #[derive(Accounts)] 8 | #[instruction(amount: u64)] 9 | pub struct DelegationWithdraw<'info> { 10 | #[account(mut)] 11 | pub authority: Signer<'info>, 12 | 13 | #[account( 14 | mut, 15 | associated_token::authority = authority, 16 | associated_token::mint = config.mint, 17 | )] 18 | pub authority_tokens: Account<'info, TokenAccount>, 19 | 20 | #[account(address = Config::pubkey())] 21 | pub config: Account<'info, Config>, 22 | 23 | #[account( 24 | mut, 25 | seeds = [ 26 | SEED_DELEGATION, 27 | delegation.worker.as_ref(), 28 | delegation.id.to_be_bytes().as_ref(), 29 | ], 30 | bump, 31 | has_one = authority, 32 | )] 33 | pub delegation: Account<'info, Delegation>, 34 | 35 | #[account( 36 | mut, 37 | associated_token::authority = delegation, 38 | associated_token::mint = config.mint, 39 | )] 40 | pub delegation_tokens: Account<'info, TokenAccount>, 41 | 42 | #[account(address = anchor_spl::token::ID)] 43 | pub token_program: Program<'info, Token>, 44 | } 45 | 46 | pub fn handler(ctx: Context, amount: u64) -> Result<()> { 47 | // Get accounts. 48 | let authority_tokens = &ctx.accounts.authority_tokens; 49 | let delegation = &ctx.accounts.delegation; 50 | let delegation_tokens = &ctx.accounts.delegation_tokens; 51 | let token_program = &ctx.accounts.token_program; 52 | 53 | // Transfer tokens from authority tokens to delegation 54 | let bump = ctx.bumps.delegation; 55 | transfer( 56 | CpiContext::new_with_signer( 57 | token_program.to_account_info(), 58 | Transfer { 59 | from: delegation_tokens.to_account_info(), 60 | to: authority_tokens.to_account_info(), 61 | authority: delegation.to_account_info(), 62 | }, 63 | &[&[ 64 | SEED_DELEGATION, 65 | delegation.worker.as_ref(), 66 | delegation.id.to_be_bytes().as_ref(), 67 | &[bump], 68 | ]], 69 | ), 70 | amount, 71 | )?; 72 | 73 | Ok(()) 74 | } 75 | -------------------------------------------------------------------------------- /programs/network/src/instructions/fee_collect.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{errors::*, objects::*}, 3 | anchor_lang::prelude::*, 4 | }; 5 | 6 | #[derive(Accounts)] 7 | #[instruction(amount: u64, penalty: bool)] 8 | pub struct FeeCollect<'info> { 9 | #[account( 10 | mut, 11 | seeds = [ 12 | SEED_FEE, 13 | fee.worker.as_ref(), 14 | ], 15 | bump, 16 | )] 17 | pub fee: Account<'info, Fee>, 18 | 19 | #[account()] 20 | pub signer: Signer<'info>, 21 | } 22 | 23 | pub fn handler(ctx: Context, amount: u64, penalty: bool) -> Result<()> { 24 | // Get accounts. 25 | let fee = &mut ctx.accounts.fee; 26 | 27 | // Increment the collected fee counter. 28 | if penalty { 29 | fee.penalty_balance = fee.penalty_balance.checked_add(amount).unwrap(); 30 | } else { 31 | fee.collected_balance = fee.collected_balance.checked_add(amount).unwrap(); 32 | } 33 | 34 | // Verify there are enough lamports to distribute at the end of the epoch. 35 | let lamport_balance = fee.to_account_info().lamports(); 36 | let data_len = 8 + fee.try_to_vec()?.len(); 37 | let min_rent_balance = Rent::get().unwrap().minimum_balance(data_len); 38 | 39 | msg!( 40 | "Fee collection! lamports: {} collected: {} penalty: {} rent: {}", 41 | lamport_balance, 42 | fee.collected_balance, 43 | fee.penalty_balance, 44 | min_rent_balance 45 | ); 46 | 47 | require!( 48 | fee.collected_balance 49 | .checked_add(fee.penalty_balance) 50 | .unwrap() 51 | .checked_add(min_rent_balance) 52 | .unwrap() 53 | .ge(&lamport_balance), 54 | ClockworkError::InsufficientFeeBalance 55 | ); 56 | 57 | Ok(()) 58 | } 59 | -------------------------------------------------------------------------------- /programs/network/src/instructions/initialize.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::state::*, 3 | anchor_lang::{prelude::*, solana_program::system_program}, 4 | anchor_spl::token::Mint, 5 | std::mem::size_of, 6 | }; 7 | 8 | #[derive(Accounts)] 9 | pub struct Initialize<'info> { 10 | #[account(mut)] 11 | pub admin: Signer<'info>, 12 | 13 | #[account( 14 | init, 15 | seeds = [SEED_CONFIG], 16 | bump, 17 | payer = admin, 18 | space = 8 + size_of::(), 19 | )] 20 | pub config: Account<'info, Config>, 21 | 22 | #[account()] 23 | pub mint: Account<'info, Mint>, 24 | 25 | #[account( 26 | init, 27 | seeds = [SEED_REGISTRY], 28 | bump, 29 | payer = admin, 30 | space = 8 + size_of::(), 31 | )] 32 | pub registry: Account<'info, Registry>, 33 | 34 | #[account( 35 | init, 36 | seeds = [ 37 | SEED_SNAPSHOT, 38 | (0 as u64).to_be_bytes().as_ref(), 39 | ], 40 | bump, 41 | payer = admin, 42 | space = 8 + size_of::(), 43 | )] 44 | pub snapshot: Account<'info, Snapshot>, 45 | 46 | #[account(address = system_program::ID)] 47 | pub system_program: Program<'info, System>, 48 | } 49 | 50 | pub fn handler(ctx: Context) -> Result<()> { 51 | // Get accounts 52 | let admin = &ctx.accounts.admin; 53 | let config = &mut ctx.accounts.config; 54 | let mint = &ctx.accounts.mint; 55 | let registry = &mut ctx.accounts.registry; 56 | let snapshot = &mut ctx.accounts.snapshot; 57 | 58 | // Initialize accounts. 59 | config.init(admin.key(), mint.key())?; 60 | registry.init()?; 61 | snapshot.init(0)?; 62 | 63 | Ok(()) 64 | } 65 | -------------------------------------------------------------------------------- /programs/network/src/instructions/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod config_update; 2 | pub mod delegation_claim; 3 | pub mod delegation_create; 4 | pub mod delegation_deposit; 5 | pub mod delegation_withdraw; 6 | pub mod initialize; 7 | pub mod penalty_claim; 8 | pub mod pool_create; 9 | pub mod pool_rotate; 10 | pub mod pool_update; 11 | pub mod registry_nonce_hash; 12 | pub mod registry_unlock; 13 | pub mod unstake_create; 14 | pub mod worker_claim; 15 | pub mod worker_create; 16 | pub mod worker_update; 17 | 18 | pub use config_update::*; 19 | pub use delegation_claim::*; 20 | pub use delegation_create::*; 21 | pub use delegation_deposit::*; 22 | pub use delegation_withdraw::*; 23 | pub use initialize::*; 24 | pub use penalty_claim::*; 25 | pub use pool_create::*; 26 | pub use pool_rotate::*; 27 | pub use pool_update::*; 28 | pub use registry_nonce_hash::*; 29 | pub use registry_unlock::*; 30 | pub use unstake_create::*; 31 | pub use worker_claim::*; 32 | pub use worker_create::*; 33 | pub use worker_update::*; 34 | -------------------------------------------------------------------------------- /programs/network/src/instructions/penalty_claim.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{errors::*, state::*}, 3 | anchor_lang::prelude::*, 4 | }; 5 | 6 | #[derive(Accounts)] 7 | pub struct PenaltyClaim<'info> { 8 | #[account(mut, address = config.admin)] 9 | pub admin: Signer<'info>, 10 | 11 | #[account(address = Config::pubkey())] 12 | pub config: Account<'info, Config>, 13 | 14 | #[account(mut)] 15 | pub pay_to: SystemAccount<'info>, 16 | 17 | #[account( 18 | mut, 19 | seeds = [ 20 | SEED_PENALTY, 21 | penalty.worker.as_ref(), 22 | ], 23 | bump, 24 | )] 25 | pub penalty: Account<'info, Penalty>, 26 | } 27 | 28 | pub fn handler(ctx: Context) -> Result<()> { 29 | // Get accounts. 30 | let penalty = &mut ctx.accounts.penalty; 31 | let pay_to = &mut ctx.accounts.pay_to; 32 | 33 | // Calculate how many lamports are 34 | let lamport_balance = penalty.to_account_info().lamports(); 35 | let data_len = 8 + penalty.try_to_vec()?.len(); 36 | let min_rent_balance = Rent::get().unwrap().minimum_balance(data_len); 37 | let claimable_balance = lamport_balance.checked_sub(min_rent_balance).unwrap(); 38 | require!( 39 | claimable_balance.gt(&0), 40 | ClockworkError::InsufficientPenaltyBalance 41 | ); 42 | 43 | // Pay reimbursment for base transaction fee 44 | **penalty.to_account_info().try_borrow_mut_lamports()? = penalty 45 | .to_account_info() 46 | .lamports() 47 | .checked_sub(claimable_balance) 48 | .unwrap(); 49 | **pay_to.to_account_info().try_borrow_mut_lamports()? = pay_to 50 | .to_account_info() 51 | .lamports() 52 | .checked_add(claimable_balance) 53 | .unwrap(); 54 | 55 | Ok(()) 56 | } 57 | -------------------------------------------------------------------------------- /programs/network/src/instructions/pool_create.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{errors::*, state::*}, 3 | anchor_lang::{prelude::*, solana_program::system_program}, 4 | std::mem::size_of, 5 | }; 6 | 7 | #[derive(Accounts)] 8 | pub struct PoolCreate<'info> { 9 | #[account(address = config.admin)] 10 | pub admin: Signer<'info>, 11 | 12 | #[account( 13 | address = Config::pubkey(), 14 | has_one = admin 15 | )] 16 | pub config: Account<'info, Config>, 17 | 18 | #[account(mut)] 19 | pub payer: Signer<'info>, 20 | 21 | #[account( 22 | init, 23 | seeds = [ 24 | SEED_POOL, 25 | registry.total_pools.to_be_bytes().as_ref(), 26 | ], 27 | bump, 28 | payer = payer, 29 | space = 8 + size_of::() + size_of::(), 30 | )] 31 | pub pool: Account<'info, Pool>, 32 | 33 | #[account( 34 | mut, 35 | seeds = [SEED_REGISTRY], 36 | bump, 37 | constraint = !registry.locked @ ClockworkError::RegistryLocked 38 | )] 39 | pub registry: Box>, 40 | 41 | #[account(address = system_program::ID)] 42 | pub system_program: Program<'info, System>, 43 | } 44 | 45 | pub fn handler(ctx: Context) -> Result<()> { 46 | // Get accounts 47 | let pool = &mut ctx.accounts.pool; 48 | let registry = &mut ctx.accounts.registry; 49 | 50 | // Initialize the pool account. 51 | pool.init(registry.total_pools)?; 52 | 53 | // Increment the registry's pool counter. 54 | registry.total_pools = registry.total_pools.checked_add(1).unwrap(); 55 | 56 | Ok(()) 57 | } 58 | -------------------------------------------------------------------------------- /programs/network/src/instructions/pool_rotate.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{errors::*, state::*}, 3 | anchor_lang::prelude::*, 4 | }; 5 | 6 | // TODO Make pool rotation a function of the epoch pubkey. 7 | // Workers should self-select into the delegate pool on deterministic epochs. 8 | // If a worker is not active, they will not rotate into the pool. 9 | // This gives curent workers (presumably active) extra time in the pool. 10 | 11 | #[derive(Accounts)] 12 | pub struct PoolRotate<'info> { 13 | #[account(address = Config::pubkey())] 14 | pub config: Account<'info, Config>, 15 | 16 | #[account( 17 | mut, 18 | seeds = [ 19 | SEED_POOL, 20 | pool.id.to_be_bytes().as_ref(), 21 | ], 22 | bump, 23 | )] 24 | pub pool: Account<'info, Pool>, 25 | 26 | #[account(address = Registry::pubkey())] 27 | pub registry: Account<'info, Registry>, 28 | 29 | #[account(mut)] 30 | pub signatory: Signer<'info>, 31 | 32 | #[account( 33 | address = snapshot.pubkey(), 34 | constraint = snapshot.id.eq(®istry.current_epoch) 35 | )] 36 | pub snapshot: Account<'info, Snapshot>, 37 | 38 | #[account( 39 | address = snapshot_frame.pubkey(), 40 | has_one = snapshot, 41 | has_one = worker 42 | )] 43 | pub snapshot_frame: Account<'info, SnapshotFrame>, 44 | 45 | #[account( 46 | address = worker.pubkey(), 47 | has_one = signatory 48 | )] 49 | pub worker: Account<'info, Worker>, 50 | } 51 | 52 | pub fn handler(ctx: Context) -> Result<()> { 53 | // Get accounts 54 | let pool = &mut ctx.accounts.pool; 55 | let registry = &ctx.accounts.registry; 56 | let snapshot = &ctx.accounts.snapshot; 57 | let snapshot_frame = &ctx.accounts.snapshot_frame; 58 | let worker = &ctx.accounts.worker; 59 | 60 | // Verify the pool has excess space or the worker can rotate in at this time. 61 | require!( 62 | pool.workers.len().lt(&(pool.size as usize)) 63 | || is_rotation_window_open(®istry, &snapshot, &snapshot_frame).unwrap(), 64 | ClockworkError::PoolFull 65 | ); 66 | 67 | // Verify the worker is not already in the pool. 68 | require!( 69 | !pool.workers.contains(&worker.key()), 70 | ClockworkError::AlreadyInPool 71 | ); 72 | 73 | // Rotate the worker into the pool. 74 | pool.rotate(worker.key())?; 75 | 76 | Ok(()) 77 | } 78 | 79 | fn is_rotation_window_open( 80 | registry: &Account, 81 | snapshot: &Account, 82 | snapshot_frame: &Account, 83 | ) -> Result { 84 | // Return true if the sample is within the entry's stake range 85 | match registry.nonce.checked_rem(snapshot.total_stake) { 86 | None => Ok(false), 87 | Some(sample) => Ok(sample >= snapshot_frame.stake_offset 88 | && sample 89 | < snapshot_frame 90 | .stake_offset 91 | .checked_add(snapshot_frame.stake_amount) 92 | .unwrap()), 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /programs/network/src/instructions/pool_update.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::state::*, 3 | anchor_lang::{ 4 | prelude::*, 5 | solana_program::system_program, 6 | system_program::{transfer, Transfer}, 7 | }, 8 | std::mem::size_of, 9 | }; 10 | 11 | #[derive(Accounts)] 12 | #[instruction(settings: PoolSettings)] 13 | pub struct PoolUpdate<'info> { 14 | #[account()] 15 | pub admin: Signer<'info>, 16 | 17 | #[account( 18 | address = Config::pubkey(), 19 | has_one = admin 20 | )] 21 | pub config: Account<'info, Config>, 22 | 23 | #[account(mut)] 24 | pub payer: Signer<'info>, 25 | 26 | #[account(mut, address = pool.pubkey())] 27 | pub pool: Account<'info, Pool>, 28 | 29 | #[account(address = system_program::ID)] 30 | pub system_program: Program<'info, System>, 31 | } 32 | 33 | pub fn handler(ctx: Context, settings: PoolSettings) -> Result<()> { 34 | // Get accounts 35 | let payer = &ctx.accounts.payer; 36 | let pool = &mut ctx.accounts.pool; 37 | let system_program = &ctx.accounts.system_program; 38 | 39 | // Update the pool settings 40 | pool.update(&settings)?; 41 | 42 | // Reallocate memory for the pool account 43 | let data_len = 8 + size_of::() as u32 + settings.size.checked_mul(size_of::() as u32).unwrap(); 44 | pool.to_account_info().realloc(data_len as usize, false)?; 45 | 46 | // If lamports are required to maintain rent-exemption, pay them 47 | let minimum_rent = Rent::get().unwrap().minimum_balance(data_len as usize); 48 | if minimum_rent > pool.to_account_info().lamports() { 49 | transfer( 50 | CpiContext::new( 51 | system_program.to_account_info(), 52 | Transfer { 53 | from: payer.to_account_info(), 54 | to: pool.to_account_info(), 55 | }, 56 | ), 57 | minimum_rent 58 | .checked_sub(pool.to_account_info().lamports()) 59 | .unwrap(), 60 | )?; 61 | } 62 | 63 | Ok(()) 64 | } 65 | -------------------------------------------------------------------------------- /programs/network/src/instructions/registry_nonce_hash.rs: -------------------------------------------------------------------------------- 1 | use clockwork_utils::thread::ThreadResponse; 2 | 3 | use {crate::state::*, anchor_lang::prelude::*}; 4 | 5 | #[derive(Accounts)] 6 | pub struct RegistryNonceHash<'info> { 7 | #[account(address = Config::pubkey())] 8 | pub config: Account<'info, Config>, 9 | 10 | #[account( 11 | mut, 12 | seeds = [SEED_REGISTRY], 13 | bump 14 | )] 15 | pub registry: Account<'info, Registry>, 16 | 17 | #[account(address = config.hasher_thread)] 18 | pub thread: Signer<'info>, 19 | } 20 | 21 | pub fn handler(ctx: Context) -> Result { 22 | let registry = &mut ctx.accounts.registry; 23 | registry.hash_nonce()?; 24 | Ok(ThreadResponse::default()) 25 | } 26 | -------------------------------------------------------------------------------- /programs/network/src/instructions/registry_unlock.rs: -------------------------------------------------------------------------------- 1 | use {crate::state::*, anchor_lang::prelude::*}; 2 | 3 | #[derive(Accounts)] 4 | pub struct RegistryUnlock<'info> { 5 | #[account(mut)] 6 | pub admin: Signer<'info>, 7 | 8 | #[account(seeds = [SEED_CONFIG], bump, has_one = admin)] 9 | pub config: Account<'info, Config>, 10 | 11 | #[account( 12 | mut, 13 | seeds = [SEED_REGISTRY], 14 | bump 15 | )] 16 | pub registry: Account<'info, Registry>, 17 | } 18 | 19 | pub fn handler(ctx: Context) -> Result<()> { 20 | let registry = &mut ctx.accounts.registry; 21 | registry.locked = false; 22 | Ok(()) 23 | } 24 | -------------------------------------------------------------------------------- /programs/network/src/instructions/unstake_create.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{errors::*, state::*}, 3 | anchor_lang::{prelude::*, solana_program::system_program}, 4 | std::mem::size_of 5 | }; 6 | 7 | #[derive(Accounts)] 8 | #[instruction(amount: u64)] 9 | pub struct UnstakeCreate<'info> { 10 | #[account(mut)] 11 | pub authority: Signer<'info>, 12 | 13 | #[account( 14 | seeds = [ 15 | SEED_DELEGATION, 16 | delegation.worker.as_ref(), 17 | delegation.id.to_be_bytes().as_ref(), 18 | ], 19 | bump, 20 | has_one = authority, 21 | has_one = worker, 22 | )] 23 | pub delegation: Account<'info, Delegation>, 24 | 25 | #[account( 26 | mut, 27 | seeds = [SEED_REGISTRY], 28 | bump, 29 | constraint = !registry.locked @ ClockworkError::RegistryLocked 30 | )] 31 | pub registry: Account<'info, Registry>, 32 | 33 | #[account(address = system_program::ID)] 34 | pub system_program: Program<'info, System>, 35 | 36 | #[account( 37 | init, 38 | seeds = [ 39 | SEED_UNSTAKE, 40 | registry.total_unstakes.to_be_bytes().as_ref(), 41 | ], 42 | bump, 43 | payer = authority, 44 | space = 8 + size_of::(), 45 | )] 46 | pub unstake: Account<'info, Unstake>, 47 | 48 | #[account(address = worker.pubkey())] 49 | pub worker: Account<'info, Worker>, 50 | } 51 | 52 | pub fn handler(ctx: Context, amount: u64) -> Result<()> { 53 | // Get accounts. 54 | let authority = &ctx.accounts.authority; 55 | let delegation = &ctx.accounts.delegation; 56 | let registry = &mut ctx.accounts.registry; 57 | let unstake = &mut ctx.accounts.unstake; 58 | let worker = &ctx.accounts.worker; 59 | 60 | // Validate the request is valid. 61 | require!(amount.le(&delegation.stake_amount), ClockworkError::InvalidUnstakeAmount); 62 | 63 | // Initialize the unstake account. 64 | unstake.init(amount, authority.key(), delegation.key(), registry.total_unstakes, worker.key())?; 65 | 66 | // Increment the registry's unstake counter. 67 | registry.total_unstakes = registry.total_unstakes.checked_add(1).unwrap(); 68 | 69 | Ok(()) 70 | } 71 | -------------------------------------------------------------------------------- /programs/network/src/instructions/worker_claim.rs: -------------------------------------------------------------------------------- 1 | use {crate::state::*, anchor_lang::prelude::*}; 2 | 3 | #[derive(Accounts)] 4 | #[instruction(amount: u64)] 5 | pub struct WorkerClaim<'info> { 6 | #[account()] 7 | pub authority: Signer<'info>, 8 | 9 | #[account(mut)] 10 | pub pay_to: SystemAccount<'info>, 11 | 12 | #[account( 13 | mut, 14 | seeds = [ 15 | SEED_WORKER, 16 | worker.id.to_be_bytes().as_ref() 17 | ], 18 | bump, 19 | has_one = authority 20 | )] 21 | pub worker: Account<'info, Worker>, 22 | } 23 | 24 | pub fn handler(ctx: Context, amount: u64) -> Result<()> { 25 | // Get accounts 26 | let pay_to = &mut ctx.accounts.pay_to; 27 | let worker = &mut ctx.accounts.worker; 28 | 29 | // Decrement the worker's commission balance. 30 | worker.commission_balance = worker.commission_balance.checked_sub(amount).unwrap(); 31 | 32 | // Transfer commission to the worker. 33 | **worker.to_account_info().try_borrow_mut_lamports()? = worker 34 | .to_account_info() 35 | .lamports() 36 | .checked_sub(amount) 37 | .unwrap(); 38 | **pay_to.to_account_info().try_borrow_mut_lamports()? = pay_to 39 | .to_account_info() 40 | .lamports() 41 | .checked_add(amount) 42 | .unwrap(); 43 | 44 | Ok(()) 45 | } 46 | -------------------------------------------------------------------------------- /programs/network/src/instructions/worker_create.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{errors::*, state::*}, 3 | anchor_lang::{ 4 | prelude::*, 5 | solana_program::{system_program, sysvar}, 6 | }, 7 | anchor_spl::{ 8 | associated_token::AssociatedToken, 9 | token::{Mint, Token, TokenAccount}, 10 | }, 11 | std::mem::size_of, 12 | }; 13 | 14 | 15 | #[derive(Accounts)] 16 | pub struct WorkerCreate<'info> { 17 | #[account(address = anchor_spl::associated_token::ID)] 18 | pub associated_token_program: Program<'info, AssociatedToken>, 19 | 20 | #[account(mut)] 21 | pub authority: Signer<'info>, 22 | 23 | #[account(address = Config::pubkey())] 24 | pub config: Box>, 25 | 26 | #[account( 27 | init, 28 | seeds = [ 29 | SEED_FEE, 30 | worker.key().as_ref(), 31 | ], 32 | bump, 33 | payer = authority, 34 | space = 8 + size_of::(), 35 | )] 36 | pub fee: Box>, 37 | 38 | #[account( 39 | init, 40 | seeds = [ 41 | SEED_PENALTY, 42 | worker.key().as_ref(), 43 | ], 44 | bump, 45 | payer = authority, 46 | space = 8 + size_of::(), 47 | )] 48 | pub penalty: Box>, 49 | 50 | #[account(address = config.mint)] 51 | pub mint: Box>, 52 | 53 | #[account( 54 | mut, 55 | seeds = [SEED_REGISTRY], 56 | bump, 57 | constraint = !registry.locked @ ClockworkError::RegistryLocked 58 | )] 59 | pub registry: Box>, 60 | 61 | #[account(address = sysvar::rent::ID)] 62 | pub rent: Sysvar<'info, Rent>, 63 | 64 | #[account(constraint = signatory.key().ne(&authority.key()) @ ClockworkError::InvalidSignatory)] 65 | pub signatory: Signer<'info>, 66 | 67 | #[account(address = system_program::ID)] 68 | pub system_program: Program<'info, System>, 69 | 70 | #[account(address = anchor_spl::token::ID)] 71 | pub token_program: Program<'info, Token>, 72 | 73 | #[account( 74 | init, 75 | seeds = [ 76 | SEED_WORKER, 77 | registry.total_workers.to_be_bytes().as_ref(), 78 | ], 79 | bump, 80 | payer = authority, 81 | space = 8 + size_of::(), 82 | )] 83 | pub worker: Box>, 84 | 85 | #[account( 86 | init, 87 | payer = authority, 88 | associated_token::authority = worker, 89 | associated_token::mint = mint, 90 | )] 91 | pub worker_tokens: Box>, 92 | 93 | } 94 | 95 | pub fn handler(ctx: Context) -> Result<()> { 96 | // Get accounts 97 | let authority = &mut ctx.accounts.authority; 98 | let fee = &mut ctx.accounts.fee; 99 | let penalty = &mut ctx.accounts.penalty; 100 | let registry = &mut ctx.accounts.registry; 101 | let signatory = &mut ctx.accounts.signatory; 102 | let worker = &mut ctx.accounts.worker; 103 | 104 | // Initialize the worker accounts. 105 | worker.init(authority, registry.total_workers, signatory)?; 106 | fee.init(worker.key())?; 107 | penalty.init(worker.key())?; 108 | 109 | // Update the registry's worker counter. 110 | registry.total_workers = registry.total_workers.checked_add(1).unwrap(); 111 | 112 | Ok(()) 113 | } 114 | -------------------------------------------------------------------------------- /programs/network/src/instructions/worker_update.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::state::*, 3 | anchor_lang::{ 4 | prelude::*, 5 | solana_program::system_program, 6 | system_program::{transfer, Transfer}, 7 | }, 8 | }; 9 | 10 | #[derive(Accounts)] 11 | #[instruction(settings: WorkerSettings)] 12 | pub struct WorkerUpdate<'info> { 13 | #[account(mut)] 14 | pub authority: Signer<'info>, 15 | 16 | #[account(address = system_program::ID)] 17 | pub system_program: Program<'info, System>, 18 | 19 | #[account( 20 | mut, 21 | seeds = [ 22 | SEED_WORKER, 23 | worker.id.to_be_bytes().as_ref(), 24 | ], 25 | bump, 26 | has_one = authority, 27 | )] 28 | pub worker: Account<'info, Worker>, 29 | } 30 | 31 | pub fn handler(ctx: Context, settings: WorkerSettings) -> Result<()> { 32 | // Get accounts 33 | let authority = &ctx.accounts.authority; 34 | let worker = &mut ctx.accounts.worker; 35 | let system_program = &ctx.accounts.system_program; 36 | 37 | // Update the worker 38 | worker.update(settings)?; 39 | 40 | // Realloc memory for the worker account 41 | let data_len = 8 + worker.try_to_vec()?.len(); 42 | worker.to_account_info().realloc(data_len, false)?; 43 | 44 | // If lamports are required to maintain rent-exemption, pay them 45 | let minimum_rent = Rent::get().unwrap().minimum_balance(data_len); 46 | if minimum_rent > worker.to_account_info().lamports() { 47 | transfer( 48 | CpiContext::new( 49 | system_program.to_account_info(), 50 | Transfer { 51 | from: authority.to_account_info(), 52 | to: worker.to_account_info(), 53 | }, 54 | ), 55 | minimum_rent 56 | .checked_sub(worker.to_account_info().lamports()) 57 | .unwrap(), 58 | )?; 59 | } 60 | 61 | Ok(()) 62 | } 63 | -------------------------------------------------------------------------------- /programs/network/src/jobs/delete_snapshot/job.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::{prelude::*, solana_program::instruction::Instruction, InstructionData}; 2 | use clockwork_utils::thread::ThreadResponse; 3 | 4 | use crate::state::*; 5 | 6 | #[derive(Accounts)] 7 | pub struct DeleteSnapshotJob<'info> { 8 | #[account(address = Config::pubkey())] 9 | pub config: Account<'info, Config>, 10 | 11 | #[account( 12 | address = Registry::pubkey(), 13 | constraint = !registry.locked 14 | )] 15 | pub registry: Account<'info, Registry>, 16 | 17 | #[account(address = config.epoch_thread)] 18 | pub thread: Signer<'info>, 19 | } 20 | 21 | pub fn handler(ctx: Context) -> Result { 22 | let config = &ctx.accounts.config; 23 | let registry = &ctx.accounts.registry; 24 | let thread = &mut ctx.accounts.thread; 25 | 26 | Ok(ThreadResponse { 27 | dynamic_instruction: Some( 28 | Instruction { 29 | program_id: crate::ID, 30 | accounts: crate::accounts::DeleteSnapshotProcessSnapshot { 31 | config: config.key(), 32 | registry: registry.key(), 33 | snapshot: Snapshot::pubkey(registry.current_epoch.checked_sub(1).unwrap()), 34 | thread: thread.key(), 35 | } 36 | .to_account_metas(Some(true)), 37 | data: crate::instruction::DeleteSnapshotProcessSnapshot {}.data(), 38 | } 39 | .into(), 40 | ), 41 | close_to: None, 42 | trigger: None, 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /programs/network/src/jobs/delete_snapshot/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod job; 2 | pub mod process_entry; 3 | pub mod process_frame; 4 | pub mod process_snapshot; 5 | 6 | pub use job::*; 7 | pub use process_entry::*; 8 | pub use process_frame::*; 9 | pub use process_snapshot::*; 10 | -------------------------------------------------------------------------------- /programs/network/src/jobs/delete_snapshot/process_frame.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::{prelude::*, InstructionData, solana_program::instruction::Instruction}; 2 | use clockwork_utils::thread::ThreadResponse; 3 | 4 | use crate::state::*; 5 | 6 | #[derive(Accounts)] 7 | pub struct DeleteSnapshotProcessFrame<'info> { 8 | #[account(address = Config::pubkey())] 9 | pub config: Account<'info, Config>, 10 | 11 | #[account( 12 | address = Registry::pubkey(), 13 | constraint = !registry.locked 14 | )] 15 | pub registry: Account<'info, Registry>, 16 | 17 | #[account( 18 | mut, 19 | seeds = [ 20 | SEED_SNAPSHOT, 21 | snapshot.id.to_be_bytes().as_ref(), 22 | ], 23 | bump, 24 | constraint = snapshot.id.lt(®istry.current_epoch) 25 | )] 26 | pub snapshot: Account<'info, Snapshot>, 27 | 28 | #[account( 29 | mut, 30 | seeds = [ 31 | SEED_SNAPSHOT_FRAME, 32 | snapshot_frame.snapshot.as_ref(), 33 | snapshot_frame.id.to_be_bytes().as_ref(), 34 | ], 35 | bump, 36 | has_one = snapshot, 37 | )] 38 | pub snapshot_frame: Account<'info, SnapshotFrame>, 39 | 40 | #[account( 41 | mut, 42 | address = config.epoch_thread 43 | )] 44 | pub thread: Signer<'info>, 45 | } 46 | 47 | pub fn handler(ctx: Context) -> Result { 48 | // Get accounts 49 | let config = &ctx.accounts.config; 50 | let registry = &ctx.accounts.registry; 51 | let snapshot = &mut ctx.accounts.snapshot; 52 | let snapshot_frame = &mut ctx.accounts.snapshot_frame; 53 | let thread = &mut ctx.accounts.thread; 54 | 55 | // If this frame has no entries, then close the frame account. 56 | if snapshot_frame.total_entries.eq(&0) { 57 | let snapshot_frame_lamports = snapshot_frame.to_account_info().lamports(); 58 | **snapshot_frame.to_account_info().lamports.borrow_mut() = 0; 59 | **thread.to_account_info().lamports.borrow_mut() = thread 60 | .to_account_info() 61 | .lamports() 62 | .checked_add(snapshot_frame_lamports) 63 | .unwrap(); 64 | 65 | 66 | // If this is also the last frame in the snapshot, then close the snapshot account. 67 | if snapshot_frame.id.checked_add(1).unwrap().eq(&snapshot.total_frames) { 68 | let snapshot_lamports = snapshot.to_account_info().lamports(); 69 | **snapshot.to_account_info().lamports.borrow_mut() = 0; 70 | **thread.to_account_info().lamports.borrow_mut() = thread 71 | .to_account_info() 72 | .lamports() 73 | .checked_add(snapshot_lamports) 74 | .unwrap(); 75 | } 76 | } 77 | 78 | // Build the next instruction. 79 | let dynamic_instruction = if snapshot_frame.total_entries.gt(&0) { 80 | // This frame has entries. Delete the entries. 81 | Some( 82 | Instruction { 83 | program_id: crate::ID, 84 | accounts: crate::accounts::DeleteSnapshotProcessEntry { 85 | config: config.key(), 86 | registry: registry.key(), 87 | snapshot: snapshot.key(), 88 | snapshot_entry: SnapshotEntry::pubkey(snapshot_frame.key(), 0), 89 | snapshot_frame: snapshot_frame.key(), 90 | thread: thread.key(), 91 | }.to_account_metas(Some(true)), 92 | data: crate::instruction::DeleteSnapshotProcessEntry{}.data() 93 | }.into() 94 | ) 95 | } else if snapshot_frame.id.checked_add(1).unwrap().lt(&snapshot.total_frames) { 96 | // There are no more entries in this frame. Move on to the next frame. 97 | Some( 98 | Instruction { 99 | program_id: crate::ID, 100 | accounts: crate::accounts::DeleteSnapshotProcessFrame { 101 | config: config.key(), 102 | registry: registry.key(), 103 | snapshot: snapshot.key(), 104 | snapshot_frame: SnapshotFrame::pubkey(snapshot.key(), snapshot_frame.id.checked_add(1).unwrap()), 105 | thread: thread.key(), 106 | }.to_account_metas(Some(true)), 107 | data: crate::instruction::DeleteSnapshotProcessFrame {}.data() 108 | }.into() 109 | ) 110 | } else { 111 | // This frame has no entries, and it was the last frame. We are done! 112 | None 113 | }; 114 | 115 | Ok( ThreadResponse { dynamic_instruction, ..ThreadResponse::default() } ) 116 | } 117 | -------------------------------------------------------------------------------- /programs/network/src/jobs/delete_snapshot/process_snapshot.rs: -------------------------------------------------------------------------------- 1 | use clockwork_utils::thread::ThreadResponse; 2 | use anchor_lang::{prelude::*, InstructionData, solana_program::instruction::Instruction}; 3 | 4 | use crate::state::*; 5 | 6 | #[derive(Accounts)] 7 | pub struct DeleteSnapshotProcessSnapshot<'info> { 8 | #[account(address = Config::pubkey())] 9 | pub config: Account<'info, Config>, 10 | 11 | #[account( 12 | address = Registry::pubkey(), 13 | constraint = !registry.locked 14 | )] 15 | pub registry: Account<'info, Registry>, 16 | 17 | #[account( 18 | mut, 19 | seeds = [ 20 | SEED_SNAPSHOT, 21 | snapshot.id.to_be_bytes().as_ref(), 22 | ], 23 | bump, 24 | constraint = snapshot.id.lt(®istry.current_epoch) 25 | )] 26 | pub snapshot: Account<'info, Snapshot>, 27 | 28 | #[account( 29 | mut, 30 | address = config.epoch_thread 31 | )] 32 | pub thread: Signer<'info>, 33 | } 34 | 35 | pub fn handler(ctx: Context) -> Result { 36 | // Get accounts 37 | let config = &ctx.accounts.config; 38 | let registry = &ctx.accounts.registry; 39 | let snapshot = &mut ctx.accounts.snapshot; 40 | let thread = &mut ctx.accounts.thread; 41 | 42 | // If this snapshot has no entries, then close immediately 43 | if snapshot.total_frames.eq(&0) { 44 | let snapshot_lamports = snapshot.to_account_info().lamports(); 45 | **snapshot.to_account_info().lamports.borrow_mut() = 0; 46 | **thread.to_account_info().lamports.borrow_mut() = thread 47 | .to_account_info() 48 | .lamports() 49 | .checked_add(snapshot_lamports) 50 | .unwrap(); 51 | } 52 | 53 | // Build next instruction the thread. 54 | let dynamic_instruction = if snapshot.total_frames.gt(&0) { 55 | // There are frames in this snapshot. Delete them. 56 | Some( 57 | Instruction { 58 | program_id: crate::ID, 59 | accounts: crate::accounts::DeleteSnapshotProcessFrame { 60 | config: config.key(), 61 | registry: registry.key(), 62 | snapshot: snapshot.key(), 63 | snapshot_frame: SnapshotFrame::pubkey(snapshot.key(), 0), 64 | thread: thread.key(), 65 | }.to_account_metas(Some(true)), 66 | data: crate::instruction::DeleteSnapshotProcessFrame{}.data() 67 | }.into() 68 | ) 69 | } else { 70 | // This snaphot has no frames. We are done! 71 | None 72 | }; 73 | 74 | Ok(ThreadResponse { dynamic_instruction, close_to:None, trigger: None }) 75 | } 76 | -------------------------------------------------------------------------------- /programs/network/src/jobs/distribute_fees/job.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::{prelude::*, solana_program::instruction::Instruction, InstructionData}; 2 | use clockwork_utils::thread::ThreadResponse; 3 | 4 | use crate::state::*; 5 | 6 | #[derive(Accounts)] 7 | pub struct DistributeFeesJob<'info> { 8 | #[account(address = Config::pubkey())] 9 | pub config: Account<'info, Config>, 10 | 11 | #[account( 12 | mut, 13 | seeds = [SEED_REGISTRY], 14 | bump, 15 | )] 16 | pub registry: Account<'info, Registry>, 17 | 18 | #[account(address = config.epoch_thread)] 19 | pub thread: Signer<'info>, 20 | } 21 | 22 | pub fn handler(ctx: Context) -> Result { 23 | // Get accounts. 24 | let config = &ctx.accounts.config; 25 | let registry = &mut ctx.accounts.registry; 26 | let thread = &ctx.accounts.thread; 27 | 28 | // Lock the registry. 29 | registry.locked = true; 30 | 31 | // Process the snapshot. 32 | Ok(ThreadResponse { 33 | dynamic_instruction: Some( 34 | Instruction { 35 | program_id: crate::ID, 36 | accounts: crate::accounts::DistributeFeesProcessSnapshot { 37 | config: config.key(), 38 | registry: registry.key(), 39 | snapshot: Snapshot::pubkey(registry.current_epoch), 40 | thread: thread.key(), 41 | } 42 | .to_account_metas(Some(true)), 43 | data: crate::instruction::DistributeFeesProcessSnapshot {}.data(), 44 | } 45 | .into(), 46 | ), 47 | close_to: None, 48 | trigger: None, 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /programs/network/src/jobs/distribute_fees/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod job; 2 | pub mod process_entry; 3 | pub mod process_frame; 4 | pub mod process_snapshot; 5 | 6 | pub use job::*; 7 | pub use process_entry::*; 8 | pub use process_frame::*; 9 | pub use process_snapshot::*; 10 | -------------------------------------------------------------------------------- /programs/network/src/jobs/distribute_fees/process_snapshot.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::{prelude::*, solana_program::instruction::Instruction, InstructionData}; 2 | use clockwork_utils::thread::ThreadResponse; 3 | 4 | use crate::state::*; 5 | 6 | // DONE Payout yield. 7 | // Transfer lamports collected by Fee accounts to Delegation accounts based on the stake balance distributions of the current Epoch's SnapshotEntries. 8 | 9 | // DONE Process unstake requests. 10 | // For each "unstake request" transfer tokens from the Worker stake account to the Delegation authority's token account. 11 | // Decrement the Delegation's stake balance by the amount unstaked. 12 | 13 | // DONE Lock delegated stakes. 14 | // Transfer tokens from the Delegation's stake account to the Worker's stake account. 15 | // Increment the Delegation's stake balance by the amount moved. 16 | 17 | // DONE Take a snapshot. 18 | // Capture a snapshot (cumulative sum) of the total stake and broken-down delegation balances. 19 | // SnapshotFrames capture worker-level aggregate stake balances. 20 | // SnapshotEntries capture delegation-level individual stake balances. 21 | 22 | // DONE Cutover from current epoch to new epoch. 23 | 24 | #[derive(Accounts)] 25 | pub struct DistributeFeesProcessSnapshot<'info> { 26 | #[account(address = Config::pubkey())] 27 | pub config: Account<'info, Config>, 28 | 29 | #[account(seeds = [SEED_REGISTRY], bump)] 30 | pub registry: Account<'info, Registry>, 31 | 32 | #[account( 33 | address = snapshot.pubkey(), 34 | constraint = snapshot.id.eq(®istry.current_epoch) 35 | )] 36 | pub snapshot: Account<'info, Snapshot>, 37 | 38 | #[account(address = config.epoch_thread)] 39 | pub thread: Signer<'info>, 40 | } 41 | 42 | pub fn handler(ctx: Context) -> Result { 43 | let config = &ctx.accounts.config; 44 | let registry = &mut ctx.accounts.registry; 45 | let snapshot = &ctx.accounts.snapshot; 46 | let thread = &ctx.accounts.thread; 47 | 48 | Ok(ThreadResponse { 49 | dynamic_instruction: if snapshot.total_frames.gt(&0) { 50 | Some( 51 | Instruction { 52 | program_id: crate::ID, 53 | accounts: crate::accounts::DistributeFeesProcessFrame { 54 | config: config.key(), 55 | fee: Fee::pubkey(Worker::pubkey(0)), 56 | registry: registry.key(), 57 | snapshot: snapshot.key(), 58 | snapshot_frame: SnapshotFrame::pubkey(snapshot.key(), 0), 59 | thread: thread.key(), 60 | worker: Worker::pubkey(0), 61 | } 62 | .to_account_metas(Some(true)), 63 | data: crate::instruction::DistributeFeesProcessFrame {}.data(), 64 | } 65 | .into(), 66 | ) 67 | } else { 68 | None 69 | }, 70 | close_to: None, 71 | trigger: None, 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /programs/network/src/jobs/increment_epoch/job.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use clockwork_utils::thread::ThreadResponse; 3 | 4 | use crate::state::*; 5 | 6 | #[derive(Accounts)] 7 | pub struct EpochCutover<'info> { 8 | #[account(address = Config::pubkey())] 9 | pub config: Account<'info, Config>, 10 | 11 | #[account( 12 | mut, 13 | seeds = [SEED_REGISTRY], 14 | bump, 15 | )] 16 | pub registry: Account<'info, Registry>, 17 | 18 | #[account(address = config.epoch_thread)] 19 | pub thread: Signer<'info>, 20 | } 21 | 22 | pub fn handler(ctx: Context) -> Result { 23 | let registry = &mut ctx.accounts.registry; 24 | registry.current_epoch = registry.current_epoch.checked_add(1).unwrap(); 25 | registry.locked = false; 26 | 27 | Ok(ThreadResponse { 28 | close_to: None, 29 | dynamic_instruction: None, 30 | trigger: None, 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /programs/network/src/jobs/increment_epoch/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod job; 2 | 3 | pub use job::*; 4 | -------------------------------------------------------------------------------- /programs/network/src/jobs/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod delete_snapshot; 2 | pub mod distribute_fees; 3 | pub mod increment_epoch; 4 | pub mod process_unstakes; 5 | pub mod stake_delegations; 6 | pub mod take_snapshot; 7 | 8 | pub use delete_snapshot::*; 9 | pub use distribute_fees::*; 10 | pub use increment_epoch::*; 11 | pub use process_unstakes::*; 12 | pub use stake_delegations::*; 13 | pub use take_snapshot::*; 14 | -------------------------------------------------------------------------------- /programs/network/src/jobs/process_unstakes/job.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::{prelude::*, solana_program::instruction::Instruction, InstructionData}; 2 | use clockwork_utils::thread::ThreadResponse; 3 | 4 | use crate::state::*; 5 | 6 | #[derive(Accounts)] 7 | pub struct ProcessUnstakesJob<'info> { 8 | #[account(address = Config::pubkey())] 9 | pub config: Account<'info, Config>, 10 | 11 | #[account( 12 | address = Registry::pubkey(), 13 | constraint = registry.locked 14 | )] 15 | pub registry: Account<'info, Registry>, 16 | 17 | #[account(address = config.epoch_thread)] 18 | pub thread: Signer<'info>, 19 | } 20 | 21 | pub fn handler(ctx: Context) -> Result { 22 | // Get accounts. 23 | let config = &ctx.accounts.config; 24 | let registry = &ctx.accounts.registry; 25 | let thread = &ctx.accounts.thread; 26 | 27 | // Return next instruction for thread. 28 | Ok(ThreadResponse { 29 | dynamic_instruction: if registry.total_unstakes.gt(&0) { 30 | Some( 31 | Instruction { 32 | program_id: crate::ID, 33 | accounts: crate::accounts::UnstakePreprocess { 34 | config: config.key(), 35 | registry: registry.key(), 36 | thread: thread.key(), 37 | unstake: Unstake::pubkey(0), 38 | } 39 | .to_account_metas(Some(true)), 40 | data: crate::instruction::UnstakePreprocess {}.data(), 41 | } 42 | .into(), 43 | ) 44 | } else { 45 | None 46 | }, 47 | close_to: None, 48 | trigger: None, 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /programs/network/src/jobs/process_unstakes/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod job; 2 | pub mod unstake_preprocess; 3 | pub mod unstake_process; 4 | 5 | pub use job::*; 6 | pub use unstake_preprocess::*; 7 | pub use unstake_process::*; 8 | -------------------------------------------------------------------------------- /programs/network/src/jobs/process_unstakes/unstake_preprocess.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::{prelude::*, solana_program::instruction::Instruction, InstructionData}; 2 | use anchor_spl::associated_token::get_associated_token_address; 3 | use clockwork_utils::thread::ThreadResponse; 4 | 5 | use crate::state::*; 6 | 7 | #[derive(Accounts)] 8 | pub struct UnstakePreprocess<'info> { 9 | #[account(address = Config::pubkey())] 10 | pub config: Account<'info, Config>, 11 | 12 | #[account( 13 | address = Registry::pubkey(), 14 | constraint = registry.locked 15 | )] 16 | pub registry: Account<'info, Registry>, 17 | 18 | #[account(address = config.epoch_thread)] 19 | pub thread: Signer<'info>, 20 | 21 | #[account(address = unstake.pubkey())] 22 | pub unstake: Account<'info, Unstake>, 23 | } 24 | 25 | pub fn handler(ctx: Context) -> Result { 26 | // Get accounts. 27 | let config = &ctx.accounts.config; 28 | let registry = &ctx.accounts.registry; 29 | let thread = &ctx.accounts.thread; 30 | let unstake = &ctx.accounts.unstake; 31 | 32 | // Return next instruction for thread. 33 | Ok(ThreadResponse { 34 | dynamic_instruction: Some( 35 | Instruction { 36 | program_id: crate::ID, 37 | accounts: crate::accounts::UnstakeProcess { 38 | authority: unstake.authority, 39 | authority_tokens: get_associated_token_address(&unstake.authority, &config.mint), 40 | config: config.key(), 41 | delegation: unstake.delegation, 42 | registry: registry.key(), 43 | thread: thread.key(), 44 | token_program: anchor_spl::token::ID, 45 | unstake: unstake.key(), 46 | worker: unstake.worker, 47 | worker_tokens: get_associated_token_address(&unstake.worker, &config.mint), 48 | } 49 | .to_account_metas(Some(true)), 50 | data: crate::instruction::UnstakeProcess {}.data(), 51 | } 52 | .into(), 53 | ), 54 | ..ThreadResponse::default() 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /programs/network/src/jobs/stake_delegations/job.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::{prelude::*, solana_program::instruction::Instruction, InstructionData}; 2 | use clockwork_utils::thread::ThreadResponse; 3 | 4 | use crate::state::*; 5 | 6 | #[derive(Accounts)] 7 | pub struct StakeDelegationsJob<'info> { 8 | #[account(address = Config::pubkey())] 9 | pub config: Account<'info, Config>, 10 | 11 | #[account( 12 | address = Registry::pubkey(), 13 | constraint = registry.locked 14 | )] 15 | pub registry: Account<'info, Registry>, 16 | 17 | #[account(address = config.epoch_thread)] 18 | pub thread: Signer<'info>, 19 | } 20 | 21 | pub fn handler(ctx: Context) -> Result { 22 | let config = &ctx.accounts.config; 23 | let registry = &ctx.accounts.registry; 24 | let thread = &ctx.accounts.thread; 25 | 26 | Ok(ThreadResponse { 27 | dynamic_instruction: if registry.total_workers.gt(&0) { 28 | Some( 29 | Instruction { 30 | program_id: crate::ID, 31 | accounts: crate::accounts::StakeDelegationsProcessWorker { 32 | config: config.key(), 33 | registry: registry.key(), 34 | thread: thread.key(), 35 | worker: Worker::pubkey(0), 36 | } 37 | .to_account_metas(Some(true)), 38 | data: crate::instruction::StakeDelegationsProcessWorker {}.data(), 39 | } 40 | .into(), 41 | ) 42 | } else { 43 | None 44 | }, 45 | close_to: None, 46 | trigger: None, 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /programs/network/src/jobs/stake_delegations/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod job; 2 | pub mod process_delegation; 3 | pub mod process_worker; 4 | 5 | pub use job::*; 6 | pub use process_delegation::*; 7 | pub use process_worker::*; 8 | -------------------------------------------------------------------------------- /programs/network/src/jobs/stake_delegations/process_worker.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::{prelude::*, solana_program::instruction::Instruction, InstructionData}; 2 | use anchor_spl::associated_token::get_associated_token_address; 3 | use clockwork_utils::thread::ThreadResponse; 4 | 5 | use crate::state::*; 6 | 7 | #[derive(Accounts)] 8 | pub struct StakeDelegationsProcessWorker<'info> { 9 | #[account(address = Config::pubkey())] 10 | pub config: Account<'info, Config>, 11 | 12 | #[account( 13 | address = Registry::pubkey(), 14 | constraint = registry.locked 15 | )] 16 | pub registry: Account<'info, Registry>, 17 | 18 | #[account(address = config.epoch_thread)] 19 | pub thread: Signer<'info>, 20 | 21 | #[account(address = worker.pubkey())] 22 | pub worker: Account<'info, Worker>, 23 | } 24 | 25 | pub fn handler(ctx: Context) -> Result { 26 | // Get accounts. 27 | let config = &ctx.accounts.config; 28 | let registry = &ctx.accounts.registry; 29 | let thread = &ctx.accounts.thread; 30 | let worker = &ctx.accounts.worker; 31 | 32 | // Build the next instruction for the thread. 33 | let dynamic_instruction = if worker.total_delegations.gt(&0) { 34 | // This worker has delegations. Stake their deposits. 35 | let delegation_pubkey = Delegation::pubkey(worker.key(), 0); 36 | Some( 37 | Instruction { 38 | program_id: crate::ID, 39 | accounts: crate::accounts::StakeDelegationsProcessDelegation { 40 | config: config.key(), 41 | delegation: delegation_pubkey, 42 | delegation_stake: get_associated_token_address( 43 | &delegation_pubkey, 44 | &config.mint, 45 | ), 46 | registry: registry.key(), 47 | thread: thread.key(), 48 | token_program: anchor_spl::token::ID, 49 | worker: worker.key(), 50 | worker_stake: get_associated_token_address(&worker.key(), &config.mint), 51 | } 52 | .to_account_metas(Some(true)), 53 | data: crate::instruction::StakeDelegationsProcessDelegation {}.data(), 54 | } 55 | .into(), 56 | ) 57 | } else if worker 58 | .id 59 | .checked_add(1) 60 | .unwrap() 61 | .lt(®istry.total_workers) 62 | { 63 | // This worker has no delegations. Move on to the next worker. 64 | Some( 65 | Instruction { 66 | program_id: crate::ID, 67 | accounts: crate::accounts::StakeDelegationsProcessWorker { 68 | config: config.key(), 69 | registry: registry.key(), 70 | thread: thread.key(), 71 | worker: Worker::pubkey(worker.id.checked_add(1).unwrap()), 72 | } 73 | .to_account_metas(Some(true)), 74 | data: crate::instruction::StakeDelegationsProcessWorker {}.data(), 75 | } 76 | .into(), 77 | ) 78 | } else { 79 | None 80 | }; 81 | 82 | Ok(ThreadResponse { 83 | dynamic_instruction, 84 | close_to: None, 85 | trigger: None, 86 | }) 87 | } 88 | -------------------------------------------------------------------------------- /programs/network/src/jobs/take_snapshot/create_snapshot.rs: -------------------------------------------------------------------------------- 1 | use std::mem::size_of; 2 | 3 | use anchor_lang::{ 4 | prelude::*, 5 | solana_program::{instruction::Instruction, system_program}, 6 | InstructionData, 7 | }; 8 | use anchor_spl::associated_token::get_associated_token_address; 9 | use clockwork_utils::thread::{ThreadResponse, PAYER_PUBKEY}; 10 | 11 | use crate::state::*; 12 | 13 | #[derive(Accounts)] 14 | pub struct TakeSnapshotCreateSnapshot<'info> { 15 | #[account(address = Config::pubkey())] 16 | pub config: Account<'info, Config>, 17 | 18 | #[account(mut)] 19 | pub payer: Signer<'info>, 20 | 21 | #[account( 22 | address = Registry::pubkey(), 23 | constraint = registry.locked 24 | )] 25 | pub registry: Account<'info, Registry>, 26 | 27 | #[account( 28 | init, 29 | seeds = [ 30 | SEED_SNAPSHOT, 31 | registry.current_epoch.checked_add(1).unwrap().to_be_bytes().as_ref(), 32 | ], 33 | bump, 34 | space = 8 + size_of::(), 35 | payer = payer 36 | )] 37 | pub snapshot: Account<'info, Snapshot>, 38 | 39 | #[account(address = system_program::ID)] 40 | pub system_program: Program<'info, System>, 41 | 42 | #[account(address = config.epoch_thread)] 43 | pub thread: Signer<'info>, 44 | } 45 | 46 | pub fn handler(ctx: Context) -> Result { 47 | // Get accounts 48 | let config = &ctx.accounts.config; 49 | let registry = &ctx.accounts.registry; 50 | let snapshot = &mut ctx.accounts.snapshot; 51 | let system_program = &ctx.accounts.system_program; 52 | let thread = &ctx.accounts.thread; 53 | 54 | // Start a new snapshot. 55 | snapshot.init(registry.current_epoch.checked_add(1).unwrap())?; 56 | 57 | Ok(ThreadResponse { 58 | dynamic_instruction: if registry.total_workers.gt(&0) { 59 | // The registry has workers. Create a snapshot frame for the zeroth worker. 60 | let snapshot_frame_pubkey = SnapshotFrame::pubkey(snapshot.key(), 0); 61 | let worker_pubkey = Worker::pubkey(0); 62 | Some( 63 | Instruction { 64 | program_id: crate::ID, 65 | accounts: crate::accounts::TakeSnapshotCreateFrame { 66 | config: config.key(), 67 | payer: PAYER_PUBKEY, 68 | registry: registry.key(), 69 | snapshot: snapshot.key(), 70 | snapshot_frame: snapshot_frame_pubkey, 71 | system_program: system_program.key(), 72 | thread: thread.key(), 73 | worker: worker_pubkey, 74 | worker_stake: get_associated_token_address(&worker_pubkey, &config.mint), 75 | } 76 | .to_account_metas(Some(true)), 77 | data: crate::instruction::TakeSnapshotCreateFrame {}.data(), 78 | } 79 | .into(), 80 | ) 81 | } else { 82 | None 83 | }, 84 | close_to: None, 85 | trigger: None, 86 | }) 87 | } 88 | -------------------------------------------------------------------------------- /programs/network/src/jobs/take_snapshot/job.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::{ 2 | prelude::*, 3 | solana_program::{instruction::Instruction, system_program}, 4 | InstructionData, 5 | }; 6 | use clockwork_utils::thread::{ThreadResponse, PAYER_PUBKEY}; 7 | 8 | use crate::state::*; 9 | 10 | #[derive(Accounts)] 11 | pub struct TakeSnapshotJob<'info> { 12 | #[account(address = Config::pubkey())] 13 | pub config: Account<'info, Config>, 14 | 15 | #[account( 16 | address = Registry::pubkey(), 17 | constraint = registry.locked 18 | )] 19 | pub registry: Account<'info, Registry>, 20 | 21 | #[account(address = config.epoch_thread)] 22 | pub thread: Signer<'info>, 23 | } 24 | 25 | pub fn handler(ctx: Context) -> Result { 26 | // Get accounts 27 | let config = &ctx.accounts.config; 28 | let registry = &ctx.accounts.registry; 29 | let thread = &ctx.accounts.thread; 30 | 31 | Ok(ThreadResponse { 32 | dynamic_instruction: Some( 33 | Instruction { 34 | program_id: crate::ID, 35 | accounts: crate::accounts::TakeSnapshotCreateSnapshot { 36 | config: config.key(), 37 | payer: PAYER_PUBKEY, 38 | registry: registry.key(), 39 | snapshot: Snapshot::pubkey(registry.current_epoch.checked_add(1).unwrap()), 40 | system_program: system_program::ID, 41 | thread: thread.key(), 42 | } 43 | .to_account_metas(Some(true)), 44 | data: crate::instruction::TakeSnapshotCreateSnapshot {}.data(), 45 | } 46 | .into(), 47 | ), 48 | close_to: None, 49 | trigger: None, 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /programs/network/src/jobs/take_snapshot/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod create_entry; 2 | pub mod create_frame; 3 | pub mod create_snapshot; 4 | pub mod job; 5 | 6 | pub use create_entry::*; 7 | pub use create_frame::*; 8 | pub use create_snapshot::*; 9 | pub use job::*; 10 | -------------------------------------------------------------------------------- /programs/network/src/state/config.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::{prelude::*, AnchorDeserialize}; 2 | 3 | pub const SEED_CONFIG: &[u8] = b"config"; 4 | 5 | /** 6 | * Config 7 | */ 8 | 9 | #[account] 10 | #[derive(Debug)] 11 | pub struct Config { 12 | pub admin: Pubkey, 13 | pub epoch_thread: Pubkey, 14 | pub hasher_thread: Pubkey, 15 | pub mint: Pubkey, 16 | } 17 | 18 | impl Config { 19 | pub fn pubkey() -> Pubkey { 20 | Pubkey::find_program_address(&[SEED_CONFIG], &crate::ID).0 21 | } 22 | } 23 | 24 | /** 25 | * ConfigSettings 26 | */ 27 | 28 | #[derive(AnchorSerialize, AnchorDeserialize)] 29 | pub struct ConfigSettings { 30 | pub admin: Pubkey, 31 | pub epoch_thread: Pubkey, 32 | pub hasher_thread: Pubkey, 33 | pub mint: Pubkey, 34 | } 35 | 36 | /** 37 | * ConfigAccount 38 | */ 39 | 40 | pub trait ConfigAccount { 41 | fn init(&mut self, admin: Pubkey, mint: Pubkey) -> Result<()>; 42 | 43 | fn update(&mut self, settings: ConfigSettings) -> Result<()>; 44 | } 45 | 46 | impl ConfigAccount for Account<'_, Config> { 47 | fn init(&mut self, admin: Pubkey, mint: Pubkey) -> Result<()> { 48 | self.admin = admin; 49 | self.mint = mint; 50 | Ok(()) 51 | } 52 | 53 | fn update(&mut self, settings: ConfigSettings) -> Result<()> { 54 | self.admin = settings.admin; 55 | self.epoch_thread = settings.epoch_thread; 56 | self.hasher_thread = settings.hasher_thread; 57 | self.mint = settings.mint; 58 | Ok(()) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /programs/network/src/state/delegation.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::{prelude::*, AnchorDeserialize}; 2 | 3 | pub const SEED_DELEGATION: &[u8] = b"delegation"; 4 | 5 | /// An account to manage a token holder's stake delegation with a particiular a worker. 6 | #[account] 7 | #[derive(Debug)] 8 | pub struct Delegation { 9 | /// The authority of this delegation account. 10 | pub authority: Pubkey, 11 | 12 | /// The id of this delegation (auto-incrementing integer relative to worker) 13 | pub id: u64, 14 | 15 | /// The number of delegated tokens currently locked with the worker. 16 | pub stake_amount: u64, 17 | 18 | /// The worker to delegate stake to. 19 | pub worker: Pubkey, 20 | 21 | /// The number of lamports claimable as yield by the authority. 22 | pub yield_balance: u64, 23 | } 24 | 25 | impl Delegation { 26 | pub fn pubkey(worker: Pubkey, id: u64) -> Pubkey { 27 | Pubkey::find_program_address( 28 | &[SEED_DELEGATION, worker.as_ref(), id.to_be_bytes().as_ref()], 29 | &crate::ID, 30 | ) 31 | .0 32 | } 33 | } 34 | 35 | /// DelegationAccount 36 | pub trait DelegationAccount { 37 | fn pubkey(&self) -> Pubkey; 38 | 39 | fn init(&mut self, authority: Pubkey, id: u64, worker: Pubkey) -> Result<()>; 40 | } 41 | 42 | impl DelegationAccount for Account<'_, Delegation> { 43 | fn pubkey(&self) -> Pubkey { 44 | Delegation::pubkey(self.worker, self.id) 45 | } 46 | 47 | fn init(&mut self, authority: Pubkey, id: u64, worker: Pubkey) -> Result<()> { 48 | self.authority = authority; 49 | self.id = id; 50 | self.stake_amount = 0; 51 | self.worker = worker; 52 | self.yield_balance = 0; 53 | Ok(()) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /programs/network/src/state/epoch.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::{prelude::*, AnchorDeserialize}; 2 | 3 | use super::Snapshot; 4 | 5 | pub const SEED_EPOCH: &[u8] = b"epoch"; 6 | 7 | /** 8 | * Epoch 9 | */ 10 | 11 | #[account] 12 | #[derive(Debug)] 13 | pub struct Epoch { 14 | pub id: u64, 15 | pub snapshot: Pubkey, 16 | } 17 | 18 | impl Epoch { 19 | pub fn pubkey(id: u64) -> Pubkey { 20 | Pubkey::find_program_address(&[SEED_EPOCH, id.to_be_bytes().as_ref()], &crate::ID).0 21 | } 22 | } 23 | 24 | /** 25 | * EpochAccount 26 | */ 27 | 28 | pub trait EpochAccount { 29 | fn pubkey(&self) -> Pubkey; 30 | 31 | fn init(&mut self, id: u64) -> Result<()>; 32 | } 33 | 34 | impl EpochAccount for Account<'_, Epoch> { 35 | fn pubkey(&self) -> Pubkey { 36 | Epoch::pubkey(self.id) 37 | } 38 | 39 | fn init(&mut self, id: u64) -> Result<()> { 40 | self.id = id; 41 | self.snapshot = Snapshot::pubkey(self.pubkey()); 42 | Ok(()) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /programs/network/src/state/fee.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | pub const SEED_FEE: &[u8] = b"fee"; 4 | 5 | /// Escrows the lamport balance owed to a particular worker. 6 | #[account] 7 | #[derive(Debug)] 8 | pub struct Fee { 9 | /// The number of lamports that are distributable for this epoch period. 10 | pub distributable_balance: u64, 11 | /// The worker who received the fees. 12 | pub worker: Pubkey, 13 | } 14 | 15 | impl Fee { 16 | /// Derive the pubkey of a fee account. 17 | pub fn pubkey(worker: Pubkey) -> Pubkey { 18 | Pubkey::find_program_address(&[SEED_FEE, worker.as_ref()], &crate::ID).0 19 | } 20 | } 21 | 22 | /// Trait for reading and writing to a fee account. 23 | pub trait FeeAccount { 24 | /// Get the pubkey of the fee account. 25 | fn pubkey(&self) -> Pubkey; 26 | 27 | /// Initialize the account to hold fee object. 28 | fn init(&mut self, worker: Pubkey) -> Result<()>; 29 | } 30 | 31 | impl FeeAccount for Account<'_, Fee> { 32 | fn pubkey(&self) -> Pubkey { 33 | Fee::pubkey(self.worker) 34 | } 35 | 36 | fn init(&mut self, worker: Pubkey) -> Result<()> { 37 | self.distributable_balance = 0; 38 | self.worker = worker; 39 | Ok(()) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /programs/network/src/state/mod.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | mod delegation; 3 | mod fee; 4 | mod penalty; 5 | mod pool; 6 | mod registry; 7 | mod snapshot; 8 | mod snapshot_entry; 9 | mod snapshot_frame; 10 | mod unstake; 11 | mod worker; 12 | 13 | pub use config::*; 14 | pub use delegation::*; 15 | pub use fee::*; 16 | pub use penalty::*; 17 | pub use pool::*; 18 | pub use registry::*; 19 | pub use snapshot::*; 20 | pub use snapshot_entry::*; 21 | pub use snapshot_frame::*; 22 | pub use unstake::*; 23 | pub use worker::*; 24 | -------------------------------------------------------------------------------- /programs/network/src/state/penalty.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::{prelude::*, AnchorDeserialize}; 2 | 3 | pub const SEED_PENALTY: &[u8] = b"penalty"; 4 | 5 | /// Escrows the lamport balance owed to a particular worker. 6 | #[account] 7 | #[derive(Debug)] 8 | pub struct Penalty { 9 | /// The worker who was penalized. 10 | pub worker: Pubkey, 11 | } 12 | 13 | impl Penalty { 14 | /// Derive the pubkey of a fee account. 15 | pub fn pubkey(worker: Pubkey) -> Pubkey { 16 | Pubkey::find_program_address(&[SEED_PENALTY, worker.as_ref()], &crate::ID).0 17 | } 18 | } 19 | 20 | /// Trait for reading and writing to a penalty account. 21 | pub trait PenaltyAccount { 22 | /// Get the pubkey of the penalty account. 23 | fn pubkey(&self) -> Pubkey; 24 | 25 | /// Initialize the account to hold penalty object. 26 | fn init(&mut self, worker: Pubkey) -> Result<()>; 27 | } 28 | 29 | impl PenaltyAccount for Account<'_, Penalty> { 30 | fn pubkey(&self) -> Pubkey { 31 | Penalty::pubkey(self.worker) 32 | } 33 | 34 | fn init(&mut self, worker: Pubkey) -> Result<()> { 35 | self.worker = worker; 36 | Ok(()) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /programs/network/src/state/pool.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::{prelude::*, AnchorDeserialize}; 2 | 3 | pub const SEED_POOL: &[u8] = b"pool"; 4 | 5 | const DEFAULT_POOL_SIZE: u32 = 1; 6 | 7 | 8 | /** 9 | * Pool 10 | */ 11 | 12 | #[account] 13 | #[derive(Debug)] 14 | pub struct Pool { 15 | pub id: u64, 16 | pub size: u32, 17 | pub workers: Vec, 18 | } 19 | 20 | 21 | impl Pool { 22 | pub fn pubkey(id: u64) -> Pubkey { 23 | Pubkey::find_program_address(&[SEED_POOL, id.to_be_bytes().as_ref()], &crate::ID).0 24 | } 25 | } 26 | /** 27 | * PoolSettings 28 | */ 29 | 30 | #[derive(AnchorSerialize, AnchorDeserialize)] 31 | pub struct PoolSettings { 32 | pub size: u32, 33 | } 34 | 35 | /** 36 | * PoolAccount 37 | */ 38 | 39 | pub trait PoolAccount { 40 | fn pubkey(&self) -> Pubkey; 41 | 42 | fn init(&mut self, id: u64) -> Result<()>; 43 | 44 | fn rotate(&mut self, worker: Pubkey) -> Result<()>; 45 | 46 | fn update(&mut self, settings: &PoolSettings) -> Result<()>; 47 | } 48 | 49 | impl PoolAccount for Account<'_, Pool> { 50 | fn pubkey(&self) -> Pubkey { 51 | Pool::pubkey(self.id) 52 | } 53 | 54 | fn init(&mut self, id: u64) -> Result<()> { 55 | self.id = id; 56 | self.size = DEFAULT_POOL_SIZE; 57 | self.workers = Vec::new(); 58 | Ok(()) 59 | } 60 | 61 | fn rotate(&mut self, worker: Pubkey) -> Result<()> { 62 | // Push new worker into the pool. 63 | self.workers.push(worker); 64 | 65 | // Drain pool to the configured size limit. 66 | while self.workers.len() > self.size as usize { 67 | self.workers.remove(0); 68 | } 69 | 70 | Ok(()) 71 | } 72 | 73 | fn update(&mut self, settings: &PoolSettings) -> Result<()> { 74 | self.size = settings.size; 75 | 76 | // Drain pool to the configured size limit. 77 | while self.workers.len() > self.size as usize { 78 | self.workers.remove(0); 79 | } 80 | 81 | Ok(()) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /programs/network/src/state/registry.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::hash_map::DefaultHasher, 3 | hash::{Hash, Hasher}, 4 | }; 5 | 6 | use anchor_lang::{prelude::*, AnchorDeserialize}; 7 | 8 | pub const SEED_REGISTRY: &[u8] = b"registry"; 9 | 10 | /// Registry 11 | 12 | #[account] 13 | #[derive(Debug)] 14 | pub struct Registry { 15 | pub current_epoch: u64, 16 | pub locked: bool, 17 | pub nonce: u64, 18 | pub total_pools: u64, 19 | pub total_unstakes: u64, 20 | pub total_workers: u64, 21 | } 22 | 23 | impl Registry { 24 | pub fn pubkey() -> Pubkey { 25 | Pubkey::find_program_address(&[SEED_REGISTRY], &crate::ID).0 26 | } 27 | } 28 | 29 | /** 30 | * RegistryAccount 31 | */ 32 | 33 | pub trait RegistryAccount { 34 | fn init(&mut self) -> Result<()>; 35 | 36 | fn hash_nonce(&mut self) -> Result<()>; 37 | } 38 | 39 | impl RegistryAccount for Account<'_, Registry> { 40 | fn init(&mut self) -> Result<()> { 41 | self.current_epoch = 0; 42 | self.locked = false; 43 | self.total_workers = 0; 44 | Ok(()) 45 | } 46 | 47 | fn hash_nonce(&mut self) -> Result<()> { 48 | let mut hasher = DefaultHasher::new(); 49 | Clock::get().unwrap().slot.hash(&mut hasher); 50 | self.nonce.hash(&mut hasher); 51 | self.nonce = hasher.finish(); 52 | Ok(()) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /programs/network/src/state/snapshot.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::{prelude::*, AnchorDeserialize}; 2 | 3 | pub const SEED_SNAPSHOT: &[u8] = b"snapshot"; 4 | 5 | /// Snapshot 6 | #[account] 7 | #[derive(Debug)] 8 | pub struct Snapshot { 9 | pub id: u64, 10 | pub total_frames: u64, 11 | pub total_stake: u64, 12 | } 13 | 14 | impl Snapshot { 15 | pub fn pubkey(id: u64) -> Pubkey { 16 | Pubkey::find_program_address(&[SEED_SNAPSHOT, id.to_be_bytes().as_ref()], &crate::ID).0 17 | } 18 | } 19 | 20 | /// SnapshotAccount 21 | pub trait SnapshotAccount { 22 | fn pubkey(&self) -> Pubkey; 23 | 24 | fn init(&mut self, id: u64) -> Result<()>; 25 | } 26 | 27 | impl SnapshotAccount for Account<'_, Snapshot> { 28 | fn pubkey(&self) -> Pubkey { 29 | Snapshot::pubkey(self.id) 30 | } 31 | 32 | fn init(&mut self, id: u64) -> Result<()> { 33 | self.id = id; 34 | self.total_frames = 0; 35 | self.total_stake = 0; 36 | Ok(()) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /programs/network/src/state/snapshot_entry.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::{prelude::*, AnchorDeserialize}; 2 | 3 | pub const SEED_SNAPSHOT_ENTRY: &[u8] = b"snapshot_entry"; 4 | 5 | /** 6 | * SnapshotEntry 7 | */ 8 | 9 | #[account] 10 | #[derive(Debug)] 11 | pub struct SnapshotEntry { 12 | pub delegation: Pubkey, 13 | pub id: u64, 14 | pub snapshot_frame: Pubkey, 15 | pub stake_amount: u64, 16 | } 17 | 18 | impl SnapshotEntry { 19 | pub fn pubkey(snapshot_frame: Pubkey, id: u64) -> Pubkey { 20 | Pubkey::find_program_address( 21 | &[ 22 | SEED_SNAPSHOT_ENTRY, 23 | snapshot_frame.as_ref(), 24 | id.to_be_bytes().as_ref(), 25 | ], 26 | &crate::ID, 27 | ) 28 | .0 29 | } 30 | } 31 | 32 | /** 33 | * SnapshotEntryAccount 34 | */ 35 | 36 | pub trait SnapshotEntryAccount { 37 | fn pubkey(&self) -> Pubkey; 38 | 39 | fn init( 40 | &mut self, 41 | delegation: Pubkey, 42 | id: u64, 43 | snapshot_frame: Pubkey, 44 | stake_amount: u64, 45 | ) -> Result<()>; 46 | } 47 | 48 | impl SnapshotEntryAccount for Account<'_, SnapshotEntry> { 49 | fn pubkey(&self) -> Pubkey { 50 | SnapshotEntry::pubkey(self.snapshot_frame, self.id) 51 | } 52 | 53 | fn init( 54 | &mut self, 55 | delegation: Pubkey, 56 | id: u64, 57 | snapshot_frame: Pubkey, 58 | stake_amount: u64, 59 | ) -> Result<()> { 60 | self.delegation = delegation; 61 | self.id = id; 62 | self.snapshot_frame = snapshot_frame; 63 | self.stake_amount = stake_amount; 64 | Ok(()) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /programs/network/src/state/snapshot_frame.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::{prelude::*, AnchorDeserialize}; 2 | 3 | pub const SEED_SNAPSHOT_FRAME: &[u8] = b"snapshot_frame"; 4 | 5 | /** 6 | * SnapshotFrame 7 | */ 8 | #[account] 9 | #[derive(Debug)] 10 | pub struct SnapshotFrame { 11 | pub id: u64, 12 | pub snapshot: Pubkey, 13 | pub stake_amount: u64, 14 | pub stake_offset: u64, 15 | pub total_entries: u64, 16 | pub worker: Pubkey, 17 | } 18 | 19 | impl SnapshotFrame { 20 | pub fn pubkey(snapshot: Pubkey, id: u64) -> Pubkey { 21 | Pubkey::find_program_address( 22 | &[ 23 | SEED_SNAPSHOT_FRAME, 24 | snapshot.as_ref(), 25 | id.to_be_bytes().as_ref(), 26 | ], 27 | &crate::ID, 28 | ) 29 | .0 30 | } 31 | } 32 | 33 | /** 34 | * SnapshotFrameAccount 35 | */ 36 | 37 | pub trait SnapshotFrameAccount { 38 | fn pubkey(&self) -> Pubkey; 39 | 40 | fn init( 41 | &mut self, 42 | id: u64, 43 | snapshot: Pubkey, 44 | stake_amount: u64, 45 | stake_offset: u64, 46 | worker: Pubkey, 47 | ) -> Result<()>; 48 | } 49 | 50 | impl SnapshotFrameAccount for Account<'_, SnapshotFrame> { 51 | fn pubkey(&self) -> Pubkey { 52 | SnapshotFrame::pubkey(self.snapshot, self.id) 53 | } 54 | 55 | fn init( 56 | &mut self, 57 | id: u64, 58 | snapshot: Pubkey, 59 | stake_amount: u64, 60 | stake_offset: u64, 61 | worker: Pubkey, 62 | ) -> Result<()> { 63 | self.id = id; 64 | self.snapshot = snapshot; 65 | self.stake_offset = stake_offset; 66 | self.stake_amount = stake_amount; 67 | self.total_entries = 0; 68 | self.worker = worker; 69 | Ok(()) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /programs/network/src/state/unstake.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::{prelude::*, AnchorDeserialize}; 2 | 3 | pub const SEED_UNSTAKE: &[u8] = b"unstake"; 4 | 5 | /// Unstake 6 | #[account] 7 | #[derive(Debug)] 8 | pub struct Unstake { 9 | pub amount: u64, 10 | pub authority: Pubkey, 11 | pub delegation: Pubkey, 12 | pub id: u64, 13 | pub worker: Pubkey, 14 | } 15 | 16 | impl Unstake { 17 | pub fn pubkey(id: u64) -> Pubkey { 18 | Pubkey::find_program_address(&[SEED_UNSTAKE, id.to_be_bytes().as_ref()], &crate::ID).0 19 | } 20 | } 21 | 22 | /// UnstakeAccount 23 | pub trait UnstakeAccount { 24 | fn pubkey(&self) -> Pubkey; 25 | 26 | fn init( 27 | &mut self, 28 | amount: u64, 29 | authority: Pubkey, 30 | delegation: Pubkey, 31 | id: u64, 32 | worker: Pubkey, 33 | ) -> Result<()>; 34 | } 35 | 36 | impl UnstakeAccount for Account<'_, Unstake> { 37 | fn pubkey(&self) -> Pubkey { 38 | Unstake::pubkey(self.id) 39 | } 40 | 41 | fn init( 42 | &mut self, 43 | amount: u64, 44 | authority: Pubkey, 45 | delegation: Pubkey, 46 | id: u64, 47 | worker: Pubkey, 48 | ) -> Result<()> { 49 | self.amount = amount; 50 | self.authority = authority.key(); 51 | self.delegation = delegation; 52 | self.id = id; 53 | self.worker = worker; 54 | Ok(()) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /programs/network/src/state/worker.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::{prelude::*, AnchorDeserialize}; 2 | 3 | use crate::errors::*; 4 | 5 | pub const SEED_WORKER: &[u8] = b"worker"; 6 | 7 | /// Worker 8 | #[account] 9 | #[derive(Debug)] 10 | pub struct Worker { 11 | /// The worker's authority (owner). 12 | pub authority: Pubkey, 13 | /// The number of lamports claimable by the authority as commission for running the worker. 14 | pub commission_balance: u64, 15 | /// Integer between 0 and 100 determining the percentage of fees worker will keep as commission. 16 | pub commission_rate: u64, 17 | /// The worker's id. 18 | pub id: u64, 19 | /// The worker's signatory address (used to sign txs). 20 | pub signatory: Pubkey, 21 | /// The number delegations allocated to this worker. 22 | pub total_delegations: u64, 23 | } 24 | 25 | impl Worker { 26 | pub fn pubkey(id: u64) -> Pubkey { 27 | Pubkey::find_program_address(&[SEED_WORKER, id.to_be_bytes().as_ref()], &crate::ID).0 28 | } 29 | } 30 | 31 | /// WorkerSettings 32 | #[derive(AnchorSerialize, AnchorDeserialize)] 33 | pub struct WorkerSettings { 34 | pub commission_rate: u64, 35 | pub signatory: Pubkey, 36 | } 37 | 38 | /// WorkerAccount 39 | pub trait WorkerAccount { 40 | fn pubkey(&self) -> Pubkey; 41 | 42 | fn init(&mut self, authority: &mut Signer, id: u64, signatory: &Signer) -> Result<()>; 43 | 44 | fn update(&mut self, settings: WorkerSettings) -> Result<()>; 45 | } 46 | 47 | impl WorkerAccount for Account<'_, Worker> { 48 | fn pubkey(&self) -> Pubkey { 49 | Worker::pubkey(self.id) 50 | } 51 | 52 | fn init(&mut self, authority: &mut Signer, id: u64, signatory: &Signer) -> Result<()> { 53 | self.authority = authority.key(); 54 | self.commission_balance = 0; 55 | self.commission_rate = 0; 56 | self.id = id; 57 | self.signatory = signatory.key(); 58 | self.total_delegations = 0; 59 | Ok(()) 60 | } 61 | 62 | fn update(&mut self, settings: WorkerSettings) -> Result<()> { 63 | require!( 64 | settings.commission_rate.ge(&0) && settings.commission_rate.le(&100), 65 | ClockworkError::InvalidCommissionRate 66 | ); 67 | self.commission_rate = settings.commission_rate; 68 | 69 | require!( 70 | settings.signatory.ne(&self.authority), 71 | ClockworkError::InvalidSignatory 72 | ); 73 | self.signatory = settings.signatory; 74 | Ok(()) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /programs/thread/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "clockwork-thread-program" 3 | version = "2.0.20" 4 | description = "Clockwork thread program" 5 | edition = "2021" 6 | license = "AGPL-3.0-or-later" 7 | homepage = "https://clockwork.xyz" 8 | repository = "https://github.com/clockwork-xyz/clockwork" 9 | documentation = "https://docs.clockwork.xyz" 10 | readme = "./README.md" 11 | keywords = ["solana"] 12 | 13 | [lib] 14 | crate-type = ["cdylib", "lib"] 15 | name = "clockwork_thread_program" 16 | 17 | [features] 18 | no-entrypoint = [] 19 | no-idl = [] 20 | no-log-ix-name = [] 21 | cpi = ["no-entrypoint"] 22 | default = [] 23 | idl-build = ["anchor-lang/idl-build", "clockwork-network-program/idl-build", "clockwork-thread-program-v1/idl-build", "clockwork-utils/idl-build"] 24 | 25 | [dependencies] 26 | anchor-lang = "0.30.0" 27 | chrono = { version = "0.4.19", default-features = false, features = ["alloc"] } 28 | clockwork-cron = { path = "../../cron", version = "=2.0.20" } 29 | clockwork-network-program = { path = "../network", features = ["cpi"], version = "=2.0.20" } 30 | clockwork-thread-program-v1 = { path = "v1", version = "=1.4.4" } 31 | clockwork-utils = { path = "../../utils", version = "=2.0.20" } 32 | pyth-sdk-solana = "0.10.1" 33 | static-pubkey = "1.0.3" 34 | version = "3.0.0" 35 | -------------------------------------------------------------------------------- /programs/thread/README.md: -------------------------------------------------------------------------------- 1 | # Clockwork Thread Program v2 2 | -------------------------------------------------------------------------------- /programs/thread/src/errors.rs: -------------------------------------------------------------------------------- 1 | //! Errors thrown by the program. 2 | 3 | use anchor_lang::prelude::*; 4 | 5 | /// Errors for the the Clockwork thread program. 6 | #[error_code] 7 | pub enum ClockworkError { 8 | /// Thrown if a exec response has an invalid program ID or cannot be parsed. 9 | #[msg("The exec response could not be parsed")] 10 | InvalidThreadResponse, 11 | 12 | /// Thrown if a thread has an invalid state and cannot complete the operation. 13 | #[msg("The thread is in an invalid state")] 14 | InvalidThreadState, 15 | 16 | /// TThe provided trigger variant is invalid. 17 | #[msg("The trigger variant cannot be changed")] 18 | InvalidTriggerVariant, 19 | 20 | /// Thrown if a exec instruction is invalid because the thread's trigger condition has not been met. 21 | #[msg("The trigger condition has not been activated")] 22 | TriggerConditionFailed, 23 | 24 | #[msg("This operation cannot be processes because the thread is currently busy")] 25 | ThreadBusy, 26 | 27 | /// Thrown if a request is invalid because the thread is currently paused. 28 | #[msg("The thread is currently paused")] 29 | ThreadPaused, 30 | 31 | /// Thrown if a exec instruction would cause a thread to exceed its rate limit. 32 | #[msg("The thread's rate limit has been reached")] 33 | RateLimitExeceeded, 34 | 35 | /// Thrown if a thread authority attempts to set a rate limit above the maximum allowed value. 36 | #[msg("Thread rate limits cannot exceed the maximum allowed value")] 37 | MaxRateLimitExceeded, 38 | 39 | /// Thrown if an inner instruction attempted to write to an unauthorized address. 40 | #[msg("Inner instruction attempted to write to an unauthorized address")] 41 | UnauthorizedWrite, 42 | 43 | /// Thrown if the user attempts to withdraw SOL that would put a thread below it's minimum rent threshold. 44 | #[msg("Withdrawing this amount would leave the thread with less than the minimum required SOL for rent exemption")] 45 | WithdrawalTooLarge, 46 | } 47 | -------------------------------------------------------------------------------- /programs/thread/src/instructions/get_crate_info.rs: -------------------------------------------------------------------------------- 1 | use { 2 | anchor_lang::{prelude::*, system_program}, 3 | clockwork_utils::CrateInfo, 4 | }; 5 | 6 | /// Accounts required for the `get_crate_info` instruction. 7 | /// We are not using system program actually 8 | /// But anchor does not support empty structs: https://github.com/coral-xyz/anchor/pull/1659 9 | #[derive(Accounts)] 10 | pub struct GetCrateInfo<'info> { 11 | #[account(address = system_program::ID)] 12 | pub system_program: Program<'info, System>, 13 | } 14 | 15 | pub fn handler(_ctx: Context) -> Result { 16 | let spec = format!( 17 | "https://github.com/clockwork-xyz/clockwork/blob/v{}/programs/thread/Cargo.toml", 18 | version!() 19 | ); 20 | let blob = ""; 21 | let info = CrateInfo { 22 | spec: spec.into(), 23 | blob: blob.into(), 24 | }; 25 | msg!("{}", info); 26 | 27 | Ok(info) 28 | } 29 | -------------------------------------------------------------------------------- /programs/thread/src/instructions/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod get_crate_info; 2 | pub mod thread_create; 3 | pub mod thread_delete; 4 | pub mod thread_exec; 5 | pub mod thread_instruction_add; 6 | pub mod thread_instruction_remove; 7 | pub mod thread_kickoff; 8 | pub mod thread_pause; 9 | pub mod thread_reset; 10 | pub mod thread_resume; 11 | pub mod thread_update; 12 | pub mod thread_withdraw; 13 | 14 | pub use get_crate_info::*; 15 | pub use thread_create::*; 16 | pub use thread_delete::*; 17 | pub use thread_exec::*; 18 | pub use thread_instruction_add::*; 19 | pub use thread_instruction_remove::*; 20 | pub use thread_kickoff::*; 21 | pub use thread_pause::*; 22 | pub use thread_reset::*; 23 | pub use thread_resume::*; 24 | pub use thread_update::*; 25 | pub use thread_withdraw::*; 26 | -------------------------------------------------------------------------------- /programs/thread/src/instructions/thread_create.rs: -------------------------------------------------------------------------------- 1 | use std::mem::size_of; 2 | 3 | use anchor_lang::{ 4 | prelude::*, 5 | solana_program::system_program, 6 | system_program::{transfer, Transfer} 7 | }; 8 | use clockwork_utils::thread::{Trigger, SerializableInstruction}; 9 | 10 | use crate::state::*; 11 | 12 | /// The minimum exec fee that may be set on a thread. 13 | const MINIMUM_FEE: u64 = 1000; 14 | 15 | /// Accounts required by the `thread_create` instruction. 16 | #[derive(Accounts)] 17 | #[instruction(amount: u64, id: Vec, instructions: Vec, trigger: Trigger)] 18 | pub struct ThreadCreate<'info> { 19 | /// The authority (owner) of the thread. 20 | #[account()] 21 | pub authority: Signer<'info>, 22 | 23 | /// The payer for account initializations. 24 | #[account(mut)] 25 | pub payer: Signer<'info>, 26 | 27 | /// The Solana system program. 28 | #[account(address = system_program::ID)] 29 | pub system_program: Program<'info, System>, 30 | 31 | /// The thread to be created. 32 | #[account( 33 | init, 34 | seeds = [ 35 | SEED_THREAD, 36 | authority.key().as_ref(), 37 | id.as_slice(), 38 | ], 39 | bump, 40 | payer= payer, 41 | space = vec![ 42 | 8, 43 | size_of::(), 44 | id.len(), 45 | instructions.try_to_vec()?.len(), 46 | trigger.try_to_vec()?.len(), 47 | NEXT_INSTRUCTION_SIZE, 48 | ].iter().sum() 49 | )] 50 | pub thread: Account<'info, Thread>, 51 | } 52 | 53 | pub fn handler(ctx: Context, amount: u64, id: Vec, instructions: Vec, trigger: Trigger) -> Result<()> { 54 | // Get accounts 55 | let authority = &ctx.accounts.authority; 56 | let payer = &ctx.accounts.payer; 57 | let system_program = &ctx.accounts.system_program; 58 | let thread = &mut ctx.accounts.thread; 59 | 60 | // Initialize the thread 61 | let bump = ctx.bumps.thread; 62 | thread.authority = authority.key(); 63 | thread.bump = bump; 64 | thread.created_at = Clock::get().unwrap().into(); 65 | thread.exec_context = None; 66 | thread.fee = MINIMUM_FEE; 67 | thread.id = id; 68 | thread.instructions = instructions; 69 | thread.name = String::new(); 70 | thread.next_instruction = None; 71 | thread.paused = false; 72 | thread.rate_limit = u64::MAX; 73 | thread.trigger = trigger; 74 | 75 | // Transfer SOL from payer to the thread. 76 | transfer( 77 | CpiContext::new( 78 | system_program.to_account_info(), 79 | Transfer { 80 | from: payer.to_account_info(), 81 | to: thread.to_account_info(), 82 | }, 83 | ), 84 | amount 85 | )?; 86 | 87 | Ok(()) 88 | } 89 | -------------------------------------------------------------------------------- /programs/thread/src/instructions/thread_delete.rs: -------------------------------------------------------------------------------- 1 | use {crate::state::*, anchor_lang::prelude::*}; 2 | 3 | /// Accounts required by the `thread_delete` instruction. 4 | #[derive(Accounts)] 5 | pub struct ThreadDelete<'info> { 6 | /// The authority (owner) of the thread. 7 | #[account( 8 | constraint = authority.key().eq(&thread.authority) || authority.key().eq(&thread.key()) 9 | )] 10 | pub authority: Signer<'info>, 11 | 12 | /// The address to return the data rent lamports to. 13 | #[account(mut)] 14 | pub close_to: SystemAccount<'info>, 15 | 16 | /// The thread to be delete. 17 | #[account( 18 | mut, 19 | seeds = [ 20 | SEED_THREAD, 21 | thread.authority.as_ref(), 22 | thread.id.as_slice(), 23 | ], 24 | bump = thread.bump, 25 | )] 26 | pub thread: Account<'info, Thread>, 27 | } 28 | 29 | pub fn handler(ctx: Context) -> Result<()> { 30 | let thread = &ctx.accounts.thread; 31 | let close_to = &ctx.accounts.close_to; 32 | 33 | let thread_lamports = thread.to_account_info().lamports(); 34 | **thread.to_account_info().try_borrow_mut_lamports()? = thread 35 | .to_account_info() 36 | .lamports() 37 | .checked_sub(thread_lamports) 38 | .unwrap(); 39 | **close_to.to_account_info().try_borrow_mut_lamports()? = close_to 40 | .to_account_info() 41 | .lamports() 42 | .checked_add(thread_lamports) 43 | .unwrap(); 44 | 45 | Ok(()) 46 | } 47 | -------------------------------------------------------------------------------- /programs/thread/src/instructions/thread_instruction_add.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::{ 2 | prelude::*, 3 | solana_program::system_program, 4 | system_program::{transfer, Transfer}, 5 | }; 6 | 7 | use crate::state::*; 8 | 9 | /// Accounts required by the `thread_instruction_add` instruction. 10 | #[derive(Accounts)] 11 | #[instruction(instruction: SerializableInstruction)] 12 | pub struct ThreadInstructionAdd<'info> { 13 | /// The authority (owner) of the thread. 14 | #[account(mut)] 15 | pub authority: Signer<'info>, 16 | 17 | /// The Solana system program 18 | #[account(address = system_program::ID)] 19 | pub system_program: Program<'info, System>, 20 | 21 | /// The thread to be paused. 22 | #[account( 23 | mut, 24 | seeds = [ 25 | SEED_THREAD, 26 | thread.authority.as_ref(), 27 | thread.id.as_slice(), 28 | ], 29 | bump = thread.bump, 30 | has_one = authority 31 | )] 32 | pub thread: Account<'info, Thread>, 33 | } 34 | 35 | pub fn handler( 36 | ctx: Context, 37 | instruction: SerializableInstruction, 38 | ) -> Result<()> { 39 | // Get accounts 40 | let authority = &ctx.accounts.authority; 41 | let thread = &mut ctx.accounts.thread; 42 | let system_program = &ctx.accounts.system_program; 43 | 44 | // Append the instruction. 45 | thread.instructions.push(instruction); 46 | 47 | // Reallocate mem for the thread account. 48 | thread.realloc()?; 49 | 50 | // If lamports are required to maintain rent-exemption, pay them. 51 | let data_len = thread.to_account_info().data_len(); 52 | let minimum_rent = Rent::get().unwrap().minimum_balance(data_len); 53 | if minimum_rent > thread.to_account_info().lamports() { 54 | transfer( 55 | CpiContext::new( 56 | system_program.to_account_info(), 57 | Transfer { 58 | from: authority.to_account_info(), 59 | to: thread.to_account_info(), 60 | }, 61 | ), 62 | minimum_rent 63 | .checked_sub(thread.to_account_info().lamports()) 64 | .unwrap(), 65 | )?; 66 | } 67 | 68 | Ok(()) 69 | } 70 | -------------------------------------------------------------------------------- /programs/thread/src/instructions/thread_instruction_remove.rs: -------------------------------------------------------------------------------- 1 | use {crate::state::*, anchor_lang::prelude::*}; 2 | 3 | /// Accounts required by the `thread_instruction_remove` instruction. 4 | #[derive(Accounts)] 5 | #[instruction(index: u64)] 6 | pub struct ThreadInstructionRemove<'info> { 7 | /// The authority (owner) of the thread. 8 | #[account()] 9 | pub authority: Signer<'info>, 10 | 11 | /// The thread to be edited. 12 | #[account( 13 | mut, 14 | seeds = [ 15 | SEED_THREAD, 16 | thread.authority.as_ref(), 17 | thread.id.as_slice(), 18 | ], 19 | bump = thread.bump, 20 | has_one = authority 21 | )] 22 | pub thread: Account<'info, Thread>, 23 | } 24 | 25 | pub fn handler(ctx: Context, index: u64) -> Result<()> { 26 | // Get accounts 27 | let thread = &mut ctx.accounts.thread; 28 | 29 | // Pause the thread 30 | thread.instructions.remove(index as usize); 31 | 32 | Ok(()) 33 | } 34 | -------------------------------------------------------------------------------- /programs/thread/src/instructions/thread_pause.rs: -------------------------------------------------------------------------------- 1 | use {crate::state::*, anchor_lang::prelude::*}; 2 | 3 | /// Accounts required by the `thread_delete` instruction. 4 | #[derive(Accounts)] 5 | pub struct ThreadPause<'info> { 6 | /// The authority (owner) of the thread. 7 | #[account()] 8 | pub authority: Signer<'info>, 9 | 10 | /// The thread to be paused. 11 | #[account( 12 | mut, 13 | seeds = [ 14 | SEED_THREAD, 15 | thread.authority.as_ref(), 16 | thread.id.as_slice(), 17 | ], 18 | bump = thread.bump, 19 | has_one = authority 20 | )] 21 | pub thread: Account<'info, Thread>, 22 | } 23 | 24 | pub fn handler(ctx: Context) -> Result<()> { 25 | // Get accounts 26 | let thread = &mut ctx.accounts.thread; 27 | 28 | // Pause the thread 29 | thread.paused = true; 30 | 31 | Ok(()) 32 | } 33 | -------------------------------------------------------------------------------- /programs/thread/src/instructions/thread_reset.rs: -------------------------------------------------------------------------------- 1 | use {crate::state::*, anchor_lang::prelude::*}; 2 | 3 | /// Accounts required by the `thread_reset` instruction. 4 | #[derive(Accounts)] 5 | pub struct ThreadReset<'info> { 6 | /// The authority (owner) of the thread. 7 | #[account()] 8 | pub authority: Signer<'info>, 9 | 10 | /// The thread to be paused. 11 | #[account( 12 | mut, 13 | seeds = [ 14 | SEED_THREAD, 15 | thread.authority.as_ref(), 16 | thread.id.as_slice(), 17 | ], 18 | bump = thread.bump, 19 | has_one = authority 20 | )] 21 | pub thread: Account<'info, Thread>, 22 | } 23 | 24 | pub fn handler(ctx: Context) -> Result<()> { 25 | // Get accounts 26 | let thread = &mut ctx.accounts.thread; 27 | 28 | // Full reset the thread state. 29 | thread.next_instruction = None; 30 | thread.exec_context = None; 31 | thread.created_at = Clock::get().unwrap().into(); 32 | 33 | Ok(()) 34 | } 35 | -------------------------------------------------------------------------------- /programs/thread/src/instructions/thread_resume.rs: -------------------------------------------------------------------------------- 1 | use {crate::state::*, anchor_lang::prelude::*}; 2 | 3 | /// Accounts required by the `thread_resume` instruction. 4 | #[derive(Accounts)] 5 | pub struct ThreadResume<'info> { 6 | /// The authority (owner) of the thread. 7 | #[account()] 8 | pub authority: Signer<'info>, 9 | 10 | /// The thread to be resumed. 11 | #[account( 12 | mut, 13 | seeds = [ 14 | SEED_THREAD, 15 | thread.authority.as_ref(), 16 | thread.id.as_slice(), 17 | ], 18 | bump = thread.bump, 19 | has_one = authority 20 | )] 21 | pub thread: Account<'info, Thread>, 22 | } 23 | 24 | pub fn handler(ctx: Context) -> Result<()> { 25 | // Get accounts 26 | let thread = &mut ctx.accounts.thread; 27 | 28 | // Resume the thread 29 | thread.paused = false; 30 | 31 | // Update the exec context 32 | match thread.exec_context { 33 | None => {} 34 | Some(exec_context) => { 35 | match exec_context.trigger_context { 36 | TriggerContext::Cron { started_at: _ } => { 37 | // Jump ahead to the current timestamp 38 | thread.exec_context = Some(ExecContext { 39 | trigger_context: TriggerContext::Cron { 40 | started_at: Clock::get().unwrap().unix_timestamp, 41 | }, 42 | ..exec_context 43 | }); 44 | } 45 | _ => { 46 | // Nothing to do. 47 | } 48 | } 49 | } 50 | } 51 | 52 | Ok(()) 53 | } 54 | -------------------------------------------------------------------------------- /programs/thread/src/instructions/thread_update.rs: -------------------------------------------------------------------------------- 1 | use crate::{errors::ClockworkError, state::*}; 2 | 3 | use anchor_lang::{ 4 | prelude::*, 5 | solana_program::system_program, 6 | system_program::{transfer, Transfer}, 7 | }; 8 | 9 | /// Accounts required by the `thread_update` instruction. 10 | #[derive(Accounts)] 11 | #[instruction(settings: ThreadSettings)] 12 | pub struct ThreadUpdate<'info> { 13 | /// The authority (owner) of the thread. 14 | #[account(mut)] 15 | pub authority: Signer<'info>, 16 | 17 | /// The Solana system program 18 | #[account(address = system_program::ID)] 19 | pub system_program: Program<'info, System>, 20 | 21 | /// The thread to be updated. 22 | #[account( 23 | mut, 24 | seeds = [ 25 | SEED_THREAD, 26 | thread.authority.as_ref(), 27 | thread.id.as_slice(), 28 | ], 29 | bump = thread.bump, 30 | has_one = authority, 31 | )] 32 | pub thread: Account<'info, Thread>, 33 | } 34 | 35 | pub fn handler(ctx: Context, settings: ThreadSettings) -> Result<()> { 36 | // Get accounts 37 | let authority = &ctx.accounts.authority; 38 | let thread = &mut ctx.accounts.thread; 39 | let system_program = &ctx.accounts.system_program; 40 | 41 | // Update the thread. 42 | if let Some(fee) = settings.fee { 43 | thread.fee = fee; 44 | } 45 | 46 | // If provided, update the thread's instruction set. 47 | if let Some(instructions) = settings.instructions { 48 | thread.instructions = instructions; 49 | } 50 | 51 | // If provided, update the rate limit. 52 | if let Some(rate_limit) = settings.rate_limit { 53 | thread.rate_limit = rate_limit; 54 | } 55 | 56 | // If provided, update the thread's trigger and reset the exec context. 57 | if let Some(trigger) = settings.trigger { 58 | // Require the thread is not in the middle of processing. 59 | require!( 60 | std::mem::discriminant(&thread.trigger) == std::mem::discriminant(&trigger), 61 | ClockworkError::InvalidTriggerVariant 62 | ); 63 | thread.trigger = trigger.clone(); 64 | 65 | // If the user updates an account trigger, the trigger context is no longer valid. 66 | // Here we reset the trigger context to zero to re-prime the trigger. 67 | if thread.exec_context.is_some() { 68 | thread.exec_context = Some(ExecContext { 69 | trigger_context: match trigger { 70 | Trigger::Account { 71 | address: _, 72 | offset: _, 73 | size: _, 74 | } => TriggerContext::Account { data_hash: 0 }, 75 | _ => thread.exec_context.unwrap().trigger_context, 76 | }, 77 | ..thread.exec_context.unwrap() 78 | }); 79 | } 80 | } 81 | 82 | // Reallocate mem for the thread account 83 | thread.realloc()?; 84 | 85 | // If lamports are required to maintain rent-exemption, pay them 86 | let data_len = thread.to_account_info().data_len(); 87 | let minimum_rent = Rent::get().unwrap().minimum_balance(data_len); 88 | if minimum_rent > thread.to_account_info().lamports() { 89 | transfer( 90 | CpiContext::new( 91 | system_program.to_account_info(), 92 | Transfer { 93 | from: authority.to_account_info(), 94 | to: thread.to_account_info(), 95 | }, 96 | ), 97 | minimum_rent 98 | .checked_sub(thread.to_account_info().lamports()) 99 | .unwrap(), 100 | )?; 101 | } 102 | 103 | Ok(()) 104 | } 105 | -------------------------------------------------------------------------------- /programs/thread/src/instructions/thread_withdraw.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{errors::*, state::*}, 3 | anchor_lang::prelude::*, 4 | }; 5 | 6 | /// Accounts required by the `thread_withdraw` instruction. 7 | #[derive(Accounts)] 8 | #[instruction(amount: u64)] 9 | pub struct ThreadWithdraw<'info> { 10 | /// The authority (owner) of the thread. 11 | #[account()] 12 | pub authority: Signer<'info>, 13 | 14 | /// The account to withdraw lamports to. 15 | #[account(mut)] 16 | pub pay_to: SystemAccount<'info>, 17 | 18 | /// The thread to be. 19 | #[account( 20 | mut, 21 | seeds = [ 22 | SEED_THREAD, 23 | thread.authority.as_ref(), 24 | thread.id.as_slice(), 25 | ], 26 | bump = thread.bump, 27 | has_one = authority, 28 | )] 29 | pub thread: Account<'info, Thread>, 30 | } 31 | 32 | pub fn handler(ctx: Context, amount: u64) -> Result<()> { 33 | // Get accounts 34 | let pay_to = &mut ctx.accounts.pay_to; 35 | let thread = &mut ctx.accounts.thread; 36 | 37 | // Calculate the minimum rent threshold 38 | let data_len = 8 + thread.try_to_vec()?.len(); 39 | let minimum_rent = Rent::get().unwrap().minimum_balance(data_len); 40 | let post_balance = thread 41 | .to_account_info() 42 | .lamports() 43 | .checked_sub(amount) 44 | .unwrap(); 45 | require!( 46 | post_balance.gt(&minimum_rent), 47 | ClockworkError::WithdrawalTooLarge 48 | ); 49 | 50 | // Withdraw balance from thread to the pay_to account 51 | **thread.to_account_info().try_borrow_mut_lamports()? = thread 52 | .to_account_info() 53 | .lamports() 54 | .checked_sub(amount) 55 | .unwrap(); 56 | **pay_to.to_account_info().try_borrow_mut_lamports()? = pay_to 57 | .to_account_info() 58 | .lamports() 59 | .checked_add(amount) 60 | .unwrap(); 61 | 62 | Ok(()) 63 | } 64 | -------------------------------------------------------------------------------- /programs/thread/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This program allows users to create transaction threads on Solana. Threads are dynamic, long-running 2 | //! transaction threads that can persist across blocks and even run indefinitely. Developers can use threads 3 | //! to schedule transactions and automate smart-contracts without relying on centralized infrastructure. 4 | #[macro_use] 5 | extern crate version; 6 | 7 | pub mod errors; 8 | pub mod state; 9 | 10 | mod instructions; 11 | 12 | use anchor_lang::prelude::*; 13 | use clockwork_utils::{ 14 | thread::{SerializableInstruction, Trigger}, 15 | CrateInfo, 16 | }; 17 | use instructions::*; 18 | use state::*; 19 | 20 | declare_id!("CLoCKyJ6DXBJqqu2VWx9RLbgnwwR6BMHHuyasVmfMzBh"); 21 | 22 | /// Program for creating transaction threads on Solana. 23 | #[program] 24 | pub mod thread_program { 25 | use super::*; 26 | 27 | /// Return the crate information via `sol_set_return_data/sol_get_return_data` 28 | pub fn get_crate_info(ctx: Context) -> Result { 29 | get_crate_info::handler(ctx) 30 | } 31 | 32 | /// Executes the next instruction on thread. 33 | pub fn thread_exec(ctx: Context) -> Result<()> { 34 | thread_exec::handler(ctx) 35 | } 36 | 37 | /// Creates a new transaction thread. 38 | pub fn thread_create( 39 | ctx: Context, 40 | amount: u64, 41 | id: Vec, 42 | instructions: Vec, 43 | trigger: Trigger, 44 | ) -> Result<()> { 45 | thread_create::handler(ctx, amount, id, instructions, trigger) 46 | } 47 | 48 | /// Closes an existing thread account and returns the lamports to the owner. 49 | pub fn thread_delete(ctx: Context) -> Result<()> { 50 | thread_delete::handler(ctx) 51 | } 52 | 53 | /// Appends a new instruction to the thread's instruction set. 54 | pub fn thread_instruction_add( 55 | ctx: Context, 56 | instruction: SerializableInstruction, 57 | ) -> Result<()> { 58 | thread_instruction_add::handler(ctx, instruction) 59 | } 60 | 61 | /// Removes an instruction to the thread's instruction set at the provied index. 62 | pub fn thread_instruction_remove( 63 | ctx: Context, 64 | index: u64, 65 | ) -> Result<()> { 66 | thread_instruction_remove::handler(ctx, index) 67 | } 68 | 69 | /// Kicks off a thread if its trigger condition is active. 70 | pub fn thread_kickoff(ctx: Context) -> Result<()> { 71 | thread_kickoff::handler(ctx) 72 | } 73 | 74 | /// Pauses an active thread. 75 | pub fn thread_pause(ctx: Context) -> Result<()> { 76 | thread_pause::handler(ctx) 77 | } 78 | 79 | /// Resumes a paused thread. 80 | pub fn thread_resume(ctx: Context) -> Result<()> { 81 | thread_resume::handler(ctx) 82 | } 83 | 84 | /// Resets a thread's next instruction. 85 | pub fn thread_reset(ctx: Context) -> Result<()> { 86 | thread_reset::handler(ctx) 87 | } 88 | 89 | /// Allows an owner to update the mutable properties of a thread. 90 | pub fn thread_update(ctx: Context, settings: ThreadSettings) -> Result<()> { 91 | thread_update::handler(ctx, settings) 92 | } 93 | 94 | /// Allows an owner to withdraw from a thread's lamport balance. 95 | pub fn thread_withdraw(ctx: Context, amount: u64) -> Result<()> { 96 | thread_withdraw::handler(ctx, amount) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /programs/thread/src/state/crate_info.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::{prelude::*, AnchorDeserialize, AnchorSerialize}; 2 | use std::fmt::{Display, Formatter}; 3 | 4 | /// The crate build information 5 | #[derive(AnchorDeserialize, AnchorSerialize, Clone, Debug)] 6 | pub struct CrateInfo { 7 | /// The link to the crate spec 8 | pub spec: String, 9 | /// Arbitrary blob that can be set by developers 10 | pub blob: String, 11 | } 12 | 13 | impl Display for CrateInfo { 14 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 15 | write!(f, "spec:{} blob:{}", self.spec, self.blob) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /programs/thread/src/state/mod.rs: -------------------------------------------------------------------------------- 1 | //! All objects needed to describe and manage the program's state. 2 | 3 | mod thread; 4 | mod versioned_thread; 5 | 6 | pub use clockwork_utils::thread::*; 7 | pub use thread::*; 8 | pub use versioned_thread::*; 9 | -------------------------------------------------------------------------------- /programs/thread/v1/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "clockwork-thread-program-v1" 3 | version = "1.4.4" 4 | edition = "2021" 5 | readme = "./README.md" 6 | description = "Clockwork thread program v1" 7 | license = "AGPL-3.0-or-later" 8 | 9 | [lib] 10 | crate-type = ["cdylib", "lib"] 11 | name = "clockwork_thread_program_v1" 12 | 13 | [features] 14 | default = ["cpi"] 15 | no-entrypoint = [] 16 | no-idl = [] 17 | no-log-ix-name = [] 18 | cpi = ["no-entrypoint"] 19 | idl-build = ["anchor-lang/idl-build"] 20 | 21 | [dependencies] 22 | anchor-lang = "0.30.0" 23 | clockwork-anchor-gen = { version = "0.3.2", features = ["compat-program-result"] } 24 | -------------------------------------------------------------------------------- /programs/thread/v1/README.md: -------------------------------------------------------------------------------- 1 | # Clockwork Thread Program v1 2 | -------------------------------------------------------------------------------- /programs/thread/v1/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::solana_program::entrypoint::ProgramResult; 2 | 3 | clockwork_anchor_gen::generate_cpi_interface!(idl_path = "idl.json"); 4 | 5 | anchor_lang::prelude::declare_id!("3XXuUFfweXBwFgFfYaejLvZE4cGZiHgKiGfMtdxNzYmv"); 6 | 7 | pub const SEED_THREAD: &[u8] = b"thread"; 8 | 9 | impl Thread { 10 | /// Derive the pubkey of a thread account. 11 | pub fn pubkey(authority: Pubkey, id: String) -> Pubkey { 12 | Pubkey::find_program_address( 13 | &[SEED_THREAD, authority.as_ref(), id.as_bytes()], 14 | &crate::ID, 15 | ) 16 | .0 17 | } 18 | } 19 | 20 | impl PartialEq for Thread { 21 | fn eq(&self, other: &Self) -> bool { 22 | self.authority.eq(&other.authority) && self.id.eq(&other.id) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /programs/webhook/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "clockwork-webhook-program" 3 | version = "2.0.20" 4 | description = "Clockwork webhook program" 5 | edition = "2021" 6 | license = "AGPL-3.0-or-later" 7 | homepage = "https://clockwork.xyz" 8 | repository = "https://github.com/clockwork-xyz/clockwork" 9 | documentation = "https://docs.clockwork.xyz" 10 | readme = "./README.md" 11 | keywords = ["solana"] 12 | 13 | [lib] 14 | crate-type = ["cdylib", "lib"] 15 | name = "clockwork_webhook_program" 16 | 17 | [features] 18 | no-entrypoint = [] 19 | no-idl = [] 20 | no-log-ix-name = [] 21 | cpi = ["no-entrypoint"] 22 | default = [] 23 | idl-build = ["anchor-lang/idl-build", "clockwork-network-program/idl-build", "clockwork-utils/idl-build"] 24 | 25 | [dependencies] 26 | anchor-lang = { features = ["init-if-needed"], version = "0.30.0" } 27 | clockwork-network-program = { path = "../network", features = ["cpi"], version = "=2.0.20" } 28 | clockwork-utils = { path = "../../utils", version = "=2.0.20" } 29 | serde = "1.0.152" 30 | -------------------------------------------------------------------------------- /programs/webhook/README.md: -------------------------------------------------------------------------------- 1 | # Clockwork Http 2 | -------------------------------------------------------------------------------- /programs/webhook/src/errors.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | #[error_code] 4 | pub enum ClockworkError { 5 | #[msg("This instruction requires admin authority")] 6 | AdminAuthorityInvalid, 7 | 8 | #[msg("You cannot claim more than the collectable balance")] 9 | InvalidClaimAmount, 10 | 11 | #[msg("Http method is not recognized")] 12 | InvalidHttpMethod, 13 | 14 | #[msg("Invalid number of workers")] 15 | InvalidWorkers, 16 | } 17 | -------------------------------------------------------------------------------- /programs/webhook/src/instructions/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod webhook_create; 2 | pub mod webhook_respond; 3 | 4 | pub use webhook_create::*; 5 | pub use webhook_respond::*; 6 | -------------------------------------------------------------------------------- /programs/webhook/src/instructions/webhook_create.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, mem::size_of}; 2 | 3 | use anchor_lang::{ 4 | prelude::*, 5 | solana_program::system_program, 6 | system_program::{transfer, Transfer}, 7 | }; 8 | 9 | use crate::state::{Relayer, HttpMethod, Webhook, SEED_WEBHOOK}; 10 | 11 | static WEBHOOK_FEE: u64 = 1_000_000; 12 | 13 | #[derive(Accounts)] 14 | #[instruction( 15 | body: Vec, 16 | // headers: HashMap, 17 | id: Vec, 18 | method: HttpMethod, 19 | url: String 20 | )] 21 | pub struct WebhookCreate<'info> { 22 | #[account()] 23 | pub authority: Signer<'info>, 24 | 25 | #[account(mut)] 26 | pub payer: Signer<'info>, 27 | 28 | #[account( 29 | init, 30 | seeds = [ 31 | SEED_WEBHOOK, 32 | authority.key().as_ref(), 33 | id.as_slice(), 34 | ], 35 | bump, 36 | space = 8 + size_of::(), 37 | payer = payer 38 | )] 39 | pub webhook: Account<'info, Webhook>, 40 | 41 | #[account(address = system_program::ID)] 42 | pub system_program: Program<'info, System>, 43 | } 44 | 45 | pub fn handler<'info>( 46 | ctx: Context, 47 | body: Vec, 48 | // headers: HashMap, 49 | id: Vec, 50 | method: HttpMethod, 51 | url: String, 52 | ) -> Result<()> { 53 | // Get accounts 54 | let authority = &ctx.accounts.authority; 55 | let payer = &mut ctx.accounts.payer; 56 | let webhook = &mut ctx.accounts.webhook; 57 | let system_program = &ctx.accounts.system_program; 58 | 59 | // Initialize the webhook account 60 | let current_slot = Clock::get().unwrap().slot; 61 | webhook.authority = authority.key(); 62 | webhook.body = body; 63 | webhook.created_at = current_slot; 64 | // webhook.headers = headers; 65 | webhook.id = id; 66 | webhook.method = method; 67 | webhook.relayer = Relayer::Clockwork; 68 | webhook.url = url; 69 | 70 | // Transfer fees into webhook account to hold in escrow. 71 | transfer( 72 | CpiContext::new( 73 | system_program.to_account_info(), 74 | Transfer { 75 | from: payer.to_account_info(), 76 | to: webhook.to_account_info(), 77 | }, 78 | ), 79 | WEBHOOK_FEE, 80 | )?; 81 | 82 | Ok(()) 83 | } 84 | -------------------------------------------------------------------------------- /programs/webhook/src/instructions/webhook_respond.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::state::{Webhook, SEED_WEBHOOK}, 3 | anchor_lang::{prelude::*, system_program}, 4 | }; 5 | 6 | static TIMEOUT_THRESHOLD: u64 = 100; // 100 slots 7 | 8 | #[derive(Accounts)] 9 | #[instruction()] 10 | pub struct WebhookRespond<'info> { 11 | #[account(mut)] 12 | pub ack_authority: Signer<'info>, 13 | 14 | #[account( 15 | mut, 16 | seeds = [ 17 | SEED_WEBHOOK, 18 | webhook.authority.as_ref(), 19 | ], 20 | bump, 21 | // close = caller, 22 | // has_one = caller 23 | )] 24 | pub webhook: Account<'info, Webhook>, 25 | 26 | #[account(address = system_program::ID)] 27 | pub system_program: Program<'info, System>, 28 | 29 | #[account()] 30 | pub worker: SystemAccount<'info>, 31 | } 32 | 33 | pub fn handler<'info>(ctx: Context) -> Result<()> { 34 | // Get accounts 35 | // let fee = &mut ctx.accounts.fee; 36 | let webhook = &mut ctx.accounts.webhook; 37 | let worker = &mut ctx.accounts.worker; 38 | 39 | // Payout webhook fee 40 | let current_slot = Clock::get().unwrap().slot; 41 | let is_authorized_worker = webhook.workers.contains(&worker.key()); 42 | let is_within_execution_window = 43 | current_slot < webhook.created_at.checked_add(TIMEOUT_THRESHOLD).unwrap(); 44 | if is_authorized_worker && is_within_execution_window { 45 | // Pay worker for executing webhook 46 | // fee.pay_to_worker(webhook)?; 47 | } else { 48 | // Either someone is spamming or this webhook has timed out. Do not pay worker. 49 | // TODO Perhaps rather than being paid to the admin, this could be put in an escrow account where all workers could claim equal rewards. 50 | // TODO If not claimed within X slots, the admin can claim their rewards and close the account. 51 | // fee.pay_to_admin(webhook)?; 52 | } 53 | 54 | Ok(()) 55 | } 56 | -------------------------------------------------------------------------------- /programs/webhook/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod errors; 2 | pub mod state; 3 | 4 | mod instructions; 5 | 6 | use anchor_lang::prelude::*; 7 | use instructions::*; 8 | use state::*; 9 | 10 | declare_id!("E7p5KFo8kKCDm6BUnWtnVFkQSYh6ZA6xaGAuvpv8NXTa"); 11 | 12 | #[program] 13 | pub mod webhook_program { 14 | pub use super::*; 15 | 16 | pub fn webhook_create<'info>( 17 | ctx: Context, 18 | body: Vec, 19 | // headers: std::collections::HashMap, 20 | id: Vec, 21 | method: HttpMethod, 22 | url: String, 23 | ) -> Result<()> { 24 | webhook_create::handler(ctx, body, 25 | // headers, 26 | id, method, url) 27 | } 28 | 29 | pub fn webhook_respond<'info>(ctx: Context) -> Result<()> { 30 | webhook_respond::handler(ctx) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /programs/webhook/src/state/mod.rs: -------------------------------------------------------------------------------- 1 | mod webhook; 2 | 3 | pub use webhook::*; 4 | -------------------------------------------------------------------------------- /programs/webhook/src/state/webhook.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | fmt::{Display, Formatter}, 4 | str::FromStr, 5 | }; 6 | 7 | use anchor_lang::{prelude::*, AnchorDeserialize}; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | use crate::errors::ClockworkError; 11 | 12 | pub const SEED_WEBHOOK: &[u8] = b"webhook"; 13 | 14 | #[account] 15 | #[derive(Debug, Deserialize, Serialize)] 16 | pub struct Webhook { 17 | pub authority: Pubkey, 18 | pub body: Vec, 19 | pub created_at: u64, 20 | // pub headers: HashMap, 21 | pub id: Vec, 22 | pub method: HttpMethod, 23 | pub relayer: Relayer, 24 | pub url: String, 25 | pub workers: Vec, 26 | } 27 | 28 | impl Webhook { 29 | pub fn pubkey(authority: Pubkey, id: Vec) -> Pubkey { 30 | Pubkey::find_program_address( 31 | &[SEED_WEBHOOK, authority.as_ref(), id.as_slice()], 32 | &crate::ID, 33 | ) 34 | .0 35 | } 36 | } 37 | 38 | /// WebhookAccount ... 39 | pub trait WebhookAccount { 40 | fn pubkey(&self) -> Pubkey; 41 | } 42 | 43 | impl WebhookAccount for Account<'_, Webhook> { 44 | fn pubkey(&self) -> Pubkey { 45 | Webhook::pubkey(self.authority, self.id.clone()) 46 | } 47 | } 48 | 49 | /// HttpMethod 50 | #[derive(AnchorDeserialize, AnchorSerialize, Deserialize, Serialize, Clone, Debug, PartialEq)] 51 | pub enum HttpMethod { 52 | Get, 53 | Post, 54 | } 55 | 56 | impl Display for HttpMethod { 57 | fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { 58 | match *self { 59 | HttpMethod::Get => write!(f, "GET"), 60 | HttpMethod::Post => write!(f, "POST"), 61 | } 62 | } 63 | } 64 | 65 | impl FromStr for HttpMethod { 66 | type Err = Error; 67 | 68 | fn from_str(input: &str) -> std::result::Result { 69 | match input.to_uppercase().as_str() { 70 | "GET" => Ok(HttpMethod::Get), 71 | "POST" => Ok(HttpMethod::Post), 72 | _ => Err(ClockworkError::InvalidHttpMethod.into()), 73 | } 74 | } 75 | } 76 | 77 | #[derive(AnchorDeserialize, AnchorSerialize, Deserialize, Serialize, Debug, Clone, PartialEq)] 78 | pub enum Relayer { 79 | Clockwork, 80 | Custom(String), 81 | } 82 | -------------------------------------------------------------------------------- /relayer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "clockwork-relayer" 3 | version = "2.0.20" 4 | edition = "2021" 5 | description = "Clockwork relayer for webhook requests" 6 | license = "AGPL-3.0-or-later" 7 | homepage = "https://clockwork.xyz" 8 | repository = "https://github.com/clockwork-xyz/relayer" 9 | documentation = "https://docs.clockwork.xyz" 10 | readme = "./README.md" 11 | keywords = ["solana"] 12 | publish = false 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [dependencies] 17 | actix-cors = "0.6.4" 18 | actix-web = "4.3.1" 19 | anchor-lang = "0.30.0" 20 | byte-unit = "4.0.18" 21 | clockwork-webhook-program = { path = "../programs/webhook", version = "=2.0.20" } 22 | clockwork-relayer-api = { path = "api", version = "=2.0.20" } 23 | curve25519-dalek = "3.2.1" 24 | lazy_static = "1.4.0" 25 | reqwest = "0.11.14" 26 | serde = "1.0.152" 27 | serde_json = "1.0.94" 28 | solana-client = "^1.18.14" 29 | solana-zk-token-sdk = "^1.18.14" 30 | solana-sdk = "^1.18.14" 31 | tokio = "1.26.0" 32 | bincode = "1.3.3" 33 | rayon = "1.7.0" 34 | regex = "1.7.1" 35 | -------------------------------------------------------------------------------- /relayer/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-clockwork/clockwork/84f200aa089a0b1ac95223cb78d96482a9127e23/relayer/README.md -------------------------------------------------------------------------------- /relayer/api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "clockwork-relayer-api" 3 | version = "2.0.20" 4 | edition = "2021" 5 | description = "Clockwork relayer for webhook requests" 6 | license = "AGPL-3.0-or-later" 7 | homepage = "https://clockwork.xyz" 8 | repository = "https://github.com/clockwork-xyz/relayer" 9 | documentation = "https://docs.clockwork.xyz" 10 | readme = "./README.md" 11 | keywords = ["solana"] 12 | 13 | [lib] 14 | crate-type = ["cdylib", "lib"] 15 | name = "clockwork_relayer_api" 16 | 17 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 18 | 19 | [dependencies] 20 | # actix-web = "4.3.1" 21 | # byte-unit = "4.0.18" 22 | # curve25519-dalek = "3.2.1" 23 | # lazy_static = "1.4.0" 24 | serde = "1.0.152" 25 | # shadow-drive-sdk = "0.6.1" 26 | # solana-zk-token-sdk = "^1.18.14" 27 | solana-sdk = "^1.18.14" 28 | # tokio = "1.26.0" 29 | bincode = "1.3.3" 30 | -------------------------------------------------------------------------------- /relayer/api/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-clockwork/clockwork/84f200aa089a0b1ac95223cb78d96482a9127e23/relayer/api/README.md -------------------------------------------------------------------------------- /relayer/api/src/lib.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use solana_sdk::{pubkey::Pubkey, signature::Signature}; 3 | 4 | #[derive(Deserialize, Serialize)] 5 | pub struct SignedRequest { 6 | pub msg: T, 7 | pub signer: Pubkey, 8 | pub signature: Signature, 9 | } 10 | 11 | impl SignedRequest { 12 | pub fn authenticate(&self) -> bool { 13 | let msg_bytes = bincode::serialize(&self.msg).unwrap(); 14 | self.signature 15 | .verify(&self.signer.to_bytes(), msg_bytes.as_slice()) 16 | } 17 | } 18 | 19 | #[derive(Deserialize, Serialize)] 20 | pub struct Relay { 21 | pub webhook: Pubkey, 22 | } 23 | 24 | #[derive(Deserialize, Serialize)] 25 | pub struct SecretCreate { 26 | pub name: String, 27 | pub word: String, 28 | } 29 | 30 | #[derive(Deserialize, Serialize)] 31 | pub struct SecretGet { 32 | pub name: String, 33 | } 34 | 35 | #[derive(Deserialize, Serialize)] 36 | pub struct SecretList {} 37 | 38 | #[derive(Deserialize, Serialize)] 39 | pub struct SecretListResponse { 40 | pub secrets: Vec, 41 | } 42 | 43 | #[derive(Deserialize, Serialize)] 44 | pub struct SecretApprove { 45 | pub name: String, 46 | pub delegate: Pubkey, 47 | } 48 | 49 | #[derive(Deserialize, Serialize)] 50 | pub struct SecretRevoke { 51 | pub name: String, 52 | pub delegate: Pubkey, 53 | } 54 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.78.0" 3 | components = [ "rustfmt", "rust-analyzer" ] 4 | profile = "minimal" 5 | targets = [ "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "aarch64-apple-darwin"] 6 | -------------------------------------------------------------------------------- /scripts/build-all.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | usage() { 6 | exitcode=0 7 | if [[ -n "$1" ]]; then 8 | exitcode=1 9 | echo "Error: $*" 10 | fi 11 | cat <] [--release] [--target ] 13 | EOF 14 | exit $exitcode 15 | } 16 | 17 | # Set default target triple from 'cargo -vV' 18 | defaultTargetTriple=$(cargo -vV | grep 'host:' | cut -d ' ' -f2) 19 | 20 | # Set build flags 21 | 22 | # Setup rust the default rust-version 23 | maybeRustVersion= 24 | installDir= 25 | buildVariant=debug 26 | maybeReleaseFlag= 27 | targetTriple="$defaultTargetTriple" 28 | while [[ -n $1 ]]; do 29 | if [[ ${1:0:1} = - ]]; then 30 | case $1 in 31 | --release) 32 | maybeReleaseFlag=--release 33 | buildVariant=release 34 | shift 35 | ;; 36 | --target) 37 | targetTriple=$2 38 | shift 2 39 | ;; 40 | *) 41 | usage "Unknown option: $1" 42 | ;; 43 | esac 44 | elif [[ ${1:0:1} = + ]]; then 45 | maybeRustVersion=${1:1} 46 | shift 47 | else 48 | installDir=$1 49 | shift 50 | fi 51 | done 52 | 53 | # If target triple is still unset, use default 54 | if [[ -z "$targetTriple" ]]; then 55 | targetTriple="$defaultTargetTriple" 56 | fi 57 | 58 | 59 | if [ -z "$maybeRustVersion" ]; then 60 | source scripts/ci/rust-version.sh 61 | maybeRustVersion="$rust_stable" 62 | else 63 | rustup install "$maybeRustVersion" 64 | fi 65 | 66 | # Print final configuration 67 | echo "Using Rust version: $maybeRustVersion" 68 | echo "Build variant: $buildVariant" 69 | echo "Target triple: $targetTriple" 70 | echo "Install directory: $installDir" 71 | echo "Release flag: ${maybeReleaseFlag:---not-set}" 72 | 73 | # Check the install directory is provided 74 | if [[ -z "$installDir" ]]; then 75 | usage "Install directory not specified" 76 | exit 1 77 | fi 78 | 79 | # Create the install directory 80 | installDir="$(mkdir -p "$installDir"; cd "$installDir"; pwd)" 81 | mkdir -p "$installDir/lib" 82 | mkdir -p "$installDir/bin" 83 | echo "Install location: $installDir ($buildVariant)" 84 | cd "$(dirname "$0")"/.. 85 | SECONDS=0 86 | 87 | # Enumerate the bins 88 | BINS=( 89 | clockwork 90 | ) 91 | 92 | # Create bin args 93 | binArgs=() 94 | for bin in "${BINS[@]}"; do 95 | binArgs+=(--bin "$bin") 96 | done 97 | 98 | # Build programs 99 | ( 100 | set -x 101 | anchor build --arch sbf 102 | ) 103 | 104 | # Define lib extension 105 | case $targetTriple in 106 | *darwin*) 107 | pluginFilename=libclockwork_plugin.dylib 108 | ;; 109 | *) 110 | pluginFilename=libclockwork_plugin.so 111 | ;; 112 | esac 113 | 114 | # Build the repo 115 | ( 116 | set -x 117 | cargo +"$maybeRustVersion" build --locked $maybeReleaseFlag "${binArgs[@]}" --lib --target "$targetTriple" 118 | # Copy binaries 119 | case $targetTriple in 120 | *darwin*) 121 | pluginFilename=libclockwork_plugin.dylib 122 | cp -fv "target/$targetTriple/$buildVariant/$pluginFilename" "$installDir"/lib 123 | mv "$installDir"/lib/libclockwork_plugin.dylib "$installDir"/lib/libclockwork_plugin.so 124 | 125 | cp -fv "$installDir"/lib/libclockwork_plugin.so "$installDir" 126 | ;; 127 | *) 128 | pluginFilename=libclockwork_plugin.so 129 | cp -fv "target/$targetTriple/$buildVariant/$pluginFilename" "$installDir"/lib 130 | 131 | cp -fv "target/$targetTriple/$buildVariant/$pluginFilename" "$installDir" 132 | ;; 133 | esac 134 | 135 | for bin in "${BINS[@]}"; do 136 | rm -fv "$installDir/bin/$bin" 137 | cp -fv "target/$targetTriple/$buildVariant/$bin" "$installDir/bin" 138 | 139 | cp -fv "target/$targetTriple/$buildVariant/$bin" "$installDir" 140 | done 141 | 142 | cp -fv "target/deploy/clockwork_network_program.so" "$installDir/lib" 143 | cp -fv "target/deploy/clockwork_thread_program.so" "$installDir/lib" 144 | cp -fv "target/deploy/clockwork_webhook_program.so" "$installDir/lib" 145 | 146 | cp -fv "target/deploy/clockwork_network_program.so" "$installDir" 147 | cp -fv "target/deploy/clockwork_thread_program.so" "$installDir" 148 | cp -fv "target/deploy/clockwork_webhook_program.so" "$installDir" 149 | ) 150 | 151 | # Success message 152 | echo "Done after $SECONDS seconds" 153 | echo 154 | echo "To use these binaries:" 155 | echo " export PATH=\"$installDir\"/bin:\"\$PATH\"" 156 | -------------------------------------------------------------------------------- /scripts/cargo-publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Publish shared libs 4 | cargo publish -p clockwork-cron 5 | sleep 25 6 | cargo publish -p clockwork-macros 7 | sleep 25 8 | cargo publish -p clockwork-utils 9 | sleep 25 10 | 11 | # Publish programs 12 | cargo publish -p clockwork-network-program 13 | sleep 25 14 | cargo publish -p clockwork-thread-program 15 | sleep 25 16 | cargo publish -p clockwork-webhook-program 17 | sleep 25 18 | 19 | # Publish SDK 20 | cargo publish -p clockwork-sdk 21 | sleep 25 22 | 23 | # Publish downstream bins and libs 24 | # These are most likely to fail due to Anchor dependency issues. 25 | cargo publish -p clockwork-client 26 | sleep 25 27 | cargo publish -p clockwork-cli 28 | -------------------------------------------------------------------------------- /scripts/ci/bump-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | if [[ $# -eq 0 ]]; then 6 | echo "Usage: $0 [--dry-run] []" 7 | exit 1 8 | fi 9 | 10 | bump=$1 11 | shift 12 | 13 | while [[ $# -gt 0 ]]; do 14 | case $1 in 15 | --dry-run) 16 | dry_run="--dry-run" 17 | ;; 18 | *) 19 | args+=("$1") 20 | ;; 21 | esac 22 | shift 23 | done 24 | 25 | # Print current version 26 | current_version=$(cat ./VERSION) 27 | echo "Current version: $current_version" 28 | 29 | # Run cargo set-version 30 | cargo set-version --locked --workspace --bump "$bump" $dry_run "${args[@]}" 31 | if [ -n "$dry_run" ]; then 32 | echo "Dry run, exiting..." 33 | exit 0 34 | fi 35 | 36 | # We need to retrieve the actual semver version from the Cargo.toml files 37 | actual_version=$(cargo metadata --format-version=1 | jq -r '.packages[] | select(.name == "clockwork-sdk") | .version') 38 | echo $actual_version >VERSION 39 | echo "New version: $actual_version" 40 | 41 | # Export these so that they can be extracted from a Github Actions 42 | export old_version="$current_version" 43 | export new_version="$actual_version" 44 | -------------------------------------------------------------------------------- /scripts/ci/create-tarball.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | usage() { 5 | echo "Usage: $0 [--target ]" 6 | exit 1 7 | } 8 | 9 | TARGET=$(cargo -vV | awk '/host:/ {print $2}') 10 | while [[ $# -gt 0 ]]; do 11 | case "$1" in 12 | --target) 13 | TARGET=$2 14 | shift 2 15 | ;; 16 | *) 17 | usage 18 | ;; 19 | esac 20 | done 21 | 22 | RELEASE_BASENAME="${RELEASE_BASENAME:=clockwork-geyser-plugin-release}" 23 | TARBALL_BASENAME="${TARBALL_BASENAME:="$RELEASE_BASENAME"}" 24 | 25 | echo --- Creating release tarball 26 | ( 27 | var=$(pwd) 28 | echo "The current working directory $var" 29 | 30 | set -x 31 | rm -rf "${RELEASE_BASENAME:?}"/ 32 | mkdir "${RELEASE_BASENAME}"/ 33 | 34 | COMMIT="$(git rev-parse HEAD)" 35 | ( 36 | echo "channel: $CI_TAG" 37 | echo "commit: $COMMIT" 38 | echo "target: $TARGET" 39 | ) > "${RELEASE_BASENAME}"/version.yml 40 | 41 | var=$(pwd) 42 | echo "The current working directory $var" 43 | 44 | source ./scripts/ci/rust-version.sh stable 45 | ./scripts/build-all.sh +"${rust_stable:?}" --release --target "$TARGET" "${RELEASE_BASENAME}" 46 | 47 | RELEASE_NAME="${TARBALL_BASENAME}-${TARGET}" 48 | 49 | tar cvf "$RELEASE_NAME".tar "${RELEASE_BASENAME}" 50 | bzip2 -f "$RELEASE_NAME".tar 51 | cp -fv "${RELEASE_BASENAME}"/version.yml "$RELEASE_NAME".yml 52 | ) 53 | 54 | echo --- ok 55 | -------------------------------------------------------------------------------- /scripts/ci/rust-version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Source: 4 | # https://github.com/solana-labs/solana-accountsdb-plugin-postgres/blob/master/ci/rust-version.sh 5 | 6 | # 7 | # This file maintains the rust versions for use by CI. 8 | # 9 | # Obtain the environment variables without any automatic toolchain updating: 10 | # $ source ci/rust-version.sh 11 | # 12 | # Obtain the environment variables updating both stable and nightly, only stable, or 13 | # only nightly: 14 | # $ source ci/rust-version.sh all 15 | # $ source ci/rust-version.sh stable 16 | # $ source ci/rust-version.sh nightly 17 | 18 | # Then to build with either stable or nightly: 19 | # $ cargo +"$rust_stable" build 20 | # $ cargo +"$rust_nightly" build 21 | # 22 | 23 | if [[ -n $RUST_STABLE_VERSION ]]; then 24 | stable_version="$RUST_STABLE_VERSION" 25 | else 26 | stable_version=1.78.0 27 | fi 28 | 29 | if [[ -n $RUST_NIGHTLY_VERSION ]]; then 30 | nightly_version="$RUST_NIGHTLY_VERSION" 31 | else 32 | nightly_version=2022-02-24 33 | fi 34 | 35 | 36 | export rust_stable="$stable_version" 37 | export rust_stable_docker_image=solanalabs/rust:"$stable_version" 38 | 39 | export rust_nightly=nightly-"$nightly_version" 40 | export rust_nightly_docker_image=solanalabs/rust-nightly:"$nightly_version" 41 | 42 | [[ -z $1 ]] || ( 43 | rustup_install() { 44 | declare toolchain=$1 45 | if ! cargo +"$toolchain" -V > /dev/null; then 46 | echo "$0: Missing toolchain? Installing...: $toolchain" >&2 47 | rustup install "$toolchain" 48 | cargo +"$toolchain" -V 49 | fi 50 | } 51 | 52 | set -e 53 | cd "$(dirname "${BASH_SOURCE[0]}")" 54 | case $1 in 55 | stable) 56 | rustup_install "$rust_stable" 57 | ;; 58 | nightly) 59 | rustup_install "$rust_nightly" 60 | ;; 61 | all) 62 | rustup_install "$rust_stable" 63 | rustup_install "$rust_nightly" 64 | ;; 65 | *) 66 | echo "$0: Note: ignoring unknown argument: $1" >&2 67 | ;; 68 | esac 69 | ) 70 | -------------------------------------------------------------------------------- /scripts/ci/solana-version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Prints the Solana version. 4 | 5 | set -e 6 | 7 | cd "$(dirname "$0")/../../plugin" 8 | 9 | cargo read-manifest | jq -r '.dependencies[] | select(.name == "solana-geyser-plugin-interface") | .req' -------------------------------------------------------------------------------- /scripts/debug_plugin.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | # Rebuid programs 6 | rm -rf lib/clockwork_thread_program.so 7 | cd programs/thread && anchor build; cd -; 8 | cp -fv target/deploy/clockwork_thread_program.so lib/ 9 | 10 | # Rebuild plugin 11 | rm -rf lib/libclockwork_plugin.dylib 12 | cargo build --manifest-path plugin/Cargo.toml 13 | cp -fv target/debug/libclockwork_plugin.dylib lib/ 14 | 15 | # bpf-program 16 | crate_name="hello_clockwork" 17 | cd ~/examples/$crate_name 18 | anchor build 19 | cd - 20 | 21 | # Clean ledger 22 | rm -rf test-ledger 23 | 24 | RUST_LOG=clockwork_plugin clockwork localnet \ 25 | --bpf-program ~/examples/$crate_name/target/deploy/$crate_name-keypair.json \ 26 | --bpf-program ~/examples/$crate_name/target/deploy/$crate_name.so 27 | 28 | -------------------------------------------------------------------------------- /scripts/mint-token.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Create mint 4 | log=$(spl-token create-token | grep 'Creating token') 5 | mint=${log: -44} 6 | 7 | # Create token account 8 | log=$(spl-token create-account $mint | grep 'Creating account') 9 | account=${log: -44} 10 | 11 | # Mint 10 tokens to the current keypair 12 | balance=10 13 | spl-token mint $mint $balance 14 | -------------------------------------------------------------------------------- /scripts/refresh-program-ids.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Delete target folder 4 | cargo clean 5 | 6 | # Build with Anchor 7 | anchor build 8 | 9 | # Get pubkey addresses 10 | program_id_network=$(solana address -k target/deploy/clockwork_network_program-keypair.json) 11 | program_id_thread=$(solana address -k target/deploy/clockwork_thread_program-keypair.json) 12 | program_id_webhook=$(solana address -k target/deploy/clockwork_webhook_program-keypair.json) 13 | 14 | # Update declared program IDs 15 | sed -i '' -e 's/^declare_id!(".*");/declare_id!("'${program_id_network}'");/g' programs/network/src/lib.rs 16 | sed -i '' -e 's/^declare_id!(".*");/declare_id!("'${program_id_thread}'");/g' programs/thread/src/lib.rs 17 | sed -i '' -e 's/^declare_id!(".*");/declare_id!("'${program_id_webhook}'");/g' programs/webhook/src/lib.rs 18 | 19 | # Update Anchor config 20 | sed -i '' -e 's/^clockwork_network_program = ".*"/clockwork_network_program = "'${program_id_network}'"/g' Anchor.toml 21 | sed -i '' -e 's/^clockwork_thread_program = ".*"/clockwork_thread_program = "'${program_id_thread}'"/g' Anchor.toml 22 | sed -i '' -e 's/^clockwork_webhook_program = ".*"/clockwork_webhook_program = "'${program_id_webhook}'"/g' Anchor.toml 23 | 24 | # Rebuild 25 | anchor build 26 | -------------------------------------------------------------------------------- /scripts/stake-localnet.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Stake local node with the Clockwork network 4 | cd cli 5 | cargo run -- node register ../test-ledger/validator-keypair.json 6 | sleep 2 7 | cargo run -- node stake 100000000000 $(solana address -k ../test-ledger/validator-keypair.json) # 100 tokens 8 | -------------------------------------------------------------------------------- /scripts/update-solana.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Get new version 4 | read -r -p " New version: " new_version 5 | 6 | # Find all Cargo.toml files 7 | cargo_tomls=($(find . -type f -name "Cargo.toml")) 8 | 9 | # Find and replace with new_version 10 | for cargo_toml in "${cargo_tomls[@]}"; do 11 | sed -i '' -e "/^solana-/s/=.*/= \"$new_version\"/g" $cargo_toml 12 | done 13 | 14 | # Find and replace version in dockerfile 15 | sed -i '' -e "/^ENV SOLANA_VERSION=v/s/v.*/v"$new_version"/g" './Dockerfile' 16 | 17 | 18 | -------------------------------------------------------------------------------- /sdk/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-clockwork/clockwork/84f200aa089a0b1ac95223cb78d96482a9127e23/sdk/.gitignore -------------------------------------------------------------------------------- /sdk/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "clockwork-sdk" 3 | version = "2.0.20" 4 | description = "An SDK for building automated programs on Solana" 5 | edition = "2021" 6 | license = "AGPL-3.0-or-later" 7 | homepage = "https://clockwork.xyz" 8 | repository = "https://github.com/clockwork-xyz/clockwork" 9 | documentation = "https://docs.clockwork.xyz" 10 | readme = "./README.md" 11 | keywords = ["solana"] 12 | 13 | [lib] 14 | name = "clockwork_sdk" 15 | 16 | [dependencies] 17 | anchor-lang = "0.30.0" 18 | chrono = { version = "0.4.19", default-features = false, features = ["alloc"] } 19 | clockwork-thread-program = { path = "../programs/thread", features = ["cpi"], version = "=2.0.20" } 20 | nom = "~7" 21 | once_cell = "1.5.2" 22 | 23 | [features] 24 | default = [] 25 | idl-build = ["anchor-lang/idl-build"] 26 | -------------------------------------------------------------------------------- /sdk/README.md: -------------------------------------------------------------------------------- 1 | # Clockwork SDK 2 | -------------------------------------------------------------------------------- /sdk/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use clockwork_thread_program::errors; 2 | pub use clockwork_thread_program::program::ThreadProgram; 3 | pub use clockwork_thread_program::ID; 4 | 5 | pub mod state { 6 | pub use clockwork_thread_program::state::{ 7 | ClockData, ExecContext, SerializableAccount, SerializableInstruction, Thread, 8 | ThreadAccount, ThreadResponse, ThreadSettings, Trigger, TriggerContext, 9 | }; 10 | } 11 | 12 | pub mod utils { 13 | pub use clockwork_thread_program::state::PAYER_PUBKEY; 14 | pub use clockwork_thread_program::state::Equality; 15 | } 16 | 17 | pub mod cpi { 18 | use anchor_lang::prelude::{CpiContext, Result}; 19 | 20 | pub use clockwork_thread_program::cpi::accounts::{ 21 | ThreadCreate, ThreadDelete, ThreadPause, ThreadReset, ThreadResume, ThreadUpdate, 22 | ThreadWithdraw, 23 | }; 24 | 25 | pub fn thread_create<'info>( 26 | ctx: CpiContext<'_, '_, '_, 'info, ThreadCreate<'info>>, 27 | amount: u64, 28 | id: Vec, 29 | instructions: Vec, 30 | trigger: crate::state::Trigger, 31 | ) -> Result<()> { 32 | clockwork_thread_program::cpi::thread_create(ctx, amount, id, instructions, trigger) 33 | } 34 | 35 | pub fn thread_delete<'info>( 36 | ctx: CpiContext<'_, '_, '_, 'info, ThreadDelete<'info>>, 37 | ) -> Result<()> { 38 | clockwork_thread_program::cpi::thread_delete(ctx) 39 | } 40 | 41 | pub fn thread_pause<'info>( 42 | ctx: CpiContext<'_, '_, '_, 'info, ThreadPause<'info>>, 43 | ) -> Result<()> { 44 | clockwork_thread_program::cpi::thread_pause(ctx) 45 | } 46 | 47 | pub fn thread_resume<'info>( 48 | ctx: CpiContext<'_, '_, '_, 'info, ThreadResume<'info>>, 49 | ) -> Result<()> { 50 | clockwork_thread_program::cpi::thread_resume(ctx) 51 | } 52 | 53 | pub fn thread_reset<'info>( 54 | ctx: CpiContext<'_, '_, '_, 'info, ThreadReset<'info>>, 55 | ) -> Result<()> { 56 | clockwork_thread_program::cpi::thread_reset(ctx) 57 | } 58 | 59 | pub fn thread_update<'info>( 60 | ctx: CpiContext<'_, '_, '_, 'info, ThreadUpdate<'info>>, 61 | settings: crate::state::ThreadSettings, 62 | ) -> Result<()> { 63 | clockwork_thread_program::cpi::thread_update(ctx, settings) 64 | } 65 | 66 | pub fn thread_withdraw<'info>( 67 | ctx: CpiContext<'_, '_, '_, 'info, ThreadWithdraw<'info>>, 68 | amount: u64, 69 | ) -> Result<()> { 70 | clockwork_thread_program::cpi::thread_withdraw(ctx, amount) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "clockwork-utils" 3 | version = "2.0.20" 4 | description = "Tools for building blocks on Solana" 5 | edition = "2021" 6 | license = "AGPL-3.0-or-later" 7 | homepage = "https://clockwork.xyz" 8 | repository = "https://github.com/clockwork-xyz/clockwork" 9 | documentation = "https://docs.clockwork.xyz" 10 | readme = "./README.md" 11 | keywords = ["solana"] 12 | 13 | [lib] 14 | name = "clockwork_utils" 15 | 16 | [features] 17 | idl-build = ["anchor-lang/idl-build"] 18 | 19 | [dependencies] 20 | anchor-lang = "0.30.0" 21 | base64 = "~0.13" 22 | serde = { version = "1.0", features = ["derive"] } 23 | serde_json = "1.0" 24 | static-pubkey = "1.0.3" 25 | 26 | -------------------------------------------------------------------------------- /utils/README.md: -------------------------------------------------------------------------------- 1 | Clockwork utils -------------------------------------------------------------------------------- /utils/src/explorer.rs: -------------------------------------------------------------------------------- 1 | const EXPLORER_URL: &str = "https://explorer.solana.com"; 2 | const CK_EXPLORER_URL: &str = "https://explorer.clockwork.xyz"; 3 | 4 | 5 | #[derive(Default)] 6 | pub struct Explorer { 7 | cluster: String, 8 | custom_rpc: Option, 9 | } 10 | 11 | impl From for Explorer { 12 | fn from(json_rpc_url: String) -> Self { 13 | match &json_rpc_url.to_lowercase() { 14 | url if url.contains("devnet") => Explorer::devnet(), 15 | url if url.contains("testnet") => Explorer::testnet(), 16 | url if url.contains("mainnet") => Explorer::mainnet(), 17 | _ => { 18 | Explorer::custom(json_rpc_url) 19 | } 20 | } 21 | } 22 | } 23 | 24 | impl Explorer { 25 | pub fn mainnet() -> Self { 26 | Self { 27 | cluster: "mainnet-beta".into(), 28 | ..Default::default() 29 | } 30 | } 31 | 32 | pub fn testnet() -> Self { 33 | Self { 34 | cluster: "testnet".into(), 35 | ..Default::default() 36 | } 37 | } 38 | 39 | pub fn devnet() -> Self { 40 | Self { 41 | cluster: "devnet".into(), 42 | ..Default::default() 43 | } 44 | } 45 | 46 | pub fn custom(custom_rpc: String) -> Self { 47 | Self { 48 | cluster: "custom".into(), 49 | custom_rpc: Some(custom_rpc), 50 | } 51 | } 52 | 53 | /// Ex: https://explorer.solana.com/tx/{tx} 54 | /// ?cluster=custom 55 | /// &customUrl=http://localhost:8899 56 | pub fn tx_url(&self, tx: T) -> String { 57 | let url = format!("{}/tx/{}?cluster={}", EXPLORER_URL, tx, self.cluster); 58 | if self.cluster == "custom" { 59 | url + "&customUrl=" + self.custom_rpc.as_ref().unwrap() 60 | } else { 61 | url 62 | } 63 | } 64 | 65 | /// Ex: https://explorer.clockwork.xyz/thread/{thread} 66 | /// ?network=custom 67 | /// &customRPC=http://localhost:8899 68 | pub fn thread_url(&self, thread: T, program_id: U) -> String { 69 | let url = format!("{}/address/{}?programID={}&network={}", CK_EXPLORER_URL, 70 | thread, program_id, self 71 | .cluster); 72 | if self.cluster == "custom" { 73 | url + "&customRPC=" + self.custom_rpc.as_ref().unwrap() 74 | } else { 75 | url 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /utils/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod explorer; 2 | pub mod pubkey; 3 | pub mod thread; 4 | 5 | use std::fmt::{Debug, Display, Formatter}; 6 | 7 | use anchor_lang::{prelude::Pubkey, prelude::*, AnchorDeserialize}; 8 | use base64; 9 | 10 | /// Crate build information 11 | #[derive(AnchorDeserialize, AnchorSerialize, Clone, Debug)] 12 | pub struct CrateInfo { 13 | /// The link to the crate spec 14 | pub spec: String, 15 | /// Arbitrary blob that can be set by developers 16 | pub blob: String, 17 | } 18 | 19 | impl Display for CrateInfo { 20 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 21 | write!(f, "spec:{} blob:{}", self.spec, self.blob) 22 | } 23 | } 24 | 25 | /// Parse struct from sol_set_return_data in program's logs 26 | pub trait ProgramLogsDeserializable { 27 | fn try_from_program_logs( 28 | program_logs: Vec, 29 | program_id: &Pubkey, 30 | ) -> std::result::Result 31 | where 32 | Self: Sized; 33 | } 34 | 35 | impl ProgramLogsDeserializable for T 36 | where 37 | T: AnchorDeserialize, 38 | { 39 | fn try_from_program_logs( 40 | program_logs: Vec, 41 | program_id: &Pubkey, 42 | ) -> std::result::Result { 43 | // A Program's return data appears in the log in this format: 44 | // "Program return: " 45 | // https://github.com/solana-labs/solana/blob/b8837c04ec3976c9c16d028fbee86f87823fb97f/program-runtime/src/stable_log.rs#L68 46 | let preimage = format!("Program return: {} ", program_id.to_string()); 47 | 48 | // Extract the return data after Program return: 49 | let get_return_data_base64 = program_logs 50 | .iter() 51 | .find(|&s| s.starts_with(&preimage)) 52 | .ok_or(ErrorCode::AccountDidNotDeserialize)? 53 | .strip_prefix(&preimage) 54 | .ok_or(ErrorCode::AccountDidNotDeserialize)?; 55 | 56 | let decoded = base64::decode(get_return_data_base64) 57 | .map_err(|_err| ErrorCode::AccountDidNotDeserialize)?; 58 | 59 | T::try_from_slice(&decoded).map_err(|_err| ErrorCode::AccountDidNotDeserialize) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /utils/src/pubkey.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::Pubkey; 2 | 3 | pub trait Abbreviated { 4 | fn abbreviated(&self) -> String; 5 | } 6 | 7 | impl Abbreviated for Pubkey { 8 | fn abbreviated(&self) -> String { 9 | let s = self.to_string(); 10 | let len = s.len(); 11 | format!("{}..{}", s.get(0..4).unwrap(), s.get(len - 4..len).unwrap()).to_string() 12 | // format!("{}{}", s.get(len - 8..len).unwrap()).to_string() 13 | } 14 | } 15 | --------------------------------------------------------------------------------