Skip to content

BOM_1.6.1#260

Closed
jakubuid wants to merge 11 commits intomasterfrom
develop
Closed

BOM_1.6.1#260
jakubuid wants to merge 11 commits intomasterfrom
develop

Conversation

@jakubuid
Copy link
Collaborator

No description provided.

jakubuid and others added 8 commits January 22, 2026 09:12
- Fix pagination race conditions by canceling in-flight requests on filter change
- Include existing data in LoadingMore state to prevent UI flicker
- Use Locale.ROOT for ISO 8601 date parsing
- Remove tokenSymbol from Transaction (not returned by API)
- Remove unused TOKEN_ADDRESS_TO_SYMBOL mapping

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Build transaction list immutably to avoid race conditions
- Propagate CancellationException instead of showing error state
- Clarify comment about Locale usage in date formatting

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@claude
Copy link

claude bot commented Jan 23, 2026

Claude finished @jakubuid's task in 2m 6s —— View job


Code Review for BOM_1.6.1

Reviewed changes for version 1.6.1 release focusing on POS module transaction history feature.

Summary: Version bump to 1.6.1 (BOM only) adds transaction history API to POS module with new merchant API endpoint, data models, UI screen, and pagination support. 7 pending CI checks (lint, build, tests running).

Found 6 issue(s)

Issue 1: Hardcoded internal API key in build config

ID: buildconfig-apikey-exposure-f8d2
File: product/pos/build.gradle.kts:32
Severity: HIGH
Category: security

Context:

  • Pattern: Line 32 embeds INTERNAL_MERCHANT_API env var directly into BuildConfig, compiled into APK
  • Risk: If env var empty or compromised, API key shipped in release builds → reverse engineering exposure
  • Impact: Unauthorized merchant API access, potential data breach, merchant impersonation
  • Trigger: Any APK decompilation or ProGuard bypass reveals hardcoded key

Recommendation: Use runtime config or secure key storage:

// Remove from buildConfigField, use secured runtime injection instead
// Or restrict to debug builds only:
buildTypes {
    debug {
        buildConfigField(type = "String", name = "INTERNAL_MERCHANT_API", value = "\"${System.getenv("INTERNAL_MERCHANT_API") ?: ""}\"")
    }
    release {
        buildConfigField(type = "String", name = "INTERNAL_MERCHANT_API", value = "\"\"")
    }
}

Issue 2: Missing timeout/retry for merchant API calls

ID: apiclient-merchantapi-timeout-9a3c
File: product/pos/src/main/kotlin/com/walletconnect/pos/api/ApiClient.kt:263-298
Severity: HIGH
Category: performance

Context:

  • Pattern: getTransactionHistory calls merchantApi with no retry logic, only HTTP client timeouts (30s)
  • Risk: Network blips cause immediate failure → poor UX in intermittent connectivity
  • Impact: Transaction history fails to load frequently, users see errors unnecessarily
  • Trigger: Unstable network, temporary API unavailability

Recommendation: Add exponential backoff retry (WalletConnect Pay architecture requirement):

suspend fun getTransactionHistory(...): ApiResult<TransactionHistoryResponse> {
    return retryWithBackoff(maxAttempts = 3, initialDelayMs = 500) {
        try {
            val response = merchantApi.getTransactionHistory(...)
            // ... existing logic
        } catch (e: IOException) { throw e } // Retryable
    }
}

Issue 3: Race condition in transaction history pagination

ID: posviewmodel-pagination-race-c1b5
File: sample/pos/src/main/kotlin/com/walletconnect/sample/pos/POSViewModel.kt:240-246
Severity: MEDIUM
Category: code_quality

Context:

  • Pattern: loadMoreTransactions() checks state but doesn't guard against rapid calls (scroll events can fire multiple times before first completes)
  • Risk: Concurrent loadTransactionHistory calls → duplicate API requests, duplicate transactions in list
  • Impact: UI shows duplicate transactions, wasted API calls, potential pagination cursor corruption
  • Trigger: Rapid scrolling near end of list before first load-more completes

Recommendation: Add loading guard:

fun loadMoreTransactions() {
    val state = _transactionHistoryState.value
    if (state is TransactionHistoryUiState.Success && state.hasMore &&
        transactionLoadingJob?.isActive != true) {  // Add guard
        loadTransactionHistory(refresh = false)
    }
}

