[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[libeufin] branch master updated: libeufin-bank: implement main()
From: |
gnunet |
Subject: |
[libeufin] branch master updated: libeufin-bank: implement main() |
Date: |
Fri, 22 Sep 2023 11:29:45 +0200 |
This is an automated email from the git hooks/post-receive script.
dold pushed a commit to branch master
in repository libeufin.
The following commit(s) were added to refs/heads/master by this push:
new f52e5964 libeufin-bank: implement main()
f52e5964 is described below
commit f52e5964270a2509323c78803317dffe9dde3bc4
Author: Florian Dold <florian@dold.me>
AuthorDate: Fri Sep 22 11:29:35 2023 +0200
libeufin-bank: implement main()
---
bank/src/main/kotlin/tech/libeufin/bank/Main.kt | 76 +++++++++++++++++++---
.../tech/libeufin/bank/accountsMgmtHandlers.kt | 4 +-
.../kotlin/tech/libeufin/bank/talerWebHandlers.kt | 8 +--
.../tech/libeufin/bank/talerWireGatewayHandlers.kt | 6 +-
.../kotlin/tech/libeufin/bank/tokenHandlers.kt | 2 +-
.../tech/libeufin/bank/transactionsHandlers.kt | 6 +-
bank/src/test/kotlin/LibeuFinApiTest.kt | 20 ++++--
bank/src/test/kotlin/TalerApiTest.kt | 33 +++++++---
util/src/main/kotlin/TalerConfig.kt | 8 +++
9 files changed, 126 insertions(+), 37 deletions(-)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
index 8183fefe..696b397f 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
@@ -20,12 +20,24 @@
package tech.libeufin.bank
+import TalerConfig
+import com.github.ajalt.clikt.core.CliktCommand
+import com.github.ajalt.clikt.core.context
+import com.github.ajalt.clikt.core.subcommands
+import com.github.ajalt.clikt.output.CliktHelpFormatter
+import com.github.ajalt.clikt.parameters.options.default
+import com.github.ajalt.clikt.parameters.options.flag
+import com.github.ajalt.clikt.parameters.options.option
+import com.github.ajalt.clikt.parameters.options.versionOption
+import com.github.ajalt.clikt.parameters.types.int
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.plugins.*
import io.ktor.server.plugins.requestvalidation.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
+import io.ktor.server.engine.*
+import io.ktor.server.netty.*
import io.ktor.server.plugins.callloging.*
import kotlinx.serialization.*
import io.ktor.server.plugins.cors.routing.*
@@ -33,6 +45,9 @@ import io.ktor.server.plugins.statuspages.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
@@ -44,10 +59,10 @@ import org.slf4j.LoggerFactory
import org.slf4j.event.Level
import tech.libeufin.util.*
import java.time.Duration
+import kotlin.system.exitProcess
// GLOBALS
private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.bank.Main")
-private val db = Database(System.getProperty("BANK_DB_CONNECTION_STRING"))
const val GENERIC_UNDEFINED = -1 // Filler for ECs that don't exist yet.
val TOKEN_DEFAULT_DURATION_US = Duration.ofDays(1L).seconds * 1000000
@@ -62,6 +77,7 @@ object RelativeTimeSerializer : KSerializer<RelativeTime> {
override fun serialize(encoder: Encoder, value: RelativeTime) {
throw internalServerError("Encoding of RelativeTime not implemented.")
// API doesn't require this.
}
+
override fun deserialize(decoder: Decoder): RelativeTime {
val jsonInput = decoder as? JsonDecoder ?: throw
internalServerError("RelativeTime had no JsonDecoder")
val json = try {
@@ -77,7 +93,8 @@ object RelativeTimeSerializer : KSerializer<RelativeTime> {
if (maybeDUs.content != "forever") throw badRequest("Only
'forever' allowed for d_us as string, but '${maybeDUs.content}' was found")
return RelativeTime(d_us = Long.MAX_VALUE)
}
- val dUs: Long = maybeDUs.longOrNull ?: throw badRequest("Could not
convert d_us: '${maybeDUs.content}' to a number")
+ val dUs: Long =
+ maybeDUs.longOrNull ?: throw badRequest("Could not convert d_us:
'${maybeDUs.content}' to a number")
return RelativeTime(d_us = dUs)
}
@@ -91,9 +108,11 @@ object TalerAmountSerializer : KSerializer<TalerAmount> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("TalerAmount", PrimitiveKind.STRING)
+
override fun serialize(encoder: Encoder, value: TalerAmount) {
throw internalServerError("Encoding of TalerAmount not implemented.")
// API doesn't require this.
}
+
override fun deserialize(decoder: Decoder): TalerAmount {
val maybeAmount = try {
decoder.decodeString()
@@ -116,7 +135,7 @@ object TalerAmountSerializer : KSerializer<TalerAmount> {
*
* Returns the authenticated customer, or null if they failed.
*/
-fun ApplicationCall.myAuth(requiredScope: TokenScope): Customer? {
+fun ApplicationCall.myAuth(db: Database, requiredScope: TokenScope): Customer?
{
// Extracting the Authorization header.
val header = getAuthorizationRawHeader(this.request) ?: throw badRequest(
"Authorization header not found.",
@@ -139,7 +158,11 @@ fun ApplicationCall.myAuth(requiredScope: TokenScope):
Customer? {
}
}
-val webApp: Application.() -> Unit = {
+
+/**
+ * Set up web server handlers for the Taler corebank API.
+ */
+fun Application.corebankWebApp(db: Database) {
install(CallLogging) {
this.level = Level.DEBUG
this.logger = tech.libeufin.bank.logger
@@ -181,7 +204,7 @@ val webApp: Application.() -> Unit = {
* (Ktor native) type doesn't easily map to the Taler error
* format.
*/
- exception<BadRequestException> {call, cause ->
+ exception<BadRequestException> { call, cause ->
/**
* NOTE: extracting the root cause helps with JSON error messages,
* because they mention the particular way they are invalid, but
OTOH
@@ -198,11 +221,13 @@ val webApp: Application.() -> Unit = {
val errorMessage: String? = rootCause?.message ?: cause.message
logger.error(errorMessage)
// Telling apart invalid JSON vs missing parameter vs invalid
parameter.
- val talerErrorCode = when(cause) {
+ val talerErrorCode = when (cause) {
is MissingRequestParameterException ->
TalerErrorCode.TALER_EC_GENERIC_PARAMETER_MISSING
+
is ParameterConversionException ->
TalerErrorCode.TALER_EC_GENERIC_PARAMETER_MALFORMED
+
else -> TalerErrorCode.TALER_EC_GENERIC_JSON_INVALID
}
call.respond(
@@ -210,7 +235,8 @@ val webApp: Application.() -> Unit = {
message = TalerError(
code = talerErrorCode.code,
hint = errorMessage
- ))
+ )
+ )
}
/**
* This branch triggers when a bank handler throws it, and namely
@@ -218,7 +244,7 @@ val webApp: Application.() -> Unit = {
* should be preferred to catch errors, as it allows to include the
* Taler specific error detail.
*/
- exception<LibeufinBankException> {call, cause ->
+ exception<LibeufinBankException> { call, cause ->
logger.error(cause.talerError.hint)
call.respond(
status = cause.httpStatus,
@@ -226,7 +252,7 @@ val webApp: Application.() -> Unit = {
)
}
// Catch-all branch to mean that the bank wasn't able to manage one
error.
- exception<Exception> {call, cause ->
+ exception<Exception> { call, cause ->
cause.printStackTrace()
logger.error(cause.message)
call.respond(
@@ -250,4 +276,34 @@ val webApp: Application.() -> Unit = {
this.talerIntegrationHandlers(db)
this.talerWireGatewayHandlers(db)
}
-}
\ No newline at end of file
+}
+
+class LibeufinBankCommand : CliktCommand() {
+ init {
+ versionOption(getVersion())
+ }
+
+ override fun run() = Unit
+}
+
+class ServeBank : CliktCommand("Run libeufin-bank HTTP server", name =
"serve") {
+ init {
+ context {
+ helpFormatter = CliktHelpFormatter(showDefaultValues = true)
+ }
+ }
+
+ override fun run() {
+ val config = TalerConfig.load()
+ val dbConnStr = config.requireValueString("libeufin-bank-db-postgres",
"config")
+ logger.info("using database '$dbConnStr'")
+ val db = Database(dbConnStr)
+ embeddedServer(Netty, port = 8080) {
+ corebankWebApp(db)
+ }.start(wait = true)
+ }
+}
+
+fun main(args: Array<String>) {
+ LibeufinBankCommand().subcommands(ServeBank()).main(args)
+}
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/accountsMgmtHandlers.kt
b/bank/src/main/kotlin/tech/libeufin/bank/accountsMgmtHandlers.kt
index 77394248..f6b225f5 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/accountsMgmtHandlers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/accountsMgmtHandlers.kt
@@ -23,7 +23,7 @@ fun Routing.accountsMgmtHandlers(db: Database) {
// check if only admin.
val maybeOnlyAdmin = db.configGet("only_admin_registrations")
if (maybeOnlyAdmin?.lowercase() == "yes") {
- val customer: Customer? = call.myAuth(TokenScope.readwrite)
+ val customer: Customer? = call.myAuth(db, TokenScope.readwrite)
if (customer == null || customer.login != "admin")
throw LibeufinBankException(
httpStatus = HttpStatusCode.Unauthorized,
@@ -110,7 +110,7 @@ fun Routing.accountsMgmtHandlers(db: Database) {
return@post
}
get("/accounts/{USERNAME}") {
- val c = call.myAuth(TokenScope.readonly) ?: throw unauthorized("Login
failed")
+ val c = call.myAuth(db, TokenScope.readonly) ?: throw
unauthorized("Login failed")
val resourceName = call.maybeUriComponent("USERNAME") ?: throw
badRequest(
hint = "No username found in the URI",
talerErrorCode = TalerErrorCode.TALER_EC_GENERIC_PARAMETER_MISSING
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/talerWebHandlers.kt
b/bank/src/main/kotlin/tech/libeufin/bank/talerWebHandlers.kt
index 83d6adb3..258bc005 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/talerWebHandlers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/talerWebHandlers.kt
@@ -38,7 +38,7 @@ import java.util.*
fun Routing.talerWebHandlers(db: Database) {
post("/accounts/{USERNAME}/withdrawals") {
- val c = call.myAuth(TokenScope.readwrite) ?: throw unauthorized()
+ val c = call.myAuth(db, TokenScope.readwrite) ?: throw unauthorized()
// Admin not allowed to withdraw in the name of customers:
val accountName = call.expectUriComponent("USERNAME")
if (c.login != accountName)
@@ -79,7 +79,7 @@ fun Routing.talerWebHandlers(db: Database) {
return@post
}
get("/accounts/{USERNAME}/withdrawals/{withdrawal_id}") {
- val c = call.myAuth(TokenScope.readonly) ?: throw unauthorized()
+ val c = call.myAuth(db, TokenScope.readonly) ?: throw unauthorized()
val accountName = call.expectUriComponent("USERNAME")
// Admin allowed to see the details
if (c.login != accountName && c.login != "admin") throw forbidden()
@@ -96,7 +96,7 @@ fun Routing.talerWebHandlers(db: Database) {
return@get
}
post("/accounts/{USERNAME}/withdrawals/{withdrawal_id}/abort") {
- val c = call.myAuth(TokenScope.readonly) ?: throw unauthorized()
+ val c = call.myAuth(db, TokenScope.readonly) ?: throw unauthorized()
// Admin allowed to abort.
if (!call.getResourceName("USERNAME").canI(c)) throw forbidden()
val op = getWithdrawal(db, call.expectUriComponent("withdrawal_id"))
@@ -114,7 +114,7 @@ fun Routing.talerWebHandlers(db: Database) {
return@post
}
post("/accounts/{USERNAME}/withdrawals/{withdrawal_id}/confirm") {
- val c = call.myAuth(TokenScope.readwrite) ?: throw unauthorized()
+ val c = call.myAuth(db, TokenScope.readwrite) ?: throw unauthorized()
// No admin allowed.
if(!call.getResourceName("USERNAME").canI(c, withAdmin = false)) throw
forbidden()
val op = getWithdrawal(db, call.expectUriComponent("withdrawal_id"))
diff --git
a/bank/src/main/kotlin/tech/libeufin/bank/talerWireGatewayHandlers.kt
b/bank/src/main/kotlin/tech/libeufin/bank/talerWireGatewayHandlers.kt
index b223310d..85d2ca54 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/talerWireGatewayHandlers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/talerWireGatewayHandlers.kt
@@ -37,7 +37,7 @@ fun Routing.talerWireGatewayHandlers(db: Database) {
return@get
}
get("/accounts/{USERNAME}/taler-wire-gateway/history/incoming") {
- val c = call.myAuth(TokenScope.readonly) ?: throw unauthorized()
+ val c = call.myAuth(db, TokenScope.readonly) ?: throw unauthorized()
if (!call.getResourceName("USERNAME").canI(c, withAdmin = true)) throw
forbidden()
val params = getHistoryParams(call.request)
val bankAccount = db.bankAccountGetFromOwnerId(c.expectRowId())
@@ -69,7 +69,7 @@ fun Routing.talerWireGatewayHandlers(db: Database) {
return@get
}
post("/accounts/{USERNAME}/taler-wire-gateway/transfer") {
- val c = call.myAuth(TokenScope.readwrite) ?: throw unauthorized()
+ val c = call.myAuth(db, TokenScope.readwrite) ?: throw unauthorized()
if (!call.getResourceName("USERNAME").canI(c, withAdmin = false))
throw forbidden()
val req = call.receive<TransferRequest>()
// Checking for idempotency.
@@ -124,7 +124,7 @@ fun Routing.talerWireGatewayHandlers(db: Database) {
return@post
}
post("/accounts/{USERNAME}/taler-wire-gateway/admin/add-incoming") {
- val c = call.myAuth(TokenScope.readwrite) ?: throw unauthorized()
+ val c = call.myAuth(db, TokenScope.readwrite) ?: throw unauthorized()
if (!call.getResourceName("USERNAME").canI(c, withAdmin = false))
throw forbidden()
val req = call.receive<AddIncomingRequest>()
val amount = parseTalerAmount(req.amount)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/tokenHandlers.kt
b/bank/src/main/kotlin/tech/libeufin/bank/tokenHandlers.kt
index 738adad9..98166116 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/tokenHandlers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/tokenHandlers.kt
@@ -37,7 +37,7 @@ fun Routing.tokenHandlers(db: Database) {
throw internalServerError("Token deletion not implemented.")
}
post("/accounts/{USERNAME}/token") {
- val customer = call.myAuth(TokenScope.refreshable) ?: throw
unauthorized("Authentication failed")
+ val customer = call.myAuth(db, TokenScope.refreshable) ?: throw
unauthorized("Authentication failed")
val endpointOwner = call.maybeUriComponent("USERNAME")
if (customer.login != endpointOwner)
throw forbidden(
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/transactionsHandlers.kt
b/bank/src/main/kotlin/tech/libeufin/bank/transactionsHandlers.kt
index dd1a2a1e..c6f67a61 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/transactionsHandlers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/transactionsHandlers.kt
@@ -18,7 +18,7 @@ private val logger: Logger =
LoggerFactory.getLogger("tech.libeufin.bank.transac
fun Routing.transactionsHandlers(db: Database) {
get("/accounts/{USERNAME}/transactions") {
- val c = call.myAuth(TokenScope.readonly) ?: throw unauthorized()
+ val c = call.myAuth(db, TokenScope.readonly) ?: throw unauthorized()
val resourceName = call.expectUriComponent("USERNAME")
if (c.login != resourceName && c.login != "admin") throw forbidden()
// Collecting params.
@@ -51,7 +51,7 @@ fun Routing.transactionsHandlers(db: Database) {
}
// Creates a bank transaction.
post("/accounts/{USERNAME}/transactions") {
- val c = call.myAuth(TokenScope.readwrite) ?: throw unauthorized()
+ val c = call.myAuth(db, TokenScope.readwrite) ?: throw unauthorized()
val resourceName = call.expectUriComponent("USERNAME")
// admin has no rights here.
if ((c.login != resourceName) && (call.getAuthToken() == null))
@@ -98,7 +98,7 @@ fun Routing.transactionsHandlers(db: Database) {
return@post
}
get("/accounts/{USERNAME}/transactions/{T_ID}") {
- val c = call.myAuth(TokenScope.readonly) ?: throw unauthorized()
+ val c = call.myAuth(db, TokenScope.readonly) ?: throw unauthorized()
val accountOwner = call.expectUriComponent("USERNAME")
// auth ok, check rights.
if (c.login != "admin" && c.login != accountOwner)
diff --git a/bank/src/test/kotlin/LibeuFinApiTest.kt
b/bank/src/test/kotlin/LibeuFinApiTest.kt
index 9403c840..23256bd8 100644
--- a/bank/src/test/kotlin/LibeuFinApiTest.kt
+++ b/bank/src/test/kotlin/LibeuFinApiTest.kt
@@ -54,7 +54,9 @@ class LibeuFinApiTest {
assert(db.bankAccountCreate(genBankAccount(barId!!)))
for (i in 1..10) { db.bankTransactionCreate(genTx("test-$i")) }
testApplication {
- application(webApp)
+ application {
+ corebankWebApp(db)
+ }
val asc = client.get("/accounts/foo/transactions?delta=2") {
basicAuth("foo", "pw")
expectSuccess = true
@@ -84,7 +86,9 @@ class LibeuFinApiTest {
assert(db.bankAccountCreate(genBankAccount(barId!!)))
// accounts exist, now create one transaction.
testApplication {
- application(webApp)
+ application {
+ corebankWebApp(db)
+ }
client.post("/accounts/foo/transactions") {
expectSuccess = true
basicAuth("foo", "pw")
@@ -111,7 +115,9 @@ class LibeuFinApiTest {
val db = initDb()
assert(db.customerCreate(customerFoo) != null)
testApplication {
- application(webApp)
+ application {
+ corebankWebApp(db)
+ }
client.post("/accounts/foo/token") {
expectSuccess = true
contentType(ContentType.Application.Json)
@@ -170,7 +176,9 @@ class LibeuFinApiTest {
)
))
testApplication {
- application(webApp)
+ application {
+ corebankWebApp(db)
+ }
val r = client.get("/accounts/foo") {
expectSuccess = true
basicAuth("foo", "pw")
@@ -212,7 +220,9 @@ class LibeuFinApiTest {
val ibanPayto = genIbanPaytoUri()
// Bank needs those to operate:
db.configSet("max_debt_ordinary_customers", "KUDOS:11")
- application(webApp)
+ application {
+ corebankWebApp(db)
+ }
var resp = client.post("/accounts") {
expectSuccess = false
contentType(ContentType.Application.Json)
diff --git a/bank/src/test/kotlin/TalerApiTest.kt
b/bank/src/test/kotlin/TalerApiTest.kt
index 8f88eea6..2f6f3e0a 100644
--- a/bank/src/test/kotlin/TalerApiTest.kt
+++ b/bank/src/test/kotlin/TalerApiTest.kt
@@ -4,7 +4,6 @@ import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.testing.*
import kotlinx.serialization.json.Json
-import org.junit.Ignore
import org.junit.Test
import tech.libeufin.bank.*
import tech.libeufin.util.CryptoUtil
@@ -60,7 +59,9 @@ class TalerApiTest {
))
// Do POST /transfer.
testApplication {
- application(webApp)
+ application {
+ corebankWebApp(db)
+ }
val req = """
{
"request_uid": "entropic 0",
@@ -143,7 +144,9 @@ class TalerApiTest {
)
// Bar expects two entries in the incoming history
testApplication {
- application(webApp)
+ application {
+ corebankWebApp(db)
+ }
val resp =
client.get("/accounts/bar/taler-wire-gateway/history/incoming?delta=5") {
basicAuth("bar", "secret")
expectSuccess = true
@@ -167,7 +170,9 @@ class TalerApiTest {
TalerAmount(1000, 0, "KUDOS")
))
testApplication {
- application(webApp)
+ application {
+ corebankWebApp(db)
+ }
client.post("/accounts/foo/taler-wire-gateway/admin/add-incoming")
{
expectSuccess = true
contentType(ContentType.Application.Json)
@@ -199,7 +204,9 @@ class TalerApiTest {
amount = TalerAmount(1, 0, "KUDOS")
))
testApplication {
- application(webApp)
+ application {
+ corebankWebApp(db)
+ }
val r =
client.post("/taler-integration/withdrawal-operation/${uuid}") {
expectSuccess = true
contentType(ContentType.Application.Json)
@@ -229,7 +236,9 @@ class TalerApiTest {
amount = TalerAmount(1, 0, "KUDOS")
))
testApplication {
- application(webApp)
+ application {
+ corebankWebApp(db)
+ }
val r =
client.get("/taler-integration/withdrawal-operation/${uuid}") {
expectSuccess = true
}
@@ -252,7 +261,9 @@ class TalerApiTest {
val op = db.talerWithdrawalGet(uuid)
assert(op?.aborted == false)
testApplication {
- application(webApp)
+ application {
+ corebankWebApp(db)
+ }
client.post("/accounts/foo/withdrawals/${uuid}/abort") {
expectSuccess = true
basicAuth("foo", "pw")
@@ -268,7 +279,9 @@ class TalerApiTest {
assert(db.customerCreate(customerFoo) != null)
assert(db.bankAccountCreate(bankAccountFoo))
testApplication {
- application(webApp)
+ application {
+ corebankWebApp(db)
+ }
// Creating the withdrawal as if the SPA did it.
val r = client.post("/accounts/foo/withdrawals") {
basicAuth("foo", "pw")
@@ -312,7 +325,9 @@ class TalerApiTest {
// Starting the bank and POSTing as Foo to /confirm the operation.
testApplication {
- application(webApp)
+ application {
+ corebankWebApp(db)
+ }
client.post("/accounts/foo/withdrawals/${uuid}/confirm") {
expectSuccess = true // Sufficient to assert on success.
basicAuth("foo", "pw")
diff --git a/util/src/main/kotlin/TalerConfig.kt
b/util/src/main/kotlin/TalerConfig.kt
index 83ca40a0..e74e9584 100644
--- a/util/src/main/kotlin/TalerConfig.kt
+++ b/util/src/main/kotlin/TalerConfig.kt
@@ -120,6 +120,14 @@ class TalerConfig {
return Optional.ofNullable(lookupEntry(section, option)?.value)
}
+ fun requireValueString(section: String, option: String): String {
+ val entry = lookupEntry(section, option)
+ if (entry == null) {
+ throw TalerConfigError("expected string in configuration section
$section option $option")
+ }
+ return entry.value
+ }
+
/**
* Create a string representation of the loaded configuration.
*/
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- [libeufin] branch master updated: libeufin-bank: implement main(),
gnunet <=