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,44 @@
package org.hyperledger.iroha.sdk.core.model

@JvmInline
value class JsonValue(val rawJson: String) {
companion object {
fun string(value: String): JsonValue {
val sb = StringBuilder(value.length + 2)
sb.append('"')
for (c in value) {
when (c) {
'"' -> sb.append("\\\"")
'\\' -> sb.append("\\\\")
'\b' -> sb.append("\\b")
'\u000C' -> sb.append("\\f")
'\n' -> sb.append("\\n")
'\r' -> sb.append("\\r")
'\t' -> sb.append("\\t")
else -> {
if (c < ' ') {
sb.append("\\u00")
sb.append(HEX_DIGITS[(c.code shr 4) and 0xF])
sb.append(HEX_DIGITS[c.code and 0xF])
} else {
sb.append(c)
}
}
}
}
sb.append('"')
return JsonValue(sb.toString())
}

fun number(value: Long): JsonValue = JsonValue(value.toString())

fun bool(value: Boolean): JsonValue = JsonValue(if (value) "true" else "false")

fun raw(json: String): JsonValue = JsonValue(json)

private val HEX_DIGITS = charArrayOf(
'0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ class TransactionPayload(
val executable: Executable = Executable.ivm(byteArrayOf()),
val timeToLiveMs: Long? = null,
val nonce: Int? = null,
metadata: Map<String, String> = emptyMap(),
metadata: Map<String, JsonValue> = emptyMap(),
) {
private val _metadata: Map<String, String> = metadata.toMap()
private val _metadata: Map<String, JsonValue> = metadata.toMap()

val metadata: Map<String, String> get() = _metadata
val metadata: Map<String, JsonValue> get() = _metadata

init {
require(chainId.isNotBlank()) { "chainId must not be blank" }
Expand All @@ -50,7 +50,7 @@ class TransactionPayload(
executable: Executable = this.executable,
timeToLiveMs: Long? = this.timeToLiveMs,
nonce: Int? = this.nonce,
metadata: Map<String, String> = this.metadata,
metadata: Map<String, JsonValue> = this.metadata,
): TransactionPayload = TransactionPayload(
chainId = chainId,
authority = authority,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import org.hyperledger.iroha.sdk.client.ClientResponse
import org.hyperledger.iroha.sdk.client.IrohaClient
import org.hyperledger.iroha.sdk.client.PipelineStatusOptions
import org.hyperledger.iroha.sdk.core.model.Executable
import org.hyperledger.iroha.sdk.core.model.JsonValue
import org.hyperledger.iroha.sdk.core.model.TransactionPayload
import org.hyperledger.iroha.sdk.core.model.instructions.TransferWirePayloadEncoder
import org.hyperledger.iroha.sdk.crypto.IrohaHash
Expand Down Expand Up @@ -193,7 +194,7 @@ class NexusAppClient @JvmOverloads constructor(
executable = Executable.instructions(listOf(instruction)),
timeToLiveMs = normalized.ttlMs,
nonce = normalized.nonce,
metadata = normalized.metadata,
metadata = normalized.metadata.mapValues { JsonValue.string(it.value) },
)
val payloadBytes = codecAdapter.encodeTransaction(payload)
val signable = NexusSignableTransaction(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import org.hyperledger.iroha.sdk.client.TransportSecurity
import org.hyperledger.iroha.sdk.client.transport.TransportRequest
import org.hyperledger.iroha.sdk.core.model.Executable
import org.hyperledger.iroha.sdk.core.model.InstructionBox
import org.hyperledger.iroha.sdk.core.model.JsonValue
import org.hyperledger.iroha.sdk.core.model.TransactionPayload
import org.hyperledger.iroha.sdk.crypto.Signer
import org.hyperledger.iroha.sdk.norito.NoritoCodec
Expand Down Expand Up @@ -1216,7 +1217,7 @@ class IrohaOfflineNoteTransactionSubmitter @JvmOverloads constructor(
authority = authority,
creationTimeMs = clock.getAsLong(),
executable = Executable.instructions(instructions),
metadata = transactionMetadata,
metadata = transactionMetadata.mapValues { JsonValue.string(it.value) },
)
return client.submitTransaction(transactionBuilder.encodeAndSign(payload, signer))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import org.hyperledger.iroha.sdk.address.decodeCompactPublicKeyPayload
import org.hyperledger.iroha.sdk.address.requireCanonicalI105Address
import org.hyperledger.iroha.sdk.core.model.Executable
import org.hyperledger.iroha.sdk.core.model.InstructionBox
import org.hyperledger.iroha.sdk.core.model.JsonValue
import org.hyperledger.iroha.sdk.core.model.TransactionPayload
import org.hyperledger.iroha.sdk.core.model.WirePayload
import org.hyperledger.iroha.sdk.norito.NoritoAdapters
Expand Down Expand Up @@ -302,23 +303,23 @@ internal class TransactionPayloadAdapter : TypeAdapter<TransactionPayload> {
}
}

private class JsonAdapter : TypeAdapter<String> {
private class JsonValueFieldAdapter : TypeAdapter<String> {
override fun encode(encoder: NoritoEncoder, value: String) {
encodeSizedField(encoder, JSON_STRING_ADAPTER, value)
encodeSizedField(encoder, STRING_ADAPTER, value)
}

override fun decode(decoder: NoritoDecoder): String {
return decodeSizedField(decoder, JSON_STRING_ADAPTER)
return decodeSizedField(decoder, STRING_ADAPTER)
}

override fun isSelfDelimiting(): Boolean = true
}

private class MetadataAdapter : TypeAdapter<Map<String, String>> {
private class MetadataAdapter : TypeAdapter<Map<String, JsonValue>> {
private val entryListAdapter: TypeAdapter<List<MetadataEntry>> =
NoritoAdapters.sequence(MetadataEntryAdapter())

override fun encode(encoder: NoritoEncoder, value: Map<String, String>) {
override fun encode(encoder: NoritoEncoder, value: Map<String, JsonValue>) {
val keys = value.keys.sorted()
val entries = keys.map { key ->
val entryValue = value[key]
Expand All @@ -328,50 +329,36 @@ internal class TransactionPayloadAdapter : TypeAdapter<TransactionPayload> {
entryListAdapter.encode(encoder, entries)
}

override fun decode(decoder: NoritoDecoder): Map<String, String> {
override fun decode(decoder: NoritoDecoder): Map<String, JsonValue> {
val entries = entryListAdapter.decode(decoder)
val decoded = LinkedHashMap<String, String>(entries.size)
val decoded = LinkedHashMap<String, JsonValue>(entries.size)
for (entry in entries) {
require(decoded.put(entry.key, entry.value) == null) { "Duplicate metadata key" }
}
return decoded
}
}

private class MetadataEntry(val key: String, val value: String)
private class MetadataEntry(val key: String, val value: JsonValue)

private class MetadataEntryAdapter : TypeAdapter<MetadataEntry> {
override fun encode(encoder: NoritoEncoder, value: MetadataEntry) {
encodeSizedField(encoder, STRING_ADAPTER, value.key)
encodeSizedField(encoder, JSON_ADAPTER, value.value)
encodeSizedField(encoder, JSON_VALUE_ADAPTER, value.value.rawJson)
}

override fun decode(decoder: NoritoDecoder): MetadataEntry {
val key = decodeSizedField(decoder, STRING_ADAPTER)
val value = decodeSizedField(decoder, JSON_ADAPTER)
return MetadataEntry(key, value)
}
}

private class JsonStringAdapter : TypeAdapter<String> {
override fun encode(encoder: NoritoEncoder, value: String) {
STRING_ADAPTER.encode(encoder, encodeJsonString(value))
}

override fun decode(decoder: NoritoDecoder): String {
val raw = STRING_ADAPTER.decode(decoder)
return decodeJsonString(raw)
val raw = decodeSizedField(decoder, JSON_VALUE_ADAPTER)
return MetadataEntry(key, JsonValue.raw(raw))
}

override fun isSelfDelimiting(): Boolean = true
}

companion object {
private val STRING_ADAPTER: TypeAdapter<String> = NoritoAdapters.stringAdapter()
private val ACCOUNT_ID_ADAPTER: TypeAdapter<String> = AccountIdAdapter()
private val CHAIN_ID_ADAPTER: TypeAdapter<String> = ChainIdAdapter()
private val JSON_STRING_ADAPTER: TypeAdapter<String> = JsonStringAdapter()
private val JSON_ADAPTER: TypeAdapter<String> = JsonAdapter()
private val JSON_VALUE_ADAPTER: TypeAdapter<String> = JsonValueFieldAdapter()
private val UINT64_ADAPTER: TypeAdapter<Long> = NoritoAdapters.uint(64)
private val UINT16_ADAPTER: TypeAdapter<Long> = NoritoAdapters.uint(16)
private val UINT8_ADAPTER: TypeAdapter<Long> = NoritoAdapters.uint(8)
Expand All @@ -388,11 +375,9 @@ internal class TransactionPayloadAdapter : TypeAdapter<TransactionPayload> {
private val TTL_ADAPTER: TypeAdapter<Optional<Long>> = NoritoAdapters.option(NoritoAdapters.uint(64))
private val NONCE_ADAPTER: TypeAdapter<Optional<Long>> = NoritoAdapters.option(NoritoAdapters.uint(32))
private val EXECUTABLE_ADAPTER: TypeAdapter<Executable> = ExecutableAdapter()
private val METADATA_ADAPTER: TypeAdapter<Map<String, String>> = MetadataAdapter()
private val METADATA_ADAPTER: TypeAdapter<Map<String, JsonValue>> = MetadataAdapter()
private const val INSTRUCTION_BOX_SCHEMA = "iroha.data_model.isi.InstructionBox.v1"

private val HEX_DIGITS = "0123456789ABCDEF".toCharArray()

internal fun encodeInstructionBox(value: InstructionBox): ByteArray =
NoritoCodec.encode(value, INSTRUCTION_BOX_SCHEMA, InstructionAdapter())

Expand Down Expand Up @@ -479,85 +464,5 @@ internal class TransactionPayloadAdapter : TypeAdapter<TransactionPayload> {
false
}
}

private fun encodeJsonString(value: String): String {
val builder = StringBuilder(value.length + 2)
builder.append('"')
for (c in value) {
when (c) {
'"' -> builder.append("\\\"")
'\\' -> builder.append("\\\\")
'\b' -> builder.append("\\b")
'\u000C' -> builder.append("\\f")
'\n' -> builder.append("\\n")
'\r' -> builder.append("\\r")
'\t' -> builder.append("\\t")
else -> {
if (c < '\u0020') {
builder.append("\\u00")
builder.append(HEX_DIGITS[(c.code shr 4) and 0xF])
builder.append(HEX_DIGITS[c.code and 0xF])
} else {
builder.append(c)
}
}
}
}
builder.append('"')
return builder.toString()
}

private fun decodeJsonString(raw: String?): String {
if (raw == null) return raw.toString()
val trimmed = raw.trim()
if (trimmed.length < 2 || trimmed[0] != '"' || trimmed[trimmed.length - 1] != '"') return raw
return try {
parseJsonString(trimmed)
} catch (_: IllegalArgumentException) {
raw
}
}

private fun parseJsonString(input: String): String {
val builder = StringBuilder()
var i = 1
while (i < input.length - 1) {
val c = input[i++]
if (c == '\\') {
require(i < input.length - 1) { "Invalid JSON escape" }
val esc = input[i++]
when (esc) {
'"' -> builder.append('"')
'\\' -> builder.append('\\')
'/' -> builder.append('/')
'b' -> builder.append('\b')
'f' -> builder.append('\u000C')
'n' -> builder.append('\n')
'r' -> builder.append('\r')
't' -> builder.append('\t')
'u' -> {
require(i + 4 <= input.length - 1) { "Invalid unicode escape" }
var codePoint = 0
for (j in 0 until 4) {
codePoint = (codePoint shl 4) or hexNibble(input[i + j])
}
builder.append(codePoint.toChar())
i += 4
}
else -> throw IllegalArgumentException("Unsupported escape: \\$esc")
}
} else {
builder.append(c)
}
}
return builder.toString()
}

private fun hexNibble(c: Char): Int = when (c) {
in '0'..'9' -> c - '0'
in 'a'..'f' -> 10 + (c - 'a')
in 'A'..'F' -> 10 + (c - 'A')
else -> throw IllegalArgumentException("Invalid hex digit: $c")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import org.hyperledger.iroha.sdk.address.encodePublicKeyMultihash
import org.hyperledger.iroha.sdk.client.transport.TransportRequest
import org.hyperledger.iroha.sdk.client.transport.TransportResponse
import org.hyperledger.iroha.sdk.crypto.IrohaHash
import org.hyperledger.iroha.sdk.core.model.JsonValue
import org.hyperledger.iroha.sdk.core.model.TransactionPayload
import org.hyperledger.iroha.sdk.norito.NoritoAdapters
import org.hyperledger.iroha.sdk.norito.NoritoCodec
Expand Down Expand Up @@ -2318,7 +2319,7 @@ class HttpClientTransportTest {
creationTimeMs = 1_700_000_000_000L + seed,
timeToLiveMs = 5_000L,
nonce = seed + 1,
metadata = mapOf("note" to "tx-$seed"),
metadata = mapOf("note" to JsonValue.string("tx-$seed")),
),
)
val signature = ByteArray(64) { (seed + 1).toByte() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,28 +93,28 @@ class TransactionPayloadTest {
@Test
fun `blank metadata key throws`() {
assertFailsWith<IllegalArgumentException> {
TransactionPayload(metadata = mapOf(" " to "value"), creationTimeMs = 1000L)
TransactionPayload(metadata = mapOf(" " to JsonValue.string("value")), creationTimeMs = 1000L)
}
}

@Test
fun `defensive copy on metadata input`() {
val original = mutableMapOf("key" to "value")
val original = mutableMapOf("key" to JsonValue.string("value"))
val payload = TransactionPayload(metadata = original, creationTimeMs = 1000L)
original["injected"] = "bad"
original["injected"] = JsonValue.string("bad")
assertEquals(1, payload.metadata.size)
assertEquals("value", payload.metadata["key"])
assertEquals(JsonValue.string("value"), payload.metadata["key"])
}

@Test
fun `metadata getter returns immutable snapshot`() {
val payload = TransactionPayload(
metadata = mapOf("a" to "1"),
metadata = mapOf("a" to JsonValue.string("1")),
creationTimeMs = 1000L,
)
val meta = payload.metadata
assertFailsWith<UnsupportedOperationException> {
(meta as MutableMap)["b"] = "2"
(meta as MutableMap)["b"] = JsonValue.string("2")
}
}

Expand Down Expand Up @@ -151,7 +151,7 @@ class TransactionPayloadTest {
executable = executable,
timeToLiveMs = 500,
nonce = 1,
metadata = mapOf("k" to "v"),
metadata = mapOf("k" to JsonValue.string("v")),
)
val b = TransactionPayload(
chainId = "c",
Expand All @@ -160,7 +160,7 @@ class TransactionPayloadTest {
executable = executable,
timeToLiveMs = 500,
nonce = 1,
metadata = mapOf("k" to "v"),
metadata = mapOf("k" to JsonValue.string("v")),
)
assertEquals(a, b)
assertEquals(a.hashCode(), b.hashCode())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import org.hyperledger.iroha.sdk.client.JsonParser
import org.hyperledger.iroha.sdk.client.ToriiCanonicalRequestAuth
import org.hyperledger.iroha.sdk.client.transport.TransportRequest
import org.hyperledger.iroha.sdk.client.transport.TransportResponse
import org.hyperledger.iroha.sdk.core.model.JsonValue
import org.hyperledger.iroha.sdk.core.model.WirePayload
import org.hyperledger.iroha.sdk.crypto.Signer
import org.hyperledger.iroha.sdk.tx.SignedTransaction
Expand Down Expand Up @@ -2788,7 +2789,7 @@ class OfflineNoteTest {

val signed = assertNotNull(client.submittedTransaction)
val payload = codec.decodeTransaction(signed.encodedPayload())
assertEquals(metadata, payload.metadata)
assertEquals(metadata.mapValues { JsonValue.string(it.value) }, payload.metadata)
}

@Test
Expand Down
Loading