Skip to content
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
8 changes: 5 additions & 3 deletions docs/api-documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ curl "https://forest-explorer.chainsafe.dev/api/claim_token?faucet_info=Calibnet
**Response:**

```bash
ServerError|Invalid address: Not a valid Testnet address
ServerError|Invalid address - failed to parse: Not a valid Testnet address
```

#### 429 Too Many Requests
Expand Down Expand Up @@ -241,13 +241,13 @@ curl "https://forest-explorer.chainsafe.dev/api/claim_token_all?address=invalida
{
"faucet_info": "CalibnetUSDFC",
"error": {
"ServerError": "Invalid address: Not a valid Testnet address"
"ServerError": "Invalid address - failed to parse: Not a valid Testnet address"
}
},
{
"faucet_info": "CalibnetFIL",
"error": {
"ServerError": "Invalid address: Not a valid Testnet address"
"ServerError": "Invalid address - failed to parse: Not a valid Testnet address"
}
}
]
Expand Down Expand Up @@ -291,6 +291,8 @@ curl "https://forest-explorer.chainsafe.dev/api/claim_token_all?address=0xAe9C4b

- Each address is subject to rate limiting to prevent abuse.
- This API only distributes Calibnet `tFIL` and `tUSDFC` tokens.
- ID address or its corresponding eth style `0xff…ID` address are restricted to
claim `tUSDFC` tokens.

## Rate Limits

Expand Down
69 changes: 16 additions & 53 deletions e2e/test_claim_token_api_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,21 @@ export const TEST_SCENARIOS = {
name: 'Invalid address format for CalibnetUSDFC',
faucet_info: FaucetTypes.CalibnetUSDFC,
address: TEST_ADDRESSES.T1_FORMAT_ADDRESS,
expectedStatus: STATUS_CODES.INTERNAL_SERVER_ERROR,
expectedStatus: STATUS_CODES.BAD_REQUEST,
expectedErrorContains: 'invalid address'
},
{
name: 'CalibnetUSDFC (0xff...ID) - restricted address (RESTRICTED)',
faucet_info: FaucetTypes.CalibnetUSDFC,
address: TEST_ADDRESSES.ETH_ID_CORRESPONDING,
expectedStatus: STATUS_CODES.BAD_REQUEST,
expectedErrorContains: 'invalid address'
},
{
name: 'CalibnetUSDFC (t0) - restricted address (RESTRICTED)',
faucet_info: FaucetTypes.CalibnetUSDFC,
address: TEST_ADDRESSES.T0_ADDRESS,
expectedStatus: STATUS_CODES.BAD_REQUEST,
expectedErrorContains: 'invalid address'
}
],
Expand Down Expand Up @@ -186,19 +200,6 @@ export const TEST_SCENARIOS = {
faucet_info: FaucetTypes.CalibnetUSDFC,
address: TEST_ADDRESSES.T410_ADDRESS,
expectedStatus: STATUS_CODES.TOO_MANY_REQUESTS
},
{
name: 'CalibnetUSDFC (t0) - RATE LIMITED (within CalibnetUSDFC cooldown)',
faucet_info: FaucetTypes.CalibnetUSDFC,
address: TEST_ADDRESSES.T0_ADDRESS,
expectedStatus: STATUS_CODES.TOO_MANY_REQUESTS
},
// CalibnetUSDFC doesn't support the t1 format address
{
name: 'CalibnetUSDFC (ID) - RATE LIMITED (within CalibnetUSDFC cooldown)',
faucet_info: FaucetTypes.CalibnetUSDFC,
address: TEST_ADDRESSES.ETH_ID_CORRESPONDING,
expectedStatus: STATUS_CODES.TOO_MANY_REQUESTS
}
],

Expand Down Expand Up @@ -295,44 +296,6 @@ export const TEST_SCENARIOS = {
expectedStatus: STATUS_CODES.TOO_MANY_REQUESTS,
waitBefore: 0, // No wait needed, should be capped, already from the previous step
walletCapErrorResponse: true,
},

// TODO(forest-explorer): https://github.com/ChainSafe/forest-explorer/issues/335
// Token sent to the t0 and it's eth mapping address 0x
// are not accessible. Hence commenting out the following
// test cases.
// === CalibnetUSDFC ID Wallet (fresh wallet, 0 transactions) ===
// {
// name: 'CalibnetUSDFC (ID) - 1st SUCCESS (fresh wallet)',
// faucet_info: FaucetTypes.CalibnetUSDFC,
// address: TEST_ADDRESSES.ETH_ID_CORRESPONDING,
// expectedStatus: STATUS_CODES.SUCCESS,
// waitBefore: 65, // Wait for cooldown from the previous test group to expire
// walletCapErrorResponse: false,
// },
// {
// name: 'CalibnetUSDFC (ID) - 2nd SUCCESS (reaches cap)',
// faucet_info: FaucetTypes.CalibnetUSDFC,
// address: TEST_ADDRESSES.ETH_ID_CORRESPONDING,
// expectedStatus: STATUS_CODES.SUCCESS,
// waitBefore: 65, // Wait for cooldown from its own 1st transaction
// walletCapErrorResponse: false,
// },
// {
// name: 'CalibnetUSDFC (ID) - 3rd attempt (WALLET CAPPED)',
// faucet_info: FaucetTypes.CalibnetUSDFC,
// address: TEST_ADDRESSES.ETH_ID_CORRESPONDING,
// expectedStatus: STATUS_CODES.TOO_MANY_REQUESTS,
// waitBefore: 65, // Wait for cooldown from its own 2nd transaction
// walletCapErrorResponse: true,
// },
// {
// name: 'CalibnetUSDFC (t0) - check equivalence (WALLET CAPPED)',
// faucet_info: FaucetTypes.CalibnetUSDFC,
// address: TEST_ADDRESSES.T0_ADDRESS, // This is the same wallet as the ID address
// expectedStatus: STATUS_CODES.TOO_MANY_REQUESTS,
// waitBefore: 0, // No wait needed, should be capped already
// walletCapErrorResponse: true,
// },
}
]
};
108 changes: 101 additions & 7 deletions src/faucet/server_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ pub async fn claim_token(

let network = faucet_info.network();
set_current_network(network);
let recipient = parse_and_validate_address(&address, network)?;
let recipient = parse_and_validate_address(&address, faucet_info)?;
let rpc = Provider::from_network(network);
let from = faucet_address(faucet_info)
.await?
Expand Down Expand Up @@ -271,18 +271,36 @@ pub async fn claim_token_all(address: String) -> Result<Vec<ClaimResponse>, Serv
Ok(results)
}

/// Checks if the provided address is valid for the faucet, ensuring invalid addresses are rejected.
#[cfg(feature = "ssr")]
fn check_valid_address(address: Address, faucet_info: FaucetInfo) -> Result<(), ServerFnError> {
use fvm_shared::address::Protocol;

if matches!(faucet_info, FaucetInfo::CalibnetUSDFC)
&& (address.protocol() != Protocol::Delegated)
{
log::error!("Invalid address: {:?}", address);
set_response_status(StatusCode::BAD_REQUEST);
return Err(ServerFnError::ServerError("Invalid address: Only Ethereum-compatible addresses (delegated t4 addresses or native Ethereum 0x addresses) are allowed for Calibnet USDFC token claims.".to_string()));
}
Ok(())
}

