-
Notifications
You must be signed in to change notification settings - Fork 3
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
Implement GetBalance client call and server endpoint #217
Changes from 22 commits
5894279
0dba7c2
f21cdf0
35aa08d
a1d2f91
09f4762
494156b
6a57244
6b74577
7a0ae63
18dd13f
ceadf77
0ce5957
da2aafe
15d18d3
8a31cda
61a1446
393e9eb
1fad2ed
01d6a12
9f90c3c
0e8d498
c67f00a
43bf4c8
f500e35
c71048d
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 |
---|---|---|
|
@@ -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 | ||
|
@@ -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" | ||
|
||
|
@@ -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/" | ||
|
@@ -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") | ||
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. what exception gets swallowed? 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 exception thrown if Balance.parse(it.toString()) fails in the try/catch, line 123. it gets re-thrown as TbdexResponseException as implemented per this suggestion |
||
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() | ||
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. ah does this throw an exception if the request fails? e.g. timeout, 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. correct. from the docs:
|
||
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() | ||
} | ||
|
||
|
@@ -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() | ||
|
||
|
@@ -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() | ||
|
||
|
@@ -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() | ||
|
||
|
@@ -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() | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -59,7 +62,8 @@ 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 | ||
) | ||
|
||
|
||
|
@@ -77,6 +81,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 ?: FakeBalancesApi() | ||
jiyoonie9 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/** | ||
* Configures the Ktor application with necessary settings, including content negotiation. | ||
|
@@ -102,6 +107,23 @@ class TbdexHttpServer(private val config: TbdexHttpServerConfig) { | |
) | ||
} | ||
|
||
get("/offerings") { | ||
getOfferings( | ||
call, | ||
offeringsApi, | ||
callbacks.getOfferings | ||
) | ||
} | ||
|
||
get("/balances") { | ||
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. this should be an optional endpoint 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. ooh, i added the suggested change you posted earlier, but actually instead o that, i'd like to wrap this in a condition that checks for a new config value 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. i think it will be good for testing to still have 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. i thought it might be helpful to share how the optional endpoint is implemented in js right now: https://github.com/TBD54566975/tbdex-js/blob/main/packages/http-server/src/http-server.ts#L45 and also this comment thread on the PR for it: TBD54566975/tbdex-js#212 (comment) @KendallWeihe's proposal was simplifying it by keeping consistent with the other fields, and having the consumer explicitly pass in but either approach is cool, prob just good to make sure its aligned. i can open an issue in JS to add a 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. okey, i've aligned on essentially using balancesApi null check essentially the config on whether to enable balances. i like |
||
getBalances( | ||
call, | ||
balancesApi, | ||
callbacks.getBalances, | ||
pfiDid | ||
) | ||
} | ||
|
||
route("/exchanges") { | ||
post { | ||
createExchange( | ||
|
@@ -139,13 +161,6 @@ class TbdexHttpServer(private val config: TbdexHttpServerConfig) { | |
} | ||
} | ||
|
||
get("/offerings") { | ||
getOfferings( | ||
call, | ||
offeringsApi, | ||
callbacks.getOfferings | ||
) | ||
} | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
package tbdex.sdk.httpserver.handlers | ||
|
||
import io.ktor.http.HttpHeaders | ||
import io.ktor.http.HttpStatusCode | ||
import io.ktor.server.application.ApplicationCall | ||
import io.ktor.server.response.respond | ||
import tbdex.sdk.httpclient.RequestToken | ||
import tbdex.sdk.httpclient.models.ErrorDetail | ||
import tbdex.sdk.httpserver.models.BalancesApi | ||
import tbdex.sdk.httpserver.models.CallbackError | ||
import tbdex.sdk.httpserver.models.ErrorResponse | ||
import tbdex.sdk.httpserver.models.GetBalancesCallback | ||
import tbdex.sdk.protocol.models.Balance | ||
|
||
/** | ||
* Get balances response | ||
* | ||
* @property data list of Balances | ||
*/ | ||
class GetBalancesResponse( | ||
val data: List<Balance>? | ||
) | ||
/** | ||
* Get balances request handler | ||
* | ||
* @param call Ktor server application call | ||
* @param balancesApi Balances API interface | ||
* @param callback Callback function to be invoked | ||
*/ | ||
@Suppress("SwallowedException") | ||
suspend fun getBalances( | ||
call: ApplicationCall, | ||
balancesApi: BalancesApi, | ||
callback: GetBalancesCallback?, | ||
pfiDid: String | ||
) { | ||
|
||
val authzHeader = call.request.headers[HttpHeaders.Authorization] | ||
if (authzHeader == null) { | ||
call.respond( | ||
HttpStatusCode.Unauthorized, | ||
ErrorResponse( | ||
errors = listOf( | ||
ErrorDetail( | ||
detail = "Authorization header required" | ||
) | ||
) | ||
) | ||
) | ||
return | ||
} | ||
|
||
val arr = authzHeader.split("Bearer ") | ||
if (arr.size != 2) { | ||
call.respond( | ||
HttpStatusCode.Unauthorized, | ||
ErrorResponse( | ||
errors = listOf( | ||
ErrorDetail( | ||
detail = "Malformed Authorization header. Expected: Bearer TOKEN_HERE" | ||
) | ||
) | ||
) | ||
) | ||
return | ||
} | ||
|
||
val token = arr[1] | ||
val requesterDid: String | ||
try { | ||
requesterDid = RequestToken.verify(token, pfiDid) | ||
} catch (e: Exception) { | ||
call.respond( | ||
HttpStatusCode.Unauthorized, | ||
ErrorResponse( | ||
errors = listOf( | ||
ErrorDetail( | ||
detail = "Could not verify Authorization header." | ||
) | ||
) | ||
) | ||
) | ||
return | ||
Comment on lines
+38
to
+83
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. I haven't used 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. yes. there is a ton of repeat here for all protected endpoints' request handler. Issue #227 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. nice. had a similar issue in swift TBD54566975/tbdex-swift#85 js could use some drying up also. will create an issue 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. here: TBD54566975/tbdex-js#223 |
||
} | ||
|
||
val balances = balancesApi.getBalances(requesterDid) | ||
|
||
if (callback == null) { | ||
call.respond(HttpStatusCode.OK, GetBalancesResponse(data = balances)) | ||
return | ||
} | ||
|
||
try { | ||
callback.invoke(call) | ||
} catch (e: CallbackError) { | ||
call.respond(e.statusCode, ErrorResponse(e.details)) | ||
return | ||
} catch (e: Exception) { | ||
val errorDetail = ErrorDetail(detail = e.message ?: "Unknown error while getting Balances") | ||
call.respond(HttpStatusCode.InternalServerError, ErrorResponse(listOf(errorDetail))) | ||
return | ||
} | ||
|
||
call.respond(HttpStatusCode.OK, GetBalancesResponse(data = balances)) | ||
} |
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.
Thinking out loud: in tbdex-js, we have both
TbdexRequestError
andTbdexResponseError
, representing errors that happen before and after the client call respectively. It's a slight misnomer to throw a RequestException for both request and response errors, so I'm making a mental note that in my next tidy-up PR, I may explore introducing aTbdexRequestException
class. In any case, it's relatively low priority and outside the scope of this PR.