Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ extension Client {
)
}

public func transfer(exchangedFiat: ExchangedFiat, verifiedState: VerifiedState, owner: AccountCluster, destination: PublicKey, destinationOwner: PublicKey? = nil, chatMetadata: ChatPaymentMetadata? = nil, rendezvous: PublicKey) async throws {
/// Submits a transfer under `rendezvous` as its intent ID; pass `intentIDIsSensitive` for a server-issued ID that anyone reading the logs could spend before it settles.
public func transfer(exchangedFiat: ExchangedFiat, verifiedState: VerifiedState, owner: AccountCluster, destination: PublicKey, destinationOwner: PublicKey? = nil, chatMetadata: ChatPaymentMetadata? = nil, rendezvous: PublicKey, intentIDIsSensitive: Bool = false) async throws {
let appMetadata = try chatMetadata?.serializedAppMetadata()
_ = try await withCheckedThrowingContinuation { c in
transactionService.transfer(
Expand All @@ -56,7 +57,8 @@ extension Client {
destinationOwner: destinationOwner,
appMetadata: appMetadata,
owner: owner.authority.keyPair,
rendezvous: rendezvous
rendezvous: rendezvous,
intentIDIsSensitive: intentIDIsSensitive
) { c.resume(with: $0) }
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// IntentLogID.swift
// FlipcashCore
//

import Foundation

/// How an intent ID appears in logs: in full, or masked to its last six base58 characters while it is still usable.
struct IntentLogID: Sendable, Equatable {

/// Characters of the base58 ID kept when masked.
static let suffixLength = 6

let id: PublicKey

/// Whether anyone who reads the ID before it settles could spend it, as with a payment request's `payment_intent_id`.
let isSensitive: Bool

/// The ID to log before the intent settles or after it fails.
var pending: String {
isSensitive ? "…" + String(id.base58.suffix(Self.suffixLength)) : id.base58
}

/// The ID to log once the intent has succeeded.
var settled: String {
id.base58
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ final class TransactionService: Sendable {

// MARK: - Transfer -

func transfer(exchangedFiat: ExchangedFiat, verifiedState: VerifiedState, sourceCluster: AccountCluster, destination: PublicKey, destinationOwner: PublicKey? = nil, appMetadata: Data? = nil, owner: KeyPair, rendezvous: PublicKey, completion: @Sendable @escaping (Result<(), Error>) -> Void) {
func transfer(exchangedFiat: ExchangedFiat, verifiedState: VerifiedState, sourceCluster: AccountCluster, destination: PublicKey, destinationOwner: PublicKey? = nil, appMetadata: Data? = nil, owner: KeyPair, rendezvous: PublicKey, intentIDIsSensitive: Bool = false, completion: @Sendable @escaping (Result<(), Error>) -> Void) {
logger.info("Sending transfer")

let intent = IntentTransfer(
Expand All @@ -69,7 +69,7 @@ final class TransactionService: Sendable {
appMetadata: appMetadata
)

submit(intent: intent, owner: owner) { result in
submit(intent: intent, owner: owner, intentIDIsSensitive: intentIDIsSensitive) { result in
switch result {
case .success(_):
logger.info("Transfer succeeded")
Expand Down Expand Up @@ -682,8 +682,9 @@ final class TransactionService: Sendable {

// MARK: - Submit -

private func submit<T>(intent: T, owner: KeyPair, deviceToken: Data? = nil, completion: @Sendable @escaping (Result<T, ErrorSubmitIntent>) -> Void) where T: IntentType {
logger.info("Submitting intent", metadata: ["type": "\(T.self)", "intentId": "\(intent.id.base58)"])
private func submit<T>(intent: T, owner: KeyPair, deviceToken: Data? = nil, intentIDIsSensitive: Bool = false, completion: @Sendable @escaping (Result<T, ErrorSubmitIntent>) -> Void) where T: IntentType {
let logID = IntentLogID(id: intent.id, isSensitive: intentIDIsSensitive)
logger.info("Submitting intent", metadata: ["type": "\(T.self)", "intentId": "\(logID.pending)"])

// `IntentType` is a reference type that is not `Sendable`; the response
// handler mutates it (`apply(parameters:)`) within the single-threaded
Expand Down Expand Up @@ -712,14 +713,14 @@ final class TransactionService: Sendable {
logger.info("Received server parameters, submitting signatures", metadata: [
"type": "\(T.self)",
"paramCount": "\(parameters.serverParameters.count)",
"intentId": "\(intent.id.base58)"
"intentId": "\(logID.pending)"
])

} catch {
logger.error("Received server parameters but failed to apply them", metadata: [
"type": "\(T.self)",
"paramCount": "\(parameters.serverParameters.count)",
"intentId": "\(intent.id.base58)",
"intentId": "\(logID.pending)",
"error": "\(error)"
])
reference.cancel()
Expand All @@ -732,7 +733,7 @@ final class TransactionService: Sendable {
logger.info("Intent submitted successfully", metadata: [
"type": "\(T.self)",
"code": "\(success.code.rawValue)",
"intentId": "\(intent.id.base58)"
"intentId": "\(logID.settled)"
])
reference.cancel()
completion(.success(intent))
Expand All @@ -755,7 +756,7 @@ final class TransactionService: Sendable {
"type": "\(T.self)",
"code": "\(error.code)",
"detailCount": "\(error.errorDetails.count)",
"intentId": "\(intent.id.base58)",
"intentId": "\(logID.pending)",
"invalidSignatures": "\(invalidSignatures)"
])

Expand Down
26 changes: 26 additions & 0 deletions FlipcashCore/Tests/FlipcashCoreTests/IntentLogIDTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Foundation
import Testing
@testable import FlipcashCore

@Suite("IntentLogID")
struct IntentLogIDTests {

private let id = PublicKey.generate()!

@Test("A sensitive intent ID logs only its last six characters until it succeeds")
func sensitiveMasksUntilSettled() {
let logID = IntentLogID(id: id, isSensitive: true)

#expect(logID.pending == "…" + String(id.base58.suffix(6)))
#expect(!logID.pending.contains(id.base58))
#expect(logID.settled == id.base58)
}

@Test("A normal intent ID logs in full before and after it settles")
func normalLogsInFull() {
let logID = IntentLogID(id: id, isSensitive: false)

#expect(logID.pending == id.base58)
#expect(logID.settled == id.base58)
}
}
Loading