Skip to content

Commit 83a470e

Browse files
committed
fix: check duplicated BIP21
1 parent 74a53a5 commit 83a470e

3 files changed

Lines changed: 82 additions & 0 deletions

File tree

app/src/main/java/to/bitkit/utils/Bip21Utils.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,22 @@ import to.bitkit.models.SATS_IN_BTC
44

55
object Bip21Utils {
66

7+
private const val BIP21_PREFIX = "bitcoin:"
8+
9+
/**
10+
* Checks if a BIP21 URI is duplicated (contains multiple bitcoin: prefixes).
11+
* Workaround for https://github.com/synonymdev/bitkit-core/issues/63
12+
* @return true if the input contains duplicated BIP21 URIs, false otherwise
13+
*/
14+
fun isDuplicatedBip21(input: String): Boolean {
15+
val lowercased = input.lowercase()
16+
val firstIndex = lowercased.indexOf(BIP21_PREFIX)
17+
if (firstIndex == -1) return false
18+
19+
val secondIndex = lowercased.indexOf(BIP21_PREFIX, firstIndex + BIP21_PREFIX.length)
20+
return secondIndex != -1
21+
}
22+
723
fun buildBip21Url(
824
bitcoinAddress: String,
925
amountSats: ULong? = null,

app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ import to.bitkit.ui.shared.toast.ToastEventBus
108108
import to.bitkit.ui.shared.toast.ToastQueueManager
109109
import to.bitkit.ui.sheets.SendRoute
110110
import to.bitkit.ui.theme.TRANSITION_SCREEN_MS
111+
import to.bitkit.utils.Bip21Utils
111112
import to.bitkit.utils.Logger
112113
import to.bitkit.utils.NetworkValidationHelper
113114
import to.bitkit.utils.jsonLogOf
@@ -702,6 +703,16 @@ class AppViewModel @Inject constructor(
702703
}
703704

704705
private suspend fun validateAddressWithFeedback(input: String) = withContext(bgDispatcher) {
706+
// TODO Workaround for https://github.com/synonymdev/bitkit-core/issues/63
707+
if (Bip21Utils.isDuplicatedBip21(input)) {
708+
showAddressValidationError(
709+
titleRes = R.string.other__scan_err_decoding,
710+
descriptionRes = R.string.other__scan__error__generic,
711+
testTag = "DuplicatedBip21Toast",
712+
)
713+
return@withContext
714+
}
715+
705716
val scanResult = runCatching { decode(input) }
706717

707718
if (scanResult.isFailure) {
@@ -983,6 +994,17 @@ class AppViewModel @Inject constructor(
983994
resetSendState()
984995
resetQuickPay()
985996

997+
// TODO Workaround for https://github.com/synonymdev/bitkit-core/issues/63
998+
if (Bip21Utils.isDuplicatedBip21(result)) {
999+
toast(
1000+
type = Toast.ToastType.ERROR,
1001+
title = context.getString(R.string.other__scan_err_decoding),
1002+
description = context.getString(R.string.other__scan__error__generic),
1003+
testTag = "DuplicatedBip21Toast",
1004+
)
1005+
return@withContext
1006+
}
1007+
9861008
@Suppress("ForbiddenComment") // TODO: wrap `decode` from bindings in a `CoreService` method and call that one
9871009
val scan = runCatching { decode(result) }
9881010
.onFailure { Logger.error("Failed to decode scan data: '$result'", it, context = TAG) }

app/src/test/java/to/bitkit/utils/Bip21UrlBuilderTest.kt

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import org.junit.Test
55
import org.junit.runner.RunWith
66
import org.junit.runners.JUnit4
77
import to.bitkit.utils.Bip21Utils.buildBip21Url
8+
import to.bitkit.utils.Bip21Utils.isDuplicatedBip21
89

910
@RunWith(JUnit4::class)
1011
class Bip21UrlBuilderTest {
@@ -171,4 +172,47 @@ class Bip21UrlBuilderTest {
171172
val expected = "bitcoin:$address?amount=0.0001&message=Bitkit&lightning=${invoice.encodeToUrl()}"
172173
Assert.assertEquals(expected, buildBip21Url(address, amount, lightningInvoice = invoice))
173174
}
175+
176+
// Tests for isDuplicatedBip21 - Workaround for bitkit-core#63
177+
178+
@Test
179+
fun `isDuplicatedBip21 returns false for single valid BIP21 URI`() {
180+
val input = "bitcoin:bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq?amount=0.001&message=Bitkit"
181+
Assert.assertFalse(isDuplicatedBip21(input))
182+
}
183+
184+
@Test
185+
fun `isDuplicatedBip21 returns true when BIP21 URI is duplicated`() {
186+
val first = "bitcoin:bcrt1qr289x0fhg62672e8urudfnxnsr8tcax64xk2vk?amount=0.0000002&message=Bitkit"
187+
val second = "bitcoin:bcrt1qr289x0fhg62672e8urudfnxnsr8tcax64xk2vk?amount=0.0000003&message=Bitkit"
188+
val input = first + second
189+
Assert.assertTrue(isDuplicatedBip21(input))
190+
}
191+
192+
@Test
193+
fun `isDuplicatedBip21 handles case-insensitive bitcoin prefix`() {
194+
val first = "BITCOIN:bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq?amount=0.001"
195+
val second = "bitcoin:bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq?amount=0.002"
196+
val input = first + second
197+
Assert.assertTrue(isDuplicatedBip21(input))
198+
}
199+
200+
@Test
201+
fun `isDuplicatedBip21 returns false for non-bitcoin URIs`() {
202+
val input = "lnbc500n1p3k9v3pp5kzmj..."
203+
Assert.assertFalse(isDuplicatedBip21(input))
204+
}
205+
206+
@Test
207+
fun `isDuplicatedBip21 returns false for empty string`() {
208+
Assert.assertFalse(isDuplicatedBip21(""))
209+
}
210+
211+
@Test
212+
fun `isDuplicatedBip21 handles mixed case duplicated URIs`() {
213+
val first = "Bitcoin:bc1qaddr1?amount=0.001"
214+
val second = "BITCOIN:bc1qaddr2?amount=0.002"
215+
val input = first + second
216+
Assert.assertTrue(isDuplicatedBip21(input))
217+
}
174218
}

0 commit comments

Comments
 (0)