mirror of
https://github.com/hyperledger/fabric-samples.git
synced 2026-06-17 15:35:09 +00:00
se eliminarom mas ejemplos
This commit is contained in:
parent
2ed8904c0f
commit
fd3acdb16e
129 changed files with 0 additions and 23841 deletions
3
asset-transfer-ledger-queries/.gitignore
vendored
3
asset-transfer-ledger-queries/.gitignore
vendored
|
|
@ -1,3 +0,0 @@
|
|||
.idea/
|
||||
.gradle/
|
||||
bin/
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
#
|
||||
# https://help.github.com/articles/dealing-with-line-endings/
|
||||
#
|
||||
# These are explicitly windows files and should use crlf
|
||||
*.bat text eol=crlf
|
||||
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
/*
|
||||
* This file was generated by the Gradle 'init' task.
|
||||
*
|
||||
* This generated file contains a sample Java project to get you started.
|
||||
* For more details take a look at the Java Quickstart chapter in the Gradle
|
||||
* User Manual available at https://docs.gradle.org/6.5/userguide/tutorial_java_projects.html
|
||||
*/
|
||||
|
||||
plugins {
|
||||
// Apply the java plugin to add support for Java
|
||||
id 'java'
|
||||
|
||||
// Apply the application plugin to add support for building a CLI application.
|
||||
id 'application'
|
||||
}
|
||||
ext {
|
||||
javaMainClass = "application.java.App"
|
||||
}
|
||||
|
||||
repositories {
|
||||
// You can declare any Maven/Ivy/file repository here.
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// This dependency is used by the application.
|
||||
implementation 'com.google.guava:guava:29.0-jre'
|
||||
implementation 'org.hyperledger.fabric:fabric-gateway-java:2.1.1'
|
||||
}
|
||||
|
||||
application {
|
||||
// Define the main class for the application.
|
||||
mainClassName = 'application.java.App'
|
||||
}
|
||||
|
||||
// task for running the app after building dependencies
|
||||
task runApp(type: Exec) {
|
||||
dependsOn build
|
||||
group = "Execution"
|
||||
description = "Run the main class with ExecTask"
|
||||
commandLine "java", "-classpath", sourceSets.main.runtimeClasspath.getAsPath(), javaMainClass
|
||||
}
|
||||
Binary file not shown.
|
|
@ -1,7 +0,0 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
@ -1,249 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
/*
|
||||
* This file was generated by the Gradle 'init' task.
|
||||
*
|
||||
* The settings file is used to specify which projects to include in your build.
|
||||
*
|
||||
* Detailed information about configuring a multi-project build in Gradle can be found
|
||||
* in the user manual at https://docs.gradle.org/6.5/userguide/multi_project_builds.html
|
||||
*/
|
||||
|
||||
rootProject.name = 'application-java'
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
/*
|
||||
* Copyright IBM Corp. All Rights Reserved.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
// Running TestApp:
|
||||
// gradle runApp
|
||||
|
||||
package application.java;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import org.hyperledger.fabric.gateway.Contract;
|
||||
import org.hyperledger.fabric.gateway.Gateway;
|
||||
import org.hyperledger.fabric.gateway.Network;
|
||||
import org.hyperledger.fabric.gateway.Wallet;
|
||||
import org.hyperledger.fabric.gateway.Wallets;
|
||||
|
||||
|
||||
public class App {
|
||||
|
||||
static {
|
||||
System.setProperty("org.hyperledger.fabric.sdk.service_discovery.as_localhost", "true");
|
||||
}
|
||||
|
||||
// helper function for getting connected to the gateway
|
||||
public static Gateway connect() throws Exception{
|
||||
// Load a file system based wallet for managing identities.
|
||||
Path walletPath = Paths.get("wallet");
|
||||
Wallet wallet = Wallets.newFileSystemWallet(walletPath);
|
||||
// load a CCP
|
||||
Path networkConfigPath = Paths.get("..", "..", "test-network", "organizations", "peerOrganizations", "org1.example.com", "connection-org1.yaml");
|
||||
|
||||
Gateway.Builder builder = Gateway.createBuilder();
|
||||
builder.identity(wallet, "appUser").networkConfig(networkConfigPath).discovery(true);
|
||||
return builder.connect();
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
// enrolls the admin and registers the user
|
||||
try {
|
||||
EnrollAdmin.main(null);
|
||||
RegisterUser.main(null);
|
||||
} catch (Exception e) {
|
||||
System.err.println(e);
|
||||
}
|
||||
|
||||
// connect to the network and invoke the smart contract
|
||||
try (Gateway gateway = connect()) {
|
||||
|
||||
// get the network and contract
|
||||
Network network = gateway.getNetwork("mychannel");
|
||||
Contract contract = network.getContract("ledger");
|
||||
|
||||
byte[] result;
|
||||
|
||||
System.out.println("Submit Transaction: InitLedger creates the initial set of assets on the ledger.");
|
||||
contract.submitTransaction("InitLedger");
|
||||
|
||||
System.out.println("\n");
|
||||
// passing in 2 empty strings will query all the assets
|
||||
result = contract.evaluateTransaction("GetAssetsByRange", "", "");
|
||||
System.out.println("Evaluate Transaction: GetAssetsByRange, result: " + new String(result));
|
||||
|
||||
System.out.println("\n");
|
||||
System.out.println("Submit Transaction: CreateAsset asset13");
|
||||
// CreateAsset creates an asset with ID asset13, color yellow, owner Tom, size 5 and appraisedValue of 1300
|
||||
contract.submitTransaction("CreateAsset", "asset13", "yellow", "5", "Tom", "1300");
|
||||
|
||||
System.out.println("\n");
|
||||
System.out.println("Evaluate Transaction: ReadAsset asset13");
|
||||
// ReadAsset returns an asset with given assetID
|
||||
result = contract.evaluateTransaction("ReadAsset", "asset13");
|
||||
System.out.println("result: " + new String(result));
|
||||
|
||||
System.out.println("\n");
|
||||
System.out.println("Evaluate Transaction: AssetExists asset1");
|
||||
// AssetExists returns "true" if an asset with given assetID exist
|
||||
result = contract.evaluateTransaction("AssetExists", "asset1");
|
||||
System.out.println("result: " + new String(result));
|
||||
|
||||
System.out.println("\n");
|
||||
System.out.println("Submit Transaction: DeleteAsset asset1");
|
||||
contract.submitTransaction("DeleteAsset", "asset1");
|
||||
|
||||
System.out.println("\n");
|
||||
System.out.println("Evaluate Transaction: AssetExists asset1");
|
||||
// AssetExists returns "true" if an asset with given assetID exist
|
||||
result = contract.evaluateTransaction("AssetExists", "asset1");
|
||||
System.out.println("result: " + new String(result));
|
||||
|
||||
System.out.println("\n");
|
||||
System.out.println("Submit Transaction: TransferAsset asset2 from owner Tomoko > owner Tom");
|
||||
// TransferAsset transfers an asset with given ID to new owner Tom
|
||||
contract.submitTransaction("TransferAsset", "asset2", "Tom");
|
||||
|
||||
// Rich Query with Pagination (Only supported if CouchDB is used as state database)
|
||||
System.out.println("\n");
|
||||
System.out.println("Evaluate Transaction:QueryAssetsWithPagination Tom's assets");
|
||||
result = contract.evaluateTransaction("QueryAssetsWithPagination","{\"selector\":{\"docType\":\"asset\",\"owner\":\"Tom\"}, \"use_index\":[\"_design/indexOwnerDoc\", \"indexOwner\"]}","3","");
|
||||
System.out.println("result: " + new String(result));
|
||||
|
||||
System.out.println("\n");
|
||||
System.out.println("Submit Transaction: TransferAssetByColor yellow assets > newOwner Michel");
|
||||
contract.submitTransaction("TransferAssetByColor", "yellow", "Michel");
|
||||
|
||||
// Rich Query (Only supported if CouchDB is used as state database):
|
||||
System.out.println("\n");
|
||||
System.out.println("Evaluate Transaction:QueryAssetsByOwner Michel");
|
||||
result = contract.evaluateTransaction("QueryAssetsByOwner", "Michel");
|
||||
System.out.println("result: " + new String(result));
|
||||
|
||||
System.out.println("\n");
|
||||
System.out.println("Evaluate Transaction:GetAssetHistory asset13");
|
||||
result = contract.evaluateTransaction("GetAssetHistory", "asset13");
|
||||
System.out.println("result: " + new String(result));
|
||||
|
||||
// Rich Query (Only supported if CouchDB is used as state database):
|
||||
System.out.println("\n");
|
||||
System.out.println("Evaluate Transaction:QueryAssets assets of size 15");
|
||||
result = contract.evaluateTransaction("QueryAssets", "{\"selector\":{\"size\":15}}");
|
||||
System.out.println("result: " + new String(result));
|
||||
|
||||
// Rich Query with index design doc and index name specified (Only supported if CouchDB is used as state database):
|
||||
System.out.println("\n");
|
||||
System.out.println("Evaluate Transaction:QueryAssets Jin Soo's assets");
|
||||
result = contract.evaluateTransaction("QueryAssets","{\"selector\":{\"docType\":\"asset\",\"owner\":\"Jin Soo\"}, \"use_index\":[\"_design/indexOwnerDoc\", \"indexOwner\"]}");
|
||||
System.out.println("result: " + new String(result));
|
||||
|
||||
// Range Query with Pagination
|
||||
System.out.println("\n");
|
||||
System.out.println("Evaluate Transaction:GetAssetsByRangeWithPagination assets 3-5");
|
||||
result = contract.evaluateTransaction("GetAssetsByRangeWithPagination", "asset3", "asset6", "3","");
|
||||
System.out.println("result: " + new String(result));
|
||||
}
|
||||
catch(Exception e){
|
||||
System.err.println(e);
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
/*
|
||||
* Copyright IBM Corp. All Rights Reserved.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package application.java;
|
||||
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.hyperledger.fabric.gateway.Wallet;
|
||||
import org.hyperledger.fabric.gateway.Wallets;
|
||||
import org.hyperledger.fabric.gateway.Identities;
|
||||
import org.hyperledger.fabric.gateway.Identity;
|
||||
import org.hyperledger.fabric.sdk.Enrollment;
|
||||
import org.hyperledger.fabric.sdk.security.CryptoSuite;
|
||||
import org.hyperledger.fabric.sdk.security.CryptoSuiteFactory;
|
||||
import org.hyperledger.fabric_ca.sdk.EnrollmentRequest;
|
||||
import org.hyperledger.fabric_ca.sdk.HFCAClient;
|
||||
|
||||
public class EnrollAdmin {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
|
||||
// Create a CA client for interacting with the CA.
|
||||
Properties props = new Properties();
|
||||
props.put("pemFile",
|
||||
"../../test-network/organizations/peerOrganizations/org1.example.com/ca/ca.org1.example.com-cert.pem");
|
||||
props.put("allowAllHostNames", "true");
|
||||
HFCAClient caClient = HFCAClient.createNewInstance("https://localhost:7054", props);
|
||||
CryptoSuite cryptoSuite = CryptoSuiteFactory.getDefault().getCryptoSuite();
|
||||
caClient.setCryptoSuite(cryptoSuite);
|
||||
|
||||
// Create a wallet for managing identities
|
||||
Wallet wallet = Wallets.newFileSystemWallet(Paths.get("wallet"));
|
||||
|
||||
// Check to see if we've already enrolled the admin user.
|
||||
if (wallet.get("admin") != null) {
|
||||
System.out.println("An identity for the admin user \"admin\" already exists in the wallet");
|
||||
return;
|
||||
}
|
||||
|
||||
// Enroll the admin user, and import the new identity into the wallet.
|
||||
final EnrollmentRequest enrollmentRequestTLS = new EnrollmentRequest();
|
||||
enrollmentRequestTLS.addHost("localhost");
|
||||
enrollmentRequestTLS.setProfile("tls");
|
||||
Enrollment enrollment = caClient.enroll("admin", "adminpw", enrollmentRequestTLS);
|
||||
Identity user = Identities.newX509Identity("Org1MSP", enrollment);
|
||||
wallet.put("admin", user);
|
||||
System.out.println("Successfully enrolled user \"admin\" and imported it into the wallet");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
/*
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package application.java;
|
||||
|
||||
import java.nio.file.Paths;
|
||||
import java.security.PrivateKey;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
|
||||
import org.hyperledger.fabric.gateway.Wallet;
|
||||
import org.hyperledger.fabric.gateway.Wallets;
|
||||
import org.hyperledger.fabric.gateway.Identities;
|
||||
import org.hyperledger.fabric.gateway.Identity;
|
||||
import org.hyperledger.fabric.gateway.X509Identity;
|
||||
import org.hyperledger.fabric.sdk.Enrollment;
|
||||
import org.hyperledger.fabric.sdk.User;
|
||||
import org.hyperledger.fabric.sdk.security.CryptoSuite;
|
||||
import org.hyperledger.fabric.sdk.security.CryptoSuiteFactory;
|
||||
import org.hyperledger.fabric_ca.sdk.HFCAClient;
|
||||
import org.hyperledger.fabric_ca.sdk.RegistrationRequest;
|
||||
|
||||
public class RegisterUser {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
|
||||
// Create a CA client for interacting with the CA.
|
||||
Properties props = new Properties();
|
||||
props.put("pemFile",
|
||||
"../../test-network/organizations/peerOrganizations/org1.example.com/ca/ca.org1.example.com-cert.pem");
|
||||
props.put("allowAllHostNames", "true");
|
||||
HFCAClient caClient = HFCAClient.createNewInstance("https://localhost:7054", props);
|
||||
CryptoSuite cryptoSuite = CryptoSuiteFactory.getDefault().getCryptoSuite();
|
||||
caClient.setCryptoSuite(cryptoSuite);
|
||||
|
||||
// Create a wallet for managing identities
|
||||
Wallet wallet = Wallets.newFileSystemWallet(Paths.get("wallet"));
|
||||
|
||||
// Check to see if we've already enrolled the user.
|
||||
if (wallet.get("appUser") != null) {
|
||||
System.out.println("An identity for the user \"appUser\" already exists in the wallet");
|
||||
return;
|
||||
}
|
||||
|
||||
X509Identity adminIdentity = (X509Identity)wallet.get("admin");
|
||||
if (adminIdentity == null) {
|
||||
System.out.println("\"admin\" needs to be enrolled and added to the wallet first");
|
||||
return;
|
||||
}
|
||||
User admin = new User() {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "admin";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getRoles() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAccount() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAffiliation() {
|
||||
return "org1.department1";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Enrollment getEnrollment() {
|
||||
return new Enrollment() {
|
||||
|
||||
@Override
|
||||
public PrivateKey getKey() {
|
||||
return adminIdentity.getPrivateKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCert() {
|
||||
return Identities.toPemString(adminIdentity.getCertificate());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMspId() {
|
||||
return "Org1MSP";
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// Register the user, enroll the user, and import the new identity into the wallet.
|
||||
RegistrationRequest registrationRequest = new RegistrationRequest("appUser");
|
||||
registrationRequest.setAffiliation("org1.department1");
|
||||
registrationRequest.setEnrollmentID("appUser");
|
||||
String enrollmentSecret = caClient.register(registrationRequest, admin);
|
||||
Enrollment enrollment = caClient.enroll("appUser", enrollmentSecret);
|
||||
Identity user = Identities.newX509Identity("Org1MSP", enrollment);
|
||||
wallet.put("appUser", user);
|
||||
System.out.println("Successfully enrolled user \"appUser\" and imported it into the wallet");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
# initialize root logger with level ERROR for stdout and fout
|
||||
log4j.rootLogger=ERROR,stdout,fout
|
||||
# set the log level for these components
|
||||
log4j.logger.com.endeca=INFO
|
||||
log4j.logger.com.endeca.itl.web.metrics=INFO
|
||||
|
||||
# add a ConsoleAppender to the logger stdout to write to the console
|
||||
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
|
||||
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
|
||||
# use a simple message format
|
||||
log4j.appender.stdout.layout.ConversionPattern=%m%n
|
||||
|
||||
# add a FileAppender to the logger fout
|
||||
log4j.appender.fout=org.apache.log4j.FileAppender
|
||||
# create a log file
|
||||
log4j.appender.fout.File=crawl.log
|
||||
log4j.appender.fout.layout=org.apache.log4j.PatternLayout
|
||||
# use a more detailed message pattern
|
||||
log4j.appender.fout.layout.ConversionPattern=%p\t%d{ISO8601}\t%r\t%c\t[%t]\t%m%n
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
coverage
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
env: {
|
||||
node: true,
|
||||
mocha: true
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 8,
|
||||
sourceType: 'script'
|
||||
},
|
||||
extends: 'eslint:recommended',
|
||||
rules: {
|
||||
indent: ['error', 'tab'],
|
||||
'linebreak-style': ['error', 'unix'],
|
||||
quotes: ['error', 'single'],
|
||||
semi: ['error', 'always'],
|
||||
'no-unused-vars': ['error', { args: 'none' }],
|
||||
'no-console': 'off',
|
||||
curly: 'error',
|
||||
eqeqeq: 'error',
|
||||
'no-throw-literal': 'error',
|
||||
strict: 'error',
|
||||
'no-var': 'error',
|
||||
'dot-notation': 'error',
|
||||
'no-trailing-spaces': 'error',
|
||||
'no-use-before-define': 'error',
|
||||
'no-useless-call': 'error',
|
||||
'no-with': 'error',
|
||||
'operator-linebreak': 'error',
|
||||
yoda: 'error',
|
||||
'quote-props': ['error', 'as-needed']
|
||||
}
|
||||
};
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
package-lock.json
|
||||
|
||||
wallet
|
||||
!wallet/.gitkeep
|
||||
|
|
@ -1,252 +0,0 @@
|
|||
/*
|
||||
* Copyright IBM Corp. All Rights Reserved.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const { Gateway, Wallets } = require('fabric-network');
|
||||
const FabricCAServices = require('fabric-ca-client');
|
||||
const path = require('path');
|
||||
const { buildCAClient, registerAndEnrollUser, enrollAdmin } = require('../../test-application/javascript/CAUtil.js');
|
||||
const { buildCCPOrg1, buildWallet } = require('../../test-application/javascript/AppUtil.js');
|
||||
|
||||
const channelName = 'mychannel';
|
||||
const chaincodeName = 'ledger';
|
||||
const mspOrg1 = 'Org1MSP';
|
||||
|
||||
const walletPath = path.join(__dirname, 'wallet');
|
||||
const userId = 'appUser';
|
||||
|
||||
function prettyJSONString(inputString) {
|
||||
return JSON.stringify(JSON.parse(inputString), null, 2);
|
||||
}
|
||||
|
||||
// pre-requisites:
|
||||
// - fabric-sample two organization test-network setup with two peers, ordering service,
|
||||
// and 2 certificate authorities, with the state database using couchdb
|
||||
// ===> from directory /fabric-samples/test-network
|
||||
// ./network.sh up createChannel -ca -s couchdb
|
||||
// - Use any of the asset-transfer-ledger-queries chaincodes deployed on the channel "mychannel"
|
||||
// with the chaincode name of "ledger". The following deploy command will package,
|
||||
// install, approve, and commit the javascript chaincode, all the actions it takes
|
||||
// to deploy a chaincode to a channel.
|
||||
// ===> from directory /fabric-samples/test-network
|
||||
// ./network.sh deployCC -ccn ledger -ccp ../asset-transfer-ledger-queries/chaincode-javascript/ -ccl javascript
|
||||
// - Be sure that node.js is installed
|
||||
// ===> from directory /fabric-samples/asset-transfer-ledger-queries/application-javascript
|
||||
// node -v
|
||||
// - npm installed code dependencies
|
||||
// ===> from directory /fabric-samples/asset-transfer-ledger-queries/application-javascript
|
||||
// npm install
|
||||
// - to run this test application
|
||||
// ===> from directory /fabric-samples/asset-transfer-ledger-queries/application-javascript
|
||||
// node app.js
|
||||
|
||||
// NOTE: If you see kind an error like these:
|
||||
/*
|
||||
2020-08-07T20:23:17.590Z - error: [DiscoveryService]: send[mychannel] - Channel:mychannel received discovery error:access denied
|
||||
******** FAILED to run the application: Error: DiscoveryService: mychannel error: access denied
|
||||
|
||||
OR
|
||||
|
||||
Failed to register user : Error: fabric-ca request register failed with errors [[ { code: 20, message: 'Authentication failure' } ]]
|
||||
******** FAILED to run the application: Error: Identity not found in wallet: appUser
|
||||
*/
|
||||
// Delete the /fabric-samples/asset-transfer-ledger-queries/application-javascript/wallet directory
|
||||
// and retry this application.
|
||||
//
|
||||
// The certificate authority must have been restarted and the saved certificates for the
|
||||
// admin and application user are not valid. Deleting the wallet store will force these to be reset
|
||||
// with the new certificate authority.
|
||||
//
|
||||
|
||||
/**
|
||||
* A test application to show ledger queries operations with any of the asset-transfer-ledger-queries chaincodes
|
||||
* -- How to submit a transaction
|
||||
* -- How to query and check the results
|
||||
*
|
||||
* To see the SDK workings, try setting the logging to show on the console before running
|
||||
* export HFC_LOGGING='{"debug":"console"}'
|
||||
*/
|
||||
async function main() {
|
||||
let skipInit = false;
|
||||
if (process.argv.length > 2) {
|
||||
if (process.argv[2] === 'skipInit') {
|
||||
skipInit = true;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// build an in memory object with the network configuration (also known as a connection profile)
|
||||
const ccp = buildCCPOrg1();
|
||||
|
||||
// build an instance of the fabric ca services client based on
|
||||
// the information in the network configuration
|
||||
const caClient = buildCAClient(FabricCAServices, ccp, 'ca.org1.example.com');
|
||||
|
||||
// setup the wallet to hold the credentials of the application user
|
||||
const wallet = await buildWallet(Wallets, walletPath);
|
||||
|
||||
// in a real application this would be done on an administrative flow, and only once
|
||||
await enrollAdmin(caClient, wallet, mspOrg1);
|
||||
|
||||
// in a real application this would be done only when a new user was required to be added
|
||||
// and would be part of an administrative flow
|
||||
await registerAndEnrollUser(caClient, wallet, mspOrg1, userId, 'org1.department1');
|
||||
|
||||
// Create a new gateway instance for interacting with the fabric network.
|
||||
// In a real application this would be done as the backend server session is setup for
|
||||
// a user that has been verified.
|
||||
const gateway = new Gateway();
|
||||
|
||||
try {
|
||||
// setup the gateway instance
|
||||
// The user will now be able to create connections to the fabric network and be able to
|
||||
// submit transactions and query. All transactions submitted by this gateway will be
|
||||
// signed by this user using the credentials stored in the wallet.
|
||||
await gateway.connect(ccp, {
|
||||
wallet,
|
||||
identity: userId,
|
||||
discovery: { enabled: true, asLocalhost: true } // using asLocalhost as this gateway is using a fabric network deployed locally
|
||||
});
|
||||
|
||||
// Build a network instance based on the channel where the smart contract is deployed
|
||||
const network = await gateway.getNetwork(channelName);
|
||||
|
||||
// Get the contract from the network.
|
||||
const contract = network.getContract(chaincodeName);
|
||||
|
||||
// Initialize a set of asset data on the channel using the chaincode 'InitLedger' function.
|
||||
// This type of transaction would only be run once by an application the first time it was started after it
|
||||
// deployed the first time. Any updates to the chaincode deployed later would likely not need to run
|
||||
// an "init" type function.
|
||||
if (!skipInit) {
|
||||
try {
|
||||
console.log('\n--> Submit Transaction: InitLedger, function creates the initial set of assets on the ledger');
|
||||
await contract.submitTransaction('InitLedger');
|
||||
console.log('*** Result: committed');
|
||||
} catch (initError) {
|
||||
// this is error is OK if we are rerunning this app without restarting
|
||||
console.log(`******** initLedger failed :: ${initError}`);
|
||||
}
|
||||
} else {
|
||||
console.log('*** not executing "InitLedger');
|
||||
}
|
||||
|
||||
let result;
|
||||
|
||||
// Let's try a query operation (function).
|
||||
// This will be sent to just one peer and the results will be shown.
|
||||
console.log('\n--> Evaluate Transaction: GetAssetsByRange, function returns assets in a specific range from asset1 to before asset6');
|
||||
result = await contract.evaluateTransaction('GetAssetsByRange', 'asset1', 'asset6');
|
||||
console.log(`*** Result: ${prettyJSONString(result.toString())}`);
|
||||
|
||||
console.log('\n--> Evaluate Transaction: GetAssetsByRange, function use an open start and open end range to return assest1 to asset6');
|
||||
result = await contract.evaluateTransaction('GetAssetsByRange', '', '');
|
||||
console.log(`*** Result: ${prettyJSONString(result.toString())}`);
|
||||
|
||||
console.log('\n--> Evaluate Transaction: GetAssetsByRange, function use an fixed start (asset3) and open end range to return assest3 to asset6');
|
||||
result = await contract.evaluateTransaction('GetAssetsByRange', 'asset3', '');
|
||||
console.log(`*** Result: ${prettyJSONString(result.toString())}`);
|
||||
|
||||
console.log('\n--> Evaluate Transaction: GetAssetsByRange, function use an open start and fixed end (asset3) range to return assest1 to asset2');
|
||||
result = await contract.evaluateTransaction('GetAssetsByRange', '', 'asset3');
|
||||
console.log(`*** Result: ${prettyJSONString(result.toString())}`);
|
||||
|
||||
// Now let's try to submit a transaction.
|
||||
// This will be sent to both peers and if both peers endorse the transaction, the endorsed proposal will be sent
|
||||
// to the orderer to be committed by each of the peer's to the channel ledger.
|
||||
console.log('\n--> Submit Transaction: CreateAsset, creates new asset with ID(asset7), color(yellow), size(5), owner(Tom), and appraisedValue(1300) arguments');
|
||||
await contract.submitTransaction('CreateAsset', 'asset7', 'yellow', '5', 'Tom', '1300');
|
||||
console.log('*** Result: committed');
|
||||
|
||||
console.log('\n--> Evaluate Transaction: ReadAsset, function returns information about an asset with ID(asset7)');
|
||||
result = await contract.evaluateTransaction('ReadAsset', 'asset7');
|
||||
console.log(`*** Result: ${prettyJSONString(result.toString())}`);
|
||||
|
||||
console.log('\n--> Evaluate Transaction: AssetExists, function returns "true" if an asset with ID(asset7) exist');
|
||||
result = await contract.evaluateTransaction('AssetExists', 'asset7');
|
||||
console.log(`*** Result: ${prettyJSONString(result.toString())}`);
|
||||
|
||||
// Now let's try to submit a transaction that deletes an asset
|
||||
// This will be sent to both peers and if both peers endorse the transaction, the endorsed proposal will be sent
|
||||
// to the orderer to be committed by each of the peer's to the channel ledger.
|
||||
console.log('\n--> Submit Transaction: DeleteAsset with ID(asset7)');
|
||||
await contract.submitTransaction('DeleteAsset', 'asset7');
|
||||
console.log('*** Result: committed');
|
||||
|
||||
console.log('\n--> Evaluate Transaction: AssetExists, function returns "false" if an asset with ID(asset7) does not exist');
|
||||
result = await contract.evaluateTransaction('AssetExists', 'asset7');
|
||||
console.log(`*** Result: ${prettyJSONString(result.toString())}`);
|
||||
|
||||
console.log('\n--> Submit Transaction: TransferAsset, transfer asset(asset2) to new owner(Max)');
|
||||
await contract.submitTransaction('TransferAsset', 'asset2', 'Max');
|
||||
console.log('*** Result: committed');
|
||||
|
||||
console.log('\n--> Evaluate Transaction: ReadAsset, function returns information about an asset with ID(asset2)');
|
||||
result = await contract.evaluateTransaction('ReadAsset', 'asset2');
|
||||
console.log(`*** Result: ${prettyJSONString(result.toString())}`);
|
||||
|
||||
// Rich Query with Pagination (Only supported if CouchDB is used as state database)
|
||||
console.log('\n--> Evaluate Transaction: QueryAssetsWithPagination, function returns "Max" assets');
|
||||
result = await contract.evaluateTransaction('QueryAssetsWithPagination', '{"selector":{"docType":"asset","owner":"Max"}, "use_index":["_design/indexOwnerDoc", "indexOwner"]}', '1', '');
|
||||
console.log(`*** Result: ${prettyJSONString(result.toString())}`);
|
||||
|
||||
// Recover the bookmark from previous query. Normally it will be inside a variable.
|
||||
const resultJson = JSON.parse(result.toString());
|
||||
|
||||
console.log('\n--> Evaluate Transaction: QueryAssetsWithPagination, function returns "Max" assets next page');
|
||||
result = await contract.evaluateTransaction('QueryAssetsWithPagination', '{"selector":{"docType":"asset","owner":"Max"}, "use_index":["_design/indexOwnerDoc", "indexOwner"]}', '1', resultJson.bookmark);
|
||||
console.log(`*** Result: ${prettyJSONString(result.toString())}`);
|
||||
|
||||
console.log('\n--> Submit Transaction: TransferAssetByColor, transfer all yellow assets to new owner(Michel)');
|
||||
await contract.submitTransaction('TransferAssetByColor', 'yellow', 'Michel');
|
||||
console.log('*** Result: committed');
|
||||
|
||||
// Rich Query (Only supported if CouchDB is used as state database):
|
||||
console.log('\n--> Evaluate Transaction: QueryAssetsByOwner, find all assets with owner(Michel)');
|
||||
result = await contract.evaluateTransaction('QueryAssetsByOwner', 'Michel');
|
||||
console.log(`*** Result: ${prettyJSONString(result.toString())}`);
|
||||
|
||||
console.log('\n--> Evaluate Transaction: GetAssetHistory, get the history of an asset(asset7)');
|
||||
result = await contract.evaluateTransaction('GetAssetHistory', 'asset7');
|
||||
console.log(`*** Result: ${prettyJSONString(result.toString())}`);
|
||||
|
||||
// Rich Query (Only supported if CouchDB is used as state database):
|
||||
console.log('\n--> Evaluate Transaction: QueryAssets, assets of size 15');
|
||||
result = await contract.evaluateTransaction('QueryAssets', '{"selector":{"size":15}}');
|
||||
console.log(`*** Result: ${prettyJSONString(result.toString())}`);
|
||||
|
||||
// Rich Query with index design doc and index name specified (Only supported if CouchDB is used as state database):
|
||||
console.log('\n--> Evaluate Transaction: QueryAssets, Jin Soo\'s assets');
|
||||
result = await contract.evaluateTransaction('QueryAssets', '{"selector":{"docType":"asset","owner":"Jin Soo"}, "use_index":["_design/indexOwnerDoc", "indexOwner"]}');
|
||||
console.log(`*** Result: ${prettyJSONString(result.toString())}`);
|
||||
|
||||
// Range Query with Pagination
|
||||
console.log('\n--> Evaluate Transaction: GetAssetsByRangeWithPagination - get page 1 of assets from asset2 to asset6 (asset2, asset3)');
|
||||
result = await contract.evaluateTransaction('GetAssetsByRangeWithPagination', 'asset2', 'asset6', '2', '');
|
||||
console.log(`*** Result: ${prettyJSONString(result.toString())}`);
|
||||
|
||||
// Range Query with Pagination
|
||||
console.log('\n--> Evaluate Transaction: GetAssetsByRangeWithPagination - get page 2 of assets from asset2 to asset6 (asset4, asset5)');
|
||||
result = await contract.evaluateTransaction('GetAssetsByRangeWithPagination', 'asset2', 'asset6', '2', 'asset4');
|
||||
console.log(`*** Result: ${prettyJSONString(result.toString())}`);
|
||||
|
||||
console.log('*** all tests completed');
|
||||
} finally {
|
||||
// Disconnect from the gateway when the application is closing
|
||||
// This will close all connections to the network
|
||||
gateway.disconnect();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`******** FAILED to run the application: ${error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('*** application ending');
|
||||
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
{
|
||||
"name": "asset-transfer-ledger-queries",
|
||||
"version": "1.0.0",
|
||||
"description": "Asset transfer ledger queries application implemented in JavaScript",
|
||||
"engines": {
|
||||
"node": ">=12",
|
||||
"npm": ">=5"
|
||||
},
|
||||
"engineStrict": true,
|
||||
"author": "Hyperledger",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"run": "node app.js",
|
||||
"lint": "eslint *.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"fabric-ca-client": "^2.2.19",
|
||||
"fabric-network": "^2.2.19"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^7.32.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"index":{"fields":["docType","owner"]},"ddoc":"indexOwnerDoc", "name":"indexOwner","type":"json"}
|
||||
|
|
@ -1,469 +0,0 @@
|
|||
/*
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/*
|
||||
====CHAINCODE EXECUTION SAMPLES (CLI) ==================
|
||||
|
||||
==== Invoke assets ====
|
||||
peer chaincode invoke -C myc1 -n asset_transfer -c '{"Args":["CreateAsset","asset1","blue","5","tom","35"]}'
|
||||
peer chaincode invoke -C myc1 -n asset_transfer -c '{"Args":["CreateAsset","asset2","red","4","tom","50"]}'
|
||||
peer chaincode invoke -C myc1 -n asset_transfer -c '{"Args":["CreateAsset","asset3","blue","6","tom","70"]}'
|
||||
peer chaincode invoke -C myc1 -n asset_transfer -c '{"Args":["TransferAsset","asset2","jerry"]}'
|
||||
peer chaincode invoke -C myc1 -n asset_transfer -c '{"Args":["TransferAssetByColor","blue","jerry"]}'
|
||||
peer chaincode invoke -C myc1 -n asset_transfer -c '{"Args":["DeleteAsset","asset1"]}'
|
||||
|
||||
==== Query assets ====
|
||||
peer chaincode query -C myc1 -n asset_transfer -c '{"Args":["ReadAsset","asset1"]}'
|
||||
peer chaincode query -C myc1 -n asset_transfer -c '{"Args":["GetAssetsByRange","asset1","asset3"]}'
|
||||
peer chaincode query -C myc1 -n asset_transfer -c '{"Args":["GetAssetHistory","asset1"]}'
|
||||
|
||||
Rich Query (Only supported if CouchDB is used as state database):
|
||||
peer chaincode query -C myc1 -n asset_transfer -c '{"Args":["QueryAssetsByOwner","tom"]}'
|
||||
peer chaincode query -C myc1 -n asset_transfer -c '{"Args":["QueryAssets","{\"selector\":{\"owner\":\"tom\"}}"]}'
|
||||
|
||||
Rich Query with Pagination (Only supported if CouchDB is used as state database):
|
||||
peer chaincode query -C myc1 -n asset_transfer -c '{"Args":["QueryAssetsWithPagination","{\"selector\":{\"owner\":\"tom\"}}","3",""]}'
|
||||
|
||||
INDEXES TO SUPPORT COUCHDB RICH QUERIES
|
||||
|
||||
Indexes in CouchDB are required in order to make JSON queries efficient and are required for
|
||||
any JSON query with a sort. Indexes may be packaged alongside
|
||||
chaincode in a META-INF/statedb/couchdb/indexes directory. Each index must be defined in its own
|
||||
text file with extension *.json with the index definition formatted in JSON following the
|
||||
CouchDB index JSON syntax as documented at:
|
||||
http://docs.couchdb.org/en/2.3.1/api/database/find.html#db-index
|
||||
|
||||
This asset transfer ledger example chaincode demonstrates a packaged
|
||||
index which you can find in META-INF/statedb/couchdb/indexes/indexOwner.json.
|
||||
|
||||
If you have access to the your peer's CouchDB state database in a development environment,
|
||||
you may want to iteratively test various indexes in support of your chaincode queries. You
|
||||
can use the CouchDB Fauxton interface or a command line curl utility to create and update
|
||||
indexes. Then once you finalize an index, include the index definition alongside your
|
||||
chaincode in the META-INF/statedb/couchdb/indexes directory, for packaging and deployment
|
||||
to managed environments.
|
||||
|
||||
In the examples below you can find index definitions that support asset transfer ledger
|
||||
chaincode queries, along with the syntax that you can use in development environments
|
||||
to create the indexes in the CouchDB Fauxton interface or a curl command line utility.
|
||||
|
||||
|
||||
Index for docType, owner.
|
||||
|
||||
Example curl command line to define index in the CouchDB channel_chaincode database
|
||||
curl -i -X POST -H "Content-Type: application/json" -d "{\"index\":{\"fields\":[\"docType\",\"owner\"]},\"name\":\"indexOwner\",\"ddoc\":\"indexOwnerDoc\",\"type\":\"json\"}" http://hostname:port/myc1_assets/_index
|
||||
|
||||
|
||||
Index for docType, owner, size (descending order).
|
||||
|
||||
Example curl command line to define index in the CouchDB channel_chaincode database:
|
||||
curl -i -X POST -H "Content-Type: application/json" -d "{\"index\":{\"fields\":[{\"size\":\"desc\"},{\"docType\":\"desc\"},{\"owner\":\"desc\"}]},\"ddoc\":\"indexSizeSortDoc\", \"name\":\"indexSizeSortDesc\",\"type\":\"json\"}" http://hostname:port/myc1_assets/_index
|
||||
|
||||
Rich Query with index design doc and index name specified (Only supported if CouchDB is used as state database):
|
||||
peer chaincode query -C myc1 -n asset_transfer -c '{"Args":["QueryAssets","{\"selector\":{\"docType\":\"asset\",\"owner\":\"tom\"}, \"use_index\":[\"_design/indexOwnerDoc\", \"indexOwner\"]}"]}'
|
||||
|
||||
Rich Query with index design doc specified only (Only supported if CouchDB is used as state database):
|
||||
peer chaincode query -C myc1 -n asset_transfer -c '{"Args":["QueryAssets","{\"selector\":{\"docType\":{\"$eq\":\"asset\"},\"owner\":{\"$eq\":\"tom\"},\"size\":{\"$gt\":0}},\"fields\":[\"docType\",\"owner\",\"size\"],\"sort\":[{\"size\":\"desc\"}],\"use_index\":\"_design/indexSizeSortDoc\"}"]}'
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/hyperledger/fabric-chaincode-go/v2/shim"
|
||||
"github.com/hyperledger/fabric-contract-api-go/v2/contractapi"
|
||||
)
|
||||
|
||||
const index = "color~name"
|
||||
|
||||
// SimpleChaincode implements the fabric-contract-api-go programming model
|
||||
type SimpleChaincode struct {
|
||||
contractapi.Contract
|
||||
}
|
||||
|
||||
type Asset struct {
|
||||
DocType string `json:"docType"` //docType is used to distinguish the various types of objects in state database
|
||||
ID string `json:"ID"` //the field tags are needed to keep case from bouncing around
|
||||
Color string `json:"color"`
|
||||
Size int `json:"size"`
|
||||
Owner string `json:"owner"`
|
||||
AppraisedValue int `json:"appraisedValue"`
|
||||
}
|
||||
|
||||
// HistoryQueryResult structure used for returning result of history query
|
||||
type HistoryQueryResult struct {
|
||||
Record *Asset `json:"record"`
|
||||
TxId string `json:"txId"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
IsDelete bool `json:"isDelete"`
|
||||
}
|
||||
|
||||
// PaginatedQueryResult structure used for returning paginated query results and metadata
|
||||
type PaginatedQueryResult struct {
|
||||
Records []*Asset `json:"records"`
|
||||
FetchedRecordsCount int32 `json:"fetchedRecordsCount"`
|
||||
Bookmark string `json:"bookmark"`
|
||||
}
|
||||
|
||||
// CreateAsset initializes a new asset in the ledger
|
||||
func (t *SimpleChaincode) CreateAsset(ctx contractapi.TransactionContextInterface, assetID, color string, size int, owner string, appraisedValue int) error {
|
||||
exists, err := t.AssetExists(ctx, assetID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get asset: %v", err)
|
||||
}
|
||||
if exists {
|
||||
return fmt.Errorf("asset already exists: %s", assetID)
|
||||
}
|
||||
|
||||
asset := &Asset{
|
||||
DocType: "asset",
|
||||
ID: assetID,
|
||||
Color: color,
|
||||
Size: size,
|
||||
Owner: owner,
|
||||
AppraisedValue: appraisedValue,
|
||||
}
|
||||
assetBytes, err := json.Marshal(asset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ctx.GetStub().PutState(assetID, assetBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create an index to enable color-based range queries, e.g. return all blue assets.
|
||||
// An 'index' is a normal key-value entry in the ledger.
|
||||
// The key is a composite key, with the elements that you want to range query on listed first.
|
||||
// In our case, the composite key is based on indexName~color~name.
|
||||
// This will enable very efficient state range queries based on composite keys matching indexName~color~*
|
||||
colorNameIndexKey, err := ctx.GetStub().CreateCompositeKey(index, []string{asset.Color, asset.ID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Save index entry to world state. Only the key name is needed, no need to store a duplicate copy of the asset.
|
||||
// Note - passing a 'nil' value will effectively delete the key from state, therefore we pass null character as value
|
||||
value := []byte{0x00}
|
||||
return ctx.GetStub().PutState(colorNameIndexKey, value)
|
||||
}
|
||||
|
||||
// ReadAsset retrieves an asset from the ledger
|
||||
func (t *SimpleChaincode) ReadAsset(ctx contractapi.TransactionContextInterface, assetID string) (*Asset, error) {
|
||||
assetBytes, err := ctx.GetStub().GetState(assetID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get asset %s: %v", assetID, err)
|
||||
}
|
||||
if assetBytes == nil {
|
||||
return nil, fmt.Errorf("asset %s does not exist", assetID)
|
||||
}
|
||||
|
||||
var asset Asset
|
||||
err = json.Unmarshal(assetBytes, &asset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &asset, nil
|
||||
}
|
||||
|
||||
// DeleteAsset removes an asset key-value pair from the ledger
|
||||
func (t *SimpleChaincode) DeleteAsset(ctx contractapi.TransactionContextInterface, assetID string) error {
|
||||
asset, err := t.ReadAsset(ctx, assetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ctx.GetStub().DelState(assetID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete asset %s: %v", assetID, err)
|
||||
}
|
||||
|
||||
colorNameIndexKey, err := ctx.GetStub().CreateCompositeKey(index, []string{asset.Color, asset.ID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete index entry
|
||||
return ctx.GetStub().DelState(colorNameIndexKey)
|
||||
}
|
||||
|
||||
// TransferAsset transfers an asset by setting a new owner name on the asset
|
||||
func (t *SimpleChaincode) TransferAsset(ctx contractapi.TransactionContextInterface, assetID, newOwner string) error {
|
||||
asset, err := t.ReadAsset(ctx, assetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
asset.Owner = newOwner
|
||||
assetBytes, err := json.Marshal(asset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ctx.GetStub().PutState(assetID, assetBytes)
|
||||
}
|
||||
|
||||
// constructQueryResponseFromIterator constructs a slice of assets from the resultsIterator
|
||||
func constructQueryResponseFromIterator(resultsIterator shim.StateQueryIteratorInterface) ([]*Asset, error) {
|
||||
var assets []*Asset
|
||||
for resultsIterator.HasNext() {
|
||||
queryResult, err := resultsIterator.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var asset Asset
|
||||
err = json.Unmarshal(queryResult.Value, &asset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
assets = append(assets, &asset)
|
||||
}
|
||||
|
||||
return assets, nil
|
||||
}
|
||||
|
||||
// GetAssetsByRange performs a range query based on the start and end keys provided.
|
||||
// Read-only function results are not typically submitted to ordering. If the read-only
|
||||
// results are submitted to ordering, or if the query is used in an update transaction
|
||||
// and submitted to ordering, then the committing peers will re-execute to guarantee that
|
||||
// result sets are stable between endorsement time and commit time. The transaction is
|
||||
// invalidated by the committing peers if the result set has changed between endorsement
|
||||
// time and commit time.
|
||||
// Therefore, range queries are a safe option for performing update transactions based on query results.
|
||||
func (t *SimpleChaincode) GetAssetsByRange(ctx contractapi.TransactionContextInterface, startKey, endKey string) ([]*Asset, error) {
|
||||
resultsIterator, err := ctx.GetStub().GetStateByRange(startKey, endKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resultsIterator.Close()
|
||||
|
||||
return constructQueryResponseFromIterator(resultsIterator)
|
||||
}
|
||||
|
||||
// TransferAssetByColor will transfer assets of a given color to a certain new owner.
|
||||
// Uses GetStateByPartialCompositeKey (range query) against color~name 'index'.
|
||||
// Committing peers will re-execute range queries to guarantee that result sets are stable
|
||||
// between endorsement time and commit time. The transaction is invalidated by the
|
||||
// committing peers if the result set has changed between endorsement time and commit time.
|
||||
// Therefore, range queries are a safe option for performing update transactions based on query results.
|
||||
// Example: GetStateByPartialCompositeKey/RangeQuery
|
||||
func (t *SimpleChaincode) TransferAssetByColor(ctx contractapi.TransactionContextInterface, color, newOwner string) error {
|
||||
// Execute a key range query on all keys starting with 'color'
|
||||
coloredAssetResultsIterator, err := ctx.GetStub().GetStateByPartialCompositeKey(index, []string{color})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer coloredAssetResultsIterator.Close()
|
||||
|
||||
for coloredAssetResultsIterator.HasNext() {
|
||||
responseRange, err := coloredAssetResultsIterator.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, compositeKeyParts, err := ctx.GetStub().SplitCompositeKey(responseRange.Key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(compositeKeyParts) > 1 {
|
||||
returnedAssetID := compositeKeyParts[1]
|
||||
asset, err := t.ReadAsset(ctx, returnedAssetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
asset.Owner = newOwner
|
||||
assetBytes, err := json.Marshal(asset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ctx.GetStub().PutState(returnedAssetID, assetBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("transfer failed for asset %s: %v", returnedAssetID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// QueryAssetsByOwner queries for assets based on the owners name.
|
||||
// This is an example of a parameterized query where the query logic is baked into the chaincode,
|
||||
// and accepting a single query parameter (owner).
|
||||
// Only available on state databases that support rich query (e.g. CouchDB)
|
||||
// Example: Parameterized rich query
|
||||
func (t *SimpleChaincode) QueryAssetsByOwner(ctx contractapi.TransactionContextInterface, owner string) ([]*Asset, error) {
|
||||
queryString := fmt.Sprintf(`{"selector":{"docType":"asset","owner":"%s"}}`, owner)
|
||||
return getQueryResultForQueryString(ctx, queryString)
|
||||
}
|
||||
|
||||
// QueryAssets uses a query string to perform a query for assets.
|
||||
// Query string matching state database syntax is passed in and executed as is.
|
||||
// Supports ad hoc queries that can be defined at runtime by the client.
|
||||
// If this is not desired, follow the QueryAssetsForOwner example for parameterized queries.
|
||||
// Only available on state databases that support rich query (e.g. CouchDB)
|
||||
// Example: Ad hoc rich query
|
||||
func (t *SimpleChaincode) QueryAssets(ctx contractapi.TransactionContextInterface, queryString string) ([]*Asset, error) {
|
||||
return getQueryResultForQueryString(ctx, queryString)
|
||||
}
|
||||
|
||||
// getQueryResultForQueryString executes the passed in query string.
|
||||
// The result set is built and returned as a byte array containing the JSON results.
|
||||
func getQueryResultForQueryString(ctx contractapi.TransactionContextInterface, queryString string) ([]*Asset, error) {
|
||||
resultsIterator, err := ctx.GetStub().GetQueryResult(queryString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resultsIterator.Close()
|
||||
|
||||
return constructQueryResponseFromIterator(resultsIterator)
|
||||
}
|
||||
|
||||
// GetAssetsByRangeWithPagination performs a range query based on the start and end key,
|
||||
// page size and a bookmark.
|
||||
// The number of fetched records will be equal to or lesser than the page size.
|
||||
// Paginated range queries are only valid for read only transactions.
|
||||
// Example: Pagination with Range Query
|
||||
func (t *SimpleChaincode) GetAssetsByRangeWithPagination(ctx contractapi.TransactionContextInterface, startKey string, endKey string, pageSize int, bookmark string) (*PaginatedQueryResult, error) {
|
||||
|
||||
resultsIterator, responseMetadata, err := ctx.GetStub().GetStateByRangeWithPagination(startKey, endKey, int32(pageSize), bookmark)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resultsIterator.Close()
|
||||
|
||||
assets, err := constructQueryResponseFromIterator(resultsIterator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &PaginatedQueryResult{
|
||||
Records: assets,
|
||||
FetchedRecordsCount: responseMetadata.FetchedRecordsCount,
|
||||
Bookmark: responseMetadata.Bookmark,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// QueryAssetsWithPagination uses a query string, page size and a bookmark to perform a query
|
||||
// for assets. Query string matching state database syntax is passed in and executed as is.
|
||||
// The number of fetched records would be equal to or lesser than the specified page size.
|
||||
// Supports ad hoc queries that can be defined at runtime by the client.
|
||||
// If this is not desired, follow the QueryAssetsForOwner example for parameterized queries.
|
||||
// Only available on state databases that support rich query (e.g. CouchDB)
|
||||
// Paginated queries are only valid for read only transactions.
|
||||
// Example: Pagination with Ad hoc Rich Query
|
||||
func (t *SimpleChaincode) QueryAssetsWithPagination(ctx contractapi.TransactionContextInterface, queryString string, pageSize int, bookmark string) (*PaginatedQueryResult, error) {
|
||||
|
||||
return getQueryResultForQueryStringWithPagination(ctx, queryString, int32(pageSize), bookmark)
|
||||
}
|
||||
|
||||
// getQueryResultForQueryStringWithPagination executes the passed in query string with
|
||||
// pagination info. The result set is built and returned as a byte array containing the JSON results.
|
||||
func getQueryResultForQueryStringWithPagination(ctx contractapi.TransactionContextInterface, queryString string, pageSize int32, bookmark string) (*PaginatedQueryResult, error) {
|
||||
|
||||
resultsIterator, responseMetadata, err := ctx.GetStub().GetQueryResultWithPagination(queryString, pageSize, bookmark)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resultsIterator.Close()
|
||||
|
||||
assets, err := constructQueryResponseFromIterator(resultsIterator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &PaginatedQueryResult{
|
||||
Records: assets,
|
||||
FetchedRecordsCount: responseMetadata.FetchedRecordsCount,
|
||||
Bookmark: responseMetadata.Bookmark,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetAssetHistory returns the chain of custody for an asset since issuance.
|
||||
func (t *SimpleChaincode) GetAssetHistory(ctx contractapi.TransactionContextInterface, assetID string) ([]HistoryQueryResult, error) {
|
||||
log.Printf("GetAssetHistory: ID %v", assetID)
|
||||
|
||||
resultsIterator, err := ctx.GetStub().GetHistoryForKey(assetID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resultsIterator.Close()
|
||||
|
||||
var records []HistoryQueryResult
|
||||
for resultsIterator.HasNext() {
|
||||
response, err := resultsIterator.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var asset Asset
|
||||
if len(response.Value) > 0 {
|
||||
err = json.Unmarshal(response.Value, &asset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
asset = Asset{
|
||||
ID: assetID,
|
||||
}
|
||||
}
|
||||
|
||||
record := HistoryQueryResult{
|
||||
TxId: response.TxId,
|
||||
Timestamp: response.Timestamp.AsTime(),
|
||||
Record: &asset,
|
||||
IsDelete: response.IsDelete,
|
||||
}
|
||||
records = append(records, record)
|
||||
}
|
||||
|
||||
return records, nil
|
||||
}
|
||||
|
||||
// AssetExists returns true when asset with given ID exists in the ledger.
|
||||
func (t *SimpleChaincode) AssetExists(ctx contractapi.TransactionContextInterface, assetID string) (bool, error) {
|
||||
assetBytes, err := ctx.GetStub().GetState(assetID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to read asset %s from world state. %v", assetID, err)
|
||||
}
|
||||
|
||||
return assetBytes != nil, nil
|
||||
}
|
||||
|
||||
// InitLedger creates the initial set of assets in the ledger.
|
||||
func (t *SimpleChaincode) InitLedger(ctx contractapi.TransactionContextInterface) error {
|
||||
assets := []Asset{
|
||||
{DocType: "asset", ID: "asset1", Color: "blue", Size: 5, Owner: "Tomoko", AppraisedValue: 300},
|
||||
{DocType: "asset", ID: "asset2", Color: "red", Size: 5, Owner: "Brad", AppraisedValue: 400},
|
||||
{DocType: "asset", ID: "asset3", Color: "green", Size: 10, Owner: "Jin Soo", AppraisedValue: 500},
|
||||
{DocType: "asset", ID: "asset4", Color: "yellow", Size: 10, Owner: "Max", AppraisedValue: 600},
|
||||
{DocType: "asset", ID: "asset5", Color: "black", Size: 15, Owner: "Adriana", AppraisedValue: 700},
|
||||
{DocType: "asset", ID: "asset6", Color: "white", Size: 15, Owner: "Michel", AppraisedValue: 800},
|
||||
}
|
||||
|
||||
for _, asset := range assets {
|
||||
err := t.CreateAsset(ctx, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
chaincode, err := contractapi.NewChaincode(&SimpleChaincode{})
|
||||
if err != nil {
|
||||
log.Panicf("Error creating asset chaincode: %v", err)
|
||||
}
|
||||
|
||||
if err := chaincode.Start(); err != nil {
|
||||
log.Panicf("Error starting asset chaincode: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
module github.com/hyperledger/fabric-samples/asset-transfer-ledger-queries/chaincode-go
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/hyperledger/fabric-chaincode-go/v2 v2.0.0-20240618210511-f7903324a8af
|
||||
github.com/hyperledger/fabric-contract-api-go/v2 v2.0.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/spec v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/gobuffalo/envy v1.10.2 // indirect
|
||||
github.com/gobuffalo/packd v1.0.2 // indirect
|
||||
github.com/gobuffalo/packr v1.30.1 // indirect
|
||||
github.com/hyperledger/fabric-protos-go-apiv2 v0.3.3 // indirect
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/net v0.24.0 // indirect
|
||||
golang.org/x/sys v0.19.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
|
||||
google.golang.org/grpc v1.64.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
||||
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
||||
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
|
||||
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
|
||||
github.com/gobuffalo/envy v1.10.2 h1:EIi03p9c3yeuRCFPOKcSfajzkLb3hrRjEpHGI8I2Wo4=
|
||||
github.com/gobuffalo/envy v1.10.2/go.mod h1:qGAGwdvDsaEtPhfBzb3o0SfDea8ByGn9j8bKmVft9z8=
|
||||
github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs=
|
||||
github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q=
|
||||
github.com/gobuffalo/packd v1.0.2 h1:Yg523YqnOxGIWCp69W12yYBKsoChwI7mtu6ceM9Bwfw=
|
||||
github.com/gobuffalo/packd v1.0.2/go.mod h1:sUc61tDqGMXON80zpKGp92lDb86Km28jfvX7IAyxFT8=
|
||||
github.com/gobuffalo/packr v1.30.1 h1:hu1fuVR3fXEZR7rXNW3h8rqSML8EVAf6KNm0NKO/wKg=
|
||||
github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIaviy289eRuk=
|
||||
github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hyperledger/fabric-chaincode-go/v2 v2.0.0-20240618210511-f7903324a8af h1:WT4NjX7Uk03GSeH++jF3a0wp4FhybTM86zDPCETvmSk=
|
||||
github.com/hyperledger/fabric-chaincode-go/v2 v2.0.0-20240618210511-f7903324a8af/go.mod h1:f/ER25FaBepxJugwpLhbD2hLAoZaZEVqkBjOcHjw72Y=
|
||||
github.com/hyperledger/fabric-contract-api-go/v2 v2.0.0 h1:IDiCGVOBlRd6zpL0Y+f6V7IpBqa4/Z5JAK9SF7a5ea8=
|
||||
github.com/hyperledger/fabric-contract-api-go/v2 v2.0.0/go.mod h1:pdqhe7ALf4lmXgQdprCyNWYdnCPxgj02Vhf8JF5w8po=
|
||||
github.com/hyperledger/fabric-protos-go-apiv2 v0.3.3 h1:Xpd6fzG/KjAOHJsq7EQXY2l+qi/y8muxBaY7R6QWABk=
|
||||
github.com/hyperledger/fabric-protos-go-apiv2 v0.3.3/go.mod h1:2pq0ui6ZWA0cC8J+eCErgnMDCS1kPOEYVY+06ZAK0qE=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w=
|
||||
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
|
||||
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
|
||||
google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
coverage
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
env: {
|
||||
node: true,
|
||||
mocha: true
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 8,
|
||||
sourceType: 'script'
|
||||
},
|
||||
extends: 'eslint:recommended',
|
||||
rules: {
|
||||
indent: ['error', 'tab'],
|
||||
'linebreak-style': ['error', 'unix'],
|
||||
quotes: ['error', 'single'],
|
||||
semi: ['error', 'always'],
|
||||
'no-unused-vars': ['error', { args: 'none' }],
|
||||
'no-console': 'off',
|
||||
curly: 'error',
|
||||
eqeqeq: 'error',
|
||||
'no-throw-literal': 'error',
|
||||
strict: 'error',
|
||||
'no-var': 'error',
|
||||
'dot-notation': 'error',
|
||||
'no-trailing-spaces': 'error',
|
||||
'no-use-before-define': 'error',
|
||||
'no-useless-call': 'error',
|
||||
'no-with': 'error',
|
||||
'operator-linebreak': 'error',
|
||||
yoda: 'error',
|
||||
'quote-props': ['error', 'as-needed']
|
||||
}
|
||||
};
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"index":{"fields":["docType","owner"]},"ddoc":"indexOwnerDoc", "name":"indexOwner","type":"json"}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
/*
|
||||
* Copyright IBM Corp. All Rights Reserved.
|
||||
*
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const CC = require('./lib/asset_transfer_ledger_chaincode.js');
|
||||
|
||||
module.exports.CC = CC;
|
||||
module.exports.contracts = [ CC ];
|
||||
|
|
@ -1,411 +0,0 @@
|
|||
/*
|
||||
* Copyright IBM Corp. All Rights Reserved.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
// ====CHAINCODE EXECUTION SAMPLES (CLI) ==================
|
||||
|
||||
// ==== Invoke assets ====
|
||||
// peer chaincode invoke -C CHANNEL_NAME -n asset_transfer -c '{"Args":["CreateAsset","asset1","blue","35","Tom","100"]}'
|
||||
// peer chaincode invoke -C CHANNEL_NAME -n asset_transfer -c '{"Args":["CreateAsset","asset2","red","50","Tom","150"]}'
|
||||
// peer chaincode invoke -C CHANNEL_NAME -n asset_transfer -c '{"Args":["CreateAsset","asset3","blue","70","Tom","200"]}'
|
||||
// peer chaincode invoke -C CHANNEL_NAME -n asset_transfer -c '{"Args":["TransferAsset","asset2","jerry"]}'
|
||||
// peer chaincode invoke -C CHANNEL_NAME -n asset_transfer -c '{"Args":["TransferAssetByColor","blue","jerry"]}'
|
||||
// peer chaincode invoke -C CHANNEL_NAME -n asset_transfer -c '{"Args":["DeleteAsset","asset1"]}'
|
||||
|
||||
// ==== Query assets ====
|
||||
// peer chaincode query -C CHANNEL_NAME -n asset_transfer -c '{"Args":["ReadAsset","asset1"]}'
|
||||
// peer chaincode query -C CHANNEL_NAME -n asset_transfer -c '{"Args":["GetAssetsByRange","asset1","asset3"]}'
|
||||
// peer chaincode query -C CHANNEL_NAME -n asset_transfer -c '{"Args":["GetAssetHistory","asset1"]}'
|
||||
|
||||
// Rich Query (Only supported if CouchDB is used as state database):
|
||||
// peer chaincode query -C CHANNEL_NAME -n asset_transfer -c '{"Args":["QueryAssetsByOwner","Tom"]}' output issue
|
||||
// peer chaincode query -C CHANNEL_NAME -n asset_transfer -c '{"Args":["QueryAssets","{\"selector\":{\"owner\":\"Tom\"}}"]}'
|
||||
|
||||
// Rich Query with Pagination (Only supported if CouchDB is used as state database):
|
||||
// peer chaincode query -C CHANNEL_NAME -n asset_transfer -c '{"Args":["QueryAssetsWithPagination","{\"selector\":{\"owner\":\"Tom\"}}","3",""]}'
|
||||
|
||||
// INDEXES TO SUPPORT COUCHDB RICH QUERIES
|
||||
//
|
||||
// Indexes in CouchDB are required in order to make JSON queries efficient and are required for
|
||||
// any JSON query with a sort. Indexes may be packaged alongside
|
||||
// chaincode in a META-INF/statedb/couchdb/indexes directory. Each index must be defined in its own
|
||||
// text file with extension *.json with the index definition formatted in JSON following the
|
||||
// CouchDB index JSON syntax as documented at:
|
||||
// http://docs.couchdb.org/en/2.3.1/api/database/find.html#db-index
|
||||
//
|
||||
// This asset transfer ledger example chaincode demonstrates a packaged
|
||||
// index which you can find in META-INF/statedb/couchdb/indexes/indexOwner.json.
|
||||
//
|
||||
// If you have access to the your peer's CouchDB state database in a development environment,
|
||||
// you may want to iteratively test various indexes in support of your chaincode queries. You
|
||||
// can use the CouchDB Fauxton interface or a command line curl utility to create and update
|
||||
// indexes. Then once you finalize an index, include the index definition alongside your
|
||||
// chaincode in the META-INF/statedb/couchdb/indexes directory, for packaging and deployment
|
||||
// to managed environments.
|
||||
//
|
||||
// In the examples below you can find index definitions that support asset transfer ledger
|
||||
// chaincode queries, along with the syntax that you can use in development environments
|
||||
// to create the indexes in the CouchDB Fauxton interface or a curl command line utility.
|
||||
//
|
||||
|
||||
// Index for docType, owner.
|
||||
//
|
||||
// Example curl command line to define index in the CouchDB channel_chaincode database
|
||||
// curl -i -X POST -H "Content-Type: application/json" -d "{\"index\":{\"fields\":[\"docType\",\"owner\"]},\"name\":\"indexOwner\",\"ddoc\":\"indexOwnerDoc\",\"type\":\"json\"}" http://hostname:port/myc1_assets/_index
|
||||
//
|
||||
|
||||
// Index for docType, owner, size (descending order).
|
||||
//
|
||||
// Example curl command line to define index in the CouchDB channel_chaincode database
|
||||
// curl -i -X POST -H "Content-Type: application/json" -d "{\"index\":{\"fields\":[{\"size\":\"desc\"},{\"docType\":\"desc\"},{\"owner\":\"desc\"}]},\"ddoc\":\"indexSizeSortDoc\", \"name\":\"indexSizeSortDesc\",\"type\":\"json\"}" http://hostname:port/myc1_assets/_index
|
||||
|
||||
// Rich Query with index design doc and index name specified (Only supported if CouchDB is used as state database):
|
||||
// peer chaincode query -C CHANNEL_NAME -n ledger -c '{"Args":["QueryAssets","{\"selector\":{\"docType\":\"asset\",\"owner\":\"Tom\"}, \"use_index\":[\"_design/indexOwnerDoc\", \"indexOwner\"]}"]}'
|
||||
|
||||
// Rich Query with index design doc specified only (Only supported if CouchDB is used as state database):
|
||||
// peer chaincode query -C CHANNEL_NAME -n ledger -c '{"Args":["QueryAssets","{\"selector\":{\"docType\":{\"$eq\":\"asset\"},\"owner\":{\"$eq\":\"Tom\"},\"size\":{\"$gt\":0}},\"fields\":[\"docType\",\"owner\",\"size\"],\"sort\":[{\"size\":\"desc\"}],\"use_index\":\"_design/indexSizeSortDoc\"}"]}'
|
||||
|
||||
'use strict';
|
||||
|
||||
const {Contract} = require('fabric-contract-api');
|
||||
|
||||
class Chaincode extends Contract {
|
||||
|
||||
// CreateAsset - create a new asset, store into chaincode state
|
||||
async CreateAsset(ctx, assetID, color, size, owner, appraisedValue) {
|
||||
const exists = await this.AssetExists(ctx, assetID);
|
||||
if (exists) {
|
||||
throw new Error(`The asset ${assetID} already exists`);
|
||||
}
|
||||
|
||||
// ==== Create asset object and marshal to JSON ====
|
||||
let asset = {
|
||||
docType: 'asset',
|
||||
assetID: assetID,
|
||||
color: color,
|
||||
size: size,
|
||||
owner: owner,
|
||||
appraisedValue: appraisedValue
|
||||
};
|
||||
|
||||
|
||||
// === Save asset to state ===
|
||||
await ctx.stub.putState(assetID, Buffer.from(JSON.stringify(asset)));
|
||||
let indexName = 'color~name';
|
||||
let colorNameIndexKey = await ctx.stub.createCompositeKey(indexName, [asset.color, asset.assetID]);
|
||||
|
||||
// Save index entry to state. Only the key name is needed, no need to store a duplicate copy of the marble.
|
||||
// Note - passing a 'nil' value will effectively delete the key from state, therefore we pass null character as value
|
||||
await ctx.stub.putState(colorNameIndexKey, Buffer.from('\u0000'));
|
||||
}
|
||||
|
||||
// ReadAsset returns the asset stored in the world state with given id.
|
||||
async ReadAsset(ctx, id) {
|
||||
const assetJSON = await ctx.stub.getState(id); // get the asset from chaincode state
|
||||
if (!assetJSON || assetJSON.length === 0) {
|
||||
throw new Error(`Asset ${id} does not exist`);
|
||||
}
|
||||
|
||||
return assetJSON.toString();
|
||||
}
|
||||
|
||||
// delete - remove a asset key/value pair from state
|
||||
async DeleteAsset(ctx, id) {
|
||||
if (!id) {
|
||||
throw new Error('Asset name must not be empty');
|
||||
}
|
||||
|
||||
let exists = await this.AssetExists(ctx, id);
|
||||
if (!exists) {
|
||||
throw new Error(`Asset ${id} does not exist`);
|
||||
}
|
||||
|
||||
// to maintain the color~name index, we need to read the asset first and get its color
|
||||
let valAsbytes = await ctx.stub.getState(id); // get the asset from chaincode state
|
||||
let jsonResp = {};
|
||||
if (!valAsbytes) {
|
||||
jsonResp.error = `Asset does not exist: ${id}`;
|
||||
throw new Error(jsonResp);
|
||||
}
|
||||
let assetJSON;
|
||||
try {
|
||||
assetJSON = JSON.parse(valAsbytes.toString());
|
||||
} catch (err) {
|
||||
jsonResp = {};
|
||||
jsonResp.error = `Failed to decode JSON of: ${id}`;
|
||||
throw new Error(jsonResp);
|
||||
}
|
||||
await ctx.stub.deleteState(id); //remove the asset from chaincode state
|
||||
|
||||
// delete the index
|
||||
let indexName = 'color~name';
|
||||
let colorNameIndexKey = ctx.stub.createCompositeKey(indexName, [assetJSON.color, assetJSON.assetID]);
|
||||
if (!colorNameIndexKey) {
|
||||
throw new Error(' Failed to create the createCompositeKey');
|
||||
}
|
||||
// Delete index entry to state.
|
||||
await ctx.stub.deleteState(colorNameIndexKey);
|
||||
}
|
||||
|
||||
// TransferAsset transfers a asset by setting a new owner name on the asset
|
||||
async TransferAsset(ctx, assetName, newOwner) {
|
||||
|
||||
let assetAsBytes = await ctx.stub.getState(assetName);
|
||||
if (!assetAsBytes || !assetAsBytes.toString()) {
|
||||
throw new Error(`Asset ${assetName} does not exist`);
|
||||
}
|
||||
let assetToTransfer = {};
|
||||
try {
|
||||
assetToTransfer = JSON.parse(assetAsBytes.toString()); //unmarshal
|
||||
} catch (err) {
|
||||
let jsonResp = {};
|
||||
jsonResp.error = 'Failed to decode JSON of: ' + assetName;
|
||||
throw new Error(jsonResp);
|
||||
}
|
||||
assetToTransfer.owner = newOwner; //change the owner
|
||||
|
||||
let assetJSONasBytes = Buffer.from(JSON.stringify(assetToTransfer));
|
||||
await ctx.stub.putState(assetName, assetJSONasBytes); //rewrite the asset
|
||||
}
|
||||
|
||||
// GetAssetsByRange performs a range query based on the start and end keys provided.
|
||||
// Read-only function results are not typically submitted to ordering. If the read-only
|
||||
// results are submitted to ordering, or if the query is used in an update transaction
|
||||
// and submitted to ordering, then the committing peers will re-execute to guarantee that
|
||||
// result sets are stable between endorsement time and commit time. The transaction is
|
||||
// invalidated by the committing peers if the result set has changed between endorsement
|
||||
// time and commit time.
|
||||
// Therefore, range queries are a safe option for performing update transactions based on query results.
|
||||
async GetAssetsByRange(ctx, startKey, endKey) {
|
||||
|
||||
let resultsIterator = await ctx.stub.getStateByRange(startKey, endKey);
|
||||
let results = await this._GetAllResults(resultsIterator, false);
|
||||
|
||||
return JSON.stringify(results);
|
||||
}
|
||||
|
||||
// TransferAssetByColor will transfer assets of a given color to a certain new owner.
|
||||
// Uses a GetStateByPartialCompositeKey (range query) against color~name 'index'.
|
||||
// Committing peers will re-execute range queries to guarantee that result sets are stable
|
||||
// between endorsement time and commit time. The transaction is invalidated by the
|
||||
// committing peers if the result set has changed between endorsement time and commit time.
|
||||
// Therefore, range queries are a safe option for performing update transactions based on query results.
|
||||
// Example: GetStateByPartialCompositeKey/RangeQuery
|
||||
async TransferAssetByColor(ctx, color, newOwner) {
|
||||
// Query the color~name index by color
|
||||
// This will execute a key range query on all keys starting with 'color'
|
||||
let coloredAssetResultsIterator = await ctx.stub.getStateByPartialCompositeKey('color~name', [color]);
|
||||
|
||||
// Iterate through result set and for each asset found, transfer to newOwner
|
||||
let responseRange = await coloredAssetResultsIterator.next();
|
||||
while (!responseRange.done) {
|
||||
if (!responseRange || !responseRange.value || !responseRange.value.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
let objectType;
|
||||
let attributes;
|
||||
(
|
||||
{objectType, attributes} = await ctx.stub.splitCompositeKey(responseRange.value.key)
|
||||
);
|
||||
|
||||
console.log(objectType);
|
||||
let returnedAssetName = attributes[1];
|
||||
|
||||
// Now call the transfer function for the found asset.
|
||||
// Re-use the same function that is used to transfer individual assets
|
||||
await this.TransferAsset(ctx, returnedAssetName, newOwner);
|
||||
responseRange = await coloredAssetResultsIterator.next();
|
||||
}
|
||||
}
|
||||
|
||||
// QueryAssetsByOwner queries for assets based on a passed in owner.
|
||||
// This is an example of a parameterized query where the query logic is baked into the chaincode,
|
||||
// and accepting a single query parameter (owner).
|
||||
// Only available on state databases that support rich query (e.g. CouchDB)
|
||||
// Example: Parameterized rich query
|
||||
async QueryAssetsByOwner(ctx, owner) {
|
||||
let queryString = {};
|
||||
queryString.selector = {};
|
||||
queryString.selector.docType = 'asset';
|
||||
queryString.selector.owner = owner;
|
||||
return await this.GetQueryResultForQueryString(ctx, JSON.stringify(queryString)); //shim.success(queryResults);
|
||||
}
|
||||
|
||||
// Example: Ad hoc rich query
|
||||
// QueryAssets uses a query string to perform a query for assets.
|
||||
// Query string matching state database syntax is passed in and executed as is.
|
||||
// Supports ad hoc queries that can be defined at runtime by the client.
|
||||
// If this is not desired, follow the QueryAssetsForOwner example for parameterized queries.
|
||||
// Only available on state databases that support rich query (e.g. CouchDB)
|
||||
async QueryAssets(ctx, queryString) {
|
||||
return await this.GetQueryResultForQueryString(ctx, queryString);
|
||||
}
|
||||
|
||||
// GetQueryResultForQueryString executes the passed in query string.
|
||||
// Result set is built and returned as a byte array containing the JSON results.
|
||||
async GetQueryResultForQueryString(ctx, queryString) {
|
||||
|
||||
let resultsIterator = await ctx.stub.getQueryResult(queryString);
|
||||
let results = await this._GetAllResults(resultsIterator, false);
|
||||
|
||||
return JSON.stringify(results);
|
||||
}
|
||||
|
||||
// Example: Pagination with Range Query
|
||||
// GetAssetsByRangeWithPagination performs a range query based on the start & end key,
|
||||
// page size and a bookmark.
|
||||
// The number of fetched records will be equal to or lesser than the page size.
|
||||
// Paginated range queries are only valid for read only transactions.
|
||||
async GetAssetsByRangeWithPagination(ctx, startKey, endKey, pageSize, bookmark) {
|
||||
|
||||
const {iterator, metadata} = await ctx.stub.getStateByRangeWithPagination(startKey, endKey, pageSize, bookmark);
|
||||
let results = {};
|
||||
|
||||
results.results = await this._GetAllResults(iterator, false);
|
||||
|
||||
results.fetchedRecordsCount = metadata.fetchedRecordsCount;
|
||||
|
||||
results.bookmark = metadata.bookmark;
|
||||
|
||||
return JSON.stringify(results);
|
||||
}
|
||||
|
||||
// Example: Pagination with Ad hoc Rich Query
|
||||
// QueryAssetsWithPagination uses a query string, page size and a bookmark to perform a query
|
||||
// for assets. Query string matching state database syntax is passed in and executed as is.
|
||||
// The number of fetched records would be equal to or lesser than the specified page size.
|
||||
// Supports ad hoc queries that can be defined at runtime by the client.
|
||||
// If this is not desired, follow the QueryAssetsForOwner example for parameterized queries.
|
||||
// Only available on state databases that support rich query (e.g. CouchDB)
|
||||
// Paginated queries are only valid for read only transactions.
|
||||
async QueryAssetsWithPagination(ctx, queryString, pageSize, bookmark) {
|
||||
|
||||
const {iterator, metadata} = await ctx.stub.getQueryResultWithPagination(queryString, pageSize, bookmark);
|
||||
let results = {};
|
||||
|
||||
results.results = await this._GetAllResults(iterator, false);
|
||||
|
||||
results.fetchedRecordsCount = metadata.fetchedRecordsCount;
|
||||
|
||||
results.bookmark = metadata.bookmark;
|
||||
|
||||
return JSON.stringify(results);
|
||||
}
|
||||
|
||||
// GetAssetHistory returns the chain of custody for an asset since issuance.
|
||||
async GetAssetHistory(ctx, assetName) {
|
||||
|
||||
let resultsIterator = await ctx.stub.getHistoryForKey(assetName);
|
||||
let results = await this._GetAllResults(resultsIterator, true);
|
||||
|
||||
return JSON.stringify(results);
|
||||
}
|
||||
|
||||
// AssetExists returns true when asset with given ID exists in world state
|
||||
async AssetExists(ctx, assetName) {
|
||||
// ==== Check if asset already exists ====
|
||||
let assetState = await ctx.stub.getState(assetName);
|
||||
return assetState && assetState.length > 0;
|
||||
}
|
||||
|
||||
// This is JavaScript so without Funcation Decorators, all functions are assumed
|
||||
// to be transaction functions
|
||||
//
|
||||
// For internal functions... prefix them with _
|
||||
async _GetAllResults(iterator, isHistory) {
|
||||
let allResults = [];
|
||||
let res = await iterator.next();
|
||||
while (!res.done) {
|
||||
if (res.value && res.value.value.toString()) {
|
||||
let jsonRes = {};
|
||||
console.log(res.value.value.toString('utf8'));
|
||||
if (isHistory && isHistory === true) {
|
||||
jsonRes.TxId = res.value.txId;
|
||||
jsonRes.Timestamp = res.value.timestamp;
|
||||
try {
|
||||
jsonRes.Value = JSON.parse(res.value.value.toString('utf8'));
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
jsonRes.Value = res.value.value.toString('utf8');
|
||||
}
|
||||
} else {
|
||||
jsonRes.Key = res.value.key;
|
||||
try {
|
||||
jsonRes.Record = JSON.parse(res.value.value.toString('utf8'));
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
jsonRes.Record = res.value.value.toString('utf8');
|
||||
}
|
||||
}
|
||||
allResults.push(jsonRes);
|
||||
}
|
||||
res = await iterator.next();
|
||||
}
|
||||
iterator.close();
|
||||
return allResults;
|
||||
}
|
||||
|
||||
// InitLedger creates sample assets in the ledger
|
||||
async InitLedger(ctx) {
|
||||
const assets = [
|
||||
{
|
||||
assetID: 'asset1',
|
||||
color: 'blue',
|
||||
size: 5,
|
||||
owner: 'Tom',
|
||||
appraisedValue: 100
|
||||
},
|
||||
{
|
||||
assetID: 'asset2',
|
||||
color: 'red',
|
||||
size: 5,
|
||||
owner: 'Brad',
|
||||
appraisedValue: 100
|
||||
},
|
||||
{
|
||||
assetID: 'asset3',
|
||||
color: 'green',
|
||||
size: 10,
|
||||
owner: 'Jin Soo',
|
||||
appraisedValue: 200
|
||||
},
|
||||
{
|
||||
assetID: 'asset4',
|
||||
color: 'yellow',
|
||||
size: 10,
|
||||
owner: 'Max',
|
||||
appraisedValue: 200
|
||||
},
|
||||
{
|
||||
assetID: 'asset5',
|
||||
color: 'black',
|
||||
size: 15,
|
||||
owner: 'Adriana',
|
||||
appraisedValue: 250
|
||||
},
|
||||
{
|
||||
assetID: 'asset6',
|
||||
color: 'white',
|
||||
size: 15,
|
||||
owner: 'Michel',
|
||||
appraisedValue: 250
|
||||
},
|
||||
];
|
||||
|
||||
for (const asset of assets) {
|
||||
await this.CreateAsset(
|
||||
ctx,
|
||||
asset.assetID,
|
||||
asset.color,
|
||||
asset.size,
|
||||
asset.owner,
|
||||
asset.appraisedValue
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Chaincode;
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,23 +0,0 @@
|
|||
{
|
||||
"name": "asset-transfer-ledger-queries",
|
||||
"version": "1.0.0",
|
||||
"description": "asset chaincode implemented in node.js",
|
||||
"main": "index.js",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "fabric-chaincode-node start",
|
||||
"lint": "eslint *.js */**.js",
|
||||
"test": ""
|
||||
},
|
||||
"engine-strict": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"fabric-contract-api": "~2.5",
|
||||
"fabric-shim": "~2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.57.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,169 +0,0 @@
|
|||
# State-based endorsement asset transfer sample
|
||||
|
||||
Transactions that are submitted to Hyperledger Fabric networks need to be endorsed
|
||||
by peers that are joined to a channel before the transaction can be added to the
|
||||
ledger. Fabric peers endorse transactions by executing a smart contract using the
|
||||
inputs of the transaction proposal. The peers then sign the input and output
|
||||
generated by the smart contract execution. The endorsement policy specifies the
|
||||
set of organizations whose peers need to endorse a transaction before it can be
|
||||
added to the ledger.
|
||||
|
||||
Each chaincode that is deployed to a channel has an endorsement policy that governs
|
||||
the assets managed by the chaincode smart contracts. However, you can override the
|
||||
chaincode level endorsement policy to create an endorsement policy for a specific key,
|
||||
either on the public channel ledger or in a private collection.
|
||||
State-based endorsement policies, also known as key-level endorsement policies, allow
|
||||
channel members to use different endorsement policies for assets that are managed by
|
||||
the same smart contract. For more information about endorsement policies and
|
||||
state-based endorsement, visit the
|
||||
[Endorsement Policies](https://hyperledger-fabric.readthedocs.io/en/release-2.2/endorsement-policies.html)
|
||||
topic in the Fabric documentation.
|
||||
|
||||
The implementation provided by State Based interface creates a policy which requires signatures from all the Org principals added, and hence is equivalent to an AND policy. For other advanced State Based policy implementations which are not supported by State Based interface directly like OR or NOutOf policies, please refer to method implementations setStateBasedEndorsementNOutOf(), which can be used as an alternative for setStateBasedEndorsement() inside asset-transfer-sbe smart contracts.
|
||||
|
||||
## About the Sample
|
||||
|
||||
The state-based endorsement (SBE) asset transfer sample demonstrates how to use
|
||||
key-level endorsement policies to ensure that an asset only is endorsed by an
|
||||
asset owner. In the course of the tutorial, you will use the smart contract
|
||||
to complete the following transfer scenario:
|
||||
|
||||
- Deploy the SBE smart contract to a channel that was created using the Fabric
|
||||
test network. The channel will have two members, Org1 and Org2, that will
|
||||
participate in the asset transfer. Each organization operates one peer that
|
||||
is joined to the channel.
|
||||
- Create an asset using the chaincode endorsement policy. The chaincode level
|
||||
endorsement policy requires that a majority of organizations on the channel
|
||||
endorse a transaction. This means that a transaction that creates an asset
|
||||
needs to be endorsed by peers that belong to Org1 and Org2. When the asset is
|
||||
created, the smart contract creates a state-based endorsement policy that
|
||||
specifies that only the organization that owns that asset may endorse update
|
||||
transactions. Because the asset is owned by Org1, any future updates to the asset
|
||||
need to be endorsed by the Org1 peer.
|
||||
- Update the asset by only endorsing with Org1, this will use the state-based
|
||||
endorsement policy applied to the asset when it was created by the chaincode.
|
||||
- Transfer the asset to Org2. During the execution of the transfer transaction,
|
||||
the chaincode will create a new state-based endorsement policy that reflects
|
||||
the new asset owner for the asset.
|
||||
- Update the asset once more, this time with Org2 as the owner. Because the
|
||||
state-based endorsement policy has been updated, this transaction only needs
|
||||
to be endorsed by Org2.
|
||||
|
||||
## Deploy the smart contract
|
||||
|
||||
We are going to run the SBE smart contract using the Fabric test network. Open a command terminal and navigate to test network directory in your local clone of the `fabric-samples`. We will operate from the `test-network` directory for the remainder of the tutorial.
|
||||
```
|
||||
cd fabric-samples/test-network
|
||||
```
|
||||
|
||||
Run the following command to deploy the test network and create a channel named `mychannel`:
|
||||
|
||||
```
|
||||
./network.sh up createChannel
|
||||
```
|
||||
|
||||
You can use the test network script to deploy the smart contract to the channel that was just created. The script uses the Fabric chaincode lifecycle to deploy the smart contract to the channel. We will use the default chaincode level endorsement policy used by the Fabric chaincode lifecycle, which requires an endorsement from a majority of channel members. In our case, this will require that both Org1 and Org2 endorse a transaction (2 of 2). Deploy the smart contract to `mychannel` using the following command:
|
||||
```
|
||||
./network.sh deployCC -ccn sbe -ccp ../asset-transfer-sbe/chaincode-typescript/ -ccl typescript
|
||||
```
|
||||
|
||||
Set the following environment variables to interact with the network as a user from Org1:
|
||||
|
||||
```
|
||||
export PATH=${PWD}/../bin:${PWD}:$PATH
|
||||
export FABRIC_CFG_PATH=$PWD/../config/
|
||||
export CORE_PEER_TLS_ENABLED=true
|
||||
export CORE_PEER_LOCALMSPID=Org1MSP
|
||||
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp
|
||||
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
|
||||
export CORE_PEER_ADDRESS=localhost:7051
|
||||
```
|
||||
|
||||
## Run the transfer scenario
|
||||
|
||||
We can now invoke the SBE smart contract to create a new asset:
|
||||
```
|
||||
peer chaincode invoke -o localhost:7050 --waitForEvent --ordererTLSHostnameOverride orderer.example.com --tls --cafile "${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem" -C mychannel -n sbe --peerAddresses localhost:7051 --tlsRootCertFiles "${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt" --peerAddresses localhost:9051 --tlsRootCertFiles "${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt" -c '{"function":"CreateAsset","Args":["asset1","100","Org1User1"]}'
|
||||
```
|
||||
The create transaction needs to target both peers from Org1 and Org2 to meet the chaincode endorsement policy. The chaincode will read the MSP ID of the client user submitting the transaction and assign that organization as the asset owner. As a result, the asset will initially be owned by Org1.
|
||||
|
||||
You can query the asset using with the following command:
|
||||
```
|
||||
peer chaincode query -C mychannel -n sbe -c '{"Args":["ReadAsset","asset1"]}'
|
||||
```
|
||||
The result is a new asset owned by Org1, identified using the Org1 MSP ID `Org1MSP`:
|
||||
`{"ID":"asset1","Value":100,"Owner":"Org1User1","OwnerOrg":"Org1MSP"}`
|
||||
|
||||
In addition to creating the asset, the `CreateAsset` function also sets a state-based endorsement policy for the asset. Only a peer of the asset owner, can successfully endorse an asset update. To demonstrate the key-level endorsement policy, lets try to update the asset while targeting the Org2 peer:
|
||||
```
|
||||
peer chaincode invoke -o localhost:7050 --waitForEvent --ordererTLSHostnameOverride orderer.example.com --tls --cafile "${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem" -C mychannel -n sbe --peerAddresses localhost:9051 --tlsRootCertFiles "${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt" -c '{"function":"UpdateAsset","Args":["asset1","200"]}'
|
||||
```
|
||||
The result is an endorsement policy failure:
|
||||
```
|
||||
Error: transaction invalidated with status (ENDORSEMENT_POLICY_FAILURE) - proposal response: <nil>
|
||||
```
|
||||
|
||||
If we attempt to update the asset with an endorsement from the Org1 peer, the update succeeds:
|
||||
```
|
||||
peer chaincode invoke -o localhost:7050 --waitForEvent --ordererTLSHostnameOverride orderer.example.com --tls --cafile "${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem" -C mychannel -n sbe --peerAddresses localhost:7051 --tlsRootCertFiles "${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt" -c '{"function":"UpdateAsset","Args":["asset1","200"]}'
|
||||
```
|
||||
You can query the asset one more time to verify that the update was successful:
|
||||
```
|
||||
peer chaincode query -C mychannel -n sbe -c '{"Args":["ReadAsset","asset1"]}'
|
||||
```
|
||||
|
||||
The asset value is now 200:
|
||||
```
|
||||
{"ID":"asset1","Value":200,"Owner":"Org1User1","OwnerOrg":"Org1MSP"}
|
||||
```
|
||||
|
||||
Now that we have tested the asset key-level endorsement policy, we can transfer the asset to Org2. Run the following command to transfer the asset from Org1 to Org2. This time the Org2 MSP ID is provided as a transaction input. The `TransferAsset` function will update the endorsement policy to specify that only a peer of the new owner can update the asset. Note that this command targets the Org1 peer.
|
||||
|
||||
```
|
||||
peer chaincode invoke -o localhost:7050 --waitForEvent --ordererTLSHostnameOverride orderer.example.com --tls --cafile "${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem" -C mychannel -n sbe --peerAddresses localhost:7051 --tlsRootCertFiles "${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt" -c '{"function":"TransferAsset","Args":["asset1","Org2User1","Org2MSP"]}'
|
||||
```
|
||||
|
||||
We can query the asset to see that the owner has been updated from Org1 to Org2:
|
||||
```
|
||||
peer chaincode query -C mychannel -n sbe -c '{"Args":["ReadAsset","asset1"]}'
|
||||
```
|
||||
|
||||
The owning organization is now Org2:
|
||||
```
|
||||
{"ID":"asset1","Value":200,"Owner":"Org2User1","OwnerOrg":"Org2MSP"}
|
||||
```
|
||||
|
||||
Org2 now needs to endorse any asset updates. Run the following command to try to update the asset with an endorsement from the Org1 peer:
|
||||
```
|
||||
peer chaincode invoke -o localhost:7050 --waitForEvent --ordererTLSHostnameOverride orderer.example.com --tls --cafile "${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem" -C mychannel -n sbe --peerAddresses localhost:7051 --tlsRootCertFiles "${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt" -c '{"function":"UpdateAsset","Args":["asset1","300"]}'
|
||||
```
|
||||
|
||||
The response will be an endorsement policy failure:
|
||||
```
|
||||
Error: transaction invalidated with status (ENDORSEMENT_POLICY_FAILURE) - proposal response: <nil>
|
||||
```
|
||||
|
||||
Now try to update the asset with an endorsement from the Org2 peer:
|
||||
```
|
||||
peer chaincode invoke -o localhost:7050 --waitForEvent --ordererTLSHostnameOverride orderer.example.com --tls --cafile "${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem" -C mychannel -n sbe --peerAddresses localhost:9051 --tlsRootCertFiles "${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt" -c '{"function":"UpdateAsset","Args":["asset1","300"]}'
|
||||
```
|
||||
|
||||
You can query the asset again to verify that the transaction update succeeded:
|
||||
```
|
||||
peer chaincode query -C mychannel -n sbe -c '{"Args":["ReadAsset","asset1"]}'
|
||||
```
|
||||
|
||||
The asset value is now 300:
|
||||
```
|
||||
{"ID":"asset1","Value":300,"Owner":"Org2User1","OwnerOrg":"Org2MSP"}
|
||||
```
|
||||
|
||||
Note that the transaction to update the asset was submitted by a user from Org1, even though the asset was owned by Org2. The transfer enabled by the SBE smart contract is a simple scenario meant only to demonstrate the use of state-based endorsement policies. The smart contract can use access control to specify that an asset can only be updated by its owner. Private data collections can also be used to ensure that transfers need to be endorsed by the owner and recipient of the transfer, instead of just the asset owner. For a more realistic example of an asset transfer scenario, see the [Secured asset transfer in Fabric](https://hyperledger-fabric.readthedocs.io/en/master/secured_asset_transfer/secured_private_asset_transfer_tutorial.html) tutorial.
|
||||
|
||||
## Clean up
|
||||
|
||||
When you are finished, you can bring down the test network. The command will remove all the nodes of the test network, and delete any ledger data that you created:
|
||||
|
||||
```
|
||||
./network.sh down
|
||||
```
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
coverage
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
env: {
|
||||
node: true,
|
||||
mocha: true
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 8,
|
||||
sourceType: 'script'
|
||||
},
|
||||
extends: 'eslint:recommended',
|
||||
rules: {
|
||||
indent: ['error', 'tab'],
|
||||
'linebreak-style': ['error', 'unix'],
|
||||
quotes: ['error', 'single'],
|
||||
semi: ['error', 'always'],
|
||||
'no-unused-vars': ['error', { args: 'none' }],
|
||||
'no-console': 'off',
|
||||
curly: 'error',
|
||||
eqeqeq: 'error',
|
||||
'no-throw-literal': 'error',
|
||||
strict: 'error',
|
||||
'no-var': 'error',
|
||||
'dot-notation': 'error',
|
||||
'no-trailing-spaces': 'error',
|
||||
'no-use-before-define': 'error',
|
||||
'no-useless-call': 'error',
|
||||
'no-with': 'error',
|
||||
'operator-linebreak': 'error',
|
||||
yoda: 'error',
|
||||
'quote-props': ['error', 'as-needed']
|
||||
}
|
||||
};
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
package-lock.json
|
||||
|
||||
wallet
|
||||
!wallet/.gitkeep
|
||||
|
|
@ -1,356 +0,0 @@
|
|||
/*
|
||||
* Copyright IBM Corp. All Rights Reserved.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* A test application to show state based endorsements operations with a running
|
||||
* asset-transfer-sbe chaincode with discovery.
|
||||
* -- How to submit a transaction
|
||||
* -- How to query
|
||||
* -- How to limit the organizations involved in a transaction
|
||||
*
|
||||
* To see the SDK workings, try setting the logging to show on the console before running
|
||||
* export HFC_LOGGING='{"debug":"console"}'
|
||||
*/
|
||||
|
||||
// pre-requisites:
|
||||
// - fabric-sample two organization test-network setup with two peers, ordering service,
|
||||
// and 2 certificate authorities
|
||||
// ===> from directory /fabric-samples/test-network
|
||||
// ./network.sh up createChannel -ca
|
||||
// - Use any of the asset-transfer-sbe chaincodes deployed on the channel "mychannel"
|
||||
// with the chaincode name of "sbe". The following deploy command will package,
|
||||
// install, approve, and commit the javascript chaincode, all the actions it takes
|
||||
// to deploy a chaincode to a channel.
|
||||
// ===> from directory /fabric-samples/test-network
|
||||
// ./network.sh deployCC -ccn sbe -ccp ../asset-transfer-sbe/chaincode-typescript/ -ccl typescript
|
||||
// - Be sure that node.js is installed
|
||||
// ===> from directory /fabric-samples/asset-transfer-sbe/application-javascript
|
||||
// node -v
|
||||
// - npm installed code dependencies
|
||||
// ===> from directory /fabric-samples/asset-transfer-sbe/application-javascript
|
||||
// npm install
|
||||
// - to run this test application
|
||||
// ===> from directory /fabric-samples/asset-transfer-sbe/application-javascript
|
||||
// node app.js
|
||||
|
||||
// NOTE: If you see an error like these:
|
||||
/*
|
||||
|
||||
Error in setup: Error: DiscoveryService: mychannel error: access denied
|
||||
|
||||
OR
|
||||
|
||||
Failed to register user : Error: fabric-ca request register failed with errors [[ { code: 20, message: 'Authentication failure' } ]]
|
||||
|
||||
*/
|
||||
// Delete the /fabric-samples/asset-transfer-sbe/application-javascript/wallet directory
|
||||
// and retry this application.
|
||||
//
|
||||
// The certificate authority must have been restarted and the saved certificates for the
|
||||
// admin and application user are not valid. Deleting the wallet store will force these to be reset
|
||||
// with the new certificate authority.
|
||||
//
|
||||
|
||||
const { Gateway, Wallets } = require('fabric-network');
|
||||
const FabricCAServices = require('fabric-ca-client');
|
||||
const path = require('path');
|
||||
const { buildCAClient, registerAndEnrollUser, enrollAdmin } = require('../../test-application/javascript/CAUtil.js');
|
||||
const { buildCCPOrg1, buildCCPOrg2, buildWallet } = require('../../test-application/javascript/AppUtil.js');
|
||||
|
||||
const channelName = 'mychannel';
|
||||
const chaincodeName = 'sbe';
|
||||
|
||||
const org1 = 'Org1MSP';
|
||||
const org2 = 'Org2MSP';
|
||||
const Org1UserId = 'appUser1';
|
||||
const Org2UserId = 'appUser2';
|
||||
|
||||
async function initGatewayForOrg1() {
|
||||
console.log('\n--> Fabric client user & Gateway init: Using Org1 identity to Org1 Peer');
|
||||
// build an in memory object with the network configuration (also known as a connection profile)
|
||||
const ccpOrg1 = buildCCPOrg1();
|
||||
|
||||
// build an instance of the fabric ca services client based on
|
||||
// the information in the network configuration
|
||||
const caOrg1Client = buildCAClient(FabricCAServices, ccpOrg1, 'ca.org1.example.com');
|
||||
|
||||
// setup the wallet to cache the credentials of the application user, on the app server locally
|
||||
const walletPathOrg1 = path.join(__dirname, 'wallet', 'org1');
|
||||
const walletOrg1 = await buildWallet(Wallets, walletPathOrg1);
|
||||
|
||||
// in a real application this would be done on an administrative flow, and only once
|
||||
// stores admin identity in local wallet, if needed
|
||||
await enrollAdmin(caOrg1Client, walletOrg1, org1);
|
||||
// register & enroll application user with CA, which is used as client identify to make chaincode calls
|
||||
// and stores app user identity in local wallet
|
||||
// In a real application this would be done only when a new user was required to be added
|
||||
// and would be part of an administrative flow
|
||||
await registerAndEnrollUser(caOrg1Client, walletOrg1, org1, Org1UserId, 'org1.department1');
|
||||
|
||||
try {
|
||||
// Create a new gateway for connecting to Org's peer node.
|
||||
const gatewayOrg1 = new Gateway();
|
||||
//connect using Discovery enabled
|
||||
await gatewayOrg1.connect(ccpOrg1,
|
||||
{ wallet: walletOrg1, identity: Org1UserId, discovery: { enabled: true, asLocalhost: true } });
|
||||
|
||||
return gatewayOrg1;
|
||||
} catch (error) {
|
||||
console.error(`Error in connecting to gateway for Org1: ${error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function initGatewayForOrg2() {
|
||||
console.log('\n--> Fabric client user & Gateway init: Using Org2 identity to Org2 Peer');
|
||||
const ccpOrg2 = buildCCPOrg2();
|
||||
const caOrg2Client = buildCAClient(FabricCAServices, ccpOrg2, 'ca.org2.example.com');
|
||||
|
||||
const walletPathOrg2 = path.join(__dirname, 'wallet', 'org2');
|
||||
const walletOrg2 = await buildWallet(Wallets, walletPathOrg2);
|
||||
|
||||
await enrollAdmin(caOrg2Client, walletOrg2, org2);
|
||||
await registerAndEnrollUser(caOrg2Client, walletOrg2, org2, Org2UserId, 'org2.department1');
|
||||
|
||||
try {
|
||||
// Create a new gateway for connecting to Org's peer node.
|
||||
const gatewayOrg2 = new Gateway();
|
||||
await gatewayOrg2.connect(ccpOrg2,
|
||||
{ wallet: walletOrg2, identity: Org2UserId, discovery: { enabled: true, asLocalhost: true } });
|
||||
|
||||
return gatewayOrg2;
|
||||
} catch (error) {
|
||||
console.error(`Error in connecting to gateway for Org2: ${error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function checkAsset(org, assetKey, resultBuffer, value, ownerOrg) {
|
||||
let asset;
|
||||
if (resultBuffer) {
|
||||
asset = JSON.parse(resultBuffer.toString('utf8'));
|
||||
}
|
||||
|
||||
if (asset && value) {
|
||||
if (asset.Value === value && asset.OwnerOrg === ownerOrg) {
|
||||
console.log(`*** Result from ${org} - asset ${asset.ID} has value of ${asset.Value} and owned by ${asset.OwnerOrg}`);
|
||||
} else {
|
||||
console.log(`*** Failed from ${org} - asset ${asset.ID} has value of ${asset.Value} and owned by ${asset.OwnerOrg}`);
|
||||
}
|
||||
} else if (!asset && value === 0 ) {
|
||||
console.log(`*** Success from ${org} - asset ${assetKey} does not exist`);
|
||||
} else {
|
||||
console.log('*** Failed - asset read failed');
|
||||
}
|
||||
}
|
||||
|
||||
async function readAssetByBothOrgs(assetKey, value, ownerOrg, contractOrg1, contractOrg2) {
|
||||
if (value) {
|
||||
console.log(`\n--> Evaluate Transaction: ReadAsset, - ${assetKey} should have a value of ${value} and owned by ${ownerOrg}`);
|
||||
} else {
|
||||
console.log(`\n--> Evaluate Transaction: ReadAsset, - ${assetKey} should not exist`);
|
||||
}
|
||||
let resultBuffer;
|
||||
resultBuffer = await contractOrg1.evaluateTransaction('ReadAsset', assetKey);
|
||||
checkAsset('Org1', assetKey, resultBuffer, value, ownerOrg);
|
||||
resultBuffer = await contractOrg2.evaluateTransaction('ReadAsset', assetKey);
|
||||
checkAsset('Org2', assetKey, resultBuffer, value, ownerOrg);
|
||||
}
|
||||
|
||||
// This application uses fabric-samples/test-network based setup and the companion chaincode
|
||||
// For this illustration, both Org1 & Org2 client identities will be used, however
|
||||
// notice they are used by two different "gateway"s to simulate two different running
|
||||
// applications from two different organizations.
|
||||
async function main() {
|
||||
try {
|
||||
// use a random key so that we can run multiple times
|
||||
const assetKey = `asset-${Math.floor(Math.random() * 100) + 1}`;
|
||||
|
||||
/** ******* Fabric client init: Using Org1 identity to Org1 Peer ******* */
|
||||
const gatewayOrg1 = await initGatewayForOrg1();
|
||||
const networkOrg1 = await gatewayOrg1.getNetwork(channelName);
|
||||
const contractOrg1 = networkOrg1.getContract(chaincodeName);
|
||||
|
||||
/** ******* Fabric client init: Using Org2 identity to Org2 Peer ******* */
|
||||
const gatewayOrg2 = await initGatewayForOrg2();
|
||||
const networkOrg2 = await gatewayOrg2.getNetwork(channelName);
|
||||
const contractOrg2 = networkOrg2.getContract(chaincodeName);
|
||||
|
||||
try {
|
||||
let transaction;
|
||||
|
||||
try {
|
||||
// Create an asset by organization Org1, this will require that both organization endorse.
|
||||
// The endorsement will be handled by Discovery, since the gateway was connected with discovery enabled.
|
||||
console.log(`\n--> Submit Transaction: CreateAsset, ${assetKey} as Org1 - endorsed by Org1 and Org2`);
|
||||
await contractOrg1.submitTransaction('CreateAsset', assetKey, '100', 'Tom');
|
||||
console.log('*** Result: committed, now asset will only require Org1 to endorse');
|
||||
} catch (createError) {
|
||||
console.log(`*** Failed: create - ${createError}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await readAssetByBothOrgs(assetKey, 100, org1, contractOrg1, contractOrg2);
|
||||
|
||||
try {
|
||||
// Since the gateway is using discovery we should limit the organizations used by
|
||||
// discovery to endorse. This way we only have to know the organization and not
|
||||
// the actual peers that may be active at any given time.
|
||||
console.log(`\n--> Submit Transaction: UpdateAsset ${assetKey}, as Org1 - endorse by Org1`);
|
||||
transaction = contractOrg1.createTransaction('UpdateAsset');
|
||||
transaction.setEndorsingOrganizations(org1);
|
||||
await transaction.submit(assetKey, '200');
|
||||
console.log('*** Result: committed');
|
||||
} catch (updateError) {
|
||||
console.log(`*** Failed: update - ${updateError}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await readAssetByBothOrgs(assetKey, 200, org1, contractOrg1, contractOrg2);
|
||||
|
||||
try {
|
||||
// Submit a transaction to make an update to the asset that has a key-level endorsement policy
|
||||
// set to only allow Org1 to make updates. The following example will not use the "setEndorsingOrganizations"
|
||||
// to limit the organizations that will do the endorsement, this means that it will be sent to all
|
||||
// organizations in the chaincode endorsement policy. When Org1 endorses, the transaction will be committed
|
||||
// if Org2 endorses or not.
|
||||
console.log(`\n--> Submit Transaction: UpdateAsset ${assetKey}, as Org1 - endorse by Org1 and Org2`);
|
||||
transaction = contractOrg1.createTransaction('UpdateAsset');
|
||||
await transaction.submit(assetKey, '300');
|
||||
console.log('*** Result: committed - because Org1 and Org2 both endorsed, while only the Org1 endorsement was required and checked');
|
||||
} catch (updateError) {
|
||||
console.log(`*** Failed: update - ${updateError}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await readAssetByBothOrgs(assetKey, 300, org1, contractOrg1, contractOrg2);
|
||||
|
||||
try {
|
||||
// Again submit the change to both Organizations by not using "setEndorsingOrganizations". Since only
|
||||
// Org1 is required to approve, the transaction will be committed.
|
||||
console.log(`\n--> Submit Transaction: UpdateAsset ${assetKey}, as Org2 - endorse by Org1 and Org2`);
|
||||
transaction = contractOrg2.createTransaction('UpdateAsset');
|
||||
await transaction.submit(assetKey, '400');
|
||||
console.log('*** Result: committed - because Org1 was on the discovery list, Org2 did not endorse');
|
||||
} catch (updateError) {
|
||||
console.log(`*** Failed: update - ${updateError}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await readAssetByBothOrgs(assetKey, 400, org1, contractOrg1, contractOrg2);
|
||||
|
||||
try {
|
||||
// Try to update by sending only to Org2, since the state-based-endorsement says that
|
||||
// Org1 is the only organization allowed to update, the transaction will fail.
|
||||
console.log(`\n--> Submit Transaction: UpdateAsset ${assetKey}, as Org2 - endorse by Org2`);
|
||||
transaction = contractOrg2.createTransaction('UpdateAsset');
|
||||
transaction.setEndorsingOrganizations(org2);
|
||||
await transaction.submit(assetKey, '500');
|
||||
console.log('*** Failed: committed - this should have failed to endorse and commit');
|
||||
} catch (updateError) {
|
||||
console.log(`*** Successfully caught the error: \n ${updateError}`);
|
||||
}
|
||||
|
||||
await readAssetByBothOrgs(assetKey, 400, org1, contractOrg1, contractOrg2);
|
||||
|
||||
try {
|
||||
// Make a change to the state-based-endorsement policy making Org2 the owner.
|
||||
console.log(`\n--> Submit Transaction: TransferAsset ${assetKey}, as Org1 - endorse by Org1`);
|
||||
transaction = contractOrg1.createTransaction('TransferAsset');
|
||||
transaction.setEndorsingOrganizations(org1);
|
||||
await transaction.submit(assetKey, 'Henry', org2);
|
||||
console.log('*** Result: committed');
|
||||
} catch (transferError) {
|
||||
console.log(`*** Failed: transfer - ${transferError}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await readAssetByBothOrgs(assetKey, 400, org2, contractOrg1, contractOrg2);
|
||||
|
||||
try {
|
||||
// Make sure that Org2 can now make updates, notice how the transaction has limited the
|
||||
// endorsement to only Org2.
|
||||
console.log(`\n--> Submit Transaction: UpdateAsset ${assetKey}, as Org2 - endorse by Org2`);
|
||||
transaction = contractOrg2.createTransaction('UpdateAsset');
|
||||
transaction.setEndorsingOrganizations(org2);
|
||||
await transaction.submit(assetKey, '600');
|
||||
console.log('*** Result: committed');
|
||||
} catch (updateError) {
|
||||
console.log(`*** Failed: update - ${updateError}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await readAssetByBothOrgs(assetKey, 600, org2, contractOrg1, contractOrg2);
|
||||
|
||||
try {
|
||||
// With Org2 now the owner and the state-based-endorsement policy only allowing organization Org2
|
||||
// to make updates, a transaction only to Org1 will fail.
|
||||
console.log(`\n--> Submit Transaction: UpdateAsset ${assetKey}, as Org1 - endorse by Org1`);
|
||||
transaction = contractOrg1.createTransaction('UpdateAsset');
|
||||
transaction.setEndorsingOrganizations(org1);
|
||||
await transaction.submit(assetKey, '700');
|
||||
console.log('*** Failed: committed - this should have failed to endorse and commit');
|
||||
} catch (updateError) {
|
||||
console.log(`*** Successfully caught the error: \n ${updateError}`);
|
||||
}
|
||||
|
||||
await readAssetByBothOrgs(assetKey, 600, org2, contractOrg1, contractOrg2);
|
||||
|
||||
try {
|
||||
// With Org2 the owner and the state-based-endorsement policy only allowing organization Org2
|
||||
// to make updates, a transaction to delete by Org1 will fail.
|
||||
console.log(`\n--> Submit Transaction: DeleteAsset ${assetKey}, as Org1 - endorse by Org1`);
|
||||
transaction = contractOrg1.createTransaction('DeleteAsset');
|
||||
transaction.setEndorsingOrganizations(org1);
|
||||
await transaction.submit(assetKey);
|
||||
console.log('*** Failed: committed - this should have failed to endorse and commit');
|
||||
} catch (updateError) {
|
||||
console.log(`*** Successfully caught the error: \n ${updateError}`);
|
||||
}
|
||||
|
||||
try {
|
||||
// With Org2 the owner and the state-based-endorsement policy only allowing organization Org2
|
||||
// to make updates, a transaction to delete by Org2 will succeed.
|
||||
console.log(`\n--> Submit Transaction: DeleteAsset ${assetKey}, as Org2 - endorse by Org2`);
|
||||
transaction = contractOrg2.createTransaction('DeleteAsset');
|
||||
transaction.setEndorsingOrganizations(org2);
|
||||
await transaction.submit(assetKey);
|
||||
console.log('*** Result: committed');
|
||||
} catch (deleteError) {
|
||||
console.log(`*** Failed: delete - ${deleteError}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// The asset should now be deleted, both orgs should not be able to read it
|
||||
try {
|
||||
await readAssetByBothOrgs(assetKey, 0, org2, contractOrg1, contractOrg2);
|
||||
} catch (readDeleteError) {
|
||||
console.log(`*** Successfully caught the error: ${readDeleteError}`);
|
||||
}
|
||||
|
||||
} catch (runError) {
|
||||
console.error(`Error in transaction: ${runError}`);
|
||||
if (runError.stack) {
|
||||
console.error(runError.stack);
|
||||
}
|
||||
process.exit(1);
|
||||
} finally {
|
||||
// Disconnect from the gateway peer when all work for this client identity is complete
|
||||
gatewayOrg1.disconnect();
|
||||
gatewayOrg2.disconnect();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error in setup: ${error}`);
|
||||
if (error.stack) {
|
||||
console.error(error.stack);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
{
|
||||
"name": "asset-transfer-sbe",
|
||||
"version": "1.0.0",
|
||||
"description": "Asset transfer state based endorsement application implemented in JavaScript",
|
||||
"engines": {
|
||||
"node": ">=12",
|
||||
"npm": ">=5"
|
||||
},
|
||||
"engineStrict": true,
|
||||
"author": "Hyperledger",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"lint": "eslint *.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"fabric-ca-client": "^2.2.19",
|
||||
"fabric-network": "^2.2.19"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^7.32.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
#
|
||||
# https://help.github.com/articles/dealing-with-line-endings/
|
||||
#
|
||||
# These are explicitly windows files and should use crlf
|
||||
*.bat text eol=crlf
|
||||
|
||||
10
asset-transfer-sbe/chaincode-java/.gitignore
vendored
10
asset-transfer-sbe/chaincode-java/.gitignore
vendored
|
|
@ -1,10 +0,0 @@
|
|||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
/.classpath
|
||||
/.gradle/
|
||||
/.project
|
||||
/.settings/
|
||||
/bin/
|
||||
/build/
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id 'com.github.johnrengelman.shadow' version '8.1.1'
|
||||
id 'application'
|
||||
id 'checkstyle'
|
||||
id 'jacoco'
|
||||
}
|
||||
|
||||
group 'org.hyperledger.fabric.samples'
|
||||
version '1.0-SNAPSHOT'
|
||||
|
||||
dependencies {
|
||||
implementation 'org.hyperledger.fabric-chaincode-java:fabric-chaincode-shim:2.5.+'
|
||||
implementation 'org.hyperledger.fabric:fabric-protos:0.3.3'
|
||||
implementation 'com.owlike:genson:1.6'
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
url 'https://jitpack.io'
|
||||
}
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(11)
|
||||
}
|
||||
}
|
||||
|
||||
application {
|
||||
mainClass = 'org.hyperledger.fabric.contract.ContractRouter'
|
||||
}
|
||||
|
||||
checkstyle {
|
||||
toolVersion '8.21'
|
||||
configFile file("config/checkstyle/checkstyle.xml")
|
||||
}
|
||||
|
||||
checkstyleMain {
|
||||
source ='src/main/java'
|
||||
}
|
||||
|
||||
checkstyleTest {
|
||||
source ='src/test/java'
|
||||
}
|
||||
|
||||
jacocoTestReport {
|
||||
dependsOn test
|
||||
}
|
||||
|
||||
jacocoTestCoverageVerification {
|
||||
violationRules {
|
||||
rule {
|
||||
limit {
|
||||
minimum = 0.9
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
finalizedBy jacocoTestReport
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
testLogging {
|
||||
events "passed", "skipped", "failed"
|
||||
}
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
archiveBaseName = 'chaincode'
|
||||
archiveVersion = ''
|
||||
archiveClassifier = ''
|
||||
mergeServiceFiles()
|
||||
|
||||
manifest {
|
||||
attributes 'Main-Class': 'org.hyperledger.fabric.contract.ContractRouter'
|
||||
}
|
||||
}
|
||||
|
||||
check.dependsOn jacocoTestCoverageVerification
|
||||
installDist.dependsOn check
|
||||
|
|
@ -1,172 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
<!DOCTYPE module PUBLIC
|
||||
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
|
||||
"https://checkstyle.org/dtds/configuration_1_3.dtd">
|
||||
|
||||
<!--
|
||||
|
||||
Checkstyle configuration that matches the Eclipse formatter
|
||||
|
||||
Checkstyle is very configurable. Be sure to read the documentation at
|
||||
http://checkstyle.sourceforge.net (or in your downloaded distribution).
|
||||
|
||||
Most Checks are configurable, be sure to consult the documentation.
|
||||
|
||||
To completely disable a check, just comment it out or delete it from the file.
|
||||
|
||||
Finally, it is worth reading the documentation.
|
||||
|
||||
-->
|
||||
|
||||
<module name="Checker">
|
||||
<!--
|
||||
If you set the basedir property below, then all reported file
|
||||
names will be relative to the specified directory. See
|
||||
https://checkstyle.org/5.x/config.html#Checker
|
||||
|
||||
<property name="basedir" value="${basedir}"/>
|
||||
-->
|
||||
|
||||
<property name="fileExtensions" value="java, properties, xml"/>
|
||||
|
||||
<module name="SuppressionFilter">
|
||||
<property name="file" value="${config_loc}/suppressions.xml"/>
|
||||
<property name="optional" value="false"/>
|
||||
</module>
|
||||
|
||||
<!-- Excludes all 'module-info.java' files -->
|
||||
<!-- See https://checkstyle.org/config_filefilters.html -->
|
||||
<module name="BeforeExecutionExclusionFileFilter">
|
||||
<property name="fileNamePattern" value="module\-info\.java$"/>
|
||||
</module>
|
||||
|
||||
<!-- Checks that a package-info.java file exists for each package. -->
|
||||
<!-- See http://checkstyle.sourceforge.net/config_javadoc.html#JavadocPackage -->
|
||||
<!-- <module name="JavadocPackage"/> -->
|
||||
|
||||
<!-- Checks whether files end with a new line. -->
|
||||
<!-- See http://checkstyle.sourceforge.net/config_misc.html#NewlineAtEndOfFile -->
|
||||
<module name="NewlineAtEndOfFile"/>
|
||||
|
||||
<!-- Checks that property files contain the same keys. -->
|
||||
<!-- See http://checkstyle.sourceforge.net/config_misc.html#Translation -->
|
||||
<module name="Translation"/>
|
||||
|
||||
<!-- Checks for Size Violations. -->
|
||||
<!-- See http://checkstyle.sourceforge.net/config_sizes.html -->
|
||||
<module name="FileLength"/>
|
||||
|
||||
<!-- Checks for whitespace -->
|
||||
<!-- See http://checkstyle.sourceforge.net/config_whitespace.html -->
|
||||
<module name="FileTabCharacter"/>
|
||||
|
||||
<!-- Miscellaneous other checks. -->
|
||||
<!-- See http://checkstyle.sourceforge.net/config_misc.html -->
|
||||
<module name="RegexpSingleline">
|
||||
<property name="format" value="\s+$"/>
|
||||
<property name="minimum" value="0"/>
|
||||
<property name="maximum" value="0"/>
|
||||
<property name="message" value="Line has trailing spaces."/>
|
||||
</module>
|
||||
|
||||
<!-- Checks for Headers -->
|
||||
<!-- See http://checkstyle.sourceforge.net/config_header.html -->
|
||||
<!-- <module name="Header"> -->
|
||||
<!-- <property name="headerFile" value="${checkstyle.header.file}"/> -->
|
||||
<!-- <property name="fileExtensions" value="java"/> -->
|
||||
<!-- </module> -->
|
||||
|
||||
<module name="TreeWalker">
|
||||
|
||||
<!-- Checks for Javadoc comments. -->
|
||||
<!-- See http://checkstyle.sourceforge.net/config_javadoc.html -->
|
||||
<!-- <module name="JavadocMethod"/> -->
|
||||
<!-- <module name="JavadocType"/> -->
|
||||
<!-- <module name="JavadocVariable"/> -->
|
||||
<!-- <module name="JavadocStyle"/> -->
|
||||
<!-- <module name="MissingJavadocMethod"/> -->
|
||||
|
||||
<!-- Checks for Naming Conventions. -->
|
||||
<!-- See http://checkstyle.sourceforge.net/config_naming.html -->
|
||||
<module name="ConstantName"/>
|
||||
<module name="LocalFinalVariableName"/>
|
||||
<module name="LocalVariableName"/>
|
||||
<module name="PackageName"/>
|
||||
<module name="StaticVariableName"/>
|
||||
<module name="TypeName"/>
|
||||
|
||||
<!-- Checks for imports -->
|
||||
<!-- See http://checkstyle.sourceforge.net/config_import.html -->
|
||||
<module name="AvoidStarImport"/>
|
||||
<module name="IllegalImport"/> <!-- defaults to sun.* packages -->
|
||||
<module name="RedundantImport"/>
|
||||
<module name="UnusedImports">
|
||||
<property name="processJavadoc" value="false"/>
|
||||
</module>
|
||||
|
||||
<!-- Checks for Size Violations. -->
|
||||
<!-- See http://checkstyle.sourceforge.net/config_sizes.html -->
|
||||
<module name="MethodLength"/>
|
||||
<module name="ParameterNumber"/>
|
||||
|
||||
<!-- Checks for whitespace -->
|
||||
<!-- See http://checkstyle.sourceforge.net/config_whitespace.html -->
|
||||
<module name="EmptyForIteratorPad"/>
|
||||
<module name="GenericWhitespace"/>
|
||||
<module name="MethodParamPad"/>
|
||||
<module name="NoWhitespaceAfter"/>
|
||||
<module name="NoWhitespaceBefore"/>
|
||||
<module name="OperatorWrap"/>
|
||||
<module name="ParenPad"/>
|
||||
<module name="TypecastParenPad"/>
|
||||
<module name="WhitespaceAfter"/>
|
||||
<module name="WhitespaceAround"/>
|
||||
|
||||
<!-- Modifier Checks -->
|
||||
<!-- See http://checkstyle.sourceforge.net/config_modifiers.html -->
|
||||
<module name="ModifierOrder"/>
|
||||
<module name="RedundantModifier"/>
|
||||
|
||||
<!-- Checks for blocks. You know, those {}'s -->
|
||||
<!-- See http://checkstyle.sourceforge.net/config_blocks.html -->
|
||||
<module name="AvoidNestedBlocks"/>
|
||||
<module name="EmptyBlock"/>
|
||||
<module name="LeftCurly"/>
|
||||
<module name="NeedBraces"/>
|
||||
<module name="RightCurly"/>
|
||||
|
||||
<!-- Checks for common coding problems -->
|
||||
<!-- See http://checkstyle.sourceforge.net/config_coding.html -->
|
||||
<module name="EmptyStatement"/>
|
||||
<module name="EqualsHashCode"/>
|
||||
<module name="HiddenField">
|
||||
<property name="ignoreConstructorParameter" value="true"/>
|
||||
<property name="ignoreSetter" value="true"/>
|
||||
</module>
|
||||
<module name="IllegalInstantiation"/>
|
||||
<module name="InnerAssignment"/>
|
||||
<module name="MissingSwitchDefault"/>
|
||||
<module name="MultipleVariableDeclarations"/>
|
||||
<module name="SimplifyBooleanExpression"/>
|
||||
<module name="SimplifyBooleanReturn"/>
|
||||
|
||||
<!-- Checks for class design -->
|
||||
<!-- See http://checkstyle.sourceforge.net/config_design.html -->
|
||||
<module name="DesignForExtension"/>
|
||||
<module name="FinalClass"/>
|
||||
<module name="HideUtilityClassConstructor"/>
|
||||
<module name="InterfaceIsType"/>
|
||||
<module name="VisibilityModifier">
|
||||
<property name="allowPublicFinalFields" value="true"/>
|
||||
</module>
|
||||
|
||||
<!-- Miscellaneous other checks. -->
|
||||
<!-- See http://checkstyle.sourceforge.net/config_misc.html -->
|
||||
<module name="ArrayTypeStyle"/>
|
||||
<module name="FinalParameters"/>
|
||||
<module name="TodoComment"/>
|
||||
<module name="UpperEll"/>
|
||||
|
||||
</module>
|
||||
|
||||
</module>
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
|
||||
<!DOCTYPE suppressions PUBLIC
|
||||
"-//Checkstyle//DTD SuppressionFilter Configuration 1.2//EN"
|
||||
"https://checkstyle.org/dtds/suppressions_1_2.dtd">
|
||||
|
||||
<suppressions>
|
||||
<suppress files="ChaincodeTest.java" checks="ParameterNumber" />
|
||||
</suppressions>
|
||||
Binary file not shown.
|
|
@ -1,7 +0,0 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
249
asset-transfer-sbe/chaincode-java/gradlew
vendored
249
asset-transfer-sbe/chaincode-java/gradlew
vendored
|
|
@ -1,249 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
92
asset-transfer-sbe/chaincode-java/gradlew.bat
vendored
92
asset-transfer-sbe/chaincode-java/gradlew.bat
vendored
|
|
@ -1,92 +0,0 @@
|
|||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
rootProject.name = 'sbe'
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.hyperledger.fabric.samples.sbe;
|
||||
|
||||
import com.owlike.genson.annotation.JsonProperty;
|
||||
import org.hyperledger.fabric.contract.annotation.DataType;
|
||||
import org.hyperledger.fabric.contract.annotation.Property;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@DataType()
|
||||
public final class Asset {
|
||||
|
||||
@Property()
|
||||
private final String ID;
|
||||
|
||||
@Property()
|
||||
private int Value;
|
||||
|
||||
@Property()
|
||||
private String Owner;
|
||||
|
||||
@Property()
|
||||
private String OwnerOrg;
|
||||
|
||||
@JsonProperty("ID")
|
||||
public String getID() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
@JsonProperty("Value")
|
||||
public int getValue() {
|
||||
return Value;
|
||||
}
|
||||
|
||||
public void setValue(final int Value) {
|
||||
this.Value = Value;
|
||||
}
|
||||
|
||||
@JsonProperty("Owner")
|
||||
public String getOwner() {
|
||||
return Owner;
|
||||
}
|
||||
|
||||
public void setOwner(final String Owner) {
|
||||
this.Owner = Owner;
|
||||
}
|
||||
|
||||
@JsonProperty("OwnerOrg")
|
||||
public String getOwnerOrg() {
|
||||
return OwnerOrg;
|
||||
}
|
||||
|
||||
public void setOwnerOrg(final String OwnerOrg) {
|
||||
this.OwnerOrg = OwnerOrg;
|
||||
}
|
||||
|
||||
public Asset(@JsonProperty("ID") final String ID, @JsonProperty("Value") final int Value,
|
||||
@JsonProperty("Owner") final String Owner, @JsonProperty("OwnerOrg") final String OwnerOrg) {
|
||||
this.ID = ID;
|
||||
this.Value = Value;
|
||||
this.Owner = Owner;
|
||||
this.OwnerOrg = OwnerOrg;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Asset asset = (Asset) o;
|
||||
return getValue() == asset.getValue()
|
||||
&&
|
||||
getID().equals(asset.getID())
|
||||
&&
|
||||
getOwner().equals(asset.getOwner())
|
||||
&&
|
||||
getOwnerOrg().equals(asset.getOwnerOrg());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(getID(), getValue(), getOwner(), getOwnerOrg());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Asset{" + "ID='" + ID + '\'' + ", Value=" + Value + ", Owner='"
|
||||
+ Owner + '\'' + ", OwnerOrg='" + OwnerOrg + '\'' + '}';
|
||||
}
|
||||
}
|
||||
|
|
@ -1,269 +0,0 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.hyperledger.fabric.samples.sbe;
|
||||
|
||||
import com.owlike.genson.Genson;
|
||||
import org.hyperledger.fabric.contract.Context;
|
||||
import org.hyperledger.fabric.contract.ContractInterface;
|
||||
import org.hyperledger.fabric.contract.annotation.Contract;
|
||||
import org.hyperledger.fabric.contract.annotation.Default;
|
||||
import org.hyperledger.fabric.contract.annotation.Info;
|
||||
import org.hyperledger.fabric.contract.annotation.License;
|
||||
import org.hyperledger.fabric.contract.annotation.Transaction;
|
||||
import org.hyperledger.fabric.protos.common.MSPPrincipal;
|
||||
import org.hyperledger.fabric.protos.common.MSPRole;
|
||||
import org.hyperledger.fabric.protos.common.SignaturePolicy;
|
||||
import org.hyperledger.fabric.protos.common.SignaturePolicyEnvelope;
|
||||
import org.hyperledger.fabric.shim.ChaincodeException;
|
||||
import org.hyperledger.fabric.shim.ChaincodeStub;
|
||||
import org.hyperledger.fabric.shim.ext.sbe.StateBasedEndorsement;
|
||||
import org.hyperledger.fabric.shim.ext.sbe.impl.StateBasedEndorsementFactory;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
@Contract(
|
||||
name = "sbe",
|
||||
info = @Info(
|
||||
title = "Asset Contract",
|
||||
description = "Asset Transfer Smart Contract, using State Based Endorsement(SBE), implemented in Java",
|
||||
version = "0.0.1-SNAPSHOT",
|
||||
license = @License(
|
||||
name = "Apache 2.0 License",
|
||||
url = "http://www.apache.org/licenses/LICENSE-2.0.html")))
|
||||
@Default
|
||||
public final class AssetContract implements ContractInterface {
|
||||
private final Genson genson = new Genson();
|
||||
|
||||
private enum AssetTransferErrors {
|
||||
ASSET_NOT_FOUND,
|
||||
ASSET_ALREADY_EXISTS
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new asset.
|
||||
* Sets the endorsement policy of the assetId Key, such that current owner Org Peer is required to endorse future updates.
|
||||
* Optionally, set the endorsement policy of the assetId Key, such that any 1(N) out of the Org's specified can endorse future updates.
|
||||
*
|
||||
* @param ctx the transaction context
|
||||
* @param assetId the id of the new asset
|
||||
* @param value the value of the new asset
|
||||
* @param owner the owner of the new asset
|
||||
* @return the created asset
|
||||
*/
|
||||
@Transaction(intent = Transaction.TYPE.SUBMIT)
|
||||
public Asset CreateAsset(final Context ctx, final String assetId, final int value, final String owner) {
|
||||
ChaincodeStub stub = ctx.getStub();
|
||||
|
||||
if (AssetExists(ctx, assetId)) {
|
||||
String errorMessage = String.format("Asset %s already exists", assetId);
|
||||
System.out.println(errorMessage);
|
||||
throw new ChaincodeException(errorMessage, AssetTransferErrors.ASSET_ALREADY_EXISTS.toString());
|
||||
}
|
||||
|
||||
final String ownerOrg = getClientOrgId(ctx);
|
||||
Asset asset = new Asset(assetId, value, owner, ownerOrg);
|
||||
String assetJSON = genson.serialize(asset);
|
||||
stub.putStringState(assetId, assetJSON);
|
||||
|
||||
// Set the endorsement policy of the assetId Key, such that current owner Org is required to endorse future updates
|
||||
setStateBasedEndorsement(ctx, assetId, List.of(ownerOrg));
|
||||
|
||||
// Optionally, set the endorsement policy of the assetId Key, such that any 1 Org (N) out of the specified Orgs can endorse future updates
|
||||
// setStateBasedEndorsementNOutOf(ctx, assetId, 1, new String[]{"Org1MSP", "Org2MSP"});
|
||||
|
||||
return asset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an asset with the given assetId.
|
||||
*
|
||||
* @param ctx the transaction context
|
||||
* @param assetId the id of the asset
|
||||
* @return the asset found on the ledger if there was one
|
||||
*/
|
||||
@Transaction(intent = Transaction.TYPE.EVALUATE)
|
||||
public String ReadAsset(final Context ctx, final String assetId) {
|
||||
ChaincodeStub stub = ctx.getStub();
|
||||
String assetJSON = stub.getStringState(assetId);
|
||||
|
||||
if (assetJSON == null || assetJSON.isEmpty()) {
|
||||
String errorMessage = String.format("Asset %s does not exist", assetId);
|
||||
System.out.println(errorMessage);
|
||||
throw new ChaincodeException(errorMessage, AssetTransferErrors.ASSET_NOT_FOUND.toString());
|
||||
}
|
||||
|
||||
return assetJSON;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the properties of an existing asset.
|
||||
* Needs an endorsement of current owner Org Peer.
|
||||
*
|
||||
* @param ctx the transaction context
|
||||
* @param assetId the id of the asset being updated
|
||||
* @param newValue the value of the asset being updated
|
||||
* @return the updated asset
|
||||
*/
|
||||
@Transaction(intent = Transaction.TYPE.SUBMIT)
|
||||
public Asset UpdateAsset(final Context ctx, final String assetId, final int newValue) {
|
||||
ChaincodeStub stub = ctx.getStub();
|
||||
|
||||
String assetString = ReadAsset(ctx, assetId);
|
||||
Asset asset = genson.deserialize(assetString, Asset.class);
|
||||
asset.setValue(newValue);
|
||||
String updatedAssetJSON = genson.serialize(asset);
|
||||
stub.putStringState(assetId, updatedAssetJSON);
|
||||
|
||||
return asset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given asset.
|
||||
* Needs an endorsement of current owner Org Peer.
|
||||
*
|
||||
* @param ctx the transaction context
|
||||
* @param assetId the id of the asset being deleted
|
||||
*/
|
||||
@Transaction(intent = Transaction.TYPE.SUBMIT)
|
||||
public void DeleteAsset(final Context ctx, final String assetId) {
|
||||
ChaincodeStub stub = ctx.getStub();
|
||||
|
||||
if (!AssetExists(ctx, assetId)) {
|
||||
String errorMessage = String.format("Asset %s does not exist", assetId);
|
||||
System.out.println(errorMessage);
|
||||
throw new ChaincodeException(errorMessage, AssetTransferErrors.ASSET_NOT_FOUND.toString());
|
||||
}
|
||||
|
||||
stub.delState(assetId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the owner & ownerOrg field of asset with given assetId, ownerOrg must be a valid Org MSP Id.
|
||||
* Needs an endorsement of current owner Org Peer.
|
||||
* Re-sets the endorsement policy of the assetId Key, such that new owner Org Peer is required to endorse future updates.
|
||||
*
|
||||
* @param ctx the transaction context
|
||||
* @param assetId the id of the asset being transferred
|
||||
* @param newOwner the new owner
|
||||
* @param newOwnerOrg the new owner Org MSPID
|
||||
* @return the updated asset
|
||||
*/
|
||||
@Transaction(intent = Transaction.TYPE.SUBMIT)
|
||||
public Asset TransferAsset(final Context ctx, final String assetId, final String newOwner, final String newOwnerOrg) {
|
||||
ChaincodeStub stub = ctx.getStub();
|
||||
|
||||
String assetString = ReadAsset(ctx, assetId);
|
||||
Asset asset = genson.deserialize(assetString, Asset.class);
|
||||
asset.setOwner(newOwner);
|
||||
asset.setOwnerOrg(newOwnerOrg);
|
||||
String updatedAssetJSON = genson.serialize(asset);
|
||||
stub.putStringState(assetId, updatedAssetJSON);
|
||||
|
||||
// Re-Set the endorsement policy of the assetId Key, such that a new owner Org Peer is required to endorse future updates
|
||||
setStateBasedEndorsement(ctx, assetId, List.of(newOwnerOrg));
|
||||
|
||||
// Optionally, set the endorsement policy of the assetId Key, such that any 1 Org (N) out of the specified Orgs can endorse future updates
|
||||
// setStateBasedEndorsementNOutOf(ctx, assetId, 1, List.of("Org1MSP", "Org2MSP"));
|
||||
|
||||
return asset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the existence of the asset.
|
||||
*
|
||||
* @param ctx the transaction context
|
||||
* @param assetId the id of the asset
|
||||
* @return boolean indicating the existence of the asset
|
||||
*/
|
||||
private boolean AssetExists(final Context ctx, final String assetId) {
|
||||
ChaincodeStub stub = ctx.getStub();
|
||||
String assetJSON = stub.getStringState(assetId);
|
||||
|
||||
return (assetJSON != null && !assetJSON.isEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the client's OrgId (MSPID)
|
||||
*
|
||||
* @param ctx the transaction context
|
||||
* @return String value of the Org MSPID
|
||||
*/
|
||||
private static String getClientOrgId(final Context ctx) {
|
||||
return ctx.getClientIdentity().getMSPID();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an endorsement policy to the assetId Key.
|
||||
* Enforces that the owner Org must endorse future update transactions for the specified assetId Key.
|
||||
*
|
||||
* @param ctx the transaction context
|
||||
* @param assetId the id of the asset
|
||||
* @param ownerOrgs the list of Owner Org MSPID's
|
||||
*/
|
||||
private static void setStateBasedEndorsement(final Context ctx, final String assetId, final List<String> ownerOrgs) {
|
||||
StateBasedEndorsement stateBasedEndorsement = StateBasedEndorsementFactory.getInstance().newStateBasedEndorsement(null);
|
||||
stateBasedEndorsement.addOrgs(StateBasedEndorsement.RoleType.RoleTypeMember, ownerOrgs.toArray(new String[0]));
|
||||
ctx.getStub().setStateValidationParameter(assetId, stateBasedEndorsement.policy());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an endorsement policy to the assetId Key.
|
||||
* Enforces that a given number of Orgs (N) out of the specified Orgs must endorse future update transactions for the specified assetId Key.
|
||||
*
|
||||
* @param ctx the transaction context
|
||||
* @param assetId the id of the asset
|
||||
* @param nOrgs the number of N Orgs to endorse out of the list of Orgs provided
|
||||
* @param ownerOrgs the list of Owner Org MSPID's
|
||||
*/
|
||||
private static void setStateBasedEndorsementNOutOf(final Context ctx, final String assetId, final int nOrgs, final List<String> ownerOrgs) {
|
||||
ctx.getStub().setStateValidationParameter(assetId, policy(nOrgs, ownerOrgs));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a policy that requires a given number (N) of Org principals signatures out of the provided list of Orgs
|
||||
*
|
||||
* @param nOrgs the number of Org principals signatures required to endorse (out of the provided list of Orgs)
|
||||
* @param mspIds the list of Owner Org MSPID's
|
||||
*/
|
||||
private static byte[] policy(final int nOrgs, final List<String> mspIds) {
|
||||
mspIds.sort(Comparator.naturalOrder());
|
||||
|
||||
var principals = mspIds.stream()
|
||||
.map(mspId -> MSPRole.newBuilder()
|
||||
.setMspIdentifier(mspId)
|
||||
.setRole(MSPRole.MSPRoleType.MEMBER)
|
||||
.build())
|
||||
.map(role -> MSPPrincipal.newBuilder()
|
||||
.setPrincipalClassification(MSPPrincipal.Classification.ROLE)
|
||||
.setPrincipal(role.toByteString())
|
||||
.build())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
var signPolicy = IntStream.range(0, mspIds.size())
|
||||
.mapToObj(AssetContract::signedBy)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Create the policy such that it requires any N signature's from all the principals provided
|
||||
return SignaturePolicyEnvelope.newBuilder()
|
||||
.setVersion(0)
|
||||
.setRule(nOutOf(nOrgs, signPolicy))
|
||||
.addAllIdentities(principals)
|
||||
.build()
|
||||
.toByteArray();
|
||||
}
|
||||
|
||||
private static SignaturePolicy signedBy(final int index) {
|
||||
return SignaturePolicy.newBuilder().setSignedBy(index).build();
|
||||
}
|
||||
|
||||
private static SignaturePolicy nOutOf(final int n, final List<SignaturePolicy> policies) {
|
||||
return SignaturePolicy.newBuilder().setNOutOf(
|
||||
SignaturePolicy.NOutOf.newBuilder().setN(n).addAllRules(policies).build()
|
||||
).build();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
package-lock.json
|
||||
|
||||
# Compiled TypeScript files
|
||||
dist
|
||||
|
||||
# Editor Config
|
||||
.editorconfig
|
||||
|
||||
# npm ignore
|
||||
.npmignore
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import js from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(js.configs.recommended, ...tseslint.configs.strictTypeChecked, {
|
||||
languageOptions: {
|
||||
ecmaVersion: 2023,
|
||||
sourceType: 'module',
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
});
|
||||
2205
asset-transfer-sbe/chaincode-typescript/npm-shrinkwrap.json
generated
2205
asset-transfer-sbe/chaincode-typescript/npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,35 +0,0 @@
|
|||
{
|
||||
"name": "asset-transfer-sbe",
|
||||
"version": "0.0.1",
|
||||
"description": "Asset Transfer contract, using State Based Endorsement(SBE), implemented in TypeScript",
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint src",
|
||||
"pretest": "npm run lint",
|
||||
"test": "echo 'No tests implemented'",
|
||||
"start": "fabric-chaincode-node start",
|
||||
"build": "tsc",
|
||||
"build:watch": "tsc -w",
|
||||
"prepublishOnly": "npm run build",
|
||||
"postinstall": "npm dedupe"
|
||||
},
|
||||
"engineStrict": true,
|
||||
"author": "Hyperledger",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"fabric-contract-api": "~2.5",
|
||||
"fabric-shim": "~2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.19.33",
|
||||
"@eslint/js": "^9.3.0",
|
||||
"@tsconfig/node18": "^18.2.4",
|
||||
"eslint": "^8.57.0",
|
||||
"typescript": "~5.4.5",
|
||||
"typescript-eslint": "^7.11.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Object, Property } from 'fabric-contract-api';
|
||||
|
||||
@Object()
|
||||
export class Asset {
|
||||
@Property()
|
||||
public ID: string = '';
|
||||
|
||||
@Property()
|
||||
public Value: number = 0;
|
||||
|
||||
@Property()
|
||||
public Owner: string = '';
|
||||
|
||||
@Property()
|
||||
public OwnerOrg: string = '';
|
||||
}
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Context, Contract, Info, Transaction } from 'fabric-contract-api';
|
||||
import { Asset } from './asset';
|
||||
import { KeyEndorsementPolicy } from 'fabric-shim';
|
||||
|
||||
@Info({title: 'AssetContract', description: 'Asset Transfer Smart Contract, using State Based Endorsement(SBE), implemented in TypeScript' })
|
||||
export class AssetContract extends Contract {
|
||||
// CreateAsset creates a new asset
|
||||
// CreateAsset sets the endorsement policy of the assetId Key, such that current owner Org Peer is required to endorse future updates
|
||||
@Transaction()
|
||||
public async CreateAsset(ctx: Context, assetId: string, value: number, owner: string): Promise<void> {
|
||||
const exists = await this.AssetExists(ctx, assetId);
|
||||
if (exists) {
|
||||
throw new Error(`The asset ${assetId} already exists`);
|
||||
}
|
||||
const ownerOrg = AssetContract.getClientOrgId(ctx);
|
||||
const asset = new Asset();
|
||||
asset.ID = assetId;
|
||||
asset.Value = value;
|
||||
asset.Owner = owner;
|
||||
asset.OwnerOrg = ownerOrg;
|
||||
const buffer = Buffer.from(JSON.stringify(asset));
|
||||
// Create the asset
|
||||
await ctx.stub.putState(assetId, buffer);
|
||||
|
||||
// Set the endorsement policy of the assetId Key, such that current owner Org is required to endorse future updates
|
||||
await AssetContract.setStateBasedEndorsement(ctx, assetId, [ownerOrg]);
|
||||
|
||||
// Optionally, set the endorsement policy of the assetId Key, such that any 1 Org (N) out of the specified Orgs can endorse future updates
|
||||
// await AssetContract.setStateBasedEndorsementNOutOf(ctx, assetId, 1, ["Org1MSP", "Org2MSP"]);
|
||||
}
|
||||
|
||||
// ReadAsset returns asset with given assetId
|
||||
@Transaction(false)
|
||||
public async ReadAsset(ctx: Context, assetId: string): Promise<string> {
|
||||
const exists = await this.AssetExists(ctx, assetId);
|
||||
if (!exists) {
|
||||
throw new Error(`The asset ${assetId} does not exist`);
|
||||
}
|
||||
// Read the asset
|
||||
const assetJSON = await ctx.stub.getState(assetId);
|
||||
return assetJSON.toString();
|
||||
}
|
||||
|
||||
// UpdateAsset updates an existing asset
|
||||
// UpdateAsset needs an endorsement of current owner Org Peer
|
||||
@Transaction()
|
||||
public async UpdateAsset(ctx: Context, assetId: string, newValue: number): Promise<void> {
|
||||
const assetString = await this.ReadAsset(ctx, assetId);
|
||||
const asset = JSON.parse(assetString) as Asset;
|
||||
asset.Value = newValue;
|
||||
const buffer = Buffer.from(JSON.stringify(asset));
|
||||
// Update the asset
|
||||
await ctx.stub.putState(assetId, buffer);
|
||||
}
|
||||
|
||||
// DeleteAsset deletes an given asset
|
||||
// DeleteAsset needs an endorsement of current owner Org Peer
|
||||
@Transaction()
|
||||
public async DeleteAsset(ctx: Context, assetId: string): Promise<void> {
|
||||
const exists = await this.AssetExists(ctx, assetId);
|
||||
if (!exists) {
|
||||
throw new Error(`The asset ${assetId} does not exist`);
|
||||
}
|
||||
// Delete the asset
|
||||
await ctx.stub.deleteState(assetId);
|
||||
}
|
||||
|
||||
// TransferAsset updates the Owner & OwnerOrg field of asset with given assetId, OwnerOrg must be a valid Org MSP Id
|
||||
// TransferAsset needs an endorsement of current owner Org Peer
|
||||
// TransferAsset re-sets the endorsement policy of the assetId Key, such that new owner Org Peer is required to endorse future updates
|
||||
@Transaction()
|
||||
public async TransferAsset(ctx: Context, assetId: string, newOwner: string, newOwnerOrg: string): Promise<void> {
|
||||
const assetString = await this.ReadAsset(ctx, assetId);
|
||||
const asset = JSON.parse(assetString) as Asset;
|
||||
asset.Owner = newOwner;
|
||||
asset.OwnerOrg = newOwnerOrg;
|
||||
// Update the asset
|
||||
await ctx.stub.putState(assetId, Buffer.from(JSON.stringify(asset)));
|
||||
// Re-Set the endorsement policy of the assetId Key, such that a new owner Org Peer is required to endorse future updates
|
||||
await AssetContract.setStateBasedEndorsement(ctx, asset.ID, [newOwnerOrg]);
|
||||
|
||||
// Optionally, set the endorsement policy of the assetId Key, such that any 1 Org (N) out of the specified Orgs can endorse future updates
|
||||
// await AssetContract.setStateBasedEndorsementNOutOf(ctx, assetId, 1, ["Org1MSP", "Org2MSP"]);
|
||||
}
|
||||
|
||||
// AssetExists returns true when asset with given ID exists
|
||||
public async AssetExists(ctx: Context, assetId: string): Promise<boolean> {
|
||||
const buffer = await ctx.stub.getState(assetId);
|
||||
return buffer.length > 0;
|
||||
}
|
||||
|
||||
// getClientOrgId gets the client's OrgId (MSPID)
|
||||
private static getClientOrgId(ctx: Context): string {
|
||||
return ctx.clientIdentity.getMSPID();
|
||||
}
|
||||
|
||||
// setStateBasedEndorsement sets an endorsement policy to the assetId Key
|
||||
// setStateBasedEndorsement enforces that the owner Org must endorse future update transactions for the specified assetId Key
|
||||
private static async setStateBasedEndorsement(ctx: Context, assetId: string, ownerOrgs: string[]): Promise<void> {
|
||||
const ep = new KeyEndorsementPolicy();
|
||||
ep.addOrgs('MEMBER', ...ownerOrgs);
|
||||
await ctx.stub.setStateValidationParameter(assetId, ep.getPolicy());
|
||||
}
|
||||
|
||||
// setStateBasedEndorsementNOutOf sets an endorsement policy to the assetId Key
|
||||
// setStateBasedEndorsementNOutOf enforces that a given number of Orgs (N) out of the specified Orgs must endorse future update transactions for the specified assetId Key.
|
||||
private static async setStateBasedEndorsementNOutOf(ctx: Context, assetId: string, nOrgs: number, ownerOrgs: string[]): Promise<void> {
|
||||
const ROLE_TYPE_MEMBER = 'MEMBER';
|
||||
|
||||
// Use the KeyEndorsementPolicy helper form the chaincode libarries
|
||||
// If you need more advanced policies, please use that helper as a reference point.
|
||||
const keyEndorsementPolicy = new KeyEndorsementPolicy();
|
||||
keyEndorsementPolicy.addOrgs(ROLE_TYPE_MEMBER, ...ownerOrgs);
|
||||
|
||||
await ctx.stub.setStateValidationParameter(assetId, keyEndorsementPolicy.getPolicy());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { AssetContract } from './assetContract';
|
||||
export { AssetContract } from './assetContract';
|
||||
|
||||
export const contracts: unknown[] = [ AssetContract ];
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@tsconfig/node18/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "dist",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noImplicitReturns": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src/"]
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
# Asset transfer secured agreement sample
|
||||
|
||||
The asset transfer events sample demonstrates how to transfer a private asset between two organizations without publicly sharing data .
|
||||
|
||||
## About the sample
|
||||
|
||||
This sample includes smart contract and application code in multiple languages. This sample shows how Fabric features state based endorsement, private data, and access control to provide secured transactions.
|
||||
|
||||
### Application
|
||||
|
||||
Refer [Secured asset transfer in Fabric](https://hyperledger-fabric.readthedocs.io/en/latest/secured_asset_transfer/secured_private_asset_transfer_tutorial.html) for application details .
|
||||
|
||||
### Smart Contract
|
||||
|
||||
The smart contract (in folder `chaincode-go`) implements the following functions to support the application:
|
||||
|
||||
- CreateAsset
|
||||
- ChangePublicDescription
|
||||
- AgreeToSell
|
||||
- AgreeToBuy
|
||||
- VerifyAssetProperties
|
||||
- TransferAsset
|
||||
- ReadAsset
|
||||
- GetAssetPrivateProperties
|
||||
- GetAssetSalesPrice
|
||||
- GetAssetBidPrice
|
||||
- GetAssetHashId
|
||||
- QueryAssetSaleAgreements
|
||||
- QueryAssetBuyAgreements
|
||||
- QueryAssetHistory
|
||||
|
||||
## Running the sample
|
||||
|
||||
Like other samples, the Fabric test network is used to deploy and run this sample. Follow these steps in order:
|
||||
|
||||
1. Create the test network and a channel (from the `test-network` folder).
|
||||
```
|
||||
./network.sh up createChannel -c mychannel -ca
|
||||
```
|
||||
|
||||
1. Deploy the smart contract implementations.
|
||||
```
|
||||
# To deploy the go chaincode implementation
|
||||
./network.sh deployCC -ccn secured -ccp ../asset-transfer-secured-agreement/chaincode-go/ -ccl go -ccep "OR('Org1MSP.peer','Org2MSP.peer')"
|
||||
```
|
||||
|
||||
1. Run the application (from the `asset-transfer-secured-agreement` folder).
|
||||
```
|
||||
# To run the Typescript sample application
|
||||
cd application-gateway-typescript
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
## Clean up
|
||||
|
||||
When you are finished, you can bring down the test network (from the `test-network` folder). The command will remove all the nodes of the test network, and delete any ledger data that you created.
|
||||
|
||||
```
|
||||
./network.sh down
|
||||
```
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Compiled TypeScript files
|
||||
dist
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import js from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(js.configs.recommended, ...tseslint.configs.strictTypeChecked, {
|
||||
languageOptions: {
|
||||
ecmaVersion: 2023,
|
||||
sourceType: 'module',
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
{
|
||||
"name": "asset-transfer-basic",
|
||||
"version": "1.0.0",
|
||||
"description": "Asset Transfer Secured Agreement Application implemented in typeScript using fabric-gateway",
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"build:watch": "tsc -w",
|
||||
"lint": "eslint src",
|
||||
"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.10",
|
||||
"@hyperledger/fabric-gateway": "^1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.3.0",
|
||||
"@tsconfig/node18": "^18.2.2",
|
||||
"@types/node": "^18.18.6",
|
||||
"eslint": "^8.57.0",
|
||||
"typescript": "~5.4",
|
||||
"typescript-eslint": "^7.13.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,215 +0,0 @@
|
|||
/*
|
||||
* Copyright IBM Corp. All Rights Reserved.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { connect } from '@hyperledger/fabric-gateway';
|
||||
|
||||
import { newGrpcConnection, newIdentity, newSigner, tlsCertPathOrg1, peerEndpointOrg1, peerNameOrg1, certDirectoryPathOrg1, mspIdOrg1, keyDirectoryPathOrg1, tlsCertPathOrg2, peerEndpointOrg2, peerNameOrg2, certDirectoryPathOrg2, mspIdOrg2, keyDirectoryPathOrg2 } from './connect';
|
||||
import { ContractWrapper } from './contractWrapper';
|
||||
import { RED, RESET } from './utils';
|
||||
|
||||
const channelName = 'mychannel';
|
||||
const chaincodeName = 'secured';
|
||||
|
||||
// Use a random key so that we can run multiple times
|
||||
const now = Date.now().toString();
|
||||
let assetKey: string;
|
||||
|
||||
async function main(): Promise<void> {
|
||||
|
||||
// The gRPC client connection from org1 should be shared by all Gateway connections to this endpoint.
|
||||
const clientOrg1 = await newGrpcConnection(
|
||||
tlsCertPathOrg1,
|
||||
peerEndpointOrg1,
|
||||
peerNameOrg1
|
||||
);
|
||||
|
||||
const gatewayOrg1 = connect({
|
||||
client: clientOrg1,
|
||||
identity: await newIdentity(certDirectoryPathOrg1, mspIdOrg1),
|
||||
signer: await newSigner(keyDirectoryPathOrg1),
|
||||
});
|
||||
|
||||
// The gRPC client connection from org2 should be shared by all Gateway connections to this endpoint.
|
||||
const clientOrg2 = await newGrpcConnection(
|
||||
tlsCertPathOrg2,
|
||||
peerEndpointOrg2,
|
||||
peerNameOrg2
|
||||
);
|
||||
|
||||
const gatewayOrg2 = connect({
|
||||
client: clientOrg2,
|
||||
identity: await newIdentity(certDirectoryPathOrg2, mspIdOrg2),
|
||||
signer: await newSigner(keyDirectoryPathOrg2),
|
||||
});
|
||||
|
||||
|
||||
try {
|
||||
|
||||
// Get the smart contract from the network for Org1.
|
||||
const contractOrg1 = gatewayOrg1.getNetwork(channelName).getContract(chaincodeName);
|
||||
const contractWrapperOrg1 = new ContractWrapper(contractOrg1, mspIdOrg1);
|
||||
|
||||
// Get the smart contract from the network for Org2.
|
||||
const contractOrg2 = gatewayOrg2.getNetwork(channelName).getContract(chaincodeName);
|
||||
const contractWrapperOrg2 = new ContractWrapper(contractOrg2, mspIdOrg2);
|
||||
|
||||
// Create an asset by organization Org1, this only requires the owning organization to endorse.
|
||||
assetKey = await contractWrapperOrg1.createAsset(mspIdOrg1,
|
||||
`Asset owned by ${mspIdOrg1} is not for sale`, { ObjectType: 'asset_properties', Color: 'blue', Size: 35 });
|
||||
|
||||
// Read the public details by org1.
|
||||
await contractWrapperOrg1.readAsset(assetKey, mspIdOrg1);
|
||||
|
||||
// Read the public details by org2.
|
||||
await contractWrapperOrg2.readAsset(assetKey, mspIdOrg1);
|
||||
|
||||
// Org1 should be able to read the private data details of the asset.
|
||||
await contractWrapperOrg1.getAssetPrivateProperties(assetKey, mspIdOrg1);
|
||||
|
||||
// Org2 is not the owner and does not have the private details, read expected to fail.
|
||||
try {
|
||||
await contractWrapperOrg2.getAssetPrivateProperties(assetKey, mspIdOrg1);
|
||||
} catch (e) {
|
||||
console.log(`${RED}*** Successfully caught the failure: getAssetPrivateProperties - ${String(e)}${RESET}`);
|
||||
}
|
||||
|
||||
// Org1 updates the assets public description.
|
||||
await contractWrapperOrg1.changePublicDescription({assetId: assetKey,
|
||||
ownerOrg: mspIdOrg1,
|
||||
publicDescription: `Asset ${assetKey} owned by ${mspIdOrg1} is for sale`});
|
||||
|
||||
// Read the public details by org1.
|
||||
await contractWrapperOrg1.readAsset(assetKey, mspIdOrg1);
|
||||
|
||||
// Read the public details by org2.
|
||||
await contractWrapperOrg2.readAsset(assetKey, mspIdOrg1);
|
||||
|
||||
// This is an update to the public state and requires the owner(Org1) to endorse and sent by the owner org client (Org1).
|
||||
// Since the client is from Org2, which is not the owner, this will fail.
|
||||
try{
|
||||
await contractWrapperOrg2.changePublicDescription({assetId: assetKey,
|
||||
ownerOrg: mspIdOrg1,
|
||||
publicDescription: `Asset ${assetKey} owned by ${mspIdOrg2} is NOT for sale`});
|
||||
} catch(e) {
|
||||
console.log(`${RED}*** Successfully caught the failure: changePublicDescription - ${String(e)}${RESET}`);
|
||||
}
|
||||
|
||||
// Read the public details by org1.
|
||||
await contractWrapperOrg1.readAsset(assetKey, mspIdOrg1);
|
||||
|
||||
// Read the public details by org2.
|
||||
await contractWrapperOrg2.readAsset(assetKey, mspIdOrg1);
|
||||
|
||||
// Agree to a sell by org1.
|
||||
await contractWrapperOrg1.agreeToSell({
|
||||
assetId: assetKey,
|
||||
price: 110,
|
||||
tradeId: now,
|
||||
});
|
||||
|
||||
// Check the private information about the asset from Org2. Org1 would have to send Org2 asset details,
|
||||
// so the hash of the details may be checked by the chaincode.
|
||||
await contractWrapperOrg2.verifyAssetProperties(assetKey, {color:'blue', size:35});
|
||||
|
||||
// Agree to a buy by org2.
|
||||
await contractWrapperOrg2.agreeToBuy( {assetId: assetKey,
|
||||
price: 100,
|
||||
tradeId: now}, { ObjectType: 'asset_properties', Color: 'blue', Size: 35 });
|
||||
|
||||
// Org1 should be able to read the sale price of this asset.
|
||||
await contractWrapperOrg1.getAssetSalesPrice(assetKey, mspIdOrg1);
|
||||
|
||||
// Org2 has not set a sale price and this should fail.
|
||||
try{
|
||||
await contractWrapperOrg2.getAssetSalesPrice(assetKey, mspIdOrg1);
|
||||
} catch(e) {
|
||||
console.log(`${RED}*** Successfully caught the failure: getAssetSalesPrice - ${String(e)}${RESET}`);
|
||||
}
|
||||
|
||||
// Org1 has not agreed to buy so this should fail.
|
||||
try{
|
||||
await contractWrapperOrg1.getAssetBidPrice(assetKey, mspIdOrg2);
|
||||
} catch(e) {
|
||||
console.log(`${RED}*** Successfully caught the failure: getAssetBidPrice - ${String(e)}${RESET}`);
|
||||
}
|
||||
// Org2 should be able to see the price it has agreed.
|
||||
await contractWrapperOrg2.getAssetBidPrice(assetKey, mspIdOrg2);
|
||||
|
||||
// Org1 will try to transfer the asset to Org2
|
||||
// This will fail due to the sell price and the bid price are not the same.
|
||||
try{
|
||||
await contractWrapperOrg1.transferAsset({ assetId: assetKey, price: 110, tradeId: now}, [ mspIdOrg1, mspIdOrg2 ], mspIdOrg1, mspIdOrg2);
|
||||
} catch(e) {
|
||||
console.log(`${RED}*** Successfully caught the failure: transferAsset - ${String(e)}${RESET}`);
|
||||
}
|
||||
// Agree to a sell by Org1, the seller will agree to the bid price of Org2.
|
||||
await contractWrapperOrg1.agreeToSell({assetId:assetKey, price:100, tradeId:now});
|
||||
|
||||
// Read the public details by org1.
|
||||
await contractWrapperOrg1.readAsset(assetKey, mspIdOrg1);
|
||||
|
||||
// Read the public details by org2.
|
||||
await contractWrapperOrg2.readAsset(assetKey, mspIdOrg1);
|
||||
|
||||
// Org1 should be able to read the private data details of the asset.
|
||||
await contractWrapperOrg1.getAssetPrivateProperties(assetKey, mspIdOrg1);
|
||||
|
||||
// Org1 should be able to read the sale price of this asset.
|
||||
await contractWrapperOrg1.getAssetSalesPrice(assetKey, mspIdOrg1);
|
||||
|
||||
// Org2 should be able to see the price it has agreed.
|
||||
await contractWrapperOrg2.getAssetBidPrice(assetKey, mspIdOrg2);
|
||||
|
||||
// Org2 user will try to transfer the asset to Org1.
|
||||
// This will fail as the owner is Org1.
|
||||
try{
|
||||
await contractWrapperOrg2.transferAsset({ assetId: assetKey, price: 100, tradeId: now}, [ mspIdOrg1, mspIdOrg2 ], mspIdOrg1, mspIdOrg2);
|
||||
} catch(e) {
|
||||
console.log(`${RED}*** Successfully caught the failure: transferAsset - ${String(e)}${RESET}`);
|
||||
}
|
||||
|
||||
// Org1 will transfer the asset to Org2.
|
||||
// This will now complete as the sell price and the bid price are the same.
|
||||
await contractWrapperOrg1.transferAsset({ assetId: assetKey, price: 100, tradeId: now}, [ mspIdOrg1, mspIdOrg2 ], mspIdOrg1, mspIdOrg2);
|
||||
|
||||
// Read the public details by org1.
|
||||
await contractWrapperOrg1.readAsset(assetKey, mspIdOrg2);
|
||||
|
||||
// Read the public details by org2.
|
||||
await contractWrapperOrg2.readAsset(assetKey, mspIdOrg2);
|
||||
|
||||
// Org2 should be able to read the private data details of this asset.
|
||||
await contractWrapperOrg2.getAssetPrivateProperties(assetKey, mspIdOrg2);
|
||||
|
||||
// Org1 should not be able to read the private data details of this asset, expected to fail.
|
||||
try{
|
||||
await contractWrapperOrg1.getAssetPrivateProperties(assetKey, mspIdOrg2);
|
||||
} catch(e) {
|
||||
console.log(`${RED}*** Successfully caught the failure: getAssetPrivateProperties - ${String(e)}${RESET}`);
|
||||
}
|
||||
|
||||
// This is an update to the public state and requires only the owner to endorse.
|
||||
// Org2 wants to indicate that the items is no longer for sale.
|
||||
await contractWrapperOrg2.changePublicDescription( {assetId: assetKey, ownerOrg: mspIdOrg2, publicDescription: `Asset ${assetKey} owned by ${mspIdOrg2} is NOT for sale`});
|
||||
|
||||
// Read the public details by org1.
|
||||
await contractWrapperOrg1.readAsset(assetKey, mspIdOrg2);
|
||||
|
||||
// Read the public details by org2.
|
||||
await contractWrapperOrg2.readAsset(assetKey, mspIdOrg2);
|
||||
|
||||
} finally {
|
||||
gatewayOrg1.close();
|
||||
gatewayOrg2.close();
|
||||
clientOrg1.close();
|
||||
clientOrg2.close();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error: unknown) => {
|
||||
console.error('******** FAILED to run the application:', error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
/*
|
||||
* Copyright IBM Corp. All Rights Reserved.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as grpc from '@grpc/grpc-js';
|
||||
import { Identity, Signer, signers } from '@hyperledger/fabric-gateway';
|
||||
import * as crypto from 'crypto';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
// MSP Id's of Organizations
|
||||
export const mspIdOrg1 = 'Org1MSP';
|
||||
export const mspIdOrg2 = 'Org2MSP';
|
||||
|
||||
// Path to org1 crypto materials.
|
||||
export const cryptoPathOrg1 = path.resolve(__dirname, '..', '..', '..', 'test-network', 'organizations', 'peerOrganizations', 'org1.example.com');
|
||||
|
||||
// Path to user private key directory.
|
||||
export const keyDirectoryPathOrg1 = path.resolve(cryptoPathOrg1, 'users', 'User1@org1.example.com', 'msp', 'keystore');
|
||||
|
||||
// Path to user certificate.
|
||||
export const certDirectoryPathOrg1 = path.resolve(cryptoPathOrg1, 'users', 'User1@org1.example.com', 'msp', 'signcerts');
|
||||
|
||||
// Path to peer tls certificate.
|
||||
export const tlsCertPathOrg1 = path.resolve(cryptoPathOrg1, 'peers', 'peer0.org1.example.com', 'tls', 'ca.crt');
|
||||
|
||||
// Path to org2 crypto materials.
|
||||
export const cryptoPathOrg2 = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
'test-network',
|
||||
'organizations',
|
||||
'peerOrganizations',
|
||||
'org2.example.com'
|
||||
);
|
||||
|
||||
// Path to org2 user private key directory.
|
||||
export const keyDirectoryPathOrg2 = path.resolve(
|
||||
cryptoPathOrg2,
|
||||
'users',
|
||||
'User1@org2.example.com',
|
||||
'msp',
|
||||
'keystore'
|
||||
);
|
||||
|
||||
// Path to org2 user certificate.
|
||||
export const certDirectoryPathOrg2 = path.resolve(
|
||||
cryptoPathOrg2,
|
||||
'users',
|
||||
'User1@org2.example.com',
|
||||
'msp',
|
||||
'signcerts'
|
||||
);
|
||||
|
||||
// Path to org2 peer tls certificate.
|
||||
export const tlsCertPathOrg2 = path.resolve(
|
||||
cryptoPathOrg2,
|
||||
'peers',
|
||||
'peer0.org2.example.com',
|
||||
'tls',
|
||||
'ca.crt'
|
||||
);
|
||||
// Gateway peer endpoint.
|
||||
export const peerEndpointOrg1 = 'localhost:7051';
|
||||
export const peerEndpointOrg2 = 'localhost:9051';
|
||||
|
||||
// Gateway peer container name.
|
||||
export const peerNameOrg1 = 'peer0.org1.example.com';
|
||||
export const peerNameOrg2 = 'peer0.org2.example.com';
|
||||
|
||||
// Collection Names
|
||||
export const org1PrivateCollectionName = 'Org1MSPPrivateCollection';
|
||||
export const org2PrivateCollectionName = 'Org2MSPPrivateCollection';
|
||||
|
||||
export async function newGrpcConnection(
|
||||
tlsCertPath: string,
|
||||
peerEndpoint: string,
|
||||
peerName: string
|
||||
): 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': peerName,
|
||||
});
|
||||
}
|
||||
|
||||
export async function newIdentity(certDirectoryPath: string, mspId: string): Promise<Identity> {
|
||||
const certPath = await getFirstDirFileName(certDirectoryPath);
|
||||
const credentials = await fs.readFile(certPath);
|
||||
return { mspId, credentials };
|
||||
}
|
||||
|
||||
export async function newSigner(keyDirectoryPath: string): Promise<Signer> {
|
||||
const keyPath = await getFirstDirFileName(keyDirectoryPath);
|
||||
const privateKeyPem = await fs.readFile(keyPath);
|
||||
const privateKey = crypto.createPrivateKey(privateKeyPem);
|
||||
return signers.newPrivateKeySigner(privateKey);
|
||||
}
|
||||
|
||||
async function getFirstDirFileName(dirPath: string): Promise<string> {
|
||||
const files = await fs.readdir(dirPath);
|
||||
const file = files[0];
|
||||
if (!file) {
|
||||
throw new Error(`No files in directory: ${dirPath}`);
|
||||
}
|
||||
return path.join(dirPath, file);
|
||||
}
|
||||
|
|
@ -1,271 +0,0 @@
|
|||
/*
|
||||
* Copyright IBM Corp. All Rights Reserved.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import { Contract } from '@hyperledger/fabric-gateway';
|
||||
import { TextDecoder } from 'util';
|
||||
import { GREEN, parse, RED, RESET } from './utils';
|
||||
import crypto from 'crypto';
|
||||
import { mspIdOrg2 } from './connect';
|
||||
|
||||
const randomBytes = crypto.randomBytes(256).toString('hex');
|
||||
|
||||
interface AssetJSON {
|
||||
objectType: string;
|
||||
assetID: string;
|
||||
ownerOrg: string;
|
||||
publicDescription: string;
|
||||
}
|
||||
|
||||
interface AssetPropertiesJSON {
|
||||
objectType: string;
|
||||
color: string;
|
||||
size: number;
|
||||
salt: string;
|
||||
}
|
||||
|
||||
interface AssetPriceJSON {
|
||||
assetID: string;
|
||||
price: number;
|
||||
tradeID: string;
|
||||
}
|
||||
|
||||
export interface AssetPrivateData {
|
||||
ObjectType: string;
|
||||
Color: string;
|
||||
Size: number;
|
||||
}
|
||||
|
||||
export interface Asset {
|
||||
assetId: string;
|
||||
ownerOrg: string;
|
||||
publicDescription: string;
|
||||
}
|
||||
|
||||
export interface AssetProperties {
|
||||
color: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface AssetPrice {
|
||||
assetId: string;
|
||||
price: number;
|
||||
tradeId: string;
|
||||
}
|
||||
|
||||
export class ContractWrapper {
|
||||
|
||||
readonly #contract: Contract;
|
||||
readonly #org: string;
|
||||
readonly #utf8Decoder = new TextDecoder();
|
||||
readonly #randomBytes: string = randomBytes;
|
||||
#endorsingOrgs: { [id: string]: string[] };
|
||||
|
||||
public constructor(contract: Contract, org: string) {
|
||||
this.#contract = contract;
|
||||
this.#org = org;
|
||||
this.#endorsingOrgs = {};
|
||||
}
|
||||
|
||||
public async createAsset(ownerOrg: string, publicDescription: string, privateData: AssetPrivateData): Promise<string> {
|
||||
console.log(`${GREEN}--> Submit Transaction: CreateAsset as ${ownerOrg} - endorsed by Org1.${RESET}`);
|
||||
const assetPropertiesJSON: AssetPropertiesJSON = {
|
||||
objectType: 'asset_properties',
|
||||
color: privateData.Color,
|
||||
size: privateData.Size,
|
||||
salt: this.#randomBytes };
|
||||
|
||||
const resultBytes = await this.#contract.submit('CreateAsset', {
|
||||
arguments: [publicDescription],
|
||||
transientData: { asset_properties: JSON.stringify(assetPropertiesJSON)},
|
||||
});
|
||||
const assetID = this.#utf8Decoder.decode(resultBytes);
|
||||
this.#endorsingOrgs[assetID] = [ownerOrg];
|
||||
console.log(`*** Result: committed, asset ${assetID} is owned by ${ownerOrg}`);
|
||||
return assetID;
|
||||
}
|
||||
|
||||
public async readAsset(assetKey: string, ownerOrg: string): Promise<void> {
|
||||
console.log(`${GREEN}--> Evaluate Transactions: ReadAsset as ${this.#org}, - ${assetKey} should be owned by ${ownerOrg}.${RESET}`);
|
||||
|
||||
const resultBytes = await this.#contract.evaluateTransaction('ReadAsset', assetKey);
|
||||
|
||||
const result = this.#utf8Decoder.decode(resultBytes);
|
||||
if (result.length !== 0) {
|
||||
const json = parse<AssetJSON>(result);
|
||||
if (json.ownerOrg === ownerOrg) {
|
||||
console.log(`*** Result from ${this.#org} - asset ${json.assetID} owned by ${json.ownerOrg} DESC: ${json.publicDescription}`);
|
||||
} else {
|
||||
console.log(`${RED}*** Failed owner check from ${this.#org} - asset ${json.assetID} owned by ${json.ownerOrg} DESC:${json.publicDescription}.${RESET}`);
|
||||
}
|
||||
} else {
|
||||
throw new Error('No Asset Found');
|
||||
}
|
||||
}
|
||||
|
||||
public async getAssetPrivateProperties(assetKey: string, ownerOrg: string): Promise<void> {
|
||||
console.log(`${GREEN}--> Evaluate Transaction: GetAssetPrivateProperties, - ${assetKey} from organization ${this.#org}.${RESET}`);
|
||||
if(this.#org !== ownerOrg) {
|
||||
console.log(`${GREEN}* Expected to fail as ${this.#org} is not the owner and does not have the private details.${RESET}`);
|
||||
}
|
||||
|
||||
const resultBytes = await this.#contract.evaluateTransaction('GetAssetPrivateProperties', assetKey);
|
||||
|
||||
const resultString = this.#utf8Decoder.decode(resultBytes);
|
||||
const json = parse<AssetPropertiesJSON>(resultString);
|
||||
const result: AssetProperties = {
|
||||
color: json.color,
|
||||
size: json.size,
|
||||
};
|
||||
console.log('*** Result:', result);
|
||||
}
|
||||
|
||||
|
||||
public async changePublicDescription(asset: Asset): Promise<void> {
|
||||
console.log(`${GREEN}--> Submit Transaction: ChangePublicDescription ${asset.assetId}, as ${this.#org} - endorse by ${this.#org}.${RESET}`);
|
||||
if (asset.ownerOrg !== this.#org) {
|
||||
console.log(`${GREEN}* Expected to fail as ${this.#org} is not the owner.${RESET}`);
|
||||
}
|
||||
|
||||
await this.#contract.submit('ChangePublicDescription', {
|
||||
arguments:[asset.assetId, asset.publicDescription],
|
||||
endorsingOrganizations: this.#endorsingOrgs[asset.assetId]
|
||||
});
|
||||
|
||||
console.log(`*** Result: committed, Desc: ${asset.publicDescription}`);
|
||||
}
|
||||
|
||||
public async agreeToSell(assetPrice: AssetPrice): Promise<void> {
|
||||
|
||||
console.log(`${GREEN}--> Submit Transaction: AgreeToSell, ${assetPrice.assetId} as ${this.#org} - endorsed by ${this.#org}.${RESET}`);
|
||||
const assetPriceJSON: AssetPriceJSON = {
|
||||
assetID:assetPrice.assetId,
|
||||
price:assetPrice.price,
|
||||
tradeID:assetPrice.tradeId
|
||||
};
|
||||
|
||||
await this.#contract.submit('AgreeToSell', {
|
||||
arguments:[assetPrice.assetId],
|
||||
transientData: {asset_price: JSON.stringify(assetPriceJSON)},
|
||||
endorsingOrganizations: this.#endorsingOrgs[assetPrice.assetId]
|
||||
});
|
||||
|
||||
console.log(`*** Result: committed, ${this.#org} has agreed to sell asset ${assetPrice.assetId} for ${String(assetPrice.price)}`);
|
||||
}
|
||||
|
||||
public async verifyAssetProperties(assetId: string, assetProperties: AssetProperties): Promise<void> {
|
||||
console.log(`${GREEN}--> Evalute: VerifyAssetProperties, ${assetId} as ${this.#org} - endorsed by ${this.#org} and ${mspIdOrg2}.${RESET}`);
|
||||
const assetPropertiesJSON: AssetPropertiesJSON = {objectType: 'asset_properties',
|
||||
color: assetProperties.color,
|
||||
size: assetProperties.size,
|
||||
salt: this.#randomBytes };
|
||||
|
||||
const resultBytes = await this.#contract.evaluate('VerifyAssetProperties', {
|
||||
arguments:[assetId],
|
||||
transientData: {asset_properties: JSON.stringify(assetPropertiesJSON)},
|
||||
});
|
||||
|
||||
const resultString = this.#utf8Decoder.decode(resultBytes);
|
||||
if (resultString.length !== 0) {
|
||||
const json = parse<AssetPropertiesJSON>(resultString);
|
||||
if (typeof json === 'object') {
|
||||
console.log(`*** Success VerifyAssetProperties, private information about asset ${assetId} has been verified by ${this.#org}`);
|
||||
} else {
|
||||
console.log(`*** Failed: VerifyAssetProperties, private information about asset ${assetId} has not been verified by ${this.#org}`);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Private information about asset ${assetId} has not been verified by ${this.#org}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async agreeToBuy(assetPrice: AssetPrice, privateData: AssetPrivateData): Promise<void> {
|
||||
|
||||
console.log(`${GREEN}--> Submit Transaction: AgreeToBuy, ${assetPrice.assetId} as ${this.#org} - endorsed by ${this.#org} and ${mspIdOrg2}.${RESET}`);
|
||||
const assetPropertiesJSON: AssetPropertiesJSON = {
|
||||
objectType: 'asset_properties',
|
||||
color: privateData.Color,
|
||||
size: privateData.Size,
|
||||
salt: this.#randomBytes };
|
||||
|
||||
const assetPriceJSON: AssetPriceJSON = {
|
||||
assetID: assetPrice.assetId,
|
||||
price: assetPrice.price,
|
||||
tradeID: assetPrice.tradeId
|
||||
};
|
||||
|
||||
await this.#contract.submit('AgreeToBuy', {
|
||||
arguments:[assetPrice.assetId],
|
||||
transientData: {
|
||||
asset_price: JSON.stringify(assetPriceJSON),
|
||||
asset_properties: JSON.stringify(assetPropertiesJSON)
|
||||
},
|
||||
endorsingOrganizations: this.#endorsingOrgs[assetPrice.assetId]
|
||||
});
|
||||
|
||||
console.log(`*** Result: committed, ${this.#org} has agreed to buy asset ${assetPrice.assetId} for 100`);
|
||||
|
||||
}
|
||||
|
||||
public async getAssetSalesPrice(assetKey: string, ownerOrg: string): Promise<void> {
|
||||
|
||||
console.log(`${GREEN}--> Evaluate Transaction: GetAssetSalesPrice, - ${assetKey} from organization ${this.#org}.${RESET}`);
|
||||
if(this.#org !== ownerOrg) {
|
||||
console.log(`${GREEN}* Expected to fail as ${this.#org} has not set a sale price.${RESET}`);
|
||||
}
|
||||
|
||||
const resultBytes = await this.#contract.evaluateTransaction('GetAssetSalesPrice', assetKey);
|
||||
|
||||
const resultString = this.#utf8Decoder.decode(resultBytes);
|
||||
const json = parse<AssetPriceJSON>(resultString);
|
||||
|
||||
const result: AssetPrice = {
|
||||
assetId: json.assetID,
|
||||
price: json.price,
|
||||
tradeId: json.tradeID
|
||||
};
|
||||
|
||||
console.log('*** Result: GetAssetSalesPrice', result);
|
||||
}
|
||||
|
||||
public async getAssetBidPrice(assetKey: string, buyerOrgID: string): Promise<void> {
|
||||
|
||||
console.log(`${GREEN}--> Evaluate Transaction: GetAssetBidPrice, - ${assetKey} from organization ${this.#org}.${RESET}`);
|
||||
if(this.#org !== buyerOrgID){
|
||||
console.log(`${GREEN}* Expected to fail as ${this.#org} has not agreed to buy.${RESET}`);
|
||||
}
|
||||
|
||||
const resultBytes = await this.#contract.evaluateTransaction('GetAssetBidPrice', assetKey);
|
||||
|
||||
const resultString = this.#utf8Decoder.decode(resultBytes);
|
||||
const json = parse<AssetPriceJSON>(resultString);
|
||||
const result: AssetPrice = {
|
||||
assetId: json.assetID,
|
||||
price: json.price,
|
||||
tradeId: json.tradeID,
|
||||
};
|
||||
|
||||
console.log('*** Result: GetAssetBidPrice', result);
|
||||
}
|
||||
|
||||
public async transferAsset(assetPrice: AssetPrice, endorsingOrganizations: string[], ownerOrgID: string, buyerOrgID: string): Promise<void> {
|
||||
|
||||
console.log(`${GREEN}--> Submit Transaction: TransferAsset, ${assetPrice.assetId} as ${this.#org } - endorsed by ${this.#org} and ${buyerOrgID}.${RESET}`);
|
||||
|
||||
if (this.#org !== ownerOrgID) {
|
||||
console.log(`${GREEN}* Expected to fail as the owner is ${ownerOrgID}.${RESET}`);
|
||||
} else if (assetPrice.price === 110) {
|
||||
console.log(`${GREEN}* Expected to fail as sell price and the bid price are not the same.${RESET}`);
|
||||
}
|
||||
|
||||
const assetPriceJSON: AssetPriceJSON = { assetID: assetPrice.assetId, price:assetPrice.price, tradeID:assetPrice.tradeId};
|
||||
|
||||
await this.#contract.submit('TransferAsset', {
|
||||
arguments:[assetPrice.assetId, buyerOrgID],
|
||||
transientData: { asset_price: JSON.stringify(assetPriceJSON) },
|
||||
endorsingOrganizations: endorsingOrganizations
|
||||
});
|
||||
|
||||
console.log(`${GREEN}*** Result: committed, ${this.#org} has transfered the asset ${assetPrice.assetId} to ${buyerOrgID}.${RESET}`);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
/*
|
||||
* Copyright IBM Corp. All Rights Reserved.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
export const RED = '\x1b[31m\n';
|
||||
export const GREEN = '\x1b[32m\n';
|
||||
export const RESET = '\x1b[0m';
|
||||
|
||||
export function parse<T>(data: string): T {
|
||||
return JSON.parse(data) as T;
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
{
|
||||
"extends": "@tsconfig/node18/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"noUnusedLocals": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["./src/**/*"],
|
||||
"exclude": ["./src/**/*.spec.ts"]
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
[Secured asset transfer in Fabric Tutorial](https://hyperledger-fabric.readthedocs.io/en/latest/secured_asset_transfer/secured_private_asset_transfer_tutorial.html)
|
||||
|
|
@ -1,619 +0,0 @@
|
|||
/*
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/hyperledger/fabric-chaincode-go/v2/pkg/statebased"
|
||||
"github.com/hyperledger/fabric-chaincode-go/v2/shim"
|
||||
"github.com/hyperledger/fabric-contract-api-go/v2/contractapi"
|
||||
)
|
||||
|
||||
const (
|
||||
typeAssetForSale = "S"
|
||||
typeAssetBid = "B"
|
||||
typeAssetSaleReceipt = "SR"
|
||||
typeAssetBuyReceipt = "BR"
|
||||
)
|
||||
|
||||
type SmartContract struct {
|
||||
contractapi.Contract
|
||||
}
|
||||
|
||||
// Asset struct and properties must be exported (start with capitals) to work with contract api metadata
|
||||
type Asset struct {
|
||||
ObjectType string `json:"objectType"` // ObjectType is used to distinguish different object types in the same chaincode namespace
|
||||
ID string `json:"assetID"`
|
||||
OwnerOrg string `json:"ownerOrg"`
|
||||
PublicDescription string `json:"publicDescription"`
|
||||
}
|
||||
|
||||
type receipt struct {
|
||||
price int
|
||||
timestamp time.Time
|
||||
}
|
||||
|
||||
// CreateAsset creates an asset, sets it as owned by the client's org and returns its id
|
||||
// the id of the asset corresponds to the hash of the properties of the asset that are passed by transiet field
|
||||
func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface, publicDescription string) (string, error) {
|
||||
transientMap, err := ctx.GetStub().GetTransient()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error getting transient: %v", err)
|
||||
}
|
||||
|
||||
// Asset properties must be retrieved from the transient field as they are private
|
||||
immutablePropertiesJSON, ok := transientMap["asset_properties"]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("asset_properties key not found in the transient map")
|
||||
}
|
||||
|
||||
// AssetID will be the hash of the asset's properties
|
||||
hash := sha256.New()
|
||||
hash.Write(immutablePropertiesJSON)
|
||||
assetID := hex.EncodeToString(hash.Sum(nil))
|
||||
|
||||
// Get the clientOrgId from the input, will be used for implicit collection, owner, and state-based endorsement policy
|
||||
clientOrgID, err := getClientOrgID(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// In this scenario, client is only authorized to read/write private data from its own peer, therefore verify client org id matches peer org id.
|
||||
err = verifyClientOrgMatchesPeerOrg(clientOrgID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
asset := Asset{
|
||||
ObjectType: "asset",
|
||||
ID: assetID,
|
||||
OwnerOrg: clientOrgID,
|
||||
PublicDescription: publicDescription,
|
||||
}
|
||||
assetBytes, err := json.Marshal(asset)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create asset JSON: %v", err)
|
||||
}
|
||||
|
||||
err = ctx.GetStub().PutState(assetID, assetBytes)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to put asset in public data: %v", err)
|
||||
}
|
||||
|
||||
// Set the endorsement policy such that an owner org peer is required to endorse future updates.
|
||||
// In practice, consider additional endorsers such as a trusted third party to further secure transfers.
|
||||
endorsingOrgs := []string{clientOrgID}
|
||||
err = setAssetStateBasedEndorsement(ctx, asset.ID, endorsingOrgs)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed setting state based endorsement for buyer and seller: %v", err)
|
||||
}
|
||||
|
||||
// Persist private immutable asset properties to owner's private data collection
|
||||
collection := buildCollectionName(clientOrgID)
|
||||
err = ctx.GetStub().PutPrivateData(collection, assetID, immutablePropertiesJSON)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to put Asset private details: %v", err)
|
||||
}
|
||||
|
||||
return assetID, nil
|
||||
}
|
||||
|
||||
// ChangePublicDescription updates the assets public description. Only the current owner can update the public description
|
||||
func (s *SmartContract) ChangePublicDescription(ctx contractapi.TransactionContextInterface, assetID string, newDescription string) error {
|
||||
|
||||
clientOrgID, err := getClientOrgID(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
asset, err := s.ReadAsset(ctx, assetID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get asset: %v", err)
|
||||
}
|
||||
|
||||
// Auth check to ensure that client's org actually owns the asset
|
||||
if clientOrgID != asset.OwnerOrg {
|
||||
return fmt.Errorf("a client from %s cannot update the description of a asset owned by %s", clientOrgID, asset.OwnerOrg)
|
||||
}
|
||||
|
||||
asset.PublicDescription = newDescription
|
||||
updatedAssetJSON, err := json.Marshal(asset)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal asset: %v", err)
|
||||
}
|
||||
|
||||
return ctx.GetStub().PutState(assetID, updatedAssetJSON)
|
||||
}
|
||||
|
||||
// AgreeToSell adds seller's asking price to seller's implicit private data collection.
|
||||
func (s *SmartContract) AgreeToSell(ctx contractapi.TransactionContextInterface, assetID string) error {
|
||||
asset, err := s.ReadAsset(ctx, assetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
clientOrgID, err := getClientOrgID(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify that this client belongs to the peer's org
|
||||
err = verifyClientOrgMatchesPeerOrg(clientOrgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify that this clientOrgId actually owns the asset.
|
||||
if clientOrgID != asset.OwnerOrg {
|
||||
return fmt.Errorf("a client from %s cannot sell an asset owned by %s", clientOrgID, asset.OwnerOrg)
|
||||
}
|
||||
|
||||
return agreeToPrice(ctx, assetID, typeAssetForSale)
|
||||
}
|
||||
|
||||
// AgreeToBuy adds buyer's bid price and asset properties to buyer's implicit private data collection
|
||||
func (s *SmartContract) AgreeToBuy(ctx contractapi.TransactionContextInterface, assetID string) error {
|
||||
transientMap, err := ctx.GetStub().GetTransient()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting transient: %v", err)
|
||||
}
|
||||
|
||||
clientOrgID, err := getClientOrgID(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify that this client belongs to the peer's org
|
||||
err = verifyClientOrgMatchesPeerOrg(clientOrgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Asset properties must be retrieved from the transient field as they are private
|
||||
immutablePropertiesJSON, ok := transientMap["asset_properties"]
|
||||
if !ok {
|
||||
return fmt.Errorf("asset_properties key not found in the transient map")
|
||||
}
|
||||
|
||||
// Persist private immutable asset properties to seller's private data collection
|
||||
collection := buildCollectionName(clientOrgID)
|
||||
err = ctx.GetStub().PutPrivateData(collection, assetID, immutablePropertiesJSON)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to put Asset private details: %v", err)
|
||||
}
|
||||
|
||||
return agreeToPrice(ctx, assetID, typeAssetBid)
|
||||
}
|
||||
|
||||
// agreeToPrice adds a bid or ask price to caller's implicit private data collection
|
||||
func agreeToPrice(ctx contractapi.TransactionContextInterface, assetID string, priceType string) error {
|
||||
// In this scenario, both buyer and seller are authoried to read/write private about transfer after seller agrees to sell.
|
||||
clientOrgID, err := getClientOrgID(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
transMap, err := ctx.GetStub().GetTransient()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting transient: %v", err)
|
||||
}
|
||||
|
||||
// Asset price must be retrieved from the transient field as they are private
|
||||
price, ok := transMap["asset_price"]
|
||||
if !ok {
|
||||
return fmt.Errorf("asset_price key not found in the transient map")
|
||||
}
|
||||
|
||||
collection := buildCollectionName(clientOrgID)
|
||||
|
||||
// Persist the agreed to price in a collection sub-namespace based on priceType key prefix,
|
||||
// to avoid collisions between private asset properties, sell price, and buy price
|
||||
assetPriceKey, err := ctx.GetStub().CreateCompositeKey(priceType, []string{assetID})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create composite key: %v", err)
|
||||
}
|
||||
|
||||
// The Price hash will be verified later, therefore always pass and persist price bytes as is,
|
||||
// so that there is no risk of nondeterministic marshaling.
|
||||
err = ctx.GetStub().PutPrivateData(collection, assetPriceKey, price)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to put asset bid: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyAssetProperties allows a buyer to validate the properties of
|
||||
// an asset they intend to buy against the owner's implicit private data collection
|
||||
// and verifies that the asset properties never changed from the origin of the asset by checking their hash against the assetID
|
||||
func (s *SmartContract) VerifyAssetProperties(ctx contractapi.TransactionContextInterface, assetID string) (bool, error) {
|
||||
transMap, err := ctx.GetStub().GetTransient()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error getting transient: %v", err)
|
||||
}
|
||||
|
||||
// Asset properties must be retrieved from the transient field as they are private
|
||||
immutablePropertiesJSON, ok := transMap["asset_properties"]
|
||||
if !ok {
|
||||
return false, fmt.Errorf("asset_properties key not found in the transient map")
|
||||
}
|
||||
|
||||
asset, err := s.ReadAsset(ctx, assetID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to get asset: %v", err)
|
||||
}
|
||||
|
||||
collectionOwner := buildCollectionName(asset.OwnerOrg)
|
||||
immutablePropertiesOnChainHash, err := ctx.GetStub().GetPrivateDataHash(collectionOwner, assetID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to read asset private properties hash from seller's collection: %v", err)
|
||||
}
|
||||
if immutablePropertiesOnChainHash == nil {
|
||||
return false, fmt.Errorf("asset private properties hash does not exist: %s", assetID)
|
||||
}
|
||||
|
||||
hash := sha256.New()
|
||||
hash.Write(immutablePropertiesJSON)
|
||||
calculatedPropertiesHash := hash.Sum(nil)
|
||||
|
||||
// verify that the hash of the passed immutable properties matches the on-chain hash
|
||||
if !bytes.Equal(immutablePropertiesOnChainHash, calculatedPropertiesHash) {
|
||||
return false, fmt.Errorf("hash %x for passed immutable properties %s does not match on-chain hash %x",
|
||||
calculatedPropertiesHash,
|
||||
immutablePropertiesJSON,
|
||||
immutablePropertiesOnChainHash,
|
||||
)
|
||||
}
|
||||
|
||||
// verify that the hash of the passed immutable properties and on chain hash matches the assetID
|
||||
if !(hex.EncodeToString(immutablePropertiesOnChainHash) == assetID) {
|
||||
return false, fmt.Errorf("hash %x for passed immutable properties %s does match on-chain hash %x but do not match assetID %s: asset was altered from its initial form",
|
||||
calculatedPropertiesHash,
|
||||
immutablePropertiesJSON,
|
||||
immutablePropertiesOnChainHash,
|
||||
assetID)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// TransferAsset checks transfer conditions and then transfers asset state to buyer.
|
||||
// TransferAsset can only be called by current owner
|
||||
func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterface, assetID string, buyerOrgID string) error {
|
||||
clientOrgID, err := getClientOrgID(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
transMap, err := ctx.GetStub().GetTransient()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting transient data: %v", err)
|
||||
}
|
||||
|
||||
priceJSON, ok := transMap["asset_price"]
|
||||
if !ok {
|
||||
return fmt.Errorf("asset_price key not found in the transient map")
|
||||
}
|
||||
|
||||
var agreement Agreement
|
||||
err = json.Unmarshal(priceJSON, &agreement)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal price JSON: %v", err)
|
||||
}
|
||||
|
||||
asset, err := s.ReadAsset(ctx, assetID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get asset: %v", err)
|
||||
}
|
||||
|
||||
err = verifyTransferConditions(ctx, asset, clientOrgID, buyerOrgID, priceJSON)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed transfer verification: %v", err)
|
||||
}
|
||||
|
||||
err = transferAssetState(ctx, asset, clientOrgID, buyerOrgID, agreement.Price)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed asset transfer: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// verifyTransferConditions checks that client org currently owns asset and that both parties have agreed on price
|
||||
func verifyTransferConditions(ctx contractapi.TransactionContextInterface,
|
||||
asset *Asset,
|
||||
clientOrgID string,
|
||||
buyerOrgID string,
|
||||
priceJSON []byte) error {
|
||||
|
||||
// CHECK1: Auth check to ensure that client's org actually owns the asset
|
||||
|
||||
if clientOrgID != asset.OwnerOrg {
|
||||
return fmt.Errorf("a client from %s cannot transfer a asset owned by %s", clientOrgID, asset.OwnerOrg)
|
||||
}
|
||||
|
||||
// CHECK2: Verify that buyer and seller on-chain asset defintion hash matches
|
||||
|
||||
collectionSeller := buildCollectionName(clientOrgID)
|
||||
collectionBuyer := buildCollectionName(buyerOrgID)
|
||||
sellerPropertiesOnChainHash, err := ctx.GetStub().GetPrivateDataHash(collectionSeller, asset.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read asset private properties hash from seller's collection: %v", err)
|
||||
}
|
||||
if sellerPropertiesOnChainHash == nil {
|
||||
return fmt.Errorf("asset private properties hash does not exist: %s", asset.ID)
|
||||
}
|
||||
buyerPropertiesOnChainHash, err := ctx.GetStub().GetPrivateDataHash(collectionBuyer, asset.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read asset private properties hash from seller's collection: %v", err)
|
||||
}
|
||||
if buyerPropertiesOnChainHash == nil {
|
||||
return fmt.Errorf("asset private properties hash does not exist: %s", asset.ID)
|
||||
}
|
||||
|
||||
// verify that buyer and seller on-chain asset defintion hash matches
|
||||
if !bytes.Equal(sellerPropertiesOnChainHash, buyerPropertiesOnChainHash) {
|
||||
return fmt.Errorf("on chain hash of seller %x does not match on-chain hash of buyer %x",
|
||||
sellerPropertiesOnChainHash,
|
||||
buyerPropertiesOnChainHash,
|
||||
)
|
||||
}
|
||||
|
||||
// CHECK3: Verify that seller and buyer agreed on the same price
|
||||
|
||||
// Get sellers asking price
|
||||
assetForSaleKey, err := ctx.GetStub().CreateCompositeKey(typeAssetForSale, []string{asset.ID})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create composite key: %v", err)
|
||||
}
|
||||
sellerPriceHash, err := ctx.GetStub().GetPrivateDataHash(collectionSeller, assetForSaleKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get seller price hash: %v", err)
|
||||
}
|
||||
if sellerPriceHash == nil {
|
||||
return fmt.Errorf("seller price for %s does not exist", asset.ID)
|
||||
}
|
||||
|
||||
// Get buyers bid price
|
||||
assetBidKey, err := ctx.GetStub().CreateCompositeKey(typeAssetBid, []string{asset.ID})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create composite key: %v", err)
|
||||
}
|
||||
buyerPriceHash, err := ctx.GetStub().GetPrivateDataHash(collectionBuyer, assetBidKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get buyer price hash: %v", err)
|
||||
}
|
||||
if buyerPriceHash == nil {
|
||||
return fmt.Errorf("buyer price for %s does not exist", asset.ID)
|
||||
}
|
||||
|
||||
hash := sha256.New()
|
||||
hash.Write(priceJSON)
|
||||
calculatedPriceHash := hash.Sum(nil)
|
||||
|
||||
// Verify that the hash of the passed price matches the on-chain sellers price hash
|
||||
if !bytes.Equal(calculatedPriceHash, sellerPriceHash) {
|
||||
return fmt.Errorf("hash %x for passed price JSON %s does not match on-chain hash %x, seller hasn't agreed to the passed trade id and price",
|
||||
calculatedPriceHash,
|
||||
priceJSON,
|
||||
sellerPriceHash,
|
||||
)
|
||||
}
|
||||
|
||||
// Verify that the hash of the passed price matches the on-chain buyer price hash
|
||||
if !bytes.Equal(calculatedPriceHash, buyerPriceHash) {
|
||||
return fmt.Errorf("hash %x for passed price JSON %s does not match on-chain hash %x, buyer hasn't agreed to the passed trade id and price",
|
||||
calculatedPriceHash,
|
||||
priceJSON,
|
||||
buyerPriceHash,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// transferAssetState performs the public and private state updates for the transferred asset
|
||||
// changes the endorsement for the transferred asset sbe to the new owner org
|
||||
func transferAssetState(ctx contractapi.TransactionContextInterface, asset *Asset, clientOrgID string, buyerOrgID string, price int) error {
|
||||
|
||||
// Update ownership in public state
|
||||
asset.OwnerOrg = buyerOrgID
|
||||
updatedAsset, err := json.Marshal(asset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ctx.GetStub().PutState(asset.ID, updatedAsset)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write asset for buyer: %v", err)
|
||||
}
|
||||
|
||||
// Changes the endorsement policy to the new owner org
|
||||
endorsingOrgs := []string{buyerOrgID}
|
||||
err = setAssetStateBasedEndorsement(ctx, asset.ID, endorsingOrgs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed setting state based endorsement for new owner: %v", err)
|
||||
}
|
||||
|
||||
// Delete asset description from seller collection
|
||||
collectionSeller := buildCollectionName(clientOrgID)
|
||||
err = ctx.GetStub().DelPrivateData(collectionSeller, asset.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete Asset private details from seller: %v", err)
|
||||
}
|
||||
|
||||
// Delete the price records for seller
|
||||
assetPriceKey, err := ctx.GetStub().CreateCompositeKey(typeAssetForSale, []string{asset.ID})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create composite key for seller: %v", err)
|
||||
}
|
||||
err = ctx.GetStub().DelPrivateData(collectionSeller, assetPriceKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete asset price from implicit private data collection for seller: %v", err)
|
||||
}
|
||||
|
||||
// Delete the price records for buyer
|
||||
collectionBuyer := buildCollectionName(buyerOrgID)
|
||||
assetPriceKey, err = ctx.GetStub().CreateCompositeKey(typeAssetBid, []string{asset.ID})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create composite key for buyer: %v", err)
|
||||
}
|
||||
err = ctx.GetStub().DelPrivateData(collectionBuyer, assetPriceKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete asset price from implicit private data collection for buyer: %v", err)
|
||||
}
|
||||
|
||||
// Keep record for a 'receipt' in both buyers and sellers private data collection to record the sale price and date.
|
||||
// Persist the agreed to price in a collection sub-namespace based on receipt key prefix.
|
||||
receiptBuyKey, err := ctx.GetStub().CreateCompositeKey(typeAssetBuyReceipt, []string{asset.ID, ctx.GetStub().GetTxID()})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create composite key for receipt: %v", err)
|
||||
}
|
||||
|
||||
txTimestamp, err := ctx.GetStub().GetTxTimestamp()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create timestamp for receipt: %v", err)
|
||||
}
|
||||
|
||||
assetReceipt := receipt{
|
||||
price: price,
|
||||
timestamp: txTimestamp.AsTime(),
|
||||
}
|
||||
receipt, err := json.Marshal(assetReceipt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal receipt: %v", err)
|
||||
}
|
||||
|
||||
err = ctx.GetStub().PutPrivateData(collectionBuyer, receiptBuyKey, receipt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to put private asset receipt for buyer: %v", err)
|
||||
}
|
||||
|
||||
receiptSaleKey, err := ctx.GetStub().CreateCompositeKey(typeAssetSaleReceipt, []string{ctx.GetStub().GetTxID(), asset.ID})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create composite key for receipt: %v", err)
|
||||
}
|
||||
|
||||
err = ctx.GetStub().PutPrivateData(collectionSeller, receiptSaleKey, receipt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to put private asset receipt for seller: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getClientOrgID gets the client org ID.
|
||||
func getClientOrgID(ctx contractapi.TransactionContextInterface) (string, error) {
|
||||
clientOrgID, err := ctx.GetClientIdentity().GetMSPID()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed getting client's orgID: %v", err)
|
||||
}
|
||||
|
||||
return clientOrgID, nil
|
||||
}
|
||||
|
||||
// getClientImplicitCollectionNameAndVerifyClientOrg gets the implicit collection for the client and checks that the client is from the same org as the peer
|
||||
func getClientImplicitCollectionNameAndVerifyClientOrg(ctx contractapi.TransactionContextInterface) (string, error) {
|
||||
clientOrgID, err := getClientOrgID(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = verifyClientOrgMatchesPeerOrg(clientOrgID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return buildCollectionName(clientOrgID), nil
|
||||
}
|
||||
|
||||
// verifyClientOrgMatchesPeerOrg checks that the client is from the same org as the peer
|
||||
func verifyClientOrgMatchesPeerOrg(clientOrgID string) error {
|
||||
peerOrgID, err := shim.GetMSPID()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed getting peer's orgID: %v", err)
|
||||
}
|
||||
|
||||
if clientOrgID != peerOrgID {
|
||||
return fmt.Errorf("client from org %s is not authorized to read or write private data from an org %s peer",
|
||||
clientOrgID,
|
||||
peerOrgID,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildCollectionName returns the implicit collection name for an org
|
||||
func buildCollectionName(clientOrgID string) string {
|
||||
return fmt.Sprintf("_implicit_org_%s", clientOrgID)
|
||||
}
|
||||
|
||||
// setAssetStateBasedEndorsement adds an endorsement policy to an asset so that the passed orgs need to agree upon transfer
|
||||
func setAssetStateBasedEndorsement(ctx contractapi.TransactionContextInterface, assetID string, orgsToEndorse []string) error {
|
||||
endorsementPolicy, err := statebased.NewStateEP(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = endorsementPolicy.AddOrgs(statebased.RoleTypePeer, orgsToEndorse...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add org to endorsement policy: %v", err)
|
||||
}
|
||||
policy, err := endorsementPolicy.Policy()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create endorsement policy bytes from org: %v", err)
|
||||
}
|
||||
err = ctx.GetStub().SetStateValidationParameter(assetID, policy)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set validation parameter on asset: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAssetHashId allows a potential buyer to validate the properties of an asset against the asset Id hash on chain and returns the hash
|
||||
func (s *SmartContract) GetAssetHashId(ctx contractapi.TransactionContextInterface) (string, error) {
|
||||
transientMap, err := ctx.GetStub().GetTransient()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error getting transient: %v", err)
|
||||
}
|
||||
|
||||
// Asset properties must be retrieved from the transient field as they are private
|
||||
propertiesJSON, ok := transientMap["asset_properties"]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("asset_properties key not found in the transient map")
|
||||
}
|
||||
|
||||
hash := sha256.New()
|
||||
hash.Write(propertiesJSON)
|
||||
assetID := hex.EncodeToString(hash.Sum(nil))
|
||||
|
||||
asset, err := s.ReadAsset(ctx, assetID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get asset: %v, asset properies provided do not represent any on chain asset", err)
|
||||
}
|
||||
if asset.ID != assetID {
|
||||
return "", fmt.Errorf("Asset properies provided do not correpond to any on chain asset")
|
||||
}
|
||||
return asset.ID, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
chaincode, err := contractapi.NewChaincode(new(SmartContract))
|
||||
if err != nil {
|
||||
log.Panicf("Error create transfer asset chaincode: %v", err)
|
||||
}
|
||||
|
||||
if err := chaincode.Start(); err != nil {
|
||||
log.Panicf("Error starting asset chaincode: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,172 +0,0 @@
|
|||
/*
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/hyperledger/fabric-contract-api-go/v2/contractapi"
|
||||
)
|
||||
|
||||
// QueryResult structure used for handling result of query
|
||||
type QueryResult struct {
|
||||
Record *Asset
|
||||
TxId string `json:"txId"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
|
||||
type Agreement struct {
|
||||
ID string `json:"asset_id"`
|
||||
Price int `json:"price"`
|
||||
TradeID string `json:"trade_id"`
|
||||
}
|
||||
|
||||
// ReadAsset returns the public asset data
|
||||
func (s *SmartContract) ReadAsset(ctx contractapi.TransactionContextInterface, assetID string) (*Asset, error) {
|
||||
// Since only public data is accessed in this function, no access control is required
|
||||
assetJSON, err := ctx.GetStub().GetState(assetID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read from world state: %v", err)
|
||||
}
|
||||
if assetJSON == nil {
|
||||
return nil, fmt.Errorf("%s does not exist", assetID)
|
||||
}
|
||||
|
||||
var asset *Asset
|
||||
err = json.Unmarshal(assetJSON, &asset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return asset, nil
|
||||
}
|
||||
|
||||
// GetAssetPrivateProperties returns the immutable asset properties from owner's private data collection
|
||||
func (s *SmartContract) GetAssetPrivateProperties(ctx contractapi.TransactionContextInterface, assetID string) (string, error) {
|
||||
|
||||
collection, err := getClientImplicitCollectionNameAndVerifyClientOrg(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
immutableProperties, err := ctx.GetStub().GetPrivateData(collection, assetID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read asset private properties from client org's collection: %v", err)
|
||||
}
|
||||
if immutableProperties == nil {
|
||||
return "", fmt.Errorf("asset private details does not exist in client org's collection: %s", assetID)
|
||||
}
|
||||
|
||||
return string(immutableProperties), nil
|
||||
}
|
||||
|
||||
// GetAssetSalesPrice returns the sales price
|
||||
func (s *SmartContract) GetAssetSalesPrice(ctx contractapi.TransactionContextInterface, assetID string) (string, error) {
|
||||
return getAssetPrice(ctx, assetID, typeAssetForSale)
|
||||
}
|
||||
|
||||
// GetAssetBidPrice returns the bid price
|
||||
func (s *SmartContract) GetAssetBidPrice(ctx contractapi.TransactionContextInterface, assetID string) (string, error) {
|
||||
return getAssetPrice(ctx, assetID, typeAssetBid)
|
||||
}
|
||||
|
||||
// getAssetPrice gets the bid or ask price from caller's implicit private data collection
|
||||
func getAssetPrice(ctx contractapi.TransactionContextInterface, assetID string, priceType string) (string, error) {
|
||||
|
||||
collection, err := getClientImplicitCollectionNameAndVerifyClientOrg(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
assetPriceKey, err := ctx.GetStub().CreateCompositeKey(priceType, []string{assetID})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create composite key: %v", err)
|
||||
}
|
||||
|
||||
price, err := ctx.GetStub().GetPrivateData(collection, assetPriceKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read asset price from implicit private data collection: %v", err)
|
||||
}
|
||||
if price == nil {
|
||||
return "", fmt.Errorf("asset price does not exist: %s", assetID)
|
||||
}
|
||||
|
||||
return string(price), nil
|
||||
}
|
||||
|
||||
// QueryAssetSaleAgreements returns all of an organization's proposed sales
|
||||
func (s *SmartContract) QueryAssetSaleAgreements(ctx contractapi.TransactionContextInterface) ([]Agreement, error) {
|
||||
return queryAgreementsByType(ctx, typeAssetForSale)
|
||||
}
|
||||
|
||||
// QueryAssetBuyAgreements returns all of an organization's proposed bids
|
||||
func (s *SmartContract) QueryAssetBuyAgreements(ctx contractapi.TransactionContextInterface) ([]Agreement, error) {
|
||||
return queryAgreementsByType(ctx, typeAssetBid)
|
||||
}
|
||||
|
||||
func queryAgreementsByType(ctx contractapi.TransactionContextInterface, agreeType string) ([]Agreement, error) {
|
||||
collection, err := getClientImplicitCollectionNameAndVerifyClientOrg(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Query for any object type starting with `agreeType`
|
||||
agreementsIterator, err := ctx.GetStub().GetPrivateDataByPartialCompositeKey(collection, agreeType, []string{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read from private data collection: %v", err)
|
||||
}
|
||||
defer agreementsIterator.Close()
|
||||
|
||||
var agreements []Agreement
|
||||
for agreementsIterator.HasNext() {
|
||||
resp, err := agreementsIterator.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var agreement Agreement
|
||||
err = json.Unmarshal(resp.Value, &agreement)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
agreements = append(agreements, agreement)
|
||||
}
|
||||
|
||||
return agreements, nil
|
||||
}
|
||||
|
||||
// QueryAssetHistory returns the chain of custody for a asset since issuance
|
||||
func (s *SmartContract) QueryAssetHistory(ctx contractapi.TransactionContextInterface, assetID string) ([]QueryResult, error) {
|
||||
resultsIterator, err := ctx.GetStub().GetHistoryForKey(assetID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resultsIterator.Close()
|
||||
|
||||
var results []QueryResult
|
||||
for resultsIterator.HasNext() {
|
||||
response, err := resultsIterator.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var asset *Asset
|
||||
err = json.Unmarshal(response.Value, &asset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
record := QueryResult{
|
||||
TxId: response.TxId,
|
||||
Timestamp: response.Timestamp.AsTime(),
|
||||
Record: asset,
|
||||
}
|
||||
results = append(results, record)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
module github.com/hyperledger/fabric-samples/chaincode/tradingMarbles
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/hyperledger/fabric-chaincode-go/v2 v2.0.0-20240618210511-f7903324a8af
|
||||
github.com/hyperledger/fabric-contract-api-go/v2 v2.0.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/spec v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/gobuffalo/envy v1.10.2 // indirect
|
||||
github.com/gobuffalo/packd v1.0.2 // indirect
|
||||
github.com/gobuffalo/packr v1.30.1 // indirect
|
||||
github.com/hyperledger/fabric-protos-go-apiv2 v0.3.3 // indirect
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/net v0.24.0 // indirect
|
||||
golang.org/x/sys v0.19.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
|
||||
google.golang.org/grpc v1.64.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
||||
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
||||
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
|
||||
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
|
||||
github.com/gobuffalo/envy v1.10.2 h1:EIi03p9c3yeuRCFPOKcSfajzkLb3hrRjEpHGI8I2Wo4=
|
||||
github.com/gobuffalo/envy v1.10.2/go.mod h1:qGAGwdvDsaEtPhfBzb3o0SfDea8ByGn9j8bKmVft9z8=
|
||||
github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs=
|
||||
github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q=
|
||||
github.com/gobuffalo/packd v1.0.2 h1:Yg523YqnOxGIWCp69W12yYBKsoChwI7mtu6ceM9Bwfw=
|
||||
github.com/gobuffalo/packd v1.0.2/go.mod h1:sUc61tDqGMXON80zpKGp92lDb86Km28jfvX7IAyxFT8=
|
||||
github.com/gobuffalo/packr v1.30.1 h1:hu1fuVR3fXEZR7rXNW3h8rqSML8EVAf6KNm0NKO/wKg=
|
||||
github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIaviy289eRuk=
|
||||
github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hyperledger/fabric-chaincode-go/v2 v2.0.0-20240618210511-f7903324a8af h1:WT4NjX7Uk03GSeH++jF3a0wp4FhybTM86zDPCETvmSk=
|
||||
github.com/hyperledger/fabric-chaincode-go/v2 v2.0.0-20240618210511-f7903324a8af/go.mod h1:f/ER25FaBepxJugwpLhbD2hLAoZaZEVqkBjOcHjw72Y=
|
||||
github.com/hyperledger/fabric-contract-api-go/v2 v2.0.0 h1:IDiCGVOBlRd6zpL0Y+f6V7IpBqa4/Z5JAK9SF7a5ea8=
|
||||
github.com/hyperledger/fabric-contract-api-go/v2 v2.0.0/go.mod h1:pdqhe7ALf4lmXgQdprCyNWYdnCPxgj02Vhf8JF5w8po=
|
||||
github.com/hyperledger/fabric-protos-go-apiv2 v0.3.3 h1:Xpd6fzG/KjAOHJsq7EQXY2l+qi/y8muxBaY7R6QWABk=
|
||||
github.com/hyperledger/fabric-protos-go-apiv2 v0.3.3/go.mod h1:2pq0ui6ZWA0cC8J+eCErgnMDCS1kPOEYVY+06ZAK0qE=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w=
|
||||
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
|
||||
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
|
||||
google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
@ -1 +0,0 @@
|
|||
oapi-server.yaml
|
||||
8
token-sdk/.gitignore
vendored
8
token-sdk/.gitignore
vendored
|
|
@ -1,8 +0,0 @@
|
|||
bin/
|
||||
keys/
|
||||
data/
|
||||
tokenchaincode/zkatdlog_pp.json
|
||||
|
||||
auditor/auditor
|
||||
issuer/issuer
|
||||
owner/owner
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
#build stage
|
||||
FROM golang:1.20.7-bookworm AS builder
|
||||
WORKDIR /go/src/app
|
||||
COPY go.mod .
|
||||
COPY go.sum .
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN go build -o /go/bin/app
|
||||
|
||||
#final stage
|
||||
FROM golang:1.20.7-bookworm
|
||||
COPY --from=builder /go/bin/app /app
|
||||
ENTRYPOINT /app
|
||||
LABEL Name=tokens Version=0.1.0
|
||||
|
||||
ENV PORT=9000
|
||||
ENV CONF_DIR=/conf
|
||||
EXPOSE 9000
|
||||
EXPOSE 9001
|
||||
|
|
@ -1,413 +0,0 @@
|
|||
# Token SDK Sample API
|
||||
|
||||
This is a service with a REST API that wraps the [Token SDK](https://github.com/hyperledger-labs/fabric-token-sdk) to issue, transfer and redeem tokens backed by a Hyperledger Fabric network for validation and settlement.
|
||||
|
||||
Several instances of this service form a Layer 2 network that can transact amongst each other, with an (optional but currently configured to be required) auditor role who has to approve every transaction. The ledger data does not reveal balances, transaction amounts and identities of transaction parties. UTXO Tokens are owned by pseudonymous keys and other details are obscured with Zero Knowledge Proofs.
|
||||
|
||||
This sample is intended to get familiar with the features of the Token SDK and as a starting point for a proof of concept. The sample contains a basic development setup with:
|
||||
|
||||
- An issuer service
|
||||
- An auditor service
|
||||
- Two owner services, with wallets for Alice and Bob (on Owner 1), and Carlos and Dan (on Owner 2)
|
||||
- A Certificate Authority
|
||||
- Configuration to use a Fabric test network.
|
||||
|
||||
From now on we'll call the services for the issuer, auditor and owners 'nodes' (not to be confused with Hyperledger Fabric peer nodes). Each of them runs as a separate application containing a REST API, the Fabric Smart Client and the Token SDK. The nodes talk to each other via a protocol called libp2p to create token transactions, and each of them also has a Hyperledger Fabric user to be able to submit the transaction to the settlement layer. The settlement layer is just any Fabric network that runs the Token Chaincode, which is configured with the identities of the issuer, auditor and CA to be able to validate transactions.
|
||||
|
||||

|
||||
|
||||
[plantuml source](https://www.plantuml.com/plantuml/uml/ZPB1IiD048RlUOgXteHM4nIXbD0O4RnO3mKllKmtssR9PYRiRYWYlhlPNPMsIkrjcFs_d-LZvjQXSNsh4ziewj1W2wsYKgErhwfoDQGtrqdIeMXmAs6qv4OIF4ktOzEC02quRk0z0H3STaoI78nMT123iWX9WJ2RmGRNHecnm6awkPtSGPuVmqLVASScCDXN7ffSOLmESRWekauhWKun7RDFrlOoeihQY2g_-vTSx6W8fIkwX6B8I3_SypfKSHgRU4Vd5cMUBz5ejdvwG8fDsQccZptJZy4JBALr1xvhlVdj-qNwluVtRXXJs3Fj5rEDpXVb-PzazaDcPuCBKqdpfPhZlCV6pGayNaXPeoB1bOod94Iqu_oZ-7uBcfPIrCIQjs_UaZ-wyJZtCfAvfAflzIS0)
|
||||
|
||||
# Table of Contents
|
||||
|
||||
- [Token SDK Sample API](#token-sdk-sample-api)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Features](#features)
|
||||
- [Getting started](#getting-started)
|
||||
- [Install dependencies](#install-dependencies)
|
||||
- [Quick start](#quick-start)
|
||||
- [Using the application](#using-the-application)
|
||||
- [Deep dive: what happens when doing a transfer?](#deep-dive-what-happens-when-doing-a-transfer)
|
||||
- [Alternative: manual start](#alternative-manual-start)
|
||||
- [Generate crypto material](#generate-crypto-material)
|
||||
- [Start Fabric and install the chaincode](#start-fabric-and-install-the-chaincode)
|
||||
- [Start the Token network](#start-the-token-network)
|
||||
- [View the blockchain explorer](#view-the-blockchain-explorer)
|
||||
- [Development](#development)
|
||||
- [End to end tests](#end-to-end-tests)
|
||||
- [Code structure](#code-structure)
|
||||
- [Add or change a REST API endpoint](#add-or-change-a-rest-api-endpoint)
|
||||
- [Upgrade the Token SDK and Fabric Smart Client versions](#upgrade-the-token-sdk-and-fabric-smart-client-versions)
|
||||
- [Use another Fabric network](#use-another-fabric-network)
|
||||
- [Add a user / account](#add-a-user--account)
|
||||
- [Run the service directly (instead of with docker-compose)](#run-the-service-directly-instead-of-with-docker-compose)
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
Main flows:
|
||||
|
||||
- [X] issue token
|
||||
- [X] transfer
|
||||
- [X] redeem / burn
|
||||
- [X] owner get balances
|
||||
- [X] owner transaction history
|
||||
- [ ] auditor get balances
|
||||
- [X] auditor transaction history
|
||||
- [ ] issuer transaction history
|
||||
- [ ] swap
|
||||
|
||||
Additional features:
|
||||
|
||||
- [X] Documented REST API
|
||||
- [X] Basic end to end tests
|
||||
- [X] Support for multiple token types
|
||||
- [X] Multiple accounts per node
|
||||
- [X] Use Idemix (privacy preserving) accounts created by a Fabric CA
|
||||
- [X] Pre-configured and easy to start for development
|
||||
|
||||
Out of scope for now:
|
||||
|
||||
- HTLC locks (hashed timelock contracts)
|
||||
- Register/enroll new token accounts on a running network
|
||||
- Business flows for redemption or issuance
|
||||
- Advanced transaction history (queries, rolling balance, pagination, etc)
|
||||
- Denylist / revocation and other business logic for auditor
|
||||
- Idemix users to submit the transactions to Fabric anonymously
|
||||
- Production configuration (e.g. deployment, networking, security, resilience, key management)
|
||||
|
||||
## Getting started
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- bash
|
||||
- golang 1.20+
|
||||
- git
|
||||
- docker
|
||||
- docker-compose
|
||||
|
||||
### Install dependencies
|
||||
|
||||
Download the Fabric docker images and binaries. The code only works with Fabric CA 1.5.7+, so even if you cloned the fabric-samples repo before, you may have to re-run it to get the latest versions.
|
||||
|
||||
From the fabric-samples directory:
|
||||
|
||||
```bash
|
||||
curl -sSLO https://raw.githubusercontent.com/hyperledger/fabric/main/scripts/install-fabric.sh && chmod +x install-fabric.sh
|
||||
./install-fabric.sh docker binary
|
||||
```
|
||||
|
||||
Make sure that the new binaries are in your path. Change the following line (replace `<your/path/to/>` with the actual path) and add it to your `~/.bashrc` or `~/.zshrc` file. Restart your terminal or `source` the edited file.
|
||||
|
||||
```bash
|
||||
export PATH=</your/path/to/>fabric-samples/bin:$PATH
|
||||
```
|
||||
|
||||
Validate that the CA is at 1.5.7 by executing `fabric-ca-client version`.
|
||||
|
||||
> Note: you can run this code from anywhere. If you are *not* running it from the fabric-samples/token-sdk folder, also set the following environment variable:
|
||||
> ```bash
|
||||
> export TEST_NETWORK_HOME=</your/path/to>/fabric-samples/test-network
|
||||
> ```
|
||||
>
|
||||
> See the bottom of this readme for instructions to use another Fabric network than the test network.
|
||||
|
||||
Install tokengen. Tokengen is a tool to create the configuration file for the token chaincode (once, when deploying the chaincode). It generates the public parameters that the network participants will use to generate their proofs, and it specifies the public identities of the issuer, auditor and CA for signature validation.
|
||||
|
||||
```bash
|
||||
go install github.com/hyperledger-labs/fabric-token-sdk/cmd/tokengen@v0.3.0
|
||||
```
|
||||
|
||||
### Quick start
|
||||
|
||||
The quickest way to get going is to run:
|
||||
|
||||
```bash
|
||||
./scripts/up.sh
|
||||
```
|
||||
|
||||
This generates the crypto material, starts Fabric, deploys the chaincode, and starts the token nodes.
|
||||
|
||||
When you're done and want to delete everything:
|
||||
|
||||
```bash
|
||||
./scripts/down.sh
|
||||
```
|
||||
|
||||
#### Using the application
|
||||
|
||||
The services are accessible on the following ports:
|
||||
|
||||
| port | service |
|
||||
|------|--------------------------|
|
||||
| 8080 | API documentation (web) |
|
||||
| 9000 | auditor |
|
||||
| 9100 | issuer |
|
||||
| 9200 | owner 1 (alice and bob) |
|
||||
| 9300 | owner 2 (carlos and dan) |
|
||||
|
||||
Besides that, the nodes communicate with each other via 9001, 9101, 9201 and 9301 respectively.
|
||||
|
||||
Now let's issue and transfer some tokens! View the API documentation and try some actions at [http://localhost:8080](http://localhost:8080). Or, directly from the commandline:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:9100/api/v1/issuer/issue -H 'Content-Type: application/json' -d '{
|
||||
"amount": {"code": "TOK","value": 1000},
|
||||
"counterparty": {"node": "owner1","account": "alice"},
|
||||
"message": "hello world!"
|
||||
}'
|
||||
|
||||
curl -X GET http://localhost:9200/api/v1/owner/accounts
|
||||
curl -X GET http://localhost:9300/api/v1/owner/accounts
|
||||
|
||||
curl -X POST http://localhost:9200/api/v1/owner/accounts/alice/transfer -H 'Content-Type: application/json' -d '{
|
||||
"amount": {"code": "TOK","value": 100},
|
||||
"counterparty": {"node": "owner2","account": "dan"},
|
||||
"message": "hello dan!"
|
||||
}'
|
||||
|
||||
curl -X GET http://localhost:9300/api/v1/owner/accounts/dan/transactions
|
||||
curl -X GET http://localhost:9200/api/v1/owner/accounts/alice/transactions
|
||||
```
|
||||
|
||||
Notice that the transaction overview uses the UTXO model (like bitcoin). The issuer created a new TOK token of 1000 and assigned its ownership to alice. When alice transfered 100 TOK to dan, she used the token of 1000 as **input** for her transaction. As **output**, she creates two new tokens:
|
||||
|
||||
1. one for 100 TOK with dan as the owner
|
||||
2. one with _herself_ as the owner for the remaining 900 TOK.
|
||||
|
||||
This way, each transaction can have multiple inputs and multiple outputs. Their sum should always be the same, and every new transfer must be based on previously created outputs.
|
||||
|
||||
#### Deep dive: what happens when doing a transfer?
|
||||
|
||||
It may look simple from the outside, but there's a lot going on to securely and privately transfer tokens. Let's take the example of alice (on the Owner 1 node) transfering 100 TOK to dan (on the Owner 2 node).
|
||||
|
||||
1. **Create Transaction**: Alice requests an anonymous key from dan that will own the tokens. She then creates the transaction, with commitments that can be verified by anyone, but _only_ be opened (read) by dan and the auditor. The commitments contain the value, sender and recipient of each of the in- and output tokens.
|
||||
2. **Get Endorsements**: Alice (or more precisely the TransferView in the Owner 1 node) now submits the transaction to the auditor, who validates and stores it. The auditor _may_ enforce any specific business logic that is needed for this token in this ecosystem (for instance a transaction or holding limit).
|
||||
|
||||
Alice then submits the transaction (which is now also signed by the auditor) to the Token Chaincode which is running on the Fabric peers. The chaincode verifies that all the proofs are valid and all the necessary signatures are there. Note that the peer and token chaincode cannot see what is transferred between who thanks to the zero knowledge proofs.
|
||||
3. **Commit Transaction**: Alice submits the endorsed Fabric transaction to the ordering service. Alice (Owner 1), dan (Owner 2) and the Auditor nodes have been listening for Fabric events involving this transaction. When receiving the 'commit' event, they change the status of the stored transaction to 'Confirmed'. The transaction is now final; dan owns the 100 TOK.
|
||||
|
||||
The names of the Views below correspond to the code in `owner/service/transfer.go`, `owner/service/accept.go` and `auditor/service/audit.go`.
|
||||
|
||||

|
||||
|
||||
[plantuml source](http://www.plantuml.com/plantuml/uml/TLD1JoCz3BtdLrZb0X98yEaxhTGLRA5xu50EQ0-hNZA92r6dpcpY3Eg_tsHcYDmoUvb9hFUUxHVxFh8Ed0wjOiSjmclG57SOWCj16tQUbCf_7s3nq3g32z0HjEeopHdNQM9OR3ueK-wsjALFWLyEFmOezt2n-l6qNZ_ESVuhd0TZiEELZk-LrRXvraEoZdqOMELO2JhPob18xFW8YxLkWZFmWXZYWEhA2IuU_ry_hfxEOPjWCNmYVRb8i5ekOHLGCqflOBbK6cw-bpQ_0N-wTtTx2w-Rvosn1wi9Cd3gLnLUhnc5CJKcs-Q-o3OkomRyap1o_cSR71A3isFjIiefgQCQL_bcB5kJf-F1fxYbIqzum-w0DodY5UpnAF5lI1WA8sAcCkoAuR-VNy3umy7n0OcZ2iWf47IfQPqf2jSJN5ayAOhxwa_45Ws3eouniDyZHTb0XTQQHv0qFDS-qA_19nx-NV1-5tDszqQQKy0hwLryrm6bmBzCyXsIRF1wIpq6jpkEoldBFk2MNf2iepSfENanuD12mDXvYWZdJkG9-eaCMS27Y4EMF3zJjJfPyTJvvbXwKyydarxEbTlhrjcC6AFLyzEYncpZ8jHyiYQHzNHRnflWEkhzVdeYywuT6M-nZ7ojPCwaAPE5ox6oAnZNJs9dZ5iDBoD1rRgwgwNRr6JOdEISbr-tl0PEPST9a7fvFAOHRLflze8e76g2rzReo43uCG4jVicUhPsOvqimD3r4SaZdoERvr9kpcr2IqrsL1Bfn4gsJbSCqHoWGTOzaqw7z2m00)
|
||||
|
||||
### Alternative: manual start
|
||||
|
||||
To get a better view or have more control on the different layers of the network, you can also start the services manually. If you want to do that, first bring down everything with `./scripts/down.sh`.
|
||||
|
||||
#### Generate crypto material
|
||||
|
||||
In this step, we create all the identities which are used by the Token network. We use a normal Fabric CA for this. Technically, only the Owner identities (the wallets that will hold the tokens) need some form of hierarchy; they use Idemix credentials which must be issued by a single, known issuer (see [Fabric documentation](https://hyperledger-fabric.readthedocs.io/en/latest/idemix.html) for more info about idemix). To keep things simple, we use the same CA for the other identities too. The Token SDK expects the folders for the identities to be in Fabric's 'msp' structure.
|
||||
|
||||
The following crypto will be generated:
|
||||
|
||||
- Fabric Smart Client node identities, used by the nodes to authenticate each other
|
||||
- Token Issuer identity (x509 certificate and private key)
|
||||
- Token Auditor identity (x509 certificate and private key)
|
||||
- Owner identities (idemix credentials)
|
||||
|
||||
```bash
|
||||
mkdir -p keys/ca
|
||||
docker-compose -f compose-ca.yaml up -d
|
||||
./scripts/enroll-users.sh
|
||||
```
|
||||
|
||||
> If you want, you can stop the CA now. You don't need it unless you want to register more users.
|
||||
>
|
||||
> ```bash
|
||||
> docker-compose -f compose-ca.yaml down
|
||||
> ```
|
||||
|
||||
The Issuer and Auditor identities are used by the Token Chaincode to validate token transactions. It also needs the identity of the CA that issues the Idemix credentials to the Owner wallets. The tokengen command generates the configuration that contains these identities and the cryptographic parameters for the proofs. We store it in the `tokenchaincode` folder, so that it will be baked into the chaincode docker image later.
|
||||
|
||||
```bash
|
||||
tokengen gen dlog --base 300 --exponent 5 --issuers keys/issuer/iss/msp --idemix keys/owner1/wallet/alice --auditors keys/auditor/aud/msp --output tokenchaincode
|
||||
```
|
||||
|
||||
> You only have to do this once. But if for any reason you want to re-generate the material: `rm -rf keys; rm tokenchaincode/zkatdlog_pp.json` and execute the steps above again. If any owner has existing tokens, they will now be invalid because the old proofs can not be verified with the new parameters.
|
||||
|
||||
#### Start Fabric and install the chaincode
|
||||
|
||||
For simplicity, in this sample all nodes use the credentials of User1 from Org1MSP and have Peer1 as a trusted peer. In a more serious setup, each instance would have its own (idemix) Fabric user and _may_ have it's own MSP and peers, depending on the network topology and trust relationships.
|
||||
|
||||
Start a Fabric sample network and deploy the Token Chaincode as a service:
|
||||
|
||||
```bash
|
||||
../test-network/network.sh up createChannel
|
||||
INIT_REQUIRED="--init-required" ../test-network/network.sh deployCCAAS -ccn tokenchaincode -ccp $(pwd)/tokenchaincode -cci "init" -verbose -ccs 1
|
||||
|
||||
mkdir -p keys/fabric && cp -r ../test-network/organizations keys/fabric/
|
||||
```
|
||||
|
||||
> To fully remove the whole network:
|
||||
> ```bash
|
||||
> docker stop peer0org1_tokenchaincode_ccaas peer0org2_tokenchaincode_ccaas
|
||||
> ../test-network/network.sh" down
|
||||
> rm -rf keys/fabric
|
||||
> ```
|
||||
|
||||
#### Start the Token network
|
||||
|
||||
> On the bottom of this document you'll find instructions to run the nodes as golang binaries natively instead of with docker compose.
|
||||
|
||||
```bash
|
||||
rm -rf data/auditor data/issuer data/owner1 data/owner2
|
||||
mkdir -p data/auditor data/issuer data/owner1 data/owner2
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Visit [http://localhost:8080](http://localhost:8080) to view the API documentation and execute some transactions.
|
||||
|
||||
### View the blockchain explorer
|
||||
|
||||
As a bonus, this sample contains configuration to connect the [blockchain explorer](https://github.com/hyperledger-labs/blockchain-explorer/) with the fabric-samples network. It allows you to inspect the transactions which are committed to the ledger. It shows more of what the Token SDK does under the covers.
|
||||
|
||||
Start it as follows:
|
||||
|
||||
```bash
|
||||
cd explorer
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
And visit it in the browser on [localhost:8081](http://localhost:8081).
|
||||
|
||||
To tear it down, do this (the -v is important; it removes the volumes that contain the identities and blocks):
|
||||
|
||||
```bash
|
||||
docker-compose down -v
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### End to end tests
|
||||
|
||||
See the `e2e` folder for some tests that exercise the APIs. The end to end tests require the services to be running. They create new transactions, so don't run them on a deployment you want to keep clean.
|
||||
|
||||
```bash
|
||||
go test ./e2e -count=1 -v
|
||||
```
|
||||
|
||||
### Code structure
|
||||
|
||||
This repo contains 3 different, isolated golang applications, one for each of the roles: *issuer*, *auditor*, and *owner*. They are maintained separately and each have their own dependencies. In a production scenario these would have their own lifecycle, and most likely be maintained and deployed by different organizations.
|
||||
|
||||
The code structure of each of the roles is the same. There is overlap between the roles; each has the boilerplate code to start the Fabric Smart Client and Token SDK. The main.go is almost identical; the only difference is which 'responders' the application registers. Also the contents of the routes and the services will depend on the features that a role needs:
|
||||
|
||||
- Issuers can issue funds
|
||||
- Auditors see and sign every transaction
|
||||
- Owners can transfer funds.
|
||||
|
||||
Here's an example of the code structure for the auditor:
|
||||
|
||||
```
|
||||
auditor
|
||||
├── main.go
|
||||
├── oapi-server.yaml
|
||||
├── conf
|
||||
│ └── core.yaml
|
||||
├── routes
|
||||
│ ├── operations.go
|
||||
│ ├── routes.gen.go
|
||||
│ ├── routes.go
|
||||
│ └── server.go
|
||||
└── service
|
||||
├── audit.go
|
||||
├── balance.go
|
||||
└── history.go
|
||||
```
|
||||
|
||||
As you can see, the business logic is all in the 'service' directory. The 'routes' are purely the code needed for the REST API. We chose to use *openapi-codegen* to generate the code for the routes, and *echo* as the server. The 'routes' package is just the presentation layer; you could easily replace it and call the code from the 'service' package from somewhere else. For instance if you wanted to create a CLI application for the issuer!
|
||||
|
||||

|
||||
|
||||
[plantuml](http://www.plantuml.com/plantuml/uml/RP71QlCm3CVlVWhHxnpw1X_jeR32e6FhRVIWEafgubX6ThgMqNTVQZjqAOD0PFd7pt_Pgn1Huj1RrGZs18lTboE19Mn365An7ceJMHRmhG36ht1R5qaSMl2eEsmfB00369ymWCyUZJlSM_S2_gszjqPZDEpoll0GAIGYbtymWUHiD2KedFKpSLEGxLLT_Ry3itMsAfZq1NbCy7Br99RgbWHUyHZcav2FqoWD7iNeAlGeiTBMa8ifKXF6I7lI9yUMs-iCZjoHgqBT9NByFv7pxEIZWZHYMJnIxkA91EYIRxj4uocQxzebYR24GvPcINQo-kNPN9xU2neMUDzyx67zjYrUdBoCtbIQQsh97NAhCwvYJsxSAPrn7fwEBPTSJaPrKojozT3R7m00)
|
||||
|
||||
For more information about how we interact with the Token SDK, check out an example on the [Token SDK GitHub](https://github.com/hyperledger-labs/fabric-token-sdk/blob/main/samples/fungible/README.md).
|
||||
|
||||
### Add or change a REST API endpoint
|
||||
|
||||
We generate the API based on `swagger.yaml`. To keep things a bit simple, we have only one definition which includes all of the roles (even though they are separate applications, running on different ports!) Any changes should be made in this file first. Then generate the code with:
|
||||
|
||||
```bash
|
||||
go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@latest
|
||||
|
||||
oapi-codegen -config auditor/oapi-server.yaml swagger.yaml
|
||||
oapi-codegen -config issuer/oapi-server.yaml swagger.yaml
|
||||
oapi-codegen -config owner/oapi-server.yaml swagger.yaml
|
||||
oapi-codegen -config e2e/oapi-client.yaml swagger.yaml
|
||||
```
|
||||
|
||||
### Upgrade the Token SDK and Fabric Smart Client versions
|
||||
|
||||
Token SDK and Fabric Smart Client are under active development. To upgrade to the latest versions:
|
||||
|
||||
- change the commit hash of fabric-smart-client and fabric-token-sdk in {auditor,issuer,owner}/go.mod (and do `go mod tidy`)
|
||||
- change the commit hash in tokenchaincode/Dockerfile
|
||||
- install tokengen with the new commit hash
|
||||
- update the readme
|
||||
|
||||
### Use another Fabric network
|
||||
|
||||
Of course you're not tied to the Fabric samples testnetwork. If you want to anchor your Token services to another blockchain, you have to:
|
||||
|
||||
1. Deploy the token chaincode with the generated parameters to the Fabric network
|
||||
2. Configure the token services with the correct channel, peer and orderer addresses and certs, MSP configuration, and a user (see the `core.yaml` files in the `conf` dir).
|
||||
|
||||
### Add a user / account
|
||||
|
||||
To add another user, simply register and enroll it at the Token CA (see `scripts/enroll-users.sh`), and configure it at one of the owner nodes (see `conf` dir).
|
||||
|
||||
### Run the service directly (instead of with docker-compose)
|
||||
|
||||
For a faster development cycle, you may choose to run the services outside of docker. It requires some adjustments to your environment to make the paths and routes work.
|
||||
|
||||
Add the following to your `/etc/hosts`:
|
||||
|
||||
```
|
||||
127.0.0.1 peer0.org1.example.com
|
||||
127.0.0.1 peer0.org2.example.com
|
||||
127.0.0.1 orderer.example.com
|
||||
127.0.0.1 owner1.example.com
|
||||
127.0.0.1 owner2.example.com
|
||||
127.0.0.1 auditor.example.com
|
||||
127.0.0.1 issuer.example.com
|
||||
```
|
||||
|
||||
> The Token SDK discovers the peer addresses from the channel config (after connecting to a configured trusted peer).
|
||||
|
||||
For the paths you have two options:
|
||||
|
||||
1. Find/replace all instances of /var/fsc in the conf directory with the path to this repo. **Or**
|
||||
2. Create a symlink to this folder to make the configuration files work (they don't play nice with relative paths):
|
||||
```bash
|
||||
sudo ln -s "${PWD}" /var/fsc
|
||||
```
|
||||
The advantage of this approach is that the configuration is portable across developer laptops and works with docker-compose as well as without.
|
||||
|
||||
Start the blockchain and deploy the chaincode (see above).
|
||||
|
||||
Instead of doing docker-compose up, start the token services with (each in their own terminal):
|
||||
|
||||
```bash
|
||||
mkdir bin
|
||||
go build -o bin/auditor ./auditor
|
||||
go build -o bin/issuer ./issuer
|
||||
go build -o bin/owner ./owner
|
||||
|
||||
PORT=9000 CONF_DIR=./auditor/conf ./bin/auditor
|
||||
PORT=9100 CONF_DIR=./issuer/conf ./bin/issuer
|
||||
PORT=9200 CONF_DIR=./owner/conf/owner1 ./bin/owner
|
||||
PORT=9300 CONF_DIR=./owner/conf/owner2 ./bin/owner
|
||||
```
|
||||
|
||||
Now you can use the REST APIs to control the services (see the swagger definition).
|
||||
|
||||
When you made changes in the code, stop a service with CTRL+C, `go build -o bin/owner ./owner` and start it again.
|
||||
|
||||
If you want to reset the transaction history:
|
||||
|
||||
```bash
|
||||
rm -rf data/auditor data/issuer data/owner1 data/owner2 && mkdir data/auditor data/issuer data/owner1 data/owner2
|
||||
```
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
logging:
|
||||
spec: info
|
||||
format: '%{color}%{time:2006-01-02 15:04:05.000 MST} [%{module}] %{shortfunc} -> %{level:.4s} %{id:03x}%{color:reset} %{message}'
|
||||
|
||||
# ------------------- FSC Node Configuration -------------------------
|
||||
# The FSC node is responsible for the peer to peer communication with other token services.
|
||||
fsc:
|
||||
identity:
|
||||
cert:
|
||||
file: /var/fsc/keys/auditor/fsc/msp/signcerts/cert.pem
|
||||
key:
|
||||
file: /var/fsc/keys/auditor/fsc/msp/keystore/priv_sk
|
||||
tls:
|
||||
enabled: false # TODO
|
||||
p2p:
|
||||
listenAddress: /ip4/0.0.0.0/tcp/9001
|
||||
# If empty, this is a P2P boostrap node. Otherwise, it contains the name of the FSC node that is a bootstrap node.
|
||||
# The name of the FSC node that is a bootstrap node must be set under fsc.endpoint.resolvers
|
||||
bootstrapNode:
|
||||
kvs: # key-value-store
|
||||
persistence:
|
||||
type: badger # badger or memory
|
||||
opts:
|
||||
path: /var/fsc/data/auditor/kvs
|
||||
|
||||
# The endpoint section tells how to reach other FSC node in the network.
|
||||
# For each node, the name, the domain, the identity of the node, and its addresses must be specified.
|
||||
endpoint:
|
||||
resolvers:
|
||||
- name: issuer
|
||||
identity:
|
||||
id: issuer
|
||||
path: /var/fsc/keys/issuer/fsc/msp/signcerts/cert.pem
|
||||
addresses:
|
||||
P2P: issuer.example.com:9101
|
||||
- name: owner1
|
||||
identity:
|
||||
id: owner1
|
||||
path: /var/fsc/keys/owner1/fsc/msp/signcerts/cert.pem
|
||||
addresses:
|
||||
P2P: owner1.example.com:9201
|
||||
- name: owner2
|
||||
identity:
|
||||
id: owner2
|
||||
path: /var/fsc/keys/owner2/fsc/msp/signcerts/cert.pem
|
||||
addresses:
|
||||
P2P: owner2.example.com:9301
|
||||
|
||||
# ------------------- Fabric Configuration -------------------------
|
||||
fabric:
|
||||
enabled: true
|
||||
mynetwork:
|
||||
default: true
|
||||
mspConfigPath: /var/fsc/keys/fabric/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp
|
||||
defaultMSP: Org1MSP
|
||||
msps:
|
||||
- id: Org1MSP
|
||||
mspType: bccsp
|
||||
mspID: Org1MSP
|
||||
path: /var/fsc/keys/fabric/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp
|
||||
tls:
|
||||
enabled: true
|
||||
# If the keepalive values are too low, Fabric peers will complain with: ENHANCE_YOUR_CALM, debug data: "too_many_pings"
|
||||
keepalive:
|
||||
interval: 300s
|
||||
timeout: 600s
|
||||
# List of orderer nodes this node can connect to. There must be at least one orderer node. Others are discovered.
|
||||
orderers:
|
||||
- address: orderer.example.com:7050
|
||||
connectionTimeout: 10s
|
||||
tlsEnabled: true
|
||||
tlsRootCertFile: /var/fsc/keys/fabric/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/tls/ca.crt
|
||||
serverNameOverride: orderer.example.com
|
||||
# List of trusted peers this node can connect to. There must be at least one trusted peer. Others are discovered.
|
||||
peers:
|
||||
- address: peer0.org1.example.com:7051
|
||||
connectionTimeout: 10s
|
||||
tlsEnabled: true
|
||||
tlsRootCertFile: /var/fsc/keys/fabric/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
|
||||
serverNameOverride: peer0.org1.example.com
|
||||
# Channel where the token chaincode is deployed
|
||||
channels:
|
||||
- name: mychannel
|
||||
default: true
|
||||
# Configuration of the vault used to store the RW sets assembled by this node
|
||||
vault:
|
||||
persistence:
|
||||
type: badger
|
||||
opts:
|
||||
path: /var/fsc/data/auditor/vault
|
||||
|
||||
# ------------------- Token SDK Configuration -------------------------
|
||||
token:
|
||||
enabled: true
|
||||
tms:
|
||||
mytms: # unique name of this token management system
|
||||
network: mynetwork # the name of the fabric network as configured above
|
||||
channel: mychannel # the name of the network's channel this TMS refers to, if applicable
|
||||
namespace: tokenchaincode # chaincode name
|
||||
driver: zkatdlog # privacy preserving driver (zero knowledge asset transfer)
|
||||
wallets:
|
||||
auditors:
|
||||
- id: auditor # the unique identifier of this wallet. Here is an example of use: `ttx.GetIssuerWallet(context, "issuer)`
|
||||
default: true # is this the default issuer wallet
|
||||
path: /var/fsc/keys/auditor/aud/msp
|
||||
# Internal database to keep track of token transactions.
|
||||
# It is used by auditors and token owners to track history
|
||||
ttxdb:
|
||||
persistence:
|
||||
type: badger
|
||||
opts:
|
||||
path: /var/fsc/data/auditor/txdb
|
||||
|
|
@ -1,245 +0,0 @@
|
|||
module github.com/hyperledger/fabric-samples/token-sdk/auditor
|
||||
|
||||
go 1.20
|
||||
|
||||
replace github.com/ugorji/go v1.1.4 => github.com/ugorji/go/codec v1.2.9
|
||||
|
||||
require (
|
||||
github.com/deepmap/oapi-codegen v1.15.0
|
||||
github.com/getkin/kin-openapi v0.120.0
|
||||
github.com/hyperledger-labs/fabric-smart-client v0.3.0
|
||||
github.com/hyperledger-labs/fabric-token-sdk v0.3.0
|
||||
github.com/labstack/echo/v4 v4.11.1
|
||||
github.com/pkg/errors v0.9.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.3.2 // indirect
|
||||
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect
|
||||
github.com/CloudyKit/jet/v6 v6.2.0 // indirect
|
||||
github.com/IBM/idemix v0.0.2-0.20230831093709-b7a940638990 // indirect
|
||||
github.com/IBM/idemix/bccsp/schemes/aries v0.0.0-20230831093709-b7a940638990 // indirect
|
||||
github.com/IBM/idemix/bccsp/schemes/weak-bb v0.0.0-20230831093709-b7a940638990 // indirect
|
||||
github.com/IBM/idemix/bccsp/types v0.0.0-20230831093709-b7a940638990 // indirect
|
||||
github.com/IBM/mathlib v0.0.3-0.20230831091907-c532c4d3b65c // indirect
|
||||
github.com/Joker/jade v1.1.3 // indirect
|
||||
github.com/ReneKroon/ttlcache/v2 v2.11.0 // indirect
|
||||
github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 // indirect
|
||||
github.com/ale-linux/aries-framework-go/component/kmscrypto v0.0.0-20230817163708-4b3de6d91874 // indirect
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.2.0 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/consensys/bavard v0.1.13 // indirect
|
||||
github.com/consensys/gnark-crypto v0.9.1 // indirect
|
||||
github.com/containerd/cgroups v1.1.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
||||
github.com/dgraph-io/badger/v3 v3.2103.2 // indirect
|
||||
github.com/dgraph-io/ristretto v0.1.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/elastic/gosigar v0.14.2 // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/flosch/pongo2/v4 v4.0.2 // indirect
|
||||
github.com/flynn/noise v1.0.0 // indirect
|
||||
github.com/francoispqt/gojay v1.2.13 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/gin-gonic/gin v1.9.1 // indirect
|
||||
github.com/go-kit/kit v0.10.0 // indirect
|
||||
github.com/go-logfmt/logfmt v0.5.1 // indirect
|
||||
github.com/go-logr/logr v1.2.4 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
github.com/go-openapi/swag v0.22.4 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/golang/glog v1.0.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12 // indirect
|
||||
github.com/google/flatbuffers v1.12.1 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/gopacket v1.1.19 // indirect
|
||||
github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b // indirect
|
||||
github.com/google/uuid v1.3.1 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-uuid v1.0.2 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/huin/goupnp v1.2.0 // indirect
|
||||
github.com/hyperledger-labs/orion-sdk-go v0.2.5 // indirect
|
||||
github.com/hyperledger-labs/orion-server v0.2.5 // indirect
|
||||
github.com/hyperledger-labs/weaver-dlt-interoperability/common/protos-go v1.2.3-alpha.1 // indirect
|
||||
github.com/hyperledger-labs/weaver-dlt-interoperability/sdks/fabric/go-sdk v1.2.3-alpha.1.0.20210812140206-37f430515b8c // indirect
|
||||
github.com/hyperledger/fabric v1.4.0-rc1.0.20230401164317-bd8e24856939 // indirect
|
||||
github.com/hyperledger/fabric-amcl v0.0.0-20230602173724-9e02669dceb2 // indirect
|
||||
github.com/hyperledger/fabric-chaincode-go v0.0.0-20220920210243-7bc6fa0dd58b // indirect
|
||||
github.com/hyperledger/fabric-lib-go v1.0.0 // indirect
|
||||
github.com/hyperledger/fabric-private-chaincode v0.0.0-20210907122433-d56466264e4d // indirect
|
||||
github.com/hyperledger/fabric-protos-go v0.2.0 // indirect
|
||||
github.com/invopop/yaml v0.2.0 // indirect
|
||||
github.com/ipfs/boxo v0.8.0-rc1 // indirect
|
||||
github.com/ipfs/go-cid v0.4.1 // indirect
|
||||
github.com/ipfs/go-datastore v0.6.0 // indirect
|
||||
github.com/ipfs/go-ipfs-util v0.0.2 // indirect
|
||||
github.com/ipfs/go-log v1.0.5 // indirect
|
||||
github.com/ipfs/go-log/v2 v2.5.1 // indirect
|
||||
github.com/ipld/go-ipld-prime v0.20.0 // indirect
|
||||
github.com/iris-contrib/schema v0.0.6 // indirect
|
||||
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
|
||||
github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect
|
||||
github.com/jbenet/goprocess v0.1.4 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kataras/blocks v0.0.7 // indirect
|
||||
github.com/kataras/golog v0.1.9 // indirect
|
||||
github.com/kataras/iris/v12 v12.2.6-0.20230908161203-24ba4e8933b9 // indirect
|
||||
github.com/kataras/pio v0.0.12 // indirect
|
||||
github.com/kataras/sitemap v0.0.6 // indirect
|
||||
github.com/kataras/tunnel v0.0.4 // indirect
|
||||
github.com/kilic/bls12-381 v0.1.0 // indirect
|
||||
github.com/klauspost/compress v1.16.7 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||
github.com/koron/go-ssdp v0.0.4 // indirect
|
||||
github.com/labstack/gommon v0.4.0 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
|
||||
github.com/libp2p/go-cidranger v1.1.0 // indirect
|
||||
github.com/libp2p/go-flow-metrics v0.1.0 // indirect
|
||||
github.com/libp2p/go-libp2p v0.31.0 // indirect
|
||||
github.com/libp2p/go-libp2p-asn-util v0.3.0 // indirect
|
||||
github.com/libp2p/go-libp2p-kad-dht v0.22.0 // indirect
|
||||
github.com/libp2p/go-libp2p-kbucket v0.5.0 // indirect
|
||||
github.com/libp2p/go-libp2p-record v0.2.0 // indirect
|
||||
github.com/libp2p/go-msgio v0.3.0 // indirect
|
||||
github.com/libp2p/go-nat v0.2.0 // indirect
|
||||
github.com/libp2p/go-netroute v0.2.1 // indirect
|
||||
github.com/libp2p/go-reuseport v0.4.0 // indirect
|
||||
github.com/libp2p/go-yamux/v4 v4.0.1 // indirect
|
||||
github.com/magiconair/properties v1.8.5 // indirect
|
||||
github.com/mailgun/raymond/v2 v2.0.48 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.25 // indirect
|
||||
github.com/miekg/dns v1.1.55 // indirect
|
||||
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect
|
||||
github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect
|
||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.3 // indirect
|
||||
github.com/mmcloughlin/addchain v0.4.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||
github.com/multiformats/go-base32 v0.1.0 // indirect
|
||||
github.com/multiformats/go-base36 v0.2.0 // indirect
|
||||
github.com/multiformats/go-multiaddr v0.11.0 // indirect
|
||||
github.com/multiformats/go-multiaddr-dns v0.3.1 // indirect
|
||||
github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect
|
||||
github.com/multiformats/go-multibase v0.2.0 // indirect
|
||||
github.com/multiformats/go-multicodec v0.9.0 // indirect
|
||||
github.com/multiformats/go-multihash v0.2.3 // indirect
|
||||
github.com/multiformats/go-multistream v0.4.1 // indirect
|
||||
github.com/multiformats/go-varint v0.0.7 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.11.0 // indirect
|
||||
github.com/opencontainers/runtime-spec v1.1.0 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
|
||||
github.com/pelletier/go-toml v1.9.4 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/polydawn/refmt v0.89.0 // indirect
|
||||
github.com/prometheus/client_golang v1.14.0 // indirect
|
||||
github.com/prometheus/client_model v0.4.0 // indirect
|
||||
github.com/prometheus/common v0.42.0 // indirect
|
||||
github.com/prometheus/procfs v0.9.0 // indirect
|
||||
github.com/quic-go/qpack v0.4.0 // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.3.3 // indirect
|
||||
github.com/quic-go/quic-go v0.38.1 // indirect
|
||||
github.com/quic-go/webtransport-go v0.5.3 // indirect
|
||||
github.com/raulk/go-watchdog v1.3.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/schollz/closestmatch v2.1.0+incompatible // indirect
|
||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/spf13/afero v1.6.0 // indirect
|
||||
github.com/spf13/cast v1.4.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.10.1 // indirect
|
||||
github.com/stretchr/testify v1.8.4 // indirect
|
||||
github.com/subosito/gotenv v1.2.0 // indirect
|
||||
github.com/sykesm/zap-logfmt v0.0.4 // indirect
|
||||
github.com/tdewolff/minify/v2 v2.12.9 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.6.8 // indirect
|
||||
github.com/test-go/testify v1.1.4 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect
|
||||
github.com/yosssi/ace v0.0.5 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/otel v1.14.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.13.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.13.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.13.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.13.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.14.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/dig v1.17.0 // indirect
|
||||
go.uber.org/fx v1.20.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.25.0 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.13.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect
|
||||
golang.org/x/mod v0.12.0 // indirect
|
||||
golang.org/x/net v0.15.0 // indirect
|
||||
golang.org/x/sync v0.3.0 // indirect
|
||||
golang.org/x/sys v0.12.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
|
||||
google.golang.org/grpc v1.53.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
lukechampine.com/blake3 v1.2.1 // indirect
|
||||
rsc.io/tmplfunc v0.0.3 // indirect
|
||||
)
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,114 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/hyperledger/fabric-samples/token-sdk/auditor/routes"
|
||||
"github.com/hyperledger/fabric-samples/token-sdk/auditor/service"
|
||||
|
||||
"github.com/hyperledger-labs/fabric-smart-client/pkg/api"
|
||||
"github.com/hyperledger-labs/fabric-smart-client/pkg/node"
|
||||
fabric "github.com/hyperledger-labs/fabric-smart-client/platform/fabric/sdk"
|
||||
viewregistry "github.com/hyperledger-labs/fabric-smart-client/platform/view"
|
||||
"github.com/hyperledger-labs/fabric-smart-client/platform/view/services/flogging"
|
||||
tokensdk "github.com/hyperledger-labs/fabric-token-sdk/token/sdk"
|
||||
"github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx"
|
||||
)
|
||||
|
||||
var logger = flogging.MustGetLogger("main")
|
||||
|
||||
func main() {
|
||||
dir := getEnv("CONF_DIR", "./conf")
|
||||
port := getEnv("PORT", "9000")
|
||||
|
||||
fsc := startFabricSmartClient(dir)
|
||||
// Tell the service how to respond to other nodes when they initiate an action
|
||||
registry := viewregistry.GetRegistry(fsc)
|
||||
succeedOrPanic(registry.RegisterResponder(&service.AuditView{}, &ttx.AuditingViewInitiator{}))
|
||||
|
||||
controller := routes.Controller{Service: service.TokenService{FSC: fsc}}
|
||||
err := routes.StartWebServer(port, controller, logger)
|
||||
if err != nil {
|
||||
if err == http.ErrServerClosed {
|
||||
logger.Infof("Webserver closing, exiting...", err.Error())
|
||||
fsc.Stop()
|
||||
} else {
|
||||
logger.Fatalf("echo error - %s", err.Error())
|
||||
fsc.Stop()
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Node interface {
|
||||
api.ServiceProvider
|
||||
Stop()
|
||||
}
|
||||
|
||||
func startFabricSmartClient(confDir string) Node {
|
||||
logger.Infof("Initializing Fabric Smart Client and Token SDK...")
|
||||
fsc := node.NewFromConfPath(confDir)
|
||||
succeedOrPanic(fsc.InstallSDK(fabric.NewSDK(fsc)))
|
||||
succeedOrPanic(fsc.InstallSDK(tokensdk.NewSDK(fsc)))
|
||||
succeedOrPanic(fsc.Start())
|
||||
|
||||
// Stop gracefully
|
||||
go handleSignals((map[os.Signal]func(){
|
||||
syscall.SIGINT: func() {
|
||||
logger.Info("Stopping FSC node...")
|
||||
fsc.Stop()
|
||||
os.Exit(130)
|
||||
},
|
||||
syscall.SIGTERM: func() {
|
||||
logger.Info("Stopping FSC node...")
|
||||
fsc.Stop()
|
||||
os.Exit(143)
|
||||
},
|
||||
syscall.SIGSTOP: func() {
|
||||
logger.Info("Stopping FSC node...")
|
||||
fsc.Stop()
|
||||
os.Exit(145)
|
||||
},
|
||||
syscall.SIGHUP: func() {
|
||||
logger.Info("Stopping FSC node...")
|
||||
fsc.Stop()
|
||||
os.Exit(129)
|
||||
},
|
||||
}))
|
||||
logger.Infof("FSC node is ready!")
|
||||
|
||||
return fsc
|
||||
}
|
||||
|
||||
// getEnv returns an environment variable or the fallback
|
||||
func getEnv(key, fallback string) string {
|
||||
if value, ok := os.LookupEnv(key); ok {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func succeedOrPanic(err error) {
|
||||
if err != nil {
|
||||
logger.Fatalf("Failed initializing Token SDK - %s", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func handleSignals(handlers map[os.Signal]func()) {
|
||||
var signals []os.Signal
|
||||
for sig := range handlers {
|
||||
signals = append(signals, sig)
|
||||
}
|
||||
|
||||
signalChan := make(chan os.Signal, 1)
|
||||
signal.Notify(signalChan, signals...)
|
||||
|
||||
for sig := range signalChan {
|
||||
logger.Infof("Received signal: %d (%s)", sig, sig)
|
||||
handlers[sig]()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
package: routes
|
||||
generate:
|
||||
echo-server: true
|
||||
strict-server: true
|
||||
models: true
|
||||
embedded-spec: true
|
||||
output-options:
|
||||
include-tags:
|
||||
- operations
|
||||
- auditor
|
||||
output: auditor/routes/routes.gen.go
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
/*
|
||||
Copyright IBM Corp. All Rights Reserved.
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package routes
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// (GET /readyz)
|
||||
func (c Controller) Readyz(ctx context.Context, request ReadyzRequestObject) (ReadyzResponseObject, error) {
|
||||
// TODO: what defines readiness if the REST API is available after FSC?
|
||||
return Readyz200JSONResponse{
|
||||
HealthSuccessJSONResponse: HealthSuccessJSONResponse{
|
||||
Message: "ok",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// (GET /healthz)
|
||||
func (c Controller) Healthz(ctx context.Context, request HealthzRequestObject) (HealthzResponseObject, error) {
|
||||
// TODO: how to determine health?
|
||||
return Healthz200JSONResponse{
|
||||
HealthSuccessJSONResponse: HealthSuccessJSONResponse{
|
||||
Message: "ok",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -1,579 +0,0 @@
|
|||
// Package auditor provides primitives to interact with the openapi HTTP API.
|
||||
//
|
||||
// Code generated by github.com/deepmap/oapi-codegen version v1.13.4 DO NOT EDIT.
|
||||
package routes
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/deepmap/oapi-codegen/pkg/runtime"
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// Account Information about an account and its balance
|
||||
type Account struct {
|
||||
// Balance balance in base units for each currency
|
||||
Balance []Amount `json:"balance"`
|
||||
|
||||
// Id account id as registered at the Certificate Authority
|
||||
Id string `json:"id"`
|
||||
}
|
||||
|
||||
// Amount The amount to issue, transfer or redeem.
|
||||
type Amount struct {
|
||||
// Code the code of the token
|
||||
Code string `json:"code"`
|
||||
|
||||
// Value value in base units (usually cents)
|
||||
Value int64 `json:"value"`
|
||||
}
|
||||
|
||||
// Error defines model for Error.
|
||||
type Error struct {
|
||||
// Message High level error message
|
||||
Message string `json:"message"`
|
||||
|
||||
// Payload Details about the error
|
||||
Payload string `json:"payload"`
|
||||
}
|
||||
|
||||
// TransactionRecord A transaction
|
||||
type TransactionRecord struct {
|
||||
// Amount The amount to issue, transfer or redeem.
|
||||
Amount Amount `json:"amount"`
|
||||
|
||||
// Id transaction id
|
||||
Id string `json:"id"`
|
||||
|
||||
// Message user provided message
|
||||
Message string `json:"message"`
|
||||
|
||||
// Recipient the recipient of the transaction
|
||||
Recipient string `json:"recipient"`
|
||||
|
||||
// Sender the sender of the transaction
|
||||
Sender string `json:"sender"`
|
||||
|
||||
// Status Unknown | Pending | Confirmed | Deleted
|
||||
Status string `json:"status"`
|
||||
|
||||
// Timestamp timestamp in the format: "2018-03-20T09:12:28Z"
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
|
||||
// Code The token code to filter on
|
||||
type Code = string
|
||||
|
||||
// Id account id as registered at the Certificate Authority
|
||||
type Id = string
|
||||
|
||||
// AccountSuccess defines model for AccountSuccess.
|
||||
type AccountSuccess struct {
|
||||
Message string `json:"message"`
|
||||
|
||||
// Payload Information about an account and its balance
|
||||
Payload Account `json:"payload"`
|
||||
}
|
||||
|
||||
// ErrorResponse defines model for ErrorResponse.
|
||||
type ErrorResponse = Error
|
||||
|
||||
// HealthSuccess defines model for HealthSuccess.
|
||||
type HealthSuccess struct {
|
||||
// Message ok
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// TransactionsSuccess defines model for TransactionsSuccess.
|
||||
type TransactionsSuccess struct {
|
||||
Message string `json:"message"`
|
||||
Payload []TransactionRecord `json:"payload"`
|
||||
}
|
||||
|
||||
// AuditorAccountParams defines parameters for AuditorAccount.
|
||||
type AuditorAccountParams struct {
|
||||
Code *Code `form:"code,omitempty" json:"code,omitempty"`
|
||||
}
|
||||
|
||||
// ServerInterface represents all server handlers.
|
||||
type ServerInterface interface {
|
||||
// Get an account and their balance of a certain type
|
||||
// (GET /auditor/accounts/{id})
|
||||
AuditorAccount(ctx echo.Context, id Id, params AuditorAccountParams) error
|
||||
// Get all transactions for an account
|
||||
// (GET /auditor/accounts/{id}/transactions)
|
||||
AuditorTransactions(ctx echo.Context, id Id) error
|
||||
|
||||
// (GET /healthz)
|
||||
Healthz(ctx echo.Context) error
|
||||
|
||||
// (GET /readyz)
|
||||
Readyz(ctx echo.Context) error
|
||||
}
|
||||
|
||||
// ServerInterfaceWrapper converts echo contexts to parameters.
|
||||
type ServerInterfaceWrapper struct {
|
||||
Handler ServerInterface
|
||||
}
|
||||
|
||||
// AuditorAccount converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) AuditorAccount(ctx echo.Context) error {
|
||||
var err error
|
||||
// ------------- Path parameter "id" -------------
|
||||
var id Id
|
||||
|
||||
err = runtime.BindStyledParameterWithLocation("simple", false, "id", runtime.ParamLocationPath, ctx.Param("id"), &id)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err))
|
||||
}
|
||||
|
||||
// Parameter object where we will unmarshal all parameters from the context
|
||||
var params AuditorAccountParams
|
||||
// ------------- Optional query parameter "code" -------------
|
||||
|
||||
err = runtime.BindQueryParameter("form", true, false, "code", ctx.QueryParams(), ¶ms.Code)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter code: %s", err))
|
||||
}
|
||||
|
||||
// Invoke the callback with all the unmarshaled arguments
|
||||
err = w.Handler.AuditorAccount(ctx, id, params)
|
||||
return err
|
||||
}
|
||||
|
||||
// AuditorTransactions converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) AuditorTransactions(ctx echo.Context) error {
|
||||
var err error
|
||||
// ------------- Path parameter "id" -------------
|
||||
var id Id
|
||||
|
||||
err = runtime.BindStyledParameterWithLocation("simple", false, "id", runtime.ParamLocationPath, ctx.Param("id"), &id)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err))
|
||||
}
|
||||
|
||||
// Invoke the callback with all the unmarshaled arguments
|
||||
err = w.Handler.AuditorTransactions(ctx, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// Healthz converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) Healthz(ctx echo.Context) error {
|
||||
var err error
|
||||
|
||||
// Invoke the callback with all the unmarshaled arguments
|
||||
err = w.Handler.Healthz(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
// Readyz converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) Readyz(ctx echo.Context) error {
|
||||
var err error
|
||||
|
||||
// Invoke the callback with all the unmarshaled arguments
|
||||
err = w.Handler.Readyz(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
// This is a simple interface which specifies echo.Route addition functions which
|
||||
// are present on both echo.Echo and echo.Group, since we want to allow using
|
||||
// either of them for path registration
|
||||
type EchoRouter interface {
|
||||
CONNECT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
|
||||
DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
|
||||
GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
|
||||
HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
|
||||
OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
|
||||
PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
|
||||
POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
|
||||
PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
|
||||
TRACE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
|
||||
}
|
||||
|
||||
// RegisterHandlers adds each server route to the EchoRouter.
|
||||
func RegisterHandlers(router EchoRouter, si ServerInterface) {
|
||||
RegisterHandlersWithBaseURL(router, si, "")
|
||||
}
|
||||
|
||||
// Registers handlers, and prepends BaseURL to the paths, so that the paths
|
||||
// can be served under a prefix.
|
||||
func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL string) {
|
||||
|
||||
wrapper := ServerInterfaceWrapper{
|
||||
Handler: si,
|
||||
}
|
||||
|
||||
router.GET(baseURL+"/auditor/accounts/:id", wrapper.AuditorAccount)
|
||||
router.GET(baseURL+"/auditor/accounts/:id/transactions", wrapper.AuditorTransactions)
|
||||
router.GET(baseURL+"/healthz", wrapper.Healthz)
|
||||
router.GET(baseURL+"/readyz", wrapper.Readyz)
|
||||
|
||||
}
|
||||
|
||||
type AccountSuccessJSONResponse struct {
|
||||
Message string `json:"message"`
|
||||
|
||||
// Payload Information about an account and its balance
|
||||
Payload Account `json:"payload"`
|
||||
}
|
||||
|
||||
type ErrorResponseJSONResponse Error
|
||||
|
||||
type HealthSuccessJSONResponse struct {
|
||||
// Message ok
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type TransactionsSuccessJSONResponse struct {
|
||||
Message string `json:"message"`
|
||||
Payload []TransactionRecord `json:"payload"`
|
||||
}
|
||||
|
||||
type AuditorAccountRequestObject struct {
|
||||
Id Id `json:"id"`
|
||||
Params AuditorAccountParams
|
||||
}
|
||||
|
||||
type AuditorAccountResponseObject interface {
|
||||
VisitAuditorAccountResponse(w http.ResponseWriter) error
|
||||
}
|
||||
|
||||
type AuditorAccount200JSONResponse struct{ AccountSuccessJSONResponse }
|
||||
|
||||
func (response AuditorAccount200JSONResponse) VisitAuditorAccountResponse(w http.ResponseWriter) error {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(200)
|
||||
|
||||
return json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
type AuditorAccountdefaultJSONResponse struct {
|
||||
Body Error
|
||||
StatusCode int
|
||||
}
|
||||
|
||||
func (response AuditorAccountdefaultJSONResponse) VisitAuditorAccountResponse(w http.ResponseWriter) error {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(response.StatusCode)
|
||||
|
||||
return json.NewEncoder(w).Encode(response.Body)
|
||||
}
|
||||
|
||||
type AuditorTransactionsRequestObject struct {
|
||||
Id Id `json:"id"`
|
||||
}
|
||||
|
||||
type AuditorTransactionsResponseObject interface {
|
||||
VisitAuditorTransactionsResponse(w http.ResponseWriter) error
|
||||
}
|
||||
|
||||
type AuditorTransactions200JSONResponse struct {
|
||||
TransactionsSuccessJSONResponse
|
||||
}
|
||||
|
||||
func (response AuditorTransactions200JSONResponse) VisitAuditorTransactionsResponse(w http.ResponseWriter) error {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(200)
|
||||
|
||||
return json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
type AuditorTransactionsdefaultJSONResponse struct {
|
||||
Body Error
|
||||
StatusCode int
|
||||
}
|
||||
|
||||
func (response AuditorTransactionsdefaultJSONResponse) VisitAuditorTransactionsResponse(w http.ResponseWriter) error {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(response.StatusCode)
|
||||
|
||||
return json.NewEncoder(w).Encode(response.Body)
|
||||
}
|
||||
|
||||
type HealthzRequestObject struct {
|
||||
}
|
||||
|
||||
type HealthzResponseObject interface {
|
||||
VisitHealthzResponse(w http.ResponseWriter) error
|
||||
}
|
||||
|
||||
type Healthz200JSONResponse struct{ HealthSuccessJSONResponse }
|
||||
|
||||
func (response Healthz200JSONResponse) VisitHealthzResponse(w http.ResponseWriter) error {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(200)
|
||||
|
||||
return json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
type Healthz503JSONResponse struct{ ErrorResponseJSONResponse }
|
||||
|
||||
func (response Healthz503JSONResponse) VisitHealthzResponse(w http.ResponseWriter) error {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(503)
|
||||
|
||||
return json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
type ReadyzRequestObject struct {
|
||||
}
|
||||
|
||||
type ReadyzResponseObject interface {
|
||||
VisitReadyzResponse(w http.ResponseWriter) error
|
||||
}
|
||||
|
||||
type Readyz200JSONResponse struct{ HealthSuccessJSONResponse }
|
||||
|
||||
func (response Readyz200JSONResponse) VisitReadyzResponse(w http.ResponseWriter) error {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(200)
|
||||
|
||||
return json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
type Readyz503JSONResponse struct{ ErrorResponseJSONResponse }
|
||||
|
||||
func (response Readyz503JSONResponse) VisitReadyzResponse(w http.ResponseWriter) error {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(503)
|
||||
|
||||
return json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// StrictServerInterface represents all server handlers.
|
||||
type StrictServerInterface interface {
|
||||
// Get an account and their balance of a certain type
|
||||
// (GET /auditor/accounts/{id})
|
||||
AuditorAccount(ctx context.Context, request AuditorAccountRequestObject) (AuditorAccountResponseObject, error)
|
||||
// Get all transactions for an account
|
||||
// (GET /auditor/accounts/{id}/transactions)
|
||||
AuditorTransactions(ctx context.Context, request AuditorTransactionsRequestObject) (AuditorTransactionsResponseObject, error)
|
||||
|
||||
// (GET /healthz)
|
||||
Healthz(ctx context.Context, request HealthzRequestObject) (HealthzResponseObject, error)
|
||||
|
||||
// (GET /readyz)
|
||||
Readyz(ctx context.Context, request ReadyzRequestObject) (ReadyzResponseObject, error)
|
||||
}
|
||||
|
||||
type StrictHandlerFunc = runtime.StrictEchoHandlerFunc
|
||||
type StrictMiddlewareFunc = runtime.StrictEchoMiddlewareFunc
|
||||
|
||||
func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface {
|
||||
return &strictHandler{ssi: ssi, middlewares: middlewares}
|
||||
}
|
||||
|
||||
type strictHandler struct {
|
||||
ssi StrictServerInterface
|
||||
middlewares []StrictMiddlewareFunc
|
||||
}
|
||||
|
||||
// AuditorAccount operation middleware
|
||||
func (sh *strictHandler) AuditorAccount(ctx echo.Context, id Id, params AuditorAccountParams) error {
|
||||
var request AuditorAccountRequestObject
|
||||
|
||||
request.Id = id
|
||||
request.Params = params
|
||||
|
||||
handler := func(ctx echo.Context, request interface{}) (interface{}, error) {
|
||||
return sh.ssi.AuditorAccount(ctx.Request().Context(), request.(AuditorAccountRequestObject))
|
||||
}
|
||||
for _, middleware := range sh.middlewares {
|
||||
handler = middleware(handler, "AuditorAccount")
|
||||
}
|
||||
|
||||
response, err := handler(ctx, request)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if validResponse, ok := response.(AuditorAccountResponseObject); ok {
|
||||
return validResponse.VisitAuditorAccountResponse(ctx.Response())
|
||||
} else if response != nil {
|
||||
return fmt.Errorf("Unexpected response type: %T", response)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AuditorTransactions operation middleware
|
||||
func (sh *strictHandler) AuditorTransactions(ctx echo.Context, id Id) error {
|
||||
var request AuditorTransactionsRequestObject
|
||||
|
||||
request.Id = id
|
||||
|
||||
handler := func(ctx echo.Context, request interface{}) (interface{}, error) {
|
||||
return sh.ssi.AuditorTransactions(ctx.Request().Context(), request.(AuditorTransactionsRequestObject))
|
||||
}
|
||||
for _, middleware := range sh.middlewares {
|
||||
handler = middleware(handler, "AuditorTransactions")
|
||||
}
|
||||
|
||||
response, err := handler(ctx, request)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if validResponse, ok := response.(AuditorTransactionsResponseObject); ok {
|
||||
return validResponse.VisitAuditorTransactionsResponse(ctx.Response())
|
||||
} else if response != nil {
|
||||
return fmt.Errorf("Unexpected response type: %T", response)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Healthz operation middleware
|
||||
func (sh *strictHandler) Healthz(ctx echo.Context) error {
|
||||
var request HealthzRequestObject
|
||||
|
||||
handler := func(ctx echo.Context, request interface{}) (interface{}, error) {
|
||||
return sh.ssi.Healthz(ctx.Request().Context(), request.(HealthzRequestObject))
|
||||
}
|
||||
for _, middleware := range sh.middlewares {
|
||||
handler = middleware(handler, "Healthz")
|
||||
}
|
||||
|
||||
response, err := handler(ctx, request)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if validResponse, ok := response.(HealthzResponseObject); ok {
|
||||
return validResponse.VisitHealthzResponse(ctx.Response())
|
||||
} else if response != nil {
|
||||
return fmt.Errorf("Unexpected response type: %T", response)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Readyz operation middleware
|
||||
func (sh *strictHandler) Readyz(ctx echo.Context) error {
|
||||
var request ReadyzRequestObject
|
||||
|
||||
handler := func(ctx echo.Context, request interface{}) (interface{}, error) {
|
||||
return sh.ssi.Readyz(ctx.Request().Context(), request.(ReadyzRequestObject))
|
||||
}
|
||||
for _, middleware := range sh.middlewares {
|
||||
handler = middleware(handler, "Readyz")
|
||||
}
|
||||
|
||||
response, err := handler(ctx, request)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if validResponse, ok := response.(ReadyzResponseObject); ok {
|
||||
return validResponse.VisitReadyzResponse(ctx.Response())
|
||||
} else if response != nil {
|
||||
return fmt.Errorf("Unexpected response type: %T", response)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Base64 encoded, gzipped, json marshaled Swagger object
|
||||
var swaggerSpec = []string{
|
||||
|
||||
"H4sIAAAAAAAC/9RZUW/bNhD+KwduDy2gRrKzDa3esrZYi70UaQoMbfNwls4WG4pUScqZl/q/DyQlWbJk",
|
||||
"J826onlKJFLH7767++4I37BMlZWSJK1h6Q2rUGNJlrR/ylRO7i+XLGWfa9IbFjGJJbE0rEXMZAWV6Dbl",
|
||||
"ZDLNK8uV231REFh1RRLcRrAKllxY0qAkixj9jWUlnJmX787/YhGzm8o9Gau5XLHtNmI8706u0Ba7g3nO",
|
||||
"Iqbpc8015Sy1uqbDMDDLVC0t8BzQgKYVN5Y05YAWbEHwnLTlS56hJTirbaE0t5sBQBQ8owmEWwfCVEoa",
|
||||
"8lydhZPe1llGpmFPWpLW/YtVJdwhXMn4k3HIbnqQK60qhyMYKskYXHne986MWIUbodAz87OmJUvZT/Eu",
|
||||
"gHEwaeIGCwsgW6Y+dKZ3hi47x9TiE2U2ODbksHEJWncdkJdaK33evvgaZ4/h9lanIPiFAYBXhMIW92G7",
|
||||
"C22PaqauPL2HAjFEo64mM3aK6fvye6FRGszcDvNNU2qX2LZ3BGiymtOa8rFng6zjlkpzWxh74M8pUzp3",
|
||||
"RhqrqDVu/rfE3LZK0C/JcQBfy6XSpecOcKFqCyihlQqUOXBrYIECpS/9Xsa0L9MPrTq2CrZGURNLZ0mS",
|
||||
"JNuoW3339kVvdZ4k28ugbY2wjLKuO2EfdLMAXMICDUEtHcql0kCYFZDVWpPMnHjdKUhnZZCI/ci0yvu9",
|
||||
"dHSYCF7cWwrGORCxBvZkv0G/5noNN6amCHyKL13TceKRE5Unw3AeCuEoKm0nzGmJtbC7b4YoHBW+36ml",
|
||||
"p8V3wKmSao7a98K/3ovwo9rUKMQGMhe/xyxiIXldK5T2t19YxEoueVmXLE26o7i0tCI9Irhp2+H8KYKD",
|
||||
"Bh/SSfJCPC7XlH2Ffr7iqwIErUnAvr1j2jM08oIscmGa+nVke1t3VuZjUjPQ30bCRgDOoKegw7TCLkmP",
|
||||
"JJgrs9n8NOqx6webjFfcazxbqIWbsEjmpHsVZCza2rCUPVdyyXUZRJuXZCyWFUvZPJk9fZKcPpknF8mz",
|
||||
"dDZP50/fj8OzA3k3mZiShR4DwCd7x8EkqA1pqLRa85zyYxnQY+Rmoty65a7mBlEZmWvpnLIV1u5qqAnD",
|
||||
"vqF38kqqawlf4A3JnMsVfIEuUvAFXpAgO91oe0EcwWuXnDo4dEEEUvg4Ge6PrK8TOVp64izcTX8bivrU",
|
||||
"R2269EF2HES3zDtcLtVEFw4iXSiR96Tatd+g1UE9zQn8jtkV5bDYAELOHfJFbSkHQfmKdPRRVpoM6bXj",
|
||||
"utJ8jdkGauOe3pNW8KdU134rvNFKLc2Jd8L6rnThj3ClSdoEWLOTxMVCVSSx4ixlpyfJyanXC1v4eMdY",
|
||||
"59wqHTdd0cQ3PN+6lRX5LHVl5qeL104Zz8LudhqJBpesD9Plt9sSczc+3brLC42bLQaXknmSHCrwbl+8",
|
||||
"d3PxY1bT5W77dHgN8OMX6XXr2N4IEWhgEau1YCkrrK3SOBYqQ1EoY9NnSZLEWPF4PYu9K6YuS9QblrI/",
|
||||
"aDSi2YK4boc0V7IIGWmLrjxcBkbM4srh6A6+/KbwttGBPIj7g/VtSdGf8++VGfeK+NTt4ocNuxAwuKq4",
|
||||
"iXeXC98lzoW/bP5zMJivmvX7xGJ4kd1G7Nfk9D4RaFnokJlAREfmOdlaSwPzJAEeOpwXTXelMBBc9PN/",
|
||||
"7KdnHf74a+VBMsPOI1zODsbWq38j8W5k74XUYVDXknaFdRyFH4y8KISJ6RCYeR9MtG8lQy2U8WZylEfM",
|
||||
"nI7yYwi26wYPC3Ecuu4PDHyYzX5EeLSotXzcpBE75Nm+Ij+wwLSz0QMJzUU7yvWLW9nCDXe9CteE+eaw",
|
||||
"pp6H5Qcsqd5B732WUWUhQyHMt+2W0X8T5OgHy6GG8NFNu+n8uEYucCH8zwk7pprfBdoXYzyT33dMtT8r",
|
||||
"hOc7fu3LFK5RCLJmZ8S/nrDxtsmK0GWbOw7mXLoE3X29y7Pt5fbfAAAA///PKdGnmxkAAA==",
|
||||
}
|
||||
|
||||
// GetSwagger returns the content of the embedded swagger specification file
|
||||
// or error if failed to decode
|
||||
func decodeSpec() ([]byte, error) {
|
||||
zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, ""))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error base64 decoding spec: %w", err)
|
||||
}
|
||||
zr, err := gzip.NewReader(bytes.NewReader(zipped))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decompressing spec: %w", err)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
_, err = buf.ReadFrom(zr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decompressing spec: %w", err)
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
var rawSpec = decodeSpecCached()
|
||||
|
||||
// a naive cached of a decoded swagger spec
|
||||
func decodeSpecCached() func() ([]byte, error) {
|
||||
data, err := decodeSpec()
|
||||
return func() ([]byte, error) {
|
||||
return data, err
|
||||
}
|
||||
}
|
||||
|
||||
// Constructs a synthetic filesystem for resolving external references when loading openapi specifications.
|
||||
func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) {
|
||||
res := make(map[string]func() ([]byte, error))
|
||||
if len(pathToFile) > 0 {
|
||||
res[pathToFile] = rawSpec
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// GetSwagger returns the Swagger specification corresponding to the generated code
|
||||
// in this file. The external references of Swagger specification are resolved.
|
||||
// The logic of resolving external references is tightly connected to "import-mapping" feature.
|
||||
// Externally referenced files must be embedded in the corresponding golang packages.
|
||||
// Urls can be supported but this task was out of the scope.
|
||||
func GetSwagger() (swagger *openapi3.T, err error) {
|
||||
resolvePath := PathToRawSpec("")
|
||||
|
||||
loader := openapi3.NewLoader()
|
||||
loader.IsExternalRefsAllowed = true
|
||||
loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) {
|
||||
pathToFile := url.String()
|
||||
pathToFile = path.Clean(pathToFile)
|
||||
getSpec, ok := resolvePath[pathToFile]
|
||||
if !ok {
|
||||
err1 := fmt.Errorf("path not found: %s", pathToFile)
|
||||
return nil, err1
|
||||
}
|
||||
return getSpec()
|
||||
}
|
||||
var specData []byte
|
||||
specData, err = rawSpec()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
swagger, err = loader.LoadFromData(specData)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
/*
|
||||
Copyright IBM Corp. All Rights Reserved.
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package routes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hyperledger/fabric-samples/token-sdk/auditor/service"
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
Service service.TokenService
|
||||
}
|
||||
|
||||
// Get an account and their balance of a certain type
|
||||
// (GET /auditor/accounts/{id})
|
||||
func (c Controller) AuditorAccount(ctx context.Context, request AuditorAccountRequestObject) (AuditorAccountResponseObject, error) {
|
||||
if request.Params.Code == nil {
|
||||
return AuditorAccountdefaultJSONResponse{
|
||||
Body: Error{
|
||||
Message: "code is required",
|
||||
Payload: "",
|
||||
},
|
||||
StatusCode: 400,
|
||||
}, nil
|
||||
}
|
||||
|
||||
balance, err := c.Service.GetBalance(request.Id, *request.Params.Code)
|
||||
if err != nil {
|
||||
return AuditorAccountdefaultJSONResponse{
|
||||
Body: Error{
|
||||
Message: "can't get account",
|
||||
Payload: err.Error(),
|
||||
},
|
||||
StatusCode: 500,
|
||||
}, nil
|
||||
}
|
||||
|
||||
amounts := []Amount{}
|
||||
for typ, val := range balance {
|
||||
amounts = append(amounts, Amount{
|
||||
Code: typ,
|
||||
Value: val,
|
||||
})
|
||||
}
|
||||
return AuditorAccount200JSONResponse{
|
||||
AccountSuccessJSONResponse: AccountSuccessJSONResponse{
|
||||
Message: fmt.Sprintf("got %s's %s", request.Id, *request.Params.Code),
|
||||
Payload: Account{
|
||||
Id: request.Id,
|
||||
Balance: amounts,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Get all transactions for an account
|
||||
// (GET /owner/accounts/{id}/transactions)
|
||||
func (c Controller) AuditorTransactions(ctx context.Context, request AuditorTransactionsRequestObject) (AuditorTransactionsResponseObject, error) {
|
||||
var history []service.TransactionHistoryItem
|
||||
var err error
|
||||
|
||||
history, err = c.Service.GetHistory(request.Id)
|
||||
if err != nil {
|
||||
return AuditorTransactionsdefaultJSONResponse{
|
||||
Body: Error{
|
||||
Message: "can't get history",
|
||||
Payload: err.Error(),
|
||||
},
|
||||
StatusCode: 500,
|
||||
}, nil
|
||||
}
|
||||
|
||||
pl := []TransactionRecord{}
|
||||
for _, tx := range history {
|
||||
pl = append(pl, TransactionRecord{
|
||||
Amount: Amount{
|
||||
Code: tx.TokenType,
|
||||
Value: tx.Amount,
|
||||
},
|
||||
Id: tx.TxID,
|
||||
Recipient: tx.Recipient,
|
||||
Sender: tx.Sender,
|
||||
Status: tx.Status,
|
||||
Timestamp: tx.Timestamp,
|
||||
Message: tx.Message,
|
||||
})
|
||||
}
|
||||
return AuditorTransactions200JSONResponse{
|
||||
TransactionsSuccessJSONResponse: TransactionsSuccessJSONResponse{
|
||||
Message: fmt.Sprintf("got %d transactions for %s", len(pl), request.Id),
|
||||
Payload: pl,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
/*
|
||||
Copyright IBM Corp. All Rights Reserved.
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package routes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
oapimiddleware "github.com/deepmap/oapi-codegen/pkg/middleware"
|
||||
"github.com/labstack/echo/v4"
|
||||
middleware "github.com/labstack/echo/v4/middleware"
|
||||
)
|
||||
|
||||
type Logger interface {
|
||||
Infof(template string, args ...interface{})
|
||||
Debugf(template string, args ...interface{})
|
||||
Warnf(template string, args ...interface{})
|
||||
Errorf(template string, args ...interface{})
|
||||
Fatalf(template string, args ...interface{})
|
||||
}
|
||||
|
||||
// Start web server on the main thread. It exits the application if it fails setting up.
|
||||
func StartWebServer(port string, routesImplementation StrictServerInterface, logger Logger) error {
|
||||
e := echo.New()
|
||||
baseURL := "/api/v1"
|
||||
|
||||
handler := NewStrictHandler(routesImplementation, nil)
|
||||
RegisterHandlersWithBaseURL(e, handler, baseURL)
|
||||
|
||||
// Request validator
|
||||
swagger, err := GetSwagger()
|
||||
if err != nil {
|
||||
log.Fatalf("Error loading swagger spec\n: %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
swagger.Servers = nil
|
||||
e.Group(baseURL).Use(oapimiddleware.OapiRequestValidator(swagger))
|
||||
|
||||
e.Use(middleware.CORS())
|
||||
e.Use(middleware.RequestID())
|
||||
e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
|
||||
Skipper: func(c echo.Context) bool {
|
||||
return c.Path() == "/api/v1/healthz" || c.Path() == "/api/v1/readyz"
|
||||
},
|
||||
LogRequestID: true, LogMethod: true, LogURI: true, LogStatus: true, LogLatency: true,
|
||||
LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
|
||||
if v.Status < 400 {
|
||||
logger.Infof("%d %s %s %s [%s]", v.Status, v.Method, v.URI, v.Latency.String(), v.RequestID)
|
||||
} else if v.Status >= 400 && v.Status < 500 {
|
||||
logger.Warnf("%d %s %s %s [%s]", v.Status, v.Method, v.URI, v.Latency.String(), v.RequestID)
|
||||
} else {
|
||||
logger.Errorf("%d %s %s %s [%s]", v.Status, v.Method, v.URI, v.Latency.String(), v.RequestID)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}))
|
||||
|
||||
// Start REST API server
|
||||
return e.Start(fmt.Sprintf("0.0.0.0:%s", port))
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
/*
|
||||
Copyright IBM Corp. All Rights Reserved.
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/hyperledger-labs/fabric-smart-client/platform/view/services/flogging"
|
||||
"github.com/hyperledger-labs/fabric-smart-client/platform/view/view"
|
||||
"github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var logger = flogging.MustGetLogger("service")
|
||||
|
||||
// VIEW
|
||||
|
||||
// Auditing is initiated as a response to an audit request from another
|
||||
// FSC node (not via an internal service or API).
|
||||
|
||||
type AuditView struct{}
|
||||
|
||||
func (v *AuditView) Call(context view.Context) (interface{}, error) {
|
||||
logger.Infof("incoming session from [%s]", context.Session().Info().Endpoint)
|
||||
tx, err := ttx.ReceiveTransaction(context)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "failed receiving transaction")
|
||||
logger.Error(err.Error())
|
||||
return "", err
|
||||
}
|
||||
// get auditor wallet
|
||||
w := ttx.MyAuditorWallet(context)
|
||||
if w == nil {
|
||||
err = errors.New("failed getting default auditor wallet")
|
||||
logger.Error(err.Error())
|
||||
return "", err
|
||||
}
|
||||
auditor := ttx.NewAuditor(context, w)
|
||||
|
||||
// Validate
|
||||
err = auditor.Validate(tx)
|
||||
if err != nil {
|
||||
err = errors.Wrapf(err, "transaction invalid: [%s]", tx.ID())
|
||||
logger.Error(err.Error())
|
||||
return "", err
|
||||
}
|
||||
// See https://github.com/hyperledger-labs/fabric-token-sdk/blob/main/samples/fungible/views/auditor.go for examples of auditor checks
|
||||
|
||||
logger.Infof("transaction valid: [%s]", tx.ID())
|
||||
res, err := context.RunView(ttx.NewAuditApproveView(w, tx))
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
return "", err
|
||||
}
|
||||
logger.Infof("transaction committed: [%s]", tx.ID())
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
type RegisterAuditorView struct{}
|
||||
|
||||
func (r *RegisterAuditorView) Call(context view.Context) (interface{}, error) {
|
||||
return context.RunView(ttx.NewRegisterAuditorView(
|
||||
&AuditView{},
|
||||
))
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
/*
|
||||
Copyright IBM Corp. All Rights Reserved.
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/hyperledger-labs/fabric-smart-client/pkg/api"
|
||||
"github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type TokenService struct {
|
||||
FSC api.ServiceProvider
|
||||
}
|
||||
|
||||
// SERVICE
|
||||
type ValueByTokenType map[string]int64
|
||||
|
||||
// GetBalance returns the balances per token type of a wallet
|
||||
func (s TokenService) GetBalance(wallet string, tokenType string) (typeVal ValueByTokenType, err error) {
|
||||
typeVal = make(ValueByTokenType)
|
||||
|
||||
// get auditor wallet
|
||||
w := ttx.MyAuditorWallet(s.FSC)
|
||||
if w == nil {
|
||||
err = errors.New("failed getting default auditor wallet")
|
||||
logger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
auditor := ttx.NewAuditor(s.FSC, w)
|
||||
|
||||
aqe := auditor.NewQueryExecutor()
|
||||
defer aqe.Done()
|
||||
|
||||
// TODO: how to get all TokenTypes separately?
|
||||
filter, err := aqe.NewHoldingsFilter().ByEnrollmentId(wallet).ByType(tokenType).Execute()
|
||||
if err != nil {
|
||||
err = errors.Wrapf(err, "failed retrieving holding for [%s][%s]", wallet, tokenType)
|
||||
logger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
currentHolding := filter.Sum()
|
||||
|
||||
typeVal[tokenType] = currentHolding.Int64()
|
||||
logger.Debugf("Current Holding: [%s][%s][%d]", wallet, tokenType, typeVal[tokenType])
|
||||
|
||||
return
|
||||
}
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
/*
|
||||
Copyright IBM Corp. All Rights Reserved.
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/hyperledger-labs/fabric-token-sdk/token"
|
||||
"github.com/hyperledger-labs/fabric-token-sdk/token/services/ttx"
|
||||
"github.com/hyperledger-labs/fabric-token-sdk/token/services/ttxdb"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// SERVICE
|
||||
|
||||
type TransactionHistoryItem struct {
|
||||
// TxID is the transaction ID
|
||||
TxID string
|
||||
// ActionType is the type of action performed by this transaction record
|
||||
ActionType int
|
||||
// SenderEID is the enrollment ID of the account that is sending tokens
|
||||
Sender string
|
||||
// RecipientEID is the enrollment ID of the account that is receiving tokens
|
||||
Recipient string
|
||||
// TokenType is the type of token
|
||||
TokenType string
|
||||
// Amount is positive if tokens are received. Negative otherwise
|
||||
Amount int64
|
||||
// Timestamp is the time the transaction was submitted to the db
|
||||
Timestamp time.Time
|
||||
// Status is the status of the transaction
|
||||
Status string
|
||||
// Message is the user message sent with the transaction. It comes from
|
||||
// the ApplicationMetadata and is sent in the transient field
|
||||
Message string
|
||||
}
|
||||
|
||||
// GetHistory returns the full transaction history for an auditor.
|
||||
func (s TokenService) GetHistory(wallet string) (txs []TransactionHistoryItem, err error) {
|
||||
// get auditor wallet
|
||||
w := ttx.MyAuditorWallet(s.FSC)
|
||||
if w == nil {
|
||||
err = errors.New("failed getting default auditor wallet")
|
||||
logger.Error(err.Error())
|
||||
return txs, err
|
||||
}
|
||||
auditor := ttx.NewAuditor(s.FSC, w)
|
||||
|
||||
// Get query executor
|
||||
aqe := auditor.NewQueryExecutor()
|
||||
defer aqe.Done()
|
||||
|
||||
// This retrieves all transactions to *or* from the provided wallet.
|
||||
// See QueryTransactionsParams interface for additional filters.
|
||||
it, err := aqe.Transactions(ttxdb.QueryTransactionsParams{
|
||||
SenderWallet: wallet,
|
||||
RecipientWallet: wallet,
|
||||
})
|
||||
if err != nil {
|
||||
return txs, errors.New("failed querying transactions")
|
||||
}
|
||||
defer it.Close()
|
||||
|
||||
// we need transaction info to get the transient field (application metadata)
|
||||
tip := ttx.NewTransactionInfoProvider(s.FSC, token.GetManagementService(s.FSC))
|
||||
if tip == nil {
|
||||
return txs, errors.New("failed to get transactionInfoProvider")
|
||||
}
|
||||
|
||||
// Return the list of audited transactions
|
||||
for {
|
||||
tx, err := it.Next()
|
||||
if tx == nil {
|
||||
break
|
||||
}
|
||||
transaction := TransactionHistoryItem{
|
||||
TxID: tx.TxID,
|
||||
ActionType: int(tx.ActionType),
|
||||
Sender: tx.SenderEID,
|
||||
Recipient: tx.RecipientEID,
|
||||
TokenType: tx.TokenType,
|
||||
Amount: tx.Amount.Int64(),
|
||||
Timestamp: tx.Timestamp.UTC(),
|
||||
Status: string(tx.Status),
|
||||
}
|
||||
if err != nil {
|
||||
return txs, errors.New("failed iterating over transactions")
|
||||
}
|
||||
|
||||
// set user provided message from transient field
|
||||
ti, err := tip.TransactionInfo(transaction.TxID)
|
||||
if err != nil {
|
||||
return txs, err
|
||||
}
|
||||
if ti.ApplicationMetadata != nil && string(ti.ApplicationMetadata["message"]) != "" {
|
||||
transaction.Message = string(ti.ApplicationMetadata["message"])
|
||||
}
|
||||
txs = append(txs, transaction)
|
||||
}
|
||||
return
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 34 KiB |
|
|
@ -1,23 +0,0 @@
|
|||
# Copyright IBM Corp. All Rights Reserved.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
version: '3.7'
|
||||
|
||||
services:
|
||||
ca_token_network:
|
||||
image: hyperledger/fabric-ca:1.5.7
|
||||
labels:
|
||||
service: hyperledger-fabric
|
||||
environment:
|
||||
- FABRIC_CA_HOME=/etc/hyperledger/fabric-ca-server
|
||||
- FABRIC_CA_SERVER_CA_NAME=ca-token-network
|
||||
- FABRIC_CA_SERVER_TLS_ENABLED=false
|
||||
- FABRIC_CA_SERVER_PORT=27054
|
||||
ports:
|
||||
- "27054:27054"
|
||||
command: sh -c 'fabric-ca-server start -b admin:adminpw --idemix.curve gurvy.Bn254 -d'
|
||||
volumes:
|
||||
- ${PWD}/keys/ca:/etc/hyperledger/fabric-ca-server
|
||||
container_name: ca_token_network
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 29 KiB |
|
|
@ -1,101 +0,0 @@
|
|||
version: '3.7'
|
||||
|
||||
# fabric_test is the name of the fabric-samples test network.
|
||||
# By connecting to it, we can reach the peers at their DNS names
|
||||
# (e.g. peer0.org1.example.com).
|
||||
networks:
|
||||
test:
|
||||
name: fabric_test
|
||||
external: true
|
||||
|
||||
services:
|
||||
auditor:
|
||||
hostname: auditor.example.com
|
||||
restart: always
|
||||
build:
|
||||
context: ./auditor
|
||||
dockerfile: ../Dockerfile
|
||||
volumes:
|
||||
- ./data/auditor:/var/fsc/data/auditor
|
||||
- ./auditor/conf:/conf:ro
|
||||
- ./keys:/var/fsc/keys:ro
|
||||
ports:
|
||||
- 9000:9000
|
||||
expose:
|
||||
- 9001
|
||||
networks:
|
||||
- test
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:9000/api/v1/readyz"]
|
||||
interval: "5s"
|
||||
timeout: "1s"
|
||||
retries: 20
|
||||
|
||||
issuer:
|
||||
hostname: issuer.example.com
|
||||
restart: always
|
||||
build:
|
||||
context: ./issuer
|
||||
dockerfile: ../Dockerfile
|
||||
volumes:
|
||||
- ./data/issuer:/var/fsc/data/issuer
|
||||
- ./issuer/conf:/conf:ro
|
||||
- ./keys:/var/fsc/keys:ro
|
||||
ports:
|
||||
- 9100:9000
|
||||
expose:
|
||||
- 9101
|
||||
networks:
|
||||
- test
|
||||
depends_on:
|
||||
auditor:
|
||||
condition: service_healthy
|
||||
|
||||
owner1:
|
||||
hostname: owner1.example.com
|
||||
restart: always
|
||||
build:
|
||||
context: ./owner
|
||||
dockerfile: ../Dockerfile
|
||||
volumes:
|
||||
- ./data/owner1:/var/fsc/data/owner1
|
||||
- ./owner/conf/owner1:/conf:ro
|
||||
- ./keys:/var/fsc/keys:ro
|
||||
ports:
|
||||
- 9200:9000
|
||||
expose:
|
||||
- 9201
|
||||
networks:
|
||||
- test
|
||||
depends_on:
|
||||
auditor:
|
||||
condition: service_healthy
|
||||
|
||||
owner2:
|
||||
hostname: owner2.example.com
|
||||
restart: always
|
||||
build:
|
||||
context: ./owner
|
||||
dockerfile: ../Dockerfile
|
||||
volumes:
|
||||
- ./data/owner2:/var/fsc/data/owner2
|
||||
- ./owner/conf/owner2:/conf:ro
|
||||
- ./keys:/var/fsc/keys:ro
|
||||
ports:
|
||||
- 9300:9000
|
||||
expose:
|
||||
- 9301
|
||||
networks:
|
||||
- test
|
||||
depends_on:
|
||||
auditor:
|
||||
condition: service_healthy
|
||||
|
||||
swagger-ui:
|
||||
image: swaggerapi/swagger-ui
|
||||
ports:
|
||||
- '8080:8080'
|
||||
environment:
|
||||
- URL=/swagger.yaml
|
||||
volumes:
|
||||
- ./swagger.yaml:/usr/share/nginx/html/swagger.yaml
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,249 +0,0 @@
|
|||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var auditor *ClientWithResponses
|
||||
var issuer *ClientWithResponses
|
||||
|
||||
var err error
|
||||
var CODE string = "TEST"
|
||||
var alice = Counterparty{
|
||||
Account: "alice",
|
||||
Node: "owner1",
|
||||
}
|
||||
var bob = Counterparty{
|
||||
Account: "bob",
|
||||
Node: "owner1",
|
||||
}
|
||||
var dan = Counterparty{
|
||||
Account: "dan",
|
||||
Node: "owner2",
|
||||
}
|
||||
|
||||
type ownerAPI struct {
|
||||
client *ClientWithResponses
|
||||
}
|
||||
|
||||
var owner1 ownerAPI
|
||||
var owner2 ownerAPI
|
||||
|
||||
func TestMain(t *testing.T) {
|
||||
auditor, err = NewClientWithResponses(getEnv("AUDITOR_URL", "http://localhost:9000/api/v1"))
|
||||
assert.NoError(t, err, "failed creating client")
|
||||
issuer, err = NewClientWithResponses(getEnv("ISSUER_URL", "http://localhost:9100/api/v1"))
|
||||
assert.NoError(t, err, "failed creating client")
|
||||
|
||||
client1, err := NewClientWithResponses(getEnv("OWNER1_URL", "http://localhost:9200/api/v1"))
|
||||
assert.NoError(t, err, "failed creating client")
|
||||
owner1 = ownerAPI{client: client1}
|
||||
|
||||
client2, err := NewClientWithResponses(getEnv("OWNER2_URL", "http://localhost:9300/api/v1"))
|
||||
assert.NoError(t, err, "failed creating client")
|
||||
owner2 = ownerAPI{client: client2}
|
||||
|
||||
// we have to issue funds to alice first to be able to do the other tests
|
||||
testIssuance(t)
|
||||
}
|
||||
|
||||
func testIssuance(t *testing.T) {
|
||||
accBefore := owner1.getAccounts(t)
|
||||
txBefore := owner1.getTransactions(t, "alice")
|
||||
|
||||
id := issue(t, alice, 1000)
|
||||
acc2 := owner1.getAccounts(t)
|
||||
|
||||
txAfter := owner1.getTransactions(t, "alice")
|
||||
assert.Equal(t, len(txBefore)+1, len(txAfter), "should have 1 issue transaction more", txAfter)
|
||||
assert.Equal(t, getValue(t, accBefore, "alice")+1000, getValue(t, acc2, "alice"), acc2)
|
||||
|
||||
lastTx := txAfter[len(txAfter)-1]
|
||||
assert.Equal(t, id, lastTx.Id)
|
||||
assert.Equal(t, int64(1000), lastTx.Amount.Value)
|
||||
assert.Equal(t, "alice", lastTx.Recipient)
|
||||
}
|
||||
|
||||
func TestTransfer(t *testing.T) {
|
||||
accBefore := owner1.getAccounts(t)
|
||||
txBefore := owner1.getTransactions(t, "alice")
|
||||
id := owner1.transfer(t, "alice", bob, 100)
|
||||
accAfter := owner1.getAccounts(t)
|
||||
txAfter := owner1.getTransactions(t, "alice")
|
||||
|
||||
assert.Equal(t, getValue(t, accBefore, "alice")-100, getValue(t, accAfter, "alice"), accAfter)
|
||||
assert.Equal(t, getValue(t, accBefore, "bob")+100, getValue(t, accAfter, "bob"), accAfter)
|
||||
assert.Greater(t, len(txAfter), len(txBefore))
|
||||
|
||||
// on the sender side there may be several transactions, so we check the recipient
|
||||
txBob := owner1.getTransactions(t, "bob")
|
||||
lastTx := txBob[len(txBob)-1]
|
||||
assert.Equal(t, id, lastTx.Id, txBob)
|
||||
assert.Equal(t, lastTx.Amount.Value, int64(100))
|
||||
}
|
||||
|
||||
func TestTransferToSecondNode(t *testing.T) {
|
||||
// current state
|
||||
acc1Before := owner1.getAccounts(t)
|
||||
tx1Before := owner1.getTransactions(t, "alice")
|
||||
acc2Before := owner2.getAccounts(t)
|
||||
tx2Before := owner2.getTransactions(t, "dan")
|
||||
|
||||
// transfer 100 from alice to dan
|
||||
id := owner1.transfer(t, "alice", dan, 100)
|
||||
|
||||
// after: alice
|
||||
acc1After := owner1.getAccounts(t)
|
||||
assert.Equal(t, getValue(t, acc1Before, "alice")-100, getValue(t, acc1After, "alice"), acc1After) // -100 TEST
|
||||
tx1After := owner1.getTransactions(t, "alice")
|
||||
assert.Greater(t, len(tx1After), len(tx1Before)) // +1 tx
|
||||
|
||||
// after: dan
|
||||
acc2After := owner2.getAccounts(t)
|
||||
assert.Equal(t, getValue(t, acc2Before, "dan")+100, getValue(t, acc2After, "dan"), acc2After) // +100 TEST
|
||||
tx2After := owner2.getTransactions(t, "dan")
|
||||
assert.Greater(t, len(tx2After), len(tx2Before)) // + 1 tx
|
||||
|
||||
// on the sender side there may be several transactions, so we check the recipient
|
||||
txDan := owner2.getTransactions(t, "dan")
|
||||
lastTx := txDan[len(txDan)-1]
|
||||
assert.Equal(t, id, lastTx.Id, txDan)
|
||||
assert.Equal(t, lastTx.Amount.Value, int64(100))
|
||||
owner2.testIfAuditorMatchesOwnerHistory(t, []string{"dan"})
|
||||
}
|
||||
|
||||
func TestRedeem(t *testing.T) {
|
||||
accBefore := owner1.getAccounts(t)
|
||||
id := owner1.redeem(t, "alice", 10, "test redeem")
|
||||
accAfter := owner1.getAccounts(t)
|
||||
|
||||
transactions := owner1.getTransactions(t, "alice")
|
||||
assert.Equal(t, getValue(t, accBefore, "alice")-10, getValue(t, accAfter, "alice"), accAfter)
|
||||
lastTx := transactions[len(transactions)-1]
|
||||
assert.Equal(t, id, lastTx.Id, transactions)
|
||||
assert.Equal(t, lastTx.Amount.Value, int64(10))
|
||||
assert.Equal(t, "alice", lastTx.Sender)
|
||||
assert.Equal(t, "test redeem", lastTx.Message)
|
||||
}
|
||||
|
||||
func TestIfAuditorMatchesOwnerHistory(t *testing.T) {
|
||||
owner1.testIfAuditorMatchesOwnerHistory(t, []string{"alice", "bob"})
|
||||
owner2.testIfAuditorMatchesOwnerHistory(t, []string{"carlos", "dan"})
|
||||
}
|
||||
|
||||
func issue(t *testing.T, counterparty Counterparty, value int64) string {
|
||||
res, err := issuer.IssueWithResponse(context.TODO(), IssueJSONRequestBody{
|
||||
Amount: Amount{
|
||||
Code: CODE,
|
||||
Value: value,
|
||||
},
|
||||
Counterparty: alice,
|
||||
Message: new(string),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, res.JSONDefault)
|
||||
assert.NotNil(t, res.JSON200)
|
||||
t.Logf(res.JSON200.Message)
|
||||
return res.JSON200.Payload
|
||||
}
|
||||
|
||||
func getAuditorTransactions(t *testing.T, wallet string) []TransactionRecord {
|
||||
res, err := auditor.AuditorTransactionsWithResponse(context.TODO(), wallet)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, res.JSONDefault)
|
||||
assert.NotNil(t, res.JSON200)
|
||||
t.Logf(res.JSON200.Message)
|
||||
return res.JSON200.Payload
|
||||
}
|
||||
|
||||
func (o *ownerAPI) testIfAuditorMatchesOwnerHistory(t *testing.T, accounts []string) {
|
||||
for _, w := range accounts {
|
||||
tx := o.getTransactions(t, w)
|
||||
audittx := getAuditorTransactions(t, w)
|
||||
assert.Equal(t, len(tx), len(audittx), w)
|
||||
|
||||
// Timestamp is the time of storing the tx in the database
|
||||
// so it's not the same on both sides.
|
||||
for i := 0; i < len(tx); i++ {
|
||||
tx[i].Timestamp = time.Time{}
|
||||
audittx[i].Timestamp = time.Time{}
|
||||
}
|
||||
assert.Equal(t, tx, audittx)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *ownerAPI) getTransactions(t *testing.T, wallet string) []TransactionRecord {
|
||||
res, err := o.client.OwnerTransactionsWithResponse(context.TODO(), wallet)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, res.JSONDefault)
|
||||
assert.NotNil(t, res.JSON200)
|
||||
t.Logf(res.JSON200.Message)
|
||||
return res.JSON200.Payload
|
||||
}
|
||||
|
||||
func (o *ownerAPI) transfer(t *testing.T, sender string, counterparty Counterparty, value int64) string {
|
||||
res, err := o.client.TransferWithResponse(context.TODO(), sender, TransferJSONRequestBody{
|
||||
Amount: Amount{
|
||||
Code: CODE,
|
||||
Value: value,
|
||||
},
|
||||
Counterparty: counterparty,
|
||||
Message: new(string),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, res.JSONDefault)
|
||||
assert.NotNil(t, res.JSON200)
|
||||
t.Logf(res.JSON200.Message)
|
||||
return res.JSON200.Payload
|
||||
}
|
||||
|
||||
func (o *ownerAPI) redeem(t *testing.T, wallet string, value int64, message string) string {
|
||||
res, err := o.client.RedeemWithResponse(context.TODO(), wallet, RedeemJSONRequestBody{
|
||||
Amount: Amount{
|
||||
Code: CODE,
|
||||
Value: value,
|
||||
},
|
||||
Message: &message,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, res.JSONDefault)
|
||||
assert.NotNil(t, res.JSON200)
|
||||
t.Logf(res.JSON200.Message)
|
||||
return res.JSON200.Payload
|
||||
}
|
||||
|
||||
func (o *ownerAPI) getAccounts(t *testing.T) []Account {
|
||||
res, err := o.client.OwnerAccountsWithResponse(context.TODO())
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, res.JSONDefault)
|
||||
assert.NotNil(t, res.JSON200)
|
||||
t.Logf(res.JSON200.Message)
|
||||
return res.JSON200.Payload
|
||||
}
|
||||
|
||||
func getValue(t *testing.T, acc []Account, wallet string) int64 {
|
||||
for _, a := range acc {
|
||||
if a.Id == wallet {
|
||||
for _, b := range a.Balance {
|
||||
if b.Code == CODE {
|
||||
return b.Value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Logf("%s value not found for wallet %s in %v", CODE, wallet, acc)
|
||||
return 0
|
||||
}
|
||||
|
||||
// getEnv returns an environment variable or the fallback
|
||||
func getEnv(key, fallback string) string {
|
||||
if value, ok := os.LookupEnv(key); ok {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
module github.com/hyperledger/fabric-samples/token-sdk/e2e
|
||||
|
||||
go 1.20
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
package: e2e
|
||||
generate:
|
||||
client: true
|
||||
models: true
|
||||
output: e2e/client.gen.go
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
PORT=8081
|
||||
EXPLORER_CONFIG_FILE_PATH=./config.json
|
||||
EXPLORER_PROFILE_DIR_PATH=./connection-profile
|
||||
FABRIC_CRYPTO_PATH=../../test-network/organizations
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"network-configs": {
|
||||
"test-network": {
|
||||
"name": "Test Network",
|
||||
"profile": "./connection-profile/test-network.json"
|
||||
}
|
||||
},
|
||||
"license": "Apache-2.0"
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
{
|
||||
"name": "test-network",
|
||||
"version": "1.0.0",
|
||||
"client": {
|
||||
"tlsEnable": true,
|
||||
"adminCredential": {
|
||||
"id": "exploreradmin",
|
||||
"password": "exploreradminpw"
|
||||
},
|
||||
"enableAuthentication": true,
|
||||
"organization": "Org1MSP",
|
||||
"connection": {
|
||||
"timeout": {
|
||||
"peer": {
|
||||
"endorser": "300"
|
||||
},
|
||||
"orderer": "300"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels": {
|
||||
"mychannel": {
|
||||
"peers": {
|
||||
"peer0.org1.example.com": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"organizations": {
|
||||
"Org1MSP": {
|
||||
"mspid": "Org1MSP",
|
||||
"adminPrivateKey": {
|
||||
"path": "/tmp/crypto/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/keystore/priv_sk"
|
||||
},
|
||||
"peers": ["peer0.org1.example.com"],
|
||||
"signedCert": {
|
||||
"path": "/tmp/crypto/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/signcerts/User1@org1.example.com-cert.pem"
|
||||
}
|
||||
}
|
||||
},
|
||||
"peers": {
|
||||
"peer0.org1.example.com": {
|
||||
"tlsCACerts": {
|
||||
"path": "/tmp/crypto/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt"
|
||||
},
|
||||
"url": "grpcs://peer0.org1.example.com:7051"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# see https://github.com/hyperledger-labs/blockchain-explorer
|
||||
|
||||
version: '2.1'
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
walletstore:
|
||||
|
||||
networks:
|
||||
test:
|
||||
name: fabric_test
|
||||
external: true
|
||||
|
||||
services:
|
||||
|
||||
explorerdb.mynetwork.com:
|
||||
image: ghcr.io/hyperledger-labs/explorer-db:latest
|
||||
container_name: explorerdb.mynetwork.com
|
||||
hostname: explorerdb.mynetwork.com
|
||||
environment:
|
||||
- DATABASE_DATABASE=fabricexplorer
|
||||
- DATABASE_USERNAME=hppoc
|
||||
- DATABASE_PASSWORD=password
|
||||
healthcheck:
|
||||
test: "pg_isready -h localhost -p 5432 -q -U postgres"
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
networks:
|
||||
- test
|
||||
|
||||
explorer.mynetwork.com:
|
||||
image: ghcr.io/hyperledger-labs/explorer:latest
|
||||
container_name: explorer.mynetwork.com
|
||||
hostname: explorer.mynetwork.com
|
||||
environment:
|
||||
- DATABASE_HOST=explorerdb.mynetwork.com
|
||||
- DATABASE_DATABASE=fabricexplorer
|
||||
- DATABASE_USERNAME=hppoc
|
||||
- DATABASE_PASSWD=password
|
||||
- LOG_LEVEL_APP=info
|
||||
- LOG_LEVEL_DB=info
|
||||
- LOG_LEVEL_CONSOLE=debug
|
||||
- LOG_CONSOLE_STDOUT=true
|
||||
- DISCOVERY_AS_LOCALHOST=false
|
||||
- PORT=${PORT:-8080}
|
||||
volumes:
|
||||
- ${EXPLORER_CONFIG_FILE_PATH}:/opt/explorer/app/platform/fabric/config.json
|
||||
- ${EXPLORER_PROFILE_DIR_PATH}:/opt/explorer/app/platform/fabric/connection-profile
|
||||
- ${FABRIC_CRYPTO_PATH}:/tmp/crypto
|
||||
- walletstore:/opt/explorer/wallet
|
||||
ports:
|
||||
- ${PORT:-8080}:${PORT:-8080}
|
||||
depends_on:
|
||||
explorerdb.mynetwork.com:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- test
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
go 1.20
|
||||
|
||||
use ./auditor
|
||||
use ./issuer
|
||||
use ./owner
|
||||
use ./e2e
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
github.com/bits-and-blooms/bitset v1.2.1 h1:M+/hrU9xlMp7t4TyTDQW97d3tRPVuKFC6zBEK16QnXY=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
|
||||
github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8=
|
||||
github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
|
||||
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
||||
github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU=
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
logging:
|
||||
spec: info
|
||||
format: '%{color}%{time:2006-01-02 15:04:05.000 MST} [%{module}] %{shortfunc} -> %{level:.4s} %{id:03x}%{color:reset} %{message}'
|
||||
|
||||
# ------------------- FSC Node Configuration -------------------------
|
||||
# The FSC node is responsible for the peer to peer communication with other token services.
|
||||
fsc:
|
||||
identity:
|
||||
cert:
|
||||
file: /var/fsc/keys/issuer/fsc/msp/signcerts/cert.pem
|
||||
key:
|
||||
file: /var/fsc/keys/issuer/fsc/msp/keystore/priv_sk
|
||||
tls:
|
||||
enabled: false # TODO
|
||||
p2p:
|
||||
listenAddress: /ip4/0.0.0.0/tcp/9101
|
||||
# If empty, this is a P2P boostrap node. Otherwise, it contains the name of the FSC node that is a bootstrap node.
|
||||
# The name of the FSC node that is a bootstrap node must be set under fsc.endpoint.resolvers
|
||||
bootstrapNode: auditor
|
||||
kvs: # key-value-store
|
||||
persistence:
|
||||
type: badger # badger or memory
|
||||
opts:
|
||||
path: /var/fsc/data/issuer/kvs
|
||||
|
||||
# The endpoint section tells how to reach other FSC node in the network.
|
||||
# For each node, the name, the domain, the identity of the node, and its addresses must be specified.
|
||||
endpoint:
|
||||
resolvers:
|
||||
- name: auditor
|
||||
identity:
|
||||
id: auditor
|
||||
path: /var/fsc/keys/auditor/fsc/msp/signcerts/cert.pem
|
||||
addresses:
|
||||
P2P: auditor.example.com:9001
|
||||
- name: owner1
|
||||
identity:
|
||||
id: owner1
|
||||
path: /var/fsc/keys/owner1/fsc/msp/signcerts/cert.pem
|
||||
addresses:
|
||||
P2P: owner1.example.com:9201
|
||||
aliases:
|
||||
- owner1
|
||||
- name: owner2
|
||||
identity:
|
||||
id: owner2
|
||||
path: /var/fsc/keys/owner2/fsc/msp/signcerts/cert.pem
|
||||
addresses:
|
||||
P2P: owner2.example.com:9301
|
||||
aliases:
|
||||
- owner2
|
||||
|
||||
# ------------------- Fabric Configuration -------------------------
|
||||
fabric:
|
||||
enabled: true
|
||||
mynetwork:
|
||||
default: true
|
||||
mspConfigPath: /var/fsc/keys/fabric/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp
|
||||
defaultMSP: Org1MSP
|
||||
msps:
|
||||
- id: Org1MSP
|
||||
mspType: bccsp
|
||||
mspID: Org1MSP
|
||||
path: /var/fsc/keys/fabric/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp
|
||||
tls:
|
||||
enabled: true
|
||||
# If the keepalive values are too low, Fabric peers will complain with: ENHANCE_YOUR_CALM, debug data: "too_many_pings"
|
||||
keepalive:
|
||||
interval: 300s
|
||||
timeout: 600s
|
||||
# List of orderer nodes this node can connect to. There must be at least one orderer node. Others are discovered.
|
||||
orderers:
|
||||
- address: orderer.example.com:7050
|
||||
connectionTimeout: 10s
|
||||
tlsEnabled: true
|
||||
tlsRootCertFile: /var/fsc/keys/fabric/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/tls/ca.crt
|
||||
serverNameOverride: orderer.example.com
|
||||
# List of trusted peers this node can connect to. There must be at least one trusted peer. Others are discovered.
|
||||
peers:
|
||||
- address: peer0.org1.example.com:7051
|
||||
connectionTimeout: 10s
|
||||
tlsEnabled: true
|
||||
tlsRootCertFile: /var/fsc/keys/fabric/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
|
||||
serverNameOverride: peer0.org1.example.com
|
||||
# Channel where the token chaincode is deployed
|
||||
channels:
|
||||
- name: mychannel
|
||||
default: true
|
||||
# Configuration of the vault used to store the RW sets assembled by this node
|
||||
vault:
|
||||
persistence:
|
||||
type: badger
|
||||
opts:
|
||||
path: /var/fsc/data/issuer/vault
|
||||
|
||||
# ------------------- Token SDK Configuration -------------------------
|
||||
token:
|
||||
enabled: true
|
||||
tms:
|
||||
mytms: # unique name of this token management system
|
||||
network: mynetwork # the name of the fabric network as configured above
|
||||
channel: mychannel # the name of the network's channel this TMS refers to, if applicable
|
||||
namespace: tokenchaincode # chaincode name
|
||||
driver: zkatdlog # privacy preserving driver (zero knowledge asset transfer)
|
||||
wallets:
|
||||
issuers:
|
||||
- id: issuer # the unique identifier of this wallet. Here is an example of use: `ttx.GetIssuerWallet(context, "issuer)`
|
||||
default: true # is this the default issuer wallet
|
||||
path: /var/fsc/keys/issuer/iss/msp
|
||||
# Internal database to keep track of token transactions.
|
||||
# It is used by auditors and token owners to track history
|
||||
ttxdb:
|
||||
persistence:
|
||||
type: badger
|
||||
opts:
|
||||
path: /var/fsc/data/issuer/txdb
|
||||
|
|
@ -1,245 +0,0 @@
|
|||
module github.com/hyperledger/fabric-samples/token-sdk/issuer
|
||||
|
||||
go 1.20
|
||||
|
||||
replace github.com/ugorji/go v1.1.4 => github.com/ugorji/go/codec v1.2.9
|
||||
|
||||
require (
|
||||
github.com/deepmap/oapi-codegen v1.15.0
|
||||
github.com/getkin/kin-openapi v0.120.0
|
||||
github.com/hyperledger-labs/fabric-smart-client v0.3.0
|
||||
github.com/hyperledger-labs/fabric-token-sdk v0.3.0
|
||||
github.com/labstack/echo/v4 v4.11.1
|
||||
github.com/pkg/errors v0.9.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.3.2 // indirect
|
||||
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect
|
||||
github.com/CloudyKit/jet/v6 v6.2.0 // indirect
|
||||
github.com/IBM/idemix v0.0.2-0.20230831093709-b7a940638990 // indirect
|
||||
github.com/IBM/idemix/bccsp/schemes/aries v0.0.0-20230831093709-b7a940638990 // indirect
|
||||
github.com/IBM/idemix/bccsp/schemes/weak-bb v0.0.0-20230831093709-b7a940638990 // indirect
|
||||
github.com/IBM/idemix/bccsp/types v0.0.0-20230831093709-b7a940638990 // indirect
|
||||
github.com/IBM/mathlib v0.0.3-0.20230831091907-c532c4d3b65c // indirect
|
||||
github.com/Joker/jade v1.1.3 // indirect
|
||||
github.com/ReneKroon/ttlcache/v2 v2.11.0 // indirect
|
||||
github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 // indirect
|
||||
github.com/ale-linux/aries-framework-go/component/kmscrypto v0.0.0-20230817163708-4b3de6d91874 // indirect
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.2.0 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/consensys/bavard v0.1.13 // indirect
|
||||
github.com/consensys/gnark-crypto v0.9.1 // indirect
|
||||
github.com/containerd/cgroups v1.1.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
||||
github.com/dgraph-io/badger/v3 v3.2103.2 // indirect
|
||||
github.com/dgraph-io/ristretto v0.1.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/elastic/gosigar v0.14.2 // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/flosch/pongo2/v4 v4.0.2 // indirect
|
||||
github.com/flynn/noise v1.0.0 // indirect
|
||||
github.com/francoispqt/gojay v1.2.13 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/gin-gonic/gin v1.9.1 // indirect
|
||||
github.com/go-kit/kit v0.10.0 // indirect
|
||||
github.com/go-logfmt/logfmt v0.5.1 // indirect
|
||||
github.com/go-logr/logr v1.2.4 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
github.com/go-openapi/swag v0.22.4 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/golang/glog v1.0.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12 // indirect
|
||||
github.com/google/flatbuffers v1.12.1 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/gopacket v1.1.19 // indirect
|
||||
github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b // indirect
|
||||
github.com/google/uuid v1.3.1 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-uuid v1.0.2 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/huin/goupnp v1.2.0 // indirect
|
||||
github.com/hyperledger-labs/orion-sdk-go v0.2.5 // indirect
|
||||
github.com/hyperledger-labs/orion-server v0.2.5 // indirect
|
||||
github.com/hyperledger-labs/weaver-dlt-interoperability/common/protos-go v1.2.3-alpha.1 // indirect
|
||||
github.com/hyperledger-labs/weaver-dlt-interoperability/sdks/fabric/go-sdk v1.2.3-alpha.1.0.20210812140206-37f430515b8c // indirect
|
||||
github.com/hyperledger/fabric v1.4.0-rc1.0.20230401164317-bd8e24856939 // indirect
|
||||
github.com/hyperledger/fabric-amcl v0.0.0-20230602173724-9e02669dceb2 // indirect
|
||||
github.com/hyperledger/fabric-chaincode-go v0.0.0-20220920210243-7bc6fa0dd58b // indirect
|
||||
github.com/hyperledger/fabric-lib-go v1.0.0 // indirect
|
||||
github.com/hyperledger/fabric-private-chaincode v0.0.0-20210907122433-d56466264e4d // indirect
|
||||
github.com/hyperledger/fabric-protos-go v0.2.0 // indirect
|
||||
github.com/invopop/yaml v0.2.0 // indirect
|
||||
github.com/ipfs/boxo v0.8.0-rc1 // indirect
|
||||
github.com/ipfs/go-cid v0.4.1 // indirect
|
||||
github.com/ipfs/go-datastore v0.6.0 // indirect
|
||||
github.com/ipfs/go-ipfs-util v0.0.2 // indirect
|
||||
github.com/ipfs/go-log v1.0.5 // indirect
|
||||
github.com/ipfs/go-log/v2 v2.5.1 // indirect
|
||||
github.com/ipld/go-ipld-prime v0.20.0 // indirect
|
||||
github.com/iris-contrib/schema v0.0.6 // indirect
|
||||
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
|
||||
github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect
|
||||
github.com/jbenet/goprocess v0.1.4 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kataras/blocks v0.0.7 // indirect
|
||||
github.com/kataras/golog v0.1.9 // indirect
|
||||
github.com/kataras/iris/v12 v12.2.6-0.20230908161203-24ba4e8933b9 // indirect
|
||||
github.com/kataras/pio v0.0.12 // indirect
|
||||
github.com/kataras/sitemap v0.0.6 // indirect
|
||||
github.com/kataras/tunnel v0.0.4 // indirect
|
||||
github.com/kilic/bls12-381 v0.1.0 // indirect
|
||||
github.com/klauspost/compress v1.16.7 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||
github.com/koron/go-ssdp v0.0.4 // indirect
|
||||
github.com/labstack/gommon v0.4.0 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
|
||||
github.com/libp2p/go-cidranger v1.1.0 // indirect
|
||||
github.com/libp2p/go-flow-metrics v0.1.0 // indirect
|
||||
github.com/libp2p/go-libp2p v0.31.0 // indirect
|
||||
github.com/libp2p/go-libp2p-asn-util v0.3.0 // indirect
|
||||
github.com/libp2p/go-libp2p-kad-dht v0.22.0 // indirect
|
||||
github.com/libp2p/go-libp2p-kbucket v0.5.0 // indirect
|
||||
github.com/libp2p/go-libp2p-record v0.2.0 // indirect
|
||||
github.com/libp2p/go-msgio v0.3.0 // indirect
|
||||
github.com/libp2p/go-nat v0.2.0 // indirect
|
||||
github.com/libp2p/go-netroute v0.2.1 // indirect
|
||||
github.com/libp2p/go-reuseport v0.4.0 // indirect
|
||||
github.com/libp2p/go-yamux/v4 v4.0.1 // indirect
|
||||
github.com/magiconair/properties v1.8.5 // indirect
|
||||
github.com/mailgun/raymond/v2 v2.0.48 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.25 // indirect
|
||||
github.com/miekg/dns v1.1.55 // indirect
|
||||
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect
|
||||
github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect
|
||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.3 // indirect
|
||||
github.com/mmcloughlin/addchain v0.4.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||
github.com/multiformats/go-base32 v0.1.0 // indirect
|
||||
github.com/multiformats/go-base36 v0.2.0 // indirect
|
||||
github.com/multiformats/go-multiaddr v0.11.0 // indirect
|
||||
github.com/multiformats/go-multiaddr-dns v0.3.1 // indirect
|
||||
github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect
|
||||
github.com/multiformats/go-multibase v0.2.0 // indirect
|
||||
github.com/multiformats/go-multicodec v0.9.0 // indirect
|
||||
github.com/multiformats/go-multihash v0.2.3 // indirect
|
||||
github.com/multiformats/go-multistream v0.4.1 // indirect
|
||||
github.com/multiformats/go-varint v0.0.7 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.11.0 // indirect
|
||||
github.com/opencontainers/runtime-spec v1.1.0 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
|
||||
github.com/pelletier/go-toml v1.9.4 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/polydawn/refmt v0.89.0 // indirect
|
||||
github.com/prometheus/client_golang v1.14.0 // indirect
|
||||
github.com/prometheus/client_model v0.4.0 // indirect
|
||||
github.com/prometheus/common v0.42.0 // indirect
|
||||
github.com/prometheus/procfs v0.9.0 // indirect
|
||||
github.com/quic-go/qpack v0.4.0 // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.3.3 // indirect
|
||||
github.com/quic-go/quic-go v0.38.1 // indirect
|
||||
github.com/quic-go/webtransport-go v0.5.3 // indirect
|
||||
github.com/raulk/go-watchdog v1.3.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/schollz/closestmatch v2.1.0+incompatible // indirect
|
||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/spf13/afero v1.6.0 // indirect
|
||||
github.com/spf13/cast v1.4.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.10.1 // indirect
|
||||
github.com/stretchr/testify v1.8.4 // indirect
|
||||
github.com/subosito/gotenv v1.2.0 // indirect
|
||||
github.com/sykesm/zap-logfmt v0.0.4 // indirect
|
||||
github.com/tdewolff/minify/v2 v2.12.9 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.6.8 // indirect
|
||||
github.com/test-go/testify v1.1.4 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect
|
||||
github.com/yosssi/ace v0.0.5 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/otel v1.14.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.13.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.13.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.13.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.13.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.14.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/dig v1.17.0 // indirect
|
||||
go.uber.org/fx v1.20.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.25.0 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.13.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect
|
||||
golang.org/x/mod v0.12.0 // indirect
|
||||
golang.org/x/net v0.15.0 // indirect
|
||||
golang.org/x/sync v0.3.0 // indirect
|
||||
golang.org/x/sys v0.12.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
|
||||
google.golang.org/grpc v1.53.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
lukechampine.com/blake3 v1.2.1 // indirect
|
||||
rsc.io/tmplfunc v0.0.3 // indirect
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue