From 86a88259fccd4ae903a7704b1e6a2fa571557e22 Mon Sep 17 00:00:00 2001 From: "Mark S. Lewis" Date: Tue, 22 Oct 2024 10:48:46 +0100 Subject: [PATCH] Update app dev tutorial to include Go and Java (#5042) This change uses (linked) tabs to display code snippets and language-specific commands for the user's desired programming language. In order to use the sphinx-tabs extension, the version of Python used to build the docs had to be updated. Since the `n42org/tox` Docker image used had a very old Python version baked in, and there were no more recent versions available, the `make docs` target now uses the official Python image. In addition to providing a much more current Python environment, this image natively supports arm64 in addition to amd64, allowing docs to be built on Apple Silicon Macs. Signed-off-by: Mark S. Lewis --- Makefile | 4 +- docs/requirements.txt | 57 +-- docs/source/conf.py | 75 ++-- docs/source/write_first_app.rst | 729 +++++++++++++++++++++++++------- tox.ini | 8 +- 5 files changed, 653 insertions(+), 220 deletions(-) diff --git a/Makefile b/Makefile index 8990a9e0680..354b1232e62 100644 --- a/Makefile +++ b/Makefile @@ -345,8 +345,8 @@ spaces: @scripts/check_file_name_spaces.sh .PHONY: docs -docs: - @docker run --rm -v $$(pwd):/docs n42org/tox:3.4.0 sh -c 'cd /docs && tox -e docs' +docs: # Builds the documentation in html format + @docker run --rm -v $$(pwd):/docs python:3.12-slim sh -c 'pip install --no-input tox && cd /docs && tox -e docs' .PHONY: ccaasbuilder-clean ccaasbuilder-clean/%: diff --git a/docs/requirements.txt b/docs/requirements.txt index 1b052db6f13..8886b91122d 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,38 +1,39 @@ -alabaster==0.7.16 -Babel==2.14.0 -certifi==2023.11.17 -charset-normalizer==3.3.2 +alabaster==1.0.0 +babel==2.16.0 +certifi==2024.8.30 +charset-normalizer==3.4.0 commonmark==0.9.1 -docutils==0.20.1 -idna==3.6 +docutils==0.21.2 +idna==3.10 imagesize==1.4.1 -importlib-metadata==6.7.0 -Jinja2==3.1.3 +importlib-metadata==8.5.0 +Jinja2==3.1.4 markdown-it-py==3.0.0 -MarkupSafe==2.1.3 -mdit-py-plugins==0.4.0 +MarkupSafe==3.0.1 +mdit-py-plugins==0.4.2 mdurl==0.1.2 -myst-parser==2.0.0 -packaging==23.2 -Pygments==2.17.2 -python-markdown-math==0.2 -pytz==2023.3 -PyYAML==6.0.1 +myst-parser==4.0.0 +packaging==24.1 +Pygments==2.18.0 +python-markdown-math==0.8 +pytz==2024.2 +PyYAML==6.0.2 readthedocs-sphinx-ext==2.2.5 recommonmark==0.7.1 -requests==2.31.0 +requests==2.32.3 six==1.16.0 snowballstemmer==2.2.0 -Sphinx==7.2.6 -sphinx-rtd-theme==2.0.0 -sphinxcontrib-applehelp==1.0.8 -sphinxcontrib-devhelp==1.0.6 -sphinxcontrib-htmlhelp==2.0.5 +Sphinx==8.1.3 +sphinx-rtd-theme==3.0.1 +sphinx-tabs==3.4.7 +sphinxcontrib-applehelp==2.0.0 +sphinxcontrib-devhelp==2.0.0 +sphinxcontrib-htmlhelp==2.1.0 sphinxcontrib-jquery==4.1 sphinxcontrib-jsmath==1.0.1 -sphinxcontrib-qthelp==1.0.7 -sphinxcontrib-serializinghtml==1.1.10 -sphinxcontrib-websupport==1.2.4 -typing_extensions==4.7.1 -urllib3==2.1.0 -zipp==3.15.0 +sphinxcontrib-qthelp==2.0.0 +sphinxcontrib-serializinghtml==2.0.0 +sphinxcontrib-websupport==2.0.0 +typing_extensions==4.12.2 +urllib3==2.2.3 +zipp==3.20.2 diff --git a/docs/source/conf.py b/docs/source/conf.py index 52238cac4c3..845ae94e618 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -10,29 +10,30 @@ import os import sys from os import environ -sys.path.insert(0, os.path.abspath('.')) + +sys.path.insert(0, os.path.abspath(".")) import sphinx_rtd_theme -rtd_tag = 'latest' -if environ.get('READTHEDOCS_VERSION') is not None: - rtd_tag = os.environ['READTHEDOCS_VERSION'] +rtd_tag = "latest" +if environ.get("READTHEDOCS_VERSION") is not None: + rtd_tag = os.environ["READTHEDOCS_VERSION"] placeholder_replacements = { "{BRANCH}": "main", - "{BRANCH_DOC}" : "latest", # Used to target the correct ReadTheDocs distribution version - "{RTD_TAG}": rtd_tag + "{BRANCH_DOC}": "latest", # Used to target the correct ReadTheDocs distribution version + "{RTD_TAG}": rtd_tag, } # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information -project = u'Hyperledger Fabric Docs' -copyright = u'2017-2024, Hyperledger Foundation' -author = u'Hyperledger Foundation' -release = u'main' -version = u'main' +project = "Hyperledger Fabric Docs" +copyright = "2017-2024, Hyperledger Foundation" +author = "Hyperledger Foundation" +release = "main" +version = "main" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration @@ -46,43 +47,43 @@ # Sphinx projects. # Installed version as per directive in docs/requirement.txt source_parsers = { - '.md': 'recommonmark.parser.CommonMarkParser', + ".md": "recommonmark.parser.CommonMarkParser", } -# The file extensions of source files. Sphinx considers the files with this suffix as sources. +# The file extensions of source files. Sphinx considers the files with this suffix as sources. # The value can be a dictionary mapping file extensions to file types. For example: -source_suffix = { - '.rst': 'restructuredtext', - '.md': 'markdown' -} +source_suffix = {".rst": "restructuredtext", ".md": "markdown"} # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # Used to be "master_doc" # The main toctree document -root_doc = 'index' +root_doc = "index" # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True -extensions = ['sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.intersphinx', - 'sphinx.ext.todo', - 'sphinx.ext.imgmath', - 'sphinx.ext.ifconfig', - 'sphinx.ext.viewcode', - 'myst_parser', - 'sphinxcontrib.jquery'] +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.doctest", + "sphinx.ext.intersphinx", + "sphinx.ext.todo", + "sphinx.ext.imgmath", + "sphinx.ext.ifconfig", + "sphinx.ext.viewcode", + "myst_parser", + "sphinxcontrib.jquery", + "sphinx_tabs.tabs", +] # -- Special API Accesses ------------------------------------------------- # They create an instance of the Sphinx object, documented here @@ -91,31 +92,33 @@ # # We then call it to perform special/specific customizations. + def placeholderReplace(app, docname, source): result = source[0] for key in app.config.placeholder_replacements: result = result.replace(key, app.config.placeholder_replacements[key]) source[0] = result + def setup(app): - app.add_css_file('css/custom.css') - app.add_config_value('placeholder_replacements', {}, True) - app.connect('source-read', placeholderReplace) + app.add_css_file("css/custom.css") + app.add_config_value("placeholder_replacements", {}, True) + app.connect("source-read", placeholderReplace) # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # html_css_files = ['css/custom.css'] html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] -html_static_path = ['_static'] +html_static_path = ["_static"] html_add_permalinks = True # -- MyST-specific Options ------------------------------------------------- # https://myst-parser.readthedocs.io/en/latest/configuration.html#sphinx-config-options -myst_all_links_external = True \ No newline at end of file +myst_all_links_external = True diff --git a/docs/source/write_first_app.rst b/docs/source/write_first_app.rst index e4e488d4240..70d561a55bf 100644 --- a/docs/source/write_first_app.rst +++ b/docs/source/write_first_app.rst @@ -20,7 +20,7 @@ components: .. code-block:: text - asset-transfer-basic/application-gateway-typescript + asset-transfer-basic/application-gateway-(typescript, go, java) 2. **Smart contract:** which implements the transactions that interact with the ledger. The smart contract is located in the following ``fabric-samples`` directory: @@ -29,8 +29,6 @@ components: asset-transfer-basic/chaincode-(typescript, go, java) -For this example, we will be using the TypeScript smart contract. - This tutorial consists of two principle parts: 1. **Set up a blockchain network.** @@ -53,11 +51,14 @@ Before you begin Before you can run the sample application, you need to install Fabric Samples in your environment. Follow the instructions on :doc:`getting_started` to install the required software. -The sample application in this tutorial uses the Fabric Gateway client API for Node. See the `documentation `_ +The sample application in this tutorial uses the Fabric Gateway client API. See the `documentation `_ for a up to date list of supported programming language runtimes and dependencies. -- Ensure you have a suitable version of Node installed. Instructions for installing Node can be found in the `Node.js - documentation `_. +Ensure you have a suitable version of your chosen runtime installed. For installation instructions, see: + + - `Node.js `_. + - `Go `_. + - `Java `_. Set up the blockchain network @@ -94,19 +95,24 @@ started. Deploy the smart contract ------------------------- -.. note:: This tutorial demonstrates the TypeScript versions of the Asset Transfer smart contract and application, but - you may use any smart contract language sample with the TypeScript application sample (e.g TypeScript - application calling Go smart contract functions or TypeScript application calling Java smart contract - functions, etc.). To try the Go or Java versions of the smart contract, change the ``typescript`` argument - for the ``./network.sh deployCC -ccl typescript`` command below to either ``go`` or ``java`` and follow the - instructions written to the terminal. - Next, let's deploy the chaincode package containing the smart contract by calling the ``./network.sh`` script with the chaincode name and language options. -.. code-block:: bash +.. note:: It is not necessary to use the same programming language for the smart contract and client application. + +.. tabs:: + + .. code-tab:: bash TypeScript + + ./network.sh deployCC -ccn basic -ccp ../asset-transfer-basic/chaincode-typescript/ -ccl typescript + + .. code-tab:: bash Go + + ./network.sh deployCC -ccn basic -ccp ../asset-transfer-basic/chaincode-go/ -ccl go + + .. code-tab:: bash Java - ./network.sh deployCC -ccn basic -ccp ../asset-transfer-basic/chaincode-typescript/ -ccl typescript + ./network.sh deployCC -ccn basic -ccp ../asset-transfer-basic/chaincode-java/ -ccl java This script uses the chaincode lifecycle to package, install, query installed chaincode, approve chaincode for both Org1 and Org2, and finally commit the chaincode. @@ -123,50 +129,121 @@ If the chaincode package is successfully deployed, the end of the output in your Prepare the sample application ------------------------------ -Now, let's prepare the sample Asset Transfer `TypeScript application `_ +Now, let's prepare the sample `Asset Transfer application `_ that will be used to interact with the deployed smart contract. -Open a new terminal, and navigate to the ``application-gateway-typescript`` directory. +Open a new terminal, and navigate to the application directory. This directory contains a sample application developed +using the Fabric Gateway client API. -.. code-block:: bash +.. tabs:: - cd asset-transfer-basic/application-gateway-typescript + .. group-tab:: TypeScript -This directory contains a sample application developed using the Fabric Gateway client API for Node. + .. code-block:: bash -Run the following command to install the dependencies and build the application. It may take some time to complete: + cd asset-transfer-basic/application-gateway-typescript -.. code-block:: bash + Run the following command to install the dependencies and build the application. It may take some time to complete: - npm install + .. code-block:: bash -This process installs the application dependencies defined in the application's ``package.json``. The most important -of which is the ``@hyperledger/fabric-gateway`` Node.js package; this provides the Fabric Gateway client API used -to connect a Fabric Gateway and, using a specific client identity, to submit and evaluate transactions, and receive -events. + npm install -Once ``npm install`` completes, everything is in place to run the application. + This process installs the application dependencies defined in the application's ``package.json``. The most important + of which is the ``@hyperledger/fabric-gateway`` Node.js package; this provides the Fabric Gateway client API used + to connect a Fabric Gateway and, using a specific client identity, to submit and evaluate transactions, and receive + events. -Let's take a look at the sample TypeScript application files we will be using in this tutorial. Run the following -command to list the files in this directory: + Once ``npm install`` completes, everything is in place to run the application. -.. code-block:: bash + Let's take a look at the sample TypeScript application files we will be using in this tutorial. Run the following + command to list the files in this directory: - ls + .. code-block:: bash -You should see the following: + ls -.. code-block:: text + You should see the following: + + .. code-block:: text + + dist + node_modules + package-lock.json + package.json + src + tsconfig.json + + The ``src`` directory contains the client application source code. The JavaScript output generated from this source + code during the install process is located in the ``dist`` directory, and can be ignored. + + .. group-tab:: Go + + .. code-block:: bash + + cd asset-transfer-basic/application-gateway-go + + The application dependencies are defined in the application's ``go.mod``. The most important of which is the + ``github.com/hyperledger/fabric-gateway`` module; this provides the Fabric Gateway client API used to connect a + Fabric Gateway and, using a specific client identity, to submit and evaluate transactions, and receive events. + + Let's take a look at the sample Go application files we will be using in this tutorial. Run the following + command to list the files in this directory: + + .. code-block:: bash + + ls + + You should see the following: + + .. code-block:: text + + assetTransfer.go + go.mod + go.sum + + The ``assetTransfer.go`` file contains the client application source code. + + .. group-tab:: Java + + .. code-block:: bash + + cd asset-transfer-basic/application-gateway-java + + Run the following command to install the dependencies and build the application. It may take some time to complete: + + .. code-block:: bash + + ./gradlew build + + This process installs the application dependencies defined in the application's ``build.gradle``. The most important + of which is the ``org.hyperledger.fabric:fabric-gateway`` package; this provides the Fabric Gateway client API used + to connect a Fabric Gateway and, using a specific client identity, to submit and evaluate transactions, and receive + events. + + Once ``./gradlew build`` completes, everything is in place to run the application. + + Let's take a look at the sample Java application files we will be using in this tutorial. Run the following + command to list the files in this directory: + + .. code-block:: bash - dist - node_modules - package-lock.json - package.json - src - tsconfig.json + ls -The ``src`` directory contains the client application source code. The JavaScript output generated from this source -code during the install process is located in the ``dist`` directory, and can be ignored. + You should see the following: + + .. code-block:: text + + build + build.gradle + gradle + gradlew + gradlew.bat + settings.gradle + src + + The ``src/main/java`` directory contains the client application source code. The compiled Java class files generated + from this source code during the build process is located in the ``build`` directory, and can be ignored. Run the sample application @@ -176,11 +253,21 @@ Authorities. These include a user identity for each of the organizations. The ap of one of these user identities to transact with the blockchain network. Let's run the application and then step through each of the interactions with the smart contract functions. From the -``asset-transfer-basic/application-gateway-typescript`` directory, run the following command: +``asset-transfer-basic/application-gateway-(typescript, go, java)`` directory, run the following command: -.. code-block:: bash +.. tabs:: + + .. code-tab:: bash TypeScript - npm start + npm start + + .. code-tab:: bash Go + + go run . + + .. code-tab:: bash Java + + ./gradlew run First, establish a gRPC connection to the Gateway @@ -197,25 +284,71 @@ of a peer, which provides the Fabric Gateway service. a Fabric Gateway belonging to the same organization as the client identity. If the client identity's organization does not host any gateways, then a trusted gateway in another organization should be used. -The TypeScript application creates a gRPC connection using the TLS certificate of the signing certificate authority so -that the authenticity of the gateway's TLS certificate can be verified. +The application creates a gRPC connection using the TLS certificate of the signing certificate authority so that the +authenticity of the gateway's TLS certificate can be verified. For a TLS connection to be successfully established, the endpoint address used by the client must match the address in -the gateway's TLS certificate. Since the client accesses the gateway's Docker container at a ``localhost`` address, a -gRPC option is specified to force this endpoint address to be interpreted as the gateway's configured hostname. +the gateway's TLS certificate. Since the client accesses the gateway's Docker container at a ``localhost`` address, an +override gRPC option is specified to force this endpoint address to be interpreted as the gateway's configured hostname. -.. code-block:: TypeScript +.. tabs:: - const peerEndpoint = 'localhost:7051'; + .. code-tab:: ts TypeScript - async function newGrpcConnection(): Promise { - const tlsRootCert = await fs.readFile(tlsCertPath); - const tlsCredentials = grpc.credentials.createSsl(tlsRootCert); - return new grpc.Client(peerEndpoint, tlsCredentials, { - 'grpc.ssl_target_name_override': 'peer0.org1.example.com', - }); - } + const peerEndpoint = 'localhost:7051'; + const peerHostOverride = 'peer0.org1.example.com'; + async function newGrpcConnection(): Promise { + const tlsRootCert = await fs.readFile(tlsCertPath); + const tlsCredentials = grpc.credentials.createSsl(tlsRootCert); + return new grpc.Client(peerEndpoint, tlsCredentials, { + 'grpc.ssl_target_name_override': peerHostOverride, + }); + } + + .. code-tab:: go Go + + const ( + peerEndpoint = "dns:///localhost:7051" + peerHostOverride = "peer0.org1.example.com" + ) + + func newGrpcConnection() *grpc.ClientConn { + certificatePEM, err := os.ReadFile(tlsCertPath) + if err != nil { + panic(fmt.Errorf("failed to read TLS certifcate file: %w", err)) + } + + certificate, err := identity.CertificateFromPEM(certificatePEM) + if err != nil { + panic(err) + } + + certPool := x509.NewCertPool() + certPool.AddCert(certificate) + transportCredentials := credentials.NewClientTLSFromCert(certPool, peerHostOverride) + + connection, err := grpc.NewClient(peerEndpoint, grpc.WithTransportCredentials(transportCredentials)) + if err != nil { + panic(fmt.Errorf("failed to create gRPC connection: %w", err)) + } + + return connection + } + + .. code-tab:: java Java + + private static final String PEER_ENDPOINT = "localhost:7051"; + private static final String PEER_HOST_OVERRIDE = "peer0.org1.example.com"; + + private static ManagedChannel newGrpcConnection() throws IOException { + var credentials = TlsChannelCredentials.newBuilder() + .trustManager(TLS_CERT_PATH.toFile()) + .build(); + return Grpc.newChannelBuilder(PEER_ENDPOINT, credentials) + .overrideAuthority(PEER_HOST_OVERRIDE) + .build(); + } Second, create a Gateway connection ----------------------------------- @@ -227,29 +360,111 @@ channels) accessible to the Fabric Gateway, and subsequently smart ``Contracts`` 2. Client identity used to transact with the network. 3. Signing implementation used to generate digital signatures for the client identity. +Additionally, it is good practice to specify the hash algorithm that will be used to generate the message digests +passed to the signing implementation, instead of relying on the default value. Different signing implementations may +have different hash algorithm requirements. + The sample application uses the Org1 user's X.509 certificate as the client identity, and a signing implementation based on that user's private key. -.. code-block:: TypeScript +.. tabs:: - const client = await newGrpcConnection(); + .. code-tab:: ts TypeScript - const gateway = connect({ - client, - identity: await newIdentity(), - signer: await newSigner(), - }); + const client = await newGrpcConnection(); - async function newIdentity(): Promise { - const credentials = await fs.readFile(certPath); - return { mspId: 'Org1MSP', credentials }; - } + const gateway = connect({ + client, + identity: await newIdentity(), + signer: await newSigner(), + hash: hash.sha256, + }); - async function newSigner(): Promise { - const privateKeyPem = await fs.readFile(keyPath); - const privateKey = crypto.createPrivateKey(privateKeyPem); - return signers.newPrivateKeySigner(privateKey); - } + async function newIdentity(): Promise { + const credentials = await fs.promises.readFile(certPath); + return { mspId: 'Org1MSP', credentials }; + } + + async function newSigner(): Promise { + const privateKeyPem = await fs.promises.readFile(keyPath); + const privateKey = crypto.createPrivateKey(privateKeyPem); + return signers.newPrivateKeySigner(privateKey); + } + + .. code-tab:: go Go + + clientConnection := newGrpcConnection() + defer clientConnection.Close() + + gw, err := client.Connect( + newIdentity(), + client.WithSign(newSign()), + client.WithHash(hash.SHA256), + client.WithClientConnection(clientConnection), + ) + + func newIdentity() *identity.X509Identity { + certificatePEM, err := os.ReadFile(certPath) + if err != nil { + panic(fmt.Errorf("failed to read certificate file: %w", err)) + } + + certificate, err := identity.CertificateFromPEM(certificatePEM) + if err != nil { + panic(err) + } + + id, err := identity.NewX509Identity("Org1MSP", certificate) + if err != nil { + panic(err) + } + + return id + } + + func newSign() identity.Sign { + privateKeyPEM, err := readFirstFile(keyPath) + if err != nil { + panic(fmt.Errorf("failed to read private key file: %w", err)) + } + + privateKey, err := identity.PrivateKeyFromPEM(privateKeyPEM) + if err != nil { + panic(err) + } + + sign, err := identity.NewPrivateKeySign(privateKey) + if err != nil { + panic(err) + } + + return sign + } + + .. code-tab:: java Java + + var channel = newGrpcConnection(); + + var gateway = Gateway.newInstance() + .identity(newIdentity()) + .signer(newSigner()) + .hash(Hash.SHA256) + .connection(channel) + .connect(); + + private static Identity newIdentity() throws IOException, CertificateException { + try (var certReader = Files.newBufferedReader(CERT_PATH)) { + var certificate = Identities.readX509Certificate(certReader); + return new X509Identity("Org1MSP", certificate); + } + } + + private static Signer newSigner() throws IOException, InvalidKeyException { + try (var keyReader = Files.newBufferedReader(KEY_PATH)) { + var privateKey = Identities.readPrivateKey(keyReader); + return Signers.newPrivateKeySigner(privateKey); + } + } Third, access the smart contract to be invoked @@ -257,18 +472,40 @@ Third, access the smart contract to be invoked The sample application uses the ``Gateway`` connection to get a reference to the ``Network`` and then the default ``Contract`` within a chaincode deployed on that network. -.. code-block:: TypeScript +.. tabs:: + + .. code-tab:: ts TypeScript + + const network = gateway.getNetwork(channelName); + const contract = network.getContract(chaincodeName); + + .. code-tab:: go Go + + network := gw.GetNetwork(channelName) + contract := network.GetContract(chaincodeName) + + .. code-tab:: java Java + + var network = gateway.getNetwork(CHANNEL_NAME); + var contract = network.getContract(CHAINCODE_NAME); - const network = gateway.getNetwork(channelName); - const contract = network.getContract(chaincodeName); When a chaincode package includes multiple smart contracts, you can provide both the name of the chaincode and the name -of a specific smart contract as arguments to the `getContract() `_ -call. For example: +of a specific smart contract as arguments to the ``getContract()`` call. For example: -.. code-block:: TypeScript +.. tabs:: - const contract = network.getContract(chaincodeName, smartContractName); + .. code-tab:: ts TypeScript + + const contract = network.getContract(chaincodeName, smartContractName); + + .. code-tab:: go Go + + contract := network.GetContractWithName(chaincodeName, smartContractName) + + .. code-tab:: java Java + + var contract = network.getContract(CHAINCODE_NAME, SMART_CONTRACT_NAME); Fourth, populate the ledger with sample assets @@ -283,9 +520,22 @@ assets. ``submitTransaction()`` will use the Fabric Gateway to: Sample application ``InitLedger`` call: -.. code-block:: TypeScript +.. tabs:: + + .. code-tab:: ts TypeScript + + await contract.submitTransaction('InitLedger'); + + .. code-tab:: go Go + + _, err := contract.SubmitTransaction("InitLedger") + if err != nil { + panic(fmt.Errorf("failed to submit transaction: %w", err)) + } - await contract.submitTransaction('InitLedger'); + .. code-tab:: java Java + + contract.submitTransaction("InitLedger"); Fifth, invoke transaction functions to read and write assets @@ -303,13 +553,31 @@ Below, the sample application is just getting all the assets created in the prev Sample application ``GetAllAssets`` call: -.. code-block:: TypeScript +.. tabs:: + + .. code-tab:: ts TypeScript + + const resultBytes = await contract.evaluateTransaction('GetAllAssets'); + + const resultJson = utf8Decoder.decode(resultBytes); + const result = JSON.parse(resultJson); + console.log('*** Result:', result); + + .. code-tab:: go Go + + evaluateResult, err := contract.EvaluateTransaction("GetAllAssets") + if err != nil { + panic(fmt.Errorf("failed to evaluate transaction: %w", err)) + } + + result := formatJSON(evaluateResult) + fmt.Printf("*** Result:%s\n", result) + + .. code-tab:: java Java - const resultBytes = await contract.evaluateTransaction('GetAllAssets'); + var result = contract.evaluateTransaction("GetAllAssets"); - const resultJson = utf8Decoder.decode(resultBytes); - const result = JSON.parse(resultJson); - console.log('*** Result:', result); + System.out.println("*** Result: " + prettyJson(result)); .. note:: Transaction function results are always returned as bytes since transaction functions can return any type of data. Often transaction functions return strings; or, as in the case above, a UTF-8 string of JSON data. The @@ -376,18 +644,35 @@ The sample application submits a transaction to create a new asset. Sample application ``CreateAsset`` call: -.. code-block:: TypeScript +.. tabs:: - const assetId = `asset${Date.now()}`; + .. code-tab:: ts TypeScript - await contract.submitTransaction( - 'CreateAsset', - assetId, - 'yellow', - '5', - 'Tom', - '1300', - ); + const assetId = `asset${String(Date.now())}`; + + await contract.submitTransaction( + 'CreateAsset', + assetId, + 'yellow', + '5', + 'Tom', + '1300', + ); + + .. code-tab:: go Go + + var assetId = fmt.Sprintf("asset%d", now.Unix()*1e3+int64(now.Nanosecond())/1e6) + + _, err := contract.SubmitTransaction("CreateAsset", assetId, "yellow", "5", "Tom", "1300") + if err != nil { + panic(fmt.Errorf("failed to submit transaction: %w", err)) + } + + .. code-tab:: java Java + + private final String assetId = "asset" + Instant.now().toEpochMilli(); + + contract.submitTransaction("CreateAsset", assetId, "yellow", "5", "Tom", "1300"); .. note:: In the application snippets above, it is important to note that the ``CreateAsset`` transaction is submitted with the same type and number of arguments the chaincode is expecting, and in the correct sequence. In this @@ -413,22 +698,64 @@ the application to perform work using the transaction result while waiting for i Sample application ``TransferAsset`` call: -.. code-block:: TypeScript +.. tabs:: - const commit = await contract.submitAsync('TransferAsset', { - arguments: [assetId, 'Saptha'], - }); - const oldOwner = utf8Decoder.decode(commit.getResult()); + .. code-tab:: ts TypeScript - console.log(`*** Successfully submitted transaction to transfer ownership from ${oldOwner} to Saptha`); - console.log('*** Waiting for transaction commit'); + const commit = await contract.submitAsync('TransferAsset', { + arguments: [assetId, 'Saptha'], + }); + const oldOwner = utf8Decoder.decode(commit.getResult()); - const status = await commit.getStatus(); - if (!status.successful) { - throw new Error(`Transaction ${status.transactionId} failed to commit with status code ${status.code}`); - } + console.log(`*** Successfully submitted transaction to transfer ownership from ${oldOwner} to Saptha`); + console.log('*** Waiting for transaction commit'); + + const status = await commit.getStatus(); + if (!status.successful) { + throw new Error(`Transaction ${status.transactionId} failed to commit with status code ${String(status.code)}`); + } + + console.log('*** Transaction committed successfully'); + + .. code-tab:: go Go + + submitResult, commit, err := contract.SubmitAsync("TransferAsset", client.WithArguments(assetId, "Mark")) + if err != nil { + panic(fmt.Errorf("failed to submit transaction asynchronously: %w", err)) + } + + fmt.Printf("\n*** Successfully submitted transaction to transfer ownership from %s to Mark. \n", string(submitResult)) + fmt.Println("*** Waiting for transaction commit.") + + if commitStatus, err := commit.Status(); err != nil { + panic(fmt.Errorf("failed to get commit status: %w", err)) + } else if !commitStatus.Successful { + panic(fmt.Errorf("transaction %s failed to commit with status: %d", commitStatus.TransactionID, int32(commitStatus.Code))) + } + + fmt.Printf("*** Transaction committed successfully\n") - console.log('*** Transaction committed successfully'); + .. code-tab:: java Java + + var commit = contract.newProposal("TransferAsset") + .addArguments(assetId, "Saptha") + .build() + .endorse() + .submitAsync(); + + var result = commit.getResult(); + var oldOwner = new String(result, StandardCharsets.UTF_8); + + System.out.println("*** Successfully submitted transaction to transfer ownership from " + oldOwner + " to Saptha"); + System.out.println("*** Waiting for transaction commit"); + + var status = commit.getStatus(); + if (!status.isSuccessful()) { + throw new RuntimeException("Transaction " + status.getTransactionId() + + " failed to commit with status code " + status.getCode()); + } + + System.out.println("*** Transaction committed successfully"); Terminal output: @@ -445,13 +772,31 @@ properties described, and then subsequently transferred to a new owner. Sample application ``ReadAsset`` call: -.. code-block:: TypeScript +.. tabs:: + + .. code-tab:: ts TypeScript + + const resultBytes = await contract.evaluateTransaction('ReadAsset', assetId); - const resultBytes = await contract.evaluateTransaction('ReadAsset', assetId); + const resultJson = utf8Decoder.decode(resultBytes); + const result = JSON.parse(resultJson); + console.log('*** Result:', result); - const resultJson = utf8Decoder.decode(resultBytes); - const result = JSON.parse(resultJson); - console.log('*** Result:', result); + .. code-tab:: go Go + + evaluateResult, err := contract.EvaluateTransaction("ReadAsset", assetId) + if err != nil { + panic(fmt.Errorf("failed to evaluate transaction: %w", err)) + } + + result := formatJSON(evaluateResult) + fmt.Printf("*** Result:%s\n", result) + + .. code-tab:: java Java + + var evaluateResult = contract.evaluateTransaction("ReadAsset", assetId); + + System.out.println("*** Result:" + prettyJson(evaluateResult)); Terminal output: @@ -473,52 +818,136 @@ function returns an error response, and the ``submitTransaction()`` call fails. A ``submitTransaction()`` failure may generate several different types of error, indicating the point in the submit flow that the error occurred, and containing additional information to enable the application to respond appropriately. -Consult the `API documentation `_ +Consult the API documentation (`Node.js `_, +`Go `_, +`Java `_) for details of the different error types that may be generated. Sample application failing ``UpdateAsset`` call: -.. code-block:: TypeScript - - try { - await contract.submitTransaction( - 'UpdateAsset', - 'asset70', - 'blue', - '5', - 'Tomoko', - '300', - ); - console.log('******** FAILED to return an error'); - } catch (error) { - console.log('*** Successfully caught the error: \n', error); - } +.. tabs:: + + .. code-tab:: ts TypeScript + + try { + await contract.submitTransaction( + 'UpdateAsset', + 'asset70', + 'blue', + '5', + 'Tomoko', + '300', + ); + console.log('******** FAILED to return an error'); + } catch (error) { + console.log('*** Successfully caught the error: \n', error); + } + + .. code-tab:: go Go + + _, err := contract.SubmitTransaction("UpdateAsset", "asset70", "blue", "5", "Tomoko", "300") + if err == nil { + panic("******** FAILED to return an error") + } + + fmt.Println("*** Successfully caught the error:") + + var endorseErr *client.EndorseError + var submitErr *client.SubmitError + var commitStatusErr *client.CommitStatusError + var commitErr *client.CommitError + + if errors.As(err, &endorseErr) { + fmt.Printf("Endorse error for transaction %s with gRPC status %v: %s\n", endorseErr.TransactionID, status.Code(endorseErr), endorseErr) + } else if errors.As(err, &submitErr) { + fmt.Printf("Submit error for transaction %s with gRPC status %v: %s\n", submitErr.TransactionID, status.Code(submitErr), submitErr) + } else if errors.As(err, &commitStatusErr) { + if errors.Is(err, context.DeadlineExceeded) { + fmt.Printf("Timeout waiting for transaction %s commit status: %s", commitStatusErr.TransactionID, commitStatusErr) + } else { + fmt.Printf("Error obtaining commit status for transaction %s with gRPC status %v: %s\n", commitStatusErr.TransactionID, status.Code(commitStatusErr), commitStatusErr) + } + } else if errors.As(err, &commitErr) { + fmt.Printf("Transaction %s failed to commit with status %d: %s\n", commitErr.TransactionID, int32(commitErr.Code), err) + } else { + panic(fmt.Errorf("unexpected error type %T: %w", err, err)) + } + + statusErr := status.Convert(err) + + details := statusErr.Details() + if len(details) > 0 { + fmt.Println("Error Details:") + + for _, detail := range details { + switch detail := detail.(type) { + case *gateway.ErrorDetail: + fmt.Printf("- address: %s; mspId: %s; message: %s\n", detail.Address, detail.MspId, detail.Message) + } + } + } + + .. code-tab:: java Java + + try { + contract.submitTransaction("UpdateAsset", "asset70", "blue", "5", "Tomoko", "300"); + System.out.println("******** FAILED to return an error"); + } catch (EndorseException | SubmitException | CommitStatusException e) { + System.out.println("*** Successfully caught the error:"); + e.printStackTrace(System.out); + System.out.println("Transaction ID: " + e.getTransactionId()); + } catch (CommitException e) { + System.out.println("*** Successfully caught the error:"); + e.printStackTrace(System.out); + System.out.println("Transaction ID: " + e.getTransactionId()); + System.out.println("Status code: " + e.getCode()); + } Terminal Output (with stack traces removed for clarity): -.. code-block:: text +.. tabs:: - *** Successfully caught the error: - EndorseError: 10 ABORTED: failed to endorse transaction, see attached details for more info - at ... { - code: 10, - details: [ - { - address: 'peer0.org1.example.com:7051', - message: 'error in simulation: transaction returned with failure: Error: The asset asset70 does not exist', - mspId: 'Org1MSP' - } - ], - cause: Error: 10 ABORTED: failed to endorse transaction, see attached details for more info + .. code-tab:: text TypeScript + + *** Successfully caught the error: + EndorseError: 10 ABORTED: failed to endorse transaction, see attached details for more info at ... { code: 10, - details: 'failed to endorse transaction, see attached details for more info', - metadata: Metadata { internalRepr: [Map], options: {} } - }, - transactionId: 'a92980d41eef1d6492d63acd5fbb6ef1db0f53252330ad28e548fedfdb9167fe' - } + details: [ + { + address: 'peer0.org1.example.com:7051', + message: 'chaincode response 500, the asset asset70 does not exist', + mspId: 'Org1MSP' + } + ], + cause: Error: 10 ABORTED: failed to endorse transaction, see attached details for more info + at ... { + code: 10, + details: 'failed to endorse transaction, see attached details for more info', + metadata: Metadata { internalRepr: [Map], options: {} } + }, + transactionId: 'a92980d41eef1d6492d63acd5fbb6ef1db0f53252330ad28e548fedfdb9167fe' + } + + .. code-tab:: text Go + + *** Successfully caught the error: + Endorse error for transaction 0a0bf1af9c53e0621d6dc98217fb882e0c6d5e174dc1a45f5cb4e07580528347 with gRPC status Aborted: rpc error: code = Aborted desc = failed to endorse transaction, see attached details for more info + Error Details: + - address: peer0.org1.example.com:7051; mspId: Org1MSP; message: chaincode response 500, the asset asset70 does not exist + + .. code-tab:: text Java + + *** Successfully caught the error: + org.hyperledger.fabric.client.EndorseException: io.grpc.StatusRuntimeException: ABORTED: failed to endorse transaction, see attached details for more info + at ... + Caused by: io.grpc.StatusRuntimeException: ABORTED: failed to endorse transaction, see attached details for more info + at ... + Error details: + address: peer0.org1.example.com:7051; mspId: Org1MSP; message: chaincode response 500, the asset asset70 does not exist + Transaction ID: 5dcc3576cbb851bfbd998f2413da7707761ad15911b7c7fba853e72ac1b3b002 -The ``EndorseError`` type indicates that failure occurred during endorsement, and the +The ``Endorse`` error type indicates that failure occurred during endorsement, and the `gRPC status code `_ of ``ABORTED`` indicates that the application successfully invoked the Fabric Gateway but that a failure occurred during the endorsement process. A gRPC status code of ``UNAVAILABLE`` or ``DEADLINE_EXCEEDED`` would suggest that the Fabric Gateway was not reachable or a diff --git a/tox.ini b/tox.ini index 4b7e95808cb..6a9d32ecb4f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -minversion = 3.4 +minversion = 4.23 envlist = docs, docs-linkcheck @@ -10,13 +10,13 @@ deps = -rdocs/requirements.txt commands = sphinx-build -b html -n -d {envtmpdir}/doctrees ./docs/source {toxinidir}/docs/build/html echo "Generated docs available in {toxinidir}/docs/build/html" -whitelist_externals = echo -basepython=python3.7 +allowlist_externals = echo +basepython=python3.12 ignore_basepython_conflict=True [testenv:docs-linkcheck] deps = -rdocs/requirements.txt commands = sphinx-build -b linkcheck -d {envtmpdir}/doctrees ./docs/source {toxinidir}/docs/build/linkcheck -basepython=python3.7 +basepython=python3.12 ignore_basepython_conflict=True