diff --git a/.github/settings.yml b/.github/settings.yml index 85a3d0da..fb74bdb5 100644 --- a/.github/settings.yml +++ b/.github/settings.yml @@ -12,3 +12,4 @@ repository: allow_squash_merge: true allow_merge_commit: false allow_rebase_merge: true + diff --git a/.gitignore b/.gitignore index f5102b7d..3fbf0f49 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,6 @@ builders/ config/ external-chaincode/ install-fabric.sh + +# override the ignore of all config/ folders +!full-stack-asset-transfer-guide/infrastructure/sample-network/config \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/.github/workflows/asset-tx-typescript-contract-image.yaml b/full-stack-asset-transfer-guide/.github/workflows/asset-tx-typescript-contract-image.yaml new file mode 100644 index 00000000..c65bb790 --- /dev/null +++ b/full-stack-asset-transfer-guide/.github/workflows/asset-tx-typescript-contract-image.yaml @@ -0,0 +1,24 @@ +name: Asset Tx Contract Image CI + +on: + push: + branches: + - 'main' + tags: + - 'v*' + paths: + - 'contracts/asset-transfer-typescript/**' + pull_request: + branches: + - 'main' + paths: + - 'contracts/asset-transfer-typescript/**' + +jobs: + docker_build: + name: Docker build + uses: ./.github/workflows/docker-build.yaml + with: + imagename: full-stack-asset-transfer-guide/contracts/asset-transfer-typescript + path: contracts/asset-transfer-typescript + chaincode-label: asset-transfer-typescript diff --git a/full-stack-asset-transfer-guide/.github/workflows/docker-build.yaml b/full-stack-asset-transfer-guide/.github/workflows/docker-build.yaml new file mode 100644 index 00000000..894aedc2 --- /dev/null +++ b/full-stack-asset-transfer-guide/.github/workflows/docker-build.yaml @@ -0,0 +1,78 @@ +name: Docker CI + +on: + workflow_call: + inputs: + imagename: + description: 'A Docker image name passed from the caller workflow' + required: true + type: string + path: + description: 'A path containing a Dockerfile passed from the caller workflow' + required: true + type: string + chaincode-label: + description: 'An optional chaincode package label passed from the caller workflow. If present, will prepare a chaincode package.' + required: false + type: string + +jobs: + build: + runs-on: ubuntu-latest + outputs: + image_digest: ${{ steps.publish_image.outputs.image_digest }} + + steps: + - uses: actions/checkout@v3 + - name: Build Docker image + run: | + docker build ${DOCKER_BUILD_PATH} --file ${DOCKER_BUILD_PATH}/Dockerfile --label "org.opencontainers.image.revision=${GITHUB_SHA}" --tag ${IMAGE_NAME} + docker tag ${IMAGE_NAME} ghcr.io/hyperledgendary/${IMAGE_NAME}:${GITHUB_SHA} + if [ "${GITHUB_REF:0:10}" = "refs/tags/" ]; then + docker tag ${IMAGE_NAME} ghcr.io/hyperledgendary/${IMAGE_NAME}:${GITHUB_REF_NAME} + docker tag ${IMAGE_NAME} ghcr.io/hyperledgendary/${IMAGE_NAME}:latest + fi + env: + DOCKER_BUILD_PATH: ${{ inputs.path }} + IMAGE_NAME: ${{ inputs.imagename }} + - name: Publish Docker image + id: publish_image + if: github.event_name != 'pull_request' + run: | + echo ${DOCKER_PW} | docker login ghcr.io -u ${DOCKER_USER} --password-stdin + docker push ghcr.io/hyperledgendary/${IMAGE_NAME}:${GITHUB_SHA} + if [ "${GITHUB_REF:0:10}" = "refs/tags/" ]; then + docker push ghcr.io/hyperledgendary/${IMAGE_NAME}:${GITHUB_REF_NAME} + docker push ghcr.io/hyperledgendary/${IMAGE_NAME}:latest + fi + echo ::set-output name=image_digest::$(docker inspect --format='{{index .RepoDigests 0}}' ghcr.io/hyperledgendary/${IMAGE_NAME}:${GITHUB_SHA} | cut -d'@' -f2) + env: + IMAGE_NAME: ${{ inputs.imagename }} + DOCKER_USER: ${{ github.actor }} + DOCKER_PW: ${{ secrets.GITHUB_TOKEN }} + + package: + if: inputs.chaincode-label != '' && needs.build.outputs.image_digest != '' + needs: build + runs-on: ubuntu-latest + + steps: + - name: Create package + uses: hyperledgendary/package-k8s-chaincode-action@ba10aea43e3d4f7991116527faf96e3c2b07abc7 + with: + chaincode-label: ${{ inputs.chaincode-label }} + chaincode-image: ghcr.io/hyperledgendary/${{ inputs.imagename }} + chaincode-digest: ${{ needs.build.outputs.image_digest }} + + - name: Rename package + if: startsWith(github.ref, 'refs/tags/v') + run: mv ${{ inputs.chaincode-label }}.tgz ${{ inputs.chaincode-label }}-${CHAINCODE_VERSION}.tgz + env: + IMAGE_NAME: ${{ inputs.imagename }} + CHAINCODE_VERSION: ${{ github.ref_name }} + + - name: Release package + if: startsWith(github.ref, 'refs/tags/v') + uses: softprops/action-gh-release@v1 + with: + files: ${{ inputs.chaincode-label }}-${{ github.ref_name }}.tgz \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/.github/workflows/test-ansible.yaml b/full-stack-asset-transfer-guide/.github/workflows/test-ansible.yaml new file mode 100644 index 00000000..e82f8739 --- /dev/null +++ b/full-stack-asset-transfer-guide/.github/workflows/test-ansible.yaml @@ -0,0 +1,49 @@ +name: test-ansible + +on: + pull_request: + branches: + - main + +jobs: + test-ansible: + runs-on: ubuntu-latest + + steps: + - name: checkout + uses: actions/checkout@v3 + + - name: k9s + env: + K9S_VERSION: v0.25.3 + run: | + curl --fail --silent --show-error -L https://github.com/derailed/k9s/releases/download/${K9S_VERSION}/k9s_Linux_x86_64.tar.gz -o /tmp/k9s_Linux_x86_64.tar.gz + tar -zxf /tmp/k9s_Linux_x86_64.tar.gz -C /usr/local/bin k9s + sudo chown root /usr/local/bin/k9s + sudo chmod 755 /usr/local/bin/k9s + + - name: just + env: + JUST_VERSION: 1.2.0 + run: | + curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --tag ${JUST_VERSION} --to /usr/local/bin + + - name: weft + run: | + npm install -g @hyperledger-labs/weft + + - name: fabric + run: | + curl -sSL https://raw.githubusercontent.com/hyperledger/fabric/main/scripts/install-fabric.sh | bash -s -- binary + + - name: check prereqs + run: | + export WORKSHOP_PATH="${PWD}" + export PATH="${WORKSHOP_PATH}/bin:${PATH}" + export FABRIC_CFG_PATH="${WORKSHOP_PATH}/config" + + ./check.sh + + - name: just test-ansible + run: | + just test-ansible \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/.github/workflows/test-appdev.yaml b/full-stack-asset-transfer-guide/.github/workflows/test-appdev.yaml new file mode 100644 index 00000000..d0c08e7d --- /dev/null +++ b/full-stack-asset-transfer-guide/.github/workflows/test-appdev.yaml @@ -0,0 +1,49 @@ +name: test-appdev + +on: + pull_request: + branches: + - main + +jobs: + test-appdev: + runs-on: ubuntu-latest + + steps: + - name: checkout + uses: actions/checkout@v3 + + - name: k9s + env: + K9S_VERSION: v0.25.3 + run: | + curl --fail --silent --show-error -L https://github.com/derailed/k9s/releases/download/${K9S_VERSION}/k9s_Linux_x86_64.tar.gz -o /tmp/k9s_Linux_x86_64.tar.gz + tar -zxf /tmp/k9s_Linux_x86_64.tar.gz -C /usr/local/bin k9s + sudo chown root /usr/local/bin/k9s + sudo chmod 755 /usr/local/bin/k9s + + - name: just + env: + JUST_VERSION: 1.2.0 + run: | + curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --tag ${JUST_VERSION} --to /usr/local/bin + + - name: weft + run: | + npm install -g @hyperledger-labs/weft + + - name: fabric + run: | + curl -sSL https://raw.githubusercontent.com/hyperledger/fabric/main/scripts/install-fabric.sh | bash -s -- binary + + - name: check prereqs + run: | + export WORKSHOP_PATH="${PWD}" + export PATH="${WORKSHOP_PATH}/bin:${PATH}" + export FABRIC_CFG_PATH="${WORKSHOP_PATH}/config" + + ./check.sh + + - name: just test-appdev + run: | + just test-appdev \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/.github/workflows/test-chaincode.yaml b/full-stack-asset-transfer-guide/.github/workflows/test-chaincode.yaml new file mode 100644 index 00000000..459b0711 --- /dev/null +++ b/full-stack-asset-transfer-guide/.github/workflows/test-chaincode.yaml @@ -0,0 +1,49 @@ +name: test-chaincode + +on: + pull_request: + branches: + - main + +jobs: + test-chaincode: + runs-on: ubuntu-latest + + steps: + - name: checkout + uses: actions/checkout@v3 + + - name: k9s + env: + K9S_VERSION: v0.25.3 + run: | + curl --fail --silent --show-error -L https://github.com/derailed/k9s/releases/download/${K9S_VERSION}/k9s_Linux_x86_64.tar.gz -o /tmp/k9s_Linux_x86_64.tar.gz + tar -zxf /tmp/k9s_Linux_x86_64.tar.gz -C /usr/local/bin k9s + sudo chown root /usr/local/bin/k9s + sudo chmod 755 /usr/local/bin/k9s + + - name: just + env: + JUST_VERSION: 1.2.0 + run: | + curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --tag ${JUST_VERSION} --to /usr/local/bin + + - name: weft + run: | + npm install -g @hyperledger-labs/weft + + - name: fabric + run: | + curl -sSL https://raw.githubusercontent.com/hyperledger/fabric/main/scripts/install-fabric.sh | bash -s -- binary + + - name: check prereqs + run: | + export WORKSHOP_PATH="${PWD}" + export PATH="${WORKSHOP_PATH}/bin:${PATH}" + export FABRIC_CFG_PATH="${WORKSHOP_PATH}/config" + + ./check.sh + + - name: just test-chaincode + run: | + just test-chaincode \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/.github/workflows/test-cloud.yaml b/full-stack-asset-transfer-guide/.github/workflows/test-cloud.yaml new file mode 100644 index 00000000..ce9a2c9d --- /dev/null +++ b/full-stack-asset-transfer-guide/.github/workflows/test-cloud.yaml @@ -0,0 +1,49 @@ +name: test-cloud + +on: + pull_request: + branches: + - main + +jobs: + test-cloud: + runs-on: ubuntu-latest + + steps: + - name: checkout + uses: actions/checkout@v3 + + - name: k9s + env: + K9S_VERSION: v0.25.3 + run: | + curl --fail --silent --show-error -L https://github.com/derailed/k9s/releases/download/${K9S_VERSION}/k9s_Linux_x86_64.tar.gz -o /tmp/k9s_Linux_x86_64.tar.gz + tar -zxf /tmp/k9s_Linux_x86_64.tar.gz -C /usr/local/bin k9s + sudo chown root /usr/local/bin/k9s + sudo chmod 755 /usr/local/bin/k9s + + - name: just + env: + JUST_VERSION: 1.2.0 + run: | + curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --tag ${JUST_VERSION} --to /usr/local/bin + + - name: weft + run: | + npm install -g @hyperledger-labs/weft + + - name: fabric + run: | + curl -sSL https://raw.githubusercontent.com/hyperledger/fabric/main/scripts/install-fabric.sh | bash -s -- binary + + - name: check prereqs + run: | + export WORKSHOP_PATH="${PWD}" + export PATH="${WORKSHOP_PATH}/bin:${PATH}" + export FABRIC_CFG_PATH="${WORKSHOP_PATH}/config" + + ./check.sh + + - name: just test-cloud + run: | + just test-cloud \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/.github/workflows/test-console.yaml b/full-stack-asset-transfer-guide/.github/workflows/test-console.yaml new file mode 100644 index 00000000..c3969a99 --- /dev/null +++ b/full-stack-asset-transfer-guide/.github/workflows/test-console.yaml @@ -0,0 +1,49 @@ +name: test-console + +on: + pull_request: + branches: + - main + +jobs: + test-console: + runs-on: ubuntu-latest + + steps: + - name: checkout + uses: actions/checkout@v3 + + - name: k9s + env: + K9S_VERSION: v0.25.3 + run: | + curl --fail --silent --show-error -L https://github.com/derailed/k9s/releases/download/${K9S_VERSION}/k9s_Linux_x86_64.tar.gz -o /tmp/k9s_Linux_x86_64.tar.gz + tar -zxf /tmp/k9s_Linux_x86_64.tar.gz -C /usr/local/bin k9s + sudo chown root /usr/local/bin/k9s + sudo chmod 755 /usr/local/bin/k9s + + - name: just + env: + JUST_VERSION: 1.2.0 + run: | + curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --tag ${JUST_VERSION} --to /usr/local/bin + + - name: weft + run: | + npm install -g @hyperledger-labs/weft + + - name: fabric + run: | + curl -sSL https://raw.githubusercontent.com/hyperledger/fabric/main/scripts/install-fabric.sh | bash -s -- binary + + - name: check prereqs + run: | + export WORKSHOP_PATH="${PWD}" + export PATH="${WORKSHOP_PATH}/bin:${PATH}" + export FABRIC_CFG_PATH="${WORKSHOP_PATH}/config" + + ./check.sh + + - name: just test-console + run: | + just test-console \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/.gitignore b/full-stack-asset-transfer-guide/.gitignore new file mode 100644 index 00000000..a7148016 --- /dev/null +++ b/full-stack-asset-transfer-guide/.gitignore @@ -0,0 +1,17 @@ +fabric +_cfg +node_modules +*.bin +.idea/ +_* +*tgz +*.tar.gz +~*.pptx + +bin +config/ + +.DS_Store +.idea/ + +rook \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/LICENSE b/full-stack-asset-transfer-guide/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/full-stack-asset-transfer-guide/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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 + + http://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. diff --git a/full-stack-asset-transfer-guide/README.md b/full-stack-asset-transfer-guide/README.md new file mode 100644 index 00000000..b9e4e04a --- /dev/null +++ b/full-stack-asset-transfer-guide/README.md @@ -0,0 +1,108 @@ +![Conga](https://avatars.githubusercontent.com/u/49026922?s=200&v=4) + +# Fabric Full Stack Development Workshop + +![Hyperledger](https://img.shields.io/badge/hyperledger-2F3134?style=for-the-badge&logo=hyperledger&logoColor=white) + +Hyperledger Fabric can be used to represent assets of any kind on a permissioned decentralized ledger, from fungible tokens to non-fungible tokens, including monetary products, marbles, pineapples, classic cars, fine art, and anything else you can imagine. +Fabric can be used to track and update anything about these assets, common examples include asset ownership, exchange, provenance, and lifecycle. + +This workshop will demonstrate how a generic asset transfer solution can be modeled and deployed to take advantage of a blockchains qualitites of service. + +The workshop will be split into three sections: +- Smart Contract Development +- Client Application Development +- Cloud Native Fabric Deployment + +![Intro diagram](./docs/images/readme_diagram.png) + +**OBJECTIVES:** + +- Show how an Asset Transfer smart contract can be written to encapsulate business logic + - Show how the smart contract can be developed iteratively to get correct function in a development context +- Show how client applications can be written using the Gateway functionality + - Show how the simplification of the Gateway programming model makes connecting applications more streamlined + - Show how this streamlined approach improves resilience and availability +- Show how the solution can then be deployed to a production-class environment + - Show how a Hyperledger Fabric network can be created and managed in Kubernetes (K8S) using automation + - Show how the Fabric Operator and Console can be installed via Ansible playbooks + - Show how a multi-organization configuration of Fabric can be created + +--- + +**Please ensure you've got the [required tools](./SETUP.md) on your local machine or in a virtual machine -- To check, run `./check.sh`** + +--- + + +## Before you begin.... + +Fabric is a multi-server decentralized system with orderer and peer nodes, so it can be quite complex to configure. Even the simplest smart contract needs a running Fabric Infrastructure and one size does not fit all. + +There are configurations that can run Fabric either as local binaries, in a single docker container, in multiple containers, or in K8S. +This workshop will show some of the approaches that can be used for developing applications and contracts with a minimal Fabric environment (Microfab), and how a production deployment can be achieved. +There are other ways of deploying Fabric produced by the community - these are equally valid and useful. Feel free to try the others, once you understand the basic concepts to find what works best for you. + +At a high-level remember that a solution using Fabric has (a) client application to send in transaction requests (b) Fabric infrastructure to service those requests (c) Smart Contract to action the transactions. +The nature of (b) the fabric infrastructure will change depending on your scenario; start simple and build up. The smart contracts and client application's code will remain the same no matter the way Fabric is provisioned. +There will be minor variations in deployment (eg local docker container vs remote K8S cluster) but fundamentally the process is the same. + +## Running the workshop + +![Windows](https://img.shields.io/badge/Windows-0078D6?style=for-the-badge&logo=windows&logoColor=white) If you're running on Windows, please check the [hints and tips](./docs/tips-for-windows-dev.md) + +- Ensure you've got the tools you may need, either installed locally or in a multipass virtual machine. See the [setup page](./SETUP.md) for details. +- Clone this repository to a convient location +- We suggest that you open 3 or 4 terminal windows + - One for running chaincode in dev mode + - One for running the fabric infrastructure and optionally one for monitoring it + - One for client applications + +- Work through the sections below in order, although you don't necessarily need to complete all the Exercises before moving to the next section. + +--- +## Scenario + +Lets assume the assets you are tracking on the blockchain ledger are trading cards. Each trading card represents a comic book character and has an id, size, favorite color, and owner. +These trading cards can be passed between people, with some cards having more 'value' due to rarity or having notable attributes. + +In token terms, think of these cards as non-fungible tokens. Each card has different attributes and individual cards can't be subdivided. + +We'll create a digital representation of these cards on the blockchain ledger. There are a few important aspects of this solution to consider: + +- Ledger - The blockchain ledger on each peer maintains the current state of each card (asset), as well as the history of transactions that led to the current state, so that there is no doubt about the assets issuance, provenance, attributes, and ownership. +- Asset transfer smart contract - manage changes to asset state such as the transfer of cards between people +- Organizations - Since this is a permissioned blockchain we'll model the participants as organizations that are authorized to run nodes or transact on the Fabric network. Our simple network will consist of an ordering service organization and two transacting organizations. + - Ordering service organization - runs the ordering service to ensure transactions get ordered into blocks fairly, this may be a consortium leader or regulator in the industry. Note that ordering service nodes could also be contributed from multiple organizations, this becomes especially important when running a Byzantine Fault Tolerant (BFT) ordering service. + - Owner Organizations - Each owner organization is authorized to run peers and submit transfer transactions for the cards (assets) that they own. + + +## Smart Contract Development + +- [Introduction](./docs/SmartContractDev/00-Introduction.md) +- **Exercise**: [Getting Started with a Smart Contract](./docs/SmartContractDev/01-Exercise-Getting-Started.md) +- **Exercise**: [Adding a new transaction function](./docs/SmartContractDev/02-Exercise-Adding-tx-function.md) +- Reference: + - [Detailed Test and Debug](./docs/SmartContractDev/03-Test-And-Debug-Reference.md) + +## Client Application Development + +- [Fabric Gateway](docs/ApplicationDev/01-FabricGateway.md) +- **Exercise:** [Run the client application](docs/ApplicationDev/02-Exercise-RunApplication.md) +- [Application overview](docs/ApplicationDev/03-ApplicationOverview.md) +- **Exercise:** [Implement asset transfer](docs/ApplicationDev/04-Exercise-AssetTransfer.md) +- [Chaincode events](docs/ApplicationDev/05-ChaincodeEvents.md) +- **Exercise:** [Use chaincode events](docs/ApplicationDev/06-Exercise-ChaincodeEvents.md) + +## Cloud Native Fabric + +- [Cloud Ready!](docs/CloudReady/00-setup.md) +- **Exercise:** [Deploy a Kubernetes Cluster](docs/CloudReady/10-kube.md) +- **Exercise:** [Deploy a Fabric Network](docs/CloudReady/20-fabric.md) +- **Exercise:** [Deploy a Smart Contract](docs/CloudReady/30-chaincode.md) +- **Exercise:** [Deploy a Client Application](docs/CloudReady/40-bananas.md) + +## Epilogue + +- [Go Bananas](docs/CloudReady/40-bananas.md) +- [Bring it Home](docs/CloudReady/90-teardown.md) diff --git a/full-stack-asset-transfer-guide/SETUP.md b/full-stack-asset-transfer-guide/SETUP.md new file mode 100644 index 00000000..40a84671 --- /dev/null +++ b/full-stack-asset-transfer-guide/SETUP.md @@ -0,0 +1,138 @@ +# Essential Setup + +Remember to clone this repository! + +```shell +git clone https://github.com/hyperledgendary/full-stack-asset-transfer-guide.git workshop +cd workshop +export WORKSHOP_PATH=$(pwd) +``` + +> to check the tools you already have `./check.sh` + +## Option 1: Use local environment + +Do you want to configure your local environment with the workshop dependencies? + +- To develop an application and/or contract (first two parts of workshop) follow the *DEV* setup below + +- To deploy a chaincode to kubernetes in a production manner (third part of workshop) follow the *PROD* setup below + +## Option 2: Use a Multipass Ubuntu image + +If you do not want to install dependencies on your local environment, you can use a Multipass Ubuntu image instead. + +Tip - You may need to stop any VPN client for the Multipass networking to work. + +- [Install multipass](https://multipass.run/install) + +- Launch the virtual machine and automatically install the workshop dependencies: + +```shell +multipass launch \ + --name fabric-dev \ + --disk 80G \ + --cpus 8 \ + --mem 8G \ + --cloud-init infrastructure/multipass-cloud-config.yaml +``` + +- Mount the local workshop to your multipass vm: + +```shell +multipass mount $PWD fabric-dev:/home/ubuntu/full-stack-asset-transfer-guide +``` + +- Open a shell on the virtual machine: + +```shell +multipass shell fabric-dev +``` + +Tip - The vm creation log can be seen at /var/log/cloud-init-output.log if you need to troubleshoot anything. + +- You are now inside the virtual machine. cd to the workshop directory: + +```shell +cd full-stack-asset-transfer-guide +``` + +- Install Fabric peer CLI and set environment variables +```shell +curl -sSLO https://raw.githubusercontent.com/hyperledger/fabric/main/scripts/install-fabric.sh && chmod +x install-fabric.sh +./install-fabric.sh binary +export WORKSHOP_PATH=$(pwd) +export PATH=${WORKSHOP_PATH}/bin:$PATH +export FABRIC_CFG_PATH=${WORKSHOP_PATH}/config +``` + +Note - You'll probably want three terminal windows for running the workshop, go ahead and open the shells now: + +```shell +multipass shell fabric-dev +``` + +- Eventual cleanup - To remove the multipass image when you are done with it after the workshop: +```shell +multipass delete fabric-dev +multipass purge +multipass list +``` + +## DEV - Required Tools + +You will need a set of tools to assist with chaincode and application development. +We'll assume you are developing in Node for this workshop, but you could also develop in Java or Go by installing the respective compilers. + +- [docker engine](https://docs.docker.com/engine/install/) + +- [just](https://github.com/casey/just#installation) to run all the commands here directly + +- [nvm](https://github.com/nvm-sh/nvm#installing-and-updating) to install node and npm +```shell +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash +``` + +- [node v16 and npm](https://github.com/nvm-sh/nvm#usage) to run node chaincode and applications +```shell +nvm install 16 +``` + +- [typescript](https://www.typescriptlang.org/download) to compile typescript chaincode and applications to node +```shell +npm install -g typescript +``` + +- [weft ](https://www.npmjs.com/package/@hyperledger-labs/weft) Hyperledger-Labs cli to work with identities and chaincode packages +```shell +npm install -g @hyperledger-labs/weft +``` + +- [jq](https://stedolan.github.io/jq/) jq JSON command-line processor +```shell +sudo apt-get update && sudo apt-get install -y jq +``` + +- Fabric peer CLI +```shell +curl -sSL https://raw.githubusercontent.com/hyperledger/fabric/main/scripts/install-fabric.sh | bash -s -- binary +export WORKSHOP_PATH=$(pwd) +export PATH=${WORKSHOP_PATH}/bin:$PATH +export FABRIC_CFG_PATH=${WORKSHOP_PATH}/config +``` + +## PROD - Required Tools for Kubernetes Deployment + +- [kubectl](https://kubernetes.io/docs/tasks/tools/) +- [jq](https://stedolan.github.io/jq/) +- [just](https://github.com/casey/just#installation) to run all the comamnds here directly +- [kind](https://kind.sigs.k8s.io/) if you want to create a cluster locally, see below for other options +- [k9s](https://k9scli.io) (recommended, but not essential) + +### Beta Ansible Playbooks + +The v2.0.0-beta Ansible Collection for Hyperledger Fabric is required for Kubernetes deployment. This isn't yet being published to DockerHub but is being published to Github Packages. + +For reference check the latest version of [ofs-ansible](https://github.com/IBM-Blockchain/ansible-collection/pkgs/container/ofs-ansibe) + +The Ansible scripts in the workshop are set to use the latest image here by default. diff --git a/full-stack-asset-transfer-guide/applications/conga-cards/.eslintrc.js b/full-stack-asset-transfer-guide/applications/conga-cards/.eslintrc.js new file mode 100644 index 00000000..ad992fae --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/conga-cards/.eslintrc.js @@ -0,0 +1,12 @@ +module.exports = { + root: true, + parser: '@typescript-eslint/parser', + plugins: [ + '@typescript-eslint', + ], + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'prettier', + ], +}; \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/applications/conga-cards/.gitignore b/full-stack-asset-transfer-guide/applications/conga-cards/.gitignore new file mode 100644 index 00000000..1276eb3b --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/conga-cards/.gitignore @@ -0,0 +1,18 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +# Coverage directory used by tools like istanbul +coverage + +# Dependencies +node_modules/ +jspm_packages/ +package-lock.json + +# Compiled TypeScript files +dist + +# Files generated by the application at runtime +checkpoint.json +store.log diff --git a/full-stack-asset-transfer-guide/applications/conga-cards/.npmrc b/full-stack-asset-transfer-guide/applications/conga-cards/.npmrc new file mode 100644 index 00000000..b6f27f13 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/conga-cards/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/full-stack-asset-transfer-guide/applications/conga-cards/README.md b/full-stack-asset-transfer-guide/applications/conga-cards/README.md new file mode 100644 index 00000000..e99b1bcc --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/conga-cards/README.md @@ -0,0 +1,73 @@ +# Conga Cards + +This Gateway Client application will listen for chaincode events, invoking a [#discord webhook](https://discord.com/developers/docs/resources/webhook) +to post a channel message when [Conga Cards](assets/) are created, deleted, and exchanged on a channel. + +> [Conga Comics](https://congacomic.github.io) | Life on the chain, one block at a time by Ed Moffatt & some friends + +This project is based on the [trader-typescript](../trader-typescript) sample Gateway Client application. + + +## Prerequisites + +The client application requires Node.js 16 or later. + +## Set up + +The following steps prepare the client application for execution: + +1. Ensure the [asset-transfer](../../contracts/asset-transfer-typescript/) smart contract is deployed to a running Fabric network. +1. Run `npm install` to download dependencies and compile the application code. + +> **Note:** After making any code changes to the application, be sure to recompile the application code. This can be done by explicitly running `npm install` again, or you can leave `npm run build:watch` running in a terminal window to automatically rebuild the application on any code change. + + +The client application uses environment variables to supply configuration options. You must set the following environment variables when running the application: + +- `ENDPOINT` - endpoint address for the Gateway service to which the client will connect in the form **hostname:port**. Depending on your environment, this can be the address of a specific peer within the user's organization, or an ingress endpoint that dispatches to any available peer in the user's organization. +- `MSP_ID` - member service provider ID for the user's organization. +- `CERTIFICATE` - PEM file containing the user's X.509 certificate. +- `PRIVATE_KEY` - PEM file containing the user's private key. + +The following environment variables are optional and can be set if required by your environment: + +- `CHANNEL_NAME` - Channel to which the chaincode is deployed. (Default: `mychannel`) +- `CHAINCODE_NAME` - Channel to which the chaincode is deployed. (Default: `asset-transfer`) +- `TLS_CERT` - PEM file containing the CA certificate used to authenticate the TLS connection to the Gateway peer. *Only required if using a TLS connection and a private CA.* +- `HOST_ALIAS` - the name of the Gateway peer as it appears in its TLS certificate. *Only required if the endpoint address used by the client does not match the address in the Gateway peer's TLS certificate.* + +- `WEBHOOK_URL` - the [#discord webhook](https://discord.com/developers/docs/resources/webhook) to which the channel + events will be relayed. + + +# Run + +The sample application is run as a command-line application, and is lauched using `npm start [ ...]`. The following commands are available: + +- `npm start create ` to create a new asset. +- `npm start delete ` to delete an existing asset. +- `npm start getAllAssets` to list all assets. +- `npm start read ` to view an existing asset. +- `npm start transfer ` to transfer an asset to a new owner within an organization MSP ID. +- `npm start discord` starts an event loop, relaying channel events to `${WEBHOOK_URL}` + + +## Sample Interaction: + +- Submit some transactions to a ledger: +```shell +npm start create blockbert SeanB orange + +npm start create count-blockula jkneubuhl Org1MSP purple + +npm start transfer count-blockula davidboswell Org1MSP +``` + +- Run the discord event listener: +```shell +export WEBHOOK_URL="https://discord.com/api/webhooks/123456789/xyzzy-abcdef-12345" + +npm start discord +``` + +![Sample Interaction](images/interaction.png) \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/applications/conga-cards/assets/appleplectic.png b/full-stack-asset-transfer-guide/applications/conga-cards/assets/appleplectic.png new file mode 100644 index 00000000..7a8da445 Binary files /dev/null and b/full-stack-asset-transfer-guide/applications/conga-cards/assets/appleplectic.png differ diff --git a/full-stack-asset-transfer-guide/applications/conga-cards/assets/bananomatopoeia.png b/full-stack-asset-transfer-guide/applications/conga-cards/assets/bananomatopoeia.png new file mode 100644 index 00000000..40fcba25 Binary files /dev/null and b/full-stack-asset-transfer-guide/applications/conga-cards/assets/bananomatopoeia.png differ diff --git a/full-stack-asset-transfer-guide/applications/conga-cards/assets/block-norris.png b/full-stack-asset-transfer-guide/applications/conga-cards/assets/block-norris.png new file mode 100644 index 00000000..efc2ec84 Binary files /dev/null and b/full-stack-asset-transfer-guide/applications/conga-cards/assets/block-norris.png differ diff --git a/full-stack-asset-transfer-guide/applications/conga-cards/assets/blockbert.png b/full-stack-asset-transfer-guide/applications/conga-cards/assets/blockbert.png new file mode 100644 index 00000000..43ff9b46 Binary files /dev/null and b/full-stack-asset-transfer-guide/applications/conga-cards/assets/blockbert.png differ diff --git a/full-stack-asset-transfer-guide/applications/conga-cards/assets/count-blockula.png b/full-stack-asset-transfer-guide/applications/conga-cards/assets/count-blockula.png new file mode 100644 index 00000000..e49fb0cc Binary files /dev/null and b/full-stack-asset-transfer-guide/applications/conga-cards/assets/count-blockula.png differ diff --git a/full-stack-asset-transfer-guide/applications/conga-cards/assets/darth-conga.png b/full-stack-asset-transfer-guide/applications/conga-cards/assets/darth-conga.png new file mode 100644 index 00000000..7e182204 Binary files /dev/null and b/full-stack-asset-transfer-guide/applications/conga-cards/assets/darth-conga.png differ diff --git a/full-stack-asset-transfer-guide/applications/conga-cards/assets/no-pun-intended.png b/full-stack-asset-transfer-guide/applications/conga-cards/assets/no-pun-intended.png new file mode 100644 index 00000000..24018022 Binary files /dev/null and b/full-stack-asset-transfer-guide/applications/conga-cards/assets/no-pun-intended.png differ diff --git a/full-stack-asset-transfer-guide/applications/conga-cards/assets/template.png b/full-stack-asset-transfer-guide/applications/conga-cards/assets/template.png new file mode 100644 index 00000000..b04d8126 Binary files /dev/null and b/full-stack-asset-transfer-guide/applications/conga-cards/assets/template.png differ diff --git a/full-stack-asset-transfer-guide/applications/conga-cards/hooks/captain-hook.json b/full-stack-asset-transfer-guide/applications/conga-cards/hooks/captain-hook.json new file mode 100644 index 00000000..ce0a15e0 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/conga-cards/hooks/captain-hook.json @@ -0,0 +1,5 @@ +{ + "content": "Tail recursion is its own reward.", + "avatar_url": "https://avatars.githubusercontent.com/u/49026922?s=200&v=4", + "username": "Conga-Bot" +} \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/applications/conga-cards/images/interaction.png b/full-stack-asset-transfer-guide/applications/conga-cards/images/interaction.png new file mode 100644 index 00000000..e7440050 Binary files /dev/null and b/full-stack-asset-transfer-guide/applications/conga-cards/images/interaction.png differ diff --git a/full-stack-asset-transfer-guide/applications/conga-cards/package.json b/full-stack-asset-transfer-guide/applications/conga-cards/package.json new file mode 100644 index 00000000..4123fb47 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/conga-cards/package.json @@ -0,0 +1,37 @@ +{ + "name": "conga-cards", + "version": "1.0.0", + "description": "Conga Cards client application", + "main": "dist/index.js", + "typings": "dist/index.d.ts", + "engines": { + "node": ">=16.13.0" + }, + "scripts": { + "build": "tsc", + "build:watch": "tsc -w", + "lint": "eslint ./src", + "prepare": "npm run build", + "pretest": "npm run lint", + "start": "node ./dist/app", + "test": "" + }, + "author": "Hyperledger", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "~1.6.7", + "@hyperledger/fabric-gateway": "^1.1.0", + "@hyperledger/fabric-protos": "^0.1.0-dev.2300102001.1", + "axios": "^0.27.2", + "source-map-support": "^0.5.21" + }, + "devDependencies": { + "@tsconfig/node16": "^1.0.3", + "@types/node": "^16.11.46", + "@types/source-map-support": "^0.5.6", + "@typescript-eslint/eslint-plugin": "^5.22.0", + "@typescript-eslint/parser": "^5.22.0", + "eslint": "^8.14.0", + "typescript": "~4.7.4" + } +} diff --git a/full-stack-asset-transfer-guide/applications/conga-cards/src/app.ts b/full-stack-asset-transfer-guide/applications/conga-cards/src/app.ts new file mode 100644 index 00000000..acbdf3a9 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/conga-cards/src/app.ts @@ -0,0 +1,54 @@ +/* + * Copyright contributors to the Hyperledgendary Full Stack Asset Transfer Guide project + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as sourceMapSupport from 'source-map-support'; +sourceMapSupport.install(); + +import { Command, commands } from './commands'; +import { newGatewayConnection, newGrpcConnection } from './connect'; +import { ExpectedError } from './expectedError'; + +async function main(): Promise { + const commandName = process.argv[2]; + const args = process.argv.slice(3); + + const command = commands[commandName]; + if (!command) { + printUsage(); + throw new Error(`Unknown command: ${commandName}`); + } + + await runCommand(command, args); +} + +async function runCommand(command: Command, args: string[]): Promise { + const client = await newGrpcConnection(); + try { + const gateway = await newGatewayConnection(client); + try { + await command(gateway, args); + } finally { + gateway.close(); + } + } finally { + client.close(); + } +} + +function printUsage(): void { + console.log('Arguments: [ ...]'); + console.log('Available commands:'); + console.log(`\t${Object.keys(commands).sort().join('\n\t')}`); +} + +main().catch(error => { + if (error instanceof ExpectedError) { + console.log(error); + } else { + console.error('\nUnexpected application error:', error); + process.exitCode = 1; + } +}); diff --git a/full-stack-asset-transfer-guide/applications/conga-cards/src/commands/create.ts b/full-stack-asset-transfer-guide/applications/conga-cards/src/commands/create.ts new file mode 100644 index 00000000..3d823351 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/conga-cards/src/commands/create.ts @@ -0,0 +1,26 @@ +/* + * Copyright contributors to the Hyperledgendary Full Stack Asset Transfer Guide project + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Gateway } from '@hyperledger/fabric-gateway'; +import { CHAINCODE_NAME, CHANNEL_NAME } from '../config'; +import { AssetTransfer } from '../contract'; +import { assertAllDefined } from '../utils'; + +export default async function main(gateway: Gateway, args: string[]): Promise { + const [assetId, owner, color] = assertAllDefined([args[0], args[1], args[2]], 'Arguments: '); + + const network = gateway.getNetwork(CHANNEL_NAME); + const contract = network.getContract(CHAINCODE_NAME); + + const smartContract = new AssetTransfer(contract); + await smartContract.createAsset({ + ID: assetId, + Owner: owner, + Color: color, + Size: 1, + AppraisedValue: 1, + }); +} diff --git a/full-stack-asset-transfer-guide/applications/conga-cards/src/commands/delete.ts b/full-stack-asset-transfer-guide/applications/conga-cards/src/commands/delete.ts new file mode 100644 index 00000000..1026a006 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/conga-cards/src/commands/delete.ts @@ -0,0 +1,20 @@ +/* + * Copyright contributors to the Hyperledgendary Full Stack Asset Transfer Guide project + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Gateway } from '@hyperledger/fabric-gateway'; +import { CHAINCODE_NAME, CHANNEL_NAME } from '../config'; +import { AssetTransfer } from '../contract'; +import { assertDefined } from '../utils'; + +export default async function main(gateway: Gateway, args: string[]): Promise { + const assetId = assertDefined(args[0], 'Arguments: '); + + const network = gateway.getNetwork(CHANNEL_NAME); + const contract = network.getContract(CHAINCODE_NAME); + + const smartContract = new AssetTransfer(contract); + await smartContract.deleteAsset(assetId); +} diff --git a/full-stack-asset-transfer-guide/applications/conga-cards/src/commands/discord.ts b/full-stack-asset-transfer-guide/applications/conga-cards/src/commands/discord.ts new file mode 100644 index 00000000..6e06688f --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/conga-cards/src/commands/discord.ts @@ -0,0 +1,175 @@ +/* + * Copyright contributors to the Hyperledgendary Full Stack Asset Transfer Guide project + * + * SPDX-License-Identifier: Apache-2.0 + */ +import { ChaincodeEvent, checkpointers, Gateway } from '@hyperledger/fabric-gateway'; +import * as path from 'path'; +import { CHAINCODE_NAME, CHANNEL_NAME } from '../config'; +import { Asset } from '../contract'; +import { assertDefined } from '../utils'; +import { TextDecoder } from 'util'; + +const axios = require('axios'); +const utf8Decoder = new TextDecoder(); + +const checkpointFile = path.resolve(process.env.CHECKPOINT_FILE ?? 'checkpoint.json'); + +const startBlock = BigInt(0); + +// Webhook / bot display names for create +const createUsername = 'King Conga'; +const createAvatar = 'https://avatars.githubusercontent.com/u/49026922?s=200&v=4'; + +const transferUsername = createUsername; +const transferAvatar = createAvatar; + +const deleteUsername = createUsername; +const deleteAvatar = createAvatar; + +export default async function main(gateway: Gateway): Promise { + const webhookURL = assertDefined(process.env['WEBHOOK_URL'], () => { return 'WEBHOOK_URL is not defined in the env' }); + const network = gateway.getNetwork(CHANNEL_NAME); + const checkpointer = await checkpointers.file(checkpointFile); + + console.log(`Connecting to #discord webhook ${webhookURL}`); + console.log(`Starting event discording from block ${checkpointer.getBlockNumber() ?? startBlock}`); + console.log('Last processed transaction ID within block:', checkpointer.getTransactionId()); + + const events = await network.getChaincodeEvents(CHAINCODE_NAME, { + checkpoint: checkpointer, + startBlock, // Used only if there is no checkpoint block number + }); + + try { + for await (const event of events) { + await discord(webhookURL, event); + + await checkpointer.checkpointChaincodeEvent(event) + + // Slow down the event iterator to avoid rate limitations imposed by discord. + // This could be improved to catch the "try again" error from discord and resubmit the event before + // checkpointing the iterator. + await new Promise(resolve => setTimeout(resolve, 1000)); + } + } finally { + events.close(); + } +} + +// Relay a quick message to the discord webhook to indicate the transaction has been processed. +async function discord(webhookURL: string, event: ChaincodeEvent): Promise { + + const asset = parseJson(event.payload); + console.log(`\n<-- Chaincode event received: ${event.eventName}: `, asset); + + // const message = boringLogMessage(event, asset); + const message = splashyShoutMessage(event, asset); + + deliverMessage(webhookURL, message); +} + +// Send an event to a discord webhook. +async function deliverMessage(webhookURL: string, message: any): Promise { + console.log('--> Sending to discord webhook: ' + webhookURL); + console.log(JSON.stringify(message)); + + try { + await axios.post(webhookURL, message); + + } catch (error) { + console.log(error); + } +} + +function boringLogMessage(event: ChaincodeEvent, asset: Asset): any { + const owner = ownerNickname(asset); + const text = format(event, asset, owner); + + return { + username: 'Ledger Troll', + // avatar_url: avatarURL, + content: text, + } +} + +function splashyShoutMessage(event: ChaincodeEvent, asset: Asset): any { + + const owner:any = JSON.parse(asset.Owner); + + if (event.eventName == 'CreateAsset') { + return { + username: createUsername, + avatar_url: createAvatar, + content: `${bold(owner.user)} has caught a wild ${bold(asset.ID)}!` + getRandomEmoji(), + embeds: [ + { + title: `${owner.org}`, + image: { + // an actual conga comic (sometimes png and sometimes jpg) + // url: `https://congacomic.github.io/assets/img/blockheight-${offset}.png` + url: `https://github.com/hyperledgendary/full-stack-asset-transfer-guide/blob/main/applications/conga-cards/assets/${asset.ID}.png?raw=true` + } + } + ], + }; + } + + if (event.eventName == 'TransferAsset') { + return { + username: transferUsername, + avatar_url: transferAvatar, + content: `${bold(owner.user)} is now the owner of ${bold(asset.ID)}: ✈️ ${snippet(JSON.stringify(asset, null, 2))}`, + }; + } + + if (event.eventName == 'DeletaAsset') { + return { + username: deleteUsername, + avatar_url: deleteAvatar, + content: `${bold(asset.ID)} ran away from ${bold(owner.user)}! 😮`, + }; + } + + return {}; +} + +function format(event: ChaincodeEvent, asset: Asset, owner: string): string { + return `${quote(event.transactionId)} ${italic(event.eventName)}(${bold(asset.ID)}, ${owner})`; +} + +function parseJson(jsonBytes: Uint8Array): Asset { + const json = utf8Decoder.decode(jsonBytes); + return JSON.parse(json); +} + +function quote(s: string): string { + return `\`${s}\`` +} + +function italic(s: string): string { + return `_${s}_`; +} + +function bold(s: string) { + return `**${s}**`; +} + +function snippet(s: string) { + return "```" + s + "```"; +} + +function ownerNickname(asset: Asset): string { + const owner:any = JSON.parse(asset.Owner); + + return `${owner.org}, ${owner.user}`; +} + +// https://github.com/discord/discord-example-app/blob/main/utils.js#L43 +// Simple method that returns a random emoji from list +function getRandomEmoji(): string { + const emojiList = ['😭','😄','😌','🤓','😎','😤','🤖','😶‍🌫', '🌏','📸','💿','👋','🌊','✨']; + return emojiList[Math.floor(Math.random() * emojiList.length)]; +} + + diff --git a/full-stack-asset-transfer-guide/applications/conga-cards/src/commands/getAllAssets.ts b/full-stack-asset-transfer-guide/applications/conga-cards/src/commands/getAllAssets.ts new file mode 100644 index 00000000..9b095dc4 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/conga-cards/src/commands/getAllAssets.ts @@ -0,0 +1,20 @@ +/* + * Copyright contributors to the Hyperledgendary Full Stack Asset Transfer Guide project + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Gateway } from '@hyperledger/fabric-gateway'; +import { CHAINCODE_NAME, CHANNEL_NAME } from '../config'; +import { AssetTransfer } from '../contract'; + +export default async function main(gateway: Gateway): Promise { + const network = gateway.getNetwork(CHANNEL_NAME); + const contract = network.getContract(CHAINCODE_NAME); + + const smartContract = new AssetTransfer(contract); + const assets = await smartContract.getAllAssets(); + + const assetsJson = JSON.stringify(assets, undefined, 2); + assetsJson.split('\n').forEach(line => console.log(line)); // Write line-by-line to avoid truncation +} diff --git a/full-stack-asset-transfer-guide/applications/conga-cards/src/commands/index.ts b/full-stack-asset-transfer-guide/applications/conga-cards/src/commands/index.ts new file mode 100644 index 00000000..aee38680 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/conga-cards/src/commands/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright contributors to the Hyperledgendary Full Stack Asset Transfer Guide project + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Gateway } from '@hyperledger/fabric-gateway'; +import create from './create'; +import deleteCommand from './delete'; +import discord from './discord'; +import getAllAssets from './getAllAssets'; +import read from './read'; +import transfer from './transfer'; + +export type Command = (gateway: Gateway, args: string[]) => Promise; + +export const commands: Record = { + create, + delete: deleteCommand, + discord, + getAllAssets, + read, + transfer, +}; diff --git a/full-stack-asset-transfer-guide/applications/conga-cards/src/commands/read.ts b/full-stack-asset-transfer-guide/applications/conga-cards/src/commands/read.ts new file mode 100644 index 00000000..a101fb07 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/conga-cards/src/commands/read.ts @@ -0,0 +1,23 @@ +/* + * Copyright contributors to the Hyperledgendary Full Stack Asset Transfer Guide project + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Gateway } from '@hyperledger/fabric-gateway'; +import { CHAINCODE_NAME, CHANNEL_NAME } from '../config'; +import { AssetTransfer } from '../contract'; +import { assertDefined } from '../utils'; + +export default async function main(gateway: Gateway, args: string[]): Promise { + const assetId = assertDefined(args[0], 'Arguments: '); + + const network = gateway.getNetwork(CHANNEL_NAME); + const contract = network.getContract(CHAINCODE_NAME); + + const smartContract = new AssetTransfer(contract); + const asset = await smartContract.readAsset(assetId); + + const assetsJson = JSON.stringify(asset, undefined, 2); + console.log(assetsJson); +} diff --git a/full-stack-asset-transfer-guide/applications/conga-cards/src/commands/transfer.ts b/full-stack-asset-transfer-guide/applications/conga-cards/src/commands/transfer.ts new file mode 100644 index 00000000..bdfd532e --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/conga-cards/src/commands/transfer.ts @@ -0,0 +1,20 @@ +/* + * Copyright contributors to the Hyperledgendary Full Stack Asset Transfer Guide project + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Gateway } from '@hyperledger/fabric-gateway'; +import { CHAINCODE_NAME, CHANNEL_NAME } from '../config'; +import { AssetTransfer } from '../contract'; +import { assertAllDefined } from '../utils'; + +export default async function main(gateway: Gateway, args: string[]): Promise { + const [assetId, newOwner, newOwnerOrg] = assertAllDefined([args[0], args[1], args[2]], 'Arguments: '); + + const network = gateway.getNetwork(CHANNEL_NAME); + const contract = network.getContract(CHAINCODE_NAME); + + const smartContract = new AssetTransfer(contract); + await smartContract.transferAsset(assetId, newOwner, newOwnerOrg); +} diff --git a/full-stack-asset-transfer-guide/applications/conga-cards/src/config.ts b/full-stack-asset-transfer-guide/applications/conga-cards/src/config.ts new file mode 100644 index 00000000..e6b45f7f --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/conga-cards/src/config.ts @@ -0,0 +1,45 @@ +/* + * Copyright contributors to the Hyperledgendary Full Stack Asset Transfer Guide project + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { resolve } from 'path'; +import { assertDefined } from './utils'; + +export const GATEWAY_ENDPOINT = assertEnv('ENDPOINT'); +export const MSP_ID = assertEnv('MSP_ID'); +export const CLIENT_CERT_PATH = resolve(assertEnv('CERTIFICATE')); +export const PRIVATE_KEY_PATH = resolve(assertEnv('PRIVATE_KEY')); +export const TLS_CERT_PATH = resolvePathIfPresent(process.env.TLS_CERT); +export const CHANNEL_NAME = process.env.CHANNEL_NAME ?? 'mychannel'; +export const CHAINCODE_NAME = process.env.CHAINCODE_NAME ?? 'asset-transfer'; + +// Gateway peer SSL host name override. +export const HOST_ALIAS = process.env.HOST_ALIAS; + +function assertEnv(envName: string): string { + return assertDefined(process.env[envName], () => { + console.error('The following environment variables must be set:' + + '\n ENDPOINT - Endpoint address of the gateway service' + + '\n MSP_ID - User\'s organization Member Services Provider ID' + + '\n CERTIFICATE - User\'s certificate file' + + '\n PRIVATE_KEY - User\'s private key file' + + '\n' + + '\nThe following environment variables are optional:' + + '\n CHANNEL_NAME - Channel to which the chaincode is deployed' + + '\n CHAINCODE_NAME - Channel to which the chaincode is deployed' + + '\n TLS_CERT - TLS CA root certificate (only if using TLS and private CA)' + + '\n HOST_ALIAS - TLS hostname override (only if TLS cert does not match endpoint)' + + '\n'); + return `Environment variable ${envName} not set`; + }); +} + +function resolvePathIfPresent(path: string | undefined): string | undefined { + if (path == undefined) { + return undefined; + } + + return resolve(path); +} diff --git a/full-stack-asset-transfer-guide/applications/conga-cards/src/connect.ts b/full-stack-asset-transfer-guide/applications/conga-cards/src/connect.ts new file mode 100644 index 00000000..a6bc489c --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/conga-cards/src/connect.ts @@ -0,0 +1,66 @@ +/* + * Copyright contributors to the Hyperledgendary Full Stack Asset Transfer Guide project + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as grpc from '@grpc/grpc-js'; +import { connect, Gateway, Identity, Signer, signers } from '@hyperledger/fabric-gateway'; +import * as crypto from 'crypto'; +import * as fs from 'fs'; +import * as path from 'path'; +import { CLIENT_CERT_PATH, GATEWAY_ENDPOINT, HOST_ALIAS, MSP_ID, PRIVATE_KEY_PATH, TLS_CERT_PATH } from './config'; + +export async function newGrpcConnection(): Promise { + if (TLS_CERT_PATH) { + const tlsRootCert = await fs.promises.readFile(TLS_CERT_PATH); + const tlsCredentials = grpc.credentials.createSsl(tlsRootCert); + return new grpc.Client(GATEWAY_ENDPOINT, tlsCredentials, newGrpcClientOptions()); + } + + return new grpc.Client(GATEWAY_ENDPOINT, grpc.ChannelCredentials.createInsecure()); +} + +function newGrpcClientOptions(): grpc.ClientOptions { + const result: grpc.ClientOptions = {}; + if (HOST_ALIAS) { + result['grpc.ssl_target_name_override'] = HOST_ALIAS; // Only required if server TLS cert does not match the endpoint address we use + } + return result; +} + +export async function newGatewayConnection(client: grpc.Client): Promise { + return connect({ + client, + identity: await newIdentity(), + signer: await newSigner(), + // Default timeouts for different gRPC calls + evaluateOptions: () => { + return { deadline: Date.now() + 5000 }; // 5 seconds + }, + endorseOptions: () => { + return { deadline: Date.now() + 15000 }; // 15 seconds + }, + submitOptions: () => { + return { deadline: Date.now() + 5000 }; // 5 seconds + }, + commitStatusOptions: () => { + return { deadline: Date.now() + 60000 }; // 1 minute + }, + }); +} + +async function newIdentity(): Promise { + const certPath = path.resolve(CLIENT_CERT_PATH); + const credentials = await fs.promises.readFile(certPath); + + return { mspId: MSP_ID, credentials }; +} + +async function newSigner(): Promise { + const keyPath = path.resolve(PRIVATE_KEY_PATH); + const privateKeyPem = await fs.promises.readFile(keyPath); + const privateKey = crypto.createPrivateKey(privateKeyPem); + + return signers.newPrivateKeySigner(privateKey); +} diff --git a/full-stack-asset-transfer-guide/applications/conga-cards/src/contract.ts b/full-stack-asset-transfer-guide/applications/conga-cards/src/contract.ts new file mode 100644 index 00000000..3dcb92b3 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/conga-cards/src/contract.ts @@ -0,0 +1,104 @@ +/* + * Copyright contributors to the Hyperledgendary Full Stack Asset Transfer Guide project + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CommitError, Contract, StatusCode } from '@hyperledger/fabric-gateway'; +import { TextDecoder } from 'util'; + +const RETRIES = 2; + +const utf8Decoder = new TextDecoder(); + +export interface Asset { + ID: string; + Color: string; + Size: number; + Owner: string; + AppraisedValue: number; +} + +export type AssetCreate = Omit & Partial; +export type AssetUpdate = Pick & Partial>; + +/** + * AssetTransfer presents the smart contract in a form appropriate to the business application. Internally it uses the + * Fabric Gateway client API to invoke transaction functions, and deals with the translation between the business + * application and API representation of parameters and return values. + */ +export class AssetTransfer { + readonly #contract: Contract; + + constructor(contract: Contract) { + this.#contract = contract; + } + + async createAsset(asset: AssetCreate): Promise { + await this.#contract.submit('CreateAsset', { + arguments: [JSON.stringify(asset)], + }); + } + + async getAllAssets(): Promise { + const result = await this.#contract.evaluate('GetAllAssets'); + if (result.length === 0) { + return []; + } + + return JSON.parse(utf8Decoder.decode(result)) as Asset[]; + } + + async readAsset(id: string): Promise { + const result = await this.#contract.evaluate('ReadAsset', { + arguments: [id], + }); + return JSON.parse(utf8Decoder.decode(result)) as Asset; + } + + async updateAsset(asset: AssetUpdate): Promise { + await submitWithRetry(() => this.#contract.submit('UpdateAsset', { + arguments: [JSON.stringify(asset)], + })); + } + + async deleteAsset(id: string): Promise { + await submitWithRetry(() => this.#contract.submit('DeleteAsset', { + arguments: [id], + })); + } + + async assetExists(id: string): Promise { + const result = await this.#contract.evaluate('AssetExists', { + arguments: [id], + }); + return utf8Decoder.decode(result).toLowerCase() === 'true'; + } + + async transferAsset(id: string, newOwner: string, newOwnerOrg: string): Promise { + await submitWithRetry(() => this.#contract.submit('TransferAsset', { + arguments: [id, newOwner, newOwnerOrg], + })); + } +} + +async function submitWithRetry(submit: () => Promise): Promise { + let lastError: unknown | undefined; + + for (let retryCount = 0; retryCount < RETRIES; retryCount++) { + try { + return await submit(); + } catch (err: unknown) { + lastError = err; + if (err instanceof CommitError) { + // Transaction failed validation and did not update the ledger. Handle specific transaction validation codes. + if (err.code === StatusCode.MVCC_READ_CONFLICT) { + continue; // Retry + } + } + break; // Failure -- don't retry + } + } + + throw lastError; +} diff --git a/full-stack-asset-transfer-guide/applications/conga-cards/src/expectedError.ts b/full-stack-asset-transfer-guide/applications/conga-cards/src/expectedError.ts new file mode 100644 index 00000000..3b1466b4 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/conga-cards/src/expectedError.ts @@ -0,0 +1,12 @@ +/* + * Copyright contributors to the Hyperledgendary Full Stack Asset Transfer Guide project + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export class ExpectedError extends Error { + constructor(message?: string) { + super(message); + this.name = ExpectedError.name; + } +} diff --git a/full-stack-asset-transfer-guide/applications/conga-cards/src/utils.ts b/full-stack-asset-transfer-guide/applications/conga-cards/src/utils.ts new file mode 100644 index 00000000..e9b1e594 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/conga-cards/src/utils.ts @@ -0,0 +1,75 @@ +/* + * Copyright contributors to the Hyperledgendary Full Stack Asset Transfer Guide project + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { inspect, TextDecoder } from 'util'; + +const utf8Decoder = new TextDecoder(); + +/** + * Pick a random element from an array. + * @param values Candidate elements. + */ +export function randomElement(values: T[]): T { + return values[randomInt(values.length)]; +} + +/** + * Generate a random integer in the range 0 to max - 1. + * @param max Maximum value (exclusive). + */ +export function randomInt(max: number): number { + return Math.floor(Math.random() * max); +} + +/** + * Pick a random element from an array, excluding the current value. + * @param values Candidate elements. + * @param currentValue Value to avoid. + */ +export function differentElement(values: T[], currentValue: T): T { + const candidateValues = values.filter(value => value !== currentValue); + return randomElement(candidateValues); +} + +/** + * Wait for all promises to complete, then throw an Error only if any of the promises were rejected. + * @param promises Promises to be awaited. + */ +export async function allFulfilled(promises: Promise[]): Promise { + const results = await Promise.allSettled(promises); + const failures = results + .map(result => result.status === 'rejected' && result.reason as unknown) + .filter(reason => !!reason) + .map(reason => inspect(reason)); + + if (failures.length > 0) { + const failMessages = '- ' + failures.join('\n- '); + throw new Error(`${failures.length} failures:\n${failMessages}\n`); + } +} + +export type PrintView = { + [K in keyof T]: T[K] extends Uint8Array ? string : T[K]; +}; + +export function printable(event: T): PrintView { + return Object.fromEntries( + Object.entries(event).map(([k, v]) => [k, v instanceof Uint8Array ? utf8Decoder.decode(v) : v]) + ) as PrintView; +} + +export function assertAllDefined(values: (T | undefined)[], message: string | (() => string)): T[] { + values.forEach(value => assertDefined(value, message)); + return values as T[]; +} + +export function assertDefined(value: T | undefined, message: string | (() => string)): T { + if (value == undefined) { + throw new Error(typeof message === 'string' ? message : message()); + } + + return value; +} diff --git a/full-stack-asset-transfer-guide/applications/conga-cards/tsconfig.json b/full-stack-asset-transfer-guide/applications/conga-cards/tsconfig.json new file mode 100644 index 00000000..9de9d030 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/conga-cards/tsconfig.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@tsconfig/node16/tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "rootDir": "src", + "noUnusedLocals": false, + "noImplicitReturns": true + }, + "include": [ + "src/" + ], + "exclude": [ + "src/**/*.spec.ts" + ] +} diff --git a/full-stack-asset-transfer-guide/applications/frontend/.browserslistrc b/full-stack-asset-transfer-guide/applications/frontend/.browserslistrc new file mode 100644 index 00000000..4f9ac269 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/frontend/.browserslistrc @@ -0,0 +1,16 @@ +# This file is used by the build system to adjust CSS and JS output to support the specified browsers below. +# For additional information regarding the format and rule options, please see: +# https://github.com/browserslist/browserslist#queries + +# For the full list of supported browsers by the Angular framework, please see: +# https://angular.io/guide/browser-support + +# You can see what browsers were selected by your queries by running: +# npx browserslist + +last 1 Chrome version +last 1 Firefox version +last 2 Edge major versions +last 2 Safari major versions +last 2 iOS major versions +Firefox ESR diff --git a/full-stack-asset-transfer-guide/applications/frontend/.editorconfig b/full-stack-asset-transfer-guide/applications/frontend/.editorconfig new file mode 100644 index 00000000..59d9a3a3 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/frontend/.editorconfig @@ -0,0 +1,16 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/full-stack-asset-transfer-guide/applications/frontend/.gitignore b/full-stack-asset-transfer-guide/applications/frontend/.gitignore new file mode 100644 index 00000000..0711527e --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/frontend/.gitignore @@ -0,0 +1,42 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/full-stack-asset-transfer-guide/applications/frontend/Dockerfile b/full-stack-asset-transfer-guide/applications/frontend/Dockerfile new file mode 100644 index 00000000..b3d16aab --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/frontend/Dockerfile @@ -0,0 +1,11 @@ +# FROM node:14-alpine3.14 AS build +# WORKDIR /app +# COPY package.json /app +# RUN npm install +# CMD npm run build + +# COPY . /app +# CMD [ "npm","run","prod" ] + +FROM nginx:alpine +COPY /dist/frontend /usr/share/nginx/html \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/applications/frontend/README.md b/full-stack-asset-transfer-guide/applications/frontend/README.md new file mode 100644 index 00000000..5d96edcf --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/frontend/README.md @@ -0,0 +1,18 @@ +local build + +npm install +npm start + + +k8s deployment +Step-1 Prodction build +ng build +Step-2 Build docker image & tag +docker build -t localhost:5000/frontend . +Step-3 Push image to local registary +docker push localhost:5000/frontend +Step-4 deploy to k8s +kubectl apply -f deployment.yaml -n test-network + +Step-5 Navigate to frontend using below url +https://frontend.localho.st/ \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/applications/frontend/angular.json b/full-stack-asset-transfer-guide/applications/frontend/angular.json new file mode 100644 index 00000000..63df0214 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/frontend/angular.json @@ -0,0 +1,112 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "frontend": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss" + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "dist/frontend", + "index": "src/index.html", + "main": "src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", + "src/styles.scss" + ], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kb", + "maximumError": "1mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ], + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "outputHashing": "all" + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "frontend:build:production" + }, + "development": { + "browserTarget": "frontend:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "frontend:build" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "tsconfig.spec.json", + "karmaConfig": "karma.conf.js", + "inlineStyleLanguage": "scss", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", + "src/styles.scss" + ], + "scripts": [] + } + } + } + } + }, + "cli": { + "analytics": "2231f71d-331b-4f3d-8391-0f5998405c1e" + } +} diff --git a/full-stack-asset-transfer-guide/applications/frontend/karma.conf.js b/full-stack-asset-transfer-guide/applications/frontend/karma.conf.js new file mode 100644 index 00000000..36356397 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/frontend/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage/frontend'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/full-stack-asset-transfer-guide/applications/frontend/package.json b/full-stack-asset-transfer-guide/applications/frontend/package.json new file mode 100644 index 00000000..29120e1f --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/frontend/package.json @@ -0,0 +1,40 @@ +{ + "name": "frontend", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test" + }, + "private": true, + "dependencies": { + "@angular/animations": "^14.1.0", + "@angular/cdk": "^14.2.0", + "@angular/common": "^14.1.0", + "@angular/compiler": "^14.1.0", + "@angular/core": "^14.1.0", + "@angular/forms": "^14.1.0", + "@angular/material": "^14.2.0", + "@angular/platform-browser": "^14.1.0", + "@angular/platform-browser-dynamic": "^14.1.0", + "@angular/router": "^14.1.0", + "rxjs": "~7.5.0", + "tslib": "^2.3.0", + "zone.js": "~0.11.4" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^14.1.1", + "@angular/cli": "~14.1.1", + "@angular/compiler-cli": "^14.1.0", + "@types/jasmine": "~4.0.0", + "jasmine-core": "~4.2.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.1.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.0.0", + "typescript": "~4.7.2" + } +} \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/applications/frontend/screenshots/Create.png b/full-stack-asset-transfer-guide/applications/frontend/screenshots/Create.png new file mode 100644 index 00000000..2603c4c8 Binary files /dev/null and b/full-stack-asset-transfer-guide/applications/frontend/screenshots/Create.png differ diff --git a/full-stack-asset-transfer-guide/applications/frontend/screenshots/list.png b/full-stack-asset-transfer-guide/applications/frontend/screenshots/list.png new file mode 100644 index 00000000..807721ee Binary files /dev/null and b/full-stack-asset-transfer-guide/applications/frontend/screenshots/list.png differ diff --git a/full-stack-asset-transfer-guide/applications/frontend/src/app/app-routing.module.ts b/full-stack-asset-transfer-guide/applications/frontend/src/app/app-routing.module.ts new file mode 100644 index 00000000..02972627 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/frontend/src/app/app-routing.module.ts @@ -0,0 +1,10 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +const routes: Routes = []; + +@NgModule({ + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule] +}) +export class AppRoutingModule { } diff --git a/full-stack-asset-transfer-guide/applications/frontend/src/app/app.component.html b/full-stack-asset-transfer-guide/applications/frontend/src/app/app.component.html new file mode 100644 index 00000000..252123ea --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/frontend/src/app/app.component.html @@ -0,0 +1,66 @@ +
Asset (Create,Edit,Delete,Transfer)
+
+
+ Assets + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
No. {{ (i+1) + (paginator.pageIndex * + paginator.pageSize) }} Id {{element.ID}} Color {{element.Color}} Owner {{element.Owner}} Appraised Value {{element.AppraisedValue}} Size {{element.Size}} Trasnfer + arrow_forward + Edit + edit + Delete + delete +
+ + +
+
\ No newline at end of file diff --git a/full-stack-asset-transfer-guide/applications/frontend/src/app/app.component.scss b/full-stack-asset-transfer-guide/applications/frontend/src/app/app.component.scss new file mode 100644 index 00000000..acd75952 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/frontend/src/app/app.component.scss @@ -0,0 +1,6 @@ +.layout{ + margin: 40px 10%; +} +.search{ + float: right; +} \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/applications/frontend/src/app/app.component.spec.ts b/full-stack-asset-transfer-guide/applications/frontend/src/app/app.component.spec.ts new file mode 100644 index 00000000..74b5b3eb --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/frontend/src/app/app.component.spec.ts @@ -0,0 +1,35 @@ +import { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { AppComponent } from './app.component'; + +describe('AppComponent', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + RouterTestingModule + ], + declarations: [ + AppComponent + ], + }).compileComponents(); + }); + + it('should create the app', () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + + it(`should have as title 'frontend'`, () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + expect(app.title).toEqual('frontend'); + }); + + it('should render title', () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('.content span')?.textContent).toContain('frontend app is running!'); + }); +}); diff --git a/full-stack-asset-transfer-guide/applications/frontend/src/app/app.component.ts b/full-stack-asset-transfer-guide/applications/frontend/src/app/app.component.ts new file mode 100644 index 00000000..c26dc0c1 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/frontend/src/app/app.component.ts @@ -0,0 +1,82 @@ +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Component, OnInit, ViewChild } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { MatSort } from '@angular/material/sort'; +import { MatTableDataSource } from '@angular/material/table'; +import { AssetDialogComponent } from './asset-dialog/asset-dialog.component'; +import { URLS } from './urls'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'] +}) +export class AppComponent implements OnInit { + + @ViewChild(MatSort, { static: false }) sort!: MatSort ; + @ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator; + dataSource = new MatTableDataSource(); + httpOptions; + title = 'frontend'; + assets = []; + displayedColumns: String[] = ["position", "id",'owner','color','appraisedValue',"size" ,'transfer','edit','delete']; + constructor(private _http: HttpClient, private dialog: MatDialog,private snackBar:MatSnackBar) { + this.httpOptions = { + headers: new HttpHeaders({ + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*' + + }) + } + this._http.get(URLS.LIST,this.httpOptions).subscribe(data => { + this.dataSource.data = data; + }) + } + ngOnInit() { + + } + editRow(asset:any,index:number){ + const dialogRef = this.dialog.open(AssetDialogComponent, { + width: '500px', height: '100vh',position:{right:'0'},data:asset + + }); + dialogRef.afterClosed().subscribe(result => { + if (result) { + this.dataSource.data[index]=(result) + this.dataSource._updateChangeSubscription(); + } + }); + } + delete(asset:any,index:number){ + this._http.post(URLS.DELETE,JSON.stringify({"id":asset.ID}),this.httpOptions).subscribe((data:any) => { + console.log(data); + this.dataSource.data.splice(index,1) + this.dataSource._updateChangeSubscription(); + this.snackBar.open(asset.ID+ ' deleted', '', {duration:1000 + }); + }) + } + addNewAsset(){ + const dialogRef = this.dialog.open(AssetDialogComponent, { + width: '500px', height: '100vh',position:{right:'0'}, + + }); + dialogRef.afterClosed().subscribe(result => { + if (result) { + this.dataSource.data.push(result) + this.dataSource._updateChangeSubscription(); + } + }); + } + applyFilter(event: Event) { + const filterValue = (event.target as HTMLInputElement).value; + this.dataSource.filter = filterValue.trim().toLowerCase(); + } + ngAfterViewInit() { + console.log('after ininti'); + this.dataSource.sort = this.sort; + this.dataSource.paginator = this.paginator; + } +} diff --git a/full-stack-asset-transfer-guide/applications/frontend/src/app/app.module.ts b/full-stack-asset-transfer-guide/applications/frontend/src/app/app.module.ts new file mode 100644 index 00000000..eb0ad4b7 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/frontend/src/app/app.module.ts @@ -0,0 +1,32 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; + +import { AppRoutingModule } from './app-routing.module'; +import { AppComponent } from './app.component'; +import { HttpClientModule } from '@angular/common/http'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { MatTableModule } from '@angular/material/table'; +import { MatInputModule } from '@angular/material/input'; +import { MatButtonModule } from '@angular/material/button'; +import { MatPaginatorModule } from '@angular/material/paginator'; +import { AssetDialogComponent } from './asset-dialog/asset-dialog.component'; +import { MatDialogModule } from '@angular/material/dialog'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { MatIconModule } from '@angular/material/icon'; +import { MatToolbarModule } from '@angular/material/toolbar'; + +@NgModule({ + declarations: [ + AppComponent, + AssetDialogComponent + ], + imports: [ + BrowserModule, + AppRoutingModule, HttpClientModule, BrowserAnimationsModule, MatTableModule,MatInputModule,MatButtonModule,MatPaginatorModule, + MatDialogModule,FormsModule, ReactiveFormsModule,MatSnackBarModule,MatIconModule,MatToolbarModule + ], + providers: [], + bootstrap: [AppComponent] +}) +export class AppModule { } diff --git a/full-stack-asset-transfer-guide/applications/frontend/src/app/asset-dialog/asset-dialog.component.html b/full-stack-asset-transfer-guide/applications/frontend/src/app/asset-dialog/asset-dialog.component.html new file mode 100644 index 00000000..87977d9d --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/frontend/src/app/asset-dialog/asset-dialog.component.html @@ -0,0 +1,51 @@ + + highlight_off + +
+
Asset create/edit
+
+
+
+
+ + Color + + + You must include color +
+
+ + Owner + + + You must include owner +
+
+ + Size + + + You must include size +
+
+ + Appraised Value + + + You must include appraisedValue +
+
+ + +
+
+ + +
+
+
\ No newline at end of file diff --git a/full-stack-asset-transfer-guide/applications/frontend/src/app/asset-dialog/asset-dialog.component.scss b/full-stack-asset-transfer-guide/applications/frontend/src/app/asset-dialog/asset-dialog.component.scss new file mode 100644 index 00000000..f0845f1d --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/frontend/src/app/asset-dialog/asset-dialog.component.scss @@ -0,0 +1,3 @@ +.mat-form-field{ + width: 100% !important; +} \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/applications/frontend/src/app/asset-dialog/asset-dialog.component.spec.ts b/full-stack-asset-transfer-guide/applications/frontend/src/app/asset-dialog/asset-dialog.component.spec.ts new file mode 100644 index 00000000..88866e94 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/frontend/src/app/asset-dialog/asset-dialog.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AssetDialogComponent } from './asset-dialog.component'; + +describe('AssetDialogComponent', () => { + let component: AssetDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ AssetDialogComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AssetDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/full-stack-asset-transfer-guide/applications/frontend/src/app/asset-dialog/asset-dialog.component.ts b/full-stack-asset-transfer-guide/applications/frontend/src/app/asset-dialog/asset-dialog.component.ts new file mode 100644 index 00000000..1480bcc8 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/frontend/src/app/asset-dialog/asset-dialog.component.ts @@ -0,0 +1,74 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { FormGroup, FormControl } from '@angular/forms'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { Data } from '@angular/router'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { URLS } from '../urls'; + +@Component({ + selector: 'app-asset-dialog', + templateUrl: './asset-dialog.component.html', + styleUrls: ['./asset-dialog.component.scss'] +}) +export class AssetDialogComponent implements OnInit { + stores = []; + isEdit: Boolean = false; + aseet= { "Color": "", "AppraisedValue": "", "ID": "", "Size": "", "Owner": "" }; + buttonText = "Save"; + form = new FormGroup({ + Color: new FormControl(''), + AppraisedValue: new FormControl(''), + Owner: new FormControl(''), + Size: new FormControl('') + }); + httpOptions: any; + data:any; + constructor(public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public dialogdata: Data, private snackBar: MatSnackBar, private _http: HttpClient) { + console.log(dialogdata); + this.data=dialogdata; + if (dialogdata) { + var data = dialogdata; + this.aseet.AppraisedValue = data['AppraisedValue']; + this.aseet.Color = data['Color']; + this.aseet.Size = data['Size']; + this.aseet.ID = data['Id']; + var owner = JSON.parse(data['Owner']) + this.aseet.Owner = owner.user; + } + + } + ngOnInit() { + + } + + onSubmit() { + console.log(this.form.value); + this.httpOptions = { + headers: new HttpHeaders({ + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*' + + }) + } + if (this.data) { + var request = { "Color": this.form.value.Color, "AppraisedValue": this.form.value.AppraisedValue, "ID": this.data.ID, "Size": this.form.value.Size, "Owner": { "org": "Org1MSP", "user": this.form.value.Owner } }; + this._http.post(URLS.UPDATE, JSON.stringify(request), this.httpOptions).subscribe((data: any) => { + console.log(data); + var response = { "Color": this.form.value.Color, "AppraisedValue": this.form.value.AppraisedValue, "ID": this.data.ID, "Size": this.form.value.Size, "Owner": JSON.stringify({ "org": "Org1MSP", "user": this.form.value.Owner }) }; + this.dialogRef.close(response) + }) + }else{ + this._http.post(URLS.CREATE, JSON.stringify(this.form.value), this.httpOptions).subscribe((data: any) => { + console.log(data); + var response = { "Color": this.form.value.Color, "AppraisedValue": this.form.value.AppraisedValue, "ID": data.AssetId, "Size": this.form.value.Size, "Owner": JSON.stringify({ "org": "Org1MSP", "user": this.form.value.Owner }) }; + this.dialogRef.close(response) + }) + } + + } + close() { + this.dialogRef.close(); + } +} \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/applications/frontend/src/app/urls.ts b/full-stack-asset-transfer-guide/applications/frontend/src/app/urls.ts new file mode 100644 index 00000000..5e553a3f --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/frontend/src/app/urls.ts @@ -0,0 +1,8 @@ +export class URLS { + // private static IP = "http://localhost:8080/"; + private static IP = "https://restapi.localho.st/"; + public static LIST = URLS.IP + "list"; + public static CREATE = URLS.IP + "create"; + public static UPDATE = URLS.IP + "update"; + public static DELETE = URLS.IP + "delete"; +} \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/applications/frontend/src/assets/.gitkeep b/full-stack-asset-transfer-guide/applications/frontend/src/assets/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/full-stack-asset-transfer-guide/applications/frontend/src/environments/environment.prod.ts b/full-stack-asset-transfer-guide/applications/frontend/src/environments/environment.prod.ts new file mode 100644 index 00000000..3612073b --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/frontend/src/environments/environment.prod.ts @@ -0,0 +1,3 @@ +export const environment = { + production: true +}; diff --git a/full-stack-asset-transfer-guide/applications/frontend/src/environments/environment.ts b/full-stack-asset-transfer-guide/applications/frontend/src/environments/environment.ts new file mode 100644 index 00000000..f56ff470 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/frontend/src/environments/environment.ts @@ -0,0 +1,16 @@ +// This file can be replaced during build by using the `fileReplacements` array. +// `ng build` replaces `environment.ts` with `environment.prod.ts`. +// The list of file replacements can be found in `angular.json`. + +export const environment = { + production: false +}; + +/* + * For easier debugging in development mode, you can import the following file + * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. + * + * This import should be commented out in production mode because it will have a negative impact + * on performance if an error is thrown. + */ +// import 'zone.js/plugins/zone-error'; // Included with Angular CLI. diff --git a/full-stack-asset-transfer-guide/applications/frontend/src/favicon.ico b/full-stack-asset-transfer-guide/applications/frontend/src/favicon.ico new file mode 100644 index 00000000..997406ad Binary files /dev/null and b/full-stack-asset-transfer-guide/applications/frontend/src/favicon.ico differ diff --git a/full-stack-asset-transfer-guide/applications/frontend/src/index.html b/full-stack-asset-transfer-guide/applications/frontend/src/index.html new file mode 100644 index 00000000..41bf45ce --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/frontend/src/index.html @@ -0,0 +1,19 @@ + + + + + Frontend + + + + + + + + + + + + + + diff --git a/full-stack-asset-transfer-guide/applications/frontend/src/main.ts b/full-stack-asset-transfer-guide/applications/frontend/src/main.ts new file mode 100644 index 00000000..c7b673cf --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/frontend/src/main.ts @@ -0,0 +1,12 @@ +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic().bootstrapModule(AppModule) + .catch(err => console.error(err)); diff --git a/full-stack-asset-transfer-guide/applications/frontend/src/polyfills.ts b/full-stack-asset-transfer-guide/applications/frontend/src/polyfills.ts new file mode 100644 index 00000000..429bb9ef --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/frontend/src/polyfills.ts @@ -0,0 +1,53 @@ +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes recent versions of Safari, Chrome (including + * Opera), Edge on the desktop, and iOS and Chrome on mobile. + * + * Learn more in https://angular.io/guide/browser-support + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** + * By default, zone.js will patch all possible macroTask and DomEvents + * user can disable parts of macroTask/DomEvents patch by setting following flags + * because those flags need to be set before `zone.js` being loaded, and webpack + * will put import in the top of bundle, so user need to create a separate file + * in this directory (for example: zone-flags.ts), and put the following flags + * into that file, and then add the following code before importing zone.js. + * import './zone-flags'; + * + * The flags allowed in zone-flags.ts are listed here. + * + * The following flags will work for all browsers. + * + * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame + * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick + * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + * + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + * + * (window as any).__Zone_enable_cross_context_check = true; + * + */ + +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ +import 'zone.js'; // Included with Angular CLI. + + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ diff --git a/full-stack-asset-transfer-guide/applications/frontend/src/styles.scss b/full-stack-asset-transfer-guide/applications/frontend/src/styles.scss new file mode 100644 index 00000000..7e7239a2 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/frontend/src/styles.scss @@ -0,0 +1,4 @@ +/* You can add global styles to this file, and also import other style files */ + +html, body { height: 100%; } +body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } diff --git a/full-stack-asset-transfer-guide/applications/frontend/src/test.ts b/full-stack-asset-transfer-guide/applications/frontend/src/test.ts new file mode 100644 index 00000000..c04c8760 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/frontend/src/test.ts @@ -0,0 +1,26 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context(path: string, deep?: boolean, filter?: RegExp): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting(), +); + +// Then we find all the tests. +const context = require.context('./', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/full-stack-asset-transfer-guide/applications/frontend/tsconfig.app.json b/full-stack-asset-transfer-guide/applications/frontend/tsconfig.app.json new file mode 100644 index 00000000..82d91dc4 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/frontend/tsconfig.app.json @@ -0,0 +1,15 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": [ + "src/main.ts", + "src/polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/full-stack-asset-transfer-guide/applications/frontend/tsconfig.json b/full-stack-asset-transfer-guide/applications/frontend/tsconfig.json new file mode 100644 index 00000000..ff06eae1 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/frontend/tsconfig.json @@ -0,0 +1,32 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "sourceMap": true, + "declaration": false, + "downlevelIteration": true, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "es2020", + "module": "es2020", + "lib": [ + "es2020", + "dom" + ] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/full-stack-asset-transfer-guide/applications/frontend/tsconfig.spec.json b/full-stack-asset-transfer-guide/applications/frontend/tsconfig.spec.json new file mode 100644 index 00000000..092345b0 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/frontend/tsconfig.spec.json @@ -0,0 +1,18 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "src/test.ts", + "src/polyfills.ts" + ], + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/full-stack-asset-transfer-guide/applications/ping-chaincode/.gitignore b/full-stack-asset-transfer-guide/applications/ping-chaincode/.gitignore new file mode 100644 index 00000000..76add878 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/ping-chaincode/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/applications/ping-chaincode/app.env b/full-stack-asset-transfer-guide/applications/ping-chaincode/app.env new file mode 100644 index 00000000..0bbde31a --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/ping-chaincode/app.env @@ -0,0 +1,7 @@ +CHANNEL_NAME=mychannel +CHAINCODE_NAME=asset-transfer +CONN_PROFILE_FILE=/home/matthew/github.com/hyperledgendary/full-stack-asset-transfer-guide/_cfg/Org1_gateway.json +ID_FILE=asset-transfer_appid.json +ID_DIR=/home/matthew/github.com/hyperledgendary/full-stack-asset-transfer-guide/_cfg/ +TLS_ENABLED=true +MSPID=Org1MSP diff --git a/full-stack-asset-transfer-guide/applications/ping-chaincode/package.json b/full-stack-asset-transfer-guide/applications/ping-chaincode/package.json new file mode 100644 index 00000000..4392da06 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/ping-chaincode/package.json @@ -0,0 +1,36 @@ +{ + "name": "asset-transfer-basic", + "version": "1.0.0", + "description": "Asset Transfer Basic Application implemented in typeScript using fabric-gateway", + "main": "dist/app.js", + "engines": { + "node": ">=14" + }, + "scripts": { + "build": "tsc", + "build:watch": "tsc -w", + "lint": "eslint . --ext .ts", + "prepare": "npm run build", + "pretest": "npm run lint", + "start": "node dist/app.js" + }, + "engineStrict": true, + "author": "Hyperledger", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "~1.6.7", + "@hyperledger/fabric-gateway": "^1.1.0", + "dotenv": "^16.0.1", + "env-var": "^7.1.1", + "js-yaml": "^4.1.0" + }, + "devDependencies": { + "@tsconfig/node14": "^1.0.3", + "@types/js-yaml": "^4.0.5", + "@types/node": "^14.18.16", + "@typescript-eslint/eslint-plugin": "^5.22.0", + "@typescript-eslint/parser": "^5.22.0", + "eslint": "^8.14.0", + "typescript": "~4.6.4" + } +} diff --git a/full-stack-asset-transfer-guide/applications/ping-chaincode/src/app.ts b/full-stack-asset-transfer-guide/applications/ping-chaincode/src/app.ts new file mode 100644 index 00000000..604b6143 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/ping-chaincode/src/app.ts @@ -0,0 +1,98 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { connect, Contract, Identity, Signer, signers } from '@hyperledger/fabric-gateway'; +import * as path from 'path'; +import { TextDecoder } from 'util'; +import { ConnectionHelper } from './fabric-connection-profile'; +import JSONIDAdapter from './jsonid-adapter'; + +import { dump } from 'js-yaml'; + +import {config} from 'dotenv'; +config({path:'app.env'}); +import * as env from 'env-var' + +const channelName = env.get('CHANNEL_NAME').default('mychannel').asString(); +const chaincodeName = env.get('CHAINCODE_NAME').default('conga-nft-contract').asString(); + +const connectionProfile = env.get('CONN_PROFILE_FILE').required().asString(); +const identityFile = env.get('ID_FILE').required().asString() +const identityDir = env.get('ID_DIR').required().asString() +const mspID = env.get('MSPID').required().asString() +const tls = env.get('TLS_ENABLED').default("false").asBool(); + +const utf8Decoder = new TextDecoder(); + + +async function main(): Promise { + + const cp = await ConnectionHelper.loadProfile(connectionProfile); + + + // The gRPC client connection should be shared by all Gateway connections to this endpoint. + const client = await ConnectionHelper.newGrpcConnection(cp,tls); + console.log("Created GRPC Connection") + + const jsonAdapter: JSONIDAdapter = new JSONIDAdapter(path.resolve(identityDir),mspID); + const identity = await jsonAdapter.getIdentity(identityFile); + const signer = await jsonAdapter.getSigner(identityFile); + + console.log("Loaded Identity") + const gateway = connect({ + client, + identity, + signer, + // Default timeouts for different gRPC calls + evaluateOptions: () => { + return { deadline: Date.now() + 5000 }; // 5 seconds + }, + endorseOptions: () => { + return { deadline: Date.now() + 15000 }; // 15 seconds + }, + submitOptions: () => { + return { deadline: Date.now() + 5000 }; // 5 seconds + }, + commitStatusOptions: () => { + return { deadline: Date.now() + 60000 }; // 1 minute + }, + }); + + try { + // Get a network instance representing the channel where the smart contract is deployed. + const network = gateway.getNetwork(channelName); + + // Get the smart contract from the network. + const contract = network.getContract(chaincodeName); + + // Return all the current assets on the ledger. + await ping(contract); + + } finally { + gateway.close(); + client.close(); + } +} + +main().catch(error => { + console.error('******** FAILED to run the application:', error); + process.exitCode = 1; +}); + +/** + * Evaluate a transaction to query ledger state. + */ +async function ping(contract: Contract): Promise { + console.log('\n--> Evaluate Transaction: Get Contract Metdata from : org.hyperledger.fabric:GetMetadata'); + + const resultBytes = await contract.evaluateTransaction('org.hyperledger.fabric:GetMetadata'); + + const resultJson = utf8Decoder.decode(resultBytes); + const result = JSON.parse(resultJson); + console.log('*** Result:'); + console.log(dump(result)); +} + diff --git a/full-stack-asset-transfer-guide/applications/ping-chaincode/src/fabric-connection-profile.ts b/full-stack-asset-transfer-guide/applications/ping-chaincode/src/fabric-connection-profile.ts new file mode 100644 index 00000000..cd4f9364 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/ping-chaincode/src/fabric-connection-profile.ts @@ -0,0 +1,85 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as fs from 'fs'; +import * as path from 'path'; + +import * as grpc from '@grpc/grpc-js'; +import yaml from 'js-yaml'; + +const JSON_EXT = /json/gi; +const YAML_EXT = /ya?ml/gi; + +export interface ConnectionProfile { + display_name: string; + id: string; + name: string; + type: string; + version: string; + // + certificateAuthorities: any; + client: any; + oprganizations: any; + peers: { [key: string]: Peer }; +} + +export interface Peer { + grpcOptions: grpcOptions; + url: string; + tlsCACerts: any +} + +export interface grpcOptions { + 'ssl-Target-Name-Override'?: string; + hostnameOverride?: string; + 'grpc.ssl_target_name_override'?: string; + 'grpc.default_authority'?: string; +} + +export class ConnectionHelper { + /** + * Loads the profile at the given filename. + * + * File can either by yaml or json, error is thrown is the file does + * not exist at the location given. + * + * @param profilename filename of the gateway connection profile + * @return Gateway profile as an object + */ + static loadProfile(profilename: string): ConnectionProfile { + const ccpPath = path.resolve(profilename); + if (!fs.existsSync(ccpPath)) { + throw new Error(`Profile file ${ccpPath} does not exist`); + } + + const type = path.extname(ccpPath); + + if (JSON_EXT.exec(type)) { + return JSON.parse(fs.readFileSync(ccpPath, 'utf8')); + } else if (YAML_EXT.exec(type)) { + return yaml.load(fs.readFileSync(ccpPath, 'utf8')) as ConnectionProfile; + } else { + throw new Error(`Extension of ${ccpPath} not recognised`); + } + } + + + static async newGrpcConnection(cp: ConnectionProfile, tls: boolean): Promise { + const peerEndpointURL = new URL(cp.peers[Object.keys(cp.peers)[0]].url); + const peerEndpoint = `${peerEndpointURL.hostname}:${peerEndpointURL.port}`; + + if (tls){ + const tlsRootCert = cp.peers[Object.keys(cp.peers)[0]].tlsCACerts.pem; + const tlsCredentials = grpc.credentials.createSsl(Buffer.from(tlsRootCert)); + + return new grpc.Client(peerEndpoint, tlsCredentials); + + } else { + console.log(peerEndpoint); + return new grpc.Client(peerEndpoint, grpc.ChannelCredentials.createInsecure()); + + } + } + +} diff --git a/full-stack-asset-transfer-guide/applications/ping-chaincode/src/jsonid-adapter.ts b/full-stack-asset-transfer-guide/applications/ping-chaincode/src/jsonid-adapter.ts new file mode 100644 index 00000000..992e1800 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/ping-chaincode/src/jsonid-adapter.ts @@ -0,0 +1,131 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Identity, Signer, signers } from '@hyperledger/fabric-gateway'; +import { promises as fs } from 'fs'; +import * as path from 'path'; +import * as crypto from 'crypto'; +import { errorMonitor } from 'events'; + +/** Internal interface used to describe all the possible components + * of the identity + */ +interface JSONID { + name: string; + cert: string; + ca: string; + hsm: boolean; + private_key?: string; + privateKey?: string; + mspId: string; +} + +/** + * This class can be used to map identities in a variety of JSON formats to the Identity and Signers required + * for the gateway. For example if you have an application wallet, or have exported IDs from SaaS + * + * ``` + * const jsonAdapter: JSONIDAdapter = new JSONIDAdapter(path.resolve(__dirname,'..','wallet')) + * + * const gateway = connect({ + * client, + * identity: await jsonAdapter.getIdentity("appuser"), + * signer: await jsonAdapter.getSigner("appuser"), + * }); + * ``` + * + * Though they are JSON files, typically they files will have the .id extension. Therefore + * if no extension is provided `.id` is added + */ +export default class JSONIDAdapter { + private idFilesDir: string; + private mspId = ''; + + /** + * @param idFilesDir Directory to load the files from + * @param mspId optional MSPID to apply to all identities returned if they are missing it + */ + public constructor(idFilesDir: string, mspId?: string) { + this.idFilesDir = path.resolve(idFilesDir); + + if (mspId) { + this.mspId = mspId; + } + } + + private async readIDFile(idFile: string): Promise { + let idJsonFile = path.resolve(path.join(this.idFilesDir, idFile)); + + // check if there's no extension probably means it's a waller id file + if (path.extname(idJsonFile) === '') { + idJsonFile = `${idJsonFile}.id`; + } + + let id: JSONID; + const json = JSON.parse(await fs.readFile(idJsonFile, 'utf-8')); + + // look for the nested credentials element + const credentials = json['credentials']; + + if (credentials) { + // v2 SDK Wallet format + id = { + name: idFile, + cert: credentials['certificate'], + ca: '', + hsm: false, + private_key: credentials['privateKey'], + mspId: json.mspId, + }; + } else { + // IBP exported ID style format + id = { + name: json.name, + cert: Buffer.from(json.cert, 'base64').toString(), + ca: Buffer.from(json.ca, 'base64').toString(), + hsm: json.js, + private_key: Buffer.from(json.private_key, 'base64').toString(), + mspId: this.mspId, + }; + } + return id; + } + + /** + * + * @param idFile the name of the identity to load (if no extension is provided `.id` is added) + * @returns Identity to use with the GatewayBuilder + */ + public async getIdentity(idFile: string): Promise { + const id = await this.readIDFile(idFile); + + const identity: Identity = { + credentials: Buffer.from(id.cert), + mspId: id.mspId, + }; + + return identity; + } + + /** + * + * @param idFile the name of the identity to load (if no extension is provided `.id` is added) + * @returns Signer to use with the GatewayBuilder + */ + public async getSigner(idFile: string): Promise { + const id = await this.readIDFile(idFile); + let pk; + if (id.private_key) { + pk = id.private_key; + } else if ('privateKey' in id) { + pk = id['privateKey']; + } else { + throw new Error('Unable to parse the identity json file'); + } + const privateKey = crypto.createPrivateKey(pk as string); + return signers.newPrivateKeySigner(privateKey); + } +} diff --git a/full-stack-asset-transfer-guide/applications/ping-chaincode/tsconfig.json b/full-stack-asset-transfer-guide/applications/ping-chaincode/tsconfig.json new file mode 100644 index 00000000..2052fb6e --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/ping-chaincode/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends":"@tsconfig/node14/tsconfig.json", + "compilerOptions": { + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "outDir": "dist", + "moduleResolution": "node", + "declaration": true, + "sourceMap": true, + "noImplicitAny": true + }, + "include": [ + "./src/**/*" + ], + "exclude": [ + "./src/**/*.spec.ts" + ] +} diff --git a/full-stack-asset-transfer-guide/applications/rest-api/.gitignore b/full-stack-asset-transfer-guide/applications/rest-api/.gitignore new file mode 100644 index 00000000..04c01ba7 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/rest-api/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/applications/rest-api/Dockerfile b/full-stack-asset-transfer-guide/applications/rest-api/Dockerfile new file mode 100644 index 00000000..07e5896e --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/rest-api/Dockerfile @@ -0,0 +1,6 @@ +FROM node:14-alpine3.14 AS build +WORKDIR /app +COPY package.json /app +RUN npm install +COPY . /app +CMD [ "npm","run","prod" ] \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/applications/rest-api/LICENSE b/full-stack-asset-transfer-guide/applications/rest-api/LICENSE new file mode 100644 index 00000000..898e232d --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/rest-api/LICENSE @@ -0,0 +1,17 @@ +# +# Copyright contributors to the Hyperledger Full Stack Asset Transfer project +# +# SPDX-License-Identifier: Apache-2.0 +# +# 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: +# +# http://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. +# \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/applications/rest-api/README.md b/full-stack-asset-transfer-guide/applications/rest-api/README.md new file mode 100644 index 00000000..5dcdf7fa --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/rest-api/README.md @@ -0,0 +1,37 @@ +Make sure all the certificates are generated as per the documentation in the below link +https://github.com/hyperledgendary/full-stack-asset-transfer-guide/blob/main/docs/CloudReady/40-bananas.md + + +#Local development + +npm install +npm run prod + +Import the postman collections and test the apis + + +#Kubernetes development + +Step-1 Build docker image & Tag + +docker build -t localhost:5000/rest-api . + +Step-2 Push docker image to local registary + +docker push localhost:5000/rest-api + +Step-3 Create secrets for the certicates + + kubectl create secret generic client-secret --from-file=keyPath=/home/ramdisk/my-full-stack/infrastructure/sample-network/temp/enrollments/org1/users/org1user/msp/keystore/key.pem --from-file=certPath=/home/ramdisk/my-full-stack/infrastructure/sample-network/temp/enrollments/org1/users/org1user/msp/signcerts/cert.pem --from-file=tlsCertPath=/home/ramdisk/my-full-stack/infrastructure/sample-network/temp/channel-msp/peerOrganizations/org1/msp/tlscacerts/tlsca-signcert.pem -n test-network + +please replace the path of /home/ramdisk/my-full-stack/infrastructure/sample-network/temp with your system path + +Step-4 Deploy the pods to k8s + +kubectl apply -f deployment.yaml -n test-network + +Step-5 Testing API's + +Import the apis into postman and test the apis + +create & list apis are tested.reminaing apis need be implemented diff --git a/full-stack-asset-transfer-guide/applications/rest-api/asset-transfer.postman_collection.json b/full-stack-asset-transfer-guide/applications/rest-api/asset-transfer.postman_collection.json new file mode 100644 index 00000000..40f6b142 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/rest-api/asset-transfer.postman_collection.json @@ -0,0 +1,145 @@ +{ + "info": { + "_postman_id": "56cf175c-ca6b-453a-b950-3d057e19d391", + "name": "asset-transfer", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "101085" + }, + "item": [ + { + "name": "http://localhost:8081/pokemons", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8081/pokemons", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8081", + "path": [ + "pokemons" + ] + } + }, + "response": [] + }, + { + "name": "http://localhost:8081/get", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8081/list", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8081", + "path": [ + "list" + ] + } + }, + "response": [] + }, + { + "name": "http://localhost:8081/get/1", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8081/get/1", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8081", + "path": [ + "get", + "1" + ] + } + }, + "response": [] + }, + { + "name": "http://localhost:8081/create", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"appraisedValue\":2,\n \"color\": \"color\",\n \"owner\": \"Ramesh\",\n \"size\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8081/create", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8081", + "path": [ + "create" + ] + } + }, + "response": [] + }, + { + "name": "Create", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"appraisedValue\":2,\n \"color\": \"color\",\n \"owner\": \"Ramesh\",\n \"size\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://restapi.localho.st/create", + "protocol": "http", + "host": [ + "restapi", + "localho", + "st" + ], + "path": [ + "create" + ] + } + }, + "response": [] + }, + { + "name": "List", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://restapi.localho.st/list", + "protocol": "http", + "host": [ + "restapi", + "localho", + "st" + ], + "path": [ + "list" + ] + } + }, + "response": [] + } + ] +} \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/applications/rest-api/deployment.yaml b/full-stack-asset-transfer-guide/applications/rest-api/deployment.yaml new file mode 100644 index 00000000..d18d9a0c --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/rest-api/deployment.yaml @@ -0,0 +1,81 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: rest-api + labels: + app: rest-api +spec: + replicas: 1 + selector: + matchLabels: + app: rest-api + template: + metadata: + labels: + app: rest-api + spec: + volumes: + - name: secret-volume + secret: + secretName: client-secret + containers: + - name: rest-api + image: localhost:5000/rest-api:latest + ports: + - containerPort: 8080 + env: + - name: rest-certs + valueFrom: + secretKeyRef: + name: rest-certs + key: key.pem + volumeMounts: + - name: secret-volume + readOnly: true + mountPath: "/etc/secret-volume" +--- +kind: Service +apiVersion: v1 +metadata: + name: rest-api +spec: + selector: + app: rest-api + ports: + - protocol: TCP + port: 80 + targetPort: 8080 + + + # kubectl create secret generic client-secret --from-file=keyPath=/home/ramdisk/my-full-stack/infrastructure/sample-network/temp/enrollments/org1/users/org1user/msp/keystore/key.pem --from-file=certPath=/home/ramdisk/my-full-stack/infrastructure/sample-network/temp/enrollments/org1/users/org1user/msp/signcerts/cert.pem --from-file=tlsCertPath=/home/ramdisk/my-full-stack/infrastructure/sample-network/temp/channel-msp/peerOrganizations/org1/msp/tlscacerts/tlsca-signcert.pem -n test-network +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: rest-api + annotations: + nginx.ingress.kubernetes.io/proxy-connect-timeout: 60s + # nginx.ingress.kubernetes.io/ssl-passthrough: "true" + # labels: + # app: rest-api + # app.kubernetes.io/instance: fabricpeer + # app.kubernetes.io/managed-by: fabric-operator + # app.kubernetes.io/name: fabric + # creator: fabric + # orgname: Org1MSP +spec: + ingressClassName: nginx + rules: + - host: restapi.localho.st + http: + paths: + - backend: + service: + name: rest-api + port: + number: 80 + path: / + pathType: ImplementationSpecific + tls: + - hosts: + - restapi.localho.st \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/applications/rest-api/package.json b/full-stack-asset-transfer-guide/applications/rest-api/package.json new file mode 100644 index 00000000..2b4b5130 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/rest-api/package.json @@ -0,0 +1,36 @@ +{ + "name": "asset-transfer-restapi", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "build": "tsc", + "dev": "ts-node ./src/server.ts", + "start": "nodemon ./dist/server.js", + "prod": "npm run build && npm run start", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@grpc/grpc-js": "~1.6.7", + "@hyperledger/fabric-gateway": "^1.1.0", + "@hyperledger/fabric-protos": "^0.1.0-dev.2300102001.1", + "@tsconfig/node14": "^1.0.3", + "@types/body-parser": "^1.17.0", + "@types/express": "^4.16.0", + "@types/node": "^14.18.16", + "@typescript-eslint/eslint-plugin": "^5.22.0", + "@typescript-eslint/parser": "^5.22.0", + "eslint": "^8.14.0", + "nodemon": "^1.18.3", + "ts-node": "^7.0.0", + "typescript": "~4.6.4" + }, + "dependencies": { + "body-parser": "^1.18.3", + "cors": "^2.8.5", + "express": "^4.16.3" + } +} diff --git a/full-stack-asset-transfer-guide/applications/rest-api/renovate.json b/full-stack-asset-transfer-guide/applications/rest-api/renovate.json new file mode 100644 index 00000000..f45d8f11 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/rest-api/renovate.json @@ -0,0 +1,5 @@ +{ + "extends": [ + "config:base" + ] +} diff --git a/full-stack-asset-transfer-guide/applications/rest-api/src/app.ts b/full-stack-asset-transfer-guide/applications/rest-api/src/app.ts new file mode 100644 index 00000000..30b61561 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/rest-api/src/app.ts @@ -0,0 +1,27 @@ +import * as express from 'express'; +import * as bodyParser from 'body-parser'; +import { Connection } from './connection'; +import { AssetRouter } from './assets.router'; +var cors = require('cors'); + +class App { + public app: express.Application; + public routes: AssetRouter = new AssetRouter(); + constructor() { + new Connection().init(); + this.app = express(); + this.app.use(cors()); + this.config(); + this.routes.routes(this.app); + } + + private config(): void { + // support application/json type post data + this.app.use(bodyParser.json()); + //support application/x-www-form-urlencoded post data + this.app.use(bodyParser.urlencoded({ + extended: false + })); + } +} +export default new App().app; \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/applications/rest-api/src/assets.router.ts b/full-stack-asset-transfer-guide/applications/rest-api/src/assets.router.ts new file mode 100644 index 00000000..bc9ad912 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/rest-api/src/assets.router.ts @@ -0,0 +1,110 @@ +import { Request, Response } from "express"; +const utf8Decoder = new TextDecoder(); +import { Connection } from "./connection"; +export class AssetRouter { + public routes(app): void { + app.route('/list') + .get(async (req: Request, res: Response) => { + const resultBytes = Connection.contract.evaluateTransaction('GetAllAssets'); + const resultJson = utf8Decoder.decode(await resultBytes); + const result = JSON.parse(resultJson); + res.status(200).send(result); + }) + app.route('/create') + .post((req: Request, res: Response) => { + console.log(req.body) + var Id = Date.now(); + var json = JSON.stringify({ + ID: Id + "", + Owner: req.body.Owner, + Color: req.body.Color, + Size: req.body.Size, + AppraisedValue: req.body.AppraisedValue, + }) + Connection.contract.submitTransaction('CreateAsset', json); + var response = ({ "AssetId": Id }) + res.status(200).send(response); + }) + app.route('/update') + .post((req: Request, res: Response) => { + console.log(req.body) + var Id = Date.now(); + var json = JSON.stringify({ + ID: req.body.ID, + Owner: req.body.Owner, + Color: req.body.Color, + Size: req.body.Size, + AppraisedValue: req.body.AppraisedValue, + }) + var response; + try { + Connection.contract.submitTransaction('UpdateAsset', json); + response = ({ "status": 0, "message": "Update success" }) + } catch (error) { + response = ({ "status": -1, "message": "Something went wrong" }) + } + res.status(200).send(response); + }) + app.route('/delete') + .post((req: Request, res: Response) => { + console.log(req.body) + var response; + try { + Connection.contract.submitTransaction('DeleteAsset', req.body.id); + response = ({ "status": 0, "message": "Delete success" }) + } catch (error) { + response = ({ "status": -1, "message": "Something went wrong" }) + } + res.status(200).send(response); + }) + app.route('/transfer') + .post(async (req: Request, res: Response) => { + console.log(req.body) + + console.log('\n--> Async Submit Transaction: TransferAsset, updates existing asset owner'); + + const commit = Connection.contract.submitAsync('TransferAsset', { + arguments: [req.body.assetId, 'Saptha'], + }); + const oldOwner = utf8Decoder.decode((await commit).getResult()); + + console.log(`*** Successfully submitted transaction to transfer ownership from ${oldOwner} to Saptha`); + console.log('*** Waiting for transaction commit'); + + const status = await (await commit).getStatus(); + if (!status.successful) { + throw new Error(`Transaction ${status.transactionId} failed to commit with status code ${status.code}`); + } + console.log('*** Transaction committed successfully'); + res.status(200).send(status); + }) + app.route('/updateNonExistentAsset') + .post(async (req: Request, res: Response) => { + try { + await Connection.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); + } + res.status(200).send("Success"); + }) + app.route('/get/:id') + .get(async (req: Request, res: Response) => { + let id = req.params.id; + console.log('\n--> Evaluate Transaction: ReadAsset, function returns asset attributes'); + const resultBytes = Connection.contract.evaluateTransaction('ReadAsset', id); + const resultJson = utf8Decoder.decode(await resultBytes); + const result = JSON.parse(resultJson); + console.log('*** Result:', result); + res.status(200).send(result); + }) + } + +} diff --git a/full-stack-asset-transfer-guide/applications/rest-api/src/connection.ts b/full-stack-asset-transfer-guide/applications/rest-api/src/connection.ts new file mode 100644 index 00000000..743b9241 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/rest-api/src/connection.ts @@ -0,0 +1,101 @@ +import * as grpc from '@grpc/grpc-js'; +import { connect, Contract, Identity, Signer, signers } from '@hyperledger/fabric-gateway'; +import * as crypto from 'crypto'; +import * as path from 'path'; + +import { promises as fs } from 'fs'; +const channelName = envOrDefault('CHANNEL_NAME', 'mychannel'); +const chaincodeName = envOrDefault('CHAINCODE_NAME', 'asset-transfer'); +const mspId = envOrDefault('MSP_ID', 'Org1MSP'); +//Local development and testing uncomment below code +// const WORKSHOP_CRYPTO =envOrDefault('CRYPTO_PATH', path.resolve(__dirname, '..','..', '..', 'infrastructure', 'sample-network', 'temp')); +// const keyPath = WORKSHOP_CRYPTO + "/enrollments/org1/users/org1user/msp/keystore/key.pem"; +// const certPath = WORKSHOP_CRYPTO + "/enrollments/org1/users/org1user/msp/signcerts/cert.pem" +// const tlsCertPath = WORKSHOP_CRYPTO + "/channel-msp/peerOrganizations/org1/msp/tlscacerts/tlsca-signcert.pem"; + +// //kubenetes certificates file path +const WORKSHOP_CRYPTO = "/etc/secret-volume/" +const keyPath = WORKSHOP_CRYPTO + "keyPath"; +const certPath = WORKSHOP_CRYPTO + "certPath" +const tlsCertPath = WORKSHOP_CRYPTO + "tlsCertPath"; +console.log("keyPath " + keyPath); +console.log("certPath " + certPath); +console.log("tlsCertPath " + tlsCertPath); +const peerEndpoint = "test-network-org1-peer1-peer.localho.st:443"; +const peerHostAlias = "test-network-org1-peer1-peer.localho.st"; +export class Connection { + public static contract: Contract; + public init() { + initFabric(); + } +} +async function initFabric(): Promise { + // The gRPC client connection should be shared by all Gateway connections to this endpoint. + const client = await newGrpcConnection(); + + const gateway = connect({ + client, + identity: await newIdentity(), + signer: await newSigner(), + // Default timeouts for different gRPC calls + evaluateOptions: () => { + return { deadline: Date.now() + 5000 }; // 5 seconds + }, + endorseOptions: () => { + return { deadline: Date.now() + 15000 }; // 15 seconds + }, + submitOptions: () => { + return { deadline: Date.now() + 5000 }; // 5 seconds + }, + commitStatusOptions: () => { + return { deadline: Date.now() + 60000 }; // 1 minute + }, + }); + + try { + // Get a network instance representing the channel where the smart contract is deployed. + const network = gateway.getNetwork(channelName); + + // Get the smart contract from the network. + const contract = network.getContract(chaincodeName); + Connection.contract = contract; + + // Initialize a set of asset data on the ledger using the chaincode 'InitLedger' function. + // await initLedger(contract); + + + } catch (e: any) { + console.log('sample log'); + console.log(e.message); + } finally { + console.log('error log '); + // gateway.close(); + // client.close(); + } +} +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': peerHostAlias, + }); +} + +async function newIdentity(): Promise { + const credentials = await fs.readFile(certPath); + return { mspId, credentials }; +} + +async function newSigner(): Promise { + //const files = await fs.readdir(keyDirectoryPath); + // path.resolve(keyDirectoryPath, files[0]); + const privateKeyPem = await fs.readFile(keyPath); + const privateKey = crypto.createPrivateKey(privateKeyPem); + return signers.newPrivateKeySigner(privateKey); +} +/** + * envOrDefault() will return the value of an environment variable, or a default value if the variable is undefined. + */ +function envOrDefault(key: string, defaultValue: string): string { + return process.env[key] || defaultValue; +} \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/applications/rest-api/src/server.ts b/full-stack-asset-transfer-guide/applications/rest-api/src/server.ts new file mode 100644 index 00000000..3d0c4b19 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/rest-api/src/server.ts @@ -0,0 +1,6 @@ +import app from "./app"; +const PORT = process.env.PORT || 8080; + +app.listen(PORT, () => { + console.log('listening on port ' + PORT); +}) diff --git a/full-stack-asset-transfer-guide/applications/rest-api/tsconfig.json b/full-stack-asset-transfer-guide/applications/rest-api/tsconfig.json new file mode 100644 index 00000000..f5129dc7 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/rest-api/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es6", //default is es5 + "module": "commonjs",//CommonJs style module in output + "outDir": "dist" , //change the output directory + "resolveJsonModule": true //to import out json database + }, + "include": [ + "src/**/*.ts" //which kind of files to compile + ], + "exclude": [ + "node_modules" //which files or directories to ignore + ] + } \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/applications/trader-typescript/.eslintrc.js b/full-stack-asset-transfer-guide/applications/trader-typescript/.eslintrc.js new file mode 100644 index 00000000..207a02ca --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/trader-typescript/.eslintrc.js @@ -0,0 +1,99 @@ +module.exports = { + env: { + node: true, + es2021: true, + }, + extends: [ + 'eslint:recommended', + ], + root: true, + ignorePatterns: [ + 'dist/', + ], + rules: { + 'arrow-spacing': ['error'], + 'comma-style': ['error'], + complexity: ['error', 10], + 'eol-last': ['error'], + 'generator-star-spacing': ['error', 'after'], + 'key-spacing': [ + 'error', + { + beforeColon: false, + afterColon: true, + mode: 'minimum', + }, + ], + 'keyword-spacing': ['error'], + 'no-multiple-empty-lines': ['error'], + 'no-trailing-spaces': ['error'], + 'no-whitespace-before-property': ['error'], + 'object-curly-newline': ['error'], + 'padded-blocks': ['error', 'never'], + 'rest-spread-spacing': ['error'], + 'semi-style': ['error'], + 'space-before-blocks': ['error'], + 'space-in-parens': ['error'], + 'space-unary-ops': ['error'], + 'spaced-comment': ['error'], + 'template-curly-spacing': ['error'], + 'yield-star-spacing': ['error', 'after'], + }, + overrides: [ + { + files: [ + '**/*.ts', + ], + parser: '@typescript-eslint/parser', + parserOptions: { + sourceType: 'module', + ecmaFeatures: { + impliedStrict: true, + }, + project: './tsconfig.json', + tsconfigRootDir: process.env.TSCONFIG_ROOT_DIR || __dirname, + }, + plugins: [ + '@typescript-eslint', + ], + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-requiring-type-checking', + ], + rules: { + '@typescript-eslint/comma-spacing': ['error'], + '@typescript-eslint/explicit-function-return-type': [ + 'error', + { + allowExpressions: true, + }, + ], + '@typescript-eslint/func-call-spacing': ['error'], + '@typescript-eslint/member-delimiter-style': ['error'], + '@typescript-eslint/indent': [ + 'error', + 4, + { + SwitchCase: 0, + }, + ], + '@typescript-eslint/prefer-nullish-coalescing': ['error'], + '@typescript-eslint/prefer-optional-chain': ['error'], + '@typescript-eslint/prefer-reduce-type-parameter': ['error'], + '@typescript-eslint/prefer-return-this-type': ['error'], + '@typescript-eslint/quotes': ['error', 'single'], + '@typescript-eslint/type-annotation-spacing': ['error'], + '@typescript-eslint/semi': ['error'], + '@typescript-eslint/space-before-function-paren': [ + 'error', + { + anonymous: 'never', + named: 'never', + asyncArrow: 'always', + }, + ], + }, + }, + ], +}; diff --git a/full-stack-asset-transfer-guide/applications/trader-typescript/.gitignore b/full-stack-asset-transfer-guide/applications/trader-typescript/.gitignore new file mode 100644 index 00000000..1276eb3b --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/trader-typescript/.gitignore @@ -0,0 +1,18 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +# Coverage directory used by tools like istanbul +coverage + +# Dependencies +node_modules/ +jspm_packages/ +package-lock.json + +# Compiled TypeScript files +dist + +# Files generated by the application at runtime +checkpoint.json +store.log diff --git a/full-stack-asset-transfer-guide/applications/trader-typescript/.npmrc b/full-stack-asset-transfer-guide/applications/trader-typescript/.npmrc new file mode 100644 index 00000000..b6f27f13 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/trader-typescript/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/full-stack-asset-transfer-guide/applications/trader-typescript/README.md b/full-stack-asset-transfer-guide/applications/trader-typescript/README.md new file mode 100644 index 00000000..ab584847 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/trader-typescript/README.md @@ -0,0 +1,43 @@ +# Trader sample client application + +This is a simple client application for the [asset-transfer](../../contracts/asset-transfer-typescript/) smart contract, built using the [Fabric Gateway client API](https://hyperledger.github.io/fabric-gateway/) for Fabric v2.4+. + +## Prerequisites + +The client application requires Node.js 16 or later. + +## Set up + +The following steps prepare the client application for execution: + +1. Ensure the [asset-transfer](../../contracts/asset-transfer-typescript/) smart contract is deployed to a running Fabric network. +1. Run `npm install` to download dependencies and compile the application code. + +> **Note:** After making any code changes to the application, be sure to recompile the application code. This can be done by explicitly running `npm install` again, or you can leave `npm run build:watch` running in a terminal window to automatically rebuild the application on any code change. + + +The client application uses environment variables to supply configuration options. You must set the following environment variables when running the application: + +- `ENDPOINT` - endpoint address for the Gateway service to which the client will connect in the form **hostname:port**. Depending on your environment, this can be the address of a specific peer within the user's organization, or an ingress endpoint that dispatches to any available peer in the user's organization. +- `MSP_ID` - member service provider ID for the user's organization. +- `CERTIFICATE` - PEM file containing the user's X.509 certificate. +- `PRIVATE_KEY` - PEM file containing the user's private key. + +The following environment variables are optional and can be set if required by your environment: + +- `CHANNEL_NAME` - Channel to which the chaincode is deployed. (Default: `mychannel`) +- `CHAINCODE_NAME` - Channel to which the chaincode is deployed. (Default: `asset-transfer`) +- `TLS_CERT` - PEM file containing the CA certificate used to authenticate the TLS connection to the Gateway peer. *Only required if using a TLS connection and a private CA.* +- `HOST_ALIAS` - the name of the Gateway peer as it appears in its TLS certificate. *Only required if the endpoint address used by the client does not match the address in the Gateway peer's TLS certificate.* + +# Run + +The sample application is run as a command-line application, and is lauched using `npm start [ ...]`. The following commands are available: + +- `npm start create ` to create a new asset. +- `npm start delete ` to delete an existing asset. +- `npm start getAllAssets` to list all assets. +- `npm start listen` to listen for chaincode events emitted by transaction functions. Interrupt the listener using Control-C. +- `npm start read ` to view an existing asset. +- `npm start transact` to create some random assets and perform some random operations on those assets. +- `npm start transfer ` to transfer an asset to a new owner within an organization MSP ID. diff --git a/full-stack-asset-transfer-guide/applications/trader-typescript/package.json b/full-stack-asset-transfer-guide/applications/trader-typescript/package.json new file mode 100644 index 00000000..63d3ae86 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/trader-typescript/package.json @@ -0,0 +1,34 @@ +{ + "name": "trader", + "version": "1.0.0", + "description": "Asset transfer client application", + "main": "dist/index.js", + "typings": "dist/index.d.ts", + "engines": { + "node": ">=16.13.0" + }, + "scripts": { + "build": "tsc", + "build:watch": "tsc -w", + "lint": "eslint ./src", + "prepare": "npm run build", + "pretest": "npm run lint", + "start": "node ./dist/app", + "test": "" + }, + "author": "Hyperledger", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "~1.6.7", + "@hyperledger/fabric-gateway": "^1.1.0", + "@hyperledger/fabric-protos": "^0.1.0-dev.2300102001.1" + }, + "devDependencies": { + "@tsconfig/node16": "^1.0.3", + "@types/node": "^16.11.46", + "@typescript-eslint/eslint-plugin": "^5.22.0", + "@typescript-eslint/parser": "^5.22.0", + "eslint": "^8.14.0", + "typescript": "~4.7.4" + } +} diff --git a/full-stack-asset-transfer-guide/applications/trader-typescript/src/app.ts b/full-stack-asset-transfer-guide/applications/trader-typescript/src/app.ts new file mode 100644 index 00000000..cd666a28 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/trader-typescript/src/app.ts @@ -0,0 +1,51 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Command, commands } from './commands'; +import { newGatewayConnection, newGrpcConnection } from './connect'; +import { ExpectedError } from './expectedError'; + +async function main(): Promise { + const commandName = process.argv[2]; + const args = process.argv.slice(3); + + const command = commands[commandName]; + if (!command) { + printUsage(); + throw new Error(`Unknown command: ${commandName}`); + } + + await runCommand(command, args); +} + +async function runCommand(command: Command, args: string[]): Promise { + const client = await newGrpcConnection(); + try { + const gateway = await newGatewayConnection(client); + try { + await command(gateway, args); + } finally { + gateway.close(); + } + } finally { + client.close(); + } +} + +function printUsage(): void { + console.log('Arguments: [ ...]'); + console.log('Available commands:'); + console.log(`\t${Object.keys(commands).sort().join('\n\t')}`); +} + +main().catch(error => { + if (error instanceof ExpectedError) { + console.log(error); + } else { + console.error('\nUnexpected application error:', error); + process.exitCode = 1; + } +}); diff --git a/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/create.ts b/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/create.ts new file mode 100644 index 00000000..8ecad3c9 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/create.ts @@ -0,0 +1,26 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Gateway } from '@hyperledger/fabric-gateway'; +import { CHAINCODE_NAME, CHANNEL_NAME } from '../config'; +import { AssetTransfer } from '../contract'; +import { assertAllDefined } from '../utils'; + +export default async function main(gateway: Gateway, args: string[]): Promise { + const [assetId, owner, color] = assertAllDefined([args[0], args[1], args[2]], 'Arguments: '); + + const network = gateway.getNetwork(CHANNEL_NAME); + const contract = network.getContract(CHAINCODE_NAME); + + const smartContract = new AssetTransfer(contract); + await smartContract.createAsset({ + ID: assetId, + Owner: owner, + Color: color, + Size: 1, + AppraisedValue: 1, + }); +} diff --git a/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/delete.ts b/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/delete.ts new file mode 100644 index 00000000..4cd5df34 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/delete.ts @@ -0,0 +1,20 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Gateway } from '@hyperledger/fabric-gateway'; +import { CHAINCODE_NAME, CHANNEL_NAME } from '../config'; +import { AssetTransfer } from '../contract'; +import { assertDefined } from '../utils'; + +export default async function main(gateway: Gateway, args: string[]): Promise { + const assetId = assertDefined(args[0], 'Arguments: '); + + const network = gateway.getNetwork(CHANNEL_NAME); + const contract = network.getContract(CHAINCODE_NAME); + + const smartContract = new AssetTransfer(contract); + await smartContract.deleteAsset(assetId); +} diff --git a/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/getAllAssets.ts b/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/getAllAssets.ts new file mode 100644 index 00000000..34157a2e --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/getAllAssets.ts @@ -0,0 +1,20 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Gateway } from '@hyperledger/fabric-gateway'; +import { CHAINCODE_NAME, CHANNEL_NAME } from '../config'; +import { AssetTransfer } from '../contract'; + +export default async function main(gateway: Gateway): Promise { + const network = gateway.getNetwork(CHANNEL_NAME); + const contract = network.getContract(CHAINCODE_NAME); + + const smartContract = new AssetTransfer(contract); + const assets = await smartContract.getAllAssets(); + + const assetsJson = JSON.stringify(assets, undefined, 2); + assetsJson.split('\n').forEach(line => console.log(line)); // Write line-by-line to avoid truncation +} diff --git a/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/index.ts b/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/index.ts new file mode 100644 index 00000000..1293ff64 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Gateway } from '@hyperledger/fabric-gateway'; +import create from './create'; +import deleteCommand from './delete'; +import getAllAssets from './getAllAssets'; +import listen from './listen'; +import read from './read'; +import transact from './transact'; +import transfer from './transfer'; + +export type Command = (gateway: Gateway, args: string[]) => Promise; + +export const commands: Record = { + create, + delete: deleteCommand, + getAllAssets, + listen, + read, + transact, + transfer, +}; diff --git a/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/listen.ts b/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/listen.ts new file mode 100644 index 00000000..5c9fa152 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/listen.ts @@ -0,0 +1,64 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ChaincodeEvent, checkpointers, Gateway } from '@hyperledger/fabric-gateway'; +import * as path from 'path'; +import { CHAINCODE_NAME, CHANNEL_NAME } from '../config'; +import { ExpectedError } from '../expectedError'; +import { printable } from '../utils'; + +const checkpointFile = path.resolve(process.env.CHECKPOINT_FILE ?? 'checkpoint.json'); +const simulatedFailureCount = getSimulatedFailureCount(); + +const startBlock = BigInt(0); + +export default async function main(gateway: Gateway): Promise { + const network = gateway.getNetwork(CHANNEL_NAME); + const checkpointer = await checkpointers.file(checkpointFile); + + console.log(`Starting event listening from block ${checkpointer.getBlockNumber() ?? startBlock}`); + console.log('Last processed transaction ID within block:', checkpointer.getTransactionId()); + if (simulatedFailureCount > 0) { + console.log(`Simulating a write failure every ${simulatedFailureCount} transactions`); + } + + const events = await network.getChaincodeEvents(CHAINCODE_NAME, { + startBlock, // Used only if there is no checkpoint block number + }); + + try { + for await (const event of events) { + onEvent(event); + } + } finally { + events.close(); + } +} + +function onEvent(event: ChaincodeEvent): void { + simulateFailureIfRequired(); + + console.log(printable(event)); +} + +function getSimulatedFailureCount(): number { + const value = process.env.SIMULATED_FAILURE_COUNT ?? '0'; + const count = Math.floor(Number(value)); + if (isNaN(count) || count < 0) { + throw new Error(`Invalid SIMULATED_FAILURE_COUNT value: ${String(value)}`); + } + + return count; +} + +let eventCount = 0; // Used only to simulate failures + +function simulateFailureIfRequired(): void { + if (simulatedFailureCount > 0 && eventCount++ >= simulatedFailureCount) { + eventCount = 0; + throw new ExpectedError('Simulated write failure'); + } +} diff --git a/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/read.ts b/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/read.ts new file mode 100644 index 00000000..57ac9280 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/read.ts @@ -0,0 +1,23 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Gateway } from '@hyperledger/fabric-gateway'; +import { CHAINCODE_NAME, CHANNEL_NAME } from '../config'; +import { AssetTransfer } from '../contract'; +import { assertDefined } from '../utils'; + +export default async function main(gateway: Gateway, args: string[]): Promise { + const assetId = assertDefined(args[0], 'Arguments: '); + + const network = gateway.getNetwork(CHANNEL_NAME); + const contract = network.getContract(CHAINCODE_NAME); + + const smartContract = new AssetTransfer(contract); + const asset = await smartContract.readAsset(assetId); + + const assetsJson = JSON.stringify(asset, undefined, 2); + console.log(assetsJson); +} diff --git a/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/transact.ts b/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/transact.ts new file mode 100644 index 00000000..591c26d7 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/transact.ts @@ -0,0 +1,72 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Gateway } from '@hyperledger/fabric-gateway'; +import * as crypto from 'crypto'; +import { CHAINCODE_NAME, CHANNEL_NAME } from '../config'; +import { AssetCreate, AssetTransfer } from '../contract'; +import { allFulfilled, differentElement, randomElement, randomInt } from '../utils'; + +export default async function main(gateway: Gateway): Promise { + const network = gateway.getNetwork(CHANNEL_NAME); + const contract = network.getContract(CHAINCODE_NAME); + + const smartContract = new AssetTransfer(contract); + const app = new TransactApp(smartContract); + await app.run(); +} + +const colors = ['red', 'green', 'blue']; +const maxInitialValue = 1000; +const maxInitialSize = 10; + +class TransactApp { + readonly #smartContract: AssetTransfer; + #batchSize = 6; + + constructor(smartContract: AssetTransfer) { + this.#smartContract = smartContract; + } + + async run(): Promise { + const promises = Array.from({ length: this.#batchSize }, () => this.#transact()); + await allFulfilled(promises); + } + + async #transact(): Promise { + const asset = this.#newAsset(); + + await this.#smartContract.createAsset(asset); + console.log(`Created asset ${asset.ID}`); + + // Update randomly 1 in 2 assets to a new owner. + if (randomInt(2) === 0) { + const oldColor = asset.Color; + asset.Color = differentElement(colors, oldColor); + await this.#smartContract.updateAsset(asset); + console.log(`Updated color of asset ${asset.ID} from ${oldColor} to ${asset.Color}`); + } + + // Delete randomly 1 in 4 created assets. + if (randomInt(4) === 0) { + await this.#smartContract.deleteAsset(asset.ID); + console.log(`Deleted asset ${asset.ID}`); + } + } + + #newAsset(): AssetCreate { + return { + ID: crypto.randomUUID().replaceAll('-', '').substring(0, 8), + Color: randomElement(colors), + Size: randomInt(maxInitialSize) + 1, + AppraisedValue: randomInt(maxInitialValue) + 1, + }; + } + + setbatchSize(batchSize: number): void { + this.#batchSize = batchSize; + } +} diff --git a/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/transfer.ts b/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/transfer.ts new file mode 100644 index 00000000..3b014c61 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/trader-typescript/src/commands/transfer.ts @@ -0,0 +1,20 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Gateway } from '@hyperledger/fabric-gateway'; +import { CHAINCODE_NAME, CHANNEL_NAME } from '../config'; +import { AssetTransfer } from '../contract'; +import { assertAllDefined } from '../utils'; + +export default async function main(gateway: Gateway, args: string[]): Promise { + const [assetId, newOwner, newOwnerOrg] = assertAllDefined([args[0], args[1], args[2]], 'Arguments: '); + + const network = gateway.getNetwork(CHANNEL_NAME); + const contract = network.getContract(CHAINCODE_NAME); + + const smartContract = new AssetTransfer(contract); + await smartContract.transferAsset(assetId, newOwner, newOwnerOrg); +} diff --git a/full-stack-asset-transfer-guide/applications/trader-typescript/src/config.ts b/full-stack-asset-transfer-guide/applications/trader-typescript/src/config.ts new file mode 100644 index 00000000..352f37c9 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/trader-typescript/src/config.ts @@ -0,0 +1,45 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { resolve } from 'path'; +import { assertDefined } from './utils'; + +export const GATEWAY_ENDPOINT = assertEnv('ENDPOINT'); +export const MSP_ID = assertEnv('MSP_ID'); +export const CLIENT_CERT_PATH = resolve(assertEnv('CERTIFICATE')); +export const PRIVATE_KEY_PATH = resolve(assertEnv('PRIVATE_KEY')); +export const TLS_CERT_PATH = resolvePathIfPresent(process.env.TLS_CERT); +export const CHANNEL_NAME = process.env.CHANNEL_NAME ?? 'mychannel'; +export const CHAINCODE_NAME = process.env.CHAINCODE_NAME ?? 'asset-transfer'; + +// Gateway peer SSL host name override. +export const HOST_ALIAS = process.env.HOST_ALIAS; + +function assertEnv(envName: string): string { + return assertDefined(process.env[envName], () => { + console.error('The following environment variables must be set:' + + '\n ENDPOINT - Endpoint address of the gateway service' + + '\n MSP_ID - User\'s organization Member Services Provider ID' + + '\n CERTIFICATE - User\'s certificate file' + + '\n PRIVATE_KEY - User\'s private key file' + + '\n' + + '\nThe following environment variables are optional:' + + '\n CHANNEL_NAME - Channel to which the chaincode is deployed' + + '\n CHAINCODE_NAME - Channel to which the chaincode is deployed' + + '\n TLS_CERT - TLS CA root certificate (only if using TLS and private CA)' + + '\n HOST_ALIAS - TLS hostname override (only if TLS cert does not match endpoint)' + + '\n'); + return `Environment variable ${envName} not set`; + }); +} + +function resolvePathIfPresent(path: string | undefined): string | undefined { + if (path == undefined) { + return undefined; + } + + return resolve(path); +} diff --git a/full-stack-asset-transfer-guide/applications/trader-typescript/src/connect.ts b/full-stack-asset-transfer-guide/applications/trader-typescript/src/connect.ts new file mode 100644 index 00000000..6aad1310 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/trader-typescript/src/connect.ts @@ -0,0 +1,66 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as grpc from '@grpc/grpc-js'; +import { connect, Gateway, Identity, Signer, signers } from '@hyperledger/fabric-gateway'; +import * as crypto from 'crypto'; +import * as fs from 'fs'; +import * as path from 'path'; +import { CLIENT_CERT_PATH, GATEWAY_ENDPOINT, HOST_ALIAS, MSP_ID, PRIVATE_KEY_PATH, TLS_CERT_PATH } from './config'; + +export async function newGrpcConnection(): Promise { + if (TLS_CERT_PATH) { + const tlsRootCert = await fs.promises.readFile(TLS_CERT_PATH); + const tlsCredentials = grpc.credentials.createSsl(tlsRootCert); + return new grpc.Client(GATEWAY_ENDPOINT, tlsCredentials, newGrpcClientOptions()); + } + + return new grpc.Client(GATEWAY_ENDPOINT, grpc.ChannelCredentials.createInsecure()); +} + +function newGrpcClientOptions(): grpc.ClientOptions { + const result: grpc.ClientOptions = {}; + if (HOST_ALIAS) { + result['grpc.ssl_target_name_override'] = HOST_ALIAS; // Only required if server TLS cert does not match the endpoint address we use + } + return result; +} + +export async function newGatewayConnection(client: grpc.Client): Promise { + return connect({ + client, + identity: await newIdentity(), + signer: await newSigner(), + // Default timeouts for different gRPC calls + evaluateOptions: () => { + return { deadline: Date.now() + 5000 }; // 5 seconds + }, + endorseOptions: () => { + return { deadline: Date.now() + 15000 }; // 15 seconds + }, + submitOptions: () => { + return { deadline: Date.now() + 5000 }; // 5 seconds + }, + commitStatusOptions: () => { + return { deadline: Date.now() + 60000 }; // 1 minute + }, + }); +} + +async function newIdentity(): Promise { + const certPath = path.resolve(CLIENT_CERT_PATH); + const credentials = await fs.promises.readFile(certPath); + + return { mspId: MSP_ID, credentials }; +} + +async function newSigner(): Promise { + const keyPath = path.resolve(PRIVATE_KEY_PATH); + const privateKeyPem = await fs.promises.readFile(keyPath); + const privateKey = crypto.createPrivateKey(privateKeyPem); + + return signers.newPrivateKeySigner(privateKey); +} diff --git a/full-stack-asset-transfer-guide/applications/trader-typescript/src/contract.ts b/full-stack-asset-transfer-guide/applications/trader-typescript/src/contract.ts new file mode 100644 index 00000000..e14ffd57 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/trader-typescript/src/contract.ts @@ -0,0 +1,103 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CommitError, Contract, StatusCode } from '@hyperledger/fabric-gateway'; +import { TextDecoder } from 'util'; + +const RETRIES = 2; + +const utf8Decoder = new TextDecoder(); + +export interface Asset { + ID: string; + Color: string; + Size: number; + Owner: string; + AppraisedValue: number; +} + +export type AssetCreate = Omit & Partial; +export type AssetUpdate = Pick & Partial>; + +/** + * AssetTransfer presents the smart contract in a form appropriate to the business application. Internally it uses the + * Fabric Gateway client API to invoke transaction functions, and deals with the translation between the business + * application and API representation of parameters and return values. + */ +export class AssetTransfer { + readonly #contract: Contract; + + constructor(contract: Contract) { + this.#contract = contract; + } + + async createAsset(asset: AssetCreate): Promise { + await this.#contract.submit('CreateAsset', { + arguments: [JSON.stringify(asset)], + }); + } + + async getAllAssets(): Promise { + const result = await this.#contract.evaluate('GetAllAssets'); + if (result.length === 0) { + return []; + } + + return JSON.parse(utf8Decoder.decode(result)) as Asset[]; + } + + async readAsset(id: string): Promise { + const result = await this.#contract.evaluate('ReadAsset', { + arguments: [id], + }); + return JSON.parse(utf8Decoder.decode(result)) as Asset; + } + + async updateAsset(asset: AssetUpdate): Promise { + await submitWithRetry(() => this.#contract.submit('UpdateAsset', { + arguments: [JSON.stringify(asset)], + })); + } + + async deleteAsset(id: string): Promise { + await submitWithRetry(() => this.#contract.submit('DeleteAsset', { + arguments: [id], + })); + } + + async assetExists(id: string): Promise { + const result = await this.#contract.evaluate('AssetExists', { + arguments: [id], + }); + return utf8Decoder.decode(result).toLowerCase() === 'true'; + } + + async transferAsset(id: string, newOwner: string, newOwnerOrg: string): Promise { + // TODO: Implement me! + // Submit a 'TransferAsset' transaction, which requires [id, newOwner, newOwnerOrg] arguments. + } +} + +async function submitWithRetry(submit: () => Promise): Promise { + let lastError: unknown | undefined; + + for (let retryCount = 0; retryCount < RETRIES; retryCount++) { + try { + return await submit(); + } catch (err: unknown) { + lastError = err; + if (err instanceof CommitError) { + // Transaction failed validation and did not update the ledger. Handle specific transaction validation codes. + if (err.code === StatusCode.MVCC_READ_CONFLICT) { + continue; // Retry + } + } + break; // Failure -- don't retry + } + } + + throw lastError; +} diff --git a/full-stack-asset-transfer-guide/applications/trader-typescript/src/expectedError.ts b/full-stack-asset-transfer-guide/applications/trader-typescript/src/expectedError.ts new file mode 100644 index 00000000..296d87be --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/trader-typescript/src/expectedError.ts @@ -0,0 +1,12 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export class ExpectedError extends Error { + constructor(message?: string) { + super(message); + this.name = ExpectedError.name; + } +} diff --git a/full-stack-asset-transfer-guide/applications/trader-typescript/src/utils.ts b/full-stack-asset-transfer-guide/applications/trader-typescript/src/utils.ts new file mode 100644 index 00000000..48a28890 --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/trader-typescript/src/utils.ts @@ -0,0 +1,75 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { inspect, TextDecoder } from 'util'; + +const utf8Decoder = new TextDecoder(); + +/** + * Pick a random element from an array. + * @param values Candidate elements. + */ +export function randomElement(values: T[]): T { + return values[randomInt(values.length)]; +} + +/** + * Generate a random integer in the range 0 to max - 1. + * @param max Maximum value (exclusive). + */ +export function randomInt(max: number): number { + return Math.floor(Math.random() * max); +} + +/** + * Pick a random element from an array, excluding the current value. + * @param values Candidate elements. + * @param currentValue Value to avoid. + */ +export function differentElement(values: T[], currentValue: T): T { + const candidateValues = values.filter(value => value !== currentValue); + return randomElement(candidateValues); +} + +/** + * Wait for all promises to complete, then throw an Error only if any of the promises were rejected. + * @param promises Promises to be awaited. + */ +export async function allFulfilled(promises: Promise[]): Promise { + const results = await Promise.allSettled(promises); + const failures = results + .map(result => result.status === 'rejected' && result.reason as unknown) + .filter(reason => !!reason) + .map(reason => inspect(reason)); + + if (failures.length > 0) { + const failMessages = '- ' + failures.join('\n- '); + throw new Error(`${failures.length} failures:\n${failMessages}\n`); + } +} + +export type PrintView = { + [K in keyof T]: T[K] extends Uint8Array ? string : T[K]; +}; + +export function printable(event: T): PrintView { + return Object.fromEntries( + Object.entries(event).map(([k, v]) => [k, v instanceof Uint8Array ? utf8Decoder.decode(v) : v]) + ) as PrintView; +} + +export function assertAllDefined(values: (T | undefined)[], message: string | (() => string)): T[] { + values.forEach(value => assertDefined(value, message)); + return values as T[]; +} + +export function assertDefined(value: T | undefined, message: string | (() => string)): T { + if (value == undefined) { + throw new Error(typeof message === 'string' ? message : message()); + } + + return value; +} diff --git a/full-stack-asset-transfer-guide/applications/trader-typescript/tsconfig.json b/full-stack-asset-transfer-guide/applications/trader-typescript/tsconfig.json new file mode 100644 index 00000000..b9ca6a3b --- /dev/null +++ b/full-stack-asset-transfer-guide/applications/trader-typescript/tsconfig.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@tsconfig/node16/tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "rootDir": "src", + "noUnusedLocals": true, + "noImplicitReturns": true + }, + "include": [ + "src/" + ], + "exclude": [ + "src/**/*.spec.ts" + ] +} diff --git a/full-stack-asset-transfer-guide/check.sh b/full-stack-asset-transfer-guide/check.sh new file mode 100755 index 00000000..459843f4 --- /dev/null +++ b/full-stack-asset-transfer-guide/check.sh @@ -0,0 +1,167 @@ +#!/usr/bin/env bash + +SUCCESS="✅" +WARN="⚠️ " +EXIT=0 + +if ! command -v docker &> /tmp/cmdpath +then + echo "${WARN} Please install Docker; suggested install commands:" + EXIT=1 +else + echo -e "${SUCCESS} Docker found:\t$(cat /tmp/cmdpath)" +fi + +KUBECTL_VERSION=v1.24.4 # $(curl -L -s https://dl.k8s.io/release/stable.txt) +if ! command -v kubectl &> /tmp/cmdpath +then + echo "${WARN} Please install kubectl if you want to use k8s; suggested install commands:" + + if [ $(uname -s) = Darwin ]; then + if [ $(uname -m) = arm64 ]; then + echo "curl -LO https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/darwin/arm64/kubectl" + echo "chmod +x ./kubectl" + echo "sudo mv ./kubectl /usr/local/bin/kubectl" + echo "sudo chown root: /usr/local/bin/kubectl" + else + echo "curl -LO https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/darwin/amd64/kubectl" + echo "chmod +x ./kubectl" + echo "sudo mv ./kubectl /usr/local/bin/kubectl" + echo "sudo chown root: /usr/local/bin/kubectl" + fi + else + echo "curl -LO https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl" + echo "sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl" + fi + EXIT=1 +else + echo -e "${SUCCESS} kubectl found:\t$(cat /tmp/cmdpath)" + + KUBECTL_CLIENT_VERSION=$(kubectl version --client --output=yaml | grep gitVersion | cut -c 15-) + KUBECTL_CLIENT_MINOR_VERSION=$(kubectl version --client --output=yaml | grep minor | cut -c 11-12) + if [ "${KUBECTL_CLIENT_MINOR_VERSION}" -lt "24" ]; then + echo -e "${WARN} Found kubectl client version ${KUBECTL_CLIENT_VERSION}, which may be out of date. Please ensure client version >= ${KUBECTL_VERSION}" + EXIT=1 + fi +fi + +# Install kind +KIND_VERSION=0.14.0 +if ! command -v kind &> /tmp/cmdpath +then + echo "${WARN} Please install kind; suggested install commands:" + echo + if [ $(uname -s) = Darwin ]; then + if [ $(uname -m) = arm64 ]; then + echo "sudo curl --fail --silent --show-error -L https://kind.sigs.k8s.io/dl/v${KIND_VERSION}/kind-darwin-arm64 -o /usr/local/bin/kind" + else + echo "sudo curl --fail --silent --show-error -L https://kind.sigs.k8s.io/dl/v${KIND_VERSION}/kind-darwin-amd64 -o /usr/local/bin/kind" + fi + else + echo "sudo curl --fail --silent --show-error -L https://kind.sigs.k8s.io/dl/v${KIND_VERSION}/kind-linux-amd64 -o /usr/local/bin/kind" + fi + echo "sudo chmod 755 /usr/local/bin/kind" + echo + EXIT=1 +else + echo -e "${SUCCESS} kind found:\t\t$(cat /tmp/cmdpath)" +fi + +# Install k9s +K9S_VERSION=0.25.3 +if ! command -v k9s &> /tmp/cmdpath +then + echo "${WARN} Please install k9s; suggested install commands:" + echo + if [ $(uname -s) = Darwin ]; then + if [ $(uname -m) = arm64 ]; then + echo "curl --fail --silent --show-error -L https://github.com/derailed/k9s/releases/download/v${K9S_VERSION}/k9s_Darwin_arm64.tar.gz -o /tmp/k9s_Darwin_arm64.tar.gz" + echo "tar -zxf /tmp/k9s_Darwin_arm64.tar.gz -C /usr/local/bin k9s" + else + echo "curl --fail --silent --show-error -L https://github.com/derailed/k9s/releases/download/v${K9S_VERSION}/k9s_Darwin_x86_64.tar.gz -o /tmp/k9s_Darwin_x86_64.tar.gz" + echo "tar -zxf /tmp/k9s_Darwin_x86_64.tar.gz -C /usr/local/bin k9s" + fi + else + echo "curl --fail --silent --show-error -L https://github.com/derailed/k9s/releases/download/v${K9S_VERSION}/k9s_Linux_x86_64.tar.gz -o /tmp/k9s_Linux_x86_64.tar.gz" + echo "tar -zxf /tmp/k9s_Linux_x86_64.tar.gz -C /usr/local/bin k9s" + fi + echo "sudo chown root /usr/local/bin/k9s" + echo "sudo chmod 755 /usr/local/bin/k9s" + echo + EXIT=1 +else + echo -e "${SUCCESS} k9s found:\t\t$(cat /tmp/cmdpath)" +fi + +# Install just +JUST_VERSION=1.2.0 +if ! command -v just &> /tmp/cmdpath +then + echo "${WARN} Please install just; suggested install commands:" + echo "curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --tag ${JUST_VERSION} --to /usr/local/bin" + EXIT=1 +else + echo -e "${SUCCESS} Just found:\t\t$(cat /tmp/cmdpath)" +fi + +# Install weft +if ! command -v weft &> /tmp/cmdpath +then + echo "${WARN} Please install weft; suggested install commands:" + echo "npm install -g @hyperledger-labs/weft" + EXIT=1 +else + echo -e "${SUCCESS} weft found:\t\t$(cat /tmp/cmdpath)" +fi + +# Install jq +if ! command -v jq &> /tmp/cmdpath +then + echo "${WARN} Please install jq; suggested install commands:" + echo "sudo apt-update && sudo apt-install -y jq" + EXIT=1 +else + echo -e "${SUCCESS} jq found:\t\t$(cat /tmp/cmdpath)" +fi + + +if ! command -v peer &> /tmp/cmdpath +then + echo "${WARN} Please install the peer; suggested install commands:" + echo "curl -sSL https://raw.githubusercontent.com/hyperledger/fabric/main/scripts/install-fabric.sh | bash -s -- binary" + echo 'export WORKSHOP_PATH=$(pwd)' + echo 'export PATH=${WORKSHOP_PATH}/bin:$PATH' + echo 'export FABRIC_CFG_PATH=${WORKSHOP_PATH}/config' + EXIT=1 +else + echo -e "${SUCCESS} peer found:\t\t$(cat /tmp/cmdpath)" + + # double-check that the peer binary is compiled for the correct arch. This can occur when installing fabric + # binaries into a multipass VM, then running the Linux binaries from a Mac or windows Host OS via the volume share. + peer version &> /dev/null + rc=$? + if [ $rc -ne 0 ]; then + echo -e "${WARN} Could not execute peer. Was it compiled for the correct architecture?" + peer version + fi +fi + +# tests if varname is defined in the env AND it's an existing directory +function must_declare() { + local varname=$1 + + if [[ ! -d ${!varname} ]]; then + echo "${WARN} ${varname} must be set to a directory" + EXIT=1 + + else + echo -e "${SUCCESS} ${varname}:\t${!varname}" + fi +} + +must_declare "FABRIC_CFG_PATH" +must_declare "WORKSHOP_PATH" + +rm /tmp/cmdpath &> /dev/null + +exit $EXIT diff --git a/full-stack-asset-transfer-guide/checks/check-chaincode.sh b/full-stack-asset-transfer-guide/checks/check-chaincode.sh new file mode 100755 index 00000000..3a6cf115 --- /dev/null +++ b/full-stack-asset-transfer-guide/checks/check-chaincode.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +set -eou pipefail + +# All checks run in the workshop root folder +cd "$(dirname "$0")"/.. + +. checks/utils.sh + +EXIT=0 + +function chaincode_ready() { + peer chaincode query -n asset-transfer -C mychannel -c '{"Args":["org.hyperledger.fabric:GetMetadata"]}' +} + +must_declare WORKSHOP_CRYPTO + +must_declare CORE_PEER_LOCALMSPID +must_declare CORE_PEER_ADDRESS +must_declare CORE_PEER_TLS_ENABLED +must_declare CORE_PEER_MSPCONFIGPATH +must_declare CORE_PEER_TLS_ROOTCERT_FILE +must_declare CORE_PEER_CLIENT_CONNTIMEOUT +must_declare CORE_PEER_DELIVERYCLIENT_CONNTIMEOUT +must_declare ORDERER_ENDPOINT +must_declare ORDERER_TLS_CERT + +check chaincode_ready "asset-transfer chaincode is running" + +exit $EXIT diff --git a/full-stack-asset-transfer-guide/checks/check-kube.sh b/full-stack-asset-transfer-guide/checks/check-kube.sh new file mode 100755 index 00000000..82fb354b --- /dev/null +++ b/full-stack-asset-transfer-guide/checks/check-kube.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +set -eo pipefail + +# All checks run in the workshop root folder +cd "$(dirname "$0")"/.. + +. checks/utils.sh + +EXIT=0 + + +function cluster_info() { + kubectl cluster-info &>/dev/null +} + +function nginx() { + kubectl -n ingress-nginx get all &>/dev/null + kubectl -n ingress-nginx get deployment.apps/ingress-nginx-controller &>/dev/null + curl http://${WORKSHOP_INGRESS_DOMAIN} &>/dev/null + curl --insecure https://${WORKSHOP_INGRESS_DOMAIN}:443 &>/dev/null +} + +function container_registry() { + curl --fail http://${WORKSHOP_INGRESS_DOMAIN}:5000/v2/_catalog &>/dev/null +} + + +must_declare WORKSHOP_INGRESS_DOMAIN +must_declare WORKSHOP_NAMESPACE + +check cluster_info "k8s API controller is running" +check nginx "Nginx ingress is running at https://${WORKSHOP_INGRESS_DOMAIN}" + +if [ x"${WORKSHOP_CLUSTER_RUNTIME}" == x"kind" ]; then + check container_registry "Container registry is running at ${WORKSHOP_INGRESS_DOMAIN}:5000" +fi + +exit $EXIT + diff --git a/full-stack-asset-transfer-guide/checks/check-network.sh b/full-stack-asset-transfer-guide/checks/check-network.sh new file mode 100755 index 00000000..157d15b5 --- /dev/null +++ b/full-stack-asset-transfer-guide/checks/check-network.sh @@ -0,0 +1,72 @@ +#!/bin/bash + +set -eou pipefail + +# All checks run in the workshop root folder +cd "$(dirname "$0")"/.. + +. checks/utils.sh + +EXIT=0 + +function operator_crds() { + kubectl get customresourcedefinition.apiextensions.k8s.io/ibpcas.ibp.com + kubectl get customresourcedefinition.apiextensions.k8s.io/ibpconsoles.ibp.com + kubectl get customresourcedefinition.apiextensions.k8s.io/ibporderers.ibp.com + kubectl get customresourcedefinition.apiextensions.k8s.io/ibppeers.ibp.com +} + +function fabric_resources() { + # Did it apply the CRDs? + kubectl -n ${WORKSHOP_NAMESPACE} get ibpca org0-ca + kubectl -n ${WORKSHOP_NAMESPACE} get ibpca org1-ca + kubectl -n ${WORKSHOP_NAMESPACE} get ibpca org2-ca + kubectl -n ${WORKSHOP_NAMESPACE} get ibppeer org1-peer1 + kubectl -n ${WORKSHOP_NAMESPACE} get ibppeer org1-peer2 + kubectl -n ${WORKSHOP_NAMESPACE} get ibppeer org2-peer1 + kubectl -n ${WORKSHOP_NAMESPACE} get ibppeer org2-peer2 + kubectl -n ${WORKSHOP_NAMESPACE} get ibporderer org0-orderersnode1 + kubectl -n ${WORKSHOP_NAMESPACE} get ibporderer org0-orderersnode2 + kubectl -n ${WORKSHOP_NAMESPACE} get ibporderer org0-orderersnode3 +} + +function fabric_deployment() { + kubectl -n ${WORKSHOP_NAMESPACE} get deployment fabric-operator + kubectl -n ${WORKSHOP_NAMESPACE} get deployment org0-ca + kubectl -n ${WORKSHOP_NAMESPACE} get deployment org0-orderersnode1 + kubectl -n ${WORKSHOP_NAMESPACE} get deployment org0-orderersnode2 + kubectl -n ${WORKSHOP_NAMESPACE} get deployment org0-orderersnode3 + kubectl -n ${WORKSHOP_NAMESPACE} get deployment org1-ca + kubectl -n ${WORKSHOP_NAMESPACE} get deployment org1-peer1 + kubectl -n ${WORKSHOP_NAMESPACE} get deployment org1-peer2 + kubectl -n ${WORKSHOP_NAMESPACE} get deployment org2-ca + kubectl -n ${WORKSHOP_NAMESPACE} get deployment org2-peer1 + kubectl -n ${WORKSHOP_NAMESPACE} get deployment org2-peer2 +} + +function cas_ready() { + WORKSHOP_CRYPTO=$WORKSHOP_PATH/infrastructure/sample-network/temp + + # Hit the CAs using the TLS certs, etc. + curl --fail -s --cacert $WORKSHOP_CRYPTO/cas/org0-ca/tls-cert.pem https://$WORKSHOP_NAMESPACE-org0-ca-ca.$WORKSHOP_INGRESS_DOMAIN/cainfo | jq -c + curl --fail -s --cacert $WORKSHOP_CRYPTO/cas/org1-ca/tls-cert.pem https://$WORKSHOP_NAMESPACE-org1-ca-ca.$WORKSHOP_INGRESS_DOMAIN/cainfo | jq -c + curl --fail -s --cacert $WORKSHOP_CRYPTO/cas/org2-ca/tls-cert.pem https://$WORKSHOP_NAMESPACE-org2-ca-ca.$WORKSHOP_INGRESS_DOMAIN/cainfo | jq -c +} + +function channel_msp() { + WORKSHOP_CRYPTO=$WORKSHOP_PATH/infrastructure/sample-network/temp + + find $WORKSHOP_CRYPTO/channel-msp +} + +must_declare WORKSHOP_PATH +must_declare FABRIC_CFG_PATH + +check operator_crds "fabric-operator CRDs have been installed" +check fabric_resources "Network Peers, Orderers, and CAs have been created" +check fabric_deployment "Service deployments are ready" +check cas_ready "Certificate Authorities are running" +check channel_msp "Channel has been created" + +exit $EXIT + diff --git a/full-stack-asset-transfer-guide/checks/utils.sh b/full-stack-asset-transfer-guide/checks/utils.sh new file mode 100644 index 00000000..9c4a14ef --- /dev/null +++ b/full-stack-asset-transfer-guide/checks/utils.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# +# Copyright IBM Corp All Rights Reserved +# +# SPDX-License-Identifier: Apache-2.0 +# + +SUCCESS="✅" +WARN="⚠️ " + +# tests if varname is defined in the env AND it's an existing directory +function must_declare() { + local varname=$1 + + if [[ ${!varname+x} ]] + then + printf "%s %-40s%s\n" $SUCCESS $varname ${!varname} + else + printf "%s %-40s\n" ${WARN} $varname + EXIT=1 + fi +} + + +function check() { + local name=$1 + local message=$2 + + printf "🤔 %s" $name + + if $name &>/dev/null ; then + printf "\r%s %-40s" $SUCCESS $name + else + printf "\r%s %-40s" $WARN $name + EXIT=1 + fi + + echo $message +} \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/.eslintrc.js b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/.eslintrc.js new file mode 100644 index 00000000..b6c93bfb --- /dev/null +++ b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/.eslintrc.js @@ -0,0 +1,101 @@ +module.exports = { + env: { + node: true, + es2021: true, + }, + extends: [ + 'eslint:recommended', + ], + root: true, + ignorePatterns: [ + 'dist/', + ], + rules: { + 'arrow-spacing': ['error'], + 'comma-style': ['error'], + complexity: ['error', 10], + 'eol-last': ['error'], + 'generator-star-spacing': ['error', 'after'], + 'key-spacing': [ + 'error', + { + beforeColon: false, + afterColon: true, + mode: 'minimum', + }, + ], + 'keyword-spacing': ['error'], + 'no-multiple-empty-lines': ['error'], + 'no-trailing-spaces': ['error'], + 'no-whitespace-before-property': ['error'], + 'object-curly-newline': ['error'], + 'padded-blocks': ['error', 'never'], + 'rest-spread-spacing': ['error'], + 'semi-style': ['error'], + 'space-before-blocks': ['error'], + 'space-in-parens': ['error'], + 'space-unary-ops': ['error'], + 'spaced-comment': ['error'], + 'template-curly-spacing': ['error'], + 'yield-star-spacing': ['error', 'after'], + }, + overrides: [ + { + files: [ + '**/*.ts', + ], + parser: '@typescript-eslint/parser', + parserOptions: { + sourceType: 'module', + ecmaFeatures: { + impliedStrict: true, + }, + project: './tsconfig.json', + tsconfigRootDir: process.env.TSCONFIG_ROOT_DIR || __dirname, + }, + plugins: [ + '@typescript-eslint', + ], + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-requiring-type-checking', + ], + rules: { + '@typescript-eslint/comma-spacing': ['error'], + '@typescript-eslint/explicit-function-return-type': [ + 'error', + { + allowExpressions: true, + }, + ], + '@typescript-eslint/func-call-spacing': ['error'], + '@typescript-eslint/member-delimiter-style': ['error'], + '@typescript-eslint/indent': [ + 'error', + 4, + { + SwitchCase: 0, + ignoredNodes: ["PropertyDefinition"] + }, + ], + + '@typescript-eslint/prefer-nullish-coalescing': ['error'], + '@typescript-eslint/prefer-optional-chain': ['error'], + '@typescript-eslint/prefer-reduce-type-parameter': ['error'], + '@typescript-eslint/prefer-return-this-type': ['error'], + '@typescript-eslint/quotes': ['error', 'single'], + '@typescript-eslint/type-annotation-spacing': ['error'], + '@typescript-eslint/semi': ['error'], + '@typescript-eslint/space-before-function-paren': [ + 'error', + { + anonymous: 'never', + named: 'never', + asyncArrow: 'always', + }, + ], + }, + }, + ], +}; diff --git a/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/.gitignore b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/.gitignore new file mode 100644 index 00000000..90f52b2e --- /dev/null +++ b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/.gitignore @@ -0,0 +1,19 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + + +# Coverage directory used by tools like istanbul +coverage + +# Dependency directories +node_modules/ +jspm_packages/ +package-lock.json +npm-shrinkwrap.json + +# Compiled TypeScript files +dist + +# Files created during workshop run +metadata.json diff --git a/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/.npmrc b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/.npmrc new file mode 100644 index 00000000..b6f27f13 --- /dev/null +++ b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/Dockerfile b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/Dockerfile new file mode 100644 index 00000000..a4213d34 --- /dev/null +++ b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/Dockerfile @@ -0,0 +1,69 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# +FROM node:16.14 AS builder + +WORKDIR /usr/src/app + +# Copy node.js source and build, changing owner as well +COPY --chown=node:node . /usr/src/app +ENV npm_config_cache=/usr/src/app +RUN npm install +RUN npm run build && npm shrinkwrap + + +FROM node:16.14 as prod-builder +WORKDIR /usr/src/app +COPY --chown=node:node --from=builder /usr/src/app/dist ./dist +COPY --chown=node:node --from=builder /usr/src/app/package.json ./ +COPY --chown=node:node --from=builder /usr/src/app/npm-shrinkwrap.json ./ +RUN npm ci --omit=dev && npm cache clean --force + +# ------------------------------------------------------------------------------ +# Builds the Chaincode as a Service docker version +FROM node:16.14 AS ccaas +WORKDIR /usr/src/app + +ARG TARGETARCH +ARG TARGETOS + +COPY --chown=node:node --from=prod-builder /usr/src/app . +COPY --chown=node:node docker/docker-entrypoint.sh /usr/src/app/docker-entrypoint.sh + +ARG CC_SERVER_PORT +ENV PORT $CC_SERVER_PORT +EXPOSE $CC_SERVER_PORT + +ENV TINI_VERSION=v0.19.0 +ENV PLATFORM=${TARGETARCH} +ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-${PLATFORM} /tini +RUN chmod +x /tini + +ENV NODE_ENV=production +USER node +ENTRYPOINT [ "/tini", "--", "/usr/src/app/docker-entrypoint.sh" ] + + + +# ------------------------------------------------------------------------------ +# Builds the chaincode for the k8s builder +FROM node:16.14 AS k8s +WORKDIR /usr/src/app + +ARG TARGETARCH +ARG TARGETOS + +COPY --chown=node:node --from=prod-builder /usr/src/app . +COPY --chown=node:node docker/docker-entrypoint.sh /usr/src/app/docker-entrypoint.sh + +RUN printenv + +ENV TINI_VERSION=v0.19.0 +ENV PLATFORM=${TARGETARCH} +ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-${PLATFORM} /tini +RUN chmod +x /tini + +ENV NODE_ENV=production +USER node +ENTRYPOINT [ "/tini", "--", "/usr/src/app/docker-entrypoint.sh" ] + diff --git a/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/asset-transfer-chaincode-vars.yml b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/asset-transfer-chaincode-vars.yml new file mode 100644 index 00000000..0838df68 --- /dev/null +++ b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/asset-transfer-chaincode-vars.yml @@ -0,0 +1,11 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +smart_contract_name: "asset-transfer" +smart_contract_version: "1.0.0" +smart_contract_sequence: 1 +smart_contract_package: "asset-transfer.tgz" +# smart_contract_constructor: "initLedger" +smart_contract_endorsement_policy: "" +smart_contract_collections_file: "" diff --git a/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/docker/docker-entrypoint.sh b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/docker/docker-entrypoint.sh new file mode 100755 index 00000000..4aaac8c5 --- /dev/null +++ b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/docker/docker-entrypoint.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# +# SPDX-License-Identifier: Apache-2.0 +# +set -euo pipefail +: ${CORE_PEER_TLS_ENABLED:="false"} +: ${DEBUG:="false"} + +if [ "${DEBUG,,}" = "true" ]; then + npm run start:server-debug + +elif [[ ! -v CHAINCODE_SERVER_ADDRESS ]]; then + npm start -- --peer.address $CORE_PEER_ADDRESS + +elif [ "${CORE_PEER_TLS_ENABLED,,}" = "true" ]; then + npm run start:server + +else + npm run start:server-nontls +fi + diff --git a/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/package.json b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/package.json new file mode 100644 index 00000000..1b6f63ff --- /dev/null +++ b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/package.json @@ -0,0 +1,62 @@ +{ + "name": "asset-transfer", + "version": "1.0.0", + "description": "Asset Transfer contract implemented in TypeScript", + "main": "dist/index.js", + "typings": "dist/index.d.ts", + "engines": { + "node": ">=14" + }, + "scripts": { + "lint": "eslint ./src --ext .ts", + "pretest": "npm run lint", + "test": "", + "start": "set -x && fabric-chaincode-node start", + "build": "tsc", + "build:watch": "tsc -w", + "prepublishOnly": "npm run build", + "metadata": "set -x && fabric-chaincode-node metadata generate --file metadata.json", + "docker": "docker build -f ./Dockerfile -t asset-transfer-basic .", + "package:caas": "npm run build && weft chaincode package caas --path . --label asset-transfer --address ${CHAINCODE_SERVER_ADDRESS} --archive asset-transfer-caas.tgz --quiet", + "package:k8s": "npm run build && weft chaincode package caas --path . --label asset-transfer --address ${CHAINCODE_SERVER_ADDRESS} --archive asset-transfer-caas.tgz --quiet", + "start:server-nontls": "set -x && fabric-chaincode-node server --chaincode-address=$CHAINCODE_SERVER_ADDRESS --chaincode-id=$CHAINCODE_ID", + "start:server-debug": "set -x && NODE_OPTIONS='--inspect=0.0.0.0:9229' fabric-chaincode-node server --chaincode-address=$CHAINCODE_SERVER_ADDRESS --chaincode-id=$CHAINCODE_ID", + "start:server": "set -x && fabric-chaincode-node server --chaincode-address=$CHAINCODE_SERVER_ADDRESS --chaincode-id=$CHAINCODE_ID --chaincode-tls-key-file=/hyperledger/privatekey.pem --chaincode-tls-client-cacert-file=/hyperledger/rootcert.pem --chaincode-tls-cert-file=/hyperledger/cert.pem" + }, + "author": "Hyperledger", + "license": "Apache-2.0", + "dependencies": { + "fabric-contract-api": "^2.4.0", + "fabric-shim": "^2.4.0", + "json-stringify-deterministic": "^1.0.7", + "sort-keys-recursive": "^2.1.7" + }, + "devDependencies": { + "@tsconfig/node16": "^1.0.3", + "@types/node": "^16.11.46", + "@typescript-eslint/eslint-plugin": "^5.30.7", + "@typescript-eslint/parser": "^5.30.7", + "eslint": "^8.20.0", + "typescript": "~4.7.4" + }, + "nyc": { + "extension": [ + ".ts", + ".tsx" + ], + "exclude": [ + "coverage/**", + "dist/**" + ], + "reporter": [ + "text-summary", + "html" + ], + "all": true, + "check-coverage": true, + "statements": 100, + "branches": 100, + "functions": 100, + "lines": 100 + } +} diff --git a/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/src/asset.ts b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/src/asset.ts new file mode 100644 index 00000000..c3a4db3a --- /dev/null +++ b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/src/asset.ts @@ -0,0 +1,45 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ + +import { Object as DataType, Property } from 'fabric-contract-api'; + +@DataType() +export class Asset { + @Property('ID', 'string') + ID = ''; + + @Property('Color', 'string') + Color = ''; + + @Property('Owner', 'string') + Owner = ''; + + @Property('AppraisedValue', 'number') + AppraisedValue = 0; + + @Property('Size', 'number') + Size = 0; + + constructor() { + // Nothing to do + } + + static newInstance(state: Partial = {}): Asset { + return { + ID: assertHasValue(state.ID, 'Missing ID'), + Color: state.Color ?? '', + Size: state.Size ?? 0, + Owner: assertHasValue(state.Owner, 'Missing Owner'), + AppraisedValue: state.AppraisedValue ?? 0, + }; + } +} + +function assertHasValue(value: T | undefined | null, message: string): T { + if (value == undefined || (typeof value === 'string' && value.length === 0)) { + throw new Error(message); + } + + return value; +} diff --git a/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/src/assetTransfer.ts b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/src/assetTransfer.ts new file mode 100644 index 00000000..0fd27cb7 --- /dev/null +++ b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/src/assetTransfer.ts @@ -0,0 +1,225 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +import { X509Certificate } from 'crypto'; +import { Context, Contract, Info, Param, Returns, Transaction } from 'fabric-contract-api'; +import { KeyEndorsementPolicy } from 'fabric-shim'; +import stringify from 'json-stringify-deterministic'; // Deterministic JSON.stringify() +import sortKeysRecursive from 'sort-keys-recursive'; +import { TextDecoder } from 'util'; +import { Asset } from './asset'; + +const utf8Decoder = new TextDecoder(); + +@Info({title: 'AssetTransfer', description: 'Smart contract for trading assets'}) +export class AssetTransferContract extends Contract { + /** + * CreateAsset issues a new asset to the world state with given details. + */ + @Transaction() + @Param('assetObj', 'Asset', 'Part formed JSON of Asset') + async CreateAsset(ctx: Context, state: Asset): Promise { + state.Owner = toJSON(clientIdentifier(ctx, state.Owner)); + const asset = Asset.newInstance(state); + + const exists = await this.AssetExists(ctx, asset.ID); + if (exists) { + throw new Error(`The asset ${asset.ID} already exists`); + } + + const assetBytes = marshal(asset); + await ctx.stub.putState(asset.ID, assetBytes); + + await setEndorsingOrgs(ctx, asset.ID, ctx.clientIdentity.getMSPID()); + + ctx.stub.setEvent('CreateAsset', assetBytes); + } + + /** + * ReadAsset returns an existing asset stored in the world state. + */ + @Transaction(false) + @Returns('Asset') + async ReadAsset(ctx: Context, id: string): Promise { + const existingAssetBytes = await this.#readAsset(ctx, id); + const existingAsset = Asset.newInstance(unmarshal(existingAssetBytes)); + + return existingAsset; + } + + async #readAsset(ctx: Context, id: string): Promise { + const assetBytes = await ctx.stub.getState(id); // get the asset from chaincode state + if (!assetBytes || assetBytes.length === 0) { + throw new Error(`Sorry, asset ${id} has not been created`); + } + + return assetBytes; + } + + /** + * UpdateAsset updates an existing asset in the world state with provided partial asset data, which must include + * the asset ID. + */ + @Transaction() + @Param('assetObj', 'Asset', 'Part formed JSON of Asset') + async UpdateAsset(ctx: Context, assetUpdate: Asset): Promise { + if (assetUpdate.ID === undefined) { + throw new Error('No asset ID specified'); + } + + const existingAssetBytes = await this.#readAsset(ctx, assetUpdate.ID); + const existingAsset = Asset.newInstance(unmarshal(existingAssetBytes)); + + if (!hasWritePermission(ctx, existingAsset)) { + throw new Error('Only owner can update assets'); + } + + const updatedState = Object.assign({}, existingAsset, assetUpdate, { + Owner: existingAsset.Owner, // Must transfer to change owner + }); + const updatedAsset = Asset.newInstance(updatedState); + + // overwriting original asset with new asset + const updatedAssetBytes = marshal(updatedAsset); + await ctx.stub.putState(updatedAsset.ID, updatedAssetBytes); + + await setEndorsingOrgs(ctx, updatedAsset.ID, ctx.clientIdentity.getMSPID()); + + ctx.stub.setEvent('UpdateAsset', updatedAssetBytes); + } + + /** + * DeleteAsset deletes an asset from the world state. + */ + @Transaction() + async DeleteAsset(ctx: Context, id: string): Promise { + const assetBytes = await this.#readAsset(ctx, id); // Throws if asset does not exist + const asset = Asset.newInstance(unmarshal(assetBytes)); + + if (!hasWritePermission(ctx, asset)) { + throw new Error('Only owner can delete assets'); + } + + await ctx.stub.deleteState(id); + + ctx.stub.setEvent('DeletaAsset', assetBytes); + } + + /** + * AssetExists returns true when asset with the specified ID exists in world state; otherwise false. + */ + @Transaction(false) + @Returns('boolean') + async AssetExists(ctx: Context, id: string): Promise { + const assetJson = await ctx.stub.getState(id); + return assetJson?.length > 0; + } + + /** + * TransferAsset updates the owner field of asset with the specified ID in the world state. + */ + @Transaction() + async TransferAsset(ctx: Context, id: string, newOwner: string, newOwnerOrg: string): Promise { + const assetString = await this.#readAsset(ctx, id); + const asset = Asset.newInstance(unmarshal(assetString)); + + if (!hasWritePermission(ctx, asset)) { + throw new Error('Only owner can transfer assets'); + } + + asset.Owner = toJSON(ownerIdentifier(newOwner, newOwnerOrg)); + + const assetBytes = marshal(asset); + await ctx.stub.putState(id, assetBytes); + + await setEndorsingOrgs(ctx, id, newOwnerOrg); // Subsequent updates must be endorsed by the new owning org + + ctx.stub.setEvent('TransferAsset', assetBytes); + } + + /** + * GetAllAssets returns a list of all assets found in the world state. + */ + @Transaction(false) + @Returns('string') + async GetAllAssets(ctx: Context): Promise { + // range query with empty string for startKey and endKey does an open-ended query of all assets in the chaincode namespace. + const iterator = await ctx.stub.getStateByRange('', ''); + + const assets: Asset[] = []; + for (let result = await iterator.next(); !result.done; result = await iterator.next()) { + const assetBytes = result.value.value; + try { + const asset = Asset.newInstance(unmarshal(assetBytes)); + assets.push(asset); + } catch (err) { + console.log(err); + } + } + + return marshal(assets).toString(); + } +} + +function unmarshal(bytes: Uint8Array | string): object { + const json = typeof bytes === 'string' ? bytes : utf8Decoder.decode(bytes); + const parsed: unknown = JSON.parse(json); + if (parsed === null || typeof parsed !== 'object') { + throw new Error(`Invalid JSON type (${typeof parsed}): ${json}`); + } + + return parsed; +} + +function marshal(o: object): Buffer { + return Buffer.from(toJSON(o)); +} + +function toJSON(o: object): string { + // Insert data in alphabetic order using 'json-stringify-deterministic' and 'sort-keys-recursive' + return stringify(sortKeysRecursive(o)); +} + +interface OwnerIdentifier { + org: string; + user: string; +} + +function hasWritePermission(ctx: Context, asset: Asset): boolean { + const clientId = clientIdentifier(ctx); + const ownerId = unmarshal(asset.Owner) as OwnerIdentifier; + return clientId.org === ownerId.org; +} + +function clientIdentifier(ctx: Context, user?: string): OwnerIdentifier { + return { + org: ctx.clientIdentity.getMSPID(), + user: user ?? clientCommonName(ctx), + }; +} + +function clientCommonName(ctx: Context): string { + const clientCert = new X509Certificate(ctx.clientIdentity.getIDBytes()); + const matches = clientCert.subject.match(/^CN=(.*)$/m); // [0] Matching string; [1] capture group + if (matches?.length !== 2) { + throw new Error(`Unable to identify client identity common name: ${clientCert.subject}`); + } + + return matches[1]; +} + +function ownerIdentifier(user: string, org: string): OwnerIdentifier { + return { org, user }; +} + +async function setEndorsingOrgs(ctx: Context, ledgerKey: string, ...orgs: string[]): Promise { + const policy = newMemberPolicy(...orgs); + await ctx.stub.setStateValidationParameter(ledgerKey, policy.getPolicy()); +} + +function newMemberPolicy(...orgs: string[]): KeyEndorsementPolicy { + const policy = new KeyEndorsementPolicy(); + policy.addOrgs('MEMBER', ...orgs); + return policy; +} diff --git a/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/src/index.ts b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/src/index.ts new file mode 100644 index 00000000..06f92f05 --- /dev/null +++ b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/src/index.ts @@ -0,0 +1,9 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +import { AssetTransferContract } from './assetTransfer'; + +export { AssetTransferContract } from './assetTransfer'; + +export const contracts: any[] = [AssetTransferContract]; // eslint-disable-line @typescript-eslint/no-explicit-any diff --git a/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/src/untyped.d.ts b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/src/untyped.d.ts new file mode 100644 index 00000000..4c2910bb --- /dev/null +++ b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/src/untyped.d.ts @@ -0,0 +1,10 @@ +declare module 'json-stringify-deterministic' { + interface Options { + space?: string; + cycles?: boolean; + replacer?: (k, v) => v; + stringify?: typeof JSON.stringify; + } + + export default function stringify(o: unknown, options?: Options): string; +} diff --git a/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/tsconfig.json b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/tsconfig.json new file mode 100644 index 00000000..77cc5bbd --- /dev/null +++ b/full-stack-asset-transfer-guide/contracts/asset-transfer-typescript/tsconfig.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@tsconfig/node16/tsconfig.json", + "compilerOptions": { + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "outDir": "dist", + "declaration": true, + "sourceMap": true, + "noUnusedLocals": true, + "noImplicitReturns": true + }, + "include": [ + "./src/**/*" + ], + "exclude": [ + "./src/**/*.spec.ts" + ] +} diff --git a/full-stack-asset-transfer-guide/docs/ApplicationDev/01-FabricGateway.md b/full-stack-asset-transfer-guide/docs/ApplicationDev/01-FabricGateway.md new file mode 100644 index 00000000..75ba3bc8 --- /dev/null +++ b/full-stack-asset-transfer-guide/docs/ApplicationDev/01-FabricGateway.md @@ -0,0 +1,76 @@ +# Fabric Gateway + +From Fabric v2.4 onwards, the [Fabric Gateway client API](https://hyperledger.github.io/fabric-gateway/) is the recommended API for building client applications. There are implementations in Go, Node (TypeScript / JavaScript) and Java, each providing identical capability and behavior. The client API makes use of a Fabric Gateway service embedded within Fabric v2.4+ peers. This topic describes the Fabric Gateway model and considerations for production deployment. + +The concepts described here map directly to methods provided by the client API, and will help you to understand the client API behavior. Provided you have a clear understanding of the Fabric transaction flow, you can treat this topic as reference material. + +## Background + +To understand how Fabric Gateway works, it is necessary to understand the Fabric transaction submit (and evaluate) flow. This section provides a brief recap of the Fabric transaction flow from the perspective of the client. + +### Transaction submit flow + +Submit represents a ledger update. In the following diagram, solid orange lines represent interactions between the client and network nodes, while dashed green lines represent interactions between network nodes. + +![Transaction submit flow](../images/ApplicationDev/transaction-submit-flow.png) + +1. **Endorse:** client sends transaction proposal to peers for endorsement. + - The peer executes the smart contract's transaction function against its *current* ledger state to produce a read/write set and a return value for the transaction. + - Successful endorsments must be gathered from sufficient organizations to meet the endorsement requirements, which may need to consider chaincode and state-based endorsement policies, chaincode-to-chaincode calls, and private data collections accessed by the transaction function; otherwise the transaction will fail *validation* later in the flow. +1. **Submit:** client sends endorsed transaction to an orderer to be committed into a block. +1. Orderers distribute committed blocks to all network peers, which validate the transactions against their *current* ledger state. + - Valid transactions have their read/write sets applied to update the ledger. + - Invalid transactions are marked with an appropriate validation code and do not update the ledger. + - A common reason for validation failure is MVCC_READ_CONFLICT, which means that the ledger keys accessed by the transaction were modified between endorsement and validation. This is recoverable by running the submit flow again. +1. **Commit:** client retrieves commit status for submitted transactions from peers and reports success or failure for the transaction submit depending on the transaction validation code. + +### Transaction evaluate flow + +Evaluate represents a query and is essentially just the *endorse* step of the transaction submit flow. + +1. **Evaluate:** client sends a transaction proposal to a suitable peer for endorsement and obtains a return value. + - The return value is based on the endorsing peer's *current* ledger state. + - Endorsement policies do not need to be satisfied since the transaction is not submitted to update the ledger. + - Access to private data collections needs to be considered when selecting peers. + +## Legacy client SDKs + +The following diagram demonstrates how the transaction submit flow is executed for a client using one of the legacy SDKs. Solid orange lines represent interactions between the client and network nodes, which must cross the firewall at the boundary of the network deployment. Dashed green lines represent interactions between network nodes. + +Notice that the client potentially needs to interact directly with any or all of the network nodes. + +![Legacy SDK model](../images/ApplicationDev/legacy-sdk-model.png) + +In order for the client application to operate effectively, it must make use of the discovery service provided by network peers. This requires additional network interactions beyond the ones shown in the transaction submit flow to: + +- Identify available network nodes. +- Obtain an endorsement plan based on client-supplied endorsement requirements. + +## Fabric Gateway client API + +The following diagram demonstrates for the transaction subsmit flow is executed for a client using the Fabric Gateway client API. Solid orange lines represent interactions between the client and a Gateway peer, which must cross the firewall at the boundary of the network deployment. Dashed green lines represent interactions between network nodes. + +Notice that the client only needs to interact directly with the Gateway peer. The Gateway peer operates as a client driving the transaction submit flow from within the network deployment on behalf of the client application. + +![Fabric Gateway model](../images/ApplicationDev/fabric-gateway-model.png) + + Since the Gateway is itself a peer, it has direct access to its ledger and service discovery information. This allows the client to avoid using the discovery service and to transact using only a single Gateway endpoint address. The Gateway peer is generally able to determine an appropriate endorsement plan automatically, avoiding the need for the client to have knowledge of endorsement requirements. + + For reference, a more detailed description of the Fabric Gateway service and its behavior can be found in the [Fabric documentation](https://hyperledger-fabric.readthedocs.io/en/release-2.4/gateway.html). + +## Production deployment of Fabric Gateway + +For security, client applications should connect only to Gateway peers within their own organization or, if the client's organization does not host their own peers, to Gateway peers of a trusted organization. + +The following diagram demonstrates recommended practice for enabling access to an organization cluster through a single endpoint address while maintaining high availability. This use of a load balancer or ingress controller as a proxy in front of a set of internal endpoints is commonly used when deploying Web or Application servers, so this pattern is well established. The gRPC communication between client and Gateway atually uses HTTP/2 as its transport. + +![Fabric Gateway deployment](../images/ApplicationDev/fabric-gateway-deployment.png) + +An alternative (or complementary) approach that can be employed is to assign multiple records for a single Gateway DNS name. This allows clients to select from a set of Gateway peer IP addresses associated with a single Gateway endpoint. + +Note that peers must include the externally visible endpoint address in their TLS certificates for clients to successfully complete a TLS handshake. + +For reference, more information is available in the gRPC documentation: + +- [gRPC load balancing](https://grpc.io/blog/grpc-load-balancing/). +- [gRPC name resolution](https://grpc.github.io/grpc/core/md_doc_naming.html). diff --git a/full-stack-asset-transfer-guide/docs/ApplicationDev/02-Exercise-RunApplication.md b/full-stack-asset-transfer-guide/docs/ApplicationDev/02-Exercise-RunApplication.md new file mode 100644 index 00000000..4b0dbfa4 --- /dev/null +++ b/full-stack-asset-transfer-guide/docs/ApplicationDev/02-Exercise-RunApplication.md @@ -0,0 +1,43 @@ +# Exercise: Run the client application + +> **Note:** This exercise requires the Fabric network and chaincode deployed in the [Smart Contract Development](../SmartContractDev/) exercises to be running. + +Let's make sure we can successfully run the client application and get some familiarity with how to use it. + +In a terminal window, navigate to the [applications/trader-typescript](../../applications/trader-typescript/) directory. Then complete the following steps: + +1. Install dependencies and build the client application. + ```bash + npm install + ``` + +1. Set environment variables to point to resources required by the application. + ```bash + export ENDPOINT=org1peer-api.127-0-0-1.nip.io:8080 + export MSP_ID=org1MSP + export CERTIFICATE=../../_cfg/uf/_msp/org1/org1admin/msp/signcerts/org1admin.pem + export PRIVATE_KEY=../../_cfg/uf/_msp/org1/org1admin/msp/keystore/cert_sk + ``` + +1. Run the **getAllAssets** command to check the assets that currently exist on the ledger (if any). + ```bash + npm start getAllAssets + ``` + +1. Run the **transact** command to create (and update / delete) some more sample assets. + ```bash + npm start transact + ``` + +1. Run the **getAllAssets** command again to see the new assets recorded on the ledger. + ```bash + npm start getAllAssets + ``` + +These application CLI commands represent a simplified application that performs one action per call. Note that real world applications will typically be long running and will make calls to a contract on behalf of user requests. + +## Optional steps + +Try using the **create**, **read** and **delete** commands to work with specific assets. + +See the application [Readme](../../applications/trader-typescript/README.md) for details on how to use the commands. diff --git a/full-stack-asset-transfer-guide/docs/ApplicationDev/03-ApplicationOverview.md b/full-stack-asset-transfer-guide/docs/ApplicationDev/03-ApplicationOverview.md new file mode 100644 index 00000000..f827e892 --- /dev/null +++ b/full-stack-asset-transfer-guide/docs/ApplicationDev/03-ApplicationOverview.md @@ -0,0 +1,132 @@ +# Application overview + +This topic describes key parts of the client application and how it uses the Fabric Gateway client API to interact with the network. This knowledge will allow you to extend the application in subsequent topics. + +## Connect to the Gateway service + +Connection to the peer Gateway service is driven by the **runCommand()** function in [app.ts](../../applications/trader-typescript/src/app.ts). This calls to two other functions to perform the two tasks required before the client application can transact with the Fabric network: + +1. **Create gRPC connection to peer Gateway endpoint** - this is done in the **newGrpcConnection()** function in [connect.ts](../../applications/trader-typescript/src/connect.ts): + ```typescript + const tlsCredentials = grpc.credentials.createSsl(tlsRootCert); + return new grpc.Client(GATEWAY_ENDPOINT, tlsCredentials); + ``` + The gRPC client connection is established using the [gRPC API](https://grpc.io/docs/) and is managed by the client application. The application can use the same gRPC connection to transact on behalf of many client identities. + +1. **Create peer Gateway connection** - this is done in the **newGatewayConnection()** function in [connect.ts](../../applications/trader-typescript/src/connect.ts): + ```typescript + return connect({ + client, + identity: await newIdentity(), + signer: await newSigner(), + // Default timeouts for different gRPC calls + evaluateOptions: () => { + return { deadline: Date.now() + 5000 }; // 5 seconds + }, + endorseOptions: () => { + return { deadline: Date.now() + 15000 }; // 15 seconds + }, + submitOptions: () => { + return { deadline: Date.now() + 5000 }; // 5 seconds + }, + commitStatusOptions: () => { + return { deadline: Date.now() + 60000 }; // 1 minute + }, + }); + ``` + The **Gateway** connection is established by calling the [connect()](https://hyperledger.github.io/fabric-gateway/main/api/node/functions/connect.html) factory function with a client [identity](https://hyperledger.github.io/fabric-gateway/main/api/node/interfaces/Identity.html) (user's X.509 certificate) and [signing implementation](https://hyperledger.github.io/fabric-gateway/main/api/node/functions/signers.newPrivateKeySigner.html) (based on the user's private key). It allows a specific user to interact with a Fabric network using the previously created gRPC connection. Optional configuration can also be supplied, and it is strongly recommended to include default timeouts for operations. + +## Application CLI commands + +All the CLI command implementations are located within the [commands](../../applications/trader-typescript/src/commands/) directory. Commands are exposed to [app.ts](../../applications/trader-typescript/src/app.ts) by [commands/index.ts](../../applications/trader-typescript/src/commands/index.ts). + +When invoked, the command is passed the **Gateway** instance it should use to interact with the Fabric network. To do useful work, command implementations typically performs these steps: + +1. **Get Network** - this represents a network of Fabric nodes belonging to a specific Fabric channel: + ```typescript + const network = gateway.getNetwork(CHANNEL_NAME); + ``` + +1. **Get Contract** - this represents a specific smart contract deployed in the **Network**: + ```typescript + const contract = network.getContract(CHAINCODE_NAME); + ``` + +1. **Create smart contract adapter** - this provides a view of the smart contract and its transaction functions in form that is easy to use for the client application business logic: + ```typescript + const smartContract = new AssetTransfer(contract); + ``` + +1. **Invoke transaction functions on a deployed chaincode** - for example: + - Create an asset in [commands/create.ts](../../applications/trader-typescript/src/commands/create.ts) + ```typescript + await smartContract.createAsset({ + ID: assetId, + Owner: owner, + Color: color, + Size: 1, + AppraisedValue: 1, + }); + ``` + - Read all assets in [commands/getAllAssets.ts](../../applications/trader-typescript/src/commands/getAllAssets.ts) + ```typescript + const assets = await smartContract.getAllAssets(); + ``` + +The application CLI commands represent a simplified application that performs one action per call. Note that real world applications will typically be long running, and will re-use a connection to the peer Gateway service when making transaction requests on behalf of client applications. The connection may utilize a single organization identity on behalf of various user requests. + +## Gateway API calls + +The **AssetTransfer** class in [contract.ts](../../applications/trader-typescript/src/contract.ts) presents the smart contract in a form appropriate to the business application. Internally it uses the Fabric Gateway client API to invoke transaction functions, and deals with the translation between the business application and API representation of parameters and return values. + +Refer to the [Contract API documentation](https://hyperledger.github.io/fabric-gateway/main/api/node/interfaces/Contract.html) for more details on the available calls. + +### Transaction submit + +The transaction submit function will submit the request to the peer Gateway service. The peer Gateway service will invoke chaincode and collect the required endorsements from different organization's peers to meet the contract's endorsement policy, and will then submit the transaction to the ordering service on behalf of the client application so that the blockchain ledger can be updated. + +An example of transaction submit is in the **createAsset()** method: + +```typescript +await this.#contract.submit('CreateAsset', { + arguments: [JSON.stringify(asset)], +}); +``` + +### Transaction evaluate + +The transaction evaluate function will request the peer Gateway service to invoke the chaincode and return the results to the client, without submitting a transaction to the ordering service. Use the evaluate function to query the state of the blockchain ledger. + +An example of evaluating a transaction is in the **getAllAssets()** method: +```typescript +const result = await this.#contract.evaluate('GetAllAssets'); +``` + +## Retry of transaction submit + +The nature of the transaction submit flow in Fabric means that failures can occur at different points in the flow. To aid client handling of failures, the Gateway API produces errors of specific types to indicate the point in the flow a failure occurred. The **submitWithRetry()** function in [contract.ts](../../applications/trader-typescript/src/contract.ts) retries transactions that fail to commit successfully: + +```typescript +let lastError: unknown | undefined; + +for (let retryCount = 0; retryCount < RETRIES; retryCount++) { + try { + return await submit(); + } catch (err: unknown) { + lastError = err; + if (err instanceof CommitError) { + // Transaction failed validation and did not update the ledger. Handle specific transaction validation codes. + if (err.code === StatusCode.MVCC_READ_CONFLICT) { + continue; // Retry + } + } + break; // Failure -- don't retry + } +} + +throw lastError; +``` + +See the [submit() API documentation](https://hyperledger.github.io/fabric-gateway/main/api/node/interfaces/Contract.html#submit) for the other error types that can be thrown. + +For some cases it can be useful to retry only a specific step within the transaction submit flow. The Gateway API provides a fine-grained flow to allow this. See the [Contract API documentation](https://hyperledger.github.io/fabric-gateway/main/api/node/interfaces/Contract.html) for examples of this fine-grained flow. diff --git a/full-stack-asset-transfer-guide/docs/ApplicationDev/04-Exercise-AssetTransfer.md b/full-stack-asset-transfer-guide/docs/ApplicationDev/04-Exercise-AssetTransfer.md new file mode 100644 index 00000000..0dbed183 --- /dev/null +++ b/full-stack-asset-transfer-guide/docs/ApplicationDev/04-Exercise-AssetTransfer.md @@ -0,0 +1,23 @@ +# Exercise: Implement asset transfer + +Currently, our trader application can only create, read, and delete assets by invoking the CreateAsset(), ReadAsset(), and DeleteAsset() chaincode functions. To really be useful it needs to be able to transfer assets to new owners by invoking the TransferAsset() chaincode function. + +There is already a **transfer** command implemented in [transfer.ts](../../applications/trader-typescript/src/commands/transfer.ts), which calls the `transferAsset()` method on our **AssetTransfer** class. Unfortunately, this has not yet been implemented and does nothing. + +1. Write an implementation for the `transferAsset()` method in [contract.ts](../../applications/trader-typescript/src/contract.ts). Look at the [API documentation for Contract](https://hyperledger.github.io/fabric-gateway/main/api/node/interfaces/Contract.html) and other methods within the **AssetTransfer** class for ideas on how to proceed. + +1. Recompile the application from your updated TypeScript: + ```bash + npm install + ``` + > **Tip:** You can also leave `npm run build:watch` running in a terminal window to automatically rebuild your application on any code change. + +1. Try it out! Use the **transfer** command to transfer assets to new owners with the same MSP ID. + +1. What happens if you try to manipulate (transfer, delete) an asset after transferring it to another MSP ID? + +The smart contract contains logic that only allows users in the owning organization to modify assets. It does this by checking that the Member Services Provider (MSP) ID for the client identity invoking the transaction matches the organization MSP ID of the asset owner. If you didn't notice this before, you might want to check out the smart contract code to see how this is implemented. + +## Optional steps + +Implement an **update** command in the client application to modify the properties of an asset. diff --git a/full-stack-asset-transfer-guide/docs/ApplicationDev/05-ChaincodeEvents.md b/full-stack-asset-transfer-guide/docs/ApplicationDev/05-ChaincodeEvents.md new file mode 100644 index 00000000..56ee296b --- /dev/null +++ b/full-stack-asset-transfer-guide/docs/ApplicationDev/05-ChaincodeEvents.md @@ -0,0 +1,20 @@ +# Chaincode events + +A smart contract transaction function can emit a chaincode event to communicate business events. These events are emitted only after a transaction is successfully committed and updates the ledger. Transactions that fail validation do not emit chaincode events. + +Client applications can listen for chaincode events and trigger external business processes in response to ledger updates. An example might be to schedule collection of a parcel after a delivery order is received. Events can either be replayed from any point in the blockchain, or received in realtime. + +When emitting a chaincode event, the smart contract can specify an arbitrary **payload** to be included in the event. The **payload** is used to communicate business context to client applications receiving the chaincode events. + +The [Network](https://hyperledger.github.io/fabric-gateway/main/api/node/interfaces/Network.html) object in the Gateway API provides methods to obtain chaincode events. + +To ensure the correct operation of business processes, it is important that each chaincode event is received exactly once. We don't want to collect the same parcel twice, or to miss a parcel collection! + +The Gateway API allows a [Checkpointer](https://hyperledger.github.io/fabric-gateway/main/api/node/interfaces/Checkpointer.html) to be used to track (or checkpoint) successfully processed events, and for eventing to be resumed exactly after the last checkpointed event if a failure or application restart occurs. + +For convenience, the Gateway API provides two checkpointer implementations: + +1. [File checkpointer](https://hyperledger.github.io/fabric-gateway/main/api/node/functions/checkpointers.file.html) that persists its state to the file-system. This can be used to resume eventing, even after an application restart. +1. [In-memory checkpointer](https://hyperledger.github.io/fabric-gateway/main/api/node/functions/checkpointers.inMemory.html) that stores its state only in-memory. This can be used to recover from transient failures, such as a network communication error, during a single application run. + +Client applications can also use their own checkpointer implementations, which persist their state in suitable storage such as a database, provided they conform to the simple [Checkpoint](https://hyperledger.github.io/fabric-gateway/main/api/node/interfaces/Checkpoint.html) interface. diff --git a/full-stack-asset-transfer-guide/docs/ApplicationDev/06-Exercise-ChaincodeEvents.md b/full-stack-asset-transfer-guide/docs/ApplicationDev/06-Exercise-ChaincodeEvents.md new file mode 100644 index 00000000..dadaabcb --- /dev/null +++ b/full-stack-asset-transfer-guide/docs/ApplicationDev/06-Exercise-ChaincodeEvents.md @@ -0,0 +1,46 @@ +# Exercise: Use chaincode events + +First, let's try listening for chaincode events to see what information is included in events emitted by the smart contract transaction functions. + +In a new terminal window, navigate to the [applications/trader-typescript](../../applications/trader-typescript/) directory so that we can run the listen application. +It is assumed that you have already built the application in prior steps. + +1. If you are using a new terminal window, set environment variables to point to resources required by the application. + ```bash + export ENDPOINT=org1peer-api.127-0-0-1.nip.io:8080 + export MSP_ID=org1MSP + export CERTIFICATE=../../_cfg/uf/_msp/org1/org1admin/msp/signcerts/org1admin.pem + export PRIVATE_KEY=../../_cfg/uf/_msp/org1/org1admin/msp/keystore/cert_sk + ``` + +1. Run the **listen** command to listen for ledger updates. The listen command will return prior events and also wait for future events. + ```bash + npm start listen + ``` + +1. Once you have received the available events, interrupt the application using `Control-C`. + +1. Run the **listen** command again. What do we see this time? + +On the second run of the **listen** command, you should have seen exactly the same output as the first run. This is because each run of the **listen** command retrieves all chaincode events from start of the blockchain. That's not so useful if we want to invoke external business processes in response to chaincode events. It would be much better if each event was received exactly once, regardless of whether the client application is restarted. + +Let's implement checkpointing to ensure there are no duplicate or missed events. + +5. Implement checkpointing for the reading of chaincode events in [listen.ts](../../applications/trader-typescript/src/commands/listen.ts). Look at the [API documentation for Network](https://hyperledger.github.io/fabric-gateway/main/api/node/interfaces/Network.html) for ideas on how to proceed. Be sure to only checkpoint events *after* they are successfully processed! + +1. Ensure your changes are compiled, then run the **listen** command with the SIMULATED_FAILURE_COUNT environment variable set to simulate an application error during the processing of a chancode event: + ```bash + SIMULATED_FAILURE_COUNT=3 npm start listen + ``` + +1. Run the **listen** command again. You should see event listening resume from the same chaincode event that the application failed to process on the previous run. + +> **Note:** The checkpointer persists its current listening position in a `checkpoint.json` file. If you want to remove the checkpointer's stored state and start listening from the `startBlock` again, remove the `checkpoint.json` file while the checkpointer is not in use. + +## Optional steps + +So far we have been replaying previously emitted chaincode events. Let's use the **listen** command to notify us in realtime when we take ownership of assets. + +8. Modify the **onEvent()** function in [listen.ts](../../applications/trader-typescript/src/commands/listen.ts) to notify you if you become the owner of a new (`CreateAsset` event) or transferred (`TransferAsset` event) asset. Note that the `payload` property of the event is a [Uint8Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) containing the [JSON](https://en.wikipedia.org/wiki/JSON) emitted by the smart contract. Look at the **readAsset()** method in [contract.ts](../../applications/trader-typescript/src/contract.ts) for ideas on how to convert this into a JavaScript object so you can inspect its `Owner` property. + +1. Try running the **listen** command in one terminal window while using another terminal window to create and transfer assets. diff --git a/full-stack-asset-transfer-guide/docs/ApplicationDev/README.md b/full-stack-asset-transfer-guide/docs/ApplicationDev/README.md new file mode 100644 index 00000000..12a719c7 --- /dev/null +++ b/full-stack-asset-transfer-guide/docs/ApplicationDev/README.md @@ -0,0 +1,10 @@ +# Client Application Development + +This session consists of the following topics to be covered in order: + +1. [Fabric Gateway](01-FabricGateway.md) - overview of the Fabric Gateway service used by client applications to interact with the Fabric network. +1. **Exercise:** [Run the client application](02-Exercise-RunApplication.md) - run the Trader sample application. +1. [Application overview](03-ApplicationOverview.md) - description of key parts of the application. +1. **Exercise:** [Implement asset transfer](04-Exercise-AssetTransfer.md) - enhance the client application to allow the transfer of assets. +1. [Chaincode events](05-ChaincodeEvents.md) - overview of chaincode events and their use. +1. **Exercise:** [Use chaincode events](06-Exercise-ChaincodeEvents.md) - listen for chaincode events and use them to implement notifications. diff --git a/full-stack-asset-transfer-guide/docs/CloudReady/00-setup.md b/full-stack-asset-transfer-guide/docs/CloudReady/00-setup.md new file mode 100644 index 00000000..d05b9ada --- /dev/null +++ b/full-stack-asset-transfer-guide/docs/CloudReady/00-setup.md @@ -0,0 +1,112 @@ +# Cloud Ready! + +==> [NEXT: Deploy a Kube](./10-kube.md) + +--- + +Cloud Native Fabric provides a neutral, tiered perspective of a Hyperledger Fabric Network as an integration of +containers, service endpoints, and API abstractions. In this module of the workshop, you will assemble a complete +application, building up from a local development running in isolation to a complete blockchain application running +natively on a public cloud. + +![Cloud Ready](../images/CloudReady/00-cloud-ready-2.png) + + +Each _application tier_ in the cloud native fabric stack is generally _modular_, and carries over with minimal change to +switch between different environments. At each _application tier_, client libraries and connection URLs are used to +ensure portability and independence across runtime stacks. + +For instance: + +- All interaction with Fabric peers, orderers, and CAs is performed with native binaries, communicating natively via + TCP and gRPCs. After the Fabric network has been established, all administrative and ledger access + (channel creation, user enrollment, chaincode installation, Gateway Client connections, etc.) occurs via service URL. + From the integration tier, a Fabric network is just "running somewhere" and expressed as a set of service URLs and + x509 certificates. + +- All interaction with the container orchestrator is performed directly via access to the Kubernetes API controller. In + several scenarios in the workshop, you will issue `kubectl` commands from a local system after configuring the client + to connect to a cluster "running somewhere." Note that this approach does not distinguish between a k8s cluster + running locally on the laptop, embedded on a Virtual Machine, or a fully cloud-native vendor such as AKS, EKS, or IKS. + +- In some cases a virtualization layer is necessary to emulate the cloud-native practices. For instance, a VM running + locally with multipass/vagrant (or a remote instance at EC2) can be used to supplement a local development workflow. + This can be extremely useful as a means to build platform-neutral runtimes, supporting a variety of chipsets and + development environments (WSL2, Mac M1, z/OS, amd64, etc.) + + +At some point when working with a _Cloud Native Fabric_ stack, you will feel _mildly disoriented_. When you are +_feeling lost_, focus on the tiered stack above to help re-orient yourself, find a compass bearing, and move forward. +In general, each application tier is designed to work only with the immediate layer beneath it in the stack. + +Here are some key points to keep in mind to help stay "grounded" on your voyage to the cloud: + +1. Services are backed by URLs. (It does not matter _where_ the endpoints are running, only how you _locate_ them.) + +2. Services run in [OCI Containers](https://github.com/opencontainers/image-spec) and are orchestrated by Kubernetes. + +3. All client programs run on your machine, connecting to service endpoints "running somewhere" on a hybrid cloud. + +4. At some point in this course, you may encounter a chaincode contract running in a Java Virtual Machine, running in a + docker container, executing as a service running on Kubernetes, which is running in a Docker container, which + is running on a virtual machine, which is running on an x86 emulation layer, which is running on hyperkit, which is + running on Mac M1 silicon on your laptop. At many times, you will forget _where_ your code is running, and question + if it is in fact running at all. Do not be alarmed: this is a natural reaction to container based application + workflows. (See points 1, 2, and 3 above.) + + +## Ready? + +```shell +# If the check passes, proceed to "Deploy a Kube": +./check.sh +``` + + +## Not Ready? + +To run the cloud workshop, a number of client applications are necessary to interact with the Fabric application tiers. + +Install the workshop prerequisites with instructions from running `./check.sh`, or see detailed guides from the web: + +- [fabric-samples](https://github.com/hyperledger/fabric-samples) (This GitHub project): +```shell +git clone https://github.com/hyperledger/fabric-samples.git fabric-samples +cd fabric-samples/full-stack-asset-transfer-guide +``` + +- [docker](https://www.docker.com/get-started/) + +- [kubectl](https://kubernetes.io/docs/tasks/tools/) + +- [jq](https://stedolan.github.io/jq/download/) + +- [k9s](https://k9scli.io/topics/install/) (recommended) + +- Hyperledger Fabric [client binaries](https://hyperledger-fabric.readthedocs.io/en/latest/install.html#download-fabric-samples-docker-images-and-binaries): +```shell + +curl -sSL https://raw.githubusercontent.com/hyperledger/fabric/main/scripts/install-fabric.sh | bash -s -- binary + +``` + +- Workshop environment variables: +```shell + +export WORKSHOP_PATH=$(pwd) +export FABRIC_CFG_PATH=${WORKSHOP_PATH}/config +export PATH=${WORKSHOP_PATH}/bin:$PATH + +``` + +## Still not Ready? + +For the duration of the workshop, a number of temporary, short-run virtual machines will be available on the web for +your usage. The systems have been provisioned with a [#cloud-config](../../infrastructure/ec2-cloud-config.yaml) and +include all dependencies necessary to run the cloud-native workshop. Check your "Conga Card" for the instance IP and +ssh connection details. + + +--- + +==> [NEXT: Deploy a Kube](./10-kube.md) diff --git a/full-stack-asset-transfer-guide/docs/CloudReady/10-kube.md b/full-stack-asset-transfer-guide/docs/CloudReady/10-kube.md new file mode 100644 index 00000000..effbab18 --- /dev/null +++ b/full-stack-asset-transfer-guide/docs/CloudReady/10-kube.md @@ -0,0 +1,68 @@ +# Deploy a Kubernetes Cluster + +[PREV: Setup](00-setup.md) <==> [NEXT: Deploy a Fabric Network](20-fabric.md) + +--- + +With cloud-native Fabric, all the components can run directly on your development workstation. In this exercise, you will configure: + +- A local [kind](https://kind.sigs.k8s.io) cluster, running Kubernetes in Docker. + +- A local [Ingress controller](https://github.com/kubernetes/ingress-nginx), routing traffic into the cluster at the `*.localho.st` virtual DNS domain. + +- A local [Container Registry](https://docs.docker.com/registry/insecure/), allowing you to upload chaincode Docker images to the cluster. + +![Local KIND](../images/CloudReady/10-kube.png) + + +## Ready? + +```shell + +just check-setup + +``` + +## Kubernetes IN Docker (KIND) + +- Set the cluster ingress domain and target k8s namespace. The `localho.st` domain is a public DNS wildcard resolver + mapping `*.localho.st` to 127.0.0.1. +```shell + +export WORKSHOP_INGRESS_DOMAIN=localho.st +export WORKSHOP_NAMESPACE=test-network + +``` + +- Create a [kind](https://kind.sigs.k8s.io) cluster, Nginx ingress, and local container registry: +```shell + +just kind + +``` + +- Open a new terminal window and observe the target namespace: +```shell + +# KIND will set the current kubectl context in ~/.kube/config +kubectl cluster-info + +k9s -n test-network + +``` + + +## Trouble? + +- Run KIND on a [multipass VM](11-kube-multipass.md) on your local system +- Run KIND on an [EC2 instance](12-kube-ec2-vm.md) at AWS + + +## Take it Further: + +- Run the workshop on an [IKS or EKS Cloud Kubernetes cluster](13-kube-public-cloud.md). +- Run the workshop on an AWS VM, using your AWS account and an EC2 [#cloud-config](../../infrastructure/ec2-cloud-config.yaml). + +--- +[PREV: Setup](00-setup.md) <==> [NEXT: Deploy a Fabric Network](20-fabric.md) + diff --git a/full-stack-asset-transfer-guide/docs/CloudReady/11-kube-multipass.md b/full-stack-asset-transfer-guide/docs/CloudReady/11-kube-multipass.md new file mode 100644 index 00000000..cc99e7e3 --- /dev/null +++ b/full-stack-asset-transfer-guide/docs/CloudReady/11-kube-multipass.md @@ -0,0 +1,110 @@ +# Deploy a Kubernetes Cluster + +[PREV: Setup](00-setup.md) <==> [NEXT: Deploy a Fabric Network](20-fabric.md) + +--- + +## Ready? + +```shell + +just check-setup + +``` + +**WINDOWS note**, please create the multipass VM with the command below from an elevated command prompt. Then proceed from logged into this VM with the [KIND instructions](./10-kube.md) + +## Provision a Multipass Virtual Machine + +```shell + +multipass launch \ + --name fabric-dev \ + --disk 80G \ + --cpus 8 \ + --mem 8G \ + --cloud-init infrastructure/multipass-cloud-config.yaml + +# todo: scp not volume mounts +multipass mount $PWD fabric-dev:/home/ubuntu/full-stack-asset-transfer-guide + +export WORKSHOP_IP=$(multipass info fabric-dev --format json | jq -r .info.\"fabric-dev\"."ipv4[0]") +export WORKSHOP_INGRESS_DOMAIN=$(echo $WORKSHOP_IP | tr -s '.' '-').nip.io + +echo "Multipass VM created with IP: $WORKSHOP_IP" +echo "WORKSHOP_DOMAIN=$WORKSHOP_INGRESS_DOMAIN" + +``` + + +## Start a KIND Cluster + +- Open a shell on the virtual machine: +```shell + +# todo ssh authorized_keys -> ubuntu@${WORKSHOP_IP} not multipass shell +multipass shell fabric-dev + +``` + +```shell + +cd ~/full-stack-asset-transfer-guide + +# Bind a docker container registry to the VM's external IP +export CONTAINER_REGISTRY_ADDRESS=0.0.0.0 +export CONTAINER_REGISTRY_PORT=5000 + +# Expose the Kube API controller on the VM's public interface +export KIND_API_SERVER_ADDRESS=$(hostname -I | cut -d ' ' -f 1) +export KIND_API_SERVER_PORT=8888 + +``` + +```shell + +# Create a Kubernetes cluster in Docker, configure an Nginx ingress, and docker container registry +just kind + +# KIND will set the current kube client context in ~/.kube/config +kubectl cluster-info + +# Copy the kube config to the host OS volume share: +# todo: scp not volume share +cp ~/.kube/config ~/full-stack-asset-transfer-guide/config/multipass-kube-config.yaml + +``` + +- Exit the multipass VM + +- From the host OS: +```shell + +# Connect the local kube client to the k8s API server running on the VM: +cp config/multipass-kube-config.yaml ~/.kube/config + +# Display kube client connection to k8s running on the VM: +kubectl cluster-info + +# Observe the target Kubernetes workspace: +k9s -n test-network + +``` + + +## Troubleshooting: + +- Run KIND on your [local system](10-kueb.md) +- Run KIND on an [EC2 instance](12-kube-ec2-vm.md) at AWS +- ssh to a workshop EC2 instance (see the login information on the back of your Conga Trading Card) + + +# Take it Further: + +- Run k8s directly on your laptop with [KIND](todo.md) (`export WORKSHOP_DOMAIN=localho.st`) +- Provision an EC2 instance on your AWS account with a [#cloud-config](../../infrastructure/ec2-cloud-config.yaml) +- Connect your kube client to a cloud k8s provider + + +--- +[PREV: Setup](00-setup.md) <==> [NEXT: Deploy a Fabric Network](20-fabric.md) diff --git a/full-stack-asset-transfer-guide/docs/CloudReady/12-kube-ec2-vm.md b/full-stack-asset-transfer-guide/docs/CloudReady/12-kube-ec2-vm.md new file mode 100644 index 00000000..3964b5b1 --- /dev/null +++ b/full-stack-asset-transfer-guide/docs/CloudReady/12-kube-ec2-vm.md @@ -0,0 +1,136 @@ +# Deploy a Kubernetes Cluster + +[PREV: Setup](00-setup.md) <==> [NEXT: Deploy a Fabric Network](20-fabric.md) + +--- + +When working with a cloud native Fabric stack, it is possible to connect the client application binaries +(`kubectl`, `peer`, `node`, etc.) to a Kubernetes cluster running on a remote virtual machine. In this +scenario, you will provision a VM instance at EC2, install a KIND cluster on the VM, and forward a local +port to the remote API controller with SSH. This configuration can be extremely useful in scenarios +where the local system does not have sufficient resources to "run everything," or when a local development +is focused on client-application development and Fabric needs to "just run somewhere." + +This configuration is also an effective way to minimize usage costs associated with a "full Kubernetes" +deployment on cloud vendors, enabling a natural k8s development workflow on disposable, temporary VMs. + +![EC2 Virtual Machine](../images/CloudReady/12-kube-ec2-vm.png) + + +## Provision a Virtual Machine Instance at EC2 + +**Note:** If you are participating in the workshop, an EC2 VM will be made available to you for the duration of the +event. Check your Conga Card for connection details and IP address of the remote VM. + +To create a new EC2 instance with software dependencies pre-installed via [#cloud-init](../../infrastructure/ec2-cloud-config.yaml): + +- Log in to AWS +- Use `t2.xlarge` profile (4 CPU / 8 GRAM / 80 GB gp2) +- open ports 80 (nginx), 443 (nginx), and 5000 (container registry) +- copy/paste `infrastructure/ec2-cloud-config.yaml` as the instance user-data +- Create an ssh key pair for remote login. Save locally as `~/Downloads/ec2-key.pem` +- After the instance is up, identify the PUBLIC IPV4 address. This will be used extensively for all access to the cluster: + +If you are working with a pre-existing VM, connect to the remote system and add your ~/.ssh/id-rsa.pub key to +the ubuntu user's `.ssh/authorized_keys` file. In the examples below, omit the `-i ${EC2_INSTANCE_KEY}` command +arguments. + + +## SSH Port Forward to the k8s API Controller + +- Start a new shell on the host OS and input the instance public IPv4 address and ssh public key. +```shell + +# Set to your EC2 instance Public IPv4 address and SSH connection key. E.g.: +export EC2_INSTANCE_IP=203.0.113.42 +export EC2_INSTANCE_KEY=~/Downloads/ec2-key.pem + +``` + +- Open an [ssh port forward](https://help.ubuntu.com/community/SSH/OpenSSH/PortForwarding) and interactive shell on the + remote system. While this terminal is open, all traffic directed to the host OS port 8888 will be tunneled to the k8s + API controller running on the remote VM. This presents the illusion to kubectl that the cluster is running locally, + when in fact the k8s client is communicating with the remote instance running at AWS. +```shell + +ssh -i $EC2_INSTANCE_KEY -L 8888:127.0.0.1:8888 ubuntu@${EC2_INSTANCE_IP} + +``` + + +## Kubernetes IN Docker (KIND) + +- Clone the project source code and create a KIND cluster on the remote VM: +```shell + +git clone https://github.com/hyperledgendary/full-stack-asset-transfer-guide.git +cd full-stack-asset-transfer-guide + +# Bind a docker container registry to the VM's external IP +export CONTAINER_REGISTRY_ADDRESS=0.0.0.0 +export CONTAINER_REGISTRY_PORT=5000 + +# Create a Kubernetes cluster in Docker, configure an Nginx ingress, and docker container registry +just kind + +``` + +- Leave this terminal window open in the background and observe the target k8s namespace. When this ssh session + is terminated, the ssh port forward will be closed and the host OS will no longer be able to connect to the remote + kubernetes cluster. +```shell + +k9s -n test-network + +``` + + +## Connect kubectl to the Remote Cluster: + +In the original shell opened for the workshop on your local system: + +- Copy the kube config from the remote system to the local user account: +```shell + +scp -i $EC2_INSTANCE_KEY ubuntu@${EC2_INSTANCE_IP}:~/.kube/config ~/.kube/config + +``` + +- Test the connectivity to the remote cluster. +```shell +$ kubectl cluster-info +Kubernetes control plane is running at https://127.0.0.1:8888 +CoreDNS is running at https://127.0.0.1:8888/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy + +To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'. +``` + + +## Network DNS Ingress Domain + +- Set the cluster ingress domain to an nip.io resolver. All virtual hosts in this domain will resolve to the nginx ingress. +```shell + +export WORKSHOP_INGRESS_DOMAIN=$(echo $EC2_INSTANCE_IP | cut -d ' ' -f 1 | tr -s '.' '-').nip.io +export WORKSHOP_NAMESPACE=test-network + +``` + +- Double-check that you are able to access a virtual host at the remote ingress domain. The expected response + is an HTTP 404 from nginx: +``` +$ curl foo.${WORKSHOP_INGRESS_DOMAIN} + +404 Not Found + +

404 Not Found

+
nginx
+ + +``` + + + +--- +[PREV: Setup](00-setup.md) <==> [NEXT: Deploy a Fabric Network](20-fabric.md) + diff --git a/full-stack-asset-transfer-guide/docs/CloudReady/13-kube-public-cloud.md b/full-stack-asset-transfer-guide/docs/CloudReady/13-kube-public-cloud.md new file mode 100644 index 00000000..47dfd091 --- /dev/null +++ b/full-stack-asset-transfer-guide/docs/CloudReady/13-kube-public-cloud.md @@ -0,0 +1,118 @@ +# Deploy a Kubernetes Cluster + +[PREV: Setup](00-setup.md) <==> [NEXT: Deploy a Fabric Network](20-fabric.md) + +--- + +## Ready? + +```shell + +just check-setup + +``` + +## Provision a cloud Kubernetes Instance + +- Provision an IKS or EKS Kubernetes service at IBM or Amazon Cloud. + + - Configure a 3x 4 CPU / 16 GRAM worker pool. + - single region is OK. + + +- Configure your `kubectl` CLI for access to the remote cluster. + +- Test CLI access to the cluster: +```shell + +kubectl cluster-info + +``` + +- Open a new shell and observe the target namespace: +```shell + +k9s -n test-network + +``` + + +## Configuration Options + +### IBM Cloud / IKS +```shell + +export WORKSHOP_NAMESPACE="test-network" +export WORKSHOP_CLUSTER_RUNTIME="k3s" +export WORKSHOP_COREDNS_DOMAIN_OVERRIDE="false" +export WORKSHOP_STAGE_DOCKER_IMAGES="false" +export WORKSHOP_STORAGE_CLASS="ibmc-file-gold" + +``` + + +### Amazon Web Services / EKS +```shell + +export WORKSHOP_NAMESPACE="test-network" +export WORKSHOP_CLUSTER_RUNTIME="k3s" +export WORKSHOP_COREDNS_DOMAIN_OVERRIDE="false" +export WORKSHOP_STAGE_DOCKER_IMAGES="false" +export WORKSHOP_STORAGE_CLASS="gp2" + +``` + +## Install Nginx Ingress + +- Install the Nginx controller to the cluster +```shell + +just nginx + +``` + + +## Cluster Ingress DNS Domain + +### IKS +```shell + +export INGRESS_IPADDR=$(kubectl -n ingress-nginx get svc/ingress-nginx-controller -o json | jq -r '.status.loadBalancer.ingress[0].ip') +export WORKSHOP_INGRESS_DOMAIN=$(echo $INGRESS_IPADDR | tr -s '.' '-').nip.io + +``` + +### EKS +```shell + +export INGRESS_HOSTNAME=$(kubectl -n ingress-nginx get svc/ingress-nginx-controller -o json | jq -r '.status.loadBalancer.ingress[0].hostname') +export INGRESS_IPADDR=$(dig $INGRESS_HOSTNAME +short) +export WORKSHOP_INGRESS_DOMAIN=$(echo $INGRESS_IPADDR | tr -s '.' '-').nip.io + +``` + + + + +# Take it Further + +During the workshop, one of the steps involves building a chaincode image, tagging the +image, and publishing to an insecure docker registry running at localhost:5000. For cloud +based clusters, the remote instance will not have access to the local insecure registry. + +To upload custom chaincode, configure your local docker client with access to an IBM +cloud / public container registry. In addition, make sure that the Fabric target namespace +has read access to the repository, allowing the pods created in the cluster with access to +your code. + +To run the workshop without building and uploading custom code, you can install a chaincode +package using the reference asset-transfer smart contract. This reference sample has been +made available for public read access, and does not require `imagePullSecrets` for the +chaincode pods to be started in the cluster. + +To install the reference smart contract, in the "Install Chaincode" section of the workshop, +skip the "build image" sections and [install the contract from a CI pipeline](https://github.com/jkneubuh/full-stack-asset-transfer-guide/blob/feature/iks-notes/docs/CloudReady/30-chaincode.md#install-chaincode-from-a-ci-pipeline). + + +--- +[PREV: Setup](00-setup.md) <==> [NEXT: Deploy a Fabric Network](20-fabric.md) diff --git a/full-stack-asset-transfer-guide/docs/CloudReady/20-fabric.md b/full-stack-asset-transfer-guide/docs/CloudReady/20-fabric.md new file mode 100644 index 00000000..629662a6 --- /dev/null +++ b/full-stack-asset-transfer-guide/docs/CloudReady/20-fabric.md @@ -0,0 +1,118 @@ +# Deploy a Fabric Network + +[PREV: Deploy a Kube](10-kube.md) <==> [NEXT: Install Chaincode](30-chaincode.md) + +--- + +[Fabric-operator](https://github.com/hyperledger-labs/fabric-operator) extends the core Kubernetes API with a set of +[custom resources](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) suitable for +describing the nodes of a Hyperledger Fabric Network. With the operator, a set of [CA](../../infrastructure/sample-network/config/cas), +[peer](../../infrastructure/sample-network/config/peers), and [orderer](../../infrastructure/sample-network/config/orderers) +resources are applied to the Kube API controller. In turn, the operator reflects the network as a series of `Pod`, +`Deployment`, `Service`, and `Ingress` resources in the target namespace. + +After the nodes in the Fabric network have been started, the fabric `peer` and CLI binaries are used to connect to the +network via Ingress, preparing a channel for smart contracts and application development. + +![Fabric Operator](../images/CloudReady/20-fabric.png) + + +## Ready? + +```shell + +just check-kube + +``` + +## Sample Network + +- Install the fabric-operator [Kubernetes Custom Resources](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) +```shell + +kubectl apply -k https://github.com/hyperledger-labs/fabric-operator.git/config/crd + +``` + +- Apply a series of [CA](../../infrastructure/sample-network/config/cas), [peer](../../infrastructure/sample-network/config/peers), + and [orderer](../../infrastructure/sample-network/config/orderers) resources directly to the Kube API controller. In + turn, fabric-operator will reconcile a network of Kubernetes `Pods`, `Deployments`, `Services`, and `Ingress` to + reflect the target network structure. +```shell + +just cloud-network + +``` + +- Create a Fabric channel: +```shell + +just cloud-channel + +``` + +- Set the location for the network's TLS certificates, channel MSP, and user enrollments: +```shell + +export WORKSHOP_CRYPTO=$WORKSHOP_PATH/infrastructure/sample-network/temp + +``` + + +## Post Checks + +```shell + +curl \ + -s \ + --cacert $WORKSHOP_CRYPTO/cas/org1-ca/tls-cert.pem \ + https://$WORKSHOP_NAMESPACE-org1-ca-ca.$WORKSHOP_INGRESS_DOMAIN/cainfo \ + | jq + +``` + +## Kube Navigation and Peer Logs + +To watch the peer logs throughout the workshop, we'll need to identify the pod name for one of the peers, let's find the pod name for org1-peer1 by using `kubectl`. +Set the default namespace to `test-network` so that we don't have to pass the namespace (`-n`) to each kubectl command: + +```shell +kubectl config set-context --current --namespace=test-network +kubectl get pods +``` + +You'll see the org1-peer1 pod with a name like `org1-peer1-79df64f8d8-7m9mt`, your pod name will be different! + +We can then tail the org1-peer1 log in a terminal window so that we can see proof that chaincodes get deployed, blocks get created, etc: + +```shell +kubectl logs -f org1-peer1-79df64f8d8-7m9mt peer +``` + +Now that you know how to use kubectl, let's learn the shortcut! You can easily monitor all of the the pods in the [k9s utility](https://k9scli.io/topics/install/). If you haven't started k9s yet, start it in a new terminal: + +```shell +k9s -n test-network +``` + +You'll see the fabric-operator, peer, orderer, and CA pods. Navigate around by hitting `ENTER` on one of the pods, `ENTER` again on one of the containers, and then hit `0` to tail the container's log. Go back up by hitting `ESCAPE`. More tips are available at the top of the k9s user interface. + +## Troubleshooting + +```shell + +# While running "just cloud-network and/or just cloud-channel": +tail -f infrastructure/sample-network/network-debug.log + +``` + + +# Take it Further: + +- Deploy the [Fabric Operations Console](21-fabric-operations-console.md) +- Build a network with the [Ansible Blockchain Collection](22-fabric-ansible-collection.md) + + +--- + +[PREV: Deploy a Kube](10-kube.md) <==> [NEXT: Install Chaincode](30-chaincode.md) diff --git a/full-stack-asset-transfer-guide/docs/CloudReady/21-fabric-operations-console.md b/full-stack-asset-transfer-guide/docs/CloudReady/21-fabric-operations-console.md new file mode 100644 index 00000000..36beeb47 --- /dev/null +++ b/full-stack-asset-transfer-guide/docs/CloudReady/21-fabric-operations-console.md @@ -0,0 +1,46 @@ +# Deploy a Fabric Network + +[PREV: Deploy a Kube](10-kube.md) <==> [NEXT: Install Chaincode](30-chaincode.md) + +--- + +[Fabric Operations Console](https://github.com/hyperledger-labs/fabric-operations-console) provides an +interactive GUI layer and set of service SDKs suitable for the programmatic administration of a +Fabric network. + +![Fabric Operations Console](https://github.com/hyperledger-labs/fabric-operations-console/blob/main/docs/images/architecture_hl.png) + + +## Ready? + +```shell + +just check-kube + +``` + +## Operations Console + +- Install the fabric-operator [Kubernetes Custom Resources](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) +```shell + +kubectl apply -k https://github.com/hyperledger-labs/fabric-operator.git/config/crd + +``` + +- Install the Fabric operator and console in the target namespace: +```shell + +just console + +``` + +- Connect to the console GUI at the hyperlink printed to the terminal. + +- Use the Console GUI or [Ansible Collection](22-fabric-ansible-collection.md) to + [Build a Network](https://cloud.ibm.com/docs/blockchain?topic=blockchain-ibp-console-build-network) + + +--- + +[PREV: Deploy a Kube](10-kube.md) <==> [NEXT: Install Chaincode](30-chaincode.md) diff --git a/full-stack-asset-transfer-guide/docs/CloudReady/22-fabric-ansible-collection.md b/full-stack-asset-transfer-guide/docs/CloudReady/22-fabric-ansible-collection.md new file mode 100644 index 00000000..4dc4e6f2 --- /dev/null +++ b/full-stack-asset-transfer-guide/docs/CloudReady/22-fabric-ansible-collection.md @@ -0,0 +1,144 @@ +# Deploy a Fabric Network + +[PREV: Deploy a Kube](10-kube.md) <==> [NEXT: Install Chaincode](30-chaincode.md) + +--- + +In addition to a graphical interface, [Fabric Operations Console](https://github.com/hyperledger-labs/fabric-operations-console) provides a set of RESTful service SDKs which can be utilized to realize a network in a declarative fashion using the Fabric [Blockchain Ansible Collection](https://github.com/IBM-Blockchain/ansible-collection). + +With ansible, a Fabric network of CAs, Peers, Orderers, Channels, Chaincode, and Identities are +realized by applying a series of playbooks to realize the target configuration. + +## Ready? + +```shell + +just check-kube + +``` + +## Build a Fabric Network + +The first step is to create the configuration that Ansible will use, then run the Ansible Playbooks + +### Define the namespace and storage class that will be used for console + +```shell +export WORKSHOP_NAMESPACE="fabricinfra" +# for *IBM Cloud K8S and Openshift* use this storage class +export WORKSHOP_STORAGE_CLASS="ibmc-file-gold" +``` + + +### Configure Ingress controller to the cluster +*IBMCloud IKS Clusters and Kind* + +```shell +just nginx +``` + +Check the Ingress controllers domain + +For IKS: +```shell +export INGRESS_IPADDR=$(kubectl -n ingress-nginx get svc/ingress-nginx-controller -o json | jq -r '.status.loadBalancer.ingress[0].ip') +export WORKSHOP_INGRESS_DOMAIN=$(echo $INGRESS_IPADDR | tr -s '.' '-').nip.io +``` + +For Kind: +```shell +export WORKSHOP_INGRESS_DOMAIN=localho.st +``` + +*IBM Cloud Openshift* + +The ingress subdomain can be obtained from the Cluster's dashboard, for example + +```shell +export WORKSHOP_INGRESS_DOMAIN=theclusterid.eu-gb.containers.appdomain.cloud +``` + +### Generate Ansible Playbook configuration + +```shell +# check the output to ensure the correct domain, storage class and namespace +just ansible-review-config +``` + +Please check the local `_cfg/operator-console-vars.yaml` file. Ensure that the ingress domain, storage class and namespace are correct. By default the all the `WORKSHOP_xxx` varirables are used to see the Ansible configuration, but it's worth double checking the files + +For example: +```shell +# this MUST be set to either k8s or openshift +target: openshift +# Console name/domain +console_domain: 203-0-113-42.nip.io +console_storage_class: ibmc-file-gold +``` + +**For Openshift, please ensure that the `type: openshift` is set** + +``` +target: openshift +``` + +- Set Kubectl context + +A Kubectl context is also requried - the default behaviour is use the current context. + + +Alternatively your K8S provider may give you a different command to get the K8S cxontext. +For IKS use this command instead +```shell +ibmcloud ks cluster config --cluster --output yaml > _cfg/k8s_context.yaml +``` + +The `k8s_context.yaml` will be detected by the shell scripts and that will be used + + +- Run the [00-complete](../../infrastructure/fabric_network_playbooks/00-complete.yml) play: +```shell + +# if you are using IKS/KIND +# do not do this for OpenShift +just ansible-ingress + + +# Start the operator and Fabric Operations Console +just ansible-operator +just ansible-console + +# Construct a network and channel with ansible playbooks +just ansible-network + +``` +The console will be available at the Nginx ingress domain alias: +`https://fabricinfra-hlf-console-console.` + + +In a browser, connect to this URL (accepting the self-signed certificate), log in as `admin` password `password` and view the network structure in the Operations Console user interface. (you will be prompted to change the password!) + +## Generate configuration files + +To connect applications details of the Gateway Endpoints with TLS certificates and the identies to use are required. +The Ansible scripts will have written several files to the `_cfg` directory, run `ls -1` to see the files and refer to the table below for what file is + +| Filename | Contents | +|------------------------------|-----------------------------------------------------------------------| +| 'Ordering Org Admin.json' | Ordering Organizations Admin identity | +| 'Ordering Org CA Admin.json' | Ordering Organization's Certificate Authority's Admin Identity | +| 'Org1 Admin.json' | Organization 1's Admin identity | +| 'Org1 CA Admin.json' | Organization 1's Certificate Authority's Admin Identity | +| 'Org2 Admin.json' | Organization 2's Admin identity | +| 'Org2 CA Admin.json' | Organization 2's Certificate Authority's Admin Identity | +| auth-vars.yml | Configuration for Ansible to connect to the Fabric Operations Console | +| fabric-common-vars.yml | Ansible Configuartion - for common and shared values | +| fabric-ordering-org-vars.yml | - for the ordering organization | +| fabric-org1-vars.yml | - for organization 1 | +| fabric-org2-vars.yml | - for ogranization 2 | +| operator-console-vars.yml | - for creating the operator an dconsole | + + +--- + +[PREV: Deploy a Kube](10-kube.md) <==> [NEXT: Install Chaincode](31-fabric-ansible-chaincode.md) diff --git a/full-stack-asset-transfer-guide/docs/CloudReady/30-chaincode.md b/full-stack-asset-transfer-guide/docs/CloudReady/30-chaincode.md new file mode 100644 index 00000000..f58ae936 --- /dev/null +++ b/full-stack-asset-transfer-guide/docs/CloudReady/30-chaincode.md @@ -0,0 +1,225 @@ +# Chaincode + +[PREV: Deploy a Fabric Network](20-fabric.md) <==> [NEXT: Go Bananas](40-bananas.md) + +--- + +Using the traditional chaincode lifecycle, smart contract deployment requires a Fabric administrator to prepare +and install a chaincode package declaring the contract source code, metadata, and target language/runtime. When the +contract is committed to a channel, peers are responsible for compiling a custom chaincode binary and launching the +contract as a child process. This workflow creates several issues for container-based runtimes, where the `build` +phase is additionally responsible for compiling a Docker image, and the `run` phase is responsible for managing the +lifecycle of a chaincode process in a container orchestrator. + +In container based environments such as Kubernetes, the classical chaincode deployment was generally supported with a +custom chaincode server / launcher. + +With the introduction of [Chaincode as a Service](https://hyperledger-fabric.readthedocs.io/en/latest/cc_service.html) +(>= 2.4.1), Fabric administrators were provided an alternative deployment option whereby an external chaincode +builder could be configured as a _"no-op builder"_. Using CCaaS, an administrator could prepare a custom chaincode +container, upload the image to a container registry, launch the chaincode "as a service," and complete the peer +lifecycle using traditional `peer` CLI commands. + +While the CCaaS / _no-op_ builder alleviated some mechanical issues and offered full customization of the chaincode +lifecycle, it placed two new major responsibilities on the Fabric network administrator: + +- `build` : The admin must prepare a chaincode image and upload to a container registry. +- `run` : The admin must prepare a `Service`, `Deployment`, and configure TLS for the chaincode endpoint. + +_"But - I just want to write some chaincode!"_ + + +In this exercise, we will see how the new [Kubernetes Chaincode Builder](https://github.com/hyperledger-labs/fabric-builder-k8s) +enables an external container build pipeline and eliminates the administrative burdens associated with CCaaS. + +Using the fabric-k8s-builder, you will deploy (and iterate) a smart contract definition by: + +1. Preparing a chaincode container image and uploading to a distribution registry. +2. Preparing a `type=k8s` chaincode package specifying the unique and immutable [container image digest](https://github.com/opencontainers/image-spec/blob/main/descriptor.md#digests). +3. Using the `peer` CLI binaries to install and commit the smart contract to a channel. + + +![Fabric k8s Builder](../images/CloudReady/30-chaincode.png) + + +## Ready? + +```shell + +just check-network + +``` + + +## Set the peer client environment + +```shell + +export ORG1_PEER1_ADDRESS=${WORKSHOP_NAMESPACE}-org1-peer1-peer.${WORKSHOP_INGRESS_DOMAIN}:443 +export ORG1_PEER2_ADDRESS=${WORKSHOP_NAMESPACE}-org1-peer2-peer.${WORKSHOP_INGRESS_DOMAIN}:443 + +# org1-peer1: +export CORE_PEER_LOCALMSPID=Org1MSP +export CORE_PEER_ADDRESS=${ORG1_PEER1_ADDRESS} +export CORE_PEER_TLS_ENABLED=true +export CORE_PEER_MSPCONFIGPATH=${WORKSHOP_CRYPTO}/enrollments/org1/users/org1admin/msp +export CORE_PEER_TLS_ROOTCERT_FILE=${WORKSHOP_CRYPTO}/channel-msp/peerOrganizations/org1/msp/tlscacerts/tlsca-signcert.pem +export CORE_PEER_CLIENT_CONNTIMEOUT=15s +export CORE_PEER_DELIVERYCLIENT_CONNTIMEOUT=15s +export ORDERER_ENDPOINT=${WORKSHOP_NAMESPACE}-org0-orderersnode1-orderer.${WORKSHOP_INGRESS_DOMAIN}:443 +export ORDERER_TLS_CERT=${WORKSHOP_CRYPTO}/channel-msp/ordererOrganizations/org0/orderers/org0-orderersnode1/tls/signcerts/tls-cert.pem + +``` + +## Docker Engine Configuration + +**NOTE: SKIP THIS STEP IF USING `localho.st` AS THE INGRESS DOMAIN** + +Configure the docker engine with the insecure container registry `${WORKSHOP_INGRESS_DOMAIN}:5000` + +For example: (Docker -> Preferences -> Docker Engine) +```json +{ + "insecure-registries": [ + "192-168-205-6.nip.io:5000" + ] +} +``` + +- apply and restart + +## Chaincode Revision + +```shell + +CHANNEL_NAME=mychannel +VERSION=v0.0.1 +SEQUENCE=1 + +``` + +## Build the Chaincode Docker Image + +```shell + +CHAINCODE_NAME=asset-transfer +CHAINCODE_PACKAGE=${CHAINCODE_NAME}.tgz +CONTAINER_REGISTRY=$WORKSHOP_INGRESS_DOMAIN:5000 +CHAINCODE_IMAGE=$CONTAINER_REGISTRY/$CHAINCODE_NAME + +# Build the chaincode image +docker build -t $CHAINCODE_IMAGE contracts/$CHAINCODE_NAME-typescript + +# Push the image to the insecure container registry +docker push $CHAINCODE_IMAGE + +``` + + +## Prepare a k8s Chaincode Package + +```shell + +IMAGE_DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' $CHAINCODE_IMAGE | cut -d'@' -f2) + +infrastructure/pkgcc.sh -l $CHAINCODE_NAME -n localhost:5000/$CHAINCODE_NAME -d $IMAGE_DIGEST + +``` + +## Install the Chaincode + +```shell + +# Install the chaincode package on both peers in the org +CORE_PEER_ADDRESS=${ORG1_PEER1_ADDRESS} peer lifecycle chaincode install $CHAINCODE_PACKAGE +CORE_PEER_ADDRESS=${ORG1_PEER2_ADDRESS} peer lifecycle chaincode install $CHAINCODE_PACKAGE + +export PACKAGE_ID=$(peer lifecycle chaincode calculatepackageid $CHAINCODE_PACKAGE) && echo $PACKAGE_ID + +# Approve the contract for org1 +peer lifecycle \ + chaincode approveformyorg \ + --channelID ${CHANNEL_NAME} \ + --name ${CHAINCODE_NAME} \ + --version ${VERSION} \ + --package-id ${PACKAGE_ID} \ + --sequence ${SEQUENCE} \ + --orderer ${ORDERER_ENDPOINT} \ + --tls --cafile ${ORDERER_TLS_CERT} \ + --connTimeout 15s + +# Commit the contract on the channel +peer lifecycle \ + chaincode commit \ + --channelID ${CHANNEL_NAME} \ + --name ${CHAINCODE_NAME} \ + --version ${VERSION} \ + --sequence ${SEQUENCE} \ + --orderer ${ORDERER_ENDPOINT} \ + --tls --cafile ${ORDERER_TLS_CERT} \ + --connTimeout 15s + +``` + +```shell + +peer chaincode query -n $CHAINCODE_NAME -C mychannel -c '{"Args":["org.hyperledger.fabric:GetMetadata"]}' | jq + +``` + + +# Take it Further + +## Edit, compile, upload, and re-install your chaincode: + +```shell + +SEQUENCE=$((SEQUENCE + 1)) +VERSION=v0.0.$SEQUENCE + +``` + +- Make a change to the contracts/asset-transfer-typescript source code +- build a new chaincode docker image and publish to the local container registry +- prepare a new chaincode package as above. +- install, approve, and commit as above. + + +## Install chaincode from a CI Pipeline + +```shell + +SEQUENCE=$((SEQUENCE + 1)) +VERSION=v0.1.3 +CHAINCODE_PACKAGE=asset-transfer-typescript-${VERSION}.tgz + +``` + +- Download a chaincode release artifact from GitHub: +```shell + +curl -LO https://github.com/hyperledgendary/full-stack-asset-transfer-guide/releases/download/${VERSION}/${CHAINCODE_PACKAGE} + +``` + +- install, approve, and commit as above. + + +## Debug with Chaincode as a Service + +- prepare a chaincode package with connection.json -> HOST IP:9999 (todo: link to dig out) +- compute CHAINCODE_ID=shasum CC package.tgz +- docker run -e CHAINCODE_ID -e CHAINCODE_SERVER_ADDRESS ... $CHAINCODE_IMAGE in a different shell +- install, approve, commit as above. + + +## Deploy Chaincode With Ansible + +- cp tgz from github releases -> _cfg/ +- edit _cfg/cc yaml with package name +- `just ... chaincode` + + +--- + +[PREV: Deploy a Fabric Network](20-fabric.md) <==> [NEXT: Go Bananas](40-bananas.md) diff --git a/full-stack-asset-transfer-guide/docs/CloudReady/31-fabric-ansible-chaincode.md b/full-stack-asset-transfer-guide/docs/CloudReady/31-fabric-ansible-chaincode.md new file mode 100644 index 00000000..02af54b9 --- /dev/null +++ b/full-stack-asset-transfer-guide/docs/CloudReady/31-fabric-ansible-chaincode.md @@ -0,0 +1,182 @@ +# Dploying Chaincode with Ansible + +[PREV: Deploy a Fabric Network](22-fabric-ansible-collection.md) <==> [NEXT: Go Bananas](40-bananas.md) + +--- + + +## Ready? + +```shell + +just check-network + +``` +## Build the Chaincode Docker Image + +We need to build the chaincode image, and push it to the local image registry. Here this uses docker to build the image + +```shell +# Build the chaincode image +docker build -t localho.st:5000/asset-transfer contracts/asset-transfer-typescript + +# Push the image to the insecure container registry +docker push localho.st:5000/asset-transfer + +``` + +## Prepare a k8s Chaincode Package + +A chaincode package is requried to inform the peer of which docker image should be used. (strictly it is the K8s-builder that knows which image to use; the peer uses the k8s-builder to do the work creating the container. therefore the peer that Ansible has created is specifically configured to work in k8s) + +```shell +IMAGE_DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' localho.st:5000/asset-transfer | cut -d'@' -f2) +infrastructure/pkgcc.sh -l asset-transfer -n localhost:5000/asset-transfer -d $IMAGE_DIGEST +``` + +Check the tgz package created + +```shell +ls -l asset-transfer.tgz +-rw-r--r-- 1 matthew matthew 483 Sep 5 11:05 asset-transfer.tgz +``` +You can see the file is quite small. If you like you can unpack this with tar + +```shell +tar -zxf asset-transfer.tgz && tar -zxf code.tar.gz +``` + +Copy the `asset-transfer.tgz` to the `_cfg` directory. + +```shell +cp asset-transfer.tgz ${WORKSPACE_PATH}/_cfg +``` + +## Deploy the Chaincode + +Firstly we need to create a Ansible variables files that will give the Ansible modules the information on what we want to deploy. + +An example file has been provided, check the contents to see what is needed. +```shell + +cat contracts/asset-transfer-typescript/asset-transfer-chaincode-vars.yml + +smart_contract_name: "asset-transfer" +smart_contract_version: "1.0.0" +smart_contract_sequence: 1 +smart_contract_package: "asset-transfer.tgz" +# smart_contract_constructor: "initLedger" +smart_contract_endorsement_policy: "" +smart_contract_collections_file: "" +``` + +There are three (small) playbooks that need to run to deploy the chaincode. + +- Install and Approve the chaincode on the peers. One each for [org1](../../infrastructure/production_chaincode_playbooks/19-install-and-approve-chaincode.yml) and [org2](../../infrastructure/production_chaincode_playbooks/20-install-and-approve-chaincode.yml) +- Commit the chaincode definition to the channel needs [one playbook](../../infrastructure/production_chaincode_playbooks/21-commit-chaincode.yml) + +Run all of these playbooks now; the example configuration file above will be used + +```shell +just ansible-deploy-chaincode +``` + +This will have created the chaincode containers. + +## Create a user to access the chaincode + +We can run a simple client application here to check the chaincode is deployed and accessible to each. There are two Ansible tasks that are helpful here. + +- `registered_identity` will register an identity to use for the application +- `enrolled_identity` will enroll an already registered identity, returning the certifcate and private keys needed +- `connection_profile` that will put together all the information needed for connecting an application + +```shell +just ansible-ready-application +``` + +This will create two files in `_cfg`; first look at the identity (we've shortened the certificates here) + +```shell + cat _cfg/asset-transfer_appid.json +{ + "name": "asset-transfer.admin", + "cert": "LS0..............", + "ca": "LS0tLS................", + "hsm": false, + "private_key": "LS0tLS1C..........." +} + +``` + +Now look at the `Org1_gateway.json` file; use jq to just look at the address needed for the gateway to connect to + +```shell +cat _cfg/Org1_gateway.json | jq .peers +{ + "Org1 Peer": { + "url": "grpcs://fabricinfra-org1peer-peer.localho.st:443", + "tlsCACerts": { + "pem": "-----BEGIN CERTIFICATE-----...\n-----END CERTIFICATE-----\n" + } + } +} +``` + +## Setup the application + +We can use this information directly in an application; we will use the Gateway SDK to create a simple client that will invoke the ib-built Smart Contract function to return it's metadata. This is available in all smart contracts, so it is a useful and simple way to check everything is deployed. The application is called 'ping-chaincode' for this reason + +- Change to the `applications/ping-chaincode` directory +- Check the `app.env` file, it should look similar to this + +``` +CHANNEL_NAME=mychannel +CHAINCODE_NAME=asset-transfer +CONN_PROFILE_FILE=/home/matthew/github.com/hyperledgendary/full-stack-asset-transfer-guide/_cfg/Org1_gateway.json +ID_FILE=asset-transfer_appid.json +ID_DIR=/home/matthew/github.com/hyperledgendary/full-stack-asset-transfer-guide/_cfg/ +TLS_ENABLED=true +``` + +- Build the application (it's typescript) + +```shell +npm install +npm run build +``` + +- Run the application, and look at the output. + +```shell +npm start + + +> asset-transfer-basic@1.0.0 start +> node dist/app.js + +Created GRPC Connection +Loaded Identity + +--> Evaluate Transaction: Get Contract Metdata from : org.hyperledger.fabric:GetMetadata +*** Result: +$schema: >- + https://hyperledger.github.io/fabric-chaincode-node/main/api/contract-schema.json +contracts: + AssetTransferContract: + name: AssetTransferContract + contractInstance: + name: AssetTransferContract + default: true + transactions: + - tag: + - SUBMIT + - submitTx + parameters: + - name: assetJson + description: '' + schema: + type: string + name: CreateAsset +.... +``` \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/docs/CloudReady/40-bananas.md b/full-stack-asset-transfer-guide/docs/CloudReady/40-bananas.md new file mode 100644 index 00000000..37efc70d --- /dev/null +++ b/full-stack-asset-transfer-guide/docs/CloudReady/40-bananas.md @@ -0,0 +1,159 @@ +# Gateway Client Application + +[PREV: Install Chaincode](30-chaincode.md) <==> [NEXT: Teardown](90-teardown.md) + +--- + +In this final workshop exercise, we will port the Gateway Client routines you developed in the +[ApplicationDev](../ApplicationDev) module to run on a cloud-native Fabric network. + +In order to query the ledger and submit transactions to the `asset-transfer` smart contract, the client application +must run with an identity certificate and key pair issued by the organization's CA. Once the user identity has +been enrolled with the CA, we'll use the new identity to create, transfer, and exchange virtual "token" assets +on a shared ledger. + +![Gateway Client Application](../../docs/images/CloudReady/40-gateway-client-app.png) + + +## Ready? + +```shell + +just check-chaincode + +``` + + +## Register and enroll a new user at the org CA + +```shell + +# User organization MSP ID +export MSP_ID=Org1MSP +export ORG=org1 +export USERNAME=org1user +export PASSWORD=org1userpw + +``` + +```shell + +ADMIN_MSP_DIR=$WORKSHOP_CRYPTO/enrollments/${ORG}/users/rcaadmin/msp +USER_MSP_DIR=$WORKSHOP_CRYPTO/enrollments/${ORG}/users/${USERNAME}/msp +PEER_MSP_DIR=$WORKSHOP_CRYPTO/channel-msp/peerOrganizations/${ORG}/msp + +fabric-ca-client register \ + --id.name $USERNAME \ + --id.secret $PASSWORD \ + --id.type client \ + --url https://$WORKSHOP_NAMESPACE-$ORG-ca-ca.$WORKSHOP_INGRESS_DOMAIN \ + --tls.certfiles $WORKSHOP_CRYPTO/cas/$ORG-ca/tls-cert.pem \ + --mspdir $WORKSHOP_CRYPTO/enrollments/$ORG/users/rcaadmin/msp + +fabric-ca-client enroll \ + --url https://$USERNAME:$PASSWORD@$WORKSHOP_NAMESPACE-$ORG-ca-ca.$WORKSHOP_INGRESS_DOMAIN \ + --tls.certfiles $WORKSHOP_CRYPTO/cas/$ORG-ca/tls-cert.pem \ + --mspdir $WORKSHOP_CRYPTO/enrollments/$ORG/users/$USERNAME/msp + +mv $USER_MSP_DIR/keystore/*_sk $USER_MSP_DIR/keystore/key.pem + +``` + +## Go Bananas + +- Set the gateway client to connect to the org1-peer1 as the newly enrolled `${USERNAME}`: +```shell + +# Path to private key file +export PRIVATE_KEY=${USER_MSP_DIR}/keystore/key.pem + +# Path to user certificate file +export CERTIFICATE=${USER_MSP_DIR}/signcerts/cert.pem + +# Path to CA certificate +export TLS_CERT=${PEER_MSP_DIR}/tlscacerts/tlsca-signcert.pem + +# Gateway peer SSL host name override +export HOST_ALIAS=${WORKSHOP_NAMESPACE}-${ORG}-peer1-peer.${WORKSHOP_INGRESS_DOMAIN} + +# Gateway endpoint +export ENDPOINT=$HOST_ALIAS:443 + +``` + +```shell + +pushd applications/trader-typescript + +npm install + +``` + +```shell + +# Create a yellow banana token owned by appleman@org1 +npm start create banana bananaman yellow + +npm start getAllAssets + +# Transfer the banana among users / orgs +npm start transfer banana appleman Org1MSP + +npm start getAllAssets + +# Transfer the banana among users / orgs +npm start transfer banana bananaman Org2MSP + +# Error! Which org owns the banana? +npm start transfer banana bananaman Org1MSP + +popd + +``` + +# Take it Further + +## Gateway Load Balancing + +In the example above, the gateway client connects directly to a peer using the specific peer node's +gRPCs URL. This can be extended with a level of fail-over and load balancing, by instructing the gateway +client to connect at a virtual host Ingress and Kubernetes Service. When connecting in this fashion, +Gateway client connections are load balanced across the org's peers in the network, with the gateway +peer further dispatching transaction requests to peers while maintaining a balanced ledger height. + +![Fabric Gateway deployment](../images/ApplicationDev/fabric-gateway-deployment.png) + +To set up a load-balanced Gateway [Service and Ingress](../../infrastructure/sample-network/config/gateway/org1-peer-gateway.yaml) URL in Kubernetes: + + +- Create a virtual host name / Ingress endpoint for the org peers: +```shell +pushd applications/trader-typescript + +kubectl kustomize \ + ../../infrastructure/sample-network/config/gateway \ + | envsubst \ + | kubectl -n ${WORKSHOP_NAMESPACE} apply -f - + +``` + +- Run the gateway client application, using the load-balanced Gateway service. When the gateway client +connects to the network, the gRPCs connections will be distributed across peers in the org: +```shell + +unset HOST_ALIAS +export ENDPOINT=${WORKSHOP_NAMESPACE}-org1-peer-gateway.${WORKSHOP_INGRESS_DOMAIN}:443 + +npm start getAllAssets + +popd +``` + +Note that in order to support ingress and host access with the new virtual domain, the peer +CRDs have been instructed to [designate an additional SAN alias](../../infrastructure/sample-network/config/peers/org1-peer1.yaml#L69) +/ host name when provisioning the node TLS certificate with the CA. + + +--- + +[PREV: Install Chaincode](30-chaincode.md) <==> [NEXT: Teardown](90-teardown.md) diff --git a/full-stack-asset-transfer-guide/docs/CloudReady/50-OpenShift-Deployment.md b/full-stack-asset-transfer-guide/docs/CloudReady/50-OpenShift-Deployment.md new file mode 100644 index 00000000..61a80075 --- /dev/null +++ b/full-stack-asset-transfer-guide/docs/CloudReady/50-OpenShift-Deployment.md @@ -0,0 +1,41 @@ +# Notes on deployment with OpenShift + +There are minor variations in deploying to each different K8S environment; these notes are for open shift specifically + +## Login + +Typically for setup the local k8s context you should login to OpenShift onthe command line; this usually uses token based authentication rather than a password. + +## Storage Classes + +If you don't have any storage classes created by default... + +``` +CLUSTER_TYPE=ocp ./infrastructure/setup_storage_classes.sh +``` + +This uses rook to create storage classes +Set the setup_storage_classes + +``` +WORKSHOP_STORAGE_CLASS=rook-cephfs +``` + +## Image Registry + +Using the built in image registry is possible. + +Follow the instructions at https://docs.openshift.com/container-platform/4.8/registry/accessing-the-registry.html to create a user and permissions that allow for pushing and pulling from the registry + +Expose the registry externally with instructions at https://docs.openshift.com/container-platform/4.8/registry/securing-exposing-registry.html + +## Image names + +Note the name used externally is different from the internal name of the image. The name in the chaincode package MUST be the internal name + +As an example for a bare-metal OpenShift cluster these where the internal and external names of the same image + +``` +WORKSHOP_EXTERNAL_REPO=default-route-openshift-image-registry.apps.report.cp.fyre.ibm.com:443 +WORKSHOP_INTERNAL_REPO=image-registry.openshift-image-registry.svc:5000 +``` \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/docs/CloudReady/90-teardown.md b/full-stack-asset-transfer-guide/docs/CloudReady/90-teardown.md new file mode 100644 index 00000000..afabcd7a --- /dev/null +++ b/full-stack-asset-transfer-guide/docs/CloudReady/90-teardown.md @@ -0,0 +1,56 @@ +# Teardown + +<== [PREV: Go Bananas](40-bananas.md) + +--- + + +## Conga Test + +## Final Exercise: Build Your (Social) Network + +- Write down your email address / linkedin profile / # discord handle / etc. on the back of your Conga Card. +- Use the fabric-ca-client to enroll the user / password listed on your Conga Card. +- `create()` an NFT asset for your Conga Card on the shared workshop blockchain. +- Go around the room and meet people attending the workshop. +- Exchange your Conga Cards with workshop participants, issuing a `transfer()` transaction on the shared ledger. +- When you exchange conga cards, record the new owners (both on the back of the card and the ledger!) +- Bring your Conga Trading Cards home, and reach out to all your new friends and colleagues. + + +## Thank You! + +- Discord: [#dublin-workshop](https://discord.gg/PbjpetNqyR) +- Discord: [#fabric-kubernetes](https://discord.gg/DPKagScrN7) +- Thank you for attending the workshop! +- Safe travels home, and happy coding. + + +## Workshop Teardown: + +- Shut down the network, but leave KIND / MP / etc running: +```shell + +just cloud-network-down + +``` + +- Shut down KIND, the private container registry, and Nginx: +```shell + +just unkind + +``` + +- Shut down the Multipass VM, if running: +```shell + +multipass delete fabric-dev +multipass purge + +``` + +## Cloud VMs + +- Terminate EC2 / IKS Instances (Workshop systems will be deleted after the event) + diff --git a/full-stack-asset-transfer-guide/docs/CloudReady/README.md b/full-stack-asset-transfer-guide/docs/CloudReady/README.md new file mode 100644 index 00000000..5943d873 --- /dev/null +++ b/full-stack-asset-transfer-guide/docs/CloudReady/README.md @@ -0,0 +1,15 @@ +# Cloud Native Fabric + +- [Cloud Ready!](00-setup.md) +- **Exercise:** [Deploy a Kubernetes Cluster](10-kube.md) + - (optional) With a [Multipass VM](11-kube-multipass.md) + - (optional) With an [EC2 VM](12-kube-ec2-vm.md) + - (optional) With a [public cloud](13-kube-public-cloud.md) +- **Exercise:** [Deploy a Fabric Network](20-fabric.md) + - (optional) With the [Fabric Operations Console](21-fabric-operations-console.md) + - (optional) With the [Ansible Blockchain Collection](22-fabric-ansible-collection.md) +- **Exercise:** [Deploy a Smart Contract](30-chaincode.md) + - (optional) With the [Ansible Blockchain Collection](31-fabric-ansible-chaincode.md) +- **Exercise:** [Deploy a Client Application](40-bananas.md) + + diff --git a/full-stack-asset-transfer-guide/docs/SmartContractDev/00-Introduction.md b/full-stack-asset-transfer-guide/docs/SmartContractDev/00-Introduction.md new file mode 100644 index 00000000..15b6942d --- /dev/null +++ b/full-stack-asset-transfer-guide/docs/SmartContractDev/00-Introduction.md @@ -0,0 +1,119 @@ +# Smart Contract Development + +In this section, we'll introduce the concept of the Smart Contract, and how the Hyperledger Fabric platform handles these contracts. We'll talk about some of the important aspects to keep in mind; these can be different from other types of development. + +This example shows how details of an asset may be stored in the ledger itself, with the Smart Contract controlling the asset's lifecycle. It provides transaction functions to + +- Create an asset +- Retrieve (one or all) assets +- Update an asset +- Delete an asset +- Transfer ownership between parties + +This topic addresses common design considerations for smart contracts. If you want to dive straight into the workshop code, feel free to skip straight to the [getting started](./01-Exercise-Getting-Started.md) and come back to this page as a reference later. + +Please remember that Hyperledger Fabric is a Blockchain Ledger and not a Database! + +[NEXT - Getting Stared with Code](./01-Exercise-Getting-Started.md) + +--- +## Design the assets and contracts + +An initial and perhaps the most important decision is - "what information needs to be under the ledger's control?". This is important for several reasons. + +- The ledger is the shared state between organizations, is the information being shared applicable for all organizations to see? Note that Fabric's private data collections can be used when a subset of the ledger data needs to remain private between parties. +- For any ledger state updates, which organizations need to execute the smart contract and 'endorse' the results before the change is considered a valid transaction? Is the endorsement policy fixed, or based on the data? For example you may want the current asset owner's organization to endorse, along with a regulator organization. This can be accomplished with Fabric's state-based endorsement capabilities. +- How large is the amount of data? The smaller the better! Remember this data needs to be transferred between many parties and the history of transactions is preserved on the ledger. Would a salted secure hash of a scanned document work rather than the entire document? +- Though rich JSON query of the state is possible when using CouchDB as a state database, the peer and its ledger are optimized for transaction processing rather than query. Can queries be performed off the ledger, and the results 'validated' via a smart contract? + +For any end-to-end tutorial there is a trade-off between making the scenario realistic, but not sufficiently complicated. For this tutorial, we'll define the data stored on the ledger as being a single asset 'object' with the following fields. This has been kept very simple, but the approach should be familiar. + +- ID: string unique-identifier +- Color: string representing a color +- Size: numerical value representing a size +- Owner: string representing the identity of the asset owner (organization id plus common name from the client's certificate) +- Appraised Value: numerical value + +Arguably not all these values need to be on the ledger, the data could be stored in an off-ledger 'oracle' that provides a hash of the data to store on the blockchain ledger. + +### Keys and Queries + +Think of the ledger state database as being a key-value store to persist assets, or more generally, any data record that you want to maintain on the ledger. + +It is important to take care in choosing the 'key' that will be used. You could use simple keys such as a logical key passed in from an external system, a UUID, or even use the txid of the transaction that created the asset. Composite keys are also possible. These are constructed to form a hierarchical structure and enable additional types of key-based queries. + +A key string can be formed from a list of strings, separated by the `u+0000` nil byte. There must be at least one string in the list. If there is only one string, this is referred to as `simple` key, otherwise, it is a `composite` key. For example you may want to create a composite key based on the asset 'type' and ID. The smart contract APIs help you easily create composite keys. + +For simple keys, you can query based on the key using the API `getState(key: string)`. + +You can also query a range of keys using the API `getStateByRange(startKey: string, endKey: string)`. Range queries allow you to specify a start and end key, and return an iterator of all key-values between those start and end points (inclusively). The keys are ordered in alphanumeric order. + +Composite keys provide an interesting query mechanism as they offer a range query by partial key. For example, if a composite key has the strings `fruit:pineapples:supplier_fred:consignment_xx` (using a colon here to make it easier to read, as the nil byte isn't easy to read) then it is possible to issue queries with a leading partial key. +For example, to query all the pineapple records held by `supplier_fred` you could query on the partial key `fruit:pineapples:supplier_fred`. + +A way of thinking about this is to visualize the keys as forming a hierarchy. + +Note that the 'simple' and 'composite' keys are held distinct from each other. Therefore a query on a simple key won't return anything held under a composite key, and conversely a composite key query won't return anything held under a simple key. + +The above types of queries are supported on both LevelDB and CouchDB state databases and query the 'keys' of the key-value store. + +If you use CouchDB, you can also query on the 'value' of the key-value pair using a rich JSON query. This requires the value be in JSON format (as in this tutorial). CouchDB indices can be provided in the smart contract package to make JSON queries efficient (and strongly recommended). But keep in mind that 'value' based queries will never be as efficient as 'key' based queries. + +## Transaction Functions + +Let's look at the separate transaction function types that can be written on the Smart Contract. Each one of these can be invoked from the client application. +- 'Evaluate' functions are invoked in a read-only manner to query the ledger state database on a specific peer. +- 'Submit' functions are invoked to submit a transaction to all the peers that are needed to endorse changes to the ledger, resulting in a write operation or read-write operation that gets submitted to the ordering service and ultimately committed on all peers. + +### General Aspects + +Each smart contract transaction function needs to be marked as such (using language-specific conventions). You can also specify if the function is meant to be 'submitted' or 'evaluated'. This is not enforced but is an indication to the user. + +Each function will need to consider how it handles data to marshal into the format needed for the ledger. + +Each function needs to ensure that any initial state is correct. For example, before transferring an asset from Alice to Bob, ensure that Alice does own the asset, and that Alice is indeed the identity submitting the transfer transaction. + +### Creation Functions + +Consider in the create function if you want to pass in the individual data elements or a fully formed object. This is largely a matter of personal preference; remember though that any unique identifier must be created outside of the smart contract. Any form of random choice or other non-deterministic processes can not be used since the transaction will be executed on multiple peers and the results must match. + +Often there are extra elements of data (such as the submitting organization) that need to be added. + +### Retrieval + +It is a good idea to think ahead of the types of retrieval operations that are needed. Can the key structure be created to allow for range queries? + +If rich JSON queries are required, aim to make these as simple as possible and include indexes. Also ensure that if you wish to do a rich JSON query that involves the same data as the 'key' that it is included in the JSON structure as part of the 'value'. + +There is an example of get-all type queries in this workshop. Please consider that over time this could get a very large amount of data with a performance cost, therefore it is generally not recommended! + +For advanced queries, considering creating a downstream data store optimized for the types of queries that you need. The [off-chain data sample](https://github.com/hyperledger/fabric-samples/tree/main/off_chain_data) demonstrates how to build a downstream data store based on block events. + +### Reading-your-own-writes and conflicts + +The updates a transaction function makes to the state, aren't actioned immediately; they form a set of changes that must be endorsed and ordered. There are two important consequences of this asynchronous behaviour. + +If data under a key is updated, and then queried *in the same smart contract function* the returned data will be the *original* value - not the updated value. + +Additionally, you may see transactions invalidated with a 'MVCC Conflict' error: this means that two transaction functions have executed at the same time and attempted to read and update the same keys. The first transaction to be ordered in a block will get validated, while the second transaction will get invalidated since a read input has changed since contract execution. Design your keys and applications so that the same keys will not get updated concurrently. If this is a rare occurrence then you could simply compensate for it in the application, for example by re-issuing the transaction. + +## Audit Trails vs Asset Store + +An important decision to make is whether the state held on the ledger is representing an 'audit trail' of activity or the 'source of truth' of the actual assets. Storing the information about the assets, as shown in the following samples, is conceptually straightforward but keep in mind that this is a distributed database, rather than a database. + +Storing a form of audit trail can work well with the ledger concept. The 'source of truth' here is that a certain action was taken and it's results. For example the ownership of an asset changed. Details of the actual asset may be stored off chain. This does need more infrastructure provided around the ledger but is worth considering if the primary business reason is for audit purposes. For example, tracking the state of a process and how it moved from one state to the next. + +To help with the integration of other systems it is well worth issuing events from the transaction functions. These events will be available to the client applications when the transaction is finally committed. These can be very useful in triggering other processes. + +## Is it Smart Contract or Chaincode? + +Simply both - the terms have been used in Fabric history almost interchangeable; Chaincode was the original name, but then Smart Contracts is a common a blockchain term. The class/structure that is extended/implemented in code is called `Contract`. + +The aim is to standardize on +- the Smart Contract(s) are classes/structures - the code - that you write in Go/JavaScript/TypeScript/Java etc. +- these are then packaged up and run inside a Chaincode-container (chaincode-image / chaincode-runtime depending on exactly the format of the packaging) +- the chaincode definition is more than just the Smart Contract code, as it includes things such as the couchdb indexes, and the endorsement policy + +## Packaging + +In Hyperledger Fabric v1.x and still supported as 'the old lifecycle' in v2.x, the CDS chaincode package format was used. The v2.x 'new lifecycle' should be used now - with standard `tar.gz` format. Using `tar` and `gzip` are standard techniques with standard tools. Therefore the main issue becomes what goes into those files and when/how are they used. diff --git a/full-stack-asset-transfer-guide/docs/SmartContractDev/01-Exercise-Getting-Started.md b/full-stack-asset-transfer-guide/docs/SmartContractDev/01-Exercise-Getting-Started.md new file mode 100644 index 00000000..2d9f06a7 --- /dev/null +++ b/full-stack-asset-transfer-guide/docs/SmartContractDev/01-Exercise-Getting-Started.md @@ -0,0 +1,351 @@ +# Getting Started with a Smart Contract + +[PREVIOUS - Introduction](./00-Introduction.md) <==> [NEXT Adding a Transaction Function](./02-Exercise-Adding-tx-function.md) + +--- + +Make sure you have cloned the workshop: + +```bash +git clone https://github.com/hyperledger/fabric-samples.git fabric-samples +cd fabric-samples/full-stack-asset-transfer-guide + +export WORKSHOP_PATH=$(pwd) +``` + +First please check you've got the [required tools](../../SETUP.md) needed for the dev part of this workshop (docker, just, weft, node.js, and Fabric peer binary). To double check run the `check.sh` script + +``` +${WORKSHOP_PATH}/check.sh +``` + +Let's dive straight into creating some code to manage an 'asset'; best to have two windows open, one for running the 'FabricNetwork' and one for 'ChaincodeDev'. You may wish to open a third to watch the logs of the running Fabric Network. + +## Start the Fabric Infrastructure + +We're using MicroFab for the Fabric infrastructure as it's a single container that is fast to start. +The MicroFab container includes an ordering service node and a peer process that is pre-configured to create a channel and call external chaincodes. +It also includes credentials for an `org1` organization, which will be used to run the peer. We'll use an `org1` admin user when interacting with the environment. + +We'll use `just` recipes to execute multiple commands. `just` recipes are similar to `make` but simpler to understand. You can open the [justfile](../../justfile) in the project root directory to see which commands are run with each recipe. + +Start the MicroFab container by running the `just` recipe. This will set some properties for the MicroFab environment and start the MicroFab docker container. + +```bash +just microfab +``` + +This will start the docker container (automatically download it if necessary), and also write out some configuration/data files in the `_cfg/uf` directory. +```bash +ls -1 _cfg/uf + +_cfg +_gateways +_wallets +org1admin.env +org2admin.env +``` + +A file `org1admin.env` is written out that contains the environment variables needed to run applications _as the org1 admin identity_. A second organization is created, with a `org2admin.env` - this is for later exercises and is not needed at the moment. + +Let's take a look at the environment variables and source the file to set the environment variables: + +```bash +source _cfg/uf/org1admin.env +cat _cfg/uf/org1admin.env +``` + +You'll see these environment variables set: + +```bash +# sample contents +export CORE_PEER_LOCALMSPID=org1MSP +export CORE_PEER_MSPCONFIGPATH=/workshop/full-stack-asset-transfer-guide/_cfg/uf/_msp/org1/org1admin/msp +export CORE_PEER_ADDRESS=org1peer-api.127-0-0-1.nip.io:8080 +export FABRIC_CFG_PATH=/workshop/full-stack-asset-transfer-guide/config +export CORE_PEER_CLIENT_CONNTIMEOUT=15s +export CORE_PEER_DELIVERYCLIENT_CONNTIMEOUT=15s +``` + +Next let's look at the three directories that are created `_msp`, `_gateways`, `_wallets`. If you are short on time you can [skip ahead to the next section to package and deploy a chaincode](#package-and-deploy-chaincode-to-fabric). + +Firstly the `_msp` directory contains the membership services provider (MSP) credentials necessary to run the Fabric Peer CLI commands as the org1 admin, including the user's public certificate and private key for signing transactions. The MSP location is referenced in the `CORE_PEER_MSGCONFIGPATH` environment variable and contains the credential subdirectories expected by the Peer CLI command. + +Secondly the `_gateways` directory contains two JSON files, one per organization. This file contains details of the Peer's endpoint url to connect clients to. Older Fabric Client SDKs would need all the information in this file, but the new "Gateway SDKs" remove the need for all the detail. The new Gateway SDKs just need the peer's endpoint and TLS configuration. See this [example code](../../applications/ping-chaincode/src/fabric-connection-profile.ts) on how to parse this file easily for the Gateway SDK. + +Third is the `_wallets` directory - there are three subdirectories, one each for the Ordering Organization, Organization 1, and Organization 2. These directories contain `*.id` files that contain details of identities and their respective credentials, similar to the MSP content, but in a JSON format that applications can more easily parse: + +``` +_wallets +├── Orderer +│   └── ordereradmin.id +├── org1 +│   ├── org1admin.id +│   └── org1caadmin.id +└── org2 + ├── org2admin.id + └── org2caadmin.id +``` + +`org1admin.id` contains the credentials for submitting transactions from an org1 admin. +`org1caadmin.id` contains the credentials for creating additional identities in the org1 Certificate Authority (CA). + +Note that when MicroFab started it automatically launched a Certificate Authority that created these identities and their respective credentials. + +Pick one if the id files and look at the JSON content including the public certificate and private key: + +```bash +cat _cfg/uf/_wallets/org1/org1admin.id | jq +``` + +You'll see the contents of org1admin.id: + +```bash +{ + "credentials": { + "certificate": "-----BEGIN CERTIFICATE-----\n xxxx \n-----END CERTIFICATE-----\n", + "privateKey": "-----BEGIN PRIVATE KEY-----\n xxxx \n-----END PRIVATE KEY-----\n" + }, + "mspId": "org1MSP", + "type": "X.509", + "version": 1 +} +``` + +This information then can be used by the client applications. See [this example code](../../applications/ping-chaincode/src/jsonid-adapter.ts) for how you can directly parse this file to use with the gateway. + +At this point you may wish to run `docker logs -f microfab` in a separate window to watch the activity - you don't need to setup anything specific here. + +## Package and deploy chaincode to Fabric + +We are going to use the Chaincode-As-A-Service (CCAAS) pattern for chaincode. +With the CCAAS pattern, the Fabric peer does not launch a deployed chaincode. +Instead, we will run chaincode as an external process so that we can easily start, stop, update, and debug the chaincode locally. +But we still need to tell the peer where the chaincode is running. We do this by deploying a chaincode package that only includes the name of the chaincode and chaincode address, rather than the actual chaincode source code. + +### Package and deploy chaincode using `just` recipe. + +```bash +just debugcc +``` + +You will see the chaincode id and deployment steps returned. + +### Details of this packaging and deployment + +If you would like to understand chaincode packaging and deployment process in more detail you can walk through the steps manually here. Otherwise you can [skip ahead to the next section to run the chaincode](#run-the-chaincode-locally). + +Fabric chaincode packages are a `tgz` format archive that contain two files: + +- `metadata.json` - the chaincode label and type +- `code.tar.gz` - source artifacts for the chaincode + +Create the `metadata.json` first, this tells the Peer the type of chaincode and a label to use to refer to this later + +```bash +cat << METADATAJSON-EOF > metadata.json +{ + "type": "ccaas", + "label": "asset-transfer" +} +METADATAJSON-EOF +``` + +Create the `code.tar.gz` - for the Chaincode-as-a-service, this file will contain a single JSON file `connection.json`. Traditional Fabric packaging would include all the source code of the chaincode here. In this case, we need the JSON file to contain the URL the peer will find the chaincode at and a timeout. Note this is a special hostname so the peer inside the docker container can locate the chaincode running on the host system + +``` +cat << CONNECTIONJSON-EOF > connection.json +{ + "address":"host.docker.internal:9999", + "dial_timeout":"15s" +} +CONNECTIONJSON-EOF +``` + +We can now build the actual package. Create a code.tar.gz archive containing the connection.json file. + +```bash +tar -czf code.tar.gz connection.json +``` + +Create the final chaincode package archive. + +```bash +tar -czf asset-transfer.tgz metadata.json code.tar.gz +``` + +We're going to use the peer CLI commands to install and deploy the chaincode. Chaincode is 'deployed' by indicating agreement to it and then committing it to a channel: + +``` +source _cfg/uf/org1admin.env + +peer lifecycle chaincode install asset-transfer.tgz +``` + +The ChaincodeID that is returned from this install command needs to be saved, typically this is best as an environment variable + +```bash +export CHAINCODE_ID=$(peer lifecycle chaincode calculatepackageid asset-transfer.tgz) +``` + +Next, define the chaincode on the blockchain channel by approving it and committing it. If you have already deployed the chaincode using the `just` recipe above, then increment the `--sequence` number to `2`. + +```bash +peer lifecycle chaincode approveformyorg --channelID mychannel --name asset-transfer -v 0 --package-id $CHAINCODE_ID --sequence 2 --connTimeout 15s +peer lifecycle chaincode commit --channelID mychannel --name asset-transfer -v 0 --sequence 2 --connTimeout 15s +``` + +## Run the chaincode locally + +We'll use the example typescript contract already written in `$WORKSHOP_PATH/contracts/asset-transfer-typescript`. Feel free to take a look at the contract code in [contracts/asset-transfer-typescript/src/assetTransfer.ts](../../contracts/asset-transfer-typescript/src/assetTransfer.ts). You'll see the implementation of contract functions such as `CreateAsset()` and `ReadAsset()` there. + +Use another terminal window for the chaincode. Make sure the terminal is setup with the same environment variables as the first terminal: + +``` +cd fabric-samples/full-stack-asset-transfer-guide +export WORKSHOP_PATH=$(pwd) +export PATH=${WORKSHOP_PATH}/bin:$PATH +export FABRIC_CFG_PATH=${WORKSHOP_PATH}/config +``` + +As with any typescript module we need to run `npm install` to manage the dependencies for the chaincode and then build (compile) the chaincode typescript to javascript. + +``` +cd contracts/asset-transfer-typescript + +npm install + +npm run build +``` + +An easy way to test the contract has been built ok, is to generate the 'Contract Metadata' into a `metadata.json` file. This is a language agnostic definition of the contracts, and the datatypes the contract returns. It borrows from the OpenAPI used for defining REST APIs. It is also very useful to share to teams writing client applications so they know the data structures and transaction functions they can call. +As it's a JSON document, it's amenable to process to create other resources. + +The metadata-generate command has been put into the `package.json`: + +``` +npm run metadata +``` + +Review the generated `metadata.json` and see the summary of the contract information, the transaction functions, and datatypes. This information can also be captured at runtime and is a good way of testing deployment. + + +## Iterative Development and Test + +**All the steps up until here are one time only. You can now iterate over the development of your contract** + +From your chaincode terminal window lets start the Smart Contract node module. Remember that the `CHAINCODE_ID` and the `CHAINCODE_SERVER_ADDRESS` are the only pieces of information needed. + +Note: Use your specific CHAINCODE_ID from earlier; the `CHAINCODE_SERVER_ADDRESS` is different - this is because in this case it is telling the chaincode where to listen for incoming connections from the Peer. We'll use port 9999 on the local machine. + +``` +source ${WORKSHOP_PATH}/_cfg/uf/org1admin.env + +# if you ran the justfile above, these will already be exported, but you may want to double check they are set: +export CHAINCODE_SERVER_ADDRESS=0.0.0.0:9999 +export CHAINCODE_ID=$(peer lifecycle chaincode calculatepackageid asset-transfer.tgz) + +npm run start:server-debug +``` + +### Run some transactions + +Choose a terminal window to run the transactions from; initially we'll use the `peer` CLI to run the commands. + +If this is a new terminal window set the environment variables: + +``` +cd fabric-samples/full-stack-asset-transfer-guide +export WORKSHOP_PATH=$(pwd) +export PATH=${WORKSHOP_PATH}/bin:$PATH +export FABRIC_CFG_PATH=${WORKSHOP_PATH}/config +``` + +Make sure that the peer binary and the config directory are set (run the `${WORKSHOP_PATH}/check.sh` script to double check). + +Set up the environment context for acting as the Org 1 Administrator. + +``` +source ${WORKSHOP_PATH}/_cfg/uf/org1admin.env +``` + +Use the peer CLI to issue basic query commands against the contract. For example check the metadata for the contract (if you have jq, it's easier to read if you pipe the results into jq). Use one of these commands: + +``` +peer chaincode query -C mychannel -n asset-transfer -c '{"Args":["org.hyperledger.fabric:GetMetadata"]}' +peer chaincode query -C mychannel -n asset-transfer -c '{"Args":["org.hyperledger.fabric:GetMetadata"]}' | jq +``` + +Let's create an asset with ID=001: + +``` +peer chaincode invoke -C mychannel -n asset-transfer -c '{"Args":["CreateAsset","{\"ID\":\"001\", \"Color\":\"Red\",\"Size\":52,\"Owner\":\"Fred\",\"AppraisedValue\":234234}"]}' --connTimeout 15s +``` + +If you are watching the MicroFab logs you'll see that the peer committed a new block to the ledger. + +Now read back that asset: + +``` +peer chaincode query -C mychannel -n asset-transfer -c '{"Args":["ReadAsset","001"]}' +``` + +You'll see the asset returned: + +``` +{"AppraisedValue":234234,"Color":"Red","ID":"001","Owner":"{\"org\":\"org1MSP\",\"user\":\"Fred\"}","Size":52} +``` + +### Making a change and re-running the code + +If we invoke a query command on a asset that does not exist, for example 002, we'll get back an error: + +``` +peer chaincode query -C mychannel -n asset-transfer -c '{"Args":["ReadAsset","002"]}' +``` + +returns error: + +``` +Error: endorsement failure during query. response: status:500 message:"Sorry, asset 002 has not been created" +``` + +Let's say we want to change that error message to something else. + +- Stop the running chaincode (CTRL-C in the chaincode terminal) +- Load the `src/assetTransfer.ts` file into an editor of your choice +- Around line 51, find the error string and make a modification. Remember to save the change. +- Rebuild the typescript contract: +``` +npm run build +``` + +You can now restart the contract as before + +``` +npm run start:server-debug +``` + + +And run the same query, and see the updated error message: + +``` +peer chaincode query -C mychannel -n asset-transfer -c '{"Args":["ReadAsset","002"]}' +``` + +## Debugging + +As the chaincode was started with the Node.js debug setting, you can connect a node.js debugger. For example VSCode has a good typescript/node.js debugger. + +If you select the debug tab, and open the debug configurations, add "Attach to a node.js process" configuration. +VSCode will prompt you with the template. The default port should be sufficient here. +You can then start the 'attached to process' debug, and pick the process to debug into. + +Remember to set a breakpoint at the start of the transaction function you want to debug. + +Watch out for: + - VSCode uses node, so take care in selecting the right process + - remember the client/fabric transaction timeout, whilst you have the chaincode stopped in the debugger, the timeout is still 'ticking' + + +Look at the [Test and Debugging Contracts](./03-Test-And-Debug-Reference.md) for more details and information on other languages. diff --git a/full-stack-asset-transfer-guide/docs/SmartContractDev/02-Exercise-Adding-tx-function.md b/full-stack-asset-transfer-guide/docs/SmartContractDev/02-Exercise-Adding-tx-function.md new file mode 100644 index 00000000..28548acc --- /dev/null +++ b/full-stack-asset-transfer-guide/docs/SmartContractDev/02-Exercise-Adding-tx-function.md @@ -0,0 +1,52 @@ +## Adding a Transaction Function + +[PREVIOUS - Getting Started](./01-Exercise-Getting-Started.md) <==> [NEXT - Test And Debug](./03-Test-And-Debug-Reference.md) + + In this exercise, we're going to add a transaction function to check the appraised value. We're going to write this function as part of the iterative development cycle with external chaincode-as-a-service, so there is no requirement to stop fabric or worry about versions of deployed chaincode. We will simply be updating the chaincode source code, restarting the chaincode service, and testing out the new function. + +The function will: + +- be a 'read-only' function +- take a given asset id, and an upper and lower value +- return a true/false indication if the appraised value is within the upper/lower values + +## Steps + +Firstly ensure that you've run the Smart Contract and been able to issue transactions against it. It's also worth making sure that you can stop and restart the code after making some minor changes. + +- In the `assetTransfer.ts` file create a new function `ValidateValue` . The `ReadAsset` function is a good one to use as a basis. This is a read-only function and already gets the asset from the ledger. +- Add an upper and lower value to the parameters of the function +- `ReadAsset` returns the asset directly, look at the `UpdateAsset` function for how to process the data +- Check the value and return true/false depending on if the value is in the bounds. +- If you wish also set an event. + +Remember to stop the running code, rebuild it and start again. Remember you can attach the debugger to help track down issues + +## Testing + +You can invoke this then with similar commands as in Getting Started. + +For example to check if the value is between 1000 and 4200, issue something like + +``` +peer chaincode query -C mychannel -n asset-transfer -c '{"Args":["ValidateValue","001","1000","4200"]}' +``` + +## Example implementation + +A possible implementation would be + +``` +@Transaction(false) +async ValidateValue(ctx: Context, id: string, lower:number, upper:number): Promise { + const existingAssetBytes = await this.#readAsset(ctx, id); + const existingAsset = newAsset(unmarshal(existingAssetBytes)); + + if (existingAsset.AppraisedValue > lower && existingAsset.AppraisedValue < upper){ + return true; + } else { + return false; + } + +} +``` diff --git a/full-stack-asset-transfer-guide/docs/SmartContractDev/03-Test-And-Debug-Reference.md b/full-stack-asset-transfer-guide/docs/SmartContractDev/03-Test-And-Debug-Reference.md new file mode 100644 index 00000000..38dae130 --- /dev/null +++ b/full-stack-asset-transfer-guide/docs/SmartContractDev/03-Test-And-Debug-Reference.md @@ -0,0 +1,134 @@ +# Test and Debug Reference + +[PREVIOUS - Adding a Transaction Function](./02-Exercise-Adding-tx-function.md) + +**Aim:** Stand up a Hyperledger Fabric Smart Contract so it can easily be debugged + +**Objectives:** + +- Introduce what Chaincode-as-a-Service is, and how it helps +- Show how to build & configure a Chaincode to run like this +- How to deploy these in a running Hyperledger Fabric network +- How then to debug this running Chaincode. + +--- + +## Overview + +It helps to think of three 'parts' + +- The Fabric network, consisting of the peers, orderers, certificate authorities etc. Along with configured channels and identities. + For our purposes here, this can be considered as a 'black box'. The 'black box' can be configured a number of different ways, but typically will be one or more docker containers. This workshop uses MicroFab to bring up the Fabric network in a single docker container. +- The Chaincode - this will be running in its own process or docker container. +- The editor - VSCode is covered here, but the approach should hold with other debuggers and editors. + +The _high level process_ is + +0. Stand-up Fabric +1. Develop the Smart Contract +3. Create a chaincode package using the chaincode-as-a-service approach +4. Install the chaincode to a peer and Approve/Commit the chaincode on a channel +5. Start the chaincode using the chaincode-as-a-service approach +6. Attach your debugger to the running chaincode and set a breakpoint +7. Invoke a transaction, this will then halt in the debugger to let you step over the code +8. Find the bugs and repeat **from step 5** - note that we don't need to Package/Install/Approve/Commit the chaincode again. + +This is the exact process that you will have followed in the ['Getting Started'](./01-Exercise-Getting-Started.md) section + +### What do you need? + +You'll need to have docker available to you, along with VSCode. Also, install the VSCode extensions you prefer for debugging your preferred language. Other debuggers are available and you're free to use those if you have them available. + +- For TypeScript and JavaScript VSCode has built-in support +- For Java the [JavaExtension pack](https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-pack) is suggested + +### What is Chaincode as Service? + +The chaincode-as-a-service feature is a very useful and practical way to run 'Smart Contracts. Traditionally the Fabric Peer has taken on the role of orchestrating +the complete lifecycle of the chaincode. It required access to the Docker Daemon to create images, and start containers. Java, Node.js and Go chaincode frameworks were + explicitly known to the peer including how they should be built and started. + +As a result, this makes it very hard to deploy into Kubernetes (K8S) style environments or to run in any form of debug mode. Additionally, the code is being rebuilt by + the peer therefore there is some degree of uncertainty about what dependencies have been pulled in. + +Chaincode-as-service requires you to orchestrate the build and deployment phase yourself. Whilst this is an additional step, it gives control back. The Peer still +requires a 'chaincode package' to be installed. In this case, this doesn't contain code, but the information about where the chaincode is hosted. (Hostname, Port, TLS config etc) + + +## Running the Smart Contracts + +An important point is that the code written for the Smart Contract is the same, whether it's managed by the peer or Chaincode-as-a-Service. +What is different is how that is started and packaged. The overall process is the same, regardless of whether your smart contract is written in Java/Typescript/Go. + +### TypeScript/JavaScript + +Using the Typescript contract as an example, the difference is easier to see. The package.json contains 4 'start' commands + +``` + "start": "fabric-chaincode-node start", + "start:server-nontls": "fabric-chaincode-node server --chaincode-address=$CHAINCODE_SERVER_ADDRESS --chaincode-id=$CHAINCODE_ID", + "start:server": "fabric-chaincode-node server --chaincode-address=$CHAINCODE_SERVER_ADDRESS --chaincode-id=$CHAINCODE_ID --chaincode-tls-key-file=/hyperledger/privatekey.pem --chaincode-tls-client-cacert-file=/hyperledger/rootcert.pem --chaincode-tls-cert-file=/hyperledger/cert.pem", + "start:server-debug": "set -x && NODE_OPTIONS='--inspect=0.0.0.0:9229' fabric-chaincode-node server --chaincode-address=$CHAINCODE_SERVER_ADDRESS --chaincode-id=$CHAINCODE_ID" +``` + +The first is used when the peer is completely controlling the chaincode. The second `start:server-nontls` starts in the Chaincode-as-a-service mode (without using TLS). The command +is very similar `fabric-chainmcode-node server` rather than `fabric-chaincode-node start`. Two options are provided here, these are the network address the chaincode + will listen on and its id. (aside when the Peer runs the chaincode, it does pass extra options, but they aren't seen in the package.json) + +The third `start:server` adds the required TLS configuration, but is otherwise the same. +The forth `start:server-debug` is the same as the non-TLS case, but includes the environment variable required to get Node.js to open a port to allow a debugger to connect remotely. + +### Java + +The changes for the Java chaincode are logically the same. The build.gradle (or use Maven if you wish) is exactly the same (like there were no changes to the +TypeScript compilation). With the v2.4.1 Java Chaincode libraries, there are no code changes to make or build changes. The '-as-a-service' mode will be used if + the environment variable `CHAINCODE_SERVER_ADDRESS` is set. + +For the non-TLS case the Java chaincode is started with `java -jar /chaincode.jar` - and will use the Chaincode-as-a-service mode _if_ the environment variable `CHAINCODE_SERVER_ADDRESS` is set. + +For debug, the JVM needs to put into debug mode `java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=0.0.0.0:8000 -jar /chaincode.jar` + +## How is the chaincode package different? + +A key difference is that the chaincode package does not contain code. It is used as a holder of data that indicates to the peer where the chaincode is. +What host/port and what TLS configuration is needed? Chaincode packages already can hold data about the couchdb indexes to use or the private data collections. + +Within the package, the `connection.json` is an important file. At its simplest it would be + +```json +{ + "address": "assettransfer_ccaas:9999", + "dial_timeout": "10s", + "tls_required": false +} +``` + +This is telling the peer the chaincode is on host `assettransfer_ccaas` port 9999. 10s timeout on connecting and tls is not needed. + +The packager can be constructed by hand, it's a set of json files, collected together with `tgz`. + +### Important networking warning + +The chaincode package that is installed critically contains the hostname and port that the peer is expecting the chaincode to listen on. If nothing answers the +peer, it obviously will fail the transaction. + +Note that it is ok not to have the chaincode running at all times, the peer won't complain until it is asked to actually connect to the chaincode. This is an important + ability as it allows for debugging and restarting of the container. + +The hostname that is supplied must be something that the peer, from its perspective, can resolve. Typically the peer will be inside a docker container, therefore + supplying `localhost` or `127.0.0.1` will resolve to the same container the peer is running in. + +Assuming that the peer is running in a docker container, the chaincode could either be run in its own docker container, on the same docker network as the peers + container, or it could be run directly on the host system. + +Depending your host OS, the 'specialhostname' that is used from within the docker container to access the host varies. + For example, see this [stackoverflow post](https://stackoverflow.com/questions/24319662/from-inside-of-a-docker-container-how-do-i-connect-to-the-localhost-of-the-mach#:~:text=To%20access%20host%20machine%20from,using%20it%20to%20anything%20else.&text=Then%20make%20sure%20that%20you,0.0%20.) + +The advantage of this is the chaincode can run locally on your host machine and is simple to connect to from a debugger. + +Alternatively, you can package the chaincode into its own docker container, and run that. You can still debug into this, but need to ensure that the ports of the +container are exposed correctly for your language runtime. + +## Single stepping and timeouts + +- If you are going to single step in a debugger, then you are likely to hit the Fabric transaction timeout value. By default this is 30 seconds, meaning the chaincode has to complete transactions in 30 seconds or less before the peer timesout the request. In your `config/core.yaml` update `executetimeout` to be `300s`, or add `CORE_CHAINCODE_EXECUTETIMEOUT=300s` to the environment variable options of each peer, so that you can step through your contract code in a debugger for 5 minutes for each invoked transaction function. diff --git a/full-stack-asset-transfer-guide/docs/images/ApplicationDev.pptx b/full-stack-asset-transfer-guide/docs/images/ApplicationDev.pptx new file mode 100644 index 00000000..4b1da292 Binary files /dev/null and b/full-stack-asset-transfer-guide/docs/images/ApplicationDev.pptx differ diff --git a/full-stack-asset-transfer-guide/docs/images/ApplicationDev/fabric-gateway-deployment.png b/full-stack-asset-transfer-guide/docs/images/ApplicationDev/fabric-gateway-deployment.png new file mode 100644 index 00000000..a3a42254 Binary files /dev/null and b/full-stack-asset-transfer-guide/docs/images/ApplicationDev/fabric-gateway-deployment.png differ diff --git a/full-stack-asset-transfer-guide/docs/images/ApplicationDev/fabric-gateway-model.png b/full-stack-asset-transfer-guide/docs/images/ApplicationDev/fabric-gateway-model.png new file mode 100644 index 00000000..f059cc03 Binary files /dev/null and b/full-stack-asset-transfer-guide/docs/images/ApplicationDev/fabric-gateway-model.png differ diff --git a/full-stack-asset-transfer-guide/docs/images/ApplicationDev/legacy-sdk-model.png b/full-stack-asset-transfer-guide/docs/images/ApplicationDev/legacy-sdk-model.png new file mode 100644 index 00000000..ce46f043 Binary files /dev/null and b/full-stack-asset-transfer-guide/docs/images/ApplicationDev/legacy-sdk-model.png differ diff --git a/full-stack-asset-transfer-guide/docs/images/ApplicationDev/transaction-submit-flow.png b/full-stack-asset-transfer-guide/docs/images/ApplicationDev/transaction-submit-flow.png new file mode 100644 index 00000000..b21a9758 Binary files /dev/null and b/full-stack-asset-transfer-guide/docs/images/ApplicationDev/transaction-submit-flow.png differ diff --git a/full-stack-asset-transfer-guide/docs/images/CloudReady.pptx b/full-stack-asset-transfer-guide/docs/images/CloudReady.pptx new file mode 100644 index 00000000..40ee80a3 Binary files /dev/null and b/full-stack-asset-transfer-guide/docs/images/CloudReady.pptx differ diff --git a/full-stack-asset-transfer-guide/docs/images/CloudReady/00-cloud-ready-2.png b/full-stack-asset-transfer-guide/docs/images/CloudReady/00-cloud-ready-2.png new file mode 100644 index 00000000..5b48adb0 Binary files /dev/null and b/full-stack-asset-transfer-guide/docs/images/CloudReady/00-cloud-ready-2.png differ diff --git a/full-stack-asset-transfer-guide/docs/images/CloudReady/10-kube.png b/full-stack-asset-transfer-guide/docs/images/CloudReady/10-kube.png new file mode 100644 index 00000000..8d61750d Binary files /dev/null and b/full-stack-asset-transfer-guide/docs/images/CloudReady/10-kube.png differ diff --git a/full-stack-asset-transfer-guide/docs/images/CloudReady/12-kube-ec2-vm.png b/full-stack-asset-transfer-guide/docs/images/CloudReady/12-kube-ec2-vm.png new file mode 100644 index 00000000..bcc4201f Binary files /dev/null and b/full-stack-asset-transfer-guide/docs/images/CloudReady/12-kube-ec2-vm.png differ diff --git a/full-stack-asset-transfer-guide/docs/images/CloudReady/20-fabric.png b/full-stack-asset-transfer-guide/docs/images/CloudReady/20-fabric.png new file mode 100644 index 00000000..228da427 Binary files /dev/null and b/full-stack-asset-transfer-guide/docs/images/CloudReady/20-fabric.png differ diff --git a/full-stack-asset-transfer-guide/docs/images/CloudReady/30-chaincode.png b/full-stack-asset-transfer-guide/docs/images/CloudReady/30-chaincode.png new file mode 100644 index 00000000..ad0a3f24 Binary files /dev/null and b/full-stack-asset-transfer-guide/docs/images/CloudReady/30-chaincode.png differ diff --git a/full-stack-asset-transfer-guide/docs/images/CloudReady/40-gateway-client-app.png b/full-stack-asset-transfer-guide/docs/images/CloudReady/40-gateway-client-app.png new file mode 100644 index 00000000..87222ea9 Binary files /dev/null and b/full-stack-asset-transfer-guide/docs/images/CloudReady/40-gateway-client-app.png differ diff --git a/full-stack-asset-transfer-guide/docs/images/CloudReady/kube-ec2-vm.png b/full-stack-asset-transfer-guide/docs/images/CloudReady/kube-ec2-vm.png new file mode 100644 index 00000000..17b30ad7 Binary files /dev/null and b/full-stack-asset-transfer-guide/docs/images/CloudReady/kube-ec2-vm.png differ diff --git a/full-stack-asset-transfer-guide/docs/images/cloud-vm-with-operator-network.png b/full-stack-asset-transfer-guide/docs/images/cloud-vm-with-operator-network.png new file mode 100644 index 00000000..9cd1a883 Binary files /dev/null and b/full-stack-asset-transfer-guide/docs/images/cloud-vm-with-operator-network.png differ diff --git a/full-stack-asset-transfer-guide/docs/images/multipass-operator-network.png b/full-stack-asset-transfer-guide/docs/images/multipass-operator-network.png new file mode 100644 index 00000000..70b62f29 Binary files /dev/null and b/full-stack-asset-transfer-guide/docs/images/multipass-operator-network.png differ diff --git a/full-stack-asset-transfer-guide/docs/images/multipass-test-network.png b/full-stack-asset-transfer-guide/docs/images/multipass-test-network.png new file mode 100644 index 00000000..f4d1190c Binary files /dev/null and b/full-stack-asset-transfer-guide/docs/images/multipass-test-network.png differ diff --git a/full-stack-asset-transfer-guide/docs/images/readme_diagram.png b/full-stack-asset-transfer-guide/docs/images/readme_diagram.png new file mode 100644 index 00000000..a1ba8f32 Binary files /dev/null and b/full-stack-asset-transfer-guide/docs/images/readme_diagram.png differ diff --git a/full-stack-asset-transfer-guide/docs/tips-for-windows-dev.md b/full-stack-asset-transfer-guide/docs/tips-for-windows-dev.md new file mode 100644 index 00000000..a686bacb --- /dev/null +++ b/full-stack-asset-transfer-guide/docs/tips-for-windows-dev.md @@ -0,0 +1,56 @@ +# Using Windows + +We recommend using the Windows Subsystem for Linux (WSL2) or use Multipass to create VMs. If you've never used either then Multipass is probably the quickest way to start initially. + +## Multipass + +- Setup [Multipass](https://multipass.run/) on Windows (recommened to enable Hyper_V) +- From a Windows Command Prompt + +``` +multipass launch --name fabric-dev --disk 80G --cpus 8 --mem 8G --cloud-init https://raw.githubusercontent.com/hyperledgendary/full-stack-asset-transfer-guide/main/infrastructure/multipass-cloud-config.yaml +``` + +## Using VSCode +- Setup [vscode](https://code.visualstudio.com/) and make sure you've the [remote development extension pack ](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remote-extensionpack)installed + +- Find out the IP address of the machines thats created - `multipass list` will show you this. For example + +``` +C:\Users\014961866>multipass list +Name State IPv4 Image +primary Running 172.31.125.88 Ubuntu 20.04 LTS +fabric-dev Running 172.31.118.103 Ubuntu 20.04 LTS + 172.17.0.1 +``` + +- You will need to find the private ssh key that multipass uses; this _should_ be at `C:\ProgramData\Multipass\data\ssh-keys\id_rsa` +- Copy this to you home directory (otherwise SSH will not use the file as it's 'too open') + +``` +copy C:\ProgramData\Multipass\data\ssh-keys\id_rsa %HOMEDRIVE%%HOMEPATH%\.ssh\multipass_id_rsa +``` + +- In VSCode, click on the remote development icon in the toolbar, and in the *Remote Explorer*, choose *SSH Targets* +- In the title bar of *SSH Targets*, click on the cog, and pick the default configuration file. +- Create an entry in this configuration file + - change the HostName to the IP of the multipass created VM + - ensure the identity file points to the file you copied + - you can change the `fabric-dev` name if you have multiple entries + +``` +Host fabric-dev + HostName 172.31.118.103 + User ubuntu + Port 22 + StrictHostKeyChecking no + PasswordAuthentication no + IdentityFile C:/Users//.ssh/multipass_id_rsa + IdentitiesOnly yes + LogLevel FATAL +``` + +- When save, an entry for *fabric-dev* will appear in the *SSH Targets* view +- Click on the 'Open Window' icon next to it +- First time you'll be asked to confirm the system is Linux, and VSCode will setup it's remote server. +- Then you're good to go with browsing files, and also using the inbuilt terminal. diff --git a/full-stack-asset-transfer-guide/infrastructure/configuration/fabric-common-vars.yml b/full-stack-asset-transfer-guide/infrastructure/configuration/fabric-common-vars.yml new file mode 100644 index 00000000..1f5db563 --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/configuration/fabric-common-vars.yml @@ -0,0 +1,23 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +# These vars are used in more than one file, +# i.e. needed by multiple orgs so can't just live in a per org file +ordering_org_name: "Ordering Org" +ordering_service_name: "Ordering Service" +org1_name: "Org1" +org1_msp_id: Org1MSP +org2_name: "Org2" +org2_msp_id: Org2MSP +channel_name: "mychannel" +# smart_contract_name: "fabcar" +# smart_contract_version: "1.0.0" +# smart_contract_sequence: 1 +# smart_contract_package: "fabcar@1.0.0.tgz" +# smart_contract_constructor: "initLedger" +# smart_contract_endorsement_policy: "" +# smart_contract_collections_file: "" +ca_version: ">=1.4,<2.0" +peer_version: ">=2.2,<3.0" +ordering_service_version: ">=2.2,<3.0" diff --git a/full-stack-asset-transfer-guide/infrastructure/configuration/fabric-ordering-org-vars.yml b/full-stack-asset-transfer-guide/infrastructure/configuration/fabric-ordering-org-vars.yml new file mode 100644 index 00000000..426d00fd --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/configuration/fabric-ordering-org-vars.yml @@ -0,0 +1,13 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +ca_admin_enrollment_id: admin +ca_admin_enrollment_secret: adminpw +organization_admin_enrollment_id: orderingorgadmin +organization_admin_enrollment_secret: orderingorgadminpw +ordering_service_enrollment_id: orderingorgorderer +ordering_service_enrollment_secret: orderingorgordererpw +ordering_service_msp: OrdererMSP +ordering_service_nodes: 1 +wait_timeout: 600 diff --git a/full-stack-asset-transfer-guide/infrastructure/configuration/fabric-org1-vars.yml b/full-stack-asset-transfer-guide/infrastructure/configuration/fabric-org1-vars.yml new file mode 100644 index 00000000..d9256429 --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/configuration/fabric-org1-vars.yml @@ -0,0 +1,17 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +ca_admin_enrollment_id: admin +ca_admin_enrollment_secret: adminpw +organization_admin_enrollment_id: org1admin +organization_admin_enrollment_secret: org1adminpw +peer_enrollment_id: org1peer +peer_enrollment_secret: org1peerpw +application_enrollment_id: org1app +application_enrollment_secret: org1apppw +application_enrollment_type: client +application_max_enrollments: 10 +org1_ca_name: "Org1 CA" +org1_peer_name: "Org1 Peer" +wait_timeout: 600 diff --git a/full-stack-asset-transfer-guide/infrastructure/configuration/fabric-org2-vars.yml b/full-stack-asset-transfer-guide/infrastructure/configuration/fabric-org2-vars.yml new file mode 100644 index 00000000..2480632c --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/configuration/fabric-org2-vars.yml @@ -0,0 +1,17 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +ca_admin_enrollment_id: admin +ca_admin_enrollment_secret: adminpw +organization_admin_enrollment_id: org2admin +organization_admin_enrollment_secret: org2adminpw +peer_enrollment_id: org2peer +peer_enrollment_secret: org2peerpw +application_enrollment_id: org2app +application_enrollment_secret: org2apppw +application_enrollment_type: client +application_max_enrollments: 10 +org2_ca_name: "Org2 CA" +org2_peer_name: "Org2 Peer" +wait_timeout: 600 diff --git a/full-stack-asset-transfer-guide/infrastructure/configuration/fabric-sail.yaml b/full-stack-asset-transfer-guide/infrastructure/configuration/fabric-sail.yaml new file mode 100644 index 00000000..5c8e6c34 --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/configuration/fabric-sail.yaml @@ -0,0 +1,36 @@ +network: + name: asset-transfer-basic + domain: localho.st + namespace: test-network + + organizations: + - name: org0 + orderers: + - name: org0-orderers + count: 3 + + - name: org1 + peers: + - name: org1-peer1 + anchor: true + - name: org1-peer2 + + - name: org2 + peers: + - name: org2-peer1 + anchor: true + - name: org2-peer2 + + channels: + - name: mychannel + organizations: + - org1 + - org2 + + chaincode: + - name: asset-transfer + version: v0.1.1 + package: https://github.com/hyperledgendary/asset-transfer-basic/releases/download/v0.1.1/asset-transfer-basic-v0.1.1.tgz + channels: + - name: mychannel + policy: "OR('org1.member', 'org2.member')" \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/infrastructure/configuration/operator-console-vars.yml b/full-stack-asset-transfer-guide/infrastructure/configuration/operator-console-vars.yml new file mode 100644 index 00000000..d61a7e2c --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/configuration/operator-console-vars.yml @@ -0,0 +1,39 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +# The type of K8S cluster this is using +target: ${cluster_type} + +# If the target cluster is OpenShift use this value for target +# target: openshift + +# If the target cluster is any other k8s use this value for target +# target: k8s + + + +arch: amd64 + +# k8s namespace for the operator and console +namespace: ${namespace} + +# used if this is openshift +project: ${namespace} + +# Console name/domain +console_name: hlf-console +console: hlf-console +console_domain: ${ingress_domain} + +# default configuration for the console +# password reset will be required on first login +console_email: admin +console_default_password: password + +# different k8s clusters will be shipped with differently named default storage providers +# or none at all. KIND for example has one called 'standard' +console_storage_class: ${storage_class} + + +container_cli: podman \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/infrastructure/ec2-cloud-config.yaml b/full-stack-asset-transfer-guide/infrastructure/ec2-cloud-config.yaml new file mode 100644 index 00000000..62d06c59 --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/ec2-cloud-config.yaml @@ -0,0 +1,59 @@ +#cloud-config + +write_files: + - path: /config/provision-root.sh + permissions: '0744' + content: | + #!/usr/bin/env bash + set -ex + # set -o errexit + # set -o pipefail + + # APT setup for kubectl + curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - + apt-add-repository "deb http://apt.kubernetes.io/ kubernetes-xenial main" + + # Install kubectl + apt-get -y --no-upgrade install kubectl + + # Install yq + YQ_VERSION=4.23.1 + if [ ! -x "/usr/local/bin/yq" ]; then + curl --fail --silent --show-error -L "https://github.com/mikefarah/yq/releases/download/v${YQ_VERSION}/yq_linux_amd64" -o /usr/local/bin/yq + chmod 755 /usr/local/bin/yq + fi + + # Install kind + KIND_VERSION=0.14.0 + if [ ! -x "/usr/local/bin/kind" ]; then + curl --fail --silent --show-error -L "https://kind.sigs.k8s.io/dl/v${KIND_VERSION}/kind-linux-amd64" -o /usr/local/bin/kind + chmod 755 /usr/local/bin/kind + fi + + # Install k9s + K9S_VERSION=0.25.3 + if [ ! -x "/usr/local/bin/k9s" ]; then + curl --fail --silent --show-error -L "https://github.com/derailed/k9s/releases/download/v${K9S_VERSION}/k9s_Linux_x86_64.tar.gz" -o "/tmp/k9s_Linux_x86_64.tar.gz" + tar -zxf "/tmp/k9s_Linux_x86_64.tar.gz" -C /usr/local/bin k9s + chown root:root /usr/local/bin/k9s + chmod 755 /usr/local/bin/k9s + fi + + # Install just + JUST_VERSION=1.2.0 + if [ ! -x "/usr/local/bin/just" ]; then + curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --tag ${JUST_VERSION} --to /usr/local/bin + chown root:root /usr/local/bin/just + chmod 755 /usr/local/bin/just + fi + +runcmd: + - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - + - add-apt-repository "deb [arch=$(dpkg --print-architecture)] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" + - apt-get update -y + - apt-get install -y docker.io + - usermod -a -G docker ubuntu + - apt-get install -y jq + - /config/provision-root.sh + +final_message: "The system is finally up, after $UPTIME seconds" diff --git a/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/00-complete.yml b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/00-complete.yml new file mode 100644 index 00000000..436c5059 --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/00-complete.yml @@ -0,0 +1,22 @@ +--- +- hosts: localhost + vars_files: + - /_cfg/auth-vars.yml + tasks: + - ansible.builtin.debug: + msg: "Running complete Fabric network build {{ api_endpoint}} " + - include_vars: /_cfg/auth-vars.yml + +- ansible.builtin.import_playbook: 01-create-ordering-organization-components.yml +- ansible.builtin.import_playbook: 02-create-endorsing-organization-components.yml +- ansible.builtin.import_playbook: 05-enable-capabilities.yml +- ansible.builtin.import_playbook: 06-add-organization-to-consortium.yml +- ansible.builtin.import_playbook: 09-create-channel.yml +- ansible.builtin.import_playbook: 10-join-peer-to-channel.yml +- ansible.builtin.import_playbook: 11-add-anchor-peer-to-channel.yml +- ansible.builtin.import_playbook: 12-create-endorsing-organization-components.yml +- ansible.builtin.import_playbook: 15-add-organization-to-channel.yml +- ansible.builtin.import_playbook: 17-join-peer-to-channel.yml +- ansible.builtin.import_playbook: 18-add-anchor-peer-to-channel.yml + + diff --git a/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/01-create-ordering-organization-components.yml b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/01-create-ordering-organization-components.yml new file mode 100644 index 00000000..a03d3b8e --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/01-create-ordering-organization-components.yml @@ -0,0 +1,16 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +- name: Create components for an ordering organization + hosts: localhost + vars: + state: present + organization_name: "{{ ordering_org_name }}" + organization_msp_id: "{{ ordering_service_msp }}" + wallet: "/_cfg" + vars_files: + - /_cfg/fabric-common-vars.yml + - /_cfg/fabric-ordering-org-vars.yml + roles: + - ibm.blockchain_platform.ordering_organization diff --git a/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/02-create-endorsing-organization-components.yml b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/02-create-endorsing-organization-components.yml new file mode 100644 index 00000000..4de2abfa --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/02-create-endorsing-organization-components.yml @@ -0,0 +1,18 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +- name: Create components for an endorsing organization + hosts: localhost + vars: + state: present + organization_name: "{{ org1_name }}" + organization_msp_id: "{{ org1_msp_id }}" + ca_name: "{{ org1_ca_name }}" + peer_name: "{{ org1_peer_name }}" + wallet: "/_cfg" + vars_files: + - /_cfg/fabric-common-vars.yml + - /_cfg/fabric-org1-vars.yml + roles: + - ibm.blockchain_platform.endorsing_organization diff --git a/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/05-enable-capabilities.yml b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/05-enable-capabilities.yml new file mode 100644 index 00000000..95cc5339 --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/05-enable-capabilities.yml @@ -0,0 +1,84 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +- name: Enable Fabric v2.x capabilities + hosts: localhost + vars: + wallet: "/_cfg" + vars_files: + - /_cfg/fabric-common-vars.yml + - /_cfg/fabric-ordering-org-vars.yml + tasks: + - name: Get the ordering service information + ibm.blockchain_platform.ordering_service_info: + api_endpoint: "{{ api_endpoint }}" + api_authtype: "{{ api_authtype }}" + api_key: "{{ api_key }}" + api_secret: "{{ api_secret | default(omit) }}" + api_token_endpoint: "{{ api_token_endpoint | default(omit) }}" + name: "{{ ordering_service_name }}" + register: ordering_service + + - name: Fail if the ordering service does not exist + fail: + msg: "{{ ordering_service_name }} does not exist" + when: not ordering_service.exists + + - name: Fetch the system channel configuration + ibm.blockchain_platform.channel_config: + api_endpoint: "{{ api_endpoint }}" + api_authtype: "{{ api_authtype }}" + api_key: "{{ api_key }}" + api_secret: "{{ api_secret | default(omit) }}" + api_token_endpoint: "{{ api_token_endpoint | default(omit) }}" + ordering_service: "{{ ordering_service_name }}" + identity: "{{ wallet }}/{{ ordering_org_name }} Admin.json" + msp_id: "{{ ordering_service_msp }}" + operation: fetch + name: "{{ ordering_service.ordering_service[0].system_channel_id }}" + path: original_config.bin + + - name: Create a copy of the system channel configuration + copy: + src: original_config.bin + dest: updated_config.bin + + - name: Enable Fabric v2.x capabilities + ibm.blockchain_platform.channel_capabilities: + path: updated_config.bin + channel: V2_0 + orderer: V2_0 + + - name: Compute the system channel configuration update + ibm.blockchain_platform.channel_config: + operation: compute_update + name: "{{ ordering_service.ordering_service[0].system_channel_id }}" + original: original_config.bin + updated: updated_config.bin + path: config_update.bin + register: compute_update + + - name: Sign the system channel configuration update + ibm.blockchain_platform.channel_config: + operation: sign_update + identity: "{{ wallet }}/{{ ordering_org_name }} Admin.json" + msp_id: "{{ ordering_service_msp }}" + name: "{{ ordering_service.ordering_service[0].system_channel_id }}" + path: config_update.bin + when: compute_update.path + + - name: Apply the system channel configuration update + ibm.blockchain_platform.channel_config: + api_endpoint: "{{ api_endpoint }}" + api_authtype: "{{ api_authtype }}" + api_key: "{{ api_key }}" + api_secret: "{{ api_secret | default(omit) }}" + api_token_endpoint: "{{ api_token_endpoint | default(omit) }}" + operation: apply_update + ordering_service: "{{ ordering_service_name }}" + identity: "{{ wallet }}/{{ ordering_org_name }} Admin.json" + msp_id: "{{ ordering_service_msp }}" + name: "{{ ordering_service.ordering_service[0].system_channel_id }}" + path: config_update.bin + when: compute_update.path diff --git a/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/06-add-organization-to-consortium.yml b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/06-add-organization-to-consortium.yml new file mode 100644 index 00000000..d38a4fb0 --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/06-add-organization-to-consortium.yml @@ -0,0 +1,89 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +- name: Add the organization to the consortium + hosts: localhost + vars: + wallet: "/_cfg" + vars_files: + - /_cfg/fabric-common-vars.yml + - /_cfg/fabric-ordering-org-vars.yml + tasks: + - name: Get the ordering service information + ibm.blockchain_platform.ordering_service_info: + api_endpoint: "{{ api_endpoint }}" + api_authtype: "{{ api_authtype }}" + api_key: "{{ api_key }}" + api_secret: "{{ api_secret | default(omit) }}" + api_token_endpoint: "{{ api_token_endpoint | default(omit) }}" + name: "{{ ordering_service_name }}" + register: ordering_service + + - name: Fail if the ordering service does not exist + fail: + msg: "{{ ordering_service_name }} does not exist" + when: not ordering_service.exists + + - name: Fetch the system channel configuration + ibm.blockchain_platform.channel_config: + api_endpoint: "{{ api_endpoint }}" + api_authtype: "{{ api_authtype }}" + api_key: "{{ api_key }}" + api_secret: "{{ api_secret | default(omit) }}" + api_token_endpoint: "{{ api_token_endpoint | default(omit) }}" + ordering_service: "{{ ordering_service_name }}" + identity: "{{ wallet }}/{{ ordering_org_name }} Admin.json" + msp_id: "{{ ordering_service_msp }}" + operation: fetch + name: "{{ ordering_service.ordering_service[0].system_channel_id }}" + path: original_config.bin + + - name: Create a copy of the system channel configuration + copy: + src: original_config.bin + dest: updated_config.bin + + - name: Add the organization to the consortium + ibm.blockchain_platform.consortium_member: + state: present + api_endpoint: "{{ api_endpoint }}" + api_authtype: "{{ api_authtype }}" + api_key: "{{ api_key }}" + api_secret: "{{ api_secret | default(omit) }}" + api_token_endpoint: "{{ api_token_endpoint | default(omit) }}" + organization: "{{ org1_name }}" + path: updated_config.bin + + - name: Compute the system channel configuration update + ibm.blockchain_platform.channel_config: + operation: compute_update + name: "{{ ordering_service.ordering_service[0].system_channel_id }}" + original: original_config.bin + updated: updated_config.bin + path: config_update.bin + register: compute_update + + - name: Sign the system channel configuration update + ibm.blockchain_platform.channel_config: + operation: sign_update + identity: "{{ wallet }}/{{ ordering_org_name }} Admin.json" + msp_id: "{{ ordering_service_msp }}" + name: "{{ ordering_service.ordering_service[0].system_channel_id }}" + path: config_update.bin + when: compute_update.path + + - name: Apply the system channel configuration update + ibm.blockchain_platform.channel_config: + api_endpoint: "{{ api_endpoint }}" + api_authtype: "{{ api_authtype }}" + api_key: "{{ api_key }}" + api_secret: "{{ api_secret | default(omit) }}" + api_token_endpoint: "{{ api_token_endpoint | default(omit) }}" + operation: apply_update + ordering_service: "{{ ordering_service_name }}" + identity: "{{ wallet }}/{{ ordering_org_name }} Admin.json" + msp_id: "{{ ordering_service_msp }}" + name: "{{ ordering_service.ordering_service[0].system_channel_id }}" + path: config_update.bin + when: compute_update.path diff --git a/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/09-admins-policy.json.j2 b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/09-admins-policy.json.j2 new file mode 100644 index 00000000..9bb23865 --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/09-admins-policy.json.j2 @@ -0,0 +1,24 @@ +{ + "type": 1, + "value": { + "rule": { + "n_out_of": { + "n": 1, + "rules": [ + { + "signed_by": 0 + } + ] + } + }, + "identities": [ + { + "principal_classification": "ROLE", + "principal": { + "msp_identifier": "{{ org1_msp_id }}", + "role": "ADMIN" + } + } + ] + } +} \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/09-create-channel.yml b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/09-create-channel.yml new file mode 100644 index 00000000..6b4948e8 --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/09-create-channel.yml @@ -0,0 +1,79 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +- name: Create the channel + hosts: localhost + vars: + wallet: "/_cfg" + vars_files: + - /_cfg/fabric-common-vars.yml + - /_cfg/fabric-org1-vars.yml + tasks: + - name: Check to see if the channel already exists + ibm.blockchain_platform.channel_block: + api_endpoint: "{{ api_endpoint }}" + api_authtype: "{{ api_authtype }}" + api_key: "{{ api_key }}" + api_secret: "{{ api_secret | default(omit) }}" + api_token_endpoint: "{{ api_token_endpoint | default(omit) }}" + operation: fetch + ordering_service: "{{ ordering_service_name }}" + identity: "{{ wallet }}/{{ org1_name }} Admin.json" + msp_id: "{{ org1_msp_id }}" + name: "{{ channel_name }}" + target: "0" + path: channel_genesis_block.bin + failed_when: False + register: result + + - name: Fail on any error other than the channel not existing + fail: + msg: "{{ result.msg }}" + when: result.msg is defined and 'NOT_FOUND' not in result.msg + + - name: Create the configuration update for the new channel + ibm.blockchain_platform.channel_config: + api_endpoint: "{{ api_endpoint }}" + api_authtype: "{{ api_authtype }}" + api_key: "{{ api_key }}" + api_secret: "{{ api_secret | default(omit) }}" + api_token_endpoint: "{{ api_token_endpoint | default(omit) }}" + operation: create + name: "{{ channel_name }}" + path: config_update.bin + organizations: + - "{{ org1_name }}" + policies: + Admins: "{{ lookup('template', '09-admins-policy.json.j2') }}" + Readers: "{{ lookup('template', '09-readers-policy.json.j2') }}" + Writers: "{{ lookup('template', '09-writers-policy.json.j2') }}" + Endorsement: "{{ lookup('template', '09-endorsement-policy.json.j2') }}" + LifecycleEndorsement: "{{ lookup('template', '09-lifecycle-endorsement-policy.json.j2') }}" + capabilities: + application: V2_0 + when: result.msg is defined and 'NOT_FOUND' in result.msg + + - name: Sign the channel configuration update for the new channel + ibm.blockchain_platform.channel_config: + operation: sign_update + identity: "{{ wallet }}/{{ org1_name }} Admin.json" + msp_id: "{{ org1_msp_id }}" + name: "{{ channel_name }}" + path: config_update.bin + when: result.msg is defined and 'NOT_FOUND' in result.msg + + - name: Apply the channel configuration update for the new channel + ibm.blockchain_platform.channel_config: + api_endpoint: "{{ api_endpoint }}" + api_authtype: "{{ api_authtype }}" + api_key: "{{ api_key }}" + api_secret: "{{ api_secret | default(omit) }}" + api_token_endpoint: "{{ api_token_endpoint | default(omit) }}" + operation: apply_update + ordering_service: "{{ ordering_service_name }}" + identity: "{{ wallet }}/{{ org1_name }} Admin.json" + msp_id: "{{ org1_msp_id }}" + name: "{{ channel_name }}" + path: config_update.bin + when: result.msg is defined and 'NOT_FOUND' in result.msg diff --git a/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/09-endorsement-policy.json.j2 b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/09-endorsement-policy.json.j2 new file mode 100644 index 00000000..a7b7a1a2 --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/09-endorsement-policy.json.j2 @@ -0,0 +1,24 @@ +{ + "type": 1, + "value": { + "rule": { + "n_out_of": { + "n": 1, + "rules": [ + { + "signed_by": 0 + } + ] + } + }, + "identities": [ + { + "principal_classification": "ROLE", + "principal": { + "msp_identifier": "{{ org1_msp_id }}", + "role": "MEMBER" + } + } + ] + } +} \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/09-lifecycle-endorsement-policy.json.j2 b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/09-lifecycle-endorsement-policy.json.j2 new file mode 100644 index 00000000..a7b7a1a2 --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/09-lifecycle-endorsement-policy.json.j2 @@ -0,0 +1,24 @@ +{ + "type": 1, + "value": { + "rule": { + "n_out_of": { + "n": 1, + "rules": [ + { + "signed_by": 0 + } + ] + } + }, + "identities": [ + { + "principal_classification": "ROLE", + "principal": { + "msp_identifier": "{{ org1_msp_id }}", + "role": "MEMBER" + } + } + ] + } +} \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/09-readers-policy.json.j2 b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/09-readers-policy.json.j2 new file mode 100644 index 00000000..a7b7a1a2 --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/09-readers-policy.json.j2 @@ -0,0 +1,24 @@ +{ + "type": 1, + "value": { + "rule": { + "n_out_of": { + "n": 1, + "rules": [ + { + "signed_by": 0 + } + ] + } + }, + "identities": [ + { + "principal_classification": "ROLE", + "principal": { + "msp_identifier": "{{ org1_msp_id }}", + "role": "MEMBER" + } + } + ] + } +} \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/09-writers-policy.json.j2 b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/09-writers-policy.json.j2 new file mode 100644 index 00000000..a7b7a1a2 --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/09-writers-policy.json.j2 @@ -0,0 +1,24 @@ +{ + "type": 1, + "value": { + "rule": { + "n_out_of": { + "n": 1, + "rules": [ + { + "signed_by": 0 + } + ] + } + }, + "identities": [ + { + "principal_classification": "ROLE", + "principal": { + "msp_identifier": "{{ org1_msp_id }}", + "role": "MEMBER" + } + } + ] + } +} \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/10-join-peer-to-channel.yml b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/10-join-peer-to-channel.yml new file mode 100644 index 00000000..d8c48bbc --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/10-join-peer-to-channel.yml @@ -0,0 +1,39 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +- name: Join the channel + hosts: localhost + vars: + wallet: "/_cfg" + vars_files: + - /_cfg/fabric-common-vars.yml + - /_cfg/fabric-org1-vars.yml + tasks: + - name: Fetch the genesis block for the channel + ibm.blockchain_platform.channel_block: + api_endpoint: "{{ api_endpoint }}" + api_authtype: "{{ api_authtype }}" + api_key: "{{ api_key }}" + api_secret: "{{ api_secret | default(omit) }}" + api_token_endpoint: "{{ api_token_endpoint | default(omit) }}" + operation: fetch + ordering_service: "{{ ordering_service_name }}" + identity: "{{ wallet }}/{{ org1_name }} Admin.json" + msp_id: "{{ org1_msp_id }}" + name: "{{ channel_name }}" + target: "0" + path: channel_genesis_block.bin + + - name: Join the peer to the channel + ibm.blockchain_platform.peer_channel: + api_endpoint: "{{ api_endpoint }}" + api_authtype: "{{ api_authtype }}" + api_key: "{{ api_key }}" + api_secret: "{{ api_secret | default(omit) }}" + api_token_endpoint: "{{ api_token_endpoint | default(omit) }}" + operation: join + peer: "{{ org1_peer_name }}" + identity: "{{ wallet }}/{{ org1_name }} Admin.json" + msp_id: "{{ org1_msp_id }}" + path: channel_genesis_block.bin diff --git a/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/11-add-anchor-peer-to-channel.yml b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/11-add-anchor-peer-to-channel.yml new file mode 100644 index 00000000..06cd1bb8 --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/11-add-anchor-peer-to-channel.yml @@ -0,0 +1,91 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +- name: Add the anchor peer to the channel + hosts: localhost + vars: + wallet: "/_cfg" + vars_files: + - /_cfg/fabric-common-vars.yml + - /_cfg/fabric-org1-vars.yml + tasks: + - name: Get the ordering service information + ibm.blockchain_platform.ordering_service_info: + api_endpoint: "{{ api_endpoint }}" + api_authtype: "{{ api_authtype }}" + api_key: "{{ api_key }}" + api_secret: "{{ api_secret | default(omit) }}" + api_token_endpoint: "{{ api_token_endpoint | default(omit) }}" + name: "{{ ordering_service_name }}" + register: ordering_service + + - name: Fail if the ordering service does not exist + fail: + msg: "{{ ordering_service_name }} does not exist" + when: not ordering_service.exists + + - name: Fetch the channel configuration + ibm.blockchain_platform.channel_config: + api_endpoint: "{{ api_endpoint }}" + api_authtype: "{{ api_authtype }}" + api_key: "{{ api_key }}" + api_secret: "{{ api_secret | default(omit) }}" + api_token_endpoint: "{{ api_token_endpoint | default(omit) }}" + ordering_service: "{{ ordering_service_name }}" + identity: "{{ wallet }}/{{ org1_name }} Admin.json" + msp_id: "{{ org1_msp_id }}" + operation: fetch + name: "{{ channel_name }}" + path: original_config.bin + + - name: Create a copy of the channel configuration + copy: + src: original_config.bin + dest: updated_config.bin + + - name: Update the organization + ibm.blockchain_platform.channel_member: + state: present + api_endpoint: "{{ api_endpoint }}" + api_authtype: "{{ api_authtype }}" + api_key: "{{ api_key }}" + api_secret: "{{ api_secret | default(omit) }}" + api_token_endpoint: "{{ api_token_endpoint | default(omit) }}" + organization: "{{ org1_name }}" + anchor_peers: + - "{{ org1_peer_name }}" + path: updated_config.bin + + - name: Compute the channel configuration update + ibm.blockchain_platform.channel_config: + operation: compute_update + name: "{{ channel_name }}" + original: original_config.bin + updated: updated_config.bin + path: config_update.bin + register: compute_update + + - name: Sign the channel configuration update + ibm.blockchain_platform.channel_config: + operation: sign_update + identity: "{{ wallet }}/{{ org1_name }} Admin.json" + msp_id: "{{ org1_msp_id }}" + name: "{{ channel_name }}" + path: config_update.bin + when: compute_update.path + + - name: Apply the channel configuration update + ibm.blockchain_platform.channel_config: + api_endpoint: "{{ api_endpoint }}" + api_authtype: "{{ api_authtype }}" + api_key: "{{ api_key }}" + api_secret: "{{ api_secret | default(omit) }}" + api_token_endpoint: "{{ api_token_endpoint | default(omit) }}" + operation: apply_update + ordering_service: "{{ ordering_service_name }}" + identity: "{{ wallet }}/{{ org1_name }} Admin.json" + msp_id: "{{ org1_msp_id }}" + name: "{{ channel_name }}" + path: config_update.bin + when: compute_update.path diff --git a/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/12-create-endorsing-organization-components.yml b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/12-create-endorsing-organization-components.yml new file mode 100644 index 00000000..8ca33daa --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/12-create-endorsing-organization-components.yml @@ -0,0 +1,18 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +- name: Create components for an endorsing organization + hosts: localhost + vars: + state: present + organization_name: "{{ org2_name }}" + organization_msp_id: "{{ org2_msp_id }}" + ca_name: "{{ org2_ca_name }}" + peer_name: "{{ org2_peer_name }}" + wallet: "/_cfg" + vars_files: + - /_cfg/fabric-common-vars.yml + - /_cfg/fabric-org2-vars.yml + roles: + - ibm.blockchain_platform.endorsing_organization diff --git a/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/15-add-organization-to-channel.yml b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/15-add-organization-to-channel.yml new file mode 100644 index 00000000..278bd961 --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/15-add-organization-to-channel.yml @@ -0,0 +1,124 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +- name: Add the organization to the channel + hosts: localhost + vars: + wallet: "/_cfg" + vars_files: + - /_cfg/fabric-common-vars.yml + - /_cfg/fabric-org1-vars.yml + tasks: + - name: Get the ordering service information + ibm.blockchain_platform.ordering_service_info: + api_endpoint: "{{ api_endpoint }}" + api_authtype: "{{ api_authtype }}" + api_key: "{{ api_key }}" + api_secret: "{{ api_secret | default(omit) }}" + api_token_endpoint: "{{ api_token_endpoint | default(omit) }}" + name: "{{ ordering_service_name }}" + register: ordering_service + + - name: Fail if the ordering service does not exist + fail: + msg: "{{ ordering_service_name }} does not exist" + when: not ordering_service.exists + + - name: Fetch the channel configuration + ibm.blockchain_platform.channel_config: + api_endpoint: "{{ api_endpoint }}" + api_authtype: "{{ api_authtype }}" + api_key: "{{ api_key }}" + api_secret: "{{ api_secret | default(omit) }}" + api_token_endpoint: "{{ api_token_endpoint | default(omit) }}" + ordering_service: "{{ ordering_service_name }}" + identity: "{{ wallet }}/{{ org1_name }} Admin.json" + msp_id: "{{ org1_msp_id }}" + operation: fetch + name: "{{ channel_name }}" + path: original_config.bin + + - name: Create a copy of the channel configuration + copy: + src: original_config.bin + dest: updated_config.bin + + - name: Add the organization to the channel + ibm.blockchain_platform.channel_member: + state: present + api_endpoint: "{{ api_endpoint }}" + api_authtype: "{{ api_authtype }}" + api_key: "{{ api_key }}" + api_secret: "{{ api_secret | default(omit) }}" + api_token_endpoint: "{{ api_token_endpoint | default(omit) }}" + organization: "{{ org2_name }}" + path: updated_config.bin + + - name: Update the channel admins policy + ibm.blockchain_platform.channel_policy: + state: present + path: updated_config.bin + name: Admins + policy: "{{ lookup('template', '15-admins-policy.json.j2') }}" + + - name: Update the channel readers policy + ibm.blockchain_platform.channel_policy: + state: present + path: updated_config.bin + name: Readers + policy: "{{ lookup('template', '15-readers-policy.json.j2') }}" + + - name: Update the channel writers policy + ibm.blockchain_platform.channel_policy: + state: present + path: updated_config.bin + name: Writers + policy: "{{ lookup('template', '15-writers-policy.json.j2') }}" + + - name: Update the channel endorsement policy + ibm.blockchain_platform.channel_policy: + state: present + path: updated_config.bin + name: Endorsement + policy: "{{ lookup('template', '15-endorsement-policy.json.j2') }}" + + - name: Update the channel lifecycle endorsement policy + ibm.blockchain_platform.channel_policy: + state: present + path: updated_config.bin + name: LifecycleEndorsement + policy: "{{ lookup('template', '15-lifecycle-endorsement-policy.json.j2') }}" + + - name: Compute the channel configuration update + ibm.blockchain_platform.channel_config: + operation: compute_update + name: "{{ channel_name }}" + original: original_config.bin + updated: updated_config.bin + path: config_update.bin + register: compute_update + + - name: Sign the channel configuration update + ibm.blockchain_platform.channel_config: + operation: sign_update + identity: "{{ wallet }}/{{ org1_name }} Admin.json" + msp_id: "{{ org1_msp_id }}" + name: "{{ channel_name }}" + path: config_update.bin + when: compute_update.path + + - name: Apply the channel configuration update + ibm.blockchain_platform.channel_config: + api_endpoint: "{{ api_endpoint }}" + api_authtype: "{{ api_authtype }}" + api_key: "{{ api_key }}" + api_secret: "{{ api_secret | default(omit) }}" + api_token_endpoint: "{{ api_token_endpoint | default(omit) }}" + operation: apply_update + ordering_service: "{{ ordering_service_name }}" + identity: "{{ wallet }}/{{ org1_name }} Admin.json" + msp_id: "{{ org1_msp_id }}" + name: "{{ channel_name }}" + path: config_update.bin + when: compute_update.path diff --git a/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/15-admins-policy.json.j2 b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/15-admins-policy.json.j2 new file mode 100644 index 00000000..3012d99a --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/15-admins-policy.json.j2 @@ -0,0 +1,34 @@ +{ + "type": 1, + "value": { + "rule": { + "n_out_of": { + "n": 2, + "rules": [ + { + "signed_by": 0 + }, + { + "signed_by": 1 + } + ] + } + }, + "identities": [ + { + "principal_classification": "ROLE", + "principal": { + "msp_identifier": "{{ org1_msp_id }}", + "role": "ADMIN" + } + }, + { + "principal_classification": "ROLE", + "principal": { + "msp_identifier": "{{ org2_msp_id }}", + "role": "ADMIN" + } + } + ] + } +} \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/15-endorsement-policy.json.j2 b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/15-endorsement-policy.json.j2 new file mode 100644 index 00000000..7d4a1611 --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/15-endorsement-policy.json.j2 @@ -0,0 +1,34 @@ +{ + "type": 1, + "value": { + "rule": { + "n_out_of": { + "n": 2, + "rules": [ + { + "signed_by": 0 + }, + { + "signed_by": 1 + } + ] + } + }, + "identities": [ + { + "principal_classification": "ROLE", + "principal": { + "msp_identifier": "{{ org1_msp_id }}", + "role": "MEMBER" + } + }, + { + "principal_classification": "ROLE", + "principal": { + "msp_identifier": "{{ org2_msp_id }}", + "role": "MEMBER" + } + } + ] + } +} \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/15-lifecycle-endorsement-policy.json.j2 b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/15-lifecycle-endorsement-policy.json.j2 new file mode 100644 index 00000000..7d4a1611 --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/15-lifecycle-endorsement-policy.json.j2 @@ -0,0 +1,34 @@ +{ + "type": 1, + "value": { + "rule": { + "n_out_of": { + "n": 2, + "rules": [ + { + "signed_by": 0 + }, + { + "signed_by": 1 + } + ] + } + }, + "identities": [ + { + "principal_classification": "ROLE", + "principal": { + "msp_identifier": "{{ org1_msp_id }}", + "role": "MEMBER" + } + }, + { + "principal_classification": "ROLE", + "principal": { + "msp_identifier": "{{ org2_msp_id }}", + "role": "MEMBER" + } + } + ] + } +} \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/15-readers-policy.json.j2 b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/15-readers-policy.json.j2 new file mode 100644 index 00000000..beb0596a --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/15-readers-policy.json.j2 @@ -0,0 +1,34 @@ +{ + "type": 1, + "value": { + "rule": { + "n_out_of": { + "n": 1, + "rules": [ + { + "signed_by": 0 + }, + { + "signed_by": 1 + } + ] + } + }, + "identities": [ + { + "principal_classification": "ROLE", + "principal": { + "msp_identifier": "{{ org1_msp_id }}", + "role": "MEMBER" + } + }, + { + "principal_classification": "ROLE", + "principal": { + "msp_identifier": "{{ org2_msp_id }}", + "role": "MEMBER" + } + } + ] + } +} \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/15-writers-policy.json.j2 b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/15-writers-policy.json.j2 new file mode 100644 index 00000000..beb0596a --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/15-writers-policy.json.j2 @@ -0,0 +1,34 @@ +{ + "type": 1, + "value": { + "rule": { + "n_out_of": { + "n": 1, + "rules": [ + { + "signed_by": 0 + }, + { + "signed_by": 1 + } + ] + } + }, + "identities": [ + { + "principal_classification": "ROLE", + "principal": { + "msp_identifier": "{{ org1_msp_id }}", + "role": "MEMBER" + } + }, + { + "principal_classification": "ROLE", + "principal": { + "msp_identifier": "{{ org2_msp_id }}", + "role": "MEMBER" + } + } + ] + } +} \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/16-import-ordering-service.yml b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/16-import-ordering-service.yml new file mode 100644 index 00000000..b5228b9f --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/16-import-ordering-service.yml @@ -0,0 +1,18 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +- name: Import the ordering service + hosts: localhost + vars_files: + - /_cfg/fabric-common-vars.yml + - /_cfg/fabric-org2-vars.yml + tasks: + - name: Import the ordering service + ibm.blockchain_platform.external_ordering_service: + api_endpoint: "{{ api_endpoint }}" + api_authtype: "{{ api_authtype }}" + api_key: "{{ api_key }}" + api_secret: "{{ api_secret | default(omit) }}" + api_token_endpoint: "{{ api_token_endpoint | default(omit) }}" + ordering_service: "{{ lookup('file', ordering_service_name+'.json') }}" diff --git a/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/17-join-peer-to-channel.yml b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/17-join-peer-to-channel.yml new file mode 100644 index 00000000..fd6af27c --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/17-join-peer-to-channel.yml @@ -0,0 +1,39 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +- name: Join the channel + hosts: localhost + vars: + wallet: "/_cfg" + vars_files: + - /_cfg/fabric-common-vars.yml + - /_cfg/fabric-org2-vars.yml + tasks: + - name: Fetch the genesis block for the channel + ibm.blockchain_platform.channel_block: + api_endpoint: "{{ api_endpoint }}" + api_authtype: "{{ api_authtype }}" + api_key: "{{ api_key }}" + api_secret: "{{ api_secret | default(omit) }}" + api_token_endpoint: "{{ api_token_endpoint | default(omit) }}" + operation: fetch + ordering_service: "{{ ordering_service_name }}" + identity: "{{ wallet }}/{{ org2_name }} Admin.json" + msp_id: "{{ org2_msp_id }}" + name: "{{ channel_name }}" + target: "0" + path: channel_genesis_block.bin + + - name: Join the peer to the channel + ibm.blockchain_platform.peer_channel: + api_endpoint: "{{ api_endpoint }}" + api_authtype: "{{ api_authtype }}" + api_key: "{{ api_key }}" + api_secret: "{{ api_secret | default(omit) }}" + api_token_endpoint: "{{ api_token_endpoint | default(omit) }}" + operation: join + peer: "{{ org2_peer_name }}" + identity: "{{ wallet }}/{{ org2_name }} Admin.json" + msp_id: "{{ org2_msp_id }}" + path: channel_genesis_block.bin diff --git a/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/18-add-anchor-peer-to-channel.yml b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/18-add-anchor-peer-to-channel.yml new file mode 100644 index 00000000..4ca0908f --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/fabric_network_playbooks/18-add-anchor-peer-to-channel.yml @@ -0,0 +1,91 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +- name: Add the anchor peer to the channel + hosts: localhost + vars: + wallet: "/_cfg" + vars_files: + - /_cfg/fabric-common-vars.yml + - /_cfg/fabric-org2-vars.yml + tasks: + - name: Get the ordering service information + ibm.blockchain_platform.ordering_service_info: + api_endpoint: "{{ api_endpoint }}" + api_authtype: "{{ api_authtype }}" + api_key: "{{ api_key }}" + api_secret: "{{ api_secret | default(omit) }}" + api_token_endpoint: "{{ api_token_endpoint | default(omit) }}" + name: "{{ ordering_service_name }}" + register: ordering_service + + - name: Fail if the ordering service does not exist + fail: + msg: "{{ ordering_service_name }} does not exist" + when: not ordering_service.exists + + - name: Fetch the channel configuration + ibm.blockchain_platform.channel_config: + api_endpoint: "{{ api_endpoint }}" + api_authtype: "{{ api_authtype }}" + api_key: "{{ api_key }}" + api_secret: "{{ api_secret | default(omit) }}" + api_token_endpoint: "{{ api_token_endpoint | default(omit) }}" + ordering_service: "{{ ordering_service_name }}" + identity: "{{ wallet }}/{{ org2_name }} Admin.json" + msp_id: "{{ org2_msp_id }}" + operation: fetch + name: "{{ channel_name }}" + path: original_config.bin + + - name: Create a copy of the channel configuration + copy: + src: original_config.bin + dest: updated_config.bin + + - name: Update the organization + ibm.blockchain_platform.channel_member: + state: present + api_endpoint: "{{ api_endpoint }}" + api_authtype: "{{ api_authtype }}" + api_key: "{{ api_key }}" + api_secret: "{{ api_secret | default(omit) }}" + api_token_endpoint: "{{ api_token_endpoint | default(omit) }}" + organization: "{{ org2_name }}" + anchor_peers: + - "{{ org2_peer_name }}" + path: updated_config.bin + + - name: Compute the channel configuration update + ibm.blockchain_platform.channel_config: + operation: compute_update + name: "{{ channel_name }}" + original: original_config.bin + updated: updated_config.bin + path: config_update.bin + register: compute_update + + - name: Sign the channel configuration update + ibm.blockchain_platform.channel_config: + operation: sign_update + identity: "{{ wallet }}/{{ org2_name }} Admin.json" + msp_id: "{{ org2_msp_id }}" + name: "{{ channel_name }}" + path: config_update.bin + when: compute_update.path + + - name: Apply the channel configuration update + ibm.blockchain_platform.channel_config: + api_endpoint: "{{ api_endpoint }}" + api_authtype: "{{ api_authtype }}" + api_key: "{{ api_key }}" + api_secret: "{{ api_secret | default(omit) }}" + api_token_endpoint: "{{ api_token_endpoint | default(omit) }}" + operation: apply_update + ordering_service: "{{ ordering_service_name }}" + identity: "{{ wallet }}/{{ org2_name }} Admin.json" + msp_id: "{{ org2_msp_id }}" + name: "{{ channel_name }}" + path: config_update.bin + when: compute_update.path diff --git a/full-stack-asset-transfer-guide/infrastructure/kind_console_ingress/90-KIND-ingress.yml b/full-stack-asset-transfer-guide/infrastructure/kind_console_ingress/90-KIND-ingress.yml new file mode 100644 index 00000000..257c742c --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/kind_console_ingress/90-KIND-ingress.yml @@ -0,0 +1,37 @@ +--- +- name: Setup ingress for KIND for use with Fabric Operator/Console + hosts: localhost + tasks: + - name: Create kubernetes resources for the ingress + k8s: + definition: "{{ lookup('kubernetes.core.kustomize', dir='templates/ingress') }}" + register: resultingress + + - name: Wait for the ingress + command: kubectl wait --namespace ingress-nginx --for=condition=ready pod --selector=app.kubernetes.io/component=controller --timeout=2m + changed_when: false + + # Override the cluster DNS with a local override to refer pods to the HOST interface + # when connecting to ingress. + - name: Need the cluster ip address + k8s_info: + api_version: v1 + kind: service + namespace: ingress-nginx + name: "ingress-nginx-controller" + register: ingress_info + + - name: Applying CoreDNS overrides for ingress domain + vars: + clusterip: "{{ ingress_info.resources[0].spec.clusterIP }}" + k8s: + state: present + namespace: kube-system + resource_definition: "{{ lookup('template','templates/coredns/coredns.yaml.j2') }}" + apply: yes + + - name: Rollout the CoreDNS + shell: | + kubectl -n kube-system rollout restart deployment/coredns + kubectl wait --namespace ingress-nginx --for=condition=ready pod --selector=app.kubernetes.io/component=controller --timeout=2m + changed_when: false diff --git a/full-stack-asset-transfer-guide/infrastructure/kind_console_ingress/templates/coredns/coredns.yaml.j2 b/full-stack-asset-transfer-guide/infrastructure/kind_console_ingress/templates/coredns/coredns.yaml.j2 new file mode 100644 index 00000000..59065d96 --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/kind_console_ingress/templates/coredns/coredns.yaml.j2 @@ -0,0 +1,33 @@ +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: coredns + namespace: kube-system +data: + Corefile: | + .:53 { + errors + health { + lameduck 5s + } + rewrite name regex (.*)\.localho\.st host.ingress.internal + hosts { + {{ clusterip }} host.ingress.internal + fallthrough + } + ready + kubernetes cluster.local in-addr.arpa ip6.arpa { + pods insecure + fallthrough in-addr.arpa ip6.arpa + ttl 30 + } + prometheus :9153 + forward . /etc/resolv.conf { + max_concurrent 1000 + } + cache 30 + loop + reload + loadbalance + } \ No newline at end of file diff --git a/full-stack-asset-transfer-guide/infrastructure/kind_console_ingress/templates/ingress/ingress-nginx-controller.yaml b/full-stack-asset-transfer-guide/infrastructure/kind_console_ingress/templates/ingress/ingress-nginx-controller.yaml new file mode 100644 index 00000000..72b7feed --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/kind_console_ingress/templates/ingress/ingress-nginx-controller.yaml @@ -0,0 +1,39 @@ +# +# Copyright contributors to the Hyperledger Fabric Operator project +# +# SPDX-License-Identifier: Apache-2.0 +# +# 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: +# +# http://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. +# +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + namespace: ingress-nginx + name: ingress-nginx-controller +spec: + template: + spec: + containers: + - name: controller + args: + - /nginx-ingress-controller + - --publish-service=$(POD_NAMESPACE)/ingress-nginx-controller + - --election-id=ingress-controller-leader + - --controller-class=k8s.io/ingress-nginx + - --ingress-class=nginx + - --configmap=$(POD_NAMESPACE)/ingress-nginx-controller + - --validating-webhook=:8443 + - --validating-webhook-certificate=/usr/local/certificates/cert + - --validating-webhook-key=/usr/local/certificates/key + - --enable-ssl-passthrough diff --git a/full-stack-asset-transfer-guide/infrastructure/kind_console_ingress/templates/ingress/kustomization.yaml b/full-stack-asset-transfer-guide/infrastructure/kind_console_ingress/templates/ingress/kustomization.yaml new file mode 100644 index 00000000..6d57058b --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/kind_console_ingress/templates/ingress/kustomization.yaml @@ -0,0 +1,37 @@ +# +# Copyright contributors to the Hyperledger Fabric Operator project +# +# SPDX-License-Identifier: Apache-2.0 +# +# 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: +# +# http://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. +# +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - https://github.com/kubernetes/ingress-nginx.git/deploy/static/provider/cloud?ref=controller-v1.1.2 + +patchesStrategicMerge: + - ingress-nginx-controller.yaml + +# Remove the port `appProtocol` attribute as this is not accepted by all cloud providers +patchesJson6902: + - target: + kind: Service + name: ingress-nginx-controller + version: v1 + patch: |- + - op: remove + path: "/spec/ports/0/appProtocol" + - op: remove + path: "/spec/ports/1/appProtocol" diff --git a/full-stack-asset-transfer-guide/infrastructure/kind_with_nginx.sh b/full-stack-asset-transfer-guide/infrastructure/kind_with_nginx.sh new file mode 100755 index 00000000..3b556a22 --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/kind_with_nginx.sh @@ -0,0 +1,191 @@ +#!/bin/bash +# +# Copyright contributors to the Hyperledgendary Full Stack Asset Transfer project +# +# SPDX-License-Identifier: Apache-2.0 +# +# 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: +# +# http://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. +# + +set -eo pipefail +set -x + +KIND_CLUSTER_NAME=kind +KIND_CLUSTER_IMAGE=${KIND_CLUSTER_IMAGE:-kindest/node:v1.24.4} # Important! k8s v1.25.0 brings breaking changes. +KIND_API_SERVER_ADDRESS=${KIND_API_SERVER_ADDRESS:-127.0.0.1} +KIND_API_SERVER_PORT=${KIND_API_SERVER_PORT:-8888} +CONTAINER_REGISTRY_NAME=${CONTAINER_REGISTRY_NAME:-kind-registry} +CONTAINER_REGISTRY_ADDRESS=${CONTAINER_REGISTRY_ADDRESS:-127.0.0.1} +CONTAINER_REGISTRY_PORT=${CONTAINER_REGISTRY_PORT:-5000} + +function kind_with_nginx() { + + delete_cluster + + create_cluster + + start_nginx + + apply_coredns_override + + launch_docker_registry +} + +# +# Delete a kind cluster if it exists +# +function delete_cluster() { + kind delete cluster --name $KIND_CLUSTER_NAME +} + +# +# Create a local KIND cluster +# +function create_cluster() { + cat << EOF | kind create cluster --name $KIND_CLUSTER_NAME --image $KIND_CLUSTER_IMAGE --config=- +--- +kind: Cluster +apiVersion: kind.x-k8s.io/v1alpha4 +nodes: + - role: control-plane + kubeadmConfigPatches: + - | + kind: InitConfiguration + nodeRegistration: + kubeletExtraArgs: + node-labels: "ingress-ready=true" + extraPortMappings: + - containerPort: 80 + hostPort: 80 + protocol: TCP + - containerPort: 443 + hostPort: 443 + protocol: TCP +networking: + apiServerAddress: ${KIND_API_SERVER_ADDRESS} + apiServerPort: ${KIND_API_SERVER_PORT} + +# create a cluster with the local registry enabled in containerd +containerdConfigPatches: +- |- + [plugins."io.containerd.grpc.v1.cri".registry.mirrors."localhost:${CONTAINER_REGISTRY_PORT}"] + endpoint = ["http://${CONTAINER_REGISTRY_NAME}:${CONTAINER_REGISTRY_PORT}"] +EOF + + # + # Work around a bug in KIND where DNS is not always resolved correctly on machines with IPv6 + # + for node in $(kind get nodes); + do + docker exec "$node" sysctl net.ipv4.conf.all.route_localnet=1; + done +} + +# +# Install an Nginx ingress controller bound to port 80 and 443. +# ssl_passthrough mode is enabled for TLS termination at the Fabric node enpdoints. +# +function start_nginx() { + kubectl apply -k https://github.com/hyperledger-labs/fabric-operator.git/config/ingress/kind + + sleep 10 + + kubectl wait --namespace ingress-nginx \ + --for=condition=ready pod \ + --selector=app.kubernetes.io/component=controller \ + --timeout=3m +} + +# +# Override Core DNS with a wildcard matcher for the "*.localho.st" domain, binding to the +# IP address of the Nginx ingress controller on the kubernetes internal network. Effectively this +# "steals" the domain name for *.localho.st, directing traffic to the Nginx load balancer, rather +# than to the loopback interface at 127.0.0.1. +# +function apply_coredns_override() { + CLUSTER_IP=$(kubectl -n ingress-nginx get svc ingress-nginx-controller -o json | jq -r .spec.clusterIP) + + cat << EOF | kubectl apply -f - +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: coredns + namespace: kube-system +data: + Corefile: | + .:53 { + errors + health { + lameduck 5s + } + ready + rewrite name regex (.*)\.localho\.st host.ingress.internal + hosts { + ${CLUSTER_IP} host.ingress.internal + fallthrough + } + kubernetes cluster.local in-addr.arpa ip6.arpa { + pods insecure + fallthrough in-addr.arpa ip6.arpa + ttl 30 + } + prometheus :9153 + forward . /etc/resolv.conf { + max_concurrent 1000 + } + cache 30 + loop + reload + loadbalance + } +EOF + + kubectl -n kube-system rollout restart deployment/coredns +} + +function launch_docker_registry() { + + # create registry container unless it already exists + running="$(docker inspect -f '{{.State.Running}}' "${reg_name}" 2>/dev/null || true)" + if [ "${running}" != 'true' ]; then + docker run \ + --detach \ + --restart always \ + --name "${CONTAINER_REGISTRY_NAME}" \ + --publish "${CONTAINER_REGISTRY_ADDRESS}:${CONTAINER_REGISTRY_PORT}:${CONTAINER_REGISTRY_PORT}" \ + registry:2 + fi + + # connect the registry to the cluster network + # (the network may already be connected) + docker network connect "kind" "${CONTAINER_REGISTRY_NAME}" || true + + # Document the local registry + # https://github.com/kubernetes/enhancements/tree/master/keps/sig-cluster-lifecycle/generic/1755-communicating-a-local-registry + cat < + + +pacakge_update: true +package_upgrade: true +packages: + - jq + - curl + - software-properties-common + +write_files: +- path: /config/provision-root.sh + permissions: '0744' + content: | + #!/usr/bin/env bash + set -ex + # set -o errexit + # set -o pipefail + + if [ -z $1 ]; then + HLF_VERSION=2.2.0 + else + HLF_VERSION=$1 + fi + + if [ ${HLF_VERSION:0:4} = '2.4.' ]; then + export GO_VERSION=1.17.10 + elif [ ${HLF_VERSION:0:4} = '2.2.' -o ${HLF_VERSION:0:4} = '2.3.' ]; then + export GO_VERSION=1.14.11 + elif [ ${HLF_VERSION:0:4} = '2.0.' -o ${HLF_VERSION:0:4} = '2.1.' ]; then + export GO_VERSION=1.13.15 + elif [ ${HLF_VERSION:0:4} = '1.2.' -o ${HLF_VERSION:0:4} = '1.3.' -o ${HLF_VERSION:0:4} = '1.4.' ]; then + export GO_VERSION=1.10.4 + elif [ ${HLF_VERSION:0:4} = '1.1.' ]; then + export GO_VERSION=1.9.7 + else + >&2 echo "Unexpected HLF_VERSION ${HLF_VERSION}" + >&2 echo "HLF_VERSION must be a 1.1.x, 1.2.x, 1.3.x, 1.4.x, 2.0.x, 2.1.x, 2.2.x, 2.3.x, or 2.4.x version" + exit 1 + fi + + + # APT setup for kubectl + curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - + apt-add-repository "deb http://apt.kubernetes.io/ kubernetes-xenial main" + + # Install kubectl + apt-get -y --no-upgrade install kubectl + + # Install yq + YQ_VERSION=4.23.1 + if [ ! -x "/usr/local/bin/yq" ]; then + curl --fail --silent --show-error -L "https://github.com/mikefarah/yq/releases/download/v${YQ_VERSION}/yq_linux_amd64" -o /usr/local/bin/yq + chmod 755 /usr/local/bin/yq + fi + + # Install docker compose + if [ ! -x /usr/local/bin/docker-compose ]; then + curl --fail --silent --show-error -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose + chmod 755 /usr/local/bin/docker-compose + fi + + # Install kind + KIND_VERSION=0.14.0 + if [ ! -x "/usr/local/bin/kind" ]; then + curl --fail --silent --show-error -L "https://kind.sigs.k8s.io/dl/v${KIND_VERSION}/kind-linux-amd64" -o /usr/local/bin/kind + chmod 755 /usr/local/bin/kind + fi + + # Install k9s + K9S_VERSION=0.25.3 + if [ ! -x "/usr/local/bin/k9s" ]; then + curl --fail --silent --show-error -L "https://github.com/derailed/k9s/releases/download/v${K9S_VERSION}/k9s_Linux_x86_64.tar.gz" -o "/tmp/k9s_Linux_x86_64.tar.gz" + tar -zxf "/tmp/k9s_Linux_x86_64.tar.gz" -C /usr/local/bin k9s + chown root:root /usr/local/bin/k9s + chmod 755 /usr/local/bin/k9s + fi + + # Install ccmetadata + CCMETADATA_VERSION=0.2.0 + if [ ! -x "/usr/local/bin/ccmetadata" ]; then + curl --fail --silent --show-error -L "https://github.com/hyperledgendary/ccmetadata/releases/download/v${CCMETADATA_VERSION}/ccmetadata-Linux-X64.tgz" -o "/tmp/ccmetadata-Linux-X64.tgz" + tar -zxf "/tmp/ccmetadata-Linux-X64.tgz" -C /usr/local/bin ccmetadata + chown root:root /usr/local/bin/ccmetadata + chmod 755 /usr/local/bin/ccmetadata + fi + + # Install just + JUST_VERSION=1.2.0 + if [ ! -x "/usr/local/bin/just" ]; then + curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --tag ${JUST_VERSION} --to /usr/local/bin + chown root:root /usr/local/bin/just + chmod 755 /usr/local/bin/just + fi + +- path: /config/provision-user.sh + permissions: '0777' + owner: ubuntu:ubuntu + content: | + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] || curl --fail --silent --show-error -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.3/install.sh | bash + . "$NVM_DIR/nvm.sh" + + # Install latest node v16.x, latest typescript, weft + nvm install 16 + npm install -g typescript + npm install -g @hyperledger-labs/weft + +# Use Google DNS as the mac resolvers are not 100% reliable for the npm dependency builds in Docker +bootcmd: + - printf "[Resolve]\nDNS=8.8.8.8" > /etc/systemd/resolved.conf + - [systemctl, restart, systemd-resolved] + +runcmd: + - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - + - add-apt-repository "deb [arch=$(dpkg --print-architecture)] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" + - apt-get update -y + - apt-get install -y docker.io + - usermod -a -G docker ubuntu + - /config/provision-root.sh + - su -c /config/provision-user.sh ubuntu + +final_message: "The system is finally up, after $UPTIME seconds" diff --git a/full-stack-asset-transfer-guide/infrastructure/operator_console_playbooks/01-operator-install.yml b/full-stack-asset-transfer-guide/infrastructure/operator_console_playbooks/01-operator-install.yml new file mode 100644 index 00000000..256f98b9 --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/operator_console_playbooks/01-operator-install.yml @@ -0,0 +1,13 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +- name: Deploy Opensource custom resource definitions and operator + hosts: localhost + vars_files: + - /_cfg/operator-console-vars.yml + vars: + state: present + wait_timeout: 3600 + roles: + - ibm.blockchain_platform.fabric_operator_crds diff --git a/full-stack-asset-transfer-guide/infrastructure/operator_console_playbooks/02-console-install.yml b/full-stack-asset-transfer-guide/infrastructure/operator_console_playbooks/02-console-install.yml new file mode 100644 index 00000000..13718bc2 --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/operator_console_playbooks/02-console-install.yml @@ -0,0 +1,13 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# +--- +- name: Deploy Opensource Console + hosts: localhost + vars_files: + - /_cfg/operator-console-vars.yml + vars: + state: present + wait_timeout: 3600 + roles: + - ibm.blockchain_platform.fabric_console diff --git a/full-stack-asset-transfer-guide/infrastructure/pkgcc.sh b/full-stack-asset-transfer-guide/infrastructure/pkgcc.sh new file mode 100755 index 00000000..33cce1a2 --- /dev/null +++ b/full-stack-asset-transfer-guide/infrastructure/pkgcc.sh @@ -0,0 +1,103 @@ +#!/bin/sh -l + + +# From the github action +# https://github.com/hyperledgendary/package-k8s-chaincode-action/blob/main/pkgk8scc.sh +# +# SPDX-License-Identifier: Apache-2.0 +# + +usage() { + echo "Usage: pkgk8scc.sh -l