Final version

This commit is contained in:
transatoshi
2025-09-20 01:34:06 -07:00
parent 39fbf54806
commit 7ac09f780e
3 changed files with 86 additions and 56 deletions

View File

@@ -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"

View File

@@ -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.

View File

@@ -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(),
});
}
},
);
// Load SSL keys and certs
let cert_path = "<PATHTOCERT>";
let key_path = "<PATHTOPRIVKEY>";
// 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);
// Enable CORS only from this site
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 dont 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 TLS keys
let cert_path = "/etc/ssl/cert.pem";
let key_path = "/etc/ssl/privkey.pem";
// 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());