idn_contracts/xcm/mod.rs
1/*
2 * Copyright 2025 by Ideal Labs, LLC
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17//! # IDN Client Contract Library
18//!
19//! This library provides ink! smart contracts with the ability to interact with the Ideal Network
20//! (IDN) to consume verifiable randomness through cross-chain messaging (XCM).
21//!
22//! ## Purpose
23//!
24//! The IDN Client Contract Library serves as a bridge between ink! smart contracts deployed on
25//! Polkadot parachains and the Ideal Network's randomness beacon services. It enables contracts
26//! to subscribe to, consume, and manage randomness subscriptions in a seamless, cross-chain manner.
27//!
28//! ## Key Concepts
29//!
30//! ### Randomness Pulses
31//!
32//! Pulses of randomness delivered by the IDN are cryptographically verifiable for authenticity and
33//! correctness, each containing:
34//! - A 48-byte signature computed by Drand's Quicknet
35//! - Two round numbers, a `start` and `end`, which indicate the initial and terminal rounds of the
36//! randomness beacon for which the signatures were aggregated.
37//! - Associated metadata for subscription context
38//!
39//! Contracts receive these pulses through the [`IdnConsumer::consume_pulse`] callback method,
40//! which is automatically invoked by the IDN when new randomness becomes available.
41//!
42//! ### Subscription Management
43//!
44//! Subscriptions define how contracts receive randomness and are configured with:
45//!
46//! - **Credits**: Payment budget for the subscription (more credits = more pulses available)
47//! - **Frequency**: Distribution interval measured in IDN block numbers
48//! - **Metadata**: Optional bounded data (max 128 bytes) for application-specific context such as
49//! identifiers, configuration flags, or routing information
50//! - **Subscription ID**: Unique identifier for tracking and management
51//!
52//! Subscriptions progress through states (Active → Paused → Finalized) and can be updated
53//! or terminated through the [`IdnClient`] management methods.
54//!
55//! #### Subscription State Transitions
56//!
57//! Subscriptions follow a well-defined lifecycle with specific state transitions:
58//!
59//! ```text
60//! [Creation] → [Active] ⇄ [Paused] → [Finalized]
61//! ↓
62//! [Finalized] (via kill_subscription)
63//! ```
64//!
65//! - **Active**: Subscription delivers randomness according to frequency settings
66//! - Can transition to: Paused (via `pause_subscription`), Finalized (via `kill_subscription`)
67//! - Available operations: Update parameters, pause, terminate
68//!
69//! - **Paused**: Subscription exists but does not deliver randomness
70//! - Can transition to: Active (via `reactivate_subscription`), Finalized (via
71//! `kill_subscription`)
72//! - Available operations: Update parameters, reactivate, terminate
73//!
74//! - **Finalized**: Subscription is permanently terminated and removed from storage
75//! - No further transitions possible
76//! - Credits refunded, storage deposits returned
77//! - Subscription ID can be reused for new subscriptions
78//!
79//! ### XCM Message Flow
80//!
81//! The library abstracts the complexity of cross-chain communication:
82//!
83//! 1. **Contract Call**: Contract invokes [`IdnClient`] methods (e.g., `create_subscription`)
84//! 2. **XCM Construction**: Method constructs appropriate XCM message with:
85//! - Asset withdrawal for execution fees
86//! - Runtime call to IDN Manager pallet
87//! - Fee refund and deposit instructions
88//! 3. **Cross-Chain Execution**: XCM message is sent to IDN parachain for processing
89//! 4. **Response Delivery**: IDN sends randomness back via XCM to contract's callback
90//!
91//! This flow ensures that contracts can seamlessly interact with IDN services without
92//! directly handling XCM message construction or cross-chain execution details.
93//!
94//! ### Fee Management
95//!
96//! XCM execution requires fees paid in relay chain native tokens and involves bilateral funding
97//! requirements:
98//!
99//! - **Fees**: The `max_idn_xcm_fees` parameter sets the maximum fees to pay for the execution of a
100//! single XCM message sent to the IDN chain, expressed in relay chain native tokens (DOT/PAS).
101//! - **Asset Handling**: Fees are automatically withdrawn from the contract's account
102//! - **Surplus Refund**: Unused fees are refunded back to the contract after execution
103//! - **Fee Assets**: Uses the relay chain's native token (DOT/PAS) for XCM execution
104//!
105//! #### Bilateral Funding Requirements
106//!
107//! IDN contract integration requires **two separate accounts** to be funded for proper operation:
108//!
109//! 1. **Contract's Account on IDN Chain**: Required for subscription operations
110//! - Used for: `create_subscription`, `pause_subscription`, `update_subscription`, etc.
111//! - Must be funded with: Relay chain native tokens (DOT/PAS)
112//!
113//! 2. **Contract's Account on Consumer Chain**: Required for randomness delivery
114//! - Used for: Executing contract calls to deliver randomness pulses
115//! - Must be funded with: Consumer chain's native tokens
116//!
117//! The library handles all fee-related XCM instructions automatically, but both accounts
118//! must be adequately funded or operations will fail with "Funds are unavailable" errors.
119//!
120//! ### Metadata Management
121//!
122//! Subscription metadata enables applications to attach context-specific data to their
123//! randomness subscriptions. This bounded data (maximum 128 bytes) travels with subscription
124//! operations and can be used for:
125//!
126//! - **Application Identifiers**: Distinguish between multiple subscriptions within one contract
127//! - **Configuration Flags**: Store subscription-specific settings or options
128//! - **Routing Information**: Specify how randomness should be processed or distributed
129//! - **User Context**: Associate subscriptions with specific users or sessions
130//!
131//! #### Creating Metadata
132//!
133//! Metadata can be created from various data sources:
134//! - String identifiers: Convert application names or game identifiers to bytes
135//! - Structured data: Use byte arrays for configuration flags or binary data
136//! - JSON-like data: Serialize structured information as bytes (mind the 128-byte limit)
137pub mod constants;
138pub mod types;
139
140use constants::BEACON_PUBKEY;
141use ink::{
142 env::{
143 hash::{Blake2x256, CryptoHash},
144 Error as EnvError,
145 },
146 xcm::lts::prelude::Weight,
147};
148
149#[cfg(not(test))]
150use ink::{
151 prelude::vec,
152 xcm::{
153 lts::{
154 prelude::{
155 BuyExecution, DepositAsset, OriginKind as XcmOriginKind, RefundSurplus, Transact,
156 WithdrawAsset, Xcm,
157 },
158 Asset,
159 AssetFilter::Wild,
160 AssetId, Junctions,
161 WeightLimit::Unlimited,
162 WildAsset::AllOf,
163 WildFungibility,
164 },
165 VersionedLocation, VersionedXcm,
166 },
167};
168
169// These are needed for both test and non-test
170pub use bp_idn::{
171 types::{
172 QuoteRequest, QuoteSubParams, RequestReference, SubInfoRequest, Subscription,
173 SubscriptionDetails, SubscriptionState,
174 },
175 Call as RuntimeCall, IdnManagerCall,
176};
177use codec::{Compact, Decode, Encode};
178use ink::xcm::lts::{Junction, Location};
179#[cfg(not(test))]
180use scale_info::prelude::boxed::Box;
181use scale_info::prelude::vec::Vec;
182use sp_idn_traits::pulse::Pulse as TPulse;
183use types::{
184 AccountId, Balance, CallData, CreateSubParams, Credits, IdnBlockNumber, IdnXcm, Metadata,
185 OriginKind, PalletIndex, ParaId, Pulse, Quote, SubInfoResponse, SubscriptionId,
186 UpdateSubParams,
187};
188
189use crate::xcm::constants::{CONSUME_PULSE_SEL, CONSUME_QUOTE_SEL, CONSUME_SUB_INFO_SEL};
190
191/// Contract-compatible trait for hashing with a salt
192///
193/// This trait provides a standardized way to hash data with a salt value,
194/// commonly used for generating deterministic identifiers in contract contexts.
195pub trait Hashable {
196 /// Generates a 32-byte hash of the implementor combined with a salt
197 ///
198 /// # Parameters
199 /// - `salt`: Additional entropy to include in the hash calculation
200 ///
201 /// # Returns
202 /// A 32-byte hash of the encoded data and salt
203 fn hash(&self, salt: &[u8]) -> [u8; 32];
204}
205
206/// Parameters for a contract call within the XCM execution context
207///
208/// This struct represents the parameters needed to execute a contract call
209/// via XCM messaging, mirroring the contracts pallet's call interface.
210#[derive(Encode, Decode)]
211pub struct ContractsCall {
212 /// Target contract address for the call
213 pub dest: MultiAddress,
214 /// Native token value to transfer with the call
215 #[codec(compact)]
216 pub value: Balance,
217 /// Maximum computational and storage weight for the call
218 pub gas_limit: Weight,
219 /// Optional limit for storage deposits required by the call
220 pub storage_deposit_limit: Option<Compact<Balance>>,
221 /// Encoded call data including selector and parameters
222 pub data: Vec<u8>,
223}
224
225/// Address format for contract calls
226///
227/// Represents different ways to address an account in the runtime.
228/// Currently only supports direct account ID addressing.
229#[derive(Encode, Decode)]
230pub enum MultiAddress {
231 /// Direct account ID (32-byte public key)
232 Id(AccountId),
233}
234
235/// Parameters for configuring contract call execution
236///
237/// These parameters control the execution environment and resource limits
238/// for contract calls initiated through the IDN client.
239#[derive(Encode, Decode)]
240pub struct ContractCallParams {
241 /// Native token value to transfer with the call
242 pub value: Balance,
243 /// Maximum reference time (computational cycles) for call execution
244 pub gas_limit_ref_time: u64,
245 /// Maximum proof size (storage proof bytes) for call execution
246 pub gas_limit_proof_size: u64,
247 /// Optional limit for storage deposits required by the call
248 pub storage_deposit_limit: Option<Balance>,
249}
250
251/// Default implementation of Hashable for any encodable type
252///
253/// This implementation combines the encoded form of the implementor with
254/// the provided salt and produces a Blake2x256 hash.
255impl<T> Hashable for T
256where
257 T: Encode,
258{
259 /// Generates a deterministic hash by encoding the value with salt
260 ///
261 /// The implementation:
262 /// 1. Creates a tuple of (self, salt)
263 /// 2. Encodes the tuple using SCALE codec
264 /// 3. Computes Blake2x256 hash of the encoded data
265 ///
266 /// # Parameters
267 /// - `salt`: Additional entropy for hash uniqueness
268 ///
269 /// # Returns
270 /// 32-byte Blake2x256 hash digest
271 fn hash(&self, salt: &[u8]) -> [u8; 32] {
272 let id_tuple = (self, salt);
273 // Encode the tuple using SCALE codec
274 let encoded = id_tuple.encode();
275 // Use ink!'s built-in hashing for contract environment
276 let mut output = [0u8; 32];
277 Blake2x256::hash(&encoded, &mut output);
278 output
279 }
280}
281
282/// Represents possible errors that can occur when interacting with the IDN
283#[allow(clippy::cast_possible_truncation)]
284#[derive(Debug, PartialEq, Eq)]
285#[ink::scale_derive(Encode, Decode, TypeInfo)]
286pub enum Error {
287 /// Error during XCM execution
288 XcmExecutionFailed,
289 /// Error when sending XCM message
290 XcmSendFailed,
291 /// Non XCM environment error
292 NonXcmEnvError,
293 /// Method not implemented
294 MethodNotImplemented,
295 /// Error consuming pulse
296 ConsumePulseError,
297 /// Error consuming quote
298 ConsumeQuoteError,
299 /// Error consuming subscription info
300 ConsumeSubInfoError,
301 /// Caller is not authorized
302 Unauthorized,
303 /// Invalid subscription ID
304 InvalidSubscriptionId,
305 /// Invalid Call Data
306 CallDataTooLong,
307 /// Invalid parameters
308 InvalidParams,
309 /// Other error
310 Other,
311}
312
313/// Automatic conversion from ink! environment errors to IDN client errors
314///
315/// This implementation provides seamless error handling between the ink! runtime
316/// environment and the IDN client library, particularly for XCM-related operations.
317impl From<EnvError> for Error {
318 /// Converts ink! environment errors into IDN-specific error types
319 ///
320 /// # Error Mapping
321 /// - `XcmExecutionFailed` → [`Error::XcmExecutionFailed`]
322 /// - `XcmSendFailed` → [`Error::XcmSendFailed`]
323 /// - All other errors → [`Error::NonXcmEnvError`]
324 fn from(env_error: EnvError) -> Self {
325 use ink::env::ReturnErrorCode;
326 match env_error {
327 EnvError::ReturnError(ReturnErrorCode::XcmExecutionFailed) => Error::XcmExecutionFailed,
328 EnvError::ReturnError(ReturnErrorCode::XcmSendFailed) => Error::XcmSendFailed,
329 _ => Error::NonXcmEnvError,
330 }
331 }
332}
333
334/// Result type for IDN client operations
335///
336/// This type alias simplifies error handling throughout the IDN client library.
337/// All public methods return this Result type with library-specific [`Error`] variants.
338pub type Result<T> = core::result::Result<T, Error>;
339
340/// Trait for contracts that receive data from the IDN
341#[ink::trait_definition]
342pub trait IdnConsumer {
343 /// Consumes a randomness pulse from the IDN chain.
344 ///
345 /// This function processes randomness pulses delivered by the IDN chain.
346 ///
347 /// # Parameters
348 /// - `pulse`: The randomness pulse to be consumed.
349 /// - `sub_id`: The subscription ID associated with the pulse.
350 ///
351 /// # Errors
352 /// - [`Error::ConsumePulseError`]: If the pulse cannot be consumed.
353 #[ink(message)]
354 fn consume_pulse(&mut self, pulse: Pulse, sub_id: SubscriptionId) -> Result<()>;
355
356 /// Consumes a subscription quote from the IDN chain.
357 ///
358 /// This function processes subscription fee quotes received from the IDN chain.
359 /// implementation.
360 ///
361 /// # Parameters
362 /// - `quote`: The subscription quote to be consumed.
363 ///
364 /// # Errors
365 /// - [`Error::ConsumeQuoteError`]: If the quote cannot be consumed.
366 #[ink(message)]
367 fn consume_quote(&mut self, quote: Quote) -> Result<()>;
368
369 /// Consumes subscription info from the IDN chain.
370 ///
371 /// This function processes subscription information received from the IDN chain.
372 /// implementation.
373 ///
374 /// # Parameters
375 /// - `sub_info`: The subscription information to be consumed.
376 ///
377 /// # Errors
378 /// - [`Error::ConsumeSubInfoError`]: If the subscription info cannot be consumed.
379 #[ink(message)]
380 fn consume_sub_info(&mut self, sub_info: SubInfoResponse) -> Result<()>;
381}
382
383/// Implementation of the IDN Client for cross-chain randomness operations
384///
385/// The `IdnClient` serves as the primary interface for ink! smart contracts to interact
386/// with the Ideal Network's randomness beacon services. It encapsulates all necessary
387/// configuration parameters and provides methods for subscription management, randomness
388/// consumption, and cross-chain communication via XCM.
389///
390/// # Configuration Parameters
391/// - IDN identification (parachain ID, pallet indices)
392/// - Consumer chain identification (parachain ID, pallet indices)
393/// - XCM fee management settings
394///
395/// # Key Capabilities
396/// - Create, pause, reactivate, and terminate randomness subscriptions
397/// - Handle cross-chain message construction and dispatch
398/// - Validate cryptographic authenticity and correctness of randomness pulses
399/// - Manage XCM execution fees and refunds
400#[derive(Clone, Copy, Encode, Decode, Debug)]
401#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout))]
402pub struct IdnClient {
403 /// Parachain ID of the IDN
404 pub idn_para_id: ParaId,
405 /// Pallet index for the IDN Manager pallet
406 pub idn_manager_pallet_index: PalletIndex,
407 /// ID of the parachain this contract is deployed on
408 pub self_para_id: ParaId,
409 /// Index for the contracts pallet in the parachain this contract is deployed on
410 pub self_contracts_pallet_index: PalletIndex,
411 /// Call dispatchable index for the contracts pallet in the parachain this contract is deployed
412 /// on
413 pub self_contract_call_index: u8,
414 /// The maximum fees to pay for the execution of a single XCM message sent to the
415 /// IDN chain, expressed in relay chain native tokens (DOT/PAS).
416 pub max_idn_xcm_fees: u128,
417}
418
419impl IdnClient {
420 /// Creates a new IdnClient with the specified configuration parameters
421 ///
422 /// # Arguments
423 /// * `idn_para_id` - The parachain ID of the IDN
424 /// * `idn_manager_pallet_index` - The pallet index for the IDN Manager pallet
425 /// * `self_para_id` - The parachain ID where this contract is deployed
426 /// * `self_contracts_pallet_index` - The contracts pallet index on this parachain
427 /// * `self_contract_call_index` - The call index for the contracts pallet's call dispatchable
428 /// * `max_idn_xcm_fees` - Maximum fees to pay for XCM execution (in relay chain native tokens)
429 ///
430 /// # Returns
431 /// A new `IdnClient` instance configured for cross-chain randomness operations
432 pub fn new(
433 idn_para_id: ParaId,
434 idn_manager_pallet_index: PalletIndex,
435 self_para_id: ParaId,
436 self_contracts_pallet_index: PalletIndex,
437 self_contract_call_index: u8,
438 max_idn_xcm_fees: u128,
439 ) -> Self {
440 Self {
441 idn_para_id,
442 idn_manager_pallet_index,
443 self_para_id,
444 self_contracts_pallet_index,
445 self_contract_call_index,
446 max_idn_xcm_fees,
447 }
448 }
449
450 /// Gets the pallet index for the IDN Manager pallet
451 ///
452 /// # Returns
453 /// The pallet index used to construct XCM calls to the IDN Manager
454 pub fn get_idn_manager_pallet_index(&self) -> PalletIndex {
455 self.idn_manager_pallet_index
456 }
457
458 /// Gets the parachain ID of the IDN
459 ///
460 /// # Returns
461 /// The parachain ID where IDN services are hosted
462 pub fn get_idn_para_id(&self) -> ParaId {
463 self.idn_para_id
464 }
465
466 /// Gets the contracts pallet index for this parachain
467 ///
468 /// # Returns
469 /// The pallet index for the contracts pallet on the consumer parachain
470 pub fn get_self_contracts_pallet_index(&self) -> PalletIndex {
471 self.self_contracts_pallet_index
472 }
473
474 /// Gets the call index for the contracts pallet's `call` dispatchable
475 ///
476 /// # Returns
477 /// The call index used to construct contract invocation calls via XCM
478 pub fn get_self_contract_call_index(&self) -> u8 {
479 self.self_contract_call_index
480 }
481
482 /// Gets the parachain ID of this parachain
483 ///
484 /// # Returns
485 /// The parachain ID where this contract is deployed
486 pub fn get_self_para_id(&self) -> ParaId {
487 self.self_para_id
488 }
489
490 /// Creates a new randomness subscription with the IDN.
491 ///
492 /// This method sends an XCM message to the IDN Manager pallet to create a subscription
493 /// for receiving randomness pulses. If no subscription ID is provided, one will be
494 /// automatically generated using a hash of the current block timestamp.
495 ///
496 /// # Parameters
497 /// - `credits`: Payment budget for the subscription (more credits = more pulses available)
498 /// - `frequency`: Distribution interval measured in IDN block numbers
499 /// - `metadata`: Optional bounded data for application-specific context
500 /// - `sub_id`: Optional subscription ID; if None, auto-generated
501 /// - `origin_kind`: Optional [`OriginKind`] for the XCM message; defaults to
502 /// `OriginKind::Native`
503 ///
504 /// # Returns
505 /// Returns the subscription ID that was created or provided.
506 ///
507 /// # Errors
508 /// - [`Error::XcmSendFailed`]: If the XCM message fails to send
509 /// - [`Error::XcmExecutionFailed`]: If the XCM message execution fails
510 pub fn create_subscription(
511 &self,
512 credits: Credits,
513 frequency: IdnBlockNumber,
514 metadata: Option<Metadata>,
515 sub_id: Option<SubscriptionId>,
516 call_params: Option<ContractCallParams>,
517 origin_kind: Option<OriginKind>,
518 ) -> Result<SubscriptionId> {
519 if credits == 0 || frequency == 0 {
520 return Err(Error::InvalidParams);
521 }
522
523 let dummy_pulse = Pulse::default();
524 let dummy_sub_id = SubscriptionId::default();
525 let dummy_params = (dummy_pulse, dummy_sub_id).encode();
526
527 let mut params = CreateSubParams {
528 credits,
529 target: self.self_para_sibling_location(),
530 call: self.create_callback_data(CONSUME_PULSE_SEL, dummy_params, call_params)?,
531 frequency,
532 metadata,
533 sub_id,
534 origin_kind: origin_kind.unwrap_or(OriginKind::Native),
535 };
536
537 // If `sub_id` is not provided, generate a new one and assign it to the params
538 let sub_id = match sub_id {
539 Some(sub_id) => sub_id,
540 None => {
541 let salt = ink::env::block_timestamp::<ink::env::DefaultEnvironment>().encode();
542 let sub_id = params.hash(&salt);
543 params.sub_id = Some(sub_id);
544 sub_id
545 },
546 };
547
548 let call = RuntimeCall::IdnManager(IdnManagerCall::create_subscription { params });
549
550 self.xcm_send(call)?;
551
552 // Return the subscription ID (should always be Some at this point)
553 Ok(sub_id)
554 }
555
556 /// Pauses an active subscription temporarily.
557 ///
558 /// This method sends an XCM message to pause the specified subscription. While paused,
559 /// no randomness pulses will be delivered, but the subscription remains in storage
560 /// and can be reactivated later.
561 ///
562 /// # Parameters
563 /// - `sub_id`: The subscription ID to pause
564 ///
565 /// # Errors
566 /// - [`Error::XcmSendFailed`]: If the XCM message fails to send
567 /// - [`Error::XcmExecutionFailed`]: If the XCM message execution fails
568 pub fn pause_subscription(&self, sub_id: SubscriptionId) -> Result<()> {
569 let call = RuntimeCall::IdnManager(IdnManagerCall::pause_subscription { sub_id });
570 self.xcm_send(call)
571 }
572
573 /// Reactivates a paused subscription.
574 ///
575 /// This method sends an XCM message to reactivate a previously paused subscription.
576 /// Once reactivated, randomness pulses will resume being delivered according to
577 /// the subscription's frequency settings.
578 ///
579 /// # Parameters
580 /// - `sub_id`: The subscription ID to reactivate
581 ///
582 /// # Errors
583 /// - [`Error::XcmSendFailed`]: If the XCM message fails to send
584 /// - [`Error::XcmExecutionFailed`]: If the XCM message execution fails
585 pub fn reactivate_subscription(&self, sub_id: SubscriptionId) -> Result<()> {
586 let call = RuntimeCall::IdnManager(IdnManagerCall::reactivate_subscription { sub_id });
587 self.xcm_send(call)
588 }
589
590 /// Updates an existing subscription's parameters.
591 ///
592 /// This method sends an XCM message to modify the specified subscription's settings.
593 /// Any parameter set to `Some(value)` will be updated, while `None` parameters
594 /// remain unchanged.
595 ///
596 /// # Parameters
597 /// - `sub_id`: The subscription ID to update
598 /// - `credits`: Optional new credit budget for the subscription
599 /// - `frequency`: Optional new distribution interval in IDN blocks
600 /// - `metadata`: Optional metadata update (use `Some(None)` to clear metadata)
601 ///
602 /// # Errors
603 /// - [`Error::XcmSendFailed`]: If the XCM message fails to send
604 /// - [`Error::XcmExecutionFailed`]: If the XCM message execution fails
605 pub fn update_subscription(
606 &mut self,
607 sub_id: SubscriptionId,
608 credits: Option<Credits>,
609 frequency: Option<IdnBlockNumber>,
610 metadata: Option<Option<Metadata>>,
611 ) -> Result<()> {
612 let params = UpdateSubParams { sub_id, credits, frequency, metadata };
613
614 let call = RuntimeCall::IdnManager(IdnManagerCall::update_subscription { params });
615
616 self.xcm_send(call)
617 }
618
619 /// Permanently terminates a subscription.
620 ///
621 /// This method sends an XCM message to kill the specified subscription. Once killed,
622 /// the subscription is removed from storage, any unused credits are refunded to the
623 /// origin, and the storage deposit is returned. This action cannot be undone.
624 ///
625 /// # Parameters
626 /// - `sub_id`: The subscription ID to terminate
627 ///
628 /// # Errors
629 /// - [`Error::XcmSendFailed`]: If the XCM message fails to send
630 /// - [`Error::XcmExecutionFailed`]: If the XCM message execution fails
631 pub fn kill_subscription(&self, sub_id: SubscriptionId) -> Result<()> {
632 let call = RuntimeCall::IdnManager(IdnManagerCall::kill_subscription { sub_id });
633 self.xcm_send(call)
634 }
635
636 /// Requests a subscription fee quote from the IDN.
637 ///
638 /// This method sends an XCM message to request current subscription pricing
639 /// information. The quote response is delivered via the [`IdnConsumer::consume_quote`]
640 /// callback method.
641 ///
642 /// # Parameters
643 /// - `number_of_pulses`: The number of pulses required for the lifetime of the subscription
644 /// - `frequency`: The number of blocks between pulses
645 /// - `metadata`: Optional bounded data for application-specific context
646 /// - `sub_id`: The subscription ID that would be associated with the subscription
647 /// - `req_ref`: An optional unique identifier associated with the request being sent
648 /// - `origin_kind`: Optional [`OriginKind`] for the XCM message; defaults to
649 /// `OriginKind::Native`
650 ///
651 /// # Errors
652 /// - [`Error::XcmSendFailed`]: If the XCM message fails to send
653 /// - [`Error::XcmExecutionFailed`]: If the XCM message execution fails
654 pub fn request_quote(
655 &self,
656 number_of_pulses: IdnBlockNumber,
657 frequency: IdnBlockNumber,
658 metadata: Option<Metadata>,
659 sub_id: Option<SubscriptionId>,
660 req_ref: Option<RequestReference>,
661 origin_kind: Option<OriginKind>,
662 ) -> Result<()> {
663 let req_ref = match req_ref {
664 Some(req_ref) => req_ref,
665 None => {
666 let salt = ink::env::block_number::<ink::env::DefaultEnvironment>().encode();
667 frequency.hash(&salt)
668 },
669 };
670
671 let mut dummy_params = Vec::new();
672 let create_sub_params = CreateSubParams {
673 credits: 0,
674 target: self.self_para_sibling_location(),
675 call: self.create_callback_data(CONSUME_QUOTE_SEL, dummy_params.clone(), None)?,
676 origin_kind: origin_kind.clone().unwrap_or(OriginKind::Native),
677 frequency,
678 metadata,
679 sub_id,
680 };
681
682 let quote = Quote { req_ref, fees: u128::default(), deposit: u128::default() };
683 dummy_params = quote.encode();
684 let quote_request =
685 QuoteRequest { req_ref, create_sub_params, lifetime_pulses: number_of_pulses };
686 let req = QuoteSubParams {
687 quote_request,
688 call: self.create_callback_data(CONSUME_QUOTE_SEL, dummy_params, None)?,
689 origin_kind: origin_kind.unwrap_or(OriginKind::Native),
690 };
691 let call = RuntimeCall::IdnManager(IdnManagerCall::quote_subscription { params: req });
692 self.xcm_send(call)
693 }
694
695 /// Requests information about a subscription from the IDN.
696 ///
697 /// This method sends an XCM message to request detailed information about
698 /// a subscription's current state, remaining credits, and other parameters.
699 /// The response is delivered via the [`IdnConsumer::consume_sub_info`] callback method.
700 ///
701 /// # Parameters
702 /// - `sub_id`: The subscription ID of the subscription
703 /// - `req_ref`: An optional unique identifier associated with the request being sent
704 /// - `metadata`: Optional bounded data for application-specific context. This must match the
705 /// metadata that was passed when creating the subscription.
706 /// - `call_params`: Optional execution parameters (gas limits, storage deposits). This must
707 /// match the call_params that was passed when creating the subscription
708 /// - `origin_kind`: Optional [`OriginKind`] for the XCM message; defaults to
709 /// `OriginKind::Native`
710 ///
711 /// # Errors
712 /// - [`Error::XcmSendFailed`]: If the XCM message fails to send
713 /// - [`Error::XcmExecutionFailed`]: If the XCM message execution fails
714 pub fn request_sub_info(
715 &self,
716 sub_id: SubscriptionId,
717 metadata: Option<Metadata>,
718 req_ref: Option<RequestReference>,
719 call_params: Option<ContractCallParams>,
720 origin_kind: Option<OriginKind>,
721 ) -> Result<()> {
722 let req_ref = match req_ref {
723 Some(req_ref) => req_ref,
724 None => {
725 let salt = ink::env::block_number::<ink::env::DefaultEnvironment>().encode();
726 sub_id.hash(&salt)
727 },
728 };
729
730 let dummy_sub_info_response =
731 self.create_dummy_sub_info_response(sub_id, req_ref, metadata, call_params)?;
732 let dummy_params = dummy_sub_info_response.encode();
733
734 let req = SubInfoRequest {
735 sub_id,
736 req_ref,
737 call: self.create_callback_data(CONSUME_SUB_INFO_SEL, dummy_params, None)?,
738 origin_kind: origin_kind.unwrap_or(OriginKind::Native),
739 };
740
741 let call = RuntimeCall::IdnManager(IdnManagerCall::get_subscription_info { req });
742
743 self.xcm_send(call)
744 }
745
746 /// This function is used to create the encoded callback data for the SubInfoResponse. See
747 /// create_callback_data for how dummy data is used.
748 pub fn create_dummy_sub_info_response(
749 &self,
750 sub_id: SubscriptionId,
751 req_ref: [u8; 32],
752 metadata: Option<Metadata>,
753 call_params: Option<ContractCallParams>,
754 ) -> Result<SubInfoResponse> {
755 let dummy_pulse = Pulse::default();
756 let dummy_sub_id = SubscriptionId::default();
757 let dummy_details_parameters = (dummy_pulse, dummy_sub_id).encode();
758 let dummy_details = SubscriptionDetails {
759 subscriber: sub_id.into(),
760 target: self.self_para_sibling_location(),
761 origin_kind: OriginKind::Native,
762 call: self.create_callback_data(
763 CONSUME_PULSE_SEL,
764 dummy_details_parameters,
765 call_params,
766 )?,
767 };
768 let dummy_sub = Subscription {
769 id: sub_id,
770 state: SubscriptionState::Active,
771 metadata,
772 last_delivered: Some(u32::default()),
773 details: dummy_details,
774 credits_left: u64::default(),
775 created_at: u32::default(),
776 updated_at: u32::default(),
777 credits: u64::default(),
778 frequency: u32::default(),
779 };
780 let dummy_sub_response = SubInfoResponse { req_ref, sub: dummy_sub };
781 Ok(dummy_sub_response)
782 }
783 /// Validates the cryptographic authenticity and correctness of a randomness pulse.
784 ///
785 /// This method verifies that a pulse was legitimately generated by the Drand Quicknet's
786 /// randomness beacon by checking its BLS12-381 signature against the known beacon public key
787 /// and the message we expect the beacon to have signed. This provides cryptographic proof that
788 /// the randomness originates from the drand network and hasn't been tampered with during
789 /// cross-chain delivery.
790 ///
791 /// # Verification Process
792 ///
793 /// The validation performs the following checks:
794 /// 1. Decodes the beacon's BLS12-381 public key from the hardcoded constant
795 /// 2. Calls the pulse's `authenticate` method with the public key
796 /// 3. Returns true if the signature verification succeeds, false otherwise
797 ///
798 /// # Usage Pattern
799 ///
800 /// Always validate pulses in your IdnConsumer::consume_pulse implementation before using
801 /// the randomness. Invalid pulses should be rejected and potentially logged for security
802 /// monitoring. Valid pulses can be safely processed to derive randomness for your application.
803 ///
804 /// # Security Considerations
805 ///
806 /// - **Always validate**: Never use randomness from unverified pulses in production
807 /// - **Handle failures**: Invalid pulses may indicate network attacks or data corruption
808 /// - **Log suspicious activity**: Consider logging validation failures for monitoring
809 ///
810 /// # Performance Notes
811 ///
812 /// BLS signature verification is computationally expensive. Consider caching validation
813 /// results if the same pulse might be processed multiple times, though this is uncommon
814 /// in typical usage patterns.
815 ///
816 /// # Parameters
817 /// - `pulse`: The randomness pulse to validate
818 ///
819 /// # Returns
820 /// - `true` if the pulse signature is cryptographically valid
821 /// - `false` if validation fails (invalid signature, malformed data, etc.)
822 ///
823 /// # Warning
824 /// This function consumes too much gas ~ refTime: 1344.30 ms & proofSize: 0.13 MB
825 /// See https://github.com/ideal-lab5/idn-sdk/issues/360
826 pub fn is_valid_pulse(&self, pulse: &Pulse) -> bool {
827 // Safe to unwrap: BEACON_PUBKEY is a compile-time constant, if invalid the contract
828 // shouldn't work
829 let pk = hex::decode(BEACON_PUBKEY).unwrap();
830 // Safe to panic: The public key is a well-defined constant, contract is unusable if this
831 // fails
832 pulse.authenticate(pk.try_into().expect("The public key is well-defined; qed."))
833 }
834
835 /// Get this parachain's Location as a sibling of the IDN chain
836 ///
837 /// Constructs an XCM Location that identifies this parachain from the perspective
838 /// of the IDN chain, used for targeting XCM messages back to this contract.
839 ///
840 /// # Returns
841 /// An XCM Location with `parents: 1` (relay chain) and interior `Parachain(self_para_id)`
842 fn self_para_sibling_location(&self) -> IdnXcm::Location {
843 IdnXcm::Location {
844 parents: 1, // Go up to the relay chain
845 interior: IdnXcm::Junctions::X1(
846 [IdnXcm::Junction::Parachain(self.get_self_para_id()) /* Target parachain */]
847 .into(),
848 ),
849 }
850 }
851
852 /// Sends an XCM message to the Ideal Network (IDN) chain.
853 ///
854 /// This function constructs and dispatches an XCM message using the provided `RuntimeCall`.
855 /// The message includes the following instructions:
856 /// - `WithdrawAsset`: Withdraws relay chain native tokens (DOT/PAS) from the contract's account
857 /// on the IDN chain for XCM execution fees.
858 /// - `BuyExecution`: Pays for the execution of the XCM message with the withdrawn asset.
859 /// - `Transact`: Executes the provided `RuntimeCall` on the IDN chain.
860 /// - `RefundSurplus`: Refunds any surplus fees back to the contract's account.
861 /// - `DepositAsset`: Deposits the refunded asset back into the contract's account.
862 ///
863 /// # Funding Requirements
864 ///
865 /// The contract's account on the IDN chain must be funded with sufficient relay chain
866 /// native tokens before calling this method.
867 ///
868 /// # Parameters
869 /// - `call`: The `RuntimeCall` to be executed on the IDN chain.
870 ///
871 /// # Returns
872 /// - `Ok(())` if the message is successfully sent.
873 /// - `Err(Error::XcmSendFailed)` if the message fails to send.
874 #[cfg(not(test))]
875 fn xcm_send(&self, call: RuntimeCall) -> Result<()> {
876 let idn_fee_asset = Asset {
877 id: AssetId(Location { parents: 1, interior: Junctions::Here }),
878 fun: self.max_idn_xcm_fees.into(),
879 };
880
881 let xcm_call: Xcm<RuntimeCall> = Xcm(vec![
882 WithdrawAsset(idn_fee_asset.clone().into()),
883 BuyExecution { weight_limit: Unlimited, fees: idn_fee_asset.clone() },
884 Transact {
885 origin_kind: XcmOriginKind::Xcm,
886 require_weight_at_most: Weight::MAX,
887 call: call.encode().into(),
888 },
889 RefundSurplus,
890 DepositAsset {
891 assets: Wild(AllOf { id: idn_fee_asset.id, fun: WildFungibility::Fungible }),
892 // refund any surplus back to the contract's account
893 beneficiary: self.contract_idn_location(),
894 },
895 ]);
896
897 let versioned_target: Box<VersionedLocation> = Box::new(self.sibling_idn_location().into());
898
899 let versioned_msg: Box<VersionedXcm<()>> = Box::new(VersionedXcm::V4(xcm_call.into()));
900
901 ink::env::xcm_send::<ink::env::DefaultEnvironment, ()>(&versioned_target, &versioned_msg)
902 .map_err(|_err| Error::XcmSendFailed)?;
903
904 Ok(())
905 }
906
907 /// Mock version of xcm_send for testing
908 ///
909 /// In test mode, this function simulates XCM sending without actually
910 /// calling the ink! environment XCM functions, which aren't available
911 /// in unit test environments.
912 #[cfg(test)]
913 fn xcm_send(&self, _call: RuntimeCall) -> Result<()> {
914 // In tests, we just return success to allow testing of
915 // parameter validation and call construction logic
916 Ok(())
917 }
918
919 /// Helper function to get the sibling location of the IDN parachain
920 ///
921 /// Creates an XCM Location targeting the IDN parachain from this parachain's perspective.
922 /// Used as the destination for XCM messages sent to IDN services.
923 ///
924 /// # Returns
925 /// Location with `parents: 1` (relay chain) and junction `Parachain(idn_para_id)`
926 fn sibling_idn_location(&self) -> Location {
927 Location::new(1, Junction::Parachain(self.get_idn_para_id()))
928 }
929
930 /// Helper function to get the location of this contract's address on the IDN parachain
931 ///
932 /// Creates an XCM Location representing this contract's account from the IDN chain's
933 /// perspective. Used for XCM fee refunds and asset deposits back to the contract's
934 /// account.
935 ///
936 /// # Returns
937 /// Location with `parents: 0` (local to IDN) and junction `AccountId32(contract_account)`
938 fn contract_idn_location(&self) -> Location {
939 Location::new(0, Junction::AccountId32 { network: None, id: *self.account_id().as_ref() })
940 }
941
942 /// Gets the account ID of this contract
943 #[cfg(not(test))]
944 fn account_id(&self) -> AccountId {
945 ink::env::account_id::<ink::env::DefaultEnvironment>()
946 }
947
948 /// Mock version of account_id for testing
949 #[cfg(test)]
950 fn account_id(&self) -> AccountId {
951 // Return a dummy account ID for testing
952 [88u8; 32].into()
953 }
954
955 /// Get the call data for the [`IdnConsumer`] calls
956 ///
957 /// Constructs the encoded call data needed for the IDN chain to invoke the
958 /// designated method on this contract when delivering subscription related data.
959 /// The call data includes the method selector and placeholder parameters that
960 /// will be replaced with actual data during XCM execution.
961 ///
962 /// # Process
963 /// 1. Uses dummy params for call data sizing
964 /// 2. Encodes the method selector
965 /// 3. Generates a complete contract call with gas limits and parameters
966 /// 4. Truncates dummy parameters, leaving space for real data injection
967 ///
968 /// # Parameters
969 /// - `call_params`: Optional execution parameters (gas limits, storage deposits)
970 /// - `dummy_params`: These dummy params are needed to get the full encoded length of the call
971 /// data, which we will truncate later
972 ///
973 /// # Returns
974 /// Encoded call data ready for XCM contract invocation
975 ///
976 /// # Errors
977 /// - [`Error::CallDataTooLong`]: If the generated call data exceeds size limits
978 fn create_callback_data(
979 &self,
980 selector: [u8; 4],
981 dummy_params: Vec<u8>,
982 call_params: Option<ContractCallParams>,
983 ) -> Result<CallData> {
984 const DEF_VALUE: Balance = 0;
985 const DEF_REF_TIME: u64 = 4_000_000_000;
986 const DEF_PROOF_SIZE: u64 = 200_000;
987 const DEF_STORAGE_DEPOSIT: Option<Balance> = None;
988
989 let mut data = Vec::new();
990 data.extend_from_slice(&selector);
991 data.extend_from_slice(&dummy_params);
992
993 let mut call = self.generate_call(
994 call_params.as_ref().map(|p| p.value).unwrap_or(DEF_VALUE), // value - no balance transfer needed for pulse callbacks
995 call_params.as_ref().map(|p| p.gas_limit_ref_time).unwrap_or(DEF_REF_TIME), // gas_limit_ref_time - reasonable default
996 call_params.as_ref().map(|p| p.gas_limit_proof_size).unwrap_or(DEF_PROOF_SIZE), // gas_limit_proof_size - reasonable default
997 call_params
998 .as_ref()
999 .map(|p| p.storage_deposit_limit)
1000 .unwrap_or(DEF_STORAGE_DEPOSIT), // storage_deposit_limit - use None for default
1001 data, // data - the encoded selector and params
1002 );
1003
1004 // Truncate the call data to remove the dummy params, real params will be provided by the
1005 // IDN chain when dispatching the call
1006 // We truncate the call before actually trying to create the CallData
1007 // nullifying adding the dummy data to the generated call
1008 call.truncate(call.len().saturating_sub(dummy_params.len()));
1009 CallData::try_from(call).map_err(|_| Error::CallDataTooLong)
1010 }
1011
1012 /// Generates encoded call data for contract invocation via XCM
1013 ///
1014 /// This internal method constructs the complete encoded call data needed for the
1015 /// IDN chain to invoke contract methods through XCM. It combines pallet/call indices
1016 /// with the contract call parameters to create a dispatchable runtime call.
1017 ///
1018 /// # Call Structure
1019 /// The generated call follows the format:
1020 /// `[pallet_index][call_index][ContractsCall(dest, value, gas_limit, storage_deposit_limit,
1021 /// data)]`
1022 ///
1023 /// # Parameters
1024 /// - `value`: Native tokens to transfer with the call
1025 /// - `gas_limit_ref_time`: Maximum computational time for execution
1026 /// - `gas_limit_proof_size`: Maximum storage proof size
1027 /// - `storage_deposit_limit`: Optional storage deposit limit
1028 /// - `data`: Method selector and encoded parameters
1029 ///
1030 /// # Returns
1031 /// Complete encoded call data ready for XCM `Transact` instruction
1032 #[cfg(not(test))]
1033 fn generate_call(
1034 &self,
1035 value: Balance,
1036 gas_limit_ref_time: u64,
1037 gas_limit_proof_size: u64,
1038 storage_deposit_limit: Option<Balance>,
1039 data: Vec<u8>,
1040 ) -> Vec<u8> {
1041 let mut encoded = Vec::new();
1042
1043 // Pallet and call indices as raw bytes
1044 encoded.push(self.get_self_contracts_pallet_index());
1045 encoded.push(self.get_self_contract_call_index());
1046
1047 // Create the call structure
1048 let call = ContractsCall {
1049 dest: MultiAddress::Id(ink::env::account_id::<ink::env::DefaultEnvironment>()),
1050 value,
1051 gas_limit: Weight::from_parts(gas_limit_ref_time, gas_limit_proof_size),
1052 storage_deposit_limit: storage_deposit_limit.map(Compact),
1053 data,
1054 };
1055
1056 // Encode the call parameters
1057 encoded.extend_from_slice(&call.encode());
1058
1059 encoded
1060 }
1061
1062 /// Mock version of generate_call for testing
1063 #[cfg(test)]
1064 fn generate_call(
1065 &self,
1066 value: Balance,
1067 gas_limit_ref_time: u64,
1068 gas_limit_proof_size: u64,
1069 storage_deposit_limit: Option<Balance>,
1070 data: Vec<u8>,
1071 ) -> Vec<u8> {
1072 let mut encoded = Vec::new();
1073
1074 // Pallet and call indices as raw bytes
1075 encoded.push(self.get_self_contracts_pallet_index());
1076 encoded.push(self.get_self_contract_call_index());
1077
1078 // Create the call structure with dummy account ID
1079 let call = ContractsCall {
1080 dest: MultiAddress::Id(AccountId::from([42u8; 32])), // Dummy account ID for testing
1081 value,
1082 gas_limit: Weight::from_parts(gas_limit_ref_time, gas_limit_proof_size),
1083 storage_deposit_limit: storage_deposit_limit.map(Compact),
1084 data,
1085 };
1086
1087 // Encode the call parameters
1088 encoded.extend_from_slice(&call.encode());
1089
1090 encoded
1091 }
1092}
1093
1094#[cfg(test)]
1095mod tests {
1096 use super::{
1097 constants::{
1098 CONSUMER_PARA_ID_PASEO, CONTRACTS_CALL_INDEX, CONTRACTS_PALLET_INDEX_PASEO,
1099 IDN_MANAGER_PALLET_INDEX_PASEO, IDN_PARA_ID_PASEO,
1100 },
1101 *,
1102 };
1103
1104 fn mock_client() -> IdnClient {
1105 IdnClient::new(
1106 IDN_PARA_ID_PASEO,
1107 IDN_MANAGER_PALLET_INDEX_PASEO,
1108 CONSUMER_PARA_ID_PASEO,
1109 CONTRACTS_PALLET_INDEX_PASEO,
1110 CONTRACTS_CALL_INDEX,
1111 1_000_000_000,
1112 )
1113 }
1114
1115 fn create_subscription(client: &IdnClient) -> Result<SubscriptionId> {
1116 let credits = 100u64;
1117 let frequency = 10u32;
1118 let metadata: Option<types::Metadata> = None; // Use None since BoundedVec creation is complex in tests
1119 let sub_id = Some([1u8; 32]);
1120
1121 client.create_subscription(credits, frequency, metadata, sub_id, None, None)
1122 }
1123
1124 #[test]
1125 fn test_client_basic_functionality() {
1126 let client = mock_client();
1127
1128 // Test getter methods
1129 assert_eq!(client.get_idn_manager_pallet_index(), IDN_MANAGER_PALLET_INDEX_PASEO);
1130 assert_eq!(client.get_idn_para_id(), IDN_PARA_ID_PASEO);
1131 assert_eq!(client.get_self_contracts_pallet_index(), CONTRACTS_PALLET_INDEX_PASEO);
1132 assert_eq!(client.get_self_para_id(), CONSUMER_PARA_ID_PASEO);
1133 assert!(client.request_sub_info([0; 32], None, None, None, None).is_ok());
1134 assert!(client.request_quote(100, 4, None, None, None, None).is_ok());
1135 }
1136
1137 #[test]
1138 fn test_client_encoding_decoding() {
1139 // Create a client
1140 let client = mock_client();
1141
1142 // Encode the client
1143 let encoded = client.encode();
1144
1145 // Decode the client
1146 let decoded: IdnClient = Decode::decode(&mut &encoded[..]).unwrap();
1147
1148 // Verify the decoded client has the same values
1149 assert_eq!(client.get_idn_manager_pallet_index(), decoded.get_idn_manager_pallet_index());
1150 assert_eq!(client.get_idn_para_id(), decoded.get_idn_para_id());
1151 assert_eq!(
1152 client.get_self_contracts_pallet_index(),
1153 decoded.get_self_contracts_pallet_index()
1154 );
1155 assert_eq!(client.get_self_para_id(), decoded.get_self_para_id());
1156 assert_eq!(client.max_idn_xcm_fees, decoded.max_idn_xcm_fees);
1157 }
1158
1159 #[test]
1160 fn test_edge_cases() {
1161 // Test constructor with edge case values
1162 let edge_client = IdnClient::new(u32::MAX, u8::MAX, u32::MAX, u8::MAX, u8::MAX, u128::MAX);
1163 assert_eq!(edge_client.get_idn_manager_pallet_index(), u8::MAX);
1164 assert_eq!(edge_client.get_idn_para_id(), u32::MAX);
1165 assert_eq!(edge_client.get_self_contracts_pallet_index(), u8::MAX);
1166 assert_eq!(edge_client.get_self_para_id(), u32::MAX);
1167 assert_eq!(edge_client.max_idn_xcm_fees, u128::MAX);
1168 assert!(edge_client.request_sub_info([0; 32], None, None, None, None).is_ok());
1169 assert!(edge_client.request_quote(100, 4, None, None, None, None).is_ok());
1170 }
1171
1172 #[test]
1173 fn test_error_handling() {
1174 // Verify that XCM-specific errors are properly handled in the From implementation
1175 // This only tests that our Error enum has the right variants for the XCM errors
1176 // since we can't easily construct the actual XCM errors in unit tests
1177 assert_ne!(Error::XcmExecutionFailed, Error::XcmSendFailed);
1178 assert_ne!(Error::XcmExecutionFailed, Error::NonXcmEnvError);
1179 }
1180
1181 #[test]
1182 fn test_create_subscription_xcm_send_failure() {
1183 // Note: Can't directly mock ink::env::xcm_send in unit tests, but we can check error
1184 // conversion logic
1185 let err = ink::env::Error::ReturnError(ink::env::ReturnErrorCode::XcmSendFailed);
1186 let converted: Error = err.into();
1187 assert_eq!(converted, Error::XcmSendFailed);
1188 }
1189
1190 #[test]
1191 fn test_subscription_management_api() {
1192 let client = mock_client();
1193
1194 let quote_result = client.request_quote(1, 1, None, None, None, None);
1195
1196 assert!(quote_result.is_ok());
1197
1198 let sub_id = create_subscription(&client).unwrap();
1199
1200 let sub_info_result = client.request_sub_info(sub_id, None, None, None, None);
1201
1202 assert!(sub_info_result.is_ok());
1203
1204 // Test pause subscription API
1205 let pause_result = client.pause_subscription(sub_id);
1206
1207 assert!(pause_result.is_ok());
1208
1209 // Test reactivate subscription API
1210 let reactivate_result = client.reactivate_subscription(sub_id);
1211
1212 assert!(reactivate_result.is_ok());
1213
1214 // Test kill subscription API
1215 let kill_result = client.kill_subscription(sub_id);
1216
1217 assert!(kill_result.is_ok());
1218 }
1219
1220 #[test]
1221 fn test_update_subscription_api() {
1222 let mut client = mock_client();
1223
1224 let sub_id = create_subscription(&client).unwrap();
1225
1226 // Test update subscription API with different parameter combinations
1227 let update_result = client.update_subscription(sub_id, Some(100), Some(20), Some(None));
1228
1229 assert!(update_result.is_ok());
1230
1231 // Test updating only credits
1232 let credits_only_result = client.update_subscription(sub_id, Some(200), None, None);
1233 assert!(credits_only_result.is_ok());
1234
1235 // Test updating only frequency
1236 let frequency_only_result = client.update_subscription(sub_id, None, Some(5), None);
1237 assert!(frequency_only_result.is_ok());
1238 }
1239
1240 #[test]
1241 fn test_create_subscription_edge_values() {
1242 let client = mock_client();
1243
1244 // Test create subscription API with maximum values
1245 let max_values_result = client.create_subscription(
1246 u64::MAX, // credits
1247 u32::MAX, // frequency
1248 None, // metadata - use None to avoid BoundedVec complexity
1249 Some([u8::MAX; 32]), // sub_id
1250 None, // call_params,
1251 None, // origin_kind
1252 );
1253
1254 assert!(max_values_result.is_ok());
1255
1256 // Test with minimum values
1257 let min_values_result = client.create_subscription(
1258 1, // credits
1259 1, // frequency
1260 None, // metadata
1261 None, // sub_id (auto-generated)
1262 None, // call_params
1263 None, // origin_kind
1264 );
1265
1266 assert!(min_values_result.is_ok());
1267
1268 // Test with invalid 0 values
1269 let zero_values_result = client.create_subscription(
1270 0, // credits
1271 0, // frequency
1272 None, // metadata
1273 None, // sub_id (auto-generated)
1274 None, // call_params
1275 None, // origin_kind
1276 );
1277
1278 assert!(matches!(zero_values_result, Err(Error::InvalidParams)));
1279 }
1280
1281 #[ink::test]
1282 fn test_create_callback_data() {
1283 // Setup ink! test environment
1284 let accounts = ink::env::test::default_accounts::<ink::env::DefaultEnvironment>();
1285 ink::env::test::set_caller::<ink::env::DefaultEnvironment>(accounts.alice);
1286
1287 let client = mock_client();
1288
1289 // Test that pulse callback data is generated correctly
1290 let callback_data = client.create_callback_data(CONSUME_PULSE_SEL, Vec::new(), None);
1291
1292 // Should return Ok(CallData)
1293 assert!(callback_data.is_ok());
1294
1295 // The callback data should be consistent across calls
1296 let callback_data_2 = client.create_callback_data(CONSUME_PULSE_SEL, Vec::new(), None);
1297 assert_eq!(callback_data, callback_data_2);
1298
1299 // Verify the encoded data contains the expected structure
1300 // We can't easily test the exact encoded bytes due to complex tuple encoding
1301 // but we can verify it's non-empty and consistent
1302 let encoded_data = callback_data.unwrap();
1303 assert!(!encoded_data.is_empty());
1304 }
1305
1306 #[ink::test]
1307 fn test_create_create_dummy_sub_info_response() {
1308 let client = mock_client();
1309
1310 let sub_id = [1; 32];
1311 let req_ref = [2; 32];
1312 let dummy_sub_info_response =
1313 client.create_dummy_sub_info_response(sub_id, req_ref, None, None);
1314 assert!(dummy_sub_info_response.is_ok());
1315 // Ensure that we always return the same dummy SubInfoResponse given the same data
1316 let dummy_sub_info_response2 =
1317 client.create_dummy_sub_info_response(sub_id, req_ref, None, None);
1318
1319 assert_eq!(dummy_sub_info_response2, dummy_sub_info_response);
1320 }
1321
1322 #[ink::test]
1323 fn test_location_helper_api() {
1324 // Setup ink! test environment
1325 let accounts = ink::env::test::default_accounts::<ink::env::DefaultEnvironment>();
1326 ink::env::test::set_caller::<ink::env::DefaultEnvironment>(accounts.alice);
1327
1328 let client = mock_client();
1329
1330 // Test sibling IDN location
1331 let idn_location = client.sibling_idn_location();
1332 assert_eq!(idn_location.parents, 1);
1333
1334 // Test self parachain sibling location
1335 let self_location = client.self_para_sibling_location();
1336 assert_eq!(self_location.parents, 1);
1337
1338 // Test contract IDN location - now works with mocked environment
1339 let contract_location = client.contract_idn_location();
1340 assert_eq!(contract_location.parents, 0);
1341 }
1342
1343 #[test]
1344 fn test_pulse_encode_decode() {
1345 use super::types::Pulse;
1346 // Create a test pulse - note: actual Pulse implementation may vary
1347 // This test verifies that Pulse type can be encoded/decoded properly
1348
1349 // We can't create a Pulse directly without knowing its exact constructor
1350 // but we can test that the type exists and implements the required traits
1351 let result = std::panic::catch_unwind(|| {
1352 let _pulse_type_exists = |_p: Pulse| {
1353 // This function existing proves Pulse type is available
1354 true
1355 };
1356 });
1357 assert!(result.is_ok());
1358 }
1359}