Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Partial fee estimates for SubmittableExtrinsic #910

Merged
merged 13 commits into from
Apr 17, 2023
9 changes: 4 additions & 5 deletions subxt/src/client/online_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use crate::{
tx::TxClient,
Config, Metadata,
};
use codec::{Compact, Decode};
use codec::Compact;
use derivative::Derivative;
use frame_metadata::RuntimeMetadataPrefixed;
use futures::future;
Expand Down Expand Up @@ -136,10 +136,9 @@ impl<T: Config> OnlineClient<T> {

/// Fetch the metadata from substrate using the runtime API.
async fn fetch_metadata(rpc: &Rpc<T>) -> Result<Metadata, Error> {
let bytes = rpc.state_call("Metadata_metadata", None, None).await?;
let cursor = &mut &*bytes;
let _ = <Compact<u32>>::decode(cursor)?;
let meta: RuntimeMetadataPrefixed = Decode::decode(cursor)?;
let (_, meta) = rpc
.state_call::<(Compact<u32>, RuntimeMetadataPrefixed)>("Metadata_metadata", None, None)
.await?;
jsdw marked this conversation as resolved.
Show resolved Hide resolved
Ok(meta.try_into()?)
}

Expand Down
41 changes: 13 additions & 28 deletions subxt/src/rpc/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,19 @@
//! # }
//! ```

use std::sync::Arc;

use codec::{Decode, Encode};
use frame_metadata::RuntimeMetadataPrefixed;
use serde::Serialize;

use crate::{error::Error, utils::PhantomDataSendSync, Config, Metadata};
niklasad1 marked this conversation as resolved.
Show resolved Hide resolved

use super::{
rpc_params,
types::{self, ChainHeadEvent, FollowEvent},
RpcClient, RpcClientT, Subscription,
};
use crate::{error::Error, utils::PhantomDataSendSync, Config, Metadata};
use codec::{Decode, Encode};
use frame_metadata::RuntimeMetadataPrefixed;
use serde::Serialize;
use std::sync::Arc;

/// Client for substrate rpc interfaces
pub struct Rpc<T: Config> {
Expand Down Expand Up @@ -151,25 +154,6 @@ impl<T: Config> Rpc<T> {
Ok(metadata)
}

/// Execute a runtime API call.
pub async fn call(
&self,
function: String,
call_parameters: Option<&[u8]>,
at: Option<T::Hash>,
) -> Result<types::Bytes, Error> {
let call_parameters = call_parameters.unwrap_or_default();

let bytes: types::Bytes = self
.client
.request(
"state_call",
rpc_params![function, to_hex(call_parameters), at],
)
.await?;
Ok(bytes)
}

/// Fetch system properties
pub async fn system_properties(&self) -> Result<types::SystemProperties, Error> {
self.client
Expand Down Expand Up @@ -364,22 +348,23 @@ impl<T: Config> Rpc<T> {
}

/// Execute a runtime API call.
pub async fn state_call(
pub async fn state_call<Res: Decode>(
&self,
function: &str,
call_parameters: Option<&[u8]>,
at: Option<T::Hash>,
) -> Result<types::Bytes, Error> {
) -> Result<Res, Error> {
let call_parameters = call_parameters.unwrap_or_default();

let bytes: types::Bytes = self
.client
.request(
"state_call",
rpc_params![function, to_hex(call_parameters), at],
)
.await?;
Ok(bytes)
let cursor = &mut &bytes[..];
let res: Res = Decode::decode(cursor)?;
Ok(res)
}

/// Create and submit an extrinsic and return a subscription to the events triggered.
Expand Down
9 changes: 5 additions & 4 deletions subxt/src/runtime_api/runtime_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// see LICENSE for license details.

use crate::{client::OnlineClientT, error::Error, Config};
use codec::Decode;
use derivative::Derivative;
use std::{future::Future, marker::PhantomData};

Expand Down Expand Up @@ -32,21 +33,21 @@ where
Client: OnlineClientT<T>,
{
/// Execute a raw runtime API call.
pub fn call_raw<'a>(
pub fn call_raw<'a, Res: Decode>(
&self,
function: &'a str,
call_parameters: Option<&'a [u8]>,
) -> impl Future<Output = Result<Vec<u8>, Error>> + 'a {
) -> impl Future<Output = Result<Res, Error>> + 'a {
let client = self.client.clone();
let block_hash = self.block_hash;
// Ensure that the returned future doesn't have a lifetime tied to api.runtime_api(),
// which is a temporary thing we'll be throwing away quickly:
async move {
let data = client
let data: Res = client
.rpc()
.state_call(function, call_parameters, Some(block_hash))
.await?;
Ok(data.0)
Ok(data)
}
}
}
30 changes: 25 additions & 5 deletions subxt/src/tx/tx_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,18 @@
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.

use super::TxPayload;
use std::borrow::Cow;

use codec::{Compact, Encode};
use derivative::Derivative;

use crate::{
client::{OfflineClientT, OnlineClientT},
config::{Config, ExtrinsicParams, Hasher},
error::Error,
tx::{Signer as SignerT, TxProgress},
tx::{Signer as SignerT, TxPayload, TxProgress},
utils::{Encoded, PhantomDataSendSync},
};
use codec::{Compact, Encode};
use derivative::Derivative;
use std::borrow::Cow;

// This is returned from an API below, so expose it here.
pub use crate::rpc::types::DryRunResult;
Expand Down Expand Up @@ -465,4 +466,23 @@ where
let dry_run_bytes = self.client.rpc().dry_run(self.encoded(), at).await?;
dry_run_bytes.into_dry_run_result(&self.client.metadata())
}

/// This returns an estimate for what the extrinsic is expected to cost to execute, less any tips.
/// The actual amount paid can vary from block to block based on node traffic and other factors.
pub async fn partial_fee_estimate(&self) -> Result<u128, Error> {
jsdw marked this conversation as resolved.
Show resolved Hide resolved
let mut params = self.encoded().to_vec();
(self.encoded().len() as u32).encode_to(&mut params);
niklasad1 marked this conversation as resolved.
Show resolved Hide resolved
// destructuring RuntimeDispatchInfo, see type information <https://paritytech.github.io/substrate/master/pallet_transaction_payment_rpc_runtime_api/struct.RuntimeDispatchInfo.html>
// data layout: {weight_ref_time: Compact<u64>, weight_proof_size: Compact<u64>, class: u8, partial_fee: u128}
Comment on lines +475 to +476
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice; more compact code with nice comment to explain :)

let (_, _, _, partial_fee) = self
.client
.rpc()
.state_call::<(Compact<u64>, Compact<u64>, u8, u128)>(
"TransactionPaymentApi_query_info",
Some(&params),
None,
)
.await?;
Ok(partial_fee)
}
}
29 changes: 13 additions & 16 deletions subxt/src/tx/tx_progress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -416,11 +416,7 @@ mod test {

use crate::{
client::{OfflineClientT, OnlineClientT},
config::{
extrinsic_params::BaseExtrinsicParams,
polkadot::{PlainTip, PolkadotConfig},
WithExtrinsicParams,
},
config::{extrinsic_params::BaseExtrinsicParams, polkadot::PlainTip, WithExtrinsicParams},
error::RpcError,
rpc::{types::SubstrateTxStatus, RpcSubscription, Subscription},
tx::TxProgress,
Expand All @@ -429,15 +425,23 @@ mod test {

use serde_json::value::RawValue;

type MockTxProgress = TxProgress<SubstrateConfig, MockClient>;
type MockHash = <WithExtrinsicParams<
SubstrateConfig,
BaseExtrinsicParams<SubstrateConfig, PlainTip>,
> as Config>::Hash;
type MockSubstrateTxStatus = SubstrateTxStatus<MockHash, MockHash>;

/// a mock client to satisfy trait bounds in tests
#[derive(Clone, Debug)]
struct MockClient;

impl OfflineClientT<PolkadotConfig> for MockClient {
impl OfflineClientT<SubstrateConfig> for MockClient {
fn metadata(&self) -> crate::Metadata {
panic!("just a mock impl to satisfy trait bounds")
}

fn genesis_hash(&self) -> <PolkadotConfig as crate::Config>::Hash {
fn genesis_hash(&self) -> <SubstrateConfig as crate::Config>::Hash {
panic!("just a mock impl to satisfy trait bounds")
}

Expand All @@ -446,15 +450,8 @@ mod test {
}
}

type MockTxProgress = TxProgress<PolkadotConfig, MockClient>;
type MockHash = <WithExtrinsicParams<
SubstrateConfig,
BaseExtrinsicParams<SubstrateConfig, PlainTip>,
> as Config>::Hash;
type MockSubstrateTxStatus = SubstrateTxStatus<MockHash, MockHash>;

impl OnlineClientT<PolkadotConfig> for MockClient {
fn rpc(&self) -> &crate::rpc::Rpc<PolkadotConfig> {
impl OnlineClientT<SubstrateConfig> for MockClient {
fn rpc(&self) -> &crate::rpc::Rpc<SubstrateConfig> {
panic!("just a mock impl to satisfy trait bounds")
}
}
Expand Down
9 changes: 4 additions & 5 deletions testing/integration-tests/src/blocks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// see LICENSE for license details.

use crate::test_context;
use codec::{Compact, Decode};
use codec::Compact;
use frame_metadata::RuntimeMetadataPrefixed;
use futures::StreamExt;

Expand Down Expand Up @@ -101,10 +101,9 @@ async fn runtime_api_call() -> Result<(), subxt::Error> {
let block = sub.next().await.unwrap()?;
let rt = block.runtime_api().await?;

let bytes = rt.call_raw("Metadata_metadata", None).await?;
let cursor = &mut &*bytes;
let _ = <Compact<u32>>::decode(cursor)?;
let meta: RuntimeMetadataPrefixed = Decode::decode(cursor)?;
let (_, meta) = rt
.call_raw::<(Compact<u32>, RuntimeMetadataPrefixed)>("Metadata_metadata", None)
.await?;
let metadata_call = match meta.1 {
frame_metadata::RuntimeMetadata::V14(metadata) => metadata,
_ => panic!("Metadata V14 unavailable"),
Expand Down
80 changes: 74 additions & 6 deletions testing/integration-tests/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -388,15 +388,11 @@ async fn rpc_state_call() {
let api = ctx.client();

// Call into the runtime of the chain to get the Metadata.
let metadata_bytes = api
let (_, meta) = api
.rpc()
.state_call("Metadata_metadata", None, None)
.state_call::<(Compact<u32>, RuntimeMetadataPrefixed)>("Metadata_metadata", None, None)
.await
.unwrap();

let cursor = &mut &*metadata_bytes;
let _ = <Compact<u32>>::decode(cursor).unwrap();
let meta: RuntimeMetadataPrefixed = Decode::decode(cursor).unwrap();
let metadata_call = match meta.1 {
frame_metadata::RuntimeMetadata::V14(metadata) => metadata,
_ => panic!("Metadata V14 unavailable"),
Expand Down Expand Up @@ -582,3 +578,75 @@ async fn chainhead_unstable_unpin() {
.await
.is_err());
}

/// taken from original type <https://docs.rs/pallet-transaction-payment/latest/pallet_transaction_payment/struct.FeeDetails.html>
#[derive(Encode, Decode, Debug, Clone, Eq, PartialEq)]
pub struct FeeDetails {
/// The minimum fee for a transaction to be included in a block.
pub inclusion_fee: Option<InclusionFee>,
/// tip
pub tip: u128,
}

/// taken from original type <https://docs.rs/pallet-transaction-payment/latest/pallet_transaction_payment/struct.InclusionFee.html>
/// The base fee and adjusted weight and length fees constitute the _inclusion fee_.
#[derive(Encode, Decode, Debug, Clone, Eq, PartialEq)]
pub struct InclusionFee {
/// minimum amount a user pays for a transaction.
pub base_fee: u128,
/// amount paid for the encoded length (in bytes) of the transaction.
pub len_fee: u128,
///
/// - `targeted_fee_adjustment`: This is a multiplier that can tune the final fee based on the
/// congestion of the network.
/// - `weight_fee`: This amount is computed based on the weight of the transaction. Weight
/// accounts for the execution time of a transaction.
///
/// adjusted_weight_fee = targeted_fee_adjustment * weight_fee
pub adjusted_weight_fee: u128,
}

#[tokio::test]
async fn partial_fee_estimate_correct() {
let ctx = test_context().await;
let api = ctx.client();

let alice = pair_signer(AccountKeyring::Alice.pair());
let hans = pair_signer(Sr25519Pair::generate().0);

let tx = node_runtime::tx()
.balances()
.transfer(hans.account_id().clone().into(), 1_000_000_000_000);

let signed_extrinsic = api
.tx()
.create_signed(&tx, &alice, Default::default())
.await
.unwrap();

// Method I: TransactionPaymentApi_query_info
let partial_fee_1 = signed_extrinsic.partial_fee_estimate().await.unwrap();

// Method II: TransactionPaymentApi_query_fee_details + calculations
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it's sufficient to just rely on that the result from Method I: TransactionPaymentApi_query_info could be decoded as this should already be tested by substrate I reckon.

Then remove this extra code 👇, but this is a nice way to check the result so I have mixed feelings here your call

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy either way too; I don't think we need the test but it's interesting in documenting how the things work so I'm easy :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the logic changes in the future the test will break, then we can still change/remove it I think :)

let len_bytes: [u8; 4] = (signed_extrinsic.encoded().len() as u32).to_le_bytes();
let encoded_with_len = [signed_extrinsic.encoded(), &len_bytes[..]].concat();
let InclusionFee {
base_fee,
len_fee,
adjusted_weight_fee,
} = api
.rpc()
.state_call::<FeeDetails>(
"TransactionPaymentApi_query_fee_details",
Some(&encoded_with_len),
None,
)
.await
.unwrap()
.inclusion_fee
.unwrap();
let partial_fee_2 = base_fee + len_fee + adjusted_weight_fee;

// Both methods should yield the same fee
assert_eq!(partial_fee_1, partial_fee_2);
}