Skip to main content

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}