diff --git a/README.md b/README.md
index 8eb63f2..6482c5a 100644
--- a/README.md
+++ b/README.md
@@ -19,30 +19,35 @@ A Java SDK for interacting with the Unicity network, enabling state transitions
## Installation
-### Gradle (JVM)
+### Using JitPack
+Add JitPack repository:
```groovy
-dependencies {
- implementation 'com.unicity.sdk:unicity-sdk:1.0-SNAPSHOT'
+repositories {
+ maven { url 'https://jitpack.io' }
}
```
-### Gradle (Android)
+#### For Android Projects:
+```groovy
+dependencies {
+ implementation 'com.github.unicitynetwork:java-state-transition-sdk:1.1:android'
+}
+```
+#### For JVM Projects:
```groovy
dependencies {
- implementation 'com.unicity.sdk:unicity-sdk-android:1.0-SNAPSHOT'
+ implementation 'com.github.unicitynetwork:java-state-transition-sdk:1.1:jvm'
}
```
-### Maven (JVM)
+### Using Local Maven
-```xml
-
- com.unicity.sdk
- unicity-sdk
- 1.0-SNAPSHOT
-
+```groovy
+dependencies {
+ implementation 'org.unicitylabs:java-state-transition-sdk:1.1-SNAPSHOT'
+}
```
## Quick Start
@@ -50,8 +55,8 @@ dependencies {
### Initialize the Client
```java
-import com.unicity.sdk.StateTransitionClient;
-import com.unicity.sdk.api.AggregatorClient;
+import org.unicitylabs.sdk.StateTransitionClient;
+import org.unicitylabs.sdk.api.AggregatorClient;
// Connect to the Unicity test network
String aggregatorUrl = "https://gateway-test.unicity.network";
@@ -62,12 +67,12 @@ StateTransitionClient client = new StateTransitionClient(aggregatorClient);
### Mint a Token
```java
-import com.unicity.sdk.token.*;
-import com.unicity.sdk.token.fungible.*;
-import com.unicity.sdk.transaction.*;
-import com.unicity.sdk.predicate.*;
-import com.unicity.sdk.shared.signing.SigningService;
-import com.unicity.sdk.shared.hash.HashAlgorithm;
+import org.unicitylabs.sdk.token.*;
+import org.unicitylabs.sdk.token.fungible.*;
+import org.unicitylabs.sdk.transaction.*;
+import org.unicitylabs.sdk.predicate.*;
+import org.unicitylabs.sdk.signing.SigningService;
+import org.unicitylabs.sdk.hash.HashAlgorithm;
// Create signing service from secret
byte[] secret = "your-secret-key".getBytes();
@@ -227,7 +232,7 @@ Token> updatedToken = client.finishTransaction(
### Clone the Repository
```bash
-git clone https://github.com/unicity/java-state-transition-sdk.git
+git clone https://github.com/unicitynetwork/java-state-transition-sdk.git
cd java-state-transition-sdk
```
@@ -247,7 +252,7 @@ cd java-state-transition-sdk
./gradlew integrationTest
# Run E2E tests against deployed aggregator
-AGGREGATOR_URL=https://gateway-test.unicity.network ./gradlew integrationTest --tests "*E2ETest"
+AGGREGATOR_URL=https://gateway-test.unicity.network ./gradlew integrationTest
```
## Platform-Specific Considerations
@@ -268,23 +273,25 @@ The standard JVM version uses:
## Architecture
-The SDK follows a modular architecture:
+The SDK follows a modular architecture under `org.unicitylabs.sdk`:
- **`api`**: Core API interfaces and aggregator client
-- **`api`**: Core API interfaces and aggregator client
-- **`address`**: Address schemes and implementations
-- **`predicate`**: Ownership predicates (Masked, Unmasked, Burn) and authorization
-- **`serializer`**: CBOR and JSON serializers for tokens and transactions
-- **`token`**: Token-related classes (TokenId, TokenType, TokenState) and fungible token support
-- **`transaction`**: Transaction types (Mint, Transfer, Commitment) and builders
-- **`shared`**: Common utilities
- - `cbor`: CBOR encoding/decoding
- - `hash`: Cryptographic hashing (SHA256, SHA224, SHA384, SHA512, RIPEMD160)
- - `jsonrpc`: JSON-RPC transport layer
- - `signing`: Digital signature support (ECDSA secp256k1)
- - `smt`/`smst`: Sparse Merkle Tree implementations
- - `util`: BitString and other utilities
-- **`utils`**: Helper utilities
+- **`address`**: Address schemes and implementations (DirectAddress, ProxyAddress)
+- **`hash`**: Cryptographic hashing (SHA256, SHA224, SHA384, SHA512, RIPEMD160)
+- **`jsonrpc`**: JSON-RPC transport layer with OkHttp
+- **`mtree`**: Merkle tree implementations
+ - `plain`: Sparse Merkle Tree (SMT)
+ - `sum`: Sparse Merkle Sum Tree (SMST)
+- **`predicate`**: Ownership predicates (Masked, Unmasked, Burn, Default)
+- **`serializer`**: CBOR and JSON serializers hierarchy
+ - `cbor/`: CBOR serializers for all domain objects
+ - `json/`: JSON serializers for all domain objects
+- **`signing`**: Digital signature support (ECDSA secp256k1)
+- **`token`**: Token types including fungible tokens and nametags
+ - `fungible`: Fungible token support with CoinId and TokenCoinData
+- **`transaction`**: Transaction types and builders
+ - `split`: Token splitting functionality with TokenSplitBuilder
+- **`util`**: Utilities including BitString and HexConverter
## Error Handling
@@ -326,9 +333,10 @@ The SDK includes comprehensive test suites:
Located in `src/test/java`, these test individual components in isolation.
### Integration Tests
-Located in `src/test/java/com/unicity/sdk/integration`:
-- `TokenIntegrationTest`: Tests against Docker-based local aggregator
-- `TokenE2ETest`: Tests against deployed aggregator (requires `AGGREGATOR_URL` env var)
+Located in `src/test/java/org/unicitylabs/sdk/`:
+- `integration/TokenIntegrationTest`: Tests against Docker-based local aggregator
+- `e2e/TokenE2ETest`: E2E tests using CommonTestFlow (requires `AGGREGATOR_URL` env var)
+- `e2e/BasicE2ETest`: Basic connectivity and performance tests
### Running Tests
@@ -340,7 +348,7 @@ Located in `src/test/java/com/unicity/sdk/integration`:
./gradlew integrationTest
# Specific test class
-./gradlew test --tests "com.unicity.sdk.api.RequestIdTest"
+./gradlew test --tests "org.unicitylabs.sdk.api.RequestIdTest"
```
## License
diff --git a/build.gradle.kts b/build.gradle.kts
index 59e2983..ce8e542 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -48,9 +48,17 @@ dependencies {
testImplementation("org.testcontainers:testcontainers:1.19.8")
testImplementation("org.testcontainers:junit-jupiter:1.19.8")
testImplementation("org.testcontainers:mongodb:1.19.8")
+ testImplementation("org.awaitility:awaitility:4.2.0")
testImplementation("org.slf4j:slf4j-simple:2.0.13")
testImplementation("com.google.guava:guava:33.0.0-jre")
+ // ✅ Cucumber for BDD
+ testImplementation("io.cucumber:cucumber-java:7.27.2")
+ testImplementation("io.cucumber:cucumber-junit-platform-engine:7.27.2")
+
+ // JUnit 5 Suite annotations
+ testImplementation("org.junit.platform:junit-platform-suite:1.13.4")
+
checkstyle("com.puppycrawl.tools:checkstyle:10.26.1")
}
@@ -70,9 +78,16 @@ tasks.test {
excludeTags("integration")
}
maxHeapSize = "1024m"
+ systemProperty("cucumber.junit-platform.naming-strategy", "long")
+ systemProperties(System.getProperties().toMap() as Map)
+
+ filter {
+ excludeTestsMatching("*CucumberTestRunner*")
+ excludeTestsMatching("*Cucumber*")
+ }
}
-tasks.withType{
+tasks.withType {
reports {
xml.required.set(false)
html.required.set(true)
@@ -85,6 +100,69 @@ tasks.register("integrationTest") {
}
maxHeapSize = "2048m"
shouldRunAfter(tasks.test)
+ systemProperty("cucumber.junit-platform.naming-strategy", "long")
+
+ filter {
+ excludeTestsMatching("*CucumberTestRunner*")
+ excludeTestsMatching("*Cucumber*")
+ }
+}
+
+tasks.register("tokenTests") {
+ useJUnitPlatform()
+ maxHeapSize = "1024m"
+ systemProperty("cucumber.junit-platform.naming-strategy", "long")
+ // Set the system properties first
+ systemProperties = System.getProperties().toMap() as Map
+ // Then override the specific cucumber filter (this will take precedence)
+ systemProperty("cucumber.filter.tags", "@token-transfer")
+
+ filter {
+ includeTestsMatching("*CucumberTestRunner*")
+ }
+ shouldRunAfter(tasks.test)
+}
+
+tasks.register("aggregatorTests") {
+ useJUnitPlatform()
+ maxHeapSize = "1024m"
+ systemProperty("cucumber.junit-platform.naming-strategy", "long")
+ // Set the system properties first
+ systemProperties = System.getProperties().toMap() as Map
+ systemProperty("cucumber.filter.tags", "@aggregator-connectivity")
+
+ filter {
+ includeTestsMatching("*CucumberTestRunner*")
+ }
+ shouldRunAfter(tasks.test)
+}
+
+tasks.register("advancedTokenTests") {
+ useJUnitPlatform()
+ maxHeapSize = "1024m"
+ systemProperty("cucumber.junit-platform.naming-strategy", "long")
+ // Set the system properties first
+ systemProperties = System.getProperties().toMap() as Map
+ systemProperty("cucumber.filter.tags", "@advanced-token")
+
+ filter {
+ includeTestsMatching("*CucumberTestRunner*")
+ }
+ shouldRunAfter(tasks.test)
+}
+
+// ✅ Run all cucumber tests (including integration)
+tasks.register("allCucumberTests") {
+ useJUnitPlatform()
+ maxHeapSize = "1024m"
+ systemProperty("cucumber.junit-platform.naming-strategy", "long")
+ systemProperties = System.getProperties().toMap() as Map
+ systemProperty("cucumber.filter.tags", "not @ignore")
+
+ filter {
+ includeTestsMatching("*CucumberTestRunner*")
+ }
+ shouldRunAfter(tasks.test)
}
// Create separate JARs for each platform
@@ -112,40 +190,40 @@ publishing {
groupId = project.group.toString()
artifactId = "java-state-transition-sdk"
version = project.version.toString()
-
+
// Use the Java component as base - this includes the standard JAR
from(components["java"])
-
+
// Add Android JAR as additional artifact with classifier
artifact(tasks["androidJar"]) {
classifier = "android"
}
-
- // Add JVM JAR as additional artifact with classifier
+
+ // Add JVM JAR as additional artifact with classifier
artifact(tasks["jvmJar"]) {
classifier = "jvm"
}
-
+
// Simple POM configuration without XML manipulation
pom {
name.set("Unicity State Transition SDK")
description.set("Unicity State Transition SDK for Android and JVM")
url.set("https://github.com/unicitynetwork/java-state-transition-sdk")
-
+
licenses {
license {
name.set("MIT License")
url.set("https://opensource.org/licenses/MIT")
}
}
-
+
developers {
developer {
id.set("unicitynetwork")
name.set("Unicity Network")
}
}
-
+
scm {
connection.set("scm:git:git://github.com/unicitynetwork/java-state-transition-sdk.git")
developerConnection.set("scm:git:ssh://github.com/unicitynetwork/java-state-transition-sdk.git")
diff --git a/src/test/java/org/unicitylabs/sdk/e2e/CucumberTestRunner.java b/src/test/java/org/unicitylabs/sdk/e2e/CucumberTestRunner.java
new file mode 100644
index 0000000..dad2d86
--- /dev/null
+++ b/src/test/java/org/unicitylabs/sdk/e2e/CucumberTestRunner.java
@@ -0,0 +1,25 @@
+package org.unicitylabs.sdk.e2e;
+
+import io.cucumber.junit.platform.engine.Constants;
+import org.junit.platform.suite.api.*;
+
+/**
+ * Updated Cucumber test runner configuration for E2E tests.
+ * This class configures the test execution environment and feature discovery
+ * with the new shared step definitions approach.
+ */
+@Suite
+@IncludeEngines("cucumber")
+@SelectPackages("org.unicitylabs.sdk.features")
+@ConfigurationParameter(key = Constants.GLUE_PROPERTY_NAME, value = "org.unicitylabs.sdk.e2e.steps,org.unicitylabs.sdk.e2e.steps.shared,org.unicitylabs.sdk.e2e.config")
+@ConfigurationParameter(key = Constants.PLUGIN_PROPERTY_NAME, value = "pretty,html:build/cucumber-reports/cucumber.html,json:build/cucumber-reports/cucumber.json")
+@ConfigurationParameter(key = Constants.EXECUTION_DRY_RUN_PROPERTY_NAME, value = "false")
+@ConfigurationParameter(key = Constants.PLUGIN_PUBLISH_QUIET_PROPERTY_NAME, value = "true")
+public class CucumberTestRunner {
+ static {
+ // Only set default tags if no tags are specified
+ if (System.getProperty("cucumber.filter.tags") == null) {
+ System.setProperty("cucumber.filter.tags", "not @ignore");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/unicitylabs/sdk/e2e/config/CucumberConfiguration.java b/src/test/java/org/unicitylabs/sdk/e2e/config/CucumberConfiguration.java
new file mode 100644
index 0000000..ddb7f9c
--- /dev/null
+++ b/src/test/java/org/unicitylabs/sdk/e2e/config/CucumberConfiguration.java
@@ -0,0 +1,53 @@
+package org.unicitylabs.sdk.e2e.config;
+
+import org.unicitylabs.sdk.e2e.context.TestContext;
+import io.cucumber.java.Before;
+import io.cucumber.java.After;
+
+/**
+ * Cucumber configuration for dependency injection and test lifecycle management.
+ * This ensures that TestContext is properly shared across all step definition classes.
+ */
+public class CucumberConfiguration {
+
+ private static TestContext testContext = new TestContext();
+
+ /**
+ * Provides a shared TestContext instance for all step definition classes.
+ * This method will be called by step definition classes to get
+ * the shared TestContext instance.
+ */
+ public static TestContext getTestContext() {
+ return testContext;
+ }
+
+ /**
+ * Hook that runs before each scenario to reset the test context.
+ * This ensures each scenario starts with a clean state.
+ */
+ @Before
+ public void setUp() {
+ testContext.clearTestState(); // Clear test state but keep clients if they exist
+ System.out.println("Test context cleared for new scenario");
+ }
+
+ /**
+ * Hook that runs after each scenario for cleanup.
+ * This can be used for any additional cleanup if needed.
+ */
+ @After
+ public void tearDown() {
+ // Optional: Add any cleanup logic here
+ // For now, we keep the context alive for potential debugging
+ System.out.println("Scenario completed");
+ }
+
+ /**
+ * Hook that runs after scenarios tagged with @reset to completely reset the context.
+ */
+ @After("@reset")
+ public void fullReset() {
+ testContext.reset();
+ System.out.println("Full context reset performed");
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/unicitylabs/sdk/e2e/context/TestContext.java b/src/test/java/org/unicitylabs/sdk/e2e/context/TestContext.java
new file mode 100644
index 0000000..9dfa328
--- /dev/null
+++ b/src/test/java/org/unicitylabs/sdk/e2e/context/TestContext.java
@@ -0,0 +1,268 @@
+package org.unicitylabs.sdk.e2e.context;
+
+import org.unicitylabs.sdk.StateTransitionClient;
+import org.unicitylabs.sdk.TestAggregatorClient;
+import org.unicitylabs.sdk.api.AggregatorClient;
+import org.unicitylabs.sdk.api.SubmitCommitmentResponse;
+import org.unicitylabs.sdk.hash.DataHash;
+import org.unicitylabs.sdk.predicate.Predicate;
+import org.unicitylabs.sdk.signing.SigningService;
+import org.unicitylabs.sdk.token.Token;
+import org.unicitylabs.sdk.transaction.Transaction;
+import org.unicitylabs.sdk.transaction.TransferTransactionData;
+import org.unicitylabs.sdk.utils.TestUtils;
+import org.unicitylabs.sdk.utils.helpers.CommitmentResult;
+import org.unicitylabs.sdk.utils.helpers.PendingTransfer;
+
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
+
+/**
+ * Shared test context that maintains state across all step definition classes.
+ * This allows different step definition classes to share data and avoid duplication.
+ */
+public class TestContext {
+
+ // Core clients
+ private AggregatorClient aggregatorClient;
+ private TestAggregatorClient testAggregatorClient;
+ private StateTransitionClient client;
+
+ // User management
+ private Map userSigningServices = new HashMap<>();
+ private Map userNonces = new HashMap<>();
+ private Map userSecrets = new HashMap<>();
+ private Map userPredicate = new HashMap<>();
+ private Map> userTokens = new HashMap<>();
+ private Map> nameTagTokens = new HashMap<>();
+ private final Map> pendingTransfers = new HashMap<>();
+
+
+ // Test execution state
+ private Long blockHeight;
+ private byte[] randomSecret;
+ private byte[] stateBytes;
+ private DataHash stateHash;
+ private DataHash txDataHash;
+ private SubmitCommitmentResponse commitmentResponse;
+ private long submissionDuration;
+ private Exception lastError;
+ private boolean operationSucceeded;
+
+ // Performance testing
+ private int configuredThreadCount;
+ private int configuredCommitmentsPerThread;
+ private List aggregatorClients;
+ private List> concurrentResults = new ArrayList<>();
+ private long concurrentSubmissionDuration;
+ private List bulkResults = new ArrayList<>();
+ private long bulkOperationDuration;
+
+ // Transfer chain tracking
+ private List transferChain = new ArrayList<>();
+ private Token chainToken;
+ private Map transferCustomData = new HashMap<>();
+
+ // Current operation context
+ private String currentUser;
+ private String expectedErrorType;
+ private int expectedSplitCount;
+ private int configuredUserCount;
+ private int configuredTokensPerUser;
+
+
+ // Getters and Setters
+ public AggregatorClient getAggregatorClient() { return aggregatorClient; }
+ public void setAggregatorClient(AggregatorClient aggregatorClient) { this.aggregatorClient = aggregatorClient; }
+
+ public TestAggregatorClient getTestAggregatorClient() { return testAggregatorClient; }
+ public void setTestAggregatorClient(TestAggregatorClient testAggregatorClient) { this.testAggregatorClient = testAggregatorClient; }
+
+ public StateTransitionClient getClient() { return client; }
+ public void setClient(StateTransitionClient client) { this.client = client; }
+
+ public Map getUserSigningServices() { return userSigningServices; }
+ public void setUserSigningServices(Map userSigningServices) { this.userSigningServices = userSigningServices; }
+
+ public Map getUserNonces() { return userNonces; }
+ public void setUserNonces(Map userNonces) { this.userNonces = userNonces; }
+
+ public Map getUserSecret() { return userSecrets; }
+ public void setUserSecret(Map userSecrets) { this.userSecrets = userSecrets; }
+
+ public Map getUserPredicate() {
+ return userPredicate;
+ }
+
+ public void setUserPredicate(Map userPredicate) {
+ this.userPredicate = userPredicate;
+ }
+
+ public List getAggregatorClients() {
+ return aggregatorClients;
+ }
+
+ public void setAggregatorClients(List aggregatorClients) {
+ this.aggregatorClients = aggregatorClients;
+ }
+
+ public Map> getUserTokens() { return userTokens; }
+ public void setUserTokens(Map> userTokens) { this.userTokens = userTokens; }
+
+ public Map> getNameTagTokens() { return nameTagTokens; }
+ public void setNameTagTokens(Map> nameTagTokens) { this.nameTagTokens = nameTagTokens; }
+
+ public Long getBlockHeight() { return blockHeight; }
+ public void setBlockHeight(Long blockHeight) { this.blockHeight = blockHeight; }
+
+ public byte[] getRandomSecret() { return randomSecret; }
+ public void setRandomSecret(byte[] randomSecret) { this.randomSecret = randomSecret; }
+
+ public byte[] getStateBytes() { return stateBytes; }
+ public void setStateBytes(byte[] stateBytes) { this.stateBytes = stateBytes; }
+
+ public DataHash getStateHash() { return stateHash; }
+ public void setStateHash(DataHash stateHash) { this.stateHash = stateHash; }
+
+ public DataHash getTxDataHash() { return txDataHash; }
+ public void setTxDataHash(DataHash txDataHash) { this.txDataHash = txDataHash; }
+
+ public SubmitCommitmentResponse getCommitmentResponse() { return commitmentResponse; }
+ public void setCommitmentResponse(SubmitCommitmentResponse commitmentResponse) { this.commitmentResponse = commitmentResponse; }
+
+ public long getSubmissionDuration() { return submissionDuration; }
+ public void setSubmissionDuration(long submissionDuration) { this.submissionDuration = submissionDuration; }
+
+ public Exception getLastError() { return lastError; }
+ public void setLastError(Exception lastError) { this.lastError = lastError; }
+
+ public boolean isOperationSucceeded() { return operationSucceeded; }
+ public void setOperationSucceeded(boolean operationSucceeded) { this.operationSucceeded = operationSucceeded; }
+
+ public int getConfiguredThreadCount() { return configuredThreadCount; }
+ public void setConfiguredThreadCount(int configuredThreadCount) { this.configuredThreadCount = configuredThreadCount; }
+
+ public int getConfiguredCommitmentsPerThread() { return configuredCommitmentsPerThread; }
+ public void setConfiguredCommitmentsPerThread(int configuredCommitmentsPerThread) { this.configuredCommitmentsPerThread = configuredCommitmentsPerThread; }
+
+ public List> getConcurrentResults() { return concurrentResults; }
+ public void setConcurrentResults(List> concurrentResults) { this.concurrentResults = concurrentResults; }
+
+ public long getConcurrentSubmissionDuration() { return concurrentSubmissionDuration; }
+ public void setConcurrentSubmissionDuration(long concurrentSubmissionDuration) { this.concurrentSubmissionDuration = concurrentSubmissionDuration; }
+
+ public List getBulkResults() { return bulkResults; }
+ public void setBulkResults(List bulkResults) { this.bulkResults = bulkResults; }
+
+ public long getBulkOperationDuration() { return bulkOperationDuration; }
+ public void setBulkOperationDuration(long bulkOperationDuration) { this.bulkOperationDuration = bulkOperationDuration; }
+
+ public List getTransferChain() { return transferChain; }
+ public void setTransferChain(List transferChain) { this.transferChain = transferChain; }
+
+ public Token getChainToken() { return chainToken; }
+ public void setChainToken(Token chainToken) { this.chainToken = chainToken; }
+
+ public Map getTransferCustomData() { return transferCustomData; }
+ public void setTransferCustomData(Map transferCustomData) { this.transferCustomData = transferCustomData; }
+
+ public String getCurrentUser() { return currentUser; }
+ public void setCurrentUser(String currentUser) { this.currentUser = currentUser; }
+
+ public String getExpectedErrorType() { return expectedErrorType; }
+ public void setExpectedErrorType(String expectedErrorType) { this.expectedErrorType = expectedErrorType; }
+
+ public int getExpectedSplitCount() { return expectedSplitCount; }
+ public void setExpectedSplitCount(int expectedSplitCount) { this.expectedSplitCount = expectedSplitCount; }
+
+ public int getConfiguredUserCount() { return configuredUserCount; }
+ public void setConfiguredUserCount(int configuredUserCount) { this.configuredUserCount = configuredUserCount; }
+
+ public int getConfiguredTokensPerUser() { return configuredTokensPerUser; }
+ public void setConfiguredTokensPerUser(int configuredTokensPerUser) { this.configuredTokensPerUser = configuredTokensPerUser; }
+
+ public void savePendingTransfer(String user, Token> token, Transaction tx) {
+ pendingTransfers.computeIfAbsent(user, k -> new ArrayList<>())
+ .add(new PendingTransfer(token, tx));
+ }
+
+ public List getPendingTransfers(String user) {
+ return pendingTransfers.getOrDefault(user, List.of());
+ }
+
+ public void clearPendingTransfers(String user) {
+ pendingTransfers.remove(user);
+ }
+
+
+ // Utility methods
+ public void addUserToken(String userName, Token token) {
+ userTokens.computeIfAbsent(userName, k -> new ArrayList<>()).add(token);
+ }
+
+ public Token getUserToken(String userName) {
+ List tokens = userTokens.get(userName);
+ return (tokens != null && !tokens.isEmpty()) ? tokens.get(0) : null;
+ }
+
+ public Token getUserToken(String userName, int index) {
+ List tokens = userTokens.get(userName);
+ return (tokens != null && tokens.size() > index) ? tokens.get(index) : null;
+ }
+
+ public void addNameTagToken(String userName, Token nameTagToken) {
+ nameTagTokens.computeIfAbsent(userName, k -> new ArrayList<>()).add(nameTagToken);
+ }
+
+ public Token getNameTagToken(String userName) {
+ List tokens = nameTagTokens.get(userName);
+ return (tokens != null && !tokens.isEmpty()) ? tokens.get(0) : null;
+ }
+
+ public void clearUserData() {
+ userSigningServices.clear();
+ userNonces.clear();
+ userSecrets.clear();
+ userTokens.clear();
+ nameTagTokens.clear();
+ }
+
+ public void clearTestState() {
+ configuredUserCount = 0;
+ blockHeight = null;
+ randomSecret = null;
+ stateBytes = null;
+ stateHash = null;
+ txDataHash = null;
+ commitmentResponse = null;
+ submissionDuration = 0;
+ lastError = null;
+ operationSucceeded = false;
+ concurrentResults.clear();
+ bulkResults.clear();
+ transferChain.clear();
+ chainToken = null;
+ transferCustomData.clear();
+ currentUser = null;
+ expectedErrorType = null;
+ }
+
+ public void reset() {
+ clearUserData();
+ clearTestState();
+ aggregatorClient = null;
+ testAggregatorClient = null;
+ client = null;
+ }
+
+ private List> commitmentFutures = new ArrayList<>();
+
+ public void setCommitmentFutures(List> futures) {
+ this.commitmentFutures = futures;
+ }
+
+ public List> getCommitmentFutures() {
+ return commitmentFutures;
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/unicitylabs/sdk/e2e/steps/AdvancedStepDefinitions.java b/src/test/java/org/unicitylabs/sdk/e2e/steps/AdvancedStepDefinitions.java
new file mode 100644
index 0000000..830b888
--- /dev/null
+++ b/src/test/java/org/unicitylabs/sdk/e2e/steps/AdvancedStepDefinitions.java
@@ -0,0 +1,265 @@
+package org.unicitylabs.sdk.e2e.steps;
+
+import org.unicitylabs.sdk.StateTransitionClient;
+import org.unicitylabs.sdk.address.ProxyAddress;
+import org.unicitylabs.sdk.api.AggregatorClient;
+import org.unicitylabs.sdk.api.SubmitCommitmentResponse;
+import org.unicitylabs.sdk.api.SubmitCommitmentStatus;
+import org.unicitylabs.sdk.e2e.config.CucumberConfiguration;
+import org.unicitylabs.sdk.e2e.context.TestContext;
+import org.unicitylabs.sdk.e2e.steps.shared.StepHelper;
+import org.unicitylabs.sdk.hash.DataHasher;
+import org.unicitylabs.sdk.hash.HashAlgorithm;
+import org.unicitylabs.sdk.predicate.MaskedPredicate;
+import org.unicitylabs.sdk.token.TokenState;
+import org.unicitylabs.sdk.transaction.InclusionProof;
+import org.unicitylabs.sdk.transaction.Transaction;
+import org.unicitylabs.sdk.transaction.TransferCommitment;
+import org.unicitylabs.sdk.transaction.TransferTransactionData;
+import org.unicitylabs.sdk.util.InclusionProofUtils;
+import org.unicitylabs.sdk.utils.TestUtils;
+import org.unicitylabs.sdk.signing.SigningService;
+import org.unicitylabs.sdk.token.Token;
+import org.unicitylabs.sdk.token.TokenId;
+import org.unicitylabs.sdk.token.TokenType;
+import org.unicitylabs.sdk.token.fungible.TokenCoinData;
+import org.unicitylabs.sdk.utils.helpers.PendingTransfer;
+import io.cucumber.java.en.Given;
+import io.cucumber.java.en.When;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.And;
+
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.concurrent.*;
+
+import static org.unicitylabs.sdk.utils.TestUtils.randomBytes;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Advanced step definitions for complex scenarios and edge cases.
+ */
+public class AdvancedStepDefinitions {
+
+ private final TestContext context;
+
+ public AdvancedStepDefinitions() {
+ this.context = CucumberConfiguration.getTestContext();
+ }
+
+ StepHelper helper = new StepHelper();
+
+ @When("the token is transferred through the chain of existing users")
+ public void theTokenIsTransferredThroughTheChain() throws Exception {
+ List users = new ArrayList<>(context.getUserSigningServices().keySet());
+
+ // remove the one that should go first (if it’s already inside)
+ users.removeAll(context.getTransferChain());
+
+ // prepend the transfer chain at the beginning
+ List orderedUsers = new ArrayList<>();
+ orderedUsers.addAll(context.getTransferChain()); // first element(s)
+ orderedUsers.addAll(users);
+
+
+ Token currentToken = context.getChainToken();
+
+ for (int i = 0; i < orderedUsers.size() - 1; i++) {
+ String fromUser = orderedUsers.get(i);
+ String toUser = orderedUsers.get(i + 1);
+
+ SigningService fromSigningService = context.getUserSigningServices().get(fromUser);
+ SigningService toSigningService = context.getUserSigningServices().get(toUser);
+ byte[] toNonce = context.getUserNonces().get(toUser);
+
+ // Create a simple direct address for transfer
+ var toPredicate = org.unicitylabs.sdk.predicate.MaskedPredicate.create(
+ toSigningService,
+ org.unicitylabs.sdk.hash.HashAlgorithm.SHA256,
+ toNonce
+ );
+ var toAddress = toPredicate.getReference(currentToken.getType()).toAddress();
+
+ String customData = "Transfer from " + fromUser + " to " + toUser;
+ System.out.println(customData);
+ context.getTransferCustomData().put(toUser, customData);
+
+ currentToken = TestUtils.transferToken(
+ context.getClient(),
+ currentToken,
+ fromSigningService,
+ toSigningService,
+ toNonce,
+ toAddress,
+ customData.getBytes(StandardCharsets.UTF_8),
+ List.of()
+ );
+
+ context.getTransferChain().add(toUser);
+ }
+
+ context.setChainToken(currentToken);
+ }
+
+ @Then("the final token should maintain original properties")
+ public void theFinalTokenShouldMaintainOriginalProperties() {
+ assertNotNull(context.getChainToken(), "Final token should exist");
+ assertTrue(context.getChainToken().verify().isSuccessful(), "Final token should be valid");
+
+ // Additional property validation can be added based on requirements
+ }
+
+ @And("all intermediate transfers should be recorded correctly")
+ public void allIntermediateTransfersShouldBeRecordedCorrectly() {
+ assertEquals(4, context.getTransferChain().size(), "Transfer chain should have 4 users");
+ assertEquals("Alice", context.getTransferChain().get(0), "Chain should start with Alice");
+ assertEquals("Bob", context.getTransferChain().get(3), "Chain should end with Dave");
+ }
+
+ @And("the token should have transfers in history")
+ public void theTokenShouldHaveTransfersInHistory(int expectedTransfers) {
+ int actualTransfers = context.getChainToken().getTransactions().size() - 1; // Subtract mint transaction
+ assertEquals(expectedTransfers, actualTransfers, "Token should have expected number of transfers");
+ }
+
+ // Name Tag Scenarios Steps
+ @Given("{string} creates {int} name tag tokens with different addresses")
+ public void createsNameTagTokensWithDifferentAddresses(String username, int nametagCount) throws Exception {
+ List bobNametags = new ArrayList<>();
+
+ for (int i = 0; i < nametagCount; i++) {
+ String tokenIdentifier = TestUtils.generateRandomString(10) + i;
+ Token nametagToken = helper.createNameTagTokenForUser(
+ username,
+ TestUtils.createTokenTypeFromString(tokenIdentifier),
+ tokenIdentifier,
+ TestUtils.generateRandomString(10)
+ );
+ bobNametags.add(nametagToken);
+ }
+
+ context.getNameTagTokens().put(username, bobNametags);
+ }
+
+ @When("{string} transfers tokens to each of {string} name tags")
+ public void userTransfersTokensToEachOfBobsNameTags(String fromUser, String toUser) throws Exception {
+ List nametagTokens = context.getNameTagTokens().get(toUser);
+
+ // Create tokens for Alice to transfer
+ for (int i = 0; i < nametagTokens.size(); i++) {
+ TokenId tokenId = TestUtils.generateRandomTokenId();
+ TokenCoinData coinData = TestUtils.createRandomCoinData(1);
+
+ Token > aliceToken = TestUtils.mintTokenForUser(
+ context.getClient(),
+ context.getUserSigningServices().get(fromUser),
+ context.getUserNonces().get(fromUser),
+ tokenId,
+ nametagTokens.get(i).getType(),
+ coinData
+ );
+
+ // Transfer to Bob's nametag
+ ProxyAddress proxyAddress = ProxyAddress.create(nametagTokens.get(i).getId());
+
+ helper.transferToken(
+ fromUser,
+ toUser,
+ aliceToken,
+ proxyAddress,
+ null
+ );
+ }
+ }
+
+ @And("{string} consolidates all received tokens")
+ public void userConsolidatesAllReceivedTokens(String username) {
+ // Consolidation logic would depend on specific requirements
+ // For now, we ensure Bob has received all the tokens
+ List tokens = context.getUserTokens().getOrDefault(username, new ArrayList<>());
+ // Verify Bob has received tokens
+ assertFalse(tokens.isEmpty(), "Bob should have received tokens");
+ }
+
+ @Then("{string} should own {int} tokens")
+ public void userShouldOwnTokens(String username, int expectedTokenCount) {
+ List tokens = context.getUserTokens().getOrDefault(username, new ArrayList<>());
+ assertEquals(expectedTokenCount, tokens.size(), "Bob should own expected number of tokens");
+
+ // Verify ownership
+ for (Token token : tokens) {
+ SigningService signingService = SigningService.createFromSecret(
+ context.getUserSecret().get(username)
+ );
+ assertTrue(token.verify().isSuccessful(), "Token should be valid");
+ assertTrue(TestUtils.validateTokenOwnership(token, signingService),
+ "Bob should own all tokens");
+ }
+ }
+
+ @And("all {string} nametag tokens should remain valid")
+ public void allNameTagTokensShouldRemainValid(String username) {
+ List bobNametags = context.getNameTagTokens().get(username);
+ for (Token nametag : bobNametags) {
+ assertTrue(nametag.verify().isSuccessful(), "All name tag tokens should remain valid");
+ }
+ }
+
+ @And("proxy addressing should work for all {string} name tags")
+ public void proxyAddressingShouldWorkForAllNameTags(String username) {
+ List bobNametags = context.getNameTagTokens().get(username);
+
+ for (Token nametag : bobNametags) {
+ var proxyAddress = org.unicitylabs.sdk.address.ProxyAddress.create(nametag.getId());
+ assertNotNull(proxyAddress, "Proxy address should be creatable for all name tags");
+ }
+ }
+
+ // Large Data Handling Steps
+ @Given("a token with custom data of size {int} bytes")
+ public void aTokenWithCustomDataOfSizeBytes(int dataSize) throws Exception {
+ String alice = "Alice";
+ byte[] largeData = new byte[dataSize];
+ Arrays.fill(largeData, (byte) 'A'); // Fill with 'A' characters
+
+ TokenId tokenId = TestUtils.generateRandomTokenId();
+ TokenType tokenType = TestUtils.generateRandomTokenType();
+ TokenCoinData coinData = TestUtils.createRandomCoinData(1);
+
+ // Create token with large custom data
+ MaskedPredicate predicate = MaskedPredicate.create(
+ context.getUserSigningServices().get(alice),
+ org.unicitylabs.sdk.hash.HashAlgorithm.SHA256,
+ context.getUserNonces().get(alice)
+ );
+
+ var tokenState = new org.unicitylabs.sdk.token.TokenState(predicate, largeData);
+
+ // Store for later use in transfer
+ context.setChainToken(TestUtils.mintTokenForUser(
+ context.getClient(),
+ context.getUserSigningServices().get(alice),
+ context.getUserNonces().get(alice),
+ tokenId,
+ tokenType,
+ coinData
+ ));
+ }
+
+ @And("{string} finalizes all received tokens")
+ public void finalizesAllReceivedTokens(String username) throws Exception {
+ List pendingTransfers = context.getPendingTransfers(username);
+
+ for (PendingTransfer pending : pendingTransfers) {
+ Token > token = pending.getSourceToken();
+ Transaction tx = pending.getTransaction();
+ helper.finalizeTransfer(
+ username,
+ token,
+ tx
+ );
+ }
+ context.clearPendingTransfers(username);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/unicitylabs/sdk/e2e/steps/StepDefinitions.java b/src/test/java/org/unicitylabs/sdk/e2e/steps/StepDefinitions.java
new file mode 100644
index 0000000..f5f536e
--- /dev/null
+++ b/src/test/java/org/unicitylabs/sdk/e2e/steps/StepDefinitions.java
@@ -0,0 +1,344 @@
+package org.unicitylabs.sdk.e2e.steps;
+
+import org.unicitylabs.sdk.address.ProxyAddress;
+import org.unicitylabs.sdk.e2e.config.CucumberConfiguration;
+import org.unicitylabs.sdk.e2e.context.TestContext;
+import org.unicitylabs.sdk.e2e.steps.shared.StepHelper;
+import org.unicitylabs.sdk.token.fungible.CoinId;
+import org.unicitylabs.sdk.utils.TestUtils;
+import org.unicitylabs.sdk.signing.SigningService;
+import org.unicitylabs.sdk.token.Token;
+import org.unicitylabs.sdk.token.TokenId;
+import org.unicitylabs.sdk.token.TokenType;
+import org.unicitylabs.sdk.token.fungible.TokenCoinData;
+import io.cucumber.java.en.And;
+import io.cucumber.java.en.Given;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.*;
+import java.util.stream.Collectors;
+
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Refactored step definitions that use TestContext and SharedStepDefinitions.
+ * These steps handle specific scenarios that aren't covered by the shared steps.
+ */
+public class StepDefinitions {
+
+ private final TestContext context;
+
+ public StepDefinitions() {
+ this.context = CucumberConfiguration.getTestContext();
+ }
+
+ StepHelper helper = new StepHelper();
+
+ // Token Properties Validation
+ @And("the token should maintain its original ID and type")
+ public void theTokenShouldMaintainItsOriginalIdAndType() {
+ String currentUser = context.getCurrentUser();
+ if (currentUser == null) currentUser = "Alice"; // fallback
+
+ // Get the first user's token (original) and current user's token
+ List userNames = new ArrayList<>(context.getUserTokens().keySet());
+ if (userNames.size() >= 2) {
+ Token originalToken = context.getUserToken(userNames.get(0));
+ Token currentToken = context.getUserToken(currentUser);
+
+ if (originalToken != null && currentToken != null) {
+ assertEquals(originalToken.getId(), currentToken.getId(), "Token ID should remain the same");
+ assertEquals(originalToken.getType(), currentToken.getType(), "Token type should remain the same");
+ }
+ }
+ }
+
+ @And("the token should have {int} transactions in its history")
+ public void theTokenShouldHaveTransactionsInItsHistory(int expectedTransactionCount) {
+ String currentUser = context.getCurrentUser();
+ if (currentUser == null) {
+ // Find the last user who received a token
+ List userNames = new ArrayList<>(context.getUserTokens().keySet());
+ currentUser = userNames.get(userNames.size() - 1);
+ }
+
+ Token token = context.getUserToken(currentUser);
+ assertNotNull(token, "Token should exist for validation");
+ assertEquals(expectedTransactionCount, token.getTransactions().size(), // Subtract mint transaction
+ "Token should have the expected number of transactions");
+ }
+
+ // Minting with Parameters
+ @Given("user {string} with nonce of {int} bytes")
+ public void userWithNonceOfBytes(String userName, int nonceLength) {
+ byte[] nonce = TestUtils.generateRandomBytes(nonceLength);
+ SigningService signingService = TestUtils.createSigningServiceForUser(userName, nonce);
+
+ context.getUserSigningServices().put(userName, signingService);
+ context.getUserNonces().put(userName, nonce);
+ context.getUserTokens().put(userName, new ArrayList<>());
+ context.setCurrentUser(userName);
+ }
+
+ @When("the user mints a token of type {string} with coin data containing {int} coins")
+ public void theUserMintsATokenOfTypeWithCoinDataContainingCoins(String tokenType, int coinCount) throws Exception {
+ String user = context.getCurrentUser();
+ TokenId tokenId = TestUtils.generateRandomTokenId();
+ TokenType type = TestUtils.createTokenTypeFromString(tokenType);
+ TokenCoinData coinData = TestUtils.createRandomCoinData(coinCount);
+
+ Token token = TestUtils.mintTokenForUser(
+ context.getClient(),
+ context.getUserSigningServices().get(user),
+ context.getUserNonces().get(user),
+ tokenId,
+ type,
+ coinData
+ );
+ context.addUserToken(user, token);
+ }
+
+ @Then("the token should be minted successfully")
+ public void theTokenShouldBeMintedSuccessfully() {
+ String user = context.getCurrentUser();
+ Token token = context.getUserToken(user);
+ assertNotNull(token, "Token should be minted");
+ }
+
+ @And("the token should be verified successfully")
+ public void theTokenShouldBeVerifiedSuccessfully() {
+ String user = context.getCurrentUser();
+ Token token = context.getUserToken(user);
+ assertTrue(token.verify().isSuccessful(), "Token should be verified successfully");
+ }
+
+ @And("the token should belong to the user")
+ public void theTokenShouldBelongToTheUser() {
+ String user = context.getCurrentUser();
+ Token token = context.getUserToken(user);
+ SigningService signingService = context.getUserSigningServices().get(user);
+
+ assertTrue(TestUtils.validateTokenOwnership(token, signingService),
+ "Token should belong to the user");
+ }
+
+ @Then("the name tag token should be created successfully")
+ public void theNameTagTokenShouldBeCreatedSuccessfully() {
+ String user = context.getCurrentUser();
+ Token nametagToken = context.getNameTagToken(user);
+ assertNotNull(nametagToken, "Name tag token should be created");
+ assertTrue(nametagToken.verify().isSuccessful(), "Name tag token should be valid");
+ }
+
+ @And("the name tag should be usable for proxy addressing")
+ public void theNameTagShouldBeUsableForProxyAddressing() {
+ String user = context.getCurrentUser();
+ Token nametagToken = context.getNameTagToken(user);
+ ProxyAddress proxyAddress = ProxyAddress.create(nametagToken.getId());
+ assertNotNull(proxyAddress, "Proxy address should be creatable from name tag");
+ }
+
+ // Bulk Operations
+ @Given("{int} users are configured for bulk operations")
+ public void usersAreConfiguredForBulkOperations(int userCount) {
+ context.setConfiguredUserCount(userCount);
+
+ // Setup additional users if needed
+ for (int i = 0; i < userCount; i++) {
+ String userName = "BulkUser" + i;
+ TestUtils.setupUser(userName, context.getUserSigningServices(), context.getUserNonces(), context.getUserSecret());
+ context.getUserTokens().put(userName, new ArrayList<>());
+ }
+ }
+
+ @When("each user mints {int} tokens simultaneously")
+ public void eachUserMintsTokensSimultaneously(int tokensPerUser) throws Exception {
+ context.setConfiguredTokensPerUser(tokensPerUser);
+
+ //Lower the thread pool size to avoid overload
+ int poolSize = Math.min(500, context.getConfiguredUserCount() * 2);
+ ExecutorService executor = Executors.newFixedThreadPool(poolSize);
+
+ //ExecutorService executor = Executors.newFixedThreadPool(context.getConfiguredUserCount() * 2);
+ List> futures = new ArrayList<>();
+ Map, String> futureOwners = new ConcurrentHashMap<>();
+
+ long startTime = System.currentTimeMillis();
+
+ // Create minting tasks for each user
+ for (int userIndex = 0; userIndex < context.getConfiguredUserCount(); userIndex++) {
+ String userName = "BulkUser" + userIndex;
+ SigningService signingService = context.getUserSigningServices().get(userName);
+ byte[] nonce = context.getUserNonces().get(userName);
+
+ for (int tokenIndex = 0; tokenIndex < tokensPerUser; tokenIndex++) {
+ String requestId = userName + "-token" + tokenIndex; // helpful identifier
+
+ CompletableFuture future = CompletableFuture.supplyAsync(() -> {
+ try {
+ TokenId tokenId = TestUtils.generateRandomTokenId();
+ TokenType tokenType = TestUtils.generateRandomTokenType();
+ TokenCoinData coinData = TestUtils.createRandomCoinData(2);
+
+ Token token = TestUtils.mintTokenForUser(context.getClient(), signingService, nonce, tokenId, tokenType, coinData);
+ System.out.println(token.getGenesis().getData().getSourceState());
+ // do post-processing here (still in parallel)
+ for (var entry : context.getUserSigningServices().entrySet()) {
+ if (TestUtils.validateTokenOwnership(token, entry.getValue())) {
+ context.addUserToken(entry.getKey(), token);
+ break;
+ }
+ }
+ System.out.println("[Collector] Got result from " + requestId +
+ " on thread " + Thread.currentThread().getName());
+ return TestUtils.TokenOperationResult.success("Token minted successfully (" + requestId + ")", token);
+ } catch (Exception e) {
+ e.printStackTrace();
+ System.out.println("[Collector] Failed " + requestId + " on thread " + Thread.currentThread().getName() +
+ " with " + e.getMessage());
+ return TestUtils.TokenOperationResult.failure("Failed to mint token (" + requestId + ")", e);
+ }
+ }, executor).orTimeout(30, TimeUnit.SECONDS)
+ .exceptionally(ex -> TestUtils.TokenOperationResult.failure("Timeout (" + requestId + ")", (Exception) ex));;
+
+ futures.add(future);
+ futureOwners.put(future, requestId);
+ }
+ }
+
+ // Start monitoring thread
+ ScheduledExecutorService monitor = Executors.newSingleThreadScheduledExecutor();
+ monitor.scheduleAtFixedRate(() -> {
+ long doneCount = futures.stream().filter(CompletableFuture::isDone).count();
+ long total = futures.size();
+ long pending = total - doneCount;
+
+ System.out.println("[Monitor] " + doneCount + "/" + total + " completed, " + pending + " still pending");
+
+ if (pending == 0) {
+ System.out.println("[Monitor] All requests completed. Stopping monitor.");
+ monitor.shutdown(); // ✅ stop the monitor here
+ }
+
+ // After 15s, dump details of stuck ones
+ if (System.currentTimeMillis() - startTime > 15_000 && pending > 0) {
+ List pendingRequests = futures.stream()
+ .filter(f -> !f.isDone())
+ .map(futureOwners::get)
+ .collect(Collectors.toList());
+ System.out.println("[Monitor] Still waiting for requests: " + pendingRequests);
+ }
+ }, 5, 5, TimeUnit.SECONDS);
+
+ // Wait for all operations to complete and collect results
+ List results = futures.stream()
+ .map(CompletableFuture::join)
+ .collect(Collectors.toList());
+
+ long successes = results.stream().filter(TestUtils.TokenOperationResult::isSuccess).count();
+ long failures = results.size() - successes;
+
+ System.out.println("[Summary] Successes: " + successes + ", Failures: " + failures);
+
+ long endTime = System.currentTimeMillis();
+ context.setBulkResults(results);
+ context.setBulkOperationDuration(endTime - startTime);
+
+ executor.shutdown();
+ }
+
+ @And("all tokens are verified in parallel")
+ public void allTokensAreVerifiedInParallel() {
+ // Verification is already done during token creation
+ long successfulTokens = context.getBulkResults().stream()
+ .mapToLong(result -> result.isSuccess() ? 1 : 0)
+ .sum();
+
+ System.out.println("Successfully created tokens: " + successfulTokens);
+ System.out.println("Total operation time: " + context.getBulkOperationDuration() + " ms");
+ }
+
+ @Then("all {int} tokens should be created successfully")
+ public void allTokensShouldBeCreatedSuccessfully(int expectedTotalTokens) {
+ long successfulTokens = context.getBulkResults().stream()
+ .mapToLong(result -> result.isSuccess() ? 1 : 0)
+ .sum();
+
+ assertEquals(expectedTotalTokens, successfulTokens,
+ "All tokens should be created successfully");
+ }
+
+ @And("the operation should complete within {int} seconds")
+ public void theOperationShouldCompleteWithinSeconds(int maxSeconds) {
+ long maxMilliseconds = maxSeconds * 1000L;
+ TestUtils.PerformanceValidator.validateDuration(
+ context.getBulkOperationDuration(),
+ maxMilliseconds,
+ "Bulk token creation"
+ );
+ }
+
+ @And("the success rate should be at least {int}%")
+ public void theSuccessRateShouldBeAtLeast(int minSuccessRate) {
+ long successful = context.getBulkResults().stream()
+ .mapToLong(result -> result.isSuccess() ? 1 : 0)
+ .sum();
+ long total = context.getBulkResults().size();
+
+ TestUtils.PerformanceValidator.validateSuccessRate(
+ successful,
+ total,
+ minSuccessRate / 100.0,
+ "Bulk operations"
+ );
+ }
+
+ // Transfer Chain Operations
+ @Given("{string} mints a token with {int} coin value")
+ public void userMintsATokenWithCoinValue(String userName, int coinValue) throws Exception {
+ TokenId tokenId = TestUtils.generateRandomTokenId();
+ TokenType tokenType = TestUtils.generateRandomTokenType();
+
+ // Create coin data with specified value
+ TokenCoinData coinData = createCoinDataWithValue(BigInteger.valueOf(coinValue));
+
+ Token token = TestUtils.mintTokenForUser(
+ context.getClient(),
+ context.getUserSigningServices().get(userName),
+ context.getUserNonces().get(userName),
+ tokenId,
+ tokenType,
+ coinData
+ );
+
+ context.setChainToken(token);
+ context.getTransferChain().add(userName);
+ context.setCurrentUser(userName);
+ }
+
+ @And("each transfer includes custom data validation")
+ public void eachTransferIncludesCustomDataValidation() {
+ // Validation is included in the transfer process
+ for (Map.Entry entry : context.getTransferCustomData().entrySet()) {
+ assertNotNull(entry.getValue(), "Custom data should be present for " + entry.getKey());
+ assertTrue(entry.getValue().contains("Transfer from"), "Custom data should have expected format");
+ }
+ }
+
+ @And("the token should have {int} transfers in history")
+ public void theTokenShouldHaveTransfersInHistory(int expectedTransfers) {
+ int actualTransfers = context.getChainToken().getTransactions().size(); // Subtract mint transaction
+ assertEquals(expectedTransfers, actualTransfers, "Token should have expected number of transfers");
+ }
+
+ private TokenCoinData createCoinDataWithValue(BigInteger totalValue) {
+ CoinId coinId = new CoinId(TestUtils.generateRandomBytes(32));
+ return new TokenCoinData(java.util.Map.of(coinId, totalValue));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/unicitylabs/sdk/e2e/steps/shared/SharedStepDefinitions.java b/src/test/java/org/unicitylabs/sdk/e2e/steps/shared/SharedStepDefinitions.java
new file mode 100644
index 0000000..8c84257
--- /dev/null
+++ b/src/test/java/org/unicitylabs/sdk/e2e/steps/shared/SharedStepDefinitions.java
@@ -0,0 +1,616 @@
+package org.unicitylabs.sdk.e2e.steps.shared;
+
+import org.unicitylabs.sdk.StateTransitionClient;
+import org.unicitylabs.sdk.address.DirectAddress;
+import org.unicitylabs.sdk.address.ProxyAddress;
+import org.unicitylabs.sdk.api.*;
+import org.unicitylabs.sdk.e2e.config.CucumberConfiguration;
+import org.unicitylabs.sdk.e2e.context.TestContext;
+import org.unicitylabs.sdk.predicate.UnmaskedPredicate;
+import org.unicitylabs.sdk.transaction.*;
+import org.unicitylabs.sdk.utils.TestUtils;
+import org.unicitylabs.sdk.hash.DataHash;
+import org.unicitylabs.sdk.hash.HashAlgorithm;
+import org.unicitylabs.sdk.signing.SigningService;
+import org.unicitylabs.sdk.token.Token;
+import org.unicitylabs.sdk.token.TokenId;
+import org.unicitylabs.sdk.token.TokenType;
+import org.unicitylabs.sdk.token.fungible.TokenCoinData;
+import io.cucumber.datatable.DataTable;
+import io.cucumber.java.en.And;
+import io.cucumber.java.en.Given;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.stream.Collectors;
+
+import static org.unicitylabs.sdk.utils.TestUtils.randomCoinData;
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.unicitylabs.sdk.utils.helpers.CommitmentResult;
+
+
+/**
+ * Shared step definitions that can be reused across multiple feature files.
+ * These steps use TestContext to maintain state and avoid duplication.
+ */
+public class SharedStepDefinitions {
+
+ private final TestContext context;
+
+ public SharedStepDefinitions() { // ✅ Public zero-argument constructor
+ this.context = CucumberConfiguration.getTestContext();
+ }
+
+ StepHelper helper = new StepHelper();
+
+ // Setup Steps
+ @Given("the aggregator URL is configured")
+ public void theAggregatorUrlIsConfigured() {
+ String aggregatorUrl = System.getenv("AGGREGATOR_URL");
+ assertNotNull(aggregatorUrl, "AGGREGATOR_URL environment variable must be set");
+ context.setAggregatorClient(new AggregatorClient(aggregatorUrl));
+ }
+
+ @And("the aggregator client is initialized")
+ public void theAggregatorClientIsInitialized() {
+ assertNotNull(context.getAggregatorClient(), "Aggregator client should be initialized");
+ }
+
+ @And("the state transition client is initialized")
+ public void theStateTransitionClientIsInitialized() {
+ context.setClient(new StateTransitionClient(context.getAggregatorClient()));
+ assertNotNull(context.getClient(), "State transition client should be initialized");
+ }
+
+ @And("the following users are set up with their signing services")
+ public void usersAreSetUpWithTheirSigningServices(DataTable dataTable) {
+ List users = dataTable.asList();
+ for (String user : users) {
+ TestUtils.setupUser(user, context.getUserSigningServices(), context.getUserNonces(), context.getUserSecret());
+ context.getUserTokens().put(user, new ArrayList<>());
+ }
+ }
+
+ // Aggregator Operations
+ @When("I request the current block height")
+ public void iRequestTheCurrentBlockHeight() throws Exception {
+ Long blockHeight = context.getAggregatorClient().getBlockHeight().get();
+ context.setBlockHeight(blockHeight);
+ }
+
+ @Then("the block height should be returned")
+ public void theBlockHeightShouldBeReturned() {
+ assertNotNull(context.getBlockHeight(), "Block height should not be null");
+ }
+
+ @And("the block height should be greater than {int}")
+ public void theBlockHeightShouldBeGreaterThan(int minHeight) {
+ assertTrue(context.getBlockHeight() > minHeight, "Block height should be greater than " + minHeight);
+ }
+
+ // Commitment Operations
+ @Given("a random secret of {int} bytes")
+ public void aRandomSecretOfBytes(int secretLength) {
+ byte[] randomSecret = TestUtils.generateRandomBytes(secretLength);
+ context.setRandomSecret(randomSecret);
+ assertNotNull(randomSecret);
+ assertEquals(secretLength, randomSecret.length);
+ }
+
+ @And("a state hash from {int} bytes of random data")
+ public void aStateHashFromBytesOfRandomData(int stateLength) {
+ byte[] stateBytes = TestUtils.generateRandomBytes(stateLength);
+ DataHash stateHash = TestUtils.hashData(stateBytes);
+ context.setStateBytes(stateBytes);
+ context.setStateHash(stateHash);
+ assertNotNull(stateHash);
+ }
+
+ @And("transaction data {string}")
+ public void transactionData(String txData) {
+ DataHash txDataHash = TestUtils.hashData(txData.getBytes(StandardCharsets.UTF_8));
+ context.setTxDataHash(txDataHash);
+ assertNotNull(txDataHash);
+ }
+
+ @When("I submit a commitment with the generated data")
+ public void iSubmitACommitmentWithTheGeneratedData() throws Exception {
+ long startTime = System.currentTimeMillis();
+
+ SigningService signingService = SigningService.createFromSecret(context.getRandomSecret());
+ var requestId = TestUtils.createRequestId(signingService, context.getStateHash());
+ var authenticator = TestUtils.createAuthenticator(signingService, context.getTxDataHash(), context.getStateHash());
+
+ SubmitCommitmentResponse response = context.getAggregatorClient()
+ .submitCommitment(requestId, context.getTxDataHash(), authenticator).get();
+ context.setCommitmentResponse(response);
+
+ long endTime = System.currentTimeMillis();
+ context.setSubmissionDuration(endTime - startTime);
+ }
+
+ @Then("the commitment should be submitted successfully")
+ public void theCommitmentShouldBeSubmittedSuccessfully() {
+ assertNotNull(context.getCommitmentResponse(), "Commitment response should not be null");
+ assertEquals(SubmitCommitmentStatus.SUCCESS, context.getCommitmentResponse().getStatus(),
+ "Commitment should be submitted successfully");
+ }
+
+ @And("the submission should complete in less than {int} milliseconds")
+ public void theSubmissionShouldCompleteInLessThanMilliseconds(int maxDuration) {
+ assertTrue(context.getSubmissionDuration() < maxDuration,
+ String.format("Submission took %d ms, should be less than %d ms",
+ context.getSubmissionDuration(), maxDuration));
+ }
+
+ // Multi-threaded Operations
+ @Given("I configure {int} threads with {int} commitments each")
+ public void iConfigureThreadsWithCommitmentsEach(int threadCount, int commitmentsPerThread) {
+ context.setConfiguredThreadCount(threadCount);
+ context.setConfiguredCommitmentsPerThread(commitmentsPerThread);
+
+ // Reuse existing user setup to create users
+ context.setConfiguredUserCount(threadCount);
+
+ // Setup additional users if needed
+ for (int i = 0; i < threadCount; i++) {
+ String userName = "BulkUser" + i;
+ TestUtils.setupUser(userName, context.getUserSigningServices(), context.getUserNonces(), context.getUserSecret());
+ context.getUserTokens().put(userName, new ArrayList<>());
+ }
+ }
+
+ @When("I submit all mint commitments concurrently")
+ public void iSubmitAllMintCommitmentsConcurrently() throws Exception {
+ int threadsCount = context.getConfiguredThreadCount();
+ int commitmentsPerThread = context.getConfiguredCommitmentsPerThread();
+
+ Map userSigningServices = context.getUserSigningServices();
+ ExecutorService executor = Executors.newFixedThreadPool(threadsCount);
+
+ List> futures = new ArrayList<>();
+
+ for (Map.Entry entry : userSigningServices.entrySet()) {
+ String userName = entry.getKey();
+ SigningService signingService = entry.getValue();
+
+ for (int i = 0; i < commitmentsPerThread; i++) {
+ CompletableFuture future = CompletableFuture.supplyAsync(() -> {
+ long start = System.nanoTime();
+ byte[] stateBytes = TestUtils.generateRandomBytes(32);
+ byte[] txData = TestUtils.generateRandomBytes(32);
+
+ DataHash stateHash = TestUtils.hashData(stateBytes);
+ DataHash txDataHash = TestUtils.hashData(txData);
+ RequestId requestId = TestUtils.createRequestId(signingService, stateHash);
+
+ try {
+ Authenticator authenticator = TestUtils.createAuthenticator(signingService, txDataHash, stateHash);
+
+ SubmitCommitmentResponse response = context.getAggregatorClient()
+ .submitCommitment(requestId, txDataHash, authenticator).get();
+
+ boolean success = response.getStatus() == SubmitCommitmentStatus.SUCCESS;
+ long end = System.nanoTime();
+
+ return new CommitmentResult(userName, Thread.currentThread().getName(),
+ requestId, success, start, end);
+ } catch (Exception e) {
+ long end = System.nanoTime();
+ return new CommitmentResult(userName, Thread.currentThread().getName(),
+ requestId, false, start, end);
+ }
+ }, executor);
+
+ futures.add(future);
+ }
+ }
+
+ context.setCommitmentFutures(futures);
+
+ CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+ executor.shutdown();
+ }
+
+ // Token Operations
+ @Given("{string} mints a token with random coin data")
+ public void userMintsATokenWithRandomCoinData(String username) throws Exception {
+ TokenId tokenId = TestUtils.generateRandomTokenId();
+ TokenType tokenType = context.getNameTagToken(context.getCurrentUser()).getType();
+ TokenCoinData coinData = randomCoinData(2);
+
+ Token token = TestUtils.mintTokenForUser(
+ context.getClient(),
+ context.getUserSigningServices().get(username),
+ context.getUserNonces().get(username),
+ tokenId,
+ tokenType,
+ coinData
+ );
+
+ context.addUserToken(username, token);
+ context.setCurrentUser(username);
+ }
+
+ @When("{string} transfers the token to {string} using a proxy address")
+ public void userTransfersTheTokenToUserUsingAProxyAddress(String fromUser, String toUser) throws Exception {
+ Token sourceToken = context.getUserToken(fromUser);
+ ProxyAddress proxyAddress = ProxyAddress.create(context.getNameTagToken(toUser).getId());
+ helper.transferToken(fromUser, toUser, sourceToken, proxyAddress, null);
+ }
+
+ @When("{string} transfers the token to {string} using an unmasked predicate")
+ public void userTransfersTheTokenToUserUsingAnUnmaskedPredicate(String fromUser, String toUser) throws Exception {
+ Token sourceToken = context.getUserToken(fromUser);
+ SigningService toSigningService = context.getUserSigningServices().get(toUser);
+
+ UnmaskedPredicate userPredicate = UnmaskedPredicate.create(
+ toSigningService,
+ HashAlgorithm.SHA256,
+ context.getUserNonces().get(toUser)
+ );
+ context.getUserPredicate().put(toUser, userPredicate);
+
+ DirectAddress toAddress = userPredicate.getReference(sourceToken.getType()).toAddress();
+
+ helper.transferToken(fromUser, toUser, sourceToken, toAddress, null);
+ }
+
+ @Then("{string} should own the token successfully")
+ public void userShouldOwnTheTokenSuccessfully(String username) {
+ Token token = context.getUserToken(username);
+ context.setCurrentUser(username);
+ SigningService signingService = context.getUserSigningServices().get(username);
+ assertTrue(token.verify().isSuccessful(), "Token should be valid");
+ assertTrue(token.getState().getUnlockPredicate().isOwner(signingService.getPublicKey()),
+ username + " should own the token");
+ }
+
+ @Then("all mint commitments should receive inclusion proofs within {int} seconds")
+ public void allMintCommitmentsShouldReceiveInclusionProofs(int timeoutSeconds) throws Exception {
+ List results = helper.collectCommitmentResults();
+ helper.verifyAllInclusionProofsInParallel(timeoutSeconds);
+
+ long verifiedCount = results.stream()
+ .filter(CommitmentResult::isVerified)
+ .count();
+
+ System.out.println("Verified commitments: " + verifiedCount + " / " + results.size());
+ // Print failed ones (not verified)
+ results.stream()
+ .filter(r -> !r.isVerified())
+ .forEach(r -> System.out.println(
+ "❌ Commitment failed: requestId=" + r.getRequestId().toString() + ", status=" + r.getStatus()
+ ));
+
+ assertEquals(results.size(), verifiedCount, "All commitments should be verified");
+ }
+
+ @Given("user {string} create a nametag token with custom data {string}")
+ public void userCreateANametagTokenWithCustomData(String username, String customData) throws Exception {
+ Token nametagToken = helper.createNameTagTokenForUser(
+ username,
+ TestUtils.generateRandomTokenType(),
+ java.util.UUID.randomUUID().toString(),
+ customData
+ );
+ assertNotNull(nametagToken, "Name tag token should be created");
+ assertTrue(nametagToken.verify().isSuccessful(), "Name tag token should be valid");
+ context.addNameTagToken(username, nametagToken);
+ context.setCurrentUser(username);
+ }
+
+ @Given("the aggregator URLs are configured")
+ public void theAggregatorURLsAreConfigured() {
+ // You can either use environment variables or hardcode the URLs
+ List aggregatorUrls = Arrays.asList(
+ System.getenv("AGGREGATOR_URL")
+ );
+
+ assertNotNull(aggregatorUrls, "Aggregator URLs must be configured");
+ assertFalse(aggregatorUrls.isEmpty(), "At least one aggregator URL must be provided");
+
+ List clients = new ArrayList<>();
+ for (String url : aggregatorUrls) {
+ clients.add(new AggregatorClient(url.trim()));
+ }
+
+ context.setAggregatorClients(clients);
+ }
+
+ @And("the aggregator clients are initialized")
+ public void theAggregatorClientsAreInitialized() {
+ List clients = context.getAggregatorClients();
+ assertNotNull(clients, "Aggregator clients should be initialized");
+ assertFalse(clients.isEmpty(), "At least one aggregator client should be initialized");
+ }
+
+ @When("I submit all mint commitments concurrently to all aggregators")
+ public void iSubmitAllMintCommitmentsConcurrentlyToAllAggregators() {
+ int threadsCount = context.getConfiguredThreadCount();
+ int commitmentsPerThread = context.getConfiguredCommitmentsPerThread();
+ List aggregatorClients = context.getAggregatorClients();
+
+ Map userSigningServices = context.getUserSigningServices();
+
+ // Calculate total thread pool size: threads * aggregators
+ int totalThreadPoolSize = threadsCount * aggregatorClients.size();
+ ExecutorService executor = Executors.newFixedThreadPool(totalThreadPoolSize);
+
+ List> futures = new ArrayList<>();
+
+ for (Map.Entry entry : userSigningServices.entrySet()) {
+ String userName = entry.getKey();
+ SigningService signingService = entry.getValue();
+
+ for (int i = 0; i < commitmentsPerThread; i++) {
+ // Generate the commitment data once for this iteration
+ byte[] stateBytes = TestUtils.generateRandomBytes(32);
+ byte[] txData = TestUtils.generateRandomBytes(32);
+ DataHash stateHash = TestUtils.hashData(stateBytes);
+ DataHash txDataHash = TestUtils.hashData(txData);
+ RequestId requestId = TestUtils.createRequestId(signingService, stateHash);
+
+ // Submit the same commitment to all aggregators concurrently
+ for (int aggIndex = 0; aggIndex < aggregatorClients.size(); aggIndex++) {
+ AggregatorClient aggregatorClient = aggregatorClients.get(aggIndex);
+ String aggregatorId = "Aggregator" + aggIndex;
+
+ CompletableFuture future = CompletableFuture.supplyAsync(() -> {
+ long start = System.nanoTime();
+
+ try {
+ Authenticator authenticator = TestUtils.createAuthenticator(signingService, txDataHash, stateHash);
+
+ SubmitCommitmentResponse response = aggregatorClient
+ .submitCommitment(requestId, txDataHash, authenticator).get();
+
+ boolean success = response.getStatus() == SubmitCommitmentStatus.SUCCESS;
+ long end = System.nanoTime();
+
+ return new CommitmentResult(userName + "-" + aggregatorId,
+ Thread.currentThread().getName(),
+ requestId, success, start, end);
+ } catch (Exception e) {
+ long end = System.nanoTime();
+ return new CommitmentResult(userName + "-" + aggregatorId,
+ Thread.currentThread().getName(),
+ requestId, false, start, end);
+ }
+ }, executor);
+
+ futures.add(future);
+ }
+ }
+ }
+
+ context.setCommitmentFutures(futures);
+
+ CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+ executor.shutdown();
+ }
+
+ @Then("all commitments should be processed successfully")
+ public void allCommitmentsShouldBeProcessedSuccessfully() {
+ int threadsCount = context.getConfiguredThreadCount();
+ int commitmentsPerThread = context.getConfiguredCommitmentsPerThread();
+ List aggregatorClients = context.getAggregatorClients();
+
+ Map userSigningServices = context.getUserSigningServices();
+ ExecutorService executor = Executors.newFixedThreadPool(threadsCount);
+
+ List>> futures = new ArrayList<>();
+
+ for (Map.Entry entry : userSigningServices.entrySet()) {
+ String userName = entry.getKey();
+ SigningService signingService = entry.getValue();
+
+ for (int i = 0; i < commitmentsPerThread; i++) {
+ CompletableFuture> future = CompletableFuture.supplyAsync(() -> {
+ List results = new ArrayList<>();
+
+ // Generate commitment data once
+ byte[] stateBytes = TestUtils.generateRandomBytes(32);
+ byte[] txData = TestUtils.generateRandomBytes(32);
+ DataHash stateHash = TestUtils.hashData(stateBytes);
+ DataHash txDataHash = TestUtils.hashData(txData);
+ RequestId requestId = TestUtils.createRequestId(signingService, stateHash);
+
+ // Submit to all aggregators with the same data
+ for (int aggIndex = 0; aggIndex < aggregatorClients.size(); aggIndex++) {
+ AggregatorClient aggregatorClient = aggregatorClients.get(aggIndex);
+ String aggregatorId = "Aggregator" + aggIndex;
+
+ long start = System.nanoTime();
+ try {
+ Authenticator authenticator = TestUtils.createAuthenticator(signingService, txDataHash, stateHash);
+
+ SubmitCommitmentResponse response = aggregatorClient
+ .submitCommitment(requestId, txDataHash, authenticator).get();
+
+ boolean success = response.getStatus() == SubmitCommitmentStatus.SUCCESS;
+ long end = System.nanoTime();
+
+ results.add(new CommitmentResult(userName + "-" + aggregatorId,
+ Thread.currentThread().getName(),
+ requestId, success, start, end));
+ } catch (Exception e) {
+ long end = System.nanoTime();
+ results.add(new CommitmentResult(userName + "-" + aggregatorId,
+ Thread.currentThread().getName(),
+ requestId, false, start, end));
+ }
+ }
+
+ return results;
+ }, executor);
+
+ futures.add(future);
+ }
+ }
+
+ // Flatten the results
+ List> flattenedFutures = new ArrayList<>();
+ for (CompletableFuture> future : futures) {
+ CompletableFuture flattened = future.thenCompose(results -> {
+ // Return the first result (or you could return all)
+ return CompletableFuture.completedFuture(results.get(0));
+ });
+ flattenedFutures.add(flattened);
+ }
+
+ context.setCommitmentFutures(flattenedFutures);
+
+ CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+ executor.shutdown();
+ }
+
+ @Then("all mint commitments should receive inclusion proofs from all aggregators within {int} seconds")
+ public void allMintCommitmentsShouldReceiveInclusionProofsFromAllAggregatorsWithinSeconds(int timeoutSeconds) throws Exception {
+ List results = helper.collectCommitmentResults();
+
+ // Verify inclusion proofs for all aggregators in parallel
+ helper.verifyAllInclusionProofsInParallelForMultipleAggregators(timeoutSeconds, context.getAggregatorClients());
+
+ long verifiedCount = results.stream()
+ .filter(CommitmentResult::isVerified)
+ .count();
+
+ System.out.println("=== Inclusion Proof Verification Results ===");
+ System.out.println("Total commitments: " + results.size());
+ System.out.println("Verified commitments: " + verifiedCount + " / " + results.size());
+
+ // Group results by aggregator for detailed reporting
+ Map> resultsByAggregator = results.stream()
+ .collect(Collectors.groupingBy(r -> helper.extractAggregatorFromUserName(r.getUserName())));
+
+ for (Map.Entry> entry : resultsByAggregator.entrySet()) {
+ String aggregatorId = entry.getKey();
+ List aggregatorResults = entry.getValue();
+
+ long aggregatorVerifiedCount = aggregatorResults.stream()
+ .filter(CommitmentResult::isVerified)
+ .count();
+
+ System.out.println("\n" + aggregatorId + ":");
+ System.out.println(" Verified: " + aggregatorVerifiedCount + " / " + aggregatorResults.size());
+
+ // Print failed ones for this aggregator
+ aggregatorResults.stream()
+ .filter(r -> !r.isVerified())
+ .forEach(r -> System.out.println(
+ " ❌ Failed: requestId=" + r.getRequestId().toString() +
+ ", status=" + (r.getStatus() != null ? r.getStatus() : "Unknown") +
+ ", user=" + r.getUserName()
+ ));
+
+ // Print successful ones (optional, for debugging)
+ if (aggregatorVerifiedCount > 0) {
+ System.out.println(" ✅ Successfully verified " + aggregatorVerifiedCount + " commitments");
+ }
+ }
+
+ assertEquals(results.size(), verifiedCount, "All commitments should be verified");
+ }
+
+ @Then("all mint commitments should receive inclusion proofs within {int} seconds with {int}% success rate")
+ public void allMintCommitmentsShouldReceiveInclusionProofsWithSuccessRate(int timeoutSeconds, int expectedSuccessRate) throws Exception {
+ List results = helper.collectCommitmentResults();
+
+ // Verify inclusion proofs for all aggregators in parallel
+ helper.verifyAllInclusionProofsInParallelForMultipleAggregators(timeoutSeconds, context.getAggregatorClients());
+
+ long verifiedCount = results.stream()
+ .filter(CommitmentResult::isVerified)
+ .count();
+
+ double actualSuccessRate = (double) verifiedCount / results.size() * 100;
+
+ System.out.println("=== Inclusion Proof Verification Results ===");
+ System.out.println("Total commitments: " + results.size());
+ System.out.println("Verified commitments: " + verifiedCount + " / " + results.size());
+ System.out.println("Actual success rate: " + String.format("%.2f%%", actualSuccessRate));
+ System.out.println("Expected success rate: " + expectedSuccessRate + "%");
+
+ // Detailed reporting by aggregator
+ Map> resultsByAggregator = results.stream()
+ .collect(Collectors.groupingBy(r -> helper.extractAggregatorFromUserName(r.getUserName())));
+
+ for (Map.Entry> entry : resultsByAggregator.entrySet()) {
+ String aggregatorId = entry.getKey();
+ List aggregatorResults = entry.getValue();
+
+ long aggregatorVerifiedCount = aggregatorResults.stream()
+ .filter(CommitmentResult::isVerified)
+ .count();
+
+ double aggregatorSuccessRate = (double) aggregatorVerifiedCount / aggregatorResults.size() * 100;
+
+ System.out.println("\n" + aggregatorId + ":");
+ System.out.println(" Success rate: " + String.format("%.2f%%", aggregatorSuccessRate) +
+ " (" + aggregatorVerifiedCount + " / " + aggregatorResults.size() + ")");
+ }
+
+ assertTrue(actualSuccessRate >= expectedSuccessRate,
+ String.format("Expected success rate of at least %d%%, but got %.2f%%",
+ expectedSuccessRate, actualSuccessRate));
+ }
+
+ @Then("I should see performance metrics for each aggregator")
+ public void iShouldSeePerformanceMetricsForEachAggregator() {
+ List results = helper.collectCommitmentResults();
+ List aggregatorClients = context.getAggregatorClients();
+
+ System.out.println("\n=== 📊 AGGREGATOR PERFORMANCE COMPARISON ===");
+
+ // Print detailed breakdown
+ helper.printDetailedResultsByAggregator(results, aggregatorClients.size());
+
+ // Additional performance analysis
+ helper.printPerformanceComparison(results, aggregatorClients.size());
+ }
+
+ @Then("aggregator performance should meet minimum thresholds")
+ public void aggregatorPerformanceShouldMeetMinimumThresholds() {
+ List results = helper.collectCommitmentResults();
+ List aggregatorClients = context.getAggregatorClients();
+
+ Map> resultsByAggregator = results.stream()
+ .collect(Collectors.groupingBy(r -> helper.extractAggregatorFromUserName(r.getUserName())));
+
+ for (int i = 0; i < aggregatorClients.size(); i++) {
+ String aggregatorId = "-Aggregator" + i;
+ List aggregatorResults = resultsByAggregator.getOrDefault(aggregatorId, new ArrayList<>());
+
+ if (aggregatorResults.isEmpty()) continue;
+
+ long verifiedCount = aggregatorResults.stream()
+ .filter(CommitmentResult::isVerified)
+ .count();
+
+ double successRate = (double) verifiedCount / aggregatorResults.size() * 100;
+
+ // Assert minimum success rate (configurable)
+ assertTrue(successRate >= 90.0,
+ String.format("Aggregator%d success rate (%.2f%%) should be at least 90%%", i, successRate));
+
+ // Assert reasonable average inclusion time
+ OptionalDouble avgInclusionTime = aggregatorResults.stream()
+ .filter(CommitmentResult::isVerified)
+ .mapToDouble(CommitmentResult::getInclusionDurationMillis)
+ .average();
+
+ if (avgInclusionTime.isPresent()) {
+ assertTrue(avgInclusionTime.getAsDouble() <= 30000, // 30 seconds max
+ String.format("Aggregator%d average inclusion time (%.2fms) should be under 30 seconds",
+ i, avgInclusionTime.getAsDouble()));
+ }
+
+ System.out.println("✅ Aggregator" + i + " meets performance thresholds");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/unicitylabs/sdk/e2e/steps/shared/StepHelper.java b/src/test/java/org/unicitylabs/sdk/e2e/steps/shared/StepHelper.java
new file mode 100644
index 0000000..dd8ddad
--- /dev/null
+++ b/src/test/java/org/unicitylabs/sdk/e2e/steps/shared/StepHelper.java
@@ -0,0 +1,478 @@
+package org.unicitylabs.sdk.e2e.steps.shared;
+
+import org.unicitylabs.sdk.address.Address;
+import org.unicitylabs.sdk.address.DirectAddress;
+import org.unicitylabs.sdk.address.ProxyAddress;
+import org.unicitylabs.sdk.api.AggregatorClient;
+import org.unicitylabs.sdk.api.SubmitCommitmentResponse;
+import org.unicitylabs.sdk.api.SubmitCommitmentStatus;
+import org.unicitylabs.sdk.e2e.config.CucumberConfiguration;
+import org.unicitylabs.sdk.e2e.context.TestContext;
+import org.unicitylabs.sdk.hash.DataHash;
+import org.unicitylabs.sdk.hash.HashAlgorithm;
+import org.unicitylabs.sdk.predicate.MaskedPredicate;
+import org.unicitylabs.sdk.predicate.Predicate;
+import org.unicitylabs.sdk.predicate.UnmaskedPredicate;
+import org.unicitylabs.sdk.predicate.UnmaskedPredicateReference;
+import org.unicitylabs.sdk.signing.SigningService;
+import org.unicitylabs.sdk.token.Token;
+import org.unicitylabs.sdk.token.TokenState;
+import org.unicitylabs.sdk.token.TokenType;
+import org.unicitylabs.sdk.transaction.*;
+import org.unicitylabs.sdk.utils.TestUtils;
+import org.unicitylabs.sdk.utils.helpers.CommitmentResult;
+
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.stream.Collectors;
+
+import static org.unicitylabs.sdk.util.InclusionProofUtils.waitInclusionProof;
+import static org.unicitylabs.sdk.utils.TestUtils.randomBytes;
+
+
+public class StepHelper {
+
+ private final TestContext context;
+
+ public StepHelper() { // ✅ Public zero-argument constructor
+ this.context = CucumberConfiguration.getTestContext();
+ }
+
+ public Token createNameTagTokenForUser(String userName, TokenType type, String nametag, String nametagData) throws Exception {
+ byte[] nametagNonce = TestUtils.generateRandomBytes(32);
+
+ MaskedPredicate nametagPredicate = MaskedPredicate.create(
+ SigningService.createFromMaskedSecret(context.getUserSecret().get(userName), nametagNonce),
+ HashAlgorithm.SHA256,
+ nametagNonce
+ );
+
+ TokenType nametagTokenType = TestUtils.generateRandomTokenType();
+ DirectAddress nametagAddress = nametagPredicate.getReference(nametagTokenType).toAddress();
+
+ DirectAddress userAddress = UnmaskedPredicateReference.create(
+ nametagTokenType,
+ SigningService.createFromSecret(context.getUserSecret().get(userName)),
+ HashAlgorithm.SHA256
+ ).toAddress();
+
+ var nametagMintCommitment = org.unicitylabs.sdk.transaction.MintCommitment.create(
+ new NametagMintTransactionData<>(
+ nametag,
+ nametagTokenType,
+ nametagAddress,
+ TestUtils.generateRandomBytes(32),
+ userAddress
+ )
+ );
+
+ SubmitCommitmentResponse response = context.getClient().submitCommitment(nametagMintCommitment).get();
+ if (response.getStatus() != SubmitCommitmentStatus.SUCCESS) {
+ throw new Exception("Failed to submit nametag mint commitment: " + response.getStatus());
+ }
+
+ InclusionProof inclusionProof = waitInclusionProof(context.getClient(), nametagMintCommitment).get();
+ Transaction extends MintTransactionData>> nametagGenesis = nametagMintCommitment.toTransaction(inclusionProof);
+
+ return new Token(
+ new org.unicitylabs.sdk.token.TokenState(nametagPredicate, null),
+ nametagGenesis
+ );
+ }
+
+ public void transferToken(String fromUser, String toUser, Token token, Address toAddress, String customData) throws Exception {
+ SigningService fromSigningService = context.getUserSigningServices().get(fromUser);
+
+ // Create data hash and state data if custom data provided
+ DataHash dataHash = null;
+ byte[] stateData = null;
+ if (customData != null && !customData.isEmpty()) {
+ stateData = customData.getBytes(StandardCharsets.UTF_8);
+ dataHash = TestUtils.hashData(stateData);
+ }
+
+ // Submit transfer commitment
+ TransferCommitment transferCommitment = TransferCommitment.create(
+ token,
+ toAddress,
+ randomBytes(32),
+ dataHash,
+ null,
+ fromSigningService
+ );
+
+ SubmitCommitmentResponse response = context.getClient().submitCommitment(token, transferCommitment).get();
+ if (response.getStatus() != SubmitCommitmentStatus.SUCCESS) {
+ throw new Exception("Failed to submit transfer commitment: " + response.getStatus());
+ }
+
+ // Wait for inclusion proof
+ InclusionProof inclusionProof = waitInclusionProof(
+ context.getClient(),
+ transferCommitment
+ ).get();
+ Transaction transferTransaction = transferCommitment.toTransaction(
+ token,
+ inclusionProof
+ );
+
+ context.savePendingTransfer(toUser, token, transferTransaction);
+ }
+
+ public void finalizeTransfer(String username, Token> token, Transaction tx) throws Exception {
+
+ byte[] secret = context.getUserSecret().get(username);
+
+ Token> currentNameTagToken = context.getNameTagToken(username);
+ List nametagTokens = context.getNameTagTokens().get(username);
+ if (nametagTokens != null && !nametagTokens.isEmpty()) {
+ for (Token> t : nametagTokens) {
+ String actualNametagAddress = tx.getData().getRecipient().getAddress();
+ String expectedProxyAddress = ProxyAddress.create(t.getId()).getAddress();
+
+ if (actualNametagAddress.equalsIgnoreCase(expectedProxyAddress)) {
+ currentNameTagToken = t;
+ break;
+ }
+ }
+ }
+
+ List> additionalTokens = new ArrayList<>();
+ if (currentNameTagToken != null) {
+ additionalTokens.add(currentNameTagToken);
+ }
+
+ Predicate unlockPredicate = context.getUserPredicate().get(username);
+ if (unlockPredicate == null){
+ context.getUserSigningServices().put(username, SigningService.createFromSecret(secret));
+ unlockPredicate = UnmaskedPredicate.create(
+ context.getUserSigningServices().get(username),
+ HashAlgorithm.SHA256,
+ tx.getData().getSalt()
+ );
+ }
+
+ TokenState recipientState = new TokenState(
+ unlockPredicate,
+ null
+ );
+
+ Token finalizedToken = context.getClient().finalizeTransaction(
+ token,
+ recipientState,
+ tx,
+ additionalTokens
+ );
+
+ context.addUserToken(username, finalizedToken);
+ }
+
+ public boolean submitSingleCommitment() {
+ try {
+ byte[] randomSecret = TestUtils.generateRandomBytes(32);
+ byte[] stateBytes = TestUtils.generateRandomBytes(32);
+ byte[] txData = TestUtils.generateRandomBytes(32);
+
+ DataHash stateHash = TestUtils.hashData(stateBytes);
+ DataHash txDataHash = TestUtils.hashData(txData);
+ SigningService signingService = SigningService.createFromSecret(randomSecret);
+ var requestId = TestUtils.createRequestId(signingService, stateHash);
+ var authenticator = TestUtils.createAuthenticator(signingService, txDataHash, stateHash);
+
+ SubmitCommitmentResponse response = context.getAggregatorClient()
+ .submitCommitment(requestId, txDataHash, authenticator).get();
+ return response.getStatus() == SubmitCommitmentStatus.SUCCESS;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ public void verifyAllInclusionProofsInParallel(int timeoutSeconds)
+ throws InterruptedException {
+ List results = collectCommitmentResults();
+ ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
+ CountDownLatch latch = new CountDownLatch(results.size());
+
+ long startAll = System.nanoTime();
+ long globalTimeout = startAll + TimeUnit.SECONDS.toNanos(timeoutSeconds);
+
+ for (CommitmentResult result : results) {
+ executor.submit(() -> {
+ long inclStart = System.nanoTime();
+ boolean verified = false;
+ String errorMessage = "Global timeout reached";
+
+ try {
+ while (System.nanoTime() < globalTimeout && !verified) {
+ try {
+ InclusionProof proof = context.getAggregatorClient()
+ .getInclusionProof(result.getRequestId())
+ .get(calculateRemainingTimeout(globalTimeout), TimeUnit.MILLISECONDS);
+
+ if (proof != null && proof.verify(result.getRequestId())
+ == InclusionProofVerificationStatus.OK) {
+ result.markVerified(inclStart, System.nanoTime());
+ verified = true;
+ } else {
+ // Неуспешная верификация, но продолжаем пытаться
+ InclusionProofVerificationStatus status = proof.verify(result.getRequestId());
+ errorMessage = status.toString();
+ Thread.sleep(1000); // Небольшая пауза перед повторной попыткой
+ }
+ } catch (TimeoutException e) {
+ // Таймаут отдельной операции, продолжаем цикл
+ errorMessage = "Individual operation timeout: " + e.getMessage();
+ } catch (ExecutionException e) {
+ // Ошибка выполнения, продолжаем цикл
+ errorMessage = "Execution error: " + e.getMessage();
+ Thread.sleep(1000); // Пауза перед повторной попыткой
+ }
+ }
+
+ if (!verified) {
+ result.markFailedVerification(inclStart, System.nanoTime(), errorMessage);
+ }
+
+ } catch (Exception e) {
+ result.markFailedVerification(inclStart, System.nanoTime(),
+ "Unexpected error: " + e.getMessage());
+ } finally {
+ latch.countDown();
+ }
+ });
+ }
+
+ // Wait for all tasks to complete or timeout
+ boolean finished = latch.await(timeoutSeconds, TimeUnit.SECONDS);
+ executor.shutdownNow();
+
+ long endAll = System.nanoTime();
+ System.out.println("All inclusion proofs completed in: " + ((endAll - startAll) / 1_000_000) + " ms");
+
+ if (!finished) {
+ System.err.println("Timeout reached before all inclusion proofs were verified");
+ }
+ }
+
+ private long calculateRemainingTimeout(long globalTimeoutNanos) {
+ long remaining = globalTimeoutNanos - System.nanoTime();
+ return TimeUnit.NANOSECONDS.toMillis(Math.max(remaining, 100)); // Минимум 100мс
+ }
+
+ public List collectCommitmentResults() {
+ return context.getCommitmentFutures().stream()
+ .map(f -> {
+ try {
+ return f.get(); // wait for completion
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ })
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+ }
+
+ // Helper method to extract aggregator info from username
+ public String extractAggregatorFromUserName(String userName) {
+ if (userName.contains("-Aggregator")) {
+ return userName.substring(userName.indexOf("-Aggregator"));
+ }
+ return "Unknown-Aggregator";
+ }
+
+ // Updated helper method for your existing CommitmentResult class
+ public void verifyAllInclusionProofsInParallelForMultipleAggregators(int timeoutSeconds, List aggregatorClients) throws Exception {
+ List results = collectCommitmentResults();
+
+ // Group results by aggregator
+ Map> resultsByAggregator = results.stream()
+ .collect(Collectors.groupingBy(r -> extractAggregatorFromUserName(r.getUserName())));
+
+ ExecutorService executor = Executors.newFixedThreadPool(aggregatorClients.size());
+ List> verificationFutures = new ArrayList<>();
+
+ for (int i = 0; i < aggregatorClients.size(); i++) {
+ AggregatorClient aggregatorClient = aggregatorClients.get(i);
+ String aggregatorId = "Aggregator" + i;
+ List aggregatorResults = resultsByAggregator.getOrDefault("-" + aggregatorId, new ArrayList<>());
+
+ CompletableFuture future = CompletableFuture.runAsync(() -> {
+ try {
+ verifyInclusionProofsForAggregator(aggregatorClient, aggregatorResults, timeoutSeconds);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to verify inclusion proofs for " + aggregatorId, e);
+ }
+ }, executor);
+
+ verificationFutures.add(future);
+ }
+
+ try {
+ CompletableFuture.allOf(verificationFutures.toArray(new CompletableFuture[0]))
+ .get(timeoutSeconds + 10, TimeUnit.SECONDS); // Add buffer time for processing
+ } finally {
+ executor.shutdown();
+ }
+ }
+
+ private void verifyInclusionProofsForAggregator(AggregatorClient aggregatorClient,
+ List results,
+ int timeoutSeconds) throws Exception {
+ long globalStartTime = System.currentTimeMillis();
+ long timeoutMillis = timeoutSeconds * 1000L;
+
+ for (CommitmentResult result : results) {
+ long inclusionStartTime = System.nanoTime();
+
+ if (!result.isSuccess()) {
+ long inclusionEndTime = System.nanoTime();
+ result.markFailedVerification(inclusionStartTime, inclusionEndTime, "Commitment submission failed");
+ continue;
+ }
+
+ boolean verified = false;
+ String statusMessage = "Timeout waiting for inclusion proof";
+
+ // Poll for inclusion proof with timeout
+ while (System.currentTimeMillis() - globalStartTime < timeoutMillis) {
+ try {
+ // Check if inclusion proof is available
+ InclusionProof proofResponse = aggregatorClient
+ .getInclusionProof(result.getRequestId()).get(5, TimeUnit.SECONDS);
+ if (proofResponse != null && proofResponse.verify(result.getRequestId())
+ == InclusionProofVerificationStatus.OK) {
+ System.out.println("InclusionProofVerificationStatus.OK");
+ result.markVerified(inclusionStartTime, System.nanoTime());
+ verified = true;
+ break;
+ } else {
+ InclusionProofVerificationStatus status = proofResponse.verify(result.getRequestId());
+ System.out.println(status.toString());
+ statusMessage = status.toString();
+ }
+ Thread.sleep(1000);
+ } catch (TimeoutException e) {
+ // Continue polling
+ statusMessage = "Timeout during proof retrieval";
+ } catch (Exception e) {
+ statusMessage = "Error retrieving proof: " + e.getMessage();
+ break;
+ }
+ }
+
+ long inclusionEndTime = System.nanoTime();
+
+ // Use your existing methods to mark verification result
+ if (verified) {
+ result.markVerified(inclusionStartTime, inclusionEndTime);
+ } else {
+ result.markFailedVerification(inclusionStartTime, inclusionEndTime, statusMessage);
+ }
+ }
+ }
+
+ // Method to print detailed results by aggregator
+ public void printDetailedResultsByAggregator(List results, int aggregatorCount) {
+ System.out.println("\n=== Detailed Results by Aggregator ===");
+
+ Map> resultsByAggregator = results.stream()
+ .collect(Collectors.groupingBy(r -> extractAggregatorFromUserName(r.getUserName())));
+
+ for (int i = 0; i < aggregatorCount; i++) {
+ String aggregatorId = "-Aggregator" + i;
+ List aggregatorResults = resultsByAggregator.getOrDefault(aggregatorId, new ArrayList<>());
+
+ long verifiedCount = aggregatorResults.stream()
+ .filter(CommitmentResult::isVerified)
+ .count();
+
+ double successRate = aggregatorResults.isEmpty() ? 0 :
+ (double) verifiedCount / aggregatorResults.size() * 100;
+
+ // Calculate average inclusion proof time for verified commitments
+ OptionalDouble avgInclusionTime = aggregatorResults.stream()
+ .filter(CommitmentResult::isVerified)
+ .mapToDouble(CommitmentResult::getInclusionDurationMillis)
+ .average();
+
+ System.out.println("Aggregator" + i + " (localhost:" + (3000 + i * 5080) + "):");
+ System.out.println(" Total commitments: " + aggregatorResults.size());
+ System.out.println(" Verified: " + verifiedCount + " / " + aggregatorResults.size());
+ System.out.println(" Success rate: " + String.format("%.2f%%", successRate));
+
+ if (avgInclusionTime.isPresent()) {
+ System.out.println(" Average inclusion time: " + String.format("%.2f ms", avgInclusionTime.getAsDouble()));
+ }
+
+ // Print failed verifications
+ List failed = aggregatorResults.stream()
+ .filter(r -> !r.isVerified())
+ .collect(Collectors.toList());
+
+ if (!failed.isEmpty()) {
+ System.out.println(" Failed verifications (" + failed.size() + "):");
+ failed.forEach(r -> System.out.println(" ❌ " + r.getRequestId() +
+ " - " + (r.getStatus() != null ? r.getStatus() : "Unknown error")));
+ } else {
+ System.out.println(" ✅ All commitments verified successfully!");
+ }
+
+ System.out.println();
+ }
+ }
+
+ public void printPerformanceComparison(List results, int aggregatorCount) {
+ Map> resultsByAggregator = results.stream()
+ .collect(Collectors.groupingBy(r -> extractAggregatorFromUserName(r.getUserName())));
+
+ System.out.println("=== 🏆 PERFORMANCE WINNER ANALYSIS ===");
+
+ // Find best success rate
+ double bestSuccessRate = 0;
+ String bestSuccessAggregator = "";
+
+ // Find fastest average inclusion time
+ double fastestAvgTime = Double.MAX_VALUE;
+ String fastestAggregator = "";
+
+ for (int i = 0; i < aggregatorCount; i++) {
+ String aggregatorId = "-Aggregator" + i;
+ List aggregatorResults = resultsByAggregator.getOrDefault(aggregatorId, new ArrayList<>());
+
+ if (aggregatorResults.isEmpty()) continue;
+
+ long verifiedCount = aggregatorResults.stream()
+ .filter(CommitmentResult::isVerified)
+ .count();
+
+ double successRate = (double) verifiedCount / aggregatorResults.size() * 100;
+
+ if (successRate > bestSuccessRate) {
+ bestSuccessRate = successRate;
+ bestSuccessAggregator = "Aggregator" + i;
+ }
+
+ OptionalDouble avgInclusionTime = aggregatorResults.stream()
+ .filter(CommitmentResult::isVerified)
+ .mapToDouble(CommitmentResult::getInclusionDurationMillis)
+ .average();
+
+ if (avgInclusionTime.isPresent() && avgInclusionTime.getAsDouble() < fastestAvgTime) {
+ fastestAvgTime = avgInclusionTime.getAsDouble();
+ fastestAggregator = "Aggregator" + i;
+ }
+ }
+
+ System.out.println("🥇 Highest Success Rate: " + bestSuccessAggregator +
+ " (" + String.format("%.2f%%", bestSuccessRate) + ")");
+
+ if (fastestAvgTime != Double.MAX_VALUE) {
+ System.out.println("⚡ Fastest Inclusion Time: " + fastestAggregator +
+ " (" + String.format("%.2f ms", fastestAvgTime) + ")");
+ }
+
+ System.out.println("=====================================\n");
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/unicitylabs/sdk/utils/TestUtils.java b/src/test/java/org/unicitylabs/sdk/utils/TestUtils.java
index a8a7193..6b61a4c 100644
--- a/src/test/java/org/unicitylabs/sdk/utils/TestUtils.java
+++ b/src/test/java/org/unicitylabs/sdk/utils/TestUtils.java
@@ -1,16 +1,38 @@
package org.unicitylabs.sdk.utils;
+import org.unicitylabs.sdk.api.Authenticator;
+import org.unicitylabs.sdk.api.RequestId;
import org.unicitylabs.sdk.token.fungible.CoinId;
import org.unicitylabs.sdk.token.fungible.TokenCoinData;
+import org.unicitylabs.sdk.StateTransitionClient;
+import org.unicitylabs.sdk.address.Address;
+import org.unicitylabs.sdk.api.SubmitCommitmentResponse;
+import org.unicitylabs.sdk.api.SubmitCommitmentStatus;
+import org.unicitylabs.sdk.hash.DataHash;
+import org.unicitylabs.sdk.hash.DataHasher;
+import org.unicitylabs.sdk.hash.HashAlgorithm;
+import org.unicitylabs.sdk.predicate.MaskedPredicate;
+import org.unicitylabs.sdk.signing.SigningService;
+import org.unicitylabs.sdk.token.Token;
+import org.unicitylabs.sdk.token.TokenId;
+import org.unicitylabs.sdk.token.TokenState;
+import org.unicitylabs.sdk.token.TokenType;
+import org.unicitylabs.sdk.transaction.*;
+import org.unicitylabs.sdk.util.InclusionProofUtils;
+
import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
+import java.util.List;
import java.util.Map;
/**
* Utility methods for tests.
*/
public class TestUtils {
+
+ private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
private static final SecureRandom RANDOM = new SecureRandom();
/**
@@ -39,4 +61,263 @@ public static TokenCoinData randomCoinData(int numCoins) {
}
return new TokenCoinData(coins);
}
+
+ private static final SecureRandom SECURE_RANDOM = new SecureRandom();
+
+ /**
+ * Creates a token mint commitment and submits it to the client
+ */
+ public static Token mintTokenForUser(
+ StateTransitionClient client,
+ SigningService signingService,
+ byte[] nonce,
+ TokenId tokenId,
+ TokenType tokenType,
+ TokenCoinData coinData) throws Exception {
+
+ MaskedPredicate predicate = MaskedPredicate.create(signingService, HashAlgorithm.SHA256, nonce);
+ Address address = predicate.getReference(tokenType).toAddress();
+ TokenState tokenState = new TokenState(predicate, null);
+
+ MintCommitment> mintCommitment = MintCommitment.create(
+ new MintTransactionData(
+ tokenId,
+ tokenType,
+ new TestTokenData(randomBytes(32)).getData(),
+ coinData,
+ address,
+ randomBytes(5),
+ null,
+ null
+ )
+ );
+
+ SubmitCommitmentResponse response = client.submitCommitment(mintCommitment).get();
+ if (response.getStatus() != SubmitCommitmentStatus.SUCCESS) {
+ throw new Exception("Failed to submit mint commitment: " + response.getStatus());
+ }
+
+ InclusionProof inclusionProof = InclusionProofUtils.waitInclusionProof(client, mintCommitment).get();
+ return new Token(tokenState, mintCommitment.toTransaction(inclusionProof));
+ }
+
+ /**
+ * Transfers a token from one user to another
+ */
+ public static Token transferToken(
+ StateTransitionClient client,
+ Token sourceToken,
+ SigningService fromSigningService,
+ SigningService toSigningService,
+ byte[] toNonce,
+ Address toAddress,
+ byte[] customData,
+ List> additionalTokens) throws Exception {
+
+ // Create data hash if custom data provided
+ DataHash dataHash = null;
+ if (customData != null) {
+ dataHash = hashData(customData);
+ }
+
+ // Submit transfer commitment
+ TransferCommitment transferCommitment = TransferCommitment.create(
+ sourceToken,
+ toAddress,
+ randomBytes(32),
+ dataHash,
+ null,
+ fromSigningService
+ );
+
+ SubmitCommitmentResponse response = client.submitCommitment(sourceToken, transferCommitment).get();
+ if (response.getStatus() != SubmitCommitmentStatus.SUCCESS) {
+ throw new Exception("Failed to submit transfer commitment: " + response.getStatus());
+ }
+
+ // Wait for inclusion proof
+ InclusionProof inclusionProof = InclusionProofUtils.waitInclusionProof(client, transferCommitment).get();
+ Transaction transferTransaction = transferCommitment.toTransaction(sourceToken, inclusionProof);
+
+
+ //Bob steps
+ // Create predicate for recipient
+ MaskedPredicate toPredicate = MaskedPredicate.create(toSigningService, HashAlgorithm.SHA256, toNonce);
+
+ // Finalize transaction
+ return client.finalizeTransaction(
+ sourceToken,
+ new TokenState(toPredicate, customData),
+ transferTransaction,
+ additionalTokens != null ? additionalTokens : List.of()
+ );
+ }
+
+ /**
+ * Creates random coin data with specified number of coins
+ */
+ public static TokenCoinData createRandomCoinData(int coinCount) {
+ Map coins = new java.util.HashMap<>();
+ for (int i = 0; i < coinCount; i++) {
+ CoinId coinId = new CoinId(randomBytes(32));
+ BigInteger value = BigInteger.valueOf(SECURE_RANDOM.nextInt(1000) + 100); // Random value between 100-1099
+ coins.put(coinId, value);
+ }
+ return new TokenCoinData(coins);
+ }
+
+ /**
+ * Generates random bytes of specified length
+ */
+ public static byte[] generateRandomBytes(int length) {
+ byte[] bytes = new byte[length];
+ SECURE_RANDOM.nextBytes(bytes);
+ return bytes;
+ }
+
+ /**
+ * Creates a hash of the provided data
+ */
+ public static DataHash hashData(byte[] data) {
+ return new DataHasher(HashAlgorithm.SHA256).update(data).digest();
+ }
+
+ /**
+ * Creates a hash of string data
+ */
+ public static DataHash hashData(String data) {
+ return hashData(data.getBytes(StandardCharsets.UTF_8));
+ }
+
+ /**
+ * Creates a signing service from a user name and optional nonce
+ */
+ public static SigningService createSigningServiceForUser(String userName, byte[] nonce) {
+ byte[] secret = userName.getBytes(StandardCharsets.UTF_8);
+ return SigningService.createFromMaskedSecret(secret, nonce);
+ }
+
+ /**
+ * Sets up a user with signing service and nonce in the provided maps
+ */
+ public static void setupUser(String userName,
+ Map userSigningServices,
+ Map userNonces,
+ Map userSecret) {
+ byte[] secret = userName.getBytes(StandardCharsets.UTF_8);
+ byte[] nonce = generateRandomBytes(32);
+ SigningService signingService = SigningService.createFromMaskedSecret(secret, nonce);
+
+ userSigningServices.put(userName, signingService);
+ userNonces.put(userName, nonce);
+ userSecret.put(userName,secret);
+ }
+
+ /**
+ * Validates that a token is properly owned by a signing service
+ */
+ public static boolean validateTokenOwnership(Token token, SigningService signingService) {
+ if (!token.verify().isSuccessful()) {
+ return false;
+ }
+ return token.getState().getUnlockPredicate().isOwner(signingService.getPublicKey());
+ }
+
+ public static RequestId createRequestId(SigningService signingService, DataHash stateHash) {
+ return RequestId.createFromImprint(signingService.getPublicKey(), stateHash.getImprint());
+ }
+
+ public static Authenticator createAuthenticator(SigningService signingService, DataHash txDataHash, DataHash stateHash) {
+ return Authenticator.create(signingService, txDataHash, stateHash);
+ }
+
+ /**
+ * Waits for a commitment to be included and returns the inclusion proof
+ */
+ public static InclusionProof waitForInclusionProof(StateTransitionClient client, Commitment commitment) throws Exception {
+ return InclusionProofUtils.waitInclusionProof(client, commitment).get();
+ }
+
+ /**
+ * Generates a random token ID
+ */
+ public static TokenId generateRandomTokenId() {
+ return new TokenId(randomBytes(32));
+ }
+
+ /**
+ * Generates a random token type
+ */
+ public static TokenType generateRandomTokenType() {
+ return new TokenType(randomBytes(32));
+ }
+
+ /**
+ * Creates a token type from a string identifier
+ */
+ public static TokenType createTokenTypeFromString(String identifier) {
+ return new TokenType(identifier.getBytes(StandardCharsets.UTF_8));
+ }
+
+ /**
+ * Validates performance metrics
+ */
+ public static class PerformanceValidator {
+ public static void validateDuration(long actualDuration, long maxDuration, String operation) {
+ if (actualDuration >= maxDuration) {
+ throw new AssertionError(String.format(
+ "%s took %d ms, should be less than %d ms",
+ operation, actualDuration, maxDuration));
+ }
+ }
+
+ public static void validateSuccessRate(long successful, long total, double minSuccessRate, String operation) {
+ double actualRate = (double) successful / total;
+ if (actualRate < minSuccessRate) {
+ throw new AssertionError(String.format(
+ "%s success rate %.2f%% is below required %.2f%%",
+ operation, actualRate * 100, minSuccessRate * 100));
+ }
+ }
+ }
+
+ /**
+ * Token operation result wrapper
+ */
+ public static class TokenOperationResult {
+ private final boolean success;
+ private final String message;
+ private final Token token;
+ private final Exception error;
+
+ public TokenOperationResult(boolean success, String message, Token token, Exception error) {
+ this.success = success;
+ this.message = message;
+ this.token = token;
+ this.error = error;
+ }
+
+ public static TokenOperationResult success(String message, Token token) {
+ return new TokenOperationResult(true, message, token, null);
+ }
+
+ public static TokenOperationResult failure(String message, Exception error) {
+ return new TokenOperationResult(false, message, null, error);
+ }
+
+ public boolean isSuccess() { return success; }
+ public String getMessage() { return message; }
+ public Token getToken() { return token; }
+ public Exception getError() { return error; }
+ }
+
+ public static String generateRandomString(int length) {
+ StringBuilder sb = new StringBuilder(length);
+ for (int i = 0; i < length; i++) {
+ sb.append(CHARACTERS.charAt(RANDOM.nextInt(CHARACTERS.length())));
+ }
+ return sb.toString();
+ }
+
+
}
\ No newline at end of file
diff --git a/src/test/java/org/unicitylabs/sdk/utils/helpers/CommitmentResult.java b/src/test/java/org/unicitylabs/sdk/utils/helpers/CommitmentResult.java
new file mode 100644
index 0000000..345f56d
--- /dev/null
+++ b/src/test/java/org/unicitylabs/sdk/utils/helpers/CommitmentResult.java
@@ -0,0 +1,75 @@
+package org.unicitylabs.sdk.utils.helpers;
+
+import org.unicitylabs.sdk.api.RequestId;
+import org.unicitylabs.sdk.transaction.InclusionProofVerificationStatus;
+
+public class CommitmentResult {
+ private final String userName;
+ private final String threadName;
+ private final RequestId requestId;
+ private final boolean success;
+ private final long startTime;
+ private final long endTime;
+
+ public boolean verified;
+ private long inclusionStart;
+ private long inclusionEnd;
+ private String status;
+
+ public CommitmentResult(String userName, String threadName, RequestId requestId,
+ boolean success, long startTime, long endTime) {
+ this.userName = userName;
+ this.threadName = threadName;
+ this.requestId = requestId;
+ this.success = success;
+ this.startTime = startTime;
+ this.endTime = endTime;
+ }
+
+ public boolean isSuccess() { return success; }
+
+ public void markVerified(long start, long end) {
+ this.verified = true;
+ this.inclusionStart = start;
+ this.inclusionEnd = end;
+ this.status = InclusionProofVerificationStatus.OK.toString();
+ }
+
+ public RequestId getRequestId() {
+ return this.requestId;
+ }
+
+ public void markFailedVerification(long start, long end, String status) {
+ this.verified = false;
+ this.inclusionStart = start;
+ this.inclusionEnd = end;
+ this.status = status.toString();
+ }
+
+ public boolean isVerified() {
+ return this.verified;
+ }
+
+ public String getStatus(){
+ return this.status;
+ }
+
+ // Add these getter methods for the multi-aggregator functionality
+ public String getUserName() {
+ return this.userName;
+ }
+
+ public String getThreadName() {
+ return this.threadName;
+ }
+
+ // Helper method to get inclusion proof verification duration
+ public long getInclusionDurationNanos() {
+ return this.inclusionEnd - this.inclusionStart;
+ }
+
+ public double getInclusionDurationMillis() {
+ return getInclusionDurationNanos() / 1_000_000.0;
+ }
+
+}
diff --git a/src/test/java/org/unicitylabs/sdk/utils/helpers/PendingTransfer.java b/src/test/java/org/unicitylabs/sdk/utils/helpers/PendingTransfer.java
new file mode 100644
index 0000000..db3bcbe
--- /dev/null
+++ b/src/test/java/org/unicitylabs/sdk/utils/helpers/PendingTransfer.java
@@ -0,0 +1,18 @@
+package org.unicitylabs.sdk.utils.helpers;
+
+import org.unicitylabs.sdk.token.Token;
+import org.unicitylabs.sdk.transaction.Transaction;
+import org.unicitylabs.sdk.transaction.TransferTransactionData;
+
+public class PendingTransfer {
+ private final Token sourceToken;
+ private final Transaction transaction;
+
+ public PendingTransfer(Token sourceToken, Transaction transaction) {
+ this.sourceToken = sourceToken;
+ this.transaction = transaction;
+ }
+
+ public Token getSourceToken() { return sourceToken; }
+ public Transaction getTransaction() { return transaction; }
+}
diff --git a/src/test/resources/org/unicitylabs/sdk/features/advanced-token-scenarios.feature b/src/test/resources/org/unicitylabs/sdk/features/advanced-token-scenarios.feature
new file mode 100644
index 0000000..ffafc97
--- /dev/null
+++ b/src/test/resources/org/unicitylabs/sdk/features/advanced-token-scenarios.feature
@@ -0,0 +1,107 @@
+@advanced-token
+Feature: Advanced Token Scenarios
+ As a developer using the Unicity SDK
+ I want to test complex token operations and edge cases
+ So that I can ensure the system handles advanced scenarios correctly
+
+ Background:
+ Given the aggregator URL is configured
+ And the state transition client is initialized
+ And the following users are set up with their signing services
+ | Alice |
+ | Bob |
+ | Carol |
+ | Dave |
+
+ @performance
+ @reset
+ Scenario Outline: Bulk token operations performance
+ Given users are configured for bulk operations
+ When each user mints tokens simultaneously
+ And all tokens are verified in parallel
+ Then all tokens should be created successfully
+ And the operation should complete within seconds
+ And the success rate should be at least %
+
+ Examples:
+ | userCount | tokensPerUser | totalTokens | maxDuration | minSuccessRate |
+ | 5 | 10 | 50 | 30 | 95 |
+ | 10 | 5 | 50 | 25 | 90 |
+
+ @edge-cases
+ @reset
+ Scenario Outline: Token transfer chain with validation
+ Given "Alice" mints a token with coin value
+ When the token is transferred through the chain of existing users
+ And each transfer includes custom data validation
+ Then the final token should maintain original properties
+ And all intermediate transfers should be recorded correctly
+ And the token should have transfers in history
+
+ Examples:
+ | coinValue | expectedTransfers |
+ | 1000 | 3 |
+ | 5000 | 3 |
+
+ @nametag-scenarios
+ @reset
+ Scenario Outline: Complex name tag token interactions
+ Given "Bob" creates name tag tokens with different addresses
+ When "Alice" transfers tokens to each of "Bob" name tags
+ And "Bob" finalizes all received tokens
+ And "Bob" consolidates all received tokens
+ Then "Bob" should own tokens
+ And all "Bob" nametag tokens should remain valid
+ And proxy addressing should work for all "Bob" name tags
+
+ Examples:
+ | nametagCount |
+ | 3 |
+ | 5 |
+
+# @splitting-scenarios
+# Scenario Outline: Multi-level token splitting
+# Given Carol owns a token worth coins
+# When the token is split into tokens
+# And one of the resulting tokens is split again into tokens
+# Then the total number of tokens should be
+# And the total coin value should equal the original
+# And all tokens should be independently transferable
+#
+# Examples:
+# | originalValue | firstSplit | secondSplit | totalTokens |
+# | 10000 | 3 | 2 | 4 |
+# | 20000 | 4 | 3 | 6 |
+#
+# @concurrency
+# Scenario: Concurrent operations on same token
+# Given "Alice" owns a token
+# When "Alice" attempts to transfer the token to both "Bob" and "Carol" simultaneously
+# Then only one transfer should succeed
+# And the other transfer should be rejected
+# And the token should belong to exactly one recipient
+# And no tokens should be duplicated
+#
+# @data-integrity
+# Scenario Outline: Large custom data handling
+# Given a token with custom data of size bytes
+# When the token is transferred with the large custom data
+# Then the transfer should
+# And the data integrity should be maintained
+# And the system performance should remain acceptable
+#
+# Examples:
+# | dataSize | expectation |
+# | 1024 | succeed |
+# | 10240 | succeed |
+# | 102400 | succeed |
+#
+# @mixed-predicates
+# Scenario: Mixed predicate type interactions
+# Given "Alice" uses a "masked" predicate
+# And "Bob" uses a "unmasked" predicate
+# And "Carol" uses a name tag token
+# When tokens are transferred between all users in various combinations
+# Then all transfers should work correctly regardless of predicate types
+# And token verification should pass for all predicate combinations
+# And the system should handle predicate conversions properly
\ No newline at end of file
diff --git a/src/test/resources/org/unicitylabs/sdk/features/aggregator-connectivity.feature b/src/test/resources/org/unicitylabs/sdk/features/aggregator-connectivity.feature
new file mode 100644
index 0000000..c077eee
--- /dev/null
+++ b/src/test/resources/org/unicitylabs/sdk/features/aggregator-connectivity.feature
@@ -0,0 +1,41 @@
+@aggregator-connectivity
+Feature: Aggregator Connectivity and Basic Operations
+ As a developer using the Unicity SDK
+ I want to verify connectivity with the aggregator
+ So that I can ensure the system is operational
+
+ Background:
+ Given the aggregator URL is configured
+ And the aggregator client is initialized
+
+ Scenario: Verify aggregator connectivity
+ When I request the current block height
+ Then the block height should be returned
+ And the block height should be greater than 0
+
+ Scenario Outline: Submit commitment with performance validation
+ Given a random secret of bytes
+ And a state hash from bytes of random data
+ And transaction data ""
+ When I submit a commitment with the generated data
+ Then the commitment should be submitted successfully
+ And the submission should complete in less than milliseconds
+
+ Examples:
+ | secretLength | stateLength | txData | maxDuration |
+ | 32 | 32 | test commitment performance | 5000 |
+ | 16 | 24 | simple test data | 3000 |
+
+ Scenario Outline: Parallel mint commitments with inclusion proof verification
+ Given I configure threads with commitments each
+ When I submit all mint commitments concurrently
+ Then all mint commitments should receive inclusion proofs within seconds
+
+ Examples:
+ | threadsCount | commitmentsPerThread | timeoutSeconds |
+ | 1 | 10 | 120 |
+ | 5 | 10 | 120 |
+ | 10 | 10 | 120 |
+ | 20 | 10 | 120 |
+ | 40 | 10 | 120 |
+ | 80 | 10 | 120 |
\ No newline at end of file
diff --git a/src/test/resources/org/unicitylabs/sdk/features/multiple-aggregators.feature b/src/test/resources/org/unicitylabs/sdk/features/multiple-aggregators.feature
new file mode 100644
index 0000000..f29873c
--- /dev/null
+++ b/src/test/resources/org/unicitylabs/sdk/features/multiple-aggregators.feature
@@ -0,0 +1,9 @@
+Feature: Bulk Commitment Testing with Multiple Aggregators
+
+ Scenario: Submit commitments to multiple aggregators concurrently
+ Given the aggregator URLs are configured
+ And the aggregator clients are initialized
+ And I configure 1 threads with 1 commitments each
+ When I submit all mint commitments concurrently to all aggregators
+ Then all mint commitments should receive inclusion proofs from all aggregators within 30 seconds
+ And I should see performance metrics for each aggregator
\ No newline at end of file
diff --git a/src/test/resources/org/unicitylabs/sdk/features/token-transfer.feature b/src/test/resources/org/unicitylabs/sdk/features/token-transfer.feature
new file mode 100644
index 0000000..ea775f2
--- /dev/null
+++ b/src/test/resources/org/unicitylabs/sdk/features/token-transfer.feature
@@ -0,0 +1,50 @@
+@token-transfer
+Feature: Token Transfer Operations
+ As a developer using the Unicity SDK
+ I want to perform token operations including minting, transfers, and splits
+ So that I can manage token lifecycle effectively
+
+ Background:
+ Given the aggregator URL is configured
+ And the state transition client is initialized
+ And the following users are set up with their signing services
+ | name |
+ | Alice |
+ | Bob |
+ | Carol |
+
+ Scenario: Complete token transfer flow from Alice to Bob to Carol
+ Given user "Bob" create a nametag token with custom data "Bob Custom data"
+ And "Alice" mints a token with random coin data
+ When "Alice" transfers the token to "Bob" using a proxy address
+ And "Bob" finalizes all received tokens
+ Then "Bob" should own the token successfully
+ And all "Bob" nametag tokens should remain valid
+ And the token should maintain its original ID and type
+ When "Bob" transfers the token to "Carol" using an unmasked predicate
+ And "Carol" finalizes all received tokens
+ Then "Carol" should own the token successfully
+ And the token should have 2 transactions in its history
+
+ Scenario Outline: Token minting with different configurations
+ Given user "" with nonce of bytes
+ When the user mints a token of type "" with coin data containing coins
+ Then the token should be minted successfully
+ And the token should be verified successfully
+ And the token should belong to the user
+
+ Examples:
+ | user | nonceLength | tokenType | coinCount |
+ | Alice | 32 | Standard | 2 |
+ | Bob | 24 | Premium | 3 |
+ | Carol | 16 | Basic | 1 |
+
+ Scenario Outline: Name tag token creation and usage
+ Given user "" create a nametag token with custom data ""
+ Then the name tag token should be created successfully
+ And the name tag should be usable for proxy addressing
+
+ Examples:
+ | user | nametagData |
+ | Bob | Bob's Address |
+ | Alice | Alice's Tag |
\ No newline at end of file