mirror of
https://github.com/hyperledger/fabric-samples.git
synced 2026-06-17 15:35:09 +00:00
se eliminaron applications-gateways inecesarios
This commit is contained in:
parent
356622a410
commit
5f7ddaeb4a
75 changed files with 0 additions and 21531 deletions
|
|
@ -1,294 +0,0 @@
|
|||
/*
|
||||
Copyright 2021 IBM All Rights Reserved.
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/hyperledger/fabric-gateway/pkg/client"
|
||||
"github.com/hyperledger/fabric-gateway/pkg/identity"
|
||||
"github.com/hyperledger/fabric-protos-go-apiv2/gateway"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
const (
|
||||
mspID = "Org1MSP"
|
||||
cryptoPath = "../../test-network/organizations/peerOrganizations/org1.example.com"
|
||||
certPath = cryptoPath + "/users/User1@org1.example.com/msp/signcerts"
|
||||
keyPath = cryptoPath + "/users/User1@org1.example.com/msp/keystore"
|
||||
tlsCertPath = cryptoPath + "/peers/peer0.org1.example.com/tls/ca.crt"
|
||||
peerEndpoint = "dns:///localhost:7051"
|
||||
gatewayPeer = "peer0.org1.example.com"
|
||||
)
|
||||
|
||||
var now = time.Now()
|
||||
var assetId = fmt.Sprintf("asset%d", now.Unix()*1e3+int64(now.Nanosecond())/1e6)
|
||||
|
||||
func main() {
|
||||
// The gRPC client connection should be shared by all Gateway connections to this endpoint
|
||||
clientConnection := newGrpcConnection()
|
||||
defer clientConnection.Close()
|
||||
|
||||
id := newIdentity()
|
||||
sign := newSign()
|
||||
|
||||
// Create a Gateway connection for a specific client identity
|
||||
gw, err := client.Connect(
|
||||
id,
|
||||
client.WithSign(sign),
|
||||
client.WithClientConnection(clientConnection),
|
||||
// Default timeouts for different gRPC calls
|
||||
client.WithEvaluateTimeout(5*time.Second),
|
||||
client.WithEndorseTimeout(15*time.Second),
|
||||
client.WithSubmitTimeout(5*time.Second),
|
||||
client.WithCommitStatusTimeout(1*time.Minute),
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer gw.Close()
|
||||
|
||||
// Override default values for chaincode and channel name as they may differ in testing contexts.
|
||||
chaincodeName := "basic"
|
||||
if ccname := os.Getenv("CHAINCODE_NAME"); ccname != "" {
|
||||
chaincodeName = ccname
|
||||
}
|
||||
|
||||
channelName := "mychannel"
|
||||
if cname := os.Getenv("CHANNEL_NAME"); cname != "" {
|
||||
channelName = cname
|
||||
}
|
||||
|
||||
network := gw.GetNetwork(channelName)
|
||||
contract := network.GetContract(chaincodeName)
|
||||
|
||||
initLedger(contract)
|
||||
getAllAssets(contract)
|
||||
createAsset(contract)
|
||||
readAssetByID(contract)
|
||||
transferAssetAsync(contract)
|
||||
exampleErrorHandling(contract)
|
||||
}
|
||||
|
||||
// newGrpcConnection creates a gRPC connection to the Gateway server.
|
||||
func newGrpcConnection() *grpc.ClientConn {
|
||||
certificatePEM, err := os.ReadFile(tlsCertPath)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to read TLS certifcate file: %w", err))
|
||||
}
|
||||
|
||||
certificate, err := identity.CertificateFromPEM(certificatePEM)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
certPool := x509.NewCertPool()
|
||||
certPool.AddCert(certificate)
|
||||
transportCredentials := credentials.NewClientTLSFromCert(certPool, gatewayPeer)
|
||||
|
||||
connection, err := grpc.NewClient(peerEndpoint, grpc.WithTransportCredentials(transportCredentials))
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to create gRPC connection: %w", err))
|
||||
}
|
||||
|
||||
return connection
|
||||
}
|
||||
|
||||
// newIdentity creates a client identity for this Gateway connection using an X.509 certificate.
|
||||
func newIdentity() *identity.X509Identity {
|
||||
certificatePEM, err := readFirstFile(certPath)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to read certificate file: %w", err))
|
||||
}
|
||||
|
||||
certificate, err := identity.CertificateFromPEM(certificatePEM)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
id, err := identity.NewX509Identity(mspID, certificate)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
// newSign creates a function that generates a digital signature from a message digest using a private key.
|
||||
func newSign() identity.Sign {
|
||||
privateKeyPEM, err := readFirstFile(keyPath)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to read private key file: %w", err))
|
||||
}
|
||||
|
||||
privateKey, err := identity.PrivateKeyFromPEM(privateKeyPEM)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
sign, err := identity.NewPrivateKeySign(privateKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return sign
|
||||
}
|
||||
|
||||
func readFirstFile(dirPath string) ([]byte, error) {
|
||||
dir, err := os.Open(dirPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fileNames, err := dir.Readdirnames(1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return os.ReadFile(path.Join(dirPath, fileNames[0]))
|
||||
}
|
||||
|
||||
// This type of transaction would typically only be run once by an application the first time it was started after its
|
||||
// initial deployment. A new version of the chaincode deployed later would likely not need to run an "init" function.
|
||||
func initLedger(contract *client.Contract) {
|
||||
fmt.Printf("\n--> Submit Transaction: InitLedger, function creates the initial set of assets on the ledger \n")
|
||||
|
||||
_, err := contract.SubmitTransaction("InitLedger")
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to submit transaction: %w", err))
|
||||
}
|
||||
|
||||
fmt.Printf("*** Transaction committed successfully\n")
|
||||
}
|
||||
|
||||
// Evaluate a transaction to query ledger state.
|
||||
func getAllAssets(contract *client.Contract) {
|
||||
fmt.Println("\n--> Evaluate Transaction: GetAllAssets, function returns all the current assets on the ledger")
|
||||
|
||||
evaluateResult, err := contract.EvaluateTransaction("GetAllAssets")
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to evaluate transaction: %w", err))
|
||||
}
|
||||
result := formatJSON(evaluateResult)
|
||||
|
||||
fmt.Printf("*** Result:%s\n", result)
|
||||
}
|
||||
|
||||
// Submit a transaction synchronously, blocking until it has been committed to the ledger.
|
||||
func createAsset(contract *client.Contract) {
|
||||
fmt.Printf("\n--> Submit Transaction: CreateAsset, creates new asset with ID, Color, Size, Owner and AppraisedValue arguments \n")
|
||||
|
||||
_, err := contract.SubmitTransaction("CreateAsset", assetId, "yellow", "5", "Tom", "1300")
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to submit transaction: %w", err))
|
||||
}
|
||||
|
||||
fmt.Printf("*** Transaction committed successfully\n")
|
||||
}
|
||||
|
||||
// Evaluate a transaction by assetID to query ledger state.
|
||||
func readAssetByID(contract *client.Contract) {
|
||||
fmt.Printf("\n--> Evaluate Transaction: ReadAsset, function returns asset attributes\n")
|
||||
|
||||
evaluateResult, err := contract.EvaluateTransaction("ReadAsset", assetId)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to evaluate transaction: %w", err))
|
||||
}
|
||||
result := formatJSON(evaluateResult)
|
||||
|
||||
fmt.Printf("*** Result:%s\n", result)
|
||||
}
|
||||
|
||||
// Submit transaction asynchronously, blocking until the transaction has been sent to the orderer, and allowing
|
||||
// this thread to process the chaincode response (e.g. update a UI) without waiting for the commit notification
|
||||
func transferAssetAsync(contract *client.Contract) {
|
||||
fmt.Printf("\n--> Async Submit Transaction: TransferAsset, updates existing asset owner")
|
||||
|
||||
submitResult, commit, err := contract.SubmitAsync("TransferAsset", client.WithArguments(assetId, "Mark"))
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to submit transaction asynchronously: %w", err))
|
||||
}
|
||||
|
||||
fmt.Printf("\n*** Successfully submitted transaction to transfer ownership from %s to Mark. \n", string(submitResult))
|
||||
fmt.Println("*** Waiting for transaction commit.")
|
||||
|
||||
if commitStatus, err := commit.Status(); err != nil {
|
||||
panic(fmt.Errorf("failed to get commit status: %w", err))
|
||||
} else if !commitStatus.Successful {
|
||||
panic(fmt.Errorf("transaction %s failed to commit with status: %d", commitStatus.TransactionID, int32(commitStatus.Code)))
|
||||
}
|
||||
|
||||
fmt.Printf("*** Transaction committed successfully\n")
|
||||
}
|
||||
|
||||
// Submit transaction, passing in the wrong number of arguments ,expected to throw an error containing details of any error responses from the smart contract.
|
||||
func exampleErrorHandling(contract *client.Contract) {
|
||||
fmt.Println("\n--> Submit Transaction: UpdateAsset asset70, asset70 does not exist and should return an error")
|
||||
|
||||
_, err := contract.SubmitTransaction("UpdateAsset", "asset70", "blue", "5", "Tomoko", "300")
|
||||
if err == nil {
|
||||
panic("******** FAILED to return an error")
|
||||
}
|
||||
|
||||
fmt.Println("*** Successfully caught the error:")
|
||||
|
||||
var endorseErr *client.EndorseError
|
||||
var submitErr *client.SubmitError
|
||||
var commitStatusErr *client.CommitStatusError
|
||||
var commitErr *client.CommitError
|
||||
|
||||
if errors.As(err, &endorseErr) {
|
||||
fmt.Printf("Endorse error for transaction %s with gRPC status %v: %s\n", endorseErr.TransactionID, status.Code(endorseErr), endorseErr)
|
||||
} else if errors.As(err, &submitErr) {
|
||||
fmt.Printf("Submit error for transaction %s with gRPC status %v: %s\n", submitErr.TransactionID, status.Code(submitErr), submitErr)
|
||||
} else if errors.As(err, &commitStatusErr) {
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
fmt.Printf("Timeout waiting for transaction %s commit status: %s", commitStatusErr.TransactionID, commitStatusErr)
|
||||
} else {
|
||||
fmt.Printf("Error obtaining commit status for transaction %s with gRPC status %v: %s\n", commitStatusErr.TransactionID, status.Code(commitStatusErr), commitStatusErr)
|
||||
}
|
||||
} else if errors.As(err, &commitErr) {
|
||||
fmt.Printf("Transaction %s failed to commit with status %d: %s\n", commitErr.TransactionID, int32(commitErr.Code), err)
|
||||
} else {
|
||||
panic(fmt.Errorf("unexpected error type %T: %w", err, err))
|
||||
}
|
||||
|
||||
// Any error that originates from a peer or orderer node external to the gateway will have its details
|
||||
// embedded within the gRPC status error. The following code shows how to extract that.
|
||||
statusErr := status.Convert(err)
|
||||
|
||||
details := statusErr.Details()
|
||||
if len(details) > 0 {
|
||||
fmt.Println("Error Details:")
|
||||
|
||||
for _, detail := range details {
|
||||
switch detail := detail.(type) {
|
||||
case *gateway.ErrorDetail:
|
||||
fmt.Printf("- address: %s, mspId: %s, message: %s\n", detail.Address, detail.MspId, detail.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Format JSON data
|
||||
func formatJSON(data []byte) string {
|
||||
var prettyJSON bytes.Buffer
|
||||
if err := json.Indent(&prettyJSON, data, "", " "); err != nil {
|
||||
panic(fmt.Errorf("failed to parse JSON: %w", err))
|
||||
}
|
||||
return prettyJSON.String()
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
module assetTransfer
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/hyperledger/fabric-gateway v1.5.0
|
||||
github.com/hyperledger/fabric-protos-go-apiv2 v0.3.3
|
||||
google.golang.org/grpc v1.63.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||
golang.org/x/crypto v0.22.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/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
)
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/hyperledger/fabric-gateway v1.5.0 h1:JChlqtJNm2479Q8YWJ6k8wwzOiu2IRrV3K8ErsQmdTU=
|
||||
github.com/hyperledger/fabric-gateway v1.5.0/go.mod h1:v13OkXAp7pKi4kh6P6epn27SyivRbljr8Gkfy8JlbtM=
|
||||
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/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
|
||||
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||
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/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo=
|
||||
go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
|
||||
google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
|
||||
google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
|
@ -1 +0,0 @@
|
|||
engine-strict=true
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import js from '@eslint/js';
|
||||
import globals from 'globals';
|
||||
|
||||
export default [
|
||||
js.configs.recommended,
|
||||
{
|
||||
languageOptions: {
|
||||
ecmaVersion: 2023,
|
||||
sourceType: 'commonjs',
|
||||
globals: {
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
{
|
||||
"name": "asset-transfer-basic",
|
||||
"version": "1.0.0",
|
||||
"description": "Asset Transfer Basic Application implemented in JavaScript using fabric-gateway",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint src",
|
||||
"pretest": "npm run lint",
|
||||
"start": "node src/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.5.0",
|
||||
"eslint": "^9.5.0",
|
||||
"globals": "^15.6.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,300 +0,0 @@
|
|||
/*
|
||||
* Copyright IBM Corp. All Rights Reserved.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
const grpc = require('@grpc/grpc-js');
|
||||
const { connect, signers } = require('@hyperledger/fabric-gateway');
|
||||
const crypto = require('node:crypto');
|
||||
const fs = require('node:fs/promises');
|
||||
const path = require('node:path');
|
||||
const { TextDecoder } = require('node:util');
|
||||
|
||||
const channelName = envOrDefault('CHANNEL_NAME', 'mychannel');
|
||||
const chaincodeName = envOrDefault('CHAINCODE_NAME', 'basic');
|
||||
const mspId = envOrDefault('MSP_ID', 'Org1MSP');
|
||||
|
||||
// Path to crypto materials.
|
||||
const cryptoPath = envOrDefault(
|
||||
'CRYPTO_PATH',
|
||||
path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
'test-network',
|
||||
'organizations',
|
||||
'peerOrganizations',
|
||||
'org1.example.com'
|
||||
)
|
||||
);
|
||||
|
||||
// Path to user private key directory.
|
||||
const keyDirectoryPath = envOrDefault(
|
||||
'KEY_DIRECTORY_PATH',
|
||||
path.resolve(
|
||||
cryptoPath,
|
||||
'users',
|
||||
'User1@org1.example.com',
|
||||
'msp',
|
||||
'keystore'
|
||||
)
|
||||
);
|
||||
|
||||
// Path to user certificate directory.
|
||||
const certDirectoryPath = envOrDefault(
|
||||
'CERT_DIRECTORY_PATH',
|
||||
path.resolve(
|
||||
cryptoPath,
|
||||
'users',
|
||||
'User1@org1.example.com',
|
||||
'msp',
|
||||
'signcerts'
|
||||
)
|
||||
);
|
||||
|
||||
// Path to peer tls certificate.
|
||||
const tlsCertPath = envOrDefault(
|
||||
'TLS_CERT_PATH',
|
||||
path.resolve(cryptoPath, 'peers', 'peer0.org1.example.com', 'tls', 'ca.crt')
|
||||
);
|
||||
|
||||
// Gateway peer endpoint.
|
||||
const peerEndpoint = envOrDefault('PEER_ENDPOINT', 'localhost:7051');
|
||||
|
||||
// Gateway peer SSL host name override.
|
||||
const peerHostAlias = envOrDefault('PEER_HOST_ALIAS', 'peer0.org1.example.com');
|
||||
|
||||
const utf8Decoder = new TextDecoder();
|
||||
const assetId = `asset${String(Date.now())}`;
|
||||
|
||||
async function main() {
|
||||
displayInputParameters();
|
||||
|
||||
// The gRPC client connection should be shared by all Gateway connections to this endpoint.
|
||||
const client = await newGrpcConnection();
|
||||
|
||||
const gateway = connect({
|
||||
client,
|
||||
identity: await newIdentity(),
|
||||
signer: await newSigner(),
|
||||
// Default timeouts for different gRPC calls
|
||||
evaluateOptions: () => {
|
||||
return { deadline: Date.now() + 5000 }; // 5 seconds
|
||||
},
|
||||
endorseOptions: () => {
|
||||
return { deadline: Date.now() + 15000 }; // 15 seconds
|
||||
},
|
||||
submitOptions: () => {
|
||||
return { deadline: Date.now() + 5000 }; // 5 seconds
|
||||
},
|
||||
commitStatusOptions: () => {
|
||||
return { deadline: Date.now() + 60000 }; // 1 minute
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
// Get a network instance representing the channel where the smart contract is deployed.
|
||||
const network = gateway.getNetwork(channelName);
|
||||
|
||||
// Get the smart contract from the network.
|
||||
const contract = network.getContract(chaincodeName);
|
||||
|
||||
// Initialize a set of asset data on the ledger using the chaincode 'InitLedger' function.
|
||||
await initLedger(contract);
|
||||
|
||||
// Return all the current assets on the ledger.
|
||||
await getAllAssets(contract);
|
||||
|
||||
// Create a new asset on the ledger.
|
||||
await createAsset(contract);
|
||||
|
||||
// Update an existing asset asynchronously.
|
||||
await transferAssetAsync(contract);
|
||||
|
||||
// Get the asset details by assetID.
|
||||
await readAssetByID(contract);
|
||||
|
||||
// Update an asset which does not exist.
|
||||
await updateNonExistentAsset(contract);
|
||||
} finally {
|
||||
gateway.close();
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('******** FAILED to run the application:', error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
||||
async function newGrpcConnection() {
|
||||
const tlsRootCert = await fs.readFile(tlsCertPath);
|
||||
const tlsCredentials = grpc.credentials.createSsl(tlsRootCert);
|
||||
return new grpc.Client(peerEndpoint, tlsCredentials, {
|
||||
'grpc.ssl_target_name_override': peerHostAlias,
|
||||
});
|
||||
}
|
||||
|
||||
async function newIdentity() {
|
||||
const certPath = await getFirstDirFileName(certDirectoryPath);
|
||||
const credentials = await fs.readFile(certPath);
|
||||
return { mspId, credentials };
|
||||
}
|
||||
|
||||
async function getFirstDirFileName(dirPath) {
|
||||
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);
|
||||
}
|
||||
|
||||
async function newSigner() {
|
||||
const keyPath = await getFirstDirFileName(keyDirectoryPath);
|
||||
const privateKeyPem = await fs.readFile(keyPath);
|
||||
const privateKey = crypto.createPrivateKey(privateKeyPem);
|
||||
return signers.newPrivateKeySigner(privateKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* This type of transaction would typically only be run once by an application the first time it was started after its
|
||||
* initial deployment. A new version of the chaincode deployed later would likely not need to run an "init" function.
|
||||
*/
|
||||
async function initLedger(contract) {
|
||||
console.log(
|
||||
'\n--> Submit Transaction: InitLedger, function creates the initial set of assets on the ledger'
|
||||
);
|
||||
|
||||
await contract.submitTransaction('InitLedger');
|
||||
|
||||
console.log('*** Transaction committed successfully');
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a transaction to query ledger state.
|
||||
*/
|
||||
async function getAllAssets(contract) {
|
||||
console.log(
|
||||
'\n--> Evaluate Transaction: GetAllAssets, function returns all the current assets on the ledger'
|
||||
);
|
||||
|
||||
const resultBytes = await contract.evaluateTransaction('GetAllAssets');
|
||||
|
||||
const resultJson = utf8Decoder.decode(resultBytes);
|
||||
const result = JSON.parse(resultJson);
|
||||
console.log('*** Result:', result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a transaction synchronously, blocking until it has been committed to the ledger.
|
||||
*/
|
||||
async function createAsset(contract) {
|
||||
console.log(
|
||||
'\n--> Submit Transaction: CreateAsset, creates new asset with ID, Color, Size, Owner and AppraisedValue arguments'
|
||||
);
|
||||
|
||||
await contract.submitTransaction(
|
||||
'CreateAsset',
|
||||
assetId,
|
||||
'yellow',
|
||||
'5',
|
||||
'Tom',
|
||||
'1300'
|
||||
);
|
||||
|
||||
console.log('*** Transaction committed successfully');
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit transaction asynchronously, allowing the application to process the smart contract response (e.g. update a UI)
|
||||
* while waiting for the commit notification.
|
||||
*/
|
||||
async function transferAssetAsync(contract) {
|
||||
console.log(
|
||||
'\n--> Async Submit Transaction: TransferAsset, updates existing asset owner'
|
||||
);
|
||||
|
||||
const commit = await contract.submitAsync('TransferAsset', {
|
||||
arguments: [assetId, 'Saptha'],
|
||||
});
|
||||
const oldOwner = utf8Decoder.decode(commit.getResult());
|
||||
|
||||
console.log(
|
||||
`*** Successfully submitted transaction to transfer ownership from ${oldOwner} to Saptha`
|
||||
);
|
||||
console.log('*** Waiting for transaction commit');
|
||||
|
||||
const status = await commit.getStatus();
|
||||
if (!status.successful) {
|
||||
throw new Error(
|
||||
`Transaction ${
|
||||
status.transactionId
|
||||
} failed to commit with status code ${String(status.code)}`
|
||||
);
|
||||
}
|
||||
|
||||
console.log('*** Transaction committed successfully');
|
||||
}
|
||||
|
||||
async function readAssetByID(contract) {
|
||||
console.log(
|
||||
'\n--> Evaluate Transaction: ReadAsset, function returns asset attributes'
|
||||
);
|
||||
|
||||
const resultBytes = await contract.evaluateTransaction(
|
||||
'ReadAsset',
|
||||
assetId
|
||||
);
|
||||
|
||||
const resultJson = utf8Decoder.decode(resultBytes);
|
||||
const result = JSON.parse(resultJson);
|
||||
console.log('*** Result:', result);
|
||||
}
|
||||
|
||||
/**
|
||||
* submitTransaction() will throw an error containing details of any error responses from the smart contract.
|
||||
*/
|
||||
async function updateNonExistentAsset(contract) {
|
||||
console.log(
|
||||
'\n--> Submit Transaction: UpdateAsset asset70, asset70 does not exist and should return an error'
|
||||
);
|
||||
|
||||
try {
|
||||
await contract.submitTransaction(
|
||||
'UpdateAsset',
|
||||
'asset70',
|
||||
'blue',
|
||||
'5',
|
||||
'Tomoko',
|
||||
'300'
|
||||
);
|
||||
console.log('******** FAILED to return an error');
|
||||
} catch (error) {
|
||||
console.log('*** Successfully caught the error: \n', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* envOrDefault() will return the value of an environment variable, or a default value if the variable is undefined.
|
||||
*/
|
||||
function envOrDefault(key, defaultValue) {
|
||||
return process.env[key] || defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* displayInputParameters() will print the global scope parameters used by the main driver routine.
|
||||
*/
|
||||
function displayInputParameters() {
|
||||
console.log(`channelName: ${channelName}`);
|
||||
console.log(`chaincodeName: ${chaincodeName}`);
|
||||
console.log(`mspId: ${mspId}`);
|
||||
console.log(`cryptoPath: ${cryptoPath}`);
|
||||
console.log(`keyDirectoryPath: ${keyDirectoryPath}`);
|
||||
console.log(`certDirectoryPath: ${certDirectoryPath}`);
|
||||
console.log(`tlsCertPath: ${tlsCertPath}`);
|
||||
console.log(`peerEndpoint: ${peerEndpoint}`);
|
||||
console.log(`peerHostAlias: ${peerHostAlias}`);
|
||||
}
|
||||
|
|
@ -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 +0,0 @@
|
|||
engine-strict=true
|
||||
|
|
@ -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 Basic 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,246 +0,0 @@
|
|||
/*
|
||||
* Copyright IBM Corp. All Rights Reserved.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as grpc from '@grpc/grpc-js';
|
||||
import { connect, Contract, Identity, Signer, signers } from '@hyperledger/fabric-gateway';
|
||||
import * as crypto from 'crypto';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as path from 'path';
|
||||
import { TextDecoder } from 'util';
|
||||
|
||||
const channelName = envOrDefault('CHANNEL_NAME', 'mychannel');
|
||||
const chaincodeName = envOrDefault('CHAINCODE_NAME', 'basic');
|
||||
const mspId = envOrDefault('MSP_ID', 'Org1MSP');
|
||||
|
||||
// Path to crypto materials.
|
||||
const cryptoPath = envOrDefault('CRYPTO_PATH', path.resolve(__dirname, '..', '..', '..', 'test-network', 'organizations', 'peerOrganizations', 'org1.example.com'));
|
||||
|
||||
// Path to user private key directory.
|
||||
const keyDirectoryPath = envOrDefault('KEY_DIRECTORY_PATH', path.resolve(cryptoPath, 'users', 'User1@org1.example.com', 'msp', 'keystore'));
|
||||
|
||||
// Path to user certificate directory.
|
||||
const certDirectoryPath = envOrDefault('CERT_DIRECTORY_PATH', path.resolve(cryptoPath, 'users', 'User1@org1.example.com', 'msp', 'signcerts'));
|
||||
|
||||
// Path to peer tls certificate.
|
||||
const tlsCertPath = envOrDefault('TLS_CERT_PATH', path.resolve(cryptoPath, 'peers', 'peer0.org1.example.com', 'tls', 'ca.crt'));
|
||||
|
||||
// Gateway peer endpoint.
|
||||
const peerEndpoint = envOrDefault('PEER_ENDPOINT', 'localhost:7051');
|
||||
|
||||
// Gateway peer SSL host name override.
|
||||
const peerHostAlias = envOrDefault('PEER_HOST_ALIAS', 'peer0.org1.example.com');
|
||||
|
||||
const utf8Decoder = new TextDecoder();
|
||||
const assetId = `asset${String(Date.now())}`;
|
||||
|
||||
async function main(): Promise<void> {
|
||||
displayInputParameters();
|
||||
|
||||
// The gRPC client connection should be shared by all Gateway connections to this endpoint.
|
||||
const client = await newGrpcConnection();
|
||||
|
||||
const gateway = connect({
|
||||
client,
|
||||
identity: await newIdentity(),
|
||||
signer: await newSigner(),
|
||||
// Default timeouts for different gRPC calls
|
||||
evaluateOptions: () => {
|
||||
return { deadline: Date.now() + 5000 }; // 5 seconds
|
||||
},
|
||||
endorseOptions: () => {
|
||||
return { deadline: Date.now() + 15000 }; // 15 seconds
|
||||
},
|
||||
submitOptions: () => {
|
||||
return { deadline: Date.now() + 5000 }; // 5 seconds
|
||||
},
|
||||
commitStatusOptions: () => {
|
||||
return { deadline: Date.now() + 60000 }; // 1 minute
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
// Get a network instance representing the channel where the smart contract is deployed.
|
||||
const network = gateway.getNetwork(channelName);
|
||||
|
||||
// Get the smart contract from the network.
|
||||
const contract = network.getContract(chaincodeName);
|
||||
|
||||
// Initialize a set of asset data on the ledger using the chaincode 'InitLedger' function.
|
||||
await initLedger(contract);
|
||||
|
||||
// Return all the current assets on the ledger.
|
||||
await getAllAssets(contract);
|
||||
|
||||
// Create a new asset on the ledger.
|
||||
await createAsset(contract);
|
||||
|
||||
// Update an existing asset asynchronously.
|
||||
await transferAssetAsync(contract);
|
||||
|
||||
// Get the asset details by assetID.
|
||||
await readAssetByID(contract);
|
||||
|
||||
// Update an asset which does not exist.
|
||||
await updateNonExistentAsset(contract)
|
||||
} finally {
|
||||
gateway.close();
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error: unknown) => {
|
||||
console.error('******** FAILED to run the application:', error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
||||
async function newGrpcConnection(): Promise<grpc.Client> {
|
||||
const tlsRootCert = await fs.readFile(tlsCertPath);
|
||||
const tlsCredentials = grpc.credentials.createSsl(tlsRootCert);
|
||||
return new grpc.Client(peerEndpoint, tlsCredentials, {
|
||||
'grpc.ssl_target_name_override': peerHostAlias,
|
||||
});
|
||||
}
|
||||
|
||||
async function newIdentity(): Promise<Identity> {
|
||||
const certPath = await getFirstDirFileName(certDirectoryPath);
|
||||
const credentials = await fs.readFile(certPath);
|
||||
return { mspId, credentials };
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
async function newSigner(): Promise<Signer> {
|
||||
const keyPath = await getFirstDirFileName(keyDirectoryPath);
|
||||
const privateKeyPem = await fs.readFile(keyPath);
|
||||
const privateKey = crypto.createPrivateKey(privateKeyPem);
|
||||
return signers.newPrivateKeySigner(privateKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* This type of transaction would typically only be run once by an application the first time it was started after its
|
||||
* initial deployment. A new version of the chaincode deployed later would likely not need to run an "init" function.
|
||||
*/
|
||||
async function initLedger(contract: Contract): Promise<void> {
|
||||
console.log('\n--> Submit Transaction: InitLedger, function creates the initial set of assets on the ledger');
|
||||
|
||||
await contract.submitTransaction('InitLedger');
|
||||
|
||||
console.log('*** Transaction committed successfully');
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a transaction to query ledger state.
|
||||
*/
|
||||
async function getAllAssets(contract: Contract): Promise<void> {
|
||||
console.log('\n--> Evaluate Transaction: GetAllAssets, function returns all the current assets on the ledger');
|
||||
|
||||
const resultBytes = await contract.evaluateTransaction('GetAllAssets');
|
||||
|
||||
const resultJson = utf8Decoder.decode(resultBytes);
|
||||
const result: unknown = JSON.parse(resultJson);
|
||||
console.log('*** Result:', result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a transaction synchronously, blocking until it has been committed to the ledger.
|
||||
*/
|
||||
async function createAsset(contract: Contract): Promise<void> {
|
||||
console.log('\n--> Submit Transaction: CreateAsset, creates new asset with ID, Color, Size, Owner and AppraisedValue arguments');
|
||||
|
||||
await contract.submitTransaction(
|
||||
'CreateAsset',
|
||||
assetId,
|
||||
'yellow',
|
||||
'5',
|
||||
'Tom',
|
||||
'1300',
|
||||
);
|
||||
|
||||
console.log('*** Transaction committed successfully');
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit transaction asynchronously, allowing the application to process the smart contract response (e.g. update a UI)
|
||||
* while waiting for the commit notification.
|
||||
*/
|
||||
async function transferAssetAsync(contract: Contract): Promise<void> {
|
||||
console.log('\n--> Async Submit Transaction: TransferAsset, updates existing asset owner');
|
||||
|
||||
const commit = await contract.submitAsync('TransferAsset', {
|
||||
arguments: [assetId, 'Saptha'],
|
||||
});
|
||||
const oldOwner = utf8Decoder.decode(commit.getResult());
|
||||
|
||||
console.log(`*** Successfully submitted transaction to transfer ownership from ${oldOwner} to Saptha`);
|
||||
console.log('*** Waiting for transaction commit');
|
||||
|
||||
const status = await commit.getStatus();
|
||||
if (!status.successful) {
|
||||
throw new Error(`Transaction ${status.transactionId} failed to commit with status code ${String(status.code)}`);
|
||||
}
|
||||
|
||||
console.log('*** Transaction committed successfully');
|
||||
}
|
||||
|
||||
async function readAssetByID(contract: Contract): Promise<void> {
|
||||
console.log('\n--> Evaluate Transaction: ReadAsset, function returns asset attributes');
|
||||
|
||||
const resultBytes = await contract.evaluateTransaction('ReadAsset', assetId);
|
||||
|
||||
const resultJson = utf8Decoder.decode(resultBytes);
|
||||
const result: unknown = JSON.parse(resultJson);
|
||||
console.log('*** Result:', result);
|
||||
}
|
||||
|
||||
/**
|
||||
* submitTransaction() will throw an error containing details of any error responses from the smart contract.
|
||||
*/
|
||||
async function updateNonExistentAsset(contract: Contract): Promise<void>{
|
||||
console.log('\n--> Submit Transaction: UpdateAsset asset70, asset70 does not exist and should return an error');
|
||||
|
||||
try {
|
||||
await contract.submitTransaction(
|
||||
'UpdateAsset',
|
||||
'asset70',
|
||||
'blue',
|
||||
'5',
|
||||
'Tomoko',
|
||||
'300',
|
||||
);
|
||||
console.log('******** FAILED to return an error');
|
||||
} catch (error) {
|
||||
console.log('*** Successfully caught the error: \n', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* envOrDefault() will return the value of an environment variable, or a default value if the variable is undefined.
|
||||
*/
|
||||
function envOrDefault(key: string, defaultValue: string): string {
|
||||
return process.env[key] || defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* displayInputParameters() will print the global scope parameters used by the main driver routine.
|
||||
*/
|
||||
function displayInputParameters(): void {
|
||||
console.log(`channelName: ${channelName}`);
|
||||
console.log(`chaincodeName: ${chaincodeName}`);
|
||||
console.log(`mspId: ${mspId}`);
|
||||
console.log(`cryptoPath: ${cryptoPath}`);
|
||||
console.log(`keyDirectoryPath: ${keyDirectoryPath}`);
|
||||
console.log(`certDirectoryPath: ${certDirectoryPath}`);
|
||||
console.log(`tlsCertPath: ${tlsCertPath}`);
|
||||
console.log(`peerEndpoint: ${peerEndpoint}`);
|
||||
console.log(`peerHostAlias: ${peerHostAlias}`);
|
||||
}
|
||||
|
|
@ -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,5 +0,0 @@
|
|||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
coverage
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
env: {
|
||||
node: true,
|
||||
mocha: true,
|
||||
es6: true
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 8,
|
||||
sourceType: 'script'
|
||||
},
|
||||
extends: "eslint:recommended",
|
||||
rules: {
|
||||
indent: ['error', 4],
|
||||
'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-tabs': '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'],
|
||||
'no-constant-condition': ["error", { "checkLoops": false }]
|
||||
}
|
||||
};
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Report cache used by istanbul
|
||||
.nyc_output
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
package-lock.json
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
/*
|
||||
* Copyright IBM Corp. All Rights Reserved.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assetTransfer = require('./lib/assetTransfer');
|
||||
|
||||
module.exports.AssetTransfer = assetTransfer;
|
||||
module.exports.contracts = [assetTransfer];
|
||||
|
|
@ -1,167 +0,0 @@
|
|||
/*
|
||||
* Copyright IBM Corp. All Rights Reserved.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
// Deterministic JSON.stringify()
|
||||
const stringify = require('json-stringify-deterministic');
|
||||
const sortKeysRecursive = require('sort-keys-recursive');
|
||||
const { Contract } = require('fabric-contract-api');
|
||||
|
||||
class AssetTransfer extends Contract {
|
||||
|
||||
async InitLedger(ctx) {
|
||||
const assets = [
|
||||
{
|
||||
ID: 'asset1',
|
||||
Color: 'blue',
|
||||
Size: 5,
|
||||
Owner: 'Tomoko',
|
||||
AppraisedValue: 300,
|
||||
},
|
||||
{
|
||||
ID: 'asset2',
|
||||
Color: 'red',
|
||||
Size: 5,
|
||||
Owner: 'Brad',
|
||||
AppraisedValue: 400,
|
||||
},
|
||||
{
|
||||
ID: 'asset3',
|
||||
Color: 'green',
|
||||
Size: 10,
|
||||
Owner: 'Jin Soo',
|
||||
AppraisedValue: 500,
|
||||
},
|
||||
{
|
||||
ID: 'asset4',
|
||||
Color: 'yellow',
|
||||
Size: 10,
|
||||
Owner: 'Max',
|
||||
AppraisedValue: 600,
|
||||
},
|
||||
{
|
||||
ID: 'asset5',
|
||||
Color: 'black',
|
||||
Size: 15,
|
||||
Owner: 'Adriana',
|
||||
AppraisedValue: 700,
|
||||
},
|
||||
{
|
||||
ID: 'asset6',
|
||||
Color: 'white',
|
||||
Size: 15,
|
||||
Owner: 'Michel',
|
||||
AppraisedValue: 800,
|
||||
},
|
||||
];
|
||||
|
||||
for (const asset of assets) {
|
||||
asset.docType = 'asset';
|
||||
// example of how to write to world state deterministically
|
||||
// use convetion of alphabetic order
|
||||
// we insert data in alphabetic order using 'json-stringify-deterministic' and 'sort-keys-recursive'
|
||||
// when retrieving data, in any lang, the order of data will be the same and consequently also the corresonding hash
|
||||
await ctx.stub.putState(asset.ID, Buffer.from(stringify(sortKeysRecursive(asset))));
|
||||
}
|
||||
}
|
||||
|
||||
// CreateAsset issues a new asset to the world state with given details.
|
||||
async CreateAsset(ctx, id, color, size, owner, appraisedValue) {
|
||||
const exists = await this.AssetExists(ctx, id);
|
||||
if (exists) {
|
||||
throw new Error(`The asset ${id} already exists`);
|
||||
}
|
||||
|
||||
const asset = {
|
||||
ID: id,
|
||||
Color: color,
|
||||
Size: size,
|
||||
Owner: owner,
|
||||
AppraisedValue: appraisedValue,
|
||||
};
|
||||
// we insert data in alphabetic order using 'json-stringify-deterministic' and 'sort-keys-recursive'
|
||||
await ctx.stub.putState(id, Buffer.from(stringify(sortKeysRecursive(asset))));
|
||||
return JSON.stringify(asset);
|
||||
}
|
||||
|
||||
// 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(`The asset ${id} does not exist`);
|
||||
}
|
||||
return assetJSON.toString();
|
||||
}
|
||||
|
||||
// UpdateAsset updates an existing asset in the world state with provided parameters.
|
||||
async UpdateAsset(ctx, id, color, size, owner, appraisedValue) {
|
||||
const exists = await this.AssetExists(ctx, id);
|
||||
if (!exists) {
|
||||
throw new Error(`The asset ${id} does not exist`);
|
||||
}
|
||||
|
||||
// overwriting original asset with new asset
|
||||
const updatedAsset = {
|
||||
ID: id,
|
||||
Color: color,
|
||||
Size: size,
|
||||
Owner: owner,
|
||||
AppraisedValue: appraisedValue,
|
||||
};
|
||||
// we insert data in alphabetic order using 'json-stringify-deterministic' and 'sort-keys-recursive'
|
||||
return ctx.stub.putState(id, Buffer.from(stringify(sortKeysRecursive(updatedAsset))));
|
||||
}
|
||||
|
||||
// DeleteAsset deletes an given asset from the world state.
|
||||
async DeleteAsset(ctx, id) {
|
||||
const exists = await this.AssetExists(ctx, id);
|
||||
if (!exists) {
|
||||
throw new Error(`The asset ${id} does not exist`);
|
||||
}
|
||||
return ctx.stub.deleteState(id);
|
||||
}
|
||||
|
||||
// AssetExists returns true when asset with given ID exists in world state.
|
||||
async AssetExists(ctx, id) {
|
||||
const assetJSON = await ctx.stub.getState(id);
|
||||
return assetJSON && assetJSON.length > 0;
|
||||
}
|
||||
|
||||
// TransferAsset updates the owner field of asset with given id in the world state.
|
||||
async TransferAsset(ctx, id, newOwner) {
|
||||
const assetString = await this.ReadAsset(ctx, id);
|
||||
const asset = JSON.parse(assetString);
|
||||
const oldOwner = asset.Owner;
|
||||
asset.Owner = newOwner;
|
||||
// we insert data in alphabetic order using 'json-stringify-deterministic' and 'sort-keys-recursive'
|
||||
await ctx.stub.putState(id, Buffer.from(stringify(sortKeysRecursive(asset))));
|
||||
return oldOwner;
|
||||
}
|
||||
|
||||
// GetAllAssets returns all assets found in the world state.
|
||||
async GetAllAssets(ctx) {
|
||||
const allResults = [];
|
||||
// range query with empty string for startKey and endKey does an open-ended query of all assets in the chaincode namespace.
|
||||
const iterator = await ctx.stub.getStateByRange('', '');
|
||||
let result = await iterator.next();
|
||||
while (!result.done) {
|
||||
const strValue = Buffer.from(result.value.value.toString()).toString('utf8');
|
||||
let record;
|
||||
try {
|
||||
record = JSON.parse(strValue);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
record = strValue;
|
||||
}
|
||||
allResults.push(record);
|
||||
result = await iterator.next();
|
||||
}
|
||||
return JSON.stringify(allResults);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AssetTransfer;
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,50 +0,0 @@
|
|||
{
|
||||
"name": "asset-transfer-basic",
|
||||
"version": "1.0.0",
|
||||
"description": "Asset-Transfer-Basic contract implemented in JavaScript",
|
||||
"main": "index.js",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint *.js */**.js",
|
||||
"pretest": "npm run lint",
|
||||
"test": "nyc mocha --recursive",
|
||||
"start": "fabric-chaincode-node start"
|
||||
},
|
||||
"engineStrict": true,
|
||||
"author": "Hyperledger",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"fabric-contract-api": "~2.5",
|
||||
"fabric-shim": "~2.5",
|
||||
"json-stringify-deterministic": "^1.0.0",
|
||||
"sort-keys-recursive": "^2.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^4.4.1",
|
||||
"eslint": "^8.57.0",
|
||||
"mocha": "^10.4.0",
|
||||
"nyc": "^15.1.0",
|
||||
"sinon": "^18.0.0",
|
||||
"sinon-chai": "^3.7.0"
|
||||
},
|
||||
"nyc": {
|
||||
"exclude": [
|
||||
"coverage/**",
|
||||
"test/**",
|
||||
"index.js",
|
||||
".eslintrc.js"
|
||||
],
|
||||
"reporter": [
|
||||
"text-summary",
|
||||
"html"
|
||||
],
|
||||
"all": true,
|
||||
"check-coverage": true,
|
||||
"statements": 100,
|
||||
"branches": 100,
|
||||
"functions": 100,
|
||||
"lines": 100
|
||||
}
|
||||
}
|
||||
|
|
@ -1,268 +0,0 @@
|
|||
/*
|
||||
* Copyright IBM Corp. All Rights Reserved.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
const sinon = require('sinon');
|
||||
const chai = require('chai');
|
||||
const sinonChai = require('sinon-chai');
|
||||
const expect = chai.expect;
|
||||
|
||||
const { Context } = require('fabric-contract-api');
|
||||
const { ChaincodeStub } = require('fabric-shim');
|
||||
|
||||
const AssetTransfer = require('../lib/assetTransfer.js');
|
||||
|
||||
let assert = sinon.assert;
|
||||
chai.use(sinonChai);
|
||||
|
||||
describe('Asset Transfer Basic Tests', () => {
|
||||
let transactionContext, chaincodeStub, asset;
|
||||
beforeEach(() => {
|
||||
transactionContext = new Context();
|
||||
|
||||
chaincodeStub = sinon.createStubInstance(ChaincodeStub);
|
||||
transactionContext.setChaincodeStub(chaincodeStub);
|
||||
|
||||
chaincodeStub.putState.callsFake((key, value) => {
|
||||
if (!chaincodeStub.states) {
|
||||
chaincodeStub.states = {};
|
||||
}
|
||||
chaincodeStub.states[key] = value;
|
||||
});
|
||||
|
||||
chaincodeStub.getState.callsFake(async (key) => {
|
||||
let ret;
|
||||
if (chaincodeStub.states) {
|
||||
ret = chaincodeStub.states[key];
|
||||
}
|
||||
return Promise.resolve(ret);
|
||||
});
|
||||
|
||||
chaincodeStub.deleteState.callsFake(async (key) => {
|
||||
if (chaincodeStub.states) {
|
||||
delete chaincodeStub.states[key];
|
||||
}
|
||||
return Promise.resolve(key);
|
||||
});
|
||||
|
||||
chaincodeStub.getStateByRange.callsFake(async () => {
|
||||
function* internalGetStateByRange() {
|
||||
if (chaincodeStub.states) {
|
||||
// Shallow copy
|
||||
const copied = Object.assign({}, chaincodeStub.states);
|
||||
|
||||
for (let key in copied) {
|
||||
yield {value: copied[key]};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve(internalGetStateByRange());
|
||||
});
|
||||
|
||||
asset = {
|
||||
ID: 'asset1',
|
||||
Color: 'blue',
|
||||
Size: 5,
|
||||
Owner: 'Tomoko',
|
||||
AppraisedValue: 300,
|
||||
};
|
||||
});
|
||||
|
||||
describe('Test InitLedger', () => {
|
||||
it('should return error on InitLedger', async () => {
|
||||
chaincodeStub.putState.rejects('failed inserting key');
|
||||
let assetTransfer = new AssetTransfer();
|
||||
try {
|
||||
await assetTransfer.InitLedger(transactionContext);
|
||||
assert.fail('InitLedger should have failed');
|
||||
} catch (err) {
|
||||
expect(err.name).to.equal('failed inserting key');
|
||||
}
|
||||
});
|
||||
|
||||
it('should return success on InitLedger', async () => {
|
||||
let assetTransfer = new AssetTransfer();
|
||||
await assetTransfer.InitLedger(transactionContext);
|
||||
let ret = JSON.parse((await chaincodeStub.getState('asset1')).toString());
|
||||
expect(ret).to.eql(Object.assign({docType: 'asset'}, asset));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test CreateAsset', () => {
|
||||
it('should return error on CreateAsset', async () => {
|
||||
chaincodeStub.putState.rejects('failed inserting key');
|
||||
|
||||
let assetTransfer = new AssetTransfer();
|
||||
try {
|
||||
await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue);
|
||||
assert.fail('CreateAsset should have failed');
|
||||
} catch(err) {
|
||||
expect(err.name).to.equal('failed inserting key');
|
||||
}
|
||||
});
|
||||
|
||||
it('should return success on CreateAsset', async () => {
|
||||
let assetTransfer = new AssetTransfer();
|
||||
|
||||
await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue);
|
||||
|
||||
let ret = JSON.parse((await chaincodeStub.getState(asset.ID)).toString());
|
||||
expect(ret).to.eql(asset);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test ReadAsset', () => {
|
||||
it('should return error on ReadAsset', async () => {
|
||||
let assetTransfer = new AssetTransfer();
|
||||
await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue);
|
||||
|
||||
try {
|
||||
await assetTransfer.ReadAsset(transactionContext, 'asset2');
|
||||
assert.fail('ReadAsset should have failed');
|
||||
} catch (err) {
|
||||
expect(err.message).to.equal('The asset asset2 does not exist');
|
||||
}
|
||||
});
|
||||
|
||||
it('should return success on ReadAsset', async () => {
|
||||
let assetTransfer = new AssetTransfer();
|
||||
await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue);
|
||||
|
||||
let ret = JSON.parse(await chaincodeStub.getState(asset.ID));
|
||||
expect(ret).to.eql(asset);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test UpdateAsset', () => {
|
||||
it('should return error on UpdateAsset', async () => {
|
||||
let assetTransfer = new AssetTransfer();
|
||||
await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue);
|
||||
|
||||
try {
|
||||
await assetTransfer.UpdateAsset(transactionContext, 'asset2', 'orange', 10, 'Me', 500);
|
||||
assert.fail('UpdateAsset should have failed');
|
||||
} catch (err) {
|
||||
expect(err.message).to.equal('The asset asset2 does not exist');
|
||||
}
|
||||
});
|
||||
|
||||
it('should return success on UpdateAsset', async () => {
|
||||
let assetTransfer = new AssetTransfer();
|
||||
await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue);
|
||||
|
||||
await assetTransfer.UpdateAsset(transactionContext, 'asset1', 'orange', 10, 'Me', 500);
|
||||
let ret = JSON.parse(await chaincodeStub.getState(asset.ID));
|
||||
let expected = {
|
||||
ID: 'asset1',
|
||||
Color: 'orange',
|
||||
Size: 10,
|
||||
Owner: 'Me',
|
||||
AppraisedValue: 500
|
||||
};
|
||||
expect(ret).to.eql(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test DeleteAsset', () => {
|
||||
it('should return error on DeleteAsset', async () => {
|
||||
let assetTransfer = new AssetTransfer();
|
||||
await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue);
|
||||
|
||||
try {
|
||||
await assetTransfer.DeleteAsset(transactionContext, 'asset2');
|
||||
assert.fail('DeleteAsset should have failed');
|
||||
} catch (err) {
|
||||
expect(err.message).to.equal('The asset asset2 does not exist');
|
||||
}
|
||||
});
|
||||
|
||||
it('should return success on DeleteAsset', async () => {
|
||||
let assetTransfer = new AssetTransfer();
|
||||
await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue);
|
||||
|
||||
await assetTransfer.DeleteAsset(transactionContext, asset.ID);
|
||||
let ret = await chaincodeStub.getState(asset.ID);
|
||||
expect(ret).to.equal(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test TransferAsset', () => {
|
||||
it('should return error on TransferAsset', async () => {
|
||||
let assetTransfer = new AssetTransfer();
|
||||
await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue);
|
||||
|
||||
try {
|
||||
await assetTransfer.TransferAsset(transactionContext, 'asset2', 'Me');
|
||||
assert.fail('DeleteAsset should have failed');
|
||||
} catch (err) {
|
||||
expect(err.message).to.equal('The asset asset2 does not exist');
|
||||
}
|
||||
});
|
||||
|
||||
it('should return success on TransferAsset', async () => {
|
||||
let assetTransfer = new AssetTransfer();
|
||||
await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue);
|
||||
|
||||
await assetTransfer.TransferAsset(transactionContext, asset.ID, 'Me');
|
||||
let ret = JSON.parse((await chaincodeStub.getState(asset.ID)).toString());
|
||||
expect(ret).to.eql(Object.assign({}, asset, {Owner: 'Me'}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test GetAllAssets', () => {
|
||||
it('should return success on GetAllAssets', async () => {
|
||||
let assetTransfer = new AssetTransfer();
|
||||
|
||||
await assetTransfer.CreateAsset(transactionContext, 'asset1', 'blue', 5, 'Robert', 100);
|
||||
await assetTransfer.CreateAsset(transactionContext, 'asset2', 'orange', 10, 'Paul', 200);
|
||||
await assetTransfer.CreateAsset(transactionContext, 'asset3', 'red', 15, 'Troy', 300);
|
||||
await assetTransfer.CreateAsset(transactionContext, 'asset4', 'pink', 20, 'Van', 400);
|
||||
|
||||
let ret = await assetTransfer.GetAllAssets(transactionContext);
|
||||
ret = JSON.parse(ret);
|
||||
expect(ret.length).to.equal(4);
|
||||
|
||||
let expected = [
|
||||
{ID: 'asset1', Color: 'blue', Size: 5, Owner: 'Robert', AppraisedValue: 100},
|
||||
{ID: 'asset2', Color: 'orange', Size: 10, Owner: 'Paul', AppraisedValue: 200},
|
||||
{ID: 'asset3', Color: 'red', Size: 15, Owner: 'Troy', AppraisedValue: 300},
|
||||
{ID: 'asset4', Color: 'pink', Size: 20, Owner: 'Van', AppraisedValue: 400}
|
||||
];
|
||||
|
||||
expect(ret).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should return success on GetAllAssets for non JSON value', async () => {
|
||||
let assetTransfer = new AssetTransfer();
|
||||
|
||||
chaincodeStub.putState.onFirstCall().callsFake((key, value) => {
|
||||
if (!chaincodeStub.states) {
|
||||
chaincodeStub.states = {};
|
||||
}
|
||||
chaincodeStub.states[key] = 'non-json-value';
|
||||
});
|
||||
|
||||
await assetTransfer.CreateAsset(transactionContext, 'asset1', 'blue', 5, 'Robert', 100);
|
||||
await assetTransfer.CreateAsset(transactionContext, 'asset2', 'orange', 10, 'Paul', 200);
|
||||
await assetTransfer.CreateAsset(transactionContext, 'asset3', 'red', 15, 'Troy', 300);
|
||||
await assetTransfer.CreateAsset(transactionContext, 'asset4', 'pink', 20, 'Van', 400);
|
||||
|
||||
let ret = await assetTransfer.GetAllAssets(transactionContext);
|
||||
ret = JSON.parse(ret);
|
||||
expect(ret.length).to.equal(4);
|
||||
|
||||
let expected = [
|
||||
'non-json-value',
|
||||
{ID: 'asset2', Color: 'orange', Size: 10, Owner: 'Paul', AppraisedValue: 200},
|
||||
{ID: 'asset3', Color: 'red', Size: 15, Owner: 'Troy', AppraisedValue: 300},
|
||||
{ID: 'asset4', Color: 'pink', Size: 20, Owner: 'Van', AppraisedValue: 400}
|
||||
];
|
||||
|
||||
expect(ret).to.eql(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1 +0,0 @@
|
|||
node_modules
|
||||
|
|
@ -1,16 +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
|
||||
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
FROM node:16 AS builder
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Copy node.js source and build, changing owner as well
|
||||
COPY --chown=node:node . /usr/src/app
|
||||
ENV npm_config_cache=/usr/src/app
|
||||
RUN npm ci && npm run package
|
||||
|
||||
|
||||
FROM node:16 AS production
|
||||
ARG CC_SERVER_PORT
|
||||
|
||||
# Setup tini to work better handle signals
|
||||
ENV TINI_VERSION v0.19.0
|
||||
ENV PLATFORM=amd64
|
||||
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-${PLATFORM} /tini
|
||||
RUN chmod +x /tini
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
COPY --chown=node:node --from=builder /usr/src/app/dist ./dist
|
||||
COPY --chown=node:node --from=builder /usr/src/app/package.json ./
|
||||
COPY --chown=node:node --from=builder /usr/src/app/npm-shrinkwrap.json ./
|
||||
COPY --chown=node:node docker/docker-entrypoint.sh /usr/src/app/docker-entrypoint.sh
|
||||
|
||||
RUN npm ci --omit=dev && npm cache clean --force
|
||||
|
||||
ENV PORT $CC_SERVER_PORT
|
||||
EXPOSE $CC_SERVER_PORT
|
||||
ENV NODE_ENV=production
|
||||
|
||||
USER node
|
||||
ENTRYPOINT [ "/tini", "--", "/usr/src/app/docker-entrypoint.sh" ]
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
set -euo pipefail
|
||||
: ${CORE_PEER_TLS_ENABLED:="false"}
|
||||
: ${DEBUG:="false"}
|
||||
|
||||
if [ "${DEBUG,,}" = "true" ]; then
|
||||
npm run start:server-debug
|
||||
elif [ "${CORE_PEER_TLS_ENABLED,,}" = "true" ]; then
|
||||
npm run start:server
|
||||
else
|
||||
npm run start:server-nontls
|
||||
fi
|
||||
|
||||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,41 +0,0 @@
|
|||
{
|
||||
"name": "asset-transfer-basic",
|
||||
"version": "1.0.0",
|
||||
"description": "Asset Transfer Basic contract implemented in TypeScript",
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint src",
|
||||
"pretest": "npm run lint",
|
||||
"test": "",
|
||||
"start": "set -x && fabric-chaincode-node start",
|
||||
"build": "tsc",
|
||||
"build:watch": "tsc -w",
|
||||
"prepublishOnly": "npm run build",
|
||||
"docker": "docker build -f ./Dockerfile -t asset-transfer-basic .",
|
||||
"package": "npm run build && npm shrinkwrap",
|
||||
"start:server-nontls": "set -x && fabric-chaincode-node server --chaincode-address=$CHAINCODE_SERVER_ADDRESS --chaincode-id=$CHAINCODE_ID",
|
||||
"start:server-debug": "set -x && NODE_OPTIONS='--inspect=0.0.0.0:9229' fabric-chaincode-node server --chaincode-address=$CHAINCODE_SERVER_ADDRESS --chaincode-id=$CHAINCODE_ID",
|
||||
"start:server": "set -x && fabric-chaincode-node server --chaincode-address=$CHAINCODE_SERVER_ADDRESS --chaincode-id=$CHAINCODE_ID --chaincode-tls-key-file=/hyperledger/privatekey.pem --chaincode-tls-client-cacert-file=/hyperledger/rootcert.pem --chaincode-tls-cert-file=/hyperledger/cert.pem"
|
||||
},
|
||||
"engineStrict": true,
|
||||
"author": "Hyperledger",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"fabric-contract-api": "~2.5",
|
||||
"fabric-shim": "~2.5",
|
||||
"json-stringify-deterministic": "^1.0.0",
|
||||
"sort-keys-recursive": "^2.1.0"
|
||||
},
|
||||
"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,26 +0,0 @@
|
|||
/*
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {Object, Property} from 'fabric-contract-api';
|
||||
|
||||
@Object()
|
||||
export class Asset {
|
||||
@Property()
|
||||
public docType?: string;
|
||||
|
||||
@Property()
|
||||
public ID: string = '';
|
||||
|
||||
@Property()
|
||||
public Color: string = '';
|
||||
|
||||
@Property()
|
||||
public Size: number = 0;
|
||||
|
||||
@Property()
|
||||
public Owner: string = '';
|
||||
|
||||
@Property()
|
||||
public AppraisedValue: number = 0;
|
||||
}
|
||||
|
|
@ -1,173 +0,0 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
// Deterministic JSON.stringify()
|
||||
import {Context, Contract, Info, Returns, Transaction} from 'fabric-contract-api';
|
||||
import stringify from 'json-stringify-deterministic';
|
||||
import sortKeysRecursive from 'sort-keys-recursive';
|
||||
import {Asset} from './asset';
|
||||
|
||||
@Info({title: 'AssetTransfer', description: 'Smart contract for trading assets'})
|
||||
export class AssetTransferContract extends Contract {
|
||||
|
||||
@Transaction()
|
||||
public async InitLedger(ctx: Context): Promise<void> {
|
||||
const assets: Asset[] = [
|
||||
{
|
||||
ID: 'asset1',
|
||||
Color: 'blue',
|
||||
Size: 5,
|
||||
Owner: 'Tomoko',
|
||||
AppraisedValue: 300,
|
||||
},
|
||||
{
|
||||
ID: 'asset2',
|
||||
Color: 'red',
|
||||
Size: 5,
|
||||
Owner: 'Brad',
|
||||
AppraisedValue: 400,
|
||||
},
|
||||
{
|
||||
ID: 'asset3',
|
||||
Color: 'green',
|
||||
Size: 10,
|
||||
Owner: 'Jin Soo',
|
||||
AppraisedValue: 500,
|
||||
},
|
||||
{
|
||||
ID: 'asset4',
|
||||
Color: 'yellow',
|
||||
Size: 10,
|
||||
Owner: 'Max',
|
||||
AppraisedValue: 600,
|
||||
},
|
||||
{
|
||||
ID: 'asset5',
|
||||
Color: 'black',
|
||||
Size: 15,
|
||||
Owner: 'Adriana',
|
||||
AppraisedValue: 700,
|
||||
},
|
||||
{
|
||||
ID: 'asset6',
|
||||
Color: 'white',
|
||||
Size: 15,
|
||||
Owner: 'Michel',
|
||||
AppraisedValue: 800,
|
||||
},
|
||||
];
|
||||
|
||||
for (const asset of assets) {
|
||||
asset.docType = 'asset';
|
||||
// example of how to write to world state deterministically
|
||||
// use convetion of alphabetic order
|
||||
// we insert data in alphabetic order using 'json-stringify-deterministic' and 'sort-keys-recursive'
|
||||
// when retrieving data, in any lang, the order of data will be the same and consequently also the corresonding hash
|
||||
await ctx.stub.putState(asset.ID, Buffer.from(stringify(sortKeysRecursive(asset))));
|
||||
console.info(`Asset ${asset.ID} initialized`);
|
||||
}
|
||||
}
|
||||
|
||||
// CreateAsset issues a new asset to the world state with given details.
|
||||
@Transaction()
|
||||
public async CreateAsset(ctx: Context, id: string, color: string, size: number, owner: string, appraisedValue: number): Promise<void> {
|
||||
const exists = await this.AssetExists(ctx, id);
|
||||
if (exists) {
|
||||
throw new Error(`The asset ${id} already exists`);
|
||||
}
|
||||
|
||||
const asset = {
|
||||
ID: id,
|
||||
Color: color,
|
||||
Size: size,
|
||||
Owner: owner,
|
||||
AppraisedValue: appraisedValue,
|
||||
};
|
||||
// we insert data in alphabetic order using 'json-stringify-deterministic' and 'sort-keys-recursive'
|
||||
await ctx.stub.putState(id, Buffer.from(stringify(sortKeysRecursive(asset))));
|
||||
}
|
||||
|
||||
// ReadAsset returns the asset stored in the world state with given id.
|
||||
@Transaction(false)
|
||||
public async ReadAsset(ctx: Context, id: string): Promise<string> {
|
||||
const assetJSON = await ctx.stub.getState(id); // get the asset from chaincode state
|
||||
if (assetJSON.length === 0) {
|
||||
throw new Error(`The asset ${id} does not exist`);
|
||||
}
|
||||
return assetJSON.toString();
|
||||
}
|
||||
|
||||
// UpdateAsset updates an existing asset in the world state with provided parameters.
|
||||
@Transaction()
|
||||
public async UpdateAsset(ctx: Context, id: string, color: string, size: number, owner: string, appraisedValue: number): Promise<void> {
|
||||
const exists = await this.AssetExists(ctx, id);
|
||||
if (!exists) {
|
||||
throw new Error(`The asset ${id} does not exist`);
|
||||
}
|
||||
|
||||
// overwriting original asset with new asset
|
||||
const updatedAsset = {
|
||||
ID: id,
|
||||
Color: color,
|
||||
Size: size,
|
||||
Owner: owner,
|
||||
AppraisedValue: appraisedValue,
|
||||
};
|
||||
// we insert data in alphabetic order using 'json-stringify-deterministic' and 'sort-keys-recursive'
|
||||
return ctx.stub.putState(id, Buffer.from(stringify(sortKeysRecursive(updatedAsset))));
|
||||
}
|
||||
|
||||
// DeleteAsset deletes an given asset from the world state.
|
||||
@Transaction()
|
||||
public async DeleteAsset(ctx: Context, id: string): Promise<void> {
|
||||
const exists = await this.AssetExists(ctx, id);
|
||||
if (!exists) {
|
||||
throw new Error(`The asset ${id} does not exist`);
|
||||
}
|
||||
return ctx.stub.deleteState(id);
|
||||
}
|
||||
|
||||
// AssetExists returns true when asset with given ID exists in world state.
|
||||
@Transaction(false)
|
||||
@Returns('boolean')
|
||||
public async AssetExists(ctx: Context, id: string): Promise<boolean> {
|
||||
const assetJSON = await ctx.stub.getState(id);
|
||||
return assetJSON.length > 0;
|
||||
}
|
||||
|
||||
// TransferAsset updates the owner field of asset with given id in the world state, and returns the old owner.
|
||||
@Transaction()
|
||||
public async TransferAsset(ctx: Context, id: string, newOwner: string): Promise<string> {
|
||||
const assetString = await this.ReadAsset(ctx, id);
|
||||
const asset = JSON.parse(assetString) as Asset;
|
||||
const oldOwner = asset.Owner;
|
||||
asset.Owner = newOwner;
|
||||
// we insert data in alphabetic order using 'json-stringify-deterministic' and 'sort-keys-recursive'
|
||||
await ctx.stub.putState(id, Buffer.from(stringify(sortKeysRecursive(asset))));
|
||||
return oldOwner;
|
||||
}
|
||||
|
||||
// GetAllAssets returns all assets found in the world state.
|
||||
@Transaction(false)
|
||||
@Returns('string')
|
||||
public async GetAllAssets(ctx: Context): Promise<string> {
|
||||
const allResults = [];
|
||||
// range query with empty string for startKey and endKey does an open-ended query of all assets in the chaincode namespace.
|
||||
const iterator = await ctx.stub.getStateByRange('', '');
|
||||
let result = await iterator.next();
|
||||
while (!result.done) {
|
||||
const strValue = Buffer.from(result.value.value.toString()).toString('utf8');
|
||||
let record;
|
||||
try {
|
||||
record = JSON.parse(strValue) as Asset;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
record = strValue;
|
||||
}
|
||||
allResults.push(record);
|
||||
result = await iterator.next();
|
||||
}
|
||||
return JSON.stringify(allResults);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {type Contract} from 'fabric-contract-api';
|
||||
import {AssetTransferContract} from './assetTransfer';
|
||||
|
||||
export const contracts: typeof Contract[] = [AssetTransferContract];
|
||||
|
|
@ -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/"]
|
||||
}
|
||||
2
asset-transfer-basic/rest-api-go/.gitignore
vendored
2
asset-transfer-basic/rest-api-go/.gitignore
vendored
|
|
@ -1,2 +0,0 @@
|
|||
Requests.http
|
||||
rest-api-go
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
# Asset Transfer REST API Sample
|
||||
|
||||
This is a simple REST server written in golang with endpoints for chaincode invoke and query.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
- Setup fabric test network and deploy the asset transfer chaincode by [following this instructions](https://hyperledger-fabric.readthedocs.io/en/release-2.4/test_network.html).
|
||||
|
||||
- cd into rest-api-go directory
|
||||
- Download required dependencies using `go mod download`
|
||||
- Run `go run main.go` to run the REST server
|
||||
|
||||
## Sending Requests
|
||||
|
||||
Invoke endpoint accepts POST requests with chaincode function and arguments. Query endpoint accepts get requests with chaincode function and arguments.
|
||||
|
||||
Sample chaincode invoke for the "createAsset" function. Response will contain transaction ID for a successful invoke.
|
||||
|
||||
``` sh
|
||||
curl --request POST \
|
||||
--url http://localhost:3000/invoke \
|
||||
--header 'content-type: application/x-www-form-urlencoded' \
|
||||
--data = \
|
||||
--data channelid=mychannel \
|
||||
--data chaincodeid=basic \
|
||||
--data function=createAsset \
|
||||
--data args=Asset123 \
|
||||
--data args=yellow \
|
||||
--data args=54 \
|
||||
--data args=Tom \
|
||||
--data args=13005
|
||||
```
|
||||
Sample chaincode query for getting asset details.
|
||||
|
||||
``` sh
|
||||
curl --request GET \
|
||||
--url 'http://localhost:3000/query?channelid=mychannel&chaincodeid=basic&function=ReadAsset&args=Asset123'
|
||||
```
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
module rest-api-go
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/hyperledger/fabric-gateway v1.5.0
|
||||
google.golang.org/grpc v1.63.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/hyperledger/fabric-protos-go-apiv2 v0.3.3 // indirect
|
||||
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||
golang.org/x/crypto v0.22.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/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
)
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/hyperledger/fabric-gateway v1.5.0 h1:JChlqtJNm2479Q8YWJ6k8wwzOiu2IRrV3K8ErsQmdTU=
|
||||
github.com/hyperledger/fabric-gateway v1.5.0/go.mod h1:v13OkXAp7pKi4kh6P6epn27SyivRbljr8Gkfy8JlbtM=
|
||||
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/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
|
||||
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||
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/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo=
|
||||
go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
|
||||
google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
|
||||
google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"rest-api-go/web"
|
||||
)
|
||||
|
||||
func main() {
|
||||
//Initialize setup for Org1
|
||||
cryptoPath := "../../test-network/organizations/peerOrganizations/org1.example.com"
|
||||
orgConfig := web.OrgSetup{
|
||||
OrgName: "Org1",
|
||||
MSPID: "Org1MSP",
|
||||
CertPath: cryptoPath + "/users/User1@org1.example.com/msp/signcerts/cert.pem",
|
||||
KeyPath: cryptoPath + "/users/User1@org1.example.com/msp/keystore/",
|
||||
TLSCertPath: cryptoPath + "/peers/peer0.org1.example.com/tls/ca.crt",
|
||||
PeerEndpoint: "dns:///localhost:7051",
|
||||
GatewayPeer: "peer0.org1.example.com",
|
||||
}
|
||||
|
||||
orgSetup, err := web.Initialize(orgConfig)
|
||||
if err != nil {
|
||||
fmt.Println("Error initializing setup for Org1: ", err)
|
||||
}
|
||||
web.Serve(web.OrgSetup(*orgSetup))
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/hyperledger/fabric-gateway/pkg/client"
|
||||
)
|
||||
|
||||
// OrgSetup contains organization's config to interact with the network.
|
||||
type OrgSetup struct {
|
||||
OrgName string
|
||||
MSPID string
|
||||
CryptoPath string
|
||||
CertPath string
|
||||
KeyPath string
|
||||
TLSCertPath string
|
||||
PeerEndpoint string
|
||||
GatewayPeer string
|
||||
Gateway client.Gateway
|
||||
}
|
||||
|
||||
// Serve starts http web server.
|
||||
func Serve(setups OrgSetup) {
|
||||
http.HandleFunc("/query", setups.Query)
|
||||
http.HandleFunc("/invoke", setups.Invoke)
|
||||
fmt.Println("Listening (http://localhost:3000/)...")
|
||||
if err := http.ListenAndServe(":3000", nil); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/hyperledger/fabric-gateway/pkg/client"
|
||||
"github.com/hyperledger/fabric-gateway/pkg/identity"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
// Initialize the setup for the organization.
|
||||
func Initialize(setup OrgSetup) (*OrgSetup, error) {
|
||||
log.Printf("Initializing connection for %s...\n", setup.OrgName)
|
||||
clientConnection := setup.newGrpcConnection()
|
||||
id := setup.newIdentity()
|
||||
sign := setup.newSign()
|
||||
|
||||
gateway, err := client.Connect(
|
||||
id,
|
||||
client.WithSign(sign),
|
||||
client.WithClientConnection(clientConnection),
|
||||
client.WithEvaluateTimeout(5*time.Second),
|
||||
client.WithEndorseTimeout(15*time.Second),
|
||||
client.WithSubmitTimeout(5*time.Second),
|
||||
client.WithCommitStatusTimeout(1*time.Minute),
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
setup.Gateway = *gateway
|
||||
log.Println("Initialization complete")
|
||||
return &setup, nil
|
||||
}
|
||||
|
||||
// newGrpcConnection creates a gRPC connection to the Gateway server.
|
||||
func (setup OrgSetup) newGrpcConnection() *grpc.ClientConn {
|
||||
certificate, err := loadCertificate(setup.TLSCertPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
certPool := x509.NewCertPool()
|
||||
certPool.AddCert(certificate)
|
||||
transportCredentials := credentials.NewClientTLSFromCert(certPool, setup.GatewayPeer)
|
||||
|
||||
connection, err := grpc.NewClient(setup.PeerEndpoint, grpc.WithTransportCredentials(transportCredentials))
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to create gRPC connection: %w", err))
|
||||
}
|
||||
|
||||
return connection
|
||||
}
|
||||
|
||||
// newIdentity creates a client identity for this Gateway connection using an X.509 certificate.
|
||||
func (setup OrgSetup) newIdentity() *identity.X509Identity {
|
||||
certificate, err := loadCertificate(setup.CertPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
id, err := identity.NewX509Identity(setup.MSPID, certificate)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
// newSign creates a function that generates a digital signature from a message digest using a private key.
|
||||
func (setup OrgSetup) newSign() identity.Sign {
|
||||
files, err := os.ReadDir(setup.KeyPath)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to read private key directory: %w", err))
|
||||
}
|
||||
privateKeyPEM, err := os.ReadFile(path.Join(setup.KeyPath, files[0].Name()))
|
||||
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to read private key file: %w", err))
|
||||
}
|
||||
|
||||
privateKey, err := identity.PrivateKeyFromPEM(privateKeyPEM)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
sign, err := identity.NewPrivateKeySign(privateKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return sign
|
||||
}
|
||||
|
||||
func loadCertificate(filename string) (*x509.Certificate, error) {
|
||||
certificatePEM, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read certificate file: %w", err)
|
||||
}
|
||||
return identity.CertificateFromPEM(certificatePEM)
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/hyperledger/fabric-gateway/pkg/client"
|
||||
)
|
||||
|
||||
// Invoke handles chaincode invoke requests.
|
||||
func (setup *OrgSetup) Invoke(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Println("Received Invoke request")
|
||||
if err := r.ParseForm(); err != nil {
|
||||
fmt.Fprintf(w, "ParseForm() err: %s", err)
|
||||
return
|
||||
}
|
||||
chainCodeName := r.FormValue("chaincodeid")
|
||||
channelID := r.FormValue("channelid")
|
||||
function := r.FormValue("function")
|
||||
args := r.Form["args"]
|
||||
fmt.Printf("channel: %s, chaincode: %s, function: %s, args: %s\n", channelID, chainCodeName, function, args)
|
||||
network := setup.Gateway.GetNetwork(channelID)
|
||||
contract := network.GetContract(chainCodeName)
|
||||
txn_proposal, err := contract.NewProposal(function, client.WithArguments(args...))
|
||||
if err != nil {
|
||||
fmt.Fprintf(w, "Error creating txn proposal: %s", err)
|
||||
return
|
||||
}
|
||||
txn_endorsed, err := txn_proposal.Endorse()
|
||||
if err != nil {
|
||||
fmt.Fprintf(w, "Error endorsing txn: %s", err)
|
||||
return
|
||||
}
|
||||
txn_committed, err := txn_endorsed.Submit()
|
||||
if err != nil {
|
||||
fmt.Fprintf(w, "Error submitting transaction: %s", err)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, "Transaction ID : %s Response: %s", txn_committed.TransactionID(), txn_endorsed.Result())
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Query handles chaincode query requests.
|
||||
func (setup OrgSetup) Query(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Println("Received Query request")
|
||||
queryParams := r.URL.Query()
|
||||
chainCodeName := queryParams.Get("chaincodeid")
|
||||
channelID := queryParams.Get("channelid")
|
||||
function := queryParams.Get("function")
|
||||
args := r.URL.Query()["args"]
|
||||
fmt.Printf("channel: %s, chaincode: %s, function: %s, args: %s\n", channelID, chainCodeName, function, args)
|
||||
network := setup.Gateway.GetNetwork(channelID)
|
||||
contract := network.GetContract(chainCodeName)
|
||||
evaluateResponse, err := contract.EvaluateTransaction(function, args...)
|
||||
if err != nil {
|
||||
fmt.Fprintf(w, "Error: %s", err)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, "Response: %s", evaluateResponse)
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
node_modules
|
||||
npm-debug.log
|
||||
Dockerfile
|
||||
.gitignore
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
[*.ts]
|
||||
indent_size = 4
|
||||
quote_type = single
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
# Sample .env file
|
||||
#
|
||||
# These are the minimum configuration variables required to start the sample
|
||||
#
|
||||
# See src/config.ts for details and for all the available configuration
|
||||
# variables
|
||||
#
|
||||
|
||||
HLF_CONNECTION_PROFILE_ORG1=
|
||||
|
||||
HLF_CERTIFICATE_ORG1=
|
||||
|
||||
HLF_PRIVATE_KEY_ORG1=
|
||||
|
||||
HLF_CONNECTION_PROFILE_ORG2=
|
||||
|
||||
HLF_CERTIFICATE_ORG2=
|
||||
|
||||
HLF_PRIVATE_KEY_ORG2=
|
||||
|
||||
ORG1_APIKEY=
|
||||
|
||||
ORG2_APIKEY=
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
{
|
||||
"env": {
|
||||
"node": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["src/**/*.ts"],
|
||||
"parserOptions": {
|
||||
"project": ["./tsconfig.json"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
.env
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Compiled TypeScript files
|
||||
dist
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
FROM node:18-alpine3.14 AS build
|
||||
|
||||
RUN apk add --no-cache g++ make python3 dumb-init
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --chown=node:node . /app
|
||||
|
||||
RUN npm ci
|
||||
RUN npm run build
|
||||
RUN npm prune --production
|
||||
|
||||
FROM node:18-alpine3.14
|
||||
ENV NODE_ENV production
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=build /usr/bin/dumb-init /usr/bin/dumb-init
|
||||
COPY --chown=node:node --from=build /app .
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
USER node
|
||||
|
||||
ENTRYPOINT [ "dumb-init", "--", "npm", "run"]
|
||||
CMD ["start"]
|
||||
|
|
@ -1,253 +0,0 @@
|
|||
# Asset Transfer REST API Sample
|
||||
|
||||
Sample REST server to demonstrate good Fabric Node SDK practices.
|
||||
|
||||
The REST API is only intended to work with the [basic asset transfer example](https://github.com/hyperledger/fabric-samples/tree/main/asset-transfer-basic).
|
||||
|
||||
To install the basic asset transfer chaincode on a local Fabric network, follow the [Using the Fabric test network](https://hyperledger-fabric.readthedocs.io/en/release-2.4/test_network.html) tutorial. You need to go at least as far as the step where the ledger gets initialized with assets.
|
||||
|
||||
## Overview
|
||||
|
||||
The primary aim of this sample is to show how to write a long running client application using the Fabric Node SDK.
|
||||
In particular, client applications **should not** create new connections for every transaction.
|
||||
|
||||
The sample also demonstrates possible error handling approaches and handles multiple requests from multiple identities.
|
||||
|
||||
The following sections describe the structure of the sample, or [skip to the usage section](#usage) to try it out first.
|
||||
|
||||
### Fabric network connections
|
||||
|
||||
The sample creates two long lived connections to a Fabric network in order to submit and evaluate transactions using two different identities.
|
||||
|
||||
The connections are made when the application starts and are retained for the life of the REST server.
|
||||
|
||||
Related files:
|
||||
|
||||
- [src/fabric.ts](src/fabric.ts)
|
||||
All the sample code which interacts with the Fabric network via the Fabric SDK.
|
||||
For example, the `createGateway` function which connects to the Fabric network.
|
||||
- [src/index.ts](src/index.ts)
|
||||
The primary entry point for the sample.
|
||||
Connects to the Fabric network and starts the REST server.
|
||||
|
||||
### Error handling
|
||||
|
||||
In this sample submit transactions are retried if they fail with any error, **except** for errors from the smart contract, or duplicate transaction errors.
|
||||
|
||||
Alternatively you might prefer to modify the sample to only retry transactions which fail with specific errors instead, for example:
|
||||
|
||||
- MVCC_READ_CONFLICT
|
||||
- PHANTOM_READ_CONFLICT
|
||||
- ENDORSEMENT_POLICY_FAILURE
|
||||
- CHAINCODE_VERSION_CONFLICT
|
||||
- EXPIRED_CHAINCODE
|
||||
|
||||
Related files:
|
||||
- [src/errors.ts](src/errors.ts)
|
||||
All the Fabric transaction error handling and retry logic.
|
||||
|
||||
### Asset REST API
|
||||
|
||||
While the basic asset transfer chaincode maps well to an `/api/assets` REST API, response times when submitting transactions to a Fabric network are problematic for REST APIs.
|
||||
|
||||
A common approach to handle long running operations in REST APIs is to immediately return `202 ACCEPTED`, with the operation being represented by another resource, namely a `job` in this sample.
|
||||
|
||||
Jobs are used for submitting transactions to create, update, delete, or transfer an asset.
|
||||
The `202 ACCEPTED` response includes a `jobId` which can be used with the `/api/jobs` endpoint to get the results of the create, update, delete, or transfer request.
|
||||
|
||||
Jobs are not used to get assets, because evaluating transactions is typically much faster.
|
||||
|
||||
Related files:
|
||||
- [src/assets.router.ts](src/assets.router.ts)
|
||||
Defines the main `/api/assets` endpoint.
|
||||
- [src/fabric.ts](src/fabric.ts)
|
||||
All the sample code which interacts with the Fabric network via the Fabric SDK.
|
||||
- [src/jobs.router.ts](src/jobs.router.ts)
|
||||
Defines the `/api/jobs` endpoint for getting job status.
|
||||
- [src/jobs.ts](src/jobs.ts)
|
||||
Job queue implementation details.
|
||||
- [src/transactions.router.ts](src/transactions.router.ts)
|
||||
Defines the `/api/transactions` endpoint for getting transaction status.
|
||||
|
||||
**Note:** If you are not specifically interested in REST APIs, you should only need to look at the files in the [Fabric network connections](#fabric-network-connections) and [Error handling](#error-handling) sections above.
|
||||
|
||||
### REST server
|
||||
|
||||
The remaining sample files are related to the REST server aspects of the sample, rather than Fabric itself:
|
||||
|
||||
- [src/auth.ts](src/auth.ts)
|
||||
Basic API key authentication strategy used for the sample.
|
||||
- [src/config.ts](src/config.ts)
|
||||
Descriptions of all the available configuration environment variables.
|
||||
- [src/jobs.ts](src/jobs.ts)
|
||||
Job queue implementation details.
|
||||
- [src/logger.ts](src/logger.ts)
|
||||
Logging implementation details.
|
||||
- [src/redis.ts](src/redis.ts)
|
||||
Redis implementation details.
|
||||
- [src/server.ts](src/server.ts)
|
||||
Express server implementation details.
|
||||
|
||||
**Note:** If you are not specifically interested in REST APIs, you should only need to look at the files in the [Fabric network connections](#fabric-network-connections) and [Error handling](#error-handling) sections above.
|
||||
|
||||
## Usage
|
||||
|
||||
To build and start the sample REST server, you'll need to [download and install an LTS version of node](https://nodejs.org/en/download/)
|
||||
|
||||
Clone the `fabric-samples` repository and change to the `fabric-samples/asset-transfer-basic/rest-api-typescript` directory before running the following commands
|
||||
|
||||
**Note:** these instructions should work with the main branch of `fabric-samples`
|
||||
|
||||
Install dependencies
|
||||
|
||||
```shell
|
||||
npm install
|
||||
```
|
||||
|
||||
Build the REST server
|
||||
|
||||
```shell
|
||||
npm run build
|
||||
```
|
||||
|
||||
Create a `.env` file to configure the server for the test network (make sure TEST_NETWORK_HOME is set to the fully qualified `test-network` directory)
|
||||
|
||||
```shell
|
||||
TEST_NETWORK_HOME=$HOME/fabric-samples/test-network npm run generateEnv
|
||||
```
|
||||
|
||||
**Note:** see [src/config.ts](src/config.ts) for details of configuring the sample
|
||||
|
||||
Start a Redis server (Redis is used to store the queue of submit transactions)
|
||||
|
||||
```shell
|
||||
export REDIS_PASSWORD=$(uuidgen)
|
||||
npm run start:redis
|
||||
```
|
||||
|
||||
Start the sample REST server
|
||||
|
||||
```shell
|
||||
npm run start:dev
|
||||
```
|
||||
|
||||
### Docker image
|
||||
|
||||
It's also possible to use the [published docker image](https://github.com/hyperledger/fabric-samples/pkgs/container/fabric-rest-sample) to run the sample
|
||||
|
||||
Clone the `fabric-samples` repository and change to the `fabric-samples/asset-transfer-basic/rest-api-typescript` directory before running the following commands
|
||||
|
||||
Create a `.env` file to configure the server for the test network (make sure `TEST_NETWORK_HOME` is set to the fully qualified `test-network` directory and `AS_LOCAL_HOST` is set to `false` so that the server works inside the Docker Compose network)
|
||||
|
||||
```shell
|
||||
TEST_NETWORK_HOME=$HOME/fabric-samples/test-network AS_LOCAL_HOST=false npm run generateEnv
|
||||
```
|
||||
|
||||
**Note:** see [src/config.ts](src/config.ts) for details of configuring the sample
|
||||
|
||||
Start the sample REST server and Redis server
|
||||
|
||||
```shell
|
||||
export REDIS_PASSWORD=$(uuidgen)
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## REST API demo
|
||||
|
||||
If everything went well, you can now open a new terminal and try out some basic asset transfer REST calls!
|
||||
|
||||
The examples below require a `SAMPLE_APIKEY` environment variable which must be set to an API key from the `.env` file created above.
|
||||
|
||||
For example, to use the ORG1_APIKEY...
|
||||
|
||||
```
|
||||
SAMPLE_APIKEY=$(grep ORG1_APIKEY .env | cut -d '=' -f 2-)
|
||||
```
|
||||
|
||||
### Get all assets...
|
||||
|
||||
```shell
|
||||
curl --header "X-Api-Key: ${SAMPLE_APIKEY}" http://localhost:3000/api/assets
|
||||
```
|
||||
|
||||
You should see all the available assets, for example
|
||||
|
||||
```
|
||||
[{"AppraisedValue":300,"Color":"blue","ID":"asset1","Owner":"Tomoko","Size":5},{"AppraisedValue":400,"Color":"red","ID":"asset2","Owner":"Brad","Size":5},{"AppraisedValue":500,"Color":"green","ID":"asset3","Owner":"Jin Soo","Size":10},{"AppraisedValue":600,"Color":"yellow","ID":"asset4","Owner":"Max","Size":10},{"AppraisedValue":700,"Color":"black","ID":"asset5","Owner":"Adriana","Size":15},{"AppraisedValue":800,"Color":"white","ID":"asset6","Owner":"Michel","Size":15}]
|
||||
```
|
||||
|
||||
### Check whether an asset exists...
|
||||
|
||||
```shell
|
||||
curl --include --header "X-Api-Key: ${SAMPLE_APIKEY}" --request OPTIONS http://localhost:3000/api/assets/asset7
|
||||
```
|
||||
|
||||
### Create an asset...
|
||||
|
||||
```shell
|
||||
curl --include --header "Content-Type: application/json" --header "X-Api-Key: ${SAMPLE_APIKEY}" --request POST --data '{"ID":"asset7","Color":"red","Size":42,"Owner":"Jean","AppraisedValue":101}' http://localhost:3000/api/assets
|
||||
```
|
||||
|
||||
The response should include a `jobId` which you can use to check the job status in next step
|
||||
|
||||
```
|
||||
{"status":"Accepted","jobId":"1","timestamp":"2021-10-22T16:27:09.426Z"}
|
||||
```
|
||||
|
||||
### Read job status...
|
||||
|
||||
```shell
|
||||
curl --header "X-Api-Key: ${SAMPLE_APIKEY}" http://localhost:3000/api/jobs/__job_id__
|
||||
```
|
||||
|
||||
The response should include a list of `transactionIds` which you can use to check the transaction status in next step, for example
|
||||
|
||||
```
|
||||
{"jobId":"1","transactionIds":["1dd35c2e5d840fec1dccc6e8cfce886c660c103de3e7b93dd774d04f39eef82a"],"transactionPayload":""}
|
||||
```
|
||||
|
||||
There may be more transaction IDs if the job was retried
|
||||
|
||||
### Read transaction status...
|
||||
|
||||
```shell
|
||||
curl --header "X-Api-Key: ${SAMPLE_APIKEY}" http://localhost:3000/api/transactions/__transaction_id__
|
||||
```
|
||||
|
||||
The response will show the validation code of the transaction, for example
|
||||
|
||||
```
|
||||
{"transactionId":"1dd35c2e5d840fec1dccc6e8cfce886c660c103de3e7b93dd774d04f39eef82a","validationCode":"VALID"}
|
||||
```
|
||||
|
||||
Alternatively, you will get a 404 not found response if the transaction was not committed
|
||||
|
||||
### Read an asset...
|
||||
|
||||
```shell
|
||||
curl --header "X-Api-Key: ${SAMPLE_APIKEY}" http://localhost:3000/api/assets/asset7
|
||||
```
|
||||
|
||||
You should see the newly created asset, for example
|
||||
|
||||
```
|
||||
{"AppraisedValue":101,"Color":"red","ID":"asset7","Owner":"Jean","Size":42}
|
||||
```
|
||||
|
||||
### Update an asset...
|
||||
|
||||
```shell
|
||||
curl --include --header "Content-Type: application/json" --header "X-Api-Key: ${SAMPLE_APIKEY}" --request PUT --data '{"ID":"asset7","Color":"red","Size":11,"Owner":"Jean","AppraisedValue":101}' http://localhost:3000/api/assets/asset7
|
||||
```
|
||||
|
||||
### Transfer an asset...
|
||||
|
||||
```shell
|
||||
curl --include --header "Content-Type: application/json" --header "X-Api-Key: ${SAMPLE_APIKEY}" --request PATCH --data '[{"op":"replace","path":"/Owner","value":"Ashleigh"}]' http://localhost:3000/api/assets/asset7
|
||||
```
|
||||
|
||||
### Delete an asset...
|
||||
|
||||
```shell
|
||||
curl --include --header "X-Api-Key: ${SAMPLE_APIKEY}" --request DELETE http://localhost:3000/api/assets/asset7
|
||||
```
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
// Demo file for use with REST Client for Visual Studio Code
|
||||
// See https://github.com/Huachao/vscode-restclient
|
||||
//
|
||||
// Edit the values below to match your environment if required
|
||||
@hostname = localhost
|
||||
@port = {{$dotenv PORT}}
|
||||
@baseUrl = http://{{hostname}}:{{port}}
|
||||
@apiUrl = {{baseUrl}}/api
|
||||
@api-key = {{$dotenv ORG1_APIKEY}}
|
||||
|
||||
### Check the server is ready
|
||||
|
||||
GET {{baseUrl}}/ready HTTP/1.1
|
||||
|
||||
### Check the server is still live
|
||||
|
||||
GET {{baseUrl}}/live HTTP/1.1
|
||||
|
||||
### Get all assets
|
||||
|
||||
GET {{apiUrl}}/assets HTTP/1.1
|
||||
X-Api-Key: {{api-key}}
|
||||
|
||||
### Check if asset exists
|
||||
|
||||
OPTIONS {{apiUrl}}/assets/asset7 HTTP/1.1
|
||||
X-Api-Key: {{api-key}}
|
||||
|
||||
### Create asset
|
||||
|
||||
POST {{apiUrl}}/assets HTTP/1.1
|
||||
content-type: application/json
|
||||
X-Api-Key: {{api-key}}
|
||||
|
||||
{
|
||||
"ID": "asset7",
|
||||
"Color": "red",
|
||||
"Size": 42,
|
||||
"Owner": "Jean",
|
||||
"AppraisedValue": 101
|
||||
}
|
||||
|
||||
### Read job status
|
||||
|
||||
GET {{apiUrl}}/jobs/__job_id__ HTTP/1.1
|
||||
X-Api-Key: {{api-key}}
|
||||
|
||||
### Read transaction status
|
||||
|
||||
GET {{apiUrl}}/transactions/__transaction_id__ HTTP/1.1
|
||||
X-Api-Key: {{api-key}}
|
||||
|
||||
### Read asset
|
||||
|
||||
GET {{apiUrl}}/assets/asset7 HTTP/1.1
|
||||
X-Api-Key: {{api-key}}
|
||||
|
||||
### Update asset
|
||||
|
||||
PUT {{apiUrl}}/assets/asset7 HTTP/1.1
|
||||
content-type: application/json
|
||||
X-Api-Key: {{api-key}}
|
||||
|
||||
{
|
||||
"ID": "asset7",
|
||||
"Color": "red",
|
||||
"Size": 11,
|
||||
"Owner": "Jean",
|
||||
"AppraisedValue": 101
|
||||
}
|
||||
|
||||
### Transfer asset
|
||||
|
||||
PATCH {{apiUrl}}/assets/asset7 HTTP/1.1
|
||||
content-type: application/json
|
||||
X-Api-Key: {{api-key}}
|
||||
|
||||
[
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/Owner",
|
||||
"value": "Ashleigh"
|
||||
}
|
||||
]
|
||||
|
||||
### Delete asset
|
||||
|
||||
DELETE {{apiUrl}}/assets/asset7 HTTP/1.1
|
||||
X-Api-Key: {{api-key}}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
version: '3'
|
||||
# Replace network name with the fabric test-network name
|
||||
services:
|
||||
redis:
|
||||
image: 'redis'
|
||||
command: ['--maxmemory-policy','noeviction','--requirepass','${REDIS_PASSWORD}']
|
||||
ports:
|
||||
- 6379:6379
|
||||
networks:
|
||||
- fabric_test
|
||||
|
||||
nodeapp:
|
||||
image: 'ghcr.io/hyperledger/fabric-rest-sample:latest'
|
||||
command: ['start:dotenv']
|
||||
ports:
|
||||
- 3000:3000
|
||||
env_file:
|
||||
- ./.env
|
||||
environment:
|
||||
- REDIS_PASSWORD
|
||||
networks:
|
||||
- fabric_test
|
||||
|
||||
|
||||
networks:
|
||||
fabric_test:
|
||||
external: true
|
||||
|
|
@ -1,208 +0,0 @@
|
|||
/*
|
||||
* For a detailed explanation regarding each configuration property and type check, visit:
|
||||
* https://jestjs.io/docs/configuration
|
||||
*/
|
||||
|
||||
export default {
|
||||
// All imported modules in your tests should be mocked automatically
|
||||
// automock: false,
|
||||
|
||||
// Stop running tests after `n` failures
|
||||
// bail: 0,
|
||||
|
||||
// The directory where Jest should store its cached dependency information
|
||||
// cacheDirectory: "/private/var/folders/04/rqvxpdk52gvf1_qq9l8gt4d40000gn/T/jest_dx",
|
||||
|
||||
// Automatically clear mock calls and instances between every test
|
||||
clearMocks: true,
|
||||
|
||||
// Indicates whether the coverage information should be collected while executing the test
|
||||
collectCoverage: true,
|
||||
|
||||
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
||||
// collectCoverageFrom: undefined,
|
||||
|
||||
// The directory where Jest should output its coverage files
|
||||
coverageDirectory: 'coverage',
|
||||
|
||||
// An array of regexp pattern strings used to skip coverage collection
|
||||
// coveragePathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// Indicates which provider should be used to instrument code for coverage
|
||||
coverageProvider: 'v8',
|
||||
|
||||
// A list of reporter names that Jest uses when writing coverage reports
|
||||
// coverageReporters: [
|
||||
// "json",
|
||||
// "text",
|
||||
// "lcov",
|
||||
// "clover"
|
||||
// ],
|
||||
|
||||
// An object that configures minimum threshold enforcement for coverage results
|
||||
// coverageThreshold: undefined,
|
||||
|
||||
// A path to a custom dependency extractor
|
||||
// dependencyExtractor: undefined,
|
||||
|
||||
// Make calling deprecated APIs throw helpful error messages
|
||||
// errorOnDeprecated: false,
|
||||
|
||||
// Force coverage collection from ignored files using an array of glob patterns
|
||||
// forceCoverageMatch: [],
|
||||
|
||||
// A path to a module which exports an async function that is triggered once before all test suites
|
||||
// globalSetup: undefined,
|
||||
|
||||
// A path to a module which exports an async function that is triggered once after all test suites
|
||||
// globalTeardown: undefined,
|
||||
|
||||
// A set of global variables that need to be available in all test environments
|
||||
// globals: {},
|
||||
|
||||
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
|
||||
// maxWorkers: "50%",
|
||||
|
||||
// An array of directory names to be searched recursively up from the requiring module's location
|
||||
// moduleDirectories: [
|
||||
// "node_modules"
|
||||
// ],
|
||||
|
||||
// An array of file extensions your modules use
|
||||
// moduleFileExtensions: [
|
||||
// "js",
|
||||
// "jsx",
|
||||
// "ts",
|
||||
// "tsx",
|
||||
// "json",
|
||||
// "node"
|
||||
// ],
|
||||
|
||||
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
||||
// moduleNameMapper: {},
|
||||
|
||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||
// modulePathIgnorePatterns: [],
|
||||
|
||||
// Activates notifications for test results
|
||||
// notify: false,
|
||||
|
||||
// An enum that specifies notification mode. Requires { notify: true }
|
||||
// notifyMode: "failure-change",
|
||||
|
||||
// A preset that is used as a base for Jest's configuration
|
||||
preset: 'ts-jest',
|
||||
|
||||
// Run tests from one or more projects
|
||||
// projects: undefined,
|
||||
|
||||
// Use this configuration option to add custom reporters to Jest
|
||||
// reporters: undefined,
|
||||
|
||||
// Automatically reset mock state between every test
|
||||
// resetMocks: false,
|
||||
|
||||
// Reset the module registry before running each individual test
|
||||
// resetModules: false,
|
||||
|
||||
// A path to a custom resolver
|
||||
// resolver: undefined,
|
||||
|
||||
// Automatically restore mock state between every test
|
||||
// restoreMocks: false,
|
||||
|
||||
// The root directory that Jest should scan for tests and modules within
|
||||
// rootDir: undefined,
|
||||
|
||||
// A list of paths to directories that Jest should use to search for files in
|
||||
roots: ['<rootDir>/src'],
|
||||
|
||||
// Allows you to use a custom runner instead of Jest's default test runner
|
||||
// runner: "jest-runner",
|
||||
|
||||
// The paths to modules that run some code to configure or set up the testing environment before each test
|
||||
// setupFiles: [],
|
||||
|
||||
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
||||
// setupFilesAfterEnv: [],
|
||||
|
||||
// The number of seconds after which a test is considered as slow and reported as such in the results.
|
||||
// slowTestThreshold: 5,
|
||||
|
||||
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
||||
// snapshotSerializers: [],
|
||||
|
||||
// The test environment that will be used for testing
|
||||
// testEnvironment: "jest-environment-node",
|
||||
|
||||
// Options that will be passed to the testEnvironment
|
||||
// testEnvironmentOptions: {},
|
||||
|
||||
// Adds a location field to test results
|
||||
// testLocationInResults: false,
|
||||
|
||||
// The glob patterns Jest uses to detect test files
|
||||
testMatch: [
|
||||
// "**/__tests__/**/*.[jt]s?(x)",
|
||||
'**/?(*.)+(spec|test).[tj]s?(x)',
|
||||
],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||
// testPathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// The regexp pattern or array of patterns that Jest uses to detect test files
|
||||
// testRegex: [],
|
||||
|
||||
// This option allows the use of a custom results processor
|
||||
// testResultsProcessor: undefined,
|
||||
|
||||
// This option allows use of a custom test runner
|
||||
// testRunner: "jest-circus/runner",
|
||||
|
||||
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
|
||||
// testURL: "http://localhost",
|
||||
|
||||
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
|
||||
// timers: "real",
|
||||
|
||||
// A map from regular expressions to paths to transformers
|
||||
// transform: undefined,
|
||||
|
||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||
// transformIgnorePatterns: [
|
||||
// "/node_modules/",
|
||||
// "\\.pnp\\.[^\\/]+$"
|
||||
// ],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||
// unmockedModulePathPatterns: undefined,
|
||||
|
||||
// Indicates whether each individual test should be reported during the run
|
||||
// verbose: undefined,
|
||||
|
||||
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
|
||||
// watchPathIgnorePatterns: [],
|
||||
|
||||
// Whether to use watchman for file crawling
|
||||
// watchman: true,
|
||||
};
|
||||
|
||||
// Required environment variable values for the config.ts file
|
||||
process.env = Object.assign(process.env, {
|
||||
HLF_CONNECTION_PROFILE_ORG1: '{"name":"mock-profile-org1"}',
|
||||
HLF_CERTIFICATE_ORG1:
|
||||
'"-----BEGIN CERTIFICATE-----\\nMOCK\\n-----END CERTIFICATE-----\\n"',
|
||||
HLF_PRIVATE_KEY_ORG1:
|
||||
'"-----BEGIN PRIVATE KEY-----\\nMOCK\\n-----END PRIVATE KEY-----\\n"',
|
||||
HLF_CONNECTION_PROFILE_ORG2: '{"name":"mock-profile-org2"}',
|
||||
HLF_CERTIFICATE_ORG2:
|
||||
'"-----BEGIN CERTIFICATE-----\\nMOCK\\n-----END CERTIFICATE-----\\n"',
|
||||
HLF_PRIVATE_KEY_ORG2:
|
||||
'"-----BEGIN PRIVATE KEY-----\\nMOCK\\n-----END PRIVATE KEY-----\\n"',
|
||||
ORG1_APIKEY: 'ORG1MOCKAPIKEY',
|
||||
ORG2_APIKEY: 'ORG2MOCKAPIKEY',
|
||||
});
|
||||
7884
asset-transfer-basic/rest-api-typescript/package-lock.json
generated
7884
asset-transfer-basic/rest-api-typescript/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,70 +0,0 @@
|
|||
{
|
||||
"name": "asset-transfer-basic",
|
||||
"version": "1.0.0",
|
||||
"description": "Asset Transfer Basic REST API implemented in TypeScript",
|
||||
"main": "dist/index.js",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"dependencies": {
|
||||
"bullmq": "^1.47.2",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^10.0.0",
|
||||
"env-var": "^7.0.1",
|
||||
"express": "^4.18.2",
|
||||
"express-validator": "^6.12.0",
|
||||
"fabric-network": "^2.2.19",
|
||||
"helmet": "^4.6.0",
|
||||
"http-status-codes": "^2.1.4",
|
||||
"ioredis": "^4.27.8",
|
||||
"long": "^5.2.3",
|
||||
"passport": "^0.6.0",
|
||||
"passport-headerapikey": "^1.2.2",
|
||||
"pino": "^6.11.3",
|
||||
"pino-http": "^5.5.0",
|
||||
"source-map-support": "^0.5.19"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node12": "^12.1.0",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/express": "^4.17.12",
|
||||
"@types/ioredis": "^4.26.4",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/node": "^12.20.55",
|
||||
"@types/passport": "^1.0.7",
|
||||
"@types/pino": "^6.3.8",
|
||||
"@types/pino-http": "^5.4.1",
|
||||
"@types/supertest": "^2.0.11",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.0",
|
||||
"@typescript-eslint/parser": "^6.7.0",
|
||||
"eslint": "^8.49.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"ioredis-mock": "^5.6.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-mock-extended": "^3.0.5",
|
||||
"pino-pretty": "^5.0.2",
|
||||
"prettier": "^2.3.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"supertest": "^6.1.4",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "^10.1.0",
|
||||
"typescript": "~5.2.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prebuild": "npm run lint",
|
||||
"build": "tsc",
|
||||
"clean": "rimraf ./dist",
|
||||
"format": "prettier --write \"{src,test}/**/*.ts\"",
|
||||
"generateEnv": "./scripts/generateEnv.sh",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"start": "node --require source-map-support/register ./dist",
|
||||
"start:dotenv": "node --require source-map-support/register --require dotenv/config ./dist",
|
||||
"start:dev": "node --require source-map-support/register --require dotenv/config ./dist | pino-pretty",
|
||||
"start:redis": "docker run -p 6379:6379 --name fabric-sample-redis -d redis --maxmemory-policy noeviction --requirepass \"${REDIS_PASSWORD}\"",
|
||||
"test": "jest"
|
||||
},
|
||||
"author": "Hyperledger",
|
||||
"license": "Apache-2.0",
|
||||
"private": true
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
${AS_LOCAL_HOST:=true}
|
||||
|
||||
: "${TEST_NETWORK_HOME:=../..}"
|
||||
: "${CONNECTION_PROFILE_FILE_ORG1:=${TEST_NETWORK_HOME}/organizations/peerOrganizations/org1.example.com/connection-org1.json}"
|
||||
: "${CERTIFICATE_FILE_ORG1:=${TEST_NETWORK_HOME}/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/signcerts/User1@org1.example.com-cert.pem}"
|
||||
: "${PRIVATE_KEY_FILE_ORG1:=${TEST_NETWORK_HOME}/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/keystore/priv_sk}"
|
||||
|
||||
: "${CONNECTION_PROFILE_FILE_ORG2:=${TEST_NETWORK_HOME}/organizations/peerOrganizations/org2.example.com/connection-org2.json}"
|
||||
: "${CERTIFICATE_FILE_ORG2:=${TEST_NETWORK_HOME}/organizations/peerOrganizations/org2.example.com/users/User1@org2.example.com/msp/signcerts/User1@org2.example.com-cert.pem}"
|
||||
: "${PRIVATE_KEY_FILE_ORG2:=${TEST_NETWORK_HOME}/organizations/peerOrganizations/org2.example.com/users/User1@org2.example.com/msp/keystore/priv_sk}"
|
||||
|
||||
|
||||
cat << ENV_END > .env
|
||||
# Generated .env file
|
||||
# See src/config.ts for details of all the available configuration variables
|
||||
#
|
||||
|
||||
LOG_LEVEL=info
|
||||
|
||||
PORT=3000
|
||||
|
||||
HLF_CERTIFICATE_ORG1="$(cat ${CERTIFICATE_FILE_ORG1} | sed -e 's/$/\\n/' | tr -d '\r\n')"
|
||||
|
||||
HLF_PRIVATE_KEY_ORG1="$(cat ${PRIVATE_KEY_FILE_ORG1} | sed -e 's/$/\\n/' | tr -d '\r\n')"
|
||||
|
||||
HLF_CERTIFICATE_ORG2="$(cat ${CERTIFICATE_FILE_ORG2} | sed -e 's/$/\\n/' | tr -d '\r\n')"
|
||||
|
||||
HLF_PRIVATE_KEY_ORG2="$(cat ${PRIVATE_KEY_FILE_ORG2} | sed -e 's/$/\\n/' | tr -d '\r\n')"
|
||||
|
||||
REDIS_PORT=6379
|
||||
|
||||
ORG1_APIKEY=$(uuidgen)
|
||||
|
||||
ORG2_APIKEY=$(uuidgen)
|
||||
|
||||
ENV_END
|
||||
|
||||
if [ "${AS_LOCAL_HOST}" = "true" ]; then
|
||||
|
||||
cat << LOCAL_HOST_END >> .env
|
||||
AS_LOCAL_HOST=true
|
||||
|
||||
HLF_CONNECTION_PROFILE_ORG1=$(cat ${CONNECTION_PROFILE_FILE_ORG1} | jq -c .)
|
||||
|
||||
HLF_CONNECTION_PROFILE_ORG2=$(cat ${CONNECTION_PROFILE_FILE_ORG2} | jq -c .)
|
||||
|
||||
REDIS_HOST=localhost
|
||||
|
||||
LOCAL_HOST_END
|
||||
|
||||
elif [ "${AS_LOCAL_HOST}" = "false" ]; then
|
||||
|
||||
cat << WITH_HOSTNAME_END >> .env
|
||||
AS_LOCAL_HOST=false
|
||||
|
||||
HLF_CONNECTION_PROFILE_ORG1=$(cat ${CONNECTION_PROFILE_FILE_ORG1} | jq -c '.peers["peer0.org1.example.com"].url = "grpcs://peer0.org1.example.com:7051" | .certificateAuthorities["ca.org1.example.com"].url = "https://ca.org1.example.com:7054"')
|
||||
|
||||
HLF_CONNECTION_PROFILE_ORG2=$(cat ${CONNECTION_PROFILE_FILE_ORG2} | jq -c '.peers["peer0.org2.example.com"].url = "grpcs://peer0.org2.example.com:9051" | .certificateAuthorities["ca.org2.example.com"].url = "https://ca.org2.example.com:8054"')
|
||||
|
||||
REDIS_HOST=redis
|
||||
|
||||
WITH_HOSTNAME_END
|
||||
|
||||
fi
|
||||
|
|
@ -1,742 +0,0 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Job, Queue } from 'bullmq';
|
||||
import { Application } from 'express';
|
||||
import { Contract, Transaction } from 'fabric-network';
|
||||
import * as fabricProtos from 'fabric-protos';
|
||||
import { mock, MockProxy } from 'jest-mock-extended';
|
||||
import { jest } from '@jest/globals';
|
||||
import request from 'supertest';
|
||||
import * as config from '../config';
|
||||
import { createServer } from '../server';
|
||||
|
||||
jest.mock('../config');
|
||||
jest.mock('bullmq');
|
||||
|
||||
const mockAsset1 = {
|
||||
ID: 'asset1',
|
||||
Color: 'blue',
|
||||
Size: 5,
|
||||
Owner: 'Tomoko',
|
||||
AppraisedValue: 300,
|
||||
};
|
||||
const mockAsset1Buffer = Buffer.from(JSON.stringify(mockAsset1));
|
||||
|
||||
const mockAsset2 = {
|
||||
ID: 'asset2',
|
||||
Color: 'red',
|
||||
Size: 5,
|
||||
Owner: 'Brad',
|
||||
AppraisedValue: 400,
|
||||
};
|
||||
|
||||
const mockAllAssetsBuffer = Buffer.from(
|
||||
JSON.stringify([mockAsset1, mockAsset2])
|
||||
);
|
||||
|
||||
// TODO add tests for server errors
|
||||
describe('Asset Transfer Besic REST API', () => {
|
||||
let app: Application;
|
||||
let mockJobQueue: MockProxy<Queue>;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await createServer();
|
||||
|
||||
const mockJob = mock<Job>();
|
||||
mockJob.id = '1';
|
||||
mockJobQueue = mock<Queue>();
|
||||
mockJobQueue.add.mockResolvedValue(mockJob);
|
||||
app.locals.jobq = mockJobQueue;
|
||||
});
|
||||
|
||||
describe('/ready', () => {
|
||||
it('GET should respond with 200 OK json', async () => {
|
||||
const response = await request(app).get('/ready');
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(response.header).toHaveProperty(
|
||||
'content-type',
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
status: 'OK',
|
||||
timestamp: expect.any(String),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/live', () => {
|
||||
it('GET should respond with 200 OK json', async () => {
|
||||
const mockBlockchainInfoProto =
|
||||
fabricProtos.common.BlockchainInfo.create();
|
||||
mockBlockchainInfoProto.height = 42;
|
||||
const mockBlockchainInfoBuffer = Buffer.from(
|
||||
fabricProtos.common.BlockchainInfo.encode(
|
||||
mockBlockchainInfoProto
|
||||
).finish()
|
||||
);
|
||||
|
||||
const mockOrg1QsccContract = mock<Contract>();
|
||||
mockOrg1QsccContract.evaluateTransaction
|
||||
.calledWith('GetChainInfo')
|
||||
.mockResolvedValue(mockBlockchainInfoBuffer);
|
||||
app.locals[config.mspIdOrg1] = {
|
||||
qsccContract: mockOrg1QsccContract,
|
||||
};
|
||||
|
||||
const mockOrg2QsccContract = mock<Contract>();
|
||||
mockOrg2QsccContract.evaluateTransaction
|
||||
.calledWith('GetChainInfo')
|
||||
.mockResolvedValue(mockBlockchainInfoBuffer);
|
||||
app.locals[config.mspIdOrg2] = {
|
||||
qsccContract: mockOrg2QsccContract,
|
||||
};
|
||||
|
||||
const response = await request(app).get('/live');
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(response.header).toHaveProperty(
|
||||
'content-type',
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
status: 'OK',
|
||||
timestamp: expect.any(String),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/api/assets', () => {
|
||||
let mockGetAllAssetsTransaction: MockProxy<Transaction>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockGetAllAssetsTransaction = mock<Transaction>();
|
||||
const mockBasicContract = mock<Contract>();
|
||||
mockBasicContract.createTransaction
|
||||
.calledWith('GetAllAssets')
|
||||
.mockReturnValue(mockGetAllAssetsTransaction);
|
||||
app.locals[config.mspIdOrg1] = {
|
||||
assetContract: mockBasicContract,
|
||||
};
|
||||
});
|
||||
|
||||
it('GET should respond with 401 unauthorized json when an invalid API key is specified', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/assets')
|
||||
.set('X-Api-Key', 'NOTTHERIGHTAPIKEY');
|
||||
expect(response.statusCode).toEqual(401);
|
||||
expect(response.header).toHaveProperty(
|
||||
'content-type',
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
reason: 'NO_VALID_APIKEY',
|
||||
status: 'Unauthorized',
|
||||
timestamp: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('GET should respond with an empty json array when there are no assets', async () => {
|
||||
mockGetAllAssetsTransaction.evaluate.mockResolvedValue(
|
||||
Buffer.from('')
|
||||
);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/assets')
|
||||
.set('X-Api-Key', 'ORG1MOCKAPIKEY');
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(response.header).toHaveProperty(
|
||||
'content-type',
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(response.body).toEqual([]);
|
||||
});
|
||||
|
||||
it('GET should respond with json array of assets', async () => {
|
||||
mockGetAllAssetsTransaction.evaluate.mockResolvedValue(
|
||||
mockAllAssetsBuffer
|
||||
);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/assets')
|
||||
.set('X-Api-Key', 'ORG1MOCKAPIKEY');
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(response.header).toHaveProperty(
|
||||
'content-type',
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(response.body).toEqual([
|
||||
{
|
||||
ID: 'asset1',
|
||||
Color: 'blue',
|
||||
Size: 5,
|
||||
Owner: 'Tomoko',
|
||||
AppraisedValue: 300,
|
||||
},
|
||||
{
|
||||
ID: 'asset2',
|
||||
Color: 'red',
|
||||
Size: 5,
|
||||
Owner: 'Brad',
|
||||
AppraisedValue: 400,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('POST should respond with 401 unauthorized json when an invalid API key is specified', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/assets')
|
||||
.send({
|
||||
ID: 'asset6',
|
||||
Color: 'white',
|
||||
Size: 15,
|
||||
Owner: 'Michel',
|
||||
AppraisedValue: 800,
|
||||
})
|
||||
.set('X-Api-Key', 'NOTTHERIGHTAPIKEY');
|
||||
expect(response.statusCode).toEqual(401);
|
||||
expect(response.header).toHaveProperty(
|
||||
'content-type',
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
reason: 'NO_VALID_APIKEY',
|
||||
status: 'Unauthorized',
|
||||
timestamp: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('POST should respond with 400 bad request json for invalid asset json', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/assets')
|
||||
.send({
|
||||
wrongidfield: 'asset3',
|
||||
Color: 'red',
|
||||
Size: 5,
|
||||
Owner: 'Brad',
|
||||
AppraisedValue: 400,
|
||||
})
|
||||
.set('X-Api-Key', 'ORG1MOCKAPIKEY');
|
||||
expect(response.statusCode).toEqual(400);
|
||||
expect(response.header).toHaveProperty(
|
||||
'content-type',
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
status: 'Bad Request',
|
||||
reason: 'VALIDATION_ERROR',
|
||||
errors: [
|
||||
{
|
||||
location: 'body',
|
||||
msg: 'must be a string',
|
||||
param: 'ID',
|
||||
},
|
||||
],
|
||||
message: 'Invalid request body',
|
||||
timestamp: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('POST should respond with 202 accepted json', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/assets')
|
||||
.send({
|
||||
ID: 'asset3',
|
||||
Color: 'red',
|
||||
Size: 5,
|
||||
Owner: 'Brad',
|
||||
AppraisedValue: 400,
|
||||
})
|
||||
.set('X-Api-Key', 'ORG1MOCKAPIKEY');
|
||||
expect(response.statusCode).toEqual(202);
|
||||
expect(response.header).toHaveProperty(
|
||||
'content-type',
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
status: 'Accepted',
|
||||
jobId: '1',
|
||||
timestamp: expect.any(String),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/api/assets/:id', () => {
|
||||
let mockAssetExistsTransaction: MockProxy<Transaction>;
|
||||
let mockReadAssetTransaction: MockProxy<Transaction>;
|
||||
|
||||
beforeEach(() => {
|
||||
const mockBasicContract = mock<Contract>();
|
||||
|
||||
mockAssetExistsTransaction = mock<Transaction>();
|
||||
mockBasicContract.createTransaction
|
||||
.calledWith('AssetExists')
|
||||
.mockReturnValue(mockAssetExistsTransaction);
|
||||
|
||||
mockReadAssetTransaction = mock<Transaction>();
|
||||
mockBasicContract.createTransaction
|
||||
.calledWith('ReadAsset')
|
||||
.mockReturnValue(mockReadAssetTransaction);
|
||||
|
||||
app.locals[config.mspIdOrg1] = {
|
||||
assetContract: mockBasicContract,
|
||||
};
|
||||
});
|
||||
|
||||
it('OPTIONS should respond with 401 unauthorized json when an invalid API key is specified', async () => {
|
||||
const response = await request(app)
|
||||
.options('/api/assets/asset1')
|
||||
.set('X-Api-Key', 'NOTTHERIGHTAPIKEY');
|
||||
expect(response.statusCode).toEqual(401);
|
||||
expect(response.header).toHaveProperty(
|
||||
'content-type',
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
reason: 'NO_VALID_APIKEY',
|
||||
status: 'Unauthorized',
|
||||
timestamp: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('OPTIONS should respond with 404 not found json without the allow header when there is no asset with the specified ID', async () => {
|
||||
mockAssetExistsTransaction.evaluate
|
||||
.calledWith('asset3')
|
||||
.mockResolvedValue(Buffer.from('false'));
|
||||
|
||||
const response = await request(app)
|
||||
.options('/api/assets/asset3')
|
||||
.set('X-Api-Key', 'ORG1MOCKAPIKEY');
|
||||
expect(response.statusCode).toEqual(404);
|
||||
expect(response.header).toHaveProperty(
|
||||
'content-type',
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(response.header).not.toHaveProperty('allow');
|
||||
expect(response.body).toEqual({
|
||||
status: 'Not Found',
|
||||
timestamp: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('OPTIONS should respond with 200 OK json with the allow header', async () => {
|
||||
mockAssetExistsTransaction.evaluate
|
||||
.calledWith('asset1')
|
||||
.mockResolvedValue(Buffer.from('true'));
|
||||
|
||||
const response = await request(app)
|
||||
.options('/api/assets/asset1')
|
||||
.set('X-Api-Key', 'ORG1MOCKAPIKEY');
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(response.header).toHaveProperty(
|
||||
'content-type',
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(response.header).toHaveProperty(
|
||||
'allow',
|
||||
'DELETE,GET,OPTIONS,PATCH,PUT'
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
status: 'OK',
|
||||
timestamp: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('GET should respond with 401 unauthorized json when an invalid API key is specified', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/assets/asset1')
|
||||
.set('X-Api-Key', 'NOTTHERIGHTAPIKEY');
|
||||
expect(response.statusCode).toEqual(401);
|
||||
expect(response.header).toHaveProperty(
|
||||
'content-type',
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
reason: 'NO_VALID_APIKEY',
|
||||
status: 'Unauthorized',
|
||||
timestamp: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('GET should respond with 404 not found json when there is no asset with the specified ID', async () => {
|
||||
mockReadAssetTransaction.evaluate
|
||||
.calledWith('asset3')
|
||||
.mockRejectedValue(
|
||||
new Error('the asset asset3 does not exist')
|
||||
);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/assets/asset3')
|
||||
.set('X-Api-Key', 'ORG1MOCKAPIKEY');
|
||||
expect(response.statusCode).toEqual(404);
|
||||
expect(response.header).toHaveProperty(
|
||||
'content-type',
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
status: 'Not Found',
|
||||
timestamp: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('GET should respond with the asset json when the asset exists', async () => {
|
||||
mockReadAssetTransaction.evaluate
|
||||
.calledWith('asset1')
|
||||
.mockResolvedValue(mockAsset1Buffer);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/assets/asset1')
|
||||
.set('X-Api-Key', 'ORG1MOCKAPIKEY');
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(response.header).toHaveProperty(
|
||||
'content-type',
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
ID: 'asset1',
|
||||
Color: 'blue',
|
||||
Size: 5,
|
||||
Owner: 'Tomoko',
|
||||
AppraisedValue: 300,
|
||||
});
|
||||
});
|
||||
|
||||
it('PUT should respond with 401 unauthorized json when an invalid API key is specified', async () => {
|
||||
const response = await request(app)
|
||||
.put('/api/assets/asset1')
|
||||
.send({
|
||||
ID: 'asset3',
|
||||
Color: 'red',
|
||||
Size: 5,
|
||||
Owner: 'Brad',
|
||||
AppraisedValue: 400,
|
||||
})
|
||||
.set('X-Api-Key', 'NOTTHERIGHTAPIKEY');
|
||||
expect(response.statusCode).toEqual(401);
|
||||
expect(response.header).toHaveProperty(
|
||||
'content-type',
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
reason: 'NO_VALID_APIKEY',
|
||||
status: 'Unauthorized',
|
||||
timestamp: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('PUT should respond with 400 bad request json when IDs do not match', async () => {
|
||||
const response = await request(app)
|
||||
.put('/api/assets/asset1')
|
||||
.send({
|
||||
ID: 'asset2',
|
||||
Color: 'red',
|
||||
Size: 5,
|
||||
Owner: 'Brad',
|
||||
AppraisedValue: 400,
|
||||
})
|
||||
.set('X-Api-Key', 'ORG1MOCKAPIKEY');
|
||||
expect(response.statusCode).toEqual(400);
|
||||
expect(response.header).toHaveProperty(
|
||||
'content-type',
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
status: 'Bad Request',
|
||||
reason: 'ASSET_ID_MISMATCH',
|
||||
message: 'Asset IDs must match',
|
||||
timestamp: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('PUT should respond with 400 bad request json for invalid asset json', async () => {
|
||||
const response = await request(app)
|
||||
.put('/api/assets/asset1')
|
||||
.send({
|
||||
wrongID: 'asset1',
|
||||
Color: 'red',
|
||||
Size: 5,
|
||||
Owner: 'Brad',
|
||||
AppraisedValue: 400,
|
||||
})
|
||||
.set('X-Api-Key', 'ORG1MOCKAPIKEY');
|
||||
expect(response.statusCode).toEqual(400);
|
||||
expect(response.header).toHaveProperty(
|
||||
'content-type',
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
status: 'Bad Request',
|
||||
reason: 'VALIDATION_ERROR',
|
||||
errors: [
|
||||
{
|
||||
location: 'body',
|
||||
msg: 'must be a string',
|
||||
param: 'ID',
|
||||
},
|
||||
],
|
||||
message: 'Invalid request body',
|
||||
timestamp: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('PUT should respond with 202 accepted json', async () => {
|
||||
const response = await request(app)
|
||||
.put('/api/assets/asset1')
|
||||
.send({
|
||||
ID: 'asset1',
|
||||
Color: 'red',
|
||||
Size: 5,
|
||||
Owner: 'Brad',
|
||||
AppraisedValue: 400,
|
||||
})
|
||||
.set('X-Api-Key', 'ORG1MOCKAPIKEY');
|
||||
expect(response.statusCode).toEqual(202);
|
||||
expect(response.header).toHaveProperty(
|
||||
'content-type',
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
status: 'Accepted',
|
||||
jobId: '1',
|
||||
timestamp: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('PATCH should respond with 401 unauthorized json when an invalid API key is specified', async () => {
|
||||
const response = await request(app)
|
||||
.patch('/api/assets/asset1')
|
||||
.send([{ op: 'replace', path: '/Owner', value: 'Ashleigh' }])
|
||||
.set('X-Api-Key', 'NOTTHERIGHTAPIKEY');
|
||||
expect(response.statusCode).toEqual(401);
|
||||
expect(response.header).toHaveProperty(
|
||||
'content-type',
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
reason: 'NO_VALID_APIKEY',
|
||||
status: 'Unauthorized',
|
||||
timestamp: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('PATCH should respond with 400 bad request json for invalid patch op/path', async () => {
|
||||
const response = await request(app)
|
||||
.patch('/api/assets/asset1')
|
||||
.send([{ op: 'replace', path: '/color', value: 'orange' }])
|
||||
.set('X-Api-Key', 'ORG1MOCKAPIKEY');
|
||||
expect(response.statusCode).toEqual(400);
|
||||
expect(response.header).toHaveProperty(
|
||||
'content-type',
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
status: 'Bad Request',
|
||||
reason: 'VALIDATION_ERROR',
|
||||
errors: [
|
||||
{
|
||||
location: 'body',
|
||||
msg: "path must be '/Owner'",
|
||||
param: '[0].path',
|
||||
value: '/color',
|
||||
},
|
||||
],
|
||||
message: 'Invalid request body',
|
||||
timestamp: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('PATCH should respond with 202 accepted json', async () => {
|
||||
const response = await request(app)
|
||||
.patch('/api/assets/asset1')
|
||||
.send([{ op: 'replace', path: '/Owner', value: 'Ashleigh' }])
|
||||
.set('X-Api-Key', 'ORG1MOCKAPIKEY');
|
||||
expect(response.statusCode).toEqual(202);
|
||||
expect(response.header).toHaveProperty(
|
||||
'content-type',
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
status: 'Accepted',
|
||||
jobId: '1',
|
||||
timestamp: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('DELETE should respond with 401 unauthorized json when an invalid API key is specified', async () => {
|
||||
const response = await request(app)
|
||||
.delete('/api/assets/asset1')
|
||||
.set('X-Api-Key', 'NOTTHERIGHTAPIKEY');
|
||||
expect(response.statusCode).toEqual(401);
|
||||
expect(response.header).toHaveProperty(
|
||||
'content-type',
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
reason: 'NO_VALID_APIKEY',
|
||||
status: 'Unauthorized',
|
||||
timestamp: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('DELETE should respond with 202 accepted json', async () => {
|
||||
const response = await request(app)
|
||||
.delete('/api/assets/asset1')
|
||||
.set('X-Api-Key', 'ORG1MOCKAPIKEY');
|
||||
expect(response.statusCode).toEqual(202);
|
||||
expect(response.header).toHaveProperty(
|
||||
'content-type',
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
status: 'Accepted',
|
||||
jobId: '1',
|
||||
timestamp: expect.any(String),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/api/jobs/:id', () => {
|
||||
it('GET should respond with 401 unauthorized json when an invalid API key is specified', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/jobs/1')
|
||||
.set('X-Api-Key', 'NOTTHERIGHTAPIKEY');
|
||||
expect(response.statusCode).toEqual(401);
|
||||
expect(response.header).toHaveProperty(
|
||||
'content-type',
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
reason: 'NO_VALID_APIKEY',
|
||||
status: 'Unauthorized',
|
||||
timestamp: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('GET should respond with 404 not found json when there is no job with the specified ID', async () => {
|
||||
jest.mocked(Job.fromId).mockResolvedValue(undefined);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/jobs/3')
|
||||
.set('X-Api-Key', 'ORG1MOCKAPIKEY');
|
||||
expect(response.statusCode).toEqual(404);
|
||||
expect(response.header).toHaveProperty(
|
||||
'content-type',
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
status: 'Not Found',
|
||||
timestamp: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('GET should respond with json details for the specified job ID', async () => {
|
||||
const mockJob = mock<Job>();
|
||||
mockJob.id = '2';
|
||||
mockJob.data = {
|
||||
transactionIds: ['txn1', 'txn2'],
|
||||
};
|
||||
mockJob.returnvalue = {
|
||||
transactionError: 'Mock error',
|
||||
transactionPayload: Buffer.from('Mock payload'),
|
||||
};
|
||||
mockJobQueue.getJob.calledWith('2').mockResolvedValue(mockJob);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/jobs/2')
|
||||
.set('X-Api-Key', 'ORG1MOCKAPIKEY');
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(response.header).toHaveProperty(
|
||||
'content-type',
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
jobId: '2',
|
||||
transactionIds: ['txn1', 'txn2'],
|
||||
transactionError: 'Mock error',
|
||||
transactionPayload: 'Mock payload',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/api/transactions/:id', () => {
|
||||
let mockGetTransactionByIDTransaction: MockProxy<Transaction>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockGetTransactionByIDTransaction = mock<Transaction>();
|
||||
const mockQsccContract = mock<Contract>();
|
||||
mockQsccContract.createTransaction
|
||||
.calledWith('GetTransactionByID')
|
||||
.mockReturnValue(mockGetTransactionByIDTransaction);
|
||||
app.locals[config.mspIdOrg1] = {
|
||||
qsccContract: mockQsccContract,
|
||||
};
|
||||
});
|
||||
|
||||
it('GET should respond with 401 unauthorized json when an invalid API key is specified', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/transactions/txn1')
|
||||
.set('X-Api-Key', 'NOTTHERIGHTAPIKEY');
|
||||
expect(response.statusCode).toEqual(401);
|
||||
expect(response.header).toHaveProperty(
|
||||
'content-type',
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
reason: 'NO_VALID_APIKEY',
|
||||
status: 'Unauthorized',
|
||||
timestamp: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('GET should respond with 404 not found json when there is no transaction with the specified ID', async () => {
|
||||
mockGetTransactionByIDTransaction.evaluate
|
||||
.calledWith('mychannel', 'txn3')
|
||||
.mockRejectedValue(
|
||||
new Error(
|
||||
'Failed to get transaction with id txn3, error Entry not found in index'
|
||||
)
|
||||
);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/transactions/txn3')
|
||||
.set('X-Api-Key', 'ORG1MOCKAPIKEY');
|
||||
expect(response.statusCode).toEqual(404);
|
||||
expect(response.header).toHaveProperty(
|
||||
'content-type',
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
status: 'Not Found',
|
||||
timestamp: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('GET should respond with json details for the specified transaction ID', async () => {
|
||||
const processedTransactionProto =
|
||||
fabricProtos.protos.ProcessedTransaction.create();
|
||||
processedTransactionProto.validationCode =
|
||||
fabricProtos.protos.TxValidationCode.VALID;
|
||||
const processedTransactionBuffer = Buffer.from(
|
||||
fabricProtos.protos.ProcessedTransaction.encode(
|
||||
processedTransactionProto
|
||||
).finish()
|
||||
);
|
||||
mockGetTransactionByIDTransaction.evaluate
|
||||
.calledWith('mychannel', 'txn2')
|
||||
.mockResolvedValue(processedTransactionBuffer);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/transactions/txn2')
|
||||
.set('X-Api-Key', 'ORG1MOCKAPIKEY');
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(response.header).toHaveProperty(
|
||||
'content-type',
|
||||
'application/json; charset=utf-8'
|
||||
);
|
||||
expect(response.body).toEqual({
|
||||
transactionId: 'txn2',
|
||||
validationCode: 'VALID',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,353 +0,0 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* This sample is intended to work with the basic asset transfer
|
||||
* chaincode which imposes some constraints on what is possible here.
|
||||
*
|
||||
* For example,
|
||||
* - There is no validation for Asset IDs
|
||||
* - There are no error codes from the chaincode
|
||||
*
|
||||
* To avoid timeouts, long running tasks should be decoupled from HTTP request
|
||||
* processing
|
||||
*
|
||||
* Submit transactions can potentially be very long running, especially if the
|
||||
* transaction fails and needs to be retried one or more times
|
||||
*
|
||||
* To allow requests to respond quickly enough, this sample queues submit
|
||||
* requests for processing asynchronously and immediately returns 202 Accepted
|
||||
*/
|
||||
|
||||
import express, { Request, Response } from 'express';
|
||||
import { body, validationResult } from 'express-validator';
|
||||
import { Contract } from 'fabric-network';
|
||||
import { getReasonPhrase, StatusCodes } from 'http-status-codes';
|
||||
import { Queue } from 'bullmq';
|
||||
import { AssetNotFoundError } from './errors';
|
||||
import { evatuateTransaction } from './fabric';
|
||||
import { addSubmitTransactionJob } from './jobs';
|
||||
import { logger } from './logger';
|
||||
|
||||
const { ACCEPTED, BAD_REQUEST, INTERNAL_SERVER_ERROR, NOT_FOUND, OK } =
|
||||
StatusCodes;
|
||||
|
||||
export const assetsRouter = express.Router();
|
||||
|
||||
assetsRouter.get('/', async (req: Request, res: Response) => {
|
||||
logger.debug('Get all assets request received');
|
||||
try {
|
||||
const mspId = req.user as string;
|
||||
const contract = req.app.locals[mspId]?.assetContract as Contract;
|
||||
|
||||
const data = await evatuateTransaction(contract, 'GetAllAssets');
|
||||
let assets = [];
|
||||
if (data.length > 0) {
|
||||
assets = JSON.parse(data.toString());
|
||||
}
|
||||
|
||||
return res.status(OK).json(assets);
|
||||
} catch (err) {
|
||||
logger.error({ err }, 'Error processing get all assets request');
|
||||
return res.status(INTERNAL_SERVER_ERROR).json({
|
||||
status: getReasonPhrase(INTERNAL_SERVER_ERROR),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
assetsRouter.post(
|
||||
'/',
|
||||
body().isObject().withMessage('body must contain an asset object'),
|
||||
body('ID', 'must be a string').notEmpty(),
|
||||
body('Color', 'must be a string').notEmpty(),
|
||||
body('Size', 'must be a number').isNumeric(),
|
||||
body('Owner', 'must be a string').notEmpty(),
|
||||
body('AppraisedValue', 'must be a number').isNumeric(),
|
||||
async (req: Request, res: Response) => {
|
||||
logger.debug(req.body, 'Create asset request received');
|
||||
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(BAD_REQUEST).json({
|
||||
status: getReasonPhrase(BAD_REQUEST),
|
||||
reason: 'VALIDATION_ERROR',
|
||||
message: 'Invalid request body',
|
||||
timestamp: new Date().toISOString(),
|
||||
errors: errors.array(),
|
||||
});
|
||||
}
|
||||
|
||||
const mspId = req.user as string;
|
||||
const assetId = req.body.ID;
|
||||
|
||||
try {
|
||||
const submitQueue = req.app.locals.jobq as Queue;
|
||||
const jobId = await addSubmitTransactionJob(
|
||||
submitQueue,
|
||||
mspId,
|
||||
'CreateAsset',
|
||||
assetId,
|
||||
req.body.Color,
|
||||
req.body.Size,
|
||||
req.body.Owner,
|
||||
req.body.AppraisedValue
|
||||
);
|
||||
|
||||
return res.status(ACCEPTED).json({
|
||||
status: getReasonPhrase(ACCEPTED),
|
||||
jobId: jobId,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
{ err },
|
||||
'Error processing create asset request for asset ID %s',
|
||||
assetId
|
||||
);
|
||||
|
||||
return res.status(INTERNAL_SERVER_ERROR).json({
|
||||
status: getReasonPhrase(INTERNAL_SERVER_ERROR),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
assetsRouter.options('/:assetId', async (req: Request, res: Response) => {
|
||||
const assetId = req.params.assetId;
|
||||
logger.debug('Asset options request received for asset ID %s', assetId);
|
||||
|
||||
try {
|
||||
const mspId = req.user as string;
|
||||
const contract = req.app.locals[mspId]?.assetContract as Contract;
|
||||
|
||||
const data = await evatuateTransaction(
|
||||
contract,
|
||||
'AssetExists',
|
||||
assetId
|
||||
);
|
||||
const exists = data.toString() === 'true';
|
||||
|
||||
if (exists) {
|
||||
return res
|
||||
.status(OK)
|
||||
.set({
|
||||
Allow: 'DELETE,GET,OPTIONS,PATCH,PUT',
|
||||
})
|
||||
.json({
|
||||
status: getReasonPhrase(OK),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
return res.status(NOT_FOUND).json({
|
||||
status: getReasonPhrase(NOT_FOUND),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
{ err },
|
||||
'Error processing asset options request for asset ID %s',
|
||||
assetId
|
||||
);
|
||||
return res.status(INTERNAL_SERVER_ERROR).json({
|
||||
status: getReasonPhrase(INTERNAL_SERVER_ERROR),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
assetsRouter.get('/:assetId', async (req: Request, res: Response) => {
|
||||
const assetId = req.params.assetId;
|
||||
logger.debug('Read asset request received for asset ID %s', assetId);
|
||||
|
||||
try {
|
||||
const mspId = req.user as string;
|
||||
const contract = req.app.locals[mspId]?.assetContract as Contract;
|
||||
|
||||
const data = await evatuateTransaction(contract, 'ReadAsset', assetId);
|
||||
const asset = JSON.parse(data.toString());
|
||||
|
||||
return res.status(OK).json(asset);
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
{ err },
|
||||
'Error processing read asset request for asset ID %s',
|
||||
assetId
|
||||
);
|
||||
|
||||
if (err instanceof AssetNotFoundError) {
|
||||
return res.status(NOT_FOUND).json({
|
||||
status: getReasonPhrase(NOT_FOUND),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(INTERNAL_SERVER_ERROR).json({
|
||||
status: getReasonPhrase(INTERNAL_SERVER_ERROR),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
assetsRouter.put(
|
||||
'/:assetId',
|
||||
body().isObject().withMessage('body must contain an asset object'),
|
||||
body('ID', 'must be a string').notEmpty(),
|
||||
body('Color', 'must be a string').notEmpty(),
|
||||
body('Size', 'must be a number').isNumeric(),
|
||||
body('Owner', 'must be a string').notEmpty(),
|
||||
body('AppraisedValue', 'must be a number').isNumeric(),
|
||||
async (req: Request, res: Response) => {
|
||||
logger.debug(req.body, 'Update asset request received');
|
||||
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(BAD_REQUEST).json({
|
||||
status: getReasonPhrase(BAD_REQUEST),
|
||||
reason: 'VALIDATION_ERROR',
|
||||
message: 'Invalid request body',
|
||||
timestamp: new Date().toISOString(),
|
||||
errors: errors.array(),
|
||||
});
|
||||
}
|
||||
|
||||
if (req.params.assetId != req.body.ID) {
|
||||
return res.status(BAD_REQUEST).json({
|
||||
status: getReasonPhrase(BAD_REQUEST),
|
||||
reason: 'ASSET_ID_MISMATCH',
|
||||
message: 'Asset IDs must match',
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
const mspId = req.user as string;
|
||||
const assetId = req.params.assetId;
|
||||
|
||||
try {
|
||||
const submitQueue = req.app.locals.jobq as Queue;
|
||||
const jobId = await addSubmitTransactionJob(
|
||||
submitQueue,
|
||||
mspId,
|
||||
'UpdateAsset',
|
||||
assetId,
|
||||
req.body.color,
|
||||
req.body.size,
|
||||
req.body.owner,
|
||||
req.body.appraisedValue
|
||||
);
|
||||
|
||||
return res.status(ACCEPTED).json({
|
||||
status: getReasonPhrase(ACCEPTED),
|
||||
jobId: jobId,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
{ err },
|
||||
'Error processing update asset request for asset ID %s',
|
||||
assetId
|
||||
);
|
||||
|
||||
return res.status(INTERNAL_SERVER_ERROR).json({
|
||||
status: getReasonPhrase(INTERNAL_SERVER_ERROR),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
assetsRouter.patch(
|
||||
'/:assetId',
|
||||
body()
|
||||
.isArray({
|
||||
min: 1,
|
||||
max: 1,
|
||||
})
|
||||
.withMessage(
|
||||
'body must contain an array with a single patch operation'
|
||||
),
|
||||
body('*.op', "operation must be 'replace'").equals('replace'),
|
||||
body('*.path', "path must be '/Owner'").equals('/Owner'),
|
||||
body('*.value', 'must be a string').isString(),
|
||||
async (req: Request, res: Response) => {
|
||||
logger.debug(req.body, 'Transfer asset request received');
|
||||
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(BAD_REQUEST).json({
|
||||
status: getReasonPhrase(BAD_REQUEST),
|
||||
reason: 'VALIDATION_ERROR',
|
||||
message: 'Invalid request body',
|
||||
timestamp: new Date().toISOString(),
|
||||
errors: errors.array(),
|
||||
});
|
||||
}
|
||||
|
||||
const mspId = req.user as string;
|
||||
const assetId = req.params.assetId;
|
||||
const newOwner = req.body[0].value;
|
||||
|
||||
try {
|
||||
const submitQueue = req.app.locals.jobq as Queue;
|
||||
const jobId = await addSubmitTransactionJob(
|
||||
submitQueue,
|
||||
mspId,
|
||||
'TransferAsset',
|
||||
assetId,
|
||||
newOwner
|
||||
);
|
||||
|
||||
return res.status(ACCEPTED).json({
|
||||
status: getReasonPhrase(ACCEPTED),
|
||||
jobId: jobId,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
{ err },
|
||||
'Error processing update asset request for asset ID %s',
|
||||
req.params.assetId
|
||||
);
|
||||
|
||||
return res.status(INTERNAL_SERVER_ERROR).json({
|
||||
status: getReasonPhrase(INTERNAL_SERVER_ERROR),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
assetsRouter.delete('/:assetId', async (req: Request, res: Response) => {
|
||||
logger.debug(req.body, 'Delete asset request received');
|
||||
|
||||
const mspId = req.user as string;
|
||||
const assetId = req.params.assetId;
|
||||
|
||||
try {
|
||||
const submitQueue = req.app.locals.jobq as Queue;
|
||||
const jobId = await addSubmitTransactionJob(
|
||||
submitQueue,
|
||||
mspId,
|
||||
'DeleteAsset',
|
||||
assetId
|
||||
);
|
||||
|
||||
return res.status(ACCEPTED).json({
|
||||
status: getReasonPhrase(ACCEPTED),
|
||||
jobId: jobId,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
{ err },
|
||||
'Error processing delete asset request for asset ID %s',
|
||||
assetId
|
||||
);
|
||||
|
||||
return res.status(INTERNAL_SERVER_ERROR).json({
|
||||
status: getReasonPhrase(INTERNAL_SERVER_ERROR),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { logger } from './logger';
|
||||
import passport from 'passport';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { HeaderAPIKeyStrategy } from 'passport-headerapikey';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
import * as config from './config';
|
||||
|
||||
const { UNAUTHORIZED } = StatusCodes;
|
||||
|
||||
export const fabricAPIKeyStrategy: HeaderAPIKeyStrategy =
|
||||
new HeaderAPIKeyStrategy(
|
||||
{ header: 'X-API-Key', prefix: '' },
|
||||
false,
|
||||
function (apikey, done) {
|
||||
logger.debug({ apikey }, 'Checking X-API-Key');
|
||||
if (apikey === config.org1ApiKey) {
|
||||
const user = config.mspIdOrg1;
|
||||
logger.debug('User set to %s', user);
|
||||
done(null, user);
|
||||
} else if (apikey === config.org2ApiKey) {
|
||||
const user = config.mspIdOrg2;
|
||||
logger.debug('User set to %s', user);
|
||||
done(null, user);
|
||||
} else {
|
||||
logger.debug({ apikey }, 'No valid X-API-Key');
|
||||
return done(null, false);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const authenticateApiKey = (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): void => {
|
||||
passport.authenticate(
|
||||
'headerapikey',
|
||||
{ session: false },
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(err: any, user: Express.User, _info: any) => {
|
||||
if (err) return next(err);
|
||||
if (!user)
|
||||
return res.status(UNAUTHORIZED).json({
|
||||
status: getReasonPhrase(UNAUTHORIZED),
|
||||
reason: 'NO_VALID_APIKEY',
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
req.logIn(user, { session: false }, async (err) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
return next();
|
||||
});
|
||||
}
|
||||
)(req, res, next);
|
||||
};
|
||||
|
|
@ -1,577 +0,0 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
|
||||
describe('Config values', () => {
|
||||
const ORIGINAL_ENV = process.env;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.resetModules();
|
||||
process.env = { ...ORIGINAL_ENV };
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.env = { ...ORIGINAL_ENV };
|
||||
});
|
||||
|
||||
describe('logLevel', () => {
|
||||
it('defaults to "info"', () => {
|
||||
const config = require('./config');
|
||||
expect(config.logLevel).toBe('info');
|
||||
});
|
||||
|
||||
it('can be configured using the "LOG_LEVEL" environment variable', () => {
|
||||
process.env.LOG_LEVEL = 'debug';
|
||||
const config = require('./config');
|
||||
expect(config.logLevel).toBe('debug');
|
||||
});
|
||||
|
||||
it('throws an error when the "LOG_LEVEL" environment variable has an invalid log level', () => {
|
||||
process.env.LOG_LEVEL = 'ludicrous';
|
||||
expect(() => {
|
||||
require('./config');
|
||||
}).toThrow(
|
||||
'env-var: "LOG_LEVEL" should be one of [fatal, error, warn, info, debug, trace, silent]'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('port', () => {
|
||||
it('defaults to "3000"', () => {
|
||||
const config = require('./config');
|
||||
expect(config.port).toBe(3000);
|
||||
});
|
||||
|
||||
it('can be configured using the "PORT" environment variable', () => {
|
||||
process.env.PORT = '8000';
|
||||
const config = require('./config');
|
||||
expect(config.port).toBe(8000);
|
||||
});
|
||||
|
||||
it('throws an error when the "PORT" environment variable has an invalid port number', () => {
|
||||
process.env.PORT = '65536';
|
||||
expect(() => {
|
||||
require('./config');
|
||||
}).toThrow(
|
||||
'env-var: "PORT" cannot assign a port number greater than 65535. An example of a valid value would be: 3000'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('submitJobBackoffType', () => {
|
||||
it('defaults to "fixed"', () => {
|
||||
const config = require('./config');
|
||||
expect(config.submitJobBackoffType).toBe('fixed');
|
||||
});
|
||||
|
||||
it('can be configured using the "SUBMIT_JOB_BACKOFF_TYPE" environment variable', () => {
|
||||
process.env.SUBMIT_JOB_BACKOFF_TYPE = 'exponential';
|
||||
const config = require('./config');
|
||||
expect(config.submitJobBackoffType).toBe('exponential');
|
||||
});
|
||||
|
||||
it('throws an error when the "LOG_LEVEL" environment variable has an invalid log level', () => {
|
||||
process.env.SUBMIT_JOB_BACKOFF_TYPE = 'jitter';
|
||||
expect(() => {
|
||||
require('./config');
|
||||
}).toThrow(
|
||||
'env-var: "SUBMIT_JOB_BACKOFF_TYPE" should be one of [fixed, exponential]'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('submitJobBackoffDelay', () => {
|
||||
it('defaults to "3000"', () => {
|
||||
const config = require('./config');
|
||||
expect(config.submitJobBackoffDelay).toBe(3000);
|
||||
});
|
||||
|
||||
it('can be configured using the "SUBMIT_JOB_BACKOFF_DELAY" environment variable', () => {
|
||||
process.env.SUBMIT_JOB_BACKOFF_DELAY = '9999';
|
||||
const config = require('./config');
|
||||
expect(config.submitJobBackoffDelay).toBe(9999);
|
||||
});
|
||||
|
||||
it('throws an error when the "SUBMIT_JOB_BACKOFF_DELAY" environment variable has an invalid number', () => {
|
||||
process.env.SUBMIT_JOB_BACKOFF_DELAY = 'short';
|
||||
expect(() => {
|
||||
require('./config');
|
||||
}).toThrow(
|
||||
'env-var: "SUBMIT_JOB_BACKOFF_DELAY" should be a valid integer. An example of a valid value would be: 3000'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('submitJobAttempts', () => {
|
||||
it('defaults to "5"', () => {
|
||||
const config = require('./config');
|
||||
expect(config.submitJobAttempts).toBe(5);
|
||||
});
|
||||
|
||||
it('can be configured using the "SUBMIT_JOB_ATTEMPTS" environment variable', () => {
|
||||
process.env.SUBMIT_JOB_ATTEMPTS = '9999';
|
||||
const config = require('./config');
|
||||
expect(config.submitJobAttempts).toBe(9999);
|
||||
});
|
||||
|
||||
it('throws an error when the "SUBMIT_JOB_ATTEMPTS" environment variable has an invalid number', () => {
|
||||
process.env.SUBMIT_JOB_ATTEMPTS = 'lots';
|
||||
expect(() => {
|
||||
require('./config');
|
||||
}).toThrow(
|
||||
'env-var: "SUBMIT_JOB_ATTEMPTS" should be a valid integer. An example of a valid value would be: 5'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('submitJobConcurrency', () => {
|
||||
it('defaults to "5"', () => {
|
||||
const config = require('./config');
|
||||
expect(config.submitJobConcurrency).toBe(5);
|
||||
});
|
||||
|
||||
it('can be configured using the "SUBMIT_JOB_CONCURRENCY" environment variable', () => {
|
||||
process.env.SUBMIT_JOB_CONCURRENCY = '9999';
|
||||
const config = require('./config');
|
||||
expect(config.submitJobConcurrency).toBe(9999);
|
||||
});
|
||||
|
||||
it('throws an error when the "SUBMIT_JOB_CONCURRENCY" environment variable has an invalid number', () => {
|
||||
process.env.SUBMIT_JOB_CONCURRENCY = 'lots';
|
||||
expect(() => {
|
||||
require('./config');
|
||||
}).toThrow(
|
||||
'env-var: "SUBMIT_JOB_CONCURRENCY" should be a valid integer. An example of a valid value would be: 5'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('maxCompletedSubmitJobs', () => {
|
||||
it('defaults to "1000"', () => {
|
||||
const config = require('./config');
|
||||
expect(config.maxCompletedSubmitJobs).toBe(1000);
|
||||
});
|
||||
|
||||
it('can be configured using the "MAX_COMPLETED_SUBMIT_JOBS" environment variable', () => {
|
||||
process.env.MAX_COMPLETED_SUBMIT_JOBS = '9999';
|
||||
const config = require('./config');
|
||||
expect(config.maxCompletedSubmitJobs).toBe(9999);
|
||||
});
|
||||
|
||||
it('throws an error when the "MAX_COMPLETED_SUBMIT_JOBS" environment variable has an invalid number', () => {
|
||||
process.env.MAX_COMPLETED_SUBMIT_JOBS = 'lots';
|
||||
expect(() => {
|
||||
require('./config');
|
||||
}).toThrow(
|
||||
'env-var: "MAX_COMPLETED_SUBMIT_JOBS" should be a valid integer. An example of a valid value would be: 1000'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('maxFailedSubmitJobs', () => {
|
||||
it('defaults to "1000"', () => {
|
||||
const config = require('./config');
|
||||
expect(config.maxFailedSubmitJobs).toBe(1000);
|
||||
});
|
||||
|
||||
it('can be configured using the "MAX_FAILED_SUBMIT_JOBS" environment variable', () => {
|
||||
process.env.MAX_FAILED_SUBMIT_JOBS = '9999';
|
||||
const config = require('./config');
|
||||
expect(config.maxFailedSubmitJobs).toBe(9999);
|
||||
});
|
||||
|
||||
it('throws an error when the "MAX_FAILED_SUBMIT_JOBS" environment variable has an invalid number', () => {
|
||||
process.env.MAX_FAILED_SUBMIT_JOBS = 'lots';
|
||||
expect(() => {
|
||||
require('./config');
|
||||
}).toThrow(
|
||||
'env-var: "MAX_FAILED_SUBMIT_JOBS" should be a valid integer. An example of a valid value would be: 1000'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('submitJobQueueScheduler', () => {
|
||||
it('defaults to "true"', () => {
|
||||
const config = require('./config');
|
||||
expect(config.submitJobQueueScheduler).toBe(true);
|
||||
});
|
||||
|
||||
it('can be configured using the "SUBMIT_JOB_QUEUE_SCHEDULER" environment variable', () => {
|
||||
process.env.SUBMIT_JOB_QUEUE_SCHEDULER = 'false';
|
||||
const config = require('./config');
|
||||
expect(config.submitJobQueueScheduler).toBe(false);
|
||||
});
|
||||
|
||||
it('throws an error when the "SUBMIT_JOB_QUEUE_SCHEDULER" environment variable has an invalid boolean value', () => {
|
||||
process.env.SUBMIT_JOB_QUEUE_SCHEDULER = '11';
|
||||
expect(() => {
|
||||
require('./config');
|
||||
}).toThrow(
|
||||
'env-var: "SUBMIT_JOB_QUEUE_SCHEDULER" should be either "true", "false", "TRUE", or "FALSE". An example of a valid value would be: true'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('asLocalhost', () => {
|
||||
it('defaults to "true"', () => {
|
||||
const config = require('./config');
|
||||
expect(config.asLocalhost).toBe(true);
|
||||
});
|
||||
|
||||
it('can be configured using the "AS_LOCAL_HOST" environment variable', () => {
|
||||
process.env.AS_LOCAL_HOST = 'false';
|
||||
const config = require('./config');
|
||||
expect(config.asLocalhost).toBe(false);
|
||||
});
|
||||
|
||||
it('throws an error when the "AS_LOCAL_HOST" environment variable has an invalid boolean value', () => {
|
||||
process.env.AS_LOCAL_HOST = '11';
|
||||
expect(() => {
|
||||
require('./config');
|
||||
}).toThrow(
|
||||
'env-var: "AS_LOCAL_HOST" should be either "true", "false", "TRUE", or "FALSE". An example of a valid value would be: true'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mspIdOrg1', () => {
|
||||
it('defaults to "Org1MSP"', () => {
|
||||
const config = require('./config');
|
||||
expect(config.mspIdOrg1).toBe('Org1MSP');
|
||||
});
|
||||
|
||||
it('can be configured using the "HLF_MSP_ID_ORG1" environment variable', () => {
|
||||
process.env.HLF_MSP_ID_ORG1 = 'Test1MSP';
|
||||
const config = require('./config');
|
||||
expect(config.mspIdOrg1).toBe('Test1MSP');
|
||||
});
|
||||
});
|
||||
|
||||
describe('mspIdOrg2', () => {
|
||||
it('defaults to "Org2MSP"', () => {
|
||||
const config = require('./config');
|
||||
expect(config.mspIdOrg2).toBe('Org2MSP');
|
||||
});
|
||||
|
||||
it('can be configured using the "HLF_MSP_ID_ORG2" environment variable', () => {
|
||||
process.env.HLF_MSP_ID_ORG2 = 'Test2MSP';
|
||||
const config = require('./config');
|
||||
expect(config.mspIdOrg2).toBe('Test2MSP');
|
||||
});
|
||||
});
|
||||
|
||||
describe('channelName', () => {
|
||||
it('defaults to "mychannel"', () => {
|
||||
const config = require('./config');
|
||||
expect(config.channelName).toBe('mychannel');
|
||||
});
|
||||
|
||||
it('can be configured using the "HLF_CHANNEL_NAME" environment variable', () => {
|
||||
process.env.HLF_CHANNEL_NAME = 'testchannel';
|
||||
const config = require('./config');
|
||||
expect(config.channelName).toBe('testchannel');
|
||||
});
|
||||
});
|
||||
|
||||
describe('chaincodeName', () => {
|
||||
it('defaults to "basic"', () => {
|
||||
const config = require('./config');
|
||||
expect(config.chaincodeName).toBe('basic');
|
||||
});
|
||||
|
||||
it('can be configured using the "HLF_CHAINCODE_NAME" environment variable', () => {
|
||||
process.env.HLF_CHAINCODE_NAME = 'testcc';
|
||||
const config = require('./config');
|
||||
expect(config.chaincodeName).toBe('testcc');
|
||||
});
|
||||
});
|
||||
|
||||
describe('commitTimeout', () => {
|
||||
it('defaults to "300"', () => {
|
||||
const config = require('./config');
|
||||
expect(config.commitTimeout).toBe(300);
|
||||
});
|
||||
|
||||
it('can be configured using the "HLF_COMMIT_TIMEOUT" environment variable', () => {
|
||||
process.env.HLF_COMMIT_TIMEOUT = '9999';
|
||||
const config = require('./config');
|
||||
expect(config.commitTimeout).toBe(9999);
|
||||
});
|
||||
|
||||
it('throws an error when the "HLF_COMMIT_TIMEOUT" environment variable has an invalid number', () => {
|
||||
process.env.HLF_COMMIT_TIMEOUT = 'short';
|
||||
expect(() => {
|
||||
require('./config');
|
||||
}).toThrow(
|
||||
'env-var: "HLF_COMMIT_TIMEOUT" should be a valid integer. An example of a valid value would be: 300'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('endorseTimeout', () => {
|
||||
it('defaults to "30"', () => {
|
||||
const config = require('./config');
|
||||
expect(config.endorseTimeout).toBe(30);
|
||||
});
|
||||
|
||||
it('can be configured using the "HLF_ENDORSE_TIMEOUT" environment variable', () => {
|
||||
process.env.HLF_ENDORSE_TIMEOUT = '9999';
|
||||
const config = require('./config');
|
||||
expect(config.endorseTimeout).toBe(9999);
|
||||
});
|
||||
|
||||
it('throws an error when the "HLF_ENDORSE_TIMEOUT" environment variable has an invalid number', () => {
|
||||
process.env.HLF_ENDORSE_TIMEOUT = 'short';
|
||||
expect(() => {
|
||||
require('./config');
|
||||
}).toThrow(
|
||||
'env-var: "HLF_ENDORSE_TIMEOUT" should be a valid integer. An example of a valid value would be: 30'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('queryTimeout', () => {
|
||||
it('defaults to "3"', () => {
|
||||
const config = require('./config');
|
||||
expect(config.queryTimeout).toBe(3);
|
||||
});
|
||||
|
||||
it('can be configured using the "HLF_QUERY_TIMEOUT" environment variable', () => {
|
||||
process.env.HLF_QUERY_TIMEOUT = '9999';
|
||||
const config = require('./config');
|
||||
expect(config.queryTimeout).toBe(9999);
|
||||
});
|
||||
|
||||
it('throws an error when the "HLF_QUERY_TIMEOUT" environment variable has an invalid number', () => {
|
||||
process.env.HLF_QUERY_TIMEOUT = 'long';
|
||||
expect(() => {
|
||||
require('./config');
|
||||
}).toThrow(
|
||||
'env-var: "HLF_QUERY_TIMEOUT" should be a valid integer. An example of a valid value would be: 3'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('connectionProfileOrg1', () => {
|
||||
it('throws an error when the "HLF_CONNECTION_PROFILE_ORG1" environment variable is not set', () => {
|
||||
delete process.env.HLF_CONNECTION_PROFILE_ORG1;
|
||||
expect(() => {
|
||||
require('./config');
|
||||
}).toThrow(
|
||||
'env-var: "HLF_CONNECTION_PROFILE_ORG1" is a required variable, but it was not set. An example of a valid value would be: {"name":"test-network-org1","version":"1.0.0","client":{"organization":"Org1" ... }'
|
||||
);
|
||||
});
|
||||
|
||||
it('can be configured using the "HLF_CONNECTION_PROFILE_ORG1" environment variable', () => {
|
||||
process.env.HLF_CONNECTION_PROFILE_ORG1 =
|
||||
'{"name":"test-network-org1"}';
|
||||
const config = require('./config');
|
||||
expect(config.connectionProfileOrg1).toStrictEqual({
|
||||
name: 'test-network-org1',
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error when the "HLF_CONNECTION_PROFILE_ORG1" environment variable is set to invalid json', () => {
|
||||
process.env.HLF_CONNECTION_PROFILE_ORG1 = 'testing';
|
||||
expect(() => {
|
||||
require('./config');
|
||||
}).toThrow(
|
||||
'env-var: "HLF_CONNECTION_PROFILE_ORG1" should be valid (parseable) JSON. An example of a valid value would be: {"name":"test-network-org1","version":"1.0.0","client":{"organization":"Org1" ... }'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('certificateOrg1', () => {
|
||||
it('throws an error when the "HLF_CERTIFICATE_ORG1" environment variable is not set', () => {
|
||||
delete process.env.HLF_CERTIFICATE_ORG1;
|
||||
expect(() => {
|
||||
require('./config');
|
||||
}).toThrow(
|
||||
'env-var: "HLF_CERTIFICATE_ORG1" is a required variable, but it was not set. An example of a valid value would be: "-----BEGIN CERTIFICATE-----\\n...\\n-----END CERTIFICATE-----\\n"'
|
||||
);
|
||||
});
|
||||
|
||||
it('can be configured using the "HLF_CERTIFICATE_ORG1" environment variable', () => {
|
||||
process.env.HLF_CERTIFICATE_ORG1 = 'ORG1CERT';
|
||||
const config = require('./config');
|
||||
expect(config.certificateOrg1).toBe('ORG1CERT');
|
||||
});
|
||||
});
|
||||
|
||||
describe('privateKeyOrg1', () => {
|
||||
it('throws an error when the "HLF_PRIVATE_KEY_ORG1" environment variable is not set', () => {
|
||||
delete process.env.HLF_PRIVATE_KEY_ORG1;
|
||||
expect(() => {
|
||||
require('./config');
|
||||
}).toThrow(
|
||||
'env-var: "HLF_PRIVATE_KEY_ORG1" is a required variable, but it was not set. An example of a valid value would be: "-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n"'
|
||||
);
|
||||
});
|
||||
|
||||
it('can be configured using the "HLF_PRIVATE_KEY_ORG1" environment variable', () => {
|
||||
process.env.HLF_PRIVATE_KEY_ORG1 = 'ORG1PK';
|
||||
const config = require('./config');
|
||||
expect(config.privateKeyOrg1).toBe('ORG1PK');
|
||||
});
|
||||
});
|
||||
|
||||
describe('connectionProfileOrg2', () => {
|
||||
it('throws an error when the "HLF_CONNECTION_PROFILE_ORG2" environment variable is not set', () => {
|
||||
delete process.env.HLF_CONNECTION_PROFILE_ORG2;
|
||||
expect(() => {
|
||||
require('./config');
|
||||
}).toThrow(
|
||||
'env-var: "HLF_CONNECTION_PROFILE_ORG2" is a required variable, but it was not set. An example of a valid value would be: {"name":"test-network-org2","version":"1.0.0","client":{"organization":"Org2" ... }'
|
||||
);
|
||||
});
|
||||
|
||||
it('can be configured using the "HLF_CONNECTION_PROFILE_ORG2" environment variable', () => {
|
||||
process.env.HLF_CONNECTION_PROFILE_ORG2 =
|
||||
'{"name":"test-network-org2"}';
|
||||
const config = require('./config');
|
||||
expect(config.connectionProfileOrg2).toStrictEqual({
|
||||
name: 'test-network-org2',
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error when the "HLF_CONNECTION_PROFILE_ORG2" environment variable is set to invalid json', () => {
|
||||
process.env.HLF_CONNECTION_PROFILE_ORG2 = 'testing';
|
||||
expect(() => {
|
||||
require('./config');
|
||||
}).toThrow(
|
||||
'env-var: "HLF_CONNECTION_PROFILE_ORG2" should be valid (parseable) JSON. An example of a valid value would be: {"name":"test-network-org2","version":"1.0.0","client":{"organization":"Org2" ... }'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('certificateOrg2', () => {
|
||||
it('throws an error when the "HLF_CERTIFICATE_ORG2" environment variable is not set', () => {
|
||||
delete process.env.HLF_CERTIFICATE_ORG2;
|
||||
expect(() => {
|
||||
require('./config');
|
||||
}).toThrow(
|
||||
'env-var: "HLF_CERTIFICATE_ORG2" is a required variable, but it was not set. An example of a valid value would be: "-----BEGIN CERTIFICATE-----\\n...\\n-----END CERTIFICATE-----\\n"'
|
||||
);
|
||||
});
|
||||
|
||||
it('can be configured using the "HLF_CERTIFICATE_ORG2" environment variable', () => {
|
||||
process.env.HLF_CERTIFICATE_ORG2 = 'ORG2CERT';
|
||||
const config = require('./config');
|
||||
expect(config.certificateOrg2).toBe('ORG2CERT');
|
||||
});
|
||||
});
|
||||
|
||||
describe('privateKeyOrg2', () => {
|
||||
it('throws an error when the "HLF_PRIVATE_KEY_ORG2" environment variable is not set', () => {
|
||||
delete process.env.HLF_PRIVATE_KEY_ORG2;
|
||||
expect(() => {
|
||||
require('./config');
|
||||
}).toThrow(
|
||||
'env-var: "HLF_PRIVATE_KEY_ORG2" is a required variable, but it was not set. An example of a valid value would be: "-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n"'
|
||||
);
|
||||
});
|
||||
|
||||
it('can be configured using the "HLF_PRIVATE_KEY_ORG2" environment variable', () => {
|
||||
process.env.HLF_PRIVATE_KEY_ORG2 = 'ORG2PK';
|
||||
const config = require('./config');
|
||||
expect(config.privateKeyOrg2).toBe('ORG2PK');
|
||||
});
|
||||
});
|
||||
|
||||
describe('redisHost', () => {
|
||||
it('defaults to "localhost"', () => {
|
||||
const config = require('./config');
|
||||
expect(config.redisHost).toBe('localhost');
|
||||
});
|
||||
|
||||
it('can be configured using the "REDIS_HOST" environment variable', () => {
|
||||
process.env.REDIS_HOST = 'redis.example.org';
|
||||
const config = require('./config');
|
||||
expect(config.redisHost).toBe('redis.example.org');
|
||||
});
|
||||
});
|
||||
|
||||
describe('redisPort', () => {
|
||||
it('defaults to "6379"', () => {
|
||||
const config = require('./config');
|
||||
expect(config.redisPort).toBe(6379);
|
||||
});
|
||||
|
||||
it('can be configured with a valid port number using the "REDIS_PORT" environment variable', () => {
|
||||
process.env.REDIS_PORT = '9736';
|
||||
const config = require('./config');
|
||||
expect(config.redisPort).toBe(9736);
|
||||
});
|
||||
|
||||
it('throws an error when the "REDIS_PORT" environment variable has an invalid port number', () => {
|
||||
process.env.REDIS_PORT = '65536';
|
||||
expect(() => {
|
||||
require('./config');
|
||||
}).toThrow(
|
||||
'env-var: "REDIS_PORT" cannot assign a port number greater than 65535. An example of a valid value would be: 6379'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('redisUsername', () => {
|
||||
it('has no default value', () => {
|
||||
const config = require('./config');
|
||||
expect(config.redisUsername).toBeUndefined();
|
||||
});
|
||||
|
||||
it('can be configured using the "REDIS_USERNAME" environment variable', () => {
|
||||
process.env.REDIS_USERNAME = 'test';
|
||||
const config = require('./config');
|
||||
expect(config.redisUsername).toBe('test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('redisPassword', () => {
|
||||
it('has no default value', () => {
|
||||
const config = require('./config');
|
||||
expect(config.redisPassword).toBeUndefined();
|
||||
});
|
||||
|
||||
it('can be configured using the "REDIS_PASSWORD" environment variable', () => {
|
||||
process.env.REDIS_PASSWORD = 'testpw';
|
||||
const config = require('./config');
|
||||
expect(config.redisPassword).toBe('testpw');
|
||||
});
|
||||
});
|
||||
|
||||
describe('org1ApiKey', () => {
|
||||
it('throws an error when the "ORG1_APIKEY" environment variable is not set', () => {
|
||||
delete process.env.ORG1_APIKEY;
|
||||
expect(() => {
|
||||
require('./config');
|
||||
}).toThrow(
|
||||
'env-var: "ORG1_APIKEY" is a required variable, but it was not set. An example of a valid value would be: 123'
|
||||
);
|
||||
});
|
||||
|
||||
it('can be configured using the "ORG1_APIKEY" environment variable', () => {
|
||||
process.env.ORG1_APIKEY = 'org1ApiKey';
|
||||
const config = require('./config');
|
||||
expect(config.org1ApiKey).toBe('org1ApiKey');
|
||||
});
|
||||
});
|
||||
|
||||
describe('org2ApiKey', () => {
|
||||
it('throws an error when the "ORG1_APIKEY" environment variable is not set', () => {
|
||||
delete process.env.ORG2_APIKEY;
|
||||
expect(() => {
|
||||
require('./config');
|
||||
}).toThrow(
|
||||
'env-var: "ORG2_APIKEY" is a required variable, but it was not set. An example of a valid value would be: 456'
|
||||
);
|
||||
});
|
||||
|
||||
it('can be configured using the "ORG1_APIKEY" environment variable', () => {
|
||||
process.env.ORG2_APIKEY = 'org2ApiKey';
|
||||
const config = require('./config');
|
||||
expect(config.org2ApiKey).toBe('org2ApiKey');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,293 +0,0 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* The sample REST server can be configured using the environment variables
|
||||
* documented below
|
||||
*
|
||||
* In a local development environment, these variables can be loaded from a
|
||||
* .env file by starting the server with the following command:
|
||||
*
|
||||
* npm start:dev
|
||||
*
|
||||
* The scripts/generateEnv.sh script can be used to generate a suitable .env
|
||||
* file for the Fabric Test Network
|
||||
*/
|
||||
|
||||
import * as env from 'env-var';
|
||||
|
||||
export const ORG1 = 'Org1';
|
||||
export const ORG2 = 'Org2';
|
||||
|
||||
export const JOB_QUEUE_NAME = 'submit';
|
||||
|
||||
/**
|
||||
* Log level for the REST server
|
||||
*/
|
||||
export const logLevel = env
|
||||
.get('LOG_LEVEL')
|
||||
.default('info')
|
||||
.asEnum(['fatal', 'error', 'warn', 'info', 'debug', 'trace', 'silent']);
|
||||
|
||||
/**
|
||||
* The port to start the REST server on
|
||||
*/
|
||||
export const port = env
|
||||
.get('PORT')
|
||||
.default('3000')
|
||||
.example('3000')
|
||||
.asPortNumber();
|
||||
|
||||
/**
|
||||
* The type of backoff to use for retrying failed submit jobs
|
||||
*/
|
||||
export const submitJobBackoffType = env
|
||||
.get('SUBMIT_JOB_BACKOFF_TYPE')
|
||||
.default('fixed')
|
||||
.asEnum(['fixed', 'exponential']);
|
||||
|
||||
/**
|
||||
* Backoff delay for retrying failed submit jobs in milliseconds
|
||||
*/
|
||||
export const submitJobBackoffDelay = env
|
||||
.get('SUBMIT_JOB_BACKOFF_DELAY')
|
||||
.default('3000')
|
||||
.example('3000')
|
||||
.asIntPositive();
|
||||
|
||||
/**
|
||||
* The total number of attempts to try a submit job until it completes
|
||||
*/
|
||||
export const submitJobAttempts = env
|
||||
.get('SUBMIT_JOB_ATTEMPTS')
|
||||
.default('5')
|
||||
.example('5')
|
||||
.asIntPositive();
|
||||
|
||||
/**
|
||||
* The maximum number of submit jobs that can be processed in parallel
|
||||
*/
|
||||
export const submitJobConcurrency = env
|
||||
.get('SUBMIT_JOB_CONCURRENCY')
|
||||
.default('5')
|
||||
.example('5')
|
||||
.asIntPositive();
|
||||
|
||||
/**
|
||||
* The number of completed submit jobs to keep
|
||||
*/
|
||||
export const maxCompletedSubmitJobs = env
|
||||
.get('MAX_COMPLETED_SUBMIT_JOBS')
|
||||
.default('1000')
|
||||
.example('1000')
|
||||
.asIntPositive();
|
||||
|
||||
/**
|
||||
* The number of failed submit jobs to keep
|
||||
*/
|
||||
export const maxFailedSubmitJobs = env
|
||||
.get('MAX_FAILED_SUBMIT_JOBS')
|
||||
.default('1000')
|
||||
.example('1000')
|
||||
.asIntPositive();
|
||||
|
||||
/**
|
||||
* Whether to initialise a scheduler for the submit job queue
|
||||
* There must be at least on queue scheduler to handle retries and you may want
|
||||
* more than one for redundancy
|
||||
*/
|
||||
export const submitJobQueueScheduler = env
|
||||
.get('SUBMIT_JOB_QUEUE_SCHEDULER')
|
||||
.default('true')
|
||||
.example('true')
|
||||
.asBoolStrict();
|
||||
|
||||
/**
|
||||
* Whether to convert discovered host addresses to be 'localhost'
|
||||
* This should be set to 'true' when running a docker composed fabric network on the
|
||||
* local system, e.g. using the test network; otherwise should it should be 'false'
|
||||
*/
|
||||
export const asLocalhost = env
|
||||
.get('AS_LOCAL_HOST')
|
||||
.default('true')
|
||||
.example('true')
|
||||
.asBoolStrict();
|
||||
|
||||
/**
|
||||
* The Org1 MSP ID
|
||||
*/
|
||||
export const mspIdOrg1 = env
|
||||
.get('HLF_MSP_ID_ORG1')
|
||||
.default(`${ORG1}MSP`)
|
||||
.example(`${ORG1}MSP`)
|
||||
.asString();
|
||||
|
||||
/**
|
||||
* The Org2 MSP ID
|
||||
*/
|
||||
export const mspIdOrg2 = env
|
||||
.get('HLF_MSP_ID_ORG2')
|
||||
.default(`${ORG2}MSP`)
|
||||
.example(`${ORG2}MSP`)
|
||||
.asString();
|
||||
|
||||
/**
|
||||
* Name of the channel which the basic asset sample chaincode has been installed on
|
||||
*/
|
||||
export const channelName = env
|
||||
.get('HLF_CHANNEL_NAME')
|
||||
.default('mychannel')
|
||||
.example('mychannel')
|
||||
.asString();
|
||||
|
||||
/**
|
||||
* Name used to install the basic asset sample
|
||||
*/
|
||||
export const chaincodeName = env
|
||||
.get('HLF_CHAINCODE_NAME')
|
||||
.default('basic')
|
||||
.example('basic')
|
||||
.asString();
|
||||
|
||||
/**
|
||||
* The transaction submit timeout in seconds for commit notification to complete
|
||||
*/
|
||||
export const commitTimeout = env
|
||||
.get('HLF_COMMIT_TIMEOUT')
|
||||
.default('300')
|
||||
.example('300')
|
||||
.asIntPositive();
|
||||
|
||||
/**
|
||||
* The transaction submit timeout in seconds for the endorsement to complete
|
||||
*/
|
||||
export const endorseTimeout = env
|
||||
.get('HLF_ENDORSE_TIMEOUT')
|
||||
.default('30')
|
||||
.example('30')
|
||||
.asIntPositive();
|
||||
|
||||
/**
|
||||
* The transaction query timeout in seconds
|
||||
*/
|
||||
export const queryTimeout = env
|
||||
.get('HLF_QUERY_TIMEOUT')
|
||||
.default('3')
|
||||
.example('3')
|
||||
.asIntPositive();
|
||||
|
||||
/**
|
||||
* The Org1 connection profile JSON
|
||||
*/
|
||||
export const connectionProfileOrg1 = env
|
||||
.get('HLF_CONNECTION_PROFILE_ORG1')
|
||||
.required()
|
||||
.example(
|
||||
'{"name":"test-network-org1","version":"1.0.0","client":{"organization":"Org1" ... }'
|
||||
)
|
||||
.asJsonObject() as Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Certificate for an Org1 identity to evaluate and submit transactions
|
||||
*/
|
||||
export const certificateOrg1 = env
|
||||
.get('HLF_CERTIFICATE_ORG1')
|
||||
.required()
|
||||
.example(
|
||||
'"-----BEGIN CERTIFICATE-----\\n...\\n-----END CERTIFICATE-----\\n"'
|
||||
)
|
||||
.asString();
|
||||
|
||||
/**
|
||||
* Private key for an Org1 identity to evaluate and submit transactions
|
||||
*/
|
||||
export const privateKeyOrg1 = env
|
||||
.get('HLF_PRIVATE_KEY_ORG1')
|
||||
.required()
|
||||
.example(
|
||||
'"-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n"'
|
||||
)
|
||||
.asString();
|
||||
|
||||
/**
|
||||
* The Org2 connection profile JSON
|
||||
*/
|
||||
export const connectionProfileOrg2 = env
|
||||
.get('HLF_CONNECTION_PROFILE_ORG2')
|
||||
.required()
|
||||
.example(
|
||||
'{"name":"test-network-org2","version":"1.0.0","client":{"organization":"Org2" ... }'
|
||||
)
|
||||
.asJsonObject() as Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Certificate for an Org2 identity to evaluate and submit transactions
|
||||
*/
|
||||
export const certificateOrg2 = env
|
||||
.get('HLF_CERTIFICATE_ORG2')
|
||||
.required()
|
||||
.example(
|
||||
'"-----BEGIN CERTIFICATE-----\\n...\\n-----END CERTIFICATE-----\\n"'
|
||||
)
|
||||
.asString();
|
||||
|
||||
/**
|
||||
* Private key for an Org2 identity to evaluate and submit transactions
|
||||
*/
|
||||
export const privateKeyOrg2 = env
|
||||
.get('HLF_PRIVATE_KEY_ORG2')
|
||||
.required()
|
||||
.example(
|
||||
'"-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n"'
|
||||
)
|
||||
.asString();
|
||||
|
||||
/**
|
||||
* The host the Redis server is running on
|
||||
*/
|
||||
export const redisHost = env
|
||||
.get('REDIS_HOST')
|
||||
.default('localhost')
|
||||
.example('localhost')
|
||||
.asString();
|
||||
|
||||
/**
|
||||
* The port the Redis server is running on
|
||||
*/
|
||||
export const redisPort = env
|
||||
.get('REDIS_PORT')
|
||||
.default('6379')
|
||||
.example('6379')
|
||||
.asPortNumber();
|
||||
|
||||
/**
|
||||
* Username for the Redis server
|
||||
*/
|
||||
export const redisUsername = env
|
||||
.get('REDIS_USERNAME')
|
||||
.example('fabric')
|
||||
.asString();
|
||||
|
||||
/**
|
||||
* Password for the Redis server
|
||||
*/
|
||||
export const redisPassword = env.get('REDIS_PASSWORD').asString();
|
||||
|
||||
/**
|
||||
* API key for Org1
|
||||
* Specify this API key with the X-Api-Key header to use the Org1 connection profile and credentials
|
||||
*/
|
||||
export const org1ApiKey = env
|
||||
.get('ORG1_APIKEY')
|
||||
.required()
|
||||
.example('123')
|
||||
.asString();
|
||||
|
||||
/**
|
||||
* API key for Org2
|
||||
* Specify this API key with the X-Api-Key header to use the Org2 connection profile and credentials
|
||||
*/
|
||||
export const org2ApiKey = env
|
||||
.get('ORG2_APIKEY')
|
||||
.required()
|
||||
.example('456')
|
||||
.asString();
|
||||
|
|
@ -1,329 +0,0 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { TimeoutError, TransactionError } from 'fabric-network';
|
||||
import {
|
||||
AssetExistsError,
|
||||
AssetNotFoundError,
|
||||
TransactionNotFoundError,
|
||||
getRetryAction,
|
||||
handleError,
|
||||
isDuplicateTransactionError,
|
||||
isErrorLike,
|
||||
RetryAction,
|
||||
} from './errors';
|
||||
|
||||
import { mock } from 'jest-mock-extended';
|
||||
|
||||
describe('Errors', () => {
|
||||
describe('isErrorLike', () => {
|
||||
it('returns false for null', () => {
|
||||
expect(isErrorLike(null)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for undefined', () => {
|
||||
expect(isErrorLike(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for empty object', () => {
|
||||
expect(isErrorLike({})).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for string', () => {
|
||||
expect(isErrorLike('true')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for non-error object', () => {
|
||||
expect(isErrorLike({ size: 42 })).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for invalid error object', () => {
|
||||
expect(isErrorLike({ name: 'MockError', message: 42 })).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for error like object with invalid stack', () => {
|
||||
expect(
|
||||
isErrorLike({
|
||||
name: 'MockError',
|
||||
message: 'Fail',
|
||||
stack: false,
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true for error like object', () => {
|
||||
expect(isErrorLike({ name: 'MockError', message: 'Fail' })).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('returns true for new Error', () => {
|
||||
expect(isErrorLike(new Error('Error'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isDuplicateTransactionError', () => {
|
||||
it('returns true for a TransactionError with a transaction code of DUPLICATE_TXID', () => {
|
||||
const mockDuplicateTransactionError = mock<TransactionError>();
|
||||
mockDuplicateTransactionError.transactionCode = 'DUPLICATE_TXID';
|
||||
|
||||
expect(
|
||||
isDuplicateTransactionError(mockDuplicateTransactionError)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for a TransactionError without a transaction code of MVCC_READ_CONFLICT', () => {
|
||||
const mockDuplicateTransactionError = mock<TransactionError>();
|
||||
mockDuplicateTransactionError.transactionCode =
|
||||
'MVCC_READ_CONFLICT';
|
||||
|
||||
expect(
|
||||
isDuplicateTransactionError(mockDuplicateTransactionError)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true for an error when all endorsement details are duplicate transaction found', () => {
|
||||
const mockDuplicateTransactionError = {
|
||||
errors: [
|
||||
{
|
||||
endorsements: [
|
||||
{
|
||||
details: 'duplicate transaction found',
|
||||
},
|
||||
{
|
||||
details: 'duplicate transaction found',
|
||||
},
|
||||
{
|
||||
details: 'duplicate transaction found',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(
|
||||
isDuplicateTransactionError(mockDuplicateTransactionError)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for an error when at least one endorsement details are duplicate transaction found', () => {
|
||||
const mockDuplicateTransactionError = {
|
||||
errors: [
|
||||
{
|
||||
endorsements: [
|
||||
{
|
||||
details: 'duplicate transaction found',
|
||||
},
|
||||
{
|
||||
details: 'mock endorsement details',
|
||||
},
|
||||
{
|
||||
details: 'mock endorsement details',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(
|
||||
isDuplicateTransactionError(mockDuplicateTransactionError)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for an error without duplicate transaction endorsement details', () => {
|
||||
const mockDuplicateTransactionError = {
|
||||
errors: [
|
||||
{
|
||||
endorsements: [
|
||||
{
|
||||
details: 'mock endorsement details',
|
||||
},
|
||||
{
|
||||
details: 'mock endorsement details',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(
|
||||
isDuplicateTransactionError(mockDuplicateTransactionError)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for an error without endorsement details', () => {
|
||||
const mockDuplicateTransactionError = {
|
||||
errors: [
|
||||
{
|
||||
rejections: [
|
||||
{
|
||||
details: 'duplicate transaction found',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(
|
||||
isDuplicateTransactionError(mockDuplicateTransactionError)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for a basic Error object without endorsement details', () => {
|
||||
expect(
|
||||
isDuplicateTransactionError(
|
||||
new Error('duplicate transaction found')
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for an undefined error', () => {
|
||||
expect(isDuplicateTransactionError(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for a null error', () => {
|
||||
expect(isDuplicateTransactionError(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRetryAction', () => {
|
||||
it('returns RetryAction.None for duplicate transaction errors', () => {
|
||||
const mockDuplicateTransactionError = {
|
||||
errors: [
|
||||
{
|
||||
endorsements: [
|
||||
{
|
||||
details: 'duplicate transaction found',
|
||||
},
|
||||
{
|
||||
details: 'duplicate transaction found',
|
||||
},
|
||||
{
|
||||
details: 'duplicate transaction found',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getRetryAction(mockDuplicateTransactionError)).toBe(
|
||||
RetryAction.None
|
||||
);
|
||||
});
|
||||
|
||||
it('returns RetryAction.None for a TransactionNotFoundError', () => {
|
||||
const mockTransactionNotFoundError = new TransactionNotFoundError(
|
||||
'Failed to get transaction with id txn, error Entry not found in index',
|
||||
'txn1'
|
||||
);
|
||||
|
||||
expect(getRetryAction(mockTransactionNotFoundError)).toBe(
|
||||
RetryAction.None
|
||||
);
|
||||
});
|
||||
|
||||
it('returns RetryAction.None for an AssetExistsError', () => {
|
||||
const mockAssetExistsError = new AssetExistsError(
|
||||
'The asset MOCK_ASSET already exists',
|
||||
'txn1'
|
||||
);
|
||||
|
||||
expect(getRetryAction(mockAssetExistsError)).toBe(RetryAction.None);
|
||||
});
|
||||
|
||||
it('returns RetryAction.None for an AssetNotFoundError', () => {
|
||||
const mockAssetNotFoundError = new AssetNotFoundError(
|
||||
'the asset MOCK_ASSET does not exist',
|
||||
'txn1'
|
||||
);
|
||||
|
||||
expect(getRetryAction(mockAssetNotFoundError)).toBe(
|
||||
RetryAction.None
|
||||
);
|
||||
});
|
||||
|
||||
it('returns RetryAction.WithExistingTransactionId for a TimeoutError', () => {
|
||||
const mockTimeoutError = new TimeoutError('MOCK TIMEOUT ERROR');
|
||||
|
||||
expect(getRetryAction(mockTimeoutError)).toBe(
|
||||
RetryAction.WithExistingTransactionId
|
||||
);
|
||||
});
|
||||
|
||||
it('returns RetryAction.WithNewTransactionId for an MVCC_READ_CONFLICT TransactionError', () => {
|
||||
const mockTransactionError = mock<TransactionError>();
|
||||
mockTransactionError.transactionCode = 'MVCC_READ_CONFLICT';
|
||||
|
||||
expect(getRetryAction(mockTransactionError)).toBe(
|
||||
RetryAction.WithNewTransactionId
|
||||
);
|
||||
});
|
||||
|
||||
it('returns RetryAction.WithNewTransactionId for an Error', () => {
|
||||
const mockError = new Error('MOCK ERROR');
|
||||
|
||||
expect(getRetryAction(mockError)).toBe(
|
||||
RetryAction.WithNewTransactionId
|
||||
);
|
||||
});
|
||||
|
||||
it('returns RetryAction.WithNewTransactionId for a string error', () => {
|
||||
const mockError = 'MOCK ERROR';
|
||||
|
||||
expect(getRetryAction(mockError)).toBe(
|
||||
RetryAction.WithNewTransactionId
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleError', () => {
|
||||
it.each([
|
||||
'the asset GOCHAINCODE already exists',
|
||||
'Asset JAVACHAINCODE already exists',
|
||||
'The asset JSCHAINCODE already exists',
|
||||
])(
|
||||
'returns a AssetExistsError for errors with an asset already exists message: %s',
|
||||
(msg) => {
|
||||
expect(handleError('txn1', new Error(msg))).toStrictEqual(
|
||||
new AssetExistsError(msg, 'txn1')
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
it.each([
|
||||
'the asset GOCHAINCODE does not exist',
|
||||
'Asset JAVACHAINCODE does not exist',
|
||||
'The asset JSCHAINCODE does not exist',
|
||||
])(
|
||||
'returns a AssetNotFoundError for errors with an asset does not exist message: %s',
|
||||
(msg) => {
|
||||
expect(handleError('txn1', new Error(msg))).toStrictEqual(
|
||||
new AssetNotFoundError(msg, 'txn1')
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
it.each([
|
||||
'Failed to get transaction with id txn, error Entry not found in index',
|
||||
'Failed to get transaction with id txn, error no such transaction ID [txn] in index',
|
||||
])(
|
||||
'returns a TransactionNotFoundError for errors with a transaction not found message: %s',
|
||||
(msg) => {
|
||||
expect(handleError('txn1', new Error(msg))).toStrictEqual(
|
||||
new TransactionNotFoundError(msg, 'txn1')
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
it('returns the original error for errors with other messages', () => {
|
||||
expect(handleError('txn1', new Error('MOCK ERROR'))).toStrictEqual(
|
||||
new Error('MOCK ERROR')
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the original error for errors of other types', () => {
|
||||
expect(handleError('txn1', 42)).toEqual(42);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,279 +0,0 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* This file contains all the error handling for Fabric transactions, including
|
||||
* whether a transaction should be retried.
|
||||
*/
|
||||
|
||||
import { TimeoutError, TransactionError } from 'fabric-network';
|
||||
import { logger } from './logger';
|
||||
|
||||
/**
|
||||
* Base type for errors from the smart contract.
|
||||
*
|
||||
* These errors will not be retried.
|
||||
*/
|
||||
export class ContractError extends Error {
|
||||
transactionId: string;
|
||||
|
||||
constructor(message: string, transactionId: string) {
|
||||
super(message);
|
||||
Object.setPrototypeOf(this, ContractError.prototype);
|
||||
|
||||
this.name = 'TransactionError';
|
||||
this.transactionId = transactionId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the error which occurs when the transaction being submitted or
|
||||
* evaluated is not implemented in a smart contract.
|
||||
*/
|
||||
export class TransactionNotFoundError extends ContractError {
|
||||
constructor(message: string, transactionId: string) {
|
||||
super(message, transactionId);
|
||||
Object.setPrototypeOf(this, TransactionNotFoundError.prototype);
|
||||
|
||||
this.name = 'TransactionNotFoundError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the error which occurs in the basic asset transfer smart contract
|
||||
* implementation when an asset already exists.
|
||||
*/
|
||||
export class AssetExistsError extends ContractError {
|
||||
constructor(message: string, transactionId: string) {
|
||||
super(message, transactionId);
|
||||
Object.setPrototypeOf(this, AssetExistsError.prototype);
|
||||
|
||||
this.name = 'AssetExistsError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the error which occurs in the basic asset transfer smart contract
|
||||
* implementation when an asset does not exist.
|
||||
*/
|
||||
export class AssetNotFoundError extends ContractError {
|
||||
constructor(message: string, transactionId: string) {
|
||||
super(message, transactionId);
|
||||
Object.setPrototypeOf(this, AssetNotFoundError.prototype);
|
||||
|
||||
this.name = 'AssetNotFoundError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enumeration of possible retry actions.
|
||||
*/
|
||||
export enum RetryAction {
|
||||
/**
|
||||
* Transactions should be retried using the same transaction ID to protect
|
||||
* against duplicate transactions being committed if a timeout error occurs
|
||||
*/
|
||||
WithExistingTransactionId,
|
||||
|
||||
/**
|
||||
* Transactions which could not be committed due to other errors require a
|
||||
* new transaction ID when retrying
|
||||
*/
|
||||
WithNewTransactionId,
|
||||
|
||||
/**
|
||||
* Transactions that failed due to a duplicate transaction error, or errors
|
||||
* from the smart contract, should not be retried
|
||||
*/
|
||||
None,
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the required transaction retry action for an error.
|
||||
*
|
||||
* For this sample transactions are considered retriable if they fail with any
|
||||
* error, *except* for duplicate transaction errors, or errors from the smart
|
||||
* contract.
|
||||
*
|
||||
* You might decide to retry transactions which fail with specific errors
|
||||
* instead, for example:
|
||||
* - MVCC_READ_CONFLICT
|
||||
* - PHANTOM_READ_CONFLICT
|
||||
* - ENDORSEMENT_POLICY_FAILURE
|
||||
* - CHAINCODE_VERSION_CONFLICT
|
||||
* - EXPIRED_CHAINCODE
|
||||
*/
|
||||
export const getRetryAction = (err: unknown): RetryAction => {
|
||||
if (isDuplicateTransactionError(err) || err instanceof ContractError) {
|
||||
return RetryAction.None;
|
||||
} else if (err instanceof TimeoutError) {
|
||||
return RetryAction.WithExistingTransactionId;
|
||||
}
|
||||
|
||||
return RetryAction.WithNewTransactionId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Type guard to make catching unknown errors easier
|
||||
*/
|
||||
export const isErrorLike = (err: unknown): err is Error => {
|
||||
return (
|
||||
err != undefined &&
|
||||
err != null &&
|
||||
typeof (err as Error).name === 'string' &&
|
||||
typeof (err as Error).message === 'string' &&
|
||||
((err as Error).stack === undefined ||
|
||||
typeof (err as Error).stack === 'string')
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks whether an error was caused by a duplicate transaction.
|
||||
*
|
||||
* This is ...painful.
|
||||
*/
|
||||
export const isDuplicateTransactionError = (err: unknown): boolean => {
|
||||
logger.debug({ err }, 'Checking for duplicate transaction error');
|
||||
|
||||
if (err === undefined || err === null) return false;
|
||||
|
||||
let isDuplicate;
|
||||
if (typeof (err as TransactionError).transactionCode === 'string') {
|
||||
// Checking whether a commit failure is caused by a duplicate transaction
|
||||
// is straightforward because the transaction code should be available
|
||||
isDuplicate =
|
||||
(err as TransactionError).transactionCode === 'DUPLICATE_TXID';
|
||||
} else {
|
||||
// Checking whether an endorsement failure is caused by a duplicate
|
||||
// transaction is only possible by processing error strings, which is not ideal.
|
||||
const endorsementError = err as {
|
||||
errors: { endorsements: { details: string }[] }[];
|
||||
};
|
||||
|
||||
isDuplicate = endorsementError?.errors?.some((err) =>
|
||||
err?.endorsements?.some((endorsement) =>
|
||||
endorsement?.details?.startsWith('duplicate transaction found')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return isDuplicate === true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Matches asset already exists error strings from the asset contract
|
||||
*
|
||||
* The regex needs to match the following error messages:
|
||||
* - "the asset %s already exists"
|
||||
* - "The asset ${id} already exists"
|
||||
* - "Asset %s already exists"
|
||||
*/
|
||||
const matchAssetAlreadyExistsMessage = (message: string): string | null => {
|
||||
const assetAlreadyExistsRegex = /([tT]he )?[aA]sset \w* already exists/g;
|
||||
const assetAlreadyExistsMatch = message.match(assetAlreadyExistsRegex);
|
||||
logger.debug(
|
||||
{ message: message, result: assetAlreadyExistsMatch },
|
||||
'Checking for asset already exists message'
|
||||
);
|
||||
|
||||
if (assetAlreadyExistsMatch !== null) {
|
||||
return assetAlreadyExistsMatch[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Matches asset does not exist error strings from the asset contract
|
||||
*
|
||||
* The regex needs to match the following error messages:
|
||||
* - "the asset %s does not exist"
|
||||
* - "The asset ${id} does not exist"
|
||||
* - "Asset %s does not exist"
|
||||
*/
|
||||
const matchAssetDoesNotExistMessage = (message: string): string | null => {
|
||||
const assetDoesNotExistRegex = /([tT]he )?[aA]sset \w* does not exist/g;
|
||||
const assetDoesNotExistMatch = message.match(assetDoesNotExistRegex);
|
||||
logger.debug(
|
||||
{ message: message, result: assetDoesNotExistMatch },
|
||||
'Checking for asset does not exist message'
|
||||
);
|
||||
|
||||
if (assetDoesNotExistMatch !== null) {
|
||||
return assetDoesNotExistMatch[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Matches transaction does not exist error strings from the contract API
|
||||
*
|
||||
* The regex needs to match the following error messages:
|
||||
* - "Failed to get transaction with id %s, error Entry not found in index"
|
||||
* - "Failed to get transaction with id %s, error no such transaction ID [%s] in index"
|
||||
*/
|
||||
const matchTransactionDoesNotExistMessage = (
|
||||
message: string
|
||||
): string | null => {
|
||||
const transactionDoesNotExistRegex =
|
||||
/Failed to get transaction with id [^,]*, error (?:(?:Entry not found)|(?:no such transaction ID \[[^\]]*\])) in index/g;
|
||||
const transactionDoesNotExistMatch = message.match(
|
||||
transactionDoesNotExistRegex
|
||||
);
|
||||
logger.debug(
|
||||
{ message: message, result: transactionDoesNotExistMatch },
|
||||
'Checking for transaction does not exist message'
|
||||
);
|
||||
|
||||
if (transactionDoesNotExistMatch !== null) {
|
||||
return transactionDoesNotExistMatch[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles errors from evaluating and submitting transactions.
|
||||
*
|
||||
* Smart contract errors from the basic asset transfer samples do not use
|
||||
* error codes so matching strings is the only option, which is not ideal.
|
||||
*
|
||||
* Note: the error message text is not the same for the Go, Java, and
|
||||
* Javascript implementations of the chaincode!
|
||||
*/
|
||||
export const handleError = (
|
||||
transactionId: string,
|
||||
err: unknown
|
||||
): Error | unknown => {
|
||||
logger.debug({ transactionId: transactionId, err }, 'Processing error');
|
||||
|
||||
if (isErrorLike(err)) {
|
||||
const assetAlreadyExistsMatch = matchAssetAlreadyExistsMessage(
|
||||
err.message
|
||||
);
|
||||
if (assetAlreadyExistsMatch !== null) {
|
||||
return new AssetExistsError(assetAlreadyExistsMatch, transactionId);
|
||||
}
|
||||
|
||||
const assetDoesNotExistMatch = matchAssetDoesNotExistMessage(
|
||||
err.message
|
||||
);
|
||||
if (assetDoesNotExistMatch !== null) {
|
||||
return new AssetNotFoundError(
|
||||
assetDoesNotExistMatch,
|
||||
transactionId
|
||||
);
|
||||
}
|
||||
|
||||
const transactionDoesNotExistMatch =
|
||||
matchTransactionDoesNotExistMessage(err.message);
|
||||
if (transactionDoesNotExistMatch !== null) {
|
||||
return new TransactionNotFoundError(
|
||||
transactionDoesNotExistMatch,
|
||||
transactionId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return err;
|
||||
};
|
||||
|
|
@ -1,314 +0,0 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
createGateway,
|
||||
createWallet,
|
||||
getContracts,
|
||||
getNetwork,
|
||||
evatuateTransaction,
|
||||
submitTransaction,
|
||||
getBlockHeight,
|
||||
getTransactionValidationCode,
|
||||
} from './fabric';
|
||||
import * as config from './config';
|
||||
|
||||
import {
|
||||
AssetExistsError,
|
||||
AssetNotFoundError,
|
||||
TransactionNotFoundError,
|
||||
} from './errors';
|
||||
|
||||
import {
|
||||
Contract,
|
||||
Gateway,
|
||||
GatewayOptions,
|
||||
Network,
|
||||
Transaction,
|
||||
Wallet,
|
||||
} from 'fabric-network';
|
||||
|
||||
import * as fabricProtos from 'fabric-protos';
|
||||
|
||||
import { MockProxy, mock } from 'jest-mock-extended';
|
||||
import Long from 'long';
|
||||
|
||||
jest.mock('./config');
|
||||
jest.mock('fabric-network', () => {
|
||||
type FabricNetworkModule = jest.Mocked<typeof import('fabric-network')>;
|
||||
const originalModule: FabricNetworkModule =
|
||||
jest.requireActual('fabric-network');
|
||||
const mockModule: FabricNetworkModule =
|
||||
jest.createMockFromModule('fabric-network');
|
||||
|
||||
return {
|
||||
__esModule: true,
|
||||
...mockModule,
|
||||
Wallets: originalModule.Wallets,
|
||||
};
|
||||
});
|
||||
jest.mock('ioredis', () => require('ioredis-mock/jest'));
|
||||
|
||||
describe('Fabric', () => {
|
||||
describe('createWallet', () => {
|
||||
it('creates a wallet containing identities for both orgs', async () => {
|
||||
const wallet = await createWallet();
|
||||
|
||||
expect(await wallet.list()).toStrictEqual(['Org1MSP', 'Org2MSP']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createGateway', () => {
|
||||
it('creates a Gateway and connects using the provided arguments', async () => {
|
||||
const connectionProfile = config.connectionProfileOrg1;
|
||||
const identity = config.mspIdOrg1;
|
||||
const mockWallet = mock<Wallet>();
|
||||
|
||||
const gateway = await createGateway(
|
||||
connectionProfile,
|
||||
identity,
|
||||
mockWallet
|
||||
);
|
||||
|
||||
expect(gateway.connect).toBeCalledWith(
|
||||
connectionProfile,
|
||||
expect.objectContaining<GatewayOptions>({
|
||||
wallet: mockWallet,
|
||||
identity,
|
||||
discovery: expect.any(Object),
|
||||
eventHandlerOptions: expect.any(Object),
|
||||
queryHandlerOptions: expect.any(Object),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNetwork', () => {
|
||||
it('gets a Network instance for the required channel from the Gateway', async () => {
|
||||
const mockGateway = mock<Gateway>();
|
||||
|
||||
await getNetwork(mockGateway);
|
||||
|
||||
expect(mockGateway.getNetwork).toHaveBeenCalledWith(
|
||||
config.channelName
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getContracts', () => {
|
||||
it('gets the asset and qscc contracts from the network', async () => {
|
||||
const mockBasicContract = mock<Contract>();
|
||||
const mockSystemContract = mock<Contract>();
|
||||
const mockNetwork = mock<Network>();
|
||||
mockNetwork.getContract
|
||||
.calledWith(config.chaincodeName)
|
||||
.mockReturnValue(mockBasicContract);
|
||||
mockNetwork.getContract
|
||||
.calledWith('qscc')
|
||||
.mockReturnValue(mockSystemContract);
|
||||
|
||||
const contracts = await getContracts(mockNetwork);
|
||||
|
||||
expect(contracts).toStrictEqual({
|
||||
assetContract: mockBasicContract,
|
||||
qsccContract: mockSystemContract,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('evatuateTransaction', () => {
|
||||
const mockPayload = Buffer.from('MOCK PAYLOAD');
|
||||
let mockTransaction: MockProxy<Transaction>;
|
||||
let mockContract: MockProxy<Contract>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockTransaction = mock<Transaction>();
|
||||
mockTransaction.evaluate.mockResolvedValue(mockPayload);
|
||||
mockContract = mock<Contract>();
|
||||
mockContract.createTransaction
|
||||
.calledWith('txn')
|
||||
.mockReturnValue(mockTransaction);
|
||||
});
|
||||
|
||||
it('gets the result of evaluating a transaction', async () => {
|
||||
const result = await evatuateTransaction(
|
||||
mockContract,
|
||||
'txn',
|
||||
'arga',
|
||||
'argb'
|
||||
);
|
||||
expect(result.toString()).toBe(mockPayload.toString());
|
||||
});
|
||||
|
||||
it('throws an AssetExistsError an asset already exists error occurs', async () => {
|
||||
mockTransaction.evaluate.mockRejectedValue(
|
||||
new Error('The asset JSCHAINCODE already exists')
|
||||
);
|
||||
|
||||
await expect(async () => {
|
||||
await evatuateTransaction(mockContract, 'txn', 'arga', 'argb');
|
||||
}).rejects.toThrow(AssetExistsError);
|
||||
});
|
||||
|
||||
it('throws an AssetNotFoundError if an asset does not exist error occurs', async () => {
|
||||
mockTransaction.evaluate.mockRejectedValue(
|
||||
new Error('The asset JSCHAINCODE does not exist')
|
||||
);
|
||||
|
||||
await expect(async () => {
|
||||
await evatuateTransaction(mockContract, 'txn', 'arga', 'argb');
|
||||
}).rejects.toThrow(AssetNotFoundError);
|
||||
});
|
||||
|
||||
it('throws a TransactionNotFoundError if a transaction not found error occurs', async () => {
|
||||
mockTransaction.evaluate.mockRejectedValue(
|
||||
new Error(
|
||||
'Failed to get transaction with id txn, error Entry not found in index'
|
||||
)
|
||||
);
|
||||
|
||||
await expect(async () => {
|
||||
await evatuateTransaction(mockContract, 'txn', 'arga', 'argb');
|
||||
}).rejects.toThrow(TransactionNotFoundError);
|
||||
});
|
||||
|
||||
it('throws an Error for other errors', async () => {
|
||||
mockTransaction.evaluate.mockRejectedValue(new Error('MOCK ERROR'));
|
||||
await expect(async () => {
|
||||
await evatuateTransaction(mockContract, 'txn', 'arga', 'argb');
|
||||
}).rejects.toThrow(Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('submitTransaction', () => {
|
||||
let mockTransaction: MockProxy<Transaction>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockTransaction = mock<Transaction>();
|
||||
});
|
||||
|
||||
it('gets the result of submitting a transaction', async () => {
|
||||
const mockPayload = Buffer.from('MOCK PAYLOAD');
|
||||
mockTransaction.submit.mockResolvedValue(mockPayload);
|
||||
|
||||
const result = await submitTransaction(
|
||||
mockTransaction,
|
||||
'txn',
|
||||
'arga',
|
||||
'argb'
|
||||
);
|
||||
expect(result.toString()).toBe(mockPayload.toString());
|
||||
});
|
||||
|
||||
it('throws an AssetExistsError an asset already exists error occurs', async () => {
|
||||
mockTransaction.submit.mockRejectedValue(
|
||||
new Error('The asset JSCHAINCODE already exists')
|
||||
);
|
||||
|
||||
await expect(async () => {
|
||||
await submitTransaction(
|
||||
mockTransaction,
|
||||
'mspid',
|
||||
'txn',
|
||||
'arga',
|
||||
'argb'
|
||||
);
|
||||
}).rejects.toThrow(AssetExistsError);
|
||||
});
|
||||
|
||||
it('throws an AssetNotFoundError if an asset does not exist error occurs', async () => {
|
||||
mockTransaction.submit.mockRejectedValue(
|
||||
new Error('The asset JSCHAINCODE does not exist')
|
||||
);
|
||||
|
||||
await expect(async () => {
|
||||
await submitTransaction(
|
||||
mockTransaction,
|
||||
'mspid',
|
||||
'txn',
|
||||
'arga',
|
||||
'argb'
|
||||
);
|
||||
}).rejects.toThrow(AssetNotFoundError);
|
||||
});
|
||||
|
||||
it('throws a TransactionNotFoundError if a transaction not found error occurs', async () => {
|
||||
mockTransaction.submit.mockRejectedValue(
|
||||
new Error(
|
||||
'Failed to get transaction with id txn, error Entry not found in index'
|
||||
)
|
||||
);
|
||||
|
||||
await expect(async () => {
|
||||
await submitTransaction(
|
||||
mockTransaction,
|
||||
'mspid',
|
||||
'txn',
|
||||
'arga',
|
||||
'argb'
|
||||
);
|
||||
}).rejects.toThrow(TransactionNotFoundError);
|
||||
});
|
||||
|
||||
it('throws an Error for other errors', async () => {
|
||||
mockTransaction.submit.mockRejectedValue(new Error('MOCK ERROR'));
|
||||
|
||||
await expect(async () => {
|
||||
await submitTransaction(
|
||||
mockTransaction,
|
||||
'mspid',
|
||||
'txn',
|
||||
'arga',
|
||||
'argb'
|
||||
);
|
||||
}).rejects.toThrow(Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTransactionValidationCode', () => {
|
||||
it('gets the validation code from a processed transaction', async () => {
|
||||
const processedTransactionProto =
|
||||
fabricProtos.protos.ProcessedTransaction.create();
|
||||
processedTransactionProto.validationCode =
|
||||
fabricProtos.protos.TxValidationCode.VALID;
|
||||
const processedTransactionBuffer = Buffer.from(
|
||||
fabricProtos.protos.ProcessedTransaction.encode(
|
||||
processedTransactionProto
|
||||
).finish()
|
||||
);
|
||||
|
||||
const mockTransaction = mock<Transaction>();
|
||||
mockTransaction.evaluate.mockResolvedValue(
|
||||
processedTransactionBuffer
|
||||
);
|
||||
const mockContract = mock<Contract>();
|
||||
mockContract.createTransaction
|
||||
.calledWith('GetTransactionByID')
|
||||
.mockReturnValue(mockTransaction);
|
||||
expect(
|
||||
await getTransactionValidationCode(mockContract, 'txn1')
|
||||
).toBe('VALID');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBlockHeight', () => {
|
||||
it('gets the current block height', async () => {
|
||||
const mockBlockchainInfoProto =
|
||||
fabricProtos.common.BlockchainInfo.create();
|
||||
mockBlockchainInfoProto.height = 42;
|
||||
const mockBlockchainInfoBuffer = Buffer.from(
|
||||
fabricProtos.common.BlockchainInfo.encode(
|
||||
mockBlockchainInfoProto
|
||||
).finish()
|
||||
);
|
||||
const mockContract = mock<Contract>();
|
||||
mockContract.evaluateTransaction
|
||||
.calledWith('GetChainInfo', 'mychannel')
|
||||
.mockResolvedValue(mockBlockchainInfoBuffer);
|
||||
|
||||
const result = (await getBlockHeight(mockContract)) as Long;
|
||||
expect(result.toInt()).toStrictEqual(42);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,205 +0,0 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
Contract,
|
||||
DefaultEventHandlerStrategies,
|
||||
DefaultQueryHandlerStrategies,
|
||||
Gateway,
|
||||
GatewayOptions,
|
||||
Network,
|
||||
Transaction,
|
||||
Wallet,
|
||||
Wallets,
|
||||
} from 'fabric-network';
|
||||
import * as protos from 'fabric-protos';
|
||||
import Long from 'long';
|
||||
import * as config from './config';
|
||||
import { handleError } from './errors';
|
||||
import { logger } from './logger';
|
||||
|
||||
/**
|
||||
* Creates an in memory wallet to hold credentials for an Org1 and Org2 user
|
||||
*
|
||||
* In this sample there is a single user for each MSP ID to demonstrate how
|
||||
* a client app might submit transactions for different users
|
||||
*
|
||||
* Alternatively a REST server could use its own identity for all transactions,
|
||||
* or it could use credentials supplied in the REST requests
|
||||
*/
|
||||
export const createWallet = async (): Promise<Wallet> => {
|
||||
const wallet = await Wallets.newInMemoryWallet();
|
||||
|
||||
const org1Identity = {
|
||||
credentials: {
|
||||
certificate: config.certificateOrg1,
|
||||
privateKey: config.privateKeyOrg1,
|
||||
},
|
||||
mspId: config.mspIdOrg1,
|
||||
type: 'X.509',
|
||||
};
|
||||
|
||||
await wallet.put(config.mspIdOrg1, org1Identity);
|
||||
|
||||
const org2Identity = {
|
||||
credentials: {
|
||||
certificate: config.certificateOrg2,
|
||||
privateKey: config.privateKeyOrg2,
|
||||
},
|
||||
mspId: config.mspIdOrg2,
|
||||
type: 'X.509',
|
||||
};
|
||||
|
||||
await wallet.put(config.mspIdOrg2, org2Identity);
|
||||
|
||||
return wallet;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a Gateway connection
|
||||
*
|
||||
* Gateway instances can and should be reused rather than connecting to submit every transaction
|
||||
*/
|
||||
export const createGateway = async (
|
||||
connectionProfile: Record<string, unknown>,
|
||||
identity: string,
|
||||
wallet: Wallet
|
||||
): Promise<Gateway> => {
|
||||
logger.debug({ connectionProfile, identity }, 'Configuring gateway');
|
||||
|
||||
const gateway = new Gateway();
|
||||
|
||||
const options: GatewayOptions = {
|
||||
wallet,
|
||||
identity,
|
||||
discovery: { enabled: true, asLocalhost: config.asLocalhost },
|
||||
eventHandlerOptions: {
|
||||
commitTimeout: config.commitTimeout,
|
||||
endorseTimeout: config.endorseTimeout,
|
||||
strategy: DefaultEventHandlerStrategies.PREFER_MSPID_SCOPE_ANYFORTX,
|
||||
},
|
||||
queryHandlerOptions: {
|
||||
timeout: config.queryTimeout,
|
||||
strategy:
|
||||
DefaultQueryHandlerStrategies.PREFER_MSPID_SCOPE_ROUND_ROBIN,
|
||||
},
|
||||
};
|
||||
|
||||
await gateway.connect(connectionProfile, options);
|
||||
|
||||
return gateway;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the network which the asset transfer sample chaincode is running on
|
||||
*
|
||||
* In addion to getting the contract, the network will also be used to
|
||||
* start a block event listener
|
||||
*/
|
||||
export const getNetwork = async (gateway: Gateway): Promise<Network> => {
|
||||
const network = await gateway.getNetwork(config.channelName);
|
||||
return network;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the asset transfer sample contract and the qscc system contract
|
||||
*
|
||||
* The system contract is used for the liveness REST endpoint
|
||||
*/
|
||||
export const getContracts = async (
|
||||
network: Network
|
||||
): Promise<{ assetContract: Contract; qsccContract: Contract }> => {
|
||||
const assetContract = network.getContract(config.chaincodeName);
|
||||
const qsccContract = network.getContract('qscc');
|
||||
return { assetContract, qsccContract };
|
||||
};
|
||||
|
||||
/**
|
||||
* Evaluate a transaction and handle any errors
|
||||
*/
|
||||
export const evatuateTransaction = async (
|
||||
contract: Contract,
|
||||
transactionName: string,
|
||||
...transactionArgs: string[]
|
||||
): Promise<Buffer> => {
|
||||
const transaction = contract.createTransaction(transactionName);
|
||||
const transactionId = transaction.getTransactionId();
|
||||
logger.trace({ transaction }, 'Evaluating transaction');
|
||||
|
||||
try {
|
||||
const payload = await transaction.evaluate(...transactionArgs);
|
||||
logger.trace(
|
||||
{ transactionId: transactionId, payload: payload.toString() },
|
||||
'Evaluate transaction response received'
|
||||
);
|
||||
return payload;
|
||||
} catch (err) {
|
||||
throw handleError(transactionId, err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Submit a transaction and handle any errors
|
||||
*/
|
||||
export const submitTransaction = async (
|
||||
transaction: Transaction,
|
||||
...transactionArgs: string[]
|
||||
): Promise<Buffer> => {
|
||||
logger.trace({ transaction }, 'Submitting transaction');
|
||||
const txnId = transaction.getTransactionId();
|
||||
|
||||
try {
|
||||
const payload = await transaction.submit(...transactionArgs);
|
||||
logger.trace(
|
||||
{ transactionId: txnId, payload: payload.toString() },
|
||||
'Submit transaction response received'
|
||||
);
|
||||
return payload;
|
||||
} catch (err) {
|
||||
throw handleError(txnId, err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the validation code of the specified transaction
|
||||
*/
|
||||
export const getTransactionValidationCode = async (
|
||||
qsccContract: Contract,
|
||||
transactionId: string
|
||||
): Promise<string> => {
|
||||
const data = await evatuateTransaction(
|
||||
qsccContract,
|
||||
'GetTransactionByID',
|
||||
config.channelName,
|
||||
transactionId
|
||||
);
|
||||
|
||||
const processedTransaction =
|
||||
protos.protos.ProcessedTransaction.decode(data);
|
||||
const validationCode =
|
||||
protos.protos.TxValidationCode[processedTransaction.validationCode];
|
||||
|
||||
logger.debug({ transactionId }, 'Validation code: %s', validationCode);
|
||||
return validationCode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the current block height
|
||||
*
|
||||
* This example of using a system contract is used for the liveness REST
|
||||
* endpoint
|
||||
*/
|
||||
export const getBlockHeight = async (
|
||||
qscc: Contract
|
||||
): Promise<number | Long> => {
|
||||
const data = await qscc.evaluateTransaction(
|
||||
'GetChainInfo',
|
||||
config.channelName
|
||||
);
|
||||
const info = protos.common.BlockchainInfo.decode(data);
|
||||
const blockHeight = info.height;
|
||||
|
||||
logger.debug('Current block height: %d', blockHeight);
|
||||
return blockHeight;
|
||||
};
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import express, { Request, Response } from 'express';
|
||||
import { Contract } from 'fabric-network';
|
||||
import { getReasonPhrase, StatusCodes } from 'http-status-codes';
|
||||
import { getBlockHeight } from './fabric';
|
||||
import { logger } from './logger';
|
||||
import * as config from './config';
|
||||
import { Queue } from 'bullmq';
|
||||
import { getJobCounts } from './jobs';
|
||||
|
||||
const { SERVICE_UNAVAILABLE, OK } = StatusCodes;
|
||||
|
||||
export const healthRouter = express.Router();
|
||||
|
||||
/*
|
||||
* Example of possible health endpoints for use in a cloud environment
|
||||
*/
|
||||
|
||||
healthRouter.get('/ready', (_req, res: Response) =>
|
||||
res.status(OK).json({
|
||||
status: getReasonPhrase(OK),
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
);
|
||||
|
||||
healthRouter.get('/live', async (req: Request, res: Response) => {
|
||||
logger.debug(req.body, 'Liveness request received');
|
||||
|
||||
try {
|
||||
const submitQueue = req.app.locals.jobq as Queue;
|
||||
const qsccOrg1 = req.app.locals[config.mspIdOrg1]
|
||||
?.qsccContract as Contract;
|
||||
const qsccOrg2 = req.app.locals[config.mspIdOrg2]
|
||||
?.qsccContract as Contract;
|
||||
|
||||
await Promise.all([
|
||||
getBlockHeight(qsccOrg1),
|
||||
getBlockHeight(qsccOrg2),
|
||||
getJobCounts(submitQueue),
|
||||
]);
|
||||
} catch (err) {
|
||||
logger.error({ err }, 'Error processing liveness request');
|
||||
|
||||
return res.status(SERVICE_UNAVAILABLE).json({
|
||||
status: getReasonPhrase(SERVICE_UNAVAILABLE),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(OK).json({
|
||||
status: getReasonPhrase(OK),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
});
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* This is the main entrypoint for the sample REST server, which is responsible
|
||||
* for connecting to the Fabric network and setting up a job queue for
|
||||
* processing submit transactions
|
||||
*/
|
||||
|
||||
import * as config from './config';
|
||||
import {
|
||||
createGateway,
|
||||
createWallet,
|
||||
getContracts,
|
||||
getNetwork,
|
||||
} from './fabric';
|
||||
import {
|
||||
initJobQueue,
|
||||
initJobQueueScheduler,
|
||||
initJobQueueWorker,
|
||||
} from './jobs';
|
||||
import { logger } from './logger';
|
||||
import { createServer } from './server';
|
||||
import { isMaxmemoryPolicyNoeviction } from './redis';
|
||||
import { Queue, QueueScheduler, Worker } from 'bullmq';
|
||||
|
||||
let jobQueue: Queue | undefined;
|
||||
let jobQueueWorker: Worker | undefined;
|
||||
let jobQueueScheduler: QueueScheduler | undefined;
|
||||
|
||||
async function main() {
|
||||
logger.info('Checking Redis config');
|
||||
if (!(await isMaxmemoryPolicyNoeviction())) {
|
||||
throw new Error(
|
||||
'Invalid redis configuration: redis instance must have the setting maxmemory-policy=noeviction'
|
||||
);
|
||||
}
|
||||
|
||||
logger.info('Creating REST server');
|
||||
const app = await createServer();
|
||||
|
||||
logger.info('Connecting to Fabric network with org1 mspid');
|
||||
const wallet = await createWallet();
|
||||
|
||||
const gatewayOrg1 = await createGateway(
|
||||
config.connectionProfileOrg1,
|
||||
config.mspIdOrg1,
|
||||
wallet
|
||||
);
|
||||
const networkOrg1 = await getNetwork(gatewayOrg1);
|
||||
const contractsOrg1 = await getContracts(networkOrg1);
|
||||
|
||||
app.locals[config.mspIdOrg1] = contractsOrg1;
|
||||
|
||||
logger.info('Connecting to Fabric network with org2 mspid');
|
||||
const gatewayOrg2 = await createGateway(
|
||||
config.connectionProfileOrg2,
|
||||
config.mspIdOrg2,
|
||||
wallet
|
||||
);
|
||||
const networkOrg2 = await getNetwork(gatewayOrg2);
|
||||
const contractsOrg2 = await getContracts(networkOrg2);
|
||||
|
||||
app.locals[config.mspIdOrg2] = contractsOrg2;
|
||||
|
||||
logger.info('Initialising submit job queue');
|
||||
jobQueue = initJobQueue();
|
||||
jobQueueWorker = initJobQueueWorker(app);
|
||||
if (config.submitJobQueueScheduler === true) {
|
||||
logger.info('Initialising submit job queue scheduler');
|
||||
jobQueueScheduler = initJobQueueScheduler();
|
||||
}
|
||||
app.locals.jobq = jobQueue;
|
||||
|
||||
logger.info('Starting REST server');
|
||||
app.listen(config.port, () => {
|
||||
logger.info('REST server started on port: %d', config.port);
|
||||
});
|
||||
}
|
||||
|
||||
main().catch(async (err) => {
|
||||
logger.error({ err }, 'Unxepected error');
|
||||
|
||||
if (jobQueueScheduler != undefined) {
|
||||
logger.debug('Closing job queue scheduler');
|
||||
await jobQueueScheduler.close();
|
||||
}
|
||||
|
||||
if (jobQueueWorker != undefined) {
|
||||
logger.debug('Closing job queue worker');
|
||||
await jobQueueWorker.close();
|
||||
}
|
||||
|
||||
if (jobQueue != undefined) {
|
||||
logger.debug('Closing job queue');
|
||||
await jobQueue.close();
|
||||
}
|
||||
});
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Queue } from 'bullmq';
|
||||
import express, { Request, Response } from 'express';
|
||||
import { getReasonPhrase, StatusCodes } from 'http-status-codes';
|
||||
import { getJobSummary, JobNotFoundError } from './jobs';
|
||||
import { logger } from './logger';
|
||||
|
||||
const { INTERNAL_SERVER_ERROR, NOT_FOUND, OK } = StatusCodes;
|
||||
|
||||
export const jobsRouter = express.Router();
|
||||
|
||||
jobsRouter.get('/:jobId', async (req: Request, res: Response) => {
|
||||
const jobId = req.params.jobId;
|
||||
logger.debug('Read request received for job ID %s', jobId);
|
||||
|
||||
try {
|
||||
const submitQueue = req.app.locals.jobq as Queue;
|
||||
|
||||
const jobSummary = await getJobSummary(submitQueue, jobId);
|
||||
|
||||
return res.status(OK).json(jobSummary);
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
{ err },
|
||||
'Error processing read request for job ID %s',
|
||||
jobId
|
||||
);
|
||||
|
||||
if (err instanceof JobNotFoundError) {
|
||||
return res.status(NOT_FOUND).json({
|
||||
status: getReasonPhrase(NOT_FOUND),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(INTERNAL_SERVER_ERROR).json({
|
||||
status: getReasonPhrase(INTERNAL_SERVER_ERROR),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -1,351 +0,0 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Job, Queue } from 'bullmq';
|
||||
import {
|
||||
addSubmitTransactionJob,
|
||||
getJobCounts,
|
||||
getJobSummary,
|
||||
processSubmitTransactionJob,
|
||||
JobNotFoundError,
|
||||
updateJobData,
|
||||
} from './jobs';
|
||||
import { Contract, Transaction } from 'fabric-network';
|
||||
import { mock, MockProxy } from 'jest-mock-extended';
|
||||
import { Application } from 'express';
|
||||
|
||||
describe('addSubmitTransactionJob', () => {
|
||||
let mockJob: MockProxy<Job>;
|
||||
let mockQueue: MockProxy<Queue>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockJob = mock<Job>();
|
||||
mockQueue = mock<Queue>();
|
||||
mockQueue.add.mockResolvedValue(mockJob);
|
||||
});
|
||||
|
||||
it('returns the new job ID', async () => {
|
||||
mockJob.id = 'mockJobId';
|
||||
|
||||
const jobid = await addSubmitTransactionJob(
|
||||
mockQueue,
|
||||
'mockMspId',
|
||||
'txn',
|
||||
'arg1',
|
||||
'arg2'
|
||||
);
|
||||
|
||||
expect(jobid).toBe('mockJobId');
|
||||
});
|
||||
|
||||
it('throws an error if there is no job ID', async () => {
|
||||
mockJob.id = undefined;
|
||||
|
||||
await expect(async () => {
|
||||
await addSubmitTransactionJob(
|
||||
mockQueue,
|
||||
'mockMspId',
|
||||
'txn',
|
||||
'arg1',
|
||||
'arg2'
|
||||
);
|
||||
}).rejects.toThrowError('Submit transaction job ID not available');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getJobSummary', () => {
|
||||
let mockQueue: MockProxy<Queue>;
|
||||
let mockJob: MockProxy<Job>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockQueue = mock<Queue>();
|
||||
mockJob = mock<Job>();
|
||||
});
|
||||
|
||||
it('throws a JobNotFoundError if the Job is undefined', async () => {
|
||||
mockQueue.getJob.calledWith('1').mockResolvedValue(undefined);
|
||||
|
||||
await expect(async () => {
|
||||
await getJobSummary(mockQueue, '1');
|
||||
}).rejects.toThrow(JobNotFoundError);
|
||||
});
|
||||
|
||||
it('gets a job summary with transaction payload data', async () => {
|
||||
mockQueue.getJob.calledWith('1').mockResolvedValue(mockJob);
|
||||
mockJob.id = '1';
|
||||
mockJob.data = {
|
||||
transactionIds: ['txn1'],
|
||||
};
|
||||
mockJob.returnvalue = {
|
||||
transactionPayload: Buffer.from('MOCK PAYLOAD'),
|
||||
};
|
||||
|
||||
expect(await getJobSummary(mockQueue, '1')).toStrictEqual({
|
||||
jobId: '1',
|
||||
transactionIds: ['txn1'],
|
||||
transactionError: undefined,
|
||||
transactionPayload: 'MOCK PAYLOAD',
|
||||
});
|
||||
});
|
||||
|
||||
it('gets a job summary with empty transaction payload data', async () => {
|
||||
mockQueue.getJob.calledWith('1').mockResolvedValue(mockJob);
|
||||
mockJob.id = '1';
|
||||
mockJob.data = {
|
||||
transactionIds: ['txn1'],
|
||||
};
|
||||
mockJob.returnvalue = {
|
||||
transactionPayload: Buffer.from(''),
|
||||
};
|
||||
|
||||
expect(await getJobSummary(mockQueue, '1')).toStrictEqual({
|
||||
jobId: '1',
|
||||
transactionIds: ['txn1'],
|
||||
transactionError: undefined,
|
||||
transactionPayload: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('gets a job summary with a transaction error', async () => {
|
||||
mockQueue.getJob.calledWith('1').mockResolvedValue(mockJob);
|
||||
mockJob.id = '1';
|
||||
mockJob.data = {
|
||||
transactionIds: ['txn1'],
|
||||
};
|
||||
mockJob.returnvalue = {
|
||||
transactionError: 'MOCK ERROR',
|
||||
};
|
||||
|
||||
expect(await getJobSummary(mockQueue, '1')).toStrictEqual({
|
||||
jobId: '1',
|
||||
transactionIds: ['txn1'],
|
||||
transactionError: 'MOCK ERROR',
|
||||
transactionPayload: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('gets a job summary when there is no return value', async () => {
|
||||
mockQueue.getJob.calledWith('1').mockResolvedValue(mockJob);
|
||||
mockJob.id = '1';
|
||||
mockJob.returnvalue = undefined;
|
||||
mockJob.data = {
|
||||
transactionIds: ['txn1'],
|
||||
};
|
||||
|
||||
expect(await getJobSummary(mockQueue, '1')).toStrictEqual({
|
||||
jobId: '1',
|
||||
transactionIds: ['txn1'],
|
||||
transactionError: undefined,
|
||||
transactionPayload: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('gets a job summary when there is no job data', async () => {
|
||||
mockQueue.getJob.calledWith('1').mockResolvedValue(mockJob);
|
||||
mockJob.id = '1';
|
||||
mockJob.data = undefined;
|
||||
mockJob.returnvalue = {
|
||||
transactionPayload: Buffer.from('MOCK PAYLOAD'),
|
||||
};
|
||||
|
||||
expect(await getJobSummary(mockQueue, '1')).toStrictEqual({
|
||||
jobId: '1',
|
||||
transactionIds: [],
|
||||
transactionError: undefined,
|
||||
transactionPayload: 'MOCK PAYLOAD',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateJobData', () => {
|
||||
let mockJob: MockProxy<Job>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockJob = mock<Job>();
|
||||
mockJob.data = {
|
||||
transactionIds: ['txn1'],
|
||||
};
|
||||
});
|
||||
|
||||
it('stores the serialized state in the job data if a transaction is specified', async () => {
|
||||
const mockSavedState = Buffer.from('MOCK SAVED STATE');
|
||||
const mockTransaction = mock<Transaction>();
|
||||
mockTransaction.getTransactionId.mockReturnValue('txn2');
|
||||
mockTransaction.serialize.mockReturnValue(mockSavedState);
|
||||
|
||||
await updateJobData(mockJob, mockTransaction);
|
||||
|
||||
expect(mockJob.update).toBeCalledTimes(1);
|
||||
expect(mockJob.update).toBeCalledWith({
|
||||
transactionIds: ['txn1', 'txn2'],
|
||||
transactionState: mockSavedState,
|
||||
});
|
||||
});
|
||||
|
||||
it('removes the serialized state from the job data if a transaction is not specified', async () => {
|
||||
await updateJobData(mockJob, undefined);
|
||||
|
||||
expect(mockJob.update).toBeCalledTimes(1);
|
||||
expect(mockJob.update).toBeCalledWith({
|
||||
transactionIds: ['txn1'],
|
||||
transactionState: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getJobCounts', () => {
|
||||
it('gets job counts from the specified queue', async () => {
|
||||
const mockQueue = mock<Queue>();
|
||||
mockQueue.getJobCounts
|
||||
.calledWith('active', 'completed', 'delayed', 'failed', 'waiting')
|
||||
.mockResolvedValue({
|
||||
active: 1,
|
||||
completed: 2,
|
||||
delayed: 3,
|
||||
failed: 4,
|
||||
waiting: 5,
|
||||
});
|
||||
|
||||
expect(await getJobCounts(mockQueue)).toStrictEqual({
|
||||
active: 1,
|
||||
completed: 2,
|
||||
delayed: 3,
|
||||
failed: 4,
|
||||
waiting: 5,
|
||||
});
|
||||
});
|
||||
|
||||
describe('processSubmitTransactionJob', () => {
|
||||
const mockContracts = new Map<string, Contract>();
|
||||
const mockPayload = Buffer.from('MOCK PAYLOAD');
|
||||
const mockSavedState = Buffer.from('MOCK SAVED STATE');
|
||||
let mockTransaction: MockProxy<Transaction>;
|
||||
let mockContract: MockProxy<Contract>;
|
||||
let mockApplication: MockProxy<Application>;
|
||||
let mockJob: MockProxy<Job>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockTransaction = mock<Transaction>();
|
||||
mockTransaction.getTransactionId.mockReturnValue(
|
||||
'mockTransactionId'
|
||||
);
|
||||
|
||||
mockContract = mock<Contract>();
|
||||
mockContract.createTransaction
|
||||
.calledWith('txn')
|
||||
.mockReturnValue(mockTransaction);
|
||||
mockContract.deserializeTransaction
|
||||
.calledWith(mockSavedState)
|
||||
.mockReturnValue(mockTransaction);
|
||||
mockContracts.set('mockMspid', mockContract);
|
||||
|
||||
mockApplication = mock<Application>();
|
||||
mockApplication.locals.mockMspid = { assetContract: mockContract };
|
||||
|
||||
mockJob = mock<Job>();
|
||||
});
|
||||
|
||||
it('gets job result with no error or payload if no contract is available for the required mspid', async () => {
|
||||
mockJob.data = {
|
||||
mspid: 'missingMspid',
|
||||
};
|
||||
|
||||
const jobResult = await processSubmitTransactionJob(
|
||||
mockApplication,
|
||||
mockJob
|
||||
);
|
||||
|
||||
expect(jobResult).toStrictEqual({
|
||||
transactionError: undefined,
|
||||
transactionPayload: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('gets a job result containing a payload if the transaction was successful first time', async () => {
|
||||
mockJob.data = {
|
||||
mspid: 'mockMspid',
|
||||
transactionName: 'txn',
|
||||
transactionArgs: ['arg1', 'arg2'],
|
||||
};
|
||||
mockTransaction.submit
|
||||
.calledWith('arg1', 'arg2')
|
||||
.mockResolvedValue(mockPayload);
|
||||
|
||||
const jobResult = await processSubmitTransactionJob(
|
||||
mockApplication,
|
||||
mockJob
|
||||
);
|
||||
|
||||
expect(jobResult).toStrictEqual({
|
||||
transactionError: undefined,
|
||||
transactionPayload: Buffer.from('MOCK PAYLOAD'),
|
||||
});
|
||||
});
|
||||
|
||||
it('gets a job result containing a payload if the transaction was successfully rerun using saved transaction state', async () => {
|
||||
mockJob.data = {
|
||||
mspid: 'mockMspid',
|
||||
transactionName: 'txn',
|
||||
transactionArgs: ['arg1', 'arg2'],
|
||||
transactionState: mockSavedState,
|
||||
};
|
||||
mockTransaction.submit
|
||||
.calledWith('arg1', 'arg2')
|
||||
.mockResolvedValue(mockPayload);
|
||||
|
||||
const jobResult = await processSubmitTransactionJob(
|
||||
mockApplication,
|
||||
mockJob
|
||||
);
|
||||
|
||||
expect(jobResult).toStrictEqual({
|
||||
transactionError: undefined,
|
||||
transactionPayload: Buffer.from('MOCK PAYLOAD'),
|
||||
});
|
||||
});
|
||||
|
||||
it('gets a job result containing an error message if the transaction fails but cannot be retried', async () => {
|
||||
mockJob.data = {
|
||||
mspid: 'mockMspid',
|
||||
transactionName: 'txn',
|
||||
transactionArgs: ['arg1', 'arg2'],
|
||||
transactionState: mockSavedState,
|
||||
};
|
||||
mockTransaction.submit
|
||||
.calledWith('arg1', 'arg2')
|
||||
.mockRejectedValue(
|
||||
new Error(
|
||||
'Failed to get transaction with id txn, error Entry not found in index'
|
||||
)
|
||||
);
|
||||
|
||||
const jobResult = await processSubmitTransactionJob(
|
||||
mockApplication,
|
||||
mockJob
|
||||
);
|
||||
|
||||
expect(jobResult).toStrictEqual({
|
||||
transactionError:
|
||||
'TransactionNotFoundError: Failed to get transaction with id txn, error Entry not found in index',
|
||||
transactionPayload: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if the transaction fails but can be retried', async () => {
|
||||
mockJob.data = {
|
||||
mspid: 'mockMspid',
|
||||
transactionName: 'txn',
|
||||
transactionArgs: ['arg1', 'arg2'],
|
||||
transactionState: mockSavedState,
|
||||
};
|
||||
mockTransaction.submit
|
||||
.calledWith('arg1', 'arg2')
|
||||
.mockRejectedValue(new Error('MOCK ERROR'));
|
||||
|
||||
await expect(async () => {
|
||||
await processSubmitTransactionJob(mockApplication, mockJob);
|
||||
}).rejects.toThrow('MOCK ERROR');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,346 +0,0 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* This sample uses BullMQ jobs to process submit transactions, which includes
|
||||
* retry support for failing jobs
|
||||
*/
|
||||
|
||||
import { ConnectionOptions, Job, Queue, QueueScheduler, Worker } from 'bullmq';
|
||||
import { Application } from 'express';
|
||||
import { Contract, Transaction } from 'fabric-network';
|
||||
import * as config from './config';
|
||||
import { getRetryAction, RetryAction } from './errors';
|
||||
import { submitTransaction } from './fabric';
|
||||
import { logger } from './logger';
|
||||
|
||||
export type JobData = {
|
||||
mspid: string;
|
||||
transactionName: string;
|
||||
transactionArgs: string[];
|
||||
transactionState?: Buffer;
|
||||
transactionIds: string[];
|
||||
};
|
||||
|
||||
export type JobResult = {
|
||||
transactionPayload?: Buffer;
|
||||
transactionError?: string;
|
||||
};
|
||||
|
||||
export type JobSummary = {
|
||||
jobId: string;
|
||||
transactionIds: string[];
|
||||
transactionPayload?: string;
|
||||
transactionError?: string;
|
||||
};
|
||||
|
||||
export class JobNotFoundError extends Error {
|
||||
jobId: string;
|
||||
|
||||
constructor(message: string, jobId: string) {
|
||||
super(message);
|
||||
Object.setPrototypeOf(this, JobNotFoundError.prototype);
|
||||
|
||||
this.name = 'JobNotFoundError';
|
||||
this.jobId = jobId;
|
||||
}
|
||||
}
|
||||
|
||||
const connection: ConnectionOptions = {
|
||||
port: config.redisPort,
|
||||
host: config.redisHost,
|
||||
username: config.redisUsername,
|
||||
password: config.redisPassword,
|
||||
};
|
||||
|
||||
/**
|
||||
* Set up the queue for submit jobs
|
||||
*/
|
||||
export const initJobQueue = (): Queue => {
|
||||
const submitQueue = new Queue(config.JOB_QUEUE_NAME, {
|
||||
connection,
|
||||
defaultJobOptions: {
|
||||
attempts: config.submitJobAttempts,
|
||||
backoff: {
|
||||
type: config.submitJobBackoffType,
|
||||
delay: config.submitJobBackoffDelay,
|
||||
},
|
||||
removeOnComplete: config.maxCompletedSubmitJobs,
|
||||
removeOnFail: config.maxFailedSubmitJobs,
|
||||
},
|
||||
});
|
||||
|
||||
return submitQueue;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set up a worker to process submit jobs on the queue, using the
|
||||
* processSubmitTransactionJob function below
|
||||
*/
|
||||
export const initJobQueueWorker = (app: Application): Worker => {
|
||||
const worker = new Worker<JobData, JobResult>(
|
||||
config.JOB_QUEUE_NAME,
|
||||
async (job): Promise<JobResult> => {
|
||||
return await processSubmitTransactionJob(app, job);
|
||||
},
|
||||
{ connection, concurrency: config.submitJobConcurrency }
|
||||
);
|
||||
|
||||
worker.on('failed', (job) => {
|
||||
logger.warn({ job }, 'Job failed');
|
||||
});
|
||||
|
||||
// Important: need to handle this error otherwise worker may stop
|
||||
// processing jobs
|
||||
worker.on('error', (err) => {
|
||||
logger.error({ err }, 'Worker error');
|
||||
});
|
||||
|
||||
if (logger.isLevelEnabled('debug')) {
|
||||
worker.on('completed', (job) => {
|
||||
logger.debug({ job }, 'Job completed');
|
||||
});
|
||||
}
|
||||
|
||||
return worker;
|
||||
};
|
||||
|
||||
/**
|
||||
* Process a submit transaction request from the job queue
|
||||
*
|
||||
* The job will be retried if this function throws an error
|
||||
*/
|
||||
export const processSubmitTransactionJob = async (
|
||||
app: Application,
|
||||
job: Job<JobData, JobResult>
|
||||
): Promise<JobResult> => {
|
||||
logger.debug({ jobId: job.id, jobName: job.name }, 'Processing job');
|
||||
|
||||
const contract = app.locals[job.data.mspid]?.assetContract as Contract;
|
||||
if (contract === undefined) {
|
||||
logger.error(
|
||||
{ jobId: job.id, jobName: job.name },
|
||||
'Contract not found for MSP ID %s',
|
||||
job.data.mspid
|
||||
);
|
||||
|
||||
// Retrying will never work without a contract, so give up with an
|
||||
// empty job result
|
||||
return {
|
||||
transactionError: undefined,
|
||||
transactionPayload: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const args = job.data.transactionArgs;
|
||||
let transaction: Transaction;
|
||||
|
||||
if (job.data.transactionState) {
|
||||
const savedState = job.data.transactionState;
|
||||
logger.debug(
|
||||
{
|
||||
jobId: job.id,
|
||||
jobName: job.name,
|
||||
savedState,
|
||||
},
|
||||
'Reusing previously saved transaction state'
|
||||
);
|
||||
|
||||
transaction = contract.deserializeTransaction(savedState);
|
||||
} else {
|
||||
logger.debug(
|
||||
{
|
||||
jobId: job.id,
|
||||
jobName: job.name,
|
||||
},
|
||||
'Using new transaction'
|
||||
);
|
||||
|
||||
transaction = contract.createTransaction(job.data.transactionName);
|
||||
await updateJobData(job, transaction);
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
{
|
||||
jobId: job.id,
|
||||
jobName: job.name,
|
||||
transactionId: transaction.getTransactionId(),
|
||||
},
|
||||
'Submitting transaction'
|
||||
);
|
||||
|
||||
try {
|
||||
const payload = await submitTransaction(transaction, ...args);
|
||||
|
||||
return {
|
||||
transactionError: undefined,
|
||||
transactionPayload: payload,
|
||||
};
|
||||
} catch (err) {
|
||||
const retryAction = getRetryAction(err);
|
||||
|
||||
if (retryAction === RetryAction.None) {
|
||||
logger.error(
|
||||
{ jobId: job.id, jobName: job.name, err },
|
||||
'Fatal transaction error occurred'
|
||||
);
|
||||
|
||||
// Not retriable so return a job result with the error details
|
||||
return {
|
||||
transactionError: `${err}`,
|
||||
transactionPayload: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
{ jobId: job.id, jobName: job.name, err },
|
||||
'Retryable transaction error occurred'
|
||||
);
|
||||
|
||||
if (retryAction === RetryAction.WithNewTransactionId) {
|
||||
logger.debug(
|
||||
{ jobId: job.id, jobName: job.name },
|
||||
'Clearing saved transaction state'
|
||||
);
|
||||
await updateJobData(job, undefined);
|
||||
}
|
||||
|
||||
// Rethrow the error to keep retrying
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set up a scheduler for the submit job queue
|
||||
*
|
||||
* This manages stalled and delayed jobs and is required for retries with backoff
|
||||
*/
|
||||
export const initJobQueueScheduler = (): QueueScheduler => {
|
||||
const queueScheduler = new QueueScheduler(config.JOB_QUEUE_NAME, {
|
||||
connection,
|
||||
});
|
||||
|
||||
queueScheduler.on('failed', (jobId, failedReason) => {
|
||||
logger.error({ jobId, failedReason }, 'Queue sceduler failure');
|
||||
});
|
||||
|
||||
return queueScheduler;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to add a new submit transaction job to the queue
|
||||
*/
|
||||
export const addSubmitTransactionJob = async (
|
||||
submitQueue: Queue<JobData, JobResult>,
|
||||
mspid: string,
|
||||
transactionName: string,
|
||||
...transactionArgs: string[]
|
||||
): Promise<string> => {
|
||||
const jobName = `submit ${transactionName} transaction`;
|
||||
const job = await submitQueue.add(jobName, {
|
||||
mspid,
|
||||
transactionName,
|
||||
transactionArgs: transactionArgs,
|
||||
transactionIds: [],
|
||||
});
|
||||
|
||||
if (job?.id === undefined) {
|
||||
throw new Error('Submit transaction job ID not available');
|
||||
}
|
||||
|
||||
return job.id;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to update the data for an existing job
|
||||
*/
|
||||
export const updateJobData = async (
|
||||
job: Job<JobData, JobResult>,
|
||||
transaction: Transaction | undefined
|
||||
): Promise<void> => {
|
||||
const newData = { ...job.data };
|
||||
|
||||
if (transaction != undefined) {
|
||||
const transationIds = ([] as string[]).concat(
|
||||
newData.transactionIds,
|
||||
transaction.getTransactionId()
|
||||
);
|
||||
newData.transactionIds = transationIds;
|
||||
|
||||
newData.transactionState = transaction.serialize();
|
||||
} else {
|
||||
newData.transactionState = undefined;
|
||||
}
|
||||
|
||||
await job.update(newData);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a job summary
|
||||
*
|
||||
* This function is used for the jobs REST endpoint
|
||||
*/
|
||||
export const getJobSummary = async (
|
||||
queue: Queue,
|
||||
jobId: string
|
||||
): Promise<JobSummary> => {
|
||||
const job: Job<JobData, JobResult> | undefined = await queue.getJob(jobId);
|
||||
logger.debug({ job }, 'Got job');
|
||||
|
||||
if (!(job && job.id != undefined)) {
|
||||
throw new JobNotFoundError(`Job ${jobId} not found`, jobId);
|
||||
}
|
||||
|
||||
let transactionIds: string[];
|
||||
if (job.data && job.data.transactionIds) {
|
||||
transactionIds = job.data.transactionIds;
|
||||
} else {
|
||||
transactionIds = [];
|
||||
}
|
||||
|
||||
let transactionError;
|
||||
let transactionPayload;
|
||||
const returnValue = job.returnvalue;
|
||||
if (returnValue) {
|
||||
if (returnValue.transactionError) {
|
||||
transactionError = returnValue.transactionError;
|
||||
}
|
||||
|
||||
if (
|
||||
returnValue.transactionPayload &&
|
||||
returnValue.transactionPayload.length > 0
|
||||
) {
|
||||
transactionPayload = returnValue.transactionPayload.toString();
|
||||
} else {
|
||||
transactionPayload = '';
|
||||
}
|
||||
}
|
||||
|
||||
const jobSummary: JobSummary = {
|
||||
jobId: job.id,
|
||||
transactionIds,
|
||||
transactionError,
|
||||
transactionPayload,
|
||||
};
|
||||
|
||||
return jobSummary;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the current job counts
|
||||
*
|
||||
* This function is used for the liveness REST endpoint
|
||||
*/
|
||||
export const getJobCounts = async (
|
||||
queue: Queue
|
||||
): Promise<{ [index: string]: number }> => {
|
||||
const jobCounts = await queue.getJobCounts(
|
||||
'active',
|
||||
'completed',
|
||||
'delayed',
|
||||
'failed',
|
||||
'waiting'
|
||||
);
|
||||
logger.debug({ jobCounts }, 'Current job counts');
|
||||
|
||||
return jobCounts;
|
||||
};
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import pino from 'pino';
|
||||
import * as config from './config';
|
||||
|
||||
export const logger = pino({
|
||||
level: config.logLevel,
|
||||
});
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { isMaxmemoryPolicyNoeviction } from './redis';
|
||||
|
||||
const mockRedisConfig = jest.fn();
|
||||
jest.mock('ioredis', () => {
|
||||
return jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
config: mockRedisConfig,
|
||||
disconnect: jest.fn(),
|
||||
};
|
||||
});
|
||||
});
|
||||
jest.mock('./config');
|
||||
|
||||
describe('Redis', () => {
|
||||
beforeEach(() => {
|
||||
mockRedisConfig.mockClear();
|
||||
});
|
||||
|
||||
describe('isMaxmemoryPolicyNoeviction', () => {
|
||||
it('returns true when the maxmemory-policy is noeviction', async () => {
|
||||
mockRedisConfig.mockReturnValue(['maxmemory-policy', 'noeviction']);
|
||||
expect(await isMaxmemoryPolicyNoeviction()).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when the maxmemory-policy is not noeviction', async () => {
|
||||
mockRedisConfig.mockReturnValue([
|
||||
'maxmemory-policy',
|
||||
'allkeys-lru',
|
||||
]);
|
||||
expect(await isMaxmemoryPolicyNoeviction()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* This sample uses the BullMQ queue system, which is built on top of Redis
|
||||
*/
|
||||
|
||||
import IORedis, { Redis, RedisOptions } from 'ioredis';
|
||||
|
||||
import * as config from './config';
|
||||
import { logger } from './logger';
|
||||
|
||||
/**
|
||||
* Check whether the maxmemory-policy config is set to noeviction
|
||||
*
|
||||
* BullMQ requires this setting in redis
|
||||
* For details, see: https://docs.bullmq.io/guide/connections
|
||||
*/
|
||||
export const isMaxmemoryPolicyNoeviction = async (): Promise<boolean> => {
|
||||
let redis: Redis | undefined;
|
||||
|
||||
const redisOptions: RedisOptions = {
|
||||
port: config.redisPort,
|
||||
host: config.redisHost,
|
||||
username: config.redisUsername,
|
||||
password: config.redisPassword,
|
||||
};
|
||||
|
||||
try {
|
||||
redis = new IORedis(redisOptions);
|
||||
|
||||
const maxmemoryPolicyConfig = await (redis as Redis).config(
|
||||
'GET',
|
||||
'maxmemory-policy'
|
||||
);
|
||||
logger.debug({ maxmemoryPolicyConfig }, 'Got maxmemory-policy config');
|
||||
|
||||
if (
|
||||
maxmemoryPolicyConfig.length == 2 &&
|
||||
'maxmemory-policy' === maxmemoryPolicyConfig[0] &&
|
||||
'noeviction' === maxmemoryPolicyConfig[1]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
} finally {
|
||||
if (redis != undefined) {
|
||||
redis.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import express, { Application, NextFunction, Request, Response } from 'express';
|
||||
import helmet from 'helmet';
|
||||
import { getReasonPhrase, StatusCodes } from 'http-status-codes';
|
||||
import passport from 'passport';
|
||||
import pinoMiddleware from 'pino-http';
|
||||
import { assetsRouter } from './assets.router';
|
||||
import { authenticateApiKey, fabricAPIKeyStrategy } from './auth';
|
||||
import { healthRouter } from './health.router';
|
||||
import { jobsRouter } from './jobs.router';
|
||||
import { logger } from './logger';
|
||||
import { transactionsRouter } from './transactions.router';
|
||||
import cors from 'cors';
|
||||
|
||||
const { BAD_REQUEST, INTERNAL_SERVER_ERROR, NOT_FOUND } = StatusCodes;
|
||||
|
||||
export const createServer = async (): Promise<Application> => {
|
||||
const app = express();
|
||||
|
||||
app.use(
|
||||
pinoMiddleware({
|
||||
logger,
|
||||
customLogLevel: function customLogLevel(res, err) {
|
||||
if (
|
||||
res.statusCode >= BAD_REQUEST &&
|
||||
res.statusCode < INTERNAL_SERVER_ERROR
|
||||
) {
|
||||
return 'warn';
|
||||
}
|
||||
|
||||
if (res.statusCode >= INTERNAL_SERVER_ERROR || err) {
|
||||
return 'error';
|
||||
}
|
||||
|
||||
return 'debug';
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// define passport startegy
|
||||
passport.use(fabricAPIKeyStrategy);
|
||||
|
||||
// initialize passport js
|
||||
app.use(passport.initialize());
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
app.use(cors());
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
// TBC
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
app.use(helmet());
|
||||
}
|
||||
|
||||
app.use('/', healthRouter);
|
||||
app.use('/api/assets', authenticateApiKey, assetsRouter);
|
||||
app.use('/api/jobs', authenticateApiKey, jobsRouter);
|
||||
app.use('/api/transactions', authenticateApiKey, transactionsRouter);
|
||||
|
||||
// For everything else
|
||||
app.use((_req, res) =>
|
||||
res.status(NOT_FOUND).json({
|
||||
status: getReasonPhrase(NOT_FOUND),
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
);
|
||||
|
||||
// Print API errors
|
||||
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
|
||||
logger.error(err);
|
||||
return res.status(INTERNAL_SERVER_ERROR).json({
|
||||
status: getReasonPhrase(INTERNAL_SERVER_ERROR),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
return app;
|
||||
};
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import express, { Request, Response } from 'express';
|
||||
import { Contract } from 'fabric-network';
|
||||
import { getReasonPhrase, StatusCodes } from 'http-status-codes';
|
||||
import { getTransactionValidationCode } from './fabric';
|
||||
import { logger } from './logger';
|
||||
import { TransactionNotFoundError } from './errors';
|
||||
|
||||
const { INTERNAL_SERVER_ERROR, NOT_FOUND, OK } = StatusCodes;
|
||||
|
||||
export const transactionsRouter = express.Router();
|
||||
|
||||
transactionsRouter.get(
|
||||
'/:transactionId',
|
||||
async (req: Request, res: Response) => {
|
||||
const mspId = req.user as string;
|
||||
const transactionId = req.params.transactionId;
|
||||
logger.debug(
|
||||
'Read request received for transaction ID %s',
|
||||
transactionId
|
||||
);
|
||||
|
||||
try {
|
||||
const qsccContract = req.app.locals[mspId]
|
||||
?.qsccContract as Contract;
|
||||
|
||||
const validationCode = await getTransactionValidationCode(
|
||||
qsccContract,
|
||||
transactionId
|
||||
);
|
||||
|
||||
return res.status(OK).json({
|
||||
transactionId,
|
||||
validationCode,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof TransactionNotFoundError) {
|
||||
return res.status(NOT_FOUND).json({
|
||||
status: getReasonPhrase(NOT_FOUND),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
logger.error(
|
||||
{ err },
|
||||
'Error processing read request for transaction ID %s',
|
||||
transactionId
|
||||
);
|
||||
|
||||
return res.status(INTERNAL_SERVER_ERROR).json({
|
||||
status: getReasonPhrase(INTERNAL_SERVER_ERROR),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@tsconfig/node12/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": [
|
||||
"src/"
|
||||
]
|
||||
}
|
||||
Loading…
Reference in a new issue