Skip to content

feat(samples/kotlin): add exchange-rates lookup flow#602

Merged
pengying merged 1 commit into
mainfrom
06-18-feat_samples_kotlin_add_exchange-rates_lookup_flow
Jun 26, 2026
Merged

feat(samples/kotlin): add exchange-rates lookup flow#602
pengying merged 1 commit into
mainfrom
06-18-feat_samples_kotlin_add_exchange-rates_lookup_flow

Conversation

@pengying

Copy link
Copy Markdown
Contributor

Add an "Exchange Rates" flow that looks up the cached FX rate, fees, and
receiving amount for a corridor. Backend: GET /api/exchange-rates proxies
to client.exchangeRates().list(). Frontend: a GetExchangeRate form
(source/destination currency + sending amount) and ExchangeRatesFlow,
wired into the sidebar.

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com

@vercel

vercel Bot commented Jun 18, 2026

Copy link
Copy Markdown

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

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
grid-flow-builder Ignored Ignored Preview Jun 26, 2026 12:21am
grid-wallet-demo Ignored Ignored Preview Jun 26, 2026 12:21am

Request Review

@pengying pengying force-pushed the 06-18-feat_samples_kotlin_add_exchange-rates_lookup_flow branch from 2f6e65c to f81b79b Compare June 18, 2026 18:19
@greptile-apps

greptile-apps Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds an Exchange Rates lookup flow: a Kotlin backend route (GET /api/exchange-rates) that proxies to client.exchangeRates().list(), and a React frontend with a GetExchangeRate form and ExchangeRatesFlow wrapper wired into the sidebar and app router.

  • Backend (ExchangeRates.kt): validates sourceCurrency as required, treats destinationCurrency and sendingAmount as optional, and follows the same try/catch + JsonUtils.prettyPrint pattern as other routes.
  • Frontend (GetExchangeRate.tsx): renders two currency selects and a sending-amount number input, then calls GET /api/exchange-rates and displays the result in ResponsePanel.
  • Wiring (App.tsx, Sidebar.tsx): adds the 'exchange-rates' FlowKey, registers the flow entry in the sidebar, and renders ExchangeRatesFlow in the main content area.

Confidence Score: 4/5

Safe to merge with one small fix: the sending-amount input should restrict to integers to match the backend's long parser.

The Kotlin route and React wiring are clean and follow existing patterns. The one active defect is in GetExchangeRate.tsx: the number input lacks step="1", so a user who enters a decimal amount (e.g. 100.50) will have it silently discarded by the backend's toLongOrNull() call — the API call proceeds but the amount the user typed is not applied, with no error shown.

samples/frontend/src/steps/GetExchangeRate.tsx — the sending-amount number input.

Important Files Changed

Filename Overview
samples/frontend/src/steps/GetExchangeRate.tsx New form step with currency selects and a sending amount field. Missing step="1" on the number input means decimal values are silently dropped by the backend's toLongOrNull() without any user feedback.
samples/kotlin/src/main/kotlin/com/grid/sample/routes/ExchangeRates.kt New GET /api/exchange-rates route that proxies to client.exchangeRates().list(). Follows existing route patterns; the unescaped e.message interpolation in the error response (line 45) was flagged in a prior review thread.
samples/frontend/src/flows/ExchangeRatesFlow.tsx Thin flow wrapper rendering GetExchangeRate inside a styled card. No issues.
samples/frontend/src/App.tsx Wires ExchangeRatesFlow into the app and changes the default active flow to 'exchange-rates'. Default-flow change was flagged in a prior review thread.
samples/frontend/src/components/Sidebar.tsx Adds 'exchange-rates' to the FlowKey union and inserts the sidebar entry. Clean change, no issues.
samples/kotlin/src/main/kotlin/com/grid/sample/Application.kt Registers the new exchangeRateRoutes() in the correct position. No issues.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant GetExchangeRate as GetExchangeRate.tsx
    participant Backend as Kotlin /api/exchange-rates
    participant GridAPI as Grid API (exchangeRates().list())

    User->>GetExchangeRate: "Select currencies & enter amount, click "Get Exchange Rate""
    GetExchangeRate->>Backend: "GET /api/exchange-rates?sourceCurrency=&destinationCurrency=&sendingAmount="
    Backend->>Backend: Validate sourceCurrency (400 if missing)
    Backend->>GridAPI: ExchangeRateListParams.builder().build()
    GridAPI-->>Backend: Exchange rate list response
    Backend->>Backend: JsonUtils.prettyPrint(response)
    Backend-->>GetExchangeRate: 200 OK (JSON)
    GetExchangeRate-->>User: Display in ResponsePanel
    note over Backend,GetExchangeRate: On exception: {"error": "${e.message}"} — 500
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User
    participant GetExchangeRate as GetExchangeRate.tsx
    participant Backend as Kotlin /api/exchange-rates
    participant GridAPI as Grid API (exchangeRates().list())

    User->>GetExchangeRate: "Select currencies & enter amount, click "Get Exchange Rate""
    GetExchangeRate->>Backend: "GET /api/exchange-rates?sourceCurrency=&destinationCurrency=&sendingAmount="
    Backend->>Backend: Validate sourceCurrency (400 if missing)
    Backend->>GridAPI: ExchangeRateListParams.builder().build()
    GridAPI-->>Backend: Exchange rate list response
    Backend->>Backend: JsonUtils.prettyPrint(response)
    Backend-->>GetExchangeRate: 200 OK (JSON)
    GetExchangeRate-->>User: Display in ResponsePanel
    note over Backend,GetExchangeRate: On exception: {"error": "${e.message}"} — 500
