1use std::fmt;
5use std::path::PathBuf;
6use std::sync::Arc;
7use std::time::Duration;
8
9use thruput_node_supervisor::{BlockDriverConfig, LogLevel, ManualBlockProducer, ProducedBlock};
10
11use thruput_json_rpc_client::native::{
12 NativeClient, NativeClientError,
13 receipt::{ReceiptPollConfig, poll_receipt},
14};
15use thruput_json_rpc_client::{HttpClient, HttpClientBuilder};
16use thruput_json_rpc_types::runtime_manifest::RuntimeManifest;
17use thruput_json_rpc_types::transaction::TxHash;
18use thruput_json_rpc_types::{
19 NetworkHash,
20 requests::{self, EmptyRequest, EventQuery, TransactionQuery},
21 responses::{
22 ChainTip, DecodedResponse, EventPage, SimulationResult, StorageResult, TransactionReceipt,
23 TransactionStatus,
24 },
25};
26use thruput_json_rpc_types::{base58::Base58, hex::HexBytes};
27use thruput_xdr::contract::ContractEntrypointID;
28use thruput_xdr::types::ContractAddress;
29
30use crate::error::{Result, TestkitError};
31
32const RECEIPT_POLL_INTERVAL: Duration = Duration::from_millis(250);
33const RECEIPT_TIMEOUT: Duration = Duration::from_secs(10);
34const PENDING_POLL_INTERVAL: Duration = Duration::from_millis(25);
35const PENDING_POLL_ATTEMPTS: usize = 400; #[derive(Clone)]
43pub struct Network {
44 inner: Arc<NetworkInner>,
45}
46
47#[derive(Debug, Clone)]
49#[must_use = "submitted transactions should be awaited or intentionally retained"]
50pub struct SubmittedTransaction {
51 network: Network,
52 tx_hash: TxHash,
53 tx_hex: String,
54}
55
56impl SubmittedTransaction {
57 pub(crate) fn new(network: Network, tx_hash: TxHash, tx_hex: String) -> Self {
58 Self {
59 network,
60 tx_hash,
61 tx_hex,
62 }
63 }
64
65 pub fn signed_transaction(&self) -> &str {
67 &self.tx_hex
68 }
69
70 pub fn tx_hash(&self) -> TxHash {
72 self.tx_hash
73 }
74
75 pub async fn await_pending(&self) -> Result<()> {
77 self.network.await_pending(self.tx_hash).await
78 }
79
80 pub async fn await_receipt(&self) -> Result<TransactionReceipt> {
82 self.network.await_receipt(self.tx_hash).await
83 }
84}
85
86#[derive(Debug, PartialEq, Eq)]
87struct FundingEvidence {
88 tx_hash: TxHash,
89 signed_transaction: HexBytes,
90}
91
92#[derive(Debug)]
93struct LocalFundingAttempt {
94 request: requests::FundAccountRequest,
95 admitted: Option<thruput_json_rpc_types::responses::FundingSubmission>,
96 evidence: Option<FundingEvidence>,
97}
98
99impl LocalFundingAttempt {
100 fn retained_evidence(&self) -> Option<Box<(TxHash, HexBytes)>> {
101 self.evidence
102 .as_ref()
103 .map(|e| Box::new((e.tx_hash, e.signed_transaction.clone())))
104 }
105
106 fn accept_response(
107 &mut self,
108 response: &thruput_json_rpc_types::responses::FundingSubmission,
109 ) -> Result<()> {
110 validate_funding_response(response, &self.request).map_err(|error| {
111 TestkitError::InvalidFundingResponse {
112 response: Box::new(response.clone()),
113 retained: self.retained_evidence(),
114 detail: error.to_string(),
115 }
116 })?;
117 let evidence = FundingEvidence {
118 tx_hash: response.transaction_hash,
119 signed_transaction: response.signed_transaction.clone(),
120 };
121 if self
122 .evidence
123 .as_ref()
124 .is_some_and(|original| original != &evidence)
125 {
126 return Err(TestkitError::InvalidFundingResponse {
127 response: Box::new(response.clone()),
128 retained: self.retained_evidence(),
129 detail: "local funding replaced retained signed evidence".into(),
130 });
131 }
132 self.admitted = Some(response.clone());
133 Ok(())
134 }
135
136 fn retain_error_evidence(&mut self, error: &NativeClientError) -> Result<()> {
137 use thruput_json_rpc_types::error::native::{DecodedError, ErrorData};
138 if let NativeClientError::Application { data, .. } = error
139 && let DecodedError::Known { data, .. } = data.as_ref()
140 {
141 let (tx_hash, signed_transaction) = match data.as_ref() {
142 ErrorData::LocalFundingUnresolved {
143 transaction_hash,
144 signed_transaction,
145 ..
146 } => (*transaction_hash, signed_transaction.clone()),
147 ErrorData::FundingAttemptStopped(stopped) => {
148 let (Some(hash), Some(bytes)) =
149 (stopped.transaction_hash, &stopped.signed_transaction)
150 else {
151 return Ok(());
152 };
153 (hash, bytes.clone())
154 }
155 _ => return Ok(()),
156 };
157 let evidence = FundingEvidence {
158 tx_hash,
159 signed_transaction,
160 };
161 validate_funding_evidence(&evidence)?;
162 if self
163 .evidence
164 .as_ref()
165 .is_some_and(|original| original != &evidence)
166 {
167 return Err(TestkitError::signing(
168 "local funding replaced retained signed evidence",
169 ));
170 }
171 self.evidence = Some(evidence);
172 }
173 Ok(())
174 }
175}
176
177struct NetworkInner {
178 rpc_url: Option<String>,
179 client: Option<NativeClient<HttpClient>>,
180 network_hash: Option<NetworkHash>,
181 faucet: tokio::sync::Mutex<Option<crate::manifest::TestAccount>>,
182 local_faucet: Option<ContractAddress>,
183 local_funding:
184 tokio::sync::Mutex<std::collections::HashMap<ContractAddress, LocalFundingAttempt>>,
185 manual_block_producer: Option<ManualBlockProducer>,
186}
187
188impl fmt::Debug for Network {
189 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190 f.debug_struct("Network")
191 .field("rpc_url", &self.inner.rpc_url)
192 .field("online", &self.inner.client.is_some())
193 .field(
194 "manual_block_production",
195 &self.inner.manual_block_producer.is_some(),
196 )
197 .finish_non_exhaustive()
198 }
199}
200
201impl Network {
202 pub(crate) async fn connect(
205 rpc_url: &str,
206 manifest: &RuntimeManifest,
207 manual_block_producer: Option<ManualBlockProducer>,
208 ) -> Result<Self> {
209 let transport = HttpClientBuilder::default()
210 .build(rpc_url)
211 .map_err(|e| TestkitError::rpc("connect", e))?;
212 let client = NativeClient::new(transport);
213 thruput_client::check_endpoint(
214 &client,
215 &[
216 "getNetworkInfo",
217 "sendTransaction",
218 "getTransactionStatus",
219 "getTransactionReceipt",
220 ],
221 )
222 .await
223 .map_err(TestkitError::native)?;
224 thruput_client::validate_network(&client, &manifest.network_hash.to_network_hash())
225 .await
226 .map_err(|error| match error {
227 thruput_client::NativeRpcError::NetworkMismatch { .. } => TestkitError::Manifest {
228 field: "networkHash",
229 detail: format!("{error} at {rpc_url}"),
230 },
231 error => TestkitError::native(error),
232 })?;
233 Ok(Self {
234 inner: Arc::new(NetworkInner {
235 rpc_url: Some(rpc_url.to_owned()),
236 client: Some(client),
237 network_hash: Some(Base58(manifest.network_hash.to_network_hash().into_inner())),
238 faucet: tokio::sync::Mutex::new(crate::manifest::derive_faucet(manifest)?),
239 local_faucet: local_faucet(manifest)?,
240 local_funding: tokio::sync::Mutex::new(std::collections::HashMap::new()),
241 manual_block_producer,
242 }),
243 })
244 }
245
246 pub(crate) fn offline() -> Self {
248 Self {
249 inner: Arc::new(NetworkInner {
250 rpc_url: None,
251 client: None,
252 network_hash: None,
253 faucet: tokio::sync::Mutex::new(None),
254 local_faucet: None,
255 local_funding: tokio::sync::Mutex::new(std::collections::HashMap::new()),
256 manual_block_producer: None,
257 }),
258 }
259 }
260
261 pub fn rpc_url(&self) -> Option<&str> {
263 self.inner.rpc_url.as_deref()
264 }
265
266 pub fn client(&self) -> Option<&NativeClient<HttpClient>> {
268 self.inner.client.as_ref()
269 }
270
271 pub async fn chain_tip(&self) -> Result<ChainTip> {
273 known(
274 self.require_client("getChainTip")?
275 .get_chain_tip(EmptyRequest {})
276 .await,
277 "getChainTip",
278 )
279 }
280
281 pub async fn submit_raw(&self, tx_hex: String) -> Result<TxHash> {
283 let bytes = thruput_json_rpc_types::hex::decode_prefixed_hex(&tx_hex)
284 .map_err(TestkitError::signing)?;
285 let signed: thruput_xdr::transaction::SignedTransaction =
286 xdrs::txs::decode(&bytes).map_err(TestkitError::signing)?;
287 let hash = TxHash(Base58(
288 thruput_signing::transaction_hash(&signed.tx).map_err(TestkitError::signing)?,
289 ));
290 let canonical = thruput_json_rpc_types::hex::prefixed_hex(&bytes);
291 crate::account::submit_signed(self, hash, &canonical).await?;
292 Ok(hash)
293 }
294
295 pub async fn receipt(&self, tx_hash: TxHash) -> Result<Option<TransactionReceipt>> {
297 let receipt: Option<TransactionReceipt> = known(
298 self.require_client("getTransactionReceipt")?
299 .get_transaction_receipt(TransactionQuery {
300 transaction_hash: tx_hash,
301 })
302 .await,
303 "getTransactionReceipt",
304 )?;
305 if let Some(observed) = &receipt
306 && observed.transaction_hash != tx_hash
307 {
308 return Err(TestkitError::ReceiptHashMismatch {
309 expected: tx_hash,
310 receipt: Box::new(observed.clone()),
311 });
312 }
313 if receipt
314 .as_ref()
315 .is_some_and(|r| r.transaction_hash == tx_hash)
316 {
317 self.inner.local_funding.lock().await.retain(|_, attempt| {
318 !attempt
319 .admitted
320 .as_ref()
321 .is_some_and(|admitted| admitted.transaction_hash == tx_hash)
322 && !attempt
323 .evidence
324 .as_ref()
325 .is_some_and(|evidence| evidence.tx_hash == tx_hash)
326 });
327 }
328 Ok(receipt)
329 }
330
331 pub async fn await_receipt(&self, tx_hash: TxHash) -> Result<TransactionReceipt> {
334 let client = self.require_client("getTransactionReceipt")?;
335 let network_hash = self.inner.network_hash.ok_or(TestkitError::Offline {
336 operation: "getTransactionReceipt",
337 })?;
338 let receipt = poll_receipt(
339 client,
340 network_hash,
341 tx_hash,
342 None,
343 ReceiptPollConfig {
344 timeout: RECEIPT_TIMEOUT,
345 interval: RECEIPT_POLL_INTERVAL,
346 },
347 std::future::pending(),
348 )
349 .await
350 .map_err(|source| TestkitError::ReceiptWait {
351 source: Box::new(source),
352 })?;
353 self.inner.local_funding.lock().await.retain(|_, attempt| {
354 !attempt
355 .admitted
356 .as_ref()
357 .is_some_and(|r| r.transaction_hash == tx_hash)
358 && !attempt
359 .evidence
360 .as_ref()
361 .is_some_and(|r| r.tx_hash == tx_hash)
362 });
363 Ok(receipt)
364 }
365
366 pub async fn transaction_status(&self, tx_hash: TxHash) -> Result<TransactionStatus> {
368 known(
369 self.require_client("getTransactionStatus")?
370 .get_transaction_status(TransactionQuery {
371 transaction_hash: tx_hash,
372 })
373 .await,
374 "getTransactionStatus",
375 )
376 }
377
378 pub async fn await_pending(&self, tx_hash: TxHash) -> Result<()> {
386 self.await_pending_inner(tx_hash).await
387 }
388
389 async fn await_pending_inner(&self, tx_hash: TxHash) -> Result<()> {
390 let mut last_status = TransactionStatus::Unknown {};
391 for _ in 0..PENDING_POLL_ATTEMPTS {
392 let status = self.transaction_status(tx_hash).await?;
393 match classify_pending_status(&status) {
394 PendingStatusClassification::Ready => return Ok(()),
395 PendingStatusClassification::Retry => {
396 last_status = status;
397 tokio::time::sleep(PENDING_POLL_INTERVAL).await;
398 }
399 PendingStatusClassification::Terminal => {
400 return Err(TestkitError::TransactionNotPending {
401 tx_hash: format!("{tx_hash:?}"),
402 status: Box::new(status),
403 });
404 }
405 }
406 }
407 Err(TestkitError::PendingTimeout {
408 tx_hash: format!("{tx_hash:?}"),
409 last_status: Box::new(last_status),
410 })
411 }
412
413 pub async fn produce_block(&self) -> Result<ProducedBlock> {
415 self.inner
416 .manual_block_producer
417 .as_ref()
418 .ok_or(TestkitError::ManualBlockProductionUnavailable)?
419 .produce_block()
420 .await
421 .map_err(|source| TestkitError::BlockProduction { source })
422 }
423
424 pub async fn get_storage_at(
426 &self,
427 address: ContractAddress,
428 key: &[u8],
429 ) -> Result<StorageResult> {
430 known(
431 self.require_client("getStorage")?
432 .get_storage(requests::StorageQuery {
433 address: Base58(address),
434 key: HexBytes(key.to_vec()),
435 })
436 .await,
437 "getStorage",
438 )
439 }
440
441 pub async fn view_call(
446 &self,
447 contract: ContractAddress,
448 entrypoint: ContractEntrypointID,
449 calldata: &[u8],
450 caller: Option<ContractAddress>,
451 ) -> Result<SimulationResult> {
452 let request = requests::SimulateInvocationRequest {
453 assumed_auth: None,
454 min_state_block_height: None,
455 address: Base58(contract),
456 entrypoint: HexBytes(entrypoint.into_inner().to_vec()),
457 calldata: HexBytes(calldata.to_vec()),
458 caller: caller.map(Base58),
459 gas_limit: None,
460 };
461 known(
462 self.require_client("simulateInvocation")?
463 .simulate_invocation(request)
464 .await,
465 "simulateInvocation",
466 )
467 }
468
469 pub async fn get_events(&self, query: EventQuery) -> Result<EventPage> {
471 known(
472 self.require_client("getEvents")?.get_events(query).await,
473 "getEvents",
474 )
475 }
476
477 pub(crate) async fn submit_fund(
478 &self,
479 recipient: ContractAddress,
480 amount: u128,
481 ) -> Result<SubmittedTransaction> {
482 self.require_client("fund")?;
483 if let Some(faucet) = self.inner.local_faucet {
484 return self.submit_local_fund(recipient, amount, faucet).await;
485 }
486 let calldata = thruput_signing::native_token_transfer_calldata(&recipient, amount)
487 .map_err(TestkitError::signing)?;
488 let faucet = self.inner.faucet.lock().await;
489 let faucet = faucet.as_ref().ok_or(TestkitError::MissingFaucet)?;
490 let mut nonce = [0; 8];
493 getrandom::fill(&mut nonce).map_err(|error| {
494 TestkitError::signing(format!("generate faucet transaction nonce: {error}"))
495 })?;
496 let (tx_hash, tx_hex) = crate::account::prepare_call(
497 faucet,
498 thruput_xdr::genesis_addresses::ADDRESS_NATIVE_TOKEN,
499 thruput_signing::NATIVE_TOKEN_TRANSFER_ENTRYPOINT,
500 &calldata,
501 u64::from_le_bytes(nonce),
502 )?;
503 crate::account::submit_signed(self, tx_hash, &tx_hex).await?;
504 Ok(SubmittedTransaction::new(self.clone(), tx_hash, tx_hex))
505 }
506
507 async fn submit_local_fund(
511 &self,
512 recipient: ContractAddress,
513 amount: u128,
514 faucet: ContractAddress,
515 ) -> Result<SubmittedTransaction> {
516 use thruput_json_rpc_types::{airdrop::ClientNonce, requests::FundAccountRequest};
517 if amount == 0 {
518 return Err(TestkitError::signing("funding amount must be positive"));
519 }
520 let mut pending = self.inner.local_funding.lock().await;
521 if let Some(request) = pending.get(&recipient) {
522 if request.request.raw_amount != amount {
523 return Err(TestkitError::FundingRequestMismatch { recipient });
524 }
525 } else {
526 let mut nonce = [0; 16];
527 getrandom::fill(&mut nonce).map_err(TestkitError::signing)?;
528 pending.insert(
529 recipient,
530 LocalFundingAttempt {
531 request: FundAccountRequest {
532 recipient: Base58(recipient),
533 raw_amount: amount,
534 faucet: Base58(faucet),
535 client_nonce: ClientNonce::new(nonce.to_vec())
536 .map_err(TestkitError::signing)?,
537 },
538 admitted: None,
539 evidence: None,
540 },
541 );
542 }
543 let attempt = pending
544 .get(&recipient)
545 .expect("bug: retained funding intent");
546 let request = attempt.request.clone();
547 let response = if let Some(response) = &attempt.admitted {
548 response.clone()
549 } else {
550 match self
551 .require_client("fund")?
552 .fund_account(request.clone())
553 .await
554 {
555 Ok(response) => known(Ok(response), "dev_fundAccount")?,
556 Err(error) => {
557 let attempt = pending
558 .get_mut(&recipient)
559 .expect("bug: retained funding intent");
560 if let Err(invalid) = attempt.retain_error_evidence(&error) {
561 return Err(TestkitError::InvalidFundingEvidence {
562 detail: invalid.to_string(),
563 retained: attempt.retained_evidence(),
564 source: Box::new(error),
565 });
566 }
567 return Err(match &attempt.evidence {
568 Some(evidence) => TestkitError::Submission {
569 tx_hash: evidence.tx_hash,
570 signed_transaction: thruput_json_rpc_types::hex::prefixed_hex(
571 &evidence.signed_transaction.0,
572 ),
573 detail: error.to_string(),
574 source: Box::new(error),
575 },
576 None => TestkitError::NativeCall {
577 method: "dev_fundAccount",
578 source: Box::new(error),
579 },
580 });
581 }
582 }
583 };
584 pending
585 .get_mut(&recipient)
586 .expect("bug: retained funding intent")
587 .accept_response(&response)?;
588 Ok(SubmittedTransaction::new(
589 self.clone(),
590 response.transaction_hash,
591 thruput_json_rpc_types::hex::prefixed_hex(&response.signed_transaction.0),
592 ))
593 }
594
595 pub(crate) fn require_client(
596 &self,
597 operation: &'static str,
598 ) -> Result<&NativeClient<HttpClient>> {
599 self.inner
600 .client
601 .as_ref()
602 .ok_or(TestkitError::Offline { operation })
603 }
604}
605
606fn known<T>(
607 result: std::result::Result<DecodedResponse<T>, NativeClientError>,
608 method: &'static str,
609) -> Result<T> {
610 match result {
611 Ok(DecodedResponse::Known(value)) => Ok(value),
612 Ok(DecodedResponse::Unsupported { raw }) => {
613 Err(TestkitError::UnsupportedResponse { method, raw })
614 }
615 Ok(_) => Err(TestkitError::rpc(
616 method,
617 "unsupported response representation",
618 )),
619 Err(source) => Err(TestkitError::NativeCall {
620 method,
621 source: Box::new(source),
622 }),
623 }
624}
625
626fn validate_funding_response(
627 response: &thruput_json_rpc_types::responses::FundingSubmission,
628 request: &requests::FundAccountRequest,
629) -> Result<()> {
630 let signed: thruput_xdr::transaction::SignedTransaction =
631 xdrs::txs::decode(&response.signed_transaction.0).map_err(TestkitError::signing)?;
632 let hash = thruput_signing::transaction_hash(&signed.tx).map_err(TestkitError::signing)?;
633 if response.recipient != request.recipient
634 || response.raw_amount != request.raw_amount
635 || response.transaction_hash.0.0 != hash
636 {
637 return Err(TestkitError::signing(
638 "local funding response does not match retained request and signed hash",
639 ));
640 }
641 Ok(())
642}
643
644fn validate_funding_evidence(evidence: &FundingEvidence) -> Result<()> {
645 let signed: thruput_xdr::transaction::SignedTransaction =
646 xdrs::txs::decode(&evidence.signed_transaction.0).map_err(TestkitError::signing)?;
647 let hash = thruput_signing::transaction_hash(&signed.tx).map_err(TestkitError::signing)?;
648 if evidence.tx_hash.0.0 != hash {
649 return Err(TestkitError::signing(
650 "local funding evidence hash does not match signed bytes",
651 ));
652 }
653 Ok(())
654}
655
656fn local_faucet(manifest: &RuntimeManifest) -> Result<Option<ContractAddress>> {
657 use thruput_json_rpc_types::runtime_manifest::{
658 BOOTSTRAP_ACCOUNT_ROLE_FAUCET, HOGD_DEV_RUNTIME_MANIFEST_KIND,
659 };
660 if manifest.kind != HOGD_DEV_RUNTIME_MANIFEST_KIND {
661 return Ok(None);
662 }
663 let mut faucets = manifest
664 .bootstrap_accounts
665 .iter()
666 .filter(|a| a.role == BOOTSTRAP_ACCOUNT_ROLE_FAUCET);
667 let faucet = faucets.next().ok_or(TestkitError::MissingFaucet)?;
668 if faucets.next().is_some()
669 || thruput_signing::PubkeyAccount::from_public_key_bytes(*faucet.public_key.0, 0).address()
670 != faucet.wallet_address.0
671 {
672 return Err(TestkitError::Manifest {
673 field: "bootstrapAccounts",
674 detail: "local faucet must uniquely match its public nonce-zero account".into(),
675 });
676 }
677 Ok(Some(faucet.wallet_address.0))
678}
679
680#[derive(Debug, Clone, Copy, PartialEq, Eq)]
681enum PendingStatusClassification {
682 Ready,
683 Retry,
684 Terminal,
685}
686
687fn classify_pending_status(status: &TransactionStatus) -> PendingStatusClassification {
688 match status {
689 TransactionStatus::Pending { .. } => PendingStatusClassification::Ready,
690 TransactionStatus::Unknown {} => PendingStatusClassification::Retry,
691 _ => PendingStatusClassification::Terminal,
692 }
693}
694
695#[cfg(test)]
696mod tests {
697 use thruput_json_rpc_types::responses::TransactionStatus;
698 use thruput_node_supervisor::BlockDriverMode;
699
700 use super::{
701 LocalNetworkConfig, Network, PendingStatusClassification, classify_pending_status,
702 };
703 use crate::TestkitError;
704
705 #[test]
706 fn local_network_keeps_instant_block_production_by_default() {
707 assert_eq!(
708 LocalNetworkConfig::default().block_driver.mode,
709 BlockDriverMode::Instant
710 );
711 }
712
713 #[test]
714 fn pending_status_classification_never_resubmits() {
715 use thruput_json_rpc_types::responses::{SchedulerError, SchedulerErrorSource};
716 assert_eq!(
717 classify_pending_status(&TransactionStatus::Pending {
718 recent_failure: None
719 }),
720 PendingStatusClassification::Ready
721 );
722 assert_eq!(
723 classify_pending_status(&TransactionStatus::Unknown {}),
724 PendingStatusClassification::Retry
725 );
726 for status in [
727 TransactionStatus::Committed {
728 block_height: 2.into(),
729 },
730 TransactionStatus::Retry {
731 recent_failure: None,
732 error: SchedulerError {
733 source: SchedulerErrorSource::Executor,
734 code: 7,
735 },
736 },
737 TransactionStatus::Dropped {
738 recent_failure: None,
739 error: SchedulerError {
740 source: SchedulerErrorSource::Executor,
741 code: 7,
742 },
743 },
744 ] {
745 assert_eq!(
746 classify_pending_status(&status),
747 PendingStatusClassification::Terminal
748 );
749 }
750 }
751
752 #[tokio::test]
753 async fn offline_network_rejects_manual_block_production() {
754 let error = Network::offline()
755 .produce_block()
756 .await
757 .expect_err("offline network must not expose manual production");
758 assert!(matches!(
759 error,
760 TestkitError::ManualBlockProductionUnavailable
761 ));
762 }
763}
764
765#[derive(Debug, Clone)]
767#[non_exhaustive]
768pub enum NetworkConfig {
769 Existing {
771 rpc_url: String,
773 runtime_manifest: PathBuf,
775 },
776 Local(LocalNetworkConfig),
778 Offline {
783 accounts: usize,
785 },
786}
787
788impl NetworkConfig {
789 pub fn existing(rpc_url: impl Into<String>, runtime_manifest: impl Into<PathBuf>) -> Self {
791 Self::Existing {
792 rpc_url: rpc_url.into(),
793 runtime_manifest: runtime_manifest.into(),
794 }
795 }
796
797 pub fn local(config: LocalNetworkConfig) -> Self {
799 Self::Local(config)
800 }
801
802 pub fn offline(accounts: usize) -> Self {
804 Self::Offline { accounts }
805 }
806}
807
808#[derive(Debug, Clone)]
814#[non_exhaustive]
815pub struct LocalNetworkConfig {
816 pub hog_devnode_binary: Option<PathBuf>,
820 pub contract_artifact_directories: Vec<PathBuf>,
822 pub rpc_port: u16,
824 pub hogd_protocol: Option<String>,
829 pub log_level: Option<LogLevel>,
834 pub initial_account_tokens: u64,
836 pub startup_timeout: Duration,
838 pub block_driver: BlockDriverConfig,
840}
841
842impl Default for LocalNetworkConfig {
843 fn default() -> Self {
844 Self {
845 hog_devnode_binary: None,
846 contract_artifact_directories: Vec::new(),
847 rpc_port: 0,
848 hogd_protocol: None,
849 log_level: None,
850 initial_account_tokens: thruput_node_supervisor::config::DEFAULT_LOCAL_ACCOUNT_TOKENS,
851 startup_timeout: Duration::from_secs(10),
852 block_driver: BlockDriverConfig::default(),
853 }
854 }
855}