Skip to content

Complete the trustline lifecycle: sponsorship state and pre-signature guards (#209) - #214

Merged
Soumen1080 merged 1 commit into
mainfrom
souri
Aug 31, 2026
Merged

Complete the trustline lifecycle: sponsorship state and pre-signature guards (#209)#214
Soumen1080 merged 1 commit into
mainfrom
souri

Conversation

@Soumen1080

@Soumen1080 Soumen1080 commented Aug 31, 2026

Copy link
Copy Markdown
Owner

User description

close #209

Detection, the reserve math and the add flow already existed. Four gaps against the stated invariants remained.

  1. The sponsored state was not distinguishable describeOnboardingNeed returned "none" for a sponsored trustline, so the state the invariants name explicitly was invisible. Added trustline_sponsored, reported after the blocking states so it never masks one, plus isBlockingNeed() - it is informational, not a blocker, since the user can receive today. TrustlinePrompt now explains whose XLM is locked and what happens if the sponsor revokes.

    useAccountOnboarding treated every non-"none" need as unfinished, which would have stalled a sponsored user on a screen they cannot act on; it now uses isBlockingNeed.

  2. Affordability was checked only in the UI buildChangeTrustTransaction documented that it did not check and that the network would reject the result - i.e. after the user signs, which is exactly what the invariant forbids. It now reads live account state and throws InsufficientReserveError (naming the shortfall in XLM) before producing an XDR, so the guard holds for every caller rather than for whoever remembered. skipAffordabilityCheck opts out for callers that already checked.

  3. Native XLM could reach the trustline path changeTrust on the native asset is invalid and every account holds XLM intrinsically. Now refused with an explanation.

  4. A failed base-reserve read pinned the hardcoded fallback for 60s The cache timestamp advanced only on success, but the cache was consulted regardless, so one Horizon blip meant a minute of guessed reserve figures presented as fact. A failed read no longer refreshes the window, and baseReserveIsLive() reports whether the number came from the ledger.

buildChangeTrustTransaction returns reserveStroops so the amount disclosed before signing is the one the transaction actually locks. The unset limit (SDK max int64) is now documented as deliberate.

Tests cover each state including sponsored, insufficient-reserve, auth-required, auth-to-maintain-liabilities, at-limit with buying liabilities, a non-default network base reserve, and fallback recovery.

Note: not executed here - this checkout has no installed node_modules and no dependencies were added.

@


CodeAnt-AI Description

Guard trustline setup before signing and explain sponsored reserves

What Changed

  • Trustline creation now checks live account funds before opening the wallet for signing and reports the XLM shortfall when the reserve is unaffordable
  • Native XLM is rejected because it does not require a trustline
  • Sponsored trustlines are identified as usable, non-blocking states, with the sponsor and reserve responsibility shown to users
  • Reserve requirements use the network’s current ledger value and retry after failed reads instead of relying on a stale fallback
  • The reserve locked by the transaction is returned so it can be disclosed before signing
  • Added coverage for reserve calculations, sponsorship, liabilities, onboarding states, and pre-signature failures

Impact

✅ Fewer doomed wallet signature requests
✅ Clearer XLM reserve shortfall messages
✅ Sponsored trustlines no longer block receiving payments

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

Complete the trustline lifecycle: sponsorship state and pre-signature guards (#209)

Detection, the reserve math and the add flow already existed. Four gaps against
the stated invariants remained.

1. The sponsored state was not distinguishable
   describeOnboardingNeed returned "none" for a sponsored trustline, so the
   state the invariants name explicitly was invisible. Added
   `trustline_sponsored`, reported after the blocking states so it never masks
   one, plus `isBlockingNeed()` - it is informational, not a blocker, since the
   user can receive today. TrustlinePrompt now explains whose XLM is locked and
   what happens if the sponsor revokes.

   useAccountOnboarding treated every non-"none" need as unfinished, which would
   have stalled a sponsored user on a screen they cannot act on; it now uses
   isBlockingNeed.

2. Affordability was checked only in the UI
   buildChangeTrustTransaction documented that it did not check and that the
   network would reject the result - i.e. after the user signs, which is exactly
   what the invariant forbids. It now reads live account state and throws
   InsufficientReserveError (naming the shortfall in XLM) before producing an
   XDR, so the guard holds for every caller rather than for whoever remembered.
   skipAffordabilityCheck opts out for callers that already checked.

3. Native XLM could reach the trustline path
   changeTrust on the native asset is invalid and every account holds XLM
   intrinsically. Now refused with an explanation.

4. A failed base-reserve read pinned the hardcoded fallback for 60s
   The cache timestamp advanced only on success, but the cache was consulted
   regardless, so one Horizon blip meant a minute of guessed reserve figures
   presented as fact. A failed read no longer refreshes the window, and
   baseReserveIsLive() reports whether the number came from the ledger.

buildChangeTrustTransaction returns reserveStroops so the amount disclosed
before signing is the one the transaction actually locks. The unset limit
(SDK max int64) is now documented as deliberate.

Tests cover each state including sponsored, insufficient-reserve, auth-required,
auth-to-maintain-liabilities, at-limit with buying liabilities, a non-default
network base reserve, and fallback recovery.

Note: not executed here - this checkout has no installed node_modules and no
dependencies were added.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@
@codeant-ai

codeant-ai Bot commented Aug 31, 2026

Copy link
Copy Markdown

🤖 CodeAnt AI — Review Status

Status Commit Started (UTC) Finished (UTC)
✅ Reviewed your PR afeebf2 Aug 31, 2026 · 11:19 11:23

@codeant-ai

codeant-ai Bot commented Aug 31, 2026

Copy link
Copy Markdown

Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@vercel

vercel Bot commented Aug 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
stellar-star Error Error Aug 31, 2026 11:19am

@Soumen1080
Soumen1080 merged commit fc48731 into main Aug 31, 2026
3 of 5 checks passed
@codeant-ai codeant-ai Bot added the size:XL This PR changes 500-999 lines, ignoring generated files label Aug 31, 2026
// A sponsored trustline is informational, not a blocker: the user can
// already receive the asset, so the flow is done even though `need`
// is not "none".
phase: isBlockingNeed(need) ? "idle" : "done",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: A sponsored trustline sets phase to done, so AccountSetupPrompt immediately calls onComplete and replaces the sponsorship disclosure with its generic ready state. [logic error]

Assessment: 🟠 Major · 🔁 Occurrence: Sometimes

Use CodeAnt Skill Fix in Cursor Fix in VSCode Claude

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** hooks/useAccountOnboarding.ts
**Line:** 86:86
**Comment:**
	*Logic Error: A sponsored trustline sets `phase` to `done`, so `AccountSetupPrompt` immediately calls `onComplete` and replaces the sponsorship disclosure with its generic ready state.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +65 to +67
if (baseReserveFromLedger && now - lastBaseReserveFetch < 60_000) {
return cachedBaseReserveStroops;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The reserve cache is shared across all Horizon URLs, so a recent read from one network can supply reserve values for another network for 60 seconds. [cache]

Assessment: 🟠 Major · 🔁 Occurrence: Sometimes

Use CodeAnt Skill Fix in Cursor Fix in VSCode Claude

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** lib/stellar/accountState.ts
**Line:** 65:67
**Comment:**
	*Cache: The reserve cache is shared across all Horizon URLs, so a recent read from one network can supply reserve values for another network for 60 seconds.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment thread lib/stellar/trustline.ts
Comment on lines +94 to +95
if (state.spendableStroops < reserveStroops) {
throw new InsufficientReserveError(reserveStroops, state.spendableStroops);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The affordability check ignores the transaction fee, so an account with exactly the required reserve passes but the signed transaction can still fail as underfunded. [logic error]

Assessment: 🟠 Major · 🔁 Occurrence: Sometimes

Use CodeAnt Skill Fix in Cursor Fix in VSCode Claude

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** lib/stellar/trustline.ts
**Line:** 94:95
**Comment:**
	*Logic Error: The affordability check ignores the transaction fee, so an account with exactly the required reserve passes but the signed transaction can still fail as underfunded.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

@codeant-ai

codeant-ai Bot commented Aug 31, 2026

Copy link
Copy Markdown

CodeAnt Nitpicks

2 code suggestions

1. Starting a new attempt clears the error flag but not reserveStroops, so callers can display the previous transaction's reserve while the current attempt is preparing or fails.

Stale reference · hooks/useTrustline.ts:35


2. skipAffordabilityCheck is checked only after getAccountState, so callers using the documented escape hatch still incur the account and ledger requests it promises to skip.

Api mismatch · lib/stellar/trustline.ts:84-87

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL This PR changes 500-999 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make the trustline lifecycle correct, including the reserve math

1 participant