commit 3bfd71615f61f17708ab97757c8fde428519eeb6
parent 0c8f9082a1f34bc187425e990bba061c3d938838
Author: MS <ms@taler.net>
Date: Mon, 2 Oct 2023 12:44:17 +0200
Implementing accounts deletion.
Diffstat:
12 files changed, 253 insertions(+), 37 deletions(-)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt b/bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt
@@ -19,7 +19,6 @@
package tech.libeufin.bank
-import CreditDebitIndicator
import io.ktor.http.*
import io.ktor.server.application.*
import kotlinx.serialization.Serializable
@@ -476,6 +475,16 @@ fun ApplicationCall.getResourceName(param: String): ResourceName =
this.expectUriComponent(param)
/**
+ * This type communicates the result of deleting an account
+ * from the database.
+ */
+enum class CustomerDeletionResult {
+ SUCCESS,
+ CUSTOMER_NOT_FOUND,
+ BALANCE_NOT_ZERO
+}
+
+/**
* This type communicates the result of a database operation
* to confirm one withdrawal operation.
*/
@@ -496,6 +505,16 @@ enum class WithdrawalConfirmationResult {
CONFLICT
}
+/**
+ * Communicates the result of creating a bank transaction in the database.
+ */
+enum class BankTransactionResult {
+ NO_CREDITOR,
+ NO_DEBTOR,
+ SUCCESS,
+ CONFLICT // balance insufficient
+}
+
// GET /config response from the Taler Integration API.
@Serializable
data class TalerIntegrationConfigResponse(
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt
@@ -185,13 +185,10 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) {
transactionDate = Instant.now()
)
when (db.bankTransactionCreate(adminPaysBonus)) {
- Database.BankTransactionResult.NO_CREDITOR -> throw internalServerError("Bonus impossible: creditor not found, despite its recent creation.")
-
- Database.BankTransactionResult.NO_DEBTOR -> throw internalServerError("Bonus impossible: admin not found.")
-
- Database.BankTransactionResult.CONFLICT -> throw internalServerError("Bonus impossible: admin has insufficient balance.")
-
- Database.BankTransactionResult.SUCCESS -> {/* continue the execution */
+ BankTransactionResult.NO_CREDITOR -> throw internalServerError("Bonus impossible: creditor not found, despite its recent creation.")
+ BankTransactionResult.NO_DEBTOR -> throw internalServerError("Bonus impossible: admin not found.")
+ BankTransactionResult.CONFLICT -> throw internalServerError("Bonus impossible: admin has insufficient balance.")
+ BankTransactionResult.SUCCESS -> {/* continue the execution */
}
}
}
@@ -257,6 +254,36 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) {
)
return@get
}
+ delete("/accounts/{USERNAME}") {
+ val c = call.authenticateBankRequest(db, TokenScope.readwrite) ?: throw unauthorized()
+ val resourceName = call.expectUriComponent("USERNAME")
+ // Checking rights.
+ if (c.login != "admin" && ctx.restrictAccountDeletion)
+ throw forbidden("Only admin allowed.")
+ if (!resourceName.canI(c, withAdmin = true))
+ throw forbidden("Insufficient rights on this account.")
+ // Not deleting reserved names.
+ if (resourceName == "bank" || resourceName == "admin")
+ throw forbidden("Cannot delete reserved accounts.")
+ val res = db.customerDeleteIfBalanceIsZero(resourceName)
+ when (res) {
+ CustomerDeletionResult.CUSTOMER_NOT_FOUND ->
+ throw notFound(
+ "Customer '$resourceName' not found",
+ talerEc = TalerErrorCode.TALER_EC_NONE // FIXME: need EC.
+ )
+ CustomerDeletionResult.BALANCE_NOT_ZERO ->
+ throw LibeufinBankException(
+ httpStatus = HttpStatusCode.PreconditionFailed,
+ talerError = TalerError(
+ hint = "Balance is not zero.",
+ code = TalerErrorCode.TALER_EC_NONE.code // FIXME: need EC.
+ )
+ )
+ CustomerDeletionResult.SUCCESS -> call.respond(HttpStatusCode.NoContent)
+ }
+ return@delete
+ }
post("/accounts/{USERNAME}/withdrawals") {
val c = call.authenticateBankRequest(db, TokenScope.readwrite)
@@ -435,13 +462,13 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) {
)
val res = db.bankTransactionCreate(dbInstructions)
when (res) {
- Database.BankTransactionResult.CONFLICT -> throw conflict(
+ BankTransactionResult.CONFLICT -> throw conflict(
"Insufficient funds",
TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
)
- Database.BankTransactionResult.NO_CREDITOR -> throw internalServerError("Creditor not found despite previous checks.")
- Database.BankTransactionResult.NO_DEBTOR -> throw internalServerError("Debtor not found despite the request was authenticated.")
- Database.BankTransactionResult.SUCCESS -> call.respond(HttpStatusCode.OK)
+ BankTransactionResult.NO_CREDITOR -> throw internalServerError("Creditor not found despite previous checks.")
+ BankTransactionResult.NO_DEBTOR -> throw internalServerError("Debtor not found despite the request was authenticated.")
+ BankTransactionResult.SUCCESS -> call.respond(HttpStatusCode.OK)
}
return@post
}
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
@@ -21,7 +21,6 @@
package tech.libeufin.bank
import org.postgresql.jdbc.PgConnection
-import org.postgresql.util.PSQLException
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import tech.libeufin.util.getJdbcConnectionFromPg
@@ -231,6 +230,27 @@ class Database(private val dbConfig: String, private val bankCurrency: String) {
}
}
+ /**
+ * Deletes a customer (including its bank account row) from
+ * the database. The bank account gets deleted by the cascade.
+ */
+ fun customerDeleteIfBalanceIsZero(login: String): CustomerDeletionResult {
+ reconnect()
+ val stmt = prepare("""
+ SELECT
+ out_nx_customer,
+ out_balance_not_zero
+ FROM customer_delete(?);
+ """)
+ stmt.setString(1, login)
+ stmt.executeQuery().apply {
+ if (!this.next()) throw internalServerError("Deletion returned nothing.")
+ if (this.getBoolean("out_nx_customer")) return CustomerDeletionResult.CUSTOMER_NOT_FOUND
+ if (this.getBoolean("out_balance_not_zero")) return CustomerDeletionResult.BALANCE_NOT_ZERO
+ return CustomerDeletionResult.SUCCESS
+ }
+ }
+
// Mostly used to get customers out of bearer tokens.
fun customerGetFromRowId(customer_id: Long): Customer? {
reconnect()
@@ -304,6 +324,7 @@ class Database(private val dbConfig: String, private val bankCurrency: String) {
)
}
}
+
// Possibly more "customerGetFrom*()" to come.
// BEARER TOKEN
@@ -566,12 +587,7 @@ class Database(private val dbConfig: String, private val bankCurrency: String) {
}
// BANK ACCOUNT TRANSACTIONS
- enum class BankTransactionResult {
- NO_CREDITOR,
- NO_DEBTOR,
- SUCCESS,
- CONFLICT // balance insufficient
- }
+
fun bankTransactionCreate(
tx: BankInternalTransaction
): BankTransactionResult {
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
@@ -83,6 +83,10 @@ data class BankApplicationContext(
*/
val restrictRegistration: Boolean,
/**
+ * Restrict account deletion to the administrator.
+ */
+ val restrictAccountDeletion: Boolean,
+ /**
* Cashout currency, if cashouts are supported.
*/
val cashoutCurrency: String?,
@@ -480,6 +484,7 @@ fun readBankApplicationContextFromConfig(cfg: TalerConfig): BankApplicationConte
suggestedWithdrawalExchange = cfg.lookupValueString("libeufin-bank", "suggested_withdrawal_exchange"),
defaultAdminDebtLimit = cfg.requireValueAmount("libeufin-bank", "default_admin_debt_limit", currency),
spaCaptchaURL = cfg.lookupValueString("libeufin-bank", "spa_captcha_url"),
+ restrictAccountDeletion = cfg.lookupValueBooleanDefault("libeufin-bank", "restrict_account_deletion", true)
)
}
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt
@@ -123,12 +123,12 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx: BankApplicationContext)
exchangeBankAccountId = exchangeBankAccount.expectRowId(),
timestamp = transferTimestamp
)
- if (dbRes.txResult == Database.BankTransactionResult.CONFLICT)
+ if (dbRes.txResult == BankTransactionResult.CONFLICT)
throw conflict(
"Insufficient balance for exchange",
TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
)
- if (dbRes.txResult == Database.BankTransactionResult.NO_CREDITOR)
+ if (dbRes.txResult == BankTransactionResult.NO_CREDITOR)
throw notFound(
"Creditor account was not found",
TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
@@ -179,7 +179,7 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx: BankApplicationContext)
* Other possible errors are highly unlikely, because of the
* previous checks on the existence of the involved bank accounts.
*/
- if (res == Database.BankTransactionResult.CONFLICT)
+ if (res == BankTransactionResult.CONFLICT)
throw conflict(
"Insufficient balance",
TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
diff --git a/bank/src/test/kotlin/Common.kt b/bank/src/test/kotlin/Common.kt
@@ -47,6 +47,7 @@ fun getTestContext(
registrationBonus = null,
suggestedWithdrawalExchange = suggestedExchange,
spaCaptchaURL = null,
+ restrictAccountDeletion = true
)
}
diff --git a/bank/src/test/kotlin/DatabaseTest.kt b/bank/src/test/kotlin/DatabaseTest.kt
@@ -18,8 +18,10 @@
*/
import org.junit.Test
+import org.postgresql.jdbc.PgConnection
import tech.libeufin.bank.*
import tech.libeufin.util.CryptoUtil
+import java.sql.DriverManager
import java.time.Instant
import java.util.Random
import java.util.UUID
@@ -125,7 +127,7 @@ class DatabaseTest {
exchangeBankAccountId = 1L,
timestamp = Instant.now()
)
- assert(res.txResult == Database.BankTransactionResult.SUCCESS)
+ assert(res.txResult == BankTransactionResult.SUCCESS)
}
@Test
@@ -167,13 +169,13 @@ class DatabaseTest {
TalerAmount(50, 0, currency)
)
val firstSpending = db.bankTransactionCreate(fooPaysBar) // Foo pays Bar and goes debit.
- assert(firstSpending == Database.BankTransactionResult.SUCCESS)
+ assert(firstSpending == BankTransactionResult.SUCCESS)
fooAccount = db.bankAccountGetFromOwnerId(fooId)
// Foo: credit -> debit
assert(fooAccount?.hasDebt == true) // Asserting Foo's debit.
// Now checking that more spending doesn't get Foo out of debit.
val secondSpending = db.bankTransactionCreate(fooPaysBar)
- assert(secondSpending == Database.BankTransactionResult.SUCCESS)
+ assert(secondSpending == BankTransactionResult.SUCCESS)
fooAccount = db.bankAccountGetFromOwnerId(fooId)
// Checking that Foo's debit is two times the paid amount
// Foo: debit -> debit
@@ -200,14 +202,14 @@ class DatabaseTest {
transactionDate = Instant.now()
)
val barPays = db.bankTransactionCreate(barPaysFoo)
- assert(barPays == Database.BankTransactionResult.SUCCESS)
+ assert(barPays == BankTransactionResult.SUCCESS)
barAccount = db.bankAccountGetFromOwnerId(barId)
val barBalanceTen: TalerAmount? = barAccount?.balance
// Bar: credit -> credit
assert(barAccount?.hasDebt == false && barBalanceTen?.value == 10L && barBalanceTen.frac == 0)
// Bar pays again to let Foo return in credit.
val barPaysAgain = db.bankTransactionCreate(barPaysFoo)
- assert(barPaysAgain == Database.BankTransactionResult.SUCCESS)
+ assert(barPaysAgain == BankTransactionResult.SUCCESS)
// Refreshing the two accounts.
barAccount = db.bankAccountGetFromOwnerId(barId)
fooAccount = db.bankAccountGetFromOwnerId(fooId)
@@ -218,7 +220,7 @@ class DatabaseTest {
assert(barAccount?.balance?.equals(TalerAmount(0, 0, "KUDOS")) == true)
// Bringing Bar to debit.
val barPaysMore = db.bankTransactionCreate(barPaysFoo)
- assert(barPaysMore == Database.BankTransactionResult.SUCCESS)
+ assert(barPaysMore == BankTransactionResult.SUCCESS)
barAccount = db.bankAccountGetFromOwnerId(barId)
fooAccount = db.bankAccountGetFromOwnerId(fooId)
// Bar: credit -> debit
@@ -226,6 +228,32 @@ class DatabaseTest {
assert(fooAccount?.balance?.equals(TalerAmount(10, 0, "KUDOS")) == true)
assert(barAccount?.balance?.equals(TalerAmount(10, 0, "KUDOS")) == true)
}
+
+ // Testing customer(+bank account) deletion logic.
+ @Test
+ fun customerDeletionTest() {
+ val db = initDb()
+ // asserting false, as foo doesn't exist yet.
+ assert(db.customerDeleteIfBalanceIsZero("foo") == CustomerDeletionResult.CUSTOMER_NOT_FOUND)
+ // Creating foo.
+ db.customerCreate(customerFoo).apply {
+ assert(this != null)
+ assert(db.bankAccountCreate(bankAccountFoo) != null)
+ }
+ // foo has zero balance, deletion should succeed.
+ assert(db.customerDeleteIfBalanceIsZero("foo") == CustomerDeletionResult.SUCCESS)
+ val db2 = initDb()
+ // Creating foo again, artificially setting its balance != zero.
+ db2.customerCreate(customerFoo).apply {
+ assert(this != null)
+ db2.bankAccountCreate(bankAccountFoo).apply {
+ assert(this != null)
+ val conn = DriverManager.getConnection("jdbc:postgresql:///libeufincheck").unwrap(PgConnection::class.java)
+ conn.execSQLUpdate("UPDATE libeufin_bank.bank_accounts SET balance.frac = 1 WHERE bank_account_id = $this")
+ }
+ }
+ assert(db.customerDeleteIfBalanceIsZero("foo") == CustomerDeletionResult.BALANCE_NOT_ZERO)
+ }
@Test
fun customerCreationTest() {
val db = initDb()
@@ -346,7 +374,7 @@ class DatabaseTest {
paymentInformationId = "pmtinfid",
transactionDate = Instant.now()
)
- ) == Database.BankTransactionResult.SUCCESS)
+ ) == BankTransactionResult.SUCCESS)
// Confirming the cash-out
assert(db.cashoutConfirm(op.cashoutUuid, 1L, 1L))
// Checking the confirmation took place.
diff --git a/bank/src/test/kotlin/LibeuFinApiTest.kt b/bank/src/test/kotlin/LibeuFinApiTest.kt
@@ -2,13 +2,16 @@ import io.ktor.client.plugins.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
+import io.ktor.server.engine.*
import io.ktor.server.testing.*
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import net.taler.wallet.crypto.Base32Crockford
import org.junit.Test
+import org.postgresql.jdbc.PgConnection
import tech.libeufin.bank.*
import tech.libeufin.util.CryptoUtil
+import java.sql.DriverManager
import java.time.Duration
import java.time.Instant
import java.time.temporal.ChronoUnit
@@ -518,6 +521,66 @@ class LibeuFinApiTest {
}
/**
+ * Tests DELETE /accounts/foo
+ */
+ @Test
+ fun deleteAccount() {
+ val db = initDb()
+ val ctx = getTestContext()
+ val adminCustomer = Customer(
+ "admin",
+ CryptoUtil.hashpw("pass"),
+ "CFO"
+ )
+ db.customerCreate(adminCustomer)
+ testApplication {
+ application {
+ corebankWebApp(db, ctx)
+ }
+ // account to delete doesn't exist.
+ client.delete("/accounts/foo") {
+ basicAuth("admin", "pass")
+ expectSuccess = false
+ }.apply {
+ assert(this.status == HttpStatusCode.NotFound)
+ }
+ // account to delete is reserved.
+ client.delete("/accounts/admin") {
+ basicAuth("admin", "pass")
+ expectSuccess = false
+ }.apply {
+ assert(this.status == HttpStatusCode.Forbidden)
+ }
+ // successful deletion
+ db.customerCreate(customerFoo).apply {
+ assert(this != null)
+ assert(db.bankAccountCreate(genBankAccount(this!!)) != null)
+ }
+ client.delete("/accounts/foo") {
+ basicAuth("admin", "pass")
+ expectSuccess = false
+ }.apply {
+ assert(this.status == HttpStatusCode.NoContent)
+ }
+ // fail to delete, due to a non-zero balance.
+ db.customerCreate(customerBar).apply {
+ assert(this != null)
+ db.bankAccountCreate(genBankAccount(this!!)).apply {
+ assert(this != null)
+ val conn = DriverManager.getConnection("jdbc:postgresql:///libeufincheck").unwrap(PgConnection::class.java)
+ conn.execSQLUpdate("UPDATE libeufin_bank.bank_accounts SET balance.val = 1 WHERE bank_account_id = $this")
+ }
+ }
+ client.delete("/accounts/bar") {
+ basicAuth("admin", "pass")
+ expectSuccess = false
+ }.apply {
+ assert(this.status == HttpStatusCode.PreconditionFailed)
+ }
+ }
+ }
+
+ /**
* Tests the GET /accounts endpoint.
*/
@Test
@@ -544,12 +607,12 @@ class LibeuFinApiTest {
// foo account
db.customerCreate(customerFoo).apply {
assert(this != null)
- db.bankAccountCreate(genBankAccount(this!!)) != null
+ assert(db.bankAccountCreate(genBankAccount(this!!)) != null)
}
// bar account
db.customerCreate(customerBar).apply {
assert(this != null)
- db.bankAccountCreate(genBankAccount(this!!)) != null
+ assert(db.bankAccountCreate(genBankAccount(this!!)) != null)
}
// Two users registered, requesting all of them.
client.get("/accounts") {
diff --git a/bank/src/test/kotlin/TalerApiTest.kt b/bank/src/test/kotlin/TalerApiTest.kt
@@ -4,6 +4,7 @@ import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.testing.*
import kotlinx.serialization.json.Json
+import net.taler.wallet.crypto.Base32Crockford
import org.junit.Test
import tech.libeufin.bank.*
import tech.libeufin.util.CryptoUtil
@@ -151,14 +152,14 @@ class TalerApiTest {
// Foo pays Bar (the exchange) twice.
val reservePubOne = "5ZFS98S1K4Y083W95GVZK638TSRE44RABVASB3AFA3R95VCW17V0"
val reservePubTwo = "TFBT5NEVT8D2GETZ4DRF7C69XZHKHJ15296HRGB1R5ARNK0SP8A0"
- assert(db.bankTransactionCreate(genTx(reservePubOne)) == Database.BankTransactionResult.SUCCESS)
- assert(db.bankTransactionCreate(genTx(reservePubTwo)) == Database.BankTransactionResult.SUCCESS)
+ assert(db.bankTransactionCreate(genTx(reservePubOne)) == BankTransactionResult.SUCCESS)
+ assert(db.bankTransactionCreate(genTx(reservePubTwo)) == BankTransactionResult.SUCCESS)
// Should not show up in the taler wire gateway API history
- assert(db.bankTransactionCreate(genTx("bogus foobar")) == Database.BankTransactionResult.SUCCESS)
+ assert(db.bankTransactionCreate(genTx("bogus foobar")) == BankTransactionResult.SUCCESS)
// Bar pays Foo once, but that should not appear in the result.
assert(
db.bankTransactionCreate(genTx("payout", creditorId = 1, debtorId = 2)) ==
- Database.BankTransactionResult.SUCCESS
+ BankTransactionResult.SUCCESS
)
// Bar expects two entries in the incoming history
testApplication {
@@ -172,9 +173,9 @@ class TalerApiTest {
val j: IncomingHistory = Json.decodeFromString(resp.bodyAsText())
assert(j.incoming_transactions.size == 2)
// Testing ranges.
- val mockReservePub = "X".repeat(52)
+ val mockReservePub = Base32Crockford.encode(ByteArray(32))
for (i in 1..400)
- assert(db.bankTransactionCreate(genTx(mockReservePub)) == Database.BankTransactionResult.SUCCESS)
+ assert(db.bankTransactionCreate(genTx(mockReservePub)) == BankTransactionResult.SUCCESS)
// forward range:
val range = client.get("/accounts/bar/taler-wire-gateway/history/incoming?delta=10&start=30") {
basicAuth("bar", "secret")
diff --git a/database-versioning/libeufin-bank-0001.sql b/database-versioning/libeufin-bank-0001.sql
@@ -91,6 +91,7 @@ CREATE TABLE IF NOT EXISTS bank_accounts
,internal_payto_uri TEXT NOT NULL UNIQUE
,owning_customer_id BIGINT NOT NULL UNIQUE -- UNIQUE enforces 1-1 map with customers
REFERENCES customers(customer_id)
+ ON DELETE CASCADE
,is_public BOOLEAN DEFAULT FALSE NOT NULL -- privacy by default
,is_taler_exchange BOOLEAN DEFAULT FALSE NOT NULL
,last_nexus_fetch_row_id BIGINT
diff --git a/database-versioning/procedures.sql b/database-versioning/procedures.sql
@@ -86,6 +86,57 @@ END $$;
COMMENT ON PROCEDURE bank_set_config(TEXT, TEXT)
IS 'Update or insert configuration values';
+CREATE OR REPLACE FUNCTION customer_delete(
+ IN in_login TEXT,
+ OUT out_nx_customer BOOLEAN,
+ OUT out_balance_not_zero BOOLEAN
+)
+LANGUAGE plpgsql
+AS $$
+DECLARE
+my_customer_id BIGINT;
+my_balance_val INT8;
+my_balance_frac INT4;
+BEGIN
+-- check if login exists
+SELECT customer_id
+ INTO my_customer_id
+ FROM customers
+ WHERE login = in_login;
+IF NOT FOUND
+THEN
+ out_nx_customer=TRUE;
+ RETURN;
+END IF;
+out_nx_customer=FALSE;
+
+-- get the balance
+SELECT
+ (balance).val as balance_val,
+ (balance).frac as balance_frac
+ INTO
+ my_balance_val,
+ my_balance_frac
+ FROM bank_accounts
+ WHERE owning_customer_id = my_customer_id;
+IF NOT FOUND
+THEN
+ RAISE EXCEPTION 'Invariant failed: customer lacks bank account';
+END IF;
+-- check that balance is zero.
+IF my_balance_val != 0 OR my_balance_frac != 0
+THEN
+ out_balance_not_zero=TRUE;
+ RETURN;
+END IF;
+out_balance_not_zero=FALSE;
+
+-- actual deletion
+DELETE FROM customers WHERE login = in_login;
+END $$;
+COMMENT ON FUNCTION customer_delete(TEXT)
+ IS 'Deletes a customer (and its bank account via cascade) if the balance is zero';
+
CREATE OR REPLACE FUNCTION taler_transfer(
IN in_request_uid TEXT,
IN in_wtid TEXT,
diff --git a/util/src/main/kotlin/Encoding.kt b/util/src/main/kotlin/Encoding.kt
@@ -53,6 +53,10 @@ object Base32Crockford {
return sb.toString()
}
+ /**
+ * Decodes the input to its binary representation, throws
+ * net.taler.wallet.crypto.EncodingException on invalid encodings.
+ */
fun decode(encoded: String, out: ByteArrayOutputStream) {
val size = encoded.length
var bitpos = 0