mirror of
https://github.com/hyperledger/fabric-samples.git
synced 2026-06-21 00:55:10 +00:00
Add first batch of pull request rework
- update Application section in README
- remove param name in app.go
- add error checks in processor/block.go
- move vars from model to transact logic
- move newAsset to transact
- use ID for well-known initialisms
- move randomelement, randomnint and differentelement to transact
- remove AssertDefined
- blockTxIdsJoinedByComma: use standard library to join elements
- return nil, instead of []byte{}
- remove go routine in listen.go
- move cache to parser
- inline processor in listen.go
- move store to main package
- move util to main package
- fixed failing cache issue
- fixed staticcheck issues
- removed cache function, implemented caching in the structs and methods
Signed-off-by: Stanislav Jakuschevskij <stas@two-giants.com>
This commit is contained in:
parent
40e627dcf5
commit
fd1a1fc38b
22 changed files with 454 additions and 694 deletions
|
|
@ -19,12 +19,15 @@ The client application provides several "commands" that can be invoked using the
|
||||||
- **getAllAssets**: Retrieve the current details of all assets recorded on the ledger. See:
|
- **getAllAssets**: Retrieve the current details of all assets recorded on the ledger. See:
|
||||||
- TypeScript: [application-typescript/src/getAllAssets.ts](application-typescript/src/getAllAssets.ts)
|
- TypeScript: [application-typescript/src/getAllAssets.ts](application-typescript/src/getAllAssets.ts)
|
||||||
- Java: [application-java/app/src/main/java/GetAllAssets.java](application-java/app/src/main/java/GetAllAssets.java)
|
- Java: [application-java/app/src/main/java/GetAllAssets.java](application-java/app/src/main/java/GetAllAssets.java)
|
||||||
|
- Go: [application-go/getAllAssets.go](application-go/getAllAssets.go)
|
||||||
- **listen**: Listen for block events, and use them to replicate ledger updates in an off-chain data store. See:
|
- **listen**: Listen for block events, and use them to replicate ledger updates in an off-chain data store. See:
|
||||||
- TypeScript: [application-typescript/src/listen.ts](application-typescript/src/listen.ts)
|
- TypeScript: [application-typescript/src/listen.ts](application-typescript/src/listen.ts)
|
||||||
- Java: [application-java/app/src/main/java/Listen.java](application-java/app/src/main/java/Listen.java)
|
- Java: [application-java/app/src/main/java/Listen.java](application-java/app/src/main/java/Listen.java)
|
||||||
|
- Go: [application-go/listen.go](application-go/listen.go)
|
||||||
- **transact**: Submit a set of transactions to create, modify and delete assets. See:
|
- **transact**: Submit a set of transactions to create, modify and delete assets. See:
|
||||||
- TypeScript: [application-typescript/src/transact.ts](application-typescript/src/transact.ts)
|
- TypeScript: [application-typescript/src/transact.ts](application-typescript/src/transact.ts)
|
||||||
- Java: [application-java/app/src/main/java/Transact.java](application-java/app/src/main/java/Transact.java)
|
- Java: [application-java/app/src/main/java/Transact.java](application-java/app/src/main/java/Transact.java)
|
||||||
|
- Go: [application-go/transact.go](application-go/transact.go)
|
||||||
|
|
||||||
To keep the sample code concise, the **listen** command writes ledger updates to an output file named `store.log` in the current working directory (which for the Java sample is the `application-java/app` directory). A real implementation could write ledger updates directly to an off-chain data store of choice. You can inspect the information captured in this file as you run the sample.
|
To keep the sample code concise, the **listen** command writes ledger updates to an output file named `store.log` in the current working directory (which for the Java sample is the `application-java/app` directory). A real implementation could write ledger updates directly to an off-chain data store of choice. You can inspect the information captured in this file as you run the sample.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import (
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
var allCommands = map[string]func(clientConnection *grpc.ClientConn){
|
var allCommands = map[string]func(*grpc.ClientConn){
|
||||||
"getAllAssets": getAllAssets,
|
"getAllAssets": getAllAssets,
|
||||||
"transact": transact,
|
"transact": transact,
|
||||||
"listen": listen,
|
"listen": listen,
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ package main
|
||||||
import (
|
import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
"offChainData/utils"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -24,27 +23,27 @@ import (
|
||||||
const peerName = "peer0.org1.example.com"
|
const peerName = "peer0.org1.example.com"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
channelName = utils.EnvOrDefault("CHANNEL_NAME", "mychannel")
|
channelName = envOrDefault("CHANNEL_NAME", "mychannel")
|
||||||
chaincodeName = utils.EnvOrDefault("CHAINCODE_NAME", "basic")
|
chaincodeName = envOrDefault("CHAINCODE_NAME", "basic")
|
||||||
mspID = utils.EnvOrDefault("MSP_ID", "Org1MSP")
|
mspID = envOrDefault("MSP_ID", "Org1MSP")
|
||||||
|
|
||||||
// Path to crypto materials.
|
// Path to crypto materials.
|
||||||
cryptoPath = utils.EnvOrDefault("CRYPTO_PATH", "../../test-network/organizations/peerOrganizations/org1.example.com")
|
cryptoPath = envOrDefault("CRYPTO_PATH", "../../test-network/organizations/peerOrganizations/org1.example.com")
|
||||||
|
|
||||||
// Path to user private key directory.
|
// Path to user private key directory.
|
||||||
keyDirectoryPath = utils.EnvOrDefault("KEY_DIRECTORY_PATH", cryptoPath+"/users/User1@org1.example.com/msp/keystore")
|
keyDirectoryPath = envOrDefault("KEY_DIRECTORY_PATH", cryptoPath+"/users/User1@org1.example.com/msp/keystore")
|
||||||
|
|
||||||
// Path to user certificate.
|
// Path to user certificate.
|
||||||
certPath = utils.EnvOrDefault("CERT_PATH", cryptoPath+"/users/User1@org1.example.com/msp/signcerts/cert.pem")
|
certPath = envOrDefault("CERT_PATH", cryptoPath+"/users/User1@org1.example.com/msp/signcerts/cert.pem")
|
||||||
|
|
||||||
// Path to peer tls certificate.
|
// Path to peer tls certificate.
|
||||||
tlsCertPath = utils.EnvOrDefault("TLS_CERT_PATH", cryptoPath+"/peers/peer0.org1.example.com/tls/ca.crt")
|
tlsCertPath = envOrDefault("TLS_CERT_PATH", cryptoPath+"/peers/peer0.org1.example.com/tls/ca.crt")
|
||||||
|
|
||||||
// Gateway peer endpoint.
|
// Gateway peer endpoint.
|
||||||
peerEndpoint = utils.EnvOrDefault("PEER_ENDPOINT", "dns:///localhost:7051")
|
peerEndpoint = envOrDefault("PEER_ENDPOINT", "dns:///localhost:7051")
|
||||||
|
|
||||||
// Gateway peer SSL host name override.
|
// Gateway peer SSL host name override.
|
||||||
peerHostAlias = utils.EnvOrDefault("PEER_HOST_ALIAS", peerName)
|
peerHostAlias = envOrDefault("PEER_HOST_ALIAS", peerName)
|
||||||
)
|
)
|
||||||
|
|
||||||
func newGrpcConnection() *grpc.ClientConn {
|
func newGrpcConnection() *grpc.ClientConn {
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ func (atb *AssetTransferBasic) DeleteAsset(id string) error {
|
||||||
func (atb *AssetTransferBasic) GetAllAssets() ([]byte, error) {
|
func (atb *AssetTransferBasic) GetAllAssets() ([]byte, error) {
|
||||||
result, err := atb.contract.Evaluate("GetAllAssets")
|
result, err := atb.contract.Evaluate("GetAllAssets")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []byte{}, fmt.Errorf("in GetAllAssets: %w", err)
|
return nil, fmt.Errorf("in GetAllAssets: %w", err)
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,22 +5,6 @@
|
||||||
*/
|
*/
|
||||||
package contract
|
package contract
|
||||||
|
|
||||||
import (
|
|
||||||
"offChainData/utils"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
colors = []string{"red", "green", "blue"}
|
|
||||||
Owners = []string{"alice", "bob", "charlie"}
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
maxInitialValue = 1000
|
|
||||||
maxInitialSize = 10
|
|
||||||
)
|
|
||||||
|
|
||||||
type Asset struct {
|
type Asset struct {
|
||||||
ID string `json:"ID"`
|
ID string `json:"ID"`
|
||||||
Color string `json:"Color"`
|
Color string `json:"Color"`
|
||||||
|
|
@ -28,18 +12,3 @@ type Asset struct {
|
||||||
Owner string `json:"Owner"`
|
Owner string `json:"Owner"`
|
||||||
AppraisedValue uint64 `json:"AppraisedValue"`
|
AppraisedValue uint64 `json:"AppraisedValue"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAsset() Asset {
|
|
||||||
id, err := uuid.NewRandom()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Asset{
|
|
||||||
ID: id.String(),
|
|
||||||
Color: utils.RandomElement(colors),
|
|
||||||
Size: uint64(utils.RandomInt(maxInitialSize) + 1),
|
|
||||||
Owner: utils.RandomElement(Owners),
|
|
||||||
AppraisedValue: uint64(utils.RandomInt(maxInitialValue) + 1),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
atb "offChainData/contract"
|
atb "offchaindata/contract"
|
||||||
|
|
||||||
"github.com/hyperledger/fabric-gateway/pkg/client"
|
"github.com/hyperledger/fabric-gateway/pkg/client"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
module offChainData
|
module offchaindata
|
||||||
|
|
||||||
go 1.22.0
|
go 1.22.0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,11 @@ package main
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"offChainData/parser"
|
"offchaindata/parser"
|
||||||
"offChainData/processor"
|
|
||||||
"offChainData/store"
|
|
||||||
"offChainData/utils"
|
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"sync"
|
"slices"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/hyperledger/fabric-gateway/pkg/client"
|
"github.com/hyperledger/fabric-gateway/pkg/client"
|
||||||
|
|
@ -27,7 +25,7 @@ func listen(clientConnection *grpc.ClientConn) {
|
||||||
fmt.Println("Gateway closed.")
|
fmt.Println("Gateway closed.")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
checkpointFile := utils.EnvOrDefault("CHECKPOINT_FILE", "checkpoint.json")
|
checkpointFile := envOrDefault("CHECKPOINT_FILE", "checkpoint.json")
|
||||||
checkpointer, err := client.NewFileCheckpointer(checkpointFile)
|
checkpointer, err := client.NewFileCheckpointer(checkpointFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|
@ -39,8 +37,8 @@ func listen(clientConnection *grpc.ClientConn) {
|
||||||
|
|
||||||
fmt.Println("Start event listening from block", checkpointer.BlockNumber())
|
fmt.Println("Start event listening from block", checkpointer.BlockNumber())
|
||||||
fmt.Println("Last processed transaction ID within block:", checkpointer.TransactionID())
|
fmt.Println("Last processed transaction ID within block:", checkpointer.TransactionID())
|
||||||
if store.SimulatedFailureCount > 0 {
|
if simulatedFailureCount > 0 {
|
||||||
fmt.Printf("Simulating a write failure every %d transactions\n", store.SimulatedFailureCount)
|
fmt.Printf("Simulating a write failure every %d transactions\n", simulatedFailureCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, close := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
ctx, close := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
@ -62,32 +60,251 @@ func listen(clientConnection *grpc.ClientConn) {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
for blockProto := range blocks {
|
||||||
wg.Add(1)
|
aBlockProcessor := blockProcessor{
|
||||||
|
parser.ParseBlock(blockProto),
|
||||||
go func() {
|
checkpointer,
|
||||||
defer wg.Done()
|
applyWritesToOffChainStore,
|
||||||
|
channelName,
|
||||||
for blockProto := range blocks {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
blockProcessor := processor.NewBlock(
|
|
||||||
parser.ParseBlock(blockProto),
|
|
||||||
checkpointer,
|
|
||||||
store.ApplyWritesToOffChainStore,
|
|
||||||
channelName,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err := blockProcessor.Process(); err != nil {
|
|
||||||
fmt.Println("\033[31m[ERROR]\033[0m", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
|
|
||||||
wg.Wait()
|
if err := aBlockProcessor.process(); err != nil {
|
||||||
|
fmt.Println("\033[31m[ERROR]\033[0m", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println("\nShutting down listener gracefully...")
|
fmt.Println("\nShutting down listener gracefully...")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type blockProcessor struct {
|
||||||
|
parsedBlock *parser.Block
|
||||||
|
checkpointer *client.FileCheckpointer
|
||||||
|
writeToStore writer
|
||||||
|
channelName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *blockProcessor) process() error {
|
||||||
|
funcName := "Process"
|
||||||
|
|
||||||
|
fmt.Println("\nReceived block", b.parsedBlock.Number())
|
||||||
|
|
||||||
|
validTransactions, err := b.validTransactions()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("in %s: %w", funcName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, validTransaction := range validTransactions {
|
||||||
|
aTransaction := transactionProcessor{
|
||||||
|
b.parsedBlock.Number(),
|
||||||
|
validTransaction,
|
||||||
|
// TODO use pointer to parent and get blockNumber, store and channelName from parent
|
||||||
|
b.writeToStore,
|
||||||
|
b.channelName,
|
||||||
|
}
|
||||||
|
if err := aTransaction.process(); err != nil {
|
||||||
|
return fmt.Errorf("in %s: %w", funcName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
channelHeader, err := validTransaction.ChannelHeader()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("in %s: %w", funcName, err)
|
||||||
|
}
|
||||||
|
transactionID := channelHeader.GetTxId()
|
||||||
|
if err := b.checkpointer.CheckpointTransaction(b.parsedBlock.Number(), transactionID); err != nil {
|
||||||
|
return fmt.Errorf("in %s: %w", funcName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.checkpointer.CheckpointBlock(b.parsedBlock.Number()); err != nil {
|
||||||
|
return fmt.Errorf("in %s: %w", funcName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *blockProcessor) validTransactions() ([]*parser.Transaction, error) {
|
||||||
|
result := []*parser.Transaction{}
|
||||||
|
newTransactions, err := b.getNewTransactions()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("in validTransactions: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, transaction := range newTransactions {
|
||||||
|
if transaction.IsValid() {
|
||||||
|
result = append(result, transaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *blockProcessor) getNewTransactions() ([]*parser.Transaction, error) {
|
||||||
|
funcName := "getNewTransactions"
|
||||||
|
|
||||||
|
transactions, err := b.parsedBlock.Transactions()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("in %s: %w", funcName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lastTransactionID := b.checkpointer.TransactionID()
|
||||||
|
if lastTransactionID == "" {
|
||||||
|
// No previously processed transactions within this block so all are new
|
||||||
|
return transactions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore transactions up to the last processed transaction ID
|
||||||
|
lastProcessedIndex, err := b.findLastProcessedIndex()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("in %s: %w", funcName, err)
|
||||||
|
}
|
||||||
|
return transactions[lastProcessedIndex+1:], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *blockProcessor) findLastProcessedIndex() (int, error) {
|
||||||
|
funcName := "findLastProcessedIndex"
|
||||||
|
|
||||||
|
transactions, err := b.parsedBlock.Transactions()
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("in %s: %w", funcName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
blockTransactionIDs := []string{}
|
||||||
|
for _, transaction := range transactions {
|
||||||
|
channelHeader, err := transaction.ChannelHeader()
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("in %s: %w", funcName, err)
|
||||||
|
}
|
||||||
|
blockTransactionIDs = append(blockTransactionIDs, channelHeader.GetTxId())
|
||||||
|
}
|
||||||
|
|
||||||
|
lastTransactionID := b.checkpointer.TransactionID()
|
||||||
|
lastProcessedIndex := -1
|
||||||
|
for index, id := range blockTransactionIDs {
|
||||||
|
if id == lastTransactionID {
|
||||||
|
lastProcessedIndex = index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if lastProcessedIndex < 0 {
|
||||||
|
return lastProcessedIndex, newTxIDNotFoundError(
|
||||||
|
lastTransactionID,
|
||||||
|
b.parsedBlock.Number(),
|
||||||
|
blockTransactionIDs,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastProcessedIndex, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type transactionProcessor struct {
|
||||||
|
blockNumber uint64
|
||||||
|
transaction *parser.Transaction
|
||||||
|
writeToStore writer
|
||||||
|
channelName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *transactionProcessor) process() error {
|
||||||
|
funcName := "process"
|
||||||
|
|
||||||
|
channelHeader, err := t.transaction.ChannelHeader()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("in %s: %w", funcName, err)
|
||||||
|
}
|
||||||
|
transactionID := channelHeader.GetTxId()
|
||||||
|
|
||||||
|
writes, err := t.writes()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("in %s: %w", funcName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(writes) == 0 {
|
||||||
|
fmt.Println("Skipping read-only or system transaction", transactionID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Process transaction", transactionID)
|
||||||
|
|
||||||
|
if err := t.writeToStore(ledgerUpdate{
|
||||||
|
BlockNumber: t.blockNumber,
|
||||||
|
TransactionID: transactionID,
|
||||||
|
Writes: writes,
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("in %s: %w", funcName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *transactionProcessor) writes() ([]write, error) {
|
||||||
|
funcName := "writes"
|
||||||
|
// TODO this entire code should live in the parser and just return the kvWrite which
|
||||||
|
// we then map to write and return
|
||||||
|
channelHeader, err := t.transaction.ChannelHeader()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("in %s: %w", funcName, err)
|
||||||
|
}
|
||||||
|
t.channelName = channelHeader.GetChannelId()
|
||||||
|
|
||||||
|
nsReadWriteSets, err := t.transaction.NamespaceReadWriteSets()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("in %s: %w", funcName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nonSystemCCReadWriteSets := []*parser.NamespaceReadWriteSet{}
|
||||||
|
for _, nsReadWriteSet := range nsReadWriteSets {
|
||||||
|
if !t.isSystemChaincode(nsReadWriteSet.Namespace()) {
|
||||||
|
nonSystemCCReadWriteSets = append(nonSystemCCReadWriteSets, nsReadWriteSet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writes := []write{}
|
||||||
|
for _, readWriteSet := range nonSystemCCReadWriteSets {
|
||||||
|
namespace := readWriteSet.Namespace()
|
||||||
|
|
||||||
|
kvReadWriteSet, err := readWriteSet.ReadWriteSet()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("in %s: %w", funcName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, kvWrite := range kvReadWriteSet.GetWrites() {
|
||||||
|
writes = append(writes, write{
|
||||||
|
ChannelName: t.channelName,
|
||||||
|
Namespace: namespace,
|
||||||
|
Key: kvWrite.GetKey(),
|
||||||
|
IsDelete: kvWrite.GetIsDelete(),
|
||||||
|
Value: string(kvWrite.GetValue()), // Convert bytes to text, purely for readability in output
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return writes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *transactionProcessor) isSystemChaincode(chaincodeName string) bool {
|
||||||
|
systemChaincodeNames := []string{
|
||||||
|
"_lifecycle",
|
||||||
|
"cscc",
|
||||||
|
"escc",
|
||||||
|
"lscc",
|
||||||
|
"qscc",
|
||||||
|
"vscc",
|
||||||
|
}
|
||||||
|
return slices.Contains(systemChaincodeNames, chaincodeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
type txIDNotFoundError struct {
|
||||||
|
txID string
|
||||||
|
blockNumber uint64
|
||||||
|
blockTxIDs []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTxIDNotFoundError(txID string, blockNumber uint64, blockTxIds []string) *txIDNotFoundError {
|
||||||
|
return &txIDNotFoundError{
|
||||||
|
txID, blockNumber, blockTxIds,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *txIDNotFoundError) Error() string {
|
||||||
|
format := "checkpoint transaction ID %s not found in block %d containing transactions: %s"
|
||||||
|
return fmt.Sprintf(format, t.txID, t.blockNumber, strings.Join(t.blockTxIDs, ", "))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,49 +2,48 @@ package parser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"offChainData/utils"
|
|
||||||
|
|
||||||
"github.com/hyperledger/fabric-protos-go-apiv2/common"
|
"github.com/hyperledger/fabric-protos-go-apiv2/common"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Block struct {
|
type Block struct {
|
||||||
block *common.Block
|
block *common.Block
|
||||||
transactions []*Transaction
|
cachedTransactions []*Transaction
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseBlock(block *common.Block) *Block {
|
func ParseBlock(block *common.Block) *Block {
|
||||||
return &Block{block, []*Transaction{}}
|
return &Block{block, nil}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Block) Number() (uint64, error) {
|
func (b *Block) Number() uint64 {
|
||||||
header, err := utils.AssertDefined(b.block.GetHeader(), "missing block header")
|
return b.block.GetHeader().GetNumber()
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("in Number: %w", err)
|
|
||||||
}
|
|
||||||
return header.GetNumber(), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Block) Transactions() ([]*Transaction, error) {
|
func (b *Block) Transactions() ([]*Transaction, error) {
|
||||||
return utils.Cache(func() ([]*Transaction, error) {
|
if b.cachedTransactions != nil {
|
||||||
funcName := "Transactions"
|
return b.cachedTransactions, nil
|
||||||
envelopes, err := b.unmarshalEnvelopesFromBlockData()
|
}
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("in %s: %w", funcName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
commonPayloads, err := b.unmarshalPayloadsFrom(envelopes)
|
funcName := "Transactions"
|
||||||
if err != nil {
|
envelopes, err := b.unmarshalEnvelopesFromBlockData()
|
||||||
return nil, fmt.Errorf("in %s: %w", funcName, err)
|
if err != nil {
|
||||||
}
|
return nil, fmt.Errorf("in %s: %w", funcName, err)
|
||||||
|
}
|
||||||
|
|
||||||
payloads, err := b.parse(commonPayloads)
|
commonPayloads, err := b.unmarshalPayloadsFrom(envelopes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("in %s: %w", funcName, err)
|
return nil, fmt.Errorf("in %s: %w", funcName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.createTransactionsFrom(payloads), nil
|
payloads, err := b.parse(commonPayloads)
|
||||||
})()
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("in %s: %w", funcName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.cachedTransactions = b.createTransactionsFrom(payloads)
|
||||||
|
|
||||||
|
return b.cachedTransactions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Block) unmarshalEnvelopesFromBlockData() ([]*common.Envelope, error) {
|
func (b *Block) unmarshalEnvelopesFromBlockData() ([]*common.Envelope, error) {
|
||||||
|
|
@ -74,20 +73,11 @@ func (*Block) unmarshalPayloadsFrom(envelopes []*common.Envelope) ([]*common.Pay
|
||||||
func (b *Block) parse(commonPayloads []*common.Payload) ([]*payload, error) {
|
func (b *Block) parse(commonPayloads []*common.Payload) ([]*payload, error) {
|
||||||
funcName := "parse"
|
funcName := "parse"
|
||||||
|
|
||||||
validationCodes, err := b.extractTransactionValidationCodes()
|
validationCodes := b.block.GetMetadata().GetMetadata()[common.BlockMetadataIndex_TRANSACTIONS_FILTER]
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("in %s: %w", funcName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result := []*payload{}
|
result := []*payload{}
|
||||||
for i, commonPayload := range commonPayloads {
|
for i, commonPayload := range commonPayloads {
|
||||||
statusCode, err := utils.AssertDefined(
|
statusCode := validationCodes[i]
|
||||||
validationCodes[i],
|
|
||||||
fmt.Sprint("missing validation code index", i),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("in %s: %w", funcName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
payload := parsePayload(commonPayload, int32(statusCode))
|
payload := parsePayload(commonPayload, int32(statusCode))
|
||||||
is, err := payload.isEndorserTransaction()
|
is, err := payload.isEndorserTransaction()
|
||||||
|
|
@ -102,21 +92,6 @@ func (b *Block) parse(commonPayloads []*common.Payload) ([]*payload, error) {
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Block) extractTransactionValidationCodes() ([]byte, error) {
|
|
||||||
metadata, err := utils.AssertDefined(
|
|
||||||
b.block.GetMetadata(),
|
|
||||||
"missing block metadata",
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("in extractTransactionValidationCodes: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return utils.AssertDefined(
|
|
||||||
metadata.GetMetadata()[common.BlockMetadataIndex_TRANSACTIONS_FILTER],
|
|
||||||
"missing transaction validation code",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*Block) createTransactionsFrom(payloads []*payload) []*Transaction {
|
func (*Block) createTransactionsFrom(payloads []*payload) []*Transaction {
|
||||||
result := []*Transaction{}
|
result := []*Transaction{}
|
||||||
for _, payload := range payloads {
|
for _, payload := range payloads {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
atb "offChainData/contract"
|
atb "offchaindata/contract"
|
||||||
|
|
||||||
"github.com/hyperledger/fabric-protos-go-apiv2/ledger/rwset"
|
"github.com/hyperledger/fabric-protos-go-apiv2/ledger/rwset"
|
||||||
"github.com/hyperledger/fabric-protos-go-apiv2/ledger/rwset/kvrwset"
|
"github.com/hyperledger/fabric-protos-go-apiv2/ledger/rwset/kvrwset"
|
||||||
|
|
@ -107,7 +107,13 @@ func Test_NamespaceReadWriteSetParsing(t *testing.T) {
|
||||||
|
|
||||||
func nsReadWriteSetFake() (*rwset.NsReadWriteSet, string, atb.Asset) {
|
func nsReadWriteSetFake() (*rwset.NsReadWriteSet, string, atb.Asset) {
|
||||||
expectedNamespace := "basic"
|
expectedNamespace := "basic"
|
||||||
expectedAsset := atb.NewAsset()
|
expectedAsset := atb.Asset{
|
||||||
|
ID: "id-1",
|
||||||
|
Color: "green",
|
||||||
|
Size: 8,
|
||||||
|
Owner: "Alice",
|
||||||
|
AppraisedValue: 346,
|
||||||
|
}
|
||||||
|
|
||||||
result := &rwset.NsReadWriteSet{
|
result := &rwset.NsReadWriteSet{
|
||||||
Namespace: expectedNamespace,
|
Namespace: expectedNamespace,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package parser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"offChainData/utils"
|
|
||||||
|
|
||||||
"github.com/hyperledger/fabric-protos-go-apiv2/ledger/rwset"
|
"github.com/hyperledger/fabric-protos-go-apiv2/ledger/rwset"
|
||||||
"github.com/hyperledger/fabric-protos-go-apiv2/peer"
|
"github.com/hyperledger/fabric-protos-go-apiv2/peer"
|
||||||
|
|
@ -10,43 +9,49 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type endorserTransaction struct {
|
type endorserTransaction struct {
|
||||||
transaction *peer.Transaction
|
transaction *peer.Transaction
|
||||||
|
cachedReadWriteSets []*readWriteSet
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseEndorserTransaction(transaction *peer.Transaction) *endorserTransaction {
|
func parseEndorserTransaction(transaction *peer.Transaction) *endorserTransaction {
|
||||||
return &endorserTransaction{transaction}
|
return &endorserTransaction{transaction, nil}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *endorserTransaction) readWriteSets() ([]*readWriteSet, error) {
|
func (p *endorserTransaction) readWriteSets() ([]*readWriteSet, error) {
|
||||||
return utils.Cache(func() ([]*readWriteSet, error) {
|
funcName := "readWriteSets"
|
||||||
funcName := "readWriteSets"
|
|
||||||
chaincodeActionPayloads, err := p.unmarshalChaincodeActionPayloads()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("in %s: %w", funcName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
chaincodeEndorsedActions, err := p.extractChaincodeEndorsedActionsFrom(chaincodeActionPayloads)
|
if p.cachedReadWriteSets != nil {
|
||||||
if err != nil {
|
return p.cachedReadWriteSets, nil
|
||||||
return nil, fmt.Errorf("in %s: %w", funcName, err)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
proposalResponsePayloads, err := p.unmarshalProposalResponsePayloadsFrom(chaincodeEndorsedActions)
|
chaincodeActionPayloads, err := p.unmarshalChaincodeActionPayloads()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("in %s: %w", funcName, err)
|
return nil, fmt.Errorf("in %s: %w", funcName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
chaincodeActions, err := p.unmarshalChaincodeActionsFrom(proposalResponsePayloads)
|
chaincodeEndorsedActions, err := p.extractChaincodeEndorsedActionsFrom(chaincodeActionPayloads)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("in %s: %w", funcName, err)
|
return nil, fmt.Errorf("in %s: %w", funcName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
txReadWriteSets, err := p.unmarshalTxReadWriteSetsFrom(chaincodeActions)
|
proposalResponsePayloads, err := p.unmarshalProposalResponsePayloadsFrom(chaincodeEndorsedActions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("in %s: %w", funcName, err)
|
return nil, fmt.Errorf("in %s: %w", funcName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.parseReadWriteSets(txReadWriteSets), nil
|
chaincodeActions, err := p.unmarshalChaincodeActionsFrom(proposalResponsePayloads)
|
||||||
})()
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("in %s: %w", funcName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
txReadWriteSets, err := p.unmarshalTxReadWriteSetsFrom(chaincodeActions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("in %s: %w", funcName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.cachedReadWriteSets = p.parseReadWriteSets(txReadWriteSets)
|
||||||
|
|
||||||
|
return p.cachedReadWriteSets, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *endorserTransaction) unmarshalChaincodeActionPayloads() ([]*peer.ChaincodeActionPayload, error) {
|
func (p *endorserTransaction) unmarshalChaincodeActionPayloads() ([]*peer.ChaincodeActionPayload, error) {
|
||||||
|
|
@ -65,18 +70,7 @@ func (p *endorserTransaction) unmarshalChaincodeActionPayloads() ([]*peer.Chainc
|
||||||
func (*endorserTransaction) extractChaincodeEndorsedActionsFrom(chaincodeActionPayloads []*peer.ChaincodeActionPayload) ([]*peer.ChaincodeEndorsedAction, error) {
|
func (*endorserTransaction) extractChaincodeEndorsedActionsFrom(chaincodeActionPayloads []*peer.ChaincodeActionPayload) ([]*peer.ChaincodeEndorsedAction, error) {
|
||||||
result := []*peer.ChaincodeEndorsedAction{}
|
result := []*peer.ChaincodeEndorsedAction{}
|
||||||
for _, payload := range chaincodeActionPayloads {
|
for _, payload := range chaincodeActionPayloads {
|
||||||
chaincodeEndorsedAction, err := utils.AssertDefined(
|
result = append(result, payload.GetAction())
|
||||||
payload.GetAction(),
|
|
||||||
"missing chaincode endorsed action",
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("in extractChaincodeEndorsedActionsFrom: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result = append(
|
|
||||||
result,
|
|
||||||
chaincodeEndorsedAction,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package parser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"offChainData/utils"
|
|
||||||
|
|
||||||
"github.com/hyperledger/fabric-protos-go-apiv2/ledger/rwset"
|
"github.com/hyperledger/fabric-protos-go-apiv2/ledger/rwset"
|
||||||
"github.com/hyperledger/fabric-protos-go-apiv2/ledger/rwset/kvrwset"
|
"github.com/hyperledger/fabric-protos-go-apiv2/ledger/rwset/kvrwset"
|
||||||
|
|
@ -10,11 +9,12 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type NamespaceReadWriteSet struct {
|
type NamespaceReadWriteSet struct {
|
||||||
nsReadWriteSet *rwset.NsReadWriteSet
|
nsReadWriteSet *rwset.NsReadWriteSet
|
||||||
|
cachedReadWriteSet *kvrwset.KVRWSet
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseNamespaceReadWriteSet(nsRwSet *rwset.NsReadWriteSet) *NamespaceReadWriteSet {
|
func parseNamespaceReadWriteSet(nsRwSet *rwset.NsReadWriteSet) *NamespaceReadWriteSet {
|
||||||
return &NamespaceReadWriteSet{nsRwSet}
|
return &NamespaceReadWriteSet{nsRwSet, nil}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NamespaceReadWriteSet) Namespace() string {
|
func (p *NamespaceReadWriteSet) Namespace() string {
|
||||||
|
|
@ -22,12 +22,14 @@ func (p *NamespaceReadWriteSet) Namespace() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *NamespaceReadWriteSet) ReadWriteSet() (*kvrwset.KVRWSet, error) {
|
func (p *NamespaceReadWriteSet) ReadWriteSet() (*kvrwset.KVRWSet, error) {
|
||||||
return utils.Cache(func() (*kvrwset.KVRWSet, error) {
|
if p.cachedReadWriteSet != nil {
|
||||||
result := kvrwset.KVRWSet{}
|
return p.cachedReadWriteSet, nil
|
||||||
if err := proto.Unmarshal(p.nsReadWriteSet.GetRwset(), &result); err != nil {
|
}
|
||||||
return nil, fmt.Errorf("in ReadWriteSet: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &result, nil
|
p.cachedReadWriteSet = &kvrwset.KVRWSet{}
|
||||||
})()
|
if err := proto.Unmarshal(p.nsReadWriteSet.GetRwset(), p.cachedReadWriteSet); err != nil {
|
||||||
|
return nil, fmt.Errorf("in ReadWriteSet: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.cachedReadWriteSet, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package parser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"offChainData/utils"
|
|
||||||
|
|
||||||
"github.com/hyperledger/fabric-protos-go-apiv2/common"
|
"github.com/hyperledger/fabric-protos-go-apiv2/common"
|
||||||
"github.com/hyperledger/fabric-protos-go-apiv2/peer"
|
"github.com/hyperledger/fabric-protos-go-apiv2/peer"
|
||||||
|
|
@ -10,30 +9,26 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type payload struct {
|
type payload struct {
|
||||||
commonPayload *common.Payload
|
commonPayload *common.Payload
|
||||||
statusCode int32
|
statusCode int32
|
||||||
|
cachedChannelHeader *common.ChannelHeader
|
||||||
}
|
}
|
||||||
|
|
||||||
func parsePayload(commonPayload *common.Payload, statusCode int32) *payload {
|
func parsePayload(commonPayload *common.Payload, statusCode int32) *payload {
|
||||||
return &payload{commonPayload, statusCode}
|
return &payload{commonPayload, statusCode, nil}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *payload) channelHeader() (*common.ChannelHeader, error) {
|
func (p *payload) channelHeader() (*common.ChannelHeader, error) {
|
||||||
return utils.Cache(func() (*common.ChannelHeader, error) {
|
if p.cachedChannelHeader != nil {
|
||||||
funcName := "channelHeader"
|
return p.cachedChannelHeader, nil
|
||||||
|
}
|
||||||
|
|
||||||
header, err := utils.AssertDefined(p.commonPayload.GetHeader(), "missing payload header")
|
p.cachedChannelHeader = &common.ChannelHeader{}
|
||||||
if err != nil {
|
if err := proto.Unmarshal(p.commonPayload.GetHeader().GetChannelHeader(), p.cachedChannelHeader); err != nil {
|
||||||
return nil, fmt.Errorf("in %s: %w", funcName, err)
|
return nil, fmt.Errorf("in channelHeader: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
result := &common.ChannelHeader{}
|
return p.cachedChannelHeader, nil
|
||||||
if err := proto.Unmarshal(header.GetChannelHeader(), result); err != nil {
|
|
||||||
return nil, fmt.Errorf("in %s: %w", funcName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
})()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *payload) endorserTransaction() (*endorserTransaction, error) {
|
func (p *payload) endorserTransaction() (*endorserTransaction, error) {
|
||||||
|
|
|
||||||
|
|
@ -1,147 +0,0 @@
|
||||||
package processor
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"offChainData/parser"
|
|
||||||
"offChainData/store"
|
|
||||||
|
|
||||||
"github.com/hyperledger/fabric-gateway/pkg/client"
|
|
||||||
)
|
|
||||||
|
|
||||||
type block struct {
|
|
||||||
parsedBlock *parser.Block
|
|
||||||
checkpointer *client.FileCheckpointer
|
|
||||||
writeToStore store.Writer
|
|
||||||
channelName string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewBlock(
|
|
||||||
parsedBlock *parser.Block,
|
|
||||||
checkpointer *client.FileCheckpointer,
|
|
||||||
writeToStore store.Writer,
|
|
||||||
channelName string,
|
|
||||||
) *block {
|
|
||||||
return &block{
|
|
||||||
parsedBlock,
|
|
||||||
checkpointer,
|
|
||||||
writeToStore,
|
|
||||||
channelName,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *block) Process() error {
|
|
||||||
funcName := "Process"
|
|
||||||
|
|
||||||
blockNumber, err := b.parsedBlock.Number()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("in %s: %w", funcName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("\nReceived block", blockNumber)
|
|
||||||
|
|
||||||
validTransactions, err := b.validTransactions()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("in %s: %w", funcName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, validTransaction := range validTransactions {
|
|
||||||
aTransaction := transaction{
|
|
||||||
blockNumber,
|
|
||||||
validTransaction,
|
|
||||||
// TODO use pointer to parent and get blockNumber, store and channelName from parent
|
|
||||||
b.writeToStore,
|
|
||||||
b.channelName,
|
|
||||||
}
|
|
||||||
if err := aTransaction.process(); err != nil {
|
|
||||||
return fmt.Errorf("in %s: %w", funcName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
channelHeader, err := validTransaction.ChannelHeader()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("in %s: %w", funcName, err)
|
|
||||||
}
|
|
||||||
transactionId := channelHeader.GetTxId()
|
|
||||||
b.checkpointer.CheckpointTransaction(blockNumber, transactionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
b.checkpointer.CheckpointBlock(blockNumber)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *block) validTransactions() ([]*parser.Transaction, error) {
|
|
||||||
result := []*parser.Transaction{}
|
|
||||||
newTransactions, err := b.getNewTransactions()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("in validTransactions: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, transaction := range newTransactions {
|
|
||||||
if transaction.IsValid() {
|
|
||||||
result = append(result, transaction)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *block) getNewTransactions() ([]*parser.Transaction, error) {
|
|
||||||
funcName := "getNewTransactions"
|
|
||||||
|
|
||||||
transactions, err := b.parsedBlock.Transactions()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("in %s: %w", funcName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
lastTransactionId := b.checkpointer.TransactionID()
|
|
||||||
if lastTransactionId == "" {
|
|
||||||
// No previously processed transactions within this block so all are new
|
|
||||||
return transactions, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore transactions up to the last processed transaction ID
|
|
||||||
lastProcessedIndex, err := b.findLastProcessedIndex()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("in %s: %w", funcName, err)
|
|
||||||
}
|
|
||||||
return transactions[lastProcessedIndex+1:], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *block) findLastProcessedIndex() (int, error) {
|
|
||||||
funcName := "findLastProcessedIndex"
|
|
||||||
|
|
||||||
transactions, err := b.parsedBlock.Transactions()
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("in %s: %w", funcName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
blockTransactionIds := []string{}
|
|
||||||
for _, transaction := range transactions {
|
|
||||||
channelHeader, err := transaction.ChannelHeader()
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("in %s: %w", funcName, err)
|
|
||||||
}
|
|
||||||
blockTransactionIds = append(blockTransactionIds, channelHeader.GetTxId())
|
|
||||||
}
|
|
||||||
|
|
||||||
lastTransactionId := b.checkpointer.TransactionID()
|
|
||||||
lastProcessedIndex := -1
|
|
||||||
for index, id := range blockTransactionIds {
|
|
||||||
if id == lastTransactionId {
|
|
||||||
lastProcessedIndex = index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if lastProcessedIndex < 0 {
|
|
||||||
blockNumber, err := b.parsedBlock.Number()
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("in %s: %w", funcName, err)
|
|
||||||
}
|
|
||||||
return lastProcessedIndex, newTxIdNotFoundError(
|
|
||||||
lastTransactionId,
|
|
||||||
blockNumber,
|
|
||||||
blockTransactionIds,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return lastProcessedIndex, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,104 +0,0 @@
|
||||||
package processor
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"offChainData/parser"
|
|
||||||
"offChainData/store"
|
|
||||||
"slices"
|
|
||||||
)
|
|
||||||
|
|
||||||
type transaction struct {
|
|
||||||
blockNumber uint64
|
|
||||||
transaction *parser.Transaction
|
|
||||||
writeToStore store.Writer
|
|
||||||
channelName string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *transaction) process() error {
|
|
||||||
funcName := "process"
|
|
||||||
|
|
||||||
channelHeader, err := t.transaction.ChannelHeader()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("in %s: %w", funcName, err)
|
|
||||||
}
|
|
||||||
transactionId := channelHeader.GetTxId()
|
|
||||||
|
|
||||||
writes, err := t.writes()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("in %s: %w", funcName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(writes) == 0 {
|
|
||||||
fmt.Println("Skipping read-only or system transaction", transactionId)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Process transaction", transactionId)
|
|
||||||
|
|
||||||
if err := t.writeToStore(store.LedgerUpdate{
|
|
||||||
BlockNumber: t.blockNumber,
|
|
||||||
TransactionId: transactionId,
|
|
||||||
Writes: writes,
|
|
||||||
}); err != nil {
|
|
||||||
return fmt.Errorf("in %s: %w", funcName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *transaction) writes() ([]store.Write, error) {
|
|
||||||
funcName := "writes"
|
|
||||||
// TODO this entire code should live in the parser and just return the kvWrite which
|
|
||||||
// we then map to store.Write and return
|
|
||||||
channelHeader, err := t.transaction.ChannelHeader()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("in %s: %w", funcName, err)
|
|
||||||
}
|
|
||||||
t.channelName = channelHeader.GetChannelId()
|
|
||||||
|
|
||||||
nsReadWriteSets, err := t.transaction.NamespaceReadWriteSets()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("in %s: %w", funcName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
nonSystemCCReadWriteSets := []*parser.NamespaceReadWriteSet{}
|
|
||||||
for _, nsReadWriteSet := range nsReadWriteSets {
|
|
||||||
if !t.isSystemChaincode(nsReadWriteSet.Namespace()) {
|
|
||||||
nonSystemCCReadWriteSets = append(nonSystemCCReadWriteSets, nsReadWriteSet)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
writes := []store.Write{}
|
|
||||||
for _, readWriteSet := range nonSystemCCReadWriteSets {
|
|
||||||
namespace := readWriteSet.Namespace()
|
|
||||||
|
|
||||||
kvReadWriteSet, err := readWriteSet.ReadWriteSet()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("in %s: %w", funcName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, kvWrite := range kvReadWriteSet.GetWrites() {
|
|
||||||
writes = append(writes, store.Write{
|
|
||||||
ChannelName: t.channelName,
|
|
||||||
Namespace: namespace,
|
|
||||||
Key: kvWrite.GetKey(),
|
|
||||||
IsDelete: kvWrite.GetIsDelete(),
|
|
||||||
Value: string(kvWrite.GetValue()), // Convert bytes to text, purely for readability in output
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return writes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *transaction) isSystemChaincode(chaincodeName string) bool {
|
|
||||||
systemChaincodeNames := []string{
|
|
||||||
"_lifecycle",
|
|
||||||
"cscc",
|
|
||||||
"escc",
|
|
||||||
"lscc",
|
|
||||||
"qscc",
|
|
||||||
"vscc",
|
|
||||||
}
|
|
||||||
return slices.Contains(systemChaincodeNames, chaincodeName)
|
|
||||||
}
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
package processor
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
type txIdNotFoundError struct {
|
|
||||||
txId string
|
|
||||||
blockNumber uint64
|
|
||||||
blockTxIds []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTxIdNotFoundError(txId string, blockNumber uint64, blockTxIds []string) *txIdNotFoundError {
|
|
||||||
return &txIdNotFoundError{
|
|
||||||
txId, blockNumber, blockTxIds,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *txIdNotFoundError) Error() string {
|
|
||||||
format := "checkpoint transaction ID %s not found in block %d containing transactions: %s"
|
|
||||||
return fmt.Sprintf(format, t.txId, t.blockNumber, t.blockTxIdsJoinedByComma())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *txIdNotFoundError) blockTxIdsJoinedByComma() string {
|
|
||||||
result := ""
|
|
||||||
for index, item := range t.blockTxIds {
|
|
||||||
if len(t.blockTxIds)-1 == index {
|
|
||||||
result += item
|
|
||||||
} else {
|
|
||||||
result += item + ", "
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +1,47 @@
|
||||||
package store
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"offChainData/utils"
|
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var storeFile = utils.EnvOrDefault("STORE_FILE", "store.log")
|
var storeFile = envOrDefault("STORE_FILE", "store.log")
|
||||||
var SimulatedFailureCount = getSimulatedFailureCount()
|
var simulatedFailureCount = getSimulatedFailureCount()
|
||||||
var transactionCount uint = 0 // Used only to simulate failures
|
var transactionCount uint = 0 // Used only to simulate failures
|
||||||
|
|
||||||
|
// Apply writes for a given transaction to off-chain data store, ideally in a single operation for fault tolerance.
|
||||||
|
type writer = func(data ledgerUpdate) error
|
||||||
|
|
||||||
|
// Ledger update made by a specific transaction.
|
||||||
|
type ledgerUpdate struct {
|
||||||
|
BlockNumber uint64
|
||||||
|
TransactionID string
|
||||||
|
Writes []write
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description of a ledger Write that can be applied to an off-chain data store.
|
||||||
|
type write struct {
|
||||||
|
// Channel whose ledger is being updated.
|
||||||
|
ChannelName string `json:"channelName"`
|
||||||
|
// Namespace within the ledger.
|
||||||
|
Namespace string `json:"namespace"`
|
||||||
|
// Key name within the ledger namespace.
|
||||||
|
Key string `json:"key"`
|
||||||
|
// Whether the key and associated value are being deleted.
|
||||||
|
IsDelete bool `json:"isDelete"`
|
||||||
|
// If `isDelete` is false, the Value written to the key; otherwise ignored.
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
// Apply writes for a given transaction to off-chain data store, ideally in a single operation for fault tolerance.
|
// Apply writes for a given transaction to off-chain data store, ideally in a single operation for fault tolerance.
|
||||||
// This implementation just writes to a file.
|
// This implementation just writes to a file.
|
||||||
func ApplyWritesToOffChainStore(data LedgerUpdate) error {
|
func applyWritesToOffChainStore(data ledgerUpdate) error {
|
||||||
funcName := "ApplyWritesToOffChainStore"
|
funcName := "applyWritesToOffChainStore"
|
||||||
|
|
||||||
if err := simulateFailureIfRequired(); err != nil {
|
if err := simulateFailureIfRequired(); err != nil {
|
||||||
return fmt.Errorf("in %s: %w", funcName, err)
|
return fmt.Errorf("in %s: %w", funcName, err)
|
||||||
|
|
@ -52,7 +75,7 @@ func ApplyWritesToOffChainStore(data LedgerUpdate) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func simulateFailureIfRequired() error {
|
func simulateFailureIfRequired() error {
|
||||||
if SimulatedFailureCount > 0 && transactionCount >= SimulatedFailureCount {
|
if simulatedFailureCount > 0 && transactionCount >= simulatedFailureCount {
|
||||||
transactionCount = 0
|
transactionCount = 0
|
||||||
return errors.New("expected error: simulated write failure")
|
return errors.New("expected error: simulated write failure")
|
||||||
}
|
}
|
||||||
|
|
@ -63,7 +86,7 @@ func simulateFailureIfRequired() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSimulatedFailureCount() uint {
|
func getSimulatedFailureCount() uint {
|
||||||
valueAsString := utils.EnvOrDefault("SIMULATED_FAILURE_COUNT", "0")
|
valueAsString := envOrDefault("SIMULATED_FAILURE_COUNT", "0")
|
||||||
valueAsFloat, err := strconv.ParseFloat(valueAsString, 64)
|
valueAsFloat, err := strconv.ParseFloat(valueAsString, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
package store
|
|
||||||
|
|
||||||
// Apply writes for a given transaction to off-chain data store, ideally in a single operation for fault tolerance.
|
|
||||||
type Writer = func(data LedgerUpdate) error
|
|
||||||
|
|
||||||
// Ledger update made by a specific transaction.
|
|
||||||
type LedgerUpdate struct {
|
|
||||||
BlockNumber uint64
|
|
||||||
TransactionId string
|
|
||||||
Writes []Write
|
|
||||||
}
|
|
||||||
|
|
||||||
// Description of a ledger Write that can be applied to an off-chain data store.
|
|
||||||
type Write struct {
|
|
||||||
// Channel whose ledger is being updated.
|
|
||||||
ChannelName string `json:"channelName"`
|
|
||||||
// Namespace within the ledger.
|
|
||||||
Namespace string `json:"namespace"`
|
|
||||||
// Key name within the ledger namespace.
|
|
||||||
Key string `json:"key"`
|
|
||||||
// Whether the key and associated value are being deleted.
|
|
||||||
IsDelete bool `json:"isDelete"`
|
|
||||||
// If `isDelete` is false, the Value written to the key; otherwise ignored.
|
|
||||||
Value string `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +1,20 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/big"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
atb "offChainData/contract"
|
atb "offchaindata/contract"
|
||||||
"offChainData/utils"
|
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/hyperledger/fabric-gateway/pkg/client"
|
"github.com/hyperledger/fabric-gateway/pkg/client"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var owners = []string{"alice", "bob", "charlie"}
|
||||||
|
|
||||||
func transact(clientConnection *grpc.ClientConn) {
|
func transact(clientConnection *grpc.ClientConn) {
|
||||||
id, options := newConnectOptions(clientConnection)
|
id, options := newConnectOptions(clientConnection)
|
||||||
gateway, err := client.Connect(id, options...)
|
gateway, err := client.Connect(id, options...)
|
||||||
|
|
@ -59,7 +63,7 @@ func (t *transactApp) run() {
|
||||||
func (t *transactApp) transact() error {
|
func (t *transactApp) transact() error {
|
||||||
funcName := "transact"
|
funcName := "transact"
|
||||||
|
|
||||||
anAsset := atb.NewAsset()
|
anAsset := newAsset()
|
||||||
|
|
||||||
err := t.smartContract.CreateAsset(anAsset)
|
err := t.smartContract.CreateAsset(anAsset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -68,8 +72,8 @@ func (t *transactApp) transact() error {
|
||||||
fmt.Println("Created asset", anAsset.ID)
|
fmt.Println("Created asset", anAsset.ID)
|
||||||
|
|
||||||
// Transfer randomly 1 in 2 assets to a new owner.
|
// Transfer randomly 1 in 2 assets to a new owner.
|
||||||
if utils.RandomInt(2) == 0 {
|
if randomInt(2) == 0 {
|
||||||
newOwner := utils.DifferentElement(atb.Owners, anAsset.Owner)
|
newOwner := differentElement(owners, anAsset.Owner)
|
||||||
oldOwner, err := t.smartContract.TransferAsset(anAsset.ID, newOwner)
|
oldOwner, err := t.smartContract.TransferAsset(anAsset.ID, newOwner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("in %s: %w", funcName, err)
|
return fmt.Errorf("in %s: %w", funcName, err)
|
||||||
|
|
@ -78,7 +82,7 @@ func (t *transactApp) transact() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete randomly 1 in 4 created assets.
|
// Delete randomly 1 in 4 created assets.
|
||||||
if utils.RandomInt(4) == 0 {
|
if randomInt(4) == 0 {
|
||||||
err := t.smartContract.DeleteAsset(anAsset.ID)
|
err := t.smartContract.DeleteAsset(anAsset.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("in %s: %w", funcName, err)
|
return fmt.Errorf("in %s: %w", funcName, err)
|
||||||
|
|
@ -87,3 +91,45 @@ func (t *transactApp) transact() error {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newAsset() atb.Asset {
|
||||||
|
id, err := uuid.NewRandom()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return atb.Asset{
|
||||||
|
ID: id.String(),
|
||||||
|
Color: randomElement([]string{"red", "green", "blue"}),
|
||||||
|
Size: uint64(randomInt(10) + 1),
|
||||||
|
Owner: randomElement(owners),
|
||||||
|
AppraisedValue: uint64(randomInt(1000) + 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick a random element from an array.
|
||||||
|
func randomElement(values []string) string {
|
||||||
|
result := values[randomInt(len(values))]
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a random integer in the range 0 to max - 1.
|
||||||
|
func randomInt(max int) int {
|
||||||
|
result, err := rand.Int(rand.Reader, big.NewInt(int64(max)))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return int(result.Int64())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick a random element from an array, excluding the current value.
|
||||||
|
func differentElement(values []string, currentValue string) string {
|
||||||
|
candidateValues := []string{}
|
||||||
|
for _, v := range values {
|
||||||
|
if v != currentValue {
|
||||||
|
candidateValues = append(candidateValues, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return randomElement(candidateValues)
|
||||||
|
}
|
||||||
|
|
|
||||||
13
off_chain_data/application-go/utils.go
Normal file
13
off_chain_data/application-go/utils.go
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func envOrDefault(key, defaultValue string) string {
|
||||||
|
result := os.Getenv(key)
|
||||||
|
if result == "" {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
package utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Pick a random element from an array.
|
|
||||||
func RandomElement(values []string) string {
|
|
||||||
result := values[RandomInt(len(values))]
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a random integer in the range 0 to max - 1.
|
|
||||||
func RandomInt(max int) int {
|
|
||||||
result, err := rand.Int(rand.Reader, big.NewInt(int64(max)))
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return int(result.Int64())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pick a random element from an array, excluding the current value.
|
|
||||||
func DifferentElement(values []string, currentValue string) string {
|
|
||||||
candidateValues := []string{}
|
|
||||||
for _, v := range values {
|
|
||||||
if v != currentValue {
|
|
||||||
candidateValues = append(candidateValues, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return RandomElement(candidateValues)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the value if it is defined; otherwise panics with an error message.
|
|
||||||
func AssertDefined[T any](value T, message string) (T, error) {
|
|
||||||
if any(value) == any(nil) {
|
|
||||||
var zeroValue T
|
|
||||||
return zeroValue, errors.New(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
return value, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrap a function call with a cache. On first call the wrapped function is invoked to
|
|
||||||
// obtain a result. Subsequent calls return the cached result.
|
|
||||||
func Cache[T any](f func() (T, error)) func() (T, error) {
|
|
||||||
var value T
|
|
||||||
var err error
|
|
||||||
var cached bool
|
|
||||||
|
|
||||||
return func() (T, error) {
|
|
||||||
if !cached {
|
|
||||||
value, err = f()
|
|
||||||
if err != nil {
|
|
||||||
var zeroValue T
|
|
||||||
return zeroValue, fmt.Errorf("in Cache: %w", err)
|
|
||||||
}
|
|
||||||
cached = true
|
|
||||||
}
|
|
||||||
return value, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func EnvOrDefault(key, defaultValue string) string {
|
|
||||||
result := os.Getenv(key)
|
|
||||||
if result == "" {
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
package utils_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"offChainData/utils"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_cachePrimitiveFunctionResult(t *testing.T) {
|
|
||||||
counter := 0
|
|
||||||
f := func() (int, error) {
|
|
||||||
counter++
|
|
||||||
return 5, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cachedFunc := utils.Cache(f)
|
|
||||||
result1, err := cachedFunc()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("unexpected error:", err)
|
|
||||||
}
|
|
||||||
result2, err := cachedFunc()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("unexpected error:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if counter != 1 {
|
|
||||||
t.Error("expected counter to be 1, but got", counter)
|
|
||||||
}
|
|
||||||
|
|
||||||
if result1 != 5 || result2 != 5 {
|
|
||||||
t.Fatal("expected results to be 5, but got", result1, result2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_whenCachedFunctionsErrors_returnError(t *testing.T) {
|
|
||||||
errorMsg := "error"
|
|
||||||
f := func() (int, error) {
|
|
||||||
return 0, errors.New(errorMsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
cachedFunc := utils.Cache(f)
|
|
||||||
_, err := cachedFunc()
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected error, but got", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err.Error() != errorMsg {
|
|
||||||
t.Fatal("expected error message to be 'error', but got", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_cacheWrappedPrimitiveFunctionResult(t *testing.T) {
|
|
||||||
controlValue := 5
|
|
||||||
multiplyControlValueBy := func(n int) (int, error) { controlValue *= n; return controlValue, nil }
|
|
||||||
|
|
||||||
cachedFunc := utils.Cache(func() (int, error) { return multiplyControlValueBy(5) })
|
|
||||||
result1, err := cachedFunc()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("unexpected error:", err)
|
|
||||||
}
|
|
||||||
result2, err := cachedFunc()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("unexpected error:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if controlValue != 25 {
|
|
||||||
t.Error("expected control value to be 25, but got", controlValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
if result1 != 25 || result2 != 25 {
|
|
||||||
t.Fatal("expected cached results to be 25, but got", result1, result2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_cacheWrappedDataStructureResult(t *testing.T) {
|
|
||||||
type GreetMe struct {
|
|
||||||
helloTo string
|
|
||||||
}
|
|
||||||
|
|
||||||
controlStruct := &GreetMe{helloTo: "Hello "}
|
|
||||||
greet := func(name string) (*GreetMe, error) { controlStruct.helloTo += name; return controlStruct, nil }
|
|
||||||
|
|
||||||
cachedFunc := utils.Cache(func() (*GreetMe, error) { return greet("John Doe") })
|
|
||||||
result1, err := cachedFunc()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("unexpected error:", err)
|
|
||||||
}
|
|
||||||
result2, err := cachedFunc()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("unexpected error:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if controlStruct.helloTo != "Hello John Doe" {
|
|
||||||
t.Error("expected control value to be 'Hello John Doe', but got", controlStruct)
|
|
||||||
}
|
|
||||||
|
|
||||||
if result1.helloTo != "Hello John Doe" || result2.helloTo != "Hello John Doe" {
|
|
||||||
t.Fatal("expected cached results to be 'Hello John Doe', but got", result1.helloTo, result2.helloTo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue