diff --git a/src/main/java/com/unicity/sdk/token/Token.java b/src/main/java/com/unicity/sdk/token/Token.java index 641d27c..299902c 100644 --- a/src/main/java/com/unicity/sdk/token/Token.java +++ b/src/main/java/com/unicity/sdk/token/Token.java @@ -44,6 +44,10 @@ public Token(TokenState state, Transaction genesis, List genesis, List> nametags) { + this(state, genesis, List.of(), nametags); + } + public Token(TokenState state, Transaction genesis) { this(state, genesis, List.of(), List.of()); } diff --git a/src/main/java/com/unicity/sdk/token/TokenId.java b/src/main/java/com/unicity/sdk/token/TokenId.java index d703096..d781689 100644 --- a/src/main/java/com/unicity/sdk/token/TokenId.java +++ b/src/main/java/com/unicity/sdk/token/TokenId.java @@ -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. @@ -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)) diff --git a/src/main/java/com/unicity/sdk/transaction/MintTransactionData.java b/src/main/java/com/unicity/sdk/transaction/MintTransactionData.java index f02cca3..0781621 100644 --- a/src/main/java/com/unicity/sdk/transaction/MintTransactionData.java +++ b/src/main/java/com/unicity/sdk/transaction/MintTransactionData.java @@ -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; @@ -56,32 +55,6 @@ public MintTransactionData( this.reason = reason; } - public static MintTransactionData 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; } diff --git a/src/main/java/com/unicity/sdk/transaction/NametagMintTransactionData.java b/src/main/java/com/unicity/sdk/transaction/NametagMintTransactionData.java new file mode 100644 index 0000000..9e80353 --- /dev/null +++ b/src/main/java/com/unicity/sdk/transaction/NametagMintTransactionData.java @@ -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 extends MintTransactionData { + 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, + new DataHasher(HashAlgorithm.SHA256) + .update(targetAddress.getAddress().getBytes(StandardCharsets.UTF_8)) + .digest(), + null + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/unicity/sdk/transaction/TransferCommitment.java b/src/main/java/com/unicity/sdk/transaction/TransferCommitment.java index 8f41af9..e057417 100644 --- a/src/main/java/com/unicity/sdk/transaction/TransferCommitment.java +++ b/src/main/java/com/unicity/sdk/transaction/TransferCommitment.java @@ -52,7 +52,7 @@ public Transaction toTransaction(Token token, throw new RuntimeException("Inclusion proof verification failed."); } - if (!inclusionProof.getAuthenticator().isPresent()) { + if (inclusionProof.getAuthenticator().isEmpty()) { throw new RuntimeException("Authenticator is missing from inclusion proof."); } diff --git a/src/test/java/com/unicity/sdk/common/split/BaseTokenSplitTest.java b/src/test/java/com/unicity/sdk/common/split/BaseTokenSplitTest.java new file mode 100644 index 0000000..ada2f55 --- /dev/null +++ b/src/test/java/com/unicity/sdk/common/split/BaseTokenSplitTest.java @@ -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>> mintCommitments = split.createSplitMintCommitments( + burnCommitment.toTransaction( + token, + InclusionProofUtils.waitInclusionProof(this.client, burnCommitment).get() + ) + ); + + for (MintCommitment> 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> splitToken = new Token<>( + state, + commitment.toTransaction( + InclusionProofUtils.waitInclusionProof(this.client, commitment).get() + ), + List.of(nametagToken) + ); + + Assertions.assertTrue(splitToken.verify().isSuccessful()); + } + + + } + + +} diff --git a/src/test/java/com/unicity/sdk/e2e/CommonTestFlow.java b/src/test/java/com/unicity/sdk/e2e/CommonTestFlow.java index 40ce47a..e9916a4 100644 --- a/src/test/java/com/unicity/sdk/e2e/CommonTestFlow.java +++ b/src/test/java/com/unicity/sdk/e2e/CommonTestFlow.java @@ -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; @@ -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], diff --git a/src/test/java/com/unicity/sdk/e2e/TokenInMemoryClientTest.java b/src/test/java/com/unicity/sdk/e2e/TokenInMemoryClientTest.java deleted file mode 100644 index 7d1bc34..0000000 --- a/src/test/java/com/unicity/sdk/e2e/TokenInMemoryClientTest.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.unicity.sdk.e2e; - -import com.unicity.sdk.StateTransitionClient; -import com.unicity.sdk.TestAggregatorClient; -import org.junit.jupiter.api.Test; - -/** - * End-to-end tests for token operations using CommonTestFlow. Matches TypeScript SDK's test - * structure. - */ -public class TokenInMemoryClientTest { - - private StateTransitionClient client = new StateTransitionClient(new TestAggregatorClient()); - - @Test - void testTransferFlow() throws Exception { - CommonTestFlow.testTransferFlow(this.client); - } - -} \ No newline at end of file diff --git a/src/test/java/com/unicity/sdk/functional/FunctionalTokenSplitTest.java b/src/test/java/com/unicity/sdk/functional/FunctionalTokenSplitTest.java new file mode 100644 index 0000000..f3f8c4d --- /dev/null +++ b/src/test/java/com/unicity/sdk/functional/FunctionalTokenSplitTest.java @@ -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()); + } +} diff --git a/src/test/java/com/unicity/sdk/token/TokenTest.java b/src/test/java/com/unicity/sdk/token/TokenTest.java index 5e6dfe5..01d16d9 100644 --- a/src/test/java/com/unicity/sdk/token/TokenTest.java +++ b/src/test/java/com/unicity/sdk/token/TokenTest.java @@ -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; @@ -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), diff --git a/src/test/java/com/unicity/sdk/utils/TokenUtils.java b/src/test/java/com/unicity/sdk/utils/TokenUtils.java new file mode 100644 index 0000000..d2fa9bf --- /dev/null +++ b/src/test/java/com/unicity/sdk/utils/TokenUtils.java @@ -0,0 +1,173 @@ +package com.unicity.sdk.utils; + +import static com.unicity.sdk.utils.TestUtils.randomBytes; +import static com.unicity.sdk.utils.TestUtils.randomCoinData; + +import com.unicity.sdk.StateTransitionClient; +import com.unicity.sdk.address.Address; +import com.unicity.sdk.api.SubmitCommitmentResponse; +import com.unicity.sdk.api.SubmitCommitmentStatus; +import com.unicity.sdk.hash.DataHash; +import com.unicity.sdk.hash.HashAlgorithm; +import com.unicity.sdk.predicate.MaskedPredicate; +import com.unicity.sdk.signing.SigningService; +import com.unicity.sdk.token.NameTagTokenState; +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.TokenCoinData; +import com.unicity.sdk.transaction.InclusionProof; +import com.unicity.sdk.transaction.MintCommitment; +import com.unicity.sdk.transaction.MintTransactionData; +import com.unicity.sdk.transaction.MintTransactionReason; +import com.unicity.sdk.transaction.NametagMintTransactionData; +import com.unicity.sdk.util.InclusionProofUtils; + +public class TokenUtils { + public static Token mintToken(StateTransitionClient client, byte[] secret) throws Exception { + return TokenUtils.mintToken( + client, + secret, + new TokenId(randomBytes(32)), + new TokenType(randomBytes(32)), + randomBytes(32), + randomCoinData(2), + randomBytes(32), + randomBytes(32), + null + ); + } + + public static Token mintToken( + StateTransitionClient client, + byte[] secret, + TokenId tokenId, + TokenType tokenType, + byte[] tokenData, + TokenCoinData coinData, + byte[] nonce, + byte[] salt, + DataHash dataHash + ) throws Exception { + SigningService signingService = SigningService.createFromSecret(secret, nonce); + + MaskedPredicate predicate = MaskedPredicate.create( + signingService, + HashAlgorithm.SHA256, + nonce + ); + + Address address = predicate.getReference(tokenType).toAddress(); + TokenState tokenState = new TokenState(predicate, null); + + MintCommitment> commitment = MintCommitment.create( + new MintTransactionData<>( + tokenId, + tokenType, + tokenData, + coinData, + address, + salt, + dataHash, + null + ) + ); + + // Submit mint transaction using StateTransitionClient + SubmitCommitmentResponse response = client + .submitCommitment(commitment) + .get(); + if (response.getStatus() != SubmitCommitmentStatus.SUCCESS) { + throw new Exception(String.format("Failed to submit mint commitment: %s", + response.getStatus())); + } + + // Wait for inclusion proof + InclusionProof inclusionProof = InclusionProofUtils.waitInclusionProof( + client, + commitment + ).get(); + + // Create mint transaction + return new Token<>( + tokenState, + commitment.toTransaction(inclusionProof) + ); + } + + public static Token mintNametagToken( + StateTransitionClient client, + byte[] secret, + String nametag, + Address targetAddress + ) throws Exception { + return mintNametagToken( + client, + secret, + new TokenType(randomBytes(32)), + randomBytes(32), + randomCoinData(2), + nametag, + targetAddress, + randomBytes(32), + randomBytes(32) + ); + } + + public static Token mintNametagToken( + StateTransitionClient client, + byte[] secret, + TokenType tokenType, + byte[] tokenData, + TokenCoinData coinData, + String nametag, + Address targetAddress, + byte[] nonce, + byte[] salt + ) throws Exception { + SigningService signingService = SigningService.createFromSecret(secret, nonce); + + MaskedPredicate predicate = MaskedPredicate.create( + signingService, + HashAlgorithm.SHA256, + nonce + ); + + Address address = predicate.getReference(tokenType).toAddress(); + TokenState tokenState = new NameTagTokenState(predicate, targetAddress); + + MintCommitment> commitment = MintCommitment.create( + new NametagMintTransactionData<>( + nametag, + tokenType, + tokenData, + coinData, + address, + salt, + targetAddress + ) + ); + + // Submit mint transaction using StateTransitionClient + SubmitCommitmentResponse response = client + .submitCommitment(commitment) + .get(); + if (response.getStatus() != SubmitCommitmentStatus.SUCCESS) { + throw new Exception(String.format("Failed to submit mint commitment: %s", + response.getStatus())); + } + + // Wait for inclusion proof + InclusionProof inclusionProof = InclusionProofUtils.waitInclusionProof( + client, + commitment + ).get(); + + // Create mint transaction + return new Token<>( + tokenState, + commitment.toTransaction(inclusionProof) + ); + } +}