#[cfg(feature = "ssr")]
fn parse_and_validate_address(
address: &str,
network: fvm_shared::address::Network,
faucet_info: FaucetInfo,
) -> Result<Address, ServerFnError> {
match crate::utils::address::parse_address(address, network) {
Ok(addr) => Ok(addr),
match crate::utils::address::parse_address(address, faucet_info.network()) {
Ok(addr) => {
check_valid_address(addr, faucet_info)?;
Ok(addr)
}
Err(e) => {
log::error!("Invalid address: {}", e);
log::error!("Invalid address - failed to parse: {}", e);
set_response_status(StatusCode::BAD_REQUEST);
Err(ServerFnError::ServerError(format!(
"Invalid address: {}",
"Invalid address - failed to parse: {}",
e
)))
}
Expand Down Expand Up @@ -403,5 +421,81 @@ fn handle_faucet_error(err: FaucetError) -> ServerFnError {

#[cfg(feature = "ssr")]
fn set_response_status(status: StatusCode) {
leptos::prelude::expect_context::<ResponseOptions>().set_status(status);
if let Some(res) = leptos::context::use_context::<ResponseOptions>() {
res.set_status(status)
}
}

#[cfg(all(test, feature = "ssr"))]
mod tests {
use crate::faucet::server_api::*;

fn assert_valid_address(address: &str, faucet: FaucetInfo) {
let network = faucet.network();
let addr = crate::utils::address::parse_address(address, network).unwrap();
assert!(check_valid_address(addr, faucet).is_ok());
assert!(parse_and_validate_address(address, faucet).is_ok());
}

fn assert_invalid_address(address: &str, faucet: FaucetInfo) {
let network = faucet.network();
let addr = crate::utils::address::parse_address(address, network).unwrap();
assert!(check_valid_address(addr, faucet).is_err());
assert!(parse_and_validate_address(address, faucet).is_err());
}

#[test]
fn test_check_valid_address_mainnet() {
let addresses = [
"f03603846",
"f1rgci272nfk4k6cpyejepzv4xstpejjckldlzidy",
"f2yjb6dq3jggychgnuhevcwe7ehv3ot2rkhkbk4qy",
"f3s5kg6rehbbmgvngpec6b7m4uxmwbscdafn2pvtrrp65wbgjuymrr2z6qbkqiunkyjul6b62buqk76q47cjeq",
"f410fv2oexfiizeuzm3xtoie3gnxfpfwwglg4q3dgxki",
"0xff0000000000000000000000000000000036fd86",
"0xAe9C4b9508c929966ef37209b336E5796D632CDc",
];
for addr in addresses.iter() {
assert_valid_address(addr, FaucetInfo::MainnetFIL);
}
}

#[test]
fn test_check_valid_address_calibnet() {
let addresses = [
"t03603846",
"t1rgci272nfk4k6cpyejepzv4xstpejjckldlzidy",
"t2yjb6dq3jggychgnuhevcwe7ehv3ot2rkhkbk4qy",
"t3s5kg6rehbbmgvngpec6b7m4uxmwbscdafn2pvtrrp65wbgjuymrr2z6qbkqiunkyjul6b62buqk76q47cjeq",
"t410fv2oexfiizeuzm3xtoie3gnxfpfwwglg4q3dgxki",
"0xff0000000000000000000000000000000036f672",
"0xAe9C4b9508c929966ef37209b336E5796D632CDc",
];
for addr in addresses.iter() {
assert_valid_address(addr, FaucetInfo::CalibnetFIL);
}
}

#[test]
fn test_check_valid_address_calibnet_usdfc() {
let valid_addresses = [
"0xAe9C4b9508c929966ef37209b336E5796D632CDc",
"t410fv2oexfiizeuzm3xtoie3gnxfpfwwglg4q3dgxki",
];
let invalid_addresses = [
"t03603846",
"t1rgci272nfk4k6cpyejepzv4xstpejjckldlzidy",
"t2yjb6dq3jggychgnuhevcwe7ehv3ot2rkhkbk4qy",
"t3s5kg6rehbbmgvngpec6b7m4uxmwbscdafn2pvtrrp65wbgjuymrr2z6qbkqiunkyjul6b62buqk76q47cjeq",
"0xff0000000000000000000000000000000036f672",
];

for addr in valid_addresses.iter() {
assert_valid_address(addr, FaucetInfo::CalibnetUSDFC);
}

for addr in invalid_addresses.iter() {
assert_invalid_address(addr, FaucetInfo::CalibnetUSDFC);
}
}
}
2 changes: 1 addition & 1 deletion src/utils/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ pub fn parse_address(raw: &str, n: Network) -> anyhow::Result<Address> {
s.chars().skip(2).all(|c| c.is_ascii_hexdigit()),
"Invalid characters in address"
);
if let Some(id) = s.strip_prefix("0xff") {
if let Some(id) = s.strip_prefix("0xff0000000000000000000000") {
let id = u64::from_str_radix(id, 16)?;
Ok(Address::new_id(id))
} else {
Expand Down
Loading