mirror of
https://github.com/transatoshi-mw/grin-faucet.git
synced 2025-10-06 15:42:47 +00:00
Final version
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "grin_faucet"
|
||||
version = "1.0.0"
|
||||
name = "grin-faucet"
|
||||
version = "1.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
@@ -10,6 +10,8 @@ tokio-rustls = "0.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
chrono = "0.4"
|
||||
simplelog = "0.11"
|
||||
arti-client = "0.30.0"
|
||||
log = "0.4"
|
||||
sha2 = "0.10"
|
||||
hex = "0.4.3"
|
||||
arti = "1.4.3"
|
||||
|
@@ -1,11 +1,11 @@
|
||||
# grin-faucet
|
||||
# grin_faucet
|
||||
|
||||
Grin faucet backend written in Rust.
|
||||
Grin faucet written in Rust with a vanilla HTML/CSS/JS frontend.
|
||||
|
||||
It is meant to run on your Grin node, you will need to edit the wallet \<PASSWORD\>, \<DIRs\> and \<URLs\> in main.rs.
|
||||
It is meant to run on your Grin node, you will need to edit the wallet \<PASSWORD\> and URLs in main.rs.
|
||||
|
||||
There is no front end or webserver included so you will need to serve an HTML/CSS/JS page with something like Nginx.
|
||||
|
||||
To run after cloning and changing parameters, cd in to it, and execute 'cargo run'. Your faucet will listen on port 3031. Check out the releases page for an executable. To create your own executable run 'cargo build --release'.
|
||||
To run after changing parameters, simply clone the repository, cd in to it, and execute 'cargo run'. Your faucet will listen on port 3031. Check out the releases page for an executable.
|
||||
|
||||
|
||||
|
126
src/main.rs
126
src/main.rs
@@ -20,13 +20,14 @@ struct Response {
|
||||
}
|
||||
|
||||
struct RateLimiter {
|
||||
last_sent: HashMap<String, chrono::DateTime<Local>>,
|
||||
last_sent_ip: HashMap<String, chrono::DateTime<Local>>,
|
||||
last_sent_address: HashMap<String, chrono::DateTime<Local>>,
|
||||
}
|
||||
|
||||
fn is_valid_address(address: &str) -> bool {
|
||||
if address.is_empty()
|
||||
|| address.contains(' ')
|
||||
|| !address.starts_with("grin1")
|
||||
|| (!address.starts_with("grin1"))
|
||||
|| !address.chars().all(|c| c.is_alphanumeric())
|
||||
|| address.len() < 62
|
||||
{
|
||||
@@ -35,18 +36,18 @@ fn is_valid_address(address: &str) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
// Function to hash the IP address
|
||||
// hash the IP address
|
||||
fn hash_ip(ip: &str) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(ip);
|
||||
let result = hasher.finalize();
|
||||
hex::encode(result) // Convert the hash to a hexadecimal string
|
||||
hex::encode(result) // hash to hex
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Initialize logging
|
||||
let log_file = File::create("app.log").unwrap();
|
||||
// logging
|
||||
let log_file = File::create("faucet.log").unwrap();
|
||||
CombinedLogger::init(vec![
|
||||
TermLogger::new(
|
||||
LevelFilter::Info,
|
||||
@@ -59,7 +60,8 @@ async fn main() {
|
||||
.unwrap();
|
||||
|
||||
let rate_limiter = Arc::new(Mutex::new(RateLimiter {
|
||||
last_sent: HashMap::new(),
|
||||
last_sent_ip: HashMap::new(),
|
||||
last_sent_address: HashMap::new(),
|
||||
}));
|
||||
|
||||
let rate_limiter_filter = warp::any().map(move || rate_limiter.clone());
|
||||
@@ -68,18 +70,18 @@ async fn main() {
|
||||
.and(warp::path("send"))
|
||||
.and(warp::body::json())
|
||||
.and(rate_limiter_filter.clone())
|
||||
.and(warp::addr::remote()) // Get the remote address
|
||||
.and(warp::addr::remote())
|
||||
.map(
|
||||
|request: SendRequest,
|
||||
rate_limiter: Arc<Mutex<RateLimiter>>,
|
||||
remote_addr: Option<std::net::SocketAddr>| {
|
||||
let address = request.address;
|
||||
|
||||
// Validate the address
|
||||
// wallet address validator
|
||||
if !is_valid_address(&address) {
|
||||
return warp::reply::json(&Response {
|
||||
message:
|
||||
"Invalid: Must start with 'grin1' and be a valid 62 character address"
|
||||
"Invalid: Must start with 'grin1' and be a valid address"
|
||||
.to_string(),
|
||||
});
|
||||
}
|
||||
@@ -87,7 +89,7 @@ async fn main() {
|
||||
let mut rate_limiter = rate_limiter.lock().unwrap();
|
||||
let now = Local::now();
|
||||
|
||||
// Hash the IP address
|
||||
// hash the IP address
|
||||
let ip_hash = match remote_addr {
|
||||
Some(addr) => hash_ip(&addr.ip().to_string()),
|
||||
None => {
|
||||
@@ -97,72 +99,98 @@ async fn main() {
|
||||
}
|
||||
};
|
||||
|
||||
info!("IP Hash: {}", ip_hash);
|
||||
|
||||
// Check if the address has been sent funds in the last 24 hours
|
||||
if let Some(last_sent) = rate_limiter.last_sent.get(&ip_hash) {
|
||||
// check IP for sends in the last 24 hours
|
||||
if let Some(last_sent) = rate_limiter.last_sent_ip.get(&ip_hash) {
|
||||
if now - *last_sent < Duration::hours(24) {
|
||||
info!("Criminal {} requested funds too often.", ip_hash);
|
||||
return warp::reply::json(&Response {
|
||||
message: "You can only request 1ツ every 24 hours".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the command
|
||||
// check address for sends in the last 24 hours
|
||||
if let Some(last_sent) = rate_limiter.last_sent_address.get(&address) {
|
||||
if now - *last_sent < Duration::hours(24) {
|
||||
return warp::reply::json(&Response {
|
||||
message: "This wallet address can only request 1ツ every 24 hours".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// grin-wallet send
|
||||
let output = Command::new("bash")
|
||||
.arg("-c")
|
||||
.arg(format!(
|
||||
"echo '<PASSWORD>' | <DIR>/grin-wallet send -d {} 1",
|
||||
"echo '<PASSWORD>' | /usr/local/bin/grin-wallet send -d {} 1",
|
||||
address
|
||||
))
|
||||
.output()
|
||||
.expect("Failed to execute command");
|
||||
|
||||
// Update the last sent time
|
||||
rate_limiter.last_sent.insert(ip_hash.clone(), now);
|
||||
|
||||
// Handle command output
|
||||
// Handle grin-wallet output
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
if stderr.is_empty() {
|
||||
if let Some(slatepack_message) = extract_slatepack_message(&stdout) {
|
||||
info!(" {}", slatepack_message);
|
||||
return warp::reply::json(&Response {
|
||||
message: slatepack_message,
|
||||
});
|
||||
} else {
|
||||
info!("Grin sent successfully to address: {}", address);
|
||||
return warp::reply::json(&Response {
|
||||
message: "Grin sent via TOR ツ".to_string(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
error!("Error sending funds to address {}: {}", address, stderr);
|
||||
// Not enough funds error
|
||||
let combined_output = format!("{}{}", stdout, stderr);
|
||||
if combined_output.contains("Not enough funds") ||
|
||||
combined_output.contains("LibWallet Error: Not enough funds") ||
|
||||
combined_output.contains("Wallet command failed: Not enough funds") {
|
||||
error!("Faucet is empty ¯\\_(ツ)_/¯");
|
||||
return warp::reply::json(&Response {
|
||||
message: format!("Error: {}", stderr),
|
||||
message: "Faucet is empty ¯\\_(ツ)_/¯".to_string(),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Offline wallet slatepack response
|
||||
if let Some(slatepack_message) = extract_slatepack_message(&stdout) {
|
||||
rate_limiter.last_sent_ip.insert(ip_hash.clone(), now);
|
||||
rate_limiter.last_sent_address.insert(address.clone(), now);
|
||||
|
||||
info!("IP: {}, Wallet: {}, Slatepack issued", ip_hash, address);
|
||||
return warp::reply::json(&Response {
|
||||
message: slatepack_message,
|
||||
});
|
||||
}
|
||||
|
||||
// Tor sending success
|
||||
if stdout.contains("WARN grin_wallet_api::owner - Attempting to send transaction via TOR")
|
||||
&& stdout.contains("Tx sent successfully")
|
||||
&& stdout.contains("Command 'send' completed successfully")
|
||||
{
|
||||
rate_limiter.last_sent_ip.insert(ip_hash.clone(), now);
|
||||
rate_limiter.last_sent_address.insert(address.clone(), now);
|
||||
|
||||
info!("Grin sent via Tor to Wallet: {} (IP: {})", address, ip_hash);
|
||||
return warp::reply::json(&Response {
|
||||
message: "Grin sent via Tor ツ".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// sending errors (log but don’t rate limit)
|
||||
error!("Error sending funds to Wallet {} (IP: {}): {}", address, ip_hash, stderr);
|
||||
warp::reply::json(&Response {
|
||||
message: "Error: Transaction did not complete successfully.".to_string(),
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
// Load SSL keys and certs
|
||||
let cert_path = "<PATHTOCERT>";
|
||||
let key_path = "<PATHTOPRIVKEY>";
|
||||
// Load TLS keys
|
||||
let cert_path = "/etc/ssl/cert.pem";
|
||||
let key_path = "/etc/ssl/privkey.pem";
|
||||
|
||||
// Enable CORS only from this site
|
||||
// Enable CORS
|
||||
let cors = warp::cors()
|
||||
.allow_origin("https://<URL>")
|
||||
.allow_methods(vec!["POST"]) // Allow POST requests
|
||||
.allow_headers(vec!["Content-Type"]); // Allow Content-Type header
|
||||
.allow_origin("https://spigot.grinminer.net")
|
||||
.allow_methods(vec!["POST"])
|
||||
.allow_headers(vec!["Content-Type"]);
|
||||
|
||||
// Start the warp server with CORS & TLS
|
||||
// Start the warp server
|
||||
warp::serve(send_faucet.with(cors))
|
||||
.tls()
|
||||
.cert_path(cert_path)
|
||||
.key_path(key_path)
|
||||
.run(([0, 0, 0, 0], 3031)) // Listen on all interfaces
|
||||
.run(([0, 0, 0, 0], 3031))
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -176,9 +204,9 @@ fn extract_slatepack_message(stdout: &str) -> Option<String> {
|
||||
let slatepack_message = &stdout[start..end + end_marker.len()];
|
||||
|
||||
let trimmed_message = if slatepack_message.starts_with(' ') {
|
||||
&slatepack_message[1..] // Remove the first character (space)
|
||||
&slatepack_message[1..] // Remove the whitespace at beginning
|
||||
} else {
|
||||
slatepack_message // Return the original message if no leading space
|
||||
slatepack_message // Return original message if no space
|
||||
};
|
||||
|
||||
return Some(trimmed_message.to_string());
|
||||
|
Reference in New Issue
Block a user