Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion samples/frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Sidebar, { FlowKey } from './components/Sidebar'
import WebhookStream from './components/WebhookStream'
import PayoutFlow from './flows/PayoutFlow'
import UsdcPayoutFlow from './flows/UsdcPayoutFlow'
import ExchangeRatesFlow from './flows/ExchangeRatesFlow'
import EmbeddedWalletFlow from './flows/EmbeddedWalletFlow'

const FLOW_META: Record<FlowKey, { title: string; subtitle: string }> = {
Expand All @@ -14,14 +15,18 @@ const FLOW_META: Record<FlowKey, { title: string; subtitle: string }> = {
title: 'Send USDC to a Wallet',
subtitle: 'Send USDC on-chain to an external wallet, funded with USD',
},
'exchange-rates': {
title: 'Exchange Rates',
subtitle: 'Look up FX rates and fees for a corridor',
},
'embedded-wallet': {
title: 'Global Account',
subtitle: 'Issue a self-custody dollar account and withdraw on behalf of a user',
},
}

export default function App() {
const [activeFlow, setActiveFlow] = useState<FlowKey>('payout')
const [activeFlow, setActiveFlow] = useState<FlowKey>('exchange-rates')

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 The default active flow was changed from '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.

Suggested change
const [activeFlow, setActiveFlow] = useState<FlowKey>('exchange-rates')
const [activeFlow, setActiveFlow] = useState<FlowKey>('payout')
Prompt To Fix With AI
This is a comment left during a code review.
Path: samples/frontend/src/App.tsx
Line: 29

Comment:
The default active flow was changed from `'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.

```suggestion
  const [activeFlow, setActiveFlow] = useState<FlowKey>('payout')
```

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

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!

const meta = FLOW_META[activeFlow]

return (
Expand All @@ -36,6 +41,7 @@ export default function App() {
<h2 className="text-lg font-semibold mb-4">{meta.title}</h2>
{activeFlow === 'payout' && <PayoutFlow key="payout" />}
{activeFlow === 'usdc-payout' && <UsdcPayoutFlow key="usdc-payout" />}
{activeFlow === 'exchange-rates' && <ExchangeRatesFlow key="exchange-rates" />}
{activeFlow === 'embedded-wallet' && <EmbeddedWalletFlow key="embedded-wallet" />}
</main>
</div>
Expand Down
7 changes: 6 additions & 1 deletion samples/frontend/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type FlowKey = 'payout' | 'usdc-payout' | 'embedded-wallet'
export type FlowKey = 'payout' | 'usdc-payout' | 'exchange-rates' | 'embedded-wallet'

interface FlowEntry {
key: FlowKey
Expand All @@ -7,6 +7,11 @@ interface FlowEntry {
}

const FLOWS: FlowEntry[] = [
{
key: 'exchange-rates',
label: 'Exchange Rates',
description: 'Look up FX rates and fees for a corridor',
},
{
key: 'payout',
label: 'Payout to Bank Account',
Expand Down
9 changes: 9 additions & 0 deletions samples/frontend/src/flows/ExchangeRatesFlow.tsx
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>
)
}
76 changes: 76 additions & 0 deletions samples/frontend/src/steps/GetExchangeRate.tsx
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

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.

<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
Expand Up @@ -41,6 +41,7 @@ fun Application.module() {
internalAccountRoutes()
authCredentialRoutes()
quoteRoutes()
exchangeRateRoutes()
sandboxRoutes()
webhookRoutes()
sseRoutes()
Expand Down
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

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.

}
}
}
}
Loading