Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions intent_settlement/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const INTENT_EXPIRY: u64 = 1800; // 30 minutes
const FILL_WINDOW: u64 = 300; // 5 minutes to fill after intent accepted
const MIN_BOND: i128 = 50 * 10_000_000; // 50 USDC minimum solver bond
const PROTOCOL_FEE_BPS: i128 = 5; // 0.05%
const MAX_BATCH_SIZE: u32 = 100; // Maximum items per batch operation
const MAX_EXTENSION_DURATION: u64 = 300; // 5 minutes max extension time

// Soroban archives ledger entries that go too long without being touched.
// Persistent Intent/Solver records get their TTL bumped on every write so
Expand Down Expand Up @@ -50,6 +52,7 @@ pub enum DataKey {
Paused,
AllowedDstToken(Address), // dst_token -> present if allowed
DstAllowlistEnabled,
ExtensionGranted(BytesN<32>), // intent_id -> true if extension already granted
}

// ─── Data Structs ─────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -793,6 +796,112 @@ impl IntentSettlement {
.publish((Symbol::new(&env, "intent_expired"),), intent_id);
}

// ── Batch Operations ──────────────────────────────────────────────────────

/// Submit multiple intents in a single transaction.
/// Processes all intents in the batch; a failure partway through will
/// revert the entire batch (Soroban transaction atomicity).
/// Bounded by MAX_BATCH_SIZE to prevent resource exhaustion.
pub fn batch_submit_intent(
env: Env,
user: Address,
intents: soroban_sdk::Vec<(String, String, i128, Address, i128, Option<u64>)>,
) -> soroban_sdk::Vec<BytesN<32>> {
if intents.len() > MAX_BATCH_SIZE as usize {
panic_with_error!(&env, Error::ZeroAmount); // No dedicated error; reuse nearest
}

let mut result = soroban_sdk::Vec::new(&env);
for (src_chain, src_token, src_amount, dst_token, min_dst_amount, deadline) in intents {
let intent_id = Self::submit_intent(
env.clone(),
user.clone(),
src_chain,
src_token,
src_amount,
dst_token,
min_dst_amount,
deadline,
);
result.push_back(intent_id);
}
result
}

/// Accept multiple intents in a single transaction.
/// Processes all intents in the batch; a failure partway through will
/// revert the entire batch (Soroban transaction atomicity).
/// Bounded by MAX_BATCH_SIZE to prevent resource exhaustion.
pub fn batch_accept_intent(
env: Env,
solver: Address,
intent_ids: soroban_sdk::Vec<BytesN<32>>,
) {
if intent_ids.len() > MAX_BATCH_SIZE as usize {
panic_with_error!(&env, Error::ZeroAmount); // No dedicated error; reuse nearest
}

for intent_id in intent_ids {
Self::accept_intent(env.clone(), solver.clone(), intent_id);
}
}

// ── Fill Window Extension ─────────────────────────────────────────────────

/// Solver requests a grace-period extension on an Accepted intent.
/// Grants exactly one extension per intent, each extending the deadline
/// by up to MAX_EXTENSION_DURATION. Further extension requests on the
/// same intent are rejected to prevent abuse.
pub fn request_extension(env: Env, solver: Address, intent_id: BytesN<32>) {
solver.require_auth();
Self::bump_instance_ttl(&env);

let mut intent: IntentRecord = env
.storage()
.persistent()
.get(&DataKey::Intent(intent_id.clone()))
.unwrap_or_else(|| panic_with_error!(&env, Error::IntentNotFound));

// Only Accepted intents can be extended
if intent.state != IntentState::Accepted {
panic_with_error!(&env, Error::IntentNotAccepted);
}

// Only the assigned solver can request an extension
if intent.solver.as_ref() != Some(&solver) {
panic_with_error!(&env, Error::Unauthorized);
}

// Each intent gets exactly one extension
if env
.storage()
.persistent()
.has(&DataKey::ExtensionGranted(intent_id.clone()))
{
panic_with_error!(&env, Error::ZeroAmount); // No dedicated error; reuse nearest
}

let now = env.ledger().timestamp();

// Extend the deadline by the full extension duration
intent.deadline = now + MAX_EXTENSION_DURATION;

// Record that this intent has used its one extension
env.storage()
.persistent()
.set(&DataKey::ExtensionGranted(intent_id.clone()), &true);

env.storage()
.persistent()
.set(&DataKey::Intent(intent_id.clone()), &intent);
Self::bump_intent_ttl(&env, &intent_id);

env.events().publish(
(Symbol::new(&env, "extension_granted"), solver),
(intent_id, intent.deadline),
);
}

// ── Views ─────────────────────────────────────────────────────────────────

/// Fetch an intent's full record by id, or None if it was never submitted.
Expand Down Expand Up @@ -847,6 +956,14 @@ impl IntentSettlement {
(intents, volume)
}

/// Total number of solvers ever registered.
pub fn get_solver_count(env: Env) -> u32 {
env.storage()
.instance()
.get(&DataKey::TotalSolvers)
.unwrap_or(0)
}

// ── Internal ──────────────────────────────────────────────────────────────

fn require_admin(env: &Env) {
Expand Down
Loading