1use alloy_primitives::Address;
2use alloy_provider::network::AnyNetwork;
3use alloy_provider::{
4 PendingTransactionError, Provider, WsConnect,
5 network::{ReceiptResponse, TransactionBuilder},
6};
7use alloy_rpc_types::serde_helpers::WithOtherFields;
8use alloy_signer_local::PrivateKeySigner;
9use alloy_sol_types::{SolConstructor, sol};
10use blueprint_clients::tangle::client::{TangleClient as TestClient, TangleConfig};
11use blueprint_core::{error, info};
12use sp_core::H160;
13use tangle_subxt::subxt::{
14 Config,
15 blocks::ExtrinsicEvents,
16 client::OnlineClientT,
17 tx::{TxProgress, signer::Signer},
18 utils::AccountId32,
19};
20use tangle_subxt::tangle_testnet_runtime::api::assets::events::created::AssetId;
21use tangle_subxt::tangle_testnet_runtime::api::runtime_types::tangle_primitives::services::pricing::PricingQuote;
22use tangle_subxt::tangle_testnet_runtime::api::runtime_types::tangle_primitives::services::types::AssetSecurityCommitment;
23use tangle_subxt::tangle_testnet_runtime::api::{
24 self,
25 runtime_types::{
26 pallet_services::module::Call,
27 sp_arithmetic::per_things::Percent,
28 tangle_primitives::services::types::{Asset, AssetSecurityRequirement, MembershipModel},
29 tangle_testnet_runtime::RuntimeCall,
30 },
31 services::{
32 calls::types::{
33 call::{Args, Job},
34 create_blueprint::Blueprint,
35 register::{Preferences, RegistrationArgs},
36 request::RequestArgs,
37 },
38 events::{JobCalled, JobResultSubmitted, MasterBlueprintServiceManagerRevised},
39 },
40};
41use blueprint_core::debug;
42
43#[derive(Debug, thiserror::Error)]
44pub enum TransactionError {
45 #[error("Failed to find `JobCalled` event")]
46 NoJobCalled,
47 #[error("Failed to get job result")]
48 NoJobResult,
49 #[error("Service not found")]
50 ServiceNotFound,
51 #[error("Created service does not match blueprint ID")]
52 ServiceIdMismatch,
53 #[error(transparent)]
54 Rpc(#[from] alloy_transport::RpcError<alloy_transport::TransportErrorKind>),
55 #[error(transparent)]
56 PendingTransaction(#[from] PendingTransactionError),
57 #[error(transparent)]
58 Subxt(#[from] tangle_subxt::subxt::Error),
59 #[error("{0}")]
60 Other(String),
61}
62
63sol! {
64 constructor(address payable _protocolFeesReceiver);
65}
66
67pub async fn deploy_new_mbsm_revision<T: Signer<TangleConfig>>(
68 evm_rpc_endpoint: &str,
69 client: &TestClient,
70 account_id: &T,
71 signer_evm: PrivateKeySigner,
72 bytecode: &[u8],
73 protocol_fees_receiver: Address,
74) -> Result<MasterBlueprintServiceManagerRevised, TransactionError> {
75 info!("Deploying new MBSM revision ...");
76
77 let wallet = alloy_provider::network::EthereumWallet::from(signer_evm);
78 let provider = alloy_provider::ProviderBuilder::new()
79 .network::<AnyNetwork>()
80 .wallet(wallet)
81 .connect_ws(WsConnect::new(evm_rpc_endpoint))
82 .await?;
83
84 let constructor_call = constructorCall {
85 _protocolFeesReceiver: protocol_fees_receiver,
86 };
87 let encoded_constructor = constructor_call.abi_encode();
88 debug!("Encoded constructor: {encoded_constructor:?}");
89
90 let deploy_code = [bytecode, encoded_constructor.as_ref()].concat();
91 debug!("Deploy code length: {:?}", deploy_code.len());
92
93 let tx = alloy_rpc_types::TransactionRequest::default()
94 .with_deploy_code(deploy_code)
95 .with_gas_limit(5_000_000);
96 let send_result = provider.send_transaction(WithOtherFields::new(tx)).await;
97 let tx = match send_result {
98 Ok(tx) => tx,
99 Err(err) => {
100 error!("Failed to send transaction: {err}");
101 return Err(err.into());
102 }
103 };
104
105 let tx_result = tx.get_receipt().await;
107 let receipt = match tx_result {
108 Ok(receipt) => receipt,
109 Err(err) => {
110 error!("Failed to deploy MBSM Contract: {err}");
111 return Err(err.into());
112 }
113 };
114
115 let mbsm_address = if receipt.status() {
117 ReceiptResponse::contract_address(&receipt).unwrap()
118 } else {
119 error!("MBSM Contract deployment failed!");
120 error!("Receipt: {receipt:#?}");
121 return Err(TransactionError::Other(
122 "MBSM Contract deployment failed!".into(),
123 ));
124 };
125
126 info!("MBSM Contract deployed at: {mbsm_address}");
127 let sudo_call = api::tx().sudo().sudo(RuntimeCall::Services(
128 Call::update_master_blueprint_service_manager {
129 address: mbsm_address.0.0.into(),
130 },
131 ));
132 let res = client
133 .subxt_client()
134 .tx()
135 .sign_and_submit_then_watch_default(&sudo_call, account_id)
136 .await?;
137 let evts = wait_for_in_block_success(res).await?;
138 let ev = evts.find_first::<MasterBlueprintServiceManagerRevised>()?;
139 match ev {
140 Some(ev) => Ok(ev),
141 None => Err(TransactionError::Other(
142 "no MBSM Revised Event emitted".into(),
143 )),
144 }
145}
146
147pub async fn create_blueprint<T: Signer<TangleConfig>>(
153 client: &TestClient,
154 account_id: &T,
155 blueprint: Blueprint,
156) -> Result<(), TransactionError> {
157 let call = api::tx().services().create_blueprint(blueprint);
158 let res = client
159 .subxt_client()
160 .tx()
161 .sign_and_submit_then_watch_default(&call, account_id)
162 .await?;
163 wait_for_in_block_success(res).await?;
164 Ok(())
165}
166
167pub async fn join_operators<T: Signer<TangleConfig>>(
173 client: &TestClient,
174 account_id: &T,
175) -> Result<(), TransactionError> {
176 info!("Joining operators ...");
177 let call_pre = api::tx()
178 .multi_asset_delegation()
179 .join_operators(1_000_000_000_000_000);
180 let res_pre = client
181 .subxt_client()
182 .tx()
183 .sign_and_submit_then_watch_default(&call_pre, account_id)
184 .await?;
185
186 wait_for_in_block_success(res_pre).await?;
187 Ok(())
188}
189
190pub async fn register_for_blueprint<T: Signer<TangleConfig>>(
196 client: &TestClient,
197 account_id: &T,
198 blueprint_id: u64,
199 preferences: Preferences,
200 registration_args: RegistrationArgs,
201 value: u128,
202) -> Result<(), TransactionError> {
203 info!("Registering to blueprint {blueprint_id} to become an operator ...");
204 let call = api::tx()
205 .services()
206 .register(blueprint_id, preferences, registration_args, value);
207 let res = client
208 .subxt_client()
209 .tx()
210 .sign_and_submit_then_watch_default(&call, account_id)
211 .await?;
212 wait_for_in_block_success(res).await?;
213 Ok(())
214}
215
216pub async fn submit_job<T: Signer<TangleConfig>>(
225 client: &TestClient,
226 user: &T,
227 service_id: u64,
228 job_id: Job,
229 job_params: Args,
230 _call_id: u64, ) -> Result<JobCalled, TransactionError> {
232 let call = api::tx().services().call(service_id, job_id, job_params);
233 let events = client
234 .subxt_client()
235 .tx()
236 .sign_and_submit_then_watch_default(&call, user)
237 .await?
238 .wait_for_finalized_success()
239 .await?;
240
241 let job_called_events = events.find::<JobCalled>().collect::<Vec<_>>();
242 for job_called in job_called_events {
243 let job_called = job_called?;
244 if job_called.service_id == service_id
245 && job_called.job == job_id
246 && user.account_id() == job_called.caller
247 {
248 return Ok(job_called);
249 }
250 }
251
252 Err(TransactionError::NoJobCalled)
253}
254
255#[allow(clippy::cast_possible_truncation)]
265pub async fn request_service<T: Signer<TangleConfig>>(
266 client: &TestClient,
267 user: &T,
268 blueprint_id: u64,
269 test_nodes: Vec<AccountId32>,
270 request_args: RequestArgs,
271 value: u128,
272 optional_assets: Option<Vec<AssetSecurityRequirement<AssetId>>>,
273) -> Result<(), TransactionError> {
274 info!(requester = ?user.account_id(), ?test_nodes, %blueprint_id, "Requesting service");
275 let min_operators = test_nodes.len() as u32;
276 let security_requirements = optional_assets.unwrap_or_else(|| {
277 vec![AssetSecurityRequirement {
278 asset: Asset::Custom(0),
279 min_exposure_percent: Percent(50),
280 max_exposure_percent: Percent(80),
281 }]
282 });
283 let call = api::tx().services().request(
284 None,
285 blueprint_id,
286 Vec::new(),
287 test_nodes,
288 request_args,
289 security_requirements,
290 1000,
291 Asset::Custom(0),
292 value,
293 MembershipModel::Fixed { min_operators },
294 );
295 let res = client
296 .subxt_client()
297 .tx()
298 .sign_and_submit_then_watch_default(&call, user)
299 .await?;
300 wait_for_in_block_success(res).await?;
301 Ok(())
302}
303
304async fn wait_for_in_block_success<T: Config, C: OnlineClientT<T>>(
305 mut res: TxProgress<T, C>,
306) -> Result<ExtrinsicEvents<T>, TransactionError> {
307 let mut val = Err("Failed to get in block success".into());
308 while let Some(Ok(event)) = res.next().await {
309 let Some(block) = event.as_in_block() else {
310 continue;
311 };
312 val = block.wait_for_success().await;
313 }
314
315 val.map_err(Into::into)
316}
317
318pub async fn wait_for_completion_of_tangle_job(
319 client: &TestClient,
320 service_id: u64,
321 call_id: u64,
322 required_count: usize,
323) -> Result<JobResultSubmitted, TransactionError> {
324 let mut count = 0;
325 let mut blocks = client.subxt_client().blocks().subscribe_best().await?;
326 while let Some(Ok(block)) = blocks.next().await {
327 let events = block.events().await?;
328 let results = events.find::<JobResultSubmitted>().collect::<Vec<_>>();
329 info!(
330 %service_id,
331 %call_id,
332 %required_count,
333 %count,
334 "Waiting for job completion. Found {} results ...",
335 results.len()
336 );
337 for result in results {
338 match result {
339 Ok(result) => {
340 if result.service_id == service_id && result.call_id == call_id {
341 count += 1;
342 if count == required_count {
343 return Ok(result);
344 }
345 }
346 }
347 Err(err) => {
348 error!("Failed to get job result: {err}");
349 }
350 }
351 }
352 }
353
354 Err(TransactionError::NoJobResult)
355}
356
357async fn get_next_service_id(client: &TestClient) -> Result<u64, TransactionError> {
358 let call = api::storage().services().next_instance_id();
359 let res = client
360 .subxt_client()
361 .storage()
362 .at_latest()
363 .await?
364 .fetch_or_default(&call)
365 .await?;
366 Ok(res)
367}
368
369pub async fn get_latest_mbsm_revision(
370 client: &TestClient,
371) -> Result<Option<(u64, H160)>, TransactionError> {
372 let call = api::storage()
373 .services()
374 .master_blueprint_service_manager_revisions();
375 let mut res = client
376 .subxt_client()
377 .storage()
378 .at_latest()
379 .await?
380 .fetch_or_default(&call)
381 .await?;
382 let ver = res.0.len() as u64;
383 Ok(res.0.pop().map(|addr| (ver, addr.0.into())))
384}
385
386#[must_use]
387pub fn get_security_requirement(a: AssetId, p: &[u8; 2]) -> AssetSecurityRequirement<AssetId> {
388 AssetSecurityRequirement {
389 asset: Asset::Custom(a),
390 min_exposure_percent: Percent(p[0]),
391 max_exposure_percent: Percent(p[1]),
392 }
393}
394
395#[must_use]
396pub fn get_security_commitment(a: Asset<AssetId>, p: u8) -> AssetSecurityCommitment<AssetId> {
397 AssetSecurityCommitment {
398 asset: a,
399 exposure_percent: Percent(p),
400 }
401}
402
403async fn approve_service<T: Signer<TangleConfig>>(
405 client: &TestClient,
406 caller: &T,
407 request_id: u64,
408 restaking_percent: u8,
409 optional_assets: Option<Vec<AssetSecurityCommitment<AssetId>>>,
410) -> Result<(), TransactionError> {
411 info!("Approving service request ...");
412 let native_security_commitments =
413 vec![get_security_commitment(Asset::Custom(0), restaking_percent)];
414 let security_commitments = match optional_assets {
415 Some(assets) => [native_security_commitments, assets].concat(),
416 None => native_security_commitments,
417 };
418
419 let call = api::tx()
420 .services()
421 .approve(request_id, security_commitments);
422 let res = client
423 .subxt_client()
424 .tx()
425 .sign_and_submit_then_watch_default(&call, caller)
426 .await?;
427 res.wait_for_finalized_success().await?;
428 Ok(())
429}
430
431async fn get_next_request_id(client: &TestClient) -> Result<u64, TransactionError> {
432 info!("Fetching next request ID ...");
433 let next_request_id_addr = api::storage().services().next_service_request_id();
434 let next_request_id = client
435 .subxt_client()
436 .storage()
437 .at_latest()
438 .await?
439 .fetch_or_default(&next_request_id_addr)
440 .await?;
441 Ok(next_request_id)
442}
443
444pub async fn request_service_for_operators<T: Signer<TangleConfig>>(
445 clients: &[TestClient],
446 sr25519_signers: &[T],
447 blueprint_id: u64,
448 request_args: RequestArgs,
449) -> Result<u64, TransactionError> {
450 let alice_signer = sr25519_signers
451 .first()
452 .ok_or(TransactionError::Other("No signers".to_string()))?;
453
454 let alice_client = clients
455 .first()
456 .ok_or(TransactionError::Other("No client".to_string()))?;
457
458 let prev_service_id = get_next_service_id(alice_client).await?;
460
461 let accounts = sr25519_signers
462 .iter()
463 .map(Signer::account_id)
464 .collect::<Vec<_>>();
465 request_service(
466 alice_client,
467 alice_signer,
468 blueprint_id,
469 accounts,
470 request_args,
471 0,
472 None,
473 )
474 .await?;
475
476 let request_id = get_next_request_id(alice_client).await?.saturating_sub(1);
478
479 for (signer, client) in sr25519_signers.iter().zip(clients) {
480 approve_service(client, signer, request_id, 50, None).await?;
481 }
482
483 let new_service_id = get_next_service_id(alice_client).await?;
485 assert!(new_service_id > prev_service_id);
486
487 let service = alice_client
489 .subxt_client()
490 .storage()
491 .at_latest()
492 .await?
493 .fetch(
494 &api::storage()
495 .services()
496 .instances(new_service_id.saturating_sub(1)),
497 )
498 .await?
499 .ok_or(TransactionError::ServiceNotFound)?;
500
501 if service.blueprint != blueprint_id {
502 return Err(TransactionError::ServiceIdMismatch);
503 }
504 Ok(new_service_id.saturating_sub(1))
505}
506
507#[allow(clippy::cast_possible_truncation, clippy::too_many_arguments)]
517pub async fn request_service_with_quotes<T: Signer<TangleConfig>>(
518 client: &TestClient,
519 user: &T,
520 blueprint_id: u64,
521 request_args: RequestArgs,
522 operators: Vec<AccountId32>,
523 quotes: Vec<PricingQuote>,
524 quote_signatures: Vec<sp_core::ecdsa::Signature>,
525 security_commitments: Vec<AssetSecurityCommitment<AssetId>>,
526 optional_assets: Option<Vec<AssetSecurityRequirement<AssetId>>>,
527) -> Result<u64, TransactionError> {
528 let quote_signatures = quote_signatures.into_iter().map(|s| s.into()).collect();
529 info!(requester = ?user.account_id(), ?operators, %blueprint_id, "Requesting service");
530 let min_operators = operators.len() as u32;
531 let security_requirements = optional_assets.unwrap_or_else(|| {
532 vec![AssetSecurityRequirement {
533 asset: Asset::Custom(0),
534 min_exposure_percent: Percent(50),
535 max_exposure_percent: Percent(80),
536 }]
537 });
538
539 let prev_service_id = get_next_service_id(client).await?;
541
542 let call = api::tx().services().request_with_signed_price_quotes(
543 None,
544 blueprint_id,
545 Vec::new(),
546 operators,
547 request_args,
548 security_requirements,
549 1000,
550 Asset::Custom(0),
551 MembershipModel::Fixed { min_operators },
552 quotes,
553 quote_signatures,
554 security_commitments,
555 );
556 let res = client
557 .subxt_client()
558 .tx()
559 .sign_and_submit_then_watch_default(&call, user)
560 .await?;
561 wait_for_in_block_success(res).await?;
562
563 let new_service_id = get_next_service_id(client).await?;
565 assert!(new_service_id > prev_service_id);
566
567 let service = client
569 .subxt_client()
570 .storage()
571 .at_latest()
572 .await?
573 .fetch(
574 &api::storage()
575 .services()
576 .instances(new_service_id.saturating_sub(1)),
577 )
578 .await?
579 .ok_or(TransactionError::ServiceNotFound)?;
580
581 if service.blueprint != blueprint_id {
582 return Err(TransactionError::ServiceIdMismatch);
583 }
584 Ok(new_service_id.saturating_sub(1))
585}
586
587#[allow(clippy::too_many_arguments)]
597pub async fn setup_operators_with_service<T: Signer<TangleConfig>>(
598 clients: &[TestClient],
599 sr25519_signers: &[T],
600 blueprint_id: u64,
601 preferences: &[Preferences],
602 registration_args: &[RegistrationArgs],
603 request_args: RequestArgs,
604 _exit_after_registration: bool,
605 skip_service_request: bool,
606) -> Result<u64, TransactionError> {
607 for (((operator, client), preferences), registration_arg) in sr25519_signers
608 .iter()
609 .zip(clients)
610 .zip(preferences)
611 .zip(registration_args)
612 {
613 join_operators(client, operator).await?;
614 register_for_blueprint(
616 client,
617 operator,
618 blueprint_id,
619 preferences.clone(),
620 registration_arg.clone(),
621 0,
622 )
623 .await?;
624 }
625
626 if skip_service_request {
627 return Ok(0);
628 }
629
630 request_service_for_operators(clients, sr25519_signers, blueprint_id, request_args).await
632}