Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8dbaa48
Update SMT
martti007 Jul 15, 2025
9b66f67
Add serializers for json and cbor
martti007 Jul 15, 2025
9c0eb55
Updated Inclusion proof and SMT code
martti007 Jul 16, 2025
7b8fdf0
Add JsonRpcError serializer
martti007 Jul 16, 2025
3ef59bc
Add all requests and appropriate serializers
martti007 Jul 17, 2025
24a5aac
Add Token json serializer
martti007 Jul 20, 2025
3f7e82b
Remove cbor encoder test
martti007 Jul 20, 2025
e76f8af
Fix StateTransitionClient
martti007 Jul 23, 2025
d5f8266
Make nametag mint work
martti007 Aug 5, 2025
4f054bd
Fix JSON nametag parsing
martti007 Aug 5, 2025
7cbe4c6
Add tests for multiple time nametag usage
martti007 Aug 5, 2025
fc4ed8a
Update code style
martti007 Aug 7, 2025
19e34fe
Update tests
martti007 Aug 7, 2025
4a0c304
Change hashing exception to runtime exception
martti007 Aug 11, 2025
09ffb63
Move smt to mtree package
martti007 Aug 12, 2025
1441ae2
Update Sum tree hashing
martti007 Aug 12, 2025
8115b75
Make path branch sibling and branch getters return optional
martti007 Aug 12, 2025
a75351e
Make nullable field get data optional
martti007 Aug 16, 2025
e89ad02
Add checkstyle, update libraries and naming for sum tree classes
martti007 Aug 16, 2025
e93d626
Almost finished token split, removed createTransaction from client
martti007 Aug 18, 2025
51571b1
Update split token, add test
martti007 Aug 18, 2025
055352f
Add note for serialization
martti007 Aug 18, 2025
672b562
Update checkstyle, fix errors for java
martti007 Aug 19, 2025
3b545e2
Fix serialization between js and java sdk
martti007 Aug 26, 2025
e89f18d
Update SMT path, mint reason verification and overall serialization
martti007 Sep 1, 2025
19f0efd
Fix doc, code style and errors
martti007 Sep 1, 2025
6c58a52
Fix some checkstyle errors
martti007 Sep 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.env
.idea
.gradle
.claude
build
src/test/resources/docker/aggregator/mongo-data
26 changes: 20 additions & 6 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
id("java-library")
id("maven-publish")
id("checkstyle")
id("com.google.protobuf") version "0.9.4"
id("ru.vyarus.animalsniffer") version "2.0.1"
}
Expand All @@ -21,21 +22,21 @@ configurations {

dependencies {
// Core dependencies that work on both platforms
implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.17.0")
implementation("com.fasterxml.jackson.core:jackson-databind:2.17.0")
implementation("org.bouncycastle:bcprov-jdk15on:1.70")
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.19.2")
implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.19.2")
implementation("com.fasterxml.jackson.core:jackson-databind:2.19.2")
implementation("org.bouncycastle:bcprov-jdk18on:1.81")
implementation("org.slf4j:slf4j-api:2.0.13")
implementation("com.squareup.okhttp3:okhttp:4.12.0")

// Platform-specific Guava
compileOnly("com.google.guava:guava:33.0.0-jre")
"android"("com.google.guava:guava:33.0.0-android")
"jvm"("com.google.guava:guava:33.0.0-jre")

// Animal Sniffer signatures
signature("com.toasttab.android:gummy-bears-api-31:0.7.0@signature")

// Testing
testImplementation(platform("org.junit:junit-bom:5.10.2"))
testImplementation("org.junit.jupiter:junit-jupiter")
Expand All @@ -44,6 +45,8 @@ dependencies {
testImplementation("org.testcontainers:mongodb:1.19.8")
testImplementation("org.slf4j:slf4j-simple:2.0.13")
testImplementation("com.google.guava:guava:33.0.0-jre")

checkstyle("com.puppycrawl.tools:checkstyle:10.26.1")
}

java {
Expand All @@ -53,13 +56,24 @@ java {
withJavadocJar()
}

checkstyle {
configFile = file("config/checkstyle/checkstyle.xml")
}

tasks.test {
useJUnitPlatform {
excludeTags("integration")
}
maxHeapSize = "1024m"
}

tasks.withType<Checkstyle>{
reports {
xml.required.set(false)
html.required.set(true)
}
}

tasks.register<Test>("integrationTest") {
useJUnitPlatform {
includeTags("integration")
Expand Down
465 changes: 465 additions & 0 deletions config/checkstyle/checkstyle.xml

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions src/main/java/com/unicity/sdk/Hashable.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.unicity.sdk;

import com.unicity.sdk.hash.DataHash;

public interface Hashable {
DataHash getHash();
}
268 changes: 66 additions & 202 deletions src/main/java/com/unicity/sdk/StateTransitionClient.java
Original file line number Diff line number Diff line change
@@ -1,218 +1,82 @@

package com.unicity.sdk;

import com.unicity.sdk.address.DirectAddress;
import com.unicity.sdk.api.AggregatorClient;
import com.unicity.sdk.api.Authenticator;
import com.unicity.sdk.api.RequestId;
import com.unicity.sdk.api.SubmitCommitmentResponse;
import com.unicity.sdk.api.SubmitCommitmentStatus;
import com.unicity.sdk.shared.hash.DataHash;
import com.unicity.sdk.shared.hash.HashAlgorithm;
import com.unicity.sdk.shared.signing.ISigningService;
import com.unicity.sdk.shared.signing.SigningService;
import com.unicity.sdk.shared.util.HexConverter;
import com.unicity.sdk.token.Token;
import com.unicity.sdk.token.TokenState;
import com.unicity.sdk.transaction.*;

import java.util.ArrayList;
import com.unicity.sdk.transaction.Commitment;
import com.unicity.sdk.transaction.InclusionProof;
import com.unicity.sdk.transaction.InclusionProofVerificationStatus;
import com.unicity.sdk.transaction.MintCommitment;
import com.unicity.sdk.transaction.MintTransactionData;
import com.unicity.sdk.transaction.Transaction;
import com.unicity.sdk.transaction.TransferCommitment;
import com.unicity.sdk.transaction.TransferTransactionData;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;

public class StateTransitionClient {
public static final byte[] MINTER_SECRET = HexConverter.decode("495f414d5f554e4956455253414c5f4d494e5445525f464f525f");

protected final AggregatorClient aggregatorClient;

public StateTransitionClient(AggregatorClient aggregatorClient) {
this.aggregatorClient = aggregatorClient;
}

public <T extends MintTransactionData<?>> CompletableFuture<Commitment<T>> submitMintTransaction(T transactionData) {
return SigningService.createFromSecret(MINTER_SECRET, transactionData.getTokenId().getBytes())
.thenCompose(signingService -> {
System.out.println("Minter tokenId: " + transactionData.getTokenId().toJSON());
System.out.println("Minter publicKey from secret: " + HexConverter.encode(signingService.getPublicKey()));
return sendTransaction(transactionData, signingService);
});
protected final AggregatorClient client;

public StateTransitionClient(AggregatorClient client) {
this.client = client;
}

public <T extends MintTransactionData<?>> CompletableFuture<SubmitCommitmentResponse> submitCommitment(
MintCommitment<T> commitment) {
return this.client.submitCommitment(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aggregator client coult simply take Commitment object as input, MintCommitment and TransferCommitment both extend it. TransactionData could know how to calculate its own hash (MintTransactionData already does) and TransferTransactionData has all the token info it needs at creation time.

commitment.getRequestId(),
commitment.getTransactionData().calculateHash(),
commitment.getAuthenticator()
);
}

public CompletableFuture<SubmitCommitmentResponse> submitCommitment(
Token<?> token,
TransferCommitment commitment) {
if (!commitment.getTransactionData().getSourceState().getUnlockPredicate()
.isOwner(commitment.getAuthenticator().getPublicKey())) {
throw new IllegalArgumentException(
"Ownership verification failed: Authenticator does not match source state predicate.");
}

public CompletableFuture<Commitment<TransactionData>> submitTransaction(
TransactionData transactionData,
ISigningService<?> signingService) {
return transactionData.getSourceState().getUnlockPredicate().isOwner(signingService.getPublicKey())
.thenCompose(isOwner -> {
if (!isOwner) {
return CompletableFuture.failedFuture(new Exception("Failed to unlock token"));
}
return sendTransaction(transactionData, signingService);
});
}

public <T extends ISerializable> CompletableFuture<Transaction<T>> createTransaction(
Commitment<T> commitment,
InclusionProof inclusionProof) {
return inclusionProof.verify(commitment.getRequestId())
.thenCompose(status -> {
if (status != InclusionProofVerificationStatus.OK) {
return CompletableFuture.failedFuture(new Exception("Inclusion proof verification failed."));
}

// For mint transactions, authenticator might be null in the inclusion proof
// This is expected behavior from the aggregator

// Check transaction hash if applicable
T transactionData = commitment.getTransactionData();
DataHash txHash = null;
if (transactionData instanceof TransactionData) {
txHash = ((TransactionData) transactionData).getHash();
} else if (transactionData instanceof MintTransactionData) {
txHash = ((MintTransactionData<?>) transactionData).getHash();
}

// Check transaction hash if both are present
if (txHash != null && inclusionProof.getTransactionHash() != null) {
if (!inclusionProof.getTransactionHash().equals(txHash)) {
return CompletableFuture.failedFuture(new Exception("Payload hash mismatch"));
}
}

return CompletableFuture.completedFuture(new Transaction<>(commitment.getTransactionData(), inclusionProof));
});
}

public <T extends Transaction<MintTransactionData<?>>> CompletableFuture<Token<T>> finishTransaction(
Token<T> token,
TokenState state,
Transaction<TransactionData> transaction) {
return finishTransaction(token, state, transaction, new ArrayList<>());
}

public <T extends Transaction<MintTransactionData<?>>> CompletableFuture<Token<T>> finishTransaction(
Token<T> token,
TokenState state,
Transaction<TransactionData> transaction,
List<Token<?>> nametagTokens) {
return transaction.getData().getSourceState().getUnlockPredicate().verify(transaction)
.thenCompose(verified -> {
if (!verified) {
return CompletableFuture.failedFuture(new Exception("Predicate verification failed"));
}

return DirectAddress.create(state.getUnlockPredicate().getReference())
.thenCompose(expectedAddress -> {
if (!expectedAddress.toString().equals(transaction.getData().getRecipient())) {
return CompletableFuture.failedFuture(new Exception("Recipient address mismatch"));
}

List<Transaction<?>> transactions = new ArrayList<>(token.getTransactions());
transactions.add(transaction);

return transaction.containsData(state.getData())
.thenCompose(contains -> {
if (!contains) {
return CompletableFuture.failedFuture(new Exception("State data is not part of transaction."));
}

return CompletableFuture.completedFuture(
new Token<>(state, token.getGenesis(), transactions, nametagTokens));
});
});
});
}

public CompletableFuture<InclusionProofVerificationStatus> getTokenStatus(
Token<? extends Transaction<MintTransactionData<?>>> token,
byte[] publicKey) {
return RequestId.create(publicKey, token.getState().getHash())
.thenCompose(requestId -> aggregatorClient.getInclusionProof(requestId))
.thenCompose(inclusionProof ->
RequestId.create(publicKey, token.getState().getHash())
.thenCompose(inclusionProof::verify));
}

public CompletableFuture<InclusionProof> getInclusionProof(Commitment<?> commitment) {
return aggregatorClient.getInclusionProof(commitment.getRequestId());
}

private <TD extends ISerializable> CompletableFuture<Commitment<TD>> sendTransaction(
TD transactionData,
ISigningService<?> signingService) {

// Get the source state hash and request ID based on the transaction data type
CompletableFuture<RequestId> requestIdFuture;
DataHash sourceStateHash;
DataHash transactionHash;

if (transactionData instanceof TransactionData) {
TransactionData txData = (TransactionData) transactionData;
sourceStateHash = txData.getSourceState().getHash();
transactionHash = txData.getHash();
requestIdFuture = RequestId.create(signingService.getPublicKey(), sourceStateHash);
} else if (transactionData instanceof MintTransactionData) {
MintTransactionData<?> mintData = (MintTransactionData<?>) transactionData;
// For mint transactions, use the sourceState from the mint data
RequestId mintSourceState = mintData.getSourceState();
sourceStateHash = mintSourceState.getHash();
transactionHash = mintData.getHash();
// TypeScript creates RequestId using: RequestId.create(signingService.publicKey, transactionData.sourceState.hash)
requestIdFuture = RequestId.create(signingService.getPublicKey(), mintSourceState.getHash());
} else {
return CompletableFuture.failedFuture(new Exception("Unsupported transaction data type"));
}

return requestIdFuture.thenCompose(requestId ->
Authenticator.create(
signingService,
transactionHash,
sourceStateHash)
.thenCompose(authenticator -> {
// Debug logging
System.out.println("Authenticator JSON: " + authenticator.toJSON());
System.out.println("RequestId: " + requestId.toJSON());
System.out.println("TransactionHash: " + transactionHash.toJSON());
if (transactionData instanceof MintTransactionData) {
MintTransactionData<?> mintData = (MintTransactionData<?>) transactionData;
System.out.println("MintSourceState: " + mintData.getSourceState().toJSON());
System.out.println("MintSourceState hash: " + mintData.getSourceState().getHash().toJSON());
System.out.println("SigningService publicKey: " + HexConverter.encode(signingService.getPublicKey()));
}

return aggregatorClient.submitTransaction(requestId, transactionHash, authenticator)
.thenCompose(result -> {
if (result.getStatus() != SubmitCommitmentStatus.SUCCESS) {
return CompletableFuture.failedFuture(
new Exception("Could not submit transaction: " + result.getStatus()));
}
return CompletableFuture.completedFuture(
new Commitment<>(requestId, transactionData, authenticator));
});
})
);
}

public CompletableFuture<SubmitCommitmentResponse> submitCommitment(Commitment<?> commitment) {
DataHash txHash = null;
ISerializable transactionData = commitment.getTransactionData();
if (transactionData instanceof TransactionData) {
txHash = ((TransactionData) transactionData).getHash();
} else if (transactionData instanceof MintTransactionData) {
txHash = ((MintTransactionData<?>) transactionData).getHash();
}

if (txHash == null) {
return CompletableFuture.failedFuture(new IllegalArgumentException("Cannot get hash from transaction data"));
}

return aggregatorClient.submitTransaction(
commitment.getRequestId(),
txHash,
commitment.getAuthenticator()
);
}

public AggregatorClient getAggregatorClient() {
return aggregatorClient;
}
return this.client.submitCommitment(commitment.getRequestId(), commitment.getTransactionData()
.calculateHash(token.getId(), token.getType()), commitment.getAuthenticator());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to extend my previous comment: transfer could also have commitment.getTransactionData().calculateHash() with no arguments and inside calculateHash() there might be a lambda which has token id and type in its closure

}

public <T extends Transaction<MintTransactionData<?>>> Token<T> finishTransaction(
Token<T> token,
TokenState state,
Transaction<TransferTransactionData> transaction
) {
return this.finishTransaction(token, state, transaction, List.of());
}

public <T extends Transaction<MintTransactionData<?>>> Token<T> finishTransaction(
Comment thread
martti007 marked this conversation as resolved.
Outdated
Token<T> token,
TokenState state,
Transaction<TransferTransactionData> transaction,
List<Token<?>> nametagTokens
) {
Objects.requireNonNull(token, "Token is null");

return token.update(state, transaction, nametagTokens);
}

public CompletableFuture<InclusionProofVerificationStatus> getTokenStatus(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unused, needs tests

Token<? extends Transaction<MintTransactionData<?>>> token,
byte[] publicKey) {
RequestId requestId = RequestId.create(publicKey,
token.getState().calculateHash(token.getId(), token.getType()));
return this.client.getInclusionProof(requestId)
.thenApply(inclusionProof -> inclusionProof.verify(requestId));
}

public CompletableFuture<InclusionProof> getInclusionProof(Commitment<?> commitment) {
return this.client.getInclusionProof(commitment.getRequestId());
}
}
7 changes: 7 additions & 0 deletions src/main/java/com/unicity/sdk/address/Address.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

package com.unicity.sdk.address;

public interface Address {
AddressScheme getScheme();
String getAddress();
}
Loading
Loading