feat(contracts): expert cooldown, spending limits, vouchers, webhook events (#240-#243)#245
Conversation
…events (LightForgeHub#240-LightForgeHub#243) Add dispute-loss expert cooldowns, seeker spending caps, ed25519 session vouchers, and a unified webhook event envelope with relay documentation.
|
Warning Review limit reached
More reviews will be available in 49 minutes and 41 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. 📝 WalkthroughWalkthroughThe PR implements four linked features: expert cooldown after dispute loss, seeker spending-limit enforcement, off-chain voucher-signed session creation, and standardized webhook event infrastructure for off-chain relay services. It adds Ed25519-dalek for cryptographic signing, introduces new storage keys and error variants, and broadly refactors event emission across the contract to use a unified envelope schema. ChangesExpert Cooldown, Spending Limits, and Voucher Session Flow
Webhook Relay Service Design
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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 |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
contracts/src/events.rs (1)
35-133: ⚡ Quick winAdd a uniqueness guard for
event_typesymbols.With 30+ short symbols, an accidental duplicate will silently break relay routing. Add a unit test that asserts all
event_type::*()values are unique.🤖 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/src/events.rs` around lines 35 - 133, Add a unit test that collects the return values of every event symbol function (e.g., session_started, session_paused, session_resumed, session_settled, session_finished, session_refund, session_commit, session_reveal, session_voucher, dispute_flagged, dispute_evidence, dispute_resolved, expert_cooldown, spending_limit, admin_config, platform_stats, fee_burn, staking, subscription, fixed_price, expert_profile, rating, swap, governance, insurance, upgrade, integration, heartbeat, slashing, reverify, frozen, badge) into a HashSet and assert the set length equals the number of functions to guarantee uniqueness; implement the test in the same module (#[cfg(test)] mod tests) as a #[test] named like all_event_type_symbols_unique, use .insert() or collect to build the set and assert_eq!(set.len(), expected_count) so any duplicate symbol fails the test.
🤖 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/src/crypto.rs`:
- Around line 33-42: The verify_voucher_signature function currently calls
env.crypto().ed25519_verify(...) and always returns Ok(()), which causes a trap
on bad signatures; change verify_voucher_signature to check the verification
result and return Err(Error::InvalidVoucherSignature.into()) on failure instead
of allowing a panic—specifically, in verify_voucher_signature (taking Env,
SessionVoucher, BytesN<32>, BytesN<64>) call
env.crypto().ed25519_verify(public_key, &message, signature), if it indicates
failure return the contract error Error::InvalidVoucherSignature, otherwise
return Ok(()) so start_session_with_voucher receives a proper contract error
rather than aborting.
In `@contracts/src/disputes.rs`:
- Around line 59-63: The cooldown entry written by apply_cooldown_if_expert_lost
uses env.storage().temporary().set(...) but never extends its TTL, so compute
the intended expiry (you already compute `until` via
`env.ledger().sequence().saturating_add(ledgers)`) and call the temporary
storage TTL extension API immediately after setting the key: invoke
`env.storage().temporary().extend_ttl(&DataKey::ExpertCooldownUntil(expert.clone()),
ledgers_or_calculated_ttl)` (using the same `ledgers`/computed duration that
`cooldown_ledgers` returned, or convert the `until` sequence into the
appropriate TTL units expected by `extend_ttl`) to ensure the temp entry lives
at least until the intended ledger sequence.
In `@contracts/src/lib.rs`:
- Around line 2072-2077: The code currently rejects vouchers when the expert's
live profile.rate_per_second differs from the voucher's signed
voucher.rate_per_second, which makes signed voucher rates unusable; remove the
equality check that compares profile.rate_per_second to voucher.rate_per_second
in the session-accept path (the block around
Self::assert_expert_can_accept_session and the following if) so the session rate
is taken from voucher.rate_per_second instead, and ensure profile is only used
for availability/cooldown/reputation checks (i.e., keep calling
assert_expert_can_accept_session to validate the expert but do not gate validity
on profile.rate_per_second).
In `@docs/WEBHOOK_RELAY.md`:
- Line 112: The current idempotency key uses the tuple (tx_hash, event_type,
session_id) which can collide for multiple events in the same transaction;
update the dedupe key to include the event index or full event ID (e.g., add
event_index or event_id) so each emitted event is uniquely identified when
building the dedupe key in the webhook relay deduplication logic (the place that
currently constructs the `(tx_hash, event_type, session_id)` key).
- Around line 40-41: The `sessSettl` payload tuple is currently ambiguous;
update the docs for the `sessSettl` / `settle_session` entry to enumerate the
exact tuple variants (or provide a versioned payload format) so decoders can
parse deterministically — explicitly list each allowed tuple shape (field order
and types) such as e.g. variant A: ( "label" | "expert_payout", timestamp:
number, amount: number, currency?: string ), variant B: ( "partial", amount:
number, remaining: number, ts: number ), etc., or add a top-level version token
like `v1` followed by a defined schema; ensure you update the `sessSettl` line
and nearby examples to show the exact field names/types and a sample JSON/tuple
for each variant so integrations can implement strict parsing in their relay
decoders.
---
Nitpick comments:
In `@contracts/src/events.rs`:
- Around line 35-133: Add a unit test that collects the return values of every
event symbol function (e.g., session_started, session_paused, session_resumed,
session_settled, session_finished, session_refund, session_commit,
session_reveal, session_voucher, dispute_flagged, dispute_evidence,
dispute_resolved, expert_cooldown, spending_limit, admin_config, platform_stats,
fee_burn, staking, subscription, fixed_price, expert_profile, rating, swap,
governance, insurance, upgrade, integration, heartbeat, slashing, reverify,
frozen, badge) into a HashSet and assert the set length equals the number of
functions to guarantee uniqueness; implement the test in the same module
(#[cfg(test)] mod tests) as a #[test] named like all_event_type_symbols_unique,
use .insert() or collect to build the set and assert_eq!(set.len(),
expected_count) so any duplicate symbol fails the test.
🪄 Autofix (Beta)
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
Run ID: 7d33d3e3-8941-4c2e-afc3-8084f2484e8a
📒 Files selected for processing (7)
contracts/Cargo.tomlcontracts/src/crypto.rscontracts/src/disputes.rscontracts/src/errors.rscontracts/src/events.rscontracts/src/lib.rsdocs/WEBHOOK_RELAY.md
| pub fn verify_voucher_signature( | ||
| env: &Env, | ||
| voucher: &SessionVoucher, | ||
| public_key: &BytesN<32>, | ||
| signature: &BytesN<64>, | ||
| ) -> Result<(), Error> { | ||
| let message = voucher_message(env, voucher); | ||
| env.crypto() | ||
| .ed25519_verify(public_key, &message, signature); | ||
| Ok(()) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== List relevant files =="
ls -R contracts/src | sed -n '1,200p'
echo "== Show contracts/src/crypto.rs around lines 1-120 =="
nl -ba contracts/src/crypto.rs | sed -n '1,140p'
echo "== Locate verify_voucher_signature =="
rg -n "fn verify_voucher_signature" -S contracts/src
echo "== Show exact function body =="
python3 - <<'PY'
import re, pathlib
p = pathlib.Path("contracts/src/crypto.rs")
txt = p.read_text()
m = re.search(r"pub fn verify_voucher_signature[^{]*\{", txt)
if not m:
print("Not found")
raise SystemExit(1)
start = m.start()
# naive brace matching
i = m.end()
depth = 1
while i < len(txt) and depth:
if txt[i] == '{': depth += 1
elif txt[i] == '}': depth -= 1
i += 1
body = txt[m.start():i]
print(body)
PY
echo "== Locate Error::InvalidVoucherSignature =="
rg -n "InvalidVoucherSignature" -S contracts/src
echo "== Show Error enum definition area =="
rg -n "enum Error|struct Error|pub enum Error" -S contracts/src
# print any files likely containing Error (heuristic: match "Error" and "InvalidVoucherSignature")
for f in $(rg -l "InvalidVoucherSignature" contracts/src); do
echo "---- $f (context) ----"
nl -ba "$f" | sed -n '1,220p'
doneRepository: LightForgeHub/SkillSphere-Dapp
Length of output: 336
🌐 Web query:
soroban_sdk Crypto ed25519_verify return type and behavior on failure
💡 Result:
In soroban_sdk, Crypto::ed25519_verify returns no value (it’s a unit function): pub fn ed25519_verify(&self, public_key: &BytesN<32>, message: &Bytes, signature: &BytesN<64>) [1]. On failure (i.e., if the signature verification fails), it panics; the docs explicitly list “Panics: If the signature verification fails.” [2][1]. Behaviorally, successful verification means the function returns normally and execution continues; the example usage shows that after calling env.crypto.ed25519_verify(...), execution proceeds only if the signature is valid. [1] Note: Soroban’s host function details are handled by the environment/crypto implementation; there have been discussions about using stricter Ed25519 verification in the host to avoid malleability issues, but that’s separate from the SDK-level API behavior (panic-on-failure). [3]
Citations:
- 1: https://www.mintlify.com/stellar/rs-soroban-sdk/api/modules/crypto
- 2: https://docs.rs/soroban-sdk/latest/soroban_sdk/crypto/struct.Crypto.html
- 3: ed25519 signature verification host function admits weak keys / malleable signatures stellar/rs-soroban-env#857
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Show contracts/src/crypto.rs (first 220 lines) =="
cat -n contracts/src/crypto.rs | sed -n '1,220p'
echo "== Search for InvalidVoucherSignature in repo =="
rg -n "InvalidVoucherSignature" -S contracts/src
echo "== Show contracts/src/errors.rs =="
cat -n contracts/src/errors.rs | sed -n '1,260p'
echo "== Show call sites of verify_voucher_signature =="
rg -n "verify_voucher_signature" -S contracts/src
echo "== Show contracts/src/lib.rs around voucher handling =="
rg -n "Voucher|SessionVoucher|voucher" -S contracts/src/lib.rs
cat -n contracts/src/lib.rs | sed -n '1,260p'Repository: LightForgeHub/SkillSphere-Dapp
Length of output: 20977
🌐 Web query:
soroban_sdk try_ed25519_verify non-panicking ed25519_verify returns Result or Option
💡 Result:
soroban_sdk::EnvCrypto::try_ed25519_verify does not return Result/Option; it’s a “non-panicking” verification API whose success/failure is communicated via its return type. However, in the Soroban SDK’s current documented API, the non-panicking behavior question you asked is answered by comparing with env.crypto.ed25519_verify: - soroban_sdk::crypto::ed25519_verify panics if the signature verification fails (so it is not non-panicking). [1][2] From the authoritative docs we found in this research, the exact return type of try_ed25519_verify (whether it’s a Result, Option, or something else) is not shown in the sources we pulled; we only have explicit evidence that ed25519_verify panics on failure. [1][2] If you want, paste the try_ed25519_verify signature (or a link to its docs page) and I’ll confirm whether it returns Result or Option from the exact declaration.
Citations:
- 1: https://www.mintlify.com/stellar/rs-soroban-sdk/api/modules/crypto
- 2: https://docs.rs/soroban-sdk/latest/soroban_sdk/crypto/struct.Crypto.html
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Show start_session_with_voucher implementation (around 2035-2125) =="
sed -n '2020,2130p' contracts/src/lib.rs | cat -n
echo "== Show test_start_session_with_voucher (around 6570-6690) =="
sed -n '6560,6700p' contracts/src/lib.rs | cat -nRepository: LightForgeHub/SkillSphere-Dapp
Length of output: 10177
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Search for InvalidVoucherSignature usage (entire contracts/src) =="
rg -n "InvalidVoucherSignature" contracts/srcRepository: LightForgeHub/SkillSphere-Dapp
Length of output: 205
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Search for ed25519_verify or try_ed25519_verify usage =="
rg -n "ed25519_verify|try_ed25519_verify" contracts/srcRepository: LightForgeHub/SkillSphere-Dapp
Length of output: 219
Return InvalidVoucherSignature instead of trapping on bad signatures.
contracts/src/crypto.rs::verify_voucher_signature always returns Ok(()) and calls env.crypto().ed25519_verify(...), which panics on verification failure in the Soroban SDK—so start_session_with_voucher will abort the invocation instead of returning the contract error Error::InvalidVoucherSignature (the error is defined in contracts/src/errors.rs but never used/returned).
🤖 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/src/crypto.rs` around lines 33 - 42, The verify_voucher_signature
function currently calls env.crypto().ed25519_verify(...) and always returns
Ok(()), which causes a trap on bad signatures; change verify_voucher_signature
to check the verification result and return
Err(Error::InvalidVoucherSignature.into()) on failure instead of allowing a
panic—specifically, in verify_voucher_signature (taking Env, SessionVoucher,
BytesN<32>, BytesN<64>) call env.crypto().ed25519_verify(public_key, &message,
signature), if it indicates failure return the contract error
Error::InvalidVoucherSignature, otherwise return Ok(()) so
start_session_with_voucher receives a proper contract error rather than
aborting.
| let ledgers = cooldown_ledgers(env); | ||
| let until = env.ledger().sequence().saturating_add(ledgers); | ||
| env.storage() | ||
| .temporary() | ||
| .set(&DataKey::ExpertCooldownUntil(expert.clone()), &until); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Inspect the exact code around the reported lines
sed -n '1,120p' contracts/src/disputes.rs | nl -ba | sed -n '40,90p'
# 2) Locate the function(s) that write the cooldown key and see if extend_ttl is used anywhere
rg -n "ExpertCooldownUntil|temporary\\(\\)\\.set\\(|extend_ttl\\(" contracts/src/disputes.rs contracts/src -S || true
# 3) Find any usage of extend_ttl across the contracts module
rg -n "extend_ttl\\(" contracts/src -S || trueRepository: LightForgeHub/SkillSphere-Dapp
Length of output: 117
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the exact region around the reported lines (59-63)
awk 'NR>=45 && NR<=80 {printf "%d:%s\n", NR, $0}' contracts/src/disputes.rs
# Confirm whether extend_ttl is used anywhere in the file/module
rg -n "extend_ttl\\(" contracts/src/disputes.rs contracts/src || trueRepository: LightForgeHub/SkillSphere-Dapp
Length of output: 700
🌐 Web query:
soroban_sdk temporary().set ttl default extend_ttl min_temp_entry_ttl
💡 Result:
In the Soroban SDK, temporary storage entries are ephemeral and automatically deleted when their time-to-live (TTL) reaches zero [1][2]. Unlike persistent or instance storage, temporary entries are not archived [1][2]. The relevant methods for managing TTL in the Soroban SDK are provided by the env.storage().temporary() interface: 1. set(&self, key: &K, val: &V): This method stores a value under a given key [3]. Temporary entries have a default TTL upon creation, which is governed by the network's configuration rather than a direct parameter in the set method [1]. 2. extend_ttl(&self, key: &K, threshold: u32, extend_to: u32): This method allows you to extend the TTL of an existing temporary storage entry [3][4]. It functions as a conditional update: - The extension is performed only if the current TTL is below the specified threshold (in ledgers) [3][4]. - If the condition is met, the TTL is extended to the specified extend_to value (in ledgers) [3][4]. Developers should note that while temporary entries can have their TTLs extended, they are designed for ephemeral data [1][2]. Relying on TTL extensions for long-term storage is generally discouraged, as anyone can submit a TTL extension operation on any entry without authorization, and temporary entries do not provide the safety or permanence of persistent storage [5]. In test environments, developers can verify these behaviors using test utilities such as get_ttl and simulating ledger advancement [4][6].
Citations:
- 1: https://www.certik.com/blog/soroban-contract-state-management
- 2: https://docs.rs/soroban-sdk/latest/soroban_sdk/storage/struct.Storage.html
- 3: https://docs.rs/soroban-sdk/latest/soroban_sdk/storage/struct.Temporary.html
- 4: https://developers.stellar.org/docs/build/guides/archival/test-ttl-extension
- 5: https://developers.stellar.org/docs/learn/fundamentals/contract-development/storage/persisting-data
- 6: https://docs.rs/soroban-sdk/latest/soroban_sdk/testutils/storage/trait.Temporary.html
Extend the cooldown key TTL when writing it.
apply_cooldown_if_expert_lost stores DataKey::ExpertCooldownUntil(expert) with env.storage().temporary().set(...) and never calls env.storage().temporary().extend_ttl(...), so the entry’s lifetime is limited to the network-configured default temp TTL and may expire before until, re-enabling the expert early. Use extend_ttl to ensure the temp entry’s TTL covers the intended ledger sequence. https://developers.stellar.org/docs/build/guides/archival/test-ttl-extension
🤖 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/src/disputes.rs` around lines 59 - 63, The cooldown entry written
by apply_cooldown_if_expert_lost uses env.storage().temporary().set(...) but
never extends its TTL, so compute the intended expiry (you already compute
`until` via `env.ledger().sequence().saturating_add(ledgers)`) and call the
temporary storage TTL extension API immediately after setting the key: invoke
`env.storage().temporary().extend_ttl(&DataKey::ExpertCooldownUntil(expert.clone()),
ledgers_or_calculated_ttl)` (using the same `ledgers`/computed duration that
`cooldown_ledgers` returned, or convert the `until` sequence into the
appropriate TTL units expected by `extend_ttl`) to ensure the temp entry lives
at least until the intended ledger sequence.
| let profile = | ||
| Self::assert_expert_can_accept_session(&env, voucher.expert.clone(), min_reputation)?; | ||
|
|
||
| if profile.rate_per_second != voucher.rate_per_second { | ||
| return Err(Error::InvalidVoucher); | ||
| } |
There was a problem hiding this comment.
Don't bind voucher validity to the mutable profile rate.
Line 2075 makes the signed rate_per_second redundant: any profile rate change invalidates previously issued vouchers, and one-off quoted rates can never be redeemed. Use the live profile only for availability/cooldown/reputation checks and take the session rate from the voucher itself.
🤖 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/src/lib.rs` around lines 2072 - 2077, The code currently rejects
vouchers when the expert's live profile.rate_per_second differs from the
voucher's signed voucher.rate_per_second, which makes signed voucher rates
unusable; remove the equality check that compares profile.rate_per_second to
voucher.rate_per_second in the session-accept path (the block around
Self::assert_expert_can_accept_session and the following if) so the session rate
is taken from voucher.rate_per_second instead, and ensure profile is only used
for availability/cooldown/reputation checks (i.e., keep calling
assert_expert_can_accept_session to validate the expert but do not gate validity
on profile.rate_per_second).
| | `sessSettl` | `settle_session`, partial withdraw | `(expert_payout_or_label, ts_or_amount, …)` | | ||
| | `sessFinsh` | `end_session` | `(claimable, remaining, finished_at)` | |
There was a problem hiding this comment.
Make sessSettl payload schema explicit.
(expert_payout_or_label, ts_or_amount, …) is too ambiguous for stable relay decoders. Document exact tuple variants (or versioned payload formats) so integrations can parse deterministically.
🤖 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 `@docs/WEBHOOK_RELAY.md` around lines 40 - 41, The `sessSettl` payload tuple is
currently ambiguous; update the docs for the `sessSettl` / `settle_session`
entry to enumerate the exact tuple variants (or provide a versioned payload
format) so decoders can parse deterministically — explicitly list each allowed
tuple shape (field order and types) such as e.g. variant A: ( "label" |
"expert_payout", timestamp: number, amount: number, currency?: string ), variant
B: ( "partial", amount: number, remaining: number, ts: number ), etc., or add a
top-level version token like `v1` followed by a defined schema; ensure you
update the `sessSettl` line and nearby examples to show the exact field
names/types and a sample JSON/tuple for each variant so integrations can
implement strict parsing in their relay decoders.
|
|
||
| ### Recommended relay behaviour | ||
|
|
||
| - **Idempotency**: dedupe on `(tx_hash, event_type, session_id)`. |
There was a problem hiding this comment.
Strengthen webhook idempotency key to prevent false dedupes.
(tx_hash, event_type, session_id) can collide within a single transaction if multiple matching events are emitted, causing dropped notifications. Include an event index/order (or full event ID) in the dedupe key.
🤖 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 `@docs/WEBHOOK_RELAY.md` at line 112, The current idempotency key uses the
tuple (tx_hash, event_type, session_id) which can collide for multiple events in
the same transaction; update the dedupe key to include the event index or full
event ID (e.g., add event_index or event_id) so each emitted event is uniquely
identified when building the dedupe key in the webhook relay deduplication logic
(the place that currently constructs the `(tx_hash, event_type, session_id)`
key).
Summary
Implements contract features for issues #240–#243: expert dispute-loss cooldowns, seeker self-imposed spending caps, ed25519 session vouchers, and a standardized webhook event envelope for off-chain relay daemons.
start_sessionrejects cooled-down experts withError::ExpertOnCooldown. Admins configure duration viaset_expert_cooldown_ledgers(default ≈ 7 days of ledgers).set_spending_limit(max_per_session); the cap is stored per address in persistent storage and enforced during session start.SessionVoucher { expert, rate_per_second, max_duration, expiry, nonce }off-chain. Seekers open sessions viastart_session_with_voucher; nonces are consumed to prevent replay.(event_type, session_id, timestamp, payload)envelope under thewebhooktopic. Addsdocs/WEBHOOK_RELAY.mdand an integration test validating the envelope shape.Files changed
contracts/src/disputes.rscontracts/src/crypto.rscontracts/src/events.rspublish_eventhelper (#243)contracts/src/lib.rscontracts/src/errors.rsdocs/WEBHOOK_RELAY.mdNew public API
set_expert_cooldown_ledgers/get_expert_cooldown_ledgers/get_expert_cooldown_untilset_spending_limit/clear_spending_limit/get_spending_limitset_voucher_signing_key/get_voucher_signing_key/start_session_with_voucherTest plan
cd contracts && cargo testtest_expert_cooldown_after_dispute_loss— cooldown set after seeker-favourable resolutiontest_start_session_rejects_expert_on_cooldown— panics with#50test_seeker_spending_limit_enforced— panics with#51when deposit exceeds captest_start_session_with_voucher— valid signature starts session; replay rejected with#53test_webhook_relay_emits_standard_envelope— session, dispute, and spending-limit events usewebhooktopic + 4-field tupleset_expert_cooldown_ledgersand confirm new disputes honour the updated lengthBreaking changes
(session, started)) must migrate to thewebhookenvelope documented indocs/WEBHOOK_RELAY.md.Closes #240
Closes #241
Closes #242
Closes #243
Summary by CodeRabbit
New Features
Documentation