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
4 changes: 4 additions & 0 deletions .kotlin/errors/errors-1770458785068.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
kotlin version: 2.0.20
error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
1. Kotlin compile daemon is ready

416 changes: 416 additions & 0 deletions README-v0.3.md

Large diffs are not rendered by default.

691 changes: 319 additions & 372 deletions README.md

Large diffs are not rendered by default.

15 changes: 7 additions & 8 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
kotlin("jvm") version "1.7.21"
kotlin("jvm") version "2.0.20"
application
}

group = "be.vamaralds"
version = "0.3"
version = "1.0"

repositories {
mavenCentral()
Expand All @@ -28,10 +27,10 @@ tasks.test {
useJUnitPlatform()
}

tasks.withType<KotlinCompile> {
kotlinOptions.jvmTarget = "1.8"
}

application {
mainClass.set("MainKt")
}
}

kotlin {
jvmToolchain(8)
}
17 changes: 16 additions & 1 deletion src/main/kotlin/Main.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
import be.vamaralds.lib.*
import org.hyperledger.fabric.gateway.Contract
import java.util.logging.Logger
import java.util.logging.Level
import java.util.logging.LogManager

fun main(args: Array<String>) {
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "off");
// Keep application logs but suppress HLF verbose logs
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "warn")
System.setProperty("org.slf4j.simpleLogger.log.be.vamaralds", "info")
System.setProperty("org.slf4j.simpleLogger.log.org.hyperledger.fabric.gateway", "error")
System.setProperty("org.slf4j.simpleLogger.log.org.hyperledger.fabric.sdk", "error")
System.setProperty("org.slf4j.simpleLogger.log.io.grpc", "error")
System.setProperty("org.slf4j.simpleLogger.showDateTime", "false")
System.setProperty("org.slf4j.simpleLogger.showThreadName", "false")
System.setProperty("org.slf4j.simpleLogger.showLogName", "false")

// Suppress OpenTelemetry span export warnings (java.util.logging)
LogManager.getLogManager().getLogger("").level = Level.SEVERE
Logger.getLogger("io.opentelemetry").level = Level.SEVERE
Logger.getLogger("io.opentelemetry.sdk.internal").level = Level.SEVERE

