Moves the Full Stack Asset Transfer Development Guide to fabric-samples (#852)

* Import Full Stack Asset Transfer Guide at commit fb554befdbbeff9e69159b54fce0b811603f29c7

Signed-off-by: Josh Kneubuhl <jkneubuh@us.ibm.com>

* Update the workshop with a new WORKSHOP_PATH under fabric-samples

Signed-off-by: Josh Kneubuhl <jkneubuh@us.ibm.com>

* Update the workshop with a new WORKSHOP_PATH under fabric-samples

Signed-off-by: Josh Kneubuhl <jkneubuh@us.ibm.com>

* missed a .git ignored directory on add

Signed-off-by: Josh Kneubuhl <jkneubuh@us.ibm.com>

* Updates to run the workshop on the Apple M1

Signed-off-by: Josh Kneubuhl <jkneubuh@us.ibm.com>

* Workaround for https://github.com/eslint/eslint/issues/15299 in the contract tslinter

Signed-off-by: Josh Kneubuhl <jkneubuh@us.ibm.com>

* Build an arch-specific CC images on M1

Signed-off-by: Josh Kneubuhl <jkneubuh@us.ibm.com>

* empty commit - force a build

Signed-off-by: Josh Kneubuhl <jkneubuh@us.ibm.com>

* revert an accidental commit that was building the top-level asset-transfer as arm64

Signed-off-by: Josh Kneubuhl <jkneubuh@us.ibm.com>

Signed-off-by: Josh Kneubuhl <jkneubuh@us.ibm.com>
This commit is contained in:
jkneubuh 2022-11-10 10:40:27 -05:00 committed by GitHub
parent 49749c6584
commit a299e18e26
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
262 changed files with 15259 additions and 0 deletions

View file

@ -12,3 +12,4 @@ repository:
allow_squash_merge: true
allow_merge_commit: false
allow_rebase_merge: true

3
.gitignore vendored
View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,17 @@
fabric
_cfg
node_modules
*.bin
.idea/
_*
*tgz
*.tar.gz
~*.pptx
bin
config/
.DS_Store
.idea/
rook

View file

@ -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.

View file

@ -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)

View file

@ -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.

View file

@ -0,0 +1,12 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: [
'@typescript-eslint',
],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
],
};

View file

@ -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

View file

@ -0,0 +1 @@
engine-strict=true

View file

