Skip to content
This repository has been archived by the owner on Dec 12, 2024. It is now read-only.

Commit

Permalink
Implement GetBalance client call and server endpoint (#217)
Browse files Browse the repository at this point in the history
* adding balance, fixing offerings. wip

* adding optional success field to close message

* fixing test data message and resource constructions. more tests are passing, but test vectors need to be fixed, also need to look at the new regex pattern decimalString and see if it still works with numbers with decimal

* in the middle of refactoring - all tests using test vectors are still failing.

* adding a todo

* refactoring method name in vector test

* bumping submodule commit

* adding ktdocs

* more ktdocs

* adding balance and resource tests for balance. adding balance test vector tests

* adding new client method to fetch balances

* wrote client tests

* adding server endpoint for balance api

* adding detekt fixes. still 1 test failing due to rfq schema error

* adding requesterDid to balanceApi.getBalance param. doing an auth token check on getbalances handler method. formatting

* refactoring existing tests. add new test for getbalance

* adding ktdoc for fakeBalancesApi#addBalance

* Apply suggestions from code review

Co-authored-by: Diane Huxley <[email protected]>

* adding try catch around parsing balance and offering

* Update httpserver/src/main/kotlin/tbdex/sdk/httpserver/TbdexHttpServer.kt

Co-authored-by: kirahsapong  <[email protected]>

* adding balancesEnabled config

* aligning with tbdex-js way of making balancesapi optional

* removed testserver

---------

Co-authored-by: Diane Huxley <[email protected]>
Co-authored-by: kirahsapong <[email protected]>
  • Loading branch information
3 people authored Mar 31, 2024
1 parent 2276108 commit d87adb2
Show file tree
Hide file tree
Showing 14 changed files with 574 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ object RequestToken {
.didDocument?.findAssertionMethodById(assertionMethodId)
?: throw RequestTokenCreateException("Assertion method not found")

// TODO: ensure that publicKeyJwk is not null
val publicKeyJwk = assertionMethod.publicKeyJwk
check(publicKeyJwk != null) { "publicKeyJwk is null" }
val keyAlias = did.keyManager.getDeterministicAlias(publicKeyJwk)
Expand Down
81 changes: 74 additions & 7 deletions httpclient/src/main/kotlin/tbdex/sdk/httpclient/TbdexHttpClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import tbdex.sdk.httpclient.models.GetExchangesFilter
import tbdex.sdk.httpclient.models.GetOfferingsFilter
import tbdex.sdk.httpclient.models.TbdexResponseException
import tbdex.sdk.protocol.Validator
import tbdex.sdk.protocol.models.Balance
import tbdex.sdk.protocol.models.Close
import tbdex.sdk.protocol.models.Message
import tbdex.sdk.protocol.models.Offering
Expand All @@ -29,6 +30,8 @@ import web5.sdk.dids.did.BearerDid
*/
object TbdexHttpClient {
private val client = OkHttpClient()
private const val CONTENT_TYPE = "Content-Type"
private const val AUTHORIZATION = "Authorization"
private val jsonMediaType = "application/json; charset=utf-8".toMediaType()
private const val JSON_HEADER = "application/json"

Expand All @@ -41,6 +44,7 @@ object TbdexHttpClient {
* @return A list of [Offering] matching the request.
* @throws TbdexResponseException for request or response errors.
*/
@Suppress("SwallowedException")
fun getOfferings(pfiDid: String, filter: GetOfferingsFilter? = null): List<Offering> {
val pfiServiceEndpoint = getPfiServiceEndpoint(pfiDid)
val baseUrl = "$pfiServiceEndpoint/offerings/"
Expand All @@ -64,7 +68,70 @@ object TbdexHttpClient {
val jsonNode = jsonMapper.readTree(responseString)
return jsonNode.get("data").elements()
.asSequence()
.map { Offering.parse(it.toString()) }
.map {
try {
Offering.parse(it.toString())
} catch (e: Exception) {
throw TbdexResponseException(
"response status: ${response.code}",
errors = listOf(
ErrorDetail(
detail = "Failed to parse offering ${e.message}"
)
)
)
}
}
.toList()
}

else -> throw buildResponseException(response)
}
}

/**
* Fetches balances from a PFI.
*
* @param pfiDid The decentralized identifier of the PFI.
* @param requesterDid The decentralized identifier of the entity requesting the balances.
* @return A list of [Balance] matching the request.
* @throws TbdexResponseException for request or response errors.
*/
@Suppress("SwallowedException")
fun getBalances(pfiDid: String, requesterDid: BearerDid): List<Balance> {
val pfiServiceEndpoint = getPfiServiceEndpoint(pfiDid)
val baseUrl = "$pfiServiceEndpoint/balances/"
val requestToken = RequestToken.generate(requesterDid, pfiDid)

val request = Request.Builder()
.url(baseUrl)
.addHeader(CONTENT_TYPE, JSON_HEADER)
.addHeader(AUTHORIZATION, "Bearer $requestToken")
.get()
.build()

val response: Response = client.newCall(request).execute()
when {
response.isSuccessful -> {
val responseString = response.body?.string()
// response body is an object with a data field
val jsonNode = jsonMapper.readTree(responseString)
return jsonNode.get("data").elements()
.asSequence()
.map {
try {
Balance.parse(it.toString())
} catch (e: Exception) {
throw TbdexResponseException(
"response status: ${response.code}",
errors = listOf(
ErrorDetail(
detail = "Failed to parse balance ${e.message}"
)
)
)
}
}
.toList()
}

Expand Down Expand Up @@ -115,7 +182,7 @@ object TbdexHttpClient {

val request = Request.Builder()
.url(url)
.addHeader("Content-Type", JSON_HEADER)
.addHeader(CONTENT_TYPE, JSON_HEADER)
.post(requestBody)
.build()

Expand Down Expand Up @@ -170,7 +237,7 @@ object TbdexHttpClient {

val request = Request.Builder()
.url(url)
.addHeader("Content-Type", JSON_HEADER)
.addHeader(CONTENT_TYPE, JSON_HEADER)
.put(requestBody)
.build()

Expand All @@ -195,8 +262,8 @@ object TbdexHttpClient {

val request = Request.Builder()
.url(baseUrl)
.addHeader("Content-Type", JSON_HEADER)
.addHeader("Authorization", "Bearer $requestToken")
.addHeader(CONTENT_TYPE, JSON_HEADER)
.addHeader(AUTHORIZATION, "Bearer $requestToken")
.get()
.build()

Expand Down Expand Up @@ -237,8 +304,8 @@ object TbdexHttpClient {

val request = Request.Builder()
.url(httpUrlBuilder.build())
.addHeader("Content-Type", JSON_HEADER)
.addHeader("Authorization", "Bearer $requestToken")
.addHeader(CONTENT_TYPE, JSON_HEADER)
.addHeader(AUTHORIZATION, "Bearer $requestToken")
.get()
.build()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,42 @@ class TbdexHttpClientTest {
assertThrows<TbdexResponseException> { TbdexHttpClient.getOfferings(pfiDid.uri, null) }
}

@Test
fun `get balances success via mockwebserver`() {
val balance = TestData.getBalance()
balance.sign(TestData.PFI_DID)
val mockBalance = listOf(balance)
val mockResponseString = Json.jsonMapper.writeValueAsString(mapOf("data" to mockBalance))
server.enqueue(MockResponse().setBody(mockResponseString).setResponseCode(HttpURLConnection.HTTP_OK))

val requesterDid = DidDht.create(InMemoryKeyManager())
val response = TbdexHttpClient.getBalances(pfiDid.uri, requesterDid)

assertEquals(1, response.size)
}

@Test
fun `get balances fail via mockwebserver`() {
val errorDetails = mapOf(
"errors" to listOf(
ErrorDetail(
id = "1",
status = "401",
code = "Unauthorized",
title = "Unauthorized",
detail = "The request is unauthorized.",
source = null,
meta = null
)
)
)

val mockResponseString = Json.jsonMapper.writeValueAsString(errorDetails)
server.enqueue(MockResponse().setBody(mockResponseString).setResponseCode(HttpURLConnection.HTTP_BAD_REQUEST))
val requesterDid = DidDht.create(InMemoryKeyManager())
assertThrows<TbdexResponseException> { TbdexHttpClient.getBalances(pfiDid.uri, requesterDid) }
}

@Test
fun `createExchange(Rfq) submits RFQ`() {

Expand Down
12 changes: 12 additions & 0 deletions httpclient/src/test/kotlin/tbdex/sdk/httpclient/TestData.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package tbdex.sdk.httpclient

import com.danubetech.verifiablecredentials.CredentialSubject
import de.fxlae.typeid.TypeId
import tbdex.sdk.protocol.models.Balance
import tbdex.sdk.protocol.models.BalanceData
import tbdex.sdk.protocol.models.MessageKind
import tbdex.sdk.protocol.models.Offering
import tbdex.sdk.protocol.models.OfferingData
Expand Down Expand Up @@ -74,6 +76,16 @@ object TestData {
return offering
}

fun getBalance() =
Balance.create(
from = PFI_DID.uri,
data = BalanceData(
currencyCode = "BTC",
available = "0.001",
)
)


fun getRfq(
to: String = PFI_DID.uri,
offeringId: String = TypeId.generate(ResourceKind.offering.name).toString(),
Expand Down
56 changes: 32 additions & 24 deletions httpserver/src/main/kotlin/tbdex/sdk/httpserver/TbdexHttpServer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@ import io.ktor.server.routing.put
import io.ktor.server.routing.route
import io.ktor.server.routing.routing
import tbdex.sdk.httpserver.handlers.createExchange
import tbdex.sdk.httpserver.handlers.getBalances
import tbdex.sdk.httpserver.handlers.getExchange
import tbdex.sdk.httpserver.handlers.getExchanges
import tbdex.sdk.httpserver.handlers.getOfferings
import tbdex.sdk.httpserver.handlers.submitMessage
import tbdex.sdk.httpserver.models.BalancesApi
import tbdex.sdk.httpserver.models.Callbacks
import tbdex.sdk.httpserver.models.CreateExchangeCallback
import tbdex.sdk.httpserver.models.ExchangesApi
import tbdex.sdk.httpserver.models.FakeBalancesApi
import tbdex.sdk.httpserver.models.FakeExchangesApi
import tbdex.sdk.httpserver.models.FakeOfferingsApi
import tbdex.sdk.httpserver.models.GetExchangeCallback
Expand All @@ -34,32 +37,21 @@ import tbdex.sdk.httpserver.models.OfferingsApi
import tbdex.sdk.httpserver.models.SubmitCloseCallback
import tbdex.sdk.httpserver.models.SubmitOrderCallback

/**
* Main function to start the TBDex HTTP server.
*/
fun main() {

embeddedServer(Netty, port = 8080) {
val serverConfig = TbdexHttpServerConfig(
port = 8080,
)
val tbdexServer = TbdexHttpServer(serverConfig)
tbdexServer.configure(this)
}.start(wait = true)
}

/**
* Configuration data for TBDex HTTP server.
*
* @property port The port on which the server will listen.
* @property offeringsApi An optional [OfferingsApi] implementation to use.
* @property exchangesApi An optional [ExchangesApi] implementation to use.
* @property offeringsApi A [OfferingsApi] implementation to use. If not provided, a [FakeOfferingsApi] will be used.
* @property exchangesApi A [ExchangesApi] implementation to use. If not provided, a [FakeExchangesApi] will be used.
* @property balancesApi A [BalancesApi] implementation to use. If not provided, Balances API will be disabled.
* For testing, consumers must explicitly pass in [FakeBalancesApi].
*/
class TbdexHttpServerConfig(
val port: Int,
val pfiDid: String? = null,
val offeringsApi: OfferingsApi? = null,
val exchangesApi: ExchangesApi? = null
val exchangesApi: ExchangesApi? = null,
val balancesApi: BalancesApi? = null
)


Expand All @@ -77,6 +69,7 @@ class TbdexHttpServer(private val config: TbdexHttpServerConfig) {
internal val pfiDid = config.pfiDid ?: "did:ex:pfi"
internal val offeringsApi = config.offeringsApi ?: FakeOfferingsApi()
internal val exchangesApi = config.exchangesApi ?: FakeExchangesApi()
internal val balancesApi = config.balancesApi

/**
* Configures the Ktor application with necessary settings, including content negotiation.
Expand All @@ -102,6 +95,28 @@ class TbdexHttpServer(private val config: TbdexHttpServerConfig) {
)
}

get("/offerings") {
getOfferings(
call,
offeringsApi,
callbacks.getOfferings
)
}

if (config.balancesApi != null) {
if (balancesApi is FakeBalancesApi) {
println("Warning: Balances API is enabled, with FakeBalancesApi test implementation.")
}
get("/balances") {
getBalances(
call,
balancesApi!!,
callbacks.getBalances,
pfiDid
)
}
}

route("/exchanges") {
post {
createExchange(
Expand Down Expand Up @@ -139,13 +154,6 @@ class TbdexHttpServer(private val config: TbdexHttpServerConfig) {
}
}

get("/offerings") {
getOfferings(
call,
offeringsApi,
callbacks.getOfferings
)
}
}
}

Expand Down
Loading

0 comments on commit d87adb2

Please sign in to comment.