diff --git a/ci/scripts/run-test-network-basic.sh b/ci/scripts/run-test-network-basic.sh index 296209657d..a893105507 100755 --- a/ci/scripts/run-test-network-basic.sh +++ b/ci/scripts/run-test-network-basic.sh @@ -123,3 +123,14 @@ SIMULATED_FAILURE_COUNT=1 npm start getAllAssets transact getAllAssets listen SIMULATED_FAILURE_COUNT=1 npm start listen popd stopNetwork + +# Run off-chain data Java application +createNetwork +print "Initializing Typescript off-chain data application" +pushd ../off_chain_data/application-java +rm -f app/checkpoint.json app/store.log +print "Running the output app" +SIMULATED_FAILURE_COUNT=1 ./gradlew run --quiet --args='getAllAssets transact getAllAssets listen' +SIMULATED_FAILURE_COUNT=1 ./gradlew run --quiet --args=listen +popd +stopNetwork diff --git a/off_chain_data/README.md b/off_chain_data/README.md index 22e64844c4..176b782092 100644 --- a/off_chain_data/README.md +++ b/off_chain_data/README.md @@ -16,11 +16,17 @@ This sample uses the block event listening capability of the [Fabric Gateway cli The client application provides several "commands" that can be invoked using the command-line: -- **getAllAssets**: Retrieve the current details of all assets recorded on the ledger. See `application-typescript/src/getAllAssets.ts`. -- **listen**: Listen for block events, and use them to replicate ledger updates in an off-chain data store. See `application-typescript/src/listen.ts`. -- **transact**: Submit a set of transactions to create, modify and delete assets. See `application-typescript/src/transact.ts`. - -To keep the sample code concise, the **listen** command writes ledger updates to an output file named `store.log` in the current working directory. A real implementation could write ledger updates directly to an off-chain data store of choice. You can inspect the information captured in this file as you run the sample. +- **getAllAssets**: Retrieve the current details of all assets recorded on the ledger. See: + - TypeScript: `application-typescript/src/getAllAssets.ts` + - Java: `application-java/app/src/main/java/GetAllAssets.java` +- **listen**: Listen for block events, and use them to replicate ledger updates in an off-chain data store. See: + - TypeScript: `application-typescript/src/listen.ts` + - Java: `application-java/app/src/main/java/Transact.java` +- **transact**: Submit a set of transactions to create, modify and delete assets. See: + - TypeScript: `application-typescript/src/transact.ts` + - Java: `application-java/app/src/main/java/Transact.java` + +To keep the sample code concise, the **listen** command writes ledger updates to an output file named `store.log` in the current working directory (which for the Java sample is the `application-java/app` directory). A real implementation could write ledger updates directly to an off-chain data store of choice. You can inspect the information captured in this file as you run the sample. Note that the **listen** command is is restartable and will resume event listening after the last successfully processed block / transaction. This is achieved using a checkpointer to persist the current listening position. Checkpoint state is persisted to a file named `checkpoint.json` in the current working directory. If no checkpoint state is present, event listening begins from the start of the ledger (block number zero). @@ -34,13 +40,13 @@ The Fabric test network is used to deploy and run this sample. Follow these step 1. Create the test network and a channel (from the `test-network` folder). - ``` + ```bash ./network.sh up createChannel -c mychannel -ca ``` 1. Deploy one of the asset-transfer-basic smart contract implementations (from the `test-network` folder). - ``` + ```bash # To deploy the TypeScript chaincode implementation ./network.sh deployCC -ccn basic -ccp ../asset-transfer-basic/chaincode-typescript/ -ccl typescript @@ -53,31 +59,45 @@ The Fabric test network is used to deploy and run this sample. Follow these step 1. Populate the ledger with some assets and use eventing to capture ledger updates (from the `off_chain_data` folder). - ``` + ```bash # To run the TypeScript sample application cd application-typescript npm install npm start transact listen + + # To run the Java sample application + cd application-java + ./gradlew run --quiet --args='transact listen' ``` 1. Interrupt the listener process using **Control-C**. 1. View the current world state of the blockchain (from the `off_chain_data` folder). You may want to compare the results to the ledger updates captured by the listener in the `store.log` file. - ``` + ```bash # To run the TypeScript sample application cd application-typescript npm --silent start getAllAssets + + # To run the Java sample application + cd application-java + ./gradlew run --quiet --args=getAllAssets ``` 1. Make some more ledger updates, then observe listener resume capability (from the `off_chain_data` folder). Note from the transaction IDs recorded to the console that the listener resumes from exactly after the last successfully processed transaction. - ``` + ```bash # To run the TypeScript sample application cd application-typescript npm start transact SIMULATED_FAILURE_COUNT=5 npm start listen npm start listen + + # To run the Java sample application + cd application-java + ./gradlew run --quiet --args=transact + SIMULATED_FAILURE_COUNT=5 ./gradlew run --quiet --args=listen + ./gradlew run --quiet --args=listen ``` 1. Interrupt the listener process using **Control-C**. diff --git a/off_chain_data/application-java/.gitignore b/off_chain_data/application-java/.gitignore new file mode 100644 index 0000000000..ebecdaa603 --- /dev/null +++ b/off_chain_data/application-java/.gitignore @@ -0,0 +1,12 @@ +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build + +# IntelliJ IDEA files +.idea + +# Files generated by the application at runtime +checkpoint.json +store.log diff --git a/off_chain_data/application-java/app/build.gradle b/off_chain_data/application-java/app/build.gradle new file mode 100644 index 0000000000..ce201541a1 --- /dev/null +++ b/off_chain_data/application-java/app/build.gradle @@ -0,0 +1,38 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +plugins { + id 'application' // Support for building a CLI application in Java. + id 'checkstyle' +} + +repositories { + mavenCentral() + maven { + url 'https://hyperledger-fabric.jfrog.io/artifactory/fabric-maven' + } +} + +dependencies { +// implementation 'com.google.guava:guava:30.1.1-jre' + implementation 'io.grpc:grpc-netty-shaded:1.46.0' + implementation 'org.hyperledger.fabric:fabric-gateway:1.0.2-dev-20220518-1' + implementation 'com.google.code.gson:gson:2.9.0' +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(11) + } +} + +checkstyle { + toolVersion '10.2' +} + +application { + mainClass = 'App' +} diff --git a/off_chain_data/application-java/app/src/main/java/App.java b/off_chain_data/application-java/app/src/main/java/App.java new file mode 100644 index 0000000000..fcde4675a0 --- /dev/null +++ b/off_chain_data/application-java/app/src/main/java/App.java @@ -0,0 +1,78 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import java.io.PrintStream; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import io.grpc.ManagedChannel; + +public final class App { + private static final long SHUTDOWN_TIMEOUT_SECONDS = 3; + private static final Map COMMANDS = Map.ofEntries( + Map.entry("getAllAssets", new GetAllAssets()), + Map.entry("transact", new Transact()), + Map.entry("listen", new Listen()) + ); + + private final List commandNames; + private final PrintStream out = System.out; + + App(final String[] args) { + commandNames = List.of(args); + } + + public void run() throws Exception { + List commands = getCommands(); + ManagedChannel grpcChannel = Connections.newGrpcConnection(); + try { + for (Command command : commands) { + command.run(grpcChannel); + } + } finally { + grpcChannel.shutdownNow().awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } + } + + private List getCommands() { + List commands = commandNames.stream() + .map(name -> { + Command command = COMMANDS.get(name); + if (command == null) { + printUsage(); + throw new IllegalArgumentException("Unknown command: " + name); + } + return command; + }) + .collect(Collectors.toList()); + + if (commands.isEmpty()) { + printUsage(); + throw new IllegalArgumentException("Missing command"); + } + + return commands; + } + + private void printUsage() { + out.println("Arguments: [ ...]"); + out.println("Available commands: " + COMMANDS.keySet()); + } + + public static void main(final String[] args) { + try { + new App(args).run(); + } catch (ExpectedException e) { + e.printStackTrace(System.out); + } catch (Exception e) { + System.err.print("\nUnexpected application error: "); + e.printStackTrace(); + System.exit(1); + } + } +} diff --git a/off_chain_data/application-java/app/src/main/java/Asset.java b/off_chain_data/application-java/app/src/main/java/Asset.java new file mode 100644 index 0000000000..be33e23338 --- /dev/null +++ b/off_chain_data/application-java/app/src/main/java/Asset.java @@ -0,0 +1,57 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Object representation of an asset. Note that the private member variable names don't follow the normal Java naming + * convention as they map to the JSON format expected by the smart contract. + */ +public final class Asset { + private final String ID; // checkstyle:ignore-line:MemberName + private String Color; // checkstyle:ignore-line:MemberName + private int Size; // checkstyle:ignore-line:MemberName + private String Owner; // checkstyle:ignore-line:MemberName + private int AppraisedValue; // checkstyle:ignore-line:MemberName + + public Asset(final String id) { + this.ID = id; + } + + public String getId() { + return ID; + } + + public String getColor() { + return Color; + } + + public void setColor(final String color) { + this.Color = color; + } + + public int getSize() { + return Size; + } + + public void setSize(final int size) { + this.Size = size; + } + + public String getOwner() { + return Owner; + } + + public void setOwner(final String owner) { + this.Owner = owner; + } + + public int getAppraisedValue() { + return AppraisedValue; + } + + public void setAppraisedValue(final int appraisedValue) { + this.AppraisedValue = appraisedValue; + } +} diff --git a/off_chain_data/application-java/app/src/main/java/AssetTransferBasic.java b/off_chain_data/application-java/app/src/main/java/AssetTransferBasic.java new file mode 100644 index 0000000000..7c33251599 --- /dev/null +++ b/off_chain_data/application-java/app/src/main/java/AssetTransferBasic.java @@ -0,0 +1,51 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import java.nio.charset.StandardCharsets; +import java.util.List; + +import com.google.gson.Gson; +import org.hyperledger.fabric.client.CommitException; +import org.hyperledger.fabric.client.CommitStatusException; +import org.hyperledger.fabric.client.Contract; +import org.hyperledger.fabric.client.EndorseException; +import org.hyperledger.fabric.client.SubmitException; + +public final class AssetTransferBasic { + private static final Gson GSON = new Gson(); + private final Contract contract; + + public AssetTransferBasic(final Contract contract) { + this.contract = contract; + } + + public void createAsset(final Asset asset) throws EndorseException, CommitException, SubmitException, CommitStatusException { + contract.submitTransaction( + "CreateAsset", + asset.getId(), + asset.getColor(), + Integer.toString(asset.getSize()), + asset.getOwner(), + Integer.toString(asset.getAppraisedValue()) + ); + } + + public String transferAsset(final String id, final String newOwner) throws EndorseException, CommitException, SubmitException, CommitStatusException { + byte[] resultBytes = contract.submitTransaction("TransferAsset", id, newOwner); + return new String(resultBytes, StandardCharsets.UTF_8); + } + + public void deleteAsset(final String id) throws EndorseException, CommitException, SubmitException, CommitStatusException { + contract.submitTransaction("DeleteAsset", id); + } + + public List getAllAssets() throws EndorseException, CommitException, SubmitException, CommitStatusException { + byte[] resultBytes = contract.submitTransaction("GetAllAssets"); + String resultJson = new String(resultBytes, StandardCharsets.UTF_8); + Asset[] assets = GSON.fromJson(resultJson, Asset[].class); + return assets != null ? List.of(assets) : List.of(); + } +} diff --git a/off_chain_data/application-java/app/src/main/java/BlockProcessor.java b/off_chain_data/application-java/app/src/main/java/BlockProcessor.java new file mode 100644 index 0000000000..2a3fad8ec9 --- /dev/null +++ b/off_chain_data/application-java/app/src/main/java/BlockProcessor.java @@ -0,0 +1,75 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import com.google.protobuf.InvalidProtocolBufferException; +import org.hyperledger.fabric.client.Checkpointer; +import parser.Block; +import parser.Transaction; + +public final class BlockProcessor { + private final Block block; + private final Checkpointer checkpointer; + private final Store store; + + public BlockProcessor(final Block block, final Checkpointer checkpointer, final Store store) { + this.block = block; + this.checkpointer = checkpointer; + this.store = store; + } + + public void process() { + long blockNumber = block.getNumber(); + System.out.println("\nReceived block " + Long.toUnsignedString(blockNumber)); + + try { + List validTransactions = getNewTransactions().stream() + .filter(Transaction::isValid) + .collect(Collectors.toList()); + + for (Transaction transaction : validTransactions) { + new TransactionProcessor(transaction, blockNumber, store).process(); + + String transactionId = transaction.getChannelHeader().getTxId(); + checkpointer.checkpointTransaction(blockNumber, transactionId); + } + + checkpointer.checkpointBlock(blockNumber); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private List getNewTransactions() throws InvalidProtocolBufferException { + List transactions = block.getTransactions(); + + Optional lastTransactionId = checkpointer.getTransactionId(); + if (lastTransactionId.isEmpty()) { + // No previously processed transactions within this block so all are new + return transactions; + } + + List transactionIds = new ArrayList<>(); + for (Transaction transaction : transactions) { + transactionIds.add(transaction.getChannelHeader().getTxId()); + } + + // Ignore transactions up to the last processed transaction ID + int lastProcessedIndex = transactionIds.indexOf(lastTransactionId.get()); + if (lastProcessedIndex < 0) { + throw new IllegalArgumentException("Checkpoint transaction ID " + lastTransactionId + " not found in block " + + Long.toUnsignedString(block.getNumber()) + " containing transactions: " + transactionIds); + } + + return transactions.subList(lastProcessedIndex + 1, transactions.size()); + } +} diff --git a/off_chain_data/application-java/app/src/main/java/Command.java b/off_chain_data/application-java/app/src/main/java/Command.java new file mode 100644 index 0000000000..35032220ba --- /dev/null +++ b/off_chain_data/application-java/app/src/main/java/Command.java @@ -0,0 +1,11 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import io.grpc.Channel; + +public interface Command { + void run(Channel grpcChannel) throws Exception; +} diff --git a/off_chain_data/application-java/app/src/main/java/Connections.java b/off_chain_data/application-java/app/src/main/java/Connections.java new file mode 100644 index 0000000000..ecae0835d4 --- /dev/null +++ b/off_chain_data/application-java/app/src/main/java/Connections.java @@ -0,0 +1,120 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import java.io.IOException; +import java.io.Reader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.InvalidKeyException; +import java.security.PrivateKey; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +import io.grpc.Channel; +import io.grpc.ManagedChannel; +import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts; +import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder; +import org.hyperledger.fabric.client.CallOption; +import org.hyperledger.fabric.client.Gateway; +import org.hyperledger.fabric.client.identity.Identities; +import org.hyperledger.fabric.client.identity.Identity; +import org.hyperledger.fabric.client.identity.Signer; +import org.hyperledger.fabric.client.identity.Signers; +import org.hyperledger.fabric.client.identity.X509Identity; + +public final class Connections { + public static final String CHANNEL_NAME = Utils.getEnvOrDefault("CHANNEL_NAME", "mychannel"); + public static final String CHAINCODE_NAME = Utils.getEnvOrDefault("CHAINCODE_NAME", "basic"); + + private static final String PEER_NAME = "peer0.org1.example.com"; + private static final String MSP_ID = Utils.getEnvOrDefault("MSP_ID", "Org1MSP"); + + // Path to crypto materials. + private static final Path CRYPTO_PATH = Utils.getEnvOrDefault( + "CRYPTO_PATH", + Paths::get, + Paths.get("..", "..", "..", "test-network", "organizations", "peerOrganizations", "org1.example.com") + ); + + // Path to user private key directory. + private static final Path KEY_DIR_PATH = Utils.getEnvOrDefault( + "KEY_DIRECTORY_PATH", + Paths::get, + CRYPTO_PATH.resolve(Paths.get("users", "User1@org1.example.com", "msp", "keystore")) + ); + + // Path to user certificate. + private static final Path CERT_PATH = Utils.getEnvOrDefault( + "CERT_PATH", + Paths::get, + CRYPTO_PATH.resolve(Paths.get("users", "User1@org1.example.com", "msp", "signcerts", "cert.pem")) + ); + + // Path to peer tls certificate. + private static final Path TLS_CERT_PATH = Utils.getEnvOrDefault( + "TLS_CERT_PATH", + Paths::get, + CRYPTO_PATH.resolve(Paths.get("peers", PEER_NAME, "tls", "ca.crt")) + ); + + // Gateway peer end point. + private static final String PEER_ENDPOINT = Utils.getEnvOrDefault("PEER_ENDPOINT", "localhost:7051"); + + // Gateway peer SSL host name override. + private static final String PEER_HOST_ALIAS = Utils.getEnvOrDefault("PEER_HOST_ALIAS", PEER_NAME); + + private static final long EVALUATE_TIMEOUT_SECONDS = 5; + private static final long ENDORSE_TIMEOUT_SECONDS = 15; + private static final long SUBMIT_TIMEOUT_SECONDS = 5; + private static final long COMMIT_STATUS_TIMEOUT_SECONDS = 60; + + private Connections() { + // Private constructor to prevent instantiation + } + + public static ManagedChannel newGrpcConnection() throws IOException, CertificateException { + Reader tlsCertReader = Files.newBufferedReader(TLS_CERT_PATH); + X509Certificate tlsCert = Identities.readX509Certificate(tlsCertReader); + + return NettyChannelBuilder.forTarget(PEER_ENDPOINT) + .sslContext(GrpcSslContexts.forClient().trustManager(tlsCert).build()).overrideAuthority(PEER_HOST_ALIAS) + .build(); + } + + public static Gateway.Builder newGatewayBuilder(final Channel grpcChannel) throws CertificateException, IOException, InvalidKeyException { + return Gateway.newInstance() + .identity(newIdentity()) + .signer(newSigner()) + .connection(grpcChannel) + .evaluateOptions(CallOption.deadlineAfter(EVALUATE_TIMEOUT_SECONDS, TimeUnit.SECONDS)) + .endorseOptions(CallOption.deadlineAfter(ENDORSE_TIMEOUT_SECONDS, TimeUnit.SECONDS)) + .submitOptions(CallOption.deadlineAfter(SUBMIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) + .commitStatusOptions(CallOption.deadlineAfter(COMMIT_STATUS_TIMEOUT_SECONDS, TimeUnit.SECONDS)); + } + + private static Identity newIdentity() throws IOException, CertificateException { + Reader certReader = Files.newBufferedReader(CERT_PATH); + X509Certificate certificate = Identities.readX509Certificate(certReader); + + return new X509Identity(MSP_ID, certificate); + } + + private static Signer newSigner() throws IOException, InvalidKeyException { + Reader keyReader = Files.newBufferedReader(getPrivateKeyPath()); + PrivateKey privateKey = Identities.readPrivateKey(keyReader); + + return Signers.newPrivateKeySigner(privateKey); + } + + private static Path getPrivateKeyPath() throws IOException { + try (Stream keyFiles = Files.list(KEY_DIR_PATH)) { + return keyFiles.findFirst().orElseThrow(); + } + } +} diff --git a/off_chain_data/application-java/app/src/main/java/ExpectedException.java b/off_chain_data/application-java/app/src/main/java/ExpectedException.java new file mode 100644 index 0000000000..c3c5e767f1 --- /dev/null +++ b/off_chain_data/application-java/app/src/main/java/ExpectedException.java @@ -0,0 +1,11 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +public class ExpectedException extends RuntimeException { + public ExpectedException(final String message) { + super(message); + } +} diff --git a/off_chain_data/application-java/app/src/main/java/GetAllAssets.java b/off_chain_data/application-java/app/src/main/java/GetAllAssets.java new file mode 100644 index 0000000000..d1999a76cb --- /dev/null +++ b/off_chain_data/application-java/app/src/main/java/GetAllAssets.java @@ -0,0 +1,40 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.cert.CertificateException; +import java.util.List; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import io.grpc.Channel; +import org.hyperledger.fabric.client.CommitException; +import org.hyperledger.fabric.client.CommitStatusException; +import org.hyperledger.fabric.client.Contract; +import org.hyperledger.fabric.client.EndorseException; +import org.hyperledger.fabric.client.Gateway; +import org.hyperledger.fabric.client.Network; +import org.hyperledger.fabric.client.SubmitException; + +public final class GetAllAssets implements Command { + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + @Override + public void run(final Channel grpcChannel) + throws CertificateException, IOException, InvalidKeyException, EndorseException, CommitException, SubmitException, CommitStatusException { + try (Gateway gateway = Connections.newGatewayBuilder(grpcChannel).connect()) { + Network network = gateway.getNetwork(Connections.CHANNEL_NAME); + Contract contract = network.getContract(Connections.CHAINCODE_NAME); + + AssetTransferBasic smartContract = new AssetTransferBasic(contract); + + List assets = smartContract.getAllAssets(); + String assetsJson = GSON.toJson(assets); + System.out.println(assetsJson); + } + } +} diff --git a/off_chain_data/application-java/app/src/main/java/Listen.java b/off_chain_data/application-java/app/src/main/java/Listen.java new file mode 100644 index 0000000000..7ef3d91b38 --- /dev/null +++ b/off_chain_data/application-java/app/src/main/java/Listen.java @@ -0,0 +1,85 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import java.io.IOException; +import java.io.StringWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.security.InvalidKeyException; +import java.security.cert.CertificateException; +import java.util.List; + +import com.google.gson.Gson; +import io.grpc.Channel; +import org.hyperledger.fabric.client.CloseableIterator; +import org.hyperledger.fabric.client.FileCheckpointer; +import org.hyperledger.fabric.client.Gateway; +import org.hyperledger.fabric.client.Network; +import org.hyperledger.fabric.protos.common.Common; +import parser.Block; +import parser.BlockParser; + +public final class Listen implements Command { + private static final Path CHECKPOINT_FILE = Paths.get(Utils.getEnvOrDefault("CHECKPOINT_FILE", "checkpoint.json")); + private static final Path STORE_FILE = Paths.get(Utils.getEnvOrDefault("STORE_FILE", "store.log")); + private static final int SIMULATED_FAILURE_COUNT = Utils.getEnvOrDefault("SIMULATED_FAILURE_COUNT", Integer::parseUnsignedInt, 0); + + private static final long START_BLOCK = 0L; + private static final Gson GSON = new Gson(); + + private int transactionCount = 0; // Used only to simulate failures + + @Override + public void run(final Channel grpcChannel) + throws CertificateException, IOException, InvalidKeyException { + try (Gateway gateway = Connections.newGatewayBuilder(grpcChannel).connect(); + FileCheckpointer checkpointer = new FileCheckpointer(CHECKPOINT_FILE)) { + Network network = gateway.getNetwork(Connections.CHANNEL_NAME); + + System.out.println("Starting event listening from block " + Long.toUnsignedString(checkpointer.getBlockNumber().orElse(START_BLOCK))); + System.out.println(checkpointer.getTransactionId() + .map(transactionId -> "Last processed transaction ID within block: " + transactionId) + .orElse("No last processed transaction ID")); + if (SIMULATED_FAILURE_COUNT > 0) { + System.out.println("Simulating a write failure every " + SIMULATED_FAILURE_COUNT + " transactions"); + } + + try (CloseableIterator blocks = network.newBlockEventsRequest() + .startBlock(START_BLOCK) // Used only if there is no checkpoint block number + .checkpoint(checkpointer) + .build() + .getEvents()) { + blocks.forEachRemaining(blockProto -> { + Block block = BlockParser.parseBlock(blockProto); + BlockProcessor processor = new BlockProcessor(block, checkpointer, this::applyWritesToOffChainStore); + processor.process(); + }); + } + } + } + + private void applyWritesToOffChainStore(final long blockNumber, final String transactionId, final List writes) throws IOException { + simulateFailureIfRequired(); + + try (StringWriter writer = new StringWriter()) { + for (Write write : writes) { + GSON.toJson(write, writer); + writer.append('\n'); + } + + Files.writeString(STORE_FILE, writer.toString(), StandardOpenOption.CREATE, StandardOpenOption.APPEND); + } + } + + private void simulateFailureIfRequired() { + if (SIMULATED_FAILURE_COUNT > 0 && transactionCount++ >= SIMULATED_FAILURE_COUNT) { + transactionCount = 0; + throw new ExpectedException("Simulated write failure"); + } + } +} diff --git a/off_chain_data/application-java/app/src/main/java/Store.java b/off_chain_data/application-java/app/src/main/java/Store.java new file mode 100644 index 0000000000..9d56119569 --- /dev/null +++ b/off_chain_data/application-java/app/src/main/java/Store.java @@ -0,0 +1,13 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import java.io.IOException; +import java.util.List; + +@FunctionalInterface +public interface Store { + void store(long blockNumber, String transactionId, List writes) throws IOException; +} diff --git a/off_chain_data/application-java/app/src/main/java/Transact.java b/off_chain_data/application-java/app/src/main/java/Transact.java new file mode 100644 index 0000000000..8e3ff54c69 --- /dev/null +++ b/off_chain_data/application-java/app/src/main/java/Transact.java @@ -0,0 +1,30 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.cert.CertificateException; + +import io.grpc.Channel; +import org.hyperledger.fabric.client.Contract; +import org.hyperledger.fabric.client.Gateway; +import org.hyperledger.fabric.client.Network; + +public final class Transact implements Command { + @Override + public void run(final Channel grpcChannel) + throws CertificateException, IOException, InvalidKeyException { + try (Gateway gateway = Connections.newGatewayBuilder(grpcChannel).connect()) { + Network network = gateway.getNetwork(Connections.CHANNEL_NAME); + Contract contract = network.getContract(Connections.CHAINCODE_NAME); + + AssetTransferBasic smartContract = new AssetTransferBasic(contract); + + TransactApp app = new TransactApp(smartContract); + app.run(); + } + } +} diff --git a/off_chain_data/application-java/app/src/main/java/TransactApp.java b/off_chain_data/application-java/app/src/main/java/TransactApp.java new file mode 100644 index 0000000000..54dfdeb02d --- /dev/null +++ b/off_chain_data/application-java/app/src/main/java/TransactApp.java @@ -0,0 +1,77 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.stream.Stream; + +import org.hyperledger.fabric.client.CommitException; +import org.hyperledger.fabric.client.CommitStatusException; +import org.hyperledger.fabric.client.EndorseException; +import org.hyperledger.fabric.client.SubmitException; + +public final class TransactApp { + private static final List COLORS = List.of("red", "green", "blue"); + private static final List OWNERS = List.of("alice", "bob", "charlie"); + private static final int MAX_INITIAL_VALUE = 1000; + private static final int MAX_INITIAL_SIZE = 10; + + private final AssetTransferBasic smartContract; + private final int batchSize = 10; + + public TransactApp(final AssetTransferBasic smartContract) { + this.smartContract = smartContract; + } + + public void run() { + CompletableFuture[] futures = Stream.generate(this::newCompletableFuture) + .limit(batchSize) + .toArray(CompletableFuture[]::new); + CompletableFuture allComplete = CompletableFuture.allOf(futures); + allComplete.join(); + } + + private CompletableFuture newCompletableFuture() { + return CompletableFuture.runAsync(() -> { + try { + transact(); + } catch (Exception e) { + throw new CompletionException(e); + } + }); + } + + private void transact() throws EndorseException, CommitException, SubmitException, CommitStatusException { + Asset asset = newAsset(); + + smartContract.createAsset(asset); + System.out.println("Created new asset " + asset.getId()); + + // Transfer randomly 1 in 2 assets to a new owner. + if (Utils.randomInt(2) == 0) { // checkstyle:ignore-line:MagicNumber + String newOwner = Utils.differentElement(OWNERS, asset.getOwner()); + String oldOwner = smartContract.transferAsset(asset.getId(), newOwner); + System.out.println("Transferred asset " + asset.getId() + " from " + oldOwner + " to " + newOwner); + } + + // Delete randomly 1 in 4 created assets. + if (Utils.randomInt(4) == 0) { // checkstyle:ignore-line:MagicNumber + smartContract.deleteAsset(asset.getId()); + System.out.println("Deleted asset " + asset.getId()); + } + } + + private Asset newAsset() { + Asset asset = new Asset(UUID.randomUUID().toString()); + asset.setColor(Utils.randomElement(COLORS)); + asset.setSize(Utils.randomInt(MAX_INITIAL_SIZE) + 1); + asset.setOwner(Utils.randomElement(OWNERS)); + asset.setAppraisedValue(Utils.randomInt(MAX_INITIAL_VALUE) + 1); + return asset; + } +} diff --git a/off_chain_data/application-java/app/src/main/java/TransactionProcessor.java b/off_chain_data/application-java/app/src/main/java/TransactionProcessor.java new file mode 100644 index 0000000000..379f616be7 --- /dev/null +++ b/off_chain_data/application-java/app/src/main/java/TransactionProcessor.java @@ -0,0 +1,71 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import com.google.protobuf.InvalidProtocolBufferException; +import parser.NamespaceReadWriteSet; +import parser.Transaction; + +public final class TransactionProcessor { + // Typically we should ignore read/write sets that apply to system chaincode namespaces. + private static final Set SYSTEM_CHAINCODE_NAMES = Set.of( + "_lifecycle", + "cscc", + "escc", + "lscc", + "qscc", + "vscc" + ); + + private final long blockNumber; + private final Transaction transaction; + private final Store store; + + public TransactionProcessor(final Transaction transaction, final long blockNumber, final Store store) { + this.blockNumber = blockNumber; + this.transaction = transaction; + this.store = store; + } + + private static boolean isSystemChaincode(final String chaincodeName) { + return SYSTEM_CHAINCODE_NAMES.contains(chaincodeName); + } + + public void process() throws IOException { + String transactionId = transaction.getChannelHeader().getTxId(); + + List writes = getWrites(); + if (writes.isEmpty()) { + System.out.println("Skipping read-only or system transaction " + transactionId); + return; + } + + System.out.println("Process transaction " + transactionId); + store.store(blockNumber, transactionId, writes); + } + + private List getWrites() throws InvalidProtocolBufferException { + String channelName = transaction.getChannelHeader().getChannelId(); + + List writes = new ArrayList<>(); + for (NamespaceReadWriteSet readWriteSet : transaction.getNamespaceReadWriteSets()) { + String namespace = readWriteSet.getNamespace(); + if (isSystemChaincode(namespace)) { + continue; + } + + readWriteSet.getReadWriteSet().getWritesList().stream() + .map(write -> new Write(channelName, namespace, write)) + .forEach(writes::add); + } + + return writes; + } +} diff --git a/off_chain_data/application-java/app/src/main/java/Utils.java b/off_chain_data/application-java/app/src/main/java/Utils.java new file mode 100644 index 0000000000..a4e5c5bf42 --- /dev/null +++ b/off_chain_data/application-java/app/src/main/java/Utils.java @@ -0,0 +1,58 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import java.util.List; +import java.util.Random; +import java.util.function.Function; +import java.util.stream.Collectors; + +public final class Utils { + private static final Random RANDOM = new Random(); + + public static String getEnvOrDefault(final String name, final String defaultValue) { + return getEnvOrDefault(name, Function.identity(), defaultValue); + } + + public static T getEnvOrDefault(final String name, final Function map, final T defaultValue) { + String result = System.getenv(name); + return result != null ? map.apply(result) : defaultValue; + } + + /** + * Generate a random integer in the range 0 to max - 1. + * @param max Maximum value (exclusive). + * @return A random number. + */ + public static int randomInt(final int max) { + return RANDOM.nextInt(max); + } + + /** + * Pick a random element from a list. + * @param values Candidate elements. + * @return A randomly selected value. + * @param Element type. + */ + public static T randomElement(final List values) { + return values.get(randomInt(values.size())); + } + + /** + * Pick a random element from an array, excluding the current value. + * @param values Candidate elements. + * @param currentValue Value to avoid. + * @return A random value. + * @param Element type. + */ + public static T differentElement(final List values, final T currentValue) { + List candidateValues = values.stream() + .filter(value -> !currentValue.equals(value)) + .collect(Collectors.toList()); + return randomElement(candidateValues); + } + + private Utils() { } +} diff --git a/off_chain_data/application-java/app/src/main/java/Write.java b/off_chain_data/application-java/app/src/main/java/Write.java new file mode 100644 index 0000000000..73f54895c5 --- /dev/null +++ b/off_chain_data/application-java/app/src/main/java/Write.java @@ -0,0 +1,68 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import java.nio.charset.StandardCharsets; + +import org.hyperledger.fabric.protos.ledger.rwset.kvrwset.KvRwset; + +/** + * Description of a ledger write that can be applied to an off-chain data store. + */ +public final class Write { + private final String channelName; + private final String namespace; + private final String key; + private final boolean isDelete; + private final String value; // Store as String for readability when serialized to JSON. + + public Write(final String channelName, final String namespace, final KvRwset.KVWrite write) { + this.channelName = channelName; + this.namespace = namespace; + this.key = write.getKey(); + this.isDelete = write.getIsDelete(); + this.value = write.getValue().toString(StandardCharsets.UTF_8); + } + + /** + * Channel whose ledger is being updated. + * @return A channel name. + */ + public String getChannelName() { + return channelName; + } + + /** + * Key name within the ledger namespace. + * @return A ledger key. + */ + public String getKey() { + return key; + } + + /** + * Whether the key and associated value are being deleted. + * @return {@code true} if the ledger key is being deleted; otherwise {@code false}. + */ + public boolean isDelete() { + return isDelete; + } + + /** + * Namespace within the ledger. + * @return A ledger namespace. + */ + public String getNamespace() { + return namespace; + } + + /** + * If {@link #isDelete()}` is {@code false}, the value written to the key; otherwise ignored. + * @return A ledger value. + */ + public byte[] getValue() { + return value.getBytes(StandardCharsets.UTF_8); + } +} diff --git a/off_chain_data/application-java/app/src/main/java/parser/Block.java b/off_chain_data/application-java/app/src/main/java/parser/Block.java new file mode 100644 index 0000000000..2aa0269c0f --- /dev/null +++ b/off_chain_data/application-java/app/src/main/java/parser/Block.java @@ -0,0 +1,18 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package parser; + +import java.util.List; + +import com.google.protobuf.InvalidProtocolBufferException; +import org.hyperledger.fabric.protos.common.Common; + +public interface Block { + long getNumber(); + List getTransactions() throws InvalidProtocolBufferException; + Common.Block toProto(); +} diff --git a/off_chain_data/application-java/app/src/main/java/parser/BlockParser.java b/off_chain_data/application-java/app/src/main/java/parser/BlockParser.java new file mode 100644 index 0000000000..bdbb609d79 --- /dev/null +++ b/off_chain_data/application-java/app/src/main/java/parser/BlockParser.java @@ -0,0 +1,17 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package parser; + +import org.hyperledger.fabric.protos.common.Common; + +public final class BlockParser { + public static Block parseBlock(final Common.Block block) { + return new ParsedBlock(block); + } + + private BlockParser() { } +} diff --git a/off_chain_data/application-java/app/src/main/java/parser/NamespaceReadWriteSet.java b/off_chain_data/application-java/app/src/main/java/parser/NamespaceReadWriteSet.java new file mode 100644 index 0000000000..f46395ef0e --- /dev/null +++ b/off_chain_data/application-java/app/src/main/java/parser/NamespaceReadWriteSet.java @@ -0,0 +1,17 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package parser; + +import com.google.protobuf.InvalidProtocolBufferException; +import org.hyperledger.fabric.protos.ledger.rwset.Rwset; +import org.hyperledger.fabric.protos.ledger.rwset.kvrwset.KvRwset; + +public interface NamespaceReadWriteSet { + String getNamespace(); + KvRwset.KVRWSet getReadWriteSet() throws InvalidProtocolBufferException; + Rwset.NsReadWriteSet toProto(); +} diff --git a/off_chain_data/application-java/app/src/main/java/parser/ParsedBlock.java b/off_chain_data/application-java/app/src/main/java/parser/ParsedBlock.java new file mode 100644 index 0000000000..0d050620a7 --- /dev/null +++ b/off_chain_data/application-java/app/src/main/java/parser/ParsedBlock.java @@ -0,0 +1,75 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package parser; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import org.hyperledger.fabric.protos.common.Common; +import org.hyperledger.fabric.protos.peer.TransactionPackage; + +class ParsedBlock implements Block { + private final Common.Block block; + private final AtomicReference> cachedTransactions = new AtomicReference<>(); + + ParsedBlock(final Common.Block block) { + this.block = block; + } + + @Override + public long getNumber() { + return block.getHeader().getNumber(); + } + + @Override + public List getTransactions() throws InvalidProtocolBufferException { + return Utils.getCachedProto(cachedTransactions, () -> { + List validationCodes = getTransactionValidationCodes(); + List payloads = getPayloads(); + + List transactions = new ArrayList<>(); + for (int i = 0; i < payloads.size(); i++) { + ParsedPayload payload = new ParsedPayload(payloads.get(i), validationCodes.get(i)); + if (payload.isEndorserTransaction()) { + transactions.add(new ParsedTransaction(payload)); + } + } + + return transactions; + }); + } + + @Override + public Common.Block toProto() { + return block; + } + + private List getPayloads() throws InvalidProtocolBufferException { + List payloads = new ArrayList<>(); + + for (ByteString envelopeBytes : block.getData().getDataList()) { + Common.Envelope envelope = Common.Envelope.parseFrom(envelopeBytes); + Common.Payload payload = Common.Payload.parseFrom(envelope.getPayload()); + payloads.add(payload); + } + + return payloads; + } + + private List getTransactionValidationCodes() { + ByteString transactionsFilter = block.getMetadata().getMetadataList().get(Common.BlockMetadataIndex.TRANSACTIONS_FILTER.getNumber()); + return StreamSupport.stream(transactionsFilter.spliterator(), false) + .map(Byte::intValue) + .map(TransactionPackage.TxValidationCode::forNumber) + .collect(Collectors.toList()); + } +} diff --git a/off_chain_data/application-java/app/src/main/java/parser/ParsedPayload.java b/off_chain_data/application-java/app/src/main/java/parser/ParsedPayload.java new file mode 100644 index 0000000000..3bfab83c01 --- /dev/null +++ b/off_chain_data/application-java/app/src/main/java/parser/ParsedPayload.java @@ -0,0 +1,44 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package parser; + +import java.util.concurrent.atomic.AtomicReference; + +import com.google.protobuf.InvalidProtocolBufferException; +import org.hyperledger.fabric.protos.common.Common; +import org.hyperledger.fabric.protos.peer.TransactionPackage; + +class ParsedPayload { + private final Common.Payload payload; + private final TransactionPackage.TxValidationCode statusCode; + private final AtomicReference cachedChannelHeader = new AtomicReference<>(); + + ParsedPayload(final Common.Payload payload, final TransactionPackage.TxValidationCode statusCode) { + this.payload = payload; + this.statusCode = statusCode; + } + + public Common.ChannelHeader getChannelHeader() throws InvalidProtocolBufferException { + return Utils.getCachedProto(cachedChannelHeader, () -> Common.ChannelHeader.parseFrom(payload.getHeader().getChannelHeader())); + } + + public TransactionPackage.TxValidationCode getValidationCode() { + return statusCode; + } + + public boolean isValid() { + return statusCode == TransactionPackage.TxValidationCode.VALID; + } + + public Common.Payload toProto() { + return payload; + } + + public boolean isEndorserTransaction() throws InvalidProtocolBufferException { + return getChannelHeader().getType() == Common.HeaderType.ENDORSER_TRANSACTION_VALUE; + } +} diff --git a/off_chain_data/application-java/app/src/main/java/parser/ParsedReadWriteSet.java b/off_chain_data/application-java/app/src/main/java/parser/ParsedReadWriteSet.java new file mode 100644 index 0000000000..38d0511163 --- /dev/null +++ b/off_chain_data/application-java/app/src/main/java/parser/ParsedReadWriteSet.java @@ -0,0 +1,50 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package parser; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import com.google.protobuf.InvalidProtocolBufferException; +import org.hyperledger.fabric.protos.ledger.rwset.Rwset; +import org.hyperledger.fabric.protos.ledger.rwset.kvrwset.KvRwset; + +class ParsedReadWriteSet implements NamespaceReadWriteSet { + private final Rwset.NsReadWriteSet readWriteSet; + private final AtomicReference cachedReadWriteSet = new AtomicReference<>(); + + static List fromTxReadWriteSet(final Rwset.TxReadWriteSet readWriteSet) { + Rwset.TxReadWriteSet.DataModel dataModel = readWriteSet.getDataModel(); + if (dataModel != Rwset.TxReadWriteSet.DataModel.KV) { + throw new IllegalArgumentException("Unexpected read/write set data model: " + dataModel.name()); + } + + return readWriteSet.getNsRwsetList().stream() + .map(ParsedReadWriteSet::new) + .collect(Collectors.toList()); + } + + ParsedReadWriteSet(final Rwset.NsReadWriteSet readWriteSet) { + this.readWriteSet = readWriteSet; + } + + @Override + public String getNamespace() { + return readWriteSet.getNamespace(); + } + + @Override + public KvRwset.KVRWSet getReadWriteSet() throws InvalidProtocolBufferException { + return Utils.getCachedProto(cachedReadWriteSet, () -> KvRwset.KVRWSet.parseFrom(readWriteSet.getRwset())); + } + + @Override + public Rwset.NsReadWriteSet toProto() { + return readWriteSet; + } +} diff --git a/off_chain_data/application-java/app/src/main/java/parser/ParsedTransaction.java b/off_chain_data/application-java/app/src/main/java/parser/ParsedTransaction.java new file mode 100644 index 0000000000..351ee2f2ef --- /dev/null +++ b/off_chain_data/application-java/app/src/main/java/parser/ParsedTransaction.java @@ -0,0 +1,69 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package parser; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import com.google.protobuf.InvalidProtocolBufferException; +import org.hyperledger.fabric.protos.common.Common; +import org.hyperledger.fabric.protos.peer.TransactionPackage; + +final class ParsedTransaction implements Transaction { + private final ParsedPayload payload; + private final AtomicReference> cachedNamespaceReadWriteSets = new AtomicReference<>(); + + ParsedTransaction(final ParsedPayload payload) { + this.payload = payload; + } + + @Override + public Common.ChannelHeader getChannelHeader() throws InvalidProtocolBufferException { + return payload.getChannelHeader(); + } + + @Override + public TransactionPackage.TxValidationCode getValidationCode() { + return payload.getValidationCode(); + } + + @Override + public boolean isValid() { + return payload.isValid(); + } + + @Override + public List getNamespaceReadWriteSets() throws InvalidProtocolBufferException { + return Utils.getCachedProto(cachedNamespaceReadWriteSets, () -> new ArrayList<>(getReadWriteSets())); + } + + @Override + public Common.Payload toProto() { + return payload.toProto(); + } + + private List getReadWriteSets() throws InvalidProtocolBufferException { + List results = new ArrayList<>(); + for (ParsedTransactionAction action : getTransactionActions()) { + results.addAll(action.getReadWriteSets()); + } + + return results; + } + + private List getTransactionActions() throws InvalidProtocolBufferException { + return getTransaction().getActionsList().stream() + .map(ParsedTransactionAction::new) + .collect(Collectors.toList()); + } + + private TransactionPackage.Transaction getTransaction() throws InvalidProtocolBufferException { + return TransactionPackage.Transaction.parseFrom(payload.toProto().getData()); + } +} diff --git a/off_chain_data/application-java/app/src/main/java/parser/ParsedTransactionAction.java b/off_chain_data/application-java/app/src/main/java/parser/ParsedTransactionAction.java new file mode 100644 index 0000000000..11f0801ce6 --- /dev/null +++ b/off_chain_data/application-java/app/src/main/java/parser/ParsedTransactionAction.java @@ -0,0 +1,47 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package parser; + +import java.util.List; + +import com.google.protobuf.InvalidProtocolBufferException; +import org.hyperledger.fabric.protos.ledger.rwset.Rwset; +import org.hyperledger.fabric.protos.peer.ProposalPackage; +import org.hyperledger.fabric.protos.peer.ProposalResponsePackage; +import org.hyperledger.fabric.protos.peer.TransactionPackage; + +final class ParsedTransactionAction { + private final TransactionPackage.TransactionAction transactionAction; + + ParsedTransactionAction(final TransactionPackage.TransactionAction transactionAction) { + this.transactionAction = transactionAction; + } + + public List getReadWriteSets() throws InvalidProtocolBufferException { + return ParsedReadWriteSet.fromTxReadWriteSet(getTxReadWriteSet()); + } + + private Rwset.TxReadWriteSet getTxReadWriteSet() throws InvalidProtocolBufferException { + return Rwset.TxReadWriteSet.parseFrom(getChaincodeAction().getResults()); + } + + private ProposalPackage.ChaincodeAction getChaincodeAction() throws InvalidProtocolBufferException { + return ProposalPackage.ChaincodeAction.parseFrom(getProposalResponsePayload().getExtension()); + } + + private ProposalResponsePackage.ProposalResponsePayload getProposalResponsePayload() throws InvalidProtocolBufferException { + return ProposalResponsePackage.ProposalResponsePayload.parseFrom(getChaincodeActionPayload().getAction().getProposalResponsePayload()); + } + + private TransactionPackage.ChaincodeActionPayload getChaincodeActionPayload() throws InvalidProtocolBufferException { + return TransactionPackage.ChaincodeActionPayload.parseFrom(transactionAction.getPayload()); + } + + public TransactionPackage.TransactionAction toProto() { + return transactionAction; + } +} diff --git a/off_chain_data/application-java/app/src/main/java/parser/Transaction.java b/off_chain_data/application-java/app/src/main/java/parser/Transaction.java new file mode 100644 index 0000000000..b5a9968923 --- /dev/null +++ b/off_chain_data/application-java/app/src/main/java/parser/Transaction.java @@ -0,0 +1,21 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package parser; + +import java.util.List; + +import com.google.protobuf.InvalidProtocolBufferException; +import org.hyperledger.fabric.protos.common.Common; +import org.hyperledger.fabric.protos.peer.TransactionPackage; + +public interface Transaction { + Common.ChannelHeader getChannelHeader() throws InvalidProtocolBufferException; + TransactionPackage.TxValidationCode getValidationCode(); + boolean isValid(); + List getNamespaceReadWriteSets() throws InvalidProtocolBufferException; + Common.Payload toProto(); +} diff --git a/off_chain_data/application-java/app/src/main/java/parser/Utils.java b/off_chain_data/application-java/app/src/main/java/parser/Utils.java new file mode 100644 index 0000000000..0ad0d1e5df --- /dev/null +++ b/off_chain_data/application-java/app/src/main/java/parser/Utils.java @@ -0,0 +1,51 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package parser; + +import java.util.concurrent.Callable; +import java.util.concurrent.CompletionException; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +import com.google.protobuf.InvalidProtocolBufferException; + +final class Utils { + public interface ProtoCall extends Callable { + @Override + T call() throws InvalidProtocolBufferException; + } + + public static T getCachedProto(final AtomicReference cache, final ProtoCall call) throws InvalidProtocolBufferException { + try { + return cache.updateAndGet(current -> current != null ? current : asSupplier(call).get()); + } catch (CompletionException e) { + Throwable cause = e.getCause(); + if (cause instanceof InvalidProtocolBufferException) { + throw (InvalidProtocolBufferException) cause; + } + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } + if (cause instanceof Error) { + throw (Error) cause; + } + throw e; + } + } + + public static Supplier asSupplier(final Callable call) { + return () -> { + try { + return call.call(); + } catch (Exception e) { + throw new CompletionException(e); + } + }; + } + + private Utils() { } +} diff --git a/off_chain_data/application-java/config/checkstyle/checkstyle.xml b/off_chain_data/application-java/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000000..cb1f70a017 --- /dev/null +++ b/off_chain_data/application-java/config/checkstyle/checkstyle.xml @@ -0,0 +1,197 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/off_chain_data/application-java/config/checkstyle/java-copyright-header.txt b/off_chain_data/application-java/config/checkstyle/java-copyright-header.txt new file mode 100644 index 0000000000..0583f45121 --- /dev/null +++ b/off_chain_data/application-java/config/checkstyle/java-copyright-header.txt @@ -0,0 +1,5 @@ +^/\*$ +^ \* Copyright IBM Corp\. All Rights Reserved\.$ +^ \*$ +^ \* SPDX-License-Identifier: Apache-2\.0$ +^ \*/$ diff --git a/off_chain_data/application-java/gradle/wrapper/gradle-wrapper.jar b/off_chain_data/application-java/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..41d9927a4d Binary files /dev/null and b/off_chain_data/application-java/gradle/wrapper/gradle-wrapper.jar differ diff --git a/off_chain_data/application-java/gradle/wrapper/gradle-wrapper.properties b/off_chain_data/application-java/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..aa991fceae --- /dev/null +++ b/off_chain_data/application-java/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/off_chain_data/application-java/gradlew b/off_chain_data/application-java/gradlew new file mode 100755 index 0000000000..1b6c787337 --- /dev/null +++ b/off_chain_data/application-java/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/off_chain_data/application-java/gradlew.bat b/off_chain_data/application-java/gradlew.bat new file mode 100644 index 0000000000..ac1b06f938 --- /dev/null +++ b/off_chain_data/application-java/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/off_chain_data/application-java/settings.gradle b/off_chain_data/application-java/settings.gradle new file mode 100644 index 0000000000..5bbda58592 --- /dev/null +++ b/off_chain_data/application-java/settings.gradle @@ -0,0 +1,8 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +rootProject.name = 'off-chain-data' +include('app') diff --git a/off_chain_data/application-typescript/src/blockParser.ts b/off_chain_data/application-typescript/src/blockParser.ts index 1bef980201..6ea18f27f9 100644 --- a/off_chain_data/application-typescript/src/blockParser.ts +++ b/off_chain_data/application-typescript/src/blockParser.ts @@ -68,15 +68,13 @@ function parsePayload(payload: common.Payload, statusCode: number): Payload { return { getChannelHeader: cachedChannelHeader, - getEndorserTransaction: cache( - () => { - if (!isEndorserTransaction()) { - throw new Error(`Unexpected payload type: ${cachedChannelHeader().getType()}`); - } - const transaction = peer.Transaction.deserializeBinary(payload.getData_asU8()); - return parseEndorserTransaction(transaction); + getEndorserTransaction: () => { + if (!isEndorserTransaction()) { + throw new Error(`Unexpected payload type: ${cachedChannelHeader().getType()}`); } - ), + const transaction = peer.Transaction.deserializeBinary(payload.getData_asU8()); + return parseEndorserTransaction(transaction); + }, getTransactionValidationCode: () => statusCode, isEndorserTransaction, isValid: () => statusCode === peer.TxValidationCode.VALID, diff --git a/off_chain_data/application-typescript/src/contract.ts b/off_chain_data/application-typescript/src/contract.ts index b14081d15d..b4cfa227c9 100644 --- a/off_chain_data/application-typescript/src/contract.ts +++ b/off_chain_data/application-typescript/src/contract.ts @@ -14,7 +14,7 @@ export interface Asset { Color: string; Size: number; Owner: string; - AppriasedValue: number; + AppraisedValue: number; } export class AssetTransferBasic { @@ -26,7 +26,7 @@ export class AssetTransferBasic { async createAsset(asset: Asset): Promise { await this.#contract.submit('CreateAsset', { - arguments: [asset.ID, asset.Color, String(asset.Size), asset.Owner, String(asset.AppriasedValue)], + arguments: [asset.ID, asset.Color, String(asset.Size), asset.Owner, String(asset.AppraisedValue)], }); } diff --git a/off_chain_data/application-typescript/src/listen.ts b/off_chain_data/application-typescript/src/listen.ts index 10dff94fc2..79e49f658a 100644 --- a/off_chain_data/application-typescript/src/listen.ts +++ b/off_chain_data/application-typescript/src/listen.ts @@ -6,11 +6,10 @@ import { Client } from '@grpc/grpc-js'; import { Checkpointer, checkpointers, connect } from '@hyperledger/fabric-gateway'; -import { ledger } from '@hyperledger/fabric-protos'; import { promises as fs } from 'fs'; import * as path from 'path'; import { TextDecoder } from 'util'; -import { Block, NamespaceReadWriteSet, parseBlock, Transaction } from './blockParser'; +import { Block, parseBlock, Transaction } from './blockParser'; import { channelName, newConnectOptions } from './connect'; import { ExpectedError } from './expectedError'; @@ -37,14 +36,13 @@ let transactionCount = 0; // Used only to simulate failures * Apply writes for a given transaction to off-chain data store, ideally in a single operation for fault tolerance. * @param data Transaction data. */ -type StoreStrategy = (data: LedgerUpdate) => Promise; +type Store = (data: LedgerUpdate) => Promise; /** * Ledger update made by a specific transaction. */ interface LedgerUpdate { blockNumber: bigint; - channelName: string; transactionId: string; writes: Write[]; } @@ -55,12 +53,12 @@ interface LedgerUpdate { interface Write { /** Channel whose ledger is being updated. */ channelName: string; + /** Namespace within the ledger. */ + namespace: string; /** Key name within the ledger namespace. */ key: string; /** Whether the key and associated value are being deleted. */ isDelete: boolean; - /** Namespace within the ledger. */ - namespace: string; /** If `isDelete` is false, the value written to the key; otherwise ignored. */ value: Uint8Array; } @@ -69,7 +67,7 @@ interface Write { * Apply writes for a given transaction to off-chain data store, ideally in a single operation for fault tolerance. * This implementation just writes to a file. */ -const applyWritesToOffChainStore: StoreStrategy = async (data) => { +const applyWritesToOffChainStore: Store = async (data) => { simulateFailureIfRequired(); const writes = data.writes @@ -120,13 +118,13 @@ export async function main(client: Client): Promise { interface BlockProcessorOptions { block: Block; checkpointer: Checkpointer; - store: StoreStrategy; + store: Store; } class BlockProcessor { readonly #block: Block; readonly #checkpointer: Checkpointer; - readonly #store: StoreStrategy; + readonly #store: Store; constructor(options: Readonly) { this.#block = options.block; @@ -135,16 +133,16 @@ class BlockProcessor { } async process(): Promise { - console.log(`\nReceived block ${this.#block.getNumber()}`); - const blockNumber = this.#block.getNumber(); + + console.log(`\nReceived block ${blockNumber}`); + const validTransactions = this.#getNewTransactions() .filter(transaction => transaction.isValid()); for (const transaction of validTransactions) { const transactionProcessor = new TransactionProcessor({ blockNumber, - channelName: transaction.getChannelHeader().getChannelId(), store: this.#store, transaction, }); @@ -179,59 +177,58 @@ class BlockProcessor { interface TransactionProcessorOptions { blockNumber: bigint; - channelName: string; - store: StoreStrategy; + store: Store; transaction: Transaction; } class TransactionProcessor { readonly #blockNumber: bigint; - readonly #channelName: string; - readonly #id: string; - readonly #namespaceReadWriteSets: NamespaceReadWriteSet[]; - readonly #store: StoreStrategy; + readonly #transaction: Transaction; + readonly #store: Store; constructor(options: Readonly) { this.#blockNumber = options.blockNumber; - this.#channelName = options.channelName; - this.#id = options.transaction.getChannelHeader().getTxId(); - this.#namespaceReadWriteSets = options.transaction.getNamespaceReadWriteSets() - .filter(readWriteSet => !isSystemChaincode(readWriteSet.getNamespace())); + this.#transaction = options.transaction; this.#store = options.store; } async process(): Promise { + const channelHeader = this.#transaction.getChannelHeader(); + const transactionId = channelHeader.getTxId(); + const writes = this.#getWrites(); if (writes.length === 0) { - console.log(`Skipping read-only or system transaction ${this.#id}`); + console.log(`Skipping read-only or system transaction ${transactionId}`); return; } - console.log(`Process transaction ${this.#id}`); + console.log(`Process transaction ${transactionId}`); await this.#store({ blockNumber: this.#blockNumber, - channelName: this.#channelName, - transactionId: this.#id, - writes: this.#getWrites(), + transactionId, + writes, }); } #getWrites(): Write[] { - return this.#namespaceReadWriteSets.flatMap(readWriteSet => { - const namespace = readWriteSet.getNamespace(); - return readWriteSet.getReadWriteSet().getWritesList().map(write => this.#newWrite(namespace, write)); - }); - } - - #newWrite(namespace: string, write: ledger.rwset.kvrwset.KVWrite): Write { - return { - channelName: this.#channelName, - key: write.getKey(), - isDelete: write.getIsDelete(), - namespace, - value: write.getValue_asU8(), - }; + const channelName = this.#transaction.getChannelHeader().getChannelId(); + + return this.#transaction.getNamespaceReadWriteSets() + .filter(readWriteSet => !isSystemChaincode(readWriteSet.getNamespace())) + .flatMap(readWriteSet => { + const namespace = readWriteSet.getNamespace(); + + return readWriteSet.getReadWriteSet().getWritesList().map(write => { + return { + channelName, + namespace, + key: write.getKey(), + isDelete: write.getIsDelete(), + value: write.getValue_asU8(), + }; + }); + }); } } diff --git a/off_chain_data/application-typescript/src/transact.ts b/off_chain_data/application-typescript/src/transact.ts index 16c5a86d73..58880fd0e5 100644 --- a/off_chain_data/application-typescript/src/transact.ts +++ b/off_chain_data/application-typescript/src/transact.ts @@ -71,7 +71,7 @@ class TransactApp { Color: randomElement(colors), Size: randomInt(maxInitialSize) + 1, Owner: randomElement(owners), - AppriasedValue: randomInt(maxInitialValue) + 1, + AppraisedValue: randomInt(maxInitialValue) + 1, }; } }