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>
1
.github/settings.yml
vendored
|
|
@ -12,3 +12,4 @@ repository:
|
||||||
allow_squash_merge: true
|
allow_squash_merge: true
|
||||||
allow_merge_commit: false
|
allow_merge_commit: false
|
||||||
allow_rebase_merge: true
|
allow_rebase_merge: true
|
||||||
|
|
||||||
|
|
|
||||||
3
.gitignore
vendored
|
|
@ -27,3 +27,6 @@ builders/
|
||||||
config/
|
config/
|
||||||
external-chaincode/
|
external-chaincode/
|
||||||
install-fabric.sh
|
install-fabric.sh
|
||||||
|
|
||||||
|
# override the ignore of all config/ folders
|
||||||
|
!full-stack-asset-transfer-guide/infrastructure/sample-network/config
|
||||||
24
full-stack-asset-transfer-guide/.github/workflows/asset-tx-typescript-contract-image.yaml
vendored
Normal 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
|
||||||
78
full-stack-asset-transfer-guide/.github/workflows/docker-build.yaml
vendored
Normal 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
|
||||||
49
full-stack-asset-transfer-guide/.github/workflows/test-ansible.yaml
vendored
Normal 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
|
||||||
49
full-stack-asset-transfer-guide/.github/workflows/test-appdev.yaml
vendored
Normal 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
|
||||||
49
full-stack-asset-transfer-guide/.github/workflows/test-chaincode.yaml
vendored
Normal 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
|
||||||
49
full-stack-asset-transfer-guide/.github/workflows/test-cloud.yaml
vendored
Normal 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
|
||||||
49
full-stack-asset-transfer-guide/.github/workflows/test-console.yaml
vendored
Normal 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
|
||||||
17
full-stack-asset-transfer-guide/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
fabric
|
||||||
|
_cfg
|
||||||
|
node_modules
|
||||||
|
*.bin
|
||||||
|
.idea/
|
||||||
|
_*
|
||||||
|
*tgz
|
||||||
|
*.tar.gz
|
||||||
|
~*.pptx
|
||||||
|
|
||||||
|
bin
|
||||||
|
config/
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
rook
|
||||||
201
full-stack-asset-transfer-guide/LICENSE
Normal 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.
|
||||||
108
full-stack-asset-transfer-guide/README.md
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|

|
||||||
|
|
||||||
|
# Fabric Full Stack Development Workshop
|
||||||
|
|
||||||
|

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

|
||||||
|
|
||||||
|
**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
|
||||||
|
|
||||||
|
 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)
|
||||||
138
full-stack-asset-transfer-guide/SETUP.md
Normal 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.
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: [
|
||||||
|
'@typescript-eslint',
|
||||||
|
],
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'prettier',
|
||||||
|
],
|
||||||
|
};
|
||||||
18
full-stack-asset-transfer-guide/applications/conga-cards/.gitignore
vendored
Normal 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
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
engine-strict=true
|
||||||
|
|
@ -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
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
|
@ -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"
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 348 KiB |
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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)];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
42
full-stack-asset-transfer-guide/applications/frontend/.gitignore
vendored
Normal 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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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/
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
|
@ -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 { }
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
.layout{
|
||||||
|
margin: 40px 10%;
|
||||||
|
}
|
||||||
|
.search{
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
@ -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!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 { }
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
.mat-form-field{
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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";
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export const environment = {
|
||||||
|
production: true
|
||||||
|
};
|
||||||
|
|
@ -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.
|
||||||
|
After Width: | Height: | Size: 948 B |
|
|
@ -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>
|
||||||
|
|
@ -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));
|
||||||
|
|
@ -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
|
||||||
|
*/
|
||||||
|
|
@ -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; }
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
2
full-stack-asset-transfer-guide/applications/ping-chaincode/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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());
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
2
full-stack-asset-transfer-guide/applications/rest-api/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
|
@ -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" ]
|
||||||
|
|
@ -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.
|
||||||
|
#
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"config:base"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import app from "./app";
|
||||||
|
const PORT = process.env.PORT || 8080;
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log('listening on port ' + PORT);
|
||||||
|
})
|
||||||
|
|
@ -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
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -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',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
18
full-stack-asset-transfer-guide/applications/trader-typescript/.gitignore
vendored
Normal 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
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
engine-strict=true
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||