From f81b79b7b9647ab94f53caf7ef9bfe1b3f28be06 Mon Sep 17 00:00:00 2001 From: peng Date: Thu, 18 Jun 2026 11:13:20 -0700 Subject: [PATCH] feat(samples/kotlin): add exchange-rates lookup flow 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) --- samples/frontend/src/App.tsx | 8 +- samples/frontend/src/components/Sidebar.tsx | 7 +- .../frontend/src/flows/ExchangeRatesFlow.tsx | 9 +++ .../frontend/src/steps/GetExchangeRate.tsx | 76 +++++++++++++++++++ .../kotlin/com/grid/sample/Application.kt | 1 + .../com/grid/sample/routes/ExchangeRates.kt | 52 +++++++++++++ 6 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 samples/frontend/src/flows/ExchangeRatesFlow.tsx create mode 100644 samples/frontend/src/steps/GetExchangeRate.tsx create mode 100644 samples/kotlin/src/main/kotlin/com/grid/sample/routes/ExchangeRates.kt diff --git a/samples/frontend/src/App.tsx b/samples/frontend/src/App.tsx index 7cfde600b..e6b222d3c 100644 --- a/samples/frontend/src/App.tsx +++ b/samples/frontend/src/App.tsx @@ -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 = { @@ -14,6 +15,10 @@ const FLOW_META: Record = { 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', @@ -21,7 +26,7 @@ const FLOW_META: Record = { } export default function App() { - const [activeFlow, setActiveFlow] = useState('payout') + const [activeFlow, setActiveFlow] = useState('exchange-rates') const meta = FLOW_META[activeFlow] return ( @@ -36,6 +41,7 @@ export default function App() {

{meta.title}

{activeFlow === 'payout' && } {activeFlow === 'usdc-payout' && } + {activeFlow === 'exchange-rates' && } {activeFlow === 'embedded-wallet' && } diff --git a/samples/frontend/src/components/Sidebar.tsx b/samples/frontend/src/components/Sidebar.tsx index 1aa96ace7..c92e32953 100644 --- a/samples/frontend/src/components/Sidebar.tsx +++ b/samples/frontend/src/components/Sidebar.tsx @@ -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 @@ -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', diff --git a/samples/frontend/src/flows/ExchangeRatesFlow.tsx b/samples/frontend/src/flows/ExchangeRatesFlow.tsx new file mode 100644 index 000000000..a83982030 --- /dev/null +++ b/samples/frontend/src/flows/ExchangeRatesFlow.tsx @@ -0,0 +1,9 @@ +import GetExchangeRate from '../steps/GetExchangeRate' + +export default function ExchangeRatesFlow() { + return ( +
+ +
+ ) +} diff --git a/samples/frontend/src/steps/GetExchangeRate.tsx b/samples/frontend/src/steps/GetExchangeRate.tsx new file mode 100644 index 000000000..8b25d1fda --- /dev/null +++ b/samples/frontend/src/steps/GetExchangeRate.tsx @@ -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(null) + const [error, setError] = useState(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>(`/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 ( +
+

+ Look up the cached FX rate, fees, and receiving amount for a currency corridor. +

+
+
+ + +
+
+ + +
+
+
+ + setSendingAmount(e.target.value)} + disabled={loading} + className={selectClass} + /> +

In the smallest unit of the source currency (e.g. cents). 100000 = 1,000.00 USD.

+
+ + +
+ ) +} diff --git a/samples/kotlin/src/main/kotlin/com/grid/sample/Application.kt b/samples/kotlin/src/main/kotlin/com/grid/sample/Application.kt index 180b4a279..5a7cac573 100644 --- a/samples/kotlin/src/main/kotlin/com/grid/sample/Application.kt +++ b/samples/kotlin/src/main/kotlin/com/grid/sample/Application.kt @@ -41,6 +41,7 @@ fun Application.module() { internalAccountRoutes() authCredentialRoutes() quoteRoutes() + exchangeRateRoutes() sandboxRoutes() webhookRoutes() sseRoutes() diff --git a/samples/kotlin/src/main/kotlin/com/grid/sample/routes/ExchangeRates.kt b/samples/kotlin/src/main/kotlin/com/grid/sample/routes/ExchangeRates.kt new file mode 100644 index 000000000..4564ffb29 --- /dev/null +++ b/samples/kotlin/src/main/kotlin/com/grid/sample/routes/ExchangeRates.kt @@ -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 + ) + } + } + } +}