├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── rust-toolchain.toml └── src ├── balance.rs ├── claim.rs ├── claim_stake_rewards.rs ├── database.rs ├── delegate_boost.rs ├── earnings.rs ├── generate_key.rs ├── main.rs ├── migrate_boosts_to_v2.rs ├── mine.rs ├── minepmc.rs ├── protomine.rs ├── signup.rs ├── stake_balance.rs ├── stats.rs ├── undelegate_boost.rs └── undelegate_stake.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | app_db.db3 3 | keypair_list 4 | return 5 | /pmc -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ore-hq-client" 3 | version = "4.1.0" 4 | edition = "2021" 5 | description = "Ore mining pool client." 6 | license = "Apache-2.0" 7 | repository = "https://github.com/Kriptikz/ore-hq-client" 8 | keywords = ["solana", "crypto", "mining", "client", "mining-pool"] 9 | 10 | [dependencies] 11 | base64 = "0.22.1" 12 | bincode = "1.3.3" 13 | clap = { version = "4.5.13", features = ["derive"] } 14 | core_affinity = "0.8.1" 15 | ore-api = "2.2.0" 16 | ore-boost-api = "0.2.0" 17 | ore-utils = "2.1.8" 18 | drillx_2 = "1.1.0" 19 | futures-util = "0.3.30" 20 | reqwest = { version = "^0.11.0", features = ["json", "rustls-tls"] } 21 | rpassword = "7.3.1" 22 | solana-sdk = "1.18.21" 23 | tokio = { version = "1.39.2", features = ["full"] } 24 | tokio-tungstenite = { version = "0.23.1", features = ["native-tls"] } 25 | url = "2.5.2" 26 | spl-token = "6.0.0" 27 | rayon = "1.10" 28 | crossbeam = "0.8.0" 29 | rand = "0.8.4" 30 | rand_chacha = "0.3.0" 31 | inquire = "0.7.5" 32 | dirs = "5.0.1" 33 | colored = "2.0" 34 | indicatif = "0.17" 35 | spl-associated-token-account = { version = "2.2", features = ["no-entrypoint"] } 36 | ore-miner-delegation = { version = "0.6.0", features = ["no-entrypoint"] } 37 | tiny-bip39 = "0.8.2" 38 | qrcode = "0.14.1" 39 | rusqlite = { version = "0.32.1", features = ["bundled"] } 40 | serde = { version = "1.0", features = ["derive"] } 41 | serde_json = "1.0" 42 | semver = "1.0" 43 | 44 | # PMC Additional Packages 45 | mimalloc = "0.1.43" 46 | chrono = "0.4.38" 47 | once_cell = "1.19.0" 48 | http = "1.1.0" 49 | 50 | [profile.release] 51 | opt-level = 3 # Full optimisations 52 | codegen-units = 1 # Better optimization with fewer codegen units 53 | lto = true # Enable Link Time Optimization (LTO) 54 | debug = false # Disable debug info to reduce binary size 55 | panic = 'abort' # Reduces the binary size further by not including unwinding information 56 | rpath = false 57 | incremental = false 58 | overflow-checks = false 59 | 60 | [build] 61 | rustflags = ["-C", "target-cpu=native"] 62 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.77.0" 3 | components = [ "rustfmt", "rust-analyzer" ] 4 | profile = "minimal" 5 | -------------------------------------------------------------------------------- /src/balance.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{signature::Keypair, signer::Signer}; 2 | 3 | use crate::claim_stake_rewards::StakeAccount; 4 | // use std::collections::HashMap; 5 | // use tokio::time::{sleep, Duration}; 6 | 7 | pub async fn balance(key: &Keypair, url: String, unsecure: bool) { 8 | let base_url = url; 9 | let client = reqwest::Client::new(); 10 | 11 | let url_prefix = if unsecure { 12 | "http".to_string() 13 | } else { 14 | "https".to_string() 15 | }; 16 | 17 | println!("Wallet: {}", key.pubkey().to_string()); 18 | 19 | // Fetch Wallet (Stakeable) Balance 20 | let balance_response = client 21 | .get(format!( 22 | "{}://{}/miner/balance?pubkey={}", 23 | url_prefix, 24 | base_url, 25 | key.pubkey().to_string() 26 | )) 27 | .send() 28 | .await 29 | .unwrap() 30 | .text() 31 | .await 32 | .unwrap(); 33 | 34 | let _balance = match balance_response.parse::() { 35 | Ok(b) => b, 36 | Err(_) => 0.0, 37 | }; 38 | 39 | // Fetch Unclaimed Rewards 40 | let rewards_response = client 41 | .get(format!( 42 | "{}://{}/miner/rewards?pubkey={}", 43 | url_prefix, 44 | base_url, 45 | key.pubkey().to_string() 46 | )) 47 | .send() 48 | .await 49 | .unwrap() 50 | .text() 51 | .await 52 | .unwrap(); 53 | 54 | let rewards = match rewards_response.parse::() { 55 | Ok(r) => r, 56 | Err(_) => 0.0, 57 | }; 58 | 59 | // Fetch Staked Balance 60 | let stake_response = client 61 | .get(format!( 62 | "{}://{}/miner/stake?pubkey={}", 63 | url_prefix, 64 | base_url, 65 | key.pubkey().to_string() 66 | )) 67 | .send() 68 | .await 69 | .unwrap() 70 | .text() 71 | .await 72 | .unwrap(); 73 | 74 | let staked_balance = if stake_response.contains("Failed to g") { 75 | println!(" Delegated stake balance: No staked account"); 76 | 0.0 77 | } else { 78 | stake_response.parse::().unwrap_or(0.0) 79 | }; 80 | 81 | // Fetch Unclaimed Stake Rewards 82 | let staker_rewards_response = client 83 | .get(format!( 84 | "{}://{}/v2/miner/boost/stake-accounts?pubkey={}", 85 | url_prefix, 86 | base_url, 87 | key.pubkey().to_string() 88 | )) 89 | .send() 90 | .await 91 | .unwrap() 92 | .text() 93 | .await 94 | .unwrap(); 95 | let stake_accounts: Vec = match serde_json::from_str(&staker_rewards_response) { 96 | Ok(sa) => { 97 | sa 98 | }, 99 | Err(_) => { 100 | println!("Failed to parse server stake accounts."); 101 | return; 102 | } 103 | }; 104 | 105 | println!(); 106 | println!("Staker Rewards:"); 107 | let mut total_staker_rewards = 0.0f64; 108 | for stake_account in stake_accounts { 109 | let claimable_rewards = stake_account.rewards_balance as f64 / 10f64.powf(ore_api::consts::TOKEN_DECIMALS as f64); 110 | println!(" {} - {:.11} ORE", stake_account.mint_pubkey, claimable_rewards); 111 | total_staker_rewards += claimable_rewards; 112 | } 113 | 114 | println!(); 115 | println!(" Unclaimed Mining Rewards: {:.11} ORE", rewards); 116 | println!(" Unclaimed Staker Rewards: {:.11} ORE", total_staker_rewards); 117 | println!(" Staked Balance: {:.11} ORE", staked_balance); 118 | println!(); 119 | 120 | let token_mints = vec![ 121 | ("oreoU2P8bN6jkk3jbaiVxYnG1dCXcYxwhwyK9jSybcp", "ORE Token"), 122 | ("DrSS5RM7zUd9qjUEdDaf31vnDUSbCrMto6mjqTrHFifN", "ORE-SOL LP"), 123 | ("meUwDp23AaxhiNKaQCyJ2EAF2T4oe1gSkEkGXSRVdZb", "ORE-ISC LP"), 124 | ]; 125 | 126 | print!("In Wallet (Stakeable):\n"); 127 | for (mint, label) in token_mints.iter() { 128 | let token_balance = 129 | get_token_balance(key, base_url.clone(), unsecure, mint.to_string()).await; 130 | println!(" {}: {}", label, token_balance); 131 | } 132 | println!(); 133 | println!("Boosted:"); 134 | for (mint, label) in token_mints.iter() { 135 | let boosted_token_balance = 136 | get_boosted_stake_balance_v2(key, base_url.clone(), unsecure, mint.to_string()).await; 137 | println!(" {}: {}", label, boosted_token_balance.max(0.0)); 138 | } 139 | } 140 | 141 | pub async fn get_token_balance(key: &Keypair, url: String, unsecure: bool, mint: String) -> f64 { 142 | let client = reqwest::Client::new(); 143 | let url_prefix = if unsecure { "http" } else { "https" }; 144 | 145 | let balance_response = client 146 | .get(format!( 147 | "{}://{}/v2/miner/balance?pubkey={}&mint={}", 148 | url_prefix, 149 | url, 150 | key.pubkey().to_string(), 151 | mint 152 | )) 153 | .send() 154 | .await 155 | .unwrap() 156 | .text() 157 | .await 158 | .unwrap(); 159 | 160 | balance_response.parse::().unwrap_or(0.0) 161 | } 162 | 163 | pub async fn get_boosted_stake_balance( 164 | key: &Keypair, 165 | url: String, 166 | unsecure: bool, 167 | mint: String, 168 | ) -> f64 { 169 | let client = reqwest::Client::new(); 170 | let url_prefix = if unsecure { "http" } else { "https" }; 171 | 172 | let balance_response = client 173 | .get(format!( 174 | "{}://{}/miner/boost/stake?pubkey={}&mint={}", 175 | url_prefix, 176 | url, 177 | key.pubkey().to_string(), 178 | mint 179 | )) 180 | .send() 181 | .await 182 | .unwrap() 183 | .text() 184 | .await 185 | .unwrap(); 186 | 187 | balance_response.parse::().unwrap_or(-1.0) 188 | } 189 | 190 | pub async fn get_boosted_stake_balance_v2( 191 | key: &Keypair, 192 | url: String, 193 | unsecure: bool, 194 | mint: String, 195 | ) -> f64 { 196 | let client = reqwest::Client::new(); 197 | let url_prefix = if unsecure { "http" } else { "https" }; 198 | 199 | let balance_response = client 200 | .get(format!( 201 | "{}://{}/v2/miner/boost/stake?pubkey={}&mint={}", 202 | url_prefix, 203 | url, 204 | key.pubkey().to_string(), 205 | mint 206 | )) 207 | .send() 208 | .await 209 | .unwrap() 210 | .text() 211 | .await 212 | .unwrap(); 213 | 214 | balance_response.parse::().unwrap_or(-1.0) 215 | } 216 | -------------------------------------------------------------------------------- /src/claim.rs: -------------------------------------------------------------------------------- 1 | use base64::{prelude::BASE64_STANDARD, Engine}; 2 | use clap::Parser; 3 | use colored::*; 4 | use inquire::{InquireError, Text}; 5 | use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer}; 6 | use spl_token::amount_to_ui_amount; 7 | use std::{str::FromStr, time::Duration}; 8 | 9 | #[derive(Debug, Parser)] 10 | pub struct ClaimArgs { 11 | #[arg( 12 | long, 13 | short('r'), 14 | value_name = "RECEIVER_PUBKEY", 15 | help = "Wallet Public Key to receive the claimed Ore to." 16 | )] 17 | pub receiver_pubkey: Option, 18 | #[arg( 19 | long, 20 | value_name = "AMOUNT", 21 | help = "Amount of ore to claim. (Minimum of 0.005 ORE)" 22 | )] 23 | pub amount: Option, 24 | #[arg(long, short, action, help = "Auto approve confirmations.")] 25 | pub y: bool, 26 | } 27 | 28 | pub async fn claim(args: ClaimArgs, key: Keypair, url: String, unsecure: bool) { 29 | let client = reqwest::Client::new(); 30 | let url_prefix = if unsecure { 31 | "http".to_string() 32 | } else { 33 | "https".to_string() 34 | }; 35 | 36 | let receiver_pubkey = match args.receiver_pubkey { 37 | Some(rpk) => match Pubkey::from_str(&rpk) { 38 | Ok(pk) => pk, 39 | Err(_) => { 40 | println!("Failed to parse provided receiver pubkey.\nDouble check the provided public key is valid and try again."); 41 | return; 42 | } 43 | }, 44 | None => key.pubkey(), 45 | }; 46 | 47 | let balance_response = client 48 | .get(format!( 49 | "{}://{}/miner/balance?pubkey={}", 50 | url_prefix, 51 | url, 52 | receiver_pubkey.to_string() 53 | )) 54 | .send() 55 | .await 56 | .unwrap() 57 | .text() 58 | .await 59 | .unwrap(); 60 | 61 | let balance = if let Ok(parsed_balance) = balance_response.parse::() { 62 | parsed_balance 63 | } else { 64 | // If the wallet balance failed to parse 65 | println!("\n Note: A 0.004 ORE fee will be deducted from your claim amount to cover the cost\n of Token Account Creation. This is a one time fee used to create the ORE Token Account."); 66 | 0.0 67 | }; 68 | 69 | let rewards_response = client 70 | .get(format!( 71 | "{}://{}/miner/rewards?pubkey={}", 72 | url_prefix, 73 | url, 74 | key.pubkey().to_string() 75 | )) 76 | .send() 77 | .await 78 | .unwrap() 79 | .text() 80 | .await 81 | .unwrap(); 82 | 83 | let rewards = rewards_response.parse::().unwrap_or(0.0); 84 | 85 | println!(" Miner Unclaimed Rewards: {:.11} ORE", rewards); 86 | println!(" Receiving Wallet Ore Balance: {:.11} ORE", balance); 87 | 88 | let minimum_claim_amount = 0.005; 89 | if rewards < minimum_claim_amount { 90 | println!(); 91 | println!(" You have not reached the required claim limit of 0.005 ORE."); 92 | println!(" Keep mining to accumulate more rewards before you can withdraw."); 93 | return; 94 | } 95 | 96 | // Convert balance to grains 97 | let balance_grains = (rewards * 10f64.powf(ore_api::consts::TOKEN_DECIMALS as f64)) as u64; 98 | 99 | // If balance is zero, inform the user and return to keypair selection 100 | if balance_grains == 0 { 101 | println!("\n There is no balance to claim."); 102 | return; 103 | } 104 | 105 | let mut claim_amount = args.amount.unwrap_or(rewards); 106 | 107 | // Prompt the user for an amount if it's not provided or less than 0.005 108 | loop { 109 | if claim_amount < minimum_claim_amount { 110 | if claim_amount != 0.0 { 111 | // Only show the message if they previously entered an invalid value 112 | println!(" Please enter a number above 0.005."); 113 | } 114 | 115 | match Text::new("\n Enter the amount to claim (minimum 0.005 ORE or 'esc' to cancel):") 116 | .prompt() 117 | { 118 | Ok(input) => { 119 | if input.trim().eq_ignore_ascii_case("esc") { 120 | println!(" Claim operation canceled."); 121 | return; 122 | } 123 | 124 | claim_amount = match input.trim().parse::() { 125 | Ok(val) if val >= 0.005 => val, 126 | _ => { 127 | println!(" Please enter a valid number above 0.005."); 128 | continue; 129 | } 130 | }; 131 | } 132 | Err(InquireError::OperationCanceled) => { 133 | println!(" Claim operation canceled."); 134 | return; 135 | } 136 | Err(_) => { 137 | println!(" Invalid input. Please try again."); 138 | continue; 139 | } 140 | } 141 | } else { 142 | break; 143 | } 144 | } 145 | 146 | // Convert the claim amount to the smallest unit 147 | let mut claim_amount_grains = 148 | (claim_amount * 10f64.powf(ore_api::consts::TOKEN_DECIMALS as f64)) as u64; 149 | 150 | // Auto-adjust the claim amount if it exceeds the available balance 151 | if claim_amount_grains > balance_grains { 152 | println!( 153 | " You do not have enough rewards to claim {} ORE.", 154 | amount_to_ui_amount(claim_amount_grains, ore_api::consts::TOKEN_DECIMALS) 155 | ); 156 | claim_amount_grains = balance_grains; 157 | println!( 158 | " Adjusting claim amount to the maximum available: {} ORE.", 159 | amount_to_ui_amount(claim_amount_grains, ore_api::consts::TOKEN_DECIMALS) 160 | ); 161 | } 162 | 163 | // RED TEXT 164 | if !args.y { 165 | match Text::new( 166 | &format!( 167 | " Are you sure you want to claim {} ORE? (Y/n or 'esc' to cancel)", 168 | amount_to_ui_amount(claim_amount_grains, ore_api::consts::TOKEN_DECIMALS) 169 | ) 170 | .red() 171 | .to_string(), 172 | ) 173 | .prompt() 174 | { 175 | Ok(confirm) => { 176 | if confirm.trim().eq_ignore_ascii_case("esc") { 177 | println!(" Claim canceled."); 178 | return; 179 | } else if confirm.trim().is_empty() || confirm.trim().to_lowercase() == "y" { 180 | } else { 181 | println!(" Claim canceled."); 182 | return; 183 | } 184 | } 185 | Err(InquireError::OperationCanceled) => { 186 | println!(" Claim operation canceled."); 187 | return; 188 | } 189 | Err(_) => { 190 | println!(" Invalid input. Claim canceled."); 191 | return; 192 | } 193 | } 194 | } 195 | 196 | let timestamp = if let Ok(response) = client 197 | .get(format!("{}://{}/timestamp", url_prefix, url)) 198 | .send() 199 | .await 200 | { 201 | if let Ok(ts) = response.text().await { 202 | if let Ok(ts) = ts.parse::() { 203 | ts 204 | } else { 205 | println!("Failed to get timestamp from server, please try again."); 206 | return; 207 | } 208 | } else { 209 | println!("Failed to get timestamp from server, please try again."); 210 | return; 211 | } 212 | } else { 213 | println!("Failed to get timestamp from server, please try again."); 214 | return; 215 | }; 216 | 217 | println!( 218 | " Sending claim request for {} ORE...", 219 | amount_to_ui_amount(claim_amount_grains, ore_api::consts::TOKEN_DECIMALS) 220 | ); 221 | 222 | let mut signed_msg = vec![]; 223 | signed_msg.extend(timestamp.to_le_bytes()); 224 | signed_msg.extend(receiver_pubkey.to_bytes()); 225 | signed_msg.extend(claim_amount_grains.to_le_bytes()); 226 | 227 | let sig = key.sign_message(&signed_msg); 228 | let auth = BASE64_STANDARD.encode(format!("{}:{}", key.pubkey(), sig)); 229 | 230 | let resp = client 231 | .post(format!( 232 | "{}://{}/v2/claim?timestamp={}&receiver_pubkey={}&amount={}", 233 | url_prefix, 234 | url, 235 | timestamp, 236 | receiver_pubkey.to_string(), 237 | claim_amount_grains 238 | )) 239 | .header("Authorization", format!("Basic {}", auth)) 240 | .send() 241 | .await; 242 | 243 | match resp { 244 | Ok(res) => match res.text().await.unwrap().as_str() { 245 | "SUCCESS" => { 246 | println!(" Successfully queued claim request!"); 247 | } 248 | "QUEUED" => { 249 | println!(" Claim is already queued for processing."); 250 | } 251 | other => { 252 | if let Ok(time) = other.parse::() { 253 | let time_left = 1800 - time; 254 | let secs = time_left % 60; 255 | let mins = (time_left / 60) % 60; 256 | println!( 257 | " You cannot claim until the time is up. Time left until next claim available: {}m {}s", 258 | mins, secs 259 | ); 260 | } else { 261 | println!(" Unexpected response: {}", other); 262 | } 263 | } 264 | }, 265 | Err(e) => { 266 | println!(" ERROR: {}", e); 267 | println!(" Retrying in 5 seconds..."); 268 | tokio::time::sleep(Duration::from_secs(5)).await; 269 | } 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/claim_stake_rewards.rs: -------------------------------------------------------------------------------- 1 | use base64::{prelude::BASE64_STANDARD, Engine}; 2 | use clap::Parser; 3 | use colored::*; 4 | use inquire::{InquireError, Text}; 5 | use serde::{Deserialize, Serialize}; 6 | use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer}; 7 | use spl_token::amount_to_ui_amount; 8 | use std::{str::FromStr, time::Duration}; 9 | 10 | #[derive(Debug, Parser)] 11 | pub struct ClaimStakeRewardsArgs { 12 | #[arg( 13 | long("mint"), 14 | short('m'), 15 | value_name = "BOOST_MINT", 16 | help = "Mint of staked boost account to claim from." 17 | )] 18 | pub mint_pubkey: String, 19 | #[arg( 20 | long, 21 | short('r'), 22 | value_name = "RECEIVER_PUBKEY", 23 | help = "Wallet Public Key to receive the claimed Ore to." 24 | )] 25 | pub receiver_pubkey: Option, 26 | #[arg( 27 | long, 28 | value_name = "AMOUNT", 29 | help = "Amount of ore to claim. (Minimum of 0.005 ORE)" 30 | )] 31 | pub amount: Option, 32 | #[arg(long, short, action, help = "Auto approve confirmations.")] 33 | pub y: bool, 34 | } 35 | 36 | #[derive(Debug, Clone, Serialize, Deserialize)] 37 | pub struct StakeAccount { 38 | pub id: i32, 39 | pub pool_id: i32, 40 | pub mint_pubkey: String, 41 | pub staker_pubkey: String, 42 | pub stake_pda: String, 43 | pub rewards_balance: u64, 44 | pub staked_balance: u64, 45 | } 46 | 47 | 48 | pub async fn claim_stake_rewards(args: ClaimStakeRewardsArgs, key: Keypair, url: String, unsecure: bool) { 49 | let client = reqwest::Client::new(); 50 | let url_prefix = if unsecure { 51 | "http".to_string() 52 | } else { 53 | "https".to_string() 54 | }; 55 | 56 | let receiver_pubkey = match args.receiver_pubkey { 57 | Some(rpk) => match Pubkey::from_str(&rpk) { 58 | Ok(pk) => pk, 59 | Err(_) => { 60 | println!("Failed to parse provided receiver pubkey.\nDouble check the provided public key is valid and try again."); 61 | return; 62 | } 63 | }, 64 | None => key.pubkey(), 65 | }; 66 | 67 | let mint_pubkey = match Pubkey::from_str(&args.mint_pubkey) { 68 | Ok(pk) => { 69 | pk 70 | }, 71 | Err(_) => { 72 | println!("Failed to parse provided mint pubkey.\nDouble check the provided public key is valid and try again."); 73 | return; 74 | } 75 | }; 76 | 77 | let balance_response = client 78 | .get(format!( 79 | "{}://{}/miner/balance?pubkey={}", 80 | url_prefix, 81 | url, 82 | receiver_pubkey.to_string() 83 | )) 84 | .send() 85 | .await 86 | .unwrap() 87 | .text() 88 | .await 89 | .unwrap(); 90 | 91 | let mut has_deduction = false; 92 | let balance = if let Ok(parsed_balance) = balance_response.parse::() { 93 | parsed_balance 94 | } else { 95 | // If the wallet balance failed to parse 96 | has_deduction = true; 97 | println!("\n Note: A 0.004 ORE fee will be deducted from your claim amount to cover the cost\n of Token Account Creation. This is a one time fee used to create the ORE Token Account."); 98 | 0.0 99 | }; 100 | 101 | let stake_accounts = client 102 | .get(format!( 103 | "{}://{}/v2/miner/boost/stake-accounts?pubkey={}", 104 | url_prefix, 105 | url, 106 | receiver_pubkey.to_string() 107 | )) 108 | .send() 109 | .await 110 | .unwrap() 111 | .text() 112 | .await 113 | .unwrap(); 114 | 115 | let stake_accounts: Vec = match serde_json::from_str(&stake_accounts) { 116 | Ok(sa) => { 117 | sa 118 | }, 119 | Err(_) => { 120 | println!("Failed to parse server stake accounts."); 121 | return; 122 | } 123 | }; 124 | 125 | let mut stake_account = stake_accounts[0].clone(); 126 | let mut found_stake = false; 127 | 128 | for sa in stake_accounts { 129 | if sa.mint_pubkey == args.mint_pubkey { 130 | stake_account = sa.clone(); 131 | found_stake = true; 132 | break; 133 | } 134 | } 135 | 136 | if !found_stake { 137 | println!("Failed to find stake account for mint: {}", args.mint_pubkey); 138 | return; 139 | } 140 | 141 | 142 | let rewards = stake_account.rewards_balance as f64 / 10f64.powf(ore_api::consts::TOKEN_DECIMALS as f64); 143 | 144 | println!(" Stake Mint: {}", args.mint_pubkey); 145 | println!(" Unclaimed Stake Rewards: {:.11} ORE", rewards); 146 | println!(" Receiving Wallet Ore Balance: {:.11} ORE", balance); 147 | 148 | let minimum_claim_amount = 0.005; 149 | if has_deduction { 150 | if rewards < minimum_claim_amount { 151 | println!(); 152 | println!(" You have not reached the required claim limit of 0.005 ORE."); 153 | println!(" Keep accumulating more rewards before you can withdraw."); 154 | return; 155 | } 156 | } 157 | 158 | // Convert balance to grains 159 | let balance_grains = stake_account.rewards_balance; 160 | 161 | // If balance is zero, inform the user and return to keypair selection 162 | if balance_grains == 0 { 163 | println!("\n There is no rewards to claim."); 164 | return; 165 | } 166 | 167 | let mut claim_amount = args.amount.unwrap_or(rewards); 168 | 169 | // Prompt the user for an amount if it's not provided or less than 0.005 170 | loop { 171 | if has_deduction && claim_amount < minimum_claim_amount { 172 | if claim_amount != 0.0 { 173 | // Only show the message if they previously entered an invalid value 174 | println!(" Please enter a number above 0.005."); 175 | } 176 | 177 | match Text::new("\n Enter the amount to claim (minimum 0.005 ORE or 'esc' to cancel):") 178 | .prompt() 179 | { 180 | Ok(input) => { 181 | if input.trim().eq_ignore_ascii_case("esc") { 182 | println!(" Claim operation canceled."); 183 | return; 184 | } 185 | 186 | claim_amount = match input.trim().parse::() { 187 | Ok(val) if val >= 0.005 => val, 188 | _ => { 189 | println!(" Please enter a valid number above 0.005."); 190 | continue; 191 | } 192 | }; 193 | } 194 | Err(InquireError::OperationCanceled) => { 195 | println!(" Claim operation canceled."); 196 | return; 197 | } 198 | Err(_) => { 199 | println!(" Invalid input. Please try again."); 200 | continue; 201 | } 202 | } 203 | } else { 204 | break; 205 | } 206 | } 207 | 208 | // Convert the claim amount to the smallest unit 209 | let mut claim_amount_grains = 210 | (claim_amount * 10f64.powf(ore_api::consts::TOKEN_DECIMALS as f64)) as u64; 211 | 212 | // Auto-adjust the claim amount if it exceeds the available balance 213 | if claim_amount_grains > balance_grains { 214 | println!( 215 | " You do not have enough rewards to claim {} ORE.", 216 | amount_to_ui_amount(claim_amount_grains, ore_api::consts::TOKEN_DECIMALS) 217 | ); 218 | claim_amount_grains = balance_grains; 219 | println!( 220 | " Adjusting claim amount to the maximum available: {} ORE.", 221 | amount_to_ui_amount(claim_amount_grains, ore_api::consts::TOKEN_DECIMALS) 222 | ); 223 | } 224 | 225 | // RED TEXT 226 | if !args.y { 227 | match Text::new( 228 | &format!( 229 | " Are you sure you want to claim {} ORE? (Y/n or 'esc' to cancel)", 230 | amount_to_ui_amount(claim_amount_grains, ore_api::consts::TOKEN_DECIMALS) 231 | ) 232 | .red() 233 | .to_string(), 234 | ) 235 | .prompt() 236 | { 237 | Ok(confirm) => { 238 | if confirm.trim().eq_ignore_ascii_case("esc") { 239 | println!(" Claim canceled."); 240 | return; 241 | } else if confirm.trim().is_empty() || confirm.trim().to_lowercase() == "y" { 242 | } else { 243 | println!(" Claim canceled."); 244 | return; 245 | } 246 | } 247 | Err(InquireError::OperationCanceled) => { 248 | println!(" Claim operation canceled."); 249 | return; 250 | } 251 | Err(_) => { 252 | println!(" Invalid input. Claim canceled."); 253 | return; 254 | } 255 | } 256 | } 257 | 258 | let timestamp = if let Ok(response) = client 259 | .get(format!("{}://{}/timestamp", url_prefix, url)) 260 | .send() 261 | .await 262 | { 263 | if let Ok(ts) = response.text().await { 264 | if let Ok(ts) = ts.parse::() { 265 | ts 266 | } else { 267 | println!("Failed to get timestamp from server, please try again."); 268 | return; 269 | } 270 | } else { 271 | println!("Failed to get timestamp from server, please try again."); 272 | return; 273 | } 274 | } else { 275 | println!("Failed to get timestamp from server, please try again."); 276 | return; 277 | }; 278 | 279 | println!( 280 | " Sending claim request for {} ORE...", 281 | amount_to_ui_amount(claim_amount_grains, ore_api::consts::TOKEN_DECIMALS) 282 | ); 283 | 284 | let mut signed_msg = vec![]; 285 | signed_msg.extend(timestamp.to_le_bytes()); 286 | signed_msg.extend(mint_pubkey.to_bytes()); 287 | signed_msg.extend(receiver_pubkey.to_bytes()); 288 | signed_msg.extend(claim_amount_grains.to_le_bytes()); 289 | 290 | let sig = key.sign_message(&signed_msg); 291 | let auth = BASE64_STANDARD.encode(format!("{}:{}", key.pubkey(), sig)); 292 | 293 | let resp = client 294 | .post(format!( 295 | "{}://{}/v2/claim-stake-rewards?timestamp={}&mint={}&receiver_pubkey={}&amount={}", 296 | url_prefix, 297 | url, 298 | timestamp, 299 | mint_pubkey.to_string(), 300 | receiver_pubkey.to_string(), 301 | claim_amount_grains 302 | )) 303 | .header("Authorization", format!("Basic {}", auth)) 304 | .send() 305 | .await; 306 | 307 | match resp { 308 | Ok(res) => match res.text().await.unwrap().as_str() { 309 | "SUCCESS" => { 310 | println!(" Successfully queued claim request!"); 311 | } 312 | "QUEUED" => { 313 | println!(" Claim is already queued for processing."); 314 | } 315 | other => { 316 | println!(" Unexpected response: {}", other); 317 | // if let Ok(time) = other.parse::() { 318 | // let time_left = 1800 - time; 319 | // let secs = time_left % 60; 320 | // let mins = (time_left / 60) % 60; 321 | // println!( 322 | // " You cannot claim until the time is up. Time left until next claim available: {}m {}s", 323 | // mins, secs 324 | // ); 325 | // } else { 326 | // } 327 | } 328 | }, 329 | Err(e) => { 330 | println!(" ERROR: {}", e); 331 | println!(" Retrying in 5 seconds..."); 332 | tokio::time::sleep(Duration::from_secs(5)).await; 333 | } 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /src/database.rs: -------------------------------------------------------------------------------- 1 | use std::{path::Path, sync::RwLock}; 2 | 3 | use rusqlite::Connection; 4 | 5 | pub struct PoolSubmissionResult { 6 | _id: i32, 7 | pool_difficulty: u32, 8 | pool_earned: u64, 9 | miner_percentage: f64, 10 | miner_difficulty: u32, 11 | miner_earned: u64, 12 | _created_at: u64, 13 | } 14 | 15 | impl PoolSubmissionResult { 16 | pub fn new( 17 | pool_difficulty: u32, 18 | pool_earned: u64, 19 | miner_percentage: f64, 20 | miner_difficulty: u32, 21 | miner_earned: u64, 22 | ) -> Self { 23 | PoolSubmissionResult { 24 | _id: 0, 25 | pool_difficulty, 26 | pool_earned, 27 | miner_percentage, 28 | miner_difficulty, 29 | miner_earned, 30 | _created_at: 0, 31 | } 32 | } 33 | } 34 | 35 | pub struct AppDatabase { 36 | connection: RwLock, 37 | } 38 | 39 | impl AppDatabase { 40 | pub fn new() -> Self { 41 | let conn = match Connection::open(Path::new("./app_db.db3")) { 42 | Ok(c) => { 43 | match c.execute( 44 | r#"CREATE TABLE IF NOT EXISTS pool_submission_results ( 45 | id INTEGER PRIMARY KEY, 46 | pool_difficulty INTEGER NOT NULL, 47 | pool_earned INTEGER NOT NULL, 48 | miner_percentage NUMERIC NOT NULL, 49 | miner_difficulty INTEGER NOT NULL, 50 | miner_earned INTEGER NOT NULL, 51 | created_at INTEGER DEFAULT CURRENT_TIMESTAMP NOT NULL 52 | )"#, 53 | (), 54 | ) { 55 | Ok(_) => c, 56 | Err(e) => { 57 | eprintln!("Error creating pool_submission_results table!"); 58 | panic!("Error: {e}"); 59 | } 60 | } 61 | } 62 | Err(_e) => { 63 | panic!("Failed to open app database"); 64 | } 65 | }; 66 | AppDatabase { 67 | connection: RwLock::new(conn), 68 | } 69 | } 70 | 71 | pub fn add_new_pool_submission(&self, new_pool_submission_result: PoolSubmissionResult) { 72 | if let Err(e) = self.connection.write().unwrap().execute( 73 | r#"INSERT INTO pool_submission_results ( 74 | pool_difficulty, 75 | pool_earned, 76 | miner_percentage, 77 | miner_difficulty, 78 | miner_earned 79 | ) VALUES (?1, ?2, ?3, ?4, ?5)"#, 80 | ( 81 | &new_pool_submission_result.pool_difficulty, 82 | &new_pool_submission_result.pool_earned, 83 | &new_pool_submission_result.miner_percentage, 84 | &new_pool_submission_result.miner_difficulty, 85 | &new_pool_submission_result.miner_earned, 86 | ), 87 | ) { 88 | eprintln!("Error: Failed to insert pool submission result.\nE: {e}"); 89 | } 90 | } 91 | 92 | pub fn get_todays_earnings(&self) -> u64 { 93 | match self.connection.write().unwrap().prepare( 94 | r#"SELECT SUM(miner_earned) as total_earned 95 | FROM pool_submission_results 96 | WHERE created_at >= date('now', 'start of day') 97 | "#, 98 | ) { 99 | Ok(mut stmt) => { 100 | let total_earned: Option = stmt.query_row([], |row| row.get(0)).unwrap(); 101 | match total_earned { 102 | Some(sum) => return sum, 103 | None => return 0, 104 | } 105 | } 106 | Err(e) => { 107 | eprintln!("Error: Failed to get todays earnings.\nE: {e}"); 108 | return 0; 109 | } 110 | } 111 | } 112 | 113 | pub fn get_daily_earnings(&self, _days: u32) -> Vec<(String, u64)> { 114 | match self.connection.write().unwrap().prepare( 115 | r#"SELECT DATE(created_at) as day,SUM(miner_earned) as total_earned 116 | FROM pool_submission_results 117 | WHERE created_at >= date('now', '-6 days') 118 | GROUP BY DATE(created_at) 119 | ORDER BY DATE(created_at) 120 | "#, 121 | ) { 122 | Ok(mut stmt) => { 123 | let earnings_iter = stmt 124 | .query_map([], |row| { 125 | let day: String = row.get(0).unwrap(); 126 | let total_earned: u64 = row.get(1).unwrap(); 127 | Ok((day, total_earned)) 128 | }) 129 | .unwrap(); 130 | 131 | let mut earnings = vec![]; 132 | for earning in earnings_iter { 133 | match earning { 134 | Ok((day, total_earned)) => { 135 | earnings.push((day, total_earned)); 136 | } 137 | Err(_) => { 138 | eprintln!("Error getting earning"); 139 | } 140 | } 141 | } 142 | 143 | return earnings; 144 | } 145 | Err(e) => { 146 | eprintln!("Error: Failed to get todays earnings.\nE: {e}"); 147 | return vec![]; 148 | } 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/delegate_boost.rs: -------------------------------------------------------------------------------- 1 | use base64::{prelude::BASE64_STANDARD, Engine}; 2 | use clap::Parser; 3 | use colored::*; 4 | use inquire::{InquireError, Text}; 5 | use reqwest::StatusCode; 6 | use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer, transaction::Transaction}; 7 | use std::{str::FromStr, time::Duration}; 8 | 9 | use crate::balance::get_token_balance; 10 | 11 | #[derive(Debug, Parser)] 12 | pub struct BoostArgs { 13 | #[arg(long, value_name = "AMOUNT", help = "Amount of ore to stake.")] 14 | pub amount: f64, 15 | 16 | #[arg(long, value_name = "MINT", help = "Mint of boost.")] 17 | pub mint: String, 18 | 19 | #[arg( 20 | long, 21 | short, 22 | action, 23 | help = "Auto stake input amount when staking window opens." 24 | )] 25 | pub auto: bool, 26 | } 27 | 28 | pub async fn delegate_boost(args: BoostArgs, key: Keypair, url: String, unsecure: bool) { 29 | let base_url = url; 30 | let client = reqwest::Client::new(); 31 | let url_prefix = if unsecure { 32 | "http".to_string() 33 | } else { 34 | "https".to_string() 35 | }; 36 | let balance = get_token_balance(&key, base_url.clone(), unsecure, args.mint.clone()).await; 37 | 38 | // Ensure stake amount does not exceed balance 39 | let boost_amount = if args.amount > balance { 40 | println!( 41 | " You do not have enough to stake {} boost tokens.\n Adjusting stake amount to the maximum available: {} boost tokens", 42 | args.amount, balance 43 | ); 44 | balance 45 | } else { 46 | args.amount 47 | }; 48 | 49 | // RED TEXT 50 | match Text::new( 51 | &format!( 52 | " Are you sure you want to stake {} boost tokens? (Y/n or 'esc' to cancel)", 53 | boost_amount 54 | ) 55 | .red() 56 | .to_string(), 57 | ) 58 | .prompt() 59 | { 60 | Ok(confirm) => { 61 | if confirm.trim().eq_ignore_ascii_case("esc") { 62 | println!(" Boosting canceled."); 63 | return; 64 | } else if confirm.trim().is_empty() || confirm.trim().to_lowercase() == "y" { 65 | // Proceed with staking 66 | } else { 67 | println!(" Boosting canceled."); 68 | return; 69 | } 70 | } 71 | Err(InquireError::OperationCanceled) => { 72 | println!(" Boosting operation canceled."); 73 | return; 74 | } 75 | Err(_) => { 76 | println!(" Invalid input. Boosting canceled."); 77 | return; 78 | } 79 | } 80 | 81 | if !args.auto { 82 | // Non-auto staking logic 83 | let timestamp = get_timestamp(&client, &url_prefix, &base_url).await; 84 | println!(" Server Timestamp: {}", timestamp); 85 | if let Some(secs_passed_hour) = timestamp.checked_rem(600) { 86 | println!(" SECS PASSED HOUR: {}", secs_passed_hour); 87 | if secs_passed_hour < 300 { 88 | println!(" Staking window opened. Staking..."); 89 | } else { 90 | println!(" Staking window not currently open. Please use --auto or wait until the start of the next hour."); 91 | return; 92 | } 93 | } else { 94 | println!(" Timestamp checked_rem error. Please try again."); 95 | return; 96 | } 97 | } else { 98 | // Auto staking logic with retry mechanism 99 | loop { 100 | let timestamp = get_timestamp(&client, &url_prefix, &base_url).await; 101 | println!(" Server Timestamp: {}", timestamp); 102 | if let Some(secs_passed_hour) = timestamp.checked_rem(600) { 103 | if secs_passed_hour < 300 { 104 | println!(" Staking window opened. Staking..."); 105 | 106 | // Attempt staking transaction 107 | loop { 108 | let resp = client 109 | .get(format!( 110 | "{}://{}/pool/authority/pubkey", 111 | url_prefix, base_url 112 | )) 113 | .send() 114 | .await 115 | .unwrap() 116 | .text() 117 | .await 118 | .unwrap(); 119 | let pool_pubkey = Pubkey::from_str(&resp).unwrap(); 120 | 121 | let resp = client 122 | .get(format!( 123 | "{}://{}/pool/fee_payer/pubkey", 124 | url_prefix, base_url 125 | )) 126 | .send() 127 | .await 128 | .unwrap() 129 | .text() 130 | .await 131 | .unwrap(); 132 | let fee_pubkey = Pubkey::from_str(&resp).unwrap(); 133 | 134 | let resp = client 135 | .get(format!("{}://{}/latest-blockhash", url_prefix, base_url)) 136 | .send() 137 | .await 138 | .unwrap() 139 | .text() 140 | .await 141 | .unwrap(); 142 | let decoded_blockhash = BASE64_STANDARD.decode(resp).unwrap(); 143 | let deserialized_blockhash = 144 | bincode::deserialize(&decoded_blockhash).unwrap(); 145 | 146 | let boost_amount_u64 = (boost_amount 147 | * 10f64.powf(ore_api::consts::TOKEN_DECIMALS as f64)) 148 | as u64; 149 | let ix = ore_miner_delegation::instruction::delegate_boost_v2( 150 | key.pubkey(), 151 | pool_pubkey, 152 | Pubkey::from_str(&args.mint).unwrap(), 153 | boost_amount_u64, 154 | ); 155 | 156 | let mut tx = Transaction::new_with_payer(&[ix], Some(&fee_pubkey)); 157 | tx.partial_sign(&[&key], deserialized_blockhash); 158 | let serialized_tx = bincode::serialize(&tx).unwrap(); 159 | let encoded_tx = BASE64_STANDARD.encode(&serialized_tx); 160 | 161 | let resp = client 162 | .post(format!( 163 | "{}://{}/v2/stake-boost?pubkey={}&mint={}&amount={}", 164 | url_prefix, 165 | base_url, 166 | key.pubkey().to_string(), 167 | args.mint, 168 | boost_amount_u64 169 | )) 170 | .body(encoded_tx) 171 | .send() 172 | .await; 173 | 174 | if let Ok(res) = resp { 175 | if let Ok(txt) = res.text().await { 176 | match txt.as_str() { 177 | "SUCCESS" => { 178 | println!(" Successfully boosted!"); 179 | return; // Exit the loop and function when successful 180 | } 181 | other => { 182 | println!(" Transaction failed: {}", other); 183 | } 184 | } 185 | } else { 186 | println!(" Transaction failed, retrying..."); 187 | } 188 | } else { 189 | println!(" Transaction failed, retrying..."); 190 | } 191 | 192 | // Wait before trying again 193 | tokio::time::sleep(Duration::from_secs(3)).await; 194 | } 195 | } else { 196 | println!(" Staking window opens in {} minutes {} seconds.", (600 - secs_passed_hour) / 60, secs_passed_hour % 60); 197 | println!(" You can let this run until it is complete."); 198 | tokio::time::sleep(Duration::from_secs(60)).await; 199 | } 200 | } else { 201 | tokio::time::sleep(Duration::from_secs(120)).await; 202 | } 203 | } 204 | } 205 | 206 | // Non-auto and auto logic converge for transaction execution 207 | let resp = client 208 | .get(format!( 209 | "{}://{}/pool/authority/pubkey", 210 | url_prefix, base_url 211 | )) 212 | .send() 213 | .await 214 | .unwrap() 215 | .text() 216 | .await 217 | .unwrap(); 218 | let pool_pubkey = Pubkey::from_str(&resp).unwrap(); 219 | 220 | let resp = client 221 | .get(format!( 222 | "{}://{}/pool/fee_payer/pubkey", 223 | url_prefix, base_url 224 | )) 225 | .send() 226 | .await 227 | .unwrap() 228 | .text() 229 | .await 230 | .unwrap(); 231 | let fee_pubkey = Pubkey::from_str(&resp).unwrap(); 232 | 233 | let resp = client 234 | .get(format!("{}://{}/latest-blockhash", url_prefix, base_url)) 235 | .send() 236 | .await 237 | .unwrap() 238 | .text() 239 | .await 240 | .unwrap(); 241 | let decoded_blockhash = BASE64_STANDARD.decode(resp).unwrap(); 242 | let deserialized_blockhash = bincode::deserialize(&decoded_blockhash).unwrap(); 243 | 244 | let boost_amount_u64 = 245 | (boost_amount * 10f64.powf(ore_api::consts::TOKEN_DECIMALS as f64)) as u64; 246 | let ix = ore_miner_delegation::instruction::delegate_boost_v2( 247 | key.pubkey(), 248 | pool_pubkey, 249 | Pubkey::from_str(&args.mint).unwrap(), 250 | boost_amount_u64, 251 | ); 252 | 253 | let mut tx = Transaction::new_with_payer(&[ix], Some(&fee_pubkey)); 254 | tx.partial_sign(&[&key], deserialized_blockhash); 255 | let serialized_tx = bincode::serialize(&tx).unwrap(); 256 | let encoded_tx = BASE64_STANDARD.encode(&serialized_tx); 257 | 258 | let resp = client 259 | .post(format!( 260 | "{}://{}/v2/stake-boost?pubkey={}&mint={}&amount={}", 261 | url_prefix, 262 | base_url, 263 | key.pubkey().to_string(), 264 | args.mint, 265 | boost_amount_u64 266 | )) 267 | .body(encoded_tx) 268 | .send() 269 | .await; 270 | if let Ok(res) = resp { 271 | if let Ok(txt) = res.text().await { 272 | match txt.as_str() { 273 | "SUCCESS" => { 274 | println!(" Successfully boosted!"); 275 | } 276 | other => { 277 | println!(" Transaction failed: {}", other); 278 | } 279 | } 280 | } else { 281 | println!(" Transaction failed, please wait and try again."); 282 | } 283 | } else { 284 | println!(" Transaction failed, please wait and try again."); 285 | } 286 | } 287 | 288 | // Helper function to fetch server timestamp 289 | async fn get_timestamp(client: &reqwest::Client, url_prefix: &str, base_url: &str) -> u64 { 290 | loop { 291 | if let Ok(response) = client 292 | .get(format!("{}://{}/timestamp", url_prefix, base_url)) 293 | .send() 294 | .await 295 | { 296 | match response.status() { 297 | StatusCode::OK => { 298 | if let Ok(ts) = response.text().await { 299 | if let Ok(ts) = ts.parse::() { 300 | return ts; 301 | } 302 | } 303 | } 304 | _ => { 305 | println!(" Server restarting, trying again in 3 seconds..."); 306 | tokio::time::sleep(Duration::from_secs(3)).await; 307 | continue; 308 | } 309 | } 310 | } 311 | println!(" Unable to retrieve timestamp, retrying in 3 seconds..."); 312 | tokio::time::sleep(Duration::from_secs(3)).await; 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /src/earnings.rs: -------------------------------------------------------------------------------- 1 | use spl_token::amount_to_ui_amount; 2 | 3 | use crate::database::AppDatabase; 4 | 5 | pub fn earnings() { 6 | let app_db = AppDatabase::new(); 7 | 8 | let daily_earnings = app_db.get_daily_earnings(7); 9 | 10 | for de in daily_earnings { 11 | println!( 12 | "Day: {}, Total Mined: {} ORE", 13 | de.0, 14 | amount_to_ui_amount(de.1, ore_api::consts::TOKEN_DECIMALS) 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/generate_key.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, io::Write}; 2 | 3 | use bip39::{Mnemonic, Seed}; 4 | use dirs::home_dir; 5 | use qrcode::render::unicode; 6 | use qrcode::QrCode; 7 | use solana_sdk::{ 8 | derivation_path::DerivationPath, 9 | signature::{write_keypair_file, Keypair}, 10 | signer::{SeedDerivable, Signer}, 11 | }; 12 | 13 | use crate::CONFIG_FILE; 14 | 15 | pub fn generate_key() { 16 | let new_mnemonic = Mnemonic::new(bip39::MnemonicType::Words12, bip39::Language::English); 17 | let phrase = new_mnemonic.clone().into_phrase(); 18 | 19 | let seed = Seed::new(&new_mnemonic, ""); 20 | 21 | let derivation_path = DerivationPath::from_absolute_path_str("m/44'/501'/0'/0'").unwrap(); 22 | 23 | if let Ok(new_key) = 24 | Keypair::from_seed_and_derivation_path(seed.as_bytes(), Some(derivation_path)) 25 | { 26 | let dir = home_dir(); 27 | 28 | if let Some(dir) = dir { 29 | let key_dir = dir.join(".config/solana/mining-hot-wallet.json"); 30 | 31 | if key_dir.exists() { 32 | println!(" Keypair already exists at {:?}", key_dir); 33 | return; 34 | } 35 | 36 | if let Some(parent_dir) = key_dir.parent() { 37 | if !parent_dir.exists() { 38 | match fs::create_dir_all(parent_dir) { 39 | Ok(_) => {} 40 | Err(e) => { 41 | println!(" Failed to create directory for wallet: {}", e); 42 | return; 43 | } 44 | } 45 | } 46 | } 47 | 48 | match write_keypair_file(&new_key, key_dir.clone()) { 49 | Ok(_) => { 50 | let config_path = std::path::PathBuf::from(CONFIG_FILE); 51 | let mut file = std::fs::OpenOptions::new() 52 | .append(true) 53 | .open(&config_path) 54 | .expect("Failed to open configuration file for appending."); 55 | 56 | writeln!( 57 | file, 58 | "{}", 59 | key_dir.to_str().expect("Failed to key_dir.to_str()") 60 | ) 61 | .expect("Failed to write keypair path to configuration file."); 62 | 63 | let pubkey = new_key.pubkey(); 64 | 65 | // Generate QR code for the public key 66 | if let Ok(code) = QrCode::new(pubkey.to_string()) { 67 | // Render the QR code without extra indentation 68 | let string = code 69 | .render::() 70 | .quiet_zone(false) // Remove additional padding or quiet zone 71 | .build(); 72 | 73 | // Print the QR code with clear separators 74 | println!(" QR Code for Public Key:\n"); 75 | println!("{}", string); 76 | } else { 77 | println!(" Failed to generate QR code for the public key."); 78 | } 79 | 80 | // Print the mining wallet information and instructions after the QR code 81 | println!(" Mining Hot Wallet Secret Phrase (Use this to import/recover your mining hot wallet):"); 82 | println!(" {}", phrase); 83 | println!("\n New Mining Hot Wallet Public Key: {}", pubkey); 84 | println!("\n The QR code above can be scanned with Phantom/Solflare wallet to fund this wallet for any reason."); 85 | println!("\n Note: Ec1ipse Pool does not require a sign up fee."); 86 | } 87 | Err(e) => { 88 | println!(" Failed to write keypair to file: {}", e); 89 | } 90 | } 91 | } else { 92 | println!(" Failed to get home directory from platform."); 93 | } 94 | } else { 95 | println!(" Failed to generate keypair, please try again. Contact support if this keeps happening."); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/migrate_boosts_to_v2.rs: -------------------------------------------------------------------------------- 1 | use base64::{prelude::BASE64_STANDARD, Engine}; 2 | use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer}; 3 | use std::str::FromStr; 4 | 5 | use crate::balance; 6 | 7 | pub async fn migrate_boosts_to_v2(key: Keypair, url: String, unsecure: bool) { 8 | println!("Migrating Boosts..."); 9 | let base_url = url; 10 | let client = reqwest::Client::new(); 11 | let url_prefix = if unsecure { 12 | "http".to_string() 13 | } else { 14 | "https".to_string() 15 | }; 16 | 17 | let token_mints = vec![ 18 | (Pubkey::from_str("oreoU2P8bN6jkk3jbaiVxYnG1dCXcYxwhwyK9jSybcp").unwrap(), "ORE Token"), 19 | (Pubkey::from_str("DrSS5RM7zUd9qjUEdDaf31vnDUSbCrMto6mjqTrHFifN").unwrap(), "ORE-SOL LP"), 20 | (Pubkey::from_str("meUwDp23AaxhiNKaQCyJ2EAF2T4oe1gSkEkGXSRVdZb").unwrap(), "ORE-ISC LP"), 21 | ]; 22 | 23 | let ore_v1_boost_amount = balance::get_boosted_stake_balance(&key, base_url.clone(), unsecure, token_mints[0].0.to_string()).await; 24 | let ore_sol_v1_boost_amount = balance::get_boosted_stake_balance(&key, base_url.clone(), unsecure, token_mints[1].0.to_string()).await; 25 | let ore_isc_v1_boost_amount = balance::get_boosted_stake_balance(&key, base_url.clone(), unsecure, token_mints[2].0.to_string()).await; 26 | 27 | let resp = client 28 | .get(format!( 29 | "{}://{}/pool/authority/pubkey", 30 | url_prefix, base_url 31 | )) 32 | .send() 33 | .await 34 | .unwrap() 35 | .text() 36 | .await 37 | .unwrap(); 38 | let pool_pubkey = Pubkey::from_str(&resp).unwrap(); 39 | 40 | let resp = client 41 | .get(format!( 42 | "{}://{}/pool/fee_payer/pubkey", 43 | url_prefix, base_url 44 | )) 45 | .send() 46 | .await 47 | .unwrap() 48 | .text() 49 | .await 50 | .unwrap(); 51 | let fee_pubkey = Pubkey::from_str(&resp).unwrap(); 52 | 53 | if ore_v1_boost_amount > 0.0 { 54 | println!("Migrating {} ORE", ore_v1_boost_amount); 55 | // migrate ore boost 56 | let ore_v2_boost_amount = balance::get_boosted_stake_balance_v2(&key, base_url.clone(), unsecure, token_mints[0].0.to_string()).await; 57 | let mut ixs = vec![]; 58 | // init boost account 59 | if ore_v2_boost_amount < 0.0 { 60 | // add init ix 61 | let ix = ore_miner_delegation::instruction::init_delegate_boost_v2(key.pubkey(), pool_pubkey, fee_pubkey, token_mints[0].0); 62 | ixs.push(ix); 63 | } 64 | // migrate balance 65 | let ix = ore_miner_delegation::instruction::migrate_boost_to_v2(key.pubkey(), pool_pubkey, token_mints[0].0); 66 | ixs.push(ix); 67 | let mut tx = solana_sdk::transaction::Transaction::new_with_payer(&ixs, Some(&fee_pubkey)); 68 | let resp = client 69 | .get(format!("{}://{}/latest-blockhash", url_prefix, base_url)) 70 | .send() 71 | .await 72 | .unwrap() 73 | .text() 74 | .await 75 | .unwrap(); 76 | let decoded_blockhash = BASE64_STANDARD.decode(resp).unwrap(); 77 | let deserialized_blockhash = bincode::deserialize(&decoded_blockhash).unwrap(); 78 | tx.partial_sign(&[&key], deserialized_blockhash); 79 | let serialized_tx = bincode::serialize(&tx).unwrap(); 80 | let encoded_tx = BASE64_STANDARD.encode(&serialized_tx); 81 | 82 | let needs_init = ixs.len() > 1; 83 | 84 | let resp = client 85 | .post(format!( 86 | "{}://{}/v2/migrate-boost?pubkey={}&mint={}&init={}", 87 | url_prefix, 88 | base_url, 89 | key.pubkey().to_string(), 90 | token_mints[0].0.to_string(), 91 | needs_init 92 | )) 93 | .body(encoded_tx) 94 | .send() 95 | .await; 96 | if let Ok(res) = resp { 97 | if let Ok(txt) = res.text().await { 98 | match txt.as_str() { 99 | "SUCCESS" => { 100 | println!(" Successfully migrated ore boost!"); 101 | } 102 | other => { 103 | println!(" Boost Migration Transaction failed: {}", other); 104 | } 105 | } 106 | } else { 107 | println!(" Boost Migration Transaction failed, please wait and try again."); 108 | } 109 | } else { 110 | println!(" Boost Migration Transaction failed, please wait and try again."); 111 | } 112 | } else { 113 | println!("No boost v1 ORE to migrate"); 114 | } 115 | 116 | if ore_sol_v1_boost_amount > 0.0 { 117 | println!("Migrating {} ORE-SOL", ore_sol_v1_boost_amount); 118 | // migrate ore boost 119 | let ore_v2_boost_amount = balance::get_boosted_stake_balance_v2(&key, base_url.clone(), unsecure, token_mints[1].0.to_string()).await; 120 | let mut ixs = vec![]; 121 | // init boost account 122 | if ore_v2_boost_amount < 0.0 { 123 | // add init ix 124 | let ix = ore_miner_delegation::instruction::init_delegate_boost_v2(key.pubkey(), pool_pubkey, fee_pubkey, token_mints[1].0); 125 | ixs.push(ix); 126 | } 127 | // migrate balance 128 | let ix = ore_miner_delegation::instruction::migrate_boost_to_v2(key.pubkey(), pool_pubkey, token_mints[1].0); 129 | ixs.push(ix); 130 | let mut tx = solana_sdk::transaction::Transaction::new_with_payer(&ixs, Some(&fee_pubkey)); 131 | let resp = client 132 | .get(format!("{}://{}/latest-blockhash", url_prefix, base_url)) 133 | .send() 134 | .await 135 | .unwrap() 136 | .text() 137 | .await 138 | .unwrap(); 139 | let decoded_blockhash = BASE64_STANDARD.decode(resp).unwrap(); 140 | let deserialized_blockhash = bincode::deserialize(&decoded_blockhash).unwrap(); 141 | tx.partial_sign(&[&key], deserialized_blockhash); 142 | let serialized_tx = bincode::serialize(&tx).unwrap(); 143 | let encoded_tx = BASE64_STANDARD.encode(&serialized_tx); 144 | 145 | let needs_init = ixs.len() > 1; 146 | 147 | let resp = client 148 | .post(format!( 149 | "{}://{}/v2/migrate-boost?pubkey={}&mint={}&init={}", 150 | url_prefix, 151 | base_url, 152 | key.pubkey().to_string(), 153 | token_mints[1].0.to_string(), 154 | needs_init 155 | )) 156 | .body(encoded_tx) 157 | .send() 158 | .await; 159 | if let Ok(res) = resp { 160 | if let Ok(txt) = res.text().await { 161 | match txt.as_str() { 162 | "SUCCESS" => { 163 | println!(" Successfully migrated ore-sol boost!"); 164 | } 165 | other => { 166 | println!(" Boost Migration Transaction failed: {}", other); 167 | } 168 | } 169 | } else { 170 | println!(" Boost Migration Transaction failed, please wait and try again."); 171 | } 172 | } else { 173 | println!(" Boost Migration Transaction failed, please wait and try again."); 174 | } 175 | } else { 176 | println!("No boost v1 ORE-SOL to migrate"); 177 | } 178 | 179 | if ore_isc_v1_boost_amount > 0.0 { 180 | println!("Migrating {} ORE-ISC", ore_isc_v1_boost_amount); 181 | // migrate ore boost 182 | let ore_v2_boost_amount = balance::get_boosted_stake_balance_v2(&key, base_url.clone(), unsecure, token_mints[2].0.to_string()).await; 183 | let mut ixs = vec![]; 184 | // init boost account 185 | if ore_v2_boost_amount < 0.0 { 186 | // add init ix 187 | let ix = ore_miner_delegation::instruction::init_delegate_boost_v2(key.pubkey(), pool_pubkey, fee_pubkey, token_mints[2].0); 188 | ixs.push(ix); 189 | } 190 | // migrate balance 191 | let ix = ore_miner_delegation::instruction::migrate_boost_to_v2(key.pubkey(), pool_pubkey, token_mints[2].0); 192 | ixs.push(ix); 193 | let mut tx = solana_sdk::transaction::Transaction::new_with_payer(&ixs, Some(&fee_pubkey)); 194 | let resp = client 195 | .get(format!("{}://{}/latest-blockhash", url_prefix, base_url)) 196 | .send() 197 | .await 198 | .unwrap() 199 | .text() 200 | .await 201 | .unwrap(); 202 | let decoded_blockhash = BASE64_STANDARD.decode(resp).unwrap(); 203 | let deserialized_blockhash = bincode::deserialize(&decoded_blockhash).unwrap(); 204 | tx.partial_sign(&[&key], deserialized_blockhash); 205 | let serialized_tx = bincode::serialize(&tx).unwrap(); 206 | let encoded_tx = BASE64_STANDARD.encode(&serialized_tx); 207 | 208 | let needs_init = ixs.len() > 1; 209 | 210 | let resp = client 211 | .post(format!( 212 | "{}://{}/v2/migrate-boost?pubkey={}&mint={}&init={}", 213 | url_prefix, 214 | base_url, 215 | key.pubkey().to_string(), 216 | token_mints[2].0.to_string(), 217 | needs_init 218 | )) 219 | .body(encoded_tx) 220 | .send() 221 | .await; 222 | if let Ok(res) = resp { 223 | if let Ok(txt) = res.text().await { 224 | match txt.as_str() { 225 | "SUCCESS" => { 226 | println!(" Successfully migrated ore-isc boost!"); 227 | } 228 | other => { 229 | println!(" Boost Migration Transaction failed: {}", other); 230 | } 231 | } 232 | } else { 233 | println!(" Boost Migration Transaction failed, please wait and try again."); 234 | } 235 | } else { 236 | println!(" Boost Migration Transaction failed, please wait and try again."); 237 | } 238 | } else { 239 | println!("No boost v1 ORE-ISC to migrate"); 240 | } 241 | 242 | println!("Boost Migrations Complete"); 243 | } 244 | -------------------------------------------------------------------------------- /src/mine.rs: -------------------------------------------------------------------------------- 1 | use colored::*; 2 | use base64::prelude::*; 3 | use clap::{arg, Parser}; 4 | use drillx_2::equix; 5 | use futures_util::stream::SplitSink; 6 | use futures_util::{SinkExt, StreamExt}; 7 | use indicatif::{ProgressBar, ProgressStyle}; 8 | use solana_sdk::{signature::Keypair, signer::Signer}; 9 | use spl_token::amount_to_ui_amount; 10 | use std::env; 11 | use std::mem::size_of; 12 | use std::sync::atomic::{AtomicBool, Ordering}; 13 | use std::{ 14 | ops::{ControlFlow, Range}, 15 | sync::Arc, 16 | time::{Duration, Instant, SystemTime, UNIX_EPOCH}, 17 | }; 18 | use tokio::net::TcpStream; 19 | use tokio::sync::mpsc::UnboundedReceiver; 20 | use tokio::sync::{mpsc::UnboundedSender, Mutex}; 21 | use tokio::time::timeout; 22 | use tokio_tungstenite::{ 23 | connect_async, 24 | tungstenite::{ 25 | handshake::client::{generate_key, Request}, 26 | Message, 27 | }, 28 | }; 29 | use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; 30 | 31 | use crate::database::{AppDatabase, PoolSubmissionResult}; 32 | 33 | #[derive(Debug)] 34 | pub struct ServerMessagePoolSubmissionResult { 35 | pub difficulty: u32, 36 | pub total_balance: f64, 37 | pub total_rewards: f64, 38 | pub top_stake: f64, 39 | pub multiplier: f64, 40 | pub active_miners: u32, 41 | pub challenge: [u8; 32], 42 | pub _best_nonce: u64, 43 | pub miner_supplied_difficulty: u32, 44 | pub miner_earned_rewards: f64, 45 | pub miner_percentage: f64, 46 | } 47 | 48 | impl ServerMessagePoolSubmissionResult { 49 | pub fn new_from_bytes(b: Vec) -> Self { 50 | let mut b_index = 1; 51 | 52 | let data_size = size_of::(); 53 | let mut data_bytes = [0u8; size_of::()]; 54 | for i in 0..data_size { 55 | data_bytes[i] = b[i + b_index]; 56 | } 57 | b_index += data_size; 58 | let difficulty = u32::from_le_bytes(data_bytes); 59 | 60 | let data_size = size_of::(); 61 | let mut data_bytes = [0u8; size_of::()]; 62 | for i in 0..data_size { 63 | data_bytes[i] = b[i + b_index]; 64 | } 65 | b_index += data_size; 66 | let total_balance = f64::from_le_bytes(data_bytes); 67 | 68 | let data_size = size_of::(); 69 | let mut data_bytes = [0u8; size_of::()]; 70 | for i in 0..data_size { 71 | data_bytes[i] = b[i + b_index]; 72 | } 73 | b_index += data_size; 74 | let total_rewards = f64::from_le_bytes(data_bytes); 75 | 76 | let data_size = size_of::(); 77 | let mut data_bytes = [0u8; size_of::()]; 78 | for i in 0..data_size { 79 | data_bytes[i] = b[i + b_index]; 80 | } 81 | b_index += data_size; 82 | let top_stake = f64::from_le_bytes(data_bytes); 83 | 84 | let data_size = size_of::(); 85 | let mut data_bytes = [0u8; size_of::()]; 86 | for i in 0..data_size { 87 | data_bytes[i] = b[i + b_index]; 88 | } 89 | b_index += data_size; 90 | let multiplier = f64::from_le_bytes(data_bytes); 91 | 92 | let data_size = size_of::(); 93 | let mut data_bytes = [0u8; size_of::()]; 94 | for i in 0..data_size { 95 | data_bytes[i] = b[i + b_index]; 96 | } 97 | b_index += data_size; 98 | let active_miners = u32::from_le_bytes(data_bytes); 99 | 100 | let data_size = 32; 101 | let mut data_bytes = [0u8; 32]; 102 | for i in 0..data_size { 103 | data_bytes[i] = b[i + b_index]; 104 | } 105 | b_index += data_size; 106 | let challenge = data_bytes.clone(); 107 | 108 | let data_size = size_of::(); 109 | let mut data_bytes = [0u8; size_of::()]; 110 | for i in 0..data_size { 111 | data_bytes[i] = b[i + b_index]; 112 | } 113 | b_index += data_size; 114 | let best_nonce = u64::from_le_bytes(data_bytes); 115 | 116 | let data_size = size_of::(); 117 | let mut data_bytes = [0u8; size_of::()]; 118 | for i in 0..data_size { 119 | data_bytes[i] = b[i + b_index]; 120 | } 121 | b_index += data_size; 122 | let miner_supplied_difficulty = u32::from_le_bytes(data_bytes); 123 | 124 | let data_size = size_of::(); 125 | let mut data_bytes = [0u8; size_of::()]; 126 | for i in 0..data_size { 127 | data_bytes[i] = b[i + b_index]; 128 | } 129 | b_index += data_size; 130 | let miner_earned_rewards = f64::from_le_bytes(data_bytes); 131 | 132 | let data_size = size_of::(); 133 | let mut data_bytes = [0u8; size_of::()]; 134 | for i in 0..data_size { 135 | data_bytes[i] = b[i + b_index]; 136 | } 137 | //b_index += data_size; 138 | let miner_percentage = f64::from_le_bytes(data_bytes); 139 | 140 | ServerMessagePoolSubmissionResult { 141 | difficulty, 142 | total_balance, 143 | total_rewards, 144 | top_stake, 145 | multiplier, 146 | active_miners, 147 | challenge, 148 | _best_nonce: best_nonce, 149 | miner_supplied_difficulty, 150 | miner_earned_rewards, 151 | miner_percentage, 152 | } 153 | } 154 | 155 | // pub fn to_message_binary(&self) -> Vec { 156 | // let mut bin_data = Vec::new(); 157 | // bin_data.push(1u8); 158 | // bin_data.extend_from_slice(&self.difficulty.to_le_bytes()); 159 | // bin_data.extend_from_slice(&self.total_balance.to_le_bytes()); 160 | // bin_data.extend_from_slice(&self.total_rewards.to_le_bytes()); 161 | // bin_data.extend_from_slice(&self.top_stake.to_le_bytes()); 162 | // bin_data.extend_from_slice(&self.multiplier.to_le_bytes()); 163 | // bin_data.extend_from_slice(&self.active_miners.to_le_bytes()); 164 | // bin_data.extend_from_slice(&self.challenge); 165 | // bin_data.extend_from_slice(&self.best_nonce.to_le_bytes()); 166 | // bin_data.extend_from_slice(&self.miner_supplied_difficulty.to_le_bytes()); 167 | // bin_data.extend_from_slice(&self.miner_earned_rewards.to_le_bytes()); 168 | // bin_data.extend_from_slice(&self.miner_percentage.to_le_bytes()); 169 | 170 | // bin_data 171 | // } 172 | } 173 | 174 | #[derive(Debug)] 175 | pub enum ServerMessage { 176 | StartMining([u8; 32], Range, u64), 177 | PoolSubmissionResult(ServerMessagePoolSubmissionResult), 178 | } 179 | 180 | #[derive(Debug, Clone, Copy)] 181 | pub struct ThreadSubmission { 182 | pub nonce: u64, 183 | pub difficulty: u32, 184 | pub d: [u8; 16], // digest 185 | } 186 | 187 | #[derive(Debug, Clone, Copy)] 188 | pub enum MessageSubmissionSystem { 189 | Submission(ThreadSubmission), 190 | Reset, 191 | Finish, 192 | } 193 | 194 | #[derive(Debug, Parser)] 195 | pub struct MineArgs { 196 | #[arg( 197 | long, 198 | value_name = "threads", 199 | default_value = "4", 200 | help = "Number of threads to use while mining" 201 | )] 202 | pub threads: u32, 203 | #[arg( 204 | long, 205 | value_name = "BUFFER", 206 | default_value = "0", 207 | help = "Buffer time in seconds, to send the submission to the server earlier" 208 | )] 209 | pub buffer: u32, 210 | } 211 | 212 | pub async fn mine(args: MineArgs, key: Keypair, url: String, unsecure: bool) { 213 | let running = Arc::new(AtomicBool::new(true)); 214 | let key = Arc::new(key); 215 | 216 | loop { 217 | let connection_started=Instant::now(); 218 | 219 | if !running.load(Ordering::SeqCst) { 220 | break; 221 | } 222 | 223 | let base_url = url.clone(); 224 | let mut ws_url_str = if unsecure { 225 | format!("ws://{}/v2/ws", url) 226 | } else { 227 | format!("wss://{}/v2/ws", url) 228 | }; 229 | 230 | let client = reqwest::Client::new(); 231 | 232 | let http_prefix = if unsecure { 233 | "http".to_string() 234 | } else { 235 | "https".to_string() 236 | }; 237 | 238 | let timestamp = match client 239 | .get(format!("{}://{}/timestamp", http_prefix, base_url)) 240 | .send() 241 | .await 242 | { 243 | Ok(res) => { 244 | if res.status().as_u16() >= 200 && res.status().as_u16() < 300 { 245 | if let Ok(ts) = res.text().await { 246 | if let Ok(ts) = ts.parse::() { 247 | ts 248 | } else { 249 | println!("Server response body for /timestamp failed to parse, contact admin."); 250 | tokio::time::sleep(Duration::from_secs(5)).await; 251 | continue; 252 | } 253 | } else { 254 | println!("Server response body for /timestamp is empty, contact admin."); 255 | tokio::time::sleep(Duration::from_secs(5)).await; 256 | continue; 257 | } 258 | } else { 259 | println!( 260 | "Failed to get timestamp from server. StatusCode: {}", 261 | res.status() 262 | ); 263 | tokio::time::sleep(Duration::from_secs(5)).await; 264 | continue; 265 | } 266 | } 267 | Err(e) => { 268 | println!("Failed to get timestamp from server.\nError: {}", e); 269 | tokio::time::sleep(Duration::from_secs(5)).await; 270 | continue; 271 | } 272 | }; 273 | 274 | println!("Server Timestamp: {}", timestamp); 275 | 276 | let ts_msg = timestamp.to_le_bytes(); 277 | let sig = key.sign_message(&ts_msg); 278 | 279 | ws_url_str.push_str(&format!("?timestamp={}", timestamp)); 280 | let url = url::Url::parse(&ws_url_str).expect("Failed to parse server url"); 281 | let host = url.host_str().expect("Invalid host in server url"); 282 | let threads = args.threads; 283 | 284 | let auth = BASE64_STANDARD.encode(format!("{}:{}", key.pubkey(), sig)); 285 | 286 | println!("Connecting to server..."); 287 | let request = Request::builder() 288 | .method("GET") 289 | .uri(url.to_string()) 290 | .header("Sec-Websocket-Key", generate_key()) 291 | .header("Host", host) 292 | .header("Upgrade", "websocket") 293 | .header("Connection", "upgrade") 294 | .header("Sec-Websocket-Version", "13") 295 | .header("Authorization", format!("Basic {}", auth)) 296 | .body(()) 297 | .unwrap(); 298 | 299 | match connect_async(request).await { 300 | Ok((ws_stream, _)) => { 301 | println!("{}{}{}", 302 | "Server: ".dimmed(), 303 | format!("Connected to network!").blue(), 304 | format!(" [{}ms]", connection_started.elapsed().as_millis()).dimmed(), 305 | ); 306 | 307 | let (sender, mut receiver) = ws_stream.split(); 308 | let (message_sender, mut message_receiver) = 309 | tokio::sync::mpsc::unbounded_channel::(); 310 | 311 | let (solution_system_message_sender, solution_system_message_receiver) = 312 | tokio::sync::mpsc::unbounded_channel::(); 313 | 314 | let sender = Arc::new(Mutex::new(sender)); 315 | let app_key = key.clone(); 316 | let app_socket_sender = sender.clone(); 317 | tokio::spawn(async move { 318 | submission_system(app_key, solution_system_message_receiver, app_socket_sender) 319 | .await; 320 | }); 321 | 322 | let solution_system_submission_sender = Arc::new(solution_system_message_sender); 323 | 324 | let msend = message_sender.clone(); 325 | let system_submission_sender = solution_system_submission_sender.clone(); 326 | let receiver_thread = tokio::spawn(async move { 327 | let mut last_start_mine_instant = Instant::now(); 328 | loop { 329 | match timeout(Duration::from_secs(45), receiver.next()).await { 330 | Ok(Some(Ok(message))) => { 331 | match process_message(message, msend.clone()) { 332 | ControlFlow::Break(_) => { 333 | break; 334 | } 335 | ControlFlow::Continue(got_start_mining) => { 336 | if got_start_mining { 337 | last_start_mine_instant = Instant::now(); 338 | } 339 | } 340 | } 341 | 342 | if last_start_mine_instant.elapsed().as_secs() >= 120 { 343 | eprintln!("Last start mining message was over 2 minutes ago. Closing websocket for reconnection."); 344 | break; 345 | } 346 | } 347 | Ok(Some(Err(e))) => { 348 | eprintln!("Websocket error: {}", e); 349 | break; 350 | } 351 | Ok(None) => { 352 | eprintln!("Websocket closed gracefully"); 353 | break; 354 | } 355 | Err(_) => { 356 | eprintln!("Websocket receiver timeout, assuming disconnection"); 357 | break; 358 | } 359 | } 360 | } 361 | 362 | println!("Websocket receiver closed or timed out."); 363 | println!("Cleaning up channels..."); 364 | let _ = system_submission_sender.send(MessageSubmissionSystem::Finish); 365 | drop(msend); 366 | drop(message_sender); 367 | }); 368 | 369 | // send Ready message 370 | let now = SystemTime::now() 371 | .duration_since(UNIX_EPOCH) 372 | .expect("Time went backwards") 373 | .as_secs(); 374 | 375 | let msg = now.to_le_bytes(); 376 | let sig = key.sign_message(&msg).to_string().as_bytes().to_vec(); 377 | let mut bin_data: Vec = Vec::new(); 378 | bin_data.push(0u8); 379 | bin_data.extend_from_slice(&key.pubkey().to_bytes()); 380 | bin_data.extend_from_slice(&msg); 381 | bin_data.extend(sig); 382 | 383 | let mut lock = sender.lock().await; 384 | let _ = lock.send(Message::Binary(bin_data)).await; 385 | drop(lock); 386 | 387 | let (db_sender, mut db_receiver) = 388 | tokio::sync::mpsc::unbounded_channel::(); 389 | 390 | tokio::spawn(async move { 391 | let app_db = AppDatabase::new(); 392 | 393 | while let Some(msg) = db_receiver.recv().await { 394 | app_db.add_new_pool_submission(msg); 395 | let total_earnings = amount_to_ui_amount( 396 | app_db.get_todays_earnings(), 397 | ore_api::consts::TOKEN_DECIMALS, 398 | ); 399 | println!("Todays Earnings: {} ORE\n", total_earnings); 400 | } 401 | }); 402 | 403 | // receive messages 404 | let s_system_submission_sender = solution_system_submission_sender.clone(); 405 | while let Some(msg) = message_receiver.recv().await { 406 | let system_submission_sender = s_system_submission_sender.clone(); 407 | let db_sender = db_sender.clone(); 408 | tokio::spawn({ 409 | let message_sender = sender.clone(); 410 | let key = key.clone(); 411 | let running = running.clone(); 412 | async move { 413 | if !running.load(Ordering::SeqCst) { 414 | return; 415 | } 416 | 417 | match msg { 418 | ServerMessage::StartMining(challenge, nonce_range, cutoff) => { 419 | println!( 420 | "\nNext Challenge: {}", 421 | BASE64_STANDARD.encode(challenge) 422 | ); 423 | println!( 424 | "Nonce range: {} - {}", 425 | nonce_range.start, nonce_range.end 426 | ); 427 | println!("Cutoff in: {}s", cutoff); 428 | 429 | // Adjust the cutoff with the buffer 430 | let mut cutoff = cutoff.saturating_sub(args.buffer as u64); 431 | if cutoff > 60 { 432 | cutoff = 55; 433 | } 434 | 435 | // Detect if running on Windows and set symbols accordingly 436 | let pb = if env::consts::OS == "windows" { 437 | ProgressBar::new_spinner().with_style( 438 | ProgressStyle::default_spinner() 439 | .tick_strings(&["-", "\\", "|", "/"]) // Use simple ASCII symbols 440 | .template("{spinner:.green} {msg}") 441 | .expect("Failed to set progress bar template"), 442 | ) 443 | } else { 444 | ProgressBar::new_spinner().with_style( 445 | ProgressStyle::default_spinner() 446 | .tick_strings(&[ 447 | "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", 448 | "⠏", 449 | ]) 450 | .template("{spinner:.red} {msg}") 451 | .expect("Failed to set progress bar template"), 452 | ) 453 | }; 454 | 455 | println!(); 456 | pb.set_message("Mining..."); 457 | pb.enable_steady_tick(Duration::from_millis(120)); 458 | 459 | // Original mining code 460 | let stop = Arc::new(AtomicBool::new(false)); 461 | let hash_timer = Instant::now(); 462 | let core_ids = core_affinity::get_core_ids().unwrap(); 463 | let nonces_per_thread = 10_000; 464 | let handles = core_ids 465 | .into_iter() 466 | .map(|i| { 467 | let running = running.clone(); // Capture running in thread 468 | let system_submission_sender = system_submission_sender.clone(); 469 | let stop_me = stop.clone(); 470 | std::thread::spawn({ 471 | let mut memory = equix::SolverMemory::new(); 472 | move || { 473 | if (i.id as u32).ge(&threads) { 474 | return None; 475 | } 476 | 477 | let _ = core_affinity::set_for_current(i); 478 | 479 | let first_nonce = nonce_range.start 480 | + (nonces_per_thread * (i.id as u64)); 481 | let mut nonce = first_nonce; 482 | let mut best_nonce = nonce; 483 | let mut best_difficulty = 0; 484 | let mut best_hash = drillx_2::Hash::default(); 485 | let mut total_hashes: u64 = 0; 486 | 487 | loop { 488 | // Check if Ctrl+C was pressed 489 | if !running.load(Ordering::SeqCst) { 490 | return None; 491 | } 492 | 493 | if stop_me.load(Ordering::Relaxed) { 494 | break; 495 | } 496 | 497 | // Create hash 498 | for hx in drillx_2::get_hashes_with_memory( 499 | &mut memory, 500 | &challenge, 501 | &nonce.to_le_bytes(), 502 | ) { 503 | total_hashes += 1; 504 | let difficulty = hx.difficulty(); 505 | if difficulty.gt(&7) && difficulty.gt(&best_difficulty) { 506 | let thread_submission = ThreadSubmission{ 507 | nonce, 508 | difficulty, 509 | d: hx.d, 510 | }; 511 | if let Err(_) = system_submission_sender.send(MessageSubmissionSystem::Submission(thread_submission)) { 512 | stop_me.store(true, Ordering::Relaxed); 513 | } 514 | best_nonce = nonce; 515 | best_difficulty = difficulty; 516 | best_hash = hx; 517 | } 518 | } 519 | 520 | // Exit if processed nonce range 521 | if nonce >= nonce_range.end { 522 | break; 523 | } 524 | 525 | if nonce % 100 == 0 { 526 | if hash_timer.elapsed().as_secs().ge(&cutoff) { 527 | if best_difficulty.ge(&8) { 528 | break; 529 | } 530 | } 531 | } 532 | 533 | // Increment nonce 534 | nonce += 1; 535 | } 536 | 537 | // Return the best nonce 538 | Some(( 539 | best_nonce, 540 | best_difficulty, 541 | best_hash, 542 | total_hashes, 543 | )) 544 | } 545 | }) 546 | }) 547 | .collect::>(); 548 | 549 | // Join handles and return best nonce 550 | let mut best_difficulty = 0; 551 | let mut total_nonces_checked = 0; 552 | for h in handles { 553 | if let Ok(Some(( 554 | _nonce, 555 | difficulty, 556 | _hash, 557 | nonces_checked, 558 | ))) = h.join() 559 | { 560 | total_nonces_checked += nonces_checked; 561 | if difficulty > best_difficulty { 562 | best_difficulty = difficulty; 563 | } 564 | } 565 | } 566 | 567 | let hash_time = hash_timer.elapsed(); 568 | 569 | // Stop the spinner after mining is done 570 | pb.finish_and_clear(); 571 | 572 | if stop.load(Ordering::Relaxed) { 573 | return; 574 | } 575 | println!("✔ Mining complete!"); 576 | println!("Processed: {}", total_nonces_checked); 577 | println!("Hash time: {:?}", hash_time); 578 | let hash_time_secs = hash_time.as_secs(); 579 | if hash_time_secs > 0 { 580 | println!( 581 | "Hashpower: {:?} H/s", 582 | total_nonces_checked.saturating_div(hash_time_secs) 583 | ); 584 | println!("Client found diff: {}", best_difficulty); 585 | } 586 | 587 | let _ = system_submission_sender 588 | .send(MessageSubmissionSystem::Reset); 589 | 590 | //tokio::time::sleep(Duration::from_secs(5 + args.buffer as u64)).await; 591 | 592 | // Ready up again 593 | let now = SystemTime::now() 594 | .duration_since(UNIX_EPOCH) 595 | .expect("Time went backwards") 596 | .as_secs(); 597 | 598 | let msg = now.to_le_bytes(); 599 | let sig = 600 | key.sign_message(&msg).to_string().as_bytes().to_vec(); 601 | let mut bin_data: Vec = Vec::new(); 602 | bin_data.push(0u8); 603 | bin_data.extend_from_slice(&key.pubkey().to_bytes()); 604 | bin_data.extend_from_slice(&msg); 605 | bin_data.extend(sig); 606 | { 607 | let mut message_sender = message_sender.lock().await; 608 | if let Err(_) = 609 | message_sender.send(Message::Binary(bin_data)).await 610 | { 611 | let _ = system_submission_sender 612 | .send(MessageSubmissionSystem::Finish); 613 | println!("Failed to send Ready message. Returning..."); 614 | return; 615 | } 616 | } 617 | } 618 | ServerMessage::PoolSubmissionResult(data) => { 619 | let pool_earned = (data.total_rewards 620 | * 10f64.powf(ore_api::consts::TOKEN_DECIMALS as f64)) 621 | as u64; 622 | let miner_earned = (data.miner_earned_rewards 623 | * 10f64.powf(ore_api::consts::TOKEN_DECIMALS as f64)) 624 | as u64; 625 | let ps = PoolSubmissionResult::new( 626 | data.difficulty, 627 | pool_earned, 628 | data.miner_percentage, 629 | data.miner_supplied_difficulty, 630 | miner_earned, 631 | ); 632 | let _ = db_sender.send(ps); 633 | 634 | let message = format!( 635 | "\n\nChallenge: {}\nPool Submitted Difficulty: {}\nPool Earned: {:.11} ORE\nPool Balance: {:.11} ORE\nPool Boosts Multiplier: {:.2}x\n----------------------\nActive Miners: {}\n----------------------\nMiner Submitted Difficulty: {}\nMiner Earned: {:.11} ORE\n{:.4}% of total pool reward\n", 636 | BASE64_STANDARD.encode(data.challenge), 637 | data.difficulty, 638 | data.total_rewards, 639 | data.total_balance, 640 | data.multiplier, 641 | data.active_miners, 642 | data.miner_supplied_difficulty, 643 | data.miner_earned_rewards, 644 | data.miner_percentage 645 | ); 646 | println!("{}", message); 647 | } 648 | } 649 | } 650 | }); 651 | } 652 | 653 | // If the websocket message receiver finishes, also finish the solution submission 654 | // sender system 655 | let _ = receiver_thread.await; 656 | let _ = solution_system_submission_sender.send(MessageSubmissionSystem::Finish); 657 | println!("Channels cleaned up, reconnecting...\n"); 658 | } 659 | Err(e) => { 660 | match e { 661 | tokio_tungstenite::tungstenite::Error::Http(e) => { 662 | if let Some(body) = e.body() { 663 | println!("Error: {:?}", String::from_utf8(body.to_vec())); 664 | } else { 665 | println!("Http Error: {:?}", e); 666 | } 667 | } 668 | _ => { 669 | println!("Error: {:?}", e); 670 | } 671 | } 672 | tokio::time::sleep(Duration::from_secs(3)).await; 673 | } 674 | } 675 | } 676 | } 677 | 678 | fn process_message( 679 | msg: Message, 680 | message_channel: UnboundedSender, 681 | ) -> ControlFlow<(), bool> { 682 | let mut got_start_mining_message = false; 683 | match msg { 684 | Message::Text(t) => { 685 | println!("{}", t); 686 | } 687 | Message::Binary(b) => { 688 | let message_type = b[0]; 689 | match message_type { 690 | 0 => { 691 | if b.len() < 49 { 692 | println!("Invalid data for Message StartMining"); 693 | } else { 694 | let mut hash_bytes = [0u8; 32]; 695 | // extract 256 bytes (32 u8's) from data for hash 696 | let mut b_index = 1; 697 | for i in 0..32 { 698 | hash_bytes[i] = b[i + b_index]; 699 | } 700 | b_index += 32; 701 | 702 | // extract 64 bytes (8 u8's) 703 | let mut cutoff_bytes = [0u8; 8]; 704 | for i in 0..8 { 705 | cutoff_bytes[i] = b[i + b_index]; 706 | } 707 | b_index += 8; 708 | let cutoff = u64::from_le_bytes(cutoff_bytes); 709 | 710 | let mut nonce_start_bytes = [0u8; 8]; 711 | for i in 0..8 { 712 | nonce_start_bytes[i] = b[i + b_index]; 713 | } 714 | b_index += 8; 715 | let nonce_start = u64::from_le_bytes(nonce_start_bytes); 716 | 717 | let mut nonce_end_bytes = [0u8; 8]; 718 | for i in 0..8 { 719 | nonce_end_bytes[i] = b[i + b_index]; 720 | } 721 | let nonce_end = u64::from_le_bytes(nonce_end_bytes); 722 | 723 | let msg = 724 | ServerMessage::StartMining(hash_bytes, nonce_start..nonce_end, cutoff); 725 | 726 | let _ = message_channel.send(msg); 727 | got_start_mining_message = true; 728 | } 729 | } 730 | 1 => { 731 | let msg = ServerMessage::PoolSubmissionResult( 732 | ServerMessagePoolSubmissionResult::new_from_bytes(b), 733 | ); 734 | let _ = message_channel.send(msg); 735 | } 736 | _ => { 737 | println!("Failed to parse server message type"); 738 | } 739 | } 740 | } 741 | Message::Ping(_) => {} 742 | Message::Pong(_) => {} 743 | Message::Close(v) => { 744 | println!("Got Close: {:?}", v); 745 | return ControlFlow::Break(()); 746 | } 747 | _ => {} 748 | } 749 | 750 | ControlFlow::Continue(got_start_mining_message) 751 | } 752 | 753 | async fn submission_system( 754 | key: Arc, 755 | mut system_message_receiver: UnboundedReceiver, 756 | socket_sender: Arc>, Message>>>, 757 | ) { 758 | let mut best_diff = 0; 759 | while let Some(msg) = system_message_receiver.recv().await { 760 | match msg { 761 | MessageSubmissionSystem::Submission(thread_submission) => { 762 | if thread_submission.difficulty > best_diff { 763 | best_diff = thread_submission.difficulty; 764 | 765 | // Send results to the server 766 | let message_type = 2u8; // 1 u8 - BestSolution Message 767 | let best_hash_bin = thread_submission.d; // 16 u8 768 | let best_nonce_bin = thread_submission.nonce.to_le_bytes(); // 8 u8 769 | 770 | let mut hash_nonce_message = [0; 24]; 771 | hash_nonce_message[0..16].copy_from_slice(&best_hash_bin); 772 | hash_nonce_message[16..24].copy_from_slice(&best_nonce_bin); 773 | let signature = key 774 | .sign_message(&hash_nonce_message) 775 | .to_string() 776 | .as_bytes() 777 | .to_vec(); 778 | 779 | let mut bin_data = [0; 57]; 780 | bin_data[00..1].copy_from_slice(&message_type.to_le_bytes()); 781 | bin_data[01..17].copy_from_slice(&best_hash_bin); 782 | bin_data[17..25].copy_from_slice(&best_nonce_bin); 783 | bin_data[25..57].copy_from_slice(&key.pubkey().to_bytes()); 784 | 785 | let mut bin_vec = bin_data.to_vec(); 786 | bin_vec.extend(signature); 787 | 788 | let mut message_sender = socket_sender.lock().await; 789 | let _ = message_sender.send(Message::Binary(bin_vec)).await; 790 | drop(message_sender); 791 | } 792 | } 793 | MessageSubmissionSystem::Reset => { 794 | best_diff = 0; 795 | 796 | // Sleep for 2 seconds to let the submission window open again 797 | tokio::time::sleep(Duration::from_secs(2)).await; 798 | } 799 | MessageSubmissionSystem::Finish => { 800 | return; 801 | } 802 | } 803 | } 804 | } 805 | -------------------------------------------------------------------------------- /src/minepmc.rs: -------------------------------------------------------------------------------- 1 | use base64::prelude::*; 2 | use drillx_2::equix; 3 | use futures_util::stream::SplitSink; 4 | use futures_util::{SinkExt, StreamExt}; 5 | use http::Method; 6 | use http::header::{SEC_WEBSOCKET_KEY, HOST, SEC_WEBSOCKET_VERSION, AUTHORIZATION, UPGRADE, CONNECTION}; 7 | use indicatif::{ProgressBar, ProgressStyle}; 8 | use solana_sdk::{signature::Keypair, signer::Signer}; 9 | use spl_token::amount_to_ui_amount; 10 | use std::env; 11 | use std::sync::atomic::{AtomicBool, Ordering}; 12 | use std::{ 13 | ops::ControlFlow, 14 | sync::Arc, 15 | time::{Duration, Instant, SystemTime, UNIX_EPOCH}, 16 | }; 17 | use tokio::net::TcpStream; 18 | use tokio::sync::mpsc::UnboundedReceiver; 19 | use tokio::sync::{mpsc::UnboundedSender, Mutex}; 20 | use tokio::time::timeout; 21 | use tokio_tungstenite::{ 22 | connect_async_with_config, 23 | tungstenite::{ 24 | handshake::client::{generate_key, Request}, 25 | Message, 26 | }, 27 | }; 28 | use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; 29 | 30 | use colored::*; 31 | use chrono::prelude::*; 32 | use std::cell::UnsafeCell; 33 | use std::sync::atomic::{AtomicU32, AtomicU64}; 34 | 35 | use crate::database::{AppDatabase, PoolSubmissionResult}; 36 | use crate::mine::{ 37 | MineArgs, 38 | ServerMessagePoolSubmissionResult, 39 | ServerMessage, 40 | MessageSubmissionSystem, 41 | ThreadSubmission, 42 | }; 43 | use crate::stats::{ 44 | get_elapsed_string, get_miner_accuracy, record_miner_accuracy, 45 | set_no_more_submissions, is_transaction_in_progress, record_tx_started, record_tx_complete, 46 | get_global_pass_start_time, set_global_pass_start_time, 47 | }; 48 | 49 | pub async fn minepmc(args: MineArgs, passedkey: Keypair, url: String, unsecure: bool) { 50 | let running = Arc::new(AtomicBool::new(true)); 51 | 52 | let key = Arc::new(passedkey); 53 | 54 | let ms_dimmed=("ms").dimmed(); 55 | 56 | let mut pass_start_time: Instant = Instant::now(); 57 | let mining_pass = Arc::new(AtomicU64::new(0)); // Create an atomic counter 58 | 59 | // OVERMINE_BY_MS: The pool server allow several secs by default between finishing mining & signing your submission. 60 | // overmine_by_ms allows shortening this duration to enable up until the server has started to submit the transaction. 61 | let overmine_by_ms_str=env::var("OVERMINE_BY_MS").unwrap_or("2000".to_string()); 62 | let overmine_by_ms: u64 = overmine_by_ms_str.parse().unwrap_or(2000); 63 | println!(" Setting overmine_by_ms duration to {}{}", overmine_by_ms.to_string().blue(), ms_dimmed); 64 | 65 | // NONCE_INIT_INTERVAL: This value is used in the calculation to guestimate how long your miner takes to do a hash. 66 | // It is used to tune how accurate you can end your mining time to a precise time 67 | // A higher interval is better (~1% of your processed count) 68 | // Aim for an accuracy of <50ms on average 69 | let nonce_init_interval_str=env::var("NONCE_INIT_INTERVAL").unwrap_or("100".to_string()); 70 | let nonce_init_interval: u64 = nonce_init_interval_str.parse().unwrap_or(100); 71 | println!(" Setting nonce_init_interval to {}", nonce_init_interval.to_string().blue()); 72 | 73 | // CORE_OFFSET: An offset so that you can begin the mining threads starting from the CORE_OFFSET value. 74 | // This allows you to potentially run multiple miners on the same machine but not tie them to all start threads on core 0 75 | let core_offset_str=env::var("CORE_OFFSET").unwrap_or("0".to_string()); 76 | let core_offset: u32 = core_offset_str.parse().unwrap_or(0); 77 | println!(" Setting core_offset to {}", core_offset.to_string().blue()); 78 | 79 | loop { 80 | let connection_started=Instant::now(); 81 | 82 | if !running.load(Ordering::SeqCst) { 83 | break; 84 | } 85 | 86 | let base_url = url.clone(); 87 | let mut ws_url_str = if unsecure { 88 | format!("ws://{}/v2/ws", url) 89 | } else { 90 | format!("wss://{}/v2/ws", url) 91 | }; 92 | 93 | // let client = reqwest::Client::new(); 94 | let client = reqwest::Client::builder() 95 | .timeout(Duration::from_secs(2)) 96 | .tcp_nodelay(true) // Disable Nagle's algorithm 97 | .tcp_keepalive(Some(Duration::from_secs(60))) 98 | .pool_idle_timeout(Some(Duration::from_secs(30))) 99 | .pool_max_idle_per_host(5) 100 | .build() 101 | .expect("Failed to setup client connection"); 102 | 103 | let http_prefix = if unsecure { 104 | "http".to_string() 105 | } else { 106 | "https".to_string() 107 | }; 108 | 109 | let timestamp = match client 110 | .get(format!("{}://{}/timestamp", http_prefix, base_url)) 111 | .send() 112 | .await 113 | { 114 | Ok(res) => { 115 | if res.status().as_u16() >= 200 && res.status().as_u16() < 300 { 116 | if let Ok(ts) = res.text().await { 117 | if let Ok(ts) = ts.parse::() { 118 | ts 119 | } else { 120 | println!("Server response body for /timestamp failed to parse, contact admin."); 121 | tokio::time::sleep(Duration::from_secs(5)).await; 122 | continue; 123 | } 124 | } else { 125 | println!("Server response body for /timestamp is empty, contact admin."); 126 | tokio::time::sleep(Duration::from_secs(5)).await; 127 | continue; 128 | } 129 | } else { 130 | println!( 131 | "Failed to get timestamp from server. StatusCode: {}", 132 | res.status() 133 | ); 134 | tokio::time::sleep(Duration::from_secs(5)).await; 135 | continue; 136 | } 137 | } 138 | Err(e) => { 139 | println!("Failed to get timestamp from server.\nError: {}", e); 140 | tokio::time::sleep(Duration::from_secs(5)).await; 141 | continue; 142 | } 143 | }; 144 | 145 | println!("\tServer Timestamp: {}", timestamp); 146 | 147 | let ts_msg = timestamp.to_le_bytes(); 148 | let sig = key.sign_message(&ts_msg); 149 | 150 | ws_url_str.push_str(&format!("?timestamp={}", timestamp)); 151 | let url = url::Url::parse(&ws_url_str).expect("Failed to parse server url"); 152 | let host = url.host_str().expect("Invalid host in server url"); 153 | let threads = args.threads; 154 | 155 | let auth = BASE64_STANDARD.encode(format!("{}:{}", key.pubkey(), sig)); 156 | 157 | println!("\tConnecting to server..."); 158 | let websocket_request = Request::builder() 159 | .method(Method::GET) 160 | .uri(url.to_string()) 161 | .header(UPGRADE, "websocket") 162 | .header(CONNECTION, "Upgrade") 163 | .header(SEC_WEBSOCKET_KEY, generate_key()) 164 | .header(HOST, host) 165 | .header(SEC_WEBSOCKET_VERSION, "13") 166 | .header(AUTHORIZATION, format!("Basic {}", auth)) 167 | .body(()) 168 | .unwrap(); 169 | 170 | match connect_async_with_config(websocket_request, None, true).await { 171 | Ok((ws_stream, _)) => { 172 | let elapsed_str2=get_elapsed_string(pass_start_time); 173 | println!("{}{}{}{}", 174 | elapsed_str2, 175 | "Server: ".dimmed(), 176 | format!("Connected to network!").blue(), 177 | format!(" [{}ms]", connection_started.elapsed().as_millis()).dimmed(), 178 | ); 179 | 180 | let (sender, mut receiver) = ws_stream.split(); 181 | let (message_sender, mut message_receiver) = 182 | tokio::sync::mpsc::unbounded_channel::(); 183 | 184 | let (solution_system_message_sender, solution_system_message_receiver) = 185 | tokio::sync::mpsc::unbounded_channel::(); 186 | 187 | let sender = Arc::new(Mutex::new(sender)); 188 | let app_key = key.clone(); 189 | let app_socket_sender = sender.clone(); 190 | tokio::spawn(async move { 191 | submission_system(app_key, solution_system_message_receiver, app_socket_sender) 192 | .await; 193 | }); 194 | 195 | let solution_system_submission_sender = Arc::new(solution_system_message_sender); 196 | 197 | let msend = message_sender.clone(); 198 | let system_submission_sender = solution_system_submission_sender.clone(); 199 | let receiver_thread = tokio::spawn(async move { 200 | let mut last_start_mine_instant = Instant::now(); 201 | loop { 202 | match timeout(Duration::from_secs(45), receiver.next()).await { 203 | Ok(Some(Ok(message))) => { 204 | match process_message(message, msend.clone()) { 205 | ControlFlow::Break(_) => { 206 | break; 207 | } 208 | ControlFlow::Continue(got_start_mining) => { 209 | if got_start_mining { 210 | last_start_mine_instant = Instant::now(); 211 | } 212 | } 213 | } 214 | 215 | if last_start_mine_instant.elapsed().as_secs() >= 120 { 216 | eprintln!("Last start mining message was over 2 minutes ago. Closing websocket for reconnection."); 217 | break; 218 | } 219 | } 220 | Ok(Some(Err(e))) => { 221 | eprintln!("Websocket error: {}", e); 222 | break; 223 | } 224 | Ok(None) => { 225 | eprintln!("Websocket closed gracefully"); 226 | break; 227 | } 228 | Err(_) => { 229 | eprintln!("Websocket receiver timeout, assuming disconnection"); 230 | break; 231 | } 232 | } 233 | } 234 | 235 | println!("Websocket receiver closed or timed out."); 236 | println!("Cleaning up channels..."); 237 | let _ = system_submission_sender.send(MessageSubmissionSystem::Finish); 238 | drop(msend); 239 | drop(message_sender); 240 | }); 241 | 242 | // send Ready message 243 | let now = SystemTime::now() 244 | .duration_since(UNIX_EPOCH) 245 | .expect("Time went backwards") 246 | .as_secs(); 247 | 248 | let msg = now.to_le_bytes(); 249 | let sig = key.sign_message(&msg).to_string().as_bytes().to_vec(); 250 | let mut bin_data: Vec = Vec::new(); 251 | bin_data.push(0u8); 252 | bin_data.extend_from_slice(&key.pubkey().to_bytes()); 253 | bin_data.extend_from_slice(&msg); 254 | bin_data.extend(sig); 255 | 256 | let mut lock = sender.lock().await; 257 | let _ = lock.send(Message::Binary(bin_data)).await; 258 | drop(lock); 259 | 260 | let (db_sender, mut db_receiver) = 261 | tokio::sync::mpsc::unbounded_channel::(); 262 | 263 | tokio::spawn(async move { 264 | let app_db = AppDatabase::new(); 265 | while let Some(msg) = db_receiver.recv().await { 266 | app_db.add_new_pool_submission(msg); 267 | let total_earnings = amount_to_ui_amount( 268 | app_db.get_todays_earnings(), 269 | ore_api::consts::TOKEN_DECIMALS, 270 | ); 271 | println!("\t{}", format!("Todays Earnings: {} ORE @ {} on {}", total_earnings, Local::now().format("%H:%M:%S"), Local::now().format("%Y-%m-%d")).green()); 272 | } 273 | }); 274 | 275 | pass_start_time = Instant::now(); 276 | set_global_pass_start_time(pass_start_time, mining_pass.load(Ordering::Relaxed)); 277 | let mining_pass_clone = mining_pass.clone(); // Clone the Arc for the async block 278 | 279 | // receive messages 280 | let s_system_submission_sender = solution_system_submission_sender.clone(); 281 | while let Some(msg) = message_receiver.recv().await { 282 | let system_submission_sender = s_system_submission_sender.clone(); 283 | let db_sender = db_sender.clone(); 284 | let mining_pass = mining_pass_clone.clone(); // Clone for the spawn 285 | tokio::spawn({ 286 | let message_sender = sender.clone(); 287 | let key = key.clone(); 288 | let running = running.clone(); 289 | async move { 290 | if !running.load(Ordering::SeqCst) { 291 | return; 292 | } 293 | 294 | // Show a name for this miner at the start of each pass - e.g. MINER_NAME=$(hostname) 295 | let miner_name = env::var("MINER_NAME").unwrap_or("".to_string()); 296 | 297 | let mut elapsed_str: String; 298 | match msg { 299 | ServerMessage::StartMining(challenge, nonce_range, cutoff) => { 300 | let elapsed_str3 = get_elapsed_string(get_global_pass_start_time()); 301 | println!("{}{} {}", 302 | elapsed_str3, 303 | "server:".dimmed(), 304 | "Start mining next pass".blue(), 305 | ); 306 | 307 | let pass_start_time = Instant::now(); 308 | let solve_start_time_local_ms = Local::now().timestamp_micros(); 309 | let ms_dimmed=("ms").dimmed(); 310 | 311 | let current_pass = mining_pass.fetch_add(1, Ordering::SeqCst) + 1; 312 | record_tx_complete(); 313 | set_no_more_submissions(false); 314 | set_global_pass_start_time(pass_start_time, current_pass as u64); 315 | 316 | println!("\n\n{} mining pass {} [{} threads]:", miner_name.clone(), current_pass, args.threads); 317 | println!("{}", format!( 318 | "Next Challenge: {}", 319 | BASE64_STANDARD.encode(challenge) 320 | ).dimmed()); 321 | println!("{}", format!( 322 | "Nonce range: {} - {}", 323 | nonce_range.start, nonce_range.end 324 | ).dimmed()); 325 | 326 | // Adjust the cutoff time to accomodate a buffer 327 | let mut cutoff = cutoff.saturating_sub(args.buffer as u64); 328 | if cutoff > 60 { 329 | cutoff = 55; 330 | } 331 | 332 | // Detect if running on Windows and set symbols accordingly 333 | let pb = if env::consts::OS == "windows" { 334 | ProgressBar::new_spinner().with_style( 335 | ProgressStyle::default_spinner() 336 | .tick_strings(&["-", "\\", "|", "/"]) // Use simple ASCII symbols 337 | .template("{spinner:.green} {msg}") 338 | .expect("Failed to set progress bar template"), 339 | ) 340 | } else { 341 | ProgressBar::new_spinner().with_style( 342 | ProgressStyle::default_spinner() 343 | .tick_strings(&[ 344 | "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", 345 | "⠏", 346 | ]) 347 | .template("{spinner:.red} {msg}") 348 | .expect("Failed to set progress bar template"), 349 | ) 350 | }; 351 | 352 | // Determine how close to the cuttoff time to mine up to 353 | let mut cutoff = cutoff.saturating_sub(args.buffer as u64); 354 | if cutoff > 60 { 355 | cutoff = 55; 356 | } 357 | let cutoff_with_overmine=(cutoff*1_000_000)+(overmine_by_ms as u64*1000); 358 | let cutoff_timestamp_ms: i128 = Local::now().timestamp_micros() as i128 359 | + (cutoff_with_overmine as i128) 360 | - (get_miner_accuracy() * 1000.0) as i128; 361 | 362 | elapsed_str = get_elapsed_string(pass_start_time); 363 | println!("{}Mine for {:.2}s - Default: {}s", elapsed_str, 364 | (cutoff_with_overmine as f64 - (get_miner_accuracy()*1000.0)) / 1_000_000.0, 365 | cutoff, 366 | ); 367 | println!("{}{}", elapsed_str, format!("Nonce range: {} - {}", nonce_range.start, nonce_range.end).dimmed()); 368 | let nonces_per_thread = (nonce_range.end-nonce_range.start).saturating_div(2).saturating_div(threads as u64); //10_000; 369 | 370 | pb.set_message(" Mining..."); 371 | pb.enable_steady_tick(Duration::from_millis(120)); 372 | 373 | let core_ids = core_affinity::get_core_ids().unwrap(); 374 | 375 | // Best solution will be updated by each thread as better difficulties are found 376 | let best_solution: Arc = MiningSolution::new(Keypair::from_bytes(&key.to_bytes()).unwrap()); 377 | 378 | // Startup Threads+1 actual threads. Extra one is a control thread 379 | let handles: Vec<_> = (0..threads).map(|thread_number| { 380 | // Get a handle to the best_solution 381 | let best_solution = Arc::clone(&best_solution); 382 | let system_submission_sender = system_submission_sender.clone(); 383 | let core_id = core_ids[(thread_number + core_offset) as usize]; 384 | let keypair_being_mined = 0; 385 | let builder = std::thread::Builder::new() 386 | .name(format!("ore_hq_cl_{}", thread_number + core_offset)) 387 | .stack_size(256*1024); // Attempt to reduce memory requirements for each thread 388 | builder.spawn({ 389 | move || { 390 | // Mining Thread 391 | let mut memory = equix::SolverMemory::new(); 392 | let _ = core_affinity::set_for_current(core_id); 393 | // println!("Assigning thread {} to core_id {}", thread_number, core_id.id); 394 | let first_nonce = nonce_range.start + (nonces_per_thread * (thread_number as u64)); 395 | let mut nonce = first_nonce; 396 | let mut nonces_current_interval = nonce_init_interval*2; 397 | let mut cutoff_nonce = nonce + nonces_current_interval; 398 | let mut current_nonces_per_ms: f64 ; // = 200.0; 399 | let mut thread_hashes: u32 = 0; 400 | let loop_start_time_local_ms = Local::now().timestamp_micros(); 401 | let mut current_timestamp_ms: i64; // = Local::now().timestamp_micros(); 402 | 403 | let left = cutoff_timestamp_ms-loop_start_time_local_ms as i128 / 1000000; 404 | let mut more_than_5_secs_left=1; 405 | if left<5 { more_than_5_secs_left=0; } 406 | 407 | let mut this_threads_difficulty=6; 408 | let mut difficulty: u32; 409 | let mut seed = [0_u8; 40]; 410 | let mut equix_builder=equix::EquiXBuilder::new(); 411 | let equix_rt = equix_builder.runtime(equix::RuntimeOption::TryCompile); 412 | let mut nonce_le_bytes: [u8; 8]; 413 | seed[00..32].copy_from_slice(&challenge); 414 | loop { 415 | nonce_le_bytes=nonce.to_le_bytes(); 416 | // let start_time=Instant::now(); 417 | seed[32..40].copy_from_slice(&nonce_le_bytes); 418 | match equix_rt.build(&seed).map_err(|_| drillx_2::DrillxError::BadEquix) { 419 | Ok(equix) => { 420 | let solutions = equix.solve_with_memory(&mut memory); 421 | for solution in solutions { 422 | let digest = solution.to_bytes(); 423 | let hash = drillx_2::hashv(&digest, &nonce_le_bytes); 424 | thread_hashes = thread_hashes.wrapping_add(1); 425 | 426 | // Determine the number of leading zeroes 427 | difficulty = 0; 428 | for byte in hash { 429 | if byte == 0 { 430 | difficulty = difficulty.wrapping_add(8); 431 | } else { 432 | difficulty = difficulty.wrapping_add(byte.leading_zeros()); 433 | break; 434 | } 435 | } 436 | 437 | if difficulty>this_threads_difficulty { 438 | this_threads_difficulty=difficulty; 439 | let better_diff = best_solution.check_for_improved_difficulty(difficulty, nonce, digest, pass_start_time, first_nonce, keypair_being_mined); 440 | if better_diff { 441 | // A higher difficulty has been found since the last difficulty was sent to server 442 | // Send higher difficulty & hope it gets there before the server processes your account 443 | let (_best_difficulty, _best_nonce, _best_digest, _key, _key_pubkey, _difficulty_submitted)= best_solution.read(); 444 | if !is_transaction_in_progress() { 445 | let thread_submission = ThreadSubmission{ 446 | nonce, 447 | difficulty: this_threads_difficulty, 448 | d: digest, 449 | }; 450 | let _ = system_submission_sender.send(MessageSubmissionSystem::Submission(thread_submission)); 451 | 452 | best_solution.update_difficulty_submitted(this_threads_difficulty); 453 | 454 | } else { 455 | let elapsed_str = get_elapsed_string(pass_start_time); 456 | println!("{}{}", elapsed_str, format!("Too late to submit {} ...", this_threads_difficulty).yellow()); 457 | } 458 | } 459 | } 460 | } 461 | }, 462 | Err(_err) => { 463 | // Handle the error case from equix 464 | // println!("Error with equix: {:?}", err); 465 | } 466 | } 467 | 468 | // Increment nonce & process only when we reach the cutoff_nonce 469 | nonce=nonce.wrapping_add(1); 470 | if nonce >= cutoff_nonce { 471 | current_timestamp_ms = Local::now().timestamp_micros(); 472 | 473 | // Determine current nonces per ms for the duration so far 474 | current_nonces_per_ms = (nonce-first_nonce) as f64 / (current_timestamp_ms as i128 - loop_start_time_local_ms as i128) as f64; 475 | 476 | if more_than_5_secs_left>0 { // called before the end of the mining pass - to target 5s before cutoff timestamp to ensure accurate finishing time 477 | nonces_current_interval = ((cutoff_timestamp_ms - current_timestamp_ms as i128 - 5_000_000) as f64 * current_nonces_per_ms) as u64; 478 | 479 | } else { // called at 5s before the end of the mining pass - to rarget 2.5ms before cutoff timestamp 480 | nonces_current_interval = ((cutoff_timestamp_ms - current_timestamp_ms as i128 - 2_500) as f64 * current_nonces_per_ms) as u64; 481 | } 482 | more_than_5_secs_left-=1; 483 | 484 | // Set the number of the cutoff nonce where the next check for completion will take place 485 | cutoff_nonce = nonce.wrapping_add(nonces_current_interval); 486 | 487 | // Exit loop if <1 non to get to cutoff 488 | if nonces_current_interval<1 { 489 | // let elapsed_str = get_elapsed_string(pass_start_time); 490 | // println!("{}[{}] Stopping as nonces_current_interval<1: {} current_nonces_per_ms: {} ms_to_go: {}", 491 | // elapsed_str, thread_number, nonces_current_interval, current_nonces_per_ms, (cutoff_timestamp_ms - current_timestamp_ms as i128)); 492 | break; 493 | } 494 | // Exit if processed nonce range 495 | if nonce >= nonce_range.end { 496 | // let elapsed_str = get_elapsed_string(pass_start_time); 497 | // println!("{}[{}] Stopping at end of nonce range: {}", elapsed_str, thread_number, nonce_range.end); 498 | break; 499 | } 500 | 501 | // Exit if mining pass has ended 502 | if is_transaction_in_progress() { 503 | // let elapsed_str = get_elapsed_string(pass_start_time); 504 | // println!("{}[{}] Stopping as transaction is in progress", elapsed_str, thread_number); 505 | break; 506 | } 507 | } 508 | } 509 | 510 | // Return the number of hashes processed - best_solution contains best difficulty from all threads 511 | Some(thread_hashes) 512 | } 513 | }) 514 | }).collect::>(); 515 | 516 | // Join handles and return best nonce 517 | let mut total_nonces_checked = 0; 518 | for h in handles { 519 | if let Ok(Some(/*nonce, difficulty, hash, */nonces_checked)) = h.unwrap().join() { 520 | total_nonces_checked += nonces_checked; 521 | } 522 | } 523 | let (best_difficulty, _best_nonce, _best_digest, _key, _key_pubkey, _difficulty_submitted)= best_solution.read(); 524 | let finished_mining_local_ms=Local::now().timestamp_micros(); 525 | let mining_took_ms = finished_mining_local_ms - solve_start_time_local_ms; 526 | 527 | // log the hash accuracy time 528 | let overmined_by_ms=(finished_mining_local_ms-cutoff_timestamp_ms as i64) as f64/1000.0; 529 | elapsed_str = get_elapsed_string(pass_start_time); 530 | println!("{}{}", 531 | elapsed_str.clone(), 532 | format!("Finished mining after {:.2}s. Accuracy: {:.0}{}", 533 | mining_took_ms as f64 /1000000.0, 534 | overmined_by_ms, ms_dimmed, 535 | ).yellow().dimmed(), 536 | ); 537 | 538 | // Detect if end of mining pass 539 | if (cutoff_timestamp_ms as i64)< (Local::now().timestamp_micros()-1_000_000) { 540 | record_miner_accuracy(overmined_by_ms); 541 | } 542 | 543 | // Stop the spinner after mining is done 544 | pb.finish_and_clear(); 545 | // println!("✔ Mining complete!"); 546 | println!("\tProcessed: {}", total_nonces_checked); 547 | println!("\tHash time: {:.2}", mining_took_ms as f64 /1000000.0); 548 | let hash_time_secs = (mining_took_ms as f64 /1000000.0) as u32; 549 | if hash_time_secs > 0 { 550 | println!( 551 | "\tHashpower: {:?} H/s", 552 | total_nonces_checked.saturating_div(hash_time_secs) 553 | ); 554 | println!("\tClient found diff: {}", best_difficulty); 555 | } 556 | 557 | let _ = system_submission_sender 558 | .send(MessageSubmissionSystem::Reset); 559 | 560 | //tokio::time::sleep(Duration::from_secs(5 + args.buffer as u64)).await; 561 | 562 | // Ready up again 563 | let now = SystemTime::now() 564 | .duration_since(UNIX_EPOCH) 565 | .expect("Time went backwards") 566 | .as_secs(); 567 | 568 | let msg = now.to_le_bytes(); 569 | let sig = 570 | key.sign_message(&msg).to_string().as_bytes().to_vec(); 571 | let mut bin_data: Vec = Vec::new(); 572 | bin_data.push(0u8); 573 | bin_data.extend_from_slice(&key.pubkey().to_bytes()); 574 | bin_data.extend_from_slice(&msg); 575 | bin_data.extend(sig); 576 | { 577 | let mut message_sender = message_sender.lock().await; 578 | if let Err(_) = 579 | message_sender.send(Message::Binary(bin_data)).await 580 | { 581 | let _ = system_submission_sender 582 | .send(MessageSubmissionSystem::Finish); 583 | println!("Failed to send Ready message. Returning..."); 584 | return; 585 | } 586 | } 587 | } 588 | ServerMessage::PoolSubmissionResult(data) => { 589 | let pool_earned = (data.total_rewards 590 | * 10f64.powf(ore_api::consts::TOKEN_DECIMALS as f64)) 591 | as u64; 592 | let miner_earned = (data.miner_earned_rewards 593 | * 10f64.powf(ore_api::consts::TOKEN_DECIMALS as f64)) 594 | as u64; 595 | let ps = PoolSubmissionResult::new( 596 | data.difficulty, 597 | pool_earned, 598 | data.miner_percentage, 599 | data.miner_supplied_difficulty, 600 | miner_earned, 601 | ); 602 | let _ = db_sender.send(ps); 603 | 604 | let message = format!( 605 | "\n_________________________________________________________________\nPrevious Challenge: {}\nPool Submitted Difficulty: {}\t\tMiner: {}\nPool Earned: {} ORE\tMiner: {} ORE\nPool Balance: {:.11} ORE\t{} of total pool reward\nTop Stake: {:.11} ORE\nPool Multiplier: {:.2}x\nActive Miners: {}\n‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾", 606 | BASE64_STANDARD.encode(data.challenge), 607 | format!("{}", data.difficulty).blue(), 608 | format!("{}", data.miner_supplied_difficulty).green(), 609 | format!("{:11}", data.total_rewards).blue(), 610 | format!("{:11}", data.miner_earned_rewards).green(), 611 | data.total_balance, 612 | format!("{:.3}%", data.miner_percentage).green(), 613 | data.top_stake, 614 | data.multiplier, 615 | data.active_miners, 616 | ); 617 | println!("{}", message); 618 | } 619 | } 620 | } 621 | }); 622 | } 623 | 624 | // If the websocket message receiver finishes, also finish the solution submission 625 | // sender system 626 | let _ = receiver_thread.await; 627 | let _ = solution_system_submission_sender.send(MessageSubmissionSystem::Finish); 628 | println!("Channels cleaned up, reconnecting...\n"); 629 | } 630 | Err(e) => { 631 | match e { 632 | tokio_tungstenite::tungstenite::Error::Http(e) => { 633 | if let Some(body) = e.body() { 634 | println!("Error: {:?}", String::from_utf8(body.to_vec())); 635 | } else { 636 | println!("Http Error: {:?}", e); 637 | } 638 | } 639 | _ => { 640 | println!("Error: {:?}", e); 641 | } 642 | } 643 | tokio::time::sleep(Duration::from_secs(3)).await; 644 | } 645 | } 646 | } 647 | } 648 | 649 | fn process_message( 650 | msg: Message, 651 | message_channel: UnboundedSender, 652 | ) -> ControlFlow<(), bool> { 653 | let pass_start_time = get_global_pass_start_time(); 654 | let elapsed_str = get_elapsed_string(pass_start_time); 655 | let mut got_start_mining_message = false; 656 | match msg { 657 | Message::Text(t) => { 658 | if t.starts_with("Pool Submitted") { 659 | println!("{}{}", elapsed_str, "Server: Rewards Received".bright_magenta()); 660 | } else { 661 | println!("{}{}{}", elapsed_str, "Server: ".dimmed(), t.blue()); 662 | } 663 | if t=="Server is sending mine transaction..." { 664 | if !is_transaction_in_progress() { 665 | record_tx_started(); 666 | } 667 | set_no_more_submissions(true); 668 | } 669 | } 670 | Message::Binary(b) => { 671 | let message_type = b[0]; 672 | match message_type { 673 | 0 => { 674 | if b.len() < 49 { 675 | println!("Invalid data for Message StartMining"); 676 | } else { 677 | let mut hash_bytes = [0u8; 32]; 678 | // extract 256 bytes (32 u8's) from data for hash 679 | let mut b_index = 1; 680 | for i in 0..32 { 681 | hash_bytes[i] = b[i + b_index]; 682 | } 683 | b_index += 32; 684 | 685 | // extract 64 bytes (8 u8's) 686 | let mut cutoff_bytes = [0u8; 8]; 687 | for i in 0..8 { 688 | cutoff_bytes[i] = b[i + b_index]; 689 | } 690 | b_index += 8; 691 | let cutoff = u64::from_le_bytes(cutoff_bytes); 692 | 693 | let mut nonce_start_bytes = [0u8; 8]; 694 | for i in 0..8 { 695 | nonce_start_bytes[i] = b[i + b_index]; 696 | } 697 | b_index += 8; 698 | let nonce_start = u64::from_le_bytes(nonce_start_bytes); 699 | 700 | let mut nonce_end_bytes = [0u8; 8]; 701 | for i in 0..8 { 702 | nonce_end_bytes[i] = b[i + b_index]; 703 | } 704 | let nonce_end = u64::from_le_bytes(nonce_end_bytes); 705 | 706 | let msg = 707 | ServerMessage::StartMining(hash_bytes, nonce_start..nonce_end, cutoff); 708 | 709 | let _ = message_channel.send(msg); 710 | got_start_mining_message = true; 711 | } 712 | } 713 | 1 => { 714 | let msg = ServerMessage::PoolSubmissionResult( 715 | ServerMessagePoolSubmissionResult::new_from_bytes(b), 716 | ); 717 | let _ = message_channel.send(msg); 718 | } 719 | _ => { 720 | println!("Failed to parse server message type"); 721 | } 722 | } 723 | } 724 | Message::Ping(_) => {} 725 | Message::Pong(_) => {} 726 | Message::Close(v) => { 727 | println!("Got Close: {:?}", v); 728 | return ControlFlow::Break(()); 729 | } 730 | _ => {} 731 | } 732 | 733 | ControlFlow::Continue(got_start_mining_message) 734 | } 735 | 736 | async fn submission_system( 737 | key: Arc, 738 | mut system_message_receiver: UnboundedReceiver, 739 | socket_sender: Arc>, Message>>>, 740 | ) { 741 | let mut best_diff = 0; 742 | while let Some(msg) = system_message_receiver.recv().await { 743 | match msg { 744 | MessageSubmissionSystem::Submission(thread_submission) => { 745 | if thread_submission.difficulty > best_diff { 746 | best_diff = thread_submission.difficulty; 747 | 748 | // Send results to the server 749 | let message_type = 2u8; // 1 u8 - BestSolution Message 750 | let best_hash_bin = thread_submission.d; // 16 u8 751 | let best_nonce_bin = thread_submission.nonce.to_le_bytes(); // 8 u8 752 | 753 | let mut hash_nonce_message = [0; 24]; 754 | hash_nonce_message[0..16].copy_from_slice(&best_hash_bin); 755 | hash_nonce_message[16..24].copy_from_slice(&best_nonce_bin); 756 | let signature = key 757 | .sign_message(&hash_nonce_message) 758 | .to_string() 759 | .as_bytes() 760 | .to_vec(); 761 | 762 | let mut bin_data = [0; 57]; 763 | bin_data[00..1].copy_from_slice(&message_type.to_le_bytes()); 764 | bin_data[01..17].copy_from_slice(&best_hash_bin); 765 | bin_data[17..25].copy_from_slice(&best_nonce_bin); 766 | bin_data[25..57].copy_from_slice(&key.pubkey().to_bytes()); 767 | 768 | let mut bin_vec = bin_data.to_vec(); 769 | bin_vec.extend(signature); 770 | 771 | let mut message_sender = socket_sender.lock().await; 772 | let _ = message_sender.send(Message::Binary(bin_vec)).await; 773 | drop(message_sender); 774 | } 775 | } 776 | MessageSubmissionSystem::Reset => { 777 | best_diff = 0; 778 | 779 | // Sleep for 2 seconds to let the submission window open again 780 | tokio::time::sleep(Duration::from_secs(2)).await; 781 | } 782 | MessageSubmissionSystem::Finish => { 783 | return; 784 | } 785 | } 786 | } 787 | } 788 | 789 | // SAFETY: We ensure that access to `digest` is properly synchronized 790 | // through the `check_for_improved_difficulty` method. 791 | unsafe impl Sync for MiningSolution {} 792 | struct MiningSolution { 793 | difficulty: AtomicU32, 794 | difficulty_submitted: AtomicU32, 795 | nonce: AtomicU64, 796 | digest: UnsafeCell<[u8; 16]>, 797 | key: Keypair, 798 | } 799 | 800 | impl MiningSolution { 801 | fn new(key: Keypair) -> Arc { 802 | let hx=drillx_2::Hash::default(); 803 | Arc::new(Self { 804 | difficulty: AtomicU32::new(0), 805 | difficulty_submitted: AtomicU32::new(0), 806 | nonce: AtomicU64::new(0), 807 | digest: UnsafeCell::new(hx.d), 808 | key, 809 | }) 810 | } 811 | 812 | fn _update_difficulty(&self, new_difficulty: u32) { 813 | self.difficulty.store(new_difficulty, Ordering::Relaxed); 814 | } 815 | 816 | fn _update_nonce(&self, new_nonce: u64) { 817 | self.nonce.store(new_nonce, Ordering::Relaxed); 818 | } 819 | 820 | fn update_difficulty_submitted(&self, the_difficulty: u32) { 821 | self.difficulty_submitted.store(the_difficulty, Ordering::Relaxed); 822 | } 823 | 824 | fn read(&self) -> (u32, u64, [u8; 16], &Keypair, [u8; 32], u32) { 825 | let difficulty = self.difficulty.load(Ordering::Relaxed); 826 | let difficulty_submitted = self.difficulty_submitted.load(Ordering::Relaxed); 827 | let nonce = self.nonce.load(Ordering::Relaxed); 828 | // SAFETY: We're only reading the digest, which is safe as long as we're not writing to it 829 | let digest = unsafe { *self.digest.get() }; 830 | // let key = unsafe { *self.key.get() }; 831 | (difficulty, nonce, digest, &self.key, self.key.pubkey().to_bytes(), difficulty_submitted) 832 | } 833 | 834 | fn check_for_improved_difficulty(&self, current_difficulty: u32, current_nonce: u64, digest: [u8; 16], _pass_start_time: Instant, _first_nonce: u64, _keypair_being_mined: u32) -> bool { 835 | if current_difficulty > self.difficulty.load(Ordering::Relaxed) { 836 | if is_transaction_in_progress() { 837 | return false; 838 | } 839 | 840 | self.difficulty.store(current_difficulty, Ordering::Relaxed); 841 | self.nonce.store(current_nonce, Ordering::Relaxed); 842 | // SAFETY: We're ensuring single-threaded access to `digest` by checking difficulty first 843 | unsafe { *self.digest.get() = digest }; 844 | 845 | println!("{}[{}{}] {} {}", 846 | "\x1B[1A ", 847 | format!("{:>4.1}", (_pass_start_time.elapsed().as_millis() as f64 / 1000.0)).dimmed(), 848 | ("s".dimmed()).to_string(), 849 | format!("Mined").dimmed(), 850 | format!("diff {}", current_difficulty).bright_cyan(), 851 | // format!("nonce {}", current_nonce-_first_nonce).cyan(), 852 | ); 853 | true 854 | } else { 855 | false 856 | } 857 | } 858 | } 859 | -------------------------------------------------------------------------------- /src/protomine.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ops::{ControlFlow, Range}, 3 | sync::Arc, 4 | time::{Duration, Instant, SystemTime, UNIX_EPOCH}, 5 | }; 6 | 7 | use base64::prelude::*; 8 | use clap::Parser; 9 | use drillx_2::equix; 10 | use futures_util::{SinkExt, StreamExt}; 11 | use rayon::prelude::*; 12 | use solana_sdk::{signature::Keypair, signer::Signer}; 13 | use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; 14 | use std::sync::Once; 15 | use tokio::sync::mpsc::{unbounded_channel, UnboundedSender}; 16 | use tokio_tungstenite::{ 17 | connect_async, 18 | tungstenite::{ 19 | handshake::client::{generate_key, Request}, 20 | Message, 21 | }, 22 | }; 23 | 24 | static INIT_RAYON: Once = Once::new(); 25 | 26 | // Constants for tuning performance 27 | const MIN_CHUNK_SIZE: u64 = 3_000_000; 28 | const MAX_CHUNK_SIZE: u64 = 30_000_000; 29 | 30 | #[derive(Debug)] 31 | pub enum ServerMessage { 32 | StartMining([u8; 32], Range, u64), 33 | } 34 | 35 | #[derive(Debug, Parser)] 36 | pub struct MineArgs { 37 | #[arg( 38 | long, 39 | value_name = "threads", 40 | default_value = "1", 41 | help = "Number of threads to use while mining" 42 | )] 43 | pub threads: usize, 44 | } 45 | 46 | struct MiningResult { 47 | nonce: u64, 48 | difficulty: u32, 49 | hash: drillx_2::Hash, 50 | _nonces_checked: u64, 51 | } 52 | 53 | impl MiningResult { 54 | fn new() -> Self { 55 | MiningResult { 56 | nonce: 0, 57 | difficulty: 0, 58 | hash: drillx_2::Hash::default(), 59 | _nonces_checked: 0, 60 | } 61 | } 62 | } 63 | 64 | fn calculate_dynamic_chunk_size(nonce_range: &Range, threads: usize) -> u64 { 65 | let range_size = nonce_range.end - nonce_range.start; 66 | let chunks_per_thread = 5; 67 | let ideal_chunk_size = range_size / (threads * chunks_per_thread) as u64; 68 | 69 | ideal_chunk_size.clamp(MIN_CHUNK_SIZE, MAX_CHUNK_SIZE) 70 | } 71 | 72 | fn optimized_mining_rayon( 73 | challenge: &[u8; 32], 74 | nonce_range: Range, 75 | cutoff_time: u64, 76 | threads: usize, 77 | ) -> (u64, u32, drillx_2::Hash, u64) { 78 | let stop_signal = Arc::new(AtomicBool::new(false)); 79 | let total_nonces_checked = Arc::new(AtomicU64::new(0)); 80 | 81 | // Initialize Rayon thread pool only once 82 | INIT_RAYON.call_once(|| { 83 | rayon::ThreadPoolBuilder::new() 84 | .num_threads(threads) 85 | .build_global() 86 | .expect("Failed to initialize global thread pool"); 87 | }); 88 | 89 | let chunk_size = calculate_dynamic_chunk_size(&nonce_range, threads); 90 | let start_time = Instant::now(); 91 | 92 | let results: Vec = (0..threads) 93 | .into_par_iter() 94 | .map(|core_id| { 95 | let mut memory = equix::SolverMemory::new(); 96 | let core_range_size = (nonce_range.end - nonce_range.start) / threads as u64; 97 | let core_start = nonce_range.start + core_id as u64 * core_range_size; 98 | let core_end = if core_id == threads - 1 { 99 | nonce_range.end 100 | } else { 101 | core_start + core_range_size 102 | }; 103 | 104 | let mut core_best = MiningResult::new(); 105 | let mut local_nonces_checked = 0; 106 | 107 | 'outer: for chunk_start in (core_start..core_end).step_by(chunk_size as usize) { 108 | let chunk_end = (chunk_start + chunk_size).min(core_end); 109 | for nonce in chunk_start..chunk_end { 110 | if start_time.elapsed().as_secs() >= cutoff_time { 111 | break 'outer; 112 | } 113 | 114 | if stop_signal.load(Ordering::Relaxed) { 115 | break 'outer; 116 | } 117 | 118 | for hx in drillx_2::get_hashes_with_memory( 119 | &mut memory, 120 | challenge, 121 | &nonce.to_le_bytes(), 122 | ) { 123 | local_nonces_checked += 1; 124 | let difficulty = hx.difficulty(); 125 | 126 | if difficulty > core_best.difficulty { 127 | core_best = MiningResult { 128 | nonce, 129 | difficulty, 130 | hash: hx, 131 | _nonces_checked: local_nonces_checked, 132 | }; 133 | } 134 | } 135 | 136 | if nonce % 100 == 0 && start_time.elapsed().as_secs() >= cutoff_time { 137 | if core_best.difficulty >= 8 { 138 | break 'outer; 139 | } 140 | } 141 | } 142 | } 143 | 144 | total_nonces_checked.fetch_add(local_nonces_checked, Ordering::Relaxed); 145 | core_best 146 | }) 147 | .collect(); 148 | 149 | stop_signal.store(true, Ordering::Relaxed); 150 | 151 | let best_result = results 152 | .into_iter() 153 | .reduce(|acc, x| { 154 | if x.difficulty > acc.difficulty { 155 | x 156 | } else { 157 | acc 158 | } 159 | }) 160 | .unwrap_or_else(MiningResult::new); 161 | 162 | ( 163 | best_result.nonce, 164 | best_result.difficulty, 165 | best_result.hash, 166 | total_nonces_checked.load(Ordering::Relaxed), 167 | ) 168 | } 169 | 170 | pub async fn protomine(args: MineArgs, key: Keypair, url: String, unsecure: bool) { 171 | let mut threads = args.threads; 172 | let max_threads = core_affinity::get_core_ids().unwrap().len(); 173 | if threads > max_threads { 174 | threads = max_threads; 175 | } 176 | 177 | loop { 178 | let base_url = url.clone(); 179 | let mut ws_url_str = if unsecure { 180 | format!("ws://{}", url) 181 | } else { 182 | format!("wss://{}", url) 183 | }; 184 | 185 | if !ws_url_str.ends_with('/') { 186 | ws_url_str.push('/'); 187 | } 188 | 189 | let client = reqwest::Client::new(); 190 | 191 | let http_prefix = if unsecure { "http" } else { "https" }; 192 | 193 | let timestamp = match client 194 | .get(format!("{}://{}/timestamp", http_prefix, base_url)) 195 | .send() 196 | .await 197 | { 198 | Ok(response) => { 199 | match response.text().await { 200 | Ok(ts) => match ts.parse::() { 201 | Ok(timestamp) => timestamp, 202 | Err(_) => { 203 | eprintln!("Server response body for /timestamp failed to parse, contact admin."); 204 | tokio::time::sleep(Duration::from_secs(3)).await; 205 | continue; 206 | } 207 | }, 208 | Err(_) => { 209 | eprintln!("Server response body for /timestamp is empty, contact admin."); 210 | tokio::time::sleep(Duration::from_secs(3)).await; 211 | continue; 212 | } 213 | } 214 | } 215 | Err(_) => { 216 | eprintln!("Server restarting, trying again in 3 seconds..."); 217 | tokio::time::sleep(Duration::from_secs(3)).await; 218 | continue; 219 | } 220 | }; 221 | 222 | println!("Server Timestamp: {}", timestamp); 223 | 224 | let ts_msg = timestamp.to_le_bytes(); 225 | let sig = key.sign_message(&ts_msg); 226 | 227 | ws_url_str.push_str(&format!("?timestamp={}", timestamp)); 228 | let url = url::Url::parse(&ws_url_str).expect("Failed to parse server url"); 229 | let host = url.host_str().expect("Invalid host in server url"); 230 | 231 | let auth = BASE64_STANDARD.encode(format!("{}:{}", key.pubkey(), sig)); 232 | 233 | println!("Connecting to server..."); 234 | let request = Request::builder() 235 | .method("GET") 236 | .uri(url.to_string()) 237 | .header("Sec-Websocket-Key", generate_key()) 238 | .header("Host", host) 239 | .header("Upgrade", "websocket") 240 | .header("Connection", "upgrade") 241 | .header("Sec-Websocket-Version", "13") 242 | .header("Authorization", format!("Basic {}", auth)) 243 | .body(()) 244 | .unwrap(); 245 | 246 | match connect_async(request).await { 247 | Ok((ws_stream, _)) => { 248 | println!("Connected to network!"); 249 | 250 | let (mut sender, mut receiver) = ws_stream.split(); 251 | let (message_sender, mut message_receiver) = unbounded_channel::(); 252 | 253 | let receiver_thread = tokio::spawn(async move { 254 | while let Some(Ok(message)) = receiver.next().await { 255 | if process_message(message, message_sender.clone()).is_break() { 256 | break; 257 | } 258 | } 259 | }); 260 | 261 | // send Ready message 262 | let now = SystemTime::now() 263 | .duration_since(UNIX_EPOCH) 264 | .expect("Time went backwards") 265 | .as_secs(); 266 | 267 | let msg = now.to_le_bytes(); 268 | let sig = key.sign_message(&msg).to_string().as_bytes().to_vec(); 269 | let mut bin_data: Vec = Vec::with_capacity(1 + 32 + 8 + sig.len()); 270 | bin_data.push(0u8); 271 | bin_data.extend_from_slice(&key.pubkey().to_bytes()); 272 | bin_data.extend_from_slice(&msg); 273 | bin_data.extend(sig); 274 | 275 | let _ = sender.send(Message::Binary(bin_data)).await; 276 | 277 | // receive messages 278 | while let Some(msg) = message_receiver.recv().await { 279 | match msg { 280 | ServerMessage::StartMining(challenge, nonce_range, cutoff) => { 281 | println!("Received start mining message!"); 282 | println!("Mining starting (Using Protomine)..."); 283 | println!("Nonce range: {} - {}", nonce_range.start, nonce_range.end); 284 | let hash_timer = Instant::now(); 285 | 286 | let cutoff_time = cutoff; // Use the provided cutoff directly 287 | 288 | let (best_nonce, best_difficulty, best_hash, total_nonces_checked) = 289 | optimized_mining_rayon( 290 | &challenge, 291 | nonce_range, 292 | cutoff_time, 293 | threads, 294 | ); 295 | 296 | let hash_time = hash_timer.elapsed(); 297 | 298 | println!("Found best diff: {}", best_difficulty); 299 | println!("Processed: {}", total_nonces_checked); 300 | println!("Hash time: {:?}", hash_time); 301 | let hash_time_secs = hash_time.as_secs(); 302 | if hash_time_secs > 0 { 303 | println!( 304 | "Hashpower: {:?} H/s", 305 | total_nonces_checked.saturating_div(hash_time_secs) 306 | ); 307 | } 308 | 309 | let message_type = 2u8; // 2 u8 - BestSolution Message 310 | let best_hash_bin = best_hash.d; // 16 u8 311 | let best_nonce_bin = best_nonce.to_le_bytes(); // 8 u8 312 | 313 | let mut hash_nonce_message = [0; 24]; 314 | hash_nonce_message[0..16].copy_from_slice(&best_hash_bin); 315 | hash_nonce_message[16..24].copy_from_slice(&best_nonce_bin); 316 | let signature = key 317 | .sign_message(&hash_nonce_message) 318 | .to_string() 319 | .as_bytes() 320 | .to_vec(); 321 | 322 | let mut bin_data = Vec::with_capacity(57 + signature.len()); 323 | bin_data.extend_from_slice(&message_type.to_le_bytes()); 324 | bin_data.extend_from_slice(&best_hash_bin); 325 | bin_data.extend_from_slice(&best_nonce_bin); 326 | bin_data.extend_from_slice(&key.pubkey().to_bytes()); 327 | bin_data.extend(signature); 328 | 329 | let _ = sender.send(Message::Binary(bin_data)).await; 330 | 331 | tokio::time::sleep(Duration::from_secs(3)).await; 332 | 333 | let now = SystemTime::now() 334 | .duration_since(UNIX_EPOCH) 335 | .expect("Time went backwards") 336 | .as_secs(); 337 | 338 | let msg = now.to_le_bytes(); 339 | let sig = key.sign_message(&msg).to_string().as_bytes().to_vec(); 340 | let mut bin_data = Vec::with_capacity(1 + 32 + 8 + sig.len()); 341 | bin_data.push(0u8); 342 | bin_data.extend_from_slice(&key.pubkey().to_bytes()); 343 | bin_data.extend_from_slice(&msg); 344 | bin_data.extend(sig); 345 | 346 | let _ = sender.send(Message::Binary(bin_data)).await; 347 | } 348 | } 349 | } 350 | 351 | let _ = receiver_thread.await; 352 | } 353 | Err(e) => { 354 | match e { 355 | tokio_tungstenite::tungstenite::Error::Http(e) => { 356 | if let Some(body) = e.body() { 357 | eprintln!("Error: {:?}", String::from_utf8_lossy(body)); 358 | } else { 359 | eprintln!("Http Error: {:?}", e); 360 | } 361 | } 362 | _ => { 363 | eprintln!("Error: {:?}", e); 364 | } 365 | } 366 | tokio::time::sleep(Duration::from_secs(3)).await; 367 | } 368 | } 369 | } 370 | } 371 | 372 | fn process_message( 373 | msg: Message, 374 | message_channel: UnboundedSender, 375 | ) -> ControlFlow<(), ()> { 376 | match msg { 377 | Message::Text(t) => { 378 | println!("\n>>> Server Message: \n{}\n", t); 379 | } 380 | Message::Binary(b) => { 381 | let message_type = b[0]; 382 | match message_type { 383 | 0 => { 384 | if b.len() < 49 { 385 | println!("Invalid data for Message StartMining"); 386 | } else { 387 | let mut hash_bytes = [0u8; 32]; 388 | // extract 256 bytes (32 u8's) from data for hash 389 | let mut b_index = 1; 390 | for i in 0..32 { 391 | hash_bytes[i] = b[i + b_index]; 392 | } 393 | b_index += 32; 394 | 395 | // extract 64 bytes (8 u8's) 396 | let mut cutoff_bytes = [0u8; 8]; 397 | for i in 0..8 { 398 | cutoff_bytes[i] = b[i + b_index]; 399 | } 400 | b_index += 8; 401 | let cutoff = u64::from_le_bytes(cutoff_bytes); 402 | 403 | let mut nonce_start_bytes = [0u8; 8]; 404 | for i in 0..8 { 405 | nonce_start_bytes[i] = b[i + b_index]; 406 | } 407 | b_index += 8; 408 | let nonce_start = u64::from_le_bytes(nonce_start_bytes); 409 | 410 | let mut nonce_end_bytes = [0u8; 8]; 411 | for i in 0..8 { 412 | nonce_end_bytes[i] = b[i + b_index]; 413 | } 414 | let nonce_end = u64::from_le_bytes(nonce_end_bytes); 415 | 416 | let msg = 417 | ServerMessage::StartMining(hash_bytes, nonce_start..nonce_end, cutoff); 418 | 419 | let _ = message_channel.send(msg); 420 | } 421 | } 422 | _ => { 423 | println!("Failed to parse server message type"); 424 | } 425 | } 426 | } 427 | Message::Ping(v) => { 428 | println!("Got Ping: {:?}", v); 429 | } 430 | Message::Pong(v) => { 431 | println!("Got Pong: {:?}", v); 432 | } 433 | Message::Close(v) => { 434 | println!("Got Close: {:?}", v); 435 | return ControlFlow::Break(()); 436 | } 437 | _ => { 438 | println!("Got invalid message data"); 439 | } 440 | } 441 | 442 | ControlFlow::Continue(()) 443 | } 444 | -------------------------------------------------------------------------------- /src/signup.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use clap::Parser; 4 | use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer}; 5 | 6 | #[derive(Debug, Parser)] 7 | pub struct SignupArgs { 8 | #[arg( 9 | long, 10 | value_name = "PUBKEY", 11 | default_value = None, 12 | help = "Miner public key to enable." 13 | )] 14 | pub pubkey: Option, 15 | } 16 | 17 | pub async fn signup(args: SignupArgs, url: String, key: Keypair, unsecure: bool) { 18 | let miner_pubkey = if args.pubkey.is_some() { 19 | match Pubkey::from_str(&args.pubkey.unwrap()) { 20 | Ok(pk) => pk, 21 | Err(_e) => { 22 | println!("Invalid miner pubkey arg provided."); 23 | return; 24 | } 25 | } 26 | } else { 27 | key.pubkey() 28 | }; 29 | 30 | let base_url = url; 31 | 32 | let client = reqwest::Client::new(); 33 | 34 | let url_prefix = if unsecure { 35 | "http".to_string() 36 | } else { 37 | "https".to_string() 38 | }; 39 | 40 | let resp = client 41 | .post(format!( 42 | "{}://{}/v2/signup?miner={}", 43 | url_prefix, 44 | base_url, 45 | miner_pubkey.to_string(), 46 | )) 47 | .body("BLANK".to_string()) 48 | .send() 49 | .await; 50 | if let Ok(res) = resp { 51 | if let Ok(txt) = res.text().await { 52 | match txt.as_str() { 53 | "SUCCESS" => { 54 | println!(" Successfully signed up!"); 55 | } 56 | "EXISTS" => { 57 | println!(" You're already signed up!"); 58 | } 59 | _ => { 60 | println!(" Transaction failed, please try again."); 61 | } 62 | } 63 | } else { 64 | println!(" Transaction failed, please wait and try again."); 65 | } 66 | } else { 67 | println!(" Transaction failed, please wait and try again."); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/stake_balance.rs: -------------------------------------------------------------------------------- 1 | use solana_sdk::{signature::Keypair, signer::Signer}; 2 | 3 | pub async fn stake_balance(key: &Keypair, url: String, unsecure: bool) { 4 | let base_url = url; 5 | let client = reqwest::Client::new(); 6 | 7 | let url_prefix = if unsecure { 8 | "http".to_string() 9 | } else { 10 | "https".to_string() 11 | }; 12 | 13 | match client 14 | .get(format!( 15 | "{}://{}/miner/stake?pubkey={}", 16 | url_prefix, 17 | base_url, 18 | key.pubkey().to_string() 19 | )) 20 | .send() 21 | .await 22 | { 23 | Ok(response) => { 24 | let balance = response.text().await.unwrap(); 25 | // Check if the balance failed to load 26 | if balance.contains("Failed to g") { 27 | println!(" Staked Balance: No staked account"); 28 | } else { 29 | println!(" Staked Balance: {:.11} ORE", balance); 30 | } 31 | } 32 | Err(e) => { 33 | println!(" Error fetching stake balance: {:?}", e); 34 | } 35 | } 36 | } 37 | 38 | pub async fn get_staked_balance(key: &Keypair, url: String, unsecure: bool) -> f64 { 39 | let base_url = url; 40 | let client = reqwest::Client::new(); 41 | let url_prefix = if unsecure { "http" } else { "https" }; 42 | 43 | match client 44 | .get(format!( 45 | "{}://{}/miner/stake?pubkey={}", 46 | url_prefix, 47 | base_url, 48 | key.pubkey().to_string() 49 | )) 50 | .send() 51 | .await 52 | { 53 | Ok(response) => { 54 | let balance_str = response.text().await.unwrap(); 55 | if balance_str.contains("Failed to g") { 56 | println!(" Delegated stake balance: No staked account"); 57 | 0.0 58 | } else { 59 | balance_str.parse::().unwrap_or(0.0) 60 | } 61 | } 62 | Err(e) => { 63 | println!(); 64 | println!(" Error fetching stake balance: {:?}", e); 65 | 0.0 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/stats.rs: -------------------------------------------------------------------------------- 1 | use colored::*; 2 | use std::time::Instant; 3 | use once_cell::sync::Lazy; 4 | use std::sync::Mutex; 5 | 6 | pub fn get_elapsed_string(elapsed: Instant) -> String { 7 | format!("[{}{}] ", format!("{:>4.1}", (elapsed.elapsed().as_millis() as f64 / 1000.0)).dimmed(), "s".dimmed()).to_string() 8 | } 9 | 10 | pub static GLOBAL_PASS_START_TIME: Lazy> = Lazy::new(|| Mutex::new(Instant::now())); 11 | pub static GLOBAL_PASS_NUMBER: Lazy> = Lazy::new(|| Mutex::new(0)); 12 | pub fn set_global_pass_start_time(i: Instant, pass_number: u64) { 13 | let mut global_pass_start_time = GLOBAL_PASS_START_TIME.lock().unwrap(); 14 | *global_pass_start_time=i.clone(); 15 | let mut global_pass_number = GLOBAL_PASS_NUMBER.lock().unwrap(); 16 | *global_pass_number=pass_number; 17 | } 18 | pub fn get_global_pass_start_time() -> Instant { 19 | let global_pass_start_time = GLOBAL_PASS_START_TIME.lock().unwrap(); 20 | *global_pass_start_time 21 | } 22 | 23 | pub static MINER_ACCURACY_BUFFER: Lazy> = Lazy::new(|| Mutex::new(CircularBuffer::new(30))); 24 | pub fn get_miner_accuracy() -> f64 { 25 | let miner_accuracy_buffer = MINER_ACCURACY_BUFFER.lock().unwrap(); 26 | miner_accuracy_buffer.calculate_median() 27 | } 28 | pub fn record_miner_accuracy(accuracy: f64) { 29 | let mut miner_accuracy_buffer = MINER_ACCURACY_BUFFER.lock().unwrap(); 30 | if accuracy>=-1000.0 && accuracy<=5_000_000.0 { 31 | miner_accuracy_buffer.insert(accuracy); 32 | println!(" Accuracy: {} {}\t\t\t[{} -> {} -> {}]", 33 | format!("{:.0}", accuracy).green(), ("ms").dimmed(), 34 | format!("{:.0}", miner_accuracy_buffer.calculate_min()), 35 | format!("{:.0}", miner_accuracy_buffer.calculate_median()).cyan(), 36 | format!("{:.0}", miner_accuracy_buffer.calculate_max()).green(), 37 | ); 38 | } else { 39 | println!(" Accuracy: {}{}\t{}", 40 | format!("{:.0}", accuracy).green(), ("ms").dimmed(), 41 | format!("Ignored as outwith tolerance").yellow(), 42 | ); 43 | } 44 | } 45 | 46 | // ------------------------------------- 47 | pub static NO_MORE_SUBMISSIONS: Lazy> = Lazy::new(|| Mutex::new(false)); 48 | pub fn set_no_more_submissions(the_state: bool) { 49 | let mut no_more_submissions = NO_MORE_SUBMISSIONS.lock().unwrap(); 50 | *no_more_submissions=the_state; 51 | // println!("record_tx_started: {}ms ago", global_tx_start_time.elapsed().as_millis()); 52 | } 53 | pub fn is_transaction_in_progress() -> bool { 54 | // no_proe_submissions==true => transaction in progress 55 | let no_more_submissions = NO_MORE_SUBMISSIONS.lock().unwrap(); 56 | *no_more_submissions 57 | } 58 | 59 | pub static TX_TIME_BUFFER: Lazy> = Lazy::new(|| Mutex::new(CircularBuffer::new(120))); 60 | pub static GLOBAL_TX_START_TIME: Lazy> = Lazy::new(|| Mutex::new(Instant::now())); 61 | pub static GLOBAL_TX_OVERTIME: Lazy> = Lazy::new(|| Mutex::new(0)); 62 | pub fn record_tx_started() { 63 | let mut global_tx_start_time = GLOBAL_TX_START_TIME.lock().unwrap(); 64 | *global_tx_start_time=Instant::now(); 65 | // println!("record_tx_started: {}ms ago", global_tx_start_time.elapsed().as_millis()); 66 | } 67 | pub fn record_tx_complete() { 68 | let global_tx_start_time = GLOBAL_TX_START_TIME.lock().unwrap(); 69 | let tx_time_ms = global_tx_start_time.elapsed().as_micros() as f64 / 1000000.0; 70 | let mut tx_time_buffer = TX_TIME_BUFFER.lock().unwrap(); 71 | tx_time_buffer.insert(tx_time_ms); 72 | 73 | if tx_time_ms>10.0 { 74 | let mut global_tx_overtime = GLOBAL_TX_OVERTIME.lock().unwrap(); 75 | *global_tx_overtime+=1; 76 | } 77 | } 78 | 79 | // Implement a circular buffer for calculating averages of last N values. 80 | pub struct CircularBuffer { 81 | data: Vec, 82 | capacity: usize, 83 | } 84 | impl CircularBuffer { 85 | pub fn new(capacity: usize) -> Self { 86 | CircularBuffer { 87 | data: Vec::with_capacity(capacity), 88 | capacity, 89 | } 90 | } 91 | 92 | pub fn insert(&mut self, value: f64) { 93 | if self.data.len() >= self.capacity { 94 | self.data.remove(0); // Remove the oldest entry 95 | } 96 | self.data.push(value); 97 | } 98 | 99 | pub fn _num_entries(&self) -> u128 { 100 | self.data.len() as u128 101 | } 102 | 103 | // pub fn latest_entry(&self) -> f64 { 104 | // if self.data.len() == 0 { 105 | // 0.0 106 | // } else { 107 | // self.data[self.data.len()-1] 108 | // } 109 | // } 110 | 111 | // pub fn get_last_entries(&self) -> Vec { 112 | // let start = self.data.len().saturating_sub(7); 113 | // self.data[start..].to_vec() 114 | // } 115 | 116 | pub fn calculate_median(&self) -> f64 { 117 | if self.data.is_empty() { 118 | return 0.0; 119 | } 120 | let mut sorted = self.data.clone(); 121 | sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); 122 | let mid = sorted.len() / 2; 123 | if sorted.len()<7 { 124 | // Return median on short lists 125 | if sorted.len() % 2 == 0 { 126 | return (sorted[mid - 1] + sorted[mid]) / 2.0; 127 | } else { 128 | return sorted[mid]; 129 | } 130 | } 131 | 132 | // Return average around the median with >7 values 133 | let min=mid-3; 134 | let max=mid+3; 135 | let mut total: f64=0.0; 136 | let mut i=min; 137 | while i<=max { 138 | total+=sorted[i]; 139 | i+=1; 140 | } 141 | total/((max-min)+1) as f64 142 | } 143 | 144 | // pub fn calculate_average(&self) -> f64 { 145 | // if self.data.is_empty() { 146 | // return 0.0; 147 | // } 148 | // let sum: f64 = self.data.iter().sum(); 149 | // sum / self.data.len() as f64 150 | // } 151 | 152 | pub fn calculate_max(&self) -> f64 { 153 | self.data.iter().cloned().max_by(|a, b| { 154 | if a.is_nan() { 155 | std::cmp::Ordering::Greater 156 | } else if b.is_nan() { 157 | std::cmp::Ordering::Less 158 | } else { 159 | a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal) 160 | } 161 | }).unwrap_or(0.0) 162 | } 163 | 164 | pub fn calculate_min(&self) -> f64 { 165 | self.data.iter().cloned().min_by(|a, b| { 166 | if a.is_nan() { 167 | std::cmp::Ordering::Greater 168 | } else if b.is_nan() { 169 | std::cmp::Ordering::Less 170 | } else { 171 | a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal) 172 | } 173 | }).unwrap_or(0.0) 174 | } 175 | } 176 | 177 | 178 | -------------------------------------------------------------------------------- /src/undelegate_boost.rs: -------------------------------------------------------------------------------- 1 | use base64::{prelude::BASE64_STANDARD, Engine}; 2 | use clap::Parser; 3 | use colored::*; 4 | use inquire::{InquireError, Text}; 5 | use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer, transaction::Transaction}; 6 | use std::str::FromStr; 7 | 8 | #[derive(Debug, Parser)] 9 | pub struct UnboostArgs { 10 | #[arg( 11 | long, 12 | value_name = "AMOUNT", 13 | help = "Amount of boost token to unstake." 14 | )] 15 | pub amount: f64, 16 | 17 | #[arg(long, value_name = "MINT", help = "Mint address of the boost token.")] 18 | pub mint: String, 19 | } 20 | 21 | pub async fn undelegate_boost(args: UnboostArgs, key: Keypair, url: String, unsecure: bool) { 22 | let base_url = url; 23 | let client = reqwest::Client::new(); 24 | let url_prefix = if unsecure { 25 | "http".to_string() 26 | } else { 27 | "https".to_string() 28 | }; 29 | 30 | // RED TEXT 31 | match Text::new( 32 | &format!( 33 | " Are you sure you want to undelegate {} boost tokens? (Y/n or 'esc' to cancel)", 34 | args.amount 35 | ) 36 | .red() 37 | .to_string(), 38 | ) 39 | .prompt() 40 | { 41 | Ok(confirm) => { 42 | if confirm.trim().eq_ignore_ascii_case("esc") { 43 | println!(" Unboosting canceled."); 44 | return; 45 | } else if confirm.trim().is_empty() || confirm.trim().to_lowercase() == "y" { 46 | // Proceed with staking 47 | } else { 48 | println!(" Unboosting canceled."); 49 | return; 50 | } 51 | } 52 | Err(InquireError::OperationCanceled) => { 53 | println!(" Unboosting operation canceled."); 54 | return; 55 | } 56 | Err(_) => { 57 | println!(" Invalid input. Unboosting canceled."); 58 | return; 59 | } 60 | } 61 | 62 | let resp = client 63 | .get(format!( 64 | "{}://{}/pool/authority/pubkey", 65 | url_prefix, base_url 66 | )) 67 | .send() 68 | .await 69 | .unwrap() 70 | .text() 71 | .await 72 | .unwrap(); 73 | let pool_pubkey = Pubkey::from_str(&resp).unwrap(); 74 | 75 | let resp = client 76 | .get(format!( 77 | "{}://{}/pool/fee_payer/pubkey", 78 | url_prefix, base_url 79 | )) 80 | .send() 81 | .await 82 | .unwrap() 83 | .text() 84 | .await 85 | .unwrap(); 86 | let fee_pubkey = Pubkey::from_str(&resp).unwrap(); 87 | 88 | let resp = client 89 | .get(format!("{}://{}/latest-blockhash", url_prefix, base_url)) 90 | .send() 91 | .await 92 | .unwrap() 93 | .text() 94 | .await 95 | .unwrap(); 96 | let decoded_blockhash = BASE64_STANDARD.decode(resp).unwrap(); 97 | let deserialized_blockhash = bincode::deserialize(&decoded_blockhash).unwrap(); 98 | 99 | let boost_amount_u64 = 100 | (args.amount * 10f64.powf(ore_api::consts::TOKEN_DECIMALS as f64)) as u64; 101 | let ix = ore_miner_delegation::instruction::undelegate_boost_v2( 102 | key.pubkey(), 103 | pool_pubkey, 104 | Pubkey::from_str(&args.mint).unwrap(), 105 | boost_amount_u64, 106 | ); 107 | 108 | let mut tx = Transaction::new_with_payer(&[ix], Some(&fee_pubkey)); 109 | tx.partial_sign(&[&key], deserialized_blockhash); 110 | let serialized_tx = bincode::serialize(&tx).unwrap(); 111 | let encoded_tx = BASE64_STANDARD.encode(&serialized_tx); 112 | 113 | let resp = client 114 | .post(format!( 115 | "{}://{}/v2/unstake-boost?pubkey={}&mint={}&amount={}", 116 | url_prefix, 117 | base_url, 118 | key.pubkey().to_string(), 119 | args.mint, 120 | boost_amount_u64 121 | )) 122 | .body(encoded_tx) 123 | .send() 124 | .await; 125 | if let Ok(res) = resp { 126 | if let Ok(txt) = res.text().await { 127 | match txt.as_str() { 128 | "SUCCESS" => { 129 | println!(" Successfully unstaked boost!"); 130 | } 131 | other => { 132 | println!(" Transaction failed: {}", other); 133 | } 134 | } 135 | } else { 136 | println!(" Transaction failed, please wait and try again."); 137 | } 138 | } else { 139 | println!(" Transaction failed, please wait and try again."); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/undelegate_stake.rs: -------------------------------------------------------------------------------- 1 | use base64::{prelude::BASE64_STANDARD, Engine}; 2 | use clap::Parser; 3 | use colored::*; 4 | use inquire::{InquireError, Text}; 5 | use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer, transaction::Transaction}; 6 | use spl_associated_token_account::get_associated_token_address; 7 | use std::str::FromStr; 8 | 9 | use crate::stake_balance; 10 | 11 | #[derive(Debug, Parser)] 12 | pub struct UnstakeArgs { 13 | #[arg(long, value_name = "AMOUNT", help = "Amount of ore to unstake.")] 14 | pub amount: f64, 15 | } 16 | 17 | pub async fn undelegate_stake(args: UnstakeArgs, key: &Keypair, url: String, unsecure: bool) { 18 | let base_url = url; 19 | let client = reqwest::Client::new(); 20 | let url_prefix = if unsecure { 21 | "http".to_string() 22 | } else { 23 | "https".to_string() 24 | }; 25 | 26 | // Fetch the staked balance 27 | let staked_balance = stake_balance::get_staked_balance(&key, base_url.clone(), unsecure).await; 28 | println!(" Current Staked Balance: {:.11} ORE", staked_balance); 29 | 30 | // Ensure unstake amount does not exceed staked balance 31 | let unstake_amount = if args.amount > staked_balance { 32 | println!( 33 | " Unstake amount exceeds staked balance. Defaulting to maximum available: {:.11} ORE", 34 | staked_balance 35 | ); 36 | staked_balance 37 | } else { 38 | args.amount 39 | }; 40 | 41 | // Add confirmation step with red text before unstaking 42 | match Text::new( 43 | &format!( 44 | " Are you sure you want to unstake {} ORE? (Y/n or 'esc' to cancel)", 45 | unstake_amount 46 | ) 47 | .red() 48 | .to_string(), 49 | ) 50 | .prompt() 51 | { 52 | Ok(confirm) => { 53 | if confirm.trim().eq_ignore_ascii_case("esc") { 54 | println!(" Unstaking canceled."); 55 | return; 56 | } else if confirm.trim().is_empty() || confirm.trim().to_lowercase() == "y" { 57 | // Proceed with unstaking 58 | } else { 59 | println!(" Unstaking canceled."); 60 | return; 61 | } 62 | } 63 | Err(InquireError::OperationCanceled) => { 64 | println!(" Unstaking operation canceled."); 65 | return; 66 | } 67 | Err(_) => { 68 | println!(" Invalid input. Unstaking canceled."); 69 | return; 70 | } 71 | } 72 | 73 | // Continue with transaction 74 | let resp = client 75 | .get(format!( 76 | "{}://{}/pool/authority/pubkey", 77 | url_prefix, base_url 78 | )) 79 | .send() 80 | .await 81 | .unwrap() 82 | .text() 83 | .await 84 | .unwrap(); 85 | let pool_pubkey = Pubkey::from_str(&resp).unwrap(); 86 | 87 | let resp = client 88 | .get(format!( 89 | "{}://{}/pool/fee_payer/pubkey", 90 | url_prefix, base_url 91 | )) 92 | .send() 93 | .await 94 | .unwrap() 95 | .text() 96 | .await 97 | .unwrap(); 98 | let fee_pubkey = Pubkey::from_str(&resp).unwrap(); 99 | 100 | let resp = client 101 | .get(format!("{}://{}/latest-blockhash", url_prefix, base_url)) 102 | .send() 103 | .await 104 | .unwrap() 105 | .text() 106 | .await 107 | .unwrap(); 108 | let decoded_blockhash = BASE64_STANDARD.decode(resp).unwrap(); 109 | let deserialized_blockhash = bincode::deserialize(&decoded_blockhash).unwrap(); 110 | 111 | let ata_address = get_associated_token_address(&key.pubkey(), &ore_api::consts::MINT_ADDRESS); 112 | 113 | let unstake_amount_u64 = 114 | (unstake_amount * 10f64.powf(ore_api::consts::TOKEN_DECIMALS as f64)) as u64; 115 | let ix = ore_miner_delegation::instruction::undelegate_stake( 116 | key.pubkey(), 117 | pool_pubkey, 118 | ata_address, 119 | unstake_amount_u64, 120 | ); 121 | 122 | let mut tx = Transaction::new_with_payer(&[ix], Some(&fee_pubkey)); 123 | tx.partial_sign(&[&key], deserialized_blockhash); 124 | 125 | let serialized_tx = bincode::serialize(&tx).unwrap(); 126 | let encoded_tx = BASE64_STANDARD.encode(&serialized_tx); 127 | 128 | let resp = client 129 | .post(format!( 130 | "{}://{}/unstake?pubkey={}&amount={}", 131 | url_prefix, 132 | base_url, 133 | key.pubkey().to_string(), 134 | unstake_amount_u64 135 | )) 136 | .body(encoded_tx) 137 | .send() 138 | .await; 139 | if let Ok(res) = resp { 140 | if let Ok(txt) = res.text().await { 141 | match txt.as_str() { 142 | "SUCCESS" => { 143 | println!(" Successfully unstaked!"); 144 | } 145 | other => { 146 | println!(" Transaction failed: {}", other); 147 | } 148 | } 149 | } else { 150 | println!(" Transaction failed, please wait and try again."); 151 | } 152 | } else { 153 | println!(" Transaction failed, please wait and try again."); 154 | } 155 | } 156 | --------------------------------------------------------------------------------