@ -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 <command> [<arg> ...]`. The following commands are available:
- `npm start create <assetId> <ownerName> <color>` to create a new asset.
- `npm start delete <assetId>` to delete an existing asset.
- `npm start getAllAssets` to list all assets.
- `npm start read <assetId>` to view an existing asset.
- `npm start transfer <assetId> <ownerName> <ownerMspId>` 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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View file

@ -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"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

View file

@ -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"
}
}

View file

@ -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<void> {
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<void> {
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: <command> [<arg1> ...]');
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;
}
});

View file

@ -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<void> {
const [assetId, owner, color] = assertAllDefined([args[0], args[1], args[2]], 'Arguments: <assetId> <ownerName> <color>');
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,
});
}

View file

@ -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<void> {
const assetId = assertDefined(args[0], 'Arguments: <assetId>');
const network = gateway.getNetwork(CHANNEL_NAME);
const contract = network.getContract(CHAINCODE_NAME);
const smartContract = new AssetTransfer(contract);
await smartContract.deleteAsset(assetId);
}

View file

@ -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<void> {
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<void> {
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<void> {
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)];
}

View file

@ -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<void> {
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
}

View file

@ -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<void>;
export const commands: Record<string, Command> = {
create,
delete: deleteCommand,
discord,
getAllAssets,
read,
transfer,
};

View file

@ -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<void> {
const assetId = assertDefined(args[0], 'Arguments: <assetId>');
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);
}

View file

@ -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<void> {
const [assetId, newOwner, newOwnerOrg] = assertAllDefined([args[0], args[1], args[2]], 'Arguments: <assetId> <ownerName> <ownerMspId>');
const network = gateway.getNetwork(CHANNEL_NAME);
const contract = network.getContract(CHAINCODE_NAME);
const smartContract = new AssetTransfer(contract);
await smartContract.transferAsset(assetId, newOwner, newOwnerOrg);
}

View file

@ -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);
}

View file

@ -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<grpc.Client> {
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<Gateway> {
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<Identity> {
const certPath = path.resolve(CLIENT_CERT_PATH);
const credentials = await fs.promises.readFile(certPath);
return { mspId: MSP_ID, credentials };
}
async function newSigner(): Promise<Signer> {
const keyPath = path.resolve(PRIVATE_KEY_PATH);
const privateKeyPem = await fs.promises.readFile(keyPath);
const privateKey = crypto.createPrivateKey(privateKeyPem);
return signers.newPrivateKeySigner(privateKey);
}

View file

@ -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<Asset, 'Owner'> & Partial<Asset>;
export type AssetUpdate = Pick<Asset, 'ID'> & Partial<Omit<Asset, 'Owner'>>;
/**
* 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<void> {
await this.#contract.submit('CreateAsset', {
arguments: [JSON.stringify(asset)],
});
}
async getAllAssets(): Promise<Asset[]> {
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<Asset> {
const result = await this.#contract.evaluate('ReadAsset', {
arguments: [id],
});
return JSON.parse(utf8Decoder.decode(result)) as Asset;
}
async updateAsset(asset: AssetUpdate): Promise<void> {
await submitWithRetry(() => this.#contract.submit('UpdateAsset', {
arguments: [JSON.stringify(asset)],
}));
}
async deleteAsset(id: string): Promise<void> {
await submitWithRetry(() => this.#contract.submit('DeleteAsset', {
arguments: [id],
}));
}
async assetExists(id: string): Promise<boolean> {
const result = await this.#contract.evaluate('AssetExists', {
arguments: [id],
});
return utf8Decoder.decode(result).toLowerCase() === 'true';
}
async transferAsset(id: string, newOwner: string, newOwnerOrg: string): Promise<void> {
await submitWithRetry(() => this.#contract.submit('TransferAsset', {
arguments: [id, newOwner, newOwnerOrg],
}));
}
}
async function submitWithRetry<T>(submit: () => Promise<T>): Promise<T> {
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;
}

View file

@ -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;
}
}

View file

@ -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<T>(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<T>(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<unknown>[]): Promise<void> {
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<T> = {
[K in keyof T]: T[K] extends Uint8Array ? string : T[K];
};
export function printable<T extends object>(event: T): PrintView<T> {
return Object.fromEntries(
Object.entries(event).map(([k, v]) => [k, v instanceof Uint8Array ? utf8Decoder.decode(v) : v])
) as PrintView<T>;
}
export function assertAllDefined<T>(values: (T | undefined)[], message: string | (() => string)): T[] {
values.forEach(value => assertDefined(value, message));
return values as T[];
}
export function assertDefined<T>(value: T | undefined, message: string | (() => string)): T {
if (value == undefined) {
throw new Error(typeof message === 'string' ? message : message());
}
return value;
}

View file

@ -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"
]
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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/

View file

@ -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"
}
}

View file

@ -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
});
};

View file

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

View file

@ -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 { }

View file

@ -0,0 +1,66 @@
<mat-toolbar color="primary"><div style="text-align: center;width: 100%;">Asset (Create,Edit,Delete,Transfer)</div></mat-toolbar>
<div class="layout">
<div class="layout">
Assets
<div class="search">
<mat-form-field>
<input matInput (keyup)="applyFilter($event)" placeholder="Search" #input>
</mat-form-field>
<button mat-raised-button color="primary" style="margin-left: 30px;" (click)="addNewAsset()">Create
Asset</button>
</div>
<table mat-table matTableExporter [dataSource]="dataSource" matSort class="mat-elevation-z8"
style="width: 100%;">
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row;let i = index; columns: displayedColumns;"></tr>
<ng-container matColumnDef="position">
<th mat-header-cell *matHeaderCellDef> No. </th>
<td mat-cell *matCellDef="let element; let i = index"> {{ (i+1) + (paginator.pageIndex *
paginator.pageSize) }} </td>
</ng-container>
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Id </th>
<td mat-cell *matCellDef="let element"> {{element.ID}} </td>
</ng-container>
<ng-container matColumnDef="color">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Color </th>
<td mat-cell *matCellDef="let element"> {{element.Color}} </td>
</ng-container>
<ng-container matColumnDef="owner">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Owner </th>
<td mat-cell *matCellDef="let element"> {{element.Owner}} </td>
</ng-container>
<ng-container matColumnDef="appraisedValue">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Appraised Value </th>
<td mat-cell *matCellDef="let element"> {{element.AppraisedValue}} </td>
</ng-container>
<ng-container matColumnDef="size">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Size </th>
<td mat-cell *matCellDef="let element"> {{element.Size}} </td>
</ng-container>
<ng-container matColumnDef="transfer">
<th mat-header-cell *matHeaderCellDef> Trasnfer </th>
<td mat-cell *matCellDef="let element;let i = index" style="padding-right: 15px;">
<span class="material-icons" style="cursor: pointer;" (click)="delete(element,i)">arrow_forward</span>
</td>
</ng-container>
<ng-container matColumnDef="edit">
<th mat-header-cell *matHeaderCellDef> Edit </th>
<td mat-cell *matCellDef="let element;let i = index" style="padding-right: 15px;">
<span class="material-icons" style="cursor: pointer;" (click)="editRow(element,i)">edit</span>
</td>
</ng-container>
<ng-container matColumnDef="delete">
<th mat-header-cell *matHeaderCellDef> Delete </th>
<td mat-cell *matCellDef="let element;let i = index" style="padding-right: 15px;">
<span class="material-icons" style="cursor: pointer;" (click)="delete(element,i)">delete</span>
</td>
</ng-container>
</table>
<mat-paginator [length]="100" showFirstLastButtons [pageSize]="10" [pageSizeOptions]="[5, 10, 25, 100]">
</mat-paginator>
</div>
</div>

View file

@ -0,0 +1,6 @@
.layout{
margin: 40px 10%;
}
.search{
float: right;
}

View file

@ -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!');
});
});

View file

@ -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<any>(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<any>(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;
}
}

View file

@ -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 { }

View file

@ -0,0 +1,51 @@
<span class="material-icons" (click)="close()" style="float: right;">
<mat-icon>highlight_off</mat-icon>
</span>
<div style="margin: 20px;">
<div style="font-size: 1.5em; text-align: center; margin-bottom: 15px;">Asset create/edit</div>
<form (ngSubmit)="onSubmit()">
<div fxLayout="column" fxLayoutAlign="space-around">
<div class="row">
<div class="col-sm-12 ">
<mat-form-field appearance="outline">
<mat-label>Color</mat-label>
<input matInput type="text" required [formControl]="form.controls['Color']" [(ngModel)]="aseet.Color">
</mat-form-field>
<small *ngIf="form.controls['Color'].hasError('required') && form.controls['Color'].touched"
class="mat-text-warn">You must include color</small>
</div>
<div class="col-sm-12 ">
<mat-form-field appearance="outline">
<mat-label>Owner</mat-label>
<input matInput type="text" required [formControl]="form.controls['Owner']" [(ngModel)]="aseet.Owner">
</mat-form-field>
<small *ngIf="form.controls['Owner'].hasError('required') && form.controls['Owner'].touched"
class="mat-text-warn">You must include owner</small>
</div>
<div class="col-sm-12 ">
<mat-form-field appearance="outline">
<mat-label>Size</mat-label>
<input matInput type="number" required [formControl]="form.controls['Size']" [(ngModel)]="aseet.Size">
</mat-form-field>
<small *ngIf="form.controls['Size'].hasError('required') && form.controls['Size'].touched"
class="mat-text-warn">You must include size</small>
</div>
<div class="col-sm-12 ">
<mat-form-field appearance="outline">
<mat-label>Appraised Value</mat-label>
<input matInput type="number" required [formControl]="form.controls['AppraisedValue']" [(ngModel)]="aseet.AppraisedValue">
</mat-form-field>
<small *ngIf="form.controls['AppraisedValue'].hasError('required') && form.controls['AppraisedValue'].touched"
class="mat-text-warn">You must include appraisedValue</small>
</div>
</div>
</div>
<div style="text-align: center;">
<button mat-raised-button color="primary" type="submit" [disabled]="!form.valid"
style="margin-top:10px;">Create/Update</button>
</div>
</form>
</div>

View file

@ -0,0 +1,3 @@
.mat-form-field{
width: 100% !important;
}

View file

@ -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<AssetDialogComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ AssetDialogComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(AssetDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -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<any, any>,
@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<any>(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<any>(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();
}
}

View file

@ -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";
}

View file

@ -0,0 +1,3 @@
export const environment = {
production: true
};

View file

@ -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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 948 B

View file

@ -0,0 +1,19 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Frontend</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0/css/bootstrap.min.css" rel="stylesheet">
<script src="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0/js/bootstrap.min.js"></script>
</head>
<body>
<app-root></app-root>
</body>
</html>

View file

@ -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));

View file

@ -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
*/

View file

@ -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; }

View file

@ -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): {
<T>(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);

View file

@ -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"
]
}

View file

@ -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
}
}

View file

@ -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"
]
}

View file

@ -0,0 +1,2 @@
node_modules
dist

View file

@ -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

View file

@ -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"
}
}

View file

@ -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<void> {
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<void> {
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));
}

View file

@ -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<grpc.Client> {
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());
}
}
}

View file

@ -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<JSONID> {
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<Identity> {
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<Signer> {
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);
}
}

View file

@ -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"
]
}

View file

@ -0,0 +1,2 @@
node_modules/
dist/

View file

@ -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" ]

View file

@ -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.
#

View file

@ -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

View file

@ -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": []
}
]
}

View file

@ -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

View file

@ -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"
}
}

View file

@ -0,0 +1,5 @@
{
"extends": [
"config:base"
]
}

View file

@ -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;

View file

@ -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);
})
}
}

View file

@ -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<void> {
// 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<grpc.Client> {
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<Identity> {
const credentials = await fs.readFile(certPath);
return { mspId, credentials };
}
async function newSigner(): Promise<Signer> {
//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;
}

View file

@ -0,0 +1,6 @@
import app from "./app";
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
console.log('listening on port ' + PORT);
})

View file

@ -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
]
}

View file

@ -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',
},
],
},
},
],
};

View file

@ -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

View file

@ -0,0 +1 @@
engine-strict=true

View file

@ -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 <command> [<arg> ...]`. The following commands are available:
- `npm start create <assetId> <ownerName> <color>` to create a new asset.
- `npm start delete <assetId>` 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 <assetId>` to view an existing asset.
- `npm start transact` to create some random assets and perform some random operations on those assets.
- `npm start transfer <assetId> <ownerName> <ownerMspId>` to transfer an asset to a new owner within an organization MSP ID.

View file

@ -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"
}
}

Some files were not shown because too many files have changed in this diff Show more