try {
TestChaincode().main(args)
Expand Down
17 changes: 13 additions & 4 deletions src/main/kotlin/be/vamaralds/lib/ChaincodeTestCase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ data class ChaincodeTestCase(
val expectedAttributeValues: List<ExpectedAttributeValue<*>> = emptyList(),
val expectedBOStates: List<ExpectedState> = emptyList(),
val expectedToSucceed: Boolean = true,
val thenMarkAsReady: Boolean = false
val thenMarkAsReady: Boolean = false,
val submittedBy: String? = null // Organization name that submits this transaction
) {
companion object {
fun fromJson(json: String): ChaincodeTestCase {
Expand All @@ -36,7 +37,8 @@ data class ChaincodeTestCase(
ExpectedState(boId, expectedStateName)
}
val expectedToSucceed = jsonObject.getBoolean("expectedToSucceed")
return ChaincodeTestCase(name, businessEventName, payload, expectedAttributeValues, expectedBOStates, expectedToSucceed, thenMarkAsReady)
val submittedBy = if (jsonObject.has("submittedBy")) jsonObject.optString("submittedBy") else null
return ChaincodeTestCase(name, businessEventName, payload, expectedAttributeValues, expectedBOStates, expectedToSucceed, thenMarkAsReady, submittedBy)
}
}
}
Expand All @@ -46,6 +48,13 @@ sealed interface ChaincodeTestResult {
fun toJsonString(): String = JSONObject(this).toString()
}

data class SuccessfulChaincodeTestResult(override val testCase: ChaincodeTestCase): ChaincodeTestResult
data class FailedChaincodeTestResult(override val testCase: ChaincodeTestCase, val reasons: List<String> = emptyList()): ChaincodeTestResult
data class SuccessfulChaincodeTestResult(
override val testCase: ChaincodeTestCase,
val validatedObjects: Map<String, JsonBusinessObject> = emptyMap()
): ChaincodeTestResult

data class FailedChaincodeTestResult(
override val testCase: ChaincodeTestCase,
val reasons: List<String> = emptyList()
): ChaincodeTestResult

12 changes: 6 additions & 6 deletions src/main/kotlin/be/vamaralds/lib/ChaincodeTestr.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ class IncorrectWalletPathException(msg: String): ChaincodeTestrException(msg)
class ChaincodeTestr(val config: ConnectionConfiguration) {
@Throws(ChaincodeTestrException::class)
fun connect(): Connection {
ChaincodeApplication.logger.info { "Initiating Connection to the HLF network" }
ChaincodeApplication.logger.debug { "Initiating Connection to the HLF network" }
//Retrieving Wallet to Interact with the Network
val walletPath = Paths.get(config.walletPath)
var wallet: Wallet?

try {
wallet = Wallets.newFileSystemWallet(walletPath)
ChaincodeApplication.logger.info { "Retrieving Connection Wallet: Successful" }
ChaincodeApplication.logger.debug { "Retrieving Connection Wallet: Successful" }
} catch (e: Exception) {
val error = IncorrectWalletPathException("Failed to read wallet in folder: $walletPath. Make sure that the wallet path is correct")
ChaincodeApplication.logger.error(error) { "Retrieving Connection Wallet: Failed: $error" }
Expand Down Expand Up @@ -48,7 +48,7 @@ class ChaincodeTestr(val config: ConnectionConfiguration) {
var gateway: Gateway?
try {
gateway = gatewayBuilder.connect()
ChaincodeApplication.logger.info { "Connection to the HLF network: Successful" }
ChaincodeApplication.logger.debug { "Connection to the HLF network: Successful" }
} catch(e: Exception) {
val error = ChaincodeTestrException("Failed to connect to the network: ${e.javaClass.simpleName}: ${e.message}")
throw error
Expand All @@ -59,21 +59,21 @@ class ChaincodeTestr(val config: ConnectionConfiguration) {

try {
network = gateway.getNetwork(config.channelName)
ChaincodeApplication.logger.info { "Selecting Channel ${config.channelName}: Successful" }
ChaincodeApplication.logger.debug { "Selecting Channel ${config.channelName}: Successful" }
} catch(e: Exception) {
val error = ChaincodeTestrException("Failed to get the network: ${e.javaClass.simpleName}: ${e.message}")
throw error
}

try {
contract = network.getContract(config.chaincodeName)
ChaincodeApplication.logger.info { "Retrieving contract ${config.chaincodeName}" }
ChaincodeApplication.logger.debug { "Retrieving contract ${config.chaincodeName}" }
} catch(e: Exception) {
val error = ChaincodeTestrException("Failed to get the contract: ${e.javaClass.simpleName}: ${e.message}")
throw error
}

ChaincodeApplication.logger.info { "Connection established to the HLF network and contract ${config.chaincodeName}" }
ChaincodeApplication.logger.debug { "Connection established to the HLF network and contract ${config.chaincodeName}" }
return Connection(wallet, gateway, network, contract)
}
}
30 changes: 19 additions & 11 deletions src/main/kotlin/be/vamaralds/lib/ContractHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,21 @@ class ContractHandler(private val contract: Contract) {

fun runTestSuite(testSuite: ChaincodeTestSuite): ChaincodeTestSuiteResult {
val results = testSuite.testsCases.map { testCase ->
Pair(testCase, runTest(testCase))
Pair(testCase, runSingleTest(testCase))
}.toMap()

return ChaincodeTestSuiteResult(testSuite.name, results)
}

private fun runTest(test: ChaincodeTestCase): ChaincodeTestResult {
ChaincodeApplication.logger.info { "Running test: ${test.name}" }
fun runSingleTest(test: ChaincodeTestCase): ChaincodeTestResult {
return runSingleTest(test, this)
}

fun runSingleTest(test: ChaincodeTestCase, validationHandler: ContractHandler): ChaincodeTestResult {
ChaincodeApplication.logger.debug { "Running test: ${test.name}" }
val errors = mutableListOf<String>()
val validatedObjects = mutableMapOf<String, JsonBusinessObject>()

try {
handleEvent(test.businessEventName, test.payload)
if(!test.expectedToSucceed)
Expand All @@ -32,15 +38,16 @@ class ContractHandler(private val contract: Contract) {

} catch(e: ContractTransactionException) {
if(test.expectedToSucceed)
return FailedChaincodeTestResult(test, listOf("Expected transaction to succeed, but it failed (${e.stackTrace})"))
return FailedChaincodeTestResult(test, listOf("Expected transaction to succeed, but it failed: ${e.message}"))
}

test.expectedAttributeValues.forEach { expectedAttributeValue ->
var boJson: JsonBusinessObject?
try {
boJson = getBusinessObject(expectedAttributeValue.boId)
boJson = validationHandler.getBusinessObject(expectedAttributeValue.boId)
validatedObjects[expectedAttributeValue.boId] = boJson
} catch(e: ContractTransactionException) {
errors.add("Failed to get Business Object with id: ${expectedAttributeValue.boId}")
errors.add("Failed to get Business Object with id: ${expectedAttributeValue.boId} - ${e.message}")
return FailedChaincodeTestResult(test, errors)
}

Expand All @@ -53,9 +60,10 @@ class ContractHandler(private val contract: Contract) {
test.expectedBOStates.forEach { expectedState ->
var boJson: JsonBusinessObject?
try {
boJson = getBusinessObject(expectedState.boId)
boJson = validationHandler.getBusinessObject(expectedState.boId)
validatedObjects[expectedState.boId] = boJson
} catch(e: ContractTransactionException) {
errors.add("Failed to get Business Object with id: ${expectedState.boId}")
errors.add("Failed to get Business Object with id: ${expectedState.boId} - ${e.message}")
return FailedChaincodeTestResult(test, errors)
}

Expand All @@ -66,7 +74,7 @@ class ContractHandler(private val contract: Contract) {
}

return if(errors.isEmpty())
SuccessfulChaincodeTestResult(test)
SuccessfulChaincodeTestResult(test, validatedObjects)
else
FailedChaincodeTestResult(test, errors)
}
Expand Down Expand Up @@ -151,10 +159,10 @@ class ContractHandler(private val contract: Contract) {

@Throws(ContractTransactionException::class)
fun evaluateTransaction(transactionName: String, vararg args: String): String {
ChaincodeApplication.logger.info { "Evaluating transaction $transactionName with args: ${args.joinToString()}" }
ChaincodeApplication.logger.debug { "Evaluating transaction $transactionName with args: ${args.joinToString()}" }
try {
val result = contract.evaluateTransaction(transactionName, *args).toString(Charsets.UTF_8)
ChaincodeApplication.logger.info { "Transaction $transactionName evaluated successfully - Result: $result" }
ChaincodeApplication.logger.debug { "Transaction $transactionName evaluated successfully - Result: $result" }
return result
} catch(e: Exception) {
when(e) {
Expand Down
78 changes: 78 additions & 0 deletions src/main/kotlin/be/vamaralds/lib/MultiOrgConnectionManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package be.vamaralds.lib

/**
* Manages multiple connections to the network, one for each organization
* This allows switching between organizations when submitting transactions
*/
class MultiOrgConnectionManager(
private val workspaceManager: WorkspaceManager,
private val chaincodeName: String
) {
private val connections = mutableMapOf<String, Connection>()
private var channelName: String? = null

/**
* Gets or creates a connection for a specific organization
*/
fun getConnection(orgName: String): Connection {
// Validate organization exists
workspaceManager.validateOrganization(orgName)

// Return existing connection if available
if (connections.containsKey(orgName)) {
return connections[orgName]!!
}

// Create new connection
ChaincodeApplication.logger.info { "Creating connection for organization: $orgName" }
ChaincodeApplication.logger.info { " Wallet: ${workspaceManager.getWalletPath(orgName)}" }
ChaincodeApplication.logger.info { " Identity: ${workspaceManager.getIdentityName(orgName)}" }

val config = ConnectionConfiguration(
walletPath = workspaceManager.getWalletPath(orgName),
identityName = workspaceManager.getIdentityName(orgName),
connectionProfilePath = workspaceManager.getConnectionProfilePath(orgName),
channelName = getChannelName(orgName),
chaincodeName = chaincodeName
)

val connection = ChaincodeTestr(config).connect()
connections[orgName] = connection

ChaincodeApplication.logger.info { "Successfully created connection for organization: $orgName" }
return connection
}

/**
* Gets the channel name (discovers on first call, then caches)
*/
private fun getChannelName(orgName: String): String {
if (channelName == null) {
channelName = workspaceManager.getChannelName(orgName)
ChaincodeApplication.logger.debug { "Discovered channel name: $channelName" }
}
return channelName!!
}

/**
* Gets all available organizations
*/
fun getAvailableOrganizations(): List<String> {
return workspaceManager.getOrganizations()
}

/**
* Closes all open connections
*/
fun closeAll() {
ChaincodeApplication.logger.info { "Closing all connections (${connections.size} organizations)" }
connections.values.forEach { connection ->
try {
connection.gateway.close()
} catch (e: Exception) {
ChaincodeApplication.logger.warn { "Error closing connection: ${e.message}" }
}
}
connections.clear()
}
}
75 changes: 75 additions & 0 deletions src/main/kotlin/be/vamaralds/lib/PayloadResolver.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package be.vamaralds.lib

import org.json.JSONArray
import org.json.JSONObject

/**
* Resolves organization placeholders in test case payloads
* Replaces $orgName with actual public keys from the workspace
*/
class PayloadResolver(private val workspaceManager: WorkspaceManager) {

/**
* Resolves all organization placeholders in a JSON payload string
* Example: {"publicKey": "$org1"} becomes {"publicKey": "MFkw..."}
*/
fun resolvePayload(payloadJson: String): String {
val jsonObject = JSONObject(payloadJson)
resolveJsonObject(jsonObject)
return jsonObject.toString()
}

/**
* Recursively resolves placeholders in a JSON object
*/
private fun resolveJsonObject(jsonObject: JSONObject) {
val keys = jsonObject.keys()
while (keys.hasNext()) {
val key = keys.next()
val value = jsonObject.get(key)

when (value) {
is String -> {
if (value.startsWith("$")) {
val orgName = value.substring(1)
try {
val publicKey = workspaceManager.getPublicKey(orgName)
jsonObject.put(key, publicKey)
ChaincodeApplication.logger.debug { "Resolved placeholder '$value' to public key for organization: $orgName" }
} catch (e: WorkspaceException) {
ChaincodeApplication.logger.warn { "Could not resolve placeholder '$value': ${e.message}" }
}
}
}
is JSONObject -> resolveJsonObject(value)
is JSONArray -> resolveJsonArray(value)
}
}
}

/**
* Recursively resolves placeholders in a JSON array
*/
private fun resolveJsonArray(jsonArray: JSONArray) {
for (i in 0 until jsonArray.length()) {
val value = jsonArray.get(i)

when (value) {
is String -> {
if (value.startsWith("$")) {
val orgName = value.substring(1)
try {
val publicKey = workspaceManager.getPublicKey(orgName)
jsonArray.put(i, publicKey)
ChaincodeApplication.logger.debug { "Resolved placeholder '$value' to public key for organization: $orgName" }
} catch (e: WorkspaceException) {
ChaincodeApplication.logger.warn { "Could not resolve placeholder '$value': ${e.message}" }
}
}
}
is JSONObject -> resolveJsonObject(value)
is JSONArray -> resolveJsonArray(value)
}
}
}
}
Loading
Loading