721 lines
22 KiB
Rust
721 lines
22 KiB
Rust
use serde::{Deserialize, Serialize};
|
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|
|
|
/// This example demonstrates how multiple BFT-CRDT use cases can be combined
|
|
/// into a comprehensive DeFi platform that operates without global consensus.
|
|
|
|
// ==== Identity Layer ====
|
|
|
|
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
|
|
pub struct IdentityId(pub String);
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Identity {
|
|
pub id: IdentityId,
|
|
pub public_key: Vec<u8>,
|
|
pub attestations_received: HashSet<AttestationId>,
|
|
pub attestations_given: HashSet<AttestationId>,
|
|
pub reputation_score: f64,
|
|
pub created_at: u64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
|
|
pub struct AttestationId(pub String);
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Attestation {
|
|
pub id: AttestationId,
|
|
pub issuer: IdentityId,
|
|
pub subject: IdentityId,
|
|
pub claim: String,
|
|
pub confidence: u8,
|
|
pub timestamp: u64,
|
|
pub expiry: Option<u64>,
|
|
pub signature: Vec<u8>,
|
|
}
|
|
|
|
// ==== Multi-Party State Channel ====
|
|
|
|
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
|
|
pub struct ChannelId(pub String);
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct StateChannel {
|
|
pub id: ChannelId,
|
|
pub participants: Vec<IdentityId>,
|
|
pub balances: HashMap<(IdentityId, String), u128>, // (user, token) -> balance
|
|
pub orders: OrderBookCRDT,
|
|
pub positions: HashMap<IdentityId, Vec<Position>>,
|
|
pub nonce: u64,
|
|
pub last_update: u64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Position {
|
|
pub id: String,
|
|
pub owner: IdentityId,
|
|
pub market: String,
|
|
pub size: i128,
|
|
pub entry_price: u128,
|
|
pub leverage: u8,
|
|
pub margin: u128,
|
|
pub unrealized_pnl: i128,
|
|
}
|
|
|
|
// ==== Order Book CRDT ====
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct OrderBookCRDT {
|
|
pub orders: HashMap<String, Order>,
|
|
pub executions: HashMap<String, Execution>,
|
|
pub cancellations: HashSet<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Order {
|
|
pub id: String,
|
|
pub trader: IdentityId,
|
|
pub side: OrderSide,
|
|
pub price: u128,
|
|
pub amount: u128,
|
|
pub remaining: u128,
|
|
pub timestamp: u64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
pub enum OrderSide {
|
|
Buy,
|
|
Sell,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Execution {
|
|
pub id: String,
|
|
pub buy_order: String,
|
|
pub sell_order: String,
|
|
pub price: u128,
|
|
pub amount: u128,
|
|
pub timestamp: u64,
|
|
}
|
|
|
|
// ==== Oracle Price Data ====
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PriceSubmission {
|
|
pub oracle: IdentityId,
|
|
pub asset: String,
|
|
pub price: u128,
|
|
pub confidence: u8,
|
|
pub timestamp: u64,
|
|
pub sources: Vec<String>,
|
|
pub signature: Vec<u8>,
|
|
}
|
|
|
|
// ==== Cross-Chain Messages ====
|
|
|
|
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
|
|
pub struct ChainId(pub String);
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct CrossChainMessage {
|
|
pub id: String,
|
|
pub source_chain: ChainId,
|
|
pub dest_chain: ChainId,
|
|
pub sender: IdentityId,
|
|
pub action: CrossChainAction,
|
|
pub timestamp: u64,
|
|
pub signatures: Vec<Vec<u8>>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub enum CrossChainAction {
|
|
Deposit { token: String, amount: u128 },
|
|
Withdraw { token: String, amount: u128 },
|
|
SyncPosition { position: Position },
|
|
LiquidationAlert { position_id: String },
|
|
}
|
|
|
|
// ==== Integrated DeFi Platform ====
|
|
|
|
pub struct IntegratedDeFiPlatform {
|
|
// Identity layer
|
|
pub identities: HashMap<IdentityId, Identity>,
|
|
pub attestations: HashMap<AttestationId, Attestation>,
|
|
|
|
// State channels
|
|
pub channels: HashMap<ChannelId, StateChannel>,
|
|
|
|
// Oracle data
|
|
pub price_submissions: BTreeMap<(String, u64), Vec<PriceSubmission>>,
|
|
|
|
// Cross-chain messages
|
|
pub cross_chain_messages: HashMap<String, CrossChainMessage>,
|
|
|
|
// Platform parameters
|
|
pub min_reputation_score: f64,
|
|
pub liquidation_threshold: f64,
|
|
}
|
|
|
|
impl IntegratedDeFiPlatform {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
identities: HashMap::new(),
|
|
attestations: HashMap::new(),
|
|
channels: HashMap::new(),
|
|
price_submissions: BTreeMap::new(),
|
|
cross_chain_messages: HashMap::new(),
|
|
min_reputation_score: 0.5,
|
|
liquidation_threshold: 0.8,
|
|
}
|
|
}
|
|
|
|
// ==== Identity Functions ====
|
|
|
|
pub fn create_identity(&mut self, id: IdentityId, public_key: Vec<u8>) -> Result<(), String> {
|
|
if self.identities.contains_key(&id) {
|
|
return Err("Identity already exists".to_string());
|
|
}
|
|
|
|
let identity = Identity {
|
|
id: id.clone(),
|
|
public_key,
|
|
attestations_received: HashSet::new(),
|
|
attestations_given: HashSet::new(),
|
|
reputation_score: 0.0,
|
|
created_at: Self::timestamp(),
|
|
};
|
|
|
|
self.identities.insert(id, identity);
|
|
Ok(())
|
|
}
|
|
|
|
pub fn create_attestation(
|
|
&mut self,
|
|
issuer: IdentityId,
|
|
subject: IdentityId,
|
|
claim: String,
|
|
confidence: u8,
|
|
) -> Result<AttestationId, String> {
|
|
// Check issuer exists and has sufficient reputation
|
|
let issuer_identity = self.identities.get(&issuer).ok_or("Issuer not found")?;
|
|
|
|
if issuer_identity.reputation_score < self.min_reputation_score {
|
|
return Err("Insufficient reputation to issue attestations".to_string());
|
|
}
|
|
|
|
let attestation_id = AttestationId(format!("att_{}", Self::timestamp()));
|
|
let attestation = Attestation {
|
|
id: attestation_id.clone(),
|
|
issuer: issuer.clone(),
|
|
subject: subject.clone(),
|
|
claim,
|
|
confidence,
|
|
timestamp: Self::timestamp(),
|
|
expiry: None,
|
|
signature: vec![1, 2, 3], // Placeholder signature
|
|
};
|
|
|
|
// Update both identities
|
|
self.attestations
|
|
.insert(attestation_id.clone(), attestation);
|
|
|
|
if let Some(issuer_identity) = self.identities.get_mut(&issuer) {
|
|
issuer_identity
|
|
.attestations_given
|
|
.insert(attestation_id.clone());
|
|
}
|
|
|
|
if let Some(subject_identity) = self.identities.get_mut(&subject) {
|
|
subject_identity
|
|
.attestations_received
|
|
.insert(attestation_id.clone());
|
|
// Simple reputation update
|
|
subject_identity.reputation_score += (confidence as f64 / 100.0) * 0.1;
|
|
}
|
|
|
|
Ok(attestation_id)
|
|
}
|
|
|
|
// ==== State Channel Functions ====
|
|
|
|
pub fn create_channel(&mut self, participants: Vec<IdentityId>) -> Result<ChannelId, String> {
|
|
// Verify all participants exist and have sufficient reputation
|
|
for participant in &participants {
|
|
let identity = self
|
|
.identities
|
|
.get(participant)
|
|
.ok_or("Participant not found")?;
|
|
|
|
if identity.reputation_score < self.min_reputation_score {
|
|
return Err(format!(
|
|
"Participant {} has insufficient reputation",
|
|
participant.0
|
|
));
|
|
}
|
|
}
|
|
|
|
let channel_id = ChannelId(format!("channel_{}", Self::timestamp()));
|
|
let channel = StateChannel {
|
|
id: channel_id.clone(),
|
|
participants,
|
|
balances: HashMap::new(),
|
|
orders: OrderBookCRDT {
|
|
orders: HashMap::new(),
|
|
executions: HashMap::new(),
|
|
cancellations: HashSet::new(),
|
|
},
|
|
positions: HashMap::new(),
|
|
nonce: 0,
|
|
last_update: Self::timestamp(),
|
|
};
|
|
|
|
self.channels.insert(channel_id.clone(), channel);
|
|
Ok(channel_id)
|
|
}
|
|
|
|
pub fn place_order(
|
|
&mut self,
|
|
channel_id: &ChannelId,
|
|
trader: &IdentityId,
|
|
side: OrderSide,
|
|
price: u128,
|
|
amount: u128,
|
|
) -> Result<String, String> {
|
|
let channel = self
|
|
.channels
|
|
.get_mut(channel_id)
|
|
.ok_or("Channel not found")?;
|
|
|
|
// Verify trader is participant
|
|
if !channel.participants.contains(trader) {
|
|
return Err("Trader not in channel".to_string());
|
|
}
|
|
|
|
// Check balance for sells or margin for buys
|
|
match side {
|
|
OrderSide::Sell => {
|
|
let balance = channel
|
|
.balances
|
|
.get(&(trader.clone(), "ETH".to_string()))
|
|
.unwrap_or(&0);
|
|
if *balance < amount {
|
|
return Err("Insufficient balance".to_string());
|
|
}
|
|
}
|
|
OrderSide::Buy => {
|
|
let usdc_balance = channel
|
|
.balances
|
|
.get(&(trader.clone(), "USDC".to_string()))
|
|
.unwrap_or(&0);
|
|
let required = price * amount / 1_000_000; // Assuming 6 decimals
|
|
if *usdc_balance < required {
|
|
return Err("Insufficient USDC balance".to_string());
|
|
}
|
|
}
|
|
}
|
|
|
|
let order_id = format!("order_{}_{}", trader.0, Self::timestamp());
|
|
let order = Order {
|
|
id: order_id.clone(),
|
|
trader: trader.clone(),
|
|
side,
|
|
price,
|
|
amount,
|
|
remaining: amount,
|
|
timestamp: Self::timestamp(),
|
|
};
|
|
|
|
channel.orders.orders.insert(order_id.clone(), order);
|
|
channel.last_update = Self::timestamp();
|
|
channel.nonce += 1;
|
|
|
|
// Try to match orders
|
|
self.match_orders(channel_id)?;
|
|
|
|
Ok(order_id)
|
|
}
|
|
|
|
fn match_orders(&mut self, channel_id: &ChannelId) -> Result<(), String> {
|
|
let channel = self
|
|
.channels
|
|
.get_mut(channel_id)
|
|
.ok_or("Channel not found")?;
|
|
|
|
let mut executions = Vec::new();
|
|
|
|
// Simple matching logic
|
|
let mut buy_orders: Vec<_> = channel
|
|
.orders
|
|
.orders
|
|
.values()
|
|
.filter(|o| matches!(o.side, OrderSide::Buy) && o.remaining > 0)
|
|
.collect();
|
|
buy_orders.sort_by_key(|o| std::cmp::Reverse(o.price));
|
|
|
|
let mut sell_orders: Vec<_> = channel
|
|
.orders
|
|
.orders
|
|
.values()
|
|
.filter(|o| matches!(o.side, OrderSide::Sell) && o.remaining > 0)
|
|
.collect();
|
|
sell_orders.sort_by_key(|o| o.price);
|
|
|
|
for buy_order in buy_orders {
|
|
for sell_order in &mut sell_orders {
|
|
if buy_order.price >= sell_order.price
|
|
&& buy_order.remaining > 0
|
|
&& sell_order.remaining > 0
|
|
{
|
|
let amount = buy_order.remaining.min(sell_order.remaining);
|
|
let execution = Execution {
|
|
id: format!("exec_{}", Self::timestamp()),
|
|
buy_order: buy_order.id.clone(),
|
|
sell_order: sell_order.id.clone(),
|
|
price: sell_order.price,
|
|
amount,
|
|
timestamp: Self::timestamp(),
|
|
};
|
|
executions.push(execution);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply executions
|
|
for execution in executions {
|
|
channel
|
|
.orders
|
|
.executions
|
|
.insert(execution.id.clone(), execution.clone());
|
|
|
|
// Update order remaining amounts
|
|
if let Some(buy_order) = channel.orders.orders.get_mut(&execution.buy_order) {
|
|
buy_order.remaining -= execution.amount;
|
|
}
|
|
if let Some(sell_order) = channel.orders.orders.get_mut(&execution.sell_order) {
|
|
sell_order.remaining -= execution.amount;
|
|
}
|
|
|
|
// Update balances
|
|
// This is simplified - real implementation would handle decimals properly
|
|
let buyer = channel
|
|
.orders
|
|
.orders
|
|
.get(&execution.buy_order)
|
|
.unwrap()
|
|
.trader
|
|
.clone();
|
|
let seller = channel
|
|
.orders
|
|
.orders
|
|
.get(&execution.sell_order)
|
|
.unwrap()
|
|
.trader
|
|
.clone();
|
|
|
|
*channel
|
|
.balances
|
|
.entry((buyer.clone(), "ETH".to_string()))
|
|
.or_insert(0) += execution.amount;
|
|
*channel
|
|
.balances
|
|
.entry((seller.clone(), "ETH".to_string()))
|
|
.or_insert(0) -= execution.amount;
|
|
|
|
let usdc_amount = execution.price * execution.amount / 1_000_000;
|
|
*channel
|
|
.balances
|
|
.entry((buyer, "USDC".to_string()))
|
|
.or_insert(0) -= usdc_amount;
|
|
*channel
|
|
.balances
|
|
.entry((seller, "USDC".to_string()))
|
|
.or_insert(0) += usdc_amount;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// ==== Oracle Functions ====
|
|
|
|
pub fn submit_price(
|
|
&mut self,
|
|
oracle: IdentityId,
|
|
asset: String,
|
|
price: u128,
|
|
confidence: u8,
|
|
) -> Result<(), String> {
|
|
// Verify oracle has sufficient reputation
|
|
let oracle_identity = self.identities.get(&oracle).ok_or("Oracle not found")?;
|
|
|
|
if oracle_identity.reputation_score < self.min_reputation_score * 2.0 {
|
|
return Err("Insufficient reputation to submit prices".to_string());
|
|
}
|
|
|
|
let submission = PriceSubmission {
|
|
oracle,
|
|
asset: asset.clone(),
|
|
price,
|
|
confidence,
|
|
timestamp: Self::timestamp(),
|
|
sources: vec!["binance".to_string(), "coinbase".to_string()],
|
|
signature: vec![1, 2, 3],
|
|
};
|
|
|
|
let key = (asset, submission.timestamp);
|
|
self.price_submissions
|
|
.entry(key)
|
|
.or_insert_with(Vec::new)
|
|
.push(submission);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn get_aggregate_price(&self, asset: &str, time_window: Duration) -> Option<u128> {
|
|
let now = Self::timestamp();
|
|
let start_time = now - time_window.as_secs();
|
|
|
|
let mut prices = Vec::new();
|
|
|
|
for ((price_asset, timestamp), submissions) in &self.price_submissions {
|
|
if price_asset == asset && *timestamp >= start_time && *timestamp <= now {
|
|
for submission in submissions {
|
|
// Weight by confidence
|
|
for _ in 0..submission.confidence {
|
|
prices.push(submission.price);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if prices.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
// Calculate weighted median
|
|
prices.sort();
|
|
Some(prices[prices.len() / 2])
|
|
}
|
|
|
|
// ==== Cross-Chain Functions ====
|
|
|
|
pub fn send_cross_chain_message(
|
|
&mut self,
|
|
source_chain: ChainId,
|
|
dest_chain: ChainId,
|
|
sender: IdentityId,
|
|
action: CrossChainAction,
|
|
) -> Result<String, String> {
|
|
let message_id = format!("msg_{}", Self::timestamp());
|
|
let message = CrossChainMessage {
|
|
id: message_id.clone(),
|
|
source_chain,
|
|
dest_chain,
|
|
sender,
|
|
action,
|
|
timestamp: Self::timestamp(),
|
|
signatures: vec![vec![1, 2, 3]], // Placeholder
|
|
};
|
|
|
|
self.cross_chain_messages
|
|
.insert(message_id.clone(), message);
|
|
Ok(message_id)
|
|
}
|
|
|
|
// ==== Liquidation Monitor ====
|
|
|
|
pub fn check_liquidations(&mut self, channel_id: &ChannelId) -> Result<Vec<String>, String> {
|
|
let channel = self.channels.get(channel_id).ok_or("Channel not found")?;
|
|
|
|
let mut liquidations = Vec::new();
|
|
|
|
for (identity, positions) in &channel.positions {
|
|
for position in positions {
|
|
// Get current price
|
|
let price = self
|
|
.get_aggregate_price(&position.market, Duration::from_secs(300))
|
|
.unwrap_or(position.entry_price);
|
|
|
|
// Calculate health
|
|
let value = (position.size.abs() as u128) * price / 1_000_000;
|
|
let health = position.margin as f64 / value as f64;
|
|
|
|
if health < self.liquidation_threshold {
|
|
liquidations.push(position.id.clone());
|
|
|
|
// Send cross-chain alert
|
|
self.send_cross_chain_message(
|
|
ChainId("ethereum".to_string()),
|
|
ChainId("arbitrum".to_string()),
|
|
identity.clone(),
|
|
CrossChainAction::LiquidationAlert {
|
|
position_id: position.id.clone(),
|
|
},
|
|
)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(liquidations)
|
|
}
|
|
|
|
// ==== CRDT Merge Function ====
|
|
|
|
pub fn merge(&mut self, other: &Self) {
|
|
// Merge identities
|
|
for (id, identity) in &other.identities {
|
|
self.identities
|
|
.entry(id.clone())
|
|
.or_insert_with(|| identity.clone());
|
|
}
|
|
|
|
// Merge attestations
|
|
for (id, attestation) in &other.attestations {
|
|
self.attestations
|
|
.entry(id.clone())
|
|
.or_insert_with(|| attestation.clone());
|
|
}
|
|
|
|
// Merge channels (simplified - real implementation would merge internal state)
|
|
for (id, channel) in &other.channels {
|
|
if let Some(our_channel) = self.channels.get_mut(id) {
|
|
// Merge orders
|
|
for (order_id, order) in &channel.orders.orders {
|
|
our_channel
|
|
.orders
|
|
.orders
|
|
.entry(order_id.clone())
|
|
.or_insert_with(|| order.clone());
|
|
}
|
|
// Merge executions
|
|
for (exec_id, execution) in &channel.orders.executions {
|
|
our_channel
|
|
.orders
|
|
.executions
|
|
.entry(exec_id.clone())
|
|
.or_insert_with(|| execution.clone());
|
|
}
|
|
// Update nonce to max
|
|
our_channel.nonce = our_channel.nonce.max(channel.nonce);
|
|
} else {
|
|
self.channels.insert(id.clone(), channel.clone());
|
|
}
|
|
}
|
|
|
|
// Merge price submissions
|
|
for (key, submissions) in &other.price_submissions {
|
|
self.price_submissions
|
|
.entry(key.clone())
|
|
.or_insert_with(Vec::new)
|
|
.extend(submissions.clone());
|
|
}
|
|
|
|
// Merge cross-chain messages
|
|
for (id, message) in &other.cross_chain_messages {
|
|
self.cross_chain_messages
|
|
.entry(id.clone())
|
|
.or_insert_with(|| message.clone());
|
|
}
|
|
}
|
|
|
|
fn timestamp() -> u64 {
|
|
SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_secs()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_integrated_platform() {
|
|
let mut platform = IntegratedDeFiPlatform::new();
|
|
|
|
// Create identities
|
|
let alice = IdentityId("alice".to_string());
|
|
let bob = IdentityId("bob".to_string());
|
|
let oracle = IdentityId("oracle1".to_string());
|
|
|
|
platform
|
|
.create_identity(alice.clone(), vec![1, 2, 3])
|
|
.unwrap();
|
|
platform
|
|
.create_identity(bob.clone(), vec![4, 5, 6])
|
|
.unwrap();
|
|
platform
|
|
.create_identity(oracle.clone(), vec![7, 8, 9])
|
|
.unwrap();
|
|
|
|
// Build reputation through attestations
|
|
platform
|
|
.identities
|
|
.get_mut(&alice)
|
|
.unwrap()
|
|
.reputation_score = 1.0;
|
|
platform
|
|
.create_attestation(alice.clone(), bob.clone(), "TrustedTrader".to_string(), 90)
|
|
.unwrap();
|
|
|
|
platform
|
|
.identities
|
|
.get_mut(&oracle)
|
|
.unwrap()
|
|
.reputation_score = 2.0;
|
|
|
|
// Create trading channel
|
|
let channel_id = platform
|
|
.create_channel(vec![alice.clone(), bob.clone()])
|
|
.unwrap();
|
|
|
|
// Add some balances
|
|
let channel = platform.channels.get_mut(&channel_id).unwrap();
|
|
channel
|
|
.balances
|
|
.insert((alice.clone(), "ETH".to_string()), 10_000_000);
|
|
channel
|
|
.balances
|
|
.insert((bob.clone(), "USDC".to_string()), 25_000_000_000);
|
|
|
|
// Submit oracle prices
|
|
platform
|
|
.submit_price(oracle.clone(), "ETH".to_string(), 2500_000_000, 95)
|
|
.unwrap();
|
|
|
|
// Place orders
|
|
platform
|
|
.place_order(
|
|
&channel_id,
|
|
&alice,
|
|
OrderSide::Sell,
|
|
2505_000_000,
|
|
5_000_000,
|
|
)
|
|
.unwrap();
|
|
platform
|
|
.place_order(&channel_id, &bob, OrderSide::Buy, 2510_000_000, 3_000_000)
|
|
.unwrap();
|
|
|
|
// Check that orders matched
|
|
let channel = platform.channels.get(&channel_id).unwrap();
|
|
assert!(!channel.orders.executions.is_empty());
|
|
|
|
// Check cross-chain functionality
|
|
platform
|
|
.send_cross_chain_message(
|
|
ChainId("ethereum".to_string()),
|
|
ChainId("polygon".to_string()),
|
|
alice.clone(),
|
|
CrossChainAction::Deposit {
|
|
token: "USDC".to_string(),
|
|
amount: 1000_000_000,
|
|
},
|
|
)
|
|
.unwrap();
|
|
|
|
assert!(!platform.cross_chain_messages.is_empty());
|
|
}
|
|
}
|