Loading
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
samples/frontend/src/steps/GetExchangeRate.tsx:57-59
Decimal `sendingAmount` is silently dropped by the backend. The Kotlin route parses the query parameter with `toLongOrNull()`, which returns `null` for any non-integer value (e.g. `"100.50"``null`). Because the `<input type="number">` has no `step` attribute, browsers treat any numeric value as valid, so a user who types `100.50` will see the form submit but their amount is quietly ignored — the API call proceeds without a `sendingAmount` and returns a result the user may misread as corresponding to their input.

```suggestion
          type="number"
          min="0"
          step="1"
          value={sendingAmount}
```

Reviews (2): Last reviewed commit: "feat(samples/kotlin): add exchange-rates..." | Re-trigger Greptile

Comment on lines +44 to +48
call.respondText(
"""{"error": "${e.message}"}""",
ContentType.Application.Json,
HttpStatusCode.InternalServerError
)

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.

P1 Unescaped e.message produces malformed JSON

If the exception message contains a " character (e.g. Invalid currency "XYZ") or a newline, the interpolated string is not valid JSON. The frontend's JSON.parse call will throw, masking the real error with a confusing parse failure instead. Using JsonUtils.mapper.writeValueAsString(e.message) would produce a properly escaped JSON string value.

Suggested change
call.respondText(
"""{"error": "${e.message}"}""",
ContentType.Application.Json,
HttpStatusCode.InternalServerError
)
call.respondText(
"""{"error": ${JsonUtils.mapper.writeValueAsString(e.message)}}""",
ContentType.Application.Json,
HttpStatusCode.InternalServerError
)
Prompt To Fix With AI
This is a comment left during a code review.
Path: samples/kotlin/src/main/kotlin/com/grid/sample/routes/ExchangeRates.kt
Line: 44-48

Comment:
**Unescaped `e.message` produces malformed JSON**

If the exception message contains a `"` character (e.g. `Invalid currency "XYZ"`) or a newline, the interpolated string is not valid JSON. The frontend's `JSON.parse` call will throw, masking the real error with a confusing parse failure instead. Using `JsonUtils.mapper.writeValueAsString(e.message)` would produce a properly escaped JSON string value.

