feat: implement batch ticket minting and purchasing optimizations - #163
Conversation
|
@Myart352 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
|
Warning Review limit reached
Next review available in: 53 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughChangesBatch registration and batch ticket minting now support atomic operations for 1–100 tickets. The flows validate authorization, capacity, privacy, per-user limits, payment rules, and cooldowns before updating ticket and event state. Batch Ticket Purchase
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Attendee
participant EventContract
participant PaymentsContract
participant TicketContract
Attendee->>EventContract: Request batch registration
EventContract->>PaymentsContract: Charge combined price
EventContract->>TicketContract: Mint batch tickets
TicketContract-->>EventContract: Return ticket IDs
EventContract-->>Attendee: Update registration and emit event
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
7105769 to
39409df
Compare
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
@Myart352 pls fix clippy & format |
- Allow clippy::too_many_arguments on batch_register_for_event, whose flat argument list intentionally mirrors register_for_event. - Apply cargo fmt to the batch_mint_ticket call site.
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (4)
contracts/ticket/src/lib.rs (2)
78-80: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winThe batch size limit
100is hard-coded in two contracts. Both files validate the same bound with a separate literal, so the two limits can drift apart. If the event contract allows a larger count than the ticket contract accepts,batch_mint_ticketrejects the call after the event contract has already validated capacity and computed the payment amount. Define the limit once in a shared location and reference it from both call sites.
contracts/ticket/src/lib.rs#L78-L80: replace the literal100inbatch_mint_ticketwith the shared constant.contracts/event/src/lib.rs#L957-L959: replace the literal100inbatch_register_for_eventwith the same shared constant.Also align the documentation. The PR description states a maximum of 50 tickets per call, while both contracts enforce 100.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@contracts/ticket/src/lib.rs` around lines 78 - 80, Define one shared batch-size limit and use it in batch_mint_ticket at contracts/ticket/src/lib.rs:78-80 and batch_register_for_event at contracts/event/src/lib.rs:957-959, replacing both hard-coded 100 literals. Update the PR documentation to state the enforced maximum of 100 tickets per call.
93-123: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the shared single-ticket mint body.
Lines 94-119 duplicate
mint_ticketlines 39-66 exactly, apart from the ID variable. Two copies of the ticket construction, persistence, indexing, and event emission will drift. A future change toTicketfields or to the index helpers must be applied twice.Extract a private helper and call it from both entry points.
♻️ Proposed refactor
fn create_ticket( env: &Env, ticket_id: u64, event_id: &Symbol, organizer: &Address, owner: &Address, ) { let ticket = Ticket { ticket_id, event_id: event_id.clone(), organizer: organizer.clone(), owner: owner.clone(), issued_at: env.ledger().timestamp(), status: TicketStatus::Valid, is_transferable: true, is_used: false, }; env.storage() .persistent() .set(&DataKey::Ticket(ticket_id), &ticket); storage::add_owner_ticket(env, owner, ticket_id); storage::add_event_ticket(env, event_id, ticket_id); events::emit_ticket_minted( env, ticket_id, ticket.event_id, ticket.owner, ticket.organizer, ticket.issued_at, ); }Then the loop becomes:
for _ in 0..count { - let ticket = Ticket { - ticket_id: next_id, - event_id: event_id.clone(), - organizer: organizer.clone(), - owner: owner.clone(), - issued_at: env.ledger().timestamp(), - status: TicketStatus::Valid, - is_transferable: true, - is_used: false, - }; - - env.storage() - .persistent() - .set(&DataKey::Ticket(next_id), &ticket); - - storage::add_owner_ticket(&env, &owner, next_id); - storage::add_event_ticket(&env, &event_id, next_id); - - events::emit_ticket_minted( - &env, - next_id, - ticket.event_id.clone(), - ticket.owner.clone(), - ticket.organizer.clone(), - ticket.issued_at, - ); - + create_ticket(&env, next_id, &event_id, &organizer, &owner); ticket_ids.push_back(next_id); next_id += 1; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@contracts/ticket/src/lib.rs` around lines 93 - 123, Extract the duplicated single-ticket construction, persistence, indexing, and event emission into a private helper such as create_ticket, parameterized by env, ticket_id, event_id, organizer, and owner. Replace the existing body in both mint_ticket entry points, including the count loop, with calls to this helper while preserving ID sequencing and ticket_ids collection.contracts/event/src/lib.rs (2)
1031-1033: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReturn the minted ticket IDs to the caller.
batch_mint_ticketreturns the generated IDs, but this code discards them into_ticket_ids. A client that buys 30 tickets has no way to learn the IDs from the return value. The client must scanget_tickets_by_ownerand filter by event, asrequest_postponement_refunddoes at lines 681-690.Change the return type to
Result<soroban_sdk::Vec<u64>, EventError>and return the IDs.♻️ Proposed change
- ) -> Result<(), EventError> { + ) -> Result<soroban_sdk::Vec<u64>, EventError> {let ticket_client = TicketContractClient::new(&env, &ticket_contract); - let _ticket_ids = + let ticket_ids = ticket_client.batch_mint_ticket(&event.event_id, &event.organizer, &attendee, &count);- Ok(()) + Ok(ticket_ids) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@contracts/event/src/lib.rs` around lines 1031 - 1033, Update the surrounding event purchase function containing the TicketContractClient::batch_mint_ticket call to return Result<soroban_sdk::Vec<u64>, EventError>, propagate the batch_mint_ticket result instead of assigning it to _ticket_ids, and ensure the minted ticket IDs are returned to the caller while preserving the existing error handling.
1037-1042: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winReplace the free-claim increment loop with one write.
Lines 1038-1040 call
storage::increment_free_claim_countonce per ticket. Percontracts/event/src/storage.rslines 361-368, each call reads the key, writes the key, and extends the TTL. Atcount = 100that is 100 read-modify-write cycles on a single ledger entry, and it inflates the transaction footprint for no benefit.Add a helper that sets the total once. The batch already read the prior value into
existingat line 998 when a claim limit is configured.♻️ Proposed refactor
Add to
contracts/event/src/storage.rs:pub fn add_free_claim_count(env: &Env, event_id: &Symbol, attendee: &Address, delta: u32) { let key = DataKey::FreeClaimCount(event_id.clone(), attendee.clone()); let count: u32 = env.storage().persistent().get(&key).unwrap_or(0u32); env.storage().persistent().set(&key, &(count + delta)); env.storage() .persistent() .extend_ttl(&key, TTL_THRESHOLD, TTL_BUMP); }Then:
if tier.price == 0 { - for _ in 0..count { - storage::increment_free_claim_count(&env, &event_id, &attendee); - } + storage::add_free_claim_count(&env, &event_id, &attendee, count); storage::set_last_free_claim(&env, &event_id, &attendee, env.ledger().timestamp()); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@contracts/event/src/lib.rs` around lines 1037 - 1042, Replace the per-ticket increment loop in the free-claim branch with a single storage update using a new add_free_claim_count helper. Implement the helper in storage to read the current FreeClaimCount, add the supplied count, write once, and extend its TTL; pass count from the batch flow while preserving the existing last-free-claim timestamp update.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@contracts/event/src/lib.rs`:
- Around line 961-980: The batch registration flow around
batch_register_for_event must explicitly handle an attendee’s existing
reservation instead of leaving tier.reserved incremented. Choose the intended
behavior: either validate and consume the reservation by decrementing the
matched tier’s reserved count and calling storage::remove_reservation, or reject
the batch call with EventError::InvalidInput when storage::has_reservation is
true; preserve normal batch registration for callers without reservations.
- Around line 990-992: Update the capacity check in the batch registration path
to include tier.reserved alongside tier.sold and count, matching the
reservation-aware checks used by register_for_event and reserve_ticket; return
TierSoldOut whenever the combined total reaches or exceeds tier.capacity.
- Line 1035: Update batch_register_for_event around save_registration so it
checks whether each attendee is already registered before saving; call
storage::save_registration only for new attendees, while still allowing repeated
purchases to proceed without rejecting existing attendees.
- Around line 982-984: Update batch_register_for_event’s max_tickets_per_user
validation to compare the attendee’s existing owned ticket total for this event
plus the requested count against the configured cap. Preserve the unlimited
behavior when max_tickets_per_user is zero and return EventError::InvalidInput
whenever the combined total exceeds the limit.
In `@contracts/ticket/src/lib.rs`:
- Around line 90-125: Update batch_mint_ticket so its maximum accepted count
stays within Soroban’s 200-write limit, accounting for the ticket and
owner/event index writes performed per iteration. Reduce the advertised count
cap to a safe upper bound, and add simulator coverage confirming the maximum
succeeds while exceeding it is rejected.
---
Nitpick comments:
In `@contracts/event/src/lib.rs`:
- Around line 1031-1033: Update the surrounding event purchase function
containing the TicketContractClient::batch_mint_ticket call to return
Result<soroban_sdk::Vec<u64>, EventError>, propagate the batch_mint_ticket
result instead of assigning it to _ticket_ids, and ensure the minted ticket IDs
are returned to the caller while preserving the existing error handling.
- Around line 1037-1042: Replace the per-ticket increment loop in the free-claim
branch with a single storage update using a new add_free_claim_count helper.
Implement the helper in storage to read the current FreeClaimCount, add the
supplied count, write once, and extend its TTL; pass count from the batch flow
while preserving the existing last-free-claim timestamp update.
In `@contracts/ticket/src/lib.rs`:
- Around line 78-80: Define one shared batch-size limit and use it in
batch_mint_ticket at contracts/ticket/src/lib.rs:78-80 and
batch_register_for_event at contracts/event/src/lib.rs:957-959, replacing both
hard-coded 100 literals. Update the PR documentation to state the enforced
maximum of 100 tickets per call.
- Around line 93-123: Extract the duplicated single-ticket construction,
persistence, indexing, and event emission into a private helper such as
create_ticket, parameterized by env, ticket_id, event_id, organizer, and owner.
Replace the existing body in both mint_ticket entry points, including the count
loop, with calls to this helper while preserving ID sequencing and ticket_ids
collection.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 4cd82461-e854-4b95-b98c-58f1dcaf2d71
📒 Files selected for processing (2)
contracts/event/src/lib.rscontracts/ticket/src/lib.rs
| let mut event = storage::get_event(&env, &event_id)?; | ||
|
|
||
| if event.status != EventStatus::Active { | ||
| return Err(EventError::EventNotActive); | ||
| } | ||
|
|
||
| require_settleable_privacy(&env, &event_id)?; | ||
|
|
||
| let mut tier_index = None; | ||
| let mut req_price: Option<i128> = None; | ||
| for i in 0..event.tiers.len() { | ||
| let t = event.tiers.get(i).ok_or(EventError::TierNotFound)?; | ||
| if t.tier_id == tier_id { | ||
| tier_index = Some(i); | ||
| req_price = Some(t.price); | ||
| break; | ||
| } | ||
| } | ||
| let index = tier_index.ok_or(EventError::TierNotFound)?; | ||
| let tier = event.tiers.get(index).ok_or(EventError::TierNotFound)?; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
The batch path ignores existing reservations, so tier.reserved leaks.
register_for_event lines 855-925 handle a caller who holds a reservation. It validates the expiry and tier, decrements tier.reserved, and calls storage::remove_reservation. batch_register_for_event performs none of these steps.
If an attendee reserves a ticket and then calls batch_register_for_event, the reservation record stays in storage and tier.reserved stays incremented. That reserved slot is never released, so the tier permanently loses sellable capacity. release_expired_reservation is the only remaining path, and it requires an explicit external call.
Decide the intended behavior and implement it. Two options are consistent:
- Consume the reservation: validate it, decrement
tier.reservedby one, and callstorage::remove_reservation. - Reject the call: return
EventError::InvalidInputwhenstorage::has_reservationreturnstrue, and require the attendee to useregister_for_event.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@contracts/event/src/lib.rs` around lines 961 - 980, The batch registration
flow around batch_register_for_event must explicitly handle an attendee’s
existing reservation instead of leaving tier.reserved incremented. Choose the
intended behavior: either validate and consume the reservation by decrementing
the matched tier’s reserved count and calling storage::remove_reservation, or
reject the batch call with EventError::InvalidInput when
storage::has_reservation is true; preserve normal batch registration for callers
without reservations.
| if event.max_tickets_per_user > 0 && count > event.max_tickets_per_user { | ||
| return Err(EventError::InvalidInput); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Locate per-user ticket accounting helpers and existing max_tickets_per_user enforcement.
set -euo pipefail
# Find any storage helper that tracks a per-user ticket count for an event.
rg -nP -C3 'user_ticket|tickets_per_user|UserTicketCount|get_user_ticket' contracts/
# Find every place max_tickets_per_user is read, to see where the limit is enforced today.
rg -nP -C5 '\bmax_tickets_per_user\b' contracts/
# Inspect the existing per-attendee ticket lookup helper used elsewhere in the event contract.
ast-grep run --pattern 'fn has_valid_ticket_for_event($$$) { $$$ }' --lang rust contracts/event/src/Repository: BuidlZone-Labs/zicket-contract
Length of output: 50386
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the batch registration path and relevant helpers without running repo code.
sed -n '930,1025p' contracts/event/src/lib.rs
printf '\n--- register_for_event nearby ---\n'
sed -n '830,920p' contracts/event/src/lib.rs
printf '\n--- helper definitions involving has_valid_ticket_for_event ---\n'
rg -n -C4 'pub.*fn has_valid_ticket_for_event|fn has_valid_ticket_for_event' contracts/event/src/lib.rs
printf '\n--- batch/register function signatures ---\n'
rg -n -C3 'pub fn (batch_register_for_event|register_for_event)|fn (batch_register_for_event|register_for_event)' contracts/event/src/lib.rsRepository: BuidlZone-Labs/zicket-contract
Length of output: 7858
Enforce max_tickets_per_user against the attendee’s owned tickets.
batch_register_for_event rejects count > event.max_tickets_per_user, but repeated calls with count == event.max_tickets_per_user bypass the limit. Use a per-event owner total, including existing tickets, so one attendee cannot exceed the configured per-user cap.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@contracts/event/src/lib.rs` around lines 982 - 984, Update
batch_register_for_event’s max_tickets_per_user validation to compare the
attendee’s existing owned ticket total for this event plus the requested count
against the configured cap. Preserve the unlimited behavior when
max_tickets_per_user is zero and return EventError::InvalidInput whenever the
combined total exceeds the limit.
| let mut ticket_ids = soroban_sdk::Vec::new(&env); | ||
| let mut next_id = read_next_ticket_id(&env); | ||
|
|
||
| for _ in 0..count { | ||
| let ticket = Ticket { | ||
| ticket_id: next_id, | ||
| event_id: event_id.clone(), | ||
| organizer: organizer.clone(), | ||
| owner: owner.clone(), | ||
| issued_at: env.ledger().timestamp(), | ||
| status: TicketStatus::Valid, | ||
| is_transferable: true, | ||
| is_used: false, | ||
| }; | ||
|
|
||
| env.storage() | ||
| .persistent() | ||
| .set(&DataKey::Ticket(next_id), &ticket); | ||
|
|
||
| storage::add_owner_ticket(&env, &owner, next_id); | ||
| storage::add_event_ticket(&env, &event_id, next_id); | ||
|
|
||
| events::emit_ticket_minted( | ||
| &env, | ||
| next_id, | ||
| ticket.event_id.clone(), | ||
| ticket.owner.clone(), | ||
| ticket.organizer.clone(), | ||
| ticket.issued_at, | ||
| ); | ||
|
|
||
| ticket_ids.push_back(next_id); | ||
| next_id += 1; | ||
| } | ||
|
|
||
| write_next_ticket_id(&env, next_id); |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🌐 Web query:
Soroban transaction resource limits max ledger entry writes per transaction
💡 Result:
On the Stellar network, Soroban transactions are subject to specific resource limits defined by network settings [1][2]. As of August 2026, the maximum number of ledger entry writes allowed per transaction is 200 [3]. These limits are part of a multidimensional resource model where transactions declare their required resources—including ledger entry accesses (reads and writes), ledger I/O (bytes read/written), and CPU instructions—within their footprint [1][4]. Key points regarding these limits: 1. Per-Transaction Limit: Current mainnet settings cap the number of ledger entries that can be written in a single transaction at 200 [3]. 2. Dynamic Configuration: These limits are not static hard-coded constants in the protocol itself but are instead network-defined settings that can be adjusted by validator consensus to reflect network capacity and performance needs [1][2][3]. 3. Verification: Because these values can change via validator vote, developers are encouraged to verify the current limits for a specific network (e.g., mainnet) by checking the active network settings using tools such as the Stellar Laboratory or by querying the network configuration [3]. 4. Context: The 200-entry write limit is designed to maintain ledger close times and prevent the execution of arbitrarily large transactions [1][2]. The limits are also balanced against ledger-wide capacity, with established ratios (often 5:1 for ledger-to-transaction limits) ensuring fair resource distribution [5]. For the most accurate and up-to-date values, you can inspect the active ConfigSettingContractLedgerCostV0 values on the network [6].
Citations:
- 1: https://github.com/stellar/stellar-docs/blob/e5c5495d/docs/learn/fundamentals/fees-resource-limits-metering.mdx
- 2: https://developers.stellar.org/docs/learn/fundamentals/fees-resource-limits-metering
- 3: https://developers.stellar.org/docs/build/guides/storage/storage-strategies
- 4: https://developers.stellar.org/docs/build/guides/transactions/simulateTransaction-Deep-Dive
- 5: https://github.com/stellar/stellar-protocol/blob/master/limits/slp-0001.md
- 6: https://docs.rs/soroban-sdk/latest/soroban%5Fsdk/xdr/struct.ConfigSettingContractLedgerCostV0.html
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate lib.rs and storage.rs =="
fd -a 'lib\.rs|storage\.rs|events\.rs' contracts/ticket/src || true
echo "== outline ticket lib =="
ast-grep outline contracts/ticket/src/lib.rs || true
echo "== relevant ticket lib lines =="
nl -ba contracts/ticket/src/lib.rs | sed -n '1,180p'
echo "== relevant storage definitions =="
for f in $(fd 'storage\.rs|events\.rs' contracts/ticket/src); do
echo "--- $f ---
"
nl -ba "$f" | sed -n '1,240p'
done
echo "== search claim/count/mint functions =="
rg -n "pub fn|count|MAX|100|mint|add_owner_ticket|add_event_ticket|read_next_ticket_id|write_next_ticket_id|emit_ticket_minted" contracts/ticket/srcRepository: BuidlZone-Labs/zicket-contract
Length of output: 1036
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== lib imports and constants =="
awk '{printf "%6d %s\n", NR, $0}' contracts/ticket/src/lib.rs | sed -n '1,180p'
echo "== storage outlines/contents =="
awk '{printf "%6d %s\n", NR, $0}' contracts/ticket/src/storage.rs | sed -n '1,260p'
echo "== lib minting range =="
awk '{printf "%6d %s\n", NR, $0}' contracts/ticket/src/lib.rs | sed -n '180,340p'
echo "== storage helper calls in lib =="
awk '{printf "%6d %s\n", NR, $0}' contracts/ticket/src/lib.rs | sed -n '340,470p'
echo "== search relevant docs/constants in repo =="
rg -n "MAX|100|batch_mint|mint_ticket|ledger entry|ResourceLimits|max|ContractLedgerCost|MAX_LEDER|LEDER|LedgerEntry|Write|LedgerKey" . || trueRepository: BuidlZone-Labs/zicket-contract
Length of output: 50386
🌐 Web query:
Soroban extend_ttl counts as write ledger entry updates storage extension TTL
💡 Result:
In Soroban, calling extend_ttl is a specific operation used to manage the Time-To-Live (TTL) of ledger entries, and while it effectively updates the entry's lifespan, it is distinct from general data mutation operations like set or update [1][2][3]. When you invoke extend_ttl (or the ExtendFootprintTTLOp operation), the network performs a storage extension that incurs "ledger rent" fees [2][3][4]. These rent fees are calculated based on the duration of the extension, the size of the ledger entry, and current storage pricing parameters [3]. Key points regarding how extend_ttl functions: 1. Nature of the Operation: extend_ttl is a dedicated host function available in the Soroban SDK (for Persistent, Temporary, and Instance storage) used to ensure an entry remains on-chain [1][5][3][6]. It is not a standard data update; rather, it is a management operation specifically for state archival [2]. 2. Fee Structure: Because extend_ttl extends the lifecycle of data, it is subject to rent fees which are categorized as a "refundable" fee component in Soroban's resource management model [3]. 3. Execution: If the current TTL of an entry already exceeds the specified threshold, the extend_ttl call is a no-op and does not trigger a fee [2]. If the TTL is below the threshold, the operation extends the lifespan to the requested extend_to value, which constitutes a ledger update for the purpose of state management [1][2]. 4. Distinct from Data Mutation: While set or update operations modify the value associated with a key, extend_ttl modifies the metadata (the expiration ledger sequence) of the ledger entry [1][5][6][4]. Both types of operations require sufficient footprint resources in the transaction [2].
Citations:
- 1: https://docs.rs/soroban-sdk/latest/soroban_sdk/storage/struct.Persistent.html
- 2: https://developers.stellar.org/docs/learn/fundamentals/contract-development/storage/state-archival
- 3: https://deepwiki.com/stellar/stellar-docs/5.4-resource-limits-and-fees
- 4: https://jamesbachini.com/soroban-data-state-management/
- 5: https://docs.rs/soroban-sdk/latest/soroban_sdk/storage/struct.Temporary.html
- 6: https://docs.rs/soroban-sdk/latest/soroban_sdk/storage/struct.Instance.html
🌐 Web query:
Soroban max ledger entry writes per transaction 2026 extend_ttl ledger entry writes
💡 Result:
In the Stellar Soroban network as of August 2026, the maximum number of ledger entry write operations allowed per transaction is governed by network configuration parameters, which can be adjusted via protocol upgrades [1][2][3]. According to recent protocol updates, such as those documented in SLP-0004, this limit has been set to 200 write entries per transaction [4]. Regarding the extend_ttl functionality: 1. Operations used to extend TTL, such as the host functions provided by env.storage().persistent().extend_ttl(...) or extend_contract_data host functions, are designed to update the expiration ledger sequence of existing ledger entries [5][6][7]. 2. These extension operations typically do not count against the standard "write ledger entries" transaction limit in the same way that creating new entries does [7]. Specifically, host functions for extending TTL explicitly state that they do not charge standard write fees (though they do charge rent fees if an extension occurs) [7]. 3. Because these operations do not modify the data content or create new entries but rather update the TTL metadata (live_until_ledger_seq), they are treated differently by the network's resource metering system [7]. Users should consult the latest ConfigSettingContractLedgerCostV0 structure for the definitive current tx_max_write_ledger_entries value on the specific network they are accessing, as these values are subject to change by validator consensus [1][3].
Citations:
- 1: https://docs.rs/soroban-sdk/latest/soroban%5Fsdk/xdr/struct.ConfigSettingContractLedgerCostV0.html
- 2: https://github.com/stellar/stellar-core/blob/master/src/ledger/NetworkConfig.h
- 3: https://developers.stellar.org/docs/learn/fundamentals/fees-resource-limits-metering
- 4: https://github.com/stellar/stellar-protocol/blob/master/limits/slp-0004.md
- 5: https://docs.rs/soroban-sdk/latest/soroban_sdk/storage/struct.Persistent.html
- 6: https://developers.stellar.org/docs/build/guides/dapps/state-archival
- 7: https://github.com/stellar/stellar-protocol/blob/master/core/cap-0046-12.md
Keep batch_mint_ticket under Soroban write limits.
Each loop iteration writes a ticket and two index/count entries. A count of 100 therefore touches ~300 ledger entries in one transaction, while Soroban caps write operations at 200. This can make the advertised maximum unusable because the batch reverts. Reduce the count cap or split batching across transactions, and test the upper bound with the contract simulator before merge.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@contracts/ticket/src/lib.rs` around lines 90 - 125, Update batch_mint_ticket
so its maximum accepted count stays within Soroban’s 200-write limit, accounting
for the ticket and owner/event index writes performed per iteration. Reduce the
advertised count cap to a safe upper bound, and add simulator coverage
confirming the maximum succeeds while exceeding it is rejected.
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Closes #147
Summary
Adds
batch_register_for_eventandbatch_mint_ticketfunctions to allow users to purchase multiple tickets in a single atomic transaction, reducing network fees and improving UX for group bookings.Changes
Event Contract (
contracts/event/src/lib.rs)batch_register_for_event(env, nonce, attendee, event_id, tier_id, count, is_verified, email_hash)that:max_tickets_per_userlimitbatch_mint_ticketon the ticket contractTicket Contract (
contracts/ticket/src/lib.rs)batch_mint_ticket(env, event_id, organizer, owner, count)that:Key Design Decisions
Summary by CodeRabbit