diff --git a/contracts/shade/src/components/cross_chain_pledge.rs b/contracts/shade/src/components/cross_chain_pledge.rs new file mode 100644 index 0000000..1fa3ad8 --- /dev/null +++ b/contracts/shade/src/components/cross_chain_pledge.rs @@ -0,0 +1,114 @@ +use soroban_sdk::{panic_with_error, Address, Env, String, Vec}; + +use crate::{ + errors::ContractError, + events, + types::{CrossChainPledge, CrossChainPledgeStatus, DataKey}, +}; + +pub fn create_pledge( + env: &Env, + source_chain: String, + source_pledge_id: u64, + destination_chain: String, + merchant: Address, + payer: Address, + token: Address, + amount: i128, + memo: Option, +) -> u64 { + merchant.require_auth(); + + let pledge_id = get_next_pledge_id(env); + let now = env.ledger().timestamp(); + + let pledge = CrossChainPledge { + id: pledge_id, + source_chain: source_chain.clone(), + source_pledge_id, + destination_chain, + merchant: merchant.clone(), + payer, + token, + amount, + status: CrossChainPledgeStatus::Pending, + created_at: now, + updated_at: now, + memo, + }; + + env.storage() + .persistent() + .set(&DataKey::CrossChainPledge(pledge_id), &pledge); + env.storage() + .persistent() + .set(&DataKey::PledgeIdBySourceChain(source_chain, source_pledge_id), &pledge_id); + + events::publish_cross_chain_pledge_created(env, &pledge); + + pledge_id +} + +pub fn update_pledge_status( + env: &Env, + pledge_id: u64, + new_status: CrossChainPledgeStatus, +) { + let admin = crate::components::core::get_admin(env); + admin.require_auth(); + + let mut pledge = get_pledge(env, pledge_id); + pledge.status = new_status; + pledge.updated_at = env.ledger().timestamp(); + + env.storage() + .persistent() + .set(&DataKey::CrossChainPledge(pledge_id), &pledge); + + events::publish_cross_chain_pledge_updated(env, &pledge); +} + +pub fn get_pledge(env: &Env, pledge_id: u64) -> CrossChainPledge { + env.storage() + .persistent() + .get(&DataKey::CrossChainPledge(pledge_id)) + .unwrap_or_else(|| panic_with_error!(env, ContractError::NotFound)) +} + +pub fn get_pledge_by_source( + env: &Env, + source_chain: String, + source_pledge_id: u64, +) -> CrossChainPledge { + let pledge_id: u64 = env + .storage() + .persistent() + .get(&DataKey::PledgeIdBySourceChain(source_chain, source_pledge_id)) + .unwrap_or_else(|| panic_with_error!(env, ContractError::NotFound)); + get_pledge(env, pledge_id) +} + +fn get_next_pledge_id(env: &Env) -> u64 { + let count: u64 = env + .storage() + .persistent() + .get(&DataKey::CrossChainPledgeCount) + .unwrap_or(0); + env.storage() + .persistent() + .set(&DataKey::CrossChainPledgeCount, &(count + 1)); + count +} + +pub fn get_all_pledges(env: &Env) -> Vec { + let count: u64 = env + .storage() + .persistent() + .get(&DataKey::CrossChainPledgeCount) + .unwrap_or(0); + let mut pledges = Vec::new(env); + for i in 0..count { + pledges.push_back(get_pledge(env, i)); + } + pledges +} diff --git a/contracts/shade/src/components/mod.rs b/contracts/shade/src/components/mod.rs index 33be813..d377946 100644 --- a/contracts/shade/src/components/mod.rs +++ b/contracts/shade/src/components/mod.rs @@ -22,5 +22,4 @@ pub mod upgrade; pub mod escrow; pub mod backer_rewards; pub mod event; -pub mod campaign; -pub mod nft; +pub mod cross_chain_pledge; diff --git a/contracts/shade/src/errors.rs b/contracts/shade/src/errors.rs index bf33429..ff52592 100644 --- a/contracts/shade/src/errors.rs +++ b/contracts/shade/src/errors.rs @@ -48,6 +48,7 @@ pub enum ContractError { TicketNotFound = 51, NotTicketOwner = 52, InvalidResalePrice = 54, + NotFound = 55, // ── Multi-sig massive withdrawal ───────────────────────────────────────── /// The withdrawal amount is below the configured threshold; no multi-sig needed. BelowMultiSigThreshold = 55, diff --git a/contracts/shade/src/events.rs b/contracts/shade/src/events.rs index 24968f5..ef8b52e 100644 --- a/contracts/shade/src/events.rs +++ b/contracts/shade/src/events.rs @@ -739,6 +739,33 @@ pub fn publish_bridge_placeholder_event( .publish(env); } +#[contractevent] +pub struct CrossChainPledgeCreatedEvent { + pub pledge_id: u64, + pub source_chain: String, + pub source_pledge_id: u64, + pub merchant: Address, + pub payer: Address, + pub token: Address, + pub amount: i128, + pub timestamp: u64, +} + +pub fn publish_cross_chain_pledge_created( + env: &Env, + pledge: &crate::types::CrossChainPledge, +) { + CrossChainPledgeCreatedEvent { + pledge_id: pledge.id, + source_chain: pledge.source_chain.clone(), + source_pledge_id: pledge.source_pledge_id, + merchant: pledge.merchant.clone(), + payer: pledge.payer.clone(), + token: pledge.token.clone(), + amount: pledge.amount, + timestamp: pledge.created_at, + } +} // ── Bridge listener / external deposit events ───────────────────────────────── #[contractevent] @@ -952,6 +979,22 @@ pub fn publish_upgrade_vote_cast_event( } #[contractevent] +pub struct CrossChainPledgeUpdatedEvent { + pub pledge_id: u64, + pub status: crate::types::CrossChainPledgeStatus, + pub timestamp: u64, +} + +pub fn publish_cross_chain_pledge_updated( + env: &Env, + pledge: &crate::types::CrossChainPledge, +) { + CrossChainPledgeUpdatedEvent { + pledge_id: pledge.id, + status: pledge.status.clone(), + timestamp: pledge.updated_at, + } +} pub struct UpgradeProposalFinalizedEvent { pub proposal_id: u64, pub executor: Address, diff --git a/contracts/shade/src/interface.rs b/contracts/shade/src/interface.rs index 5933eee..240e967 100644 --- a/contracts/shade/src/interface.rs +++ b/contracts/shade/src/interface.rs @@ -1,4 +1,7 @@ use crate::types::{ + CrossChainBridgePayload, CrossChainPledge, CrossChainPledgeStatus, Event, Invoice, InvoiceFilter, Merchant, MerchantAnalytics, + MerchantAnalyticsSummary, MerchantFilter, OracleConfig, PaymentPayload, PendingFee, Role, + Subscription, SubscriptionPlan, Ticket, TokenAnalytics, Transaction CrossChainBridgePayload, Event, EventFilter, Invoice, InvoiceFilter, InvoicePage, Merchant, MerchantFilter, MerchantPage, MerchantAnalytics, MerchantAnalyticsSummary, OracleConfig, PaymentPayload, PendingFee, Role, Subscription, SubscriptionFilter, SubscriptionPlan, @@ -493,56 +496,33 @@ pub trait ShadeTrait { /// Create a crowdfunding campaign for tiered backer rewards. fn create_backer_campaign( env: Env, + source_chain: String, + source_pledge_id: u64, + destination_chain: String, merchant: Address, - name: String, + payer: Address, token: Address, - deadline: u64, + amount: i128, + memo: Option, ) -> u64; - fn get_backer_campaign(env: Env, campaign_id: u64) -> BackerCampaign; - - /// Define reward tiers with perks. Tiers must have ascending `min_pledge`. - fn set_backer_reward_tiers( - env: Env, - merchant: Address, - campaign_id: u64, - tiers: Vec, - ); - - fn get_backer_reward_tiers(env: Env, campaign_id: u64) -> Vec; - - /// Record a backer pledge to a campaign. - fn pledge_to_campaign(env: Env, backer: Address, campaign_id: u64, amount: i128); - - fn get_backer_pledge(env: Env, campaign_id: u64, backer: Address) -> i128; - - /// Select a reward tier. Backer's total pledge must meet the tier minimum. - fn select_backer_reward_tier( + /// Update the status of a cross-chain pledge + fn update_cross_chain_pledge_status( env: Env, - backer: Address, - campaign_id: u64, - tier_index: u32, + pledge_id: u64, + new_status: CrossChainPledgeStatus, ); - fn get_backer_selected_tier(env: Env, campaign_id: u64, backer: Address) -> Option; + /// Get a cross-chain pledge by ID + fn get_cross_chain_pledge(env: Env, pledge_id: u64) -> CrossChainPledge; - /// Mark a backer's reward as fulfilled. Merchant-only. - fn fulfill_backer_reward( + /// Get a cross-chain pledge by source chain and source pledge ID + fn get_cross_chain_pledge_by_source( env: Env, - merchant: Address, - campaign_id: u64, - backer: Address, - ); - - fn is_backer_reward_fulfilled(env: Env, campaign_id: u64, backer: Address) -> bool; - - /// Claim a perk from the backer's selected tier after fulfillment. - fn claim_backer_perk(env: Env, backer: Address, campaign_id: u64, perk_index: u32); + source_chain: String, + source_pledge_id: u64, + ) -> CrossChainPledge; - fn is_backer_perk_claimed( - env: Env, - campaign_id: u64, - backer: Address, - perk_index: u32, - ) -> bool; + /// Get all cross-chain pledges + fn get_all_cross_chain_pledges(env: Env) -> Vec; } diff --git a/contracts/shade/src/shade.rs b/contracts/shade/src/shade.rs index 7ddb7fe..160f667 100644 --- a/contracts/shade/src/shade.rs +++ b/contracts/shade/src/shade.rs @@ -7,7 +7,8 @@ upgrade as upgrade_component, history as history_component, invoice as invoice_component, merchant as merchant_component, pausable as pausable_component, subscription as subscription_component, upgrade as upgrade_component, - history as history_component, escrow as escrow_component, + history as history_component, cross_chain_pledge as cross_chain_pledge_component, + escrow as escrow_component, }; use crate::errors::ContractError; use crate::events; diff --git a/contracts/shade/src/types.rs b/contracts/shade/src/types.rs index 065a1cd..5b8efa4 100644 --- a/contracts/shade/src/types.rs +++ b/contracts/shade/src/types.rs @@ -49,6 +49,10 @@ pub enum DataKey { // --- Global token analytics --- TokenAnalytics(Address), TokenVolume(Address), + // --- Cross-chain pledge system --- + CrossChainPledge(u64), + CrossChainPledgeCount, + PledgeIdBySourceChain(String, u64), // source_chain, source_pledge_id // --- Multi-sig massive withdrawal --- /// Threshold (in token base units) above which a withdrawal requires multi-sig approval. MultiSigThreshold(Address), @@ -239,6 +243,32 @@ pub struct MerchantAnalyticsSummary { pub last_updated: u64, } +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum CrossChainPledgeStatus { + Pending, + Completed, + Failed, + Refunded, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CrossChainPledge { + pub id: u64, + pub source_chain: String, + pub source_pledge_id: u64, + pub destination_chain: String, + pub merchant: Address, + pub payer: Address, + pub token: Address, + pub amount: i128, + pub status: CrossChainPledgeStatus, + pub created_at: u64, + pub updated_at: u64, + pub memo: Option, +} + #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct CrossChainBridgePayload {