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:
+ *
+ * - Asynchronously listening for commit events for this transaction when using the
+ * {@link DefaultCommitHandlers#NONE} commit handler.
+ * - Correlating client application operations with activity in Fabric peers and orderers.
+ *
+ * @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));
+ }
}