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, Default)]
18pub enum BatchMode {
19 #[default]
22 Optimistic,
23 AllOrNothing,
26 Force,
29}
30
31#[derive(Debug, Clone)]
33pub struct BatchCall {
34 pub pallet_index: u8,
36 pub call_index: u8,
38 pub args_encoded: Vec<u8>,
40}
41
42impl BatchCall {
43 pub fn new(pallet_index: u8, call_index: u8, args_encoded: Vec<u8>) -> Self {
45 Self {
46 pallet_index,
47 call_index,
48 args_encoded,
49 }
50 }
51}
52
53#[derive(Debug, Clone)]
55pub struct FeeConfig {
56 pub multiplier: f64,
58 pub max_fee: Option<u128>,
60 pub tip: u128,
62}
63
64impl Default for FeeConfig {
65 fn default() -> Self {
66 Self {
67 multiplier: 1.2,
68 max_fee: None,
69 tip: 0,
70 }
71 }
72}
73
74impl FeeConfig {
75 pub fn new() -> Self {
77 Self::default()
78 }
79
80 pub fn with_multiplier(mut self, multiplier: f64) -> Self {
82 self.multiplier = multiplier;
83 self
84 }
85
86 pub fn with_max_fee(mut self, max_fee: u128) -> Self {
88 self.max_fee = Some(max_fee);
89 self
90 }
91
92 pub fn with_tip(mut self, tip: u128) -> Self {
94 self.tip = tip;
95 self
96 }
97}
98
99#[derive(Debug, Clone)]
101pub struct RetryConfig {
102 pub max_retries: u32,
104 pub initial_delay: Duration,
106 pub max_delay: Duration,
108 pub backoff_multiplier: f64,
110}
111
112impl Default for RetryConfig {
113 fn default() -> Self {
114 Self {
115 max_retries: 3,
116 initial_delay: Duration::from_secs(2),
117 max_delay: Duration::from_secs(30),
118 backoff_multiplier: 2.0,
119 }
120 }
121}
122
123impl RetryConfig {
124 pub fn new() -> Self {
126 Self::default()
127 }
128
129 pub fn with_max_retries(mut self, max_retries: u32) -> Self {
131 self.max_retries = max_retries;
132 self
133 }
134
135 pub fn with_initial_delay(mut self, delay: Duration) -> Self {
137 self.initial_delay = delay;
138 self
139 }
140}
141
142pub struct TransactionExecutor {
144 client: OnlineClient<PolkadotConfig>,
145 fee_config: FeeConfig,
146 retry_config: RetryConfig,
147 metrics: Metrics,
148}
149
150impl TransactionExecutor {
151 pub fn new(client: OnlineClient<PolkadotConfig>, metrics: Metrics) -> Self {
153 Self {
154 client,
155 fee_config: FeeConfig::default(),
156 retry_config: RetryConfig::default(),
157 metrics,
158 }
159 }
160
161 pub fn with_fee_config(mut self, fee_config: FeeConfig) -> Self {
163 self.fee_config = fee_config;
164 self
165 }
166
167 pub fn with_retry_config(mut self, retry_config: RetryConfig) -> Self {
169 self.retry_config = retry_config;
170 self
171 }
172
173 pub async fn transfer(&self, from: &Wallet, to: &str, amount: u128) -> Result<String> {
175 info!(
176 "Submitting transfer from {} to {} of {} units",
177 from.address(),
178 to,
179 amount
180 );
181
182 use sp_core::crypto::Ss58Codec;
184 let dest = sp_core::sr25519::Public::from_ss58check(to)
185 .map_err(|e| Error::Transaction(format!("Invalid destination address: {}", e)))?;
186
187 use subxt::dynamic::Value;
189
190 let dest_value = Value::unnamed_variant("Id", vec![Value::from_bytes(dest.0)]);
191
192 let transfer_call = subxt::dynamic::tx(
193 "Balances",
194 "transfer_keep_alive",
195 vec![dest_value, Value::u128(amount)],
196 );
197
198 self.submit_extrinsic_with_retry(&transfer_call, from).await
200 }
201
202 async fn submit_extrinsic_with_retry<Call>(
204 &self,
205 call: &Call,
206 signer: &Wallet,
207 ) -> Result<String>
208 where
209 Call: subxt::tx::Payload,
210 {
211 let mut attempts = 0;
212 let mut delay = self.retry_config.initial_delay;
213
214 loop {
215 attempts += 1;
216 self.metrics.record_transaction_attempt();
217
218 match self.submit_extrinsic(call, signer).await {
219 Ok(hash) => {
220 self.metrics.record_transaction_success();
221 return Ok(hash);
222 }
223 Err(e) => {
224 if attempts >= self.retry_config.max_retries {
225 warn!("Transaction failed after {} attempts: {}", attempts, e);
226 self.metrics.record_transaction_failure();
227 return Err(e);
228 }
229
230 warn!(
231 "Transaction attempt {} failed: {}. Retrying in {:?}",
232 attempts, e, delay
233 );
234 sleep(delay).await;
235
236 delay = Duration::from_secs_f64(
238 (delay.as_secs_f64() * self.retry_config.backoff_multiplier)
239 .min(self.retry_config.max_delay.as_secs_f64()),
240 );
241 }
242 }
243 }
244 }
245
246 async fn submit_extrinsic<Call>(&self, call: &Call, signer: &Wallet) -> Result<String>
248 where
249 Call: subxt::tx::Payload,
250 {
251 debug!("Submitting extrinsic");
252
253 let pair = signer
255 .sr25519_pair()
256 .ok_or_else(|| Error::Transaction("Wallet does not have SR25519 key".to_string()))?;
257
258 let apex_signer = Sr25519Signer::new(pair.clone());
260
261 let mut progress = self
263 .client
264 .tx()
265 .sign_and_submit_then_watch_default(call, &apex_signer)
266 .await
267 .map_err(|e| Error::Transaction(format!("Failed to submit transaction: {}", e)))?;
268
269 while let Some(event) = progress.next().await {
271 let event =
272 event.map_err(|e| Error::Transaction(format!("Transaction error: {}", e)))?;
273
274 if event.as_in_block().is_some() {
275 info!("Transaction included in block");
276 }
277
278 if let Some(finalized) = event.as_finalized() {
279 let tx_hash = format!("0x{}", hex::encode(finalized.extrinsic_hash()));
280 info!("Transaction finalized: {}", tx_hash);
281
282 finalized
284 .wait_for_success()
285 .await
286 .map_err(|e| Error::Transaction(format!("Transaction failed: {}", e)))?;
287
288 return Ok(tx_hash);
289 }
290 }
291
292 Err(Error::Transaction(
293 "Transaction stream ended without finalization".to_string(),
294 ))
295 }
296
297 pub async fn estimate_fee(
307 &self,
308 pallet: &str,
309 call: &str,
310 args: Vec<subxt::dynamic::Value>,
311 _from: &Wallet,
312 ) -> Result<u128> {
313 debug!("Estimating fee for {}::{}", pallet, call);
314
315 let tx = subxt::dynamic::tx(pallet, call, args);
317
318 let payload = self
321 .client
322 .tx()
323 .create_unsigned(&tx)
324 .map_err(|e| Error::Transaction(format!("Failed to create unsigned tx: {}", e)))?;
325
326 let encoded = payload.encoded();
328
329 let call_data = {
332 use parity_scale_codec::Encode;
333 let params = (encoded, encoded.len() as u32);
336 params.encode()
337 };
338
339 let result = self
341 .client
342 .runtime_api()
343 .at_latest()
344 .await
345 .map_err(|e| Error::Connection(format!("Failed to get latest block: {}", e)))?
346 .call_raw("TransactionPaymentApi_query_info", Some(&call_data))
347 .await
348 .map_err(|e| Error::Transaction(format!("Failed to query fee info: {}", e)))?;
349
350 if result.len() >= 16 {
356 let fee_bytes = &result[result.len() - 16..];
357 let mut fee_array = [0u8; 16];
358 fee_array.copy_from_slice(fee_bytes);
359 let base_fee = u128::from_le_bytes(fee_array);
360
361 let estimated_fee = (base_fee as f64 * self.fee_config.multiplier) as u128;
363
364 if let Some(max_fee) = self.fee_config.max_fee {
366 if estimated_fee > max_fee {
367 return Err(Error::Transaction(format!(
368 "Estimated fee {} exceeds maximum {}",
369 estimated_fee, max_fee
370 )));
371 }
372 }
373
374 debug!(
375 "Estimated fee: {} (base: {}, multiplier: {})",
376 estimated_fee, base_fee, self.fee_config.multiplier
377 );
378
379 Ok(estimated_fee + self.fee_config.tip)
380 } else {
381 warn!("Unexpected fee query response format, using fallback");
382 Ok(1_000_000u128) }
385 }
386
387 pub async fn estimate_transfer_fee(
389 &self,
390 to: &str,
391 amount: u128,
392 from: &Wallet,
393 ) -> Result<u128> {
394 use sp_core::crypto::{AccountId32, Ss58Codec};
395 let to_account = AccountId32::from_ss58check(to)
396 .map_err(|e| Error::Transaction(format!("Invalid recipient address: {}", e)))?;
397
398 let to_bytes: &[u8] = to_account.as_ref();
399
400 self.estimate_fee(
401 "Balances",
402 "transfer_keep_alive",
403 vec![
404 subxt::dynamic::Value::from_bytes(to_bytes),
405 subxt::dynamic::Value::u128(amount),
406 ],
407 from,
408 )
409 .await
410 }
411
412 pub async fn execute_batch(
421 &self,
422 calls: Vec<BatchCall>,
423 wallet: &Wallet,
424 batch_mode: BatchMode,
425 ) -> Result<String> {
426 debug!(
427 "Executing batch of {} calls with mode {:?}",
428 calls.len(),
429 batch_mode
430 );
431 self.metrics.record_transaction_attempt();
432
433 if calls.is_empty() {
434 return Err(Error::Transaction("Cannot execute empty batch".to_string()));
435 }
436
437 let call_values: Vec<subxt::dynamic::Value> = calls
441 .into_iter()
442 .map(|call| {
443 let mut call_bytes = Vec::new();
445 call_bytes.push(call.pallet_index);
446 call_bytes.push(call.call_index);
447 call_bytes.extend_from_slice(&call.args_encoded);
448
449 subxt::dynamic::Value::from_bytes(&call_bytes)
451 })
452 .collect();
453
454 let calls_value = subxt::dynamic::Value::unnamed_composite(call_values);
456
457 let batch_call_name = match batch_mode {
459 BatchMode::Optimistic => "batch",
460 BatchMode::AllOrNothing => "batch_all",
461 BatchMode::Force => "force_batch",
462 };
463
464 debug!("Using Utility::{} for batch execution", batch_call_name);
465
466 let tx = subxt::dynamic::tx("Utility", batch_call_name, vec![calls_value]);
468
469 let pair = wallet
471 .sr25519_pair()
472 .ok_or_else(|| Error::Transaction("Wallet does not have SR25519 key".to_string()))?;
473
474 let apex_signer = Sr25519Signer::new(pair.clone());
476
477 let mut signed_tx = self
479 .client
480 .tx()
481 .sign_and_submit_then_watch_default(&tx, &apex_signer)
482 .await
483 .map_err(|e| Error::Transaction(format!("Failed to submit batch: {}", e)))?;
484
485 while let Some(event) = signed_tx.next().await {
487 let event =
488 event.map_err(|e| Error::Transaction(format!("Batch transaction error: {}", e)))?;
489
490 if event.as_in_block().is_some() {
491 info!("Batch transaction included in block");
492 }
493
494 if let Some(finalized) = event.as_finalized() {
495 let tx_hash = format!("0x{}", hex::encode(finalized.extrinsic_hash()));
496 info!("Batch transaction finalized: {}", tx_hash);
497
498 finalized
500 .wait_for_success()
501 .await
502 .map_err(|e| Error::Transaction(format!("Batch transaction failed: {}", e)))?;
503
504 self.metrics.record_transaction_success();
505 return Ok(tx_hash);
506 }
507 }
508
509 Err(Error::Transaction(
510 "Batch transaction stream ended without finalization".to_string(),
511 ))
512 }
513
514 pub async fn execute_batch_transfers(
518 &self,
519 transfers: Vec<(String, u128)>, wallet: &Wallet,
521 batch_mode: BatchMode,
522 ) -> Result<String> {
523 use sp_core::crypto::{AccountId32, Ss58Codec};
524
525 let mut calls = Vec::new();
527
528 for (recipient, amount) in transfers {
529 let to_account = AccountId32::from_ss58check(&recipient).map_err(|e| {
530 Error::Transaction(format!("Invalid recipient {}: {}", recipient, e))
531 })?;
532
533 let to_bytes: &[u8] = to_account.as_ref();
534
535 use parity_scale_codec::Encode;
537 let args = (to_bytes, amount).encode();
538
539 calls.push(BatchCall {
540 pallet_index: 5, call_index: 3, args_encoded: args,
543 });
544 }
545
546 self.execute_batch(calls, wallet, batch_mode).await
547 }
548}
549
550#[allow(dead_code)]
552pub struct ExtrinsicBuilder {
553 client: OnlineClient<PolkadotConfig>,
554 pallet: Option<String>,
555 call: Option<String>,
556 args: Vec<subxt::dynamic::Value>,
557}
558
559impl ExtrinsicBuilder {
560 pub fn new(client: OnlineClient<PolkadotConfig>) -> Self {
562 Self {
563 client,
564 pallet: None,
565 call: None,
566 args: Vec::new(),
567 }
568 }
569
570 pub fn pallet(mut self, pallet: impl Into<String>) -> Self {
572 self.pallet = Some(pallet.into());
573 self
574 }
575
576 pub fn call(mut self, call: impl Into<String>) -> Self {
578 self.call = Some(call.into());
579 self
580 }
581
582 pub fn arg(mut self, arg: subxt::dynamic::Value) -> Self {
584 self.args.push(arg);
585 self
586 }
587
588 pub fn args(mut self, args: Vec<subxt::dynamic::Value>) -> Self {
590 self.args.extend(args);
591 self
592 }
593
594 pub fn build(self) -> Result<impl subxt::tx::Payload> {
596 let pallet = self
597 .pallet
598 .ok_or_else(|| Error::Transaction("Pallet not set".to_string()))?;
599 let call = self
600 .call
601 .ok_or_else(|| Error::Transaction("Call not set".to_string()))?;
602
603 Ok(subxt::dynamic::tx(&pallet, &call, self.args))
604 }
605}
606
607#[cfg(test)]
608mod tests {
609 use super::*;
610
611 #[test]
612 fn test_fee_config() {
613 let config = FeeConfig::new()
614 .with_multiplier(1.5)
615 .with_max_fee(1_000_000)
616 .with_tip(100);
617
618 assert_eq!(config.multiplier, 1.5);
619 assert_eq!(config.max_fee, Some(1_000_000));
620 assert_eq!(config.tip, 100);
621 }
622
623 #[test]
624 fn test_retry_config() {
625 let config = RetryConfig::new()
626 .with_max_retries(5)
627 .with_initial_delay(Duration::from_secs(1));
628
629 assert_eq!(config.max_retries, 5);
630 assert_eq!(config.initial_delay, Duration::from_secs(1));
631 }
632
633 #[test]
634 fn test_extrinsic_builder() {
635 let pallet = Some("Balances".to_string());
637 let call = Some("transfer".to_string());
638
639 assert!(pallet.is_some());
640 assert!(call.is_some());
641 }
642}