-
Notifications
You must be signed in to change notification settings - Fork 7
feat(samples/kotlin): add exchange-rates lookup flow #602
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 06-16-feat_samples_kotlin_add_usd_usdc_wallet_payout_flow
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import GetExchangeRate from '../steps/GetExchangeRate' | ||
|
|
||
| export default function ExchangeRatesFlow() { | ||
| return ( | ||
| <div className="rounded-lg border border-blue-600 bg-gray-900 p-4"> | ||
| <GetExchangeRate /> | ||
| </div> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| import { useState } from 'react' | ||
| import ResponsePanel from '../components/ResponsePanel' | ||
| import { apiGet } from '../lib/api' | ||
|
|
||
| const CURRENCIES = ['USD', 'USDC', 'MXN', 'BRL', 'INR', 'EUR', 'GBP', 'PHP', 'CAD'] | ||
|
|
||
| export default function GetExchangeRate() { | ||
| const [sourceCurrency, setSourceCurrency] = useState('USD') | ||
| const [destinationCurrency, setDestinationCurrency] = useState('USDC') | ||
| const [sendingAmount, setSendingAmount] = useState('100000') | ||
| const [response, setResponse] = useState<string | null>(null) | ||
| const [error, setError] = useState<string | null>(null) | ||
| const [loading, setLoading] = useState(false) | ||
|
|
||
| const submit = async () => { | ||
| setLoading(true) | ||
| setError(null) | ||
| setResponse(null) | ||
| try { | ||
| const params = new URLSearchParams({ sourceCurrency, destinationCurrency }) | ||
| if (sendingAmount) params.set('sendingAmount', sendingAmount) | ||
| const data = await apiGet<Record<string, unknown>>(`/api/exchange-rates?${params.toString()}`) | ||
| setResponse(JSON.stringify(data, null, 2)) | ||
| } catch (e) { | ||
| setError((e as Error).message) | ||
| } finally { | ||
| setLoading(false) | ||
| } | ||
| } | ||
|
|
||
| const selectClass = | ||
| 'w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded text-sm text-gray-100 focus:outline-none focus:border-blue-500' | ||
|
|
||
| return ( | ||
| <div> | ||
| <p className="text-sm text-gray-400 mb-3"> | ||
| Look up the cached FX rate, fees, and receiving amount for a currency corridor. | ||
| </p> | ||
| <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> | ||
|
Comment on lines
+39
to
+52
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The two selects share the same Prompt To Fix With AIThis 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. |
||
| <div className="mb-3"> | ||
| <label htmlFor="sending-amount" className="block text-sm font-medium text-gray-300 mb-1">Sending Amount</label> | ||
| <input | ||
| id="sending-amount" | ||
| type="number" | ||
| min="0" | ||
| value={sendingAmount} | ||
| onChange={(e) => setSendingAmount(e.target.value)} | ||
| disabled={loading} | ||
| className={selectClass} | ||
| /> | ||
| <p className="text-xs text-gray-500 mt-1">In the smallest unit of the source currency (e.g. cents). 100000 = 1,000.00 USD.</p> | ||
| </div> | ||
| <button | ||
| onClick={submit} | ||
| disabled={loading} | ||
| className="px-4 py-2 bg-blue-600 hover:bg-blue-500 disabled:bg-gray-700 disabled:text-gray-500 rounded text-sm font-medium" | ||
| > | ||
| {loading ? 'Fetching...' : 'Get Exchange Rate'} | ||
| </button> | ||
| <ResponsePanel response={response} error={error} /> | ||
| </div> | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,52 @@ | ||||||||||||||||||||||
| package com.grid.sample.routes | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| import com.lightspark.grid.models.exchangerates.ExchangeRateListParams | ||||||||||||||||||||||
| import com.grid.sample.GridClientBuilder | ||||||||||||||||||||||
| import com.grid.sample.JsonUtils | ||||||||||||||||||||||
| import com.grid.sample.Log | ||||||||||||||||||||||
| import io.ktor.http.* | ||||||||||||||||||||||
| import io.ktor.server.response.* | ||||||||||||||||||||||
| import io.ktor.server.routing.* | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| fun Route.exchangeRateRoutes() { | ||||||||||||||||||||||
| route("/api/exchange-rates") { | ||||||||||||||||||||||
| get { | ||||||||||||||||||||||
| try { | ||||||||||||||||||||||
| val sourceCurrency = call.request.queryParameters["sourceCurrency"] | ||||||||||||||||||||||
| ?: return@get call.respondText( | ||||||||||||||||||||||
| """{"error": "sourceCurrency is required"}""", | ||||||||||||||||||||||
| ContentType.Application.Json, | ||||||||||||||||||||||
| HttpStatusCode.BadRequest | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| val destinationCurrency = call.request.queryParameters["destinationCurrency"] | ||||||||||||||||||||||
| val amount = call.request.queryParameters["sendingAmount"]?.toLongOrNull() | ||||||||||||||||||||||
| Log.incoming( | ||||||||||||||||||||||
| "GET", | ||||||||||||||||||||||
| "/api/exchange-rates?sourceCurrency=$sourceCurrency&destinationCurrency=$destinationCurrency&sendingAmount=$amount" | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| val params = ExchangeRateListParams.builder() | ||||||||||||||||||||||
| .sourceCurrency(sourceCurrency) | ||||||||||||||||||||||
| .apply { | ||||||||||||||||||||||
| destinationCurrency?.let { addDestinationCurrency(it) } | ||||||||||||||||||||||
| amount?.let { sendingAmount(it) } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| .build() | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Log.gridRequest("exchangeRates.list", "source=$sourceCurrency dest=$destinationCurrency amount=$amount") | ||||||||||||||||||||||
| val response = GridClientBuilder.client.exchangeRates().list(params) | ||||||||||||||||||||||
| val responseJson = JsonUtils.prettyPrint(response) | ||||||||||||||||||||||
| Log.gridResponse("exchangeRates.list", responseJson) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| call.respondText(responseJson, ContentType.Application.Json, HttpStatusCode.OK) | ||||||||||||||||||||||
| } catch (e: Exception) { | ||||||||||||||||||||||
| Log.gridError("exchangeRates.list", e) | ||||||||||||||||||||||
| call.respondText( | ||||||||||||||||||||||
| """{"error": "${e.message}"}""", | ||||||||||||||||||||||
| ContentType.Application.Json, | ||||||||||||||||||||||
| HttpStatusCode.InternalServerError | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
Comment on lines
+44
to
+48
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If the exception message contains a
Suggested change
Prompt To Fix With AIThis 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. |
||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'payout'to'exchange-rates'. This means the sample app now opens on the new flow on every fresh load, which changes the existing user experience. Consider reverting to'payout'unless the intent is to keep the newest flow as the default going forward.Prompt To Fix With AI
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!