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
@@ -0,0 +1,63 @@
package io.horizontalsystems.walletkit.core.address

import com.google.gson.JsonObject
import com.google.gson.JsonParser
import io.horizontalsystems.walletkit.core.managers.ZanoNodeManager
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.IOException
import java.util.concurrent.TimeUnit

class ZanoAliasResolver(private val zanoNodeManager: ZanoNodeManager) {

private val httpClient: OkHttpClient = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.build()

fun resolve(alias: String): String? {
val baseUrl = zanoNodeManager.currentNode.host.trimEnd('/')

val requestBody = JsonObject().apply {
addProperty("id", 0)
addProperty("jsonrpc", "2.0")
addProperty("method", "get_alias_details")
add("params", JsonObject().apply {
addProperty("alias", alias)
})
}

val request = Request.Builder()
.url("$baseUrl/json_rpc")
.post(requestBody.toString().toRequestBody("application/json".toMediaType()))
.addHeader("Content-Type", "application/json")
.build()

httpClient.newCall(request).execute().use { response ->
val responseBody = response.body?.string()

if (!response.isSuccessful || responseBody == null) {
throw IOException("Unexpected response: ${response.code}")
}

return parseAliasAddress(responseBody)
}
}

companion object {
fun parseAliasAddress(body: String): String? = try {
val json = JsonParser.parseString(body).asJsonObject
val result = if (json.has("error")) null else json.getAsJsonObject("result")

if (result?.get("status")?.asString == "OK") {
result.getAsJsonObject("alias_details")?.get("address")?.asString?.takeIf { it.isNotEmpty() }
} else {
null
}
} catch (e: Exception) {
null
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package io.horizontalsystems.walletkit.modules.address

import io.horizontalsystems.bitcoincash.MainNetBitcoinCash
import io.horizontalsystems.bitcoinkit.MainNet
import io.horizontalsystems.walletkit.core.App
import io.horizontalsystems.walletkit.core.address.ZanoAliasResolver
import io.horizontalsystems.walletkit.core.supported
import io.horizontalsystems.dashkit.MainNetDash
import io.horizontalsystems.ecash.MainNetECash
Expand Down Expand Up @@ -110,6 +112,10 @@ class AddressHandlerFactory(
domainAddressHandlers.add(AddressHandlerEns(blockchainType, EnsResolverHolder.resolver))
}

BlockchainType.Zano -> {
domainAddressHandlers.add(AddressHandlerZanoAlias(ZanoAliasResolver(App.zanoNodeManager)))
}

else -> {}
}
return domainAddressHandlers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import io.horizontalsystems.bitcoincore.utils.Base58AddressConverter
import io.horizontalsystems.bitcoincore.utils.CashAddressConverter
import io.horizontalsystems.bitcoincore.utils.SegwitAddressConverter
import io.horizontalsystems.walletkit.core.adapters.zcash.ZcashAddressValidator
import io.horizontalsystems.walletkit.core.address.ZanoAliasResolver
import io.horizontalsystems.walletkit.entities.Address
import io.horizontalsystems.walletkit.entities.BitcoinAddress
import io.horizontalsystems.walletkit.entities.MoneroWatchAddress
Expand Down Expand Up @@ -360,6 +361,42 @@ class AddressHandlerZano : IAddressHandler {
}
}

class AddressHandlerZanoAlias(private val resolver: ZanoAliasResolver) : IAddressHandler {
override val blockchainType = BlockchainType.Zano
private val cache = mutableMapOf<String, Address>()

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Data race on non-thread-safe map.

Since isSupported makes a blocking network call that doesn't cooperatively check for coroutine cancellation, typing quickly can spawn multiple coroutines that block simultaneously. When these network calls complete, they will write to this map concurrently from different threads on Dispatchers.Default, which can cause a ConcurrentModificationException or corrupt the map.

Use a ConcurrentHashMap to ensure thread safety.

🛠️ Proposed fix to ensure thread safety
-    private val cache = mutableMapOf<String, Address>()
+    private val cache = java.util.concurrent.ConcurrentHashMap<String, Address>()
📝 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
private val cache = mutableMapOf<String, Address>()
private val cache = java.util.concurrent.ConcurrentHashMap<String, Address>()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@walletkit/src/main/java/io/horizontalsystems/walletkit/modules/address/IAddressHandler.kt`
at line 366, Replace the mutableMapOf-backed cache in the address handler with a
ConcurrentHashMap, preserving the existing String-to-Address cache behavior and
access patterns.


override fun isSupported(value: String): Boolean {
val alias = normalize(value) ?: return false
if (cache.containsKey(value)) return true

return try {
val resolved = resolver.resolve(alias) ?: return false
cache[value] = Address(resolved, value, blockchainType)
true
} catch (e: Exception) {
false
}
}

override fun parseAddress(value: String): Address {
return cache[value]!!
}

companion object {
// on-chain alias charset is a-z 0-9 . - with max length 25
private val aliasRegex = Regex("^[a-z0-9.-]{1,25}\$")

fun normalize(value: String): String? {
val hasPrefix = value.startsWith("@")
val alias = if (hasPrefix) value.substring(1) else value
if (!aliasRegex.matches(alias)) return null
// without "@" the intent is ambiguous, require 2+ chars to avoid a lookup on the first keystroke
if (!hasPrefix && alias.length < 2) return null
return alias
}
}
}

class AddressHandlerPure(override val blockchainType: BlockchainType) : IAddressHandler {

override fun isSupported(value: String) = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import io.horizontalsystems.walletkit.core.ViewModelUiState
import io.horizontalsystems.walletkit.core.address.AddressCheckManager
import io.horizontalsystems.walletkit.core.address.AddressCheckResult
import io.horizontalsystems.walletkit.core.address.AddressCheckType
import io.horizontalsystems.walletkit.core.address.ZanoAliasResolver
import io.horizontalsystems.walletkit.core.factories.AddressValidatorFactory
import io.horizontalsystems.walletkit.core.managers.ActionCompletedDelegate
import io.horizontalsystems.walletkit.core.managers.RecentAddressManager
Expand All @@ -17,11 +18,14 @@ import io.horizontalsystems.walletkit.entities.Address
import io.horizontalsystems.walletkit.entities.DataState
import io.horizontalsystems.walletkit.modules.address.AddressHandlerEns
import io.horizontalsystems.walletkit.modules.address.AddressHandlerUdn
import io.horizontalsystems.walletkit.modules.address.AddressHandlerZanoAlias
import io.horizontalsystems.walletkit.modules.address.AddressParserChain
import io.horizontalsystems.walletkit.modules.address.EnsResolverHolder
import io.horizontalsystems.walletkit.modules.address.IAddressHandler
import io.horizontalsystems.walletkit.modules.contacts.ContactsRepository
import io.horizontalsystems.walletkit.modules.send.address.AddressExtractor
import io.horizontalsystems.walletkit.modules.send.address.EnterAddressValidator
import io.horizontalsystems.marketkit.models.BlockchainType
import io.horizontalsystems.marketkit.models.Token
import io.horizontalsystems.marketkit.models.TokenQuery
import io.horizontalsystems.subscriptions.core.ScamProtection
Expand Down Expand Up @@ -298,8 +302,11 @@ class EnterAddressViewModel(
val ensHandler = AddressHandlerEns(blockchainType, EnsResolverHolder.resolver)
val udnHandler =
AddressHandlerUdn(tokenQuery, coinCode, App.appConfigProvider.udnApiKey)
val addressParserChain =
AddressParserChain(domainHandlers = listOf(ensHandler, udnHandler))
val domainHandlers = mutableListOf(ensHandler, udnHandler)
if (blockchainType == BlockchainType.Zano) {
domainHandlers.add(AddressHandlerZanoAlias(ZanoAliasResolver(App.zanoNodeManager)))
}
val addressParserChain = AddressParserChain(domainHandlers = domainHandlers)
val addressUriParser = AddressUriParser(token.blockchainType, token.type)
val recentAddressManager =
RecentAddressManager(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package io.horizontalsystems.walletkit.modules.address

import io.horizontalsystems.walletkit.core.address.ZanoAliasResolver
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test

class ZanoAliasTest {

@Test
fun normalize_validAliases() {
assertEquals("zano", AddressHandlerZanoAlias.normalize("@zano"))
assertEquals("gigabyted", AddressHandlerZanoAlias.normalize("gigabyted"))
assertEquals("gigabyted", AddressHandlerZanoAlias.normalize("@gigabyted"))
assertEquals("alias123", AddressHandlerZanoAlias.normalize("alias123"))
assertEquals("a", AddressHandlerZanoAlias.normalize("@a"))
assertEquals("a".repeat(25), AddressHandlerZanoAlias.normalize("a".repeat(25)))
// dots and hyphens are part of the on-chain alias charset
assertEquals("-axel-", AddressHandlerZanoAlias.normalize("-axel-"))
assertEquals("---007", AddressHandlerZanoAlias.normalize("@---007"))
assertEquals("some.name", AddressHandlerZanoAlias.normalize("some.name"))
}

@Test
fun normalize_invalidAliases() {
assertNull(AddressHandlerZanoAlias.normalize("a"))
assertNull(AddressHandlerZanoAlias.normalize(""))
assertNull(AddressHandlerZanoAlias.normalize("@"))
assertNull(AddressHandlerZanoAlias.normalize("MyName"))
assertNull(AddressHandlerZanoAlias.normalize("with space"))
assertNull(AddressHandlerZanoAlias.normalize("under_score"))
assertNull(AddressHandlerZanoAlias.normalize("a".repeat(26)))
assertNull(AddressHandlerZanoAlias.normalize("@@double"))
assertNull(
AddressHandlerZanoAlias.normalize(
"ZxDGngsbdEvaPWoYmV995cKddBqYn1A963Wu2xRJUotE65J9FzitMtLAeYKwQewEGYVLsoc1MqRKghhGCmFEpcPo2BMnzYeCJ"
)
)
}

@Test
fun parseAliasAddress_ok() {
val body = """
{
"id": 0,
"jsonrpc": "2.0",
"result": {
"alias_details": {
"address": "ZxDGngsbdEvaPWoYmV995cKddBqYn1A963Wu2xRJUotE65J9FzitMtLAeYKwQewEGYVLsoc1MqRKghhGCmFEpcPo2BMnzYeCJ",
"comment": "the one and only!",
"tracking_key": ""
},
"status": "OK"
}
}
""".trimIndent()

assertEquals(
"ZxDGngsbdEvaPWoYmV995cKddBqYn1A963Wu2xRJUotE65J9FzitMtLAeYKwQewEGYVLsoc1MqRKghhGCmFEpcPo2BMnzYeCJ",
ZanoAliasResolver.parseAliasAddress(body)
)
}

@Test
fun parseAliasAddress_error() {
val body = """{"id":0,"jsonrpc":"2.0","error":{"code":-1,"message":"Unknown alias"}}"""
assertNull(ZanoAliasResolver.parseAliasAddress(body))
}

@Test
fun parseAliasAddress_notFound() {
// actual shape returned by public nodes for an unknown alias
val body = """
{"id":0,"jsonrpc":"2.0","result":{"alias_details":{"address":"","comment":"","tracking_key":""},"status":"NOT_FOUND"}}
""".trimIndent()
assertNull(ZanoAliasResolver.parseAliasAddress(body))
}

@Test
fun parseAliasAddress_okWithEmptyAddress() {
val body = """{"id":0,"jsonrpc":"2.0","result":{"alias_details":{"address":""},"status":"OK"}}"""
assertNull(ZanoAliasResolver.parseAliasAddress(body))
}

@Test
fun parseAliasAddress_missingAliasDetails() {
val body = """{"id":0,"jsonrpc":"2.0","result":{"status":"OK"}}"""
assertNull(ZanoAliasResolver.parseAliasAddress(body))
}

@Test
fun parseAliasAddress_malformedJson() {
assertNull(ZanoAliasResolver.parseAliasAddress("not a json"))
assertNull(ZanoAliasResolver.parseAliasAddress(""))
assertNull(ZanoAliasResolver.parseAliasAddress("[]"))
}
}
Loading