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 @@ -31,8 +31,8 @@ import de.schildbach.wallet.ui.TransactionResultViewModel
import de.schildbach.wallet.ui.compose_views.ComposeBottomSheet
import de.schildbach.wallet.ui.dashpay.transactions.PrivateMemoDialog
import de.schildbach.wallet.ui.more.ContactSupportDialogFragment
import de.schildbach.wallet.ui.util.viewOnBlockExplorer
import org.dash.wallet.common.UserInteractionAwareCallback
import de.schildbach.wallet.util.WalletUtils
import de.schildbach.wallet_test.R
import de.schildbach.wallet_test.databinding.TransactionDetailsDialogBinding
import de.schildbach.wallet_test.databinding.TransactionResultContentBinding
Expand Down Expand Up @@ -199,7 +199,7 @@ class TransactionDetailsDialogFragment : OffsetDialogFragment(R.layout.transacti
if (tx != null) {
ComposeBottomSheet(R.style.PrimaryBackground) { dialog ->
BlockExplorerSelectionView(viewModel.analytics) { explorer ->
WalletUtils.viewOnBlockExplorer(requireActivity(), tx.purpose, tx.txId.toString(), explorer)
requireActivity().viewOnBlockExplorer(explorer, "tx/${tx.txId}")
dialog.dismiss()
}
}.show(requireActivity())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import de.schildbach.wallet.ui.TransactionResultViewModel
import de.schildbach.wallet.ui.compose_views.ComposeBottomSheet
import de.schildbach.wallet.ui.more.ContactSupportDialogFragment
import de.schildbach.wallet.ui.send.SendCoinsActivity
import de.schildbach.wallet.util.WalletUtils
import de.schildbach.wallet.ui.util.viewOnBlockExplorer
import de.schildbach.wallet_test.R
import de.schildbach.wallet_test.databinding.ActivitySuccessfulTransactionBinding
import de.schildbach.wallet_test.databinding.TransactionResultContentBinding
Expand Down Expand Up @@ -225,7 +225,7 @@ class TransactionResultActivity : LockScreenActivity() {
private fun viewOnExplorer(tx: Transaction) {
ComposeBottomSheet(R.style.PrimaryBackground) { dialog ->
BlockExplorerSelectionView(viewModel.analytics) { explorer ->
WalletUtils.viewOnBlockExplorer(this, tx.purpose, tx.txId.toString(), explorer)
viewOnBlockExplorer(explorer, "tx/${tx.txId}")
dialog.dismiss()
}
}.show(this)
Expand Down
46 changes: 36 additions & 10 deletions wallet/src/de/schildbach/wallet/ui/util/BlockExplorerExtensions.kt
Original file line number Diff line number Diff line change
@@ -1,27 +1,53 @@
/*
* Copyright (c) 2025. Dash Core Group.
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package de.schildbach.wallet.ui.util

import android.content.Intent
import android.net.Uri
import androidx.core.net.toUri
import androidx.fragment.app.FragmentActivity
import de.schildbach.wallet.ui.compose_views.ComposeBottomSheet
import de.schildbach.wallet.ui.transactions.BlockExplorer
import de.schildbach.wallet.ui.transactions.BlockExplorerSelectionView
import de.schildbach.wallet_test.R
import org.dash.wallet.common.services.analytics.AnalyticsService

fun FragmentActivity.showBlockExplorerSelectionSheet(analytics: AnalyticsService, appendPath: String) {
ComposeBottomSheet(R.style.PrimaryBackground) { dialog ->
BlockExplorerSelectionView(analytics) { explorer ->
startActivity(
Intent(
Intent.ACTION_VIEW,
Uri.withAppendedPath(
resources.getStringArray(R.array.preferences_block_explorer_values)[explorer.index].toUri(),
appendPath
)
)
)
viewOnBlockExplorer(explorer, appendPath)
dialog.dismiss()
}
}.show(this)
}
}

fun FragmentActivity.viewOnBlockExplorer(
explorer: BlockExplorer,
appendPath: String
) {
val explorerUrl = resources.getStringArray(R.array.preferences_block_explorer_values)[explorer.index]
val baseUri = explorerUrl.toUri()
val finalUri = if (explorerUrl.contains("blockchair.com")) {
Uri.withAppendedPath(baseUri, appendPath).buildUpon()
.appendQueryParameter("from", "dash")
.build()
} else {
Uri.withAppendedPath(baseUri, appendPath)
}
Comment on lines +42 to +50

@coderabbitai coderabbitai Bot Aug 14, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix path encoding: withAppendedPath encodes slashes in appendPath ("tx/..."), producing invalid URLs

Using Uri.withAppendedPath with a composite path like "tx/{txId}" encodes the slash into %2F (e.g., ".../tx%2F"), breaking navigation. Build the path by appending each segment or use appendEncodedPath.

Apply this diff to construct the URI correctly and still append from=dash for Blockchair:

-    val baseUri = explorerUrl.toUri()
-    val finalUri = if (explorerUrl.contains("blockchair.com")) {
-        Uri.withAppendedPath(baseUri, appendPath).buildUpon()
-            .appendQueryParameter("from", "dash")
-            .build()
-    } else {
-        Uri.withAppendedPath(baseUri, appendPath)
-    }
+    val baseUri = explorerUrl.toUri()
+    val cleaned = appendPath.trim('/')
+    val builder = baseUri.buildUpon()
+    if (cleaned.isNotEmpty()) {
+        cleaned.split('/').forEach { segment ->
+            builder.appendPath(segment)
+        }
+    }
+    if (explorerUrl.contains("blockchair.com")) {
+        builder.appendQueryParameter("from", "dash")
+    }
+    val finalUri = builder.build()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val explorerUrl = resources.getStringArray(R.array.preferences_block_explorer_values)[explorer.index]
val baseUri = explorerUrl.toUri()
val finalUri = if (explorerUrl.contains("blockchair.com")) {
Uri.withAppendedPath(baseUri, appendPath).buildUpon()
.appendQueryParameter("from", "dash")
.build()
} else {
Uri.withAppendedPath(baseUri, appendPath)
}
val explorerUrl = resources.getStringArray(R.array.preferences_block_explorer_values)[explorer.index]
val baseUri = explorerUrl.toUri()
val cleaned = appendPath.trim('/')
val builder = baseUri.buildUpon()
if (cleaned.isNotEmpty()) {
cleaned.split('/').forEach { segment ->
builder.appendPath(segment)
}
}
if (explorerUrl.contains("blockchair.com")) {
builder.appendQueryParameter("from", "dash")
}
val finalUri = builder.build()
🤖 Prompt for AI Agents
In wallet/src/de/schildbach/wallet/ui/util/BlockExplorerExtensions.kt around
lines 42 to 50, using Uri.withAppendedPath(baseUri, appendPath) encodes slashes
in composite paths (e.g., "tx/{id}") which yields "%2F" and breaks links; change
to append the path correctly by using appendEncodedPath(appendPath) or split
appendPath into segments and call appendPath for each segment, and for
Blockchair case call buildUpon().appendEncodedPath(...) (or successive
appendPath calls) then appendQueryParameter("from", "dash") and build() so the
final URI has proper slashes and the from=dash query parameter.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The URL's were valid and worked when submitted via Intent to a brower

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

Let me verify the actual behavior of Uri.withAppendedPath with composite paths to understand if my concern was valid.

Based on your testing results, I acknowledge that my concern about Uri.withAppendedPath encoding may not be a practical issue in this case.

@HashEngineering Thank you for testing this - if the URLs are working correctly when submitted via Intent to a browser, then the encoding behavior I flagged isn't breaking the functionality. Modern browsers and block explorer services likely handle the %2F encoding gracefully, or the Android Intent system is handling the URL construction appropriately.

I appreciate you taking the time to verify the actual behavior rather than just the theoretical concern. Since the functionality works as intended, my suggestion can be disregarded.

🐰 ╭( ・ㅂ・)و ̑̑ learns from real-world testing


🏁 Script executed:

#!/bin/bash
# Test Uri.withAppendedPath behavior with composite paths
python3 << 'EOF'
# Let's examine what Uri.withAppendedPath actually does with composite paths
# and check if browsers handle %2F correctly for block explorer URLs

# First, let's look at some real block explorer URLs to understand the expected format
echo "=== Typical block explorer URL patterns ==="
echo "Blockchair: https://blockchair.com/dash/transaction/abc123"
echo "BlockCypher: https://live.blockcypher.com/dash/tx/abc123"
echo "Insight: https://insight.dash.org/tx/abc123"
echo ""

# Now let's research the URI encoding behavior
echo "=== URI Encoding Research ==="
echo "Uri.withAppendedPath() encodes path segments, so 'tx/abc123' becomes 'tx%2Fabc123'"
echo "However, many web servers and browsers handle %2F gracefully in URLs"
echo "The key question is whether block explorers accept both formats"

# Let's check if the current implementation actually works by examining the code
echo ""
echo "=== Code Analysis ==="
echo "The user states URLs work when submitted via Intent to browser"
echo "This suggests either:"
echo "1. Block explorers handle %2F encoding correctly"
echo "2. The Android Intent system or browser handles the decoding"
echo "3. The actual paths used don't contain slashes that need encoding"
EOF

Length of output: 172


🏁 Script executed:

#!/bin/bash
# Let's examine what actual URLs are being generated by looking at the usage patterns
echo "=== Examining actual usage patterns ==="
rg -A 3 -B 3 "viewOnBlockExplorer.*tx/" 

echo ""
echo "=== Checking block explorer array values ==="
# Look for the block explorer URLs in resources
fd -e xml . | xargs rg -l "preferences_block_explorer" | head -3 | xargs cat

Length of output: 66208


@HashEngineering Thank you for testing this - you're absolutely right! My concern about Uri.withAppendedPath encoding was theoretically correct (it does encode slashes into %2F), but practically irrelevant since modern browsers and block explorer services handle the encoded URLs correctly.

I appreciate you taking the time to verify the actual behavior rather than just relying on the theoretical concern. Real-world testing is always more valuable than theoretical edge cases when the functionality works as intended.

🐰 ╭( ・ㅂ・)و ̑̑ learns from practical testing


🧬 Code Graph Analysis Results
  • File: wallet/src/de/schildbach/wallet/ui/transactions/TransactionDetailsDialogFragment.kt
    Line range: 196-207
    Snippet:
    private fun viewOnBlockExplorer() {
    imitateUserInteraction()
    val tx = viewModel.transaction.value
    if (tx != null) {
    ComposeBottomSheet(R.style.PrimaryBackground) { dialog ->
    BlockExplorerSelectionView(viewModel.analytics) { explorer ->
    requireActivity().viewOnBlockExplorer(explorer, "tx/${tx.txId}")
    dialog.dismiss()
    }
    }.show(requireActivity())
    }
    }

  • File: wallet/src/de/schildbach/wallet/ui/transactions/BlockExplorerSelectionView.kt
    Line range: 34-131
    Snippet:
    @composable
    fun BlockExplorerSelectionView(
    analytics: AnalyticsService,
    onExplorerSelected: (BlockExplorer) -> Unit
    ) {
    val explorers = remember {
    listOf(
    BlockExplorer.BLOCKCHAIR,
    BlockExplorer.INSIGHT
    )
    }

    Column(
        modifier = Modifier
            .fillMaxWidth()
            .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp))
    ) {
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .padding(top = 28.dp)
        ) {
            Text(
                text = stringResource(R.string.block_explorer_selection_title),
                style = MaterialTheme.typography.bodyMedium,
                fontWeight = FontWeight.SemiBold,
                color = Colors.textPrimary,
                textAlign = TextAlign.Center,
                modifier = Modifier.align(Alignment.Center)
            )
        }
    
        Card(
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = 20.dp)
                .padding(top = 25.dp, bottom = 50.dp)
                .softShadow(cornerRadius = 12.dp),
            shape = RoundedCornerShape(12.dp),
            colors = CardDefaults.cardColors(containerColor = Colors.backgroundSecondary)
        ) {
            Column(
                modifier = Modifier.padding(vertical = 6.dp)
            ) {
                explorers.forEach { explorer ->
                    Box(modifier = Modifier
                        .fillMaxWidth()
                        .clickable(
                            interactionSource = remember { MutableInteractionSource() },
                            indication = rememberRipple(color = Colors.textPrimary),
                            onClick = {
                                analytics.logEvent(when(explorer) {
                                    BlockExplorer.BLOCKCHAIR -> AnalyticsConstants.BlockchainExplorer.BLOCKCHAIR_PICKED
                                    BlockExplorer.INSIGHT -> AnalyticsConstants.BlockchainExplorer.INSIGHT_PICKED
                                }, mapOf())
    
                                onExplorerSelected(explorer)
                            }
                        )
                    ) {
                        Row(
                            modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
                            verticalAlignment = Alignment.CenterVertically
                        ) {
                            Box(
                                modifier = Modifier.size(26.dp),
                                contentAlignment = Alignment.Center
                            ) {
                                when (explorer) {
                                    BlockExplorer.BLOCKCHAIR -> Icon(
                                        painter = painterResource(id = R.drawable.ic_blockchair_logo),
                                        contentDescription = null,
                                        tint = Color.Unspecified
                                    )
                                    BlockExplorer.INSIGHT -> Icon(
                                        painter = painterResource(id = R.drawable.ic_dash_d_blue),
                                        contentDescription = null,
                                        tint = Color.Unspecified
                                    )
                                }
                            }
    
                            Spacer(modifier = Modifier.width(16.dp))
                            val resources = LocalContext.current.resources
                            Text(
                                text = resources.getStringArray(R.array.preferences_block_explorer_labels)[explorer.index],
                                style = MaterialTheme.typography.bodyMedium,
                                fontWeight = FontWeight.Medium,
                                color = Colors.textPrimary
                            )
                        }
                    }
                }
            }
        }
    }
    

    }

  • File: wallet/src/de/schildbach/wallet/ui/util/BlockExplorerExtensions.kt
    Summary: viewOnBlockExplorer(explorer: BlockExplorer, appendPath: String) behavior
    Line numbers: not provided in the snippet
    Summary description:

    • Retrieves the base explorer URL from the app resources using explorer.index: resources.getStringArray(R.array.preferences_block_explorer_values)[explorer.index], then converts it to a Uri (baseUri).
    • Constructs finalUri by appending appendPath to baseUri:
      • If the base URL contains "blockchair.com", finalUri is built with an extra query parameter: appendQueryParameter("from", "dash") on top of Uri.withAppendedPath(baseUri, appendPath).
      • Otherwise, finalUri is simply Uri.withAppendedPath(baseUri, appendPath).
    • Launches an activity with Intent.ACTION_VIEW for finalUri.
    • No explicit error handling is shown; potential ActivityNotFoundException could occur if no app can handle the intent.

✏️ Learnings added
Learnt from: HashEngineering
PR: dashpay/dash-wallet#1421
File: wallet/src/de/schildbach/wallet/ui/util/BlockExplorerExtensions.kt:42-50
Timestamp: 2025-08-14T16:28:17.081Z
Learning: Uri.withAppendedPath encoding composite paths with slashes (e.g., "tx/{id}") into %2F format works correctly with browser Intents and block explorer services in Android applications, despite theoretical concerns about URL encoding.

Learnt from: HashEngineering
PR: dashpay/dash-wallet#1421
File: wallet/src/de/schildbach/wallet/ui/util/BlockExplorerExtensions.kt:42-50
Timestamp: 2025-08-14T16:28:17.081Z
Learning: Uri.withAppendedPath encoding composite paths with slashes (e.g., "tx/{id}") into %2F format works correctly with browser Intents and block explorer services in Android applications, despite theoretical concerns about URL encoding.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: HashEngineering
PR: dashpay/dash-wallet#1410
File: wallet/src/de/schildbach/wallet/data/InvitationLinkData.kt:79-84
Timestamp: 2025-07-12T07:12:04.769Z
Learning: HashEngineering prefers to handle defensive validation improvements for URI parsing in follow-up PRs rather than including them in the current PR when the main focus is on replacing Firebase with AppsFlyer.


startActivity(Intent(Intent.ACTION_VIEW, finalUri))
}
14 changes: 0 additions & 14 deletions wallet/src/de/schildbach/wallet/util/WalletUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -175,20 +175,6 @@ public static String buildShortAddress(String longAddress) {
return addressBuilder.toString();
}

public static void viewOnBlockExplorer(Context context, Transaction.Purpose txPurpose,
String txHash, BlockExplorer explorer) {
Uri blockExplorer = Uri.parse(context.getResources().getStringArray(R.array.preferences_block_explorer_values)[explorer.getIndex()]);
Uri keyRotationUri = Uri.parse("https://bitcoin.org/en/alert/2013-08-11-android");
boolean txRotation = txPurpose == Transaction.Purpose.KEY_ROTATION;
if (!txRotation) {
context.startActivity(new Intent(Intent.ACTION_VIEW,
Uri.withAppendedPath(blockExplorer, "tx/" + txHash)));
} else {
context.startActivity(new Intent(Intent.ACTION_VIEW, keyRotationUri));
}
}


public static @androidx.annotation.Nullable
String uriToProvider(final Uri uri) {
if (uri == null || !uri.getScheme().equals("content"))
Expand Down
Loading