1use crate::{Error, Metrics, Result, Sr25519Signer, Wallet};
11use std::time::Duration;
12use subxt::{OnlineClient, PolkadotConfig};
13use tokio::time::sleep;
14use tracing::{debug, info, warn};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum BatchMode {
19 Optimistic,
22 AllOrNothing,
25 Force,
28}
29
30impl Default for BatchMode {
31 fn default() -> Self {
32 Self::Optimistic
33 }
34}
35
36#[derive(Debug, Clone)]
38pub struct BatchCall {
39 pub pallet_index: u8,
41 pub call_index: u8,
43 pub args_encoded: Vec<u8>,
45}
46
47impl BatchCall {
48 pub fn new(pallet_index: u8, call_index: u8, args_encoded: Vec<u8>) -> Self {
50 Self {
51 pallet_index,
52 call_index,
53 args_encoded,
54 }
55 }
56}
57
58#[derive(Debug, Clone)]
60pub struct FeeConfig {
61 pub multiplier: f64,
63 pub max_fee: Option<u128>,
65 pub tip: u128,
67}
68
69impl Default for FeeConfig {
70 fn default() -> Self {
71 Self {
72 multiplier: 1.2,
73 max_fee: None,
74 tip: 0,
75 }
76 }
77}
78
79impl FeeConfig {
80 pub fn new() -> Self {
82 Self::default()
83 }
84
85 pub fn with_multiplier(mut self, multiplier: f64) -> Self {
87 self.multiplier = multiplier;
88 self
89 }
90
91 pub fn with_max_fee(mut self, max_fee: u128) -> Self {
93 self.max_fee = Some(max_fee);
94 self
95 }
96
97 pub fn with_tip(mut self, tip: u128) -> Self {
99 self.tip = tip;
100 self
101 }
102}
103
104#[derive(Debug, Clone)]
106pub struct RetryConfig {
107 pub max_retries: u32,
109 pub initial_delay: Duration,
111 pub max_delay: Duration,
113 pub backoff_multiplier: f64,
115}
116
117impl Default for RetryConfig {
118 fn default() -> Self {
119 Self {
120 max_retries: 3,
121 initial_delay: Duration::from_secs(2),
122 max_delay: Duration::from_secs(30),
123 backoff_multiplier: 2.0,
124 }
125 }
126}
127
128impl RetryConfig {
129 pub fn new() -> Self {
131 Self::default()
132 }
133
134 pub fn with_max_retries(mut self, max_retries: u32) -> Self {
136 self.max_retries = max_retries;
137 self
138 }
139
140 pub fn with_initial_delay(mut self, delay: Duration) -> Self {
142 self.initial_delay = delay;
143 self
144 }
145}
146
147pub struct TransactionExecutor {
149 client: OnlineClient<PolkadotConfig>,
150 fee_config: FeeConfig,
151 retry_config: RetryConfig,
152 metrics: Metrics,
153}
154
155impl TransactionExecutor {
156 pub fn new(client: OnlineClient<PolkadotConfig>, metrics: Metrics) -> Self {
158 Self {
159 client,
160 fee_config: FeeConfig::default(),
161 retry_config: RetryConfig::default(),
162 metrics,
163 }
164 }
165
166 pub fn with_fee_config(mut self, fee_config: FeeConfig) -> Self {
168 self.fee_config = fee_config;
169 self
170 }
171
172 pub fn with_retry_config(mut self, retry_config: RetryConfig) -> Self {
174 self.retry_config = retry_config;
175 self
176 }
177
178 pub async fn transfer(&self, from: &Wallet, to: &str, amount: u128) -> Result<String> {
180 info!(
181 "Submitting transfer from {} to {} of {} units",
182 from.address(),
183 to,
184 amount
185 );
186
187 use sp_core::crypto::Ss58Codec;
189 let dest = sp_core::sr25519::Public::from_ss58check(to)
190 .map_err(|e| Error::Transaction(format!("Invalid destination address: {}", e)))?;
191
192 use subxt::dynamic::Value;
194
195 let dest_value = Value::unnamed_variant("Id", vec![Value::from_bytes(dest.0)]);
196
197 let transfer_call = subxt::dynamic::tx(
198 "Balances",
199 "transfer_keep_alive",
200 vec![dest_value, Value::u128(amount)],
201 );
202
203 self.submit_extrinsic_with_retry(&transfer_call, from).await
205 }
206
207 async fn submit_extrinsic_with_retry<Call>(
209 &self,
210 call: &Call,
211 signer: &Wallet,
212 ) -> Result<String>
213 where
214 Call: subxt::tx::Payload,
215 {
216 let mut attempts = 0;
217 let mut delay = self.retry_config.initial_delay;
218
219 loop {
220 attempts += 1;
221 self.metrics.record_transaction_attempt();
222
223 match self.submit_extrinsic(call, signer).await {
224 Ok(hash) => {
225 self.metrics.record_transaction_success();
226 return Ok(hash);
227 }
228 Err(e) => {
229 if attempts >= self.retry_config.max_retries {
230 warn!("Transaction failed after {} attempts: {}", attempts, e);
231 self.metrics.record_transaction_failure();
232 return Err(e);
233 }
234
235 warn!(
236 "Transaction attempt {} failed: {}. Retrying in {:?}",
237 attempts, e, delay
238 );
239 sleep(delay).await;
240
241 delay = Duration::from_secs_f64(
243 (delay.as_secs_f64() * self.retry_config.backoff_multiplier)
244 .min(self.retry_config.max_delay.as_secs_f64()),
245 );
246 }
247 }
248 }
249 }
250
251 async fn submit_extrinsic<Call>(&self, call: &Call, signer: &Wallet) -> Result<String>
253 where
254 Call: subxt::tx::Payload,
255 {
256 debug!("Submitting extrinsic");
257
258 let pair = signer
260 .sr25519_pair()
261 .ok_or_else(|| Error::Transaction("Wallet does not have SR25519 key".to_string()))?;
262
263 let apex_signer = Sr25519Signer::new(pair.clone());
265
266 let mut progress = self
268 .client
269 .tx()
270 .sign_and_submit_then_watch_default(call, &apex_signer)
271 .await
272 .map_err(|e| Error::Transaction(format!("Failed to submit transaction: {}", e)))?;
273
274 while let Some(event) = progress.next().await {
276 let event =
277 event.map_err(|e| Error::Transaction(format!("Transaction error: {}", e)))?;
278
279 if event.as_in_block().is_some() {
280 info!("Transaction included in block");
281 }
282
283 if let Some(finalized) = event.as_finalized() {
284 let tx_hash = format!("0x{}", hex::encode(finalized.extrinsic_hash()));
285 info!("Transaction finalized: {}", tx_hash);
286
287 finalized
289 .wait_for_success()
290 .await
291 .map_err(|e| Error::Transaction(format!("Transaction failed: {}", e)))?;
292
293 return Ok(tx_hash);
294 }
295 }
296
297 Err(Error::Transaction(
298 "Transaction stream ended without finalization".to_string(),
299 ))
300 }
301
302 pub async fn estimate_fee(
312 &self,
313 pallet: &str,
314 call: &str,
315 args: Vec<subxt::dynamic::Value>,
316 _from: &Wallet,
317 ) -> Result<u128> {
318 debug!("Estimating fee for {}::{}", pallet, call);
319
320 let tx = subxt::dynamic::tx(pallet, call, args);
322
323 let payload = self
326 .client
327 .tx()
328 .create_unsigned(&tx)
329 .map_err(|e| Error::Transaction(format!("Failed to create unsigned tx: {}", e)))?;
330
331 let encoded = payload.encoded();
333
334 let call_data = {
337 use parity_scale_codec::Encode;
338 let params = (encoded, encoded.len() as u32);
341 params.encode()
342 };
343
344 let result = self
346 .client
347 .runtime_api()
348 .at_latest()
349 .await
350 .map_err(|e| Error::Connection(format!("Failed to get latest block: {}", e)))?
351 .call_raw("TransactionPaymentApi_query_info", Some(&call_data))
352 .await
353 .map_err(|e| Error::Transaction(format!("Failed to query fee info: {}", e)))?;
354
355 if result.len() >= 16 {
361 let fee_bytes = &result[result.len() - 16..];
362 let mut fee_array = [0u8; 16];
363 fee_array.copy_from_slice(fee_bytes);
364 let base_fee = u128::from_le_bytes(fee_array);
365
366 let estimated_fee = (base_fee as f64 * self.fee_config.multiplier) as u128;
368
369 if let Some(max_fee) = self.fee_config.max_fee {
371 if estimated_fee > max_fee {
372 return Err(Error::Transaction(format!(
373 "Estimated fee {} exceeds maximum {}",
374 estimated_fee, max_fee
375 )));
376 }
377 }
378
379 debug!(
380 "Estimated fee: {} (base: {}, multiplier: {})",
381 estimated_fee, base_fee, self.fee_config.multiplier
382 );
383
384 Ok(estimated_fee + self.fee_config.tip)
385 } else {
386 warn!("Unexpected fee query response format, using fallback");
387 Ok(1_000_000u128) }
390 }
391
392 pub async fn estimate_transfer_fee(
394 &self,
395 to: &str,
396 amount: u128,
397 from: &Wallet,
398 ) -> Result<u128> {
399 use sp_core::crypto::{AccountId32, Ss58Codec};
400 let to_account = AccountId32::from_ss58check(to)
401 .map_err(|e| Error::Transaction(format!("Invalid recipient address: {}", e)))?;
402
403 let to_bytes: &[u8] = to_account.as_ref();
404
405 self.estimate_fee(
406 "Balances",
407 "transfer_keep_alive",
408 vec![
409 subxt::dynamic::Value::from_bytes(to_bytes),
410 subxt::dynamic::Value::u128(amount),
411 ],
412 from,
413 )
414 .await
415 }
416
417 pub async fn execute_batch(
426 &self,
427 calls: Vec<BatchCall>,
428 wallet: &Wallet,
429 batch_mode: BatchMode,
430 ) -> Result<String> {
431 debug!(
432 "Executing batch of {} calls with mode {:?}",
433 calls.len(),
434 batch_mode
435 );
436 self.metrics.record_transaction_attempt();
437
438 if calls.is_empty() {
439 return Err(Error::Transaction("Cannot execute empty batch".to_string()));
440 }
441
442 let call_values: Vec<subxt::dynamic::Value> = calls
446 .into_iter()
447 .map(|call| {
448 let mut call_bytes = Vec::new();
450 call_bytes.push(call.pallet_index);
451 call_bytes.push(call.call_index);
452 call_bytes.extend_from_slice(&call.args_encoded);
453
454 subxt::dynamic::Value::from_bytes(&call_bytes)
456 })
457 .collect();
458
459 let calls_value = subxt::dynamic::Value::unnamed_composite(call_values);
461
462 let batch_call_name = match batch_mode {
464 BatchMode::Optimistic => "batch",
465 BatchMode::AllOrNothing => "batch_all",
466 BatchMode::Force => "force_batch",
467 };
468
469 debug!("Using Utility::{} for batch execution", batch_call_name);
470
471 let tx = subxt::dynamic::tx("Utility", batch_call_name, vec![calls_value]);
473
474 let pair = wallet
476 .sr25519_pair()
477 .ok_or_else(|| Error::Transaction("Wallet does not have SR25519 key".to_string()))?;
478
479 let apex_signer = Sr25519Signer::new(pair.clone());
481
482 let mut signed_tx = self
484 .client
485 .tx()
486 .sign_and_submit_then_watch_default(&tx, &apex_signer)
487 .await
488 .map_err(|e| Error::Transaction(format!("Failed to submit batch: {}", e)))?;
489
490 while let Some(event) = signed_tx.next().await {
492 let event =
493 event.map_err(|e| Error::Transaction(format!("Batch transaction error: {}", e)))?;
494
495 if event.as_in_block().is_some() {
496 info!("Batch transaction included in block");
497 }
498
499 if let Some(finalized) = event.as_finalized() {
500 let tx_hash = format!("0x{}", hex::encode(finalized.extrinsic_hash()));
501 info!("Batch transaction finalized: {}", tx_hash);
502
503 finalized
505 .wait_for_success()
506 .await
507 .map_err(|e| Error::Transaction(format!("Batch transaction failed: {}", e)))?;
508
509 self.metrics.record_transaction_success();
510 return Ok(tx_hash);
511 }
512 }
513
514 Err(Error::Transaction(
515 "Batch transaction stream ended without finalization".to_string(),
516 ))
517 }
518
519 pub async fn execute_batch_transfers(
523 &self,
524 transfers: Vec<(String, u128)>, wallet: &Wallet,
526 batch_mode: BatchMode,
527 ) -> Result<String> {
528 use sp_core::crypto::{AccountId32, Ss58Codec};
529
530 let mut calls = Vec::new();
532
533 for (recipient, amount) in transfers {
534 let to_account = AccountId32::from_ss58check(&recipient).map_err(|e| {
535 Error::Transaction(format!("Invalid recipient {}: {}", recipient, e))
536 })?;
537
538 let to_bytes: &[u8] = to_account.as_ref();
539
540 use parity_scale_codec::Encode;
542 let args = (to_bytes, amount).encode();
543
544 calls.push(BatchCall {
545 pallet_index: 5, call_index: 3, args_encoded: args,
548 });
549 }
550
551 self.execute_batch(calls, wallet, batch_mode).await
552 }
553}
554
555#[allow(dead_code)]
557pub struct ExtrinsicBuilder {
558 client: OnlineClient<PolkadotConfig>,
559 pallet: Option<String>,
560 call: Option<String>,
561 args: Vec<subxt::dynamic::Value>,
562}
563
564impl ExtrinsicBuilder {
565 pub fn new(client: OnlineClient<PolkadotConfig>) -> Self {
567 Self {
568 client,
569 pallet: None,
570 call: None,
571 args: Vec::new(),
572 }
573 }
574
575 pub fn pallet(mut self, pallet: impl Into<String>) -> Self {
577 self.pallet = Some(pallet.into());
578 self
579 }
580
581 pub fn call(mut self, call: impl Into<String>) -> Self {
583 self.call = Some(call.into());
584 self
585 }
586
587 pub fn arg(mut self, arg: subxt::dynamic::Value) -> Self {
589 self.args.push(arg);
590 self
591 }
592
593 pub fn args(mut self, args: Vec<subxt::dynamic::Value>) -> Self {
595 self.args.extend(args);
596 self
597 }
598
599 #[allow(clippy::result_large_err)]
601 pub fn build(self) -> Result<impl subxt::tx::Payload> {
602 let pallet = self
603 .pallet
604 .ok_or_else(|| Error::Transaction("Pallet not set".to_string()))?;
605 let call = self
606 .call
607 .ok_or_else(|| Error::Transaction("Call not set".to_string()))?;
608
609 Ok(subxt::dynamic::tx(&pallet, &call, self.args))
610 }
611}
612
613#[cfg(test)]
614mod tests {
615 use super::*;
616
617 #[test]
618 fn test_fee_config() {
619 let config = FeeConfig::new()
620 .with_multiplier(1.5)
621 .with_max_fee(1_000_000)
622 .with_tip(100);
623
624 assert_eq!(config.multiplier, 1.5);
625 assert_eq!(config.max_fee, Some(1_000_000));
626 assert_eq!(config.tip, 100);
627 }
628
629 #[test]
630 fn test_retry_config() {
631 let config = RetryConfig::new()
632 .with_max_retries(5)
633 .with_initial_delay(Duration::from_secs(1));
634
635 assert_eq!(config.max_retries, 5);
636 assert_eq!(config.initial_delay, Duration::from_secs(1));
637 }
638
639 #[test]
640 fn test_extrinsic_builder() {
641 let pallet = Some("Balances".to_string());
643 let call = Some("transfer".to_string());
644
645 assert!(pallet.is_some());
646 assert!(call.is_some());
647 }
648}