Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/main/java/com/unicity/sdk/token/Token.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ public Token(TokenState state, Transaction<T> genesis, List<Transaction<Transfer
this.nametags = List.copyOf(nametags);
}

public Token(TokenState state, Transaction<T> genesis, List<Token<?>> nametags) {
this(state, genesis, List.of(), nametags);
}

public Token(TokenState state, Transaction<T> genesis) {
this(state, genesis, List.of(), List.of());
}
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/com/unicity/sdk/token/TokenId.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.unicity.sdk.util.HexConverter;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Objects;

/**
* Globally unique identifier of a token.
Expand All @@ -27,6 +28,8 @@ public BitString toBitString() {
}

public static TokenId fromNameTag(String name) {
Objects.requireNonNull(name, "Name cannot be null");

return new TokenId(
new DataHasher(HashAlgorithm.SHA256)
.update(name.getBytes(StandardCharsets.UTF_8))
Expand Down
27 changes: 0 additions & 27 deletions src/main/java/com/unicity/sdk/transaction/MintTransactionData.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import com.unicity.sdk.token.TokenType;
import com.unicity.sdk.token.fungible.TokenCoinData;
import com.unicity.sdk.util.HexConverter;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
Expand Down Expand Up @@ -56,32 +55,6 @@ public MintTransactionData(
this.reason = reason;
}

public static MintTransactionData<MintTransactionReason> createForNametag(
String name,
TokenType tokenType,
byte[] tokenData,
TokenCoinData coinData,
Address recipient,
byte[] salt,
Address targetAddress
) {
Objects.requireNonNull(name, "Name cannot be null");
Objects.requireNonNull(targetAddress, "Target address cannot be null");

return new MintTransactionData<>(
TokenId.fromNameTag(name),
tokenType,
tokenData,
coinData,
recipient,
salt,
new DataHasher(HashAlgorithm.SHA256)
.update(targetAddress.getAddress().getBytes(StandardCharsets.UTF_8))
.digest(),
null
);
}

public TokenId getTokenId() {
return this.tokenId;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.unicity.sdk.transaction;

import com.unicity.sdk.address.Address;
import com.unicity.sdk.hash.DataHasher;
import com.unicity.sdk.hash.HashAlgorithm;
import com.unicity.sdk.token.TokenId;
import com.unicity.sdk.token.TokenType;
import com.unicity.sdk.token.fungible.TokenCoinData;
import java.nio.charset.StandardCharsets;

public class NametagMintTransactionData<R extends MintTransactionReason> extends MintTransactionData<R> {
public NametagMintTransactionData(
String name,
TokenType tokenType,
byte[] tokenData,
TokenCoinData coinData,
Address recipient,
byte[] salt,
Address targetAddress
) {
super(
TokenId.fromNameTag(name),
tokenType,
tokenData,
coinData,
recipient,
salt,

Copilot AI Sep 6, 2025

Copy link

Choose a reason for hiding this comment

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

[nitpick] The hashing logic for the target address should be documented. It's unclear why the target address is being hashed and used as the bearer hash parameter in the parent constructor.

Suggested change
salt,
salt,
// Hash the target address using SHA-256 and use the result as the bearer hash parameter.
// This ensures that the transaction is uniquely associated with the target address in a secure and consistent manner.

Copilot uses AI. Check for mistakes.
new DataHasher(HashAlgorithm.SHA256)
.update(targetAddress.getAddress().getBytes(StandardCharsets.UTF_8))
.digest(),
null
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public Transaction<TransferTransactionData> toTransaction(Token<?> token,
throw new RuntimeException("Inclusion proof verification failed.");
}

if (!inclusionProof.getAuthenticator().isPresent()) {
if (inclusionProof.getAuthenticator().isEmpty()) {

Copilot AI Sep 6, 2025

Copy link

Choose a reason for hiding this comment

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

The change from !isPresent() to isEmpty() suggests this might be a Collection or similar type. If this is an Optional, use !isPresent() for clarity and consistency with Optional API patterns.

Suggested change
if (inclusionProof.getAuthenticator().isEmpty()) {
if (!inclusionProof.getAuthenticator().isPresent()) {

Copilot uses AI. Check for mistakes.
throw new RuntimeException("Authenticator is missing from inclusion proof.");
}

Expand Down
202 changes: 202 additions & 0 deletions src/test/java/com/unicity/sdk/common/split/BaseTokenSplitTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package com.unicity.sdk.common.split;

import static com.unicity.sdk.utils.TestUtils.randomBytes;

import com.unicity.sdk.StateTransitionClient;
import com.unicity.sdk.address.Address;
import com.unicity.sdk.address.DirectAddress;
import com.unicity.sdk.address.ProxyAddress;
import com.unicity.sdk.api.SubmitCommitmentResponse;
import com.unicity.sdk.api.SubmitCommitmentStatus;
import com.unicity.sdk.hash.DataHash;
import com.unicity.sdk.hash.DataHasher;
import com.unicity.sdk.hash.HashAlgorithm;
import com.unicity.sdk.predicate.MaskedPredicate;
import com.unicity.sdk.signing.SigningService;
import com.unicity.sdk.token.Token;
import com.unicity.sdk.token.TokenId;
import com.unicity.sdk.token.TokenState;
import com.unicity.sdk.token.TokenType;
import com.unicity.sdk.token.fungible.CoinId;
import com.unicity.sdk.token.fungible.TokenCoinData;
import com.unicity.sdk.transaction.MintCommitment;
import com.unicity.sdk.transaction.MintTransactionData;
import com.unicity.sdk.transaction.TransferCommitment;
import com.unicity.sdk.transaction.split.SplitMintReason;
import com.unicity.sdk.transaction.split.TokenSplitBuilder;
import com.unicity.sdk.transaction.split.TokenSplitBuilder.TokenSplit;
import com.unicity.sdk.util.InclusionProofUtils;
import com.unicity.sdk.utils.TokenUtils;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public abstract class BaseTokenSplitTest {

protected StateTransitionClient client;

@Test
void testTokenSplitFullAmounts() throws Exception {
byte[] secret = "SECRET".getBytes(StandardCharsets.UTF_8);

Token<?> token = TokenUtils.mintToken(
this.client,
secret,
new TokenId(randomBytes(32)),
new TokenType(randomBytes(32)),
randomBytes(32),
new TokenCoinData(Map.of(
new CoinId("test_eur".getBytes(StandardCharsets.UTF_8)),
BigInteger.valueOf(100),
new CoinId("test_usd".getBytes(StandardCharsets.UTF_8)),
BigInteger.valueOf(100)
)),
randomBytes(32),
randomBytes(32),
null
);

String nametag = "my_nametag";

Token<?> nametagToken = TokenUtils.mintNametagToken(
this.client,
secret,
nametag,
// Direct to random address in the beginning
DirectAddress.create(DataHash.fromImprint(new byte[34]))
);

TokenSplitBuilder builder = new TokenSplitBuilder();
TokenSplit split = builder
.createToken(
new TokenId(randomBytes(32)),
new TokenType(randomBytes(32)),
null,
new TokenCoinData(Map.of(
new CoinId("test_eur".getBytes(StandardCharsets.UTF_8)),
BigInteger.valueOf(100)
)),
ProxyAddress.create(nametag),
randomBytes(32),
null
)
.createToken(
new TokenId(randomBytes(32)),
new TokenType(randomBytes(32)),
null,
new TokenCoinData(Map.of(
new CoinId("test_usd".getBytes(StandardCharsets.UTF_8)),
BigInteger.valueOf(100)
)),
ProxyAddress.create(nametag),
randomBytes(32),
null
)
.build(token);

TransferCommitment burnCommitment = split.createBurnCommitment(
randomBytes(32),
SigningService.createFromSecret(secret, token.getState().getUnlockPredicate().getNonce())
);

SubmitCommitmentResponse burnCommitmentResponse = this.client
.submitCommitment(token, burnCommitment)
.get();

if (burnCommitmentResponse.getStatus() != SubmitCommitmentStatus.SUCCESS) {
throw new Exception(String.format("Failed to submit burn commitment: %s",
burnCommitmentResponse.getStatus()));
}

List<MintCommitment<MintTransactionData<SplitMintReason>>> mintCommitments = split.createSplitMintCommitments(
burnCommitment.toTransaction(
token,
InclusionProofUtils.waitInclusionProof(this.client, burnCommitment).get()
)
);

for (MintCommitment<MintTransactionData<SplitMintReason>> commitment : mintCommitments) {
SubmitCommitmentResponse response = this.client
.submitCommitment(commitment)
.get();

if (response.getStatus() != SubmitCommitmentStatus.SUCCESS) {
throw new Exception(String.format("Failed to submit burn commitment: %s",
response.getStatus()));
}

byte[] nonce = randomBytes(32);
TokenState state = new TokenState(
MaskedPredicate.create(
SigningService.createFromSecret(secret, nonce),
HashAlgorithm.SHA256,
nonce
),
null
);
Address address = state.getUnlockPredicate()
.getReference(
commitment.getTransactionData().getTokenType()
)
.toAddress();

byte[] nametagNonce = randomBytes(32);
TokenState nametagTokenState = new TokenState(
MaskedPredicate.create(
SigningService.createFromSecret(secret, nametagNonce),
HashAlgorithm.SHA256,
nametagNonce
),
address.getAddress().getBytes(StandardCharsets.UTF_8)
);

TransferCommitment nametagCommitment = TransferCommitment.create(
nametagToken,
nametagTokenState.getUnlockPredicate().getReference(nametagToken.getType()).toAddress(),
randomBytes(32),
new DataHasher(HashAlgorithm.SHA256)
.update(address.getAddress().getBytes(StandardCharsets.UTF_8))
.digest(),
null,
SigningService.createFromSecret(
secret,
nametagToken.getState().getUnlockPredicate().getNonce()
)
);

SubmitCommitmentResponse nametagTransferResponse = this.client
.submitCommitment(nametagToken, nametagCommitment)
.get();
if (nametagTransferResponse.getStatus() != SubmitCommitmentStatus.SUCCESS) {
throw new Exception(String.format("Failed to submit nametag transfer commitment: %s",
response.getStatus()));
}

nametagToken = this.client.finalizeTransaction(
nametagToken,
nametagTokenState,
nametagCommitment.toTransaction(
nametagToken,
InclusionProofUtils.waitInclusionProof(this.client, nametagCommitment).get()
)
);

Token<MintTransactionData<SplitMintReason>> splitToken = new Token<>(
state,
commitment.toTransaction(
InclusionProofUtils.waitInclusionProof(this.client, commitment).get()
),
List.of(nametagToken)
);

Assertions.assertTrue(splitToken.verify().isSuccessful());
}


}


}
3 changes: 2 additions & 1 deletion src/test/java/com/unicity/sdk/e2e/CommonTestFlow.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.unicity.sdk.token.TokenType;
import com.unicity.sdk.token.fungible.CoinId;
import com.unicity.sdk.transaction.MintTransactionReason;
import com.unicity.sdk.transaction.NametagMintTransactionData;
import com.unicity.sdk.transaction.split.SplitMintReason;
import com.unicity.sdk.token.fungible.TokenCoinData;
import com.unicity.sdk.transaction.InclusionProof;
Expand Down Expand Up @@ -166,7 +167,7 @@ public static void testTransferFlow(StateTransitionClient client) throws Excepti
DirectAddress bobNametagAddress = bobNametagPredicate.getReference(bobNametagTokenType)
.toAddress();
MintCommitment<?> nametagMintCommitment = MintCommitment.create(
MintTransactionData.createForNametag(
new NametagMintTransactionData<>(
bobNameTag,
bobNametagTokenType,
new byte[10],
Expand Down
20 changes: 0 additions & 20 deletions src/test/java/com/unicity/sdk/e2e/TokenInMemoryClientTest.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.unicity.sdk.functional;

import com.unicity.sdk.StateTransitionClient;
import com.unicity.sdk.TestAggregatorClient;
import com.unicity.sdk.common.split.BaseTokenSplitTest;
import org.junit.jupiter.api.BeforeEach;

public class FunctionalTokenSplitTest extends BaseTokenSplitTest {
@BeforeEach
void setUp() {
this.client = new StateTransitionClient(new TestAggregatorClient());
}
}
3 changes: 2 additions & 1 deletion src/test/java/com/unicity/sdk/token/TokenTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import com.unicity.sdk.token.fungible.TokenCoinData;
import com.unicity.sdk.transaction.InclusionProof;
import com.unicity.sdk.transaction.MintTransactionData;
import com.unicity.sdk.transaction.NametagMintTransactionData;
import com.unicity.sdk.transaction.Transaction;
import com.unicity.sdk.transaction.TransferTransactionData;
import com.unicity.sdk.utils.TestUtils;
Expand Down Expand Up @@ -47,7 +48,7 @@ public void testJsonSerialization() throws IOException {
nametagNonce),
DirectAddress.create(new DataHash(HashAlgorithm.SHA256, TestUtils.randomBytes(32)))
);
MintTransactionData<?> nametagGenesisData = MintTransactionData.createForNametag(
MintTransactionData<?> nametagGenesisData = new NametagMintTransactionData<>(
UUID.randomUUID().toString(),
new TokenType(TestUtils.randomBytes(32)),
TestUtils.randomBytes(5),
Expand Down
Loading
Loading