Complete the trustline lifecycle: sponsorship state and pre-signature guards (#209) - #214
Conversation
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 — Review Status
|
Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
| // 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", |
There was a problem hiding this comment.
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
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| if (baseReserveFromLedger && now - lastBaseReserveFetch < 60_000) { | ||
| return cachedBaseReserveStroops; | ||
| } |
There was a problem hiding this comment.
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
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| if (state.spendableStroops < reserveStroops) { | ||
| throw new InsufficientReserveError(reserveStroops, state.spendableStroops); |
There was a problem hiding this comment.
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
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 Nitpicks2 code suggestions1. Starting a new attempt clears the error flag but not
|
User description
close #209
Detection, the reserve math and the add flow already existed. Four gaps against the stated invariants remained.
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, plusisBlockingNeed()- 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.
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.
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.
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
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:
This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.
Example
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:
This helps CodeAnt AI learn and adapt to your team's coding style and standards.
Example
Retrigger review
Ask CodeAnt AI to review the PR again, by typing:
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.