diff --git a/CHANGELOG.md b/CHANGELOG.md index d44adc51..302a6838 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ * Updated Prov Protos to 1.19.0-rc2 [#522](https://github.com/provenance-io/explorer-service/pull/522) * Updated gRPC query to use `query` field instead of `events` field [#523](https://github.com/provenance-io/explorer-service/pull/523) +* Fixed transaction details endpoint by handling Base64 and string encoding changes [#525](https://github.com/provenance-io/explorer-service/pull/525) ### Bug Fixes diff --git a/service/src/main/kotlin/io/provenance/explorer/domain/extensions/Extenstions.kt b/service/src/main/kotlin/io/provenance/explorer/domain/extensions/Extenstions.kt index 005735f8..06b24ad7 100644 --- a/service/src/main/kotlin/io/provenance/explorer/domain/extensions/Extenstions.kt +++ b/service/src/main/kotlin/io/provenance/explorer/domain/extensions/Extenstions.kt @@ -41,6 +41,23 @@ fun String.toByteString() = ByteString.copyFromUtf8(this) fun ByteArray.toByteString() = ByteString.copyFrom(this) fun ByteString.toBase64() = Base64.getEncoder().encodeToString(this.toByteArray()) fun String.fromBase64() = Base64.getDecoder().decode(this).decodeToString() + +fun String.tryFromBase64(): String { + return if (isBase64(this)) { + try { + Base64.getDecoder().decode(this).decodeToString() + } catch (e: IllegalArgumentException) { + this + } + } else { + this + } +} + +private fun isBase64(str: String): Boolean { + if (str.length % 4 != 0) return false + return str.matches(Regex("^[A-Za-z0-9+/=]+\$")) +} fun String.fromBase64ToMAddress() = Base64.getDecoder().decode(this).toByteString().toMAddress() fun String.toBase64() = Base64.getEncoder().encodeToString(this.toByteArray()) fun ByteArray.base64EncodeString(): String = String(Base64.getEncoder().encode(this)) diff --git a/service/src/main/kotlin/io/provenance/explorer/domain/extensions/MessageTranslationExtensions.kt b/service/src/main/kotlin/io/provenance/explorer/domain/extensions/MessageTranslationExtensions.kt index 60de06d6..d27160ab 100644 --- a/service/src/main/kotlin/io/provenance/explorer/domain/extensions/MessageTranslationExtensions.kt +++ b/service/src/main/kotlin/io/provenance/explorer/domain/extensions/MessageTranslationExtensions.kt @@ -148,8 +148,8 @@ fun List.txEventsToObjectNodePrint(protoPrinter: JsonFormat.Printer val newArray = JSON_NODE_FACTORY.arrayNode() oldArray.forEach { val newNode = JSON_NODE_FACTORY.objectNode() - val newKey = it.get("key").asText().fromBase64() - val newValue = it.get("value")?.asText()?.fromBase64() + val newKey = it.get("key").asText().tryFromBase64() + val newValue = it.get("value")?.asText()?.tryFromBase64() newNode.put("key", newKey) newNode.put("value", newValue) newArray.add(newNode) diff --git a/service/src/main/kotlin/io/provenance/explorer/service/TransactionService.kt b/service/src/main/kotlin/io/provenance/explorer/service/TransactionService.kt index 4a8a8e65..8038e512 100644 --- a/service/src/main/kotlin/io/provenance/explorer/service/TransactionService.kt +++ b/service/src/main/kotlin/io/provenance/explorer/service/TransactionService.kt @@ -177,15 +177,25 @@ class TransactionService( .ifEmpty { throw ResourceNotFoundException("Invalid transaction hash: '$txnHash'") } .getMainState(blockHeight).txV2.let { protoPrinter.print(it) } - fun getTransactionByHash(hash: String, blockHeight: Int? = null) = - getTxByHash(hash) - .ifEmpty { throw ResourceNotFoundException("Invalid transaction hash: '$hash'") } - .let { list -> - val state = list.getMainState(blockHeight) - - hydrateTxDetails(state) - .apply { this.additionalHeights = list.filterNot { it.height == state.height }.map { it.height } } - } + fun getTransactionByHash(hash: String, blockHeight: Int? = null): TxDetails { + logger.info("Fetching transaction for hash: $hash with blockHeight: $blockHeight") + return try { + getTxByHash(hash) + .ifEmpty { + throw ResourceNotFoundException("Invalid transaction hash: '$hash'") + } + .let { list -> + val state = list.getMainState(blockHeight) + hydrateTxDetails(state) + .apply { + this.additionalHeights = list.filterNot { it.height == state.height }.map { it.height } + } + } + } catch (e: Exception) { + logger.error("Error fetching transaction for hash: $hash", e) + throw e + } + } private fun hydrateTxDetails(tx: TxCacheRecord) = transaction { TxDetails( diff --git a/service/src/test/kotlin/io/provenance/explorer/domain/extensions/ExtenstionsKtTest.kt b/service/src/test/kotlin/io/provenance/explorer/domain/extensions/ExtenstionsKtTest.kt index 27a2a58d..ef0420e5 100644 --- a/service/src/test/kotlin/io/provenance/explorer/domain/extensions/ExtenstionsKtTest.kt +++ b/service/src/test/kotlin/io/provenance/explorer/domain/extensions/ExtenstionsKtTest.kt @@ -29,4 +29,23 @@ class ExtensionsKtTest { assertEquals("{\"marker\":{\"transfer-auths\":[\"tp19zf8q9swrsspkdljumwh04zjac4nkfvju6ehl9\",\"tp1tk6fqws0su7fzp090edrauaa756mdyjfdw0507\",\"tp1a53udazy8ayufvy0s434pfwjcedzqv34vfvvyc\"],\"allow-force-transfer\":false}}", actualJsonObj.get("memo").asText(), testname) } } + + @Test + fun `test tryFromBase64 with various cases`() { + val base64String = "SGVsbG8gd29ybGQ=" + val expectedDecodedString = "Hello world" + assertEquals(expectedDecodedString, base64String.tryFromBase64(), "Valid Base64 string should decode to 'Hello world'") + + val invalidBase64String = "Hello world" + val expectedInvalidOutput = invalidBase64String + assertEquals(expectedInvalidOutput, invalidBase64String.tryFromBase64(), "Invalid Base64 string should return the original string") + + val emptyString = "" + val expectedEmptyOutput = emptyString + assertEquals(expectedEmptyOutput, emptyString.tryFromBase64(), "Empty string should return the original string") + + val malformedBase64String = "SGVsbG8gd29ybGQ" + val expectedMalformedOutput = malformedBase64String + assertEquals(expectedMalformedOutput, malformedBase64String.tryFromBase64(), "Malformed Base64 string should return the original string") + } }