Skip to main content

crosschain/
crosschain.rs

1use rust_pact::{tools, fetch};
2use rust_pact::utils::KeyPair;
3use serde_json::json;
4use std::time::Duration;
5use std::thread;
6
7// Run with: cargo run --example crosschain
8// To perform real network calls set env var RUN_LIVE=1 (these hit public Chainweb endpoints)
9fn main() {
10    // Check if we should make real network calls
11    let run_live = std::env::var("RUN_LIVE").unwrap_or_default() == "1";
12    // Configurable timing via env vars (fallback to defaults)
13    let attempts: u32 = std::env::var("XCHAIN_ATTEMPTS").ok().and_then(|v| v.parse().ok()).unwrap_or(30);
14    let spv_attempts: u32 = std::env::var("XCHAIN_SPV_ATTEMPTS").ok().and_then(|v| v.parse().ok()).unwrap_or(30);
15    let interval_secs: u64 = std::env::var("XCHAIN_INTERVAL").ok().and_then(|v| v.parse().ok()).unwrap_or(5);
16    let spv_interval_secs: u64 = std::env::var("XCHAIN_SPV_INTERVAL").ok().and_then(|v| v.parse().ok()).unwrap_or(interval_secs);
17    let post_confirm_wait: u64 = std::env::var("XCHAIN_POST_CONFIRM_WAIT").ok().and_then(|v| v.parse().ok()).unwrap_or(10);
18    
19    if run_live {
20        println!("🌐 RUN_LIVE=1 detected - will make real network calls to testnet");
21        println!("⚠️  WARNING: This will attempt actual blockchain transactions!");
22    } else {
23        println!("🔒 Demo mode - using mock responses (set RUN_LIVE=1 for real calls)");
24    }
25    
26    // WARNING: Hard‑coding real secret keys is unsafe. Use env vars or a secure store in production.
27    let sender_keypair = KeyPair {
28        public_key: "10375651f1ca0110468152bb8f47b7b8a469e36dfab1c83adf60cab84b5726d3".into(),
29        secret_key: "18d3a823139cf60cab0b738e7605bb9e4a2f3ff245c270fa55d197f9b3c4c004".into(),
30        clist: None
31    };
32
33    let receiver_keypair = KeyPair {
34        public_key: "03df480e0b300c52901fdff265f0460913fea495f39972321698740536cc38e3".into(),
35        secret_key: "".into(), // Not needed for completion, only public key
36        clist: None
37    };
38
39    let token_address = "coin";
40    let sender_account = "k:10375651f1ca0110468152bb8f47b7b8a469e36dfab1c83adf60cab84b5726d3";
41    let receiver_account = "k:03df480e0b300c52901fdff265f0460913fea495f39972321698740536cc38e3";
42    let receiver_public_key = "03df480e0b300c52901fdff265f0460913fea495f39972321698740536cc38e3";
43    let amount = 1.0_f64;
44    let source_chain_id = "1"; // Chain where the transfer originates
45    let target_chain_id = "2"; // Chain where the transfer completes
46    let network_id = "testnet04";
47
48    println!("=== Cross-Chain Transfer Example ===\n");
49
50    // Step 1: Initiate cross-chain transfer on source chain
51    println!("Step 1: Initiating cross-chain transfer from chain {} to chain {}", source_chain_id, target_chain_id);
52    println!("Amount: {} {}", amount, token_address);
53    println!("From: {}", sender_account);
54    println!("To: {}", receiver_account);
55
56    let transfer_result = if run_live {
57        tools::crosschain_transfer(
58            token_address,
59            sender_account,
60            receiver_account,
61            receiver_public_key,
62            amount,
63            sender_keypair.clone(),
64            source_chain_id,
65            target_chain_id,
66            network_id,
67            None
68        )
69    } else {
70        // Mock response for demo
71        json!({
72            "requestKeys": ["KnTrT06r4gLKuA56fRMZOCBjS1MiSNoCGDO8g8VxYVI"]
73        })
74    };
75
76    println!("Transfer initiation result: {}", serde_json::to_string_pretty(&transfer_result).unwrap());
77
78    // Extract the request key (transaction hash) for polling
79    if let Some(request_keys) = transfer_result.get("requestKeys").and_then(|rks| rks.as_array()) {
80        if let Some(request_key) = request_keys.get(0).and_then(|rk| rk.as_str()) {
81            println!("\nTransaction hash (request key): {}", request_key);
82            
83            // Step 2: Poll for transaction completion on source chain
84            println!("\nStep 2: Polling for transaction completion on source chain...");
85            let api_host = tools::get_api_host(network_id, source_chain_id);
86            
87            let poll_result = if run_live {
88                poll_until_result(request_key, &api_host, attempts, interval_secs)
89            } else {
90                // Mock response for demo
91                json!({
92                    request_key: {
93                        "result": {
94                            "status": "success",
95                            "data": "Cross-chain transfer initiated"
96                        },
97                        "continuation": {
98                            "pactId": "mock-pact-id-123",
99                            "step": 0
100                        }
101                    }
102                })
103            };
104            println!("Final poll result: {}", serde_json::to_string_pretty(&poll_result).unwrap());
105
106            // Step 3: Extract pact ID from the poll result
107            if let Some(pact_id) = extract_pact_id(&poll_result) {
108                println!("\nPact ID for continuation: {}", pact_id);
109
110                // Step 4: Get SPV proof (in real scenario, you would get this from the network)
111                println!("\nStep 4: Getting SPV proof for cross-chain completion...");
112                if run_live && post_confirm_wait > 0 {
113                    println!("Waiting {}s after confirmation before requesting SPV (XCHAIN_POST_CONFIRM_WAIT)", post_confirm_wait);
114                    thread::sleep(Duration::from_secs(post_confirm_wait));
115                }
116                let spv_cmd = json!({
117                    "requestKey": request_key,
118                    "targetChainId": target_chain_id
119                });
120                
121                let spv_result = if run_live {
122                    poll_for_spv_proof(&spv_cmd, &api_host, spv_attempts, spv_interval_secs)
123                } else {
124                    // Mock SPV proof for demo
125                    json!({
126                        "proof": "mock-spv-proof-data-for-demo"
127                    })
128                };
129                println!("SPV result: {}", serde_json::to_string_pretty(&spv_result).unwrap());
130
131                // Step 5: Complete cross-chain transfer on target chain
132                if let Some(proof) = extract_spv_proof(&spv_result) {
133                    println!("\nStep 5: Completing cross-chain transfer on target chain...");
134                    
135                    let complete_result = if run_live {
136                        tools::crosschain_complete(
137                            &pact_id,
138                            &proof,
139                            receiver_account,
140                            receiver_public_key,
141                            amount,
142                            receiver_keypair.clone(),
143                            target_chain_id,
144                            network_id
145                        )
146                    } else {
147                        // Mock completion result for demo
148                        json!({
149                            "requestKeys": ["completion-tx-hash-456"],
150                            "result": "Cross-chain transfer completed successfully"
151                        })
152                    };
153
154                    println!("Cross-chain completion result: {}", serde_json::to_string_pretty(&complete_result).unwrap());
155                } else {
156                    println!("Could not extract SPV proof from result");
157                    println!("In a real scenario, you would need to wait for the transaction to be included in a block and then request the SPV proof");
158                }
159            } else {
160                println!("Could not extract pact ID from poll result");
161            }
162        }
163    } else {
164        println!("No request keys found in transfer result");
165    }
166
167    println!("\n=== Example Notes ===");
168    println!("1. This example shows the complete cross-chain transfer flow");
169    println!("2. In production, you would need to wait for block confirmations between steps");
170    println!("3. SPV proofs are generated by the blockchain network after transactions are finalized");
171    println!("4. The receiver keypair only needs the public key for the completion step");
172    println!("5. Set RUN_LIVE=1 environment variable to execute against real testnet");
173}
174
175/// Poll for transaction result until we get a successful result or timeout
176fn poll_until_result(request_key: &str, api_host: &str, max_attempts: u32, interval_seconds: u64) -> serde_json::Value {
177    let poll_cmd = json!({
178        "requestKeys": [request_key]
179    });
180    
181    for attempt in 1..=max_attempts {
182        println!("Polling attempt {}/{}", attempt, max_attempts);
183        
184        let poll_result = fetch::poll(&poll_cmd, api_host);
185        
186        // Check if we got a meaningful result
187        if has_transaction_result(&poll_result, request_key) {
188            println!("✓ Transaction found in poll result!");
189            return poll_result;
190        }
191        
192        if attempt < max_attempts {
193            println!("No result yet, waiting {} seconds before next attempt...", interval_seconds);
194            thread::sleep(Duration::from_secs(interval_seconds));
195        }
196    }
197    
198    println!("⚠️  Polling timeout reached after {} attempts", max_attempts);
199    json!({})
200}
201
202/// Check if the poll result contains actual transaction data
203fn has_transaction_result(poll_result: &serde_json::Value, request_key: &str) -> bool {
204    if let Some(result_map) = poll_result.as_object() {
205        if let Some(tx_result) = result_map.get(request_key) {
206            // Check if it's not null and has meaningful data
207            return !tx_result.is_null() && tx_result.as_object().map_or(false, |obj| !obj.is_empty());
208        }
209    }
210    false
211}
212
213/// Poll for SPV proof until available or timeout
214/// Poll for SPV proof until available or timeout. Chainweb may need several block confirmations
215/// before an SPV proof is available, especially cross-chain (yield inclusion + target adjacency).
216/// Common transient states:
217/// - Empty JSON / missing `proof`: proof not ready yet
218/// - {"error": ...} network or temporal failure; we retry unless permanent-looking
219fn poll_for_spv_proof(spv_cmd: &serde_json::Value, api_host: &str, max_attempts: u32, interval_seconds: u64) -> serde_json::Value {
220    for attempt in 1..=max_attempts {
221        println!("SPV proof attempt {}/{}", attempt, max_attempts);
222        
223        let spv_result = fetch::spv(spv_cmd, api_host);
224
225        // Log diagnostic hints when no proof
226        if !has_spv_proof(&spv_result) {
227            if let Some(err) = spv_result.get("error") {
228                println!("(diagnostic) SPV response error field: {}", err);
229            } else if spv_result.as_object().map(|o| o.is_empty()).unwrap_or(true) {
230                println!("(diagnostic) Empty SPV response – likely not yet indexed. Waiting...");
231            } else {
232                println!("(diagnostic) SPV response keys: {:?}", spv_result.as_object().map(|o| o.keys().collect::<Vec<_>>()));
233            }
234        }
235        
236        // Check if we got a valid SPV proof
237        if has_spv_proof(&spv_result) {
238            println!("✓ SPV proof obtained!");
239            return spv_result;
240        }
241        
242        if attempt < max_attempts {
243            println!("SPV proof not ready, waiting {} seconds...", interval_seconds);
244            thread::sleep(Duration::from_secs(interval_seconds));
245        }
246    }
247    
248    println!("⚠️  SPV proof polling timeout after {} attempts", max_attempts);
249    json!({})
250}
251
252/// Check if the SPV result contains a valid proof
253fn has_spv_proof(spv_result: &serde_json::Value) -> bool {
254    spv_result.get("proof")
255        .and_then(|p| p.as_str())
256        .map_or(false, |proof| !proof.is_empty())
257}
258
259fn extract_pact_id(poll_result: &serde_json::Value) -> Option<String> {
260    // Try to extract pact ID from poll result
261    poll_result
262        .as_object()?
263        .values()
264        .next()?
265        .get("continuation")?
266        .get("pactId")?
267        .as_str()
268        .map(|s| s.to_string())
269}
270
271fn extract_spv_proof(spv_result: &serde_json::Value) -> Option<String> {
272    // Try to extract SPV proof from SPV result
273    spv_result
274        .get("proof")?
275        .as_str()
276        .map(|s| s.to_string())
277}