Issue 4: Unvalidated timestamp parsing could crash

ID: transactionhistory-timestamp-crash-7e2a
File: sample/pos/src/main/kotlin/com/walletconnect/sample/pos/screens/TransactionHistoryScreen.kt:408-419
Severity: MEDIUM
Category: code_quality

Context:

  • Pattern: formatTimestamp() uses SimpleDateFormat with hardcoded pattern, catches all exceptions but returns raw timestamp on failure
  • Risk: Malformed API timestamps cause exception, fallback shows ugly ISO string to user
  • Impact: Poor UX, potential ANR if timestamp format changes server-side
  • Trigger: API returns unexpected timestamp format (milliseconds, different timezone format, etc.)

Recommendation: Use stricter parsing with multiple format fallbacks:

private fun formatTimestamp(timestamp: String): String {
    val formats = listOf(
        "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
        "yyyy-MM-dd'T'HH:mm:ss'Z'",
        "yyyy-MM-dd'T'HH:mm:ss"
    )
    formats.forEach { pattern ->
        try {
            val inputFormat = SimpleDateFormat(pattern, Locale.ROOT)
            inputFormat.timeZone = TimeZone.getTimeZone("UTC")
            // ... parse and format
        } catch (e: Exception) { /* try next format */ }
    }
    return timestamp.take(19) // Show first 19 chars as fallback
}

Issue 5: No deduplication check for transaction history messages

ID: apiclient-history-dedup-missing-4b1c
File: product/pos/src/main/kotlin/com/walletconnect/pos/api/ApiClient.kt:263
Severity: MEDIUM
Category: code_quality

Context:

  • Pattern: Transaction history API consumer doesn't implement message deduplication (WalletConnect Pay architecture requirement from custom instructions)
  • Risk: If pagination cursor repeated or API returns duplicates, no client-side guard
  • Impact: Duplicate transactions appear in UI, stats become inaccurate
  • Trigger: API bug, retry with same cursor, cache inconsistency

Recommendation: Add client-side deduplication in ViewModel:

// In loadTransactionHistory onSuccess block:
val uniqueTransactions = updatedList.distinctBy { it.paymentId }
loadedTransactions.clear()
loadedTransactions.addAll(uniqueTransactions)

Issue 6: Test coverage missing for transaction history

ID: apiclienttest-history-notcovered-8d3f
File: product/pos/src/test/kotlin/com/walletconnect/pos/ApiClientTest.kt
Severity: LOW
Category: code_quality

Context:

  • Pattern: Test file only covers payment status polling, no tests for new getTransactionHistory endpoint
  • Risk: Transaction history API changes undetected, pagination bugs, mapping errors
  • Impact: Regressions in transaction history feature ship to production
  • Trigger: Any future changes to transaction history logic

Recommendation: Add transaction history tests:

@Test
fun `getTransactionHistory - returns paginated results with cursor`() = runTest {
    // Test pagination, empty results, error handling, status filtering
}

@Test  
fun `transaction mapping - handles null fields correctly`() = runTest {
    // Test PaymentRecord.toTransaction() edge cases
}

Additional Notes:

  • CI: 7 checks pending (lint, build, DB migration, relay tests, E2E, instrumented tests running)
  • BOM version bump to 1.6.1, other modules remain 1.6.0 (correct for BOM-only release)
  • POS_VERSION correctly remains 0.0.3 (early stage module)
  • .gitignore addition appropriate for POS build output
  • Header redaction in logs (line ApiClient.kt:43-44) good security practice
  • Merchant API uses separate HTTP client with different auth (x-api-key header) - correct separation
  • Transaction status enum mappings comprehensive (11 chain IDs covered in mapChainIdToNetworkName)

Positive Observations:

  • Proper coroutine cancellation handling (CancellationException rethrows)
  • Good error tracking with ErrorTracker/EventTracker for observability
  • Immutable state updates in ViewModel to avoid race conditions
  • Pagination UI with infinite scroll implemented correctly
  • Stats card shows aggregated metrics (transactions/customers/revenue)

buildConfigField(type = "String", name = "MERCHANT_API_BASE_URL", value = "\"https://api.merchant.pay.walletconnect.com\"")
buildConfigField(type = "String", name = "PULSE_BASE_URL", value = "\"https://pulse.walletconnect.org\"")
buildConfigField(type = "String", name = "POS_PROJECT_ID", value = "\"${System.getenv("POS_PROJECT_ID") ?: ""}\"")
buildConfigField(type = "String", name = "INTERNAL_MERCHANT_API", value = "\"${System.getenv("INTERNAL_MERCHANT_API") ?: ""}\"")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Auto Review Issue: Hardcoded internal API key in build config

Severity: HIGH
Category: security
Tool: Claude Auto Review

Context:

  • Pattern: Line 32 embeds INTERNAL_MERCHANT_API env var directly into BuildConfig, compiled into APK
  • Risk: If env var empty or compromised, API key shipped in release builds → reverse engineering exposure
  • Impact: Unauthorized merchant API access, potential data breach, merchant impersonation
  • Trigger: Any APK decompilation or ProGuard bypass reveals hardcoded key

Recommendation: Use runtime config or secure key storage:

// Remove from buildConfigField, use secured runtime injection instead
// Or restrict to debug builds only:
buildTypes {
    debug {
        buildConfigField(type = "String", name = "INTERNAL_MERCHANT_API", value = "\"${System.getenv("INTERNAL_MERCHANT_API") ?: ""}\"")
    }
    release {
        buildConfigField(type = "String", name = "INTERNAL_MERCHANT_API", value = "\"\"")
    }
}

@claude
Copy link

claude bot commented Jan 23, 2026

Claude finished @jakubuid's task in 1m 24s —— View job


🔄 Incremental Review: Changes since last review

Issue Status

Previously reported issues:

  1. buildconfig-apikey-exposure-f8d2 (HIGH) - Confirmed still present at product/pos/build.gradle.kts:32
  2. apiclient-merchantapi-timeout-9a3c (HIGH) - Confirmed still present at product/pos/src/main/kotlin/com/walletconnect/pos/api/ApiClient.kt:263-298
  3. posviewmodel-pagination-race-c1b5 (MEDIUM) - Partially resolved - Code at sample/pos/src/main/kotlin/com/walletconnect/sample/pos/POSViewModel.kt:240-246 now cancels previous job (line 188) but doesn't check isActive in the guard
  4. transactionhistory-timestamp-crash-7e2a (MEDIUM) - Confirmed still present at sample/pos/src/main/kotlin/com/walletconnect/sample/pos/screens/TransactionHistoryScreen.kt:408-419
  5. apiclient-history-dedup-missing-4b1c (MEDIUM) - Confirmed still present - No deduplication logic in ViewModel
  6. apiclienttest-history-notcovered-8d3f (LOW) - Confirmed still present - No transaction history tests in product/pos/src/test/kotlin/com/walletconnect/pos/ApiClientTest.kt

New Issues Found

✅ No new issues found in latest commits

Still present: 5 issue(s) + 1 partially resolved

Issue 1: Hardcoded internal API key in build config

ID: buildconfig-apikey-exposure-f8d2
File: product/pos/build.gradle.kts:32
Severity: HIGH
Category: security

Context:

  • Pattern: Line 32 embeds INTERNAL_MERCHANT_API env var directly into BuildConfig, compiled into APK
  • Risk: API key shipped in release builds → reverse engineering exposure
  • Impact: Unauthorized merchant API access, potential data breach
  • Trigger: APK decompilation reveals hardcoded key

Recommendation:

buildTypes {
    debug {
        buildConfigField(type = "String", name = "INTERNAL_MERCHANT_API", value = "\"${System.getenv("INTERNAL_MERCHANT_API") ?: ""}\"")
    }
    release {
        buildConfigField(type = "String", name = "INTERNAL_MERCHANT_API", value = "\"\"")
    }
}

Issue 2: Missing timeout/retry for merchant API calls

ID: apiclient-merchantapi-timeout-9a3c
File: product/pos/src/main/kotlin/com/walletconnect/pos/api/ApiClient.kt:263-298
Severity: HIGH
Category: performance

Context:

  • Pattern: getTransactionHistory calls merchantApi with no retry logic
  • Risk: Network blips cause immediate failure → poor UX
  • Impact: Transaction history fails unnecessarily
  • Trigger: Unstable network, temporary API unavailability

Recommendation:

suspend fun getTransactionHistory(...): ApiResult<TransactionHistoryResponse> {
    return retryWithBackoff(maxAttempts = 3, initialDelayMs = 500) {
        // existing logic
    }
}

Issue 3: Race condition in transaction history pagination

ID: posviewmodel-pagination-race-c1b5
File: sample/pos/src/main/kotlin/com/walletconnect/sample/pos/POSViewModel.kt:240-246
Severity: MEDIUM
Category: code_quality

Context:

  • Pattern: loadMoreTransactions() cancels previous job (line 188) but loadMoreTransactions check at line 243 doesn't verify job isn't already running
  • Risk: Rapid calls before cancellation completes → duplicate requests
  • Impact: Duplicate API calls during rapid scrolling
  • Trigger: Very rapid scrolling events

Recommendation:

fun loadMoreTransactions() {
    val state = _transactionHistoryState.value
    if (state is TransactionHistoryUiState.Success && state.hasMore &&
        transactionLoadingJob?.isActive != true) {
        loadTransactionHistory(refresh = false)
    }
}

Issue 4: Unvalidated timestamp parsing could crash

ID: transactionhistory-timestamp-crash-7e2a
File: sample/pos/src/main/kotlin/com/walletconnect/sample/pos/screens/TransactionHistoryScreen.kt:408-419
Severity: MEDIUM
Category: code_quality

Context:

  • Pattern: formatTimestamp() uses SimpleDateFormat with single hardcoded pattern, fallback shows raw ISO timestamp
  • Risk: Malformed API timestamps cause exception, fallback shows ugly string
  • Impact: Poor UX if timestamp format changes
  • Trigger: API returns unexpected timestamp format

Recommendation:

private fun formatTimestamp(timestamp: String): String {
    val formats = listOf(
        "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
        "yyyy-MM-dd'T'HH:mm:ss'Z'",
        "yyyy-MM-dd'T'HH:mm:ss"
    )
    formats.forEach { pattern ->
        try {
            val inputFormat = SimpleDateFormat(pattern, Locale.ROOT)
            inputFormat.timeZone = TimeZone.getTimeZone("UTC")
            val date = inputFormat.parse(timestamp)
            return date?.let { SimpleDateFormat("MMM dd, yyyy HH:mm", Locale.getDefault()).format(it) } ?: timestamp
        } catch (e: Exception) { /* try next */ }
    }
    return timestamp.take(19)
}

Issue 5: No deduplication check for transaction history messages

ID: apiclient-history-dedup-missing-4b1c
File: sample/pos/src/main/kotlin/com/walletconnect/sample/pos/POSViewModel.kt:209-217
Severity: MEDIUM
Category: code_quality

Context:

  • Pattern: No client-side deduplication when adding transactions
  • Risk: API duplicates or pagination cursor issues → duplicate transactions in UI
  • Impact: Duplicate entries, inaccurate stats
  • Trigger: API bug, retry with same cursor

Recommendation:

// At line 216, after updatedList is built:
val uniqueTransactions = updatedList.distinctBy { it.paymentId }
loadedTransactions.clear()
loadedTransactions.addAll(uniqueTransactions)

Issue 6: Test coverage missing for transaction history

ID: apiclienttest-history-notcovered-8d3f
File: product/pos/src/test/kotlin/com/walletconnect/pos/ApiClientTest.kt
Severity: LOW
Category: code_quality

Context:

  • Pattern: Test file lacks tests for new getTransactionHistory endpoint
  • Risk: Transaction history regressions undetected
  • Impact: Bugs in transaction history ship to production
  • Trigger: Future changes to transaction history logic

Recommendation:

@Test
fun `getTransactionHistory - returns paginated results with cursor`() = runTest {
    // Test pagination, empty results, error handling, status filtering
}

@Test  
fun `transaction mapping - handles null fields correctly`() = runTest {
    // Test PaymentRecord.toTransaction() edge cases
}

Summary

Status: 5 issues still present from previous review, 1 partially addressed. No new issues introduced in latest commits.

Partial improvement: POSViewModel now cancels in-flight requests (line 188) reducing but not eliminating race conditions.

CI: SonarQube passed, 7 GitHub checks still pending.

@sonarqubecloud
Copy link

@jakubuid jakubuid closed this Jan 26, 2026
@github-actions github-actions bot locked and limited conversation to collaborators Jan 26, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant