Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions Bitkit/Services/PaykitIssuerInterop.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Foundation
import LDKNode

enum PaykitIssuerInterop {
static let bitcoinAsset = "btc"

struct EndpointPayload: Equatable {
let value: String
let min: String?
let max: String?
}

static func supportedEndpointIdentifiers(_ identifiers: [String], network: LDKNode.Network) -> [String] {
var seen = Set<String>()
return identifiers.filter { identifier in
guard seen.insert(identifier).inserted,
let methodId = PublicPaykitService.MethodId(rawValue: identifier)
else { return false }

if let onchainNetwork = methodId.onchainNetwork {
return onchainNetwork == network
}

return methodId == .bitcoinLightningBolt11 || methodId == .bitcoinLightningLnurl
}
}

static func parseEndpointPayload(_ endpointData: String) -> EndpointPayload? {
let trimmedPayload = endpointData.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedPayload.isEmpty,
let data = trimmedPayload.data(using: .utf8),
let payloadObject = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let value = (payloadObject["value"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines),
!value.isEmpty,
payloadObject["min"] == nil || payloadObject["min"] is String || payloadObject["min"] is NSNull,
payloadObject["max"] == nil || payloadObject["max"] is String || payloadObject["max"] is NSNull
else { return nil }

return EndpointPayload(
value: value,
min: payloadObject["min"] as? String,
Comment thread
ovitrif marked this conversation as resolved.
max: payloadObject["max"] as? String
)
}
}
37 changes: 14 additions & 23 deletions Bitkit/Services/PaykitPaymentRequestService.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Foundation
import LDKNode
import Paykit

struct PaykitPaymentRequest: Identifiable, Hashable {
Expand Down Expand Up @@ -39,15 +40,15 @@ struct PaykitPaymentRequest: Identifiable, Hashable {
)
}

init?(record: Paykit.PaymentRequestRecord, now: Date) {
self.init(record: record, expectedRole: .payer, now: now, requiresActionableRequest: true)
init?(record: Paykit.PaymentRequestRecord, now: Date, network: LDKNode.Network = Env.network) {
self.init(record: record, expectedRole: .payer, now: now, network: network, requiresActionableRequest: true)
}

init?(historyRecord: Paykit.PaymentRequestRecord, now: Date) {
init?(historyRecord: Paykit.PaymentRequestRecord, now: Date, network: LDKNode.Network = Env.network) {
guard let localRole = historyRecord.localRole else { return nil }
switch localRole {
case .payer, .payee:
self.init(record: historyRecord, expectedRole: localRole, now: now, requiresActionableRequest: false)
self.init(record: historyRecord, expectedRole: localRole, now: now, network: network, requiresActionableRequest: false)
case .unknown:
return nil
}
Expand All @@ -57,13 +58,14 @@ struct PaykitPaymentRequest: Identifiable, Hashable {
record: Paykit.PaymentRequestRecord,
expectedRole: Paykit.PaymentRequestLocalRole,
now: Date,
network: LDKNode.Network,
requiresActionableRequest: Bool
) {
guard record.localRole == expectedRole,
record.state != .activeRecurring,
let terms = record.terms,
terms.recurrence == nil,
terms.amount.asset == "btc",
terms.amount.asset == PaykitIssuerInterop.bitcoinAsset,
let amountSats = Self.sats(fromBitcoinAmount: terms.amount.value),
amountSats <= UInt64.max / 1000
else { return nil }
Expand All @@ -72,8 +74,9 @@ struct PaykitPaymentRequest: Identifiable, Hashable {
return nil
}

let acceptedPaymentEndpointIdentifiers = Self.supportedEndpointIdentifiers(
terms.acceptedPaymentEndpointIdentifiers
let acceptedPaymentEndpointIdentifiers = PaykitIssuerInterop.supportedEndpointIdentifiers(
terms.acceptedPaymentEndpointIdentifiers,
network: network
)
if requiresActionableRequest, acceptedPaymentEndpointIdentifiers.isEmpty {
return nil
Expand Down Expand Up @@ -187,21 +190,6 @@ struct PaykitPaymentRequest: Identifiable, Hashable {
amountSats == self.amountSats
}

private static func supportedEndpointIdentifiers(_ identifiers: [String]) -> [String] {
var seen = Set<String>()
return identifiers.filter { identifier in
guard seen.insert(identifier).inserted,
let methodId = PublicPaykitService.MethodId(rawValue: identifier)
else { return false }

if let network = methodId.onchainNetwork {
return network == Env.network
}

return methodId == .bitcoinLightningBolt11 || methodId == .bitcoinLightningLnurl
}
}

private static func sats(fromBitcoinAmount amount: String) -> UInt64? {
let components = amount.split(separator: ".", maxSplits: 1, omittingEmptySubsequences: false)
let digits = components.joined()
Expand Down Expand Up @@ -419,7 +407,10 @@ struct PaykitPaymentRequestService {
let metadataData = try JSONSerialization.data(withJSONObject: ["note": draft.note])
let metadataText = String(decoding: metadataData, as: UTF8.self)
let terms = try Paykit.PaymentRequestTerms(
amount: Paykit.PaymentRequestAmount(value: WalletViewModel.formatBitcoinAmount(sats: draft.amountSats), asset: "btc"),
amount: Paykit.PaymentRequestAmount(
value: WalletViewModel.formatBitcoinAmount(sats: draft.amountSats),
asset: PaykitIssuerInterop.bitcoinAsset
),
paymentReference: Paykit.PaymentReference(text: "bitkit-\(UUID().uuidString)"),
proposalExpiresAt: Self.timestamp(draft.expiresAt),
recurrence: nil,
Expand Down
2 changes: 1 addition & 1 deletion Bitkit/Services/PrivatePaykitService+Payments.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ extension PrivatePaykitService {
let consumedVersion = state.contacts[publicKey]?.consumedPrivatePaymentListVersionsByReceiverPath[receiverPath]
let previousPaymentListVersion = consumedVersion.map(String.init) ?? "none"
let amount = paymentRequest.map {
PaymentAmountContext(value: $0.amountValue, asset: "btc")
PaymentAmountContext(value: $0.amountValue, asset: PaykitIssuerInterop.bitcoinAsset)
}

do {
Expand Down
38 changes: 9 additions & 29 deletions Bitkit/Services/PublicPaykitService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -218,12 +218,19 @@ enum PublicPaykitService {
return MethodId.payablePreferenceOrder.compactMap { endpointsByMethodId[$0] }
}

static func parseEndpoint(methodId rawMethodId: String, endpointData: String) -> Endpoint? {
static func parseEndpoint(
methodId rawMethodId: String,
endpointData: String,
network: LDKNode.Network = Env.network
) -> Endpoint? {
guard let methodId = MethodId(rawValue: rawMethodId) else {
return nil
}
if let onchainNetwork = methodId.onchainNetwork, onchainNetwork != network {
return nil
}

guard let payload = parsePayload(endpointData) else {
guard let payload = PaykitIssuerInterop.parseEndpointPayload(endpointData) else {
Comment thread
ovitrif marked this conversation as resolved.
return nil
}

Expand Down Expand Up @@ -394,33 +401,6 @@ enum PublicPaykitService {
return invoice.routeHints().contains { !$0.isEmpty }
}

private struct ParsedPayload {
let value: String
let min: String?
let max: String?
}

private static func parsePayload(_ endpointData: String) -> ParsedPayload? {
let trimmedPayload = endpointData.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedPayload.isEmpty else {
return nil
}

if let data = trimmedPayload.data(using: .utf8),
let payloadObject = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let value = (payloadObject["value"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines),
!value.isEmpty
{
return ParsedPayload(
value: value,
min: payloadObject["min"] as? String,
max: payloadObject["max"] as? String
)
}

return nil
}

private static func applyPublishedEndpoints(_ desiredEndpoints: [Endpoint]) async throws {
try await endpointLock.withLock {
let report = try await PaykitSdkService.shared.syncPublicEndpoints(desiredEndpoints)
Expand Down
2 changes: 2 additions & 0 deletions Bitkit/Views/PaymentRequests/PaymentRequestsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ struct PaymentRequestCard: View {
await onReject?()
isRejecting = false
}
.accessibilityIdentifier("PaymentRequestReject\(request.paymentRequestId)")

CustomButton(
title: t("common__pay"),
Expand All @@ -88,6 +89,7 @@ struct PaymentRequestCard: View {
) {
onPay?()
}
.accessibilityIdentifier("PaymentRequestPay\(request.paymentRequestId)")
}
.padding(16)
.background(Color.gray5)
Expand Down
6 changes: 5 additions & 1 deletion Bitkit/Views/Wallets/Send/SendConfirmationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ struct SendConfirmationView: View {
@State private var swipeProgress: CGFloat = 0

var accentColor: Color {
if hwSend.isActive { return .blueAccent }
if hwSend.isActive {
return .blueAccent
}
return app.selectedWalletToPayFrom == .lightning ? .purpleAccent : .brandAccent
}

Expand Down Expand Up @@ -227,6 +229,8 @@ struct SendConfirmationView: View {
.padding(.horizontal, 16)
.sheetBackground()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.accessibilityElement(children: .contain)
.accessibilityIdentifier(app.contactPaymentContext?.incomingPaymentRequest == nil ? "SendConfirm" : "PaymentRequestConfirm")
.task {
ensureSendAmountFromScannedInvoicesIfNeeded()
await calculateTransactionFee()
Expand Down
Loading
Loading