Skip to content

Commit

Permalink
funds-manager: Refill gas to active wallets
Browse files Browse the repository at this point in the history
  • Loading branch information
joeykraut committed Aug 5, 2024
1 parent 283156e commit b4d1332
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 17 deletions.
9 changes: 9 additions & 0 deletions funds-manager/funds-manager-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ pub const WITHDRAW_CUSTODY_ROUTE: &str = "withdraw";

/// The route to withdraw gas from custody
pub const WITHDRAW_GAS_ROUTE: &str = "withdraw-gas";
/// The route to refill gas for all active wallets
pub const REFILL_GAS_ROUTE: &str = "refill-gas";
/// The route to register a gas wallet for a peer
pub const REGISTER_GAS_WALLET_ROUTE: &str = "register-gas-wallet";
/// The route to report active peers
Expand Down Expand Up @@ -92,6 +94,13 @@ pub struct WithdrawGasRequest {
pub destination_address: String,
}

/// The request body for refilling gas for all active wallets
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RefillGasRequest {
/// The amount of gas to top up each wallet to
pub amount: f64,
}

/// The response containing the gas wallet's address
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateGasWalletResponse {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,52 @@ use rand::thread_rng;
use tracing::info;

use crate::{
custody_client::DepositWithdrawSource,
db::models::GasWalletStatus,
error::FundsManagerError,
helpers::{create_secrets_manager_entry_with_description, get_secret},
};

use super::CustodyClient;

/// The threshold beneath which we skip refilling gas for a wallet
///
/// I.e. if the wallet's balance is within this amount of the desired fill, we
/// skip refilling
pub const GAS_REFILL_TOLERANCE: f64 = 0.001; // ETH
/// The amount to top up a newly registered gas wallet
pub const DEFAULT_TOP_UP_AMOUNT: f64 = 0.01; // ETH

impl CustodyClient {
// ------------
// | Handlers |
// ------------

/// Refill gas for all active wallets
pub(crate) async fn refill_gas_for_active_wallets(
&self,
fill_to: f64,
) -> Result<(), FundsManagerError> {
// Fetch all active gas wallets
let active_wallets = self.get_active_gas_wallets().await?;

// Filter out those that don't need refilling
let mut wallets_to_fill: Vec<(String, f64)> = Vec::new(); // (address, fill amount)
for wallet in active_wallets {
let bal = self.get_ether_balance(&wallet.address).await?;
if bal + GAS_REFILL_TOLERANCE < fill_to {
wallets_to_fill.push((wallet.address, fill_to - bal));
}
}

if wallets_to_fill.is_empty() {
return Ok(());
}

// Refill the gas wallets
self.refill_gas_for_wallets(wallets_to_fill).await
}

/// Create a new gas wallet
pub(crate) async fn create_gas_wallet(&self) -> Result<String, FundsManagerError> {
// Sample a new ethereum keypair
Expand Down Expand Up @@ -59,8 +93,9 @@ impl CustodyClient {
let secret_name = Self::gas_wallet_secret_name(&gas_wallet.address);
let secret_value = get_secret(&secret_name, &self.aws_config).await?;

// Update the gas wallet to be active and return the keypair
// Update the gas wallet to be active, top up wallets, and return the key
self.mark_gas_wallet_active(&gas_wallet.address, peer_id).await?;
self.refill_gas_for_active_wallets(DEFAULT_TOP_UP_AMOUNT).await?;
Ok(secret_value)
}

Expand Down Expand Up @@ -107,4 +142,31 @@ impl CustodyClient {
fn gas_wallet_secret_name(address: &str) -> String {
format!("gas-wallet-{}", address)
}

/// Refill gas for a set of wallets
async fn refill_gas_for_wallets(
&self,
wallets: Vec<(String, f64)>, // (address, fill amount)
) -> Result<(), FundsManagerError> {
// Get the gas hot wallet's private key
let source = DepositWithdrawSource::Gas.vault_name();
let gas_wallet = self.get_hot_wallet_by_vault(source).await?;
let signer = self.get_hot_wallet_private_key(&gas_wallet.address).await?;

// Check that the gas wallet has enough ETH to cover the refill
let total_amount = wallets.iter().map(|(_, amount)| *amount).sum::<f64>();
let my_balance = self.get_ether_balance(&gas_wallet.address).await?;
if my_balance < total_amount {
return Err(FundsManagerError::custom(
"gas wallet does not have enough ETH to cover the refill",
));
}

// Refill the balances
for (address, amount) in wallets {
self.transfer_ether(&address, amount, signer.clone()).await?;
}

Ok(())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,17 @@ impl CustodyClient {
format!("hot-wallet-{address}")
}

/// Get the hot wallet private key for a vault
pub async fn get_hot_wallet_private_key(
&self,
address: &str,
) -> Result<LocalWallet, FundsManagerError> {
let secret_name = Self::hot_wallet_secret_name(address);
let secret_value = get_secret(&secret_name, &self.aws_config).await?;

LocalWallet::from_str(&secret_value).map_err(FundsManagerError::parse)
}

/// Fetch the token balance at the given address for a wallet
async fn get_token_balance(
&self,
Expand Down
7 changes: 4 additions & 3 deletions funds-manager/funds-manager-server/src/custody_client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use ethers::middleware::SignerMiddleware;
use ethers::prelude::abigen;
use ethers::providers::{Http, Middleware, Provider};
use ethers::signers::{LocalWallet, Signer};
use ethers::types::{Address, TransactionReceipt, TransactionRequest, U256};
use ethers::types::{Address, TransactionReceipt, TransactionRequest};
use ethers::utils::format_units;
use fireblocks_sdk::types::Transaction;
use fireblocks_sdk::{
Expand Down Expand Up @@ -211,10 +211,11 @@ impl CustodyClient {
let client = SignerMiddleware::new(provider, wallet);

let to = Address::from_str(to).map_err(FundsManagerError::parse)?;
let amount = ethers::utils::parse_units(amount.to_string(), "ether")
let amount_units = ethers::utils::parse_units(amount.to_string(), "ether")
.map_err(FundsManagerError::parse)?;

let tx = TransactionRequest::new().to(to).value(U256::from(amount));
info!("Transferring {amount} ETH to {to:#x}");
let tx = TransactionRequest::new().to(to).value(amount_units);
let pending_tx =
client.send_transaction(tx, None).await.map_err(FundsManagerError::arbitrum)?;
pending_tx
Expand Down
11 changes: 11 additions & 0 deletions funds-manager/funds-manager-server/src/custody_client/queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ impl CustodyClient {
.map_err(err_str!(FundsManagerError::Db))
}

/// Get all active gas wallets
pub async fn get_active_gas_wallets(&self) -> Result<Vec<GasWallet>, FundsManagerError> {
let mut conn = self.get_db_conn().await?;
let active = GasWalletStatus::Active.to_string();
gas_wallets::table
.filter(gas_wallets::status.eq(active))
.load::<GasWallet>(&mut conn)
.await
.map_err(err_str!(FundsManagerError::Db))
}

/// Find an inactive gas wallet
pub async fn find_inactive_gas_wallet(&self) -> Result<GasWallet, FundsManagerError> {
let mut conn = self.get_db_conn().await?;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,7 @@ impl CustodyClient {
}

// Fetch the wallet private key
let secret_name = Self::hot_wallet_secret_name(&wallet.address);
let private_key = get_secret(&secret_name, &self.aws_config).await?;
let wallet =
LocalWallet::from_str(private_key.as_str()).map_err(FundsManagerError::parse)?;
let wallet = self.get_hot_wallet_private_key(&wallet.address).await?;

// Execute the erc20 transfer
let tx = self.erc20_transfer(token_address, destination_address, amount, wallet).await?;
Expand Down
30 changes: 26 additions & 4 deletions funds-manager/funds-manager-server/src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ use crate::Server;
use bytes::Bytes;
use funds_manager_api::{
CreateGasWalletResponse, CreateHotWalletRequest, CreateHotWalletResponse,
DepositAddressResponse, FeeWalletsResponse, HotWalletBalancesResponse,
DepositAddressResponse, FeeWalletsResponse, HotWalletBalancesResponse, RefillGasRequest,
RegisterGasWalletRequest, RegisterGasWalletResponse, ReportActivePeersRequest,
TransferToVaultRequest, WithdrawFeeBalanceRequest, WithdrawFundsRequest, WithdrawGasRequest,
WithdrawToHotWalletRequest,
};
use itertools::Itertools;
use serde_json::json;
use std::collections::HashMap;
use std::sync::Arc;
use warp::reply::Json;
Expand All @@ -21,7 +22,9 @@ pub const MINTS_QUERY_PARAM: &str = "mints";
/// The asset used for gas (ETH)
pub const GAS_ASSET_NAME: &str = "ETH";
/// The maximum amount of gas that can be withdrawn at a given time
pub const MAX_GAS_WITHDRAWAL_AMOUNT: f64 = 0.1; // 0.1 ETH
pub const MAX_GAS_WITHDRAWAL_AMOUNT: f64 = 1.; // ETH
/// The maximum amount that a request may refill gas to
pub const MAX_GAS_REFILL_AMOUNT: f64 = 0.1; // ETH

// --- Fee Indexing --- //

Expand Down Expand Up @@ -126,8 +129,25 @@ pub(crate) async fn withdraw_gas_handler(
.withdraw_gas(withdraw_request.amount, &withdraw_request.destination_address)
.await
.map_err(|e| warp::reject::custom(ApiError::InternalError(e.to_string())))?;
Ok(warp::reply::json(&"Withdrawal complete"))
}

Ok(warp::reply::json(&format!("Gas withdrawal of {} ETH complete", withdraw_request.amount)))
/// Handler for refilling gas for all active wallets
pub(crate) async fn refill_gas_handler(
req: RefillGasRequest,
server: Arc<Server>,
) -> Result<Json, warp::Rejection> {
// Check that the refill amount is less than the max
if req.amount > MAX_GAS_REFILL_AMOUNT {
return Err(warp::reject::custom(ApiError::BadRequest(format!(
"Requested amount {} ETH exceeds maximum allowed refill of {} ETH",
req.amount, MAX_GAS_REFILL_AMOUNT
))));
}

server.custody_client.refill_gas_for_active_wallets(req.amount).await?;
let resp = json!({});
Ok(warp::reply::json(&resp))
}

/// Handler for creating a new gas wallet
Expand Down Expand Up @@ -168,7 +188,9 @@ pub(crate) async fn report_active_peers_handler(
.record_active_gas_wallet(req.peers)
.await
.map_err(|e| warp::reject::custom(ApiError::InternalError(e.to_string())))?;
Ok(warp::reply::json(&{}))

let resp = json!({});
Ok(warp::reply::json(&resp))
}

// --- Hot Wallets --- //
Expand Down
21 changes: 16 additions & 5 deletions funds-manager/funds-manager-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,17 @@ use error::FundsManagerError;
use ethers::signers::LocalWallet;
use fee_indexer::Indexer;
use funds_manager_api::{
CreateHotWalletRequest, RegisterGasWalletRequest, ReportActivePeersRequest,
CreateHotWalletRequest, RefillGasRequest, RegisterGasWalletRequest, ReportActivePeersRequest,
TransferToVaultRequest, WithdrawFeeBalanceRequest, WithdrawGasRequest,
WithdrawToHotWalletRequest, GET_DEPOSIT_ADDRESS_ROUTE, GET_FEE_WALLETS_ROUTE, INDEX_FEES_ROUTE,
PING_ROUTE, REDEEM_FEES_ROUTE, REGISTER_GAS_WALLET_ROUTE, REPORT_ACTIVE_PEERS_ROUTE,
TRANSFER_TO_VAULT_ROUTE, WITHDRAW_CUSTODY_ROUTE, WITHDRAW_FEE_BALANCE_ROUTE,
WITHDRAW_GAS_ROUTE, WITHDRAW_TO_HOT_WALLET_ROUTE,
PING_ROUTE, REDEEM_FEES_ROUTE, REFILL_GAS_ROUTE, REGISTER_GAS_WALLET_ROUTE,
REPORT_ACTIVE_PEERS_ROUTE, TRANSFER_TO_VAULT_ROUTE, WITHDRAW_CUSTODY_ROUTE,
WITHDRAW_FEE_BALANCE_ROUTE, WITHDRAW_GAS_ROUTE, WITHDRAW_TO_HOT_WALLET_ROUTE,
};
use handlers::{
create_gas_wallet_handler, create_hot_wallet_handler, get_deposit_address_handler,
get_fee_wallets_handler, get_hot_wallet_balances_handler, index_fees_handler,
quoter_withdraw_handler, redeem_fees_handler, register_gas_wallet_handler,
quoter_withdraw_handler, redeem_fees_handler, refill_gas_handler, register_gas_wallet_handler,
report_active_peers_handler, transfer_to_vault_handler, withdraw_fee_balance_handler,
withdraw_from_vault_handler, withdraw_gas_handler,
};
Expand Down Expand Up @@ -343,6 +343,16 @@ async fn main() -> Result<(), Box<dyn Error>> {
.and(with_server(server.clone()))
.and_then(withdraw_gas_handler);

let refill_gas = warp::post()
.and(warp::path("custody"))
.and(warp::path("gas"))
.and(warp::path(REFILL_GAS_ROUTE))
.and(with_hmac_auth(server.clone()))
.map(with_json_body::<RefillGasRequest>)
.and_then(identity)
.and(with_server(server.clone()))
.and_then(refill_gas_handler);

let add_gas_wallet = warp::post()
.and(warp::path("custody"))
.and(warp::path("gas-wallets"))
Expand Down Expand Up @@ -415,6 +425,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
.or(withdraw_custody)
.or(get_deposit_address)
.or(withdraw_gas)
.or(refill_gas)
.or(report_active_peers)
.or(register_gas_wallet)
.or(add_gas_wallet)
Expand Down

0 comments on commit b4d1332

Please sign in to comment.