Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes for the testnet faucet #72

Merged
merged 2 commits into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading