Skip to content

Commit a37a428

Browse files
committed
fix: add workaround for missing duplicated BIP21 check
1 parent b60bede commit a37a428

3 files changed

Lines changed: 89 additions & 0 deletions

File tree

Bitkit/Utilities/Bip21Utils.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import Foundation
2+
3+
/// Utility methods for BIP21 URI handling
4+
enum Bip21Utils {
5+
private static let bip21Prefix = "bitcoin:"
6+
7+
/// Checks if a BIP21 URI is duplicated (contains multiple bitcoin: prefixes).
8+
/// Workaround for https://github.com/synonymdev/bitkit-core/issues/63
9+
/// - Parameter input: The string to check
10+
/// - Returns: true if the input contains duplicated BIP21 URIs, false otherwise
11+
static func isDuplicatedBip21(_ input: String) -> Bool {
12+
let lowercased = input.lowercased()
13+
guard let firstIndex = lowercased.range(of: bip21Prefix)?.upperBound else {
14+
return false
15+
}
16+
return lowercased[firstIndex...].contains(bip21Prefix)
17+
}
18+
}

Bitkit/ViewModels/AppViewModel.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,17 @@ extension AppViewModel {
230230
// Reset send state before handling new data
231231
resetSendState()
232232

233+
// Workaround for duplicated BIP21 URIs (bitkit-core#63)
234+
if Bip21Utils.isDuplicatedBip21(uri) {
235+
toast(
236+
type: .error,
237+
title: t("other__scan_err_decoding"),
238+
description: t("other__scan__error__generic"),
239+
accessibilityIdentifier: "InvalidAddressToast"
240+
)
241+
return
242+
}
243+
233244
let data = try await decode(invoice: uri)
234245

235246
switch data {
@@ -551,6 +562,15 @@ extension AppViewModel {
551562
return
552563
}
553564

565+
// Workaround for duplicated BIP21 URIs (bitkit-core#63)
566+
if Bip21Utils.isDuplicatedBip21(normalized) {
567+
guard currentSequence == manualEntryValidationSequence else { return }
568+
manualEntryValidationResult = .invalid
569+
isManualEntryInputValid = false
570+
showValidationErrorToast(for: .invalid)
571+
return
572+
}
573+
554574
// Try to decode the invoice
555575
guard let decodedData = try? await decode(invoice: normalized) else {
556576
guard currentSequence == manualEntryValidationSequence else { return }

BitkitTests/Bip21UtilsTests.swift

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
@testable import Bitkit
2+
import XCTest
3+
4+
final class Bip21UtilsTests: XCTestCase {
5+
// MARK: - isDuplicatedBip21 Tests
6+
7+
func testIsDuplicatedBip21_ReturnsFalseForSingleValidBIP21URI() {
8+
let input = "bitcoin:bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq?amount=0.001&message=Bitkit"
9+
XCTAssertFalse(Bip21Utils.isDuplicatedBip21(input))
10+
}
11+
12+
func testIsDuplicatedBip21_ReturnsTrueWhenBIP21URIIsDuplicated() {
13+
let first = "bitcoin:bcrt1qr289x0fhg62672e8urudfnxnsr8tcax64xk2vk?amount=0.0000002&message=Bitkit"
14+
let second = "bitcoin:bcrt1qr289x0fhg62672e8urudfnxnsr8tcax64xk2vk?amount=0.0000003&message=Bitkit"
15+
let input = first + second
16+
XCTAssertTrue(Bip21Utils.isDuplicatedBip21(input))
17+
}
18+
19+
func testIsDuplicatedBip21_HandlesCaseInsensitiveBitcoinPrefix() {
20+
let first = "BITCOIN:bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq?amount=0.001"
21+
let second = "bitcoin:bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq?amount=0.002"
22+
let input = first + second
23+
XCTAssertTrue(Bip21Utils.isDuplicatedBip21(input))
24+
}
25+
26+
func testIsDuplicatedBip21_ReturnsFalseForNonBitcoinURIs() {
27+
let input = "lnbc500n1p3k9v3pp5kzmj..."
28+
XCTAssertFalse(Bip21Utils.isDuplicatedBip21(input))
29+
}
30+
31+
func testIsDuplicatedBip21_ReturnsFalseForEmptyString() {
32+
XCTAssertFalse(Bip21Utils.isDuplicatedBip21(""))
33+
}
34+
35+
func testIsDuplicatedBip21_HandlesMixedCaseDuplicatedURIs() {
36+
let first = "Bitcoin:bc1qaddr1?amount=0.001"
37+
let second = "BITCOIN:bc1qaddr2?amount=0.002"
38+
let input = first + second
39+
XCTAssertTrue(Bip21Utils.isDuplicatedBip21(input))
40+
}
41+
42+
func testIsDuplicatedBip21_ReturnsFalseForPlainBitcoinAddress() {
43+
let input = "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"
44+
XCTAssertFalse(Bip21Utils.isDuplicatedBip21(input))
45+
}
46+
47+
func testIsDuplicatedBip21_ReturnsFalseForSingleBIP21WithLightningParam() {
48+
let input = "bitcoin:bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq?lightning=lnbc500n1p3k9v3pp5kzmj"
49+
XCTAssertFalse(Bip21Utils.isDuplicatedBip21(input))
50+
}
51+
}

0 commit comments

Comments
 (0)