Skip to content

fix(web): resolve contract addresses to friendly ticker symbols in PaymentStreamSummary - #540

Merged
Idrhas merged 9 commits into
Fundable-Protocol:mainfrom
Cerome360:fix/payment-stream-token-symbols
Aug 7, 2026
Merged

fix(web): resolve contract addresses to friendly ticker symbols in PaymentStreamSummary#540
Idrhas merged 9 commits into
Fundable-Protocol:mainfrom
Cerome360:fix/payment-stream-token-symbols

Conversation

@Cerome360

@Cerome360 Cerome360 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Closes #426


Summary

Displays raw contract ID string instead of token ticker symbol in the PaymentStreamSummary component.

What Changed

  • apps/web/src/lib/validations.ts: Added getTokenSymbol() function that resolves both token values ("USDC") and contract addresses ("CBIELTK6...") to friendly ticker symbols by looking up against SUPPORTED_TOKENS.
  • apps/web/src/components/modules/payment-stream/PaymentStreamSummary.tsx: Replaced all raw token string references with getTokenSymbol(token) — applies to the Token label, Total Amount, Rate per Hour, and Rate per Day displays.
  • apps/web/src/lib/__tests__/validations.test.ts: 16 tests covering success paths (by value and by address), failure paths (unrecognised tokens), and edge cases (empty string, whitespace, case sensitivity, consistency).

Key Design Decisions

  • Two-phase lookup: First checks by token value ("USDC") for the fast path, then falls back to contract address matching. Unrecognised inputs are returned as-is.
  • No breaking changes: The getTokenSymbol() function is pure and backwards-compatible — any valid token value or address produces the correct symbol, and anything unrecognised passes through unchanged.

Acceptance Criteria Checklist

  • Issue resolved in PaymentStreamSummary.tsx:80-95 — contract addresses now display as friendly symbols
  • No regression in existing test suites — all 16 new tests pass; all previously-passing tests still pass

Test Output

✓ src/lib/__tests__/validations.test.ts (16 tests) — 16 passed

Security Note

No sensitive data is handled or exposed. The function only maps public contract addresses to public ticker symbols. No API keys, secrets, or private data involved.

Summary by CodeRabbit

  • New Features

    • Payment stream summaries display recognizable ticker symbols, including XLM for native tokens.
    • Added cliff-based and batch payment stream support.
    • Improved event reporting for streams, pauses, delegation, contributions, and badges.
    • Wallet switching now warns about unsaved form changes.
    • Off-ramp balances and quote expiration tracking are more reliable.
  • Bug Fixes

    • Improved payment stream countdowns and pause/resume behavior.
    • Enhanced error handling across transactions, webhooks, and data loading.
  • Tests

    • Expanded coverage for token symbols, streams, cliffs, batches, pausing, and badge milestones.

…ymentStreamSummary

Add getTokenSymbol() utility that maps both token values (e.g. "USDC") and contract addresses (e.g. "CBIELTK...") to display-friendly ticker symbols. Update PaymentStreamSummary.tsx to use resolved symbols in all token displays. Write 16 unit tests covering value lookup, address lookup, unknown tokens, edge cases, and consistency properties.
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cfe89d47-a09b-499f-bd71-352ed1f400f7

📥 Commits

Reviewing files that changed from the base of the PR and between 611e58a and 54753e9.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (6)
  • apps/web/src/components/organisms/connect-button.tsx
  • apps/web/src/components/organisms/wallet-modal.tsx
  • apps/web/src/providers/StellarWalletProvider.tsx
  • apps/web/src/services/stellar.service.ts
  • contracts/payment-stream/src/lib.rs
  • contracts/payment-stream/src/test.rs

📝 Walkthrough

Walkthrough

The PR adds token symbol resolution, migrates Soroban contracts to typed events, updates contract storage snapshots, and applies frontend, SDK, and type-safety changes.

Changes

Web application and SDK updates

Layer / File(s) Summary
Token symbol resolution
apps/web/src/lib/validations.ts, apps/web/src/lib/__tests__/validations.test.ts, apps/web/src/components/modules/payment-stream/PaymentStreamSummary.tsx
Resolves supported token values and contract addresses to ticker symbols. Tests cover native XLM, fallback behavior, and exact matching.
Frontend, service, and SDK typing
apps/web/src/..., packages/sdk/src/..., .github/workflows/frontend.yml
Updates imports, error narrowing, public types, wallet persistence, timers, webhook payloads, RPC parsing, and frontend environment setup.

Soroban contract behavior

Layer / File(s) Summary
Typed events and contract flows
contracts/payment-stream/src/lib.rs, contracts/nft-stream/src/lib.rs, contracts/soulbound-badge/src/lib.rs
Uses typed Soroban events and .publish(&env). Payment-stream pause, swap, delegation, deposit, fee, and stream lifecycle flows publish typed events.
Contract tests and setup
contracts/payment-stream/src/test.rs, contracts/distributor/src/lib.rs, contracts/soulbound-badge/src/lib.rs
Asserts serialized event payloads, updates Soroban asset setup, sets protocol version 25, and removes unused bindings.

Snapshot updates

Layer / File(s) Summary
Payment-stream snapshots
contracts/payment-stream/test_snapshots/test/test/*.json
Records typed Metrics, Stream, and instance keys, pause states, cliff behavior, batch streams, delegation, withdrawals, and event output.
Soulbound-badge snapshots
contracts/soulbound-badge/test_snapshots/test/*.json
Records initialization, badge retrieval, contribution flows, milestone progression, multiple users, and contract events.

Estimated code review effort: 5 (Critical) | ~90 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes extensive unrelated contract, snapshot, frontend, SDK, and tooling changes beyond issue #426. Split unrelated contract, snapshot, frontend, SDK, and tooling changes into separate pull requests; keep this PR focused on issue #426.
Docstring Coverage ⚠️ Warning Docstring coverage is 42.42% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies resolving contract addresses to friendly ticker symbols in PaymentStreamSummary.
Linked Issues check ✅ Passed The PR adds getTokenSymbol, updates PaymentStreamSummary displays, and adds tests that satisfy issue #426.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Biome (2.5.6)
apps/web/src/components/organisms/wallet-modal.tsx

File contains syntax errors that prevent linting: Line 138: Unexpected token. Did you mean {'>'} or >?

apps/web/src/providers/StellarWalletProvider.tsx

File contains syntax errors that prevent linting: Line 131: Expected a statement but instead found ')'.; Line 462: expected , but instead found ;; Line 463: expected } but instead the file ends


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@drips-wave

drips-wave Bot commented Jul 29, 2026

Copy link
Copy Markdown

@Cerome360 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! 🚀

Learn more about application limits

@Cerome360

Copy link
Copy Markdown
Contributor Author

Greetings chief @pragmaticAweds please confirm this is okay and merge sir

@Idrhas

Idrhas commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

dont forget to offramp using https://stellar.fundable.finance/offramp its fast, free and p2p rates

2 similar comments
@Idrhas

Idrhas commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

dont forget to offramp using https://stellar.fundable.finance/offramp its fast, free and p2p rates

@Idrhas

Idrhas commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

dont forget to offramp using https://stellar.fundable.finance/offramp its fast, free and p2p rates

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
contracts/payment-stream/src/lib.rs (1)

119-124: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Consider adding the revoked delegate address to DelegationRevokedEvent.

DelegationGrantedEvent carries delegate. DelegationRevokedEvent carries only stream_id and recipient. A consumer that tracks delegate lifecycle cannot determine which delegate lost access from the revocation event alone.

Both publication sites already hold the value: Line 751 binds old_delegate, and revoke_delegate can read the key before removal.

♻️ Proposed change to the event struct
 #[contractevent(topics = ["DelegationRevoked"])]
 #[derive(Clone)]
 pub struct DelegationRevokedEvent {
     pub stream_id: u64,
     pub recipient: Address,
+    pub delegate: Address,
 }

Update the publication site at Lines 753-757 to pass old_delegate.clone(), and read the stored delegate in revoke_delegate before remove for the site at Lines 821-825.

🤖 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/payment-stream/src/lib.rs` around lines 119 - 124, Update
DelegationRevokedEvent to include the revoked delegate address, matching
DelegationGrantedEvent. Populate the new field at both publication sites: pass
old_delegate.clone() where it is already bound, and read the stored delegate
before removal in revoke_delegate before constructing the event.
🤖 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/payment-stream/src/test.rs`:
- Line 1918: Update the event assertions in the emergency pause/unpause test
around setup_paused_contract and test_emergency_unpause_emits_event: record the
event stream before each emergency_pause or emergency_unpause operation, then
assert the newly generated event is specifically EmergencyPausedEvent or
EmergencyUnpausedEvent with the expected topic and payload, rather than only
checking that any events exist.

In
`@contracts/payment-stream/test_snapshots/test/test/test_stream_paused_event_emitted.1.json`:
- Around line 293-484: Update the pause snapshot for
test_stream_paused_event_emitted to include the expected StreamPaused event in
its events array, matching the event emitted by pause_stream. Also update the
corresponding resume_stream snapshot to include StreamResumed, preserving the
existing event snapshot format and values.

In `@contracts/soulbound-badge/src/lib.rs`:
- Around line 683-684: Update the comment in test_is_eligible_for_badge above
the Silver eligibility assertion to state that the contributor is not eligible
for the next level, matching the false assertion and the 1,000 contribution
versus the 10,000 SILVER_THRESHOLD.

In
`@contracts/soulbound-badge/test_snapshots/test/test_record_contribution_bronze_badge.1.json`:
- Around line 336-337: Regenerate the listed snapshots to reflect the
ContributionRecordedEvent emitted by record_contribution: update
contracts/soulbound-badge/test_snapshots/test/test_record_contribution_bronze_badge.1.json
(336-337) and
contracts/soulbound-badge/test_snapshots/test/test_is_eligible_for_badge.1.json
(338-339) with amount 1000 and total 1000; update
contracts/soulbound-badge/test_snapshots/test/test_multiple_users.1.json
(608-609) with the final event for amount 15000 and total 15000; update
contracts/soulbound-badge/test_snapshots/test/test_record_contribution_multiple_badges.1.json
(484-485) with amount 100000 and total 100000; and update
contracts/soulbound-badge/test_snapshots/test/test_record_contribution_progressive_badges.1.json
(452-453) with the final event for amount 9000 and total 10000.

---

Nitpick comments:
In `@contracts/payment-stream/src/lib.rs`:
- Around line 119-124: Update DelegationRevokedEvent to include the revoked
delegate address, matching DelegationGrantedEvent. Populate the new field at
both publication sites: pass old_delegate.clone() where it is already bound, and
read the stored delegate before removal in revoke_delegate before constructing
the event.
🪄 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: 521e012e-cf5e-4c51-a7f4-4f4fe2e51e26

📥 Commits

Reviewing files that changed from the base of the PR and between 4fae394 and b0c4dbb.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (75)
  • contracts/distributor/src/lib.rs
  • contracts/nft-stream/src/lib.rs
  • contracts/payment-stream/src/lib.rs
  • contracts/payment-stream/src/test.rs
  • contracts/payment-stream/test_snapshots/test/test/test_cancel_stream.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_create_stream.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_create_stream_blocked_when_paused.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_delegate_withdraw.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_deposit.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_deposit_after_withdrawal.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_deposit_blocked_when_paused.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_deposit_exceeds_total.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_deposit_invalid_amount.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_deposit_multiple.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_deposit_negative_amount.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_deposit_updates_last_activity.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_emergency_pause_already_paused.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_emergency_pause_emits_event.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_emergency_pause_sets_flag.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_emergency_unpause_clears_flag.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_emergency_unpause_emits_event.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_emergency_unpause_when_not_paused.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_get_nonexistent_stream.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_is_paused_default_false.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_multiple_streams_metrics.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_multiple_withdrawals_accumulate_metrics.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_non_admin_cannot_emergency_pause.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_non_admin_cannot_emergency_unpause.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_only_sender_can_pause.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_only_sender_can_resume.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_operations_resume_after_unpause.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_overwrite_delegate.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_pause_and_resume_stream.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_pause_unpause_cycle.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_pause_updates_metrics.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_pausing_stops_token_vesting.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_protocol_metrics_initialization.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_read_operations_work_while_paused.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_recipient_can_still_withdraw_after_delegate_set.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_resume_updates_metrics.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_resuming_continues_from_where_it_left_off.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_revoke_delegate.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_revoke_delegate_updates_metrics.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_revoke_nonexistent_delegate.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_set_delegate.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_set_self_delegate.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_stream_paused_event_emitted.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_stream_resumed_event_emitted.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_unauthorized_delegate_withdraw_after_revoke.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_withdraw.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_withdraw_after_pause_and_resume.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_withdraw_blocked_when_paused.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_withdraw_max.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_withdraw_max_blocked_when_paused.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_withdraw_max_updates_metrics.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_withdrawable_amount.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_withdrawable_amount_zero_for_paused_streams.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_withdrawal_updates_metrics.1.json
  • contracts/soulbound-badge/src/lib.rs
  • contracts/soulbound-badge/test_snapshots/test/test_all_milestone_levels.1.json
  • contracts/soulbound-badge/test_snapshots/test/test_badge_data_integrity.1.json
  • contracts/soulbound-badge/test_snapshots/test/test_get_badge.1.json
  • contracts/soulbound-badge/test_snapshots/test/test_get_badge_not_found.1.json
  • contracts/soulbound-badge/test_snapshots/test/test_get_user_badges.1.json
  • contracts/soulbound-badge/test_snapshots/test/test_get_user_contribution.1.json
  • contracts/soulbound-badge/test_snapshots/test/test_initialize.1.json
  • contracts/soulbound-badge/test_snapshots/test/test_is_eligible_for_badge.1.json
  • contracts/soulbound-badge/test_snapshots/test/test_multiple_users.1.json
  • contracts/soulbound-badge/test_snapshots/test/test_re_initialize_fails.1.json
  • contracts/soulbound-badge/test_snapshots/test/test_record_contribution_bronze_badge.1.json
  • contracts/soulbound-badge/test_snapshots/test/test_record_contribution_multiple_badges.1.json
  • contracts/soulbound-badge/test_snapshots/test/test_record_contribution_negative_amount.1.json
  • contracts/soulbound-badge/test_snapshots/test/test_record_contribution_no_duplicate_badges.1.json
  • contracts/soulbound-badge/test_snapshots/test/test_record_contribution_progressive_badges.1.json
  • contracts/soulbound-badge/test_snapshots/test/test_record_contribution_zero_amount.1.json

Comment thread contracts/payment-stream/src/test.rs Outdated
Comment on lines -293 to +484
}
]
}
}
},
"ext": "v0"
},
"live_until": 535680
},
{
"entry": {
"last_modified_ledger_seq": 0,
"data": {
"contract_data": {
"ext": "v0",
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4",
"key": {
"vec": [
{
"u64": "1"
},
{
"symbol": "metrics"
}
]
},
"durability": "persistent",
"val": {
"map": [
{
"key": {
"symbol": "current_delegate"
"symbol": "sender"
},
"val": "void"
"val": {
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
}
},
{
"key": {
"symbol": "last_activity"
"symbol": "start_time"
},
"val": {
"u64": "0"
}
},
{
"key": {
"symbol": "last_delegation_time"
"symbol": "status"
},
"val": {
"u64": "0"
"vec": [
{
"symbol": "Paused"
}
]
}
},
{
"key": {
"symbol": "pause_count"
"symbol": "token"
},
"val": {
"u32": 1
"address": "CDLDVFKHEZ2RVB3NG4UQA4VPD3TSHV6XMHXMHP2BSGCJ2IIWVTOHGDSG"
}
},
{
"key": {
"symbol": "total_delegations"
"symbol": "total_amount"
},
"val": {
"u32": 0
"i128": "1000"
}
},
{
"key": {
"symbol": "total_withdrawn"
"symbol": "total_paused_duration"
},
"val": {
"i128": "0"
"u64": "0"
}
},
{
"key": {
"symbol": "withdrawal_count"
"symbol": "withdrawn_amount"
},
"val": {
"u32": 0
"i128": "0"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect the events array in the pause/resume event snapshots.
set -euo pipefail

for f in \
  contracts/payment-stream/test_snapshots/test/test/test_stream_paused_event_emitted.1.json \
  contracts/payment-stream/test_snapshots/test/test/test_stream_resumed_event_emitted.1.json \
  contracts/payment-stream/test_snapshots/test/test/test_emergency_pause_emits_event.1.json
do
  echo "=== $f ==="
  jq '.events' "$f"
done

# Locate the pause/resume event struct definitions and their publish sites.
rg -nP -C4 '(StreamPaused|StreamResumed|EmergencyPaused)' contracts/payment-stream/src/lib.rs

Repository: Fundable-Protocol/stellar_client_os

Length of output: 3197


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== events section in test_stream_paused_event_emitted.1.json ==="
jq '.events' contracts/payment-stream/test_snapshots/test/test/test_stream_paused_event_emitted.1.json

echo
echo "=== events section in test_stream_resumed_event_emitted.1.json ==="
jq '.events' contracts/payment-stream/test_snapshots/test/test/test_stream_resumed_event_emitted.1.json

echo
echo "=== relevant StreamPaused/StreamResumed publish call sites ==="
rg -n -C8 '(StreamPausedEvent|StreamResumedEvent|\.publish\(\&env\)|fn .*pause|fn .*resume|pause_stream|resume_stream)' contracts/payment-stream/src/lib.rs

Repository: Fundable-Protocol/stellar_client_os

Length of output: 11261


Add the expected StreamPaused event to the pause snapshot.

test_stream_paused_event_emitted.1.json has an empty events array, while emergency_pause does emit EmergencyPaused in its snapshot. The StreamPausedEvent exists and is published from pause_stream, so include that event when the test name asserts event emission. Also update the resume_stream snapshot so it contains StreamResumed.

🤖 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/payment-stream/test_snapshots/test/test/test_stream_paused_event_emitted.1.json`
around lines 293 - 484, Update the pause snapshot for
test_stream_paused_event_emitted to include the expected StreamPaused event in
its events array, matching the event emitted by pause_stream. Also update the
corresponding resume_stream snapshot to include StreamResumed, preserving the
existing event snapshot format and values.

Comment thread contracts/soulbound-badge/src/lib.rs Outdated
Comment on lines +683 to +684
// But eligible for next level
assert!(!client.is_eligible_for_badge(&contributor, MilestoneLevel::Silver));
assert!(!client.is_eligible_for_badge(&contributor, &MilestoneLevel::Silver));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the misleading comment in test_is_eligible_for_badge.

The comment says the contributor is eligible for the next level. The assertion correctly expects false, because the total contribution is 1_000 and SILVER_THRESHOLD is 10_000. Correct the comment so it describes the asserted behavior.

📝 Proposed comment fix
-        // But eligible for next level
+        // Not yet eligible for Silver: total is 1_000, threshold is 10_000
         assert!(!client.is_eligible_for_badge(&contributor, &MilestoneLevel::Silver));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// But eligible for next level
assert!(!client.is_eligible_for_badge(&contributor, MilestoneLevel::Silver));
assert!(!client.is_eligible_for_badge(&contributor, &MilestoneLevel::Silver));
// Not yet eligible for Silver: total is 1_000, threshold is 10_000
assert!(!client.is_eligible_for_badge(&contributor, &MilestoneLevel::Silver));
🤖 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/soulbound-badge/src/lib.rs` around lines 683 - 684, Update the
comment in test_is_eligible_for_badge above the Silver eligibility assertion to
state that the contributor is not eligible for the next level, matching the
false assertion and the 1,000 contribution versus the 10,000 SILVER_THRESHOLD.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
contracts/payment-stream/src/lib.rs (2)

529-577: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Call validate_stream_params from create_stream_internal to remove the duplicated checks.

Lines 537-548 and lines 566-577 contain the same four validations. The two copies can drift apart. create_batch_streams also runs the checks twice per entry.

Note that the up-front loop in create_batch_streams is not required for atomicity. A Soroban panic reverts the whole invocation, so create_stream_internal alone already guarantees no partial batch. Keep the loop only if you want the failure to occur before any token transfer.

♻️ Proposed refactor
     fn create_stream_internal(
         env: Env,
         sender: Address,
         recipient: Address,
         token: Address,
         total_amount: i128,
         initial_amount: i128,
         start_time: u64,
         end_time: u64,
         cliff_duration: u64,
     ) -> u64 {
         Self::assert_not_paused(&env);
 
-        // Validate inputs
-        if total_amount <= 0 {
-            panic_with_error!(&env, Error::InvalidAmount);
-        }
-        if initial_amount < 0 || initial_amount > total_amount {
-            panic_with_error!(&env, Error::InvalidAmount);
-        }
-        if end_time <= start_time {
-            panic_with_error!(&env, Error::InvalidTimeRange);
-        }
-        if cliff_duration >= end_time - start_time {
-            panic_with_error!(&env, Error::InvalidCliff);
-        }
+        Self::validate_stream_params(
+            &env,
+            total_amount,
+            initial_amount,
+            start_time,
+            end_time,
+            cliff_duration,
+        );
🤖 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/payment-stream/src/lib.rs` around lines 529 - 577, Update
create_stream_internal to call validate_stream_params with the stream arguments
after asserting the contract is not paused, then remove its duplicated amount,
time-range, and cliff validations. Update create_batch_streams to avoid
repeating the same validation unless intentionally retaining the upfront checks
for earlier failure before token transfers.

25-28: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Add a migration for existing payment-stream storage.

DataKey::StreamCount, DataKey::Paused, and DataKey::DexRouter use typed contracttype enum keys, while the persisted Stream structs now include cliff_duration, paused_at, and total_paused_duration. The repo has no upgrade/migrate path, so any deployed instance with pre-change storage cannot decode streams correctly after this version is installed. Add an upgrade migration or document this as a breaking redeployment.

🤖 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/payment-stream/src/lib.rs` around lines 25 - 28, Address storage
compatibility for existing payment-stream deployments by adding an explicit
upgrade/migration path that converts legacy DataKey and Stream storage to the
current typed keys and fields, including defaults for cliff_duration, paused_at,
and total_paused_duration; alternatively, document and enforce that this release
requires a breaking redeployment. Implement the migration in the contract’s
upgrade/deployment flow and preserve correct decoding of existing streams.
contracts/payment-stream/src/test.rs (1)

2755-2762: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

The 50-entry batch limit may not be executable on-chain.

This test must call env.budget().reset_unlimited() and env.cost_estimate().disable_resource_limits() to pass, and the comment states that a full 50-recipient batch exceeds the emulated mainnet write-entry budget.

MAX_STREAMS_PER_BATCH is therefore advertised as 50 but a 50-entry call would likely fail on mainnet with a resource error rather than BatchLimitExceeded. Callers get an opaque failure instead of a clear contract error.

Consider one of the following:

  • Lower MAX_STREAMS_PER_BATCH to a value that fits the real write-entry budget, and keep a test at that limit without disabling resource limits.
  • Keep 50 but add a test at the largest batch size that passes under default limits, so the practical ceiling is recorded and protected against regression.

Document the practical ceiling in the create_batch_streams doc comment either way.

🤖 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/payment-stream/src/test.rs` around lines 2755 - 2762, Align the
advertised MAX_STREAMS_PER_BATCH limit with the executable on-chain resource
ceiling: either lower it to the largest batch size that passes under default
limits, or retain 50 while adding a default-limits test covering the largest
practical batch size. Update test_create_batch_streams_50 and the
create_batch_streams documentation so the practical ceiling is explicitly
documented and protected without relying on unlimited budgets.
🧹 Nitpick comments (6)
contracts/soulbound-badge/src/lib.rs (3)

176-184: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Note that user_contribution is stale after this call.

Persisting before minting is correct: mint_badge re-reads UserContribution at lines 413-419 and appends to badges_minted, so it now sees the fresh total.

The consequence is that the local user_contribution no longer matches storage once check_and_mint_badges returns. It is missing the badges that mint_badge appended. The current code never writes it back, so the behavior is correct today. A future edit that persists the local here would silently drop the newly minted badges.

Add a short comment so the constraint survives.

🛡️ Proposed comment
         // Check for new milestone achievements
         let new_badges = Self::check_and_mint_badges(
             env.clone(),
             contributor.clone(),
             user_contribution.total_contributed,
             &user_contribution.badges_minted,
         );
 
+        // NOTE: `mint_badge` has now appended to the stored `badges_minted`.
+        // The local `user_contribution` is stale for that field. Do not persist
+        // it again here or the newly minted badges are lost.
         env.storage().persistent()
             .extend_ttl(&DataKey::UserContribution(contributor.clone()), LEDGER_THRESHOLD, LEDGER_BUMP);
🤖 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/soulbound-badge/src/lib.rs` around lines 176 - 184, Add a concise
comment immediately after `check_and_mint_badges` in the surrounding function
explaining that `user_contribution` is stale because `mint_badge` updates the
persisted `UserContribution`, and must not be written back or persisted
afterward.

427-434: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider aligning the event topic naming across contracts.

The snapshots show this contract emits badge_minted and contribution_recorded, while contracts/payment-stream/src/lib.rs emits StreamPaused, EmergencyPaused, and similar PascalCase topics. Both surfaces are being defined in this PR, so this is the cheapest point to pick one convention. Indexers that consume both contracts otherwise need two topic conventions.

🤖 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/soulbound-badge/src/lib.rs` around lines 427 - 434, Align the event
topic naming in the soulbound-badge contract with the PascalCase convention used
by the payment-stream contract. Update the event definitions corresponding to
BadgeMintedEvent and ContributionRecordedEvent so their emitted topics use
consistent PascalCase names, while preserving their existing payloads and
behavior.

727-747: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

This test duplicates the event assertion from test_record_contribution_bronze_badge.

Lines 736-747 assert the same ContributionRecordedEvent for the same 1_000 contribution that test_record_contribution_bronze_badge already asserts at lines 527-537. The added value here is the pre-contribution eligibility check at line 728.

The test is named test_is_eligible_for_badge but now asserts no eligibility outcome after the contribution. Consider dropping the duplicated event block and keeping this test focused on eligibility.

🤖 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/soulbound-badge/src/lib.rs` around lines 727 - 747, Update
test_is_eligible_for_badge in soulbound-badge/src/lib.rs to keep it focused on
eligibility by removing the duplicated ContributionRecordedEvent assertion block
after record_contribution. Preserve the pre-contribution is_eligible_for_badge
check as the unique purpose of this test, and rely on
test_record_contribution_bronze_badge for validating the emitted event.
contracts/payment-stream/src/test.rs (2)

1288-1293: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Assert a literal paused_duration instead of re-deriving it.

10u64.saturating_sub(paused_at) repeats the arithmetic that resume_stream performs. If that arithmetic changes, the expectation moves with it and the test still passes. The clock is 0 at pause and 10 at resume, so the value is known.

💚 Proposed test change
     let expected = StreamResumedEvent {
         stream_id,
-        resumed_at: env.ledger().timestamp(),
-        paused_duration: 10u64.saturating_sub(paused_at),
+        resumed_at: 10,
+        paused_duration: 10,
     }
     .to_xdr(&env, &contract_id);

paused_at then becomes unused; assert it instead of dropping it:

     client.pause_stream(&stream_id);
     let paused_at = env.ledger().timestamp();
+    assert_eq!(paused_at, 0);
🤖 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/payment-stream/src/test.rs` around lines 1288 - 1293, Update the
expected StreamResumedEvent in the resume test to use the known literal
paused_duration value rather than recalculating it from paused_at. Since
paused_at becomes unused, assert its expected pause-time value instead of
leaving it unused, while preserving the existing timestamp and event assertions.

738-740: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Correct the cross-reference and add coverage for DelegationRevokedEvent.

Two issues at this comment:

  1. This test covers revocation, but the comment points the reader to the DelegationGrantedEvent assertion. The relevant event here is DelegationRevokedEvent. The text appears to be copied from test_set_delegate at lines 635-637.
  2. No test asserts a DelegationRevokedEvent payload. The contract emits it from two changed sites in contracts/payment-stream/src/lib.rs: delegate replacement at lines 945-949 and revocation at lines 1013-1017. Both are uncovered.

Restructure this test so revoke_delegate is the last contract invocation, then assert the event payload.

💚 Proposed test change
     // Set delegate
     client.set_delegate(&stream_id, &delegate);
 
     // Check delegate is set
     let retrieved_delegate = client.get_delegate(&stream_id);
     assert_eq!(retrieved_delegate, Some(delegate.clone()));
 
-    // Revoke delegate
+    // Revoke delegate (last contract invocation so the event buffer is intact)
     client.revoke_delegate(&stream_id);
 
-    // Check delegate is removed
-    let retrieved_delegate = client.get_delegate(&stream_id);
-    assert_eq!(retrieved_delegate, None);
-
-    // (No event assertion here: the test ends with a get_delegate read, which
-    // clears the event buffer, so this test's snapshot holds no events. The
-    // DelegationGrantedEvent payload is asserted in test_delegate_withdraw.)
+    // Verify the DelegationRevokedEvent was emitted with the correct payload.
+    let expected = DelegationRevokedEvent {
+        stream_id,
+        recipient: recipient.clone(),
+    }
+    .to_xdr(&env, &contract_id);
+    let events = env.events().all();
+    assert!(
+        events.events().iter().any(|e| *e == expected),
+        "expected DelegationRevokedEvent to be emitted"
+    );

Add the import:

     use crate::{
-        DelegationGrantedEvent, EmergencyPausedEvent, EmergencyUnpausedEvent,
+        DelegationGrantedEvent, DelegationRevokedEvent, EmergencyPausedEvent, EmergencyUnpausedEvent,
         PaymentStreamContract, PaymentStreamContractClient, StreamPausedEvent, StreamResumedEvent,
         StreamParams, StreamStatus,
     };
🤖 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/payment-stream/src/test.rs` around lines 738 - 740, Update the
revocation test in contracts/payment-stream/src/test.rs so revoke_delegate is
the final contract invocation, preserving any required setup reads beforehand;
then import and assert the emitted DelegationRevokedEvent payload. Correct the
nearby comment to reference DelegationRevokedEvent and the appropriate test, and
add coverage for both delegate-replacement and explicit-revocation emission
paths where the existing tests cover those operations.
contracts/payment-stream/src/lib.rs (1)

430-440: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Silence the Clippy too_many_arguments warnings on these two functions.

Clippy reports 9/7 arguments at both sites. contracts/nft-stream/src/lib.rs already annotates its create_stream with #[allow(clippy::too_many_arguments)] for the same pattern. Apply the same annotation so the build stays clean under -D warnings.

🔧 Proposed fix
+    #[allow(clippy::too_many_arguments)]
     pub fn create_stream_with_cliff(
+    #[allow(clippy::too_many_arguments)]
     fn create_stream_internal(

Also applies to: 552-562

🤖 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/payment-stream/src/lib.rs` around lines 430 - 440, Add
#[allow(clippy::too_many_arguments)] immediately before both
create_stream_with_cliff and the other affected stream-creation function,
matching the existing annotation pattern in the NFT stream contract, so Clippy
warnings are suppressed under -D warnings.

Source: Linters/SAST tools

🤖 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.

Outside diff comments:
In `@contracts/payment-stream/src/lib.rs`:
- Around line 529-577: Update create_stream_internal to call
validate_stream_params with the stream arguments after asserting the contract is
not paused, then remove its duplicated amount, time-range, and cliff
validations. Update create_batch_streams to avoid repeating the same validation
unless intentionally retaining the upfront checks for earlier failure before
token transfers.
- Around line 25-28: Address storage compatibility for existing payment-stream
deployments by adding an explicit upgrade/migration path that converts legacy
DataKey and Stream storage to the current typed keys and fields, including
defaults for cliff_duration, paused_at, and total_paused_duration;
alternatively, document and enforce that this release requires a breaking
redeployment. Implement the migration in the contract’s upgrade/deployment flow
and preserve correct decoding of existing streams.

In `@contracts/payment-stream/src/test.rs`:
- Around line 2755-2762: Align the advertised MAX_STREAMS_PER_BATCH limit with
the executable on-chain resource ceiling: either lower it to the largest batch
size that passes under default limits, or retain 50 while adding a
default-limits test covering the largest practical batch size. Update
test_create_batch_streams_50 and the create_batch_streams documentation so the
practical ceiling is explicitly documented and protected without relying on
unlimited budgets.

---

Nitpick comments:
In `@contracts/payment-stream/src/lib.rs`:
- Around line 430-440: Add #[allow(clippy::too_many_arguments)] immediately
before both create_stream_with_cliff and the other affected stream-creation
function, matching the existing annotation pattern in the NFT stream contract,
so Clippy warnings are suppressed under -D warnings.

In `@contracts/payment-stream/src/test.rs`:
- Around line 1288-1293: Update the expected StreamResumedEvent in the resume
test to use the known literal paused_duration value rather than recalculating it
from paused_at. Since paused_at becomes unused, assert its expected pause-time
value instead of leaving it unused, while preserving the existing timestamp and
event assertions.
- Around line 738-740: Update the revocation test in
contracts/payment-stream/src/test.rs so revoke_delegate is the final contract
invocation, preserving any required setup reads beforehand; then import and
assert the emitted DelegationRevokedEvent payload. Correct the nearby comment to
reference DelegationRevokedEvent and the appropriate test, and add coverage for
both delegate-replacement and explicit-revocation emission paths where the
existing tests cover those operations.

In `@contracts/soulbound-badge/src/lib.rs`:
- Around line 176-184: Add a concise comment immediately after
`check_and_mint_badges` in the surrounding function explaining that
`user_contribution` is stale because `mint_badge` updates the persisted
`UserContribution`, and must not be written back or persisted afterward.
- Around line 427-434: Align the event topic naming in the soulbound-badge
contract with the PascalCase convention used by the payment-stream contract.
Update the event definitions corresponding to BadgeMintedEvent and
ContributionRecordedEvent so their emitted topics use consistent PascalCase
names, while preserving their existing payloads and behavior.
- Around line 727-747: Update test_is_eligible_for_badge in
soulbound-badge/src/lib.rs to keep it focused on eligibility by removing the
duplicated ContributionRecordedEvent assertion block after record_contribution.
Preserve the pre-contribution is_eligible_for_badge check as the unique purpose
of this test, and rely on test_record_contribution_bronze_badge for validating
the emitted event.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a65be089-1ddd-4ad3-b276-4b366f7ea790

📥 Commits

Reviewing files that changed from the base of the PR and between b0c4dbb and 0c66ee6.

📒 Files selected for processing (68)
  • contracts/payment-stream/src/lib.rs
  • contracts/payment-stream/src/test.rs
  • contracts/payment-stream/test_snapshots/test/test/test_cancel_before_cliff_refunds_full_balance.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_cancel_stream.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_cliff_blocks_withdrawal_before_cliff.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_cliff_linear_vesting_after_cliff.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_cliff_release_at_boundary.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_cliff_with_pause_and_resume.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_create_batch_streams.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_create_batch_streams_50.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_create_batch_streams_blocked_when_paused.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_create_batch_streams_empty.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_create_batch_streams_invalid_entry_leaves_no_state.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_create_batch_streams_invalid_entry_panics.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_create_batch_streams_over_limit.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_create_batch_streams_unauthorized.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_create_batch_streams_with_cliff.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_create_stream.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_create_stream_cliff_duration_zero.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_create_stream_with_cliff.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_create_stream_with_cliff_invalid.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_delegate_withdraw.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_deposit.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_deposit_after_withdrawal.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_deposit_blocked_when_paused.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_deposit_exceeds_total.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_deposit_invalid_amount.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_deposit_multiple.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_deposit_negative_amount.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_deposit_updates_last_activity.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_multiple_streams_metrics.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_multiple_withdrawals_accumulate_metrics.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_only_sender_can_pause.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_only_sender_can_resume.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_operations_resume_after_unpause.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_overwrite_delegate.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_pause_and_resume_stream.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_pause_updates_metrics.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_pausing_stops_token_vesting.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_read_operations_work_while_paused.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_recipient_can_still_withdraw_after_delegate_set.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_resume_updates_metrics.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_resuming_continues_from_where_it_left_off.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_revoke_delegate.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_revoke_delegate_updates_metrics.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_revoke_nonexistent_delegate.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_set_delegate.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_set_self_delegate.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_stream_paused_event_emitted.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_stream_resumed_event_emitted.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_withdraw.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_withdraw_after_pause_and_resume.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_withdraw_before_cliff.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_withdraw_blocked_when_paused.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_withdraw_max.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_withdraw_max_before_cliff.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_withdraw_max_blocked_when_paused.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_withdraw_max_updates_metrics.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_withdrawable_amount.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_withdrawable_amount_zero_for_paused_streams.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_withdrawal_updates_metrics.1.json
  • contracts/soulbound-badge/src/lib.rs
  • contracts/soulbound-badge/test_snapshots/test/test_is_eligible_for_badge.1.json
  • contracts/soulbound-badge/test_snapshots/test/test_multiple_users.1.json
  • contracts/soulbound-badge/test_snapshots/test/test_not_eligible_for_next_milestone.1.json
  • contracts/soulbound-badge/test_snapshots/test/test_record_contribution_bronze_badge.1.json
  • contracts/soulbound-badge/test_snapshots/test/test_record_contribution_multiple_badges.1.json
  • contracts/soulbound-badge/test_snapshots/test/test_record_contribution_progressive_badges.1.json
🚧 Files skipped from review as they are similar to previous changes (38)
  • contracts/payment-stream/test_snapshots/test/test/test_deposit_invalid_amount.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_resuming_continues_from_where_it_left_off.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_recipient_can_still_withdraw_after_delegate_set.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_set_delegate.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_pause_updates_metrics.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_create_stream.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_withdraw_blocked_when_paused.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_multiple_withdrawals_accumulate_metrics.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_deposit_blocked_when_paused.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_withdraw_after_pause_and_resume.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_withdraw_max.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_pause_and_resume_stream.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_operations_resume_after_unpause.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_withdrawable_amount_zero_for_paused_streams.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_deposit_negative_amount.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_deposit_multiple.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_pausing_stops_token_vesting.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_withdraw.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_revoke_delegate_updates_metrics.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_cancel_stream.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_delegate_withdraw.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_deposit_after_withdrawal.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_withdraw_max_updates_metrics.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_only_sender_can_pause.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_withdrawable_amount.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_deposit.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_withdrawal_updates_metrics.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_only_sender_can_resume.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_overwrite_delegate.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_read_operations_work_while_paused.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_revoke_nonexistent_delegate.1.json
  • contracts/soulbound-badge/test_snapshots/test/test_record_contribution_bronze_badge.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_resume_updates_metrics.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_withdraw_max_blocked_when_paused.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_revoke_delegate.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_deposit_updates_last_activity.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_set_self_delegate.1.json
  • contracts/payment-stream/test_snapshots/test/test/test_multiple_streams_metrics.1.json

- Remove @ts-nocheck, parsing errors, setState-in-effect violations, and
  forbidden require() imports
- Clean up unused vars/imports and no-explicit-any across source and tests
- Disable react-hooks/incompatible-library (React Compiler not enabled)
- Rebuild OfframpSummary countdown with interval pattern; type contract.deployer
- CI: configure frontend env vars from deployments/testnet.json
- Delete stale type shim files
- SDK: tighten types in clients and utils

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/sdk/src/utils/streamHistory.ts (1)

72-78: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Propagate response.cursor and use the raw event count for pagination.

getEvents returns a top-level response cursor; using the last matching event’s id makes pagination depend on filtered results instead of the RPC page token. Store response.cursor with the parsed result, and compare an exported raw RPC event count against pageLimit so unrelated stream events cannot finish the loop before more matching events are fetched.

🤖 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 `@packages/sdk/src/utils/streamHistory.ts` around lines 72 - 78, Update
getEvents to return response.cursor directly alongside parsedEvents, rather than
deriving the cursor from the last matching event. Use the exported raw RPC event
count for the pageLimit pagination condition so filtering unrelated stream
events does not prematurely stop fetching matching events.
packages/sdk/src/utils/errors.ts (1)

88-110: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Read the operation union before accessing tr, and read trapped contract errors from the failure path.

firstOp.tr() only exists for OperationResultCode.opInner(). For opBAD_AUTH, opNO_ACCOUNT, and opNOT_SUPPORTED, this code reaches an inaccessible union arm before the switch(firstOp.switch()) handler can return the operation-specific message.

InvokeHostFunctionResultCode.invokeHostFunctionSuccess() carries a Hash, not an ScVal return value. Contract parsing should target operation-level error return values or trapped invoke results, not the success arm.

Guard the tr() access with operationCode === xdr.OperationResultCode.opInner() and move the switch(operationCode) handler accordingly. Add fixtures for a trapped contract call and for non-inner operation errors.

🤖 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 `@packages/sdk/src/utils/errors.ts` around lines 88 - 110, Update the
operation-result handling around firstOp so it reads firstOp.switch() before any
tr() access, and only accesses the transaction result when the operation code is
xdr.OperationResultCode.opInner(). Move contract-error parsing to the
appropriate operation-level failure or trapped invoke-result path instead of
treating InvokeHostFunctionResultCode.invokeHostFunctionSuccess() as an
ScVal-bearing arm. Preserve operation-specific messages for non-inner codes such
as opBAD_AUTH, opNO_ACCOUNT, and opNOT_SUPPORTED, and add fixtures covering
trapped contract calls and non-inner operation errors.
🧹 Nitpick comments (1)
apps/web/src/graphql/analytics.service.ts (1)

118-118: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Keep network in the concrete data-source signature.

StreamDataSource.getStreams(network) and AnalyticsService still pass a network at Lines 220, 252, 294, and 336. The RPC sketch at Lines 128-130 also needs it to select the endpoint. Retain getStreams(network: string), or update the interface and every caller together.

🤖 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 `@apps/web/src/graphql/analytics.service.ts` at line 118, Restore the network:
string parameter on AnalyticsService.getStreams and the concrete
StreamDataSource.getStreams implementation, then propagate the network argument
through the RPC call and all existing callers so endpoint selection remains
network-specific.
🤖 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 `@apps/web/src/app/`(overview)/distribution/page.tsx:
- Line 203: Update the useBalanceValidation call that produces
distInsufficientBalance to pass the actual distribution amount and currently
selected token instead of an empty amount and undefined token, preserving the
existing validation flow so over-balance distributions are rejected.

In `@apps/web/src/app/api/webhooks/subscriptions/route.ts`:
- Around line 28-31: Stop returning raw Error.message values from the webhook
API catch handlers. In apps/web/src/app/api/webhooks/subscriptions/route.ts
lines 28-31 and 59-62, apps/web/src/app/api/webhooks/subscriptions/[id]/route.ts
lines 30-33, and apps/web/src/app/api/webhooks/trigger/route.ts lines 34-37, log
sanitized server-side diagnostics and return a generic 500 error response
instead.

In `@apps/web/src/components/offramp/OfframpQuoteModal.tsx`:
- Around line 45-57: Update the expiry timer useEffect so it resets timeLeft to
null before returning when offrampData?.expiresAt is absent. Keep the existing
countdown and interval behavior unchanged for quotes with an expiration.

In `@apps/web/src/providers/StellarWalletProvider.tsx`:
- Line 80: Update StellarWalletProvider’s persisted wallet-ID handling to read
`@fundable/web`:selected_wallet first and fall back to the legacy
stellar_wallet_id key, while continuing to write only the new key. Ensure every
cleanup path removes both storage keys, and add a regression test covering
auto-reconnect from a session stored under stellar_wallet_id.

In `@apps/web/src/services/webhook.service.ts`:
- Around line 124-125: Update the dead-letter file read error handling in the
surrounding service method so only an ENOENT error produces an empty deadLetters
array; rethrow every other read or parse failure after the existing logging,
preventing the later write from overwriting preserved entries.

In `@packages/sdk/src/PaymentStreamClient.ts`:
- Line 11: Remove the unused StreamStatus import from the import declarations in
PaymentStreamClient.ts, leaving all other imports unchanged.

In `@packages/sdk/src/utils/streamHistory.ts`:
- Around line 46-54: Update the initial request construction in getStreamHistory
so an absent cursor and omitted startLedger do not default to ledger 1; require
startLedger or resolve the node’s retained oldestLedger via getHealth before
creating the request. Preserve cursor precedence for continuation pages, ensure
getAllStreamHistory follows the same behavior, and add coverage for omitted
startLedger.

---

Outside diff comments:
In `@packages/sdk/src/utils/errors.ts`:
- Around line 88-110: Update the operation-result handling around firstOp so it
reads firstOp.switch() before any tr() access, and only accesses the transaction
result when the operation code is xdr.OperationResultCode.opInner(). Move
contract-error parsing to the appropriate operation-level failure or trapped
invoke-result path instead of treating
InvokeHostFunctionResultCode.invokeHostFunctionSuccess() as an ScVal-bearing
arm. Preserve operation-specific messages for non-inner codes such as
opBAD_AUTH, opNO_ACCOUNT, and opNOT_SUPPORTED, and add fixtures covering trapped
contract calls and non-inner operation errors.

In `@packages/sdk/src/utils/streamHistory.ts`:
- Around line 72-78: Update getEvents to return response.cursor directly
alongside parsedEvents, rather than deriving the cursor from the last matching
event. Use the exported raw RPC event count for the pageLimit pagination
condition so filtering unrelated stream events does not prematurely stop
fetching matching events.

---

Nitpick comments:
In `@apps/web/src/graphql/analytics.service.ts`:
- Line 118: Restore the network: string parameter on AnalyticsService.getStreams
and the concrete StreamDataSource.getStreams implementation, then propagate the
network argument through the RPC call and all existing callers so endpoint
selection remains network-specific.
🪄 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: 699f034e-d658-447c-abc9-3e4f2f55f45c

📥 Commits

Reviewing files that changed from the base of the PR and between 0c66ee6 and 611e58a.

📒 Files selected for processing (92)
  • .github/workflows/frontend.yml
  • apps/web/eslint.config.mjs
  • apps/web/src/app/(overview)/distribution/page.tsx
  • apps/web/src/app/(overview)/history/page.tsx
  • apps/web/src/app/(overview)/offramp/page.tsx
  • apps/web/src/app/api/certificates/route.ts
  • apps/web/src/app/api/compile/route.ts
  • apps/web/src/app/api/milestone-photos/route.ts
  • apps/web/src/app/api/streams/map/route.ts
  • apps/web/src/app/api/webhooks/subscriptions/[id]/route.ts
  • apps/web/src/app/api/webhooks/subscriptions/route.ts
  • apps/web/src/app/api/webhooks/trigger/route.ts
  • apps/web/src/app/error.test.tsx
  • apps/web/src/components/modules/history/DistributionDetailsModal.tsx
  • apps/web/src/components/modules/history/HistoryTable.tsx
  • apps/web/src/components/modules/payment-stream/CreatePaymentStream.tsx
  • apps/web/src/components/modules/payment-stream/PaymentStreamSummary.tsx
  • apps/web/src/components/modules/payment-stream/StreamActionsCell.tsx
  • apps/web/src/components/modules/payment-stream/StreamCountdown.tsx
  • apps/web/src/components/modules/payment-stream/StreamsHistory.tsx
  • apps/web/src/components/modules/payment-stream/StreamsTable.tsx
  • apps/web/src/components/modules/payment-stream/streamColumns.tsx
  • apps/web/src/components/molecules/RecipientRow.tsx
  • apps/web/src/components/offramp/BankSelector.tsx
  • apps/web/src/components/offramp/OfframpQuoteModal.tsx
  • apps/web/src/components/offramp/OfframpSuccessModal.tsx
  • apps/web/src/components/offramp/OfframpSummary.tsx
  • apps/web/src/components/organisms/DistributionForm.tsx
  • apps/web/src/components/organisms/ImpactMap.tsx
  • apps/web/src/components/organisms/OfframpSwapWidget.test.tsx
  • apps/web/src/components/organisms/RecipientTable.tsx
  • apps/web/src/components/organisms/SyncQueueBanner.test.tsx
  • apps/web/src/components/organisms/SyncQueueDrawer.tsx
  • apps/web/src/components/organisms/connect-button.tsx
  • apps/web/src/components/organisms/navbar.tsx
  • apps/web/src/components/organisms/wallet-modal.tsx
  • apps/web/src/components/token-balance/TokenBalance.tsx
  • apps/web/src/components/token-balance/TokenBalanceList.tsx
  • apps/web/src/components/ui/error-boundary.tsx
  • apps/web/src/components/ui/sidebar.tsx
  • apps/web/src/graphql/analytics.service.ts
  • apps/web/src/hooks/use-balance-validation.ts
  • apps/web/src/hooks/use-create-stream.test.ts
  • apps/web/src/hooks/use-create-stream.ts
  • apps/web/src/hooks/use-debounce-callback.ts
  • apps/web/src/hooks/use-distribute.ts
  • apps/web/src/hooks/use-distribution-state.test.ts
  • apps/web/src/hooks/use-distribution-transaction.ts
  • apps/web/src/hooks/use-mobile.d.ts
  • apps/web/src/hooks/use-mobile.js
  • apps/web/src/hooks/use-stream-delegation.ts
  • apps/web/src/hooks/use-stream-progress.ts
  • apps/web/src/hooks/use-tilt.ts
  • apps/web/src/hooks/use-withdraw.ts
  • apps/web/src/hooks/useOfframpBridge.test.ts
  • apps/web/src/hooks/useOfframpBridge.ts
  • apps/web/src/lib/__tests__/sanitize-error.test.ts
  • apps/web/src/lib/__tests__/stream-validation.test.ts
  • apps/web/src/lib/api.ts
  • apps/web/src/lib/geo-cache.test.ts
  • apps/web/src/lib/geo-cache.ts
  • apps/web/src/lib/rate-limit.test.ts
  • apps/web/src/lib/sanitize-error.ts
  • apps/web/src/lib/stellar.ts
  • apps/web/src/providers/StellarWalletProvider.test.tsx
  • apps/web/src/providers/StellarWalletProvider.tsx
  • apps/web/src/services/__tests__/webhook.service.test.ts
  • apps/web/src/services/certificate.service.test.ts
  • apps/web/src/services/certificate.service.ts
  • apps/web/src/services/contract.deployer.ts
  • apps/web/src/services/offramp.mock.ts
  • apps/web/src/services/offramp.service.ts
  • apps/web/src/services/stellar.service.retry.test.ts
  • apps/web/src/services/stellar.service.test.ts
  • apps/web/src/services/stellar.service.ts
  • apps/web/src/services/transform-balances.test.ts
  • apps/web/src/services/webhook.service.ts
  • apps/web/src/types/class-variance-authority.d.ts
  • apps/web/src/types/lucide-react.d.ts
  • apps/web/src/types/radix-slot.d.ts
  • apps/web/src/types/react-namespace.d.ts
  • apps/web/src/types/react-shim.d.ts
  • apps/web/src/types/webhook.ts
  • apps/web/src/utils/amount-validation.test.ts
  • apps/web/src/utils/csv-processing.ts
  • apps/web/src/utils/safe-storage.ts
  • apps/web/src/utils/stellar-validation.test.ts
  • packages/sdk/src/DistributorClient.ts
  • packages/sdk/src/PaymentStreamClient.ts
  • packages/sdk/src/utils/batchDistribution.ts
  • packages/sdk/src/utils/errors.ts
  • packages/sdk/src/utils/streamHistory.ts
💤 Files with no reviewable changes (22)
  • apps/web/src/components/organisms/SyncQueueDrawer.tsx
  • apps/web/src/components/organisms/DistributionForm.tsx
  • apps/web/src/hooks/use-mobile.js
  • apps/web/src/components/token-balance/TokenBalance.tsx
  • apps/web/src/components/organisms/SyncQueueBanner.test.tsx
  • apps/web/src/components/modules/payment-stream/StreamsHistory.tsx
  • apps/web/src/components/modules/payment-stream/StreamActionsCell.tsx
  • apps/web/src/types/radix-slot.d.ts
  • apps/web/src/hooks/use-mobile.d.ts
  • apps/web/src/lib/tests/stream-validation.test.ts
  • apps/web/src/types/class-variance-authority.d.ts
  • apps/web/src/hooks/use-create-stream.test.ts
  • apps/web/src/components/organisms/navbar.tsx
  • apps/web/src/types/lucide-react.d.ts
  • apps/web/src/components/organisms/RecipientTable.tsx
  • apps/web/src/types/react-shim.d.ts
  • apps/web/src/hooks/use-stream-delegation.ts
  • apps/web/src/hooks/useOfframpBridge.test.ts
  • apps/web/src/components/modules/history/DistributionDetailsModal.tsx
  • apps/web/src/components/modules/payment-stream/StreamCountdown.tsx
  • apps/web/src/components/modules/payment-stream/streamColumns.tsx
  • apps/web/src/types/react-namespace.d.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/src/components/modules/payment-stream/PaymentStreamSummary.tsx

};

const { distInsufficientBalance } = useBalanceValidation();
const { insufficientBalance: distInsufficientBalance } = useBalanceValidation("", undefined);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Pass the actual distribution amount and selected token.

Line 203 always passes an empty amount. useBalanceValidation returns no error when amount is empty. Therefore, distInsufficientBalance is always false and cannot prevent an over-balance distribution.

🤖 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 `@apps/web/src/app/`(overview)/distribution/page.tsx at line 203, Update the
useBalanceValidation call that produces distInsufficientBalance to pass the
actual distribution amount and currently selected token instead of an empty
amount and undefined token, preserving the existing validation flow so
over-balance distributions are rejected.

Comment on lines +28 to +31
} catch (err: unknown) {
const message = err instanceof Error ? err.message : "Unknown error";
return NextResponse.json(
{ error: err.message || "Failed to retrieve subscriptions" },
{ error: message || "Failed to retrieve subscriptions" },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not expose raw internal error messages from webhook API routes.

These public handlers return Error.message directly. WebhookService can produce filesystem and implementation details. Return a generic 500 response and log sanitized diagnostics server-side.

  • apps/web/src/app/api/webhooks/subscriptions/route.ts#L28-L31: Return a generic error for GET failures.
  • apps/web/src/app/api/webhooks/subscriptions/route.ts#L59-L62: Return a generic error for POST failures.
  • apps/web/src/app/api/webhooks/subscriptions/[id]/route.ts#L30-L33: Return a generic error for DELETE failures.
  • apps/web/src/app/api/webhooks/trigger/route.ts#L34-L37: Return a generic error for trigger failures.
📍 Affects 3 files
  • apps/web/src/app/api/webhooks/subscriptions/route.ts#L28-L31 (this comment)
  • apps/web/src/app/api/webhooks/subscriptions/route.ts#L59-L62
  • apps/web/src/app/api/webhooks/subscriptions/[id]/route.ts#L30-L33
  • apps/web/src/app/api/webhooks/trigger/route.ts#L34-L37
🤖 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 `@apps/web/src/app/api/webhooks/subscriptions/route.ts` around lines 28 - 31,
Stop returning raw Error.message values from the webhook API catch handlers. In
apps/web/src/app/api/webhooks/subscriptions/route.ts lines 28-31 and 59-62,
apps/web/src/app/api/webhooks/subscriptions/[id]/route.ts lines 30-33, and
apps/web/src/app/api/webhooks/trigger/route.ts lines 34-37, log sanitized
server-side diagnostics and return a generic 500 error response instead.

Comment on lines 45 to 57
useEffect(() => {
if (!offrampData?.expiresAt) {
setTimeLeft(null);
setIsExpired(false);
return;
}
if (!offrampData?.expiresAt) return;

const expiresAt = new Date(offrampData.expiresAt).getTime();
const tick = () => {
const remaining = Math.floor(
(new Date(offrampData.expiresAt!).getTime() - Date.now()) / 1000,
);
if (remaining <= 0) {
setTimeLeft(0);
setIsExpired(true);
return;
}
setTimeLeft(remaining);
setIsExpired(false);
const remaining = Math.floor((expiresAt - Date.now()) / 1000);
setTimeLeft(Math.max(0, remaining));
};

tick();
const intervalId = setInterval(tick, 1000);
return () => clearInterval(intervalId);
}, [offrampData?.expiresAt]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Clear stale expiry state when the quote has no expiration.

If a prior quote expired, timeLeft remains 0 when the next offrampData has no expiresAt. The modal then shows “Quote Expired” for a quote that has no expiry. Reset timeLeft to null before the early return.

Proposed fix
 useEffect(() => {
+    setTimeLeft(null);
     if (!offrampData?.expiresAt) return;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (!offrampData?.expiresAt) {
setTimeLeft(null);
setIsExpired(false);
return;
}
if (!offrampData?.expiresAt) return;
const expiresAt = new Date(offrampData.expiresAt).getTime();
const tick = () => {
const remaining = Math.floor(
(new Date(offrampData.expiresAt!).getTime() - Date.now()) / 1000,
);
if (remaining <= 0) {
setTimeLeft(0);
setIsExpired(true);
return;
}
setTimeLeft(remaining);
setIsExpired(false);
const remaining = Math.floor((expiresAt - Date.now()) / 1000);
setTimeLeft(Math.max(0, remaining));
};
tick();
const intervalId = setInterval(tick, 1000);
return () => clearInterval(intervalId);
}, [offrampData?.expiresAt]);
useEffect(() => {
setTimeLeft(null);
if (!offrampData?.expiresAt) return;
const expiresAt = new Date(offrampData.expiresAt).getTime();
const tick = () => {
const remaining = Math.floor((expiresAt - Date.now()) / 1000);
setTimeLeft(Math.max(0, remaining));
};
tick();
const intervalId = setInterval(tick, 1000);
return () => clearInterval(intervalId);
}, [offrampData?.expiresAt]);
🤖 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 `@apps/web/src/components/offramp/OfframpQuoteModal.tsx` around lines 45 - 57,
Update the expiry timer useEffect so it resets timeLeft to null before returning
when offrampData?.expiresAt is absent. Keep the existing countdown and interval
behavior unchanged for quotes with an expiration.

}
const savedAddress = safeGetItem("stellar_wallet_address");
const savedWalletId = safeGetItem("stellar_wallet_id");
const savedWalletId = safeGetItem("@fundable/web:selected_wallet");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Migrate the previous wallet-ID storage key.

The provider now reads only @fundable/web:selected_wallet, while previous sessions used stellar_wallet_id. Existing users will fail the persisted-session check and lose auto-reconnect after upgrade. Read the old key as a fallback, write the new key, and remove both keys in every cleanup path. Add a regression test for an old-key session.

Also applies to: 184-184

🤖 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 `@apps/web/src/providers/StellarWalletProvider.tsx` at line 80, Update
StellarWalletProvider’s persisted wallet-ID handling to read
`@fundable/web`:selected_wallet first and fall back to the legacy
stellar_wallet_id key, while continuing to write only the new key. Ensure every
cleanup path removes both storage keys, and add a regression test covering
auto-reconnect from a session stored under stellar_wallet_id.

Comment on lines +124 to +125
} catch (err: unknown) {
if ((err as { code?: string }).code !== 'ENOENT') {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve existing dead-letter entries on read failures.

The catch logs every non-ENOENT error but then continues with an empty deadLetters array. A malformed file or transient read failure can overwrite all previous entries at Line 131. Treat only ENOENT as an empty log and rethrow every other error.

Proposed fix
       } catch (err: unknown) {
-        if ((err as { code?: string }).code !== 'ENOENT') {
-          console.error('Failed to read dead-letter log:', err);
+        const code =
+          typeof err === 'object' && err !== null && 'code' in err
+            ? (err as { code?: unknown }).code
+            : undefined;
+        if (code !== 'ENOENT') {
+          throw err;
         }
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (err: unknown) {
if ((err as { code?: string }).code !== 'ENOENT') {
} catch (err: unknown) {
const code =
typeof err === 'object' && err !== null && 'code' in err
? (err as { code?: unknown }).code
: undefined;
if (code !== 'ENOENT') {
throw err;
}
}
🤖 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 `@apps/web/src/services/webhook.service.ts` around lines 124 - 125, Update the
dead-letter file read error handling in the surrounding service method so only
an ENOENT error produces an empty deadLetters array; rethrow every other read or
parse failure after the existing logging, preventing the later write from
overwriting preserved entries.

} from "@stellar/stellar-sdk/contract";
import { Address } from "@stellar/stellar-sdk";
import {
Stream,
StreamMetrics,
ProtocolMetrics,
StreamStatus,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the unused StreamStatus import.

The lint check reports StreamStatus as unused at Line 11. Remove it before merge.

Proposed fix
-  StreamStatus,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
StreamStatus,
🧰 Tools
🪛 GitHub Actions: SDK CI / 1_lint-and-format.txt

[warning] 11-11: ESLint: 'StreamStatus' is defined but never used. (@typescript-eslint/no-unused-vars)

🪛 GitHub Actions: SDK CI / lint-and-format

[warning] 11-11: ESLint: 'StreamStatus' is defined but never used (@typescript-eslint/no-unused-vars). Command: pnpm --filter @fundable/sdk lint

🪛 GitHub Check: lint-and-format

[warning] 11-11:
'StreamStatus' is defined but never used

🤖 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 `@packages/sdk/src/PaymentStreamClient.ts` at line 11, Remove the unused
StreamStatus import from the import declarations in PaymentStreamClient.ts,
leaving all other imports unchanged.

Source: Linters/SAST tools

Comment on lines +46 to +54
const filter: StellarSdk.rpc.Api.EventFilter = {
type: "contract",
contractIds: [contractId],
};

// cursor takes precedence over startLedger for continuation pages
const requestParams: StellarSdk.rpc.GetEventsRequest = cursor
const requestParams: StellarSdk.rpc.Api.GetEventsRequest = cursor
? { filters: [filter], cursor, limit }
: { filters: [filter], startLedger, limit };
: { filters: [filter], startLedger: startLedger ?? 1, limit };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 5 \
  'getStreamHistory|getAllStreamHistory|startLedger|cursor' \
  packages/sdk apps/web \
  --glob '*.ts' --glob '*.tsx' || true

Repository: Fundable-Protocol/stellar_client_os

Length of output: 50392


🌐 Web query:

Stellar RPC getEvents startLedger rejected if startLedger is outside retained ledger range documentation

💡 Result:

In Stellar RPC, the getEvents method requires that the startLedger parameter falls within the node's currently retained ledger range [1][2]. If startLedger is outside this range—meaning it is either older than the oldest stored ledger or newer than the latest ledger known to that specific RPC node—the request will be rejected with an error [1][2]. Key details regarding this behavior include: Ledger Retention: Stellar RPC instances maintain a bounded history of events and transactions, defined by the HISTORY_RETENTION_WINDOW configuration (defaulting to 120,960 ledgers, or approximately 7 days) [1][3]. Error Handling: When a request is rejected due to an out-of-bounds startLedger, the server typically returns a JSON-RPC error code -32600 [4][5][6]. The error message explicitly states that the startLedger must be within the current ledger range (e.g., "startLedger must be within the ledger range: [oldest] - [latest]") [4][6]. Validation: Developers are advised to use the getHealth RPC method to inspect the node's current oldestLedger and latestLedger values before submitting a getEvents request, ensuring the startLedger is within the valid bounds [1][5]. Transient vs. Permanent Errors: An error indicating that startLedger is "newer than the latest ledger" may be a transient condition caused by synchronization lag between the network head and the specific RPC node being queried; the ledger may become available shortly thereafter [6]. In contrast, an error indicating that startLedger is "older than the oldest ledger" typically implies the data has been pruned from the node's history [1][7].

Citations:


Do not default the first RPC page to ledger 1.

When cursor is absent and startLedger is omitted, this sends startLedger: 1. Stellar RPC rejects getEvents requests whose ledger is outside the node’s retained range. If a node’s oldestLedger is greater than 1, the first call fails for both getStreamHistory() and getAllStreamHistory() calls. Require startLedger, or fetch getHealth oldestLedger before creating the first request. Add a test for omitted startLedger.

🤖 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 `@packages/sdk/src/utils/streamHistory.ts` around lines 46 - 54, Update the
initial request construction in getStreamHistory so an absent cursor and omitted
startLedger do not default to ledger 1; require startLedger or resolve the
node’s retained oldestLedger via getHealth before creating the request. Preserve
cursor precedence for continuation pages, ensure getAllStreamHistory follows the
same behavior, and add coverage for omitted startLedger.

@Idrhas
Idrhas merged commit 7a6f8f0 into Fundable-Protocol:main Aug 7, 2026
0 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

web(PaymentStreamSummary): resolve contract addresses to friendly ticker symbols

2 participants