Skip to main content

blueprint_chain_setup_tangle/
transactions.rs

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    // Deploy the contract.
106    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    // Check the receipt status.
116    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
147/// Create a new blueprint
148///
149/// # Errors
150///
151/// Returns an error if the transaction fails
152pub 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
167/// Become and operator
168///
169/// # Errors
170///
171/// Returns an error if the transaction fails
172pub 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
190/// Register as an operator for `blueprint_id`
191///
192/// # Errors
193///
194/// Returns an error if the transaction fails
195pub 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
216/// Submit a job call for the given `service_id`
217///
218/// This will submit the job, and verify that there is a `JobCalled` event with the correct `service_id`,
219/// `job_id`, `caller`, and in the future, `call_id`.
220///
221/// # Errors
222///
223/// No matching `JobCalled` event is found
224pub 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, // TODO: Actually verify this
231) -> 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/// Requests a service with a given blueprint.
256///
257/// This is meant for testing.
258///
259/// `user` will be the only permitted caller, and all `test_nodes` will be selected as operators.
260///
261/// # Errors
262///
263/// Returns an error if the transaction fails
264#[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
403/// Approves a service request. This is meant for testing, and will always approve the request.
404async 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    // Get the current service ID before requesting new service
459    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    // Approve the service request and wait for completion
477    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    // Get the new service ID from events
484    let new_service_id = get_next_service_id(alice_client).await?;
485    assert!(new_service_id > prev_service_id);
486
487    // Verify the service belongs to our blueprint
488    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/// Requests a service with a given blueprint, returning the service ID.
508///
509/// This is meant for testing.
510///
511/// `user` will be the only permitted caller, and all `test_nodes` will be selected as operators.
512///
513/// # Errors
514///
515/// Returns an error if the transaction fails
516#[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    // Get the current service ID before requesting new service
540    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    // Get the new service ID from events
564    let new_service_id = get_next_service_id(client).await?;
565    assert!(new_service_id > prev_service_id);
566
567    // Verify the service belongs to our blueprint
568    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/// Setup operators and services for multiple nodes
588///
589/// # Returns
590///
591/// The service ID, or 0 if `skip_service_request` is true since no service was yet requested
592///
593/// # Errors
594///
595/// Returns an error if the transaction fails
596#[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
615        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
631    request_service_for_operators(clients, sr25519_signers, blueprint_id, request_args).await
632}