4c8f281051
- Remove nightly-only features from .rustfmt.toml and vendor/ss58-registry/rustfmt.toml - Removed features: imports_granularity, wrap_comments, comment_width, reorder_impl_items, spaces_around_ranges, binop_separator, match_arm_blocks, trailing_semicolon, trailing_comma - Format all 898 affected files with stable rustfmt - Ensures long-term reliability without nightly toolchain dependency
767 lines
23 KiB
Rust
767 lines
23 KiB
Rust
use crate::{
|
|
block_monitor::BlockMonitor,
|
|
error::Error,
|
|
helpers::StreamOf,
|
|
scenario::AccountsDescription,
|
|
transaction::{
|
|
AccountMetadata, Transaction, TransactionMonitor, TransactionStatus, TransactionsSink,
|
|
},
|
|
};
|
|
use async_trait::async_trait;
|
|
use futures::StreamExt;
|
|
use parking_lot::RwLock;
|
|
pub use pezkuwi_subxt::dynamic;
|
|
use pezkuwi_subxt::{
|
|
backend::rpc::RpcClient,
|
|
config::{
|
|
transaction_extensions::{
|
|
ChargeAssetTxPaymentParams, ChargeTransactionPaymentParams, CheckMortalityParams,
|
|
CheckNonceParams,
|
|
},
|
|
DefaultExtrinsicParams, ExtrinsicParams,
|
|
},
|
|
dynamic::{At, Value},
|
|
ext::scale_value::value,
|
|
tx::{DynamicPayload, PartialTransaction, Signer, SubmittableTransaction},
|
|
OnlineClient, PezkuwiConfig,
|
|
};
|
|
use pezkuwi_subxt_core::{config::DefaultExtrinsicParamsBuilder, utils::AccountId20};
|
|
use pezkuwi_subxt_signer::{
|
|
eth::{dev as eth_dev, Keypair as EthKeypair, Signature},
|
|
sr25519::{dev as sr25519_dev, Keypair as SrPair},
|
|
};
|
|
use std::{
|
|
any::Any,
|
|
collections::HashMap,
|
|
sync::Arc,
|
|
time::{Duration, Instant},
|
|
};
|
|
use tracing::{debug, error, trace};
|
|
|
|
const LOG_TARGET: &str = "subxt_tx";
|
|
const DEFAULT_RETRIES_FOR_PARTIAL_TX_CREATION: usize = 10;
|
|
|
|
#[derive(Clone)]
|
|
/// Ethereum runtime config definition for subxt usage purposes.
|
|
pub enum EthRuntimeConfig {}
|
|
impl pezkuwi_subxt::Config for EthRuntimeConfig {
|
|
type AccountId = AccountId20;
|
|
type Address = AccountId20;
|
|
type Signature = Signature;
|
|
type Hasher = pezkuwi_subxt::config::bizinikiwi::BlakeTwo256;
|
|
type Header = pezkuwi_subxt::config::bizinikiwi::BizinikiwiHeader<
|
|
u32,
|
|
pezkuwi_subxt::config::bizinikiwi::BlakeTwo256,
|
|
>;
|
|
type ExtrinsicParams = pezkuwi_subxt::config::DefaultExtrinsicParams<Self>;
|
|
type AssetId = u32;
|
|
}
|
|
|
|
/// Type alias for subxt config hash (Output of Hasher).
|
|
pub(crate) type HashOf<C> =
|
|
<<C as pezkuwi_subxt::Config>::Hasher as pezkuwi_subxt::config::Hasher>::Output;
|
|
/// Type alias for subxt account id.
|
|
pub(crate) type AccountIdOf<C> = <C as pezkuwi_subxt::Config>::AccountId;
|
|
|
|
/// A subxt transaction abstraction.
|
|
#[derive(Clone)]
|
|
pub struct SubxtTransaction<C: pezkuwi_subxt::Config> {
|
|
transaction: Arc<SubmittableTransaction<C, OnlineClient<C>>>,
|
|
nonce: u128,
|
|
valid_until: Option<u64>,
|
|
account_metadata: AccountMetadata,
|
|
}
|
|
|
|
/// Transaction type thart runs on `Ethereum` compatible chains.
|
|
pub type EthTransaction = SubxtTransaction<EthRuntimeConfig>;
|
|
/// Holds the RPC API connection for transaction execution.
|
|
pub type EthTransactionsSink = SubxtTransactionsSink<EthRuntimeConfig, EthKeypair>;
|
|
/// Transaction type that runs on `bizinikiwi` compatible chains.
|
|
pub type BizinikiwTransaction = SubxtTransaction<PezkuwiConfig>;
|
|
/// Holds the RPC API connection for transaction execution.
|
|
pub type BizinikiwTransactionsSink = SubxtTransactionsSink<PezkuwiConfig, SrPair>;
|
|
|
|
/// Context for building transaction payloads.
|
|
/// Generic over account type `A` to support both Substrate and Ethereum chains.
|
|
pub struct TxPayloadBuildContext<'a, A> {
|
|
/// The destination account ID.
|
|
pub to_account_id: &'a A,
|
|
/// The source account ID (signer).
|
|
pub from_account_id: &'a A,
|
|
/// Account string identifier (e.g. "0", "alice").
|
|
pub account: &'a str,
|
|
/// Transaction nonce.
|
|
pub nonce: u128,
|
|
}
|
|
|
|
/// Context type alias for Bizinikiwi chains.
|
|
pub type SubTxBuildContext<'a> = TxPayloadBuildContext<'a, AccountIdOf<PezkuwiConfig>>;
|
|
/// Context type alias for Ethereum chains.
|
|
pub type EthTxBuildContext<'a> = TxPayloadBuildContext<'a, AccountId20>;
|
|
|
|
/// Generic payload builder function type.
|
|
pub type PayloadBuilderFn<A> =
|
|
Arc<dyn Fn(&TxPayloadBuildContext<A>) -> DynamicPayload + Send + Sync>;
|
|
/// Payload builder type alias for Bizinikiwi chains.
|
|
pub type SubPayloadBuilderFn = PayloadBuilderFn<AccountIdOf<PezkuwiConfig>>;
|
|
/// Payload builder type alias for Ethereum chains.
|
|
pub type EthPayloadBuilderFn = PayloadBuilderFn<AccountId20>;
|
|
|
|
/// Creates a generic remark payload builder.
|
|
/// Works for any account type that implements `AsRef<[u8]>`.
|
|
/// Size is specified in kilobytes.
|
|
pub fn remark_payload_builder<A>(size_kb: u32) -> PayloadBuilderFn<A>
|
|
where
|
|
A: AsRef<[u8]> + Send + Sync + 'static,
|
|
{
|
|
Arc::new(move |ctx| {
|
|
let i = hex::encode(ctx.to_account_id.as_ref()).as_bytes().last().copied().unwrap();
|
|
let data = vec![i; size_kb as usize * 1024];
|
|
pezkuwi_subxt::dynamic::tx("System", "remark", vec![data])
|
|
})
|
|
}
|
|
|
|
/// Creates a transfer payload builder for Substrate chains.
|
|
pub fn sub_transfer_payload_builder() -> SubPayloadBuilderFn {
|
|
Arc::new(|ctx| {
|
|
pezkuwi_subxt::dynamic::tx(
|
|
"Balances",
|
|
"transfer_keep_alive",
|
|
vec![
|
|
value!(Id(Value::from_bytes(ctx.to_account_id.clone()))),
|
|
Value::u128(1u32.into()),
|
|
],
|
|
)
|
|
})
|
|
}
|
|
|
|
/// Creates a transfer payload builder for Ethereum chains.
|
|
pub fn eth_transfer_payload_builder() -> EthPayloadBuilderFn {
|
|
Arc::new(|ctx| {
|
|
pezkuwi_subxt::dynamic::tx(
|
|
"Balances",
|
|
"transfer_keep_alive",
|
|
vec![
|
|
Value::unnamed_composite(vec![Value::from_bytes(*ctx.to_account_id)]),
|
|
Value::u128(1u32.into()),
|
|
],
|
|
)
|
|
})
|
|
}
|
|
|
|
impl<C: pezkuwi_subxt::Config> SubxtTransaction<C> {
|
|
pub fn new(
|
|
transaction: SubmittableTransaction<C, OnlineClient<C>>,
|
|
nonce: u128,
|
|
valid_until: Option<u64>,
|
|
account_metadata: AccountMetadata,
|
|
) -> Self {
|
|
Self { transaction: Arc::new(transaction), nonce, account_metadata, valid_until }
|
|
}
|
|
}
|
|
|
|
// type TransactionSubxt2 = pezkuwi_subxt::tx::DynamicPayload;
|
|
|
|
impl<C: pezkuwi_subxt::Config> Transaction for SubxtTransaction<C> {
|
|
type HashType = HashOf<C>;
|
|
fn hash(&self) -> Self::HashType {
|
|
self.transaction.hash()
|
|
}
|
|
fn as_any(&self) -> &dyn Any {
|
|
self
|
|
}
|
|
fn nonce(&self) -> u128 {
|
|
self.nonce
|
|
}
|
|
fn account_metadata(&self) -> AccountMetadata {
|
|
self.account_metadata.clone()
|
|
}
|
|
fn valid_until(&self) -> &Option<u64> {
|
|
&self.valid_until
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct SubxtTransactionsSink<C: pezkuwi_subxt::Config, KP: Signer<C>> {
|
|
api: OnlineClient<C>,
|
|
from_accounts: Arc<RwLock<HashMap<String, (KP, AccountMetadata)>>>,
|
|
to_accounts: Arc<RwLock<HashMap<String, (KP, AccountMetadata)>>>,
|
|
nonces: Arc<RwLock<HashMap<String, u128>>>,
|
|
rpc_client: RpcClient,
|
|
current_pending_extrinsics: Arc<RwLock<Option<(Instant, usize)>>>,
|
|
block_monitor: Option<BlockMonitor<C>>,
|
|
}
|
|
|
|
const EXPECT_CONNECT: &str = "should connect to rpc client";
|
|
|
|
impl<C, KP> SubxtTransactionsSink<C, KP>
|
|
where
|
|
AccountIdOf<C>: Send + Sync + AsRef<[u8]>,
|
|
KP: Signer<C> + Clone + Send + Sync + 'static,
|
|
C: pezkuwi_subxt::Config,
|
|
{
|
|
pub async fn new() -> Self {
|
|
Self {
|
|
api: crate::subxt_api_connector::connect("ws://127.0.0.1:9933", false)
|
|
.await
|
|
.expect(EXPECT_CONNECT),
|
|
from_accounts: Default::default(),
|
|
to_accounts: Default::default(),
|
|
nonces: Default::default(),
|
|
rpc_client: RpcClient::from_url("ws://127.0.0.1:9933").await.expect(EXPECT_CONNECT),
|
|
current_pending_extrinsics: Arc::new(None.into()),
|
|
block_monitor: None,
|
|
}
|
|
}
|
|
|
|
pub async fn new_with_uri(uri: &String) -> Self {
|
|
Self {
|
|
api: crate::subxt_api_connector::connect(uri, false).await.expect(EXPECT_CONNECT),
|
|
from_accounts: Default::default(),
|
|
to_accounts: Default::default(),
|
|
nonces: Default::default(),
|
|
rpc_client: RpcClient::from_url(uri).await.expect(EXPECT_CONNECT),
|
|
current_pending_extrinsics: Arc::new(None.into()),
|
|
block_monitor: None,
|
|
}
|
|
}
|
|
|
|
pub async fn new_with_uri_with_accounts_description<G>(
|
|
uri: &str,
|
|
accounts_description: AccountsDescription,
|
|
generate_pair: G,
|
|
block_monitor: Option<BlockMonitor<C>>,
|
|
use_legacy_backend: bool,
|
|
) -> Self
|
|
where
|
|
G: GenerateKeyPairFunction<KP>,
|
|
{
|
|
let from_accounts =
|
|
derive_accounts(accounts_description.clone(), SENDER_SEED, generate_pair);
|
|
let to_accounts = derive_accounts(accounts_description, RECEIVER_SEED, generate_pair);
|
|
Self {
|
|
api: crate::subxt_api_connector::connect(uri, use_legacy_backend)
|
|
.await
|
|
.expect(EXPECT_CONNECT),
|
|
from_accounts: Arc::from(RwLock::from(from_accounts)),
|
|
to_accounts: Arc::from(RwLock::from(to_accounts)),
|
|
nonces: Default::default(),
|
|
rpc_client: crate::helpers::client(uri).await.expect(EXPECT_CONNECT).into(),
|
|
current_pending_extrinsics: Arc::new(None.into()),
|
|
block_monitor,
|
|
}
|
|
}
|
|
|
|
fn api(&self) -> OnlineClient<C> {
|
|
self.api.clone()
|
|
}
|
|
|
|
pub fn get_from_account_id(&self, account: &str) -> Option<AccountIdOf<C>> {
|
|
self.from_accounts.read().get(account).map(|a| a.0.account_id())
|
|
}
|
|
|
|
fn get_to_account_id(&self, account: &str) -> Option<AccountIdOf<C>> {
|
|
self.to_accounts.read().get(account).map(|a| a.0.account_id())
|
|
}
|
|
|
|
fn get_to_account_metadata(&self, account: &str) -> Option<AccountMetadata> {
|
|
self.to_accounts.read().get(account).map(|a| a.1.clone())
|
|
}
|
|
|
|
fn get_from_key_pair(&self, account: &str) -> Option<KP> {
|
|
self.from_accounts.read().get(account).map(|k| k.0.clone())
|
|
}
|
|
|
|
pub async fn check_account_nonce(
|
|
&self,
|
|
account: AccountIdOf<C>,
|
|
) -> Result<u128, Box<dyn std::error::Error>> {
|
|
let is_nonce_set = {
|
|
let nonces = self.nonces.read();
|
|
nonces.get(&hex::encode(account.clone())).cloned()
|
|
};
|
|
|
|
let remote_nonce = if let Some(nonce) = is_nonce_set {
|
|
nonce
|
|
} else {
|
|
check_account_nonce(self.api.clone(), account.clone()).await?
|
|
};
|
|
|
|
let mut nonces = self.nonces.write();
|
|
if let Some(nonce) = nonces.get_mut(&hex::encode(account.clone())) {
|
|
*nonce += 1;
|
|
Ok(*nonce)
|
|
} else {
|
|
nonces.insert(hex::encode(account), remote_nonce);
|
|
Ok(remote_nonce)
|
|
}
|
|
}
|
|
|
|
async fn update_count(&self) {
|
|
let i = Instant::now();
|
|
let xts_len = self
|
|
.rpc_client
|
|
.request::<Vec<serde_json::Value>>(
|
|
"author_pendingExtrinsics",
|
|
pezkuwi_subxt_rpcs::rpc_params!(),
|
|
)
|
|
.await
|
|
.expect("author_pendingExtrinsics should not fail")
|
|
.len();
|
|
*self.current_pending_extrinsics.write() = Some((i, xts_len));
|
|
}
|
|
}
|
|
|
|
/// Fetches an account storage and returns its nonce.
|
|
pub async fn check_account_nonce<C: pezkuwi_subxt::Config>(
|
|
api: OnlineClient<C>,
|
|
account: AccountIdOf<C>,
|
|
) -> Result<u128, Box<dyn std::error::Error>>
|
|
where
|
|
AccountIdOf<C>: Send + Sync + AsRef<[u8]>,
|
|
{
|
|
let storage_query = pezkuwi_subxt::dynamic::storage("System", "Account");
|
|
let storage_at = api.storage().at_latest().await?;
|
|
let storage_value = storage_at
|
|
.try_fetch(storage_query, (Value::from_bytes(account.clone()),))
|
|
.await?
|
|
.ok_or_else(|| {
|
|
format!("Sender account {:?} does not exist", hex::encode(account.clone()))
|
|
})?;
|
|
let value: pezkuwi_subxt::dynamic::Value =
|
|
storage_value.decode().map_err(|e| format!("Failed to decode storage: {e:?}"))?;
|
|
|
|
debug!(target:LOG_TARGET,"account has free balance: {:?}", value.at("data").at("free"));
|
|
debug!(target:LOG_TARGET,"account has nonce: {:?}", value.at("nonce"));
|
|
// info!("account has nonce: {:#?}", value);
|
|
let nonce = value
|
|
.at("nonce")
|
|
.ok_or("nonce is not set for the account")?
|
|
.as_u128()
|
|
.ok_or("nonce is not u128")?;
|
|
|
|
Ok(nonce)
|
|
}
|
|
|
|
#[async_trait]
|
|
impl<C, KP> TransactionsSink<HashOf<C>> for SubxtTransactionsSink<C, KP>
|
|
where
|
|
AccountIdOf<C>: Send + Sync + AsRef<[u8]>,
|
|
C: pezkuwi_subxt::Config,
|
|
KP: Signer<C> + Clone + Send + Sync + 'static,
|
|
{
|
|
async fn submit_and_watch(
|
|
&self,
|
|
tx: &dyn Transaction<HashType = HashOf<C>>,
|
|
) -> Result<StreamOf<TransactionStatus<HashOf<C>>>, Error> {
|
|
let tx = tx.as_any().downcast_ref::<SubxtTransaction<C>>().unwrap();
|
|
let result = tx.transaction.submit_and_watch().await;
|
|
|
|
match result {
|
|
Ok(stream) => Ok(stream.map(|e| e.unwrap().into()).boxed()),
|
|
Err(e) => Err(e.into()),
|
|
}
|
|
}
|
|
|
|
async fn submit(&self, tx: &dyn Transaction<HashType = HashOf<C>>) -> Result<HashOf<C>, Error> {
|
|
let tx = tx.as_any().downcast_ref::<SubxtTransaction<C>>().unwrap();
|
|
tx.transaction.submit().await.map_err(|e| e.into())
|
|
}
|
|
|
|
/// Current count of transactions being processed by sink.
|
|
async fn pending_extrinsics(&self) -> usize {
|
|
let current_pending_extrinsics = { *self.current_pending_extrinsics.read() };
|
|
if let Some((ts, _)) = current_pending_extrinsics {
|
|
if ts.elapsed() > Duration::from_millis(1000) {
|
|
self.update_count().await;
|
|
}
|
|
} else {
|
|
self.update_count().await;
|
|
}
|
|
|
|
self.current_pending_extrinsics
|
|
.read()
|
|
.expect("current_pending_extrinsics cannot be None")
|
|
.1
|
|
}
|
|
|
|
fn transaction_monitor(&self) -> Option<&dyn TransactionMonitor<HashOf<C>>> {
|
|
self.block_monitor.as_ref().map(|m| m as &dyn TransactionMonitor<HashOf<C>>)
|
|
}
|
|
}
|
|
|
|
/// Types of accounts generation.
|
|
#[derive(Debug, Clone)]
|
|
pub enum AccountGenerateRequest {
|
|
Keyring(String),
|
|
Derived(String, u32),
|
|
}
|
|
|
|
/// Seed user for sender accounts.
|
|
pub const SENDER_SEED: &str = "//Sender";
|
|
/// Seed used for receiver accounts.
|
|
pub(crate) const RECEIVER_SEED: &str = "//Receiver";
|
|
|
|
/// Generates ecdsa based keypairs.
|
|
pub fn generate_ecdsa_keypair(description: AccountGenerateRequest) -> EthKeypair {
|
|
match description {
|
|
AccountGenerateRequest::Keyring(name) => match name.as_str() {
|
|
"alice" | "alith" => eth_dev::alith(),
|
|
"bob" | "baltathar" => eth_dev::baltathar(),
|
|
"charlie" | "charleth" => eth_dev::charleth(),
|
|
"dave" | "dorothy" => eth_dev::dorothy(),
|
|
"eve" | "ethan" => eth_dev::ethan(),
|
|
"ferdie" | "faith" => eth_dev::faith(),
|
|
_ => panic!("unknown keyring name"),
|
|
},
|
|
AccountGenerateRequest::Derived(seed, i) => {
|
|
use std::str::FromStr;
|
|
let derivation = format!("{seed}//{i}");
|
|
let u = pezkuwi_subxt_signer::SecretUri::from_str(&derivation).unwrap();
|
|
<pezkuwi_subxt_signer::ecdsa::Keypair>::from_uri(&u).unwrap().into()
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Generates sr25519 based keypairs.
|
|
pub fn generate_sr25519_keypair(description: AccountGenerateRequest) -> SrPair {
|
|
match description {
|
|
AccountGenerateRequest::Keyring(name) => match name.as_str() {
|
|
"alice" | "alith" => sr25519_dev::alice(),
|
|
"bob" | "baltathar" => sr25519_dev::bob(),
|
|
"charlie" | "charleth" => sr25519_dev::charlie(),
|
|
"dave" | "dorothy" => sr25519_dev::dave(),
|
|
"eve" | "ethan" => sr25519_dev::eve(),
|
|
"ferdie" | "faith" => sr25519_dev::ferdie(),
|
|
_ => panic!("unknown keyring name"),
|
|
},
|
|
AccountGenerateRequest::Derived(seed, i) => {
|
|
use std::str::FromStr;
|
|
let derivation = format!("{seed}//{i}");
|
|
let u = pezkuwi_subxt_signer::SecretUri::from_str(&derivation).unwrap();
|
|
<pezkuwi_subxt_signer::sr25519::Keypair>::from_uri(&u).unwrap()
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Interface for implementors of keypairs generators.
|
|
pub trait GenerateKeyPairFunction<KP>:
|
|
Fn(AccountGenerateRequest) -> KP + Copy + Send + 'static
|
|
{
|
|
}
|
|
|
|
impl<T, KP> GenerateKeyPairFunction<KP> for T where
|
|
T: Fn(AccountGenerateRequest) -> KP + Copy + Send + 'static
|
|
{
|
|
}
|
|
|
|
/// Logic that derives accounts from a certain seed.
|
|
pub fn derive_accounts<C, KP, G>(
|
|
accounts_description: AccountsDescription,
|
|
seed: &str,
|
|
generate: G,
|
|
) -> HashMap<String, (KP, AccountMetadata)>
|
|
where
|
|
C: pezkuwi_subxt::Config,
|
|
KP: Signer<C> + Send + Sync + 'static,
|
|
G: GenerateKeyPairFunction<KP>,
|
|
{
|
|
match accounts_description {
|
|
AccountsDescription::Derived(range) => {
|
|
let from_id = range.start as usize;
|
|
let to_id = range.end as usize;
|
|
let n = to_id - from_id;
|
|
let t = std::cmp::min(
|
|
n,
|
|
std::thread::available_parallelism().unwrap_or(1usize.try_into().unwrap()).get(),
|
|
);
|
|
let mut threads = Vec::new();
|
|
|
|
(0..t).for_each(|thread_idx| {
|
|
// let chunk = (thread_idx * (n / t))..((thread_idx + 1) * (n / t));
|
|
let chunk =
|
|
(from_id + (thread_idx * n) / t)..(from_id + ((thread_idx + 1) * n) / t);
|
|
let seed = seed.to_string().clone();
|
|
threads.push(std::thread::spawn(move || {
|
|
chunk
|
|
.into_iter()
|
|
.map(move |i| {
|
|
(
|
|
i.to_string(),
|
|
(
|
|
generate(AccountGenerateRequest::Derived(
|
|
seed.to_string(),
|
|
i as u32,
|
|
)),
|
|
AccountMetadata::Derived(i as u32),
|
|
),
|
|
)
|
|
})
|
|
.collect::<Vec<_>>()
|
|
}));
|
|
});
|
|
|
|
threads
|
|
.into_iter()
|
|
.flat_map(|h| h.join().unwrap())
|
|
// .map(|p| (p, funds))
|
|
.collect()
|
|
},
|
|
AccountsDescription::Keyring(account) => HashMap::from([(
|
|
account.clone(),
|
|
(
|
|
generate(AccountGenerateRequest::Keyring(account.clone())),
|
|
AccountMetadata::KeyRing(account),
|
|
),
|
|
)]),
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
async fn create_online_transaction<C: pezkuwi_subxt::Config, KP, B>(
|
|
from_keypair: &KP,
|
|
nonce: u128,
|
|
mortality: &Option<u64>,
|
|
account: &str,
|
|
sink: &SubxtTransactionsSink<C, KP>,
|
|
from_account_id: &<C as pezkuwi_subxt::Config>::AccountId,
|
|
to_account_id: &<C as pezkuwi_subxt::Config>::AccountId,
|
|
tip: u128,
|
|
payload_builder: &B,
|
|
) -> Result<SubxtTransaction<C>, Error>
|
|
where
|
|
AccountIdOf<C>: Send + Sync + AsRef<[u8]>,
|
|
KP: Signer<C> + Clone + Send + Sync + 'static,
|
|
<<C as pezkuwi_subxt::Config>::ExtrinsicParams as pezkuwi_subxt::config::ExtrinsicParams<C>>::Params: From<(
|
|
(),
|
|
(),
|
|
(),
|
|
CheckNonceParams,
|
|
(),
|
|
CheckMortalityParams<C>,
|
|
ChargeAssetTxPaymentParams<C>,
|
|
ChargeTransactionPaymentParams,
|
|
(),
|
|
)>,
|
|
B: Fn(&TxPayloadBuildContext<AccountIdOf<C>>) -> DynamicPayload + ?Sized,
|
|
{
|
|
// Needed because `Params` as associated type does not implement clone, and we need to
|
|
// recreate the tx params in a loop when we can't create a partial tx with the online
|
|
// client, due to various RPC related issues or state not being up to date (currently we
|
|
// handle an error which happens when trying to create a partial tx that is based on a
|
|
// certain finalized block returned by the RPC, which is then reported as not found).
|
|
// Retrying seems to fix the issue.
|
|
fn tx_params<CC: pezkuwi_subxt::Config>(
|
|
mortality: &Option<u64>,
|
|
nonce: u64,
|
|
tip: u128,
|
|
) -> <DefaultExtrinsicParams<CC> as ExtrinsicParams<CC>>::Params {
|
|
let mut params = <DefaultExtrinsicParamsBuilder<CC>>::new().nonce(nonce).tip(tip);
|
|
if let Some(mortal) = mortality {
|
|
params = params.mortal(*mortal);
|
|
}
|
|
params.build()
|
|
}
|
|
|
|
// Creates a subxt transaction.
|
|
//
|
|
// The mortality of the transaction involves setting up a block until the transaction is valid,
|
|
// which needs fetching the last finalized block number on chain similarly to subxt:
|
|
// https://github.com/paritytech/subxt/blob/77b6abccbacf194f3889610024e2f4024e8c2822/subxt/src/tx/tx_client.rs#L600
|
|
async fn subxt_transaction<CC: pezkuwi_subxt::Config, KEYP>(
|
|
sink: &SubxtTransactionsSink<CC, KEYP>,
|
|
mut partial_tx: PartialTransaction<CC, OnlineClient<CC>>,
|
|
from_keypair: &KEYP,
|
|
nonce: u128,
|
|
mortality: &Option<u64>,
|
|
account: &str,
|
|
) -> Result<SubxtTransaction<CC>, Error>
|
|
where
|
|
KEYP: Signer<CC> + Clone + Send + Sync + 'static,
|
|
AccountIdOf<CC>: Send + Sync + AsRef<[u8]>,
|
|
{
|
|
let block_number = if mortality.is_some() {
|
|
let block_ref = sink
|
|
.api()
|
|
.backend()
|
|
.latest_finalized_block_ref()
|
|
.await
|
|
.expect("to get the last finalized block ref. qed");
|
|
let block = sink
|
|
.api()
|
|
.blocks()
|
|
.at(block_ref)
|
|
.await
|
|
.expect("to get the corresponding block header. qed");
|
|
Some(block.number().into())
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let submittable_tx = partial_tx.sign(from_keypair);
|
|
let hash = submittable_tx.hash();
|
|
debug!(target:LOG_TARGET,"built mortal tx hash: {:?}", hash);
|
|
Ok(SubxtTransaction::<CC>::new(
|
|
submittable_tx,
|
|
nonce,
|
|
mortality.and_then(|mortal| block_number.map(|number| number + mortal)),
|
|
sink.get_to_account_metadata(account).expect("account metadata exists"),
|
|
))
|
|
}
|
|
|
|
let ctx = TxPayloadBuildContext { to_account_id, from_account_id, account, nonce };
|
|
let tx_call = payload_builder(&ctx);
|
|
for _ in 0..DEFAULT_RETRIES_FOR_PARTIAL_TX_CREATION {
|
|
let params = tx_params(mortality, nonce as u64, tip);
|
|
match sink.api().tx().create_partial(&tx_call, from_account_id, params.into()).await {
|
|
Ok(tx) => {
|
|
return subxt_transaction(sink, tx, from_keypair, nonce, mortality, account).await
|
|
},
|
|
Err(_) => continue,
|
|
}
|
|
}
|
|
|
|
error!(target: LOG_TARGET, "Attempting transaction creation with the online client, to factor in the provided mortality, failed.");
|
|
Err(Error::Other("failed to create transaction with online client".to_string()))
|
|
}
|
|
|
|
/// Builds a transaction with subxt.
|
|
pub(crate) async fn build_subxt_tx<C, KP, B>(
|
|
params: &crate::transaction::BuildTransactionParams<'_>,
|
|
sink: &SubxtTransactionsSink<C, KP>,
|
|
payload_builder: &B,
|
|
) -> SubxtTransaction<C>
|
|
where
|
|
AccountIdOf<C>: Send + Sync + AsRef<[u8]>,
|
|
C: pezkuwi_subxt::Config,
|
|
KP: Signer<C> + Clone + Send + Sync + 'static,
|
|
<<C as pezkuwi_subxt::Config>::ExtrinsicParams as pezkuwi_subxt::config::ExtrinsicParams<C>>::Params: From<(
|
|
(),
|
|
(),
|
|
(),
|
|
CheckNonceParams,
|
|
(),
|
|
CheckMortalityParams<C>,
|
|
ChargeAssetTxPaymentParams<C>,
|
|
ChargeTransactionPaymentParams,
|
|
(),
|
|
)>,
|
|
B: Fn(&TxPayloadBuildContext<AccountIdOf<C>>) -> DynamicPayload + ?Sized,
|
|
{
|
|
let &crate::transaction::BuildTransactionParams { account, nonce, mortality, tip } = params;
|
|
|
|
let to_account_id = sink.get_to_account_id(account).expect("to account exists");
|
|
let from_account_id = sink.get_from_account_id(account).expect("from account exists");
|
|
let from_keypair = sink.get_from_key_pair(account).expect("from account exists");
|
|
let nonce = if let Some(nonce) = nonce {
|
|
trace!("nonce for {:?} -> {:?}", account, nonce);
|
|
*nonce
|
|
} else {
|
|
let nonce = sink
|
|
.check_account_nonce(from_account_id.clone())
|
|
.await
|
|
.expect("account nonce shall exists");
|
|
trace!("checked nonce for {:?} -> {:?}", account, nonce);
|
|
nonce
|
|
};
|
|
debug!(
|
|
target:LOG_TARGET,
|
|
account,
|
|
nonce,
|
|
?mortality,
|
|
from_account=hex::encode(from_account_id.clone()),
|
|
to_account=hex::encode(to_account_id.clone()),
|
|
"build_subxt_tx"
|
|
);
|
|
|
|
if mortality.is_some() {
|
|
create_online_transaction(
|
|
&from_keypair,
|
|
nonce,
|
|
mortality,
|
|
account,
|
|
sink,
|
|
&from_account_id,
|
|
&to_account_id,
|
|
tip,
|
|
payload_builder,
|
|
)
|
|
.await
|
|
.expect("failed to create mortal transaction")
|
|
} else {
|
|
let tx_params = <DefaultExtrinsicParamsBuilder<C>>::new()
|
|
.nonce(nonce as u64)
|
|
.tip(tip)
|
|
.build()
|
|
.into();
|
|
let ctx = TxPayloadBuildContext {
|
|
to_account_id: &to_account_id,
|
|
from_account_id: &from_account_id,
|
|
account,
|
|
nonce,
|
|
};
|
|
let tx_call = payload_builder(&ctx);
|
|
let tx = SubxtTransaction::<C>::new(
|
|
sink.api()
|
|
.tx()
|
|
.create_partial_offline(&tx_call, tx_params)
|
|
.unwrap()
|
|
.sign(&from_keypair),
|
|
nonce as u128,
|
|
None,
|
|
sink.get_to_account_metadata(account).expect("account metadata exists"),
|
|
);
|
|
debug!(target:LOG_TARGET,"built immortal tx hash: {:?}", tx.hash());
|
|
tx
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use pezkuwi_subxt::BizinikiwConfig;
|
|
|
|
use crate::{
|
|
subxt_transaction::{
|
|
derive_accounts, generate_sr25519_keypair, AccountGenerateRequest, SENDER_SEED,
|
|
},
|
|
transaction::AccountMetadata,
|
|
};
|
|
|
|
#[tokio::test]
|
|
async fn test_derive_accounts_len() {
|
|
let accounts = derive_accounts::<BizinikiwConfig, pezkuwi_subxt_signer::sr25519::Keypair, _>(
|
|
crate::scenario::AccountsDescription::Derived(0..11),
|
|
SENDER_SEED,
|
|
generate_sr25519_keypair,
|
|
);
|
|
assert_eq!(accounts.len(), 11);
|
|
for (i, (kp, meta)) in accounts {
|
|
let id = i.parse::<u32>().unwrap();
|
|
assert_eq!(
|
|
kp.public_key().0,
|
|
generate_sr25519_keypair(AccountGenerateRequest::Derived(
|
|
SENDER_SEED.to_string(),
|
|
id
|
|
))
|
|
.public_key()
|
|
.0
|
|
);
|
|
assert_eq!(AccountMetadata::Derived(id), meta);
|
|
}
|
|
|
|
let accounts = derive_accounts::<BizinikiwConfig, pezkuwi_subxt_signer::sr25519::Keypair, _>(
|
|
crate::scenario::AccountsDescription::Keyring("alice".to_string()),
|
|
SENDER_SEED,
|
|
generate_sr25519_keypair,
|
|
);
|
|
assert_eq!(accounts.len(), 1);
|
|
assert_eq!(
|
|
accounts.get("alice").unwrap().0.public_key().0,
|
|
generate_sr25519_keypair(AccountGenerateRequest::Keyring("alice".to_string()))
|
|
.public_key()
|
|
.0
|
|
);
|
|
assert_eq!(accounts.get("alice").unwrap().1, AccountMetadata::KeyRing("alice".to_string()))
|
|
}
|
|
}
|