gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-taler-android] branch master updated: accept decimal and bitcoin


From: gnunet
Subject: [taler-taler-android] branch master updated: accept decimal and bitcoin exchanges
Date: Wed, 15 Jun 2022 19:53:59 +0200

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a commit to branch master
in repository taler-android.

The following commit(s) were added to refs/heads/master by this push:
     new 5b7bb5c  accept decimal and bitcoin exchanges
5b7bb5c is described below

commit 5b7bb5cf012fc41b2fbb6a41f6f858009c1ba092
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Wed Jun 15 14:53:39 2022 -0300

    accept decimal and bitcoin exchanges
---
 .../src/main/java/net/taler/common/Amount.kt       |  11 +
 .../src/main/java/net/taler/common/Bech32.kt       | 257 +++++++++++++++++++++
 .../src/main/java/net/taler/common/CyptoUtils.kt   |  71 ++++++
 .../wallet/withdraw/ManualWithdrawFragment.kt      |  10 +-
 .../withdraw/ManualWithdrawSuccessFragment.kt      | 197 +++++++++++++++-
 .../net/taler/wallet/withdraw/WithdrawManager.kt   |  51 +++-
 .../main/res/layout/fragment_manual_withdraw.xml   |   2 +-
 wallet/src/main/res/values/strings.xml             |   4 +
 .../taler/wallet/withdraw/WithdrawManagerKtTest.kt |  50 ++++
 9 files changed, 638 insertions(+), 15 deletions(-)

diff --git a/taler-kotlin-android/src/main/java/net/taler/common/Amount.kt 
b/taler-kotlin-android/src/main/java/net/taler/common/Amount.kt
index a36c0ab..18fb6cb 100644
--- a/taler-kotlin-android/src/main/java/net/taler/common/Amount.kt
+++ b/taler-kotlin-android/src/main/java/net/taler/common/Amount.kt
@@ -56,12 +56,19 @@ public data class Amount(
     public companion object {
 
         private const val FRACTIONAL_BASE: Int = 100000000 // 1e8
+        public val SEGWIT_MIN = Amount("BTC", 0, 294)
 
         private val REGEX_CURRENCY = Regex("""^[-_*A-Za-z0-9]{1,12}$""")
         public val MAX_VALUE: Long = 2.0.pow(52).toLong()
         private const val MAX_FRACTION_LENGTH = 8
         public const val MAX_FRACTION: Int = 99_999_999
 
+        public fun fromDouble(currency: String, value: Double): Amount {
+            val intPart = Math.floor(value).toLong()
+            val fraPart = Math.floor((value - intPart) *  
FRACTIONAL_BASE).toInt()
+            return Amount(currency, intPart, fraPart)
+        }
+
         public fun zero(currency: String): Amount {
             return Amount(checkCurrency(currency), 0, 0)
         }
@@ -141,6 +148,10 @@ public data class Amount(
         return result
     }
 
+    public fun withCurrency(currency: String): Amount {
+        return Amount(checkCurrency(currency), this.value, this.fraction)
+    }
+
     public operator fun minus(other: Amount): Amount {
         check(currency == other.currency) { "Can only subtract from same 
currency" }
         var resultValue = value
diff --git a/taler-kotlin-android/src/main/java/net/taler/common/Bech32.kt 
b/taler-kotlin-android/src/main/java/net/taler/common/Bech32.kt
new file mode 100644
index 0000000..32885df
--- /dev/null
+++ b/taler-kotlin-android/src/main/java/net/taler/common/Bech32.kt
@@ -0,0 +1,257 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under 
the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
+ * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+// Copyright (c) 2020 Figure Technologies Inc.
+// The contents of this file were derived from an implementation
+// by the btcsuite developers https://github.com/btcsuite/btcutil.
+
+// Copyright (c) 2017 The btcsuite developers
+// Use of this source code is governed by an ISC
+// license that can be found in the LICENSE file.
+
+// modified version of 
https://gist.github.com/iramiller/4ebfcdfbc332a9722c4a4abeb4e16454
+
+import net.taler.common.CyptoUtils
+import kotlin.experimental.and
+import kotlin.experimental.or
+
+
+infix fun Int.min(b: Int): Int = b.takeIf { this > b } ?: this
+infix fun UByte.shl(bitCount: Int) = ((this.toInt() shl bitCount) and 
0xff).toUByte()
+infix fun UByte.shr(bitCount: Int) = (this.toInt() shr bitCount).toUByte()
+
+/**
+ * Given an array of bytes, associate an HRP and return a Bech32Data instance.
+ */
+fun ByteArray.toBech32Data(hrp: String) =
+    Bech32Data(hrp, Bech32.convertBits(this, 8, 5, true))
+
+/**
+ * Using a string in bech32 encoded address format, parses out and returns a 
Bech32Data instance
+ */
+fun String.toBech32Data() = Bech32.decode(this)
+
+/**
+ * Bech32 Data encoding instance containing data for encoding as well as a 
human readable prefix
+ */
+data class Bech32Data(val hrp: String, val fiveBitData: ByteArray) {
+
+    /**
+     * The encapsulated data as typical 8bit bytes.
+     */
+    val data = Bech32.convertBits(fiveBitData, 5, 8, false)
+
+    /**
+     * Address is the Bech32 encoded value of the data prefixed with the human 
readable portion and
+     * protected by an appended checksum.
+     */
+    val address = Bech32.encode(hrp, fiveBitData)
+
+    /**
+     * Checksum for encapsulated data + hrp
+     */
+    val checksum = Bech32.checksum(this.hrp, this.fiveBitData.toTypedArray())
+
+    /**
+     * The Bech32 Address toString prints state information for debugging 
purposes.
+     * @see address() for the bech32 encoded address string output.
+     */
+    override fun toString(): String {
+        return "bech32 : ${this.address}\nhuman: ${this.hrp} \nbytes"
+    }
+}
+
+/**
+ * BIP173 compliant processing functions for handling Bech32 encoding for 
addresses
+ */
+class Bech32 {
+
+    companion object {
+        const val CHECKSUM_SIZE = 6
+        const val MIN_VALID_LENGTH = 8
+        const val MAX_VALID_LENGTH = 90
+        const val MIN_VALID_CODEPOINT = 33
+        const val MAX_VALID_CODEPOINT = 126
+
+        const val charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
+        val gen = intArrayOf(0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 
0x2a1462b3)
+
+        fun generateFakeSegwitAddress(reservePub: String?, addr: String): 
List<String> {
+            if (reservePub == null || reservePub.isEmpty()) return 
listOf<String>()
+            val pub = CyptoUtils.decodeCrock(reservePub)
+            if (pub.size != 32) return listOf()
+
+            val first_rnd = pub.copyOfRange(0,4)
+            val second_rnd = pub.copyOfRange(0,4)
+
+            first_rnd[0] = first_rnd[0].and(0b0111_1111);
+            second_rnd[0] = second_rnd[0].or(0b1000_0000.toByte());
+
+            val first_part = ByteArray(20);
+            first_rnd.copyInto( first_part, 0, 0, 4)
+            pub.copyInto( first_part, 4, 0, 16)
+
+            val second_part = ByteArray(20);
+            second_rnd.copyInto( second_part, 0, 0, 4)
+            pub.copyInto( second_part, 4, 16, 32)
+
+            val zero = ByteArray(1)
+            zero[0] = 0
+            val hrp = when {
+                addr[0] == 'b' && addr[1] == 'c' && addr[2] == 'r' && addr[3] 
== 't' -> "bcrt"
+                addr[0] == 't' && addr[1] == 'b' -> "tb"
+                addr[0] == 'b' && addr[1] == 'c' -> "bc"
+                else -> throw Error("unknown bitcoin net")
+            }
+
+            return listOf(
+                Bech32Data(hrp, zero + convertBits(first_part, 8, 5, 
true)).address,
+                Bech32Data(hrp, zero + convertBits(second_part, 8, 5, 
true)).address,
+            )
+        }
+
+        /**
+         * Decodes a Bech32 String
+         */
+        fun decode(bech32: String): Bech32Data {
+            require(bech32.length >= MIN_VALID_LENGTH && bech32.length <= 
MAX_VALID_LENGTH) { "invalid bech32 string length" }
+            require(bech32.toCharArray().none { c -> c.toInt() < 
MIN_VALID_CODEPOINT || c.toInt() > MAX_VALID_CODEPOINT })
+            { "invalid character in bech32: ${bech32.toCharArray().map { c -> 
c.toInt() }
+                .filter { c -> c.toInt() < MIN_VALID_CODEPOINT || c.toInt() > 
MAX_VALID_CODEPOINT }}" }
+
+            require(bech32.equals(bech32.toLowerCase()) || 
bech32.equals(bech32.toUpperCase()))
+            { "bech32 must be either all upper or lower case" }
+            require(bech32.substring(1).dropLast(CHECKSUM_SIZE).contains('1')) 
{ "invalid index of '1'" }
+
+            val hrp = bech32.substringBeforeLast('1').toLowerCase()
+            val dataString = bech32.substringAfterLast('1').toLowerCase()
+
+            require(dataString.toCharArray().all { c -> charset.contains(c) }) 
{ "invalid data encoding character in bech32"}
+
+            val dataBytes = dataString.map { c -> charset.indexOf(c).toByte() 
}.toByteArray()
+            val checkBytes = dataString.takeLast(CHECKSUM_SIZE).map { c -> 
charset.indexOf(c).toByte() }.toByteArray()
+
+            val actualSum = checksum(hrp, 
dataBytes.dropLast(CHECKSUM_SIZE).toTypedArray())
+            require(1 == polymod(expandHrp(hrp).plus(dataBytes.map { d -> 
d.toInt() }))) { "checksum failed: $checkBytes != $actualSum" }
+
+            return Bech32Data(hrp, 
dataBytes.dropLast(CHECKSUM_SIZE).toByteArray())
+        }
+
+        /**
+         * ConvertBits regroups bytes with toBits set based on reading groups 
of bits as a continuous stream group by fromBits.
+         * This process is used to convert from base64 (from 8) to base32 (to 
5) or the inverse.
+         */
+        fun convertBits(data: ByteArray, fromBits: Int, toBits: Int, pad: 
Boolean): ByteArray {
+            require (fromBits in 1..8 && toBits in 1..8) { "only bit groups 
between 1 and 8 are supported"}
+
+            // resulting bytes with each containing the toBits bits from the 
input set.
+            var regrouped = arrayListOf<Byte>()
+
+            var nextByte = 0.toUByte()
+            var filledBits = 0
+
+            data.forEach { d ->
+                // discard unused bits.
+                var b = (d.toUByte() shl (8 - fromBits))
+
+                // How many bits remain to extract from input data.
+                var remainFromBits = fromBits
+
+                while (remainFromBits > 0) {
+                    // How many bits remain to be copied in
+                    val remainToBits = toBits - filledBits
+
+                    // we extract the remaining bits unless that is more than 
we need.
+                    val toExtract = remainFromBits.takeUnless { remainToBits < 
remainFromBits } ?: remainToBits
+                    check(toExtract >= 0) { "extract should be positive"}
+
+                    // move existing bits to the left to make room for bits 
toExtract, copy in bits to extract
+                    nextByte = (nextByte shl toExtract) or (b shr (8 - 
toExtract))
+
+                    // discard extracted bits and update position counters
+                    b = b shl toExtract
+                    remainFromBits -= toExtract
+                    filledBits += toExtract
+
+                    // if we have a complete group then reset.
+                    if (filledBits == toBits) {
+                        regrouped.add(nextByte.toByte())
+                        filledBits = 0
+                        nextByte = 0.toUByte()
+                    }
+                }
+            }
+
+            // pad any unfinished groups as required
+            if (pad && filledBits > 0) {
+                nextByte = nextByte shl (toBits - filledBits)
+                regrouped.add(nextByte.toByte())
+                filledBits = 0
+                nextByte = 0.toUByte()
+            }
+
+            return regrouped.toByteArray()
+        }
+
+        /**
+         * Encodes data 5-bit bytes (data) with a given human readable portion 
(hrp) into a bech32 string.
+         * @see convertBits for conversion or ideally use the Bech32Data 
extension functions
+         */
+        fun encode(hrp: String, fiveBitData: ByteArray): String {
+            return (fiveBitData.plus(checksum(hrp, fiveBitData.toTypedArray()))
+                .map { b -> charset[b.toInt()] }).joinToString("", hrp + "1")
+        }
+
+        /**
+         * Calculates a bech32 checksum based on BIP 173 specification
+         */
+        fun checksum(hrp: String, data: Array<Byte>): ByteArray {
+            var values = expandHrp(hrp)
+                .plus(data.map { d -> d.toInt() })
+                .plus(Array<Int>(6){ _ -> 0}.toIntArray())
+
+            var poly = polymod(values) xor 1
+
+            return (0..5).map {
+                ((poly shr (5 * (5-it))) and 31).toByte()
+            }.toByteArray()
+        }
+
+        /**
+         * Expands the human readable prefix per BIP173 for Checksum encoding
+         */
+        fun expandHrp(hrp: String) =
+            hrp.map { c -> c.toInt() shr 5 }
+                .plus(0)
+                .plus(hrp.map { c -> c.toInt() and 31 })
+                .toIntArray()
+
+        /**
+         * Polynomial division function for checksum calculation.  For details 
see BIP173
+         */
+        fun polymod(values: IntArray): Int {
+            var chk = 1
+            return values.map {
+                var b = chk shr 25
+                chk = ((chk and 0x1ffffff) shl 5) xor it
+                (0..4).map {
+                    if (((b shr it) and 1) == 1) {
+                        chk = chk xor gen[it]
+                    }
+                }
+            }.let { chk }
+        }
+    }
+}
\ No newline at end of file
diff --git a/taler-kotlin-android/src/main/java/net/taler/common/CyptoUtils.kt 
b/taler-kotlin-android/src/main/java/net/taler/common/CyptoUtils.kt
new file mode 100644
index 0000000..c1fbe8c
--- /dev/null
+++ b/taler-kotlin-android/src/main/java/net/taler/common/CyptoUtils.kt
@@ -0,0 +1,71 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under 
the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
+ * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.common
+
+import kotlin.math.floor
+
+object CyptoUtils {
+    internal fun getValue(c: Char): Int {
+        val a = when (c) {
+            'o','O' -> '0'
+            'i','I','l','L' -> '1'
+            'u','U' -> 'V'
+            else -> c
+        }
+        if (a in '0'..'9') {
+            return a - '0'
+        }
+        val A = if (a in 'a'..'z') a.uppercaseChar() else a
+        var dec = 0
+        if (A in 'A'..'Z') {
+            if ('I' < A) dec++
+            if ('L' < A) dec++
+            if ('O' < A) dec++
+            if ('U' < A) dec++
+            return A - 'A' + 10 - dec
+        }
+        throw Error("encoding error")
+    }
+
+    fun decodeCrock(e: String): ByteArray {
+        val size = e.length
+        var bitpos = 0
+        var bitbuf = 0
+        var readPosition = 0
+        val outLen = floor((size * 5f) / 8).toInt()
+        val out = ByteArray(outLen)
+        var outPos = 0
+        while (readPosition < size || bitpos > 0) {
+            if (readPosition < size) {
+                val v = getValue(e[readPosition++])
+                bitbuf = bitbuf.shl(5).or(v)
+                bitpos += 5
+            }
+            while (bitpos >= 8) {
+                val d = bitbuf.shr(bitpos -8).and(0xff).toByte()
+                out[outPos++] = d
+                bitpos -= 8
+            }
+            if (readPosition == size && bitpos > 0) {
+                bitbuf = bitbuf.shl( 8 - bitpos).and(0xff)
+                bitpos = if (bitbuf == 0) 0 else 8
+            }
+        }
+        return out
+    }
+
+}
\ No newline at end of file
diff --git 
a/wallet/src/main/java/net/taler/wallet/withdraw/ManualWithdrawFragment.kt 
b/wallet/src/main/java/net/taler/wallet/withdraw/ManualWithdrawFragment.kt
index 0cb39d2..660fec2 100644
--- a/wallet/src/main/java/net/taler/wallet/withdraw/ManualWithdrawFragment.kt
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/ManualWithdrawFragment.kt
@@ -66,8 +66,14 @@ class ManualWithdrawFragment : Fragment() {
             return
         }
         ui.amountLayout.error = null
-        val value = ui.amountView.text.toString().toLong()
-        val amount = Amount(exchangeItem.currency, value, 0)
+        var value = 0.0
+        try {
+            value = ui.amountView.text.toString().replace(',', '.').toDouble()
+        } catch (e: NumberFormatException) {
+            ui.amountLayout.error = getString(R.string.withdraw_amount_error)
+            return
+        }
+        val amount = Amount.fromDouble(exchangeItem.currency, value)
         ui.amountView.hideKeyboard()
 
         withdrawManager.getWithdrawalDetails(exchangeItem.exchangeBaseUrl, 
amount)
diff --git 
a/wallet/src/main/java/net/taler/wallet/withdraw/ManualWithdrawSuccessFragment.kt
 
b/wallet/src/main/java/net/taler/wallet/withdraw/ManualWithdrawSuccessFragment.kt
index cdacde6..b638627 100644
--- 
a/wallet/src/main/java/net/taler/wallet/withdraw/ManualWithdrawSuccessFragment.kt
+++ 
b/wallet/src/main/java/net/taler/wallet/withdraw/ManualWithdrawSuccessFragment.kt
@@ -31,6 +31,7 @@ import androidx.compose.foundation.border
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.wrapContentWidth
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.verticalScroll
@@ -44,6 +45,7 @@ import androidx.compose.material.Text
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.ContentCopy
 import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Alignment.Companion.CenterHorizontally
 import androidx.compose.ui.Alignment.Companion.End
 import androidx.compose.ui.Modifier
@@ -53,8 +55,13 @@ import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.colorResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.TextUnitType
 import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.em
+import androidx.compose.ui.unit.sp
 import androidx.core.content.getSystemService
 import androidx.fragment.app.Fragment
 import androidx.fragment.app.activityViewModels
@@ -82,17 +89,26 @@ class ManualWithdrawSuccessFragment : Fragment() {
         val onBankAppClick = if (componentName == null) null else {
             { startActivitySafe(intent) }
         }
-        val onCancelClick = if (status.transactionId == null) null else {
+        val tid = status.transactionId
+        val onCancelClick = if (tid == null) null else {
             {
-                transactionManager.deleteTransaction(status.transactionId)
+                transactionManager.deleteTransaction(tid)
                 
findNavController().navigate(R.id.action_nav_exchange_manual_withdrawal_success_to_nav_main)
             }
         }
         setContent {
             MdcTheme {
                 Surface {
-                    Screen(status, onBankAppClick, onCancelClick)
+                    when (status) {
+                        is WithdrawStatus.ManualTransferRequiredBitcoin -> {
+                            ScreenBitcoin(status, onBankAppClick, 
onCancelClick)
+                        }
+                        is WithdrawStatus.ManualTransferRequiredIBAN -> {
+                            ScreenIBAN(status, onBankAppClick, onCancelClick)
+                        }
+                    }
                 }
+
             }
         }
     }
@@ -104,8 +120,8 @@ class ManualWithdrawSuccessFragment : Fragment() {
 }
 
 @Composable
-private fun Screen(
-    status: WithdrawStatus.ManualTransferRequired,
+private fun ScreenIBAN(
+    status: WithdrawStatus.ManualTransferRequiredIBAN,
     bankAppClick: (() -> Unit)?,
     onCancelClick: (() -> Unit)?,
 ) {
@@ -171,6 +187,154 @@ private fun Screen(
     }
 }
 
+@Composable
+private fun ScreenBitcoin(
+    status: WithdrawStatus.ManualTransferRequiredBitcoin,
+    bankAppClick: (() -> Unit)?,
+    onCancelClick: (() -> Unit)?,
+) {
+    val scrollState = rememberScrollState()
+    Column(modifier = Modifier
+        .padding(all = 16.dp)
+        .wrapContentWidth(CenterHorizontally)
+        .verticalScroll(scrollState)
+    ) {
+        Text(
+            text = stringResource(R.string.withdraw_manual_ready_title),
+            style = MaterialTheme.typography.h5,
+        )
+        Text(
+            text = stringResource(R.string.withdraw_manual_ready_intro,
+                status.amountRaw.toString()),
+            style = MaterialTheme.typography.body1,
+            modifier = Modifier
+                .padding(vertical = 8.dp)
+        )
+        Text(
+            text = 
stringResource(R.string.withdraw_manual_bitcoin_ready_details_intro),
+            style = MaterialTheme.typography.body1,
+            modifier = Modifier
+                .padding(vertical = 8.dp)
+        )
+        Text(
+            text = 
stringResource(R.string.withdraw_manual_bitcoin_ready_details_segwit),
+            style = MaterialTheme.typography.body1,
+            modifier = Modifier
+                .padding(vertical = 8.dp)
+        )
+        DetailRow(stringResource(R.string.withdraw_manual_ready_subject), 
status.subject)
+        Text(
+            text = 
stringResource(R.string.withdraw_manual_bitcoin_ready_details_bitcoincore),
+            style = MaterialTheme.typography.body1,
+            modifier = Modifier
+                .padding(vertical = 8.dp)
+        )
+        BitcoinSegwitAddrs(
+            status.amountRaw,
+            status.account,
+            status.segwitAddrs
+        )
+        Text(
+            text = 
stringResource(R.string.withdraw_manual_bitcoin_ready_details_confirm,
+                status.amountRaw.withCurrency(Amount.SEGWIT_MIN.currency) + 
Amount.SEGWIT_MIN + Amount.SEGWIT_MIN),
+            style = MaterialTheme.typography.body1,
+            modifier = Modifier
+                .padding(vertical = 8.dp)
+        )
+        Text(
+            text = stringResource(R.string.withdraw_manual_ready_warning),
+            style = MaterialTheme.typography.body2,
+            color = colorResource(R.color.notice_text),
+            modifier = Modifier
+                .align(CenterHorizontally)
+                .padding(all = 8.dp)
+                .background(colorResource(R.color.notice_background))
+                .border(BorderStroke(2.dp, 
colorResource(R.color.notice_border)))
+                .padding(all = 16.dp)
+        )
+        if (bankAppClick != null) {
+            Button(
+                onClick = bankAppClick,
+                modifier = Modifier
+                    .padding(vertical = 16.dp)
+                    .align(CenterHorizontally),
+            ) {
+                Text(text = 
stringResource(R.string.withdraw_manual_ready_bank_button))
+            }
+        }
+        if (onCancelClick != null) {
+            Button(
+                onClick = onCancelClick,
+                colors = ButtonDefaults.buttonColors(backgroundColor = 
colorResource(R.color.red)),
+                modifier = Modifier
+                    .padding(vertical = 16.dp)
+                    .align(End),
+            ) {
+                Text(text = 
stringResource(R.string.withdraw_manual_ready_cancel))
+            }
+        }
+    }
+}
+
+@Composable
+fun BitcoinSegwitAddrs(amount: Amount, addr: String, segwitAddrs: 
List<String>) {
+    val context = LocalContext.current
+
+    val sr = segwitAddrs.map { s -> """
+${s} ${Amount.SEGWIT_MIN}
+    """.trimIndent()}.joinToString(separator = "\n")
+    val copyText = """
+${addr} ${amount.withCurrency("BTC")}
+${sr}
+    """.trimIndent()
+
+    Column {
+
+        Row (modifier = Modifier.padding(vertical = 8.dp)){
+            Column (modifier = Modifier.weight(0.3f)) {
+                Text(
+                    text = addr,
+                    style = MaterialTheme.typography.body1,
+                    fontWeight = FontWeight.Normal,
+                    fontSize = 3.em
+                )
+                Text(
+                    text = amount.withCurrency("BTC").toString(),
+                    style = MaterialTheme.typography.body1,
+                    fontWeight = FontWeight.Bold,
+                )
+            }
+        }
+        for(sAddr in segwitAddrs) {
+            Row (modifier = Modifier.padding(vertical = 8.dp)){
+                Column (modifier = Modifier.weight(0.3f)) {
+                    Text(
+                        text = sAddr,
+                        style = MaterialTheme.typography.body1,
+                        fontWeight = FontWeight.Normal,
+                        fontSize = 3.em
+                    )
+                    Text(
+                        text = Amount.SEGWIT_MIN.toString(),
+                        style = MaterialTheme.typography.body1,
+                        fontWeight = FontWeight.Bold,
+                    )
+                }
+            }
+        }
+
+        Row (verticalAlignment = Alignment.CenterVertically) {
+            IconButton(
+                onClick = { copyToClipBoard(context, "this", copyText) },
+            ) { Icon(Icons.Default.ContentCopy, stringResource(R.string.copy)) 
}
+            Text (
+                text = stringResource(R.string.copy),
+                style = MaterialTheme.typography.body1,
+            )
+        }
+    }
+
+}
 @Composable
 fun DetailRow(label: String, content: String, copy: Boolean = true) {
     val context = LocalContext.current
@@ -202,9 +366,9 @@ fun DetailRow(label: String, content: String, copy: Boolean 
= true) {
 
 @Preview
 @Composable
-fun PreviewScreen() {
+fun PreviewScreen2() {
     Surface {
-        Screen(WithdrawStatus.ManualTransferRequired(
+        ScreenIBAN(WithdrawStatus.ManualTransferRequiredIBAN(
             exchangeBaseUrl = "test.exchange.taler.net",
             uri = Uri.parse("https://taler.net";),
             iban = "ASDQWEASDZXCASDQWE",
@@ -215,6 +379,25 @@ fun PreviewScreen() {
     }
 }
 
+@Preview
+@Composable
+fun PreviewScreenBitcoin() {
+    Surface {
+        ScreenBitcoin(WithdrawStatus.ManualTransferRequiredBitcoin(
+            exchangeBaseUrl = "bitcoin.ice.bfh.ch",
+            uri = Uri.parse("https://taler.net";),
+            account = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4",
+            segwitAddrs = listOf<String>(
+                "bc1qqleages8702xvg9qcyu02yclst24xurdrynvxq",
+                "bc1qsleagehks96u7jmqrzcf0fw80ea5g57qm3m84c"
+            ),
+            subject = "0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00",
+            amountRaw = Amount("BITCOINBTC", 0, 14000000),
+            transactionId = "",
+        ), {}) {}
+    }
+}
+
 private fun copyToClipBoard(context: Context, label: String, str: String) {
     val clipboard = context.getSystemService<ClipboardManager>()
     val clip = ClipData.newPlainText(label, str)
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt 
b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt
index d96f421..ca6ba17 100644
--- a/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt
+++ b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt
@@ -16,6 +16,7 @@
 
 package net.taler.wallet.withdraw
 
+import Bech32Data
 import android.net.Uri
 import android.util.Log
 import androidx.annotation.UiThread
@@ -25,6 +26,7 @@ import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 import kotlinx.serialization.Serializable
 import net.taler.common.Amount
+import net.taler.common.CyptoUtils
 import net.taler.common.Event
 import net.taler.common.toEvent
 import net.taler.wallet.TAG
@@ -33,6 +35,11 @@ import net.taler.wallet.backend.WalletBackendApi
 import net.taler.wallet.exchanges.ExchangeFees
 import net.taler.wallet.exchanges.ExchangeItem
 import net.taler.wallet.withdraw.WithdrawStatus.ReceivedDetails
+import toBech32Data
+import kotlin.experimental.and
+import kotlin.experimental.or
+import kotlin.math.floor
+import kotlin.reflect.KProperty
 
 sealed class WithdrawStatus {
     data class Loading(val talerWithdrawUri: String? = null) : WithdrawStatus()
@@ -57,14 +64,30 @@ sealed class WithdrawStatus {
 
     object Withdrawing : WithdrawStatus()
     data class Success(val currency: String) : WithdrawStatus()
-    data class ManualTransferRequired(
+    sealed class ManualTransferRequired: WithdrawStatus() {
+        abstract val uri: Uri
+        abstract val transactionId: String?
+    }
+
+    data class ManualTransferRequiredIBAN(
         val exchangeBaseUrl: String,
-        val uri: Uri,
+        override val uri: Uri,
         val iban: String,
         val subject: String,
         val amountRaw: Amount,
-        val transactionId: String?,
-    ) : WithdrawStatus()
+        override val transactionId: String?,
+    ) : ManualTransferRequired() {
+    }
+
+    data class ManualTransferRequiredBitcoin(
+        val exchangeBaseUrl: String,
+        override val uri: Uri,
+        val account: String,
+        val segwitAddrs: List<String>,
+        val subject: String,
+        val amountRaw: Amount,
+        override val transactionId: String?,
+    ) : ManualTransferRequired()
 
     data class Error(val message: String?) : WithdrawStatus()
 }
@@ -264,6 +287,7 @@ class WithdrawManager(
 
 }
 
+
 fun createManualTransferRequired(
     amount: Amount,
     exchangeBaseUrl: String,
@@ -271,7 +295,22 @@ fun createManualTransferRequired(
     transactionId: String? = null,
 ): WithdrawStatus.ManualTransferRequired {
     val uri = Uri.parse(uriStr)
-    return WithdrawStatus.ManualTransferRequired(
+    if ("bitcoin".equals(uri.authority, true)) {
+        val msg = uri.getQueryParameter("message")
+        val reg = "\\b([A-Z0-9]{52})\\b".toRegex().find(msg.orEmpty())
+        val reserve = reg?.value ?: uri.getQueryParameter("subject")!!
+        val segwitAddrs = Bech32.generateFakeSegwitAddress(reserve, 
uri.pathSegments.first())
+        return WithdrawStatus.ManualTransferRequiredBitcoin(
+            exchangeBaseUrl = exchangeBaseUrl,
+            uri = uri,
+            account = uri.lastPathSegment!!,
+            segwitAddrs = segwitAddrs,
+            subject = reserve,
+            amountRaw = amount,
+            transactionId = transactionId,
+        )
+    }
+    return WithdrawStatus.ManualTransferRequiredIBAN(
         exchangeBaseUrl = exchangeBaseUrl,
         uri = uri,
         iban = uri.lastPathSegment!!,
@@ -280,3 +319,5 @@ fun createManualTransferRequired(
         transactionId = transactionId,
     )
 }
+
+
diff --git a/wallet/src/main/res/layout/fragment_manual_withdraw.xml 
b/wallet/src/main/res/layout/fragment_manual_withdraw.xml
index 724c3e2..ec176ff 100644
--- a/wallet/src/main/res/layout/fragment_manual_withdraw.xml
+++ b/wallet/src/main/res/layout/fragment_manual_withdraw.xml
@@ -76,7 +76,7 @@
             android:id="@+id/amountView"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:inputType="number" />
+            android:inputType="numberDecimal" />
 
     </com.google.android.material.textfield.TextInputLayout>
 
diff --git a/wallet/src/main/res/values/strings.xml 
b/wallet/src/main/res/values/strings.xml
index 7595060..592070c 100644
--- a/wallet/src/main/res/values/strings.xml
+++ b/wallet/src/main/res/values/strings.xml
@@ -119,6 +119,10 @@ GNU Taler is immune against many types of fraud, such as 
phishing of credit card
     <string name="withdraw_manual_ready_title">Exchange is ready for 
withdrawal!</string>
     <string name="withdraw_manual_ready_intro">To complete the process you 
need to wire %s to the exchange bank account</string>
     <string name="withdraw_manual_ready_details_intro">Bank transfer 
details</string>
+    <string name="withdraw_manual_bitcoin_ready_details_intro">Bitcoin 
transfer details</string>
+    <string name="withdraw_manual_bitcoin_ready_details_segwit">The exchange 
need a transaction with 3 output, one output is the exchange account and the 
other two are segwit fake address for metadata with an minimum amount.</string>
+    <string name="withdraw_manual_bitcoin_ready_details_bitcoincore">In 
bitcoincore wallet use \'Add Recipient\' button to add two additional recipient 
and copy addresses and amounts</string>
+    <string name="withdraw_manual_bitcoin_ready_details_confirm">Make sure the 
amount show %s, else you have to change the base unit to BTC</string>
     <string name="withdraw_manual_ready_iban">IBAN</string>
     <string name="withdraw_manual_ready_subject">Subject</string>
     <string name="withdraw_manual_ready_bank_button">Open in banking 
app</string>
diff --git 
a/wallet/src/test/java/net/taler/wallet/withdraw/WithdrawManagerKtTest.kt 
b/wallet/src/test/java/net/taler/wallet/withdraw/WithdrawManagerKtTest.kt
new file mode 100644
index 0000000..519082c
--- /dev/null
+++ b/wallet/src/test/java/net/taler/wallet/withdraw/WithdrawManagerKtTest.kt
@@ -0,0 +1,50 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under 
the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
FOR
+ * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.withdraw
+
+import android.net.Uri
+import net.taler.common.Amount
+import org.junit.Assert
+import org.junit.Test
+
+class WithdrawManagerKtTest {
+
+    @Test
+    fun generateMainnet() {
+        val (addr1, addr2) = 
generateFakeSegwitAddress("54ZN9AMVN1R0YZ68ZPVHHQA4KZE1V037M05FNMYH4JQ596YAKJEG",
 "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq")
+
+        Assert.assertEquals(addr1, 
"bc1q9yl4f23f8a224xagwq8hej8akuvd63yl8nyedj")
+        Assert.assertEquals(addr2, 
"bc1q4yl4f2kurkqx0gq2ltfazf9w2jdu48yaqlghnp")
+    }
+
+    @Test
+    fun generateTestnet() {
+        val (addr1, addr2) = 
generateFakeSegwitAddress("54ZN9AMVN1R0YZ68ZPVHHQA4KZE1V037M05FNMYH4JQ596YAKJEG",
 "tb1qhxrhccqexg0dv4nltgkuw4fg2ce7muplmjsn0v")
+
+        Assert.assertEquals(addr1, 
"tb1q9yl4f23f8a224xagwq8hej8akuvd63yld4l2kp")
+        Assert.assertEquals(addr2, 
"tb1q4yl4f2kurkqx0gq2ltfazf9w2jdu48ya2enygj")
+    }
+
+    @Test
+    fun generateRegnet() {
+        val (addr1, addr2) = 
generateFakeSegwitAddress("54ZN9AMVN1R0YZ68ZPVHHQA4KZE1V037M05FNMYH4JQ596YAKJEG",
 "bcrtqhxrhccqexg0dv4nltgkuw4fg2ce7muplmjsn0v")
+
+        Assert.assertEquals(addr1, 
"bcrt1q9yl4f23f8a224xagwq8hej8akuvd63yl0ux8pg")
+        Assert.assertEquals(addr2, 
"bcrt1q4yl4f2kurkqx0gq2ltfazf9w2jdu48yags2flm")
+
+    }
+}

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]