diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7990d44 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +target +scripts \ No newline at end of file diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..56aa56a --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,59 @@ +name: Build docker images + +on: + workflow_dispatch: + inputs: + tag: + description: 'The docker image tag' + required: true + push: + branches: + - master + pull_request: + branches: + - master + +env: + GIT_LFS_SKIP_SMUDGE: 1 + +jobs: + docker: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + with: + version: v0.9.1 + - name: Login to private registry + uses: docker/login-action@v1 + with: + registry: ${{ secrets.REGISTRY_URL }} + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ secrets.REGISTRY_URL }}/namada-faucet + tags: | + type=schedule + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=latest + - name: Build and Push + uses: docker/build-push-action@v4 + with: + context: . + file: Dockerfile + push: ${{ github.ref == 'refs/heads/master' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 1324b42..f0ce619 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,3 +50,15 @@ axum-macros = "0.3.8" [patch.crates-io] borsh = {git = "https://github.com/heliaxdev/borsh-rs.git", rev = "cd5223e5103c4f139e0c54cf8259b7ec5ec4073a"} + +[profile.ephemeral-build] +inherits = "release" +incremental = false +lto = "thin" +opt-level = 2 +codegen-units = 64 +strip = "symbols" +debug = false + +[profile.release] +incremental = false \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1c25f46 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +# use the default dart image as the build image +FROM rust:1.70 AS builder + +# copy the current folder into the build folder +COPY . /app + +# set the work directory +WORKDIR /app + +# install protoc +RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends --assume-yes protobuf-compiler libprotobuf-dev + +# build app +RUN cargo build --release + +# use a slim image +FROM debian:bullseye-slim + +# copy the runtime files +COPY --from=builder /app/target/release/namada-faucet /app/server +WORKDIR /app + +# start the dart server +CMD ["./server"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..4d9dcf2 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# Namada Faucet + diff --git a/justfile b/justfile new file mode 100644 index 0000000..449cde2 --- /dev/null +++ b/justfile @@ -0,0 +1,9 @@ +build: + cacrgo build + +build-release: + cargo build --release + +misc: + cargo clippy --fix --allow-dirty + cargo fmt \ No newline at end of file diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 7bf9271..d9001c9 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,3 @@ [toolchain] channel = "1.70.0" -components = ["rustc", "cargo", "rust-std", "rust-docs", "rls", "rust-src", "rust-analysis"] -targets = ['x86_64-unknown-linux-musl'] \ No newline at end of file +components = ["rustc", "cargo", "rust-std", "rust-docs", "rls", "rust-src", "rust-analysis"] \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index cffa794..0648886 100644 --- a/src/app.rs +++ b/src/app.rs @@ -20,8 +20,14 @@ use tower_http::{ trace::TraceLayer, }; -use crate::{handler::faucet as faucet_handler, sdk::{utils::{sk_from_str, str_to_address}, namada::NamadaSdk}}; use crate::{app_state::AppState, config::AppConfig, state::faucet::FaucetState}; +use crate::{ + handler::faucet as faucet_handler, + sdk::{ + namada::NamadaSdk, + utils::{sk_from_str, str_to_address}, + }, +}; lazy_static! { static ref HTTP_TIMEOUT: u64 = 30; @@ -54,10 +60,10 @@ impl ApplicationServer { let nam_address = config.nam_address.clone(); let nam_address = str_to_address(&nam_address); - let sdk = NamadaSdk::new(rpc); + let sdk = NamadaSdk::new(rpc, sk.clone(), nam_address); let routes = { - let faucet_state = FaucetState::new(&db, sdk, sk, nam_address, auth_key, difficulty, chain_id); + let faucet_state = FaucetState::new(&db, sdk, auth_key, difficulty, chain_id); Router::new() .route("/faucet", get(faucet_handler::request_challenge)) @@ -78,7 +84,10 @@ impl ApplicationServer { .timeout(Duration::from_secs(*HTTP_TIMEOUT)) .layer(cors) .layer(BufferLayer::new(4096)) - .layer(RateLimitLayer::new(rps.unwrap_or(*REQ_PER_SEC), Duration::from_secs(1))) + .layer(RateLimitLayer::new( + rps.unwrap_or(*REQ_PER_SEC), + Duration::from_secs(1), + )), ); let router = router.fallback(Self::handle_404); diff --git a/src/config.rs b/src/config.rs index 831283d..fc64262 100644 --- a/src/config.rs +++ b/src/config.rs @@ -22,10 +22,10 @@ pub struct AppConfig { pub chain_id: String, #[clap(long, env)] - pub rpc: String, + pub rpc: String, #[clap(long, env)] - pub nam_address: String, + pub nam_address: String, #[clap(long, env)] pub auth_key: Option, diff --git a/src/dto/faucet.rs b/src/dto/faucet.rs index 4cd3a09..4aefd5f 100644 --- a/src/dto/faucet.rs +++ b/src/dto/faucet.rs @@ -12,8 +12,7 @@ pub struct FaucetRequestDto { pub challenge: String, #[validate(length(equal = 64, message = "Invalid proof"))] pub tag: String, - pub transfer: Transfer - + pub transfer: Transfer, } #[derive(Clone, Serialize, Deserialize, Validate)] diff --git a/src/error/faucet.rs b/src/error/faucet.rs index 032ca97..f6a651e 100644 --- a/src/error/faucet.rs +++ b/src/error/faucet.rs @@ -24,7 +24,6 @@ impl IntoResponse for FaucetError { FaucetError::InvalidProof => StatusCode::FORBIDDEN, FaucetError::DuplicateChallenge => StatusCode::CONFLICT, FaucetError::InvalidAddress => StatusCode::BAD_REQUEST, - }; ApiErrorResponse::send(status_code.as_u16(), Some(self.to_string())) diff --git a/src/handler/faucet.rs b/src/handler/faucet.rs index 86d7a39..d18588b 100644 --- a/src/handler/faucet.rs +++ b/src/handler/faucet.rs @@ -1,14 +1,12 @@ -use std::sync::{Arc, RwLock}; - use axum::{extract::State, Json}; use axum_macros::debug_handler; -use namada::{types::address::Address, tendermint::abci::Code}; +use namada::{tendermint::abci::Code, types::address::Address}; use crate::{ dto::faucet::{FaucetRequestDto, FaucetResponseDto, FaucetResponseStatusDto}, error::{api::ApiError, faucet::FaucetError, validate::ValidatedRequest}, repository::faucet::FaucetRepositoryTrait, - state::faucet::FaucetState, sdk::namada::NamadaSdk, + state::faucet::FaucetState, }; pub async fn request_challenge( @@ -35,13 +33,13 @@ pub async fn request_transfer( let token_address = if let Ok(address) = token_address { address } else { - return Err(FaucetError::InvalidAddress.into()) + return Err(FaucetError::InvalidAddress.into()); }; let target_address = Address::decode(payload.transfer.target.clone()); let target_address = if let Ok(address) = target_address { address } else { - return Err(FaucetError::InvalidAddress.into()) + return Err(FaucetError::InvalidAddress.into()); }; if state.faucet_repo.contains(&payload.challenge) { @@ -66,14 +64,25 @@ pub async fn request_transfer( let mut locked_sdk = state.sdk.lock().await; - let sk = state.sk.clone(); - let nam_address = state.nam_address.clone(); + let sk = locked_sdk.get_secret_key(); + let nam_address = locked_sdk.get_address("nam".to_string()); let owner = Address::from(&sk.to_public()); let tx_args = locked_sdk.default_args(chain_id, vec![sk], None, nam_address.clone()); - let signing_data = locked_sdk.compute_signing_data(Some(owner.clone()), None, &tx_args).await; - let tx_data = locked_sdk.build_transfer_args(owner, target_address, token_address, payload.transfer.amount, nam_address, tx_args.clone()); - let mut tx = locked_sdk.build_transfer_tx(tx_data, signing_data.fee_payer.clone()).await; + let signing_data = locked_sdk + .compute_signing_data(Some(owner.clone()), None, &tx_args) + .await; + let tx_data = locked_sdk.build_transfer_args( + owner, + target_address, + token_address, + payload.transfer.amount, + nam_address, + tx_args.clone(), + ); + let mut tx = locked_sdk + .build_transfer_tx(tx_data, signing_data.fee_payer.clone()) + .await; locked_sdk.sign_tx(&mut tx, signing_data, &tx_args); let process_tx_response = locked_sdk.process_tx(tx, &tx_args).await; drop(locked_sdk); @@ -81,7 +90,7 @@ pub async fn request_transfer( let transfer_result = match process_tx_response { namada::ledger::tx::ProcessTxResponse::Applied(r) => r.code.eq(&"0"), namada::ledger::tx::ProcessTxResponse::Broadcast(r) => r.code.eq(&Code::Ok), - _ => false + _ => false, }; if transfer_result { @@ -90,7 +99,7 @@ pub async fn request_transfer( let response = FaucetResponseStatusDto { token: payload.transfer.token.clone(), - amount: payload.transfer.amount.clone(), + amount: payload.transfer.amount, target: payload.transfer.target.clone(), sent: transfer_result, }; diff --git a/src/lib.rs b/src/lib.rs index 7a29470..a0b4ba4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,7 @@ pub mod error; pub mod handler; pub mod repository; pub mod response; +pub mod sdk; pub mod services; pub mod state; pub mod utils; -pub mod sdk; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index d5921bd..3694580 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,8 +10,10 @@ async fn main() -> anyhow::Result<()> { dotenv().ok(); let config = Arc::new(AppConfig::parse()); let db = Arc::new(RwLock::new(AppState::default())); - - tracing_subscriber::fmt().with_max_level(tracing::Level::INFO).init(); + + tracing_subscriber::fmt() + .with_max_level(tracing::Level::INFO) + .init(); ApplicationServer::serve(config, db) .await diff --git a/src/sdk/client.rs b/src/sdk/client.rs index e18eb11..94feac8 100644 --- a/src/sdk/client.rs +++ b/src/sdk/client.rs @@ -74,7 +74,7 @@ impl ClientTrait for SdkClient { match response { Ok(response) => { let response_json = response.text().await.unwrap(); - R::Response::from_string(&response_json) + R::Response::from_string(response_json) } Err(e) => { let error_msg = e.to_string(); diff --git a/src/sdk/masp.rs b/src/sdk/masp.rs index 8bfb105..2cd98cb 100644 --- a/src/sdk/masp.rs +++ b/src/sdk/masp.rs @@ -2,27 +2,14 @@ use borsh::{BorshDeserialize, BorshSerialize}; use masp_proofs::prover::LocalTxProver; use namada::ledger::masp::{ShieldedContext, ShieldedUtils}; +#[derive(Default)] pub struct SdkShieldedCtx { pub shielded_context: ShieldedContext, } -impl Default for SdkShieldedCtx { - fn default() -> Self { - Self { - shielded_context: Default::default(), - } - } -} - -#[derive(Clone, BorshDeserialize, BorshSerialize)] +#[derive(Clone, BorshDeserialize, BorshSerialize, Default)] pub struct SdkShieldedUtils {} -impl Default for SdkShieldedUtils { - fn default() -> Self { - Self {} - } -} - #[async_trait::async_trait] impl ShieldedUtils for SdkShieldedUtils { fn local_tx_prover(&self) -> LocalTxProver { diff --git a/src/sdk/mod.rs b/src/sdk/mod.rs index c50d560..fe28347 100644 --- a/src/sdk/mod.rs +++ b/src/sdk/mod.rs @@ -1,5 +1,5 @@ -pub mod namada; pub mod client; -pub mod wallet; pub mod masp; -pub mod utils; \ No newline at end of file +pub mod namada; +pub mod utils; +pub mod wallet; diff --git a/src/sdk/namada.rs b/src/sdk/namada.rs index 96f3eae..61cc648 100644 --- a/src/sdk/namada.rs +++ b/src/sdk/namada.rs @@ -5,15 +5,15 @@ use namada::{ args::{self, InputAmount}, signing::{self, SigningTxData}, tx::ProcessTxResponse, - rpc }, proto::Tx, types::{ address::Address, chain::ChainId, key::common::{self, SecretKey}, - token::{self, NATIVE_MAX_DECIMAL_PLACES, DenominatedAmount}, - transaction::GasLimit, masp::{TransferSource, TransferTarget}, + masp::{TransferSource, TransferTarget}, + token::{self, DenominatedAmount, NATIVE_MAX_DECIMAL_PLACES}, + transaction::GasLimit, }, }; @@ -26,14 +26,22 @@ pub struct NamadaSdk { } impl NamadaSdk { - pub fn new(url: String) -> Self { + pub fn new(url: String, sk: SecretKey, nam_address: Address) -> Self { Self { http_client: SdkClient::new(url), - wallet: SdkWallet::new(), + wallet: SdkWallet::new(sk, nam_address), shielded_ctx: SdkShieldedCtx::default(), } } + pub fn get_secret_key(&mut self) -> SecretKey { + self.wallet.wallet.find_key("my_faucet", None).unwrap() + } + + pub fn get_address(&self, alias: String) -> Address { + self.wallet.wallet.find_address(alias).unwrap().clone() + } + pub fn default_args( &self, chain_id: String, @@ -52,14 +60,14 @@ impl NamadaSdk { wallet_alias_force: false, wrapper_fee_payer: fee_payer, fee_amount: Some(InputAmount::Validated(token::DenominatedAmount { - amount: token::Amount::default(), + amount: token::Amount::from_u64(0), denom: NATIVE_MAX_DECIMAL_PLACES.into(), })), - fee_token: fee_token, - gas_limit: GasLimit::default(), + fee_token, + gas_limit: GasLimit::from(20_000), expiration: None, chain_id: Some(ChainId(chain_id)), - signing_keys: signing_keys, + signing_keys, signatures: Vec::default(), tx_reveal_code_path: PathBuf::from("tx_reveal_pk.wasm"), verification_key: None, @@ -88,7 +96,7 @@ impl NamadaSdk { } pub fn sign_tx(&mut self, tx: &mut Tx, signing_data: SigningTxData, args: &args::Tx) { - signing::sign_tx(&mut self.wallet.wallet, args, tx, signing_data); + signing::sign_tx(&mut self.wallet.wallet, args, tx, signing_data).unwrap(); } pub async fn process_tx(&mut self, tx: Tx, args: &args::Tx) -> ProcessTxResponse { @@ -97,7 +105,15 @@ impl NamadaSdk { .unwrap() } - pub fn build_transfer_args(&self, source: Address, target: Address, token: Address, amount: u64, native_token: Address, args: args::Tx) -> args::TxTransfer { + pub fn build_transfer_args( + &self, + source: Address, + target: Address, + token: Address, + amount: u64, + native_token: Address, + args: args::Tx, + ) -> args::TxTransfer { args::TxTransfer { tx: args, source: TransferSource::Address(source), @@ -105,9 +121,9 @@ impl NamadaSdk { token, amount: InputAmount::Validated(DenominatedAmount { amount: token::Amount::from_u64(amount), - denom: NATIVE_MAX_DECIMAL_PLACES.into() + denom: NATIVE_MAX_DECIMAL_PLACES.into(), }), - native_token: native_token, + native_token, tx_code_path: PathBuf::from("tx_transfer.wasm"), } } diff --git a/src/sdk/utils.rs b/src/sdk/utils.rs index 7e345f8..3edc9a3 100644 --- a/src/sdk/utils.rs +++ b/src/sdk/utils.rs @@ -1,6 +1,6 @@ use std::str::FromStr; -use namada::types::{key::common::SecretKey, address::Address}; +use namada::types::{address::Address, key::common::SecretKey}; pub fn sk_from_str(sk: &str) -> SecretKey { SecretKey::from_str(sk).expect("Should be able to decode secret key.") @@ -8,4 +8,4 @@ pub fn sk_from_str(sk: &str) -> SecretKey { pub fn str_to_address(data: &str) -> Address { Address::from_str(data).expect("Should be able to decode address") -} \ No newline at end of file +} diff --git a/src/sdk/wallet.rs b/src/sdk/wallet.rs index 50ad0ba..5053e4c 100644 --- a/src/sdk/wallet.rs +++ b/src/sdk/wallet.rs @@ -1,19 +1,31 @@ use std::path::PathBuf; -use namada::ledger::wallet::{alias::Alias, ConfirmationResponse, GenRestoreKeyError, WalletUtils, Store, Wallet}; +use namada::{ + ledger::wallet::{ + alias::Alias, ConfirmationResponse, GenRestoreKeyError, Store, StoredKeypair, Wallet, + WalletUtils, + }, + types::{ + address::Address, + key::{common::SecretKey, PublicKeyHash}, + }, +}; use rand::rngs::OsRng; pub struct SdkWallet { - pub wallet: Wallet + pub wallet: Wallet, } impl SdkWallet { - pub fn new() -> Self { + pub fn new(sk: SecretKey, nam_address: Address) -> Self { let store = Store::default(); - let wallet = Wallet::new(PathBuf::new(), store); - Self { - wallet - } + let mut wallet = Wallet::new(PathBuf::new(), store); + let stored_keypair = StoredKeypair::Raw(sk.clone()); + let pk_hash = PublicKeyHash::from(&sk.to_public()); + let alias = "my_faucet".to_string(); + wallet.insert_keypair(alias, stored_keypair, pk_hash, true); + wallet.add_address("nam", nam_address, true); + Self { wallet } } } @@ -51,4 +63,4 @@ impl WalletUtils for SdkWalletUtils { // Automatically replace aliases in non-interactive mode ConfirmationResponse::Replace } -} \ No newline at end of file +} diff --git a/src/services/faucet.rs b/src/services/faucet.rs index 3191d9d..d633f47 100644 --- a/src/services/faucet.rs +++ b/src/services/faucet.rs @@ -35,22 +35,24 @@ impl FaucetService { } fn compute_tag(&self, auth_key: &String, challenge: &[u8]) -> Vec { - let key = auth::SecretKey::from_slice(&auth_key.as_bytes()).expect("Should be able to convert key to bytes"); + let key = auth::SecretKey::from_slice(auth_key.as_bytes()) + .expect("Should be able to convert key to bytes"); let tag = auth::authenticate(&key, challenge).expect("Should be able to compute tag"); tag.unprotected_as_bytes().to_vec() } pub fn verify_tag(&self, auth_key: &String, challenge: &String, tag: &String) -> bool { - let key = auth::SecretKey::from_slice(&auth_key.as_bytes()).expect("Should be able to convert key to bytes"); - + let key = auth::SecretKey::from_slice(auth_key.as_bytes()) + .expect("Should be able to convert key to bytes"); + let decoded_tag = if let Ok(decoded_tag) = HEXLOWER.decode(tag.as_bytes()) { let tag = Tag::from_slice(&decoded_tag); match tag { Ok(tag) => { let tag_bytes = tag.unprotected_as_bytes().to_vec(); tag_bytes - }, + } Err(_) => return false, } } else { @@ -59,7 +61,7 @@ impl FaucetService { let tag = Tag::from_slice(&decoded_tag).expect("Should be able to convert bytes to tag"); - let decoded_challenge = HEXLOWER.decode(&challenge.as_bytes()).expect("Test"); + let decoded_challenge = HEXLOWER.decode(challenge.as_bytes()).expect("Test"); auth::authenticate_verify(&tag, &key, &decoded_challenge).is_ok() } diff --git a/src/state/faucet.rs b/src/state/faucet.rs index e7018ca..b882a6f 100644 --- a/src/state/faucet.rs +++ b/src/state/faucet.rs @@ -1,8 +1,8 @@ -use namada::types::{key::common::SecretKey, address::Address}; - use crate::{ app_state::AppState, repository::faucet::FaucetRepository, - repository::faucet::FaucetRepositoryTrait, services::faucet::FaucetService, sdk::namada::NamadaSdk}; + repository::faucet::FaucetRepositoryTrait, sdk::namada::NamadaSdk, + services::faucet::FaucetService, +}; use std::sync::{Arc, RwLock}; use tokio::sync::Mutex; @@ -11,21 +11,23 @@ pub struct FaucetState { pub faucet_service: FaucetService, pub faucet_repo: FaucetRepository, pub sdk: Arc>, - pub sk: SecretKey, - pub nam_address: Address, pub auth_key: String, pub difficulty: u64, - pub chain_id: String + pub chain_id: String, } impl FaucetState { - pub fn new(data: &Arc>, sdk: NamadaSdk, sk: SecretKey, nam_address: Address, auth_key: String, difficulty: u64, chain_id: String) -> Self { + pub fn new( + data: &Arc>, + sdk: NamadaSdk, + auth_key: String, + difficulty: u64, + chain_id: String, + ) -> Self { Self { faucet_service: FaucetService::new(data), faucet_repo: FaucetRepository::new(data), sdk: Arc::new(Mutex::new(sdk)), - sk, - nam_address, auth_key, difficulty, chain_id, diff --git a/src/utils/pow.rs b/src/utils/pow.rs index 790e153..28e28ea 100644 --- a/src/utils/pow.rs +++ b/src/utils/pow.rs @@ -1,33 +1,42 @@ -use orion::hazardous::hash::sha2::sha256::Sha256; use data_encoding::HEXLOWER; +use orion::hazardous::hash::sha2::sha256::Sha256; pub fn is_valid_proof_of_work(challenge: &String, solution: &String, difficulty: u64) -> bool { - let decoded_challenge = if let Ok(challenge) = HEXLOWER.decode(&challenge.as_bytes()) { + let decoded_challenge = if let Ok(challenge) = HEXLOWER.decode(challenge.as_bytes()) { challenge } else { return false; }; - let decoded_solution = if let Ok(solution) = HEXLOWER.decode(&solution.as_bytes()) { + let decoded_solution = if let Ok(solution) = HEXLOWER.decode(solution.as_bytes()) { solution } else { return false; }; let mut hasher = Sha256::new(); - hasher.update(&decoded_challenge).expect("Should be able to hash bytes"); - hasher.update(&decoded_solution).expect("Should be able to hash bytes"); + hasher + .update(&decoded_challenge) + .expect("Should be able to hash bytes"); + hasher + .update(&decoded_solution) + .expect("Should be able to hash bytes"); let hash = hasher.finalize().expect("Should be able to hash bytes"); - // sketchy + // sketchy let hash_bytes = HEXLOWER.encode(hash.as_ref()); - + // TODO: rewrite with bit mask - for byte in hash_bytes.as_bytes().iter().take(difficulty as usize).cloned() { + for byte in hash_bytes + .as_bytes() + .iter() + .take(difficulty as usize) + .cloned() + { if byte != b'0' { return false; } } - true + true }