diff --git a/pom.xml b/pom.xml index 96786364..17279e2e 100644 --- a/pom.xml +++ b/pom.xml @@ -86,7 +86,7 @@ org.hyperledger.fabric-sdk-java fabric-sdk-java - 2.1.1 + 2.1.2 org.mockito diff --git a/src/main/java/org/hyperledger/fabric/gateway/Transaction.java b/src/main/java/org/hyperledger/fabric/gateway/Transaction.java index cd9c4908..3abe04c5 100644 --- a/src/main/java/org/hyperledger/fabric/gateway/Transaction.java +++ b/src/main/java/org/hyperledger/fabric/gateway/Transaction.java @@ -11,6 +11,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import org.hyperledger.fabric.gateway.spi.CommitHandlerFactory; import org.hyperledger.fabric.sdk.Peer; /** @@ -31,6 +32,17 @@ public interface Transaction { */ String getName(); + /** + * Get the transaction ID that will be used when submitting this transaction. This can be useful for: + * + * @return A transaction ID. + */ + String getTransactionId(); + /** * Set transient data that will be passed to the transaction function * but will not be stored on the ledger. This can be used to pass @@ -49,6 +61,14 @@ public interface Transaction { */ Transaction setCommitTimeout(long timeout, TimeUnit timeUnit); + /** + * Set the commit handler to use for this transaction invocation instead of the default handler configured for the + * gateway. + * @param commitHandler A commit handler implementation. + * @return this transaction object to allow method chaining. + */ + Transaction setCommitHandler(CommitHandlerFactory commitHandler); + /** * Set the peers that should be used for endorsement of transaction submitted to the ledger using * {@link #submit(String...)}. diff --git a/src/main/java/org/hyperledger/fabric/gateway/impl/TransactionImpl.java b/src/main/java/org/hyperledger/fabric/gateway/impl/TransactionImpl.java index b51b8acc..cae80b38 100644 --- a/src/main/java/org/hyperledger/fabric/gateway/impl/TransactionImpl.java +++ b/src/main/java/org/hyperledger/fabric/gateway/impl/TransactionImpl.java @@ -33,6 +33,7 @@ import org.hyperledger.fabric.sdk.exception.InvalidArgumentException; import org.hyperledger.fabric.sdk.exception.ProposalException; import org.hyperledger.fabric.sdk.exception.ServiceDiscoveryException; +import org.hyperledger.fabric.sdk.transaction.TransactionContext; import static org.hyperledger.fabric.sdk.Channel.DiscoveryOptions.createDiscoveryOptions; @@ -47,11 +48,12 @@ public final class TransactionImpl implements Transaction { private final NetworkImpl network; private final Channel channel; private final GatewayImpl gateway; - private final CommitHandlerFactory commitHandlerFactory; + private CommitHandlerFactory commitHandlerFactory; private TimePeriod commitTimeout; private final QueryHandler queryHandler; private Map transientData = null; private Collection endorsingPeers = null; + private final TransactionContext transactionContext; TransactionImpl(final ContractImpl contract, final String name) { this.contract = contract; @@ -62,6 +64,7 @@ public final class TransactionImpl implements Transaction { commitHandlerFactory = gateway.getCommitHandlerFactory(); commitTimeout = gateway.getCommitTimeout(); queryHandler = network.getQueryHandler(); + transactionContext = channel.newTransactionContext(); } @Override @@ -69,6 +72,11 @@ public String getName() { return name; } + @Override + public String getTransactionId() { + return transactionContext.getTxID(); + } + @Override public Transaction setTransient(final Map transientData) { this.transientData = transientData; @@ -81,6 +89,12 @@ public Transaction setCommitTimeout(final long timeout, final TimeUnit timeUnit) return this; } + @Override + public Transaction setCommitHandler(final CommitHandlerFactory commitHandler) { + commitHandlerFactory = commitHandler; + return this; + } + @Override public Transaction setEndorsingPeers(final Collection peers) { endorsingPeers = peers; @@ -116,8 +130,7 @@ private Collection sendTransactionProposal(final TransactionPr } else if (network.getGateway().isDiscoveryEnabled()) { Channel.DiscoveryOptions discoveryOptions = createDiscoveryOptions() .setEndorsementSelector(ServiceDiscovery.EndorsementSelector.ENDORSEMENT_SELECTION_RANDOM) - .setInspectResults(true) - .setForceDiscovery(true); + .setInspectResults(true); return channel.sendTransactionProposalToEndorsers(request, discoveryOptions); } else { return channel.sendTransactionProposal(request); @@ -127,9 +140,8 @@ private Collection sendTransactionProposal(final TransactionPr private byte[] commitTransaction(final Collection validResponses) throws TimeoutException, ContractException, InterruptedException { ProposalResponse proposalResponse = validResponses.iterator().next(); - String transactionId = proposalResponse.getTransactionID(); - CommitHandler commitHandler = commitHandlerFactory.create(transactionId, network); + CommitHandler commitHandler = commitHandlerFactory.create(getTransactionId(), network); commitHandler.startListening(); try { @@ -172,6 +184,7 @@ private void configureRequest(final TransactionRequest request, final String... request.setChaincodeName(contract.getChaincodeId()); request.setFcn(name); request.setArgs(args); + request.setTransactionContext(transactionContext); } private Collection validatePeerResponses(final Collection proposalResponses) diff --git a/src/test/java/org/hyperledger/fabric/gateway/TestUtils.java b/src/test/java/org/hyperledger/fabric/gateway/TestUtils.java index 73143e82..4480518f 100644 --- a/src/test/java/org/hyperledger/fabric/gateway/TestUtils.java +++ b/src/test/java/org/hyperledger/fabric/gateway/TestUtils.java @@ -13,16 +13,13 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.FileAttribute; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.cert.CertificateException; import java.util.Arrays; import java.util.Collection; import java.util.EnumSet; +import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; -import org.bouncycastle.operator.OperatorCreationException; import org.hyperledger.fabric.gateway.impl.GatewayImpl; import org.hyperledger.fabric.gateway.impl.identity.GatewayUser; import org.hyperledger.fabric.gateway.spi.PeerDisconnectEvent; @@ -34,10 +31,13 @@ import org.hyperledger.fabric.sdk.HFClient; import org.hyperledger.fabric.sdk.Peer; import org.hyperledger.fabric.sdk.ProposalResponse; +import org.hyperledger.fabric.sdk.QueryByChaincodeRequest; +import org.hyperledger.fabric.sdk.TransactionProposalRequest; import org.hyperledger.fabric.sdk.User; import org.hyperledger.fabric.sdk.exception.InvalidArgumentException; import org.hyperledger.fabric.sdk.exception.ServiceDiscoveryException; import org.hyperledger.fabric.sdk.identity.X509Enrollment; +import org.hyperledger.fabric.sdk.transaction.TransactionContext; import org.mockito.Mockito; public final class TestUtils { @@ -46,6 +46,8 @@ public final class TestUtils { private static final String UNUSED_FILE_PREFIX = "fgj-unused-"; private static final Path NETWORK_CONFIG_PATH = Paths.get("src", "test", "java", "org", "hyperledger", "fabric", "gateway", "connection.json"); + private final AtomicLong currentTransactionId = new AtomicLong(); + public static TestUtils getInstance() { return INSTANCE; } @@ -79,6 +81,8 @@ public HFClient newMockClient() { HFClient mockClient = Mockito.mock(HFClient.class); Mockito.when(mockClient.getUserContext()).thenReturn(user); + Mockito.when(mockClient.newTransactionProposalRequest()).thenReturn(TransactionProposalRequest.newInstance(user)); + Mockito.when(mockClient.newQueryProposalRequest()).thenReturn(QueryByChaincodeRequest.newInstance(user)); return mockClient; } @@ -100,6 +104,8 @@ public Peer newMockPeer(String name) { public Channel newMockChannel(String name) { Channel mockChannel = Mockito.mock(Channel.class); Mockito.when(mockChannel.getName()).thenReturn(name); + Mockito.when(mockChannel.newTransactionContext()) + .thenAnswer(invocation -> newMockTransactionContext()); AtomicReference sdPeerAdditionRef = new AtomicReference<>(newMockSDPeerAddition()); Mockito.when(mockChannel.getSDPeerAddition()) @@ -110,6 +116,16 @@ public Channel newMockChannel(String name) { return mockChannel; } + private TransactionContext newMockTransactionContext() { + TransactionContext mockContext = Mockito.mock(TransactionContext.class); + Mockito.when(mockContext.getTxID()).thenReturn(newFakeTransactionId()); + return mockContext; + } + + private String newFakeTransactionId() { + return Long.toHexString(currentTransactionId.incrementAndGet()); + } + private Channel.SDPeerAddition newMockSDPeerAddition() { Channel.SDPeerAddition mockPeerAddition = Mockito.mock(Channel.SDPeerAddition.class); try { @@ -166,6 +182,14 @@ public Throwable getCause() { }; } + public ProposalResponse newSuccessfulProposalResponse() { + return newSuccessfulProposalResponse(new byte[0]); + } + + public ProposalResponse newSuccessfulProposalResponse(String responsePayload) { + return newSuccessfulProposalResponse(responsePayload.getBytes()); + } + public ProposalResponse newSuccessfulProposalResponse(byte[] responsePayload) { ProposalResponse response = newProposalResponse(200, responsePayload); Mockito.when(response.getStatus()).thenReturn(ChaincodeResponse.Status.SUCCESS); diff --git a/src/test/java/org/hyperledger/fabric/gateway/impl/TransactionTest.java b/src/test/java/org/hyperledger/fabric/gateway/impl/TransactionTest.java index b600cc06..2914f283 100644 --- a/src/test/java/org/hyperledger/fabric/gateway/impl/TransactionTest.java +++ b/src/test/java/org/hyperledger/fabric/gateway/impl/TransactionTest.java @@ -11,20 +11,29 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import org.hyperledger.fabric.gateway.Contract; import org.hyperledger.fabric.gateway.ContractException; +import org.hyperledger.fabric.gateway.DefaultCommitHandlers; import org.hyperledger.fabric.gateway.Gateway; import org.hyperledger.fabric.gateway.GatewayException; +import org.hyperledger.fabric.gateway.Network; import org.hyperledger.fabric.gateway.TestUtils; +import org.hyperledger.fabric.gateway.Transaction; import org.hyperledger.fabric.gateway.spi.CommitHandler; +import org.hyperledger.fabric.gateway.spi.CommitHandlerFactory; import org.hyperledger.fabric.sdk.Channel; import org.hyperledger.fabric.sdk.HFClient; import org.hyperledger.fabric.sdk.Peer; import org.hyperledger.fabric.sdk.ProposalResponse; +import org.hyperledger.fabric.sdk.QueryByChaincodeRequest; import org.hyperledger.fabric.sdk.TransactionProposalRequest; +import org.hyperledger.fabric.sdk.TransactionRequest; +import org.hyperledger.fabric.sdk.User; +import org.hyperledger.fabric.sdk.transaction.TransactionContext; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -41,6 +50,7 @@ import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -51,6 +61,7 @@ public class TransactionTest { private Gateway gateway; private Channel channel; private Contract contract; + private CommitHandlerFactory defaultCommithandlerFactory; private CommitHandler commitHandler; private Peer peer1; private Peer peer2; @@ -63,6 +74,8 @@ public class TransactionTest { private ArgumentCaptor> peerCaptor; @Captor private ArgumentCaptor discoveryOptionsCaptor; + @Captor + private ArgumentCaptor proposalRequestCaptor; @BeforeEach public void setup() throws Exception { @@ -78,13 +91,18 @@ public void setup() throws Exception { HFClient client = testUtils.newMockClient(); when(client.getChannel(anyString())).thenReturn(channel); - when(client.newTransactionProposalRequest()).thenReturn(HFClient.createNewInstance().newTransactionProposalRequest()); - when(client.newQueryProposalRequest()).thenReturn(HFClient.createNewInstance().newQueryProposalRequest()); commitHandler = mock(CommitHandler.class); + defaultCommithandlerFactory = spy(new CommitHandlerFactory() { + @Override + public CommitHandler create(final String transactionId, final Network network) { + return commitHandler; + } + }); + gatewayBuilder = TestUtils.getInstance().newGatewayBuilder() .client(client) - .commitHandler((transactionId, network) -> commitHandler) + .commitHandler(defaultCommithandlerFactory) .commitTimeout(timeout.getTime(), timeout.getTimeUnit()); gateway = gatewayBuilder.connect(); contract = gateway.getNetwork("network").getContract("contract"); @@ -129,7 +147,7 @@ public void testEvaluateUnsuccessfulResponse() throws Exception { @Test public void testEvaluateSuccess() throws Exception { String expected = "successful result"; - ProposalResponse response = testUtils.newSuccessfulProposalResponse(expected.getBytes()); + ProposalResponse response = testUtils.newSuccessfulProposalResponse(expected); when(response.getPeer()).thenReturn(peer1); when(channel.queryByChaincode(any(), anyCollection())).thenReturn(Collections.singletonList(response)); @@ -140,7 +158,7 @@ public void testEvaluateSuccess() throws Exception { @Test public void testEvaluateSuccessWithTransient() throws Exception { String expected = "successful result"; - ProposalResponse response = testUtils.newSuccessfulProposalResponse(expected.getBytes()); + ProposalResponse response = testUtils.newSuccessfulProposalResponse(expected); when(response.getPeer()).thenReturn(peer1); when(channel.queryByChaincode(any(), anyCollection())).thenReturn(Collections.singletonList(response)); @@ -175,7 +193,7 @@ public void submit_with_bad_responses_throws_ContractException_with_responses() @Test public void testSubmitSuccess() throws Exception { String expected = "successful result"; - ProposalResponse response = testUtils.newSuccessfulProposalResponse(expected.getBytes()); + ProposalResponse response = testUtils.newSuccessfulProposalResponse(expected); when(channel.sendTransactionProposal(any())).thenReturn(Collections.singletonList(response)); byte[] result = contract.submitTransaction("txn", "arg1"); @@ -185,7 +203,7 @@ public void testSubmitSuccess() throws Exception { @Test public void testSubmitSuccessWithTransient() throws Exception { String expected = "successful result"; - ProposalResponse response = testUtils.newSuccessfulProposalResponse(expected.getBytes()); + ProposalResponse response = testUtils.newSuccessfulProposalResponse(expected); when(channel.sendTransactionProposal(any())).thenReturn(Collections.singletonList(response)); byte[] result = contract.createTransaction("txn") @@ -196,8 +214,7 @@ public void testSubmitSuccessWithTransient() throws Exception { @Test public void testUsesGatewayCommitTimeout() throws Exception { - String expected = "successful result"; - ProposalResponse response = testUtils.newSuccessfulProposalResponse(expected.getBytes()); + ProposalResponse response = testUtils.newSuccessfulProposalResponse(); when(channel.sendTransactionProposal(any())).thenReturn(Collections.singletonList(response)); contract.submitTransaction("txn", "arg1"); @@ -207,8 +224,7 @@ public void testUsesGatewayCommitTimeout() throws Exception { @Test public void testSubmitSuccessWithSomeBadProposalResponses() throws Exception { - String expected = "successful result"; - ProposalResponse goodResponse = testUtils.newSuccessfulProposalResponse(expected.getBytes()); + ProposalResponse goodResponse = testUtils.newSuccessfulProposalResponse(); when(channel.sendTransactionProposal(any())).thenReturn(Arrays.asList(failureResponse, goodResponse)); contract.submitTransaction("txn", "arg1"); @@ -219,8 +235,7 @@ public void testSubmitSuccessWithSomeBadProposalResponses() throws Exception { @Test public void testSubmitWithEndorsingPeers() throws Exception { - String expected = "successful result"; - ProposalResponse goodResponse = testUtils.newSuccessfulProposalResponse(expected.getBytes()); + ProposalResponse goodResponse = testUtils.newSuccessfulProposalResponse(); when(channel.sendTransactionProposal(any(TransactionProposalRequest.class), anyCollection())) .thenReturn(Collections.singletonList(goodResponse)); @@ -235,7 +250,7 @@ public void testSubmitWithEndorsingPeers() throws Exception { @Test public void submit_using_discovery_sets_inspect_results_option() throws Exception { String expected = "successful result"; - ProposalResponse goodResponse = testUtils.newSuccessfulProposalResponse(expected.getBytes()); + ProposalResponse goodResponse = testUtils.newSuccessfulProposalResponse(expected); when(channel.sendTransactionProposalToEndorsers(any(TransactionProposalRequest.class), any(Channel.DiscoveryOptions.class))) .thenReturn(Collections.singletonList(goodResponse)); gateway = gatewayBuilder @@ -252,8 +267,7 @@ public void submit_using_discovery_sets_inspect_results_option() throws Exceptio @Test public void commit_failure_throws_ContractException_with_proposal_responses() throws Exception { - String expected = "successful result"; - ProposalResponse response = testUtils.newSuccessfulProposalResponse(expected.getBytes()); + ProposalResponse response = testUtils.newSuccessfulProposalResponse(); when(channel.sendTransactionProposal(any())).thenReturn(Collections.singletonList(response)); ContractException commitException = new ContractException("Commit failed"); doThrow(commitException).when(commitHandler).waitForEvents(anyLong(), any(TimeUnit.class)); @@ -264,4 +278,55 @@ public void commit_failure_throws_ContractException_with_proposal_responses() th assertThat(e.getProposalResponses()).containsExactly(response); } + + @Test + public void get_transaction_ID() { + String transactionId = contract.createTransaction("txn").getTransactionId(); + + assertThat(transactionId).isNotEmpty(); + } + + @Test + public void submit_proposal_includes_transaction_ID() throws Exception { + ProposalResponse response = testUtils.newSuccessfulProposalResponse(); + when(channel.sendTransactionProposal(any())).thenReturn(Collections.singletonList(response)); + + Transaction transaction = contract.createTransaction("txn"); + String expected = transaction.getTransactionId(); + transaction.submit(); + + verify(channel).sendTransactionProposal(proposalRequestCaptor.capture()); + Optional actual = proposalRequestCaptor.getValue().getTransactionContext(); + assertThat(actual).hasValueSatisfying(context -> { + assertThat(context.getTxID()).isEqualTo(expected); + }); + } + + @Test + public void submit_uses_default_commit_handler() throws Exception { + ProposalResponse response = testUtils.newSuccessfulProposalResponse(); + when(channel.sendTransactionProposal(any())).thenReturn(Collections.singletonList(response)); + + contract.submitTransaction("txn"); + + verify(defaultCommithandlerFactory).create(anyString(), any(Network.class)); + } + + @Test + public void submit_uses_specified_commit_handler() throws Exception { + CommitHandlerFactory commitHandlerFactory = spy(new CommitHandlerFactory() { + @Override + public CommitHandler create(final String transactionId, final Network network) { + return commitHandler; + } + }); + ProposalResponse response = testUtils.newSuccessfulProposalResponse(); + when(channel.sendTransactionProposal(any())).thenReturn(Collections.singletonList(response)); + + contract.createTransaction("txn") + .setCommitHandler(commitHandlerFactory) + .submit(); + + verify(commitHandlerFactory).create(anyString(), any(Network.class)); + } }