mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-19 06:31:03 +00:00
Override conversion rate when computing message fee (#1261)
* override conversion rate when message is sent * spelling + fmt * add --conversion-rate-override cli option * try to read conversion rate from cmd output * fix output * fmt
This commit is contained in:
committed by
Bastian Köcher
parent
0ef401ae53
commit
cfdb9fe7f6
@@ -24,9 +24,10 @@ use relay_substrate_client::Chain;
|
|||||||
use sp_runtime::FixedU128;
|
use sp_runtime::FixedU128;
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
use strum::VariantNames;
|
use strum::VariantNames;
|
||||||
|
use substrate_relay_helper::helpers::target_to_source_conversion_rate;
|
||||||
|
|
||||||
/// Estimate Delivery & Dispatch Fee command.
|
/// Estimate Delivery & Dispatch Fee command.
|
||||||
#[derive(StructOpt, Debug, PartialEq, Eq)]
|
#[derive(StructOpt, Debug, PartialEq)]
|
||||||
pub struct EstimateFee {
|
pub struct EstimateFee {
|
||||||
/// A bridge instance to encode call for.
|
/// A bridge instance to encode call for.
|
||||||
#[structopt(possible_values = FullBridge::VARIANTS, case_insensitive = true)]
|
#[structopt(possible_values = FullBridge::VARIANTS, case_insensitive = true)]
|
||||||
@@ -36,15 +37,44 @@ pub struct EstimateFee {
|
|||||||
/// Hex-encoded id of lane that will be delivering the message.
|
/// Hex-encoded id of lane that will be delivering the message.
|
||||||
#[structopt(long, default_value = "00000000")]
|
#[structopt(long, default_value = "00000000")]
|
||||||
lane: HexLaneId,
|
lane: HexLaneId,
|
||||||
|
/// A way to override conversion rate between bridge tokens.
|
||||||
|
///
|
||||||
|
/// If not specified, conversion rate from runtime storage is used. It may be obsolete and
|
||||||
|
/// your message won't be relayed.
|
||||||
|
#[structopt(long)]
|
||||||
|
conversion_rate_override: Option<ConversionRateOverride>,
|
||||||
/// Payload to send over the bridge.
|
/// Payload to send over the bridge.
|
||||||
#[structopt(flatten)]
|
#[structopt(flatten)]
|
||||||
payload: crate::cli::encode_message::MessagePayload,
|
payload: crate::cli::encode_message::MessagePayload,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A way to override conversion rate between bridge tokens.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum ConversionRateOverride {
|
||||||
|
/// The actual conversion rate is computed in the same way how rate metric works.
|
||||||
|
Metric,
|
||||||
|
/// The actual conversion rate is specified explicitly.
|
||||||
|
Explicit(f64),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::str::FromStr for ConversionRateOverride {
|
||||||
|
type Err = String;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
if s.to_lowercase() == "metric" {
|
||||||
|
return Ok(ConversionRateOverride::Metric)
|
||||||
|
}
|
||||||
|
|
||||||
|
f64::from_str(s)
|
||||||
|
.map(ConversionRateOverride::Explicit)
|
||||||
|
.map_err(|e| format!("Failed to parse '{:?}'. Expected 'metric' or explicit value", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl EstimateFee {
|
impl EstimateFee {
|
||||||
/// Run the command.
|
/// Run the command.
|
||||||
pub async fn run(self) -> anyhow::Result<()> {
|
pub async fn run(self) -> anyhow::Result<()> {
|
||||||
let Self { source, bridge, lane, payload } = self;
|
let Self { source, bridge, lane, conversion_rate_override, payload } = self;
|
||||||
|
|
||||||
select_full_bridge!(bridge, {
|
select_full_bridge!(bridge, {
|
||||||
let source_client = source.to_client::<Source>().await?;
|
let source_client = source.to_client::<Source>().await?;
|
||||||
@@ -52,8 +82,9 @@ impl EstimateFee {
|
|||||||
let payload =
|
let payload =
|
||||||
Source::encode_message(payload).map_err(|e| anyhow::format_err!("{:?}", e))?;
|
Source::encode_message(payload).map_err(|e| anyhow::format_err!("{:?}", e))?;
|
||||||
|
|
||||||
let fee: BalanceOf<Source> = estimate_message_delivery_and_dispatch_fee(
|
let fee = estimate_message_delivery_and_dispatch_fee::<Source, Target, _>(
|
||||||
&source_client,
|
&source_client,
|
||||||
|
conversion_rate_override,
|
||||||
ESTIMATE_MESSAGE_FEE_METHOD,
|
ESTIMATE_MESSAGE_FEE_METHOD,
|
||||||
lane,
|
lane,
|
||||||
payload,
|
payload,
|
||||||
@@ -67,13 +98,70 @@ impl EstimateFee {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn estimate_message_delivery_and_dispatch_fee<Fee: Decode, C: Chain, P: Encode>(
|
pub(crate) async fn estimate_message_delivery_and_dispatch_fee<
|
||||||
client: &relay_substrate_client::Client<C>,
|
Source: Chain,
|
||||||
|
Target: Chain,
|
||||||
|
P: Clone + Encode,
|
||||||
|
>(
|
||||||
|
client: &relay_substrate_client::Client<Source>,
|
||||||
|
conversion_rate_override: Option<ConversionRateOverride>,
|
||||||
estimate_fee_method: &str,
|
estimate_fee_method: &str,
|
||||||
lane: bp_messages::LaneId,
|
lane: bp_messages::LaneId,
|
||||||
payload: P,
|
payload: P,
|
||||||
) -> anyhow::Result<Fee> {
|
) -> anyhow::Result<BalanceOf<Source>> {
|
||||||
let conversion_rate_override: Option<FixedU128> = None;
|
// actual conversion rate CAN be lesser than the rate stored in the runtime. So we may try to
|
||||||
|
// pay lesser fee for the message delivery. But in this case, message may be rejected by the
|
||||||
|
// lane. So we MUST use the larger of two fees - one computed with stored fee and the one
|
||||||
|
// computed with actual fee.
|
||||||
|
|
||||||
|
let conversion_rate_override = match (
|
||||||
|
conversion_rate_override,
|
||||||
|
Source::TOKEN_ID,
|
||||||
|
Target::TOKEN_ID,
|
||||||
|
) {
|
||||||
|
(Some(ConversionRateOverride::Explicit(v)), _, _) => {
|
||||||
|
let conversion_rate_override = FixedU128::from_float(v);
|
||||||
|
log::info!(target: "bridge", "Conversion rate override: {:?} (explicit)", conversion_rate_override.to_float());
|
||||||
|
Some(conversion_rate_override)
|
||||||
|
},
|
||||||
|
(Some(ConversionRateOverride::Metric), Some(source_token_id), Some(target_token_id)) => {
|
||||||
|
let conversion_rate_override = FixedU128::from_float(
|
||||||
|
target_to_source_conversion_rate(source_token_id, target_token_id).await?,
|
||||||
|
);
|
||||||
|
log::info!(target: "bridge", "Conversion rate override: {:?} (from metric)", conversion_rate_override.to_float());
|
||||||
|
Some(conversion_rate_override)
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(std::cmp::max(
|
||||||
|
do_estimate_message_delivery_and_dispatch_fee(
|
||||||
|
client,
|
||||||
|
estimate_fee_method,
|
||||||
|
lane,
|
||||||
|
payload.clone(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
do_estimate_message_delivery_and_dispatch_fee(
|
||||||
|
client,
|
||||||
|
estimate_fee_method,
|
||||||
|
lane,
|
||||||
|
payload.clone(),
|
||||||
|
conversion_rate_override,
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Estimate message delivery and dispatch fee with given conversion rate override.
|
||||||
|
async fn do_estimate_message_delivery_and_dispatch_fee<Source: Chain, P: Encode>(
|
||||||
|
client: &relay_substrate_client::Client<Source>,
|
||||||
|
estimate_fee_method: &str,
|
||||||
|
lane: bp_messages::LaneId,
|
||||||
|
payload: P,
|
||||||
|
conversion_rate_override: Option<FixedU128>,
|
||||||
|
) -> anyhow::Result<BalanceOf<Source>> {
|
||||||
let encoded_response = client
|
let encoded_response = client
|
||||||
.state_call(
|
.state_call(
|
||||||
estimate_fee_method.into(),
|
estimate_fee_method.into(),
|
||||||
@@ -81,7 +169,7 @@ pub(crate) async fn estimate_message_delivery_and_dispatch_fee<Fee: Decode, C: C
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let decoded_response: Option<Fee> = Decode::decode(&mut &encoded_response.0[..])
|
let decoded_response: Option<BalanceOf<Source>> = Decode::decode(&mut &encoded_response.0[..])
|
||||||
.map_err(relay_substrate_client::Error::ResponseParseFailed)?;
|
.map_err(relay_substrate_client::Error::ResponseParseFailed)?;
|
||||||
let fee = decoded_response.ok_or_else(|| {
|
let fee = decoded_response.ok_or_else(|| {
|
||||||
anyhow::format_err!("Unable to decode fee from: {:?}", HexBytes(encoded_response.to_vec()))
|
anyhow::format_err!("Unable to decode fee from: {:?}", HexBytes(encoded_response.to_vec()))
|
||||||
@@ -106,6 +194,8 @@ mod tests {
|
|||||||
"rialto-to-millau",
|
"rialto-to-millau",
|
||||||
"--source-port",
|
"--source-port",
|
||||||
"1234",
|
"1234",
|
||||||
|
"--conversion-rate-override",
|
||||||
|
"42.5",
|
||||||
"call",
|
"call",
|
||||||
"--sender",
|
"--sender",
|
||||||
&alice,
|
&alice,
|
||||||
@@ -122,6 +212,7 @@ mod tests {
|
|||||||
EstimateFee {
|
EstimateFee {
|
||||||
bridge: FullBridge::RialtoToMillau,
|
bridge: FullBridge::RialtoToMillau,
|
||||||
lane: HexLaneId([0, 0, 0, 0]),
|
lane: HexLaneId([0, 0, 0, 0]),
|
||||||
|
conversion_rate_override: Some(ConversionRateOverride::Explicit(42.5)),
|
||||||
source: SourceConnectionParams {
|
source: SourceConnectionParams {
|
||||||
source_host: "127.0.0.1".into(),
|
source_host: "127.0.0.1".into(),
|
||||||
source_port: 1234,
|
source_port: 1234,
|
||||||
|
|||||||
@@ -17,12 +17,12 @@
|
|||||||
use crate::cli::{
|
use crate::cli::{
|
||||||
bridge::FullBridge,
|
bridge::FullBridge,
|
||||||
encode_call::{self, CliEncodeCall},
|
encode_call::{self, CliEncodeCall},
|
||||||
estimate_fee::estimate_message_delivery_and_dispatch_fee,
|
estimate_fee::{estimate_message_delivery_and_dispatch_fee, ConversionRateOverride},
|
||||||
Balance, ExplicitOrMaximal, HexBytes, HexLaneId, Origins, SourceConnectionParams,
|
Balance, ExplicitOrMaximal, HexBytes, HexLaneId, Origins, SourceConnectionParams,
|
||||||
SourceSigningParams, TargetConnectionParams, TargetSigningParams,
|
SourceSigningParams, TargetConnectionParams, TargetSigningParams,
|
||||||
};
|
};
|
||||||
use bp_message_dispatch::{CallOrigin, MessagePayload};
|
use bp_message_dispatch::{CallOrigin, MessagePayload};
|
||||||
use bp_runtime::{BalanceOf, Chain as _};
|
use bp_runtime::Chain as _;
|
||||||
use codec::Encode;
|
use codec::Encode;
|
||||||
use frame_support::weights::Weight;
|
use frame_support::weights::Weight;
|
||||||
use relay_substrate_client::{Chain, SignParam, TransactionSignScheme, UnsignedTransaction};
|
use relay_substrate_client::{Chain, SignParam, TransactionSignScheme, UnsignedTransaction};
|
||||||
@@ -66,6 +66,12 @@ pub struct SendMessage {
|
|||||||
/// Hex-encoded lane id. Defaults to `00000000`.
|
/// Hex-encoded lane id. Defaults to `00000000`.
|
||||||
#[structopt(long, default_value = "00000000")]
|
#[structopt(long, default_value = "00000000")]
|
||||||
lane: HexLaneId,
|
lane: HexLaneId,
|
||||||
|
/// A way to override conversion rate between bridge tokens.
|
||||||
|
///
|
||||||
|
/// If not specified, conversion rate from runtime storage is used. It may be obsolete and
|
||||||
|
/// your message won't be relayed.
|
||||||
|
#[structopt(long)]
|
||||||
|
conversion_rate_override: Option<ConversionRateOverride>,
|
||||||
/// Where dispatch fee is paid?
|
/// Where dispatch fee is paid?
|
||||||
#[structopt(
|
#[structopt(
|
||||||
long,
|
long,
|
||||||
@@ -169,11 +175,13 @@ impl SendMessage {
|
|||||||
let source_sign = self.source_sign.to_keypair::<Source>()?;
|
let source_sign = self.source_sign.to_keypair::<Source>()?;
|
||||||
|
|
||||||
let lane = self.lane.clone().into();
|
let lane = self.lane.clone().into();
|
||||||
|
let conversion_rate_override = self.conversion_rate_override;
|
||||||
let fee = match self.fee {
|
let fee = match self.fee {
|
||||||
Some(fee) => fee,
|
Some(fee) => fee,
|
||||||
None => Balance(
|
None => Balance(
|
||||||
estimate_message_delivery_and_dispatch_fee::<BalanceOf<Source>, _, _>(
|
estimate_message_delivery_and_dispatch_fee::<Source, Target, _>(
|
||||||
&source_client,
|
&source_client,
|
||||||
|
conversion_rate_override,
|
||||||
ESTIMATE_MESSAGE_FEE_METHOD,
|
ESTIMATE_MESSAGE_FEE_METHOD,
|
||||||
lane,
|
lane,
|
||||||
payload.clone(),
|
payload.clone(),
|
||||||
@@ -317,6 +325,8 @@ mod tests {
|
|||||||
"1234",
|
"1234",
|
||||||
"--source-signer",
|
"--source-signer",
|
||||||
"//Alice",
|
"//Alice",
|
||||||
|
"--conversion-rate-override",
|
||||||
|
"0.75",
|
||||||
"remark",
|
"remark",
|
||||||
"--remark-payload",
|
"--remark-payload",
|
||||||
"1234",
|
"1234",
|
||||||
@@ -354,6 +364,8 @@ mod tests {
|
|||||||
"Target",
|
"Target",
|
||||||
"--target-signer",
|
"--target-signer",
|
||||||
"//Bob",
|
"//Bob",
|
||||||
|
"--conversion-rate-override",
|
||||||
|
"metric",
|
||||||
"remark",
|
"remark",
|
||||||
"--remark-payload",
|
"--remark-payload",
|
||||||
"1234",
|
"1234",
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ use sp_core::{blake2_256, storage::StorageKey, Bytes, Pair, U256};
|
|||||||
use sp_runtime::traits::{Convert, Header as HeaderT};
|
use sp_runtime::traits::{Convert, Header as HeaderT};
|
||||||
|
|
||||||
use crate::cli::{
|
use crate::cli::{
|
||||||
Balance, CliChain, SourceConnectionParams, SourceSigningParams, TargetConnectionParams,
|
estimate_fee::ConversionRateOverride, Balance, CliChain, SourceConnectionParams,
|
||||||
TargetSigningParams,
|
SourceSigningParams, TargetConnectionParams, TargetSigningParams,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Swap tokens.
|
/// Swap tokens.
|
||||||
@@ -65,6 +65,12 @@ pub struct SwapTokens {
|
|||||||
/// Target chain balance that target signer wants to swap.
|
/// Target chain balance that target signer wants to swap.
|
||||||
#[structopt(long)]
|
#[structopt(long)]
|
||||||
target_balance: Balance,
|
target_balance: Balance,
|
||||||
|
/// A way to override conversion rate between bridge tokens.
|
||||||
|
///
|
||||||
|
/// If not specified, conversion rate from runtime storage is used. It may be obsolete and
|
||||||
|
/// your message won't be relayed.
|
||||||
|
#[structopt(long)]
|
||||||
|
conversion_rate_override: Option<ConversionRateOverride>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Token swap type.
|
/// Token swap type.
|
||||||
@@ -133,6 +139,7 @@ impl SwapTokens {
|
|||||||
let source_sign = self.source_sign.to_keypair::<Target>()?;
|
let source_sign = self.source_sign.to_keypair::<Target>()?;
|
||||||
let target_client = self.target.to_client::<Target>().await?;
|
let target_client = self.target.to_client::<Target>().await?;
|
||||||
let target_sign = self.target_sign.to_keypair::<Target>()?;
|
let target_sign = self.target_sign.to_keypair::<Target>()?;
|
||||||
|
let conversion_rate_override = self.conversion_rate_override;
|
||||||
|
|
||||||
// names of variables in this function are matching names used by the
|
// names of variables in this function are matching names used by the
|
||||||
// `pallet-bridge-token-swap`
|
// `pallet-bridge-token-swap`
|
||||||
@@ -198,9 +205,14 @@ impl SwapTokens {
|
|||||||
// prepare `create_swap` call
|
// prepare `create_swap` call
|
||||||
let target_public_at_bridged_chain: AccountPublicOf<Target> =
|
let target_public_at_bridged_chain: AccountPublicOf<Target> =
|
||||||
target_sign.public().into();
|
target_sign.public().into();
|
||||||
let swap_delivery_and_dispatch_fee: BalanceOf<Source> =
|
let swap_delivery_and_dispatch_fee =
|
||||||
crate::cli::estimate_fee::estimate_message_delivery_and_dispatch_fee(
|
crate::cli::estimate_fee::estimate_message_delivery_and_dispatch_fee::<
|
||||||
|
Source,
|
||||||
|
Target,
|
||||||
|
_,
|
||||||
|
>(
|
||||||
&source_client,
|
&source_client,
|
||||||
|
conversion_rate_override.clone(),
|
||||||
ESTIMATE_SOURCE_TO_TARGET_MESSAGE_FEE_METHOD,
|
ESTIMATE_SOURCE_TO_TARGET_MESSAGE_FEE_METHOD,
|
||||||
SOURCE_TO_TARGET_LANE_ID,
|
SOURCE_TO_TARGET_LANE_ID,
|
||||||
bp_message_dispatch::MessagePayload {
|
bp_message_dispatch::MessagePayload {
|
||||||
@@ -356,9 +368,14 @@ impl SwapTokens {
|
|||||||
dispatch_fee_payment: bp_runtime::messages::DispatchFeePayment::AtSourceChain,
|
dispatch_fee_payment: bp_runtime::messages::DispatchFeePayment::AtSourceChain,
|
||||||
call: claim_swap_call.encode(),
|
call: claim_swap_call.encode(),
|
||||||
};
|
};
|
||||||
let claim_swap_delivery_and_dispatch_fee: BalanceOf<Target> =
|
let claim_swap_delivery_and_dispatch_fee =
|
||||||
crate::cli::estimate_fee::estimate_message_delivery_and_dispatch_fee(
|
crate::cli::estimate_fee::estimate_message_delivery_and_dispatch_fee::<
|
||||||
|
Target,
|
||||||
|
Source,
|
||||||
|
_,
|
||||||
|
>(
|
||||||
&target_client,
|
&target_client,
|
||||||
|
conversion_rate_override.clone(),
|
||||||
ESTIMATE_TARGET_TO_SOURCE_MESSAGE_FEE_METHOD,
|
ESTIMATE_TARGET_TO_SOURCE_MESSAGE_FEE_METHOD,
|
||||||
TARGET_TO_SOURCE_LANE_ID,
|
TARGET_TO_SOURCE_LANE_ID,
|
||||||
claim_swap_message.clone(),
|
claim_swap_message.clone(),
|
||||||
@@ -753,6 +770,7 @@ mod tests {
|
|||||||
swap_type: TokenSwapType::NoLock,
|
swap_type: TokenSwapType::NoLock,
|
||||||
source_balance: Balance(8000000000),
|
source_balance: Balance(8000000000),
|
||||||
target_balance: Balance(9000000000),
|
target_balance: Balance(9000000000),
|
||||||
|
conversion_rate_override: None,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -778,6 +796,8 @@ mod tests {
|
|||||||
"//Bob",
|
"//Bob",
|
||||||
"--target-balance",
|
"--target-balance",
|
||||||
"9000000000",
|
"9000000000",
|
||||||
|
"--conversion-rate-override",
|
||||||
|
"metric",
|
||||||
"lock-until-block",
|
"lock-until-block",
|
||||||
"--blocks-before-expire",
|
"--blocks-before-expire",
|
||||||
"1",
|
"1",
|
||||||
@@ -827,6 +847,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
source_balance: Balance(8000000000),
|
source_balance: Balance(8000000000),
|
||||||
target_balance: Balance(9000000000),
|
target_balance: Balance(9000000000),
|
||||||
|
conversion_rate_override: Some(ConversionRateOverride::Metric),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
//! Substrate relay helpers
|
//! Substrate relay helpers
|
||||||
|
|
||||||
use relay_utils::metrics::{FloatJsonValueMetric, PrometheusError};
|
use relay_utils::metrics::{FloatJsonValueMetric, PrometheusError, StandaloneMetric};
|
||||||
|
|
||||||
/// Creates standalone token price metric.
|
/// Creates standalone token price metric.
|
||||||
pub fn token_price_metric(token_id: &str) -> Result<FloatJsonValueMetric, PrometheusError> {
|
pub fn token_price_metric(token_id: &str) -> Result<FloatJsonValueMetric, PrometheusError> {
|
||||||
@@ -27,3 +27,28 @@ pub fn token_price_metric(token_id: &str) -> Result<FloatJsonValueMetric, Promet
|
|||||||
format!("Rate used to convert from {} to some BASE tokens", token_id.to_uppercase()),
|
format!("Rate used to convert from {} to some BASE tokens", token_id.to_uppercase()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compute conversion rate between two tokens immediately, without spawning any metrics.
|
||||||
|
pub async fn target_to_source_conversion_rate(
|
||||||
|
source_token_id: &str,
|
||||||
|
target_token_id: &str,
|
||||||
|
) -> anyhow::Result<f64> {
|
||||||
|
let source_token_metric = token_price_metric(source_token_id)?;
|
||||||
|
source_token_metric.update().await;
|
||||||
|
let target_token_metric = token_price_metric(target_token_id)?;
|
||||||
|
target_token_metric.update().await;
|
||||||
|
|
||||||
|
let source_token_value = *source_token_metric.shared_value_ref().read().await;
|
||||||
|
let target_token_value = *target_token_metric.shared_value_ref().read().await;
|
||||||
|
// `FloatJsonValueMetric` guarantees that the value is positive && normal, so no additional
|
||||||
|
// checks required here
|
||||||
|
match (source_token_value, target_token_value) {
|
||||||
|
(Some(source_token_value), Some(target_token_value)) =>
|
||||||
|
Ok(target_token_value / source_token_value),
|
||||||
|
_ => Err(anyhow::format_err!(
|
||||||
|
"Failed to compute conversion rate from {} to {}",
|
||||||
|
target_token_id,
|
||||||
|
source_token_id,
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user