```suggestion
                call.respondText(
                    """{"error": ${JsonUtils.mapper.writeValueAsString(e.message)}}""",
                    ContentType.Application.Json,
                    HttpStatusCode.InternalServerError
                )
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread samples/frontend/src/App.tsx
Comment on lines +39 to +52
<div className="grid grid-cols-2 gap-3 mb-3">
<div>
<label htmlFor="src-currency" className="block text-sm font-medium text-gray-300 mb-1">Source Currency</label>
<select id="src-currency" value={sourceCurrency} onChange={(e) => setSourceCurrency(e.target.value)} disabled={loading} className={selectClass}>
{CURRENCIES.map((c) => <option key={c} value={c}>{c}</option>)}
</select>
</div>
<div>
<label htmlFor="dst-currency" className="block text-sm font-medium text-gray-300 mb-1">Destination Currency</label>
<select id="dst-currency" value={destinationCurrency} onChange={(e) => setDestinationCurrency(e.target.value)} disabled={loading} className={selectClass}>
{CURRENCIES.map((c) => <option key={c} value={c}>{c}</option>)}
</select>
</div>
</div>

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.

P2 No guard against selecting identical source and destination currencies

The two selects share the same CURRENCIES list with no cross-validation, so a user can choose USD → USD. Depending on the API behaviour, this either returns a trivial result or surfaces an API error only in the response panel.

Prompt To Fix With AI
This is a comment left during a code review.
Path: samples/frontend/src/steps/GetExchangeRate.tsx
Line: 39-52

Comment:
**No guard against selecting identical source and destination currencies**

The two selects share the same `CURRENCIES` list with no cross-validation, so a user can choose `USD → USD`. Depending on the API behaviour, this either returns a trivial result or surfaces an API error only in the response panel.

How can I resolve this? If you propose a fix, please make it concise.

shreyav
shreyav previously approved these changes Jun 26, 2026

pengying commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

Merge activity

  • Jun 26, 12:16 AM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Jun 26, 12:22 AM UTC: Graphite rebased this pull request as part of a merge.
  • Jun 26, 12:22 AM UTC: @pengying merged this pull request with Graphite.

@pengying pengying changed the base branch from 06-16-feat_samples_kotlin_add_usd_usdc_wallet_payout_flow to graphite-base/602 June 26, 2026 00:19
@pengying pengying changed the base branch from graphite-base/602 to main June 26, 2026 00:20
@pengying pengying dismissed shreyav’s stale review June 26, 2026 00:20

The base branch was changed.

Add an "Exchange Rates" flow that looks up the cached FX rate, fees, and
receiving amount for a corridor. Backend: GET /api/exchange-rates proxies
to client.exchangeRates().list(). Frontend: a GetExchangeRate form
(source/destination currency + sending amount) and ExchangeRatesFlow,
wired into the sidebar.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@pengying pengying force-pushed the 06-18-feat_samples_kotlin_add_exchange-rates_lookup_flow branch from f81b79b to 8e425ad Compare June 26, 2026 00:21

@restamp-bot restamp-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

8e425ad is a pure rebase onto 704123d. Approving based on @shreyav's previous approval of f81b79b.

@pengying pengying merged commit af8d404 into main Jun 26, 2026
10 checks passed
@pengying pengying deleted the 06-18-feat_samples_kotlin_add_exchange-rates_lookup_flow branch June 26, 2026 00:22
Comment on lines +57 to +59
type="number"
min="0"
value={sendingAmount}

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.

P1 Decimal sendingAmount is silently dropped by the backend. The Kotlin route parses the query parameter with toLongOrNull(), which returns null for any non-integer value (e.g. "100.50"null). Because the <input type="number"> has no step attribute, browsers treat any numeric value as valid, so a user who types 100.50 will see the form submit but their amount is quietly ignored — the API call proceeds without a sendingAmount and returns a result the user may misread as corresponding to their input.

Suggested change
type="number"
min="0"
value={sendingAmount}
type="number"
min="0"
step="1"
value={sendingAmount}
Prompt To Fix With AI
This is a comment left during a code review.
Path: samples/frontend/src/steps/GetExchangeRate.tsx
Line: 57-59

Comment:
Decimal `sendingAmount` is silently dropped by the backend. The Kotlin route parses the query parameter with `toLongOrNull()`, which returns `null` for any non-integer value (e.g. `"100.50"``null`). Because the `<input type="number">` has no `step` attribute, browsers treat any numeric value as valid, so a user who types `100.50` will see the form submit but their amount is quietly ignored — the API call proceeds without a `sendingAmount` and returns a result the user may misread as corresponding to their input.

```suggestion
          type="number"
          min="0"
          step="1"
          value={sendingAmount}
```

How can I resolve this? If you propose a fix, please make it concise.

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.

2 participants