apex_sdk_substrate/
contracts.rs

1//! Ink! smart contract deployment and interaction
2//!
3//! This module provides functionality for deploying and interacting with
4//! ink! smart contracts on Substrate-based chains.
5//!
6//! ## Features
7//!
8//! - Deploy compiled ink! contracts (Wasm)
9//! - Call contract methods (read and write)
10//! - Parse contract metadata
11//! - Handle contract events
12//! - Gas estimation for contract calls
13//!
14//! ## Example
15//!
16//! ```rust,ignore
17//! use apex_sdk_substrate::contracts::ContractClient;
18//!
19//! // Deploy a contract
20//! let contract = ContractClient::deploy(
21//!     client,
22//!     wasm_code,
23//!     metadata,
24//!     constructor_args,
25//! ).await?;
26//!
27//! // Call a contract method
28//! let result = contract
29//!     .call("transfer")
30//!     .args(&[recipient, amount])
31//!     .execute(&wallet)
32//!     .await?;
33//! ```
34
35use crate::{Error, Result, Sr25519Signer, Wallet};
36use serde::{Deserialize, Serialize};
37use subxt::{OnlineClient, PolkadotConfig};
38use tracing::{debug, info};
39
40/// Contract address type (32-byte account ID)
41pub type ContractAddress = [u8; 32];
42
43/// Contract metadata from the ink! compilation
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct ContractMetadata {
46    /// Contract specification
47    pub spec: ContractSpec,
48    /// Storage layout
49    pub storage: StorageLayout,
50    /// Types used in the contract
51    pub types: Vec<TypeDef>,
52}
53
54/// Contract specification
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct ContractSpec {
57    /// Contract constructors
58    pub constructors: Vec<ConstructorSpec>,
59    /// Contract messages (methods)
60    pub messages: Vec<MessageSpec>,
61    /// Contract events
62    pub events: Vec<EventSpec>,
63}
64
65/// Constructor specification
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct ConstructorSpec {
68    /// Constructor name
69    pub label: String,
70    /// Selector (first 4 bytes of hash)
71    pub selector: [u8; 4],
72    /// Arguments
73    pub args: Vec<MessageArg>,
74    /// Documentation
75    pub docs: Vec<String>,
76}
77
78/// Message (method) specification
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct MessageSpec {
81    /// Message name
82    pub label: String,
83    /// Selector
84    pub selector: [u8; 4],
85    /// Arguments
86    pub args: Vec<MessageArg>,
87    /// Return type
88    pub return_type: Option<TypeRef>,
89    /// Is this a mutable call?
90    pub mutates: bool,
91    /// Is this payable?
92    pub payable: bool,
93    /// Documentation
94    pub docs: Vec<String>,
95}
96
97/// Message argument
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct MessageArg {
100    /// Argument name
101    pub label: String,
102    /// Type reference
103    pub type_ref: TypeRef,
104}
105
106/// Event specification
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct EventSpec {
109    /// Event name
110    pub label: String,
111    /// Event arguments
112    pub args: Vec<EventArg>,
113    /// Documentation
114    pub docs: Vec<String>,
115}
116
117/// Event argument
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct EventArg {
120    /// Argument name
121    pub label: String,
122    /// Type reference
123    pub type_ref: TypeRef,
124    /// Is this indexed?
125    pub indexed: bool,
126}
127
128/// Storage layout
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct StorageLayout {
131    /// Root storage key
132    pub root: LayoutKey,
133}
134
135/// Layout key
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct LayoutKey {
138    /// Key value
139    pub key: String,
140    /// Type ID
141    pub ty: u32,
142}
143
144/// Type reference
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct TypeRef {
147    /// Type ID
148    pub ty: u32,
149    /// Display name
150    pub display_name: Vec<String>,
151}
152
153/// Type definition
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct TypeDef {
156    /// Type ID
157    pub id: u32,
158    /// Type path
159    pub path: Vec<String>,
160    /// Type params
161    pub params: Vec<TypeParam>,
162    /// Type definition
163    pub def: TypeDefVariant,
164}
165
166/// Type parameter
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct TypeParam {
169    /// Parameter name
170    pub name: String,
171    /// Type reference
172    pub ty: Option<u32>,
173}
174
175/// Type definition variant
176#[derive(Debug, Clone, Serialize, Deserialize)]
177#[serde(tag = "type")]
178pub enum TypeDefVariant {
179    Composite { fields: Vec<Field> },
180    Variant { variants: Vec<Variant> },
181    Sequence { type_param: u32 },
182    Array { len: u32, type_param: u32 },
183    Tuple { fields: Vec<u32> },
184    Primitive { primitive: String },
185}
186
187/// Field definition
188#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct Field {
190    /// Field name
191    pub name: Option<String>,
192    /// Type reference
193    pub ty: u32,
194}
195
196/// Variant definition
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct Variant {
199    /// Variant name
200    pub name: String,
201    /// Fields
202    pub fields: Vec<Field>,
203    /// Index
204    pub index: u8,
205}
206
207/// Gas limit for contract calls
208#[derive(Debug, Clone, Copy, parity_scale_codec::Encode, parity_scale_codec::Decode)]
209pub struct GasLimit {
210    /// Reference time
211    pub ref_time: u64,
212    /// Proof size
213    pub proof_size: u64,
214}
215
216impl GasLimit {
217    /// Create a new gas limit
218    pub fn new(ref_time: u64, proof_size: u64) -> Self {
219        Self {
220            ref_time,
221            proof_size,
222        }
223    }
224
225    /// Default gas limit for most operations
226    pub fn default_call() -> Self {
227        Self {
228            ref_time: 1_000_000_000_000, // 1 trillion
229            proof_size: 3_145_728,       // ~3MB
230        }
231    }
232
233    /// Higher gas limit for deployment
234    pub fn default_deploy() -> Self {
235        Self {
236            ref_time: 5_000_000_000_000, // 5 trillion
237            proof_size: 10_485_760,      // ~10MB
238        }
239    }
240}
241
242/// Storage deposit limit
243#[derive(Debug, Clone, Copy)]
244pub enum StorageDepositLimit {
245    /// No limit (use account balance)
246    NoLimit,
247    /// Specific limit in tokens
248    Limited(u128),
249}
250
251/// Contract call builder
252#[allow(dead_code)]
253pub struct ContractCallBuilder {
254    contract_address: ContractAddress,
255    selector: [u8; 4],
256    args: Vec<u8>,
257    gas_limit: GasLimit,
258    storage_deposit: StorageDepositLimit,
259    value: u128,
260}
261
262impl ContractCallBuilder {
263    /// Create a new contract call builder
264    pub fn new(contract_address: ContractAddress, selector: [u8; 4]) -> Self {
265        Self {
266            contract_address,
267            selector,
268            args: Vec::new(),
269            gas_limit: GasLimit::default_call(),
270            storage_deposit: StorageDepositLimit::NoLimit,
271            value: 0,
272        }
273    }
274
275    /// Set the call arguments (SCALE-encoded)
276    pub fn args(mut self, args: &[u8]) -> Self {
277        self.args = args.to_vec();
278        self
279    }
280
281    /// Set the gas limit
282    pub fn gas_limit(mut self, limit: GasLimit) -> Self {
283        self.gas_limit = limit;
284        self
285    }
286
287    /// Set the storage deposit limit
288    pub fn storage_deposit(mut self, limit: StorageDepositLimit) -> Self {
289        self.storage_deposit = limit;
290        self
291    }
292
293    /// Set the value to transfer with the call
294    pub fn value(mut self, value: u128) -> Self {
295        self.value = value;
296        self
297    }
298
299    /// Build the call data
300    pub fn build_call_data(&self) -> Vec<u8> {
301        let mut call_data = Vec::new();
302        call_data.extend_from_slice(&self.selector);
303        call_data.extend_from_slice(&self.args);
304        call_data
305    }
306}
307
308/// Contract client for interacting with deployed contracts
309pub struct ContractClient {
310    client: OnlineClient<PolkadotConfig>,
311    address: ContractAddress,
312    metadata: Option<ContractMetadata>,
313}
314
315impl ContractClient {
316    /// Create a new contract client for an existing contract
317    pub fn new(client: OnlineClient<PolkadotConfig>, address: ContractAddress) -> Self {
318        Self {
319            client,
320            address,
321            metadata: None,
322        }
323    }
324
325    /// Create a contract client with metadata
326    pub fn with_metadata(
327        client: OnlineClient<PolkadotConfig>,
328        address: ContractAddress,
329        metadata: ContractMetadata,
330    ) -> Self {
331        Self {
332            client,
333            address,
334            metadata: Some(metadata),
335        }
336    }
337
338    /// Deploy a new contract
339    ///
340    /// # Arguments
341    ///
342    /// * `client` - Subxt client
343    /// * `wasm_code` - Compiled Wasm code
344    /// * `metadata` - Contract metadata
345    /// * `constructor_name` - Name of the constructor to call
346    /// * `constructor_args` - SCALE-encoded constructor arguments
347    /// * `wallet` - Wallet to sign the deployment transaction
348    /// * `salt` - Salt for deterministic address generation
349    ///
350    /// # Returns
351    ///
352    /// Contract client for the deployed contract
353    pub async fn deploy(
354        client: OnlineClient<PolkadotConfig>,
355        wasm_code: Vec<u8>,
356        metadata: ContractMetadata,
357        constructor_name: &str,
358        constructor_args: &[u8],
359        wallet: &Wallet,
360        salt: Option<Vec<u8>>,
361    ) -> Result<Self> {
362        info!("Deploying contract with constructor: {}", constructor_name);
363
364        // Find the constructor
365        let constructor = metadata
366            .spec
367            .constructors
368            .iter()
369            .find(|c| c.label == constructor_name)
370            .ok_or_else(|| {
371                Error::Transaction(format!("Constructor '{}' not found", constructor_name))
372            })?;
373
374        // Build constructor call data
375        let mut call_data = Vec::new();
376        call_data.extend_from_slice(&constructor.selector);
377        call_data.extend_from_slice(constructor_args);
378
379        // Prepare salt (use default if not provided)
380        let salt = salt.unwrap_or_else(|| vec![0u8; 32]);
381
382        // Build the instantiate call
383        let gas_limit = GasLimit::default_deploy();
384        let storage_deposit = StorageDepositLimit::NoLimit;
385
386        let instantiate_call = subxt::dynamic::tx(
387            "Contracts",
388            "instantiate",
389            vec![
390                subxt::dynamic::Value::u128(0), // value
391                Self::encode_gas_limit(&gas_limit)?,
392                Self::encode_storage_deposit(&storage_deposit)?,
393                subxt::dynamic::Value::from_bytes(&wasm_code),
394                subxt::dynamic::Value::from_bytes(&call_data),
395                subxt::dynamic::Value::from_bytes(&salt),
396            ],
397        );
398
399        // Submit the transaction
400        let pair = wallet
401            .sr25519_pair()
402            .ok_or_else(|| Error::Transaction("Wallet does not have SR25519 key".to_string()))?;
403
404        let signer = Sr25519Signer::new(pair.clone());
405
406        let mut progress = client
407            .tx()
408            .sign_and_submit_then_watch_default(&instantiate_call, &signer)
409            .await
410            .map_err(|e| {
411                Error::Transaction(format!("Failed to submit deploy transaction: {}", e))
412            })?;
413
414        // Wait for finalization and extract contract address
415        while let Some(event) = progress.next().await {
416            let event = event
417                .map_err(|e| Error::Transaction(format!("Deploy transaction error: {}", e)))?;
418
419            if let Some(finalized) = event.as_finalized() {
420                info!("Contract deployment finalized");
421
422                // Extract contract address from events
423                let events = finalized
424                    .fetch_events()
425                    .await
426                    .map_err(|e| Error::Transaction(format!("Failed to get events: {}", e)))?;
427
428                for evt in events.iter() {
429                    let evt = evt.map_err(|e| {
430                        Error::Transaction(format!("Failed to decode event: {}", e))
431                    })?;
432
433                    // Look for Contracts.Instantiated event
434                    if evt.pallet_name() == "Contracts" && evt.variant_name() == "Instantiated" {
435                        // Extract contract address from event data
436                        // This is a simplified version - in production, parse the actual event fields
437                        debug!("Contract instantiated event found");
438
439                        // Extract contract address from event fields
440                        // The Instantiated event is SCALE encoded, we need to decode it
441                        // For now, use the raw bytes and try to extract the address
442                        let field_bytes = evt.field_bytes();
443
444                        // The event fields are SCALE encoded - skip the first field (deployer)
445                        // and extract the second field (contract address)
446                        // This is a simplified approach; proper decoding should use the metadata
447                        if field_bytes.len() >= 64 {
448                            let mut contract_address = [0u8; 32];
449                            // Skip first 32 bytes (deployer) and take next 32 bytes (contract)
450                            contract_address.copy_from_slice(&field_bytes[32..64]);
451                            return Ok(Self::with_metadata(client, contract_address, metadata));
452                        } else {
453                            return Err(Error::Transaction(format!(
454                                "Contract event data has unexpected length: {}",
455                                field_bytes.len()
456                            )));
457                        }
458                    }
459                }
460
461                return Err(Error::Transaction(
462                    "Contract deployment succeeded but address not found in events".to_string(),
463                ));
464            }
465        }
466
467        Err(Error::Transaction(
468            "Deploy transaction stream ended without finalization".to_string(),
469        ))
470    }
471
472    /// Call a contract method (mutable)
473    ///
474    /// # Arguments
475    ///
476    /// * `method_name` - Name of the method to call
477    /// * `args` - SCALE-encoded method arguments
478    /// * `wallet` - Wallet to sign the transaction
479    ///
480    /// # Returns
481    ///
482    /// Transaction hash of the call
483    pub async fn call(&self, method_name: &str, args: &[u8], wallet: &Wallet) -> Result<String> {
484        info!("Calling contract method: {}", method_name);
485
486        // Find the message in metadata
487        let message = if let Some(ref metadata) = self.metadata {
488            metadata
489                .spec
490                .messages
491                .iter()
492                .find(|m| m.label == method_name)
493                .ok_or_else(|| Error::Transaction(format!("Method '{}' not found", method_name)))?
494        } else {
495            return Err(Error::Transaction(
496                "Contract metadata not available".to_string(),
497            ));
498        };
499
500        // Build call data
501        let mut call_data = Vec::new();
502        call_data.extend_from_slice(&message.selector);
503        call_data.extend_from_slice(args);
504
505        // Build the call transaction
506        let gas_limit = GasLimit::default_call();
507        let storage_deposit = StorageDepositLimit::NoLimit;
508
509        let call_tx = subxt::dynamic::tx(
510            "Contracts",
511            "call",
512            vec![
513                subxt::dynamic::Value::from_bytes(self.address),
514                subxt::dynamic::Value::u128(0), // value
515                Self::encode_gas_limit(&gas_limit)?,
516                Self::encode_storage_deposit(&storage_deposit)?,
517                subxt::dynamic::Value::from_bytes(&call_data),
518            ],
519        );
520
521        // Submit the transaction
522        let pair = wallet
523            .sr25519_pair()
524            .ok_or_else(|| Error::Transaction("Wallet does not have SR25519 key".to_string()))?;
525
526        let signer = Sr25519Signer::new(pair.clone());
527
528        let mut progress = self
529            .client
530            .tx()
531            .sign_and_submit_then_watch_default(&call_tx, &signer)
532            .await
533            .map_err(|e| Error::Transaction(format!("Failed to submit call transaction: {}", e)))?;
534
535        // Wait for finalization
536        while let Some(event) = progress.next().await {
537            let event =
538                event.map_err(|e| Error::Transaction(format!("Call transaction error: {}", e)))?;
539
540            if let Some(finalized) = event.as_finalized() {
541                let tx_hash = format!("0x{}", hex::encode(finalized.extrinsic_hash()));
542                info!("Contract call finalized: {}", tx_hash);
543
544                finalized
545                    .wait_for_success()
546                    .await
547                    .map_err(|e| Error::Transaction(format!("Contract call failed: {}", e)))?;
548
549                return Ok(tx_hash);
550            }
551        }
552
553        Err(Error::Transaction(
554            "Call transaction stream ended without finalization".to_string(),
555        ))
556    }
557
558    /// Read contract state (dry-run, doesn't modify state)
559    ///
560    /// This method performs a read-only call to a smart contract method without
561    /// submitting a transaction. It uses the `ContractsApi_call` runtime API to
562    /// execute the contract call and return the result.
563    ///
564    /// # Arguments
565    ///
566    /// * `method_name` - Name of the method to call (from contract metadata)
567    /// * `args` - SCALE-encoded method arguments
568    /// * `caller` - Address of the caller (32-byte account ID, for access control checks)
569    ///
570    /// # Returns
571    ///
572    /// SCALE-encoded return value from the contract method
573    ///
574    /// # Example
575    ///
576    /// ```rust,ignore
577    /// // Read the balance of an account from a token contract
578    /// let caller = [0u8; 32]; // Ilara's account
579    /// let result = contract.read("get_balance", &encoded_args, &caller).await?;
580    /// let balance: u128 = Decode::decode(&mut &result[..])?;
581    /// ```
582    ///
583    /// # Implementation Details
584    ///
585    /// This method:
586    /// 1. Looks up the method selector from contract metadata
587    /// 2. Encodes the call data (selector + arguments)
588    /// 3. Calls `ContractsApi_call` runtime API via `state_call`
589    /// 4. Decodes the `ContractExecResult` to extract the return data
590    ///
591    /// The runtime API call is made at the latest finalized block to ensure
592    /// consistent results.
593    pub async fn read(&self, method_name: &str, args: &[u8], caller: &[u8; 32]) -> Result<Vec<u8>> {
594        debug!("Reading contract state: {}", method_name);
595
596        // Find the message in metadata
597        let message = if let Some(ref metadata) = self.metadata {
598            metadata
599                .spec
600                .messages
601                .iter()
602                .find(|m| m.label == method_name)
603                .ok_or_else(|| Error::Transaction(format!("Method '{}' not found", method_name)))?
604        } else {
605            return Err(Error::Transaction(
606                "Contract metadata not available".to_string(),
607            ));
608        };
609
610        // Build call data
611        let mut call_data = Vec::new();
612        call_data.extend_from_slice(&message.selector);
613        call_data.extend_from_slice(args);
614
615        // Prepare parameters for ContractsApi_call runtime API
616        // The ContractsApi_call expects: (origin, dest, value, gas_limit, storage_deposit_limit, input_data)
617        use parity_scale_codec::Encode;
618
619        let gas_limit = GasLimit::default_call();
620        let value: u128 = 0; // No value transfer for reads
621        let storage_deposit_limit: Option<u128> = None; // No storage deposit for reads
622
623        // Encode the parameters according to the ContractsApi_call signature
624        let mut encoded_params = Vec::new();
625        caller.encode_to(&mut encoded_params); // origin
626        self.address.encode_to(&mut encoded_params); // dest
627        value.encode_to(&mut encoded_params); // value
628        gas_limit.encode_to(&mut encoded_params); // gas_limit
629        storage_deposit_limit.encode_to(&mut encoded_params); // storage_deposit_limit
630        call_data.encode_to(&mut encoded_params); // input_data
631
632        // Call the runtime API using state_call
633        let result_bytes = self
634            .client
635            .backend()
636            .call(
637                "ContractsApi_call",
638                Some(&encoded_params),
639                self.client
640                    .backend()
641                    .latest_finalized_block_ref()
642                    .await?
643                    .hash(),
644            )
645            .await
646            .map_err(|e| Error::Transaction(format!("ContractsApi_call failed: {}", e)))?;
647
648        // Decode the result
649        // ContractsApi_call returns ContractExecResult which is SCALE encoded
650        // For simplicity, we'll try to decode the common structure:
651        // ContractExecResult { gas_consumed, gas_required, storage_deposit, debug_message, result }
652
653        // The result is a SCALE-encoded ContractExecResult
654        // We need to decode it to get the actual return data
655        let decoded_result = Self::decode_contract_result(&result_bytes)?;
656
657        Ok(decoded_result)
658    }
659
660    /// Decode ContractExecResult from SCALE-encoded bytes
661    fn decode_contract_result(bytes: &[u8]) -> Result<Vec<u8>> {
662        use parity_scale_codec::Decode;
663
664        // ContractExecResult structure (simplified):
665        // struct ContractExecResult {
666        //     gas_consumed: Weight,
667        //     gas_required: Weight,
668        //     storage_deposit: StorageDeposit,
669        //     debug_message: Vec<u8>,
670        //     result: Result<ExecReturnValue, DispatchError>
671        // }
672
673        let mut input = bytes;
674
675        // Skip gas_consumed (Weight = {ref_time: u64, proof_size: u64})
676        let _gas_consumed_ref_time = u64::decode(&mut input).map_err(|e| {
677            Error::Transaction(format!("Failed to decode gas_consumed.ref_time: {}", e))
678        })?;
679        let _gas_consumed_proof_size = u64::decode(&mut input).map_err(|e| {
680            Error::Transaction(format!("Failed to decode gas_consumed.proof_size: {}", e))
681        })?;
682
683        // Skip gas_required (Weight = {ref_time: u64, proof_size: u64})
684        let _gas_required_ref_time = u64::decode(&mut input).map_err(|e| {
685            Error::Transaction(format!("Failed to decode gas_required.ref_time: {}", e))
686        })?;
687        let _gas_required_proof_size = u64::decode(&mut input).map_err(|e| {
688            Error::Transaction(format!("Failed to decode gas_required.proof_size: {}", e))
689        })?;
690
691        // Skip storage_deposit (enum with variants)
692        // StorageDeposit is an enum: Charge(u128) | Refund(u128)
693        let storage_deposit_variant = u8::decode(&mut input).map_err(|e| {
694            Error::Transaction(format!("Failed to decode storage_deposit variant: {}", e))
695        })?;
696        if storage_deposit_variant <= 1 {
697            // Has a u128 value
698            let _deposit_amount = u128::decode(&mut input).map_err(|e| {
699                Error::Transaction(format!("Failed to decode storage_deposit amount: {}", e))
700            })?;
701        }
702
703        // Skip debug_message (Vec<u8>)
704        let debug_msg = Vec::<u8>::decode(&mut input)
705            .map_err(|e| Error::Transaction(format!("Failed to decode debug_message: {}", e)))?;
706
707        if !debug_msg.is_empty() {
708            debug!(
709                "Contract debug message: {}",
710                String::from_utf8_lossy(&debug_msg)
711            );
712        }
713
714        // Decode result: Result<ExecReturnValue, DispatchError>
715        let result_variant = u8::decode(&mut input)
716            .map_err(|e| Error::Transaction(format!("Failed to decode result variant: {}", e)))?;
717
718        if result_variant == 0 {
719            // Ok variant - contains ExecReturnValue
720            // ExecReturnValue { flags: u32, data: Vec<u8> }
721            let _flags = u32::decode(&mut input)
722                .map_err(|e| Error::Transaction(format!("Failed to decode flags: {}", e)))?;
723
724            let data = Vec::<u8>::decode(&mut input)
725                .map_err(|e| Error::Transaction(format!("Failed to decode return data: {}", e)))?;
726
727            Ok(data)
728        } else {
729            // Err variant - contains DispatchError
730            Err(Error::Transaction(
731                "Contract execution failed with DispatchError".to_string(),
732            ))
733        }
734    }
735
736    /// Get the contract address
737    pub fn address(&self) -> &ContractAddress {
738        &self.address
739    }
740
741    /// Get the contract metadata
742    pub fn metadata(&self) -> Option<&ContractMetadata> {
743        self.metadata.as_ref()
744    }
745
746    // Helper methods
747
748    fn encode_gas_limit(limit: &GasLimit) -> Result<subxt::dynamic::Value> {
749        Ok(subxt::dynamic::Value::named_composite([
750            (
751                "ref_time",
752                subxt::dynamic::Value::u128(limit.ref_time as u128),
753            ),
754            (
755                "proof_size",
756                subxt::dynamic::Value::u128(limit.proof_size as u128),
757            ),
758        ]))
759    }
760
761    fn encode_storage_deposit(limit: &StorageDepositLimit) -> Result<subxt::dynamic::Value> {
762        match limit {
763            StorageDepositLimit::NoLimit => {
764                Ok(subxt::dynamic::Value::unnamed_variant("None", vec![]))
765            }
766            StorageDepositLimit::Limited(amount) => Ok(subxt::dynamic::Value::unnamed_variant(
767                "Some",
768                vec![subxt::dynamic::Value::u128(*amount)],
769            )),
770        }
771    }
772}
773
774/// Parse contract metadata from JSON
775pub fn parse_metadata(json: &str) -> Result<ContractMetadata> {
776    serde_json::from_str(json)
777        .map_err(|e| Error::Metadata(format!("Failed to parse contract metadata: {}", e)))
778}
779
780#[cfg(test)]
781mod tests {
782    use super::*;
783
784    #[test]
785    fn test_gas_limit() {
786        let limit = GasLimit::default_call();
787        assert!(limit.ref_time > 0);
788        assert!(limit.proof_size > 0);
789    }
790
791    #[test]
792    fn test_contract_call_builder() {
793        let address = [1u8; 32];
794        let selector = [0x12, 0x34, 0x56, 0x78];
795
796        let builder = ContractCallBuilder::new(address, selector)
797            .args(&[1, 2, 3])
798            .value(1000);
799
800        let call_data = builder.build_call_data();
801        assert_eq!(&call_data[0..4], &selector);
802        assert_eq!(&call_data[4..], &[1, 2, 3]);
803    }
804}