From a7fa47ef9dadb9597d13490136ccca76f221a04c Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Fri, 24 Mar 2023 12:28:58 +0100 Subject: [PATCH] more work on substrate fees --- Cargo.lock | 1 + crates/relayer-handlers/src/lib.rs | 12 ++-- crates/tx-relay/Cargo.toml | 1 + crates/tx-relay/src/evm/fees.rs | 36 +++++++++-- crates/tx-relay/src/evm/vanchor.rs | 9 +-- crates/tx-relay/src/lib.rs | 5 ++ crates/tx-relay/src/substrate/fees.rs | 64 +++++++++++++++++-- crates/tx-relay/src/substrate/mixer.rs | 2 - crates/tx-relay/src/substrate/vanchor.rs | 59 +++++++++++++++-- services/webb-relayer/src/service.rs | 2 +- tests/lib/webbRelayer.ts | 15 +++-- .../vanchorPrivateTransaction.test.ts | 53 +++++++++++---- 12 files changed, 203 insertions(+), 56 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d19919482..9e4f39bb8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8376,6 +8376,7 @@ dependencies = [ "native-tls", "once_cell", "serde", + "sp-core", "tokio 1.26.0", "tracing", "webb 0.5.20", diff --git a/crates/relayer-handlers/src/lib.rs b/crates/relayer-handlers/src/lib.rs index 866f9c467..bfa55016b 100644 --- a/crates/relayer-handlers/src/lib.rs +++ b/crates/relayer-handlers/src/lib.rs @@ -220,13 +220,9 @@ pub async fn handle_evm_fee_info( } pub async fn handle_substrate_fee_info( State(ctx): State>, - Path(chain_id): Path, + Path((chain_id, estimated_tx_fees)): Path<(u64, u128)>, ) -> Result, HandlerError> { - let chain_id = TypedChainId::from(chain_id); - get_substrate_fee_info(chain_id, ctx.as_ref()) - .await - .map(Json) - .map_err(|e| { - HandlerError(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()) - }) + Ok(Json( + get_substrate_fee_info(chain_id, estimated_tx_fees, ctx.as_ref()).await, + )) } diff --git a/crates/tx-relay/Cargo.toml b/crates/tx-relay/Cargo.toml index 8e1e88db2..16a99002a 100644 --- a/crates/tx-relay/Cargo.toml +++ b/crates/tx-relay/Cargo.toml @@ -25,6 +25,7 @@ native-tls = { workspace = true, optional = true } webb-proposals = { workspace = true } ethereum-types = { workspace = true } serde = { workspace = true } +sp-core = { workspace = true } once_cell = "1.17.0" chrono = {version = "0.4.23", features = ["serde"] } diff --git a/crates/tx-relay/src/evm/fees.rs b/crates/tx-relay/src/evm/fees.rs index fcedcc7de..eb2c5d01d 100644 --- a/crates/tx-relay/src/evm/fees.rs +++ b/crates/tx-relay/src/evm/fees.rs @@ -1,8 +1,10 @@ +use crate::{MAX_REFUND_USD, TRANSACTION_PROFIT_USD}; use chrono::DateTime; use chrono::Duration; use chrono::Utc; use once_cell::sync::Lazy; use serde::Serialize; +use std::cmp::min; use std::collections::HashMap; use std::ops::Add; use std::sync::{Arc, Mutex}; @@ -11,18 +13,18 @@ use webb::evm::contract::protocol_solidity::{ }; use webb::evm::ethers::middleware::SignerMiddleware; use webb::evm::ethers::prelude::U256; +use webb::evm::ethers::providers::Middleware; +use webb::evm::ethers::signers::Signer; use webb::evm::ethers::types::Address; use webb::evm::ethers::utils::{format_units, parse_units}; use webb_proposals::TypedChainId; use webb_relayer_context::RelayerContext; +use webb_relayer_handler_utils::CommandResponse::{Error, Network}; +use webb_relayer_handler_utils::{CommandResponse, NetworkStatus}; use webb_relayer_utils::Result; -/// Maximum refund amount per relay transaction in USD. -const MAX_REFUND_USD: f64 = 5.; /// Amount of time for which a `FeeInfo` is valid after creation static FEE_CACHE_TIME: Lazy = Lazy::new(|| Duration::minutes(1)); -/// Amount of profit that the relay should make with each transaction (in USD). -const TRANSACTION_PROFIT_USD: f64 = 5.; /// Cache for previously generated fee info. Key consists of the VAnchor address and chain id. /// Entries are valid as long as `timestamp` is no older than `FEE_CACHE_TIME`. @@ -154,9 +156,13 @@ async fn generate_fee_info( .into(); // Calculate the maximum refund amount per relay transaction in `nativeToken`. + // Ensuring that refund <= relayer balance + let relayer_balance = + relayer_balance(chain_id.chain_id(), ctx).await.unwrap(); let max_refund = parse_units(MAX_REFUND_USD / native_token_price, native_token.1)? .into(); + let max_refund = min(relayer_balance, max_refund); Ok(EvmFeeInfo { estimated_fee, @@ -170,6 +176,28 @@ async fn generate_fee_info( }) } +pub(super) async fn relayer_balance( + chain_id: u64, + ctx: &RelayerContext, +) -> std::result::Result { + let wallet = ctx.evm_wallet(&chain_id.to_string()).await.map_err(|e| { + Error(format!("Misconfigured Network: {:?}, {e}", chain_id)) + })?; + let provider = + ctx.evm_provider(&chain_id.to_string()).await.map_err(|e| { + Network(NetworkStatus::Failed { + reason: e.to_string(), + }) + })?; + let relayer_balance = provider + .get_balance(wallet.address(), None) + .await + .map_err(|e| { + Error(format!("Failed to retrieve relayer balance: {e}")) + })?; + Ok(relayer_balance) +} + /// Pull USD prices of base token from coingecko.com, and use this to calculate the transaction /// fee in `wrappedToken` wei. This fee includes a profit for the relay of `TRANSACTION_PROFIT_USD`. /// diff --git a/crates/tx-relay/src/evm/vanchor.rs b/crates/tx-relay/src/evm/vanchor.rs index 7094e3d51..df5daa919 100644 --- a/crates/tx-relay/src/evm/vanchor.rs +++ b/crates/tx-relay/src/evm/vanchor.rs @@ -1,5 +1,5 @@ use super::*; -use crate::evm::fees::{get_evm_fee_info, EvmFeeInfo}; +use crate::evm::fees::{get_evm_fee_info, relayer_balance, EvmFeeInfo}; use crate::evm::handle_evm_tx; use ethereum_types::U256; use std::{collections::HashMap, sync::Arc}; @@ -89,12 +89,7 @@ pub async fn handle_vanchor_relay_tx<'a>( let _ = stream.send(Network(NetworkStatus::Connected)).await; // ensure that relayer has enough balance for refund - let relayer_balance = provider - .get_balance(wallet.address(), None) - .await - .map_err(|e| { - Error(format!("Failed to retrieve relayer balance: {e}")) - })?; + let relayer_balance = relayer_balance(requested_chain, &ctx).await?; if cmd.ext_data.refund > relayer_balance { return Err(Error( "Requested refund is higher than relayer balance".to_string(), diff --git a/crates/tx-relay/src/lib.rs b/crates/tx-relay/src/lib.rs index ca4bd99a0..62c11da01 100644 --- a/crates/tx-relay/src/lib.rs +++ b/crates/tx-relay/src/lib.rs @@ -4,3 +4,8 @@ pub mod evm; /// Substrate Transactional Relayer. #[cfg(feature = "substrate")] pub mod substrate; + +/// Maximum refund amount per relay transaction in USD. +const MAX_REFUND_USD: f64 = 5.; +/// Amount of profit that the relay should make with each transaction (in USD). +const TRANSACTION_PROFIT_USD: f64 = 5.; diff --git a/crates/tx-relay/src/substrate/fees.rs b/crates/tx-relay/src/substrate/fees.rs index eeaca2743..d97eb3551 100644 --- a/crates/tx-relay/src/substrate/fees.rs +++ b/crates/tx-relay/src/substrate/fees.rs @@ -1,15 +1,65 @@ -use serde::Serialize; -use webb_proposals::TypedChainId; +use crate::{MAX_REFUND_USD, TRANSACTION_PROFIT_USD}; +use chrono::{DateTime, Utc}; +use serde::Deserialize; +use serde::{Deserializer, Serialize}; +use std::str::FromStr; use webb_relayer_context::RelayerContext; -use webb_relayer_utils::Result; + +const TOKEN_PRICE_USD: f64 = 0.1; +const TOKEN_DECIMALS: i32 = 18; #[derive(Debug, Serialize, Clone)] #[serde(rename_all = "camelCase")] -pub struct SubstrateFeeInfo {} +pub struct SubstrateFeeInfo { + /// Estimated fee for an average relay transaction, in `wrappedToken`. Includes network fees + /// and relay fee. + pub estimated_fee: u128, + /// Exchange rate for refund from `wrappedToken` to `nativeToken` + pub refund_exchange_rate: u128, + /// Maximum amount of `nativeToken` which can be exchanged to `wrappedToken` by relay + pub max_refund: u128, + /// Time when this FeeInfo was generated + timestamp: DateTime, +} pub async fn get_substrate_fee_info( - chain_id: TypedChainId, + chain_id: u64, + estimated_tx_fees: u128, ctx: &RelayerContext, -) -> Result { - Ok(SubstrateFeeInfo {}) +) -> SubstrateFeeInfo { + let estimated_fee = estimated_tx_fees + + matic_to_wei(TRANSACTION_PROFIT_USD / TOKEN_PRICE_USD); + let refund_exchange_rate = matic_to_wei(1.); + // TODO: should ensure that refund <= relayer balance + let max_refund = matic_to_wei(MAX_REFUND_USD / TOKEN_PRICE_USD); + SubstrateFeeInfo { + estimated_fee, + refund_exchange_rate, + max_refund, + timestamp: Utc::now(), + } +} + +/// Convert from full matic coin amount to smallest unit amount (also called wei). +/// +/// It looks like subxt has no built-in functionality for this. +fn matic_to_wei(matic: f64) -> u128 { + (matic * 10_f64.powi(TOKEN_DECIMALS)) as u128 +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(super) struct RpcFeeDetailsResponse { + #[serde(deserialize_with = "deserialize_number_string")] + pub partial_fee: u128, +} + +fn deserialize_number_string<'de, D>( + deserializer: D, +) -> std::result::Result +where + D: Deserializer<'de>, +{ + let hex: String = Deserialize::deserialize(deserializer)?; + Ok(u128::from_str(&hex).unwrap()) } diff --git a/crates/tx-relay/src/substrate/mixer.rs b/crates/tx-relay/src/substrate/mixer.rs index 97d07afc1..a69d6d900 100644 --- a/crates/tx-relay/src/substrate/mixer.rs +++ b/crates/tx-relay/src/substrate/mixer.rs @@ -10,8 +10,6 @@ use webb_relayer_handler_utils::SubstrateMixerCommand; /// Handler for Substrate Mixer commands /// -/// TODO: dont change anything here -/// /// # Arguments /// /// * `ctx` - RelayContext reference that holds the configuration diff --git a/crates/tx-relay/src/substrate/vanchor.rs b/crates/tx-relay/src/substrate/vanchor.rs index d5ce6a0ed..45da02ea6 100644 --- a/crates/tx-relay/src/substrate/vanchor.rs +++ b/crates/tx-relay/src/substrate/vanchor.rs @@ -1,6 +1,12 @@ use super::*; +use crate::substrate::fees::{get_substrate_fee_info, RpcFeeDetailsResponse}; use crate::substrate::handle_substrate_tx; +use sp_core::crypto::Pair; +use std::ops::Deref; +use webb::evm::ethers::utils::__serde_json::Value; +use webb::evm::ethers::utils::hex; use webb::substrate::protocol_substrate_runtime::api as RuntimeApi; +use webb::substrate::subxt::rpc::RpcParams; use webb::substrate::subxt::utils::AccountId32; use webb::substrate::{ protocol_substrate_runtime::api::runtime_types::{ @@ -17,8 +23,6 @@ use webb_relayer_utils::metric::Metrics; /// Handler for Substrate Anchor commands /// -/// TODO: add fee here -/// /// # Arguments /// /// * `ctx` - RelayContext reference that holds the configuration @@ -76,17 +80,60 @@ pub async fn handle_substrate_vanchor_relay_tx<'a>( Error(format!("Misconfigured Network {:?}: {e}", cmd.chain_id)) })?; - let signer = PairSigner::new(pair); + let signer = PairSigner::new(pair.clone()); + // TODO: add refund let transact_tx = RuntimeApi::tx().v_anchor_bn254().transact( cmd.id, proof_elements, ext_data_elements, ); - let transact_tx_hash = client + + let mut params = RpcParams::new(); + let signed = client .tx() - .sign_and_submit_then_watch_default(&transact_tx, &signer) - .await; + .create_signed(&transact_tx, &signer, Default::default()) + .await + .unwrap(); + params.push(hex::encode(signed.encoded())).unwrap(); + let payment_info = client + .rpc() + .request::("payment_queryInfo", params) + .await + .unwrap(); + dbg!(&payment_info); + let fee_info = + get_substrate_fee_info(requested_chain, payment_info.partial_fee, &ctx) + .await; + + // TODO: check refund amount <= relayer wallet balance + let mut params = RpcParams::new(); + params.push(hex::encode(pair.public().deref())).unwrap(); + //let balance = client.storage().at(None).await.unwrap(). + + // validate refund amount + if cmd.ext_data.refund > fee_info.max_refund { + // TODO: use error enum for these messages so they dont have to be duplicated between + // evm/substrate + let msg = format!( + "User requested a refund which is higher than the maximum of {}", + fee_info.max_refund + ); + return Err(Error(msg)); + } + + // Check that transaction fee is enough to cover network fee and relayer fee + // TODO: refund needs to be converted from wrapped token to native token once there + // is an exchange rate + if cmd.ext_data.fee < fee_info.estimated_fee + cmd.ext_data.refund { + let msg = format!( + "User sent a fee that is too low {} but expected {}", + cmd.ext_data.fee, fee_info.estimated_fee + ); + return Err(Error(msg)); + } + + let transact_tx_hash = signed.submit_and_watch().await; let event_stream = transact_tx_hash .map_err(|e| Error(format!("Error while sending Tx: {e}")))?; diff --git a/services/webb-relayer/src/service.rs b/services/webb-relayer/src/service.rs index ddae08d4e..b7d0fb401 100644 --- a/services/webb-relayer/src/service.rs +++ b/services/webb-relayer/src/service.rs @@ -133,7 +133,7 @@ pub async fn build_web_services(ctx: RelayerContext) -> crate::Result<()> { get(handle_evm_fee_info), ) .route( - "/fee_info/substrate/:chain_id", + "/fee_info/substrate/:chain_id/:estimated_tx_fees", get(handle_substrate_fee_info), ); diff --git a/tests/lib/webbRelayer.ts b/tests/lib/webbRelayer.ts index e3faf6be2..251395f7a 100644 --- a/tests/lib/webbRelayer.ts +++ b/tests/lib/webbRelayer.ts @@ -219,7 +219,7 @@ export class WebbRelayer { const response = await fetch(endpoint); return response; } - + public async getEvmFeeInfo( chainId: number, vanchor: string, @@ -229,11 +229,9 @@ export class WebbRelayer { const response = await fetch(endpoint); return response; } - - public async getSubstrateFeeInfo( - chainId: number, - ) { - const endpoint = `http://127.0.0.1:${this.opts.commonConfig.port}/api/v1/fee_info/substrate/${chainId}`; + + public async getSubstrateFeeInfo(chainId: number, partialFee: number) { + const endpoint = `http://127.0.0.1:${this.opts.commonConfig.port}/api/v1/fee_info/substrate/${chainId}/${partialFee}`; const response = await fetch(endpoint); return response; } @@ -650,7 +648,10 @@ export interface EvmFeeInfo { } export interface SubstrateFeeInfo { - + estimatedFee: BigNumber; + refundExchangeRate: BigNumber; + maxRefund: BigNumber; + timestamp: string; } export interface Contract { diff --git a/tests/test/substrate/vanchorPrivateTransaction.test.ts b/tests/test/substrate/vanchorPrivateTransaction.test.ts index 9bcd13f55..5267d983e 100644 --- a/tests/test/substrate/vanchorPrivateTransaction.test.ts +++ b/tests/test/substrate/vanchorPrivateTransaction.test.ts @@ -113,7 +113,7 @@ describe('Substrate VAnchor Private Transaction Relayer Tests', function () { }, tmp: true, configDir: tmpDirPath, - showLogs: false, + showLogs: true, }); await webbRelayer.waitUntilReady(); }); @@ -157,13 +157,24 @@ describe('Substrate VAnchor Private Transaction Relayer Tests', function () { // Bob's balance after withdrawal const bobBalanceBefore = await api.query.system.account(account.address); - // TODO: this should probably use fee + // get refund amount + const feeInfoResponse1 = await webbRelayer.getSubstrateFeeInfo( + substrateChainId, + 0 + ); + expect(feeInfoResponse1.status).equal(200); + const feeInfo1 = + await (feeInfoResponse1.json() as Promise); + const vanchorData = await vanchorWithdraw( typedSourceChainId.toString(), typedSourceChainId.toString(), account.address, data.depositUtxos, treeId, + // TODO: need to convert this once there is exchange rate between native + // token and wrapped token + feeInfo1.maxRefund, api ); // Now we construct payload for substrate private transaction. @@ -199,16 +210,20 @@ describe('Substrate VAnchor Private Transaction Relayer Tests', function () { Array.from(hexToU8a(com)) ), }; - const feeInfoResponse = await webbRelayer.getSubstrateFeeInfo( - substrateChainId - ); - expect(feeInfoResponse.status).equal(200); - const feeInfo = await (feeInfoResponse.json() as Promise); - console.log(feeInfo); - const info = api.tx.vAnchorBn254.transact(treeId, substrateProofData, - substrateExtData).paymentInfo(account); - console.log(info); + // TODO: not working yet, value hardcoded for now + //const info = api.tx.vAnchorBn254.transact(treeId, substrateProofData, + // substrateExtData).paymentInfo(account); + //console.log(info); + const partialFee = 10958835753; + const feeInfoResponse2 = await webbRelayer.getSubstrateFeeInfo( + substrateChainId, + partialFee + ); + expect(feeInfoResponse2.status).equal(200); + const feeInfo2 = + await (feeInfoResponse2.json() as Promise); + console.log(feeInfo2); // now we withdraw using private transaction await webbRelayer.substrateVAnchorWithdraw( @@ -247,7 +262,13 @@ describe('Substrate VAnchor Private Transaction Relayer Tests', function () { function currencyToUnitI128(currencyAmount: number) { const bn = BigNumber.from(currencyAmount); - return bn.mul(1_000_000_000_000); + // TODO: still needs fixing, this originally assumed 12 token decimals + console.log(bn); + const decimals = BigNumber.from(10).mul(18); + console.log(decimals); + const x = bn.mul(decimals); + console.log(x); + return x; } function createAccount(accountId: string): any { @@ -263,6 +284,7 @@ async function vanchorWithdraw( recipient: string, depositUtxos: [Utxo, Utxo], treeId: number, + refund: BigNumber, api: ApiPromise ): Promise<{ extData: any; proofData: any }> { const secret = randomAsU8a(); @@ -306,7 +328,8 @@ async function vanchorWithdraw( const output1 = await Utxo.generateUtxo({ curve: 'Bn254', backend: 'Arkworks', - amount: '0', + // TODO: is this correct? + amount: refund.toString(), chainId: typedSourceChainId, }); const output2 = await Utxo.generateUtxo({ @@ -322,11 +345,13 @@ async function vanchorWithdraw( const address = recipient; const fee = 0; - const refund = 0; + console.log("refund: ", refund); const withdrawAmount = depositUtxos.reduce((acc, utxo) => { return Number(utxo.amount) + acc; }, 0); + console.log("withdrawAmount: ", withdrawAmount); const extAmount = -withdrawAmount; + console.log("extAmount: ", extAmount); const publicAmount = -withdrawAmount;