Skip to content

Commit

Permalink
Fixes for the testnet faucet (#72)
Browse files Browse the repository at this point in the history
* - Use latest `fuel-core 0.27.0` and Rust SDK 0.63.0
- Removed caching of the coins from Rust SDK because it breaks the faucet if transaction is lost in the network.
- Send dust coins to the user who claimed coins.

* Make clippy happy
  • Loading branch information
xgreenx authored May 31, 2024
1 parent c6abfd8 commit 298d9ac
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 136 deletions.
160 changes: 96 additions & 64 deletions Cargo.lock

Large diffs are not rendered by default.

20 changes: 10 additions & 10 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ description = "A token faucet for onboarding fuel users"
[dependencies]
anyhow = "1.0"
axum = "0.5"
fuel-core-client = "0.24.3"
fuel-tx = "0.48.0"
fuel-types = "0.48.0"
fuels-accounts = { version = "0.59.0", features = ["coin-cache"] }
fuels-core = { version = "0.59.0" }
fuel-core-client = "0.27.0"
fuel-tx = "0.50.0"
fuel-types = "0.50.0"
fuels-accounts = { version = "0.63.0" }
fuels-core = { version = "0.63.0" }
handlebars = "4.2"
lazy_static = "1.4"
memoize = "0.3.1"
Expand All @@ -31,11 +31,11 @@ tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }

[dev-dependencies]
fuel-core = { version = "0.24.3", default-features = false, features = ["test-helpers"] }
fuel-core-txpool = "0.24.3"
fuel-crypto = "0.48.0"
fuel-tx = { version = "0.48.0", features = ["test-helpers"] }
fuel-types = { version = "0.48.0", features = ["random"] }
fuel-core = { version = "0.27.0", default-features = false, features = ["test-helpers"] }
fuel-core-txpool = "0.27.0"
fuel-crypto = "0.50.0"
fuel-tx = { version = "0.50.0", features = ["test-helpers"] }
fuel-types = { version = "0.50.0", features = ["random"] }
futures = "0.3"
insta = "1.14"
rand = "0.8"
Expand Down
13 changes: 9 additions & 4 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use crate::constants::{
CAPTCHA_KEY, CAPTCHA_SECRET, DEFAULT_DISPENSE_INTERVAL, DEFAULT_FAUCET_DISPENSE_AMOUNT,
DEFAULT_NODE_URL, DEFAULT_PORT, DISPENSE_AMOUNT, DISPENSE_INTERVAL, FUEL_NODE_URL,
HUMAN_LOGGING, LOG_FILTER, PUBLIC_FUEL_NODE_URL, SERVICE_PORT, TIMEOUT_SECONDS,
WALLET_SECRET_KEY,
DEFAULT_NODE_URL, DEFAULT_NUMBER_OF_RETRIES, DEFAULT_PORT, DISPENSE_AMOUNT, DISPENSE_INTERVAL,
FUEL_NODE_URL, HUMAN_LOGGING, LOG_FILTER, NUMBER_OF_RETRIES, PUBLIC_FUEL_NODE_URL,
SERVICE_PORT, TIMEOUT_SECONDS, WALLET_SECRET_KEY,
};
use secrecy::Secret;
use std::env;
Expand All @@ -18,6 +18,7 @@ pub struct Config {
pub public_node_url: String,
pub wallet_secret_key: Option<Secret<String>>,
pub dispense_amount: u64,
pub number_of_retries: u64,
pub dispense_limit_interval: u64,
pub timeout: u64,
}
Expand All @@ -42,12 +43,16 @@ impl Default for Config {
.unwrap_or_else(|_| DEFAULT_FAUCET_DISPENSE_AMOUNT.to_string())
.parse::<u64>()
.expect("expected a valid integer for DISPENSE_AMOUNT"),
number_of_retries: env::var(NUMBER_OF_RETRIES)
.unwrap_or_else(|_| DEFAULT_NUMBER_OF_RETRIES.to_string())
.parse::<u64>()
.expect("expected a valid integer for NUMBER_OF_RETRIES"),
dispense_limit_interval: env::var(DISPENSE_INTERVAL)
.unwrap_or_else(|_| DEFAULT_DISPENSE_INTERVAL.to_string())
.parse::<u64>()
.expect("expected a valid integer for DISPENSE_LIMIT_INTERVAL"),
timeout: env::var(TIMEOUT_SECONDS)
.unwrap_or_else(|_| "30".to_string())
.unwrap_or_else(|_| "10".to_string())
.parse::<u64>()
.expect("expected a valid integer for TIMEOUT_SECONDS"),
}
Expand Down
2 changes: 2 additions & 0 deletions src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ pub const WALLET_SECRET_DEV_KEY: &str =
pub const FUEL_NODE_URL: &str = "FUEL_NODE_URL";
pub const DEFAULT_NODE_URL: &str = "http://127.0.0.1:4000";
pub const DISPENSE_AMOUNT: &str = "DISPENSE_AMOUNT";
pub const NUMBER_OF_RETRIES: &str = "NUMBER_OF_RETRIES";
pub const DISPENSE_INTERVAL: &str = "DISPENSE_LIMIT_INTERVAL";
pub const DEFAULT_DISPENSE_INTERVAL: u64 = 24 * 60 * 60;
pub const DEFAULT_FAUCET_DISPENSE_AMOUNT: u64 = 10_000_000;
pub const DEFAULT_NUMBER_OF_RETRIES: u64 = 5;
pub const SERVICE_PORT: &str = "PORT";
pub const DEFAULT_PORT: u16 = 3000;

Expand Down
2 changes: 1 addition & 1 deletion src/dispense_tracker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ impl DispenseTracker {
}

pub fn has_tracked(&self, address: &Address) -> bool {
self.tracked.get(address).is_some()
self.tracked.contains_key(address)
}

pub fn is_in_progress(&self, address: &Address) -> bool {
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ mod recaptcha;
mod routes;

pub use dispense_tracker::{Clock, StdTime};
pub use routes::THE_BIGGEST_AMOUNT;

#[derive(Debug, Copy, Clone)]
pub struct CoinOutput {
Expand Down Expand Up @@ -158,6 +157,7 @@ pub async fn start_server(
.layer(TraceLayer::new_for_http())
.layer(Extension(Arc::new(wallet)))
.layer(Extension(Arc::new(client)))
.layer(Extension(Arc::new(node_info.clone())))
.layer(Extension(Arc::new(tokio::sync::Mutex::new(
FaucetState::new(&node_info.into()),
))))
Expand Down
54 changes: 33 additions & 21 deletions src/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use axum::{
Extension, Json,
};

use fuel_core_client::client::types::NodeInfo;
use fuel_core_client::client::FuelClient;
use fuel_tx::{Output, UtxoId};
use fuel_types::{Address, AssetId, Bytes32};
Expand All @@ -32,9 +33,6 @@ use std::{
};
use tracing::{error, info};

// The amount to fetch the biggest input of the faucet.
pub const THE_BIGGEST_AMOUNT: u64 = u32::MAX as u64;

lazy_static::lazy_static! {
static ref START_TIME: u64 = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64;
}
Expand Down Expand Up @@ -197,6 +195,7 @@ pub async fn dispense_tokens(
Extension(wallet): Extension<SharedWallet>,
Extension(state): Extension<SharedFaucetState>,
Extension(config): Extension<SharedConfig>,
Extension(info_node): Extension<Arc<NodeInfo>>,
Extension(client): Extension<Arc<FuelClient>>,
Extension(dispense_tracker): Extension<SharedDispenseTracker>,
) -> Result<DispenseResponse, DispenseError> {
Expand Down Expand Up @@ -252,9 +251,11 @@ pub async fn dispense_tokens(
let base_asset_id = *provider.consensus_parameters().base_asset_id();

let mut tx_id = None;
for _ in 0..5 {
for _ in 0..config.number_of_retries {
let mut guard = state.lock().await;
let inputs = if let Some(previous_coin_output) = &guard.last_output {
let amount = guard.last_output.as_ref().map_or(0, |o| o.amount);
let inputs = if amount > config.dispense_amount {
let previous_coin_output = guard.last_output.expect("Checked above");
let coin_type = CoinType::Coin(Coin {
amount: previous_coin_output.amount,
block_created: 0u32,
Expand All @@ -266,17 +267,24 @@ pub async fn dispense_tokens(

vec![Input::resource_signed(coin_type)]
} else {
get_coins(&wallet, &base_asset_id, config.dispense_amount).await?
get_coins(
&wallet,
&base_asset_id,
// Double the target amount to cover also the fee
config.dispense_amount * info_node.max_depth * 2,
)
.await?
};

let mut outputs = wallet.get_asset_outputs_for_amount(
&address.into(),
base_asset_id,
config.dispense_amount,
);
let recipient_address = address;
let faucet_address: Address = wallet.address().into();
// Add an additional output to store the stable part of the fee change.
outputs.push(Output::coin(faucet_address, 0, base_asset_id));
let outputs = vec![
Output::coin(recipient_address, config.dispense_amount, base_asset_id),
// Sends the dust change to the user
Output::change(recipient_address, 0, base_asset_id),
// Add an additional output to store the stable part of the fee change.
Output::coin(faucet_address, 0, base_asset_id),
];

let tip = guard.next_tip();

Expand Down Expand Up @@ -308,17 +316,21 @@ pub async fn dispense_tokens(
StatusCode::INTERNAL_SERVER_ERROR,
)
})?
.ok_or(error(
"Overflow during calculating `TransactionFee`".to_string(),
StatusCode::INTERNAL_SERVER_ERROR,
))?;
.ok_or_else(|| {
error(
"Overflow during calculating `TransactionFee`".to_string(),
StatusCode::INTERNAL_SERVER_ERROR,
)
})?;
let available_balance = available_balance(&tx_builder.inputs, &base_asset_id);
let stable_fee_change = available_balance
.checked_sub(fee.max_fee().saturating_add(config.dispense_amount))
.ok_or(error(
"Not enough asset to cover a max fee".to_string(),
StatusCode::INTERNAL_SERVER_ERROR,
))?;
.ok_or_else(|| {
error(
"Not enough asset to cover a max fee".to_string(),
StatusCode::INTERNAL_SERVER_ERROR,
)
})?;

*tx_builder.outputs.last_mut().unwrap() =
Output::coin(faucet_address, stable_fee_change, base_asset_id);
Expand Down
77 changes: 42 additions & 35 deletions tests/dispense.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ use fuel_core_client::client::pagination::{PageDirection, PaginationRequest};
use fuel_crypto::SecretKey;
use fuel_faucet::config::Config;
use fuel_faucet::models::DispenseInfoResponse;
use fuel_faucet::{start_server, Clock, THE_BIGGEST_AMOUNT};
use fuel_faucet::{start_server, Clock};
use fuel_tx::ConsensusParameters;
use fuel_types::Address;
use fuels_accounts::provider::Provider;
use fuels_accounts::wallet::WalletUnlocked;
use fuels_core::types::bech32::Bech32Address;
use fuels_core::types::transaction::TransactionType;
use futures::stream::FuturesUnordered;
use futures::StreamExt;
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use secrecy::Secret;
Expand Down Expand Up @@ -56,40 +58,34 @@ struct TestContext {
clock: MockClock,
}
impl TestContext {
async fn new(mut rng: StdRng) -> Self {
let dispense_amount = rng.gen_range(1..10000u64);
let secret_key: SecretKey = SecretKey::random(&mut rng);
async fn new(rng: &mut StdRng) -> Self {
let dispense_amount = 2000000;
let secret_key: SecretKey = SecretKey::random(rng);
let wallet = WalletUnlocked::new_from_private_key(secret_key, None);
let base_asset_id = [1; 32].into();

let mut generator = CoinConfigGenerator::new();
let mut coins: Vec<_> = (0..10000)
.map(|_| {
// dust
CoinConfig {
owner: wallet.address().into(),
amount: THE_BIGGEST_AMOUNT - 1,
asset_id: rng.gen(),
..generator.generate()
}
let coins: Vec<_> = (0..10000)
.map(|_| CoinConfig {
owner: wallet.address().into(),
amount: dispense_amount - 1,
asset_id: base_asset_id,
..generator.generate()
})
.collect();
// main coin
coins.push(CoinConfig {
owner: wallet.address().into(),
amount: 1 << 50,
asset_id: base_asset_id,
..generator.generate()
});

let state_config = StateConfig {
coins,
..Default::default()
};

let mut consensus_parameters = ConsensusParameters::default();
consensus_parameters
.set_fee_params(fuel_tx::FeeParameters::default().with_gas_price_factor(1));
consensus_parameters.set_fee_params(
// Values from the testnet
fuel_tx::FeeParameters::default()
.with_gas_price_factor(92)
.with_gas_per_byte(63),
);
consensus_parameters.set_base_asset_id(base_asset_id);

let chain_config = ChainConfig {
Expand All @@ -99,15 +95,16 @@ impl TestContext {

let snapshot_reader = SnapshotReader::new_in_memory(chain_config, state_config);

let config = NodeConfig {
let mut config = NodeConfig {
block_production: Trigger::Interval {
block_time: Duration::from_secs(3),
},
utxo_validation: true,
static_gas_price: 1,
static_gas_price: 20,
snapshot_reader,
..NodeConfig::local_node()
};
config.txpool.max_depth = 32;

// start node
let fuel_node = FuelService::new_node(config).await.unwrap();
Expand All @@ -123,6 +120,7 @@ impl TestContext {
node_url: format!("http://{}", fuel_node.bound_address),
wallet_secret_key: Some(Secret::new(format!("{secret_key:x}"))),
dispense_amount,
number_of_retries: 1,
..Default::default()
};

Expand All @@ -141,7 +139,7 @@ impl TestContext {

#[tokio::test]
async fn can_start_server() {
let context = TestContext::new(StdRng::seed_from_u64(42)).await;
let context = TestContext::new(&mut StdRng::seed_from_u64(42)).await;
let addr = context.addr;

let client = reqwest::Client::new();
Expand Down Expand Up @@ -193,11 +191,11 @@ async fn dispense_sends_coins_to_valid_address_non_hex() {
}

async fn _dispense_sends_coins_to_valid_address(
rng: StdRng,
mut rng: StdRng,
recipient_address: Bech32Address,
recipient_address_str: String,
) {
let context = TestContext::new(rng).await;
let context = TestContext::new(&mut rng).await;
let addr = context.addr;
let client = reqwest::Client::new();

Expand All @@ -223,7 +221,7 @@ async fn _dispense_sends_coins_to_valid_address(
.map(|coin| coin.amount)
.sum();

assert_eq!(test_balance, context.faucet_config.dispense_amount);
assert!(test_balance >= context.faucet_config.dispense_amount);
}

fn generate_recipient_addresses(count: usize, rng: &mut StdRng) -> Vec<String> {
Expand All @@ -238,9 +236,10 @@ fn generate_recipient_addresses(count: usize, rng: &mut StdRng) -> Vec<String> {
#[tokio::test]
async fn many_concurrent_requests() {
let mut rng = StdRng::seed_from_u64(42);
const COUNT: usize = 30;

const COUNT: usize = 128;
let recipient_addresses_str = generate_recipient_addresses(COUNT, &mut rng);
let context = TestContext::new(rng).await;
let context = TestContext::new(&mut rng).await;
let addr = context.addr;

let mut queries = vec![];
Expand All @@ -258,16 +257,24 @@ async fn many_concurrent_requests() {
.await
});
}
let queries = futures::future::join_all(queries).await;
for query in queries {
query.expect("Query should be successful");
let mut queries = FuturesUnordered::from_iter(queries);
let mut success = 0;
while let Some(query) = queries.next().await {
let response = query.expect("Query should be successful");
assert_eq!(
response.status(),
reqwest::StatusCode::CREATED,
"{success}/{COUNT}: {:?}",
response.bytes().await
);
success += 1;
}

let txs = context
.provider
.get_transactions(PaginationRequest {
cursor: None,
results: 1000,
results: 500,
direction: PageDirection::Forward,
})
.await
Expand All @@ -285,7 +292,7 @@ async fn dispense_once_per_day() {
let mut rng = StdRng::seed_from_u64(42);
let recipient_address: Address = rng.gen();
let recipient_address_str = format!("{}", &recipient_address);
let context = TestContext::new(rng).await;
let context = TestContext::new(&mut rng).await;
let addr = context.addr;

let dispense_interval = 24 * 60 * 60;
Expand Down

0 comments on commit 298d9ac

Please sign in to comment.