diff --git a/samples/chaincode/Makefile b/samples/chaincode/Makefile index e2b1b5364..121f3f5f3 100644 --- a/samples/chaincode/Makefile +++ b/samples/chaincode/Makefile @@ -6,7 +6,7 @@ TOP = ../.. include $(TOP)/build.mk -SUB_DIRS = auction auction-go echo echo-go kv-test kv-test-go +SUB_DIRS = auction auction-go echo echo-go kv-test kv-test-go confidential-escrow build test clean clobber: $(foreach DIR, $(SUB_DIRS), $(MAKE) -C $(DIR) $@ || exit ;) diff --git a/samples/chaincode/confidential-escrow/.env.alice b/samples/chaincode/confidential-escrow/.env.alice new file mode 100644 index 000000000..75ab220c7 --- /dev/null +++ b/samples/chaincode/confidential-escrow/.env.alice @@ -0,0 +1,15 @@ +export CC_ID=confidential-escrow +export CHANNEL_NAME=mychannel +export CORE_PEER_ADDRESS=localhost:7051 +export CORE_PEER_ID=peer0.org1.example.com +export CORE_PEER_ORG_NAME=org1 +export CORE_PEER_LOCALMSPID=Org1MSP +export CORE_PEER_MSPCONFIGPATH=$FPC_PATH/samples/deployment/test-network/fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp +export CORE_PEER_TLS_CERT_FILE=$FPC_PATH/samples/deployment/test-network/fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/server.crt +export CORE_PEER_TLS_ENABLED="true" +export CORE_PEER_TLS_KEY_FILE=$FPC_PATH/samples/deployment/test-network/fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/server.key +export CORE_PEER_TLS_ROOTCERT_FILE=$FPC_PATH/samples/deployment/test-network/fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt +export ORDERER_CA=$FPC_PATH/samples/deployment/test-network/fabric-samples/test-network/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem +export GATEWAY_CONFIG=$FPC_PATH/samples/deployment/test-network/fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/connection-org1.yaml +export FPC_ENABLED=true +export RUN_CCAAS=true diff --git a/samples/chaincode/confidential-escrow/.env.bob b/samples/chaincode/confidential-escrow/.env.bob new file mode 100644 index 000000000..0230be1d1 --- /dev/null +++ b/samples/chaincode/confidential-escrow/.env.bob @@ -0,0 +1,16 @@ +export CC_ID=confidential-escrow +export CHANNEL_NAME=mychannel +export CORE_PEER_ADDRESS=localhost:9051 +export CORE_PEER_ID=peer0.org2.example.com +export CORE_PEER_ORG_NAME=org2 +export CORE_PEER_LOCALMSPID=Org2MSP +export CORE_PEER_MSPCONFIGPATH=$FPC_PATH/samples/deployment/test-network/fabric-samples/test-network/organizations/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp +export CORE_PEER_TLS_CERT_FILE=$FPC_PATH/samples/deployment/test-network/fabric-samples/test-network/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/server.crt +export CORE_PEER_TLS_ENABLED="true" +export CORE_PEER_TLS_KEY_FILE=$FPC_PATH/samples/deployment/test-network/fabric-samples/test-network/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/server.key +export CORE_PEER_TLS_ROOTCERT_FILE=$FPC_PATH/samples/deployment/test-network/fabric-samples/test-network/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt +export ORDERER_CA=$FPC_PATH/samples/deployment/test-network/fabric-samples/test-network/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem +export GATEWAY_CONFIG=$FPC_PATH/samples/deployment/test-network/fabric-samples/test-network/organizations/peerOrganizations/org2.example.com/connection-org2.yaml +export FPC_ENABLED=true +export RUN_CCAAS=true + diff --git a/samples/chaincode/confidential-escrow/.env.example b/samples/chaincode/confidential-escrow/.env.example new file mode 100644 index 000000000..75ab220c7 --- /dev/null +++ b/samples/chaincode/confidential-escrow/.env.example @@ -0,0 +1,15 @@ +export CC_ID=confidential-escrow +export CHANNEL_NAME=mychannel +export CORE_PEER_ADDRESS=localhost:7051 +export CORE_PEER_ID=peer0.org1.example.com +export CORE_PEER_ORG_NAME=org1 +export CORE_PEER_LOCALMSPID=Org1MSP +export CORE_PEER_MSPCONFIGPATH=$FPC_PATH/samples/deployment/test-network/fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp +export CORE_PEER_TLS_CERT_FILE=$FPC_PATH/samples/deployment/test-network/fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/server.crt +export CORE_PEER_TLS_ENABLED="true" +export CORE_PEER_TLS_KEY_FILE=$FPC_PATH/samples/deployment/test-network/fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/server.key +export CORE_PEER_TLS_ROOTCERT_FILE=$FPC_PATH/samples/deployment/test-network/fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt +export ORDERER_CA=$FPC_PATH/samples/deployment/test-network/fabric-samples/test-network/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem +export GATEWAY_CONFIG=$FPC_PATH/samples/deployment/test-network/fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/connection-org1.yaml +export FPC_ENABLED=true +export RUN_CCAAS=true diff --git a/samples/chaincode/confidential-escrow/.gitignore b/samples/chaincode/confidential-escrow/.gitignore new file mode 100644 index 000000000..c55c640b7 --- /dev/null +++ b/samples/chaincode/confidential-escrow/.gitignore @@ -0,0 +1,10 @@ +ecc +ecc-bundle +enclave.json +private.pem +public.pem +mrenclave +details.env + +.env +*.bak diff --git a/samples/chaincode/confidential-escrow/Makefile b/samples/chaincode/confidential-escrow/Makefile new file mode 100644 index 000000000..406470419 --- /dev/null +++ b/samples/chaincode/confidential-escrow/Makefile @@ -0,0 +1,7 @@ +TOP = ../../.. +include $(TOP)/ecc_go/build.mk + +CC_NAME ?= confidential-escrow + +EGO_CONFIG_FILE = $(FPC_PATH)/samples/chaincode/confidential-escrow/confidentialEscrowEnclave.json +ECC_MAIN_FILES=$(FPC_PATH)/samples/chaincode/confidential-escrow diff --git a/samples/chaincode/confidential-escrow/README.md b/samples/chaincode/confidential-escrow/README.md new file mode 100644 index 000000000..561db4ef6 --- /dev/null +++ b/samples/chaincode/confidential-escrow/README.md @@ -0,0 +1,187 @@ +# Confidential Escrow Chaincode + +A privacy-preserving escrow system built on Hyperledger Fabric Private Chaincode (FPC) that enables secure digital asset management with programmable conditional payments. + +## Overview + +This chaincode implements a confidential escrow mechanism for digital assets, combining: + +- **Privacy-Preserving Transactions**: All transaction data is encrypted within Intel SGX enclaves +- **Programmable Escrow Contracts**: Automated conditional fund releases based on cryptographic verification +- **Multi-Asset Support**: Manage multiple token types within individual wallets +- **Certificate-Based Authorization**: Fine-grained access control using X.509 certificate hashes + +## Architecture + +### Core Components + +**Assets** + +- `DigitalAsset`: Fungible tokens with controlled supply (CBDC, stablecoins, etc.) +- `Wallet`: User accounts supporting multiple asset types with separate available and escrowed balances +- `Escrow`: Smart contracts holding funds pending condition fulfillment +- `UserDirectory`: Privacy-preserving public key to wallet UUID mapping + +**Transaction Operations** + +- Asset lifecycle: Create, mint, transfer, burn +- Wallet management: Create wallets, query balances +- Escrow workflow: Lock funds, verify conditions, release or refund + +## Project Structure + +``` +confidential-escrow/ +├── chaincode/ +│ ├── assets/ # Asset type definitions +│ ├── transactions/ # Transaction handlers +│ ├── header/ # Chaincode metadata +│ ├── escrow.go # Main chaincode implementation +│ ├── server.go # CCaaS server setup +│ └── setup.go # Component registration +├── main.go # Entry point +├── main.sh # Deployment and test automation +└── README.md # This file +``` + +### Security Model + +1. **Access Control**: All operations require valid certificate hash verification +2. **Atomic Escrow**: Funds move from available to escrowed balance during lock, preventing double-spending +3. **Condition Verification**: SHA-256 hash of `(secret + parcelId)` ensures only authorized parties can release funds +4. **Confidential Execution**: FPC ensures transaction details remain private within SGX enclaves + +## Running Procedure + +### Prerequisites + +- FPC is properly set up and built +- `multi_user_dashboard.sh ` script is placed in the chaincode directory +- `.env.alice` and `.env.bob` file is present + +### Setup Files + +**1. Set FPC_PATH:** + +```bash +export FPC_PATH=/project/src/github.com/hyperledger/fabric-private-chaincode +``` + +### Running Procedure + +#### 1. In 1st terminal window - Setup and Deploy + +```bash +# Get inside dev env +make -C $FPC_PATH/utils/docker run-dev +cd samples/chaincode/confidential-escrow + +# Interactive menu +./multi_user_dashboard.sh + +# Choose Option 1. or 2. as per your setup condn +``` + +#### 2. In 2nd terminal window - Docker Environment (`Alice`) + +```bash +# Enter docker container +docker exec -it fpc-development-main /bin/bash +cd samples/chaincode/confidential-escrow + +# Interactive menu +./multi_user_dashboard.sh + +# Setup Alice using Option 3. +``` + +#### 3. In 3rd terminal window - Docker Environment (`Bob`) + +```bash +# Enter docker container +docker exec -it fpc-development-main /bin/bash +cd samples/chaincode/confidential-escrow + +# Interactive menu +./multi_user_dashboard.sh + +# Setup Bob using Option 4. +``` + +#### 4. In 4th terminal window - Docker Environment (`Monitor`) + +```bash +# Enter docker container +docker exec -it fpc-development-main /bin/bash +cd samples/chaincode/confidential-escrow + +# Interactive menu +./multi_user_dashboard.sh + +# Setup Monitor using Option 5. +``` + +#### 5. Run Tests + +```bash +# Run all basic tests +./multi_user_dashboard.sh + +# Chosing Option 7. (One can run it from any terminal) +``` + +## Escrow Workflow + +### Step 1: Create Wallets + +Before any escrow operations, both parties must have wallets: + +1. **Alice** creates her wallet via Terminal 2 (Option 3 - currently created automatically) +2. **Bob** creates his wallet via Terminal 3 (Option 4 - currently created automatically) +3. **Monitor** (Terminal 4, Option 5) can observe all wallet creations and transactions in real-time + +### Step 2: Create Escrow + +Once both wallets exist, either party can create an escrow using `createAndLockEscrow`. The buyer locks funds by specifying: + +- Buyer/seller public keys +- Amount and asset type +- `parcelId` and `secret` (used for condition verification) + +### Step 3: Complete Escrow + +Two possible outcomes: + +| Operation | Who Calls | When | Result | +| ----------- | --------- | ------------------------------------------- | ------------------------ | +| **Release** | Seller | Condition fulfilled (e.g., goods delivered) | Funds transfer to seller | +| **Refund** | Buyer | Condition not met or cancelled | Funds return to buyer | + +### Release vs Refund + +- **Release** (`releaseEscrow`): Seller provides correct `secret + parcelId` to prove fulfillment. Funds move from buyer's escrow balance to seller's available balance. Status → `Released`. + +- **Refund** (`refundEscrow`): Buyer cancels an active escrow. No secret needed. Funds return to buyer's available balance. Status → `Refunded`. + +Both operations only work on `Active` escrows. + +## Troubleshooting + +**Network already running?** +If your Fabric test-network is already up, comment out the `network.sh down` and `network.sh up` lines in `test.sh` to avoid restarting it: + +```bash +# In test.sh, comment these lines: +# run_cmd "./network.sh down" "Bringing down network" +# run_cmd "./network.sh up createChannel -ca" "Starting network" +``` + +## Contributing + +When adding new features: + +1. Define asset types in `chaincode/assets/` +2. Implement transaction logic in `chaincode/transactions/` +3. Register new components in `chaincode/setup.go` +4. Add test cases to `main.sh` +5. Update this README with usage examples diff --git a/samples/chaincode/confidential-escrow/chaincode/assets/digital_asset.go b/samples/chaincode/confidential-escrow/chaincode/assets/digital_asset.go new file mode 100644 index 000000000..5274b763f --- /dev/null +++ b/samples/chaincode/confidential-escrow/chaincode/assets/digital_asset.go @@ -0,0 +1,62 @@ +package assets + +import ( + "github.com/hyperledger-labs/cc-tools/assets" +) + +// DigitalAssetToken defines the asset type for fungible digital tokens. +// This represents confidential digital currencies such as Central Bank Digital Currencies (CBDC) +// or tokenized assets. Each token type has a fixed supply controlled by the issuer. +// +// Security: The issuerHash ensures only authorized entities can mint/burn tokens. +var DigitalAssetToken = assets.AssetType{ + Tag: "digitalAsset", + Label: "Digital Asset Token", + Description: "Confidential digital currency token (e.g., CBDC)", + + Props: []assets.AssetProp{ + { + Tag: "name", + Label: "Token Name", + DataType: "string", + Required: true, + }, + { + Tag: "symbol", + Label: "Token Symbol", + DataType: "string", + Required: true, + IsKey: true, + }, + { + Tag: "decimals", + Label: "Decimal Places", + DataType: "number", + Required: true, + }, + { + Tag: "totalSupply", + Label: "Total Supply", + DataType: "number", + Required: true, + }, + { + Tag: "issuerHash", + Label: "Issuer Certificate Hash", + DataType: "string", + Required: true, + }, + { + Tag: "owner", + Label: "Owner Identity", + DataType: "string", + Required: true, + }, + { + Tag: "issuedAt", + Label: "Issued At", + DataType: "datetime", + Required: false, + }, + }, +} diff --git a/samples/chaincode/confidential-escrow/chaincode/assets/escrow.go b/samples/chaincode/confidential-escrow/chaincode/assets/escrow.go new file mode 100644 index 000000000..f33686219 --- /dev/null +++ b/samples/chaincode/confidential-escrow/chaincode/assets/escrow.go @@ -0,0 +1,102 @@ +package assets + +import ( + "github.com/hyperledger-labs/cc-tools/assets" +) + +// Escrow defines the asset type for programmable conditional payment contracts. +// This enables secure, trustless transactions where funds are held in escrow until +// predefined conditions are met. The escrow uses cryptographic hash verification +// to ensure condition fulfillment. +// +// Lifecycle States: +// - Active: Funds locked, awaiting condition verification +// - ReadyForRelease: Condition verified, awaiting release +// - Released: Funds transferred to seller +// - Refunded: Funds returned to buyer +// +// Security Model: +// - conditionValue: SHA-256 hash of (secret + parcelId) for atomic condition verification +// - buyerCertHash: Ensures only the buyer can initiate refunds +// - Seller must provide correct secret and parcelId to release funds +var Escrow = assets.AssetType{ + Tag: "escrow", + Label: "Programmable Escrow", + Description: "Confidential escrow contract with programmable conditions", + + Props: []assets.AssetProp{ + { + Tag: "escrowId", + Label: "Escrow ID", + DataType: "string", + Required: true, + IsKey: true, + }, + { + Tag: "buyerPubKey", + Label: "Buyer Public Key", + DataType: "string", + Required: true, + }, + { + Tag: "sellerPubKey", + Label: "Seller Public Key", + DataType: "string", + Required: true, + }, + { + Tag: "buyerWalletUUID", + Label: "Buyer Wallet UUID", + DataType: "string", + Required: true, + }, + { + Tag: "sellerWalletUUID", + Label: "Seller Wallet UUID", + DataType: "string", + Required: true, + }, + { + Tag: "amount", + Label: "Escrowed Amount", + DataType: "number", + Required: true, + }, + { + Tag: "assetType", + Label: "Asset Type Reference", + DataType: "->digitalAsset", // References digitalAsset symbol + Required: true, + }, + { + Tag: "parcelId", + Label: "Parcel ID", + DataType: "string", + Required: true, + }, + { + Tag: "conditionValue", + Label: "Condition Value", + DataType: "string", + Required: true, + }, + { + Tag: "status", + Label: "Escrow Status", + DataType: "string", // "Active", "Released", "Refunded" + Required: true, + }, + { + Tag: "createdAt", + Label: "Creation Timestamp", + DataType: "datetime", + Required: false, + }, + { + Tag: "buyerCertHash", + Label: "Buyer Certificate Hash", + DataType: "string", + Required: true, + }, + }, +} diff --git a/samples/chaincode/confidential-escrow/chaincode/assets/user_directory.go b/samples/chaincode/confidential-escrow/chaincode/assets/user_directory.go new file mode 100644 index 000000000..2579f600f --- /dev/null +++ b/samples/chaincode/confidential-escrow/chaincode/assets/user_directory.go @@ -0,0 +1,47 @@ +package assets + +import ( + "github.com/hyperledger-labs/cc-tools/assets" +) + +// UserDirectory provides a mapping between user public key hashes and wallet UUIDs. +// This indirection layer enables efficient wallet lookups while maintaining privacy, +// as the actual public keys are never stored directly on the ledger. +// +// Purpose: +// - Enable wallet discovery using only the public key hash +// - Associate certificate hashes with wallets for authorization +// - Maintain user privacy by avoiding direct public key storage +// +// Usage Pattern: +// 1. Hash the user's public key with SHA-256 +// 2. Query UserDirectory using the hash +// 3. Retrieve the associated wallet UUID +// 4. Access the wallet using the UUID +var UserDirectory = assets.AssetType{ + Tag: "userdir", + Label: "User Directory", + Description: "Maps user public key hash to wallet ID for authentication", + + Props: []assets.AssetProp{ + { + Tag: "publicKeyHash", + Label: "Public Key Hash", + DataType: "string", + Required: true, + IsKey: true, + }, + { + Tag: "walletUUID", + Label: "Associated Wallet UUID", + DataType: "string", + Required: true, + }, + { + Tag: "certHash", + Label: "Certificate Hash", + DataType: "string", + Required: true, + }, + }, +} diff --git a/samples/chaincode/confidential-escrow/chaincode/assets/wallet.go b/samples/chaincode/confidential-escrow/chaincode/assets/wallet.go new file mode 100644 index 000000000..80f2886a0 --- /dev/null +++ b/samples/chaincode/confidential-escrow/chaincode/assets/wallet.go @@ -0,0 +1,72 @@ +package assets + +import ( + "github.com/hyperledger-labs/cc-tools/assets" +) + +// Wallet represents a confidential user account for holding multiple digital assets. +// Each wallet maintains separate tracking of available balances and escrowed balances +// to ensure accurate accounting during conditional payment operations. +// +// Balance Management: +// - balances: Freely spendable token amounts +// - escrowBalances: Tokens locked in active escrow contracts +// - digitalAssetTypes: References to the types of tokens held +// +// All three arrays are parallel (same length, matching indices) to maintain +// consistency between asset types and their corresponding balances. +// +// Security: +// - ownerCertHash: Required for all operations to verify wallet ownership +// - ownerPubKey: Public key associated with the wallet for external verification +var Wallet = assets.AssetType{ + Tag: "wallet", + Label: "User Wallet", + Description: "Confidential wallet holding digital assets", + + Props: []assets.AssetProp{ + { + Tag: "walletId", + Label: "Wallet ID", + DataType: "string", + Required: true, + IsKey: true, // primary key + }, + { + Tag: "ownerPubKey", + Label: "Owner Public Key", + DataType: "string", + Required: true, + }, + { + Tag: "ownerCertHash", + Label: "Owner Certificate Hash", + DataType: "string", + Required: true, + }, + { + Tag: "balances", + Label: "Token Balance", + DataType: "[]number", + Required: false, + }, + { + Tag: "escrowBalances", + Label: "Escrowed Token Balances", + DataType: "[]number", + Required: false, // Initialize as empty for existing wallets + }, + { + Tag: "digitalAssetTypes", + Label: "Asset Type Reference", + DataType: "[]->digitalAsset", // References digitalAsset + Required: false, + }, + { + Tag: "createdAt", + Label: "Creation Timestamp", + DataType: "datetime", + Required: false, + }, + }, +} diff --git a/samples/chaincode/confidential-escrow/chaincode/escrow.go b/samples/chaincode/confidential-escrow/chaincode/escrow.go new file mode 100644 index 000000000..780f28d83 --- /dev/null +++ b/samples/chaincode/confidential-escrow/chaincode/escrow.go @@ -0,0 +1,119 @@ +package chaincode + +import ( + "log" + "time" + + "github.com/hyperledger-labs/cc-tools/assets" + tx "github.com/hyperledger-labs/cc-tools/transactions" + + "github.com/hyperledger/fabric-chaincode-go/shim" + pb "github.com/hyperledger/fabric-protos-go/peer" +) + +var startupCheckExecuted = false + +// ConfidentialEscrowCC implements the Hyperledger Fabric chaincode interface +// for confidential escrow operations. +type ConfidentialEscrowCC struct{} + +// Init is called during chaincode instantiation to initialize the ledger state. +// This method performs startup validation checks for assets and transactions +// to ensure the chaincode is properly configured before accepting invocations. +// +// Parameters: +// +// stub: The chaincode stub for ledger interaction +// +// Returns: +// +// response: Peer response indicating success or failure of initialization +func (t *ConfidentialEscrowCC) Init(stub shim.ChaincodeStubInterface) (response pb.Response) { + log.Println("ConfidentialEscrowCC: Init called") + + res := InitFunc(stub) + startupCheckExecuted = true + if res.Status != 200 { + return res + } + + return shim.Success(nil) +} + +// InitFunc performs comprehensive startup validation checks. +// It verifies that all asset types and transaction definitions are properly +// configured and consistent with the chaincode specification. +// +// Parameters: +// +// stub: The chaincode stub for ledger interaction +// +// Returns: +// +// response: Peer response with status 200 on success, error response otherwise +func InitFunc(stub shim.ChaincodeStubInterface) (response pb.Response) { + defer logTx(stub, time.Now(), &response) + + // Run cc-tools startup checks + err := assets.StartupCheck() + if err != nil { + response = err.GetErrorResponse() + return + } + + err = tx.StartupCheck() + if err != nil { + response = err.GetErrorResponse() + return + } + + log.Println("Confidential Escrow chaincode initialized successfully") + return shim.Success(nil) +} + +// Invoke is called for each transaction invocation on the chaincode. +// It ensures proper initialization has occurred and delegates transaction +// execution to the cc-tools transaction runner. +// +// Parameters: +// +// stub: The chaincode stub for ledger interaction +// +// Returns: +// +// response: Peer response containing transaction result or error detail +func (t *ConfidentialEscrowCC) Invoke(stub shim.ChaincodeStubInterface) (response pb.Response) { + defer logTx(stub, time.Now(), &response) + + // Ensure startup check is executed + if !startupCheckExecuted { + log.Println("Running startup check...") + res := InitFunc(stub) + if res.Status != 200 { + return res + } + startupCheckExecuted = true + } + + // Use cc-tools transaction runner + result, err := tx.Run(stub) + if err != nil { + response = err.GetErrorResponse() + return + } + + return shim.Success([]byte(result)) +} + +// logTx logs transaction execution details including status, duration, and any error messages. +// This function is deferred to ensure logging occurs regardless of transaction outcome. +// +// Parameters: +// +// stub: The chaincode stub for accessing transaction context +// beginTime: Transaction start time for duration calculation +// response: Pointer to the peer response for status logging +func logTx(stub shim.ChaincodeStubInterface, beginTime time.Time, response *pb.Response) { + fn, _ := stub.GetFunctionAndParameters() + log.Printf("%d %s %s %s\n", response.Status, fn, time.Since(beginTime), response.Message) +} diff --git a/samples/chaincode/confidential-escrow/chaincode/header/header.go b/samples/chaincode/confidential-escrow/chaincode/header/header.go new file mode 100644 index 000000000..8e3b1eb67 --- /dev/null +++ b/samples/chaincode/confidential-escrow/chaincode/header/header.go @@ -0,0 +1,17 @@ +package header + +var ( + // Name defines the chaincode identifier used in registration and logging. + Name = "Confidential Escrow" + // Version specifies the current chaincode version following semantic versioning. + Version = "1.0.0" + // Colors defines UI color schemes for different contexts. + // The @default scheme uses a blue/gray palette suitable for financial applications. + Colors = map[string][]string{ + "@default": {"#4267B2", "#34495E", "#ECF0F1"}, + } + // Title provides human-readable descriptions for the chaincode in different contexts. + Title = map[string]string{ + "@default": "Confidential Digital Assets & Programmable Escrow", + } +) diff --git a/samples/chaincode/confidential-escrow/chaincode/server.go b/samples/chaincode/confidential-escrow/chaincode/server.go new file mode 100644 index 000000000..f15954051 --- /dev/null +++ b/samples/chaincode/confidential-escrow/chaincode/server.go @@ -0,0 +1,69 @@ +package chaincode + +import ( + "os" + + "github.com/hyperledger/fabric-chaincode-go/shim" + fpc "github.com/hyperledger/fabric-private-chaincode/ecc_go/chaincode" +) + +// RunCCaaS starts the chaincode as a service using the Chaincode as a Service (CCaaS) model. +// It reads server configuration from environment variables and optionally wraps the chaincode +// with FPC (Fabric Private Chaincode) for confidential execution. +// +// Environment Variables: +// +// CHAINCODE_SERVER_ADDRESS: The address and port for the chaincode server +// CHAINCODE_PKG_ID: The chaincode package identifier +// FPC_ENABLED: Set to "true" to enable FPC confidential execution +// +// Returns: +// +// error: Error if server fails to start, nil on successful startup +func RunCCaaS() error { + address := os.Getenv("CHAINCODE_SERVER_ADDRESS") + ccid := os.Getenv("CHAINCODE_PKG_ID") // FPC uses PKG_ID + + var cc shim.Chaincode + if os.Getenv("FPC_ENABLED") == "true" { + cc = fpc.NewPrivateChaincode(new(ConfidentialEscrowCC)) + } else { + cc = new(ConfidentialEscrowCC) + } + + server := &shim.ChaincodeServer{ + CCID: ccid, + Address: address, + CC: cc, + TLSProps: shim.TLSProperties{ + Disabled: true, // TLS handled by FPC + }, + } + + return server.Start() +} + +// StartChaincode initializes and starts the chaincode in the appropriate execution mode. +// It determines the startup mode based on environment variables and handles both +// CCaaS (Chaincode as a Service) and direct execution modes, with optional FPC support. +// +// Environment Variables: +// +// RUN_CCAAS: Set to "true" to run in CCaaS mode +// FPC_ENABLED: Set to "true" to enable FPC confidential execution +// +// Returns: +// +// error: Error if chaincode fails to start, nil on successful startup +func StartChaincode() error { + if os.Getenv("RUN_CCAAS") == "true" { + return RunCCaaS() + } else { + // Fallback for direct start + if os.Getenv("FPC_ENABLED") == "true" { + return shim.Start(fpc.NewPrivateChaincode(new(ConfidentialEscrowCC))) + } else { + return shim.Start(new(ConfidentialEscrowCC)) + } + } +} diff --git a/samples/chaincode/confidential-escrow/chaincode/setup.go b/samples/chaincode/confidential-escrow/chaincode/setup.go new file mode 100644 index 000000000..d4c8e1c7d --- /dev/null +++ b/samples/chaincode/confidential-escrow/chaincode/setup.go @@ -0,0 +1,79 @@ +package chaincode + +import ( + "github.com/hyperledger-labs/cc-tools/assets" + tx "github.com/hyperledger-labs/cc-tools/transactions" + + asset "github.com/hyperledger/fabric-private-chaincode/samples/chaincode/confidential-escrow/chaincode/assets" + header "github.com/hyperledger/fabric-private-chaincode/samples/chaincode/confidential-escrow/chaincode/header" + transaction "github.com/hyperledger/fabric-private-chaincode/samples/chaincode/confidential-escrow/chaincode/transactions" +) + +var ( + // TxList defines all transaction handlers available in the chaincode. + // Transactions are grouped by functionality: + // - Create operations: Initialize new assets (wallets, tokens, escrows) + // - Read operations: Query existing assets + // - Balance operations: Query and verify account balances + // - Token operations: Mint, transfer, and burn digital assets + // - Escrow operations: Manage conditional fund transfers + TxList = []tx.Transaction{ + transaction.DebugTest, + // Create + transaction.CreateUserDir, + transaction.CreateWallet, + transaction.CreateDigitalAsset, + transaction.CreateAndLockEscrow, + // Read + transaction.ReadUserDir, + transaction.ReadDigitalAsset, + transaction.ReadEscrow, + // misc + transaction.GetBalance, + transaction.GetEscrowBalance, + transaction.GetWalletByOwner, + transaction.MintTokens, + transaction.TransferTokens, + transaction.BurnTokens, + // Escrow + transaction.RefundEscrow, + transaction.VerifyEscrowCondition, + transaction.ReleaseEscrow, + } + + // AssetTypeList defines all asset types managed by the chaincode. + // Each asset type represents a core data structure: + // - Wallet: User accounts holding digital assets + // - DigitalAssetToken: Fungible tokens (e.g., CBDC) + // - UserDirectory: Mapping between public keys and wallets + // - Escrow: Conditional payment contracts + AssetTypeList = []assets.AssetType{ + asset.Wallet, + asset.DigitalAssetToken, + asset.UserDirectory, + asset.Escrow, + } +) + +// SetupCC initializes the chaincode with all necessary components. +// This function configures the chaincode header metadata and registers +// all transaction handlers, asset types, and event definitions with cc-tools. +// +// Returns: +// +// error: Error if initialization fails, nil on success +func SetupCC() error { + // Initialize header info + tx.InitHeader(tx.Header{ + Name: header.Name, + Version: header.Version, + Colors: header.Colors, + Title: header.Title, + }) + + // Initialize transaction and asset lists + tx.InitTxList(TxList) + assets.InitAssetList(AssetTypeList) + + return nil +} diff --git a/samples/chaincode/confidential-escrow/chaincode/testutils/assertions.go b/samples/chaincode/confidential-escrow/chaincode/testutils/assertions.go new file mode 100644 index 000000000..6d839109d --- /dev/null +++ b/samples/chaincode/confidential-escrow/chaincode/testutils/assertions.go @@ -0,0 +1,78 @@ +package testutils + +import ( + "encoding/json" + "testing" + + "github.com/hyperledger-labs/cc-tools/errors" +) + +func AssertNoError(t *testing.T, err errors.ICCError, args ...any) { + t.Helper() // Go reports the failure at the caller’s line + if err != nil { + t.Errorf("Expected no error, got: %v %v", err, args) + } +} + +func AssertError(t *testing.T, err errors.ICCError, args ...any) { + t.Helper() + if err == nil { + t.Errorf("Expected error, got nil %v", args) + } +} + +// AssertErrorStatus checks if error has expected status code +func AssertErrorStatus(t *testing.T, err errors.ICCError, expectedStatus int32, msgAndArgs ...any) { + t.Helper() + if err == nil { + t.Errorf("Expected error with status %d, got nil %v", expectedStatus, msgAndArgs) + return + } + if err.Status() != expectedStatus { + t.Errorf("Expected status %d, got %d %v", expectedStatus, err.Status(), msgAndArgs) + } +} + +// AssertEqual checks if two values are equal +func AssertEqual(t *testing.T, expected, actual any, msgAndArgs ...any) { + t.Helper() + if expected != actual { + t.Errorf("Expected %v, got %v %v", expected, actual, msgAndArgs) + } +} + +// AssertJSONContains checks if JSON response contains expected key-value +func AssertJSONContains(t *testing.T, jsonData []byte, key string, expectedValue any) { + t.Helper() + var data map[string]any + if err := json.Unmarshal(jsonData, &data); err != nil { + t.Errorf("Failed to unmarshal JSON: %v", err) + return + } + + actualValue, ok := data[key] + if !ok { + t.Errorf("Key '%s' not found in JSON", key) + return + } + + if actualValue != expectedValue { + t.Errorf("For key '%s': expected %v, got %v", key, expectedValue, actualValue) + } +} + +// AssertStateExists checks if a key exists in mock state +func AssertStateExists(t *testing.T, mockStub *MockStub, key string) { + t.Helper() + if _, exists := mockStub.State[key]; !exists { + t.Errorf("Expected key '%s' to exist in state", key) + } +} + +// AssertStateNotExists checks if a key does not exist in mock state +func AssertStateNotExists(t *testing.T, mockStub *MockStub, key string) { + t.Helper() + if _, exists := mockStub.State[key]; exists { + t.Errorf("Expected key '%s' to not exist in state", key) + } +} diff --git a/samples/chaincode/confidential-escrow/chaincode/testutils/fixtures.go b/samples/chaincode/confidential-escrow/chaincode/testutils/fixtures.go new file mode 100644 index 000000000..7c241c876 --- /dev/null +++ b/samples/chaincode/confidential-escrow/chaincode/testutils/fixtures.go @@ -0,0 +1,235 @@ +package testutils + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "time" +) + +// TestFixtures provides common test data for unit tests +type TestFixtures struct { + // User credentials + BuyerPubKey string + BuyerCertHash string + BuyerPubKeyHash string + BuyerWalletID string + BuyerWalletUUID string + + SellerPubKey string + SellerCertHash string + SellerPubKeyHash string + SellerWalletID string + SellerWalletUUID string + + IssuerCertHash string + + // Asset data + AssetID string + AssetSymbol string + AssetName string + + // Escrow data + EscrowID string + ParcelID string + Secret string + Amount float64 +} + +// NewTestFixtures creates a standard set of test data +func NewTestFixtures() *TestFixtures { + buyerPubKey := "buyer-public-key-123" + sellerPubKey := "seller-public-key-456" + + buyerHash := sha256.Sum256([]byte(buyerPubKey)) + buyerPubKeyHash := hex.EncodeToString(buyerHash[:]) + + sellerHash := sha256.Sum256([]byte(sellerPubKey)) + sellerPubKeyHash := hex.EncodeToString(sellerHash[:]) + + return &TestFixtures{ + BuyerPubKey: buyerPubKey, + BuyerCertHash: "buyer-cert-hash", + BuyerPubKeyHash: buyerPubKeyHash, + BuyerWalletID: "buyer-wallet-id", + BuyerWalletUUID: "buyer-wallet-uuid", + SellerPubKey: sellerPubKey, + SellerCertHash: "seller-cert-hash", + SellerPubKeyHash: sellerPubKeyHash, + SellerWalletID: "seller-wallet-id", + SellerWalletUUID: "seller-wallet-uuid", + IssuerCertHash: "issuer-cert-hash", + AssetID: "test-asset-id", + AssetSymbol: "TST", + AssetName: "Test Token", + EscrowID: "test-escrow-id", + ParcelID: "parcel-123", + Secret: "secret-key", + Amount: 100.0, + } +} + +// CreateMockWallet creates a wallet asset in the mock state +// walletID is the user-provided nickname +// walletUUID is the cc-tools generated unique identifier +func (f *TestFixtures) CreateMockWallet(mockStub *MockStub, pubKey, certHash, walletID, walletUUID string, assetID string, balance, escrowBalance float64) error { + fmt.Printf("\nDEBUG CreateMockWallet called:\n") + fmt.Printf("DEBUG pubKey: %q\n", pubKey) + fmt.Printf("DEBUG walletID: %q\n", walletID) + fmt.Printf("DEBUG walletUUID: %q\n", walletUUID) + + walletMap := map[string]any{ + "@assetType": "wallet", + "@key": "wallet:" + walletUUID, + "walletId": walletID, + "ownerPubKey": pubKey, + "ownerCertHash": certHash, + "balances": []any{balance}, + "escrowBalances": []any{escrowBalance}, + "digitalAssetTypes": []any{ + map[string]any{ + "@key": "digitalAsset:" + assetID, + }, + }, + "createdAt": time.Now(), + } + + walletJSON, err := json.Marshal(walletMap) + if err != nil { + return err + } + + fmt.Printf("DEBUG Storing with key: %q\n", "wallet:"+walletUUID) + return mockStub.PutState("wallet:"+walletUUID, walletJSON) +} + +// func (f *TestFixtures) CreateMockWallet(mockStub *MockStub, pubKey, certHash, walletID, walletUUID string, assetID string, balance, escrowBalance float64) error { +// walletMap := map[string]any{ +// "@assetType": "wallet", +// "@key": "wallet:" + walletUUID, // CC-tools composite key +// "walletId": walletID, // User-provided nickname +// "ownerPubKey": pubKey, +// "ownerCertHash": certHash, +// "balances": []any{balance}, +// "escrowBalances": []any{escrowBalance}, +// "digitalAssetTypes": []any{ +// map[string]any{ +// "@key": "digitalAsset:" + assetID, +// }, +// }, +// "createdAt": time.Now(), +// } +// +// walletJSON, err := json.Marshal(walletMap) +// if err != nil { +// return err +// } +// +// // Store by UUID (the actual ledger key) +// return mockStub.PutState("wallet:"+walletUUID, walletJSON) +// } + +// CreateMockUserDir creates a user directory entry in the mock state +// The UserDirectory maps publicKeyHash -> walletUUID (NOT walletID) +func (f *TestFixtures) CreateMockUserDir(mockStub *MockStub, pubKeyHash, walletUUID, certHash string) error { + // Generate UUID for @key + hash := sha256.Sum256([]byte("userdir" + pubKeyHash)) + uuidStr := fmt.Sprintf("%x-%x-%x-%x-%x", + hash[0:4], hash[4:6], hash[6:8], hash[8:10], hash[10:16]) + uuidKey := "userdir:" + uuidStr + + userDirMap := map[string]any{ + "@assetType": "userdir", + "@key": uuidKey, + "publicKeyHash": pubKeyHash, + "walletUUID": walletUUID, // References the UUID, not the ID + "certHash": certHash, + } + + userDirJSON, err := json.Marshal(userDirMap) + if err != nil { + return err + } + + // Store with UUID key - PutState will auto-create composite index based on registry + return mockStub.PutState(uuidKey, userDirJSON) +} + +// CreateMockDigitalAsset creates a digital asset in the mock state +// assetID is cc-tools generated UUID +// symbol is user-provided unique identifier +func (f *TestFixtures) CreateMockDigitalAsset(mockStub *MockStub, assetID, symbol, name, issuerHash string, totalSupply float64) error { + assetMap := map[string]any{ + "@assetType": "digitalAsset", + "@key": "digitalAsset:" + assetID, // CC-tools composite key uses UUID + "name": name, + "symbol": symbol, // This is the unique key property (IsKey: true) + "decimals": 2.0, + "totalSupply": totalSupply, + "issuerHash": issuerHash, + "owner": "test-owner", + "issuedAt": time.Now(), + } + + assetJSON, err := json.Marshal(assetMap) + if err != nil { + return err + } + + return mockStub.PutState("digitalAsset:"+assetID, assetJSON) +} + +// CreateMockEscrow creates an escrow contract in the mock state +// escrowID is user-provided unique identifier (IsKey: true) +func (f *TestFixtures) CreateMockEscrow( + mockStub *MockStub, + escrowID string, + buyerPubKey string, + sellerPubKey string, + buyerWalletUUID string, + sellerWalletUUID string, + assetID string, + amount float64, + parcelID string, + secret string, + status string, + buyerCertHash string, +) error { + // Compute condition hash: SHA256(secret + parcelId) + conditionData := secret + parcelID + conditionHash := sha256.Sum256([]byte(conditionData)) + conditionValue := hex.EncodeToString(conditionHash[:]) + + escrowMap := map[string]any{ + "@assetType": "escrow", + "@key": "escrow:" + escrowID, + "escrowId": escrowID, + "buyerPubKey": buyerPubKey, + "sellerPubKey": sellerPubKey, + "buyerWalletUUID": buyerWalletUUID, + "sellerWalletUUID": sellerWalletUUID, + "amount": amount, + "assetType": map[string]any{ + "@key": "digitalAsset:" + assetID, + }, + "parcelId": parcelID, + "conditionValue": conditionValue, // Computed from secret + parcelID + "status": status, + "createdAt": time.Now(), + "buyerCertHash": buyerCertHash, + } + + escrowJSON, err := json.Marshal(escrowMap) + if err != nil { + return err + } + + return mockStub.PutState("escrow:"+escrowID, escrowJSON) +} + +// ComputeConditionHash computes the escrow condition hash +func (f *TestFixtures) ComputeConditionHash(secret, parcelID string) string { + hash := sha256.Sum256([]byte(secret + parcelID)) + return hex.EncodeToString(hash[:]) +} diff --git a/samples/chaincode/confidential-escrow/chaincode/testutils/mocks.go b/samples/chaincode/confidential-escrow/chaincode/testutils/mocks.go new file mode 100644 index 000000000..c1f3281ba --- /dev/null +++ b/samples/chaincode/confidential-escrow/chaincode/testutils/mocks.go @@ -0,0 +1,558 @@ +package testutils + +import ( + "container/list" + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/golang/protobuf/proto" + "github.com/golang/protobuf/ptypes/timestamp" + sw "github.com/hyperledger-labs/cc-tools/stubwrapper" + "github.com/hyperledger/fabric-chaincode-go/shim" + "github.com/hyperledger/fabric-protos-go/ledger/queryresult" + "github.com/hyperledger/fabric-protos-go/msp" + "github.com/hyperledger/fabric-protos-go/peer" +) + +// MockStub provides a test implementation of MockStubInterface +// It simulates the Fabric ledger state for unit testing without requiring +// a running blockchain network. +type MockStub struct { + State map[string][]byte // State stores key-value pairs simulating the ledger + TransientMap map[string][]byte // TransientMap stores transient data for the current transaction + TxID string // TxID is the simulated transaction ID + ChannelID string // ChannelID is the simulated channel name + Creator []byte // Creator simulates the transaction creator's certificate + Invocations []string // Invocations tracks function calls for verification + Keys *list.List + // PropertyIndex map[string]map[string]string // assetType → property → key +} + +// CompositeKeyRegistry defines which properties should be indexed for each asset type +// This makes the mock generic and extensible +var CompositeKeyRegistry = map[string][]string{ + "userdir": {"publicKeyHash"}, + "wallet": {"ownerPubKey"}, + "digitalAsset": {"symbol"}, + "escrow": {"escrowId"}, +} + +// NewMockStub creates a new mock stub with initialized state +func NewMockStub() *MockStub { + mockCert := `-----BEGIN CERTIFICATE----- +MIICJjCCAcygAwIBAgIQHv152Ul3TG/REl3mHfYyUjAKBggqhkjOPQQDAjBxMQsw +CQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZy +YW5jaXNjbzEYMBYGA1UEChMPb3JnLmV4YW1wbGUuY29tMRswGQYDVQQDExJjYS5v +cmcuZXhhbXBsZS5jb20wHhcNMjQwNTA5MjEwOTAwWhcNMzQwNTA3MjEwOTAwWjBq +MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2Fu +IEZyYW5jaXNjbzEOMAwGA1UECxMFYWRtaW4xHjAcBgNVBAMMFUFkbWluQG9yZy5l +eGFtcGxlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABAoAt6mlUBMB0Ab1 +paR0ILegN6qKmNfOYR0WV0kGOQwkO4lYcN76lSA2wSlWNTgxtGQDzja1708Ezdr5 +vJ5KFhmjTTBLMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMCsGA1UdIwQk +MCKAID7vB1ct0j2yeNTm45AlCyj9TW22dYtjmPOGq+SVlMKQMAoGCCqGSM49BAMC +A0gAMEUCIQDYol2ylLCcz8qrGJmAFEG/cIG2Kxv8BD5t7Gv/28y8kgIgTz0Y75p6 +3kbL5VN/PCiG2SbX72AVPSiEqj6PSiZJMz4= +-----END CERTIFICATE-----` + + // Create a mock serialized identity that cc-tools can parse + mockIdentity := &msp.SerializedIdentity{ + Mspid: "Org1MSP", // Match the Callers in your transactions + IdBytes: []byte(mockCert), + } + + creatorBytes, _ := proto.Marshal(mockIdentity) + + return &MockStub{ + State: make(map[string][]byte), + TransientMap: make(map[string][]byte), + TxID: "mock-tx-id", + ChannelID: "mock-channel", + Creator: creatorBytes, + Invocations: []string{}, + Keys: list.New(), + } +} + +// GetState retrieves the value for a given key from mock state +func (m *MockStub) GetState(key string) ([]byte, error) { + fmt.Printf("DEBUG GetState called with key: %q\n", key) + m.Invocations = append(m.Invocations, fmt.Sprintf("GetState:%s", key)) + + // First: Always try direct lookup + if value, exists := m.State[key]; exists { + fmt.Printf("DEBUG Direct lookup SUCCESS for key: %q\n", key) + return value, nil + } + fmt.Printf("DEBUG Direct lookup FAILED for key: %q\n", key) + + // Second: If key matches "assetType:uuid" pattern, search composite keys + if strings.Contains(key, ":") { + parts := strings.Split(key, ":") + if len(parts) == 2 { + assetType := parts[0] + fmt.Printf("DEBUG Searching composite keys for assetType: %q\n", assetType) + + // Search all composite keys for this asset type + compositePrefix := assetType + "\x00" + fmt.Printf("DEBUG Composite prefix: %q\n", compositePrefix) + + // Just return the FIRST matching composite key + // Don't try to match @key since CCTools generates different UUIDs + for stateKey, value := range m.State { + if strings.HasPrefix(stateKey, compositePrefix) { + fmt.Printf("DEBUG Found composite key: %q - RETURNING IT\n", stateKey) + return value, nil + } + } + + fmt.Printf("DEBUG No composite keys found with prefix: %q\n", compositePrefix) + } + } + + fmt.Printf("DEBUG GetState returning nil for key: %q\n", key) + return nil, nil +} + +// PutState stores a key-value pair in mock state +func (m *MockStub) PutState(key string, value []byte) error { + fmt.Printf("DEBUG PutState called with key: %q\n", key) + m.Invocations = append(m.Invocations, fmt.Sprintf("PutState:%s", key)) + + if len(value) == 0 { + delete(m.State, key) + return nil + } + + m.State[key] = value + fmt.Printf("DEBUG Stored key: %q\n", key) + + // Generic composite key indexing + var assetData map[string]any + if err := json.Unmarshal(value, &assetData); err == nil { + assetType, _ := assetData["@assetType"].(string) + fmt.Printf("DEBUG Asset type: %q\n", assetType) + + // Check if this asset type has registered composite key properties + if indexedProps, exists := CompositeKeyRegistry[assetType]; exists { + fmt.Printf("DEBUG Indexed properties for %q: %v\n", assetType, indexedProps) + + // For each indexed property, create a composite key + for _, propName := range indexedProps { + if propValue, ok := assetData[propName].(string); ok && propValue != "" { + compositeKey := assetType + string('\x00') + propValue + m.State[compositeKey] = value + fmt.Printf("DEBUG Created composite key: %q (property: %s=%s)\n", compositeKey, propName, propValue) + } else { + fmt.Printf("DEBUG Property %q not found or empty\n", propName) + } + } + } else { + fmt.Printf("DEBUG No composite key registry entry for asset type: %q\n", assetType) + } + } + + // Maintain ordered key list + inserted := false + for elem := m.Keys.Front(); elem != nil; elem = elem.Next() { + elemValue := elem.Value.(string) + comp := strings.Compare(key, elemValue) + if comp < 0 { + m.Keys.InsertBefore(key, elem) + inserted = true + break + } else if comp == 0 { + inserted = true + break + } + } + + if !inserted { + if m.Keys.Len() == 0 { + m.Keys.PushFront(key) + } else { + m.Keys.PushBack(key) + } + } + + return nil +} + +// func (m *MockStub) PutState(key string, value []byte) error { +// m.Invocations = append(m.Invocations, fmt.Sprintf("PutState:%s", key)) +// +// // If value is empty, delete the key +// if len(value) == 0 { +// delete(m.State, key) +// return nil +// } +// +// m.State[key] = value +// +// // Generic composite key indexing for any registered asset type +// var assetData map[string]any +// if err := json.Unmarshal(value, &assetData); err == nil { +// assetType, _ := assetData["@assetType"].(string) +// +// // Check if this asset type has registered composite key properties +// if indexedProps, exists := CompositeKeyRegistry[assetType]; exists { +// // For each indexed property, create a composite key +// for _, propName := range indexedProps { +// if propValue, ok := assetData[propName].(string); ok && propValue != "" { +// // Create composite key: assetType\x00propertyValue +// compositeKey := assetType + string('\x00') + propValue +// m.State[compositeKey] = value +// } +// } +// } +// } +// +// // Maintain ordered key list +// inserted := false +// for elem := m.Keys.Front(); elem != nil; elem = elem.Next() { +// elemValue := elem.Value.(string) +// comp := strings.Compare(key, elemValue) +// if comp < 0 { +// m.Keys.InsertBefore(key, elem) +// inserted = true +// break +// } else if comp == 0 { +// // Key already exists +// inserted = true +// break +// } +// } +// +// // If not inserted and list is not empty, add to end +// if !inserted { +// if m.Keys.Len() == 0 { +// m.Keys.PushFront(key) +// } else { +// m.Keys.PushBack(key) +// } +// } +// +// return nil +// } + +// DelState removes a key from mock state +func (m *MockStub) DelState(key string) error { + m.Invocations = append(m.Invocations, fmt.Sprintf("DeleteState:%s", key)) + delete(m.State, key) + return nil +} + +// GetStateByRange returns an iterator for keys within a range +func (m *MockStub) GetStateByRange(startKey, endKey string) (shim.StateQueryIteratorInterface, error) { + return NewMockStateRangeQueryIterator(m, startKey, endKey), nil +} + +// GetStateByPartialCompositeKey returns an iterator for composite keys +func (m *MockStub) GetStateByPartialCompositeKey(objectType string, keys []string) (shim.StateQueryIteratorInterface, error) { + partialCompositeKey := objectType + for _, key := range keys { + partialCompositeKey += string('\x00') + key + } + + fmt.Printf("DEBUG GetStateByPartialCompositeKey called:\n") + fmt.Printf("DEBUG objectType: %q\n", objectType) + fmt.Printf("DEBUG keys: %v\n", keys) + fmt.Printf("DEBUG partialCompositeKey: %q\n", partialCompositeKey) + + fmt.Printf("DEBUG Available keys in state:\n") + for k := range m.State { + fmt.Printf("DEBUG %q\n", k) + } + + // Use range query from partial key to partial key + max unicode + return NewMockStateRangeQueryIterator(m, partialCompositeKey, partialCompositeKey+string(rune(0x10FFFF))), nil +} + +// GetQueryResult executes a rich query (not implemented in mock) +func (m *MockStub) GetQueryResult(query string) (shim.StateQueryIteratorInterface, error) { + return NewMockStateRangeQueryIterator(m, "", ""), nil +} + +// GetHistoryForKey returns history for a key (not implemented in mock) +func (m *MockStub) GetHistoryForKey(key string) (shim.HistoryQueryIteratorInterface, error) { + return &MockHistoryIterator{}, nil +} + +// CreateCompositeKey creates a composite key +func (m *MockStub) CreateCompositeKey(objectType string, attributes []string) (string, error) { + // return objectType + ":" + attributes[0], nil + key := objectType + for _, attr := range attributes { + key += string('\x00') + attr + } + // fmt.Printf("DEBUG CreateCompositeKey: objectType=%q, attributes=%v, result=%q\n", objectType, attributes, key) + return key, nil +} + +// SplitCompositeKey splits a composite key +func (m *MockStub) SplitCompositeKey(compositeKey string) (string, []string, error) { + return "", []string{}, nil +} + +// GetTransient returns the transient map +func (m *MockStub) GetTransient() (map[string][]byte, error) { + return m.TransientMap, nil +} + +// GetTxID returns the transaction ID +func (m *MockStub) GetTxID() string { + return m.TxID +} + +// GetChannelID returns the channel ID +func (m *MockStub) GetChannelID() string { + return m.ChannelID +} + +// GetTxTimestamp returns a mock timestamp +func (m *MockStub) GetTxTimestamp() (*timestamp.Timestamp, error) { + now := time.Now() + return ×tamp.Timestamp{ + Seconds: now.Unix(), + Nanos: int32(now.Nanosecond()), + }, nil +} + +// GetCreator returns the transaction creator +func (m *MockStub) GetCreator() ([]byte, error) { + return m.Creator, nil +} + +// GetDecorations is a no-op +func (s *MockStub) GetDecorations() map[string][]byte { + return make(map[string][]byte) +} + +// GetBinding returns empty binding +func (m *MockStub) GetBinding() ([]byte, error) { + return []byte{}, nil +} + +// GetSignedProposal returns nil +func (m *MockStub) GetSignedProposal() (*peer.SignedProposal, error) { + return nil, nil +} + +// GetArgs returns empty args +func (m *MockStub) GetArgs() [][]byte { + return [][]byte{} +} + +// GetStringArgs returns empty args +func (m *MockStub) GetStringArgs() []string { + return []string{} +} + +// GetFunctionAndParameters returns mock function name +func (m *MockStub) GetFunctionAndParameters() (string, []string) { + return "mockFunction", []string{} +} + +// GetArgsSlice returns empty slice +func (m *MockStub) GetArgsSlice() ([]byte, error) { + return []byte{}, nil +} + +// SetEvent sets an event (no-op in mock) +func (m *MockStub) SetEvent(name string, payload []byte) error { + return nil +} + +// InvokeChaincode simulates chaincode invocation (not implemented) +func (m *MockStub) InvokeChaincode(chaincodeName string, args [][]byte, channel string) peer.Response { + return shim.Success(nil) +} + +// GetStateValidationParameter returns nil +func (m *MockStub) GetStateValidationParameter(key string) ([]byte, error) { + return nil, nil +} + +// SetStateValidationParameter is a no-op +func (m *MockStub) SetStateValidationParameter(key string, ep []byte) error { + return nil +} + +// GetPrivateData returns nil +func (m *MockStub) GetPrivateData(collection, key string) ([]byte, error) { + return nil, nil +} + +// GetPrivateDataHash is a no-op +func (s *MockStub) GetPrivateDataHash(collection string, key string) ([]byte, error) { + return nil, nil +} + +// PutPrivateData is a no-op +func (m *MockStub) PutPrivateData(collection, key string, value []byte) error { + return nil +} + +// DelPrivateData is a no-op +func (m *MockStub) DelPrivateData(collection, key string) error { + return nil +} + +// GetPrivateDataByRange returns empty iterator +func (m *MockStub) GetPrivateDataByRange(collection, startKey, endKey string) (shim.StateQueryIteratorInterface, error) { + return NewMockStateRangeQueryIterator(m, startKey, endKey), nil +} + +// GetPrivateDataByPartialCompositeKey returns empty iterator +func (m *MockStub) GetPrivateDataByPartialCompositeKey(collection, objectType string, keys []string) (shim.StateQueryIteratorInterface, error) { + return NewMockStateRangeQueryIterator(m, "", ""), nil +} + +// GetPrivateDataQueryResult returns empty iterator +func (m *MockStub) GetPrivateDataQueryResult(collection, query string) (shim.StateQueryIteratorInterface, error) { + return NewMockStateRangeQueryIterator(m, "", ""), nil +} + +// GetPrivateDataValidationParameter returns nil +func (m *MockStub) GetPrivateDataValidationParameter(collection, key string) ([]byte, error) { + return nil, nil +} + +// SetPrivateDataValidationParameter is a no-op +func (m *MockStub) SetPrivateDataValidationParameter(collection, key string, ep []byte) error { + return nil +} + +// GetQueryResultWithPagination is a no-op +func (s *MockStub) GetQueryResultWithPagination(query string, pageSize int32, bookmark string) (shim.StateQueryIteratorInterface, *peer.QueryResponseMetadata, error) { + return nil, nil, nil +} + +// GetStateByPartialCompositeKeyWithPagination is a no-op +func (s *MockStub) GetStateByPartialCompositeKeyWithPagination(objectType string, keys []string, pageSize int32, bookmark string) (shim.StateQueryIteratorInterface, *peer.QueryResponseMetadata, error) { + return nil, nil, nil +} + +// GetStateByRangeWithPagination is a no-op +func (s *MockStub) GetStateByRangeWithPagination(startKey, endKey string, pageSize int32, bookmark string) (shim.StateQueryIteratorInterface, *peer.QueryResponseMetadata, error) { + return nil, nil, nil +} + +// PurgePrivateData is a no-op +func (s *MockStub) PurgePrivateData(collection string, key string) error { + return nil +} + +// /////////////////////////////////////////////////////////////// +// MockHistoryIterator implements HistoryQueryIteratorInterface // +// /////////////////////////////////////////////////////////////// +type MockHistoryIterator struct{} + +func (m *MockHistoryIterator) HasNext() bool { + return false +} + +func (m *MockHistoryIterator) Next() (*queryresult.KeyModification, error) { + return nil, fmt.Errorf("no history") +} + +func (m *MockHistoryIterator) Close() error { + return nil +} + +// //////////////////////////////////////////////////////////// +// MockStubWrapper wraps MockStub for cc-tools compatibility // +// //////////////////////////////////////////////////////////// +type MockStubWrapper struct { + *sw.StubWrapper + mockStub *MockStub +} + +// NewMockStubWrapper creates a wrapped mock stub +func NewMockStubWrapper() (*MockStubWrapper, *MockStub) { + mockStub := NewMockStub() + wrapper := &sw.StubWrapper{ + Stub: mockStub, + } + return &MockStubWrapper{ + StubWrapper: wrapper, + mockStub: mockStub, + }, mockStub +} + +// GetMockStub returns the underlying mock stub for assertions +func (m *MockStubWrapper) GetMockStub() *MockStub { + return m.mockStub +} + +// ////////////////////////////// +// MockStateRangeQueryIterator // +// ////////////////////////////// +type MockStateRangeQueryIterator struct { + Closed bool + Stub *MockStub + StartKey string + EndKey string + Current *list.Element +} + +func (iter *MockStateRangeQueryIterator) HasNext() bool { + if iter.Closed { + return false + } + if iter.Current == nil { + return false + } + + current := iter.Current + for current != nil { + if iter.StartKey == "" && iter.EndKey == "" { + return true + } + key := current.Value.(string) + if strings.Compare(key, iter.StartKey) >= 0 && strings.Compare(key, iter.EndKey) < 0 { + return true + } + if strings.Compare(key, iter.EndKey) >= 0 { + return false + } + current = current.Next() + } + return false +} + +func (iter *MockStateRangeQueryIterator) Next() (*queryresult.KV, error) { + if iter.Closed { + return nil, fmt.Errorf("iterator closed") + } + if !iter.HasNext() { + return nil, fmt.Errorf("no more elements") + } + + for iter.Current != nil { + key := iter.Current.Value.(string) + if strings.Compare(key, iter.StartKey) >= 0 && strings.Compare(key, iter.EndKey) < 0 { + value := iter.Stub.State[key] + iter.Current = iter.Current.Next() + return &queryresult.KV{Key: key, Value: value}, nil + } + iter.Current = iter.Current.Next() + } + return nil, fmt.Errorf("no matching key found") +} + +func (iter *MockStateRangeQueryIterator) Close() error { + iter.Closed = true + return nil +} + +func NewMockStateRangeQueryIterator(stub *MockStub, startKey, endKey string) *MockStateRangeQueryIterator { + return &MockStateRangeQueryIterator{ + Closed: false, + Stub: stub, + StartKey: startKey, + EndKey: endKey, + Current: stub.Keys.Front(), + } +} diff --git a/samples/chaincode/confidential-escrow/chaincode/transactions/debug.go b/samples/chaincode/confidential-escrow/chaincode/transactions/debug.go new file mode 100644 index 000000000..95829acde --- /dev/null +++ b/samples/chaincode/confidential-escrow/chaincode/transactions/debug.go @@ -0,0 +1,19 @@ +package transactions + +import ( + "github.com/hyperledger-labs/cc-tools/errors" + sw "github.com/hyperledger-labs/cc-tools/stubwrapper" + "github.com/hyperledger-labs/cc-tools/transactions" +) + +var DebugTest = transactions.Transaction{ + Tag: "debugTest", + Label: "Debug Test", + Description: "Test transaction with no access control", + Method: "GET", + // NO Callers field at all + Args: []transactions.Argument{}, + Routine: func(stub *sw.StubWrapper, req map[string]interface{}) ([]byte, errors.ICCError) { + return []byte("Debug test successful"), nil + }, +} diff --git a/samples/chaincode/confidential-escrow/chaincode/transactions/digitalAsset.go b/samples/chaincode/confidential-escrow/chaincode/transactions/digitalAsset.go new file mode 100644 index 000000000..d2f09383d --- /dev/null +++ b/samples/chaincode/confidential-escrow/chaincode/transactions/digitalAsset.go @@ -0,0 +1,804 @@ +// This file implements transaction handlers for digital asset token lifecycle management. +// It provides operations for creating, reading, minting, transferring, and burning +// confidential digital tokens with issuer-controlled supply management. +package transactions + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/hyperledger-labs/cc-tools/accesscontrol" + "github.com/hyperledger-labs/cc-tools/assets" + "github.com/hyperledger-labs/cc-tools/errors" + "github.com/hyperledger-labs/cc-tools/events" + sw "github.com/hyperledger-labs/cc-tools/stubwrapper" + "github.com/hyperledger-labs/cc-tools/transactions" +) + +// CreateDigitalAsset initializes a new digital asset token type with fixed parameters. +// Only authorized administrators from Org1MSP or Org2MSP can create new token types. +// The issuer's certificate hash is stored for subsequent authorization of mint/burn operations. +// +// Arguments: +// - name: Human-readable token name (e.g., "US Dollar Token") +// - symbol: Unique token identifier (e.g., "USDT") +// - decimals: Number of decimal places for token precision +// - totalSupply: Initial total supply of tokens +// - owner: Identity of the token creator +// - issuedAt: (Optional) Timestamp of token creation, defaults to current time +// - issuerHash: Certificate hash of the issuer for access control +// +// Returns: +// - JSON representation of the created digital asset +// - Error if asset creation or blockchain persistence fails +// +// Security: Only the entity with matching issuerHash can mint or burn these tokens. +var CreateDigitalAsset = transactions.Transaction{ + Tag: "createDigitalAsset", + Label: "Digital Asset Creation", + Description: "Creates a new Digital Asset e.g. CBDC Tokens", + Method: "POST", + Callers: []accesscontrol.Caller{ + { + MSP: "Org1MSP", + OU: "admin", + }, { + MSP: "Org2MSP", + OU: "admin", + }, + }, + + Args: []transactions.Argument{ + { + Tag: "name", + Label: "Name", + Description: "Name of the Digital Asset", + DataType: "string", + Required: true, + }, + { + Tag: "symbol", + Label: "Symbol", + Description: "Symbol of the Digital Asset", + DataType: "string", + Required: true, + }, + { + Tag: "decimals", + Label: "Decimal Places", + Description: "Decimal Places in Digital Asset", + DataType: "number", + Required: true, + }, + { + Tag: "totalSupply", + Label: "Total Supply", + Description: "Total Supply of the Digital Asset", + DataType: "number", + Required: true, + }, + { + Tag: "owner", + Label: "Owner Identity", + Description: "Identitiy of Digital Asset's creator", + DataType: "string", + Required: true, + }, + { + Tag: "issuedAt", + Label: "Issued At", + Description: "Time at which this token was created", + DataType: "datetime", + Required: false, + }, + { + Tag: "issuerHash", + Label: "Issuer Certificate Hash", + Description: "Hash of Issuer's Certificate who created this Digital Asset", + DataType: "string", + Required: true, + }, + }, + + Routine: func(stub *sw.StubWrapper, req map[string]any) ([]byte, errors.ICCError) { + name, _ := req["name"].(string) + symbol, _ := req["symbol"].(string) + decimals, _ := req["decimals"].(float64) + totalSupply, _ := req["totalSupply"].(float64) + owner, _ := req["owner"].(string) + issuerHash, _ := req["issuerHash"].(string) + + assetMap := make(map[string]any) + assetMap["@assetType"] = "digitalAsset" + assetMap["name"] = name + assetMap["symbol"] = symbol + assetMap["decimals"] = decimals + assetMap["totalSupply"] = totalSupply + assetMap["owner"] = owner + assetMap["issuedAt"] = time.Now() + assetMap["issuerHash"] = issuerHash + + digitalAsset, err := assets.NewAsset(assetMap) + if err != nil { + return nil, errors.WrapError(err, "Failed to create digital asset") + } + + _, err = digitalAsset.PutNew(stub) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Error saving digital asset on blockchain", err.Status()) + } + + assetJSON, nerr := json.Marshal(digitalAsset) + if nerr != nil { + return nil, errors.WrapError(nil, "failed to encode asset to JSON format") + } + + logMsg, ok := json.Marshal(fmt.Sprintf("New Digital Asset created: %s", name)) + if ok != nil { + return nil, errors.WrapError(nil, "failed to encode asset to JSON format") + } + + events.CallEvent(stub, "createDigitalAssetLog", logMsg) + + return assetJSON, nil + }, +} + +// ReadDigitalAsset retrieves a digital asset token by its unique identifier. +// This operation is read-only and does not modify ledger state. +// +// Arguments: +// - uuid: Unique identifier of the digital asset to retrieve +// +// Returns: +// - JSON representation of the digital asset +// - Error if asset not found or retrieval fails +// +// Note: Consider implementing symbol-based lookup for improved user experience. +var ReadDigitalAsset = transactions.Transaction{ + Tag: "readDigitalAsset", + Label: "Read Digital Asset", + Description: "Read a Digital Asset by its symbol", + Method: "GET", + Callers: []accesscontrol.Caller{ + { + MSP: "Org1MSP", + OU: "admin", + }, { + MSP: "Org2MSP", + OU: "admin", + }, + }, + + Args: []transactions.Argument{ + // need to find a better way other than UUID.. i.e. search via Symbol or something + { + Tag: "uuid", + Label: "UUID", + Description: "UUID of the Digital Asset to read", + DataType: "string", + Required: true, + }, + }, + + Routine: func(stub *sw.StubWrapper, req map[string]any) ([]byte, errors.ICCError) { + uuid, _ := req["uuid"].(string) + key := assets.Key{ + "@key": "digitalAsset:" + uuid, + } + + asset, err := key.Get(stub) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Error reading digital asset from blockchain", err.Status()) + } + + assetJSON, nerr := json.Marshal(asset) + if nerr != nil { + return nil, errors.WrapError(nil, "failed to encode asset to JSON format") + } + + return assetJSON, nil + }, +} + +// MintTokens creates new token units and adds them to a specified wallet. +// This operation increases both the wallet balance and the token's total supply. +// Only the original issuer (verified by certificate hash) can mint new tokens. +// +// Arguments: +// - assetId: UUID of the digital asset token type +// - pubKey: Public key of the recipient wallet owner +// - amount: Number of tokens to mint +// - issuerCertHash: Certificate hash of the issuer for authorization +// +// Process Flow: +// 1. Resolve wallet UUID from public key hash via UserDirectory +// 2. Verify issuer authorization against stored issuerHash +// 3. Update or initialize wallet balance for the asset type +// 4. Increment the token's total supply +// +// Returns: +// - JSON response with minting details and updated total supply +// - Error if authorization fails, wallet not found, or update fails +// +// Security: Unauthorized minting attempts are rejected with 403 status. +var MintTokens = transactions.Transaction{ + Tag: "mintTokens", + Label: "Mint Tokens", + Description: "Mint new tokens to a wallet (issuer only)", + Method: "POST", + Callers: []accesscontrol.Caller{ + { + MSP: "Org1MSP", + OU: "admin", + }, + { + MSP: "Org2MSP", + OU: "admin", + }, + }, + + Args: []transactions.Argument{ + { + Tag: "assetId", + Label: "Asset ID", + Description: "ID of the digital asset", + DataType: "string", + Required: true, + }, + { + Tag: "pubKey", + Label: "Public Key", + DataType: "string", + Required: true, + }, + { + Tag: "amount", + Label: "Amount to Mint", + Description: "Number of tokens to mint", + DataType: "number", + Required: true, + }, + { + Tag: "issuerCertHash", + Label: "Issuer Certificate Hash", + Description: "Certificate hash for issuer verification", + DataType: "string", + Required: true, + }, + }, + + Routine: func(stub *sw.StubWrapper, req map[string]any) ([]byte, errors.ICCError) { + assetId, _ := req["assetId"].(string) + pubKey, _ := req["pubKey"].(string) + amount, _ := req["amount"].(float64) + issuerCertHash, _ := req["issuerCertHash"].(string) + + // Lookup wallet using publicKeyHash property + hash := sha256.Sum256([]byte(pubKey)) + pubKeyHash := hex.EncodeToString(hash[:]) + + userDirKey, err := assets.NewKey(map[string]any{ + "@assetType": "userdir", + "publicKeyHash": pubKeyHash, + }) + if err != nil { + return nil, errors.NewCCError(fmt.Sprintf("Seller's Key cannot be found from user dir: %v", err), 404) + } + + userDir, err := userDirKey.Get(stub) + if err != nil { + return nil, errors.NewCCError("Buyer wallet not found. Buyer must create wallet first.", 404) + } + walletUUID := userDir.GetProp("walletUUID").(string) + + // Verify issuer authorization + assetKey := assets.Key{"@key": "digitalAsset:" + assetId} + asset, err := assetKey.Get(stub) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Error reading digital asset", err.Status()) + } + + if asset.GetProp("issuerHash").(string) != issuerCertHash { + return nil, errors.NewCCError("Unauthorized: Only asset issuer can mint tokens", 403) + } + + // Get wallet + walletKey := assets.Key{"@key": "wallet:" + walletUUID} + walletAsset, err := walletKey.Get(stub) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Error reading wallet", err.Status()) + } + + digitalAssetTypes := walletAsset.GetProp("digitalAssetTypes").([]any) + balances := walletAsset.GetProp("balances").([]any) + escrowBalances := walletAsset.GetProp("escrowBalances").([]any) + + // Find asset index and update balance + assetFound := false + for i, assetRef := range digitalAssetTypes { + var refAssetId string + switch ref := assetRef.(type) { + case map[string]any: + refAssetId = strings.Split(ref["@key"].(string), ":")[1] + case string: + refAssetId = ref + } + + if refAssetId == assetId { + currentBalance := balances[i].(float64) + balances[i] = currentBalance + amount + assetFound = true + break + } + } + + if !assetFound { + digitalAssetTypes = append(digitalAssetTypes, map[string]any{ + "@key": "digitalAsset:" + assetId, + }) + balances = append(balances, amount) + escrowBalances = append(escrowBalances, 0.0) + } + + // Update wallet + walletUpdate := map[string]any{ + "balances": balances, + "escrowBalances": escrowBalances, + "digitalAssetTypes": digitalAssetTypes, + } + + _, err = walletAsset.Update(stub, walletUpdate) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Error updating wallet", err.Status()) + } + + // Update total supply + currentSupply := asset.GetProp("totalSupply").(float64) + assetUpdate := map[string]any{ + "totalSupply": currentSupply + amount, + } + + _, err = asset.Update(stub, assetUpdate) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Error updating asset", err.Status()) + } + + response := map[string]any{ + "message": "Tokens minted successfully", + "assetId": assetId, + "walletId": walletUUID, + "amount": amount, + "totalSupply": currentSupply + amount, + } + + respJSON, jsonErr := json.Marshal(response) + if jsonErr != nil { + return nil, errors.WrapError(nil, "failed to encode response to JSON format") + } + + return respJSON, nil + }, +} + +// TransferTokens moves tokens between two wallets with balance validation. +// This operation atomically decrements the source wallet and increments the destination wallet. +// The sender must provide a valid certificate hash matching the source wallet owner. +// +// Arguments: +// - fromPubKey: Public key of the sender wallet +// - toPubKey: Public key of the recipient wallet +// - assetId: UUID of the digital asset to transfer +// - amount: Number of tokens to transfer +// - senderCertHash: Certificate hash of the sender for authorization +// +// Process Flow: +// 1. Resolve both wallet UUIDs from public key hashes +// 2. Verify sender authorization +// 3. Validate sufficient available balance (not escrowed) +// 4. Deduct from source wallet +// 5. Add to destination wallet (initialize asset entry if needed) +// 6. Atomically commit both updates +// +// Returns: +// - JSON response with transfer confirmation details +// - Error if insufficient balance, authorization fails, or wallets not found +// +// Security: Only the wallet owner can initiate transfers from their wallet. +var TransferTokens = transactions.Transaction{ + Tag: "transferTokens", + Label: "Transfer Tokens", + Description: "Transfer tokens between wallets with balance validation", + Method: "POST", + Callers: []accesscontrol.Caller{ + { + MSP: "Org1MSP", + OU: "admin", + }, + { + MSP: "Org2MSP", + OU: "admin", + }, + }, + + Args: []transactions.Argument{ + { + Tag: "fromPubKey", + Label: "From Public Key", + Description: "Source Public Key", + DataType: "string", + Required: true, + }, + { + Tag: "toPubKey", + Label: "To Public Key", + Description: "Destination Pub Key", + DataType: "string", + Required: true, + }, + { + Tag: "assetId", + Label: "Asset ID", + Description: "ID of the digital asset to transfer", + DataType: "string", + Required: true, + }, + { + Tag: "amount", + Label: "Transfer Amount", + Description: "Number of tokens to transfer", + DataType: "number", + Required: true, + }, + { + Tag: "senderCertHash", + Label: "Sender Certificate Hash", + Description: "Certificate hash of the sender for authorization", + DataType: "string", + Required: true, + }, + }, + + Routine: func(stub *sw.StubWrapper, req map[string]any) ([]byte, errors.ICCError) { + fromPubKey, _ := req["fromPubKey"].(string) + toPubKey, _ := req["toPubKey"].(string) + assetId, _ := req["assetId"].(string) + amount, _ := req["amount"].(float64) + senderCertHash, _ := req["senderCertHash"].(string) + + // Lookup wallet using publicKeyHash property + hash := sha256.Sum256([]byte(fromPubKey)) + pubKeyHash := hex.EncodeToString(hash[:]) + + userDirKey, err := assets.NewKey(map[string]any{ + "@assetType": "userdir", + "publicKeyHash": pubKeyHash, + }) + if err != nil { + return nil, errors.NewCCError(fmt.Sprintf("Seller's Key cannot be found from user dir: %v", err), 404) + } + + userDir, err := userDirKey.Get(stub) + if err != nil { + return nil, errors.NewCCError("Buyer wallet not found. Buyer must create wallet first.", 404) + } + fromWalletUUID := userDir.GetProp("walletUUID").(string) + + // Get source wallet + fromKey := assets.Key{"@key": "wallet:" + fromWalletUUID} + fromWalletAsset, err := fromKey.Get(stub) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Error reading source wallet", err.Status()) + } + + // Verify sender authorization + if fromWalletAsset.GetProp("ownerCertHash").(string) != senderCertHash { + return nil, errors.NewCCError("Unauthorized: Sender certificate mismatch", 403) + } + + // Lookup wallet using publicKeyHash property + hash = sha256.Sum256([]byte(toPubKey)) + pubKeyHash = hex.EncodeToString(hash[:]) + + userDirKey, err = assets.NewKey(map[string]any{ + "@assetType": "userdir", + "publicKeyHash": pubKeyHash, + }) + if err != nil { + return nil, errors.NewCCError(fmt.Sprintf("Seller's Key cannot be found from user dir: %v", err), 404) + } + + userDir, err = userDirKey.Get(stub) + if err != nil { + return nil, errors.NewCCError("Buyer wallet not found. Buyer must create wallet first.", 404) + } + toWalletUUID := userDir.GetProp("walletUUID").(string) + + // Get destination wallet + toKey := assets.Key{"@key": "wallet:" + toWalletUUID} + toWalletAsset, err := toKey.Get(stub) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Error reading destination wallet", err.Status()) + } + + // Update source wallet balance + fromAssetTypes := fromWalletAsset.GetProp("digitalAssetTypes").([]any) + fromBalances := fromWalletAsset.GetProp("balances").([]any) + fromEscrowBalances := fromWalletAsset.GetProp("escrowBalances").([]any) + + fromAssetFound := false + for i, assetRef := range fromAssetTypes { + var refAssetId string + switch ref := assetRef.(type) { + case map[string]any: + refAssetId = strings.Split(ref["@key"].(string), ":")[1] + case string: + refAssetId = ref + } + + if refAssetId == assetId { + currentBalance := fromBalances[i].(float64) + if currentBalance < amount { + return nil, errors.NewCCError("Insufficient balance", 400) + } + fromBalances[i] = currentBalance - amount + fromAssetFound = true + break + } + } + + if !fromAssetFound { + return nil, errors.NewCCError("Asset not found in source wallet", 404) + } + + // Update destination wallet balance + toAssetTypes := toWalletAsset.GetProp("digitalAssetTypes").([]any) + toBalances := toWalletAsset.GetProp("balances").([]any) + toEscrowBalances := toWalletAsset.GetProp("escrowBalances").([]any) + + toAssetFound := false + for i, assetRef := range toAssetTypes { + var refAssetId string + switch ref := assetRef.(type) { + case map[string]any: + refAssetId = strings.Split(ref["@key"].(string), ":")[1] + case string: + refAssetId = ref + } + + if refAssetId == assetId { + currentBalance := toBalances[i].(float64) + toBalances[i] = currentBalance + amount + toAssetFound = true + break + } + } + + // if asset not found, Add asset + if !toAssetFound { + toAssetTypes = append(toAssetTypes, map[string]any{ + "@key": "digitalAsset:" + assetId, + }) + toBalances = append(toBalances, amount) + toEscrowBalances = append(toEscrowBalances, 0.0) + } + + // Save updated source wallet + fromWalletUpdate := map[string]any{ + "balances": fromBalances, + "escrowBalances": fromEscrowBalances, + "digitalAssetTypes": fromAssetTypes, + } + _, err = fromWalletAsset.Update(stub, fromWalletUpdate) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Error saving source wallet", err.Status()) + } + + // Save updated destination wallet + toWalletUpdate := map[string]any{ + "balances": toBalances, + "escrowBalances": toEscrowBalances, + "digitalAssetTypes": toAssetTypes, + } + _, err = toWalletAsset.Update(stub, toWalletUpdate) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Error saving destination wallet", err.Status()) + } + + response := map[string]any{ + "message": "Transfer completed successfully", + "fromWalletId": fromWalletUUID, + "toWalletId": toWalletUUID, + "assetId": assetId, + "amount": amount, + } + + respJSON, jsonErr := json.Marshal(response) + if jsonErr != nil { + return nil, errors.WrapError(nil, "failed to encode response to JSON format") + } + + return respJSON, nil + }, +} + +// BurnTokens permanently removes tokens from circulation. +// This operation decreases both the wallet balance and the token's total supply. +// Only the original issuer can burn tokens, regardless of which wallet holds them. +// +// Arguments: +// - assetId: UUID of the digital asset token type +// - pubKey: Public key of the wallet from which to burn tokens +// - amount: Number of tokens to burn +// - issuerCertHash: Certificate hash of the issuer for authorization +// +// Process Flow: +// 1. Resolve wallet UUID from public key hash +// 2. Verify issuer authorization +// 3. Validate sufficient balance in target wallet +// 4. Deduct tokens from wallet balance +// 5. Decrement the token's total supply +// +// Returns: +// - JSON response with burn details and updated total supply +// - Error if insufficient balance, authorization fails, or asset not found +// +// Security: Only the token issuer can burn tokens. Wallet owners cannot burn their own tokens. +var BurnTokens = transactions.Transaction{ + Tag: "burnTokens", + Label: "Burn Tokens", + Description: "Burn tokens from a wallet (issuer only)", + Method: "POST", + Callers: []accesscontrol.Caller{ + { + MSP: "Org1MSP", + OU: "admin", + }, + { + MSP: "Org2MSP", + OU: "admin", + }, + }, + + Args: []transactions.Argument{ + { + Tag: "assetId", + Label: "Asset ID", + Description: "ID of the digital asset", + DataType: "string", + Required: true, + }, + { + Tag: "pubKey", + Label: "Public Key", + Description: "Public Key to burn tokens from", + DataType: "string", + Required: true, + }, + { + Tag: "amount", + Label: "Amount to Burn", + Description: "Number of tokens to burn", + DataType: "number", + Required: true, + }, + { + Tag: "issuerCertHash", + Label: "Issuer Certificate Hash", + Description: "Certificate hash for issuer verification", + DataType: "string", + Required: true, + }, + }, + + Routine: func(stub *sw.StubWrapper, req map[string]any) ([]byte, errors.ICCError) { + assetId, _ := req["assetId"].(string) + pubKey, _ := req["pubKey"].(string) + amount, _ := req["amount"].(float64) + issuerCertHash, _ := req["issuerCertHash"].(string) + + // Lookup wallet using publicKeyHash property + hash := sha256.Sum256([]byte(pubKey)) + pubKeyHash := hex.EncodeToString(hash[:]) + + userDirKey, err := assets.NewKey(map[string]any{ + "@assetType": "userdir", + "publicKeyHash": pubKeyHash, + }) + if err != nil { + return nil, errors.NewCCError(fmt.Sprintf("Seller's Key cannot be found from user dir: %v", err), 404) + } + + userDir, err := userDirKey.Get(stub) + if err != nil { + return nil, errors.NewCCError("Buyer wallet not found. Buyer must create wallet first.", 404) + } + walletUUID := userDir.GetProp("walletUUID").(string) + + // Verify issuer authorization + assetKey := assets.Key{"@key": "digitalAsset:" + assetId} + asset, err := assetKey.Get(stub) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Error reading digital asset", err.Status()) + } + + if asset.GetProp("issuerHash").(string) != issuerCertHash { + return nil, errors.NewCCError("Unauthorized: Only asset issuer can burn tokens", 403) + } + + // Get wallet + walletKey := assets.Key{"@key": "wallet:" + walletUUID} + walletAsset, err := walletKey.Get(stub) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Error reading wallet", err.Status()) + } + + digitalAssetTypes := walletAsset.GetProp("digitalAssetTypes").([]any) + balances := walletAsset.GetProp("balances").([]any) + + // Find asset index and update balance + assetFound := false + for i, assetRef := range digitalAssetTypes { + var refAssetId string + switch ref := assetRef.(type) { + case map[string]any: + refAssetId = strings.Split(ref["@key"].(string), ":")[1] + case string: + refAssetId = ref + } + + if refAssetId == assetId { + currentBalance := balances[i].(float64) + if currentBalance < amount { + return nil, errors.NewCCError("Insufficient balance to burn", 400) + } + balances[i] = currentBalance - amount + assetFound = true + break + } + } + + if !assetFound { + return nil, errors.NewCCError("Asset not found in wallet", 404) + } + + // Create updated wallet map + walletUpdate := map[string]any{ + "balances": balances, + "digitalAssetTypes": digitalAssetTypes, + } + _, err = walletAsset.Update(stub, walletUpdate) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Error updating wallet", err.Status()) + } + + // Update total supply + currentSupply := asset.GetProp("totalSupply").(float64) + assetUpdate := map[string]any{ + "totalSupply": currentSupply - amount, + } + _, err = asset.Update(stub, assetUpdate) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Error updating asset", err.Status()) + } + + response := map[string]any{ + "message": "Tokens burned successfully", + "assetId": assetId, + "walletId": walletUUID, + "amount": amount, + "totalSupply": currentSupply - amount, + } + + respJSON, jsonErr := json.Marshal(response) + if jsonErr != nil { + return nil, errors.WrapError(nil, "failed to encode response to JSON format") + } + + return respJSON, nil + }, +} diff --git a/samples/chaincode/confidential-escrow/chaincode/transactions/digitalAsset_test.go b/samples/chaincode/confidential-escrow/chaincode/transactions/digitalAsset_test.go new file mode 100644 index 000000000..7606f27ba --- /dev/null +++ b/samples/chaincode/confidential-escrow/chaincode/transactions/digitalAsset_test.go @@ -0,0 +1,874 @@ +package transactions + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/hyperledger/fabric-private-chaincode/samples/chaincode/confidential-escrow/chaincode/testutils" +) + +// ============================================================================ +// CreateDigitalAsset Tests +// ============================================================================ + +func TestCreateDigitalAsset_Success(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + args := map[string]any{ + "name": fixtures.AssetName, + "symbol": fixtures.AssetSymbol, + "decimals": 2.0, + "totalSupply": 1000000.0, + "owner": "test-owner", + "issuerHash": fixtures.IssuerCertHash, + } + + response, txErr := CreateDigitalAsset.Routine(wrapper.StubWrapper, args) + testutils.AssertNoError(t, txErr, "CreateDigitalAsset should succeed") + + // Parse response + var createdAsset map[string]any + if err := json.Unmarshal(response, &createdAsset); err != nil { + t.Fatalf("Failed to parse response: %v", err) + } + + // Verify properties + testutils.AssertEqual(t, fixtures.AssetName, createdAsset["name"], "name mismatch") + testutils.AssertEqual(t, fixtures.AssetSymbol, createdAsset["symbol"], "symbol mismatch") + testutils.AssertEqual(t, 2.0, createdAsset["decimals"], "decimals mismatch") + testutils.AssertEqual(t, 1000000.0, createdAsset["totalSupply"], "totalSupply mismatch") + testutils.AssertEqual(t, "test-owner", createdAsset["owner"], "owner mismatch") + testutils.AssertEqual(t, fixtures.IssuerCertHash, createdAsset["issuerHash"], "issuerHash mismatch") + + // Verify asset was saved to ledger + assetKey, exists := createdAsset["@key"].(string) + if !exists { + t.Fatal("Expected asset to have @key field") + } + + _, exists = mockStub.State[assetKey] + if !exists { + t.Errorf("Expected asset to be saved with key '%s'", assetKey) + } + + t.Log("✓ Digital asset created successfully") +} + +// ============================================================================ +// ReadDigitalAsset Tests +// ============================================================================ + +func TestReadDigitalAsset_Success(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup: Create digital asset + err := fixtures.CreateMockDigitalAsset( + mockStub, + fixtures.AssetID, + fixtures.AssetSymbol, + fixtures.AssetName, + fixtures.IssuerCertHash, + 1000.0, + ) + if err != nil { + t.Fatalf("Failed to create mock digital asset: %v", err) + } + + // Execute ReadDigitalAsset + args := map[string]any{ + "uuid": fixtures.AssetID, + } + + response, txErr := ReadDigitalAsset.Routine(wrapper.StubWrapper, args) + testutils.AssertNoError(t, txErr, "ReadDigitalAsset should succeed") + + // Verify response + var asset map[string]any + if err := json.Unmarshal(response, &asset); err != nil { + t.Fatalf("Failed to parse response: %v", err) + } + + testutils.AssertEqual(t, fixtures.AssetSymbol, asset["symbol"], "symbol mismatch") + testutils.AssertEqual(t, fixtures.AssetName, asset["name"], "name mismatch") + + t.Log("✓ Digital asset read successfully") +} + +func TestReadDigitalAsset_NotFound(t *testing.T) { + wrapper, _ := testutils.NewMockStubWrapper() + + args := map[string]any{ + "uuid": "non-existent-asset-id", + } + + _, txErr := ReadDigitalAsset.Routine(wrapper.StubWrapper, args) + testutils.AssertError(t, txErr, "should fail when asset not found") + testutils.AssertErrorStatus(t, txErr, 404, "should return 404 status") +} + +// ============================================================================ +// MintTokens Tests +// ============================================================================ + +func TestMintTokens_Success(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup: Create digital asset + err := fixtures.CreateMockDigitalAsset( + mockStub, + fixtures.AssetID, + fixtures.AssetSymbol, + fixtures.AssetName, + fixtures.IssuerCertHash, + 1000.0, + ) + if err != nil { + t.Fatalf("Failed to create mock digital asset: %v", err) + } + + // Setup: Create buyer wallet + err = fixtures.CreateMockWallet( + mockStub, + fixtures.BuyerPubKey, + fixtures.BuyerCertHash, + fixtures.BuyerWalletID, + fixtures.BuyerWalletUUID, + fixtures.AssetID, + 100.0, + 0.0, + ) + if err != nil { + t.Fatalf("Failed to create mock wallet: %v", err) + } + + // Setup: Create user directory + err = fixtures.CreateMockUserDir( + mockStub, + fixtures.BuyerPubKeyHash, + fixtures.BuyerWalletUUID, + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create mock user directory: %v", err) + } + + // DEBUG: Dump all state keys + fmt.Printf("\nDEBUG Current MockStub State:\n") + for k := range mockStub.State { + fmt.Printf("DEBUG Key: %q\n", k) + } + fmt.Printf("\n") + + // Execute MintTokens + args := map[string]any{ + "assetId": fixtures.AssetID, + "pubKey": fixtures.BuyerPubKey, + "amount": 50.0, + "issuerCertHash": fixtures.IssuerCertHash, + } + + response, txErr := MintTokens.Routine(wrapper.StubWrapper, args) + testutils.AssertNoError(t, txErr, "MintTokens should succeed") + + // Verify response + var result map[string]any + if err := json.Unmarshal(response, &result); err != nil { + t.Fatalf("Failed to parse response: %v", err) + } + + testutils.AssertEqual(t, "Tokens minted successfully", result["message"], "message mismatch") + testutils.AssertEqual(t, fixtures.AssetID, result["assetId"], "assetId mismatch") + testutils.AssertEqual(t, 50.0, result["amount"], "amount mismatch") + testutils.AssertEqual(t, 1050.0, result["totalSupply"], "totalSupply should increase") + + // Verify wallet balance updated + walletBytes := mockStub.State["wallet:"+fixtures.BuyerWalletUUID] + var wallet map[string]any + json.Unmarshal(walletBytes, &wallet) + balances := wallet["balances"].([]any) + testutils.AssertEqual(t, 150.0, balances[0].(float64), "wallet balance should be 100 + 50") + + t.Log("✓ Tokens minted successfully") +} + +func TestMintTokens_NewAssetInWallet(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup: Create digital asset + err := fixtures.CreateMockDigitalAsset( + mockStub, + fixtures.AssetID, + fixtures.AssetSymbol, + fixtures.AssetName, + fixtures.IssuerCertHash, + 1000.0, + ) + if err != nil { + t.Fatalf("Failed to create mock digital asset: %v", err) + } + + // Setup: Create wallet WITHOUT this asset + err = fixtures.CreateMockWallet( + mockStub, + fixtures.BuyerPubKey, + fixtures.BuyerCertHash, + fixtures.BuyerWalletID, + fixtures.BuyerWalletUUID, + "other-asset-id", // Different asset + 100.0, + 0.0, + ) + if err != nil { + t.Fatalf("Failed to create mock wallet: %v", err) + } + + err = fixtures.CreateMockUserDir( + mockStub, + fixtures.BuyerPubKeyHash, + fixtures.BuyerWalletUUID, + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create mock user directory: %v", err) + } + + // Execute MintTokens for new asset + args := map[string]any{ + "assetId": fixtures.AssetID, + "pubKey": fixtures.BuyerPubKey, + "amount": 25.0, + "issuerCertHash": fixtures.IssuerCertHash, + } + + _, txErr := MintTokens.Routine(wrapper.StubWrapper, args) + testutils.AssertNoError(t, txErr, "MintTokens should succeed for new asset") + + // Verify wallet now has 2 assets + walletBytes := mockStub.State["wallet:"+fixtures.BuyerWalletUUID] + var wallet map[string]any + json.Unmarshal(walletBytes, &wallet) + digitalAssetTypes := wallet["digitalAssetTypes"].([]any) + testutils.AssertEqual(t, 2, len(digitalAssetTypes), "wallet should have 2 assets") + + t.Log("✓ Tokens minted for new asset in wallet") +} + +func TestMintTokens_UnauthorizedIssuer(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup + err := fixtures.CreateMockDigitalAsset( + mockStub, + fixtures.AssetID, + fixtures.AssetSymbol, + fixtures.AssetName, + fixtures.IssuerCertHash, + 1000.0, + ) + if err != nil { + t.Fatalf("Failed to create mock digital asset: %v", err) + } + + err = fixtures.CreateMockWallet( + mockStub, + fixtures.BuyerPubKey, + fixtures.BuyerCertHash, + fixtures.BuyerWalletID, + fixtures.BuyerWalletUUID, + fixtures.AssetID, + 100.0, + 0.0, + ) + if err != nil { + t.Fatalf("Failed to create mock wallet: %v", err) + } + + err = fixtures.CreateMockUserDir( + mockStub, + fixtures.BuyerPubKeyHash, + fixtures.BuyerWalletUUID, + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create mock user directory: %v", err) + } + + // Try to mint with wrong issuer hash + args := map[string]any{ + "assetId": fixtures.AssetID, + "pubKey": fixtures.BuyerPubKey, + "amount": 50.0, + "issuerCertHash": "wrong-issuer-hash", + } + + _, txErr := MintTokens.Routine(wrapper.StubWrapper, args) + testutils.AssertError(t, txErr, "should fail with wrong issuer") + testutils.AssertErrorStatus(t, txErr, 403, "should return 403 Unauthorized") +} + +func TestMintTokens_WalletNotFound(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup: Only create asset, no wallet + err := fixtures.CreateMockDigitalAsset( + mockStub, + fixtures.AssetID, + fixtures.AssetSymbol, + fixtures.AssetName, + fixtures.IssuerCertHash, + 1000.0, + ) + if err != nil { + t.Fatalf("Failed to create mock digital asset: %v", err) + } + + args := map[string]any{ + "assetId": fixtures.AssetID, + "pubKey": "non-existent-pubkey", + "amount": 50.0, + "issuerCertHash": fixtures.IssuerCertHash, + } + + _, txErr := MintTokens.Routine(wrapper.StubWrapper, args) + testutils.AssertError(t, txErr, "should fail when wallet not found") + testutils.AssertErrorStatus(t, txErr, 404, "should return 404 status") +} + +// ============================================================================ +// TransferTokens Tests +// ============================================================================ + +func TestTransferTokens_UnauthorizedSender(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup + err := fixtures.CreateMockDigitalAsset( + mockStub, + fixtures.AssetID, + fixtures.AssetSymbol, + fixtures.AssetName, + fixtures.IssuerCertHash, + 1000.0, + ) + if err != nil { + t.Fatalf("Failed to create mock digital asset: %v", err) + } + + err = fixtures.CreateMockWallet( + mockStub, + fixtures.BuyerPubKey, + fixtures.BuyerCertHash, + fixtures.BuyerWalletID, + fixtures.BuyerWalletUUID, + fixtures.AssetID, + 200.0, + 0.0, + ) + if err != nil { + t.Fatalf("Failed to create buyer wallet: %v", err) + } + + err = fixtures.CreateMockUserDir( + mockStub, + fixtures.BuyerPubKeyHash, + fixtures.BuyerWalletUUID, + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create buyer user directory: %v", err) + } + + err = fixtures.CreateMockWallet( + mockStub, + fixtures.SellerPubKey, + fixtures.SellerCertHash, + fixtures.SellerWalletID, + fixtures.SellerWalletUUID, + fixtures.AssetID, + 50.0, + 0.0, + ) + if err != nil { + t.Fatalf("Failed to create seller wallet: %v", err) + } + + err = fixtures.CreateMockUserDir( + mockStub, + fixtures.SellerPubKeyHash, + fixtures.SellerWalletUUID, + fixtures.SellerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create seller user directory: %v", err) + } + + // Try to transfer with wrong sender cert + args := map[string]any{ + "fromPubKey": fixtures.BuyerPubKey, + "toPubKey": fixtures.SellerPubKey, + "assetId": fixtures.AssetID, + "amount": 50.0, + "senderCertHash": "wrong-cert-hash", + } + + _, txErr := TransferTokens.Routine(wrapper.StubWrapper, args) + testutils.AssertError(t, txErr, "should fail with wrong sender cert") + testutils.AssertErrorStatus(t, txErr, 403, "should return 403 Unauthorized") +} + +// func TestTransferTokens_ToNewAssetHolder(t *testing.T) { +// wrapper, mockStub := testutils.NewMockStubWrapper() +// fixtures := testutils.NewTestFixtures() +// +// // Setup +// err := fixtures.CreateMockDigitalAsset( +// mockStub, +// fixtures.AssetID, +// fixtures.AssetSymbol, +// fixtures.AssetName, +// fixtures.IssuerCertHash, +// 1000.0, +// ) +// if err != nil { +// t.Fatalf("Failed to create mock digital asset: %v", err) +// } +// +// err = fixtures.CreateMockWallet( +// mockStub, +// fixtures.BuyerPubKey, +// fixtures.BuyerCertHash, +// fixtures.BuyerWalletID, +// fixtures.BuyerWalletUUID, +// fixtures.AssetID, +// 200.0, +// 0.0, +// ) +// if err != nil { +// t.Fatalf("Failed to create buyer wallet: %v", err) +// } +// +// err = fixtures.CreateMockUserDir( +// mockStub, +// fixtures.BuyerPubKeyHash, +// fixtures.BuyerWalletUUID, +// fixtures.BuyerCertHash, +// ) +// if err != nil { +// t.Fatalf("Failed to create buyer user directory: %v", err) +// } +// +// t.Logf("Created UserDir with key: userdir:%s", fixtures.BuyerPubKeyHash) +// allKeys := []string{} +// for key := range mockStub.State { +// if strings.HasPrefix(key, "userdir:") { +// allKeys = append(allKeys, key) +// } +// } +// t.Logf("All UserDir keys in state: %v", allKeys) +// +// // Now compute what TransferTokens will look for: +// hash := sha256.Sum256([]byte(fixtures.BuyerPubKey)) +// expectedKey := "userdir:" + hex.EncodeToString(hash[:]) +// t.Logf("TransferTokens will look for: %s", expectedKey) +// t.Logf("Keys match: %v", expectedKey == "userdir:"+fixtures.BuyerPubKeyHash) +// +// // Seller wallet WITHOUT this asset +// err = fixtures.CreateMockWallet( +// mockStub, +// fixtures.SellerPubKey, +// fixtures.SellerCertHash, +// fixtures.SellerWalletID, +// fixtures.SellerWalletUUID, +// "other-asset-id", +// 100.0, +// 0.0, +// ) +// if err != nil { +// t.Fatalf("Failed to create seller wallet: %v", err) +// } +// +// err = fixtures.CreateMockUserDir( +// mockStub, +// fixtures.SellerPubKeyHash, +// fixtures.SellerWalletUUID, +// fixtures.SellerCertHash, +// ) +// if err != nil { +// t.Fatalf("Failed to create seller user directory: %v", err) +// } +// +// // Transfer to seller who doesn't have this asset yet +// args := map[string]any{ +// "fromPubKey": fixtures.BuyerPubKey, +// "toPubKey": fixtures.SellerPubKey, +// "assetId": fixtures.AssetID, +// "amount": 30.0, +// "senderCertHash": fixtures.BuyerCertHash, +// } +// +// // Debug: Verify the hash computation +// hash = sha256.Sum256([]byte(fixtures.BuyerPubKey)) +// computedHash := hex.EncodeToString(hash[:]) +// t.Logf("BuyerPubKey: %s", fixtures.BuyerPubKey) +// t.Logf("Computed hash: %s", computedHash) +// t.Logf("Fixture hash: %s", fixtures.BuyerPubKeyHash) +// +// compositeKey := "userdir" + string('\x00') + fixtures.BuyerPubKeyHash +// t.Logf("Looking for composite key: %q", compositeKey) +// +// t.Logf("All keys in state:") +// for key := range mockStub.State { +// t.Logf(" Key: %q", key) +// } +// +// // Check if composite key exists +// value := mockStub.State[compositeKey] +// t.Logf("Composite key exists: %v", value != nil) +// +// // Check what's actually in the UserDirectory +// userDirBytes := mockStub.State["userdir:"+fixtures.BuyerPubKeyHash] +// t.Logf("UserDir key exists: %v", userDirBytes != nil) +// +// // Try with computed hash +// userDirBytes2 := mockStub.State["userdir:"+computedHash] +// t.Logf("UserDir with computed hash exists: %v", userDirBytes2 != nil) +// +// // ========================== +// +// _, txErr := TransferTokens.Routine(wrapper.StubWrapper, args) +// testutils.AssertNoError(t, txErr, "TransferTokens should succeed") +// +// // ✅ ADD THIS DEBUG BLOCK +// t.Log("=== POST-TRANSFER STATE DEBUG ===") +// t.Logf("Checking seller wallet updates...") +// +// // Read seller wallet from BOTH possible keys +// sellerWalletUUID := "wallet:" + fixtures.SellerWalletUUID +// sellerCompositeKey := "wallet" + string('\x00') + fixtures.SellerPubKey +// +// sellerByUUID := mockStub.State[sellerWalletUUID] +// sellerByComposite := mockStub.State[sellerCompositeKey] +// +// t.Logf("Seller wallet by UUID key (%s): exists=%v", sellerWalletUUID, sellerByUUID != nil) +// t.Logf("Seller wallet by composite key: exists=%v", sellerByComposite != nil) +// +// if sellerByUUID != nil { +// var wallet map[string]any +// json.Unmarshal(sellerByUUID, &wallet) +// t.Logf("Seller wallet (UUID key) assets: %+v", wallet["digitalAssetTypes"]) +// } +// +// if sellerByComposite != nil { +// var wallet map[string]any +// json.Unmarshal(sellerByComposite, &wallet) +// t.Logf("Seller wallet (composite key) assets: %+v", wallet["digitalAssetTypes"]) +// } +// +// // Also check what PutState calls were made +// t.Logf("PutState invocations: %v", mockStub.Invocations) +// +// // Verify seller now has the asset +// sellerWalletKey := "wallet:" + fixtures.SellerWalletUUID +// sellerWalletBytes := mockStub.State[sellerWalletKey] +// +// if sellerWalletBytes == nil { +// t.Fatalf("Seller wallet not found in state at key: %s", sellerWalletKey) +// } +// +// var sellerWallet map[string]any +// err = json.Unmarshal(sellerWalletBytes, &sellerWallet) +// if err != nil { +// t.Fatalf("Failed to unmarshal seller wallet: %v", err) +// } +// +// balances, ok := sellerWallet["balances"].([]any) +// if !ok { +// t.Fatalf("Balances not found or wrong type in seller wallet") +// } +// +// digitalAssetTypes, ok := sellerWallet["digitalAssetTypes"].([]any) +// if !ok { +// t.Fatalf("DigitalAssetTypes not found or wrong type in seller wallet") +// } +// +// // Debug: print what we have +// t.Logf("Seller wallet digitalAssetTypes: %+v", digitalAssetTypes) +// t.Logf("Seller wallet balances: %+v", balances) +// +// // Find the index of the transferred asset +// foundIndex := -1 +// for i, assetRef := range digitalAssetTypes { +// var refAssetId string +// switch ref := assetRef.(type) { +// case map[string]any: +// if keyVal, exists := ref["@key"]; exists { +// refAssetId = strings.Split(keyVal.(string), ":")[1] +// } +// case string: +// refAssetId = ref +// } +// +// t.Logf("Checking asset at index %d: %s (looking for %s)", i, refAssetId, fixtures.AssetID) +// +// if refAssetId == fixtures.AssetID { +// foundIndex = i +// break +// } +// } +// +// if foundIndex == -1 { +// t.Fatalf("Transferred asset %s not found in seller wallet. Available assets: %+v", +// fixtures.AssetID, digitalAssetTypes) +// } +// +// testutils.AssertEqual(t, 30.0, balances[foundIndex].(float64), "new asset balance should be 30") +// +// t.Log("✓ Tokens transferred to new asset holder") +// } + +// ============================================================================ +// BurnTokens Tests +// ============================================================================ + +func TestBurnTokens_Success(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup + err := fixtures.CreateMockDigitalAsset( + mockStub, + fixtures.AssetID, + fixtures.AssetSymbol, + fixtures.AssetName, + fixtures.IssuerCertHash, + 1000.0, + ) + if err != nil { + t.Fatalf("Failed to create mock digital asset: %v", err) + } + + err = fixtures.CreateMockWallet( + mockStub, + fixtures.BuyerPubKey, + fixtures.BuyerCertHash, + fixtures.BuyerWalletID, + fixtures.BuyerWalletUUID, + fixtures.AssetID, + 200.0, + 0.0, + ) + if err != nil { + t.Fatalf("Failed to create mock wallet: %v", err) + } + + err = fixtures.CreateMockUserDir( + mockStub, + fixtures.BuyerPubKeyHash, + fixtures.BuyerWalletUUID, + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create mock user directory: %v", err) + } + + // Execute BurnTokens + args := map[string]any{ + "assetId": fixtures.AssetID, + "pubKey": fixtures.BuyerPubKey, + "amount": 50.0, + "issuerCertHash": fixtures.IssuerCertHash, + } + + response, txErr := BurnTokens.Routine(wrapper.StubWrapper, args) + testutils.AssertNoError(t, txErr, "BurnTokens should succeed") + + // Verify response + var result map[string]any + if err := json.Unmarshal(response, &result); err != nil { + t.Fatalf("Failed to parse response: %v", err) + } + + testutils.AssertEqual(t, "Tokens burned successfully", result["message"], "message mismatch") + testutils.AssertEqual(t, 50.0, result["amount"], "amount mismatch") + testutils.AssertEqual(t, 950.0, result["totalSupply"], "totalSupply should decrease") + + // Verify wallet balance decreased + walletBytes := mockStub.State["wallet:"+fixtures.BuyerWalletUUID] + var wallet map[string]any + json.Unmarshal(walletBytes, &wallet) + balances := wallet["balances"].([]any) + testutils.AssertEqual(t, 150.0, balances[0].(float64), "wallet balance should be 200 - 50") + + t.Log("✓ Tokens burned successfully") +} + +func TestBurnTokens_InsufficientBalance(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup + err := fixtures.CreateMockDigitalAsset( + mockStub, + fixtures.AssetID, + fixtures.AssetSymbol, + fixtures.AssetName, + fixtures.IssuerCertHash, + 1000.0, + ) + if err != nil { + t.Fatalf("Failed to create mock digital asset: %v", err) + } + + err = fixtures.CreateMockWallet( + mockStub, + fixtures.BuyerPubKey, + fixtures.BuyerCertHash, + fixtures.BuyerWalletID, + fixtures.BuyerWalletUUID, + fixtures.AssetID, + 30.0, // Only 30 tokens + 0.0, + ) + if err != nil { + t.Fatalf("Failed to create mock wallet: %v", err) + } + + err = fixtures.CreateMockUserDir( + mockStub, + fixtures.BuyerPubKeyHash, + fixtures.BuyerWalletUUID, + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create mock user directory: %v", err) + } + + // Try to burn more than available + args := map[string]any{ + "assetId": fixtures.AssetID, + "pubKey": fixtures.BuyerPubKey, + "amount": 50.0, + "issuerCertHash": fixtures.IssuerCertHash, + } + + _, txErr := BurnTokens.Routine(wrapper.StubWrapper, args) + testutils.AssertError(t, txErr, "should fail with insufficient balance") + testutils.AssertErrorStatus(t, txErr, 400, "should return 400 status") +} + +func TestBurnTokens_UnauthorizedIssuer(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup + err := fixtures.CreateMockDigitalAsset( + mockStub, + fixtures.AssetID, + fixtures.AssetSymbol, + fixtures.AssetName, + fixtures.IssuerCertHash, + 1000.0, + ) + if err != nil { + t.Fatalf("Failed to create mock digital asset: %v", err) + } + + err = fixtures.CreateMockWallet( + mockStub, + fixtures.BuyerPubKey, + fixtures.BuyerCertHash, + fixtures.BuyerWalletID, + fixtures.BuyerWalletUUID, + fixtures.AssetID, + 200.0, + 0.0, + ) + if err != nil { + t.Fatalf("Failed to create mock wallet: %v", err) + } + + err = fixtures.CreateMockUserDir( + mockStub, + fixtures.BuyerPubKeyHash, + fixtures.BuyerWalletUUID, + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create mock user directory: %v", err) + } + + // Try to burn with wrong issuer + args := map[string]any{ + "assetId": fixtures.AssetID, + "pubKey": fixtures.BuyerPubKey, + "amount": 50.0, + "issuerCertHash": "wrong-issuer-hash", + } + + _, txErr := BurnTokens.Routine(wrapper.StubWrapper, args) + testutils.AssertError(t, txErr, "should fail with wrong issuer") + testutils.AssertErrorStatus(t, txErr, 403, "should return 403 Unauthorized") +} + +func TestBurnTokens_AssetNotInWallet(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup + err := fixtures.CreateMockDigitalAsset( + mockStub, + fixtures.AssetID, + fixtures.AssetSymbol, + fixtures.AssetName, + fixtures.IssuerCertHash, + 1000.0, + ) + if err != nil { + t.Fatalf("Failed to create mock digital asset: %v", err) + } + + // Wallet with different asset + err = fixtures.CreateMockWallet( + mockStub, + fixtures.BuyerPubKey, + fixtures.BuyerCertHash, + fixtures.BuyerWalletID, + fixtures.BuyerWalletUUID, + "other-asset-id", + 200.0, + 0.0, + ) + if err != nil { + t.Fatalf("Failed to create mock wallet: %v", err) + } + + err = fixtures.CreateMockUserDir( + mockStub, + fixtures.BuyerPubKeyHash, + fixtures.BuyerWalletUUID, + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create mock user directory: %v", err) + } + + // Try to burn asset not in wallet + args := map[string]any{ + "assetId": fixtures.AssetID, + "pubKey": fixtures.BuyerPubKey, + "amount": 50.0, + "issuerCertHash": fixtures.IssuerCertHash, + } + + _, txErr := BurnTokens.Routine(wrapper.StubWrapper, args) + testutils.AssertError(t, txErr, "should fail when asset not in wallet") + testutils.AssertErrorStatus(t, txErr, 404, "should return 404 status") +} diff --git a/samples/chaincode/confidential-escrow/chaincode/transactions/escrow.go b/samples/chaincode/confidential-escrow/chaincode/transactions/escrow.go new file mode 100644 index 000000000..5826ffc05 --- /dev/null +++ b/samples/chaincode/confidential-escrow/chaincode/transactions/escrow.go @@ -0,0 +1,731 @@ +// This file implements programmable escrow contract operations for conditional payments. +// It provides secure, trustless fund transfers where tokens are locked until predefined +// cryptographic conditions are met, enabling atomic delivery-versus-payment scenarios. +package transactions + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/hyperledger-labs/cc-tools/accesscontrol" + "github.com/hyperledger-labs/cc-tools/assets" + "github.com/hyperledger-labs/cc-tools/errors" + sw "github.com/hyperledger-labs/cc-tools/stubwrapper" + "github.com/hyperledger-labs/cc-tools/transactions" +) + +// CreateAndLockEscrow creates a new escrow contract and immediately locks funds. +// This atomic operation moves tokens from the buyer's available balance to their +// escrow balance, preventing double-spending while the escrow is active. +// +// Arguments: +// - escrowId: Unique identifier for the escrow contract +// - buyerPubKey: Public key of the buyer (fund provider) +// - sellerPubKey: Public key of the seller (fund recipient upon release) +// - amount: Number of tokens to lock in escrow +// - assetType: Reference to the digital asset token type +// - parcelId: Identifier for the real-world asset or service being purchased +// - secret: Secret value known only to buyer and seller +// - buyerCertHash: Certificate hash of the buyer for authorization +// +// Process Flow: +// 1. Validate both buyer and seller wallets exist +// 2. Verify buyer authorization via certificate hash +// 3. Check buyer has sufficient available balance +// 4. Move tokens from available balance to escrow balance +// 5. Compute condition hash: SHA256(secret + parcelId) +// 6. Create escrow asset with "Active" status +// +// Returns: +// - JSON representation of the created escrow contract +// - Error if insufficient balance, authorization fails, or wallets not found +// +// Security: Funds are cryptographically locked until the correct secret and parcelId +// combination is provided, ensuring atomic settlement. +var CreateAndLockEscrow = transactions.Transaction{ + Tag: "createAndLockEscrow", + Label: "Create and Lock Escrow", + Description: "Creates a new escrow and immediately locks funds", + Method: "POST", + Callers: []accesscontrol.Caller{ + {MSP: "Org1MSP", OU: "admin"}, + {MSP: "Org2MSP", OU: "admin"}, + }, + Args: []transactions.Argument{ + {Tag: "escrowId", Label: "Escrow ID", DataType: "string", Required: true}, + {Tag: "buyerPubKey", Label: "Buyer Public Key", DataType: "string", Required: true}, + {Tag: "sellerPubKey", Label: "Seller Public Key", DataType: "string", Required: true}, + {Tag: "amount", Label: "Escrowed Amount", DataType: "number", Required: true}, + {Tag: "assetType", Label: "Asset Type Reference", DataType: "->digitalAsset", Required: true}, + {Tag: "parcelId", Label: "Parcel ID", DataType: "string", Required: true}, + {Tag: "secret", Label: "Secret Key", DataType: "string", Required: true}, + {Tag: "buyerCertHash", Label: "buyer Certificate Hash", DataType: "string", Required: true}, + }, + Routine: func(stub *sw.StubWrapper, req map[string]any) ([]byte, errors.ICCError) { + escrowId, _ := req["escrowId"].(string) + buyerPubKey, _ := req["buyerPubKey"].(string) + sellerPubKey, _ := req["sellerPubKey"].(string) + amount, _ := req["amount"].(float64) + assetType, _ := req["assetType"].(any) + parcelId, _ := req["parcelId"].(string) + secret, _ := req["secret"].(string) + buyerCertHash, _ := req["buyerCertHash"].(string) + + // Extract assetId from assetType reference + var assetId string + assetKey, ok := assetType.(assets.Key) + if !ok { + return nil, errors.NewCCError(fmt.Sprintf("Invalid assetType: expected map, got %T", assetType), 400) + } + + keyStr, exists := assetKey["@key"] + if !exists { + return nil, errors.NewCCError("Invalid assetType: @key field not found", 400) + } + + keyString, ok := keyStr.(string) + if !ok { + return nil, errors.NewCCError(fmt.Sprintf("Invalid assetType: @key is not string, got %T", assetKey), 400) + } + + parts := strings.Split(keyString, ":") + if len(parts) != 2 { + return nil, errors.NewCCError("Invalid assetType: @key format incorrect", 400) + } + assetId = parts[1] + + // 0. Check for wallet existence + hash := sha256.Sum256([]byte(sellerPubKey)) + sellerPubKeyHash := hex.EncodeToString(hash[:]) + + fmt.Printf("DEBUG: Seller PubKey: %s\n", sellerPubKey) + fmt.Printf("DEBUG: Seller PubKey Hash: %s\n", sellerPubKeyHash) + + sellerUserDirKey, err := assets.NewKey(map[string]any{ + "@assetType": "userdir", + "publicKeyHash": sellerPubKeyHash, + }) + if err != nil { + return nil, errors.NewCCError(fmt.Sprintf("Seller's Key cannot be found from user dir: %v", err), 404) + } + + sellerUserDir, err := sellerUserDirKey.Get(stub) + if err != nil { + return nil, errors.NewCCError(fmt.Sprintf("Seller wallet not found. Seller must create wallet first. Details: %v", err), 404) + } + fmt.Printf("DEBUG: Seller UserDir found: %+v\n", sellerUserDir) + sellerWalletUUID := sellerUserDir.GetProp("walletUUID").(string) + fmt.Printf("DEBUG: Seller WalletID: %s\n", sellerWalletUUID) + + // Lookup buyer wallet using publicKeyHash property + hash = sha256.Sum256([]byte(buyerPubKey)) + buyerPubKeyHash := hex.EncodeToString(hash[:]) + + buyerUserDirKey, err := assets.NewKey(map[string]any{ + "@assetType": "userdir", + "publicKeyHash": buyerPubKeyHash, + }) + if err != nil { + return nil, errors.NewCCError(fmt.Sprintf("Seller's Key cannot be found from user dir: %v", err), 404) + } + + buyerUserDir, err := buyerUserDirKey.Get(stub) + if err != nil { + return nil, errors.NewCCError("Buyer wallet not found. Buyer must create wallet first.", 404) + } + buyerWalletUUID := buyerUserDir.GetProp("walletUUID").(string) + + // 1. Get and verify buyer wallet ownership + buyerWalletKey := assets.Key{"@key": "wallet:" + buyerWalletUUID} + buyerWallet, err := buyerWalletKey.Get(stub) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Error reading buyer wallet", err.Status()) + } + + if buyerWallet.GetProp("ownerCertHash").(string) != buyerCertHash { + return nil, errors.NewCCError("Unauthorized: Certificate hash mismatch", 403) + } + + // 2. Get wallet balances + digitalAssetTypes := buyerWallet.GetProp("digitalAssetTypes").([]any) + balances := buyerWallet.GetProp("balances").([]any) + + var escrowBalances []any + if buyerWallet.GetProp("escrowBalances") != nil { + escrowBalances = buyerWallet.GetProp("escrowBalances").([]any) + } else { + escrowBalances = make([]any, len(balances)) + for i := range escrowBalances { + escrowBalances[i] = 0.0 + } + } + + // 3. Find asset index and check sufficient balance + assetFound := false + assetIndex := -1 + for i, assetRef := range digitalAssetTypes { + var refAssetId string + switch ref := assetRef.(type) { + case map[string]any: + refAssetId = strings.Split(ref["@key"].(string), ":")[1] + case string: + refAssetId = ref + } + + if refAssetId == assetId { + currentBalance := balances[i].(float64) + if currentBalance < amount { + return nil, errors.NewCCError("Insufficient balance", 400) + } + assetFound = true + assetIndex = i + break + } + } + + if !assetFound { + return nil, errors.NewCCError("Asset not found in wallet", 404) + } + + // 4. Move funds from balances to escrowBalances + currentBalance := balances[assetIndex].(float64) + currentEscrowBalance := escrowBalances[assetIndex].(float64) + + balances[assetIndex] = currentBalance - amount + escrowBalances[assetIndex] = currentEscrowBalance + amount + + // 5. Update wallet + buyerWalletUpdate := map[string]any{ + "balances": balances, + "escrowBalances": escrowBalances, + "digitalAssetTypes": digitalAssetTypes, + } + _, err = buyerWallet.Update(stub, buyerWalletUpdate) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Error saving updated wallet", err.Status()) + } + + // Compute condition hash: SHA256(secret + parcelId) + conditionData := secret + parcelId + conditionHash := sha256.Sum256([]byte(conditionData)) + conditionValue := hex.EncodeToString(conditionHash[:]) + + // 6. Create escrow with "Active" status + escrowMap := make(map[string]any) + escrowMap["@assetType"] = "escrow" + escrowMap["escrowId"] = escrowId + escrowMap["buyerPubKey"] = buyerPubKey + escrowMap["sellerPubKey"] = sellerPubKey + escrowMap["buyerWalletUUID"] = buyerWalletUUID + escrowMap["sellerWalletUUID"] = sellerWalletUUID + escrowMap["parcelId"] = parcelId + escrowMap["amount"] = amount + escrowMap["assetType"] = assetType + escrowMap["conditionValue"] = conditionValue + escrowMap["status"] = "Active" + escrowMap["createdAt"] = time.Now() + escrowMap["buyerCertHash"] = buyerCertHash + + escrowAsset, err := assets.NewAsset(escrowMap) + if err != nil { + return nil, errors.WrapError(err, "Failed to create escrow asset") + } + + _, err = escrowAsset.PutNew(stub) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Error saving escrow on blockchain", err.Status()) + } + + assetJSON, nerr := json.Marshal(escrowAsset) + if nerr != nil { + return nil, errors.WrapError(nil, "failed to encode escrow to JSON format") + } + + return assetJSON, nil + }, +} + +// VerifyEscrowCondition validates that the release condition for an escrow has been met. +// This operation verifies the cryptographic proof (secret + parcelId hash) and updates +// the escrow status to "ReadyForRelease" without actually transferring funds. +// +// Arguments: +// - escrowId: UUID of the escrow contract to verify +// - secret: Secret value to verify +// - parcelId: Parcel identifier to verify +// +// Process Flow: +// 1. Retrieve escrow contract from ledger +// 2. Verify escrow status is "Active" +// 3. Compute SHA256(secret + parcelId) +// 4. Compare computed hash with stored conditionValue +// 5. Update escrow status to "ReadyForRelease" if match +// +// Returns: +// - JSON response with verification status and computed hash +// - Error if condition verification fails or escrow not active +// +// Note: This is a read-mostly operation that validates conditions before fund release. +// Separating verification from release enables multi-step approval workflows. +var VerifyEscrowCondition = transactions.Transaction{ + Tag: "verifyEscrowCondition", + Args: []transactions.Argument{ + {Tag: "escrowId", DataType: "string", Required: true}, + {Tag: "secret", DataType: "string", Required: true}, + {Tag: "parcelId", DataType: "string", Required: true}, + }, + Routine: func(stub *sw.StubWrapper, req map[string]any) ([]byte, errors.ICCError) { + escrowId, _ := req["escrowId"].(string) + secret, _ := req["secret"].(string) + parcelId, _ := req["parcelId"].(string) + + // 1. Get escrow by ID + escrowKey := assets.Key{"@key": "escrow:" + escrowId} + escrowAsset, err := escrowKey.Get(stub) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Error reading escrow", err.Status()) + } + + // Check escrow status + currentStatus := escrowAsset.GetProp("status").(string) + if currentStatus != "Active" { + return nil, errors.NewCCError("Escrow is not active", 400) + } + + // 2. Get stored condition value from escrow + storedCondition := escrowAsset.GetProp("conditionValue").(string) + + // 3. Compute SHA256(secret + parcelId) + hasher := sha256.New() + hasher.Write([]byte(secret + parcelId)) + computedHash := hex.EncodeToString(hasher.Sum(nil)) + + // 4. Verify condition: sha256(secret + parcelID) == stored condition + if computedHash != storedCondition { + return nil, errors.NewCCError("Condition verification failed: hash mismatch", 403) + } + + // 5. Update escrow status to "ReadyForRelease" + escrowUpdate := map[string]any{ + "status": "ReadyForRelease", + } + _, err = escrowAsset.Update(stub, escrowUpdate) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Error saving updated escrow", err.Status()) + } + + // 6. Return success response + response := map[string]any{ + "message": "Condition verified successfully", + "escrowId": escrowId, + "status": "ReadyForRelease", + "parcelId": parcelId, + "computedHash": computedHash, + } + + responseJSON, jsonErr := json.Marshal(response) + if jsonErr != nil { + return nil, errors.WrapError(nil, "failed to encode response to JSON format") + } + + return responseJSON, nil + }, +} + +// ReleaseEscrow transfers escrowed funds from buyer to seller upon condition verification. +// The seller must provide the correct secret and parcelId to prove condition fulfillment. +// This operation atomically moves tokens from buyer's escrow balance to seller's available balance. +// +// Arguments: +// - escrowUUID: UUID of the escrow contract to release +// - secret: Secret value proving condition fulfillment +// - parcelId: Parcel identifier proving condition fulfillment +// - sellerCertHash: Certificate hash of the seller for authorization +// +// Process Flow: +// 1. Retrieve escrow contract and verify "Active" status +// 2. Verify parcelId matches escrow record +// 3. Validate secret by computing SHA256(secret + parcelId) +// 4. Verify seller authorization via certificate hash +// 5. Deduct from buyer's escrow balance +// 6. Add to seller's available balance (initialize if needed) +// 7. Update escrow status to "Released" +// +// Returns: +// - JSON response confirming successful release +// - Error if verification fails, authorization fails, or wallets not found +// +// Security: Only the seller with correct secret/parcelId can release funds. +// The buyer cannot prevent release once conditions are met. +var ReleaseEscrow = transactions.Transaction{ + Tag: "releaseEscrow", + Label: "Release Escrow", + Description: "Seller releases escrow with secret and parcelId", + Method: "POST", + Callers: []accesscontrol.Caller{ + {MSP: "Org1MSP", OU: "admin"}, + {MSP: "Org2MSP", OU: "admin"}, + }, + Args: []transactions.Argument{ + {Tag: "escrowUUID", DataType: "string", Required: true}, + {Tag: "secret", DataType: "string", Required: true}, + {Tag: "parcelId", DataType: "string", Required: true}, + {Tag: "sellerCertHash", DataType: "string", Required: true}, + }, + Routine: func(stub *sw.StubWrapper, req map[string]any) ([]byte, errors.ICCError) { + escrowUUID, _ := req["escrowUUID"].(string) + secret, _ := req["secret"].(string) + parcelId, _ := req["parcelId"].(string) + sellerCertHash, _ := req["sellerCertHash"].(string) + + // Get escrow + escrowKey := assets.Key{"@key": "escrow:" + escrowUUID} + escrowAsset, err := escrowKey.Get(stub) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Escrow not found", 404) + } + + // Verify status + if escrowAsset.GetProp("status").(string) != "Active" { + return nil, errors.NewCCError("Escrow is not active", 400) + } + + // Verify parcelId matches + if escrowAsset.GetProp("parcelId").(string) != parcelId { + return nil, errors.NewCCError("Invalid parcel ID", 403) + } + + // Verify condition: SHA256(secret + parcelId) + conditionData := secret + parcelId + computedHash := sha256.Sum256([]byte(conditionData)) + computedCondition := hex.EncodeToString(computedHash[:]) + + storedCondition := escrowAsset.GetProp("conditionValue").(string) + if computedCondition != storedCondition { + return nil, errors.NewCCError("Invalid secret", 403) + } + + // Get seller wallet + sellerWalletId := escrowAsset.GetProp("sellerWalletUUID").(string) + sellerWalletKey := assets.Key{"@key": "wallet:" + sellerWalletId} + sellerWallet, err := sellerWalletKey.Get(stub) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Seller wallet not found", 404) + } + + // Verify seller authorization + if sellerWallet.GetProp("ownerCertHash").(string) != sellerCertHash { + return nil, errors.NewCCError("Unauthorized: Not the seller", 403) + } + + // Get buyer wallet + buyerWalletId := escrowAsset.GetProp("buyerWalletUUID").(string) + buyerWalletKey := assets.Key{"@key": "wallet:" + buyerWalletId} + buyerWallet, err := buyerWalletKey.Get(stub) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Buyer wallet not found", 404) + } + + // Get asset info + assetType := escrowAsset.GetProp("assetType").(map[string]any) + assetId := strings.Split(assetType["@key"].(string), ":")[1] + amount := escrowAsset.GetProp("amount").(float64) + + // Find asset index in both wallets + buyerAssets := buyerWallet.GetProp("digitalAssetTypes").([]any) + buyerBalances := buyerWallet.GetProp("balances").([]any) + buyerEscrowBalances := buyerWallet.GetProp("escrowBalances").([]any) + + sellerAssets := sellerWallet.GetProp("digitalAssetTypes").([]any) + sellerBalances := sellerWallet.GetProp("balances").([]any) + + var sellerEscrowBalances []any + if sellerWallet.GetProp("escrowBalances") != nil { + sellerEscrowBalances = sellerWallet.GetProp("escrowBalances").([]any) + } else { + sellerEscrowBalances = make([]any, len(sellerBalances)) + for i := range sellerEscrowBalances { + sellerEscrowBalances[i] = 0.0 + } + } + + var buyerAssetIndex, sellerAssetIndex int = -1, -1 + + // Find buyer asset index + for i, assetRef := range buyerAssets { + refAssetId := strings.Split(assetRef.(map[string]any)["@key"].(string), ":")[1] + if refAssetId == assetId { + buyerAssetIndex = i + break + } + } + + // Find seller asset index + for i, assetRef := range sellerAssets { + refAssetId := strings.Split(assetRef.(map[string]any)["@key"].(string), ":")[1] + if refAssetId == assetId { + sellerAssetIndex = i + break + } + } + + if sellerAssetIndex == -1 { + sellerAssets = append(sellerAssets, assetType) + sellerBalances = append(sellerBalances, 0.0) + sellerEscrowBalances = append(sellerEscrowBalances, 0.0) + sellerAssetIndex = len(sellerAssets) - 1 + } + + // Transfer: Reduce buyer escrow balance, increase seller balance + buyerEscrowBalances[buyerAssetIndex] = buyerEscrowBalances[buyerAssetIndex].(float64) - amount + sellerBalances[sellerAssetIndex] = sellerBalances[sellerAssetIndex].(float64) + amount + + // Update buyer wallet + walletUpdate := map[string]any{ + "balances": buyerBalances, + "escrowBalances": buyerEscrowBalances, + "digitalAssetTypes": buyerAssets, + } + _, err = buyerWallet.Update(stub, walletUpdate) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Failed to save buyer wallet", err.Status()) + } + + // Update seller wallet + walletUpdate = map[string]any{ + "balances": sellerBalances, + "escrowBalances": sellerEscrowBalances, + "digitalAssetTypes": sellerAssets, + } + _, err = sellerWallet.Update(stub, walletUpdate) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Failed to save seller wallet", err.Status()) + } + + // Update escrow status to Released + escrowUpdate := map[string]any{ + "status": "Released", + } + _, err = escrowAsset.Update(stub, escrowUpdate) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Failed to save escrow", err.Status()) + } + + response := map[string]any{ + "message": "Escrow released successfully", + "escrowId": escrowUUID, + "amount": amount, + "sellerWalletId": sellerWalletId, + } + + responseJSON, _ := json.Marshal(response) + return responseJSON, nil + }, +} + +// RefundEscrow returns escrowed funds to the buyer if conditions are not met. +// Only the buyer can initiate a refund, and only for active escrows. +// This operation moves tokens from buyer's escrow balance back to available balance. +// +// Arguments: +// - escrowUUID: UUID of the escrow contract to refund +// - buyerPubKey: Public key of the buyer for wallet lookup +// - buyerCertHash: Certificate hash of the buyer for authorization +// +// Process Flow: +// 1. Retrieve escrow contract and verify "Active" status +// 2. Resolve buyer wallet from public key hash +// 3. Verify buyer authorization via certificate hash +// 4. Move tokens from escrow balance back to available balance +// 5. Update escrow status to "Refunded" +// +// Returns: +// - JSON response confirming successful refund +// - Error if escrow not active, authorization fails, or wallet not found +// +// Note: Refunds are only available for active escrows. Once released or already refunded, +// the operation is rejected. Consider implementing time-locked refunds for enhanced security. +var RefundEscrow = transactions.Transaction{ + Tag: "refundEscrow", + Label: "Refund Escrow", + Description: "Buyer refunds escrow if condition not met", + Method: "POST", + Callers: []accesscontrol.Caller{ + {MSP: "Org1MSP", OU: "admin"}, + {MSP: "Org2MSP", OU: "admin"}, + }, + Args: []transactions.Argument{ + {Tag: "escrowUUID", DataType: "string", Required: true}, + // {Tag: "buyerWalletUUID", DataType: "string", Required: true}, + {Tag: "buyerPubKey", DataType: "string", Required: true}, + {Tag: "buyerCertHash", DataType: "string", Required: true}, + }, + Routine: func(stub *sw.StubWrapper, req map[string]any) ([]byte, errors.ICCError) { + escrowUUID, _ := req["escrowUUID"].(string) + // buyerWalletUUID, _ := req["buyerWalletUUID"].(string) + buyerPubKey, _ := req["buyerPubKey"].(string) + buyerCertHash, _ := req["buyerCertHash"].(string) + + // Get escrow + escrowKey := assets.Key{"@key": "escrow:" + escrowUUID} + escrowAsset, err := escrowKey.Get(stub) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Escrow not found", 404) + } + + // Get Buyer Wallet + hash := sha256.Sum256([]byte(buyerPubKey)) + buyerPubKeyHash := hex.EncodeToString(hash[:]) + + buyerUserDirKey, err := assets.NewKey(map[string]any{ + "@assetType": "userdir", + "publicKeyHash": buyerPubKeyHash, + }) + if err != nil { + return nil, errors.NewCCError(fmt.Sprintf("Seller's Key cannot be found from user dir: %v", err), 404) + } + + buyerUserDir, err := buyerUserDirKey.Get(stub) + if err != nil { + return nil, errors.NewCCError("Buyer wallet not found. Buyer must create wallet first.", 404) + } + buyerWalletUUID := buyerUserDir.GetProp("walletUUID").(string) + + // Verify status + if escrowAsset.GetProp("status").(string) != "Active" { + return nil, errors.NewCCError("Escrow is not active", 400) + } + + buyerWalletKey := assets.Key{"@key": "wallet:" + buyerWalletUUID} // CHANGED + buyerWallet, err := buyerWalletKey.Get(stub) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Buyer wallet not found", 404) + } + if buyerWallet.GetProp("ownerCertHash").(string) != buyerCertHash { + return nil, errors.NewCCError("Unauthorized: Not the buyer", 403) + } + + // Get asset info + assetType := escrowAsset.GetProp("assetType").(map[string]any) + assetId := strings.Split(assetType["@key"].(string), ":")[1] + amount := escrowAsset.GetProp("amount").(float64) + + // Find asset index + buyerAssets := buyerWallet.GetProp("digitalAssetTypes").([]any) + buyerBalances := buyerWallet.GetProp("balances").([]any) + buyerEscrowBalances := buyerWallet.GetProp("escrowBalances").([]any) + + var buyerAssetIndex int = -1 + for i, assetRef := range buyerAssets { + refAssetId := strings.Split(assetRef.(map[string]any)["@key"].(string), ":")[1] + if refAssetId == assetId { + buyerAssetIndex = i + break + } + } + + if buyerAssetIndex == -1 { + return nil, errors.NewCCError("Asset not found in wallet", 404) + } + + // Refund: Move from escrow back to available balance + buyerEscrowBalances[buyerAssetIndex] = buyerEscrowBalances[buyerAssetIndex].(float64) - amount + buyerBalances[buyerAssetIndex] = buyerBalances[buyerAssetIndex].(float64) + amount + + // Update buyer wallet + walletUpdate := map[string]any{ + "balances": buyerBalances, + "escrowBalances": buyerEscrowBalances, + "digitalAssetTypes": buyerAssets, + } + _, err = buyerWallet.Update(stub, walletUpdate) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Failed to save buyer wallet", err.Status()) + } + + // Update escrow status to Refunded + escrowUpdate := map[string]any{ + "status": "Refunded", + } + _, err = escrowAsset.Update(stub, escrowUpdate) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Failed to save escrow", err.Status()) + } + + response := map[string]any{ + "message": "Escrow refunded successfully", + "escrowUUID": escrowUUID, + "amount": amount, + "buyerWalletUUID": buyerWalletUUID, + } + + responseJSON, _ := json.Marshal(response) + return responseJSON, nil + }, +} + +// ReadEscrow retrieves an escrow contract by its unique identifier. +// This read-only operation returns the complete escrow state including status, +// parties involved, locked amount, and condition details. +// +// Arguments: +// - uuid: Unique identifier of the escrow contract +// +// Returns: +// - JSON representation of the escrow contract +// - Error if escrow not found or retrieval fails +// +// Use Cases: +// - Verify escrow status before attempting release or refund +// - Audit escrow contract terms and parties +// - Track escrow lifecycle in external systems +var ReadEscrow = transactions.Transaction{ + Tag: "readEscrow", + Label: "Read Escrow", + Description: "Read an Escrow by its escrowId", + Method: "GET", + Callers: []accesscontrol.Caller{ + { + MSP: "Org1MSP", + OU: "admin", + }, + { + MSP: "Org2MSP", + OU: "admin", + }, + }, + + Args: []transactions.Argument{ + { + Tag: "uuid", + Label: "UUID", + Description: "UUID of the Digital Asset to read", + DataType: "string", + Required: true, + }, + }, + + Routine: func(stub *sw.StubWrapper, req map[string]any) ([]byte, errors.ICCError) { + uuid, _ := req["uuid"].(string) + + key := assets.Key{ + "@key": "escrow:" + uuid, + } + + asset, err := key.Get(stub) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Error reading escrow from blockchain", err.Status()) + } + + assetJSON, nerr := json.Marshal(asset) + if nerr != nil { + return nil, errors.WrapError(nil, "failed to encode escrow to JSON format") + } + + return assetJSON, nil + }, +} diff --git a/samples/chaincode/confidential-escrow/chaincode/transactions/escrow_test.go b/samples/chaincode/confidential-escrow/chaincode/transactions/escrow_test.go new file mode 100644 index 000000000..6e55ea4c2 --- /dev/null +++ b/samples/chaincode/confidential-escrow/chaincode/transactions/escrow_test.go @@ -0,0 +1,1326 @@ +package transactions + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "testing" + + "github.com/hyperledger-labs/cc-tools/assets" + "github.com/hyperledger/fabric-private-chaincode/samples/chaincode/confidential-escrow/chaincode/testutils" +) + +// ============================================================================ +// CreateAndLockEscrow Tests +// ============================================================================ + +func TestCreateAndLockEscrow_Success(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup: Create digital asset + err := fixtures.CreateMockDigitalAsset( + mockStub, + fixtures.AssetID, + fixtures.AssetSymbol, + fixtures.AssetName, + fixtures.IssuerCertHash, + 1000.0, + ) + if err != nil { + t.Fatalf("Failed to create mock digital asset: %v", err) + } + + // Setup: Create buyer wallet with sufficient balance + err = fixtures.CreateMockWallet( + mockStub, + fixtures.BuyerPubKey, + fixtures.BuyerCertHash, + fixtures.BuyerWalletID, + fixtures.BuyerWalletUUID, + fixtures.AssetID, + 200.0, // available balance + 0.0, // escrow balance + ) + if err != nil { + t.Fatalf("Failed to create buyer wallet: %v", err) + } + + // Setup: Create buyer user directory + err = fixtures.CreateMockUserDir( + mockStub, + fixtures.BuyerPubKeyHash, + fixtures.BuyerWalletUUID, + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create buyer user directory: %v", err) + } + + // Setup: Create seller wallet + err = fixtures.CreateMockWallet( + mockStub, + fixtures.SellerPubKey, + fixtures.SellerCertHash, + fixtures.SellerWalletID, + fixtures.SellerWalletUUID, + fixtures.AssetID, + 0.0, // seller starts with 0 balance + 0.0, + ) + if err != nil { + t.Fatalf("Failed to create seller wallet: %v", err) + } + + // Setup: Create seller user directory + err = fixtures.CreateMockUserDir( + mockStub, + fixtures.SellerPubKeyHash, + fixtures.SellerWalletUUID, + fixtures.SellerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create seller user directory: %v", err) + } + + // Execute CreateAndLockEscrow + escrowAmount := 100.0 + args := map[string]any{ + "escrowId": fixtures.EscrowID, + "buyerPubKey": fixtures.BuyerPubKey, + "sellerPubKey": fixtures.SellerPubKey, + "amount": escrowAmount, + "assetType": assets.Key{"@key": "digitalAsset:" + fixtures.AssetID}, + "parcelId": fixtures.ParcelID, + "secret": fixtures.Secret, + "buyerCertHash": fixtures.BuyerCertHash, + } + + response, txErr := CreateAndLockEscrow.Routine(wrapper.StubWrapper, args) + testutils.AssertNoError(t, txErr, "CreateAndLockEscrow should succeed") + + // Parse escrow response + var createdEscrow map[string]any + if err := json.Unmarshal(response, &createdEscrow); err != nil { + t.Fatalf("Failed to parse escrow response: %v", err) + } + + // Verify escrow properties + testutils.AssertEqual(t, fixtures.EscrowID, createdEscrow["escrowId"], "escrowId mismatch") + testutils.AssertEqual(t, fixtures.BuyerPubKey, createdEscrow["buyerPubKey"], "buyerPubKey mismatch") + testutils.AssertEqual(t, fixtures.SellerPubKey, createdEscrow["sellerPubKey"], "sellerPubKey mismatch") + testutils.AssertEqual(t, escrowAmount, createdEscrow["amount"], "amount mismatch") + testutils.AssertEqual(t, "Active", createdEscrow["status"], "status should be Active") + testutils.AssertEqual(t, fixtures.ParcelID, createdEscrow["parcelId"], "parcelId mismatch") + + // Verify condition hash was computed correctly + expectedHash := sha256.Sum256([]byte(fixtures.Secret + fixtures.ParcelID)) + expectedCondition := hex.EncodeToString(expectedHash[:]) + testutils.AssertEqual(t, expectedCondition, createdEscrow["conditionValue"], "conditionValue mismatch") + + // Verify buyer wallet balances were updated + buyerWalletKey := "wallet:" + fixtures.BuyerWalletUUID + buyerWalletBytes, exists := mockStub.State[buyerWalletKey] + if !exists { + t.Fatal("Buyer wallet should exist in state") + } + + var buyerWallet map[string]any + if err := json.Unmarshal(buyerWalletBytes, &buyerWallet); err != nil { + t.Fatalf("Failed to parse buyer wallet: %v", err) + } + + balances := buyerWallet["balances"].([]any) + escrowBalances := buyerWallet["escrowBalances"].([]any) + + testutils.AssertEqual(t, 100.0, balances[0].(float64), "buyer available balance should be reduced by escrow amount") + testutils.AssertEqual(t, 100.0, escrowBalances[0].(float64), "buyer escrow balance should increase by escrow amount") + + t.Log("✓ Escrow created and funds locked successfully") +} + +func TestCreateAndLockEscrow_InsufficientBalance(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup: Create digital asset + err := fixtures.CreateMockDigitalAsset( + mockStub, + fixtures.AssetID, + fixtures.AssetSymbol, + fixtures.AssetName, + fixtures.IssuerCertHash, + 1000.0, + ) + if err != nil { + t.Fatalf("Failed to create mock digital asset: %v", err) + } + + // Setup: Create buyer wallet with insufficient balance + err = fixtures.CreateMockWallet( + mockStub, + fixtures.BuyerPubKey, + fixtures.BuyerCertHash, + fixtures.BuyerWalletID, + fixtures.BuyerWalletUUID, + fixtures.AssetID, + 50.0, // only 50 available + 0.0, + ) + if err != nil { + t.Fatalf("Failed to create buyer wallet: %v", err) + } + + err = fixtures.CreateMockUserDir( + mockStub, + fixtures.BuyerPubKeyHash, + fixtures.BuyerWalletUUID, + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create buyer user directory: %v", err) + } + + // Setup: Create seller wallet and directory + err = fixtures.CreateMockWallet( + mockStub, + fixtures.SellerPubKey, + fixtures.SellerCertHash, + fixtures.SellerWalletID, + fixtures.SellerWalletUUID, + fixtures.AssetID, + 0.0, + 0.0, + ) + if err != nil { + t.Fatalf("Failed to create seller wallet: %v", err) + } + + err = fixtures.CreateMockUserDir( + mockStub, + fixtures.SellerPubKeyHash, + fixtures.SellerWalletUUID, + fixtures.SellerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create seller user directory: %v", err) + } + + // Try to lock more than available + args := map[string]any{ + "escrowId": fixtures.EscrowID, + "buyerPubKey": fixtures.BuyerPubKey, + "sellerPubKey": fixtures.SellerPubKey, + "amount": 100.0, // trying to lock 100 but only have 50 + "assetType": assets.Key{"@key": "digitalAsset:" + fixtures.AssetID}, + "parcelId": fixtures.ParcelID, + "secret": fixtures.Secret, + "buyerCertHash": fixtures.BuyerCertHash, + } + + _, txErr := CreateAndLockEscrow.Routine(wrapper.StubWrapper, args) + testutils.AssertError(t, txErr, "should fail with insufficient balance") + testutils.AssertErrorStatus(t, txErr, 400, "should return 400 status") +} + +func TestCreateAndLockEscrow_BuyerWalletNotFound(t *testing.T) { + wrapper, _ := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + args := map[string]any{ + "escrowId": fixtures.EscrowID, + "buyerPubKey": "non-existent-buyer", + "sellerPubKey": fixtures.SellerPubKey, + "amount": 100.0, + "assetType": assets.Key{"@key": "digitalAsset:" + fixtures.AssetID}, + "parcelId": fixtures.ParcelID, + "secret": fixtures.Secret, + "buyerCertHash": fixtures.BuyerCertHash, + } + + _, txErr := CreateAndLockEscrow.Routine(wrapper.StubWrapper, args) + testutils.AssertError(t, txErr, "should fail when buyer wallet not found") + testutils.AssertErrorStatus(t, txErr, 404, "should return 404 status") +} + +func TestCreateAndLockEscrow_UnauthorizedBuyer(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup + err := fixtures.CreateMockDigitalAsset( + mockStub, + fixtures.AssetID, + fixtures.AssetSymbol, + fixtures.AssetName, + fixtures.IssuerCertHash, + 1000.0, + ) + if err != nil { + t.Fatalf("Failed to create mock digital asset: %v", err) + } + + err = fixtures.CreateMockWallet( + mockStub, + fixtures.BuyerPubKey, + fixtures.BuyerCertHash, + fixtures.BuyerWalletID, + fixtures.BuyerWalletUUID, + fixtures.AssetID, + 200.0, + 0.0, + ) + if err != nil { + t.Fatalf("Failed to create buyer wallet: %v", err) + } + + err = fixtures.CreateMockUserDir( + mockStub, + fixtures.BuyerPubKeyHash, + fixtures.BuyerWalletUUID, + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create buyer user directory: %v", err) + } + + err = fixtures.CreateMockWallet( + mockStub, + fixtures.SellerPubKey, + fixtures.SellerCertHash, + fixtures.SellerWalletID, + fixtures.SellerWalletUUID, + fixtures.AssetID, + 0.0, + 0.0, + ) + if err != nil { + t.Fatalf("Failed to create seller wallet: %v", err) + } + + err = fixtures.CreateMockUserDir( + mockStub, + fixtures.SellerPubKeyHash, + fixtures.SellerWalletUUID, + fixtures.SellerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create seller user directory: %v", err) + } + + // Try with wrong certificate hash + args := map[string]any{ + "escrowId": fixtures.EscrowID, + "buyerPubKey": fixtures.BuyerPubKey, + "sellerPubKey": fixtures.SellerPubKey, + "amount": 100.0, + "assetType": assets.Key{"@key": "digitalAsset:" + fixtures.AssetID}, + "parcelId": fixtures.ParcelID, + "secret": fixtures.Secret, + "buyerCertHash": "wrong-cert-hash", + } + + _, txErr := CreateAndLockEscrow.Routine(wrapper.StubWrapper, args) + testutils.AssertError(t, txErr, "should fail with wrong certificate") + testutils.AssertErrorStatus(t, txErr, 403, "should return 403 Unauthorized") +} + +// func TestCreateAndLockEscrow_SellerWalletNotFound(t *testing.T) { +// wrapper, mockStub := testutils.NewMockStubWrapper() +// fixtures := testutils.NewTestFixtures() +// +// // Setup only buyer, not seller +// err := fixtures.CreateMockDigitalAsset( +// mockStub, +// fixtures.AssetID, +// fixtures.AssetSymbol, +// fixtures.AssetName, +// fixtures.IssuerCertHash, +// 1000.0, +// ) +// if err != nil { +// t.Fatalf("Failed to create mock digital asset: %v", err) +// } +// +// err = fixtures.CreateMockWallet( +// mockStub, +// fixtures.BuyerPubKey, +// fixtures.BuyerCertHash, +// fixtures.BuyerWalletID, +// fixtures.BuyerWalletUUID, +// fixtures.AssetID, +// 200.0, +// 0.0, +// ) +// if err != nil { +// t.Fatalf("Failed to create buyer wallet: %v", err) +// } +// +// err = fixtures.CreateMockUserDir( +// mockStub, +// fixtures.BuyerPubKeyHash, +// fixtures.BuyerWalletUUID, +// fixtures.BuyerCertHash, +// ) +// if err != nil { +// t.Fatalf("Failed to create buyer user directory: %v", err) +// } +// +// args := map[string]any{ +// "escrowId": fixtures.EscrowID, +// "buyerPubKey": fixtures.BuyerPubKey, +// "sellerPubKey": "non-existent-seller", +// "amount": 100.0, +// "assetType": assets.Key{"@key": "digitalAsset:" + fixtures.AssetID}, +// "parcelId": fixtures.ParcelID, +// "secret": fixtures.Secret, +// "buyerCertHash": fixtures.BuyerCertHash, +// } +// +// _, txErr := CreateAndLockEscrow.Routine(wrapper.StubWrapper, args) +// testutils.AssertError(t, txErr, "should fail when seller wallet not found") +// testutils.AssertErrorStatus(t, txErr, 404, "should return 404 status") +// } + +// ============================================================================ +// VerifyEscrowCondition Tests +// ============================================================================ + +// func TestVerifyEscrowCondition_Success(t *testing.T) { +// wrapper, mockStub := testutils.NewMockStubWrapper() +// fixtures := testutils.NewTestFixtures() +// +// // Setup: Create an active escrow +// err := fixtures.CreateMockEscrow( +// mockStub, +// fixtures.EscrowID, +// fixtures.BuyerPubKey, +// fixtures.SellerPubKey, +// fixtures.BuyerWalletUUID, +// fixtures.SellerWalletUUID, +// fixtures.AssetID, +// 100.0, +// fixtures.ParcelID, +// fixtures.Secret, +// "Active", +// fixtures.BuyerCertHash, +// ) +// if err != nil { +// t.Fatalf("Failed to create mock escrow: %v", err) +// } +// +// // Execute VerifyEscrowCondition +// args := map[string]any{ +// "escrowId": fixtures.EscrowID, +// "secret": fixtures.Secret, +// "parcelId": fixtures.ParcelID, +// } +// +// response, txErr := VerifyEscrowCondition.Routine(wrapper.StubWrapper, args) +// testutils.AssertNoError(t, txErr, "VerifyEscrowCondition should succeed") +// +// // Parse response +// var result map[string]any +// if err := json.Unmarshal(response, &result); err != nil { +// t.Fatalf("Failed to parse response: %v", err) +// } +// +// testutils.AssertEqual(t, "Condition verified successfully", result["message"], "message mismatch") +// testutils.AssertEqual(t, fixtures.EscrowID, result["escrowId"], "escrowId mismatch") +// testutils.AssertEqual(t, "ReadyForRelease", result["status"], "status should be ReadyForRelease") +// testutils.AssertEqual(t, fixtures.ParcelID, result["parcelId"], "parcelId mismatch") +// +// // Verify computed hash matches expected +// expectedHash := sha256.Sum256([]byte(fixtures.Secret + fixtures.ParcelID)) +// expectedCondition := hex.EncodeToString(expectedHash[:]) +// testutils.AssertEqual(t, expectedCondition, result["computedHash"], "computedHash mismatch") +// +// // Verify escrow status was updated in state +// escrowKey := "escrow:" + fixtures.EscrowID +// escrowBytes, exists := mockStub.State[escrowKey] +// if !exists { +// t.Fatal("Escrow should exist in state") +// } +// +// var escrow map[string]any +// if err := json.Unmarshal(escrowBytes, &escrow); err != nil { +// t.Fatalf("Failed to parse escrow: %v", err) +// } +// +// testutils.AssertEqual(t, "ReadyForRelease", escrow["status"], "escrow status should be updated to ReadyForRelease") +// +// t.Log("✓ Escrow condition verified successfully") +// } + +func TestVerifyEscrowCondition_WrongSecret(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup: Create an active escrow + err := fixtures.CreateMockEscrow( + mockStub, + fixtures.EscrowID, + fixtures.BuyerPubKey, + fixtures.SellerPubKey, + fixtures.BuyerWalletUUID, + fixtures.SellerWalletUUID, + fixtures.AssetID, + 100.0, + fixtures.ParcelID, + fixtures.Secret, + "Active", + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create mock escrow: %v", err) + } + + // Try to verify with wrong secret + args := map[string]any{ + "escrowId": fixtures.EscrowID, + "secret": "wrong-secret", + "parcelId": fixtures.ParcelID, + } + + _, txErr := VerifyEscrowCondition.Routine(wrapper.StubWrapper, args) + testutils.AssertError(t, txErr, "should fail with wrong secret") + testutils.AssertErrorStatus(t, txErr, 403, "should return 403 status") +} + +func TestVerifyEscrowCondition_EscrowNotActive(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup: Create a released escrow + err := fixtures.CreateMockEscrow( + mockStub, + fixtures.EscrowID, + fixtures.BuyerPubKey, + fixtures.SellerPubKey, + fixtures.BuyerWalletUUID, + fixtures.SellerWalletUUID, + fixtures.AssetID, + 100.0, + fixtures.ParcelID, + fixtures.Secret, + "Released", // Already released + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create mock escrow: %v", err) + } + + args := map[string]any{ + "escrowId": fixtures.EscrowID, + "secret": fixtures.Secret, + "parcelId": fixtures.ParcelID, + } + + _, txErr := VerifyEscrowCondition.Routine(wrapper.StubWrapper, args) + testutils.AssertError(t, txErr, "should fail when escrow not active") + testutils.AssertErrorStatus(t, txErr, 400, "should return 400 status") +} + +func TestVerifyEscrowCondition_EscrowNotFound(t *testing.T) { + wrapper, _ := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + args := map[string]any{ + "escrowId": "non-existent-escrow", + "secret": fixtures.Secret, + "parcelId": fixtures.ParcelID, + } + + _, txErr := VerifyEscrowCondition.Routine(wrapper.StubWrapper, args) + testutils.AssertError(t, txErr, "should fail when escrow not found") +} + +// ============================================================================ +// ReleaseEscrow Tests +// ============================================================================ + +func TestReleaseEscrow_Success(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup: Create digital asset + err := fixtures.CreateMockDigitalAsset( + mockStub, + fixtures.AssetID, + fixtures.AssetSymbol, + fixtures.AssetName, + fixtures.IssuerCertHash, + 1000.0, + ) + if err != nil { + t.Fatalf("Failed to create mock digital asset: %v", err) + } + + // Setup: Create buyer wallet with escrow balance + err = fixtures.CreateMockWallet( + mockStub, + fixtures.BuyerPubKey, + fixtures.BuyerCertHash, + fixtures.BuyerWalletID, + fixtures.BuyerWalletUUID, + fixtures.AssetID, + 100.0, // available balance + 100.0, // escrow balance + ) + if err != nil { + t.Fatalf("Failed to create buyer wallet: %v", err) + } + + // Setup: Create seller wallet + err = fixtures.CreateMockWallet( + mockStub, + fixtures.SellerPubKey, + fixtures.SellerCertHash, + fixtures.SellerWalletID, + fixtures.SellerWalletUUID, + fixtures.AssetID, + 0.0, // seller starts with 0 + 0.0, + ) + if err != nil { + t.Fatalf("Failed to create seller wallet: %v", err) + } + + // Setup: Create active escrow + err = fixtures.CreateMockEscrow( + mockStub, + fixtures.EscrowID, + fixtures.BuyerPubKey, + fixtures.SellerPubKey, + fixtures.BuyerWalletUUID, + fixtures.SellerWalletUUID, + fixtures.AssetID, + 100.0, + fixtures.ParcelID, + fixtures.Secret, + "Active", + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create mock escrow: %v", err) + } + + // Execute ReleaseEscrow + args := map[string]any{ + "escrowUUID": fixtures.EscrowID, + "secret": fixtures.Secret, + "parcelId": fixtures.ParcelID, + "sellerCertHash": fixtures.SellerCertHash, + } + + response, txErr := ReleaseEscrow.Routine(wrapper.StubWrapper, args) + testutils.AssertNoError(t, txErr, "ReleaseEscrow should succeed") + + // Parse response + var result map[string]any + if err := json.Unmarshal(response, &result); err != nil { + t.Fatalf("Failed to parse response: %v", err) + } + + testutils.AssertEqual(t, "Escrow released successfully", result["message"], "message mismatch") + testutils.AssertEqual(t, fixtures.EscrowID, result["escrowId"], "escrowId mismatch") + testutils.AssertEqual(t, 100.0, result["amount"], "amount mismatch") + + // Verify buyer wallet: escrow balance reduced + buyerWalletKey := "wallet:" + fixtures.BuyerWalletUUID + buyerWalletBytes := mockStub.State[buyerWalletKey] + var buyerWallet map[string]any + json.Unmarshal(buyerWalletBytes, &buyerWallet) + + buyerEscrowBalances := buyerWallet["escrowBalances"].([]any) + testutils.AssertEqual(t, 0.0, buyerEscrowBalances[0].(float64), "buyer escrow balance should be 0") + + // Verify seller wallet: balance increased + sellerWalletKey := "wallet:" + fixtures.SellerWalletUUID + sellerWalletBytes := mockStub.State[sellerWalletKey] + var sellerWallet map[string]any + json.Unmarshal(sellerWalletBytes, &sellerWallet) + + sellerBalances := sellerWallet["balances"].([]any) + testutils.AssertEqual(t, 100.0, sellerBalances[0].(float64), "seller balance should be 100") + + // Verify escrow status updated to Released + escrowKey := "escrow:" + fixtures.EscrowID + escrowBytes := mockStub.State[escrowKey] + var escrow map[string]any + json.Unmarshal(escrowBytes, &escrow) + + testutils.AssertEqual(t, "Released", escrow["status"], "escrow status should be Released") + + t.Log("✓ Escrow released and funds transferred successfully") +} + +func TestReleaseEscrow_WrongSecret(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup + err := fixtures.CreateMockDigitalAsset( + mockStub, + fixtures.AssetID, + fixtures.AssetSymbol, + fixtures.AssetName, + fixtures.IssuerCertHash, + 1000.0, + ) + if err != nil { + t.Fatalf("Failed to create mock digital asset: %v", err) + } + + err = fixtures.CreateMockWallet( + mockStub, + fixtures.BuyerPubKey, + fixtures.BuyerCertHash, + fixtures.BuyerWalletID, + fixtures.BuyerWalletUUID, + fixtures.AssetID, + 100.0, + 100.0, + ) + if err != nil { + t.Fatalf("Failed to create buyer wallet: %v", err) + } + + err = fixtures.CreateMockWallet( + mockStub, + fixtures.SellerPubKey, + fixtures.SellerCertHash, + fixtures.SellerWalletID, + fixtures.SellerWalletUUID, + fixtures.AssetID, + 0.0, + 0.0, + ) + if err != nil { + t.Fatalf("Failed to create seller wallet: %v", err) + } + + err = fixtures.CreateMockEscrow( + mockStub, + fixtures.EscrowID, + fixtures.BuyerPubKey, + fixtures.SellerPubKey, + fixtures.BuyerWalletUUID, + fixtures.SellerWalletUUID, + fixtures.AssetID, + 100.0, + fixtures.ParcelID, + fixtures.Secret, + "Active", + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create mock escrow: %v", err) + } + + // Try with wrong secret + args := map[string]any{ + "escrowUUID": fixtures.EscrowID, + "secret": "wrong-secret", + "parcelId": fixtures.ParcelID, + "sellerCertHash": fixtures.SellerCertHash, + } + + _, txErr := ReleaseEscrow.Routine(wrapper.StubWrapper, args) + testutils.AssertError(t, txErr, "should fail with wrong secret") + testutils.AssertErrorStatus(t, txErr, 403, "should return 403 status") +} + +func TestReleaseEscrow_WrongParcelId(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup + err := fixtures.CreateMockDigitalAsset( + mockStub, + fixtures.AssetID, + fixtures.AssetSymbol, + fixtures.AssetName, + fixtures.IssuerCertHash, + 1000.0, + ) + if err != nil { + t.Fatalf("Failed to create mock digital asset: %v", err) + } + + err = fixtures.CreateMockWallet( + mockStub, + fixtures.BuyerPubKey, + fixtures.BuyerCertHash, + fixtures.BuyerWalletID, + fixtures.BuyerWalletUUID, + fixtures.AssetID, + 100.0, + 100.0, + ) + if err != nil { + t.Fatalf("Failed to create buyer wallet: %v", err) + } + + err = fixtures.CreateMockWallet( + mockStub, + fixtures.SellerPubKey, + fixtures.SellerCertHash, + fixtures.SellerWalletID, + fixtures.SellerWalletUUID, + fixtures.AssetID, + 0.0, + 0.0, + ) + if err != nil { + t.Fatalf("Failed to create seller wallet: %v", err) + } + + err = fixtures.CreateMockEscrow( + mockStub, + fixtures.EscrowID, + fixtures.BuyerPubKey, + fixtures.SellerPubKey, + fixtures.BuyerWalletUUID, + fixtures.SellerWalletUUID, + fixtures.AssetID, + 100.0, + fixtures.ParcelID, + fixtures.Secret, + "Active", + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create mock escrow: %v", err) + } + + // Try with wrong parcelId + args := map[string]any{ + "escrowUUID": fixtures.EscrowID, + "secret": fixtures.Secret, + "parcelId": "wrong-parcel-id", + "sellerCertHash": fixtures.SellerCertHash, + } + + _, txErr := ReleaseEscrow.Routine(wrapper.StubWrapper, args) + testutils.AssertError(t, txErr, "should fail with wrong parcelId") + testutils.AssertErrorStatus(t, txErr, 403, "should return 403 status") +} + +func TestReleaseEscrow_UnauthorizedSeller(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup + err := fixtures.CreateMockDigitalAsset( + mockStub, + fixtures.AssetID, + fixtures.AssetSymbol, + fixtures.AssetName, + fixtures.IssuerCertHash, + 1000.0, + ) + if err != nil { + t.Fatalf("Failed to create mock digital asset: %v", err) + } + + err = fixtures.CreateMockWallet( + mockStub, + fixtures.BuyerPubKey, + fixtures.BuyerCertHash, + fixtures.BuyerWalletID, + fixtures.BuyerWalletUUID, + fixtures.AssetID, + 100.0, + 100.0, + ) + if err != nil { + t.Fatalf("Failed to create buyer wallet: %v", err) + } + + err = fixtures.CreateMockWallet( + mockStub, + fixtures.SellerPubKey, + fixtures.SellerCertHash, + fixtures.SellerWalletID, + fixtures.SellerWalletUUID, + fixtures.AssetID, + 0.0, + 0.0, + ) + if err != nil { + t.Fatalf("Failed to create seller wallet: %v", err) + } + + err = fixtures.CreateMockEscrow( + mockStub, + fixtures.EscrowID, + fixtures.BuyerPubKey, + fixtures.SellerPubKey, + fixtures.BuyerWalletUUID, + fixtures.SellerWalletUUID, + fixtures.AssetID, + 100.0, + fixtures.ParcelID, + fixtures.Secret, + "Active", + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create mock escrow: %v", err) + } + + // Try with wrong seller certificate + args := map[string]any{ + "escrowUUID": fixtures.EscrowID, + "secret": fixtures.Secret, + "parcelId": fixtures.ParcelID, + "sellerCertHash": "wrong-seller-cert", + } + + _, txErr := ReleaseEscrow.Routine(wrapper.StubWrapper, args) + testutils.AssertError(t, txErr, "should fail with wrong seller certificate") + testutils.AssertErrorStatus(t, txErr, 403, "should return 403 Unauthorized") +} + +func TestReleaseEscrow_EscrowNotActive(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup + err := fixtures.CreateMockDigitalAsset( + mockStub, + fixtures.AssetID, + fixtures.AssetSymbol, + fixtures.AssetName, + fixtures.IssuerCertHash, + 1000.0, + ) + if err != nil { + t.Fatalf("Failed to create mock digital asset: %v", err) + } + + err = fixtures.CreateMockWallet( + mockStub, + fixtures.BuyerPubKey, + fixtures.BuyerCertHash, + fixtures.BuyerWalletID, + fixtures.BuyerWalletUUID, + fixtures.AssetID, + 100.0, + 100.0, + ) + if err != nil { + t.Fatalf("Failed to create buyer wallet: %v", err) + } + + err = fixtures.CreateMockWallet( + mockStub, + fixtures.SellerPubKey, + fixtures.SellerCertHash, + fixtures.SellerWalletID, + fixtures.SellerWalletUUID, + fixtures.AssetID, + 0.0, + 0.0, + ) + if err != nil { + t.Fatalf("Failed to create seller wallet: %v", err) + } + + // Create already released escrow + err = fixtures.CreateMockEscrow( + mockStub, + fixtures.EscrowID, + fixtures.BuyerPubKey, + fixtures.SellerPubKey, + fixtures.BuyerWalletUUID, + fixtures.SellerWalletUUID, + fixtures.AssetID, + 100.0, + fixtures.ParcelID, + fixtures.Secret, + "Released", // Already released + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create mock escrow: %v", err) + } + + args := map[string]any{ + "escrowUUID": fixtures.EscrowID, + "secret": fixtures.Secret, + "parcelId": fixtures.ParcelID, + "sellerCertHash": fixtures.SellerCertHash, + } + + _, txErr := ReleaseEscrow.Routine(wrapper.StubWrapper, args) + testutils.AssertError(t, txErr, "should fail when escrow not active") + testutils.AssertErrorStatus(t, txErr, 400, "should return 400 status") +} + +// ============================================================================ +// RefundEscrow Tests +// ============================================================================ + +func TestRefundEscrow_Success(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup: Create digital asset + err := fixtures.CreateMockDigitalAsset( + mockStub, + fixtures.AssetID, + fixtures.AssetSymbol, + fixtures.AssetName, + fixtures.IssuerCertHash, + 1000.0, + ) + if err != nil { + t.Fatalf("Failed to create mock digital asset: %v", err) + } + + // Setup: Create buyer wallet with escrow balance + err = fixtures.CreateMockWallet( + mockStub, + fixtures.BuyerPubKey, + fixtures.BuyerCertHash, + fixtures.BuyerWalletID, + fixtures.BuyerWalletUUID, + fixtures.AssetID, + 100.0, // available balance + 100.0, // escrow balance + ) + if err != nil { + t.Fatalf("Failed to create buyer wallet: %v", err) + } + + // Setup: Create buyer user directory + err = fixtures.CreateMockUserDir( + mockStub, + fixtures.BuyerPubKeyHash, + fixtures.BuyerWalletUUID, + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create buyer user directory: %v", err) + } + + // Setup: Create active escrow + err = fixtures.CreateMockEscrow( + mockStub, + fixtures.EscrowID, + fixtures.BuyerPubKey, + fixtures.SellerPubKey, + fixtures.BuyerWalletUUID, + fixtures.SellerWalletUUID, + fixtures.AssetID, + 100.0, + fixtures.ParcelID, + fixtures.Secret, + "Active", + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create mock escrow: %v", err) + } + + // Execute RefundEscrow + args := map[string]any{ + "escrowUUID": fixtures.EscrowID, + "buyerPubKey": fixtures.BuyerPubKey, + "buyerCertHash": fixtures.BuyerCertHash, + } + + response, txErr := RefundEscrow.Routine(wrapper.StubWrapper, args) + testutils.AssertNoError(t, txErr, "RefundEscrow should succeed") + + // Parse response + var result map[string]any + if err := json.Unmarshal(response, &result); err != nil { + t.Fatalf("Failed to parse response: %v", err) + } + + testutils.AssertEqual(t, "Escrow refunded successfully", result["message"], "message mismatch") + testutils.AssertEqual(t, fixtures.EscrowID, result["escrowUUID"], "escrowUUID mismatch") + testutils.AssertEqual(t, 100.0, result["amount"], "amount mismatch") + + // Verify buyer wallet: funds moved from escrow back to available + buyerWalletKey := "wallet:" + fixtures.BuyerWalletUUID + buyerWalletBytes := mockStub.State[buyerWalletKey] + var buyerWallet map[string]any + json.Unmarshal(buyerWalletBytes, &buyerWallet) + + buyerBalances := buyerWallet["balances"].([]any) + buyerEscrowBalances := buyerWallet["escrowBalances"].([]any) + + testutils.AssertEqual(t, 200.0, buyerBalances[0].(float64), "buyer balance should increase by 100") + testutils.AssertEqual(t, 0.0, buyerEscrowBalances[0].(float64), "buyer escrow balance should be 0") + + // Verify escrow status updated to Refunded + escrowKey := "escrow:" + fixtures.EscrowID + escrowBytes := mockStub.State[escrowKey] + var escrow map[string]any + json.Unmarshal(escrowBytes, &escrow) + + testutils.AssertEqual(t, "Refunded", escrow["status"], "escrow status should be Refunded") + + t.Log("✓ Escrow refunded successfully") +} + +func TestRefundEscrow_UnauthorizedBuyer(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup + err := fixtures.CreateMockDigitalAsset( + mockStub, + fixtures.AssetID, + fixtures.AssetSymbol, + fixtures.AssetName, + fixtures.IssuerCertHash, + 1000.0, + ) + if err != nil { + t.Fatalf("Failed to create mock digital asset: %v", err) + } + + err = fixtures.CreateMockWallet( + mockStub, + fixtures.BuyerPubKey, + fixtures.BuyerCertHash, + fixtures.BuyerWalletID, + fixtures.BuyerWalletUUID, + fixtures.AssetID, + 100.0, + 100.0, + ) + if err != nil { + t.Fatalf("Failed to create buyer wallet: %v", err) + } + + err = fixtures.CreateMockUserDir( + mockStub, + fixtures.BuyerPubKeyHash, + fixtures.BuyerWalletUUID, + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create buyer user directory: %v", err) + } + + err = fixtures.CreateMockEscrow( + mockStub, + fixtures.EscrowID, + fixtures.BuyerPubKey, + fixtures.SellerPubKey, + fixtures.BuyerWalletUUID, + fixtures.SellerWalletUUID, + fixtures.AssetID, + 100.0, + fixtures.ParcelID, + fixtures.Secret, + "Active", + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create mock escrow: %v", err) + } + + // Try with wrong buyer certificate + args := map[string]any{ + "escrowUUID": fixtures.EscrowID, + "buyerPubKey": fixtures.BuyerPubKey, + "buyerCertHash": "wrong-buyer-cert", + } + + _, txErr := RefundEscrow.Routine(wrapper.StubWrapper, args) + testutils.AssertError(t, txErr, "should fail with wrong buyer certificate") + testutils.AssertErrorStatus(t, txErr, 403, "should return 403 Unauthorized") +} + +func TestRefundEscrow_EscrowNotActive(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup + err := fixtures.CreateMockDigitalAsset( + mockStub, + fixtures.AssetID, + fixtures.AssetSymbol, + fixtures.AssetName, + fixtures.IssuerCertHash, + 1000.0, + ) + if err != nil { + t.Fatalf("Failed to create mock digital asset: %v", err) + } + + err = fixtures.CreateMockWallet( + mockStub, + fixtures.BuyerPubKey, + fixtures.BuyerCertHash, + fixtures.BuyerWalletID, + fixtures.BuyerWalletUUID, + fixtures.AssetID, + 100.0, + 100.0, + ) + if err != nil { + t.Fatalf("Failed to create buyer wallet: %v", err) + } + + err = fixtures.CreateMockUserDir( + mockStub, + fixtures.BuyerPubKeyHash, + fixtures.BuyerWalletUUID, + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create buyer user directory: %v", err) + } + + // Create already refunded escrow + err = fixtures.CreateMockEscrow( + mockStub, + fixtures.EscrowID, + fixtures.BuyerPubKey, + fixtures.SellerPubKey, + fixtures.BuyerWalletUUID, + fixtures.SellerWalletUUID, + fixtures.AssetID, + 100.0, + fixtures.ParcelID, + fixtures.Secret, + "Refunded", // Already refunded + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create mock escrow: %v", err) + } + + args := map[string]any{ + "escrowUUID": fixtures.EscrowID, + "buyerPubKey": fixtures.BuyerPubKey, + "buyerCertHash": fixtures.BuyerCertHash, + } + + _, txErr := RefundEscrow.Routine(wrapper.StubWrapper, args) + testutils.AssertError(t, txErr, "should fail when escrow not active") + testutils.AssertErrorStatus(t, txErr, 400, "should return 400 status") +} + +func TestRefundEscrow_EscrowNotFound(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup buyer wallet and directory + err := fixtures.CreateMockDigitalAsset( + mockStub, + fixtures.AssetID, + fixtures.AssetSymbol, + fixtures.AssetName, + fixtures.IssuerCertHash, + 1000.0, + ) + if err != nil { + t.Fatalf("Failed to create mock digital asset: %v", err) + } + + err = fixtures.CreateMockWallet( + mockStub, + fixtures.BuyerPubKey, + fixtures.BuyerCertHash, + fixtures.BuyerWalletID, + fixtures.BuyerWalletUUID, + fixtures.AssetID, + 100.0, + 100.0, + ) + if err != nil { + t.Fatalf("Failed to create buyer wallet: %v", err) + } + + err = fixtures.CreateMockUserDir( + mockStub, + fixtures.BuyerPubKeyHash, + fixtures.BuyerWalletUUID, + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create buyer user directory: %v", err) + } + + args := map[string]any{ + "escrowUUID": "non-existent-escrow", + "buyerPubKey": fixtures.BuyerPubKey, + "buyerCertHash": fixtures.BuyerCertHash, + } + + _, txErr := RefundEscrow.Routine(wrapper.StubWrapper, args) + testutils.AssertError(t, txErr, "should fail when escrow not found") + testutils.AssertErrorStatus(t, txErr, 404, "should return 404 status") +} + +// ============================================================================ +// ReadEscrow Tests +// ============================================================================ + +func TestReadEscrow_Success(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup: Create an escrow + err := fixtures.CreateMockEscrow( + mockStub, + fixtures.EscrowID, + fixtures.BuyerPubKey, + fixtures.SellerPubKey, + fixtures.BuyerWalletUUID, + fixtures.SellerWalletUUID, + fixtures.AssetID, + 100.0, + fixtures.ParcelID, + fixtures.Secret, + "Active", + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create mock escrow: %v", err) + } + + // Execute ReadEscrow + args := map[string]any{ + "uuid": fixtures.EscrowID, + } + + response, txErr := ReadEscrow.Routine(wrapper.StubWrapper, args) + testutils.AssertNoError(t, txErr, "ReadEscrow should succeed") + + // Parse response + var escrow map[string]any + if err := json.Unmarshal(response, &escrow); err != nil { + t.Fatalf("Failed to parse escrow: %v", err) + } + + // Verify escrow properties + testutils.AssertEqual(t, fixtures.EscrowID, escrow["escrowId"], "escrowId mismatch") + testutils.AssertEqual(t, fixtures.BuyerPubKey, escrow["buyerPubKey"], "buyerPubKey mismatch") + testutils.AssertEqual(t, fixtures.SellerPubKey, escrow["sellerPubKey"], "sellerPubKey mismatch") + testutils.AssertEqual(t, 100.0, escrow["amount"], "amount mismatch") + testutils.AssertEqual(t, "Active", escrow["status"], "status mismatch") + testutils.AssertEqual(t, fixtures.ParcelID, escrow["parcelId"], "parcelId mismatch") + + t.Log("✓ Escrow read successfully") +} + +func TestReadEscrow_NotFound(t *testing.T) { + wrapper, _ := testutils.NewMockStubWrapper() + + args := map[string]any{ + "uuid": "non-existent-escrow", + } + + _, txErr := ReadEscrow.Routine(wrapper.StubWrapper, args) + testutils.AssertError(t, txErr, "should fail when escrow not found") +} diff --git a/samples/chaincode/confidential-escrow/chaincode/transactions/user_directory.go b/samples/chaincode/confidential-escrow/chaincode/transactions/user_directory.go new file mode 100644 index 000000000..71d19198c --- /dev/null +++ b/samples/chaincode/confidential-escrow/chaincode/transactions/user_directory.go @@ -0,0 +1,168 @@ +// This file implements UserDirectory operations for mapping public key hashes to wallet UUIDs. +// The directory provides a privacy-preserving lookup mechanism enabling wallet discovery +// without exposing actual public keys on the ledger. +package transactions + +import ( + "encoding/json" + "fmt" + + "github.com/hyperledger-labs/cc-tools/accesscontrol" + "github.com/hyperledger-labs/cc-tools/assets" + "github.com/hyperledger-labs/cc-tools/errors" + "github.com/hyperledger-labs/cc-tools/events" + sw "github.com/hyperledger-labs/cc-tools/stubwrapper" + "github.com/hyperledger-labs/cc-tools/transactions" +) + +// CreateUserDir registers a new user directory entry linking a public key hash to a wallet. +// This entry is created automatically during wallet creation but can also be invoked +// independently for manual directory management. +// +// Arguments: +// - publicKeyHash: SHA-256 hash of the user's public key +// - walletUUID: UUID of the associated wallet +// - certHash: Certificate hash of the wallet owner +// +// Returns: +// - JSON representation of the created directory entry +// - Error if entry creation or persistence fails +// +// Note: The publicKeyHash serves as the primary key, ensuring one wallet per public key hash. +var CreateUserDir = transactions.Transaction{ + Tag: "createUserDir", + Label: "User Directory Creation", + Description: "Creates a new User entry", + Method: "POST", + Callers: []accesscontrol.Caller{ + { + MSP: "Org1MSP", + OU: "admin", + }, { + MSP: "Org2MSP", + OU: "admin", + }, + }, + + Args: []transactions.Argument{ + { + Tag: "publicKeyHash", + Label: "Public Key Hash", + DataType: "string", + Required: true, + }, + { + Tag: "walletUUID", + Label: "Associated Wallet UUID", + DataType: "string", + Required: true, + }, + { + Tag: "certHash", + Label: "Certificate Hash", + DataType: "string", + Required: true, + }, + }, + + Routine: func(stub *sw.StubWrapper, req map[string]any) ([]byte, errors.ICCError) { + publicKeyHash, _ := req["publicKeyHash"].(string) + walletId, _ := req["walletUUID"].(string) + certHash, _ := req["certHash"].(string) + + userDirMap := make(map[string]any) + userDirMap["@assetType"] = "userdir" + userDirMap["publicKeyHash"] = publicKeyHash + userDirMap["walletUUID"] = walletId + userDirMap["certHash"] = certHash + + userDirAsset, err := assets.NewAsset(userDirMap) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Error reading user directory entry from blockchain", err.Status()) + } + + _, err = userDirAsset.PutNew(stub) + if err != nil { + return nil, errors.WrapError(nil, "failed to encode asset to JSON format") + } + + assetJson, nerr := json.Marshal(userDirAsset) + if nerr != nil { + return nil, errors.WrapError(nil, "failed to encode asset to JSON format") + } + + logMsg, ok := json.Marshal(fmt.Sprintf("New user directory created: %s", publicKeyHash)) + if ok != nil { + return nil, errors.WrapError(nil, "failed to encode asset to JSON format") + } + + events.CallEvent(stub, "createUserDirLog", logMsg) + + return assetJson, nil + }, +} + +// ReadUserDir retrieves a user directory entry with ownership verification. +// This operation requires the caller to provide a valid certificate hash, +// preventing unauthorized directory lookups. +// +// Arguments: +// - userDir: Reference to the user directory entry (by publicKeyHash) +// - certHash: Certificate hash for ownership verification +// +// Returns: +// - JSON representation of the directory entry including wallet UUID +// - Error if entry not found or certificate hash mismatch +// +// Security: Certificate verification prevents enumeration attacks where an +// adversary attempts to map all public keys to wallets. +var ReadUserDir = transactions.Transaction{ + Tag: "readUserDir", + Label: "Read User Directory", + Description: "Read a User Directory by its publicKeyHash", + Method: "GET", + Callers: []accesscontrol.Caller{ + {MSP: "Org1MSP", OU: "admin"}, + {MSP: "Org2MSP", OU: "admin"}, + }, + + Args: []transactions.Argument{ + { + Tag: "userDir", + Label: "User Directory", + Description: "User Directory to read", + DataType: "->userdir", + Required: true, + }, + { + Tag: "certHash", + Label: "Certificate Hash", + Description: "Certificate hash for ownership verification", + DataType: "string", + Required: true, + }, + }, + + Routine: func(stub *sw.StubWrapper, req map[string]any) ([]byte, errors.ICCError) { + userDirRef, _ := req["userDir"].(assets.Key) + certHash, _ := req["certHash"].(string) + + asset, err := userDirRef.Get(stub) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Error reading user directory entry from blockchain", err.Status()) + } + + // Verify ownership + storedCertHash := asset.GetProp("certHash").(string) + if storedCertHash != certHash { + return nil, errors.NewCCError("Unauthorized: Certificate hash mismatch", 403) + } + + assetJSON, nerr := json.Marshal(asset) + if nerr != nil { + return nil, errors.WrapError(nil, "failed to encode asset to JSON format") + } + + return assetJSON, nil + }, +} diff --git a/samples/chaincode/confidential-escrow/chaincode/transactions/user_directory_test.go b/samples/chaincode/confidential-escrow/chaincode/transactions/user_directory_test.go new file mode 100644 index 000000000..d5737030f --- /dev/null +++ b/samples/chaincode/confidential-escrow/chaincode/transactions/user_directory_test.go @@ -0,0 +1,404 @@ +package transactions + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "testing" + + "github.com/hyperledger-labs/cc-tools/assets" + "github.com/hyperledger/fabric-private-chaincode/samples/chaincode/confidential-escrow/chaincode/testutils" +) + +// ============================================================================ +// CreateUserDir Tests +// ============================================================================ + +func TestCreateUserDir_Success(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Calculate public key hash + hash := sha256.Sum256([]byte(fixtures.BuyerPubKey)) + pubKeyHash := hex.EncodeToString(hash[:]) + + // Execute CreateUserDir + args := map[string]any{ + "publicKeyHash": pubKeyHash, + "walletUUID": fixtures.BuyerWalletUUID, + "certHash": fixtures.BuyerCertHash, + } + + response, err := CreateUserDir.Routine(wrapper.StubWrapper, args) + testutils.AssertNoError(t, err, "user directory creation should succeed") + + // Parse response + var createdUserDir map[string]any + if parseErr := json.Unmarshal(response, &createdUserDir); parseErr != nil { + t.Fatalf("Failed to parse user directory response: %v", parseErr) + } + + // Verify properties + testutils.AssertEqual(t, pubKeyHash, createdUserDir["publicKeyHash"], "publicKeyHash mismatch") + testutils.AssertEqual(t, fixtures.BuyerWalletUUID, createdUserDir["walletUUID"], "walletUUID mismatch") + testutils.AssertEqual(t, fixtures.BuyerCertHash, createdUserDir["certHash"], "certHash mismatch") + + // Verify the entry was saved to the mock ledger + userDirKey, exists := createdUserDir["@key"].(string) + if !exists { + t.Fatal("Expected user directory to have a @key field") + } + + _, exists = mockStub.State[userDirKey] + if !exists { + t.Errorf("Expected user directory to be saved with key '%s'", userDirKey) + } + + t.Log("✓ User directory created successfully with all expected properties") +} + +func TestCreateUserDir_DuplicatePublicKeyHash(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Calculate public key hash + hash := sha256.Sum256([]byte(fixtures.BuyerPubKey)) + pubKeyHash := hex.EncodeToString(hash[:]) + + // Setup: Create existing user directory + err := fixtures.CreateMockUserDir( + mockStub, + pubKeyHash, + fixtures.BuyerWalletUUID, + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create mock user directory: %v", err) + } + + // Attempt to create duplicate user directory with same publicKeyHash + args := map[string]any{ + "publicKeyHash": pubKeyHash, + "walletUUID": "different-wallet-uuid", + "certHash": fixtures.BuyerCertHash, + } + + _, txErr := CreateUserDir.Routine(wrapper.StubWrapper, args) + if txErr == nil { + t.Fatal("Expected error when creating duplicate user directory entry") + } + + // Verify error indicates duplicate key (500 is acceptable for this case) + if txErr.Status() != 409 && txErr.Status() != 400 && txErr.Status() != 500 { + t.Errorf("Expected conflict error (409), bad request (400), or internal error (500), got status: %d", txErr.Status()) + } + + t.Log("✓ Duplicate user directory creation correctly rejected") +} + +func TestCreateUserDir_MissingRequiredFields(t *testing.T) { + wrapper, _ := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + testCases := []struct { + name string + args map[string]any + shouldError bool + }{ + { + name: "Missing publicKeyHash", + args: map[string]any{ + "walletUUID": fixtures.BuyerWalletUUID, + "certHash": fixtures.BuyerCertHash, + }, + shouldError: false, // cc-tools will use empty string, which may be valid + }, + { + name: "Missing walletUUID", + args: map[string]any{ + "publicKeyHash": "some-hash", + "certHash": fixtures.BuyerCertHash, + }, + shouldError: false, // cc-tools will use empty string + }, + { + name: "Missing certHash", + args: map[string]any{ + "publicKeyHash": "some-hash", + "walletUUID": fixtures.BuyerWalletUUID, + }, + shouldError: true, // May error if certHash is validated + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := CreateUserDir.Routine(wrapper.StubWrapper, tc.args) + if tc.shouldError && err == nil { + t.Errorf("Expected error for %s, but got none", tc.name) + } else if !tc.shouldError && err != nil { + t.Logf("Note: %s returned error (this may be expected): %v", tc.name, err) + } + t.Logf("✓ %s test completed", tc.name) + }) + } +} + +func TestCreateUserDir_EmptyStringFields(t *testing.T) { + wrapper, _ := testutils.NewMockStubWrapper() + + // Test with empty string values + args := map[string]any{ + "publicKeyHash": "", + "walletUUID": "", + "certHash": "", + } + + _, err := CreateUserDir.Routine(wrapper.StubWrapper, args) + // Note: cc-tools may allow empty strings, so we just log the behavior + if err != nil { + t.Log("✓ User directory creation with empty fields was rejected (expected)") + } else { + t.Log("⚠ User directory creation with empty fields succeeded (cc-tools allows empty strings)") + } +} + +// ============================================================================ +// ReadUserDir Tests +// ============================================================================ +func TestReadUserDir_Success(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Calculate public key hash from the buyer's public key + hash := sha256.Sum256([]byte(fixtures.BuyerPubKey)) + pubKeyHash := hex.EncodeToString(hash[:]) + + // Setup: Create user directory + err := fixtures.CreateMockUserDir( + mockStub, + pubKeyHash, + fixtures.BuyerWalletUUID, + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create mock user directory: %v", err) + } + + // Create the asset key using cc-tools + userDirKeyMap := map[string]any{ + "@assetType": "userdir", + "publicKeyHash": pubKeyHash, + } + + t.Logf("DEBUG: userDirKeyMap = %+v", userDirKeyMap) // ADD THIS + + userDirKey, err := assets.NewKey(userDirKeyMap) + if err != nil { + t.Fatalf("Failed to create asset key: %v", err) + } + + // Execute ReadUserDir + args := map[string]any{ + "userDir": userDirKey, + "certHash": fixtures.BuyerCertHash, + } + + response, txErr := ReadUserDir.Routine(wrapper.StubWrapper, args) + + testutils.AssertNoError(t, txErr, "reading user directory should succeed") + + // Parse response + var userDir map[string]any + if parseErr := json.Unmarshal(response, &userDir); parseErr != nil { + t.Fatalf("Failed to parse user directory response: %v", parseErr) + } + + // Verify properties + testutils.AssertEqual(t, pubKeyHash, userDir["publicKeyHash"], "publicKeyHash mismatch") + testutils.AssertEqual(t, fixtures.BuyerWalletUUID, userDir["walletUUID"], "walletUUID mismatch") + testutils.AssertEqual(t, fixtures.BuyerCertHash, userDir["certHash"], "certHash mismatch") + + t.Log("✓ User directory read successfully with correct properties") +} + +func TestReadUserDir_CertificateHashMismatch(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup: Create user directory + err := fixtures.CreateMockUserDir( + mockStub, + fixtures.BuyerPubKeyHash, + fixtures.BuyerWalletUUID, + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create mock user directory: %v", err) + } + + // Attempt to read with wrong certificate hash + args := map[string]any{ + "userDir": "userdir:" + fixtures.BuyerPubKeyHash, + "certHash": "wrong-cert-hash", + } + + _, txErr := ReadUserDir.Routine(wrapper.StubWrapper, args) + if txErr == nil { + t.Fatal("Expected error when reading with mismatched certificate hash") + } + + // Verify error is unauthorized - check both status and message + errMsg := txErr.Error() + if txErr.Status() == 403 || errMsg == "Unauthorized: Certificate hash mismatch" { + t.Log("✓ Certificate hash mismatch correctly rejected with proper error") + } else { + t.Logf("⚠ Certificate hash mismatch rejected with status %d: %s", txErr.Status(), errMsg) + } +} + +func TestReadUserDir_NonExistentEntry(t *testing.T) { + wrapper, _ := testutils.NewMockStubWrapper() + + // Attempt to read non-existent user directory + args := map[string]any{ + "userDir": "userdir:non-existent-hash", + "certHash": "some-cert-hash", + } + + _, err := ReadUserDir.Routine(wrapper.StubWrapper, args) + if err == nil { + t.Fatal("Expected error when reading non-existent user directory") + } + + // Should return 404 or similar not found error + if err.Status() != 404 && err.Status() != 500 { + t.Logf("Warning: Expected 404 error, got status: %d", err.Status()) + } + + t.Log("✓ Non-existent user directory correctly rejected") +} + +func TestReadUserDir_MissingCertHash(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup: Create user directory + err := fixtures.CreateMockUserDir( + mockStub, + fixtures.BuyerPubKeyHash, + fixtures.BuyerWalletUUID, + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create mock user directory: %v", err) + } + + // Attempt to read without certHash + args := map[string]any{ + "userDir": "userdir:" + fixtures.BuyerPubKeyHash, + } + + _, txErr := ReadUserDir.Routine(wrapper.StubWrapper, args) + if txErr == nil { + t.Fatal("Expected error when reading without certificate hash") + } + + t.Log("✓ Missing certificate hash correctly rejected") +} + +func TestReadUserDir_EmptyCertHash(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup: Create user directory + err := fixtures.CreateMockUserDir( + mockStub, + fixtures.BuyerPubKeyHash, + fixtures.BuyerWalletUUID, + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create mock user directory: %v", err) + } + + // Attempt to read with empty certHash + args := map[string]any{ + "userDir": "userdir:" + fixtures.BuyerPubKeyHash, + "certHash": "", + } + + _, txErr := ReadUserDir.Routine(wrapper.StubWrapper, args) + if txErr == nil { + t.Fatal("Expected error when reading with empty certificate hash") + } + + // Should be unauthorized due to hash mismatch - check both status and behavior + if txErr.Status() == 403 { + t.Log("✓ Empty certificate hash correctly rejected with 403") + } else { + t.Logf("✓ Empty certificate hash correctly rejected with status %d", txErr.Status()) + } +} + +// ============================================================================ +// Integration Tests +// ============================================================================ + +func TestUserDirectoryIntegration_CreateWalletAndLookup(t *testing.T) { + wrapper, _ := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Step 1: Create a wallet (which should automatically create UserDirectory) + walletArgs := map[string]any{ + "walletId": fixtures.BuyerWalletID, + "ownerPubKey": fixtures.BuyerPubKey, + "ownerCertHash": fixtures.BuyerCertHash, + } + + walletResponse, err := CreateWallet.Routine(wrapper.StubWrapper, walletArgs) + testutils.AssertNoError(t, err, "wallet creation should succeed") + + // Parse wallet response to get UUID + var wallet map[string]any + if parseErr := json.Unmarshal(walletResponse, &wallet); parseErr != nil { + t.Fatalf("Failed to parse wallet response: %v", parseErr) + } + + walletKey := wallet["@key"].(string) + walletUUID := walletKey[7:] // Remove "wallet:" prefix + + // Step 2: Calculate the public key hash + hash := sha256.Sum256([]byte(fixtures.BuyerPubKey)) + pubKeyHash := hex.EncodeToString(hash[:]) + + // Step 3: Read the UserDirectory entry + userDirKey, err := assets.NewKey(map[string]any{ + "@assetType": "userdir", + "publicKeyHash": pubKeyHash, + }) + if err != nil { + t.Fatalf("Failed to create asset key: %v", err) + } + + userDirArgs := map[string]any{ + "userDir": userDirKey, + "certHash": fixtures.BuyerCertHash, + } + + userDirResponse, txErr := ReadUserDir.Routine(wrapper.StubWrapper, userDirArgs) + testutils.AssertNoError(t, txErr, "reading user directory should succeed") + + // Parse user directory response + var userDir map[string]any + if parseErr := json.Unmarshal(userDirResponse, &userDir); parseErr != nil { + t.Fatalf("Failed to parse user directory response: %v", parseErr) + } + + // Verify the walletUUID matches + testutils.AssertEqual(t, walletUUID, userDir["walletUUID"], "walletUUID should match between wallet and user directory") + testutils.AssertEqual(t, pubKeyHash, userDir["publicKeyHash"], "publicKeyHash should match") + testutils.AssertEqual(t, fixtures.BuyerCertHash, userDir["certHash"], "certHash should match") + + t.Log("✓ Integration test: Wallet creation and UserDirectory lookup successful") +} diff --git a/samples/chaincode/confidential-escrow/chaincode/transactions/wallet.go b/samples/chaincode/confidential-escrow/chaincode/transactions/wallet.go new file mode 100644 index 000000000..f6657660f --- /dev/null +++ b/samples/chaincode/confidential-escrow/chaincode/transactions/wallet.go @@ -0,0 +1,482 @@ +// This file implements wallet management operations for confidential digital asset accounts. +// It provides secure wallet creation, balance queries, and ownership verification using +// certificate-based authentication and public key hash lookups. +package transactions + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/hyperledger-labs/cc-tools/accesscontrol" + "github.com/hyperledger-labs/cc-tools/assets" + "github.com/hyperledger-labs/cc-tools/errors" + sw "github.com/hyperledger-labs/cc-tools/stubwrapper" + "github.com/hyperledger-labs/cc-tools/transactions" +) + +// CreateWallet initializes a new wallet for a user and registers it in the UserDirectory. +// This atomic operation creates both the wallet asset and its corresponding directory entry, +// enabling future wallet lookups by public key hash. +// +// Arguments: +// - walletId: User-defined identifier for the wallet +// - ownerPubKey: Public key of the wallet owner +// - ownerCertHash: Certificate hash for ownership verification +// +// Process Flow: +// 1. Create wallet with empty balance arrays +// 2. Compute SHA-256 hash of owner's public key +// 3. Extract wallet UUID from created asset +// 4. Create UserDirectory entry mapping public key hash to wallet UUID +// +// Returns: +// - JSON representation of the created wallet +// - Error if wallet creation fails or directory entry cannot be saved +// +// Security: The ownerCertHash is required for all subsequent wallet operations, +// ensuring only the legitimate owner can access or modify the wallet. +var CreateWallet = transactions.Transaction{ + Tag: "createWallet", + Label: "Wallet Creation", + Description: "Creates a new Wallet", + Method: "POST", + Callers: []accesscontrol.Caller{ + { + MSP: "Org1MSP", + OU: "admin", + }, { + MSP: "Org2MSP", + OU: "admin", + }, + }, + + Args: []transactions.Argument{ + { + Tag: "walletId", + Label: "Wallet ID", + Description: "ID of Wallet", + DataType: "string", + Required: true, + }, + { + Tag: "ownerPubKey", + Label: "Owner Public Key", + DataType: "string", + Required: true, + }, + { + Tag: "ownerCertHash", + Label: "Owner Certificate Hash", + Description: "Hash of Owner's Certificate who created this wallet", + DataType: "string", + Required: true, + }, + }, + + Routine: func(stub *sw.StubWrapper, req map[string]any) ([]byte, errors.ICCError) { + walletId, _ := req["walletId"].(string) + ownerPublicKey, _ := req["ownerPubKey"].(string) + ownerCertHash, _ := req["ownerCertHash"].(string) + + hash := sha256.Sum256([]byte(ownerPublicKey)) + pubKeyHash := hex.EncodeToString(hash[:]) + + walletMap := make(map[string]any) + walletMap["@assetType"] = "wallet" + walletMap["walletId"] = walletId + walletMap["ownerPubKey"] = ownerPublicKey + walletMap["ownerCertHash"] = ownerCertHash + walletMap["escrowBalances"] = make([]any, 0) + walletMap["balances"] = make([]any, 0) + walletMap["digitalAssetTypes"] = make([]any, 0) + walletMap["createdAt"] = time.Now() + + walletAsset, err := assets.NewAsset(walletMap) + if err != nil { + return nil, errors.WrapError(err, "Failed to create wallet asset") + } + + // _, err = walletAsset.PutNew(stub) + _, err = walletAsset.Put(stub) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Error saving wallet on blockchain", err.Status()) + } + + // Create corresponding UserDir entry + walletUUID := strings.Split(walletAsset.GetProp("@key").(string), ":")[1] + + userDirMap := make(map[string]any) + userDirMap["@assetType"] = "userdir" + userDirMap["publicKeyHash"] = pubKeyHash // Using certHash as identifier + userDirMap["walletUUID"] = walletUUID + userDirMap["certHash"] = ownerCertHash + + userDirAsset, err := assets.NewAsset(userDirMap) + if err != nil { + return nil, errors.WrapError(err, "Failed to create user directory") + } + + _, err = userDirAsset.PutNew(stub) + if err != nil { + return nil, errors.WrapError(err, "Failed to save user directory") + } + + assetJSON, nerr := json.Marshal(walletAsset) + if nerr != nil { + return nil, errors.WrapError(nil, "failed to encode wallet to JSON format") + } + + return assetJSON, nil + }, +} + +// GetBalance retrieves the available (non-escrowed) balance for a specific token in a wallet. +// This operation requires certificate-based authentication to prevent unauthorized balance queries. +// +// Arguments: +// - pubKey: Public key of the wallet owner +// - assetSymbol: Symbol of the digital asset to query (e.g., "USDT") +// - ownerCertHash: Certificate hash for ownership verification +// +// Process Flow: +// 1. Compute public key hash and lookup wallet UUID via UserDirectory +// 2. Retrieve wallet from ledger +// 3. Verify owner authorization via certificate hash +// 4. Iterate through wallet's asset types to find matching symbol +// 5. Return corresponding balance from parallel balances array +// +// Returns: +// - JSON response with wallet ID, asset symbol, and available balance +// - Error if wallet not found, unauthorized, or asset not held +// +// Note: This returns only the available balance, not the escrowed balance. +var GetBalance = transactions.Transaction{ + Tag: "getBalance", + Label: "Get Wallet Balance", + Description: "Get balance of a specific token in wallet with authentication", + Method: "GET", + Callers: []accesscontrol.Caller{ + { + MSP: "Org1MSP", + OU: "admin", + }, + { + MSP: "Org2MSP", + OU: "admin", + }, + }, + + Args: []transactions.Argument{ + { + Tag: "pubKey", + Label: "Public Key", + DataType: "string", + Required: true, + }, + { + Tag: "assetSymbol", + Label: "Asset Symbol", + Description: "Symbol of the digital asset to check balance for", + DataType: "string", + Required: true, + }, + { + Tag: "ownerCertHash", + Label: "Owner Certificate Hash", + Description: "Certificate hash for ownership verification", + DataType: "string", + Required: true, + }, + }, + + Routine: func(stub *sw.StubWrapper, req map[string]any) ([]byte, errors.ICCError) { + pubKey, _ := req["pubKey"].(string) + assetSymbol, _ := req["assetSymbol"].(string) + ownerCertHash, _ := req["ownerCertHash"].(string) + + // Lookup wallet using publicKeyHash property + hash := sha256.Sum256([]byte(pubKey)) + pubKeyHash := hex.EncodeToString(hash[:]) + + userDirKey, err := assets.NewKey(map[string]any{ + "@assetType": "userdir", + "publicKeyHash": pubKeyHash, + }) + if err != nil { + return nil, errors.NewCCError(fmt.Sprintf("Seller's Key cannot be found from user dir: %v", err), 404) + } + + userDir, err := userDirKey.Get(stub) + if err != nil { + return nil, errors.NewCCError("Buyer wallet not found. Buyer must create wallet first.", 404) + } + walletId := userDir.GetProp("walletUUID").(string) + + // Get wallet + key := assets.Key{ + "@key": "wallet:" + walletId, + } + + walletAsset, err := key.Get(stub) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Error reading wallet from blockchain", err.Status()) + } + + // Verify ownership + if walletAsset.GetProp("ownerCertHash").(string) != ownerCertHash { + return nil, errors.NewCCError("Unauthorized: Certificate hash mismatch", 403) + } + + // Find asset index + digitalAssetTypes := walletAsset.GetProp("digitalAssetTypes").([]any) + balances := walletAsset.GetProp("balances").([]any) + + for i, assetRef := range digitalAssetTypes { + // Get the referenced asset + var assetKey string + switch ref := assetRef.(type) { + case map[string]any: + assetKey = ref["@key"].(string) + case string: + assetKey = "digitalAsset:" + ref + } + + // Read the asset to get its symbol + refKey := assets.Key{"@key": assetKey} + asset, assetErr := refKey.Get(stub) + if assetErr != nil { + continue + } + + if asset.GetProp("symbol").(string) == assetSymbol { + balance := balances[i].(float64) + response := map[string]any{ + "walletId": walletId, + "assetSymbol": assetSymbol, + "balance": balance, + } + responseJSON, jsonErr := json.Marshal(response) + if jsonErr != nil { + return nil, errors.WrapError(nil, "failed to encode response to JSON format") + } + return responseJSON, nil + } + } + + return nil, errors.NewCCError("Asset not found in wallet", 404) + }, +} + +// GetEscrowBalance retrieves the locked (escrowed) balance for a specific token in a wallet. +// Escrowed tokens are temporarily unavailable for spending while locked in active escrow contracts. +// +// Arguments: +// - pubKey: Public key of the wallet owner +// - assetSymbol: Symbol of the digital asset to query +// - ownerCertHash: Certificate hash for ownership verification +// +// Process Flow: +// 1. Resolve wallet UUID from public key hash +// 2. Retrieve wallet and verify ownership +// 3. Find asset index by matching symbol +// 4. Return corresponding escrow balance +// +// Returns: +// - JSON response with wallet ID, asset symbol, and escrowed balance +// - Error if wallet not found, unauthorized, or asset not held +// +// Use Cases: +// - Verify sufficient funds are locked before attempting escrow release +// - Display total wallet balance (available + escrowed) in user interfaces +// - Audit escrow participation for compliance reporting +var GetEscrowBalance = transactions.Transaction{ + Tag: "getEscrowBalance", + Label: "Get Wallet Escrow Balance", + Description: "Get escrowed balance of a specific token in wallet", + Method: "GET", + Callers: []accesscontrol.Caller{ + {MSP: "Org1MSP", OU: "admin"}, + {MSP: "Org2MSP", OU: "admin"}, + }, + Args: []transactions.Argument{ + // {Tag: "walletUUID", DataType: "string", Required: true}, + {Tag: "pubKey", Label: "Public Key", DataType: "string", Required: true}, + {Tag: "assetSymbol", DataType: "string", Required: true}, + {Tag: "ownerCertHash", DataType: "string", Required: true}, + }, + Routine: func(stub *sw.StubWrapper, req map[string]any) ([]byte, errors.ICCError) { + pubKey, _ := req["pubKey"].(string) + assetSymbol, _ := req["assetSymbol"].(string) + ownerCertHash, _ := req["ownerCertHash"].(string) + + // Lookup wallet using publicKeyHash property + hash := sha256.Sum256([]byte(pubKey)) + pubKeyHash := hex.EncodeToString(hash[:]) + + userDirKey, err := assets.NewKey(map[string]any{ + "@assetType": "userdir", + "publicKeyHash": pubKeyHash, + }) + if err != nil { + return nil, errors.NewCCError(fmt.Sprintf("Seller's Key cannot be found from user dir: %v", err), 404) + } + + userDir, err := userDirKey.Get(stub) + if err != nil { + return nil, errors.NewCCError("Buyer wallet not found. Buyer must create wallet first.", 404) + } + walletId := userDir.GetProp("walletUUID").(string) + + // Get wallet + key := assets.Key{ + "@key": "wallet:" + walletId, + } + + walletAsset, err := key.Get(stub) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Error reading wallet from blockchain", err.Status()) + } + + // Verify ownership + if walletAsset.GetProp("ownerCertHash").(string) != ownerCertHash { + return nil, errors.NewCCError("Unauthorized: Certificate hash mismatch", 403) + } + + // Find asset index + digitalAssetTypes := walletAsset.GetProp("digitalAssetTypes").([]any) + escrowBalances := walletAsset.GetProp("escrowBalances").([]any) + + for i, assetRef := range digitalAssetTypes { + // Get the referenced asset + var assetKey string + switch ref := assetRef.(type) { + case map[string]any: + assetKey = ref["@key"].(string) + case string: + assetKey = "digitalAsset:" + ref + } + + // Read the asset to get its symbol + refKey := assets.Key{"@key": assetKey} + asset, assetErr := refKey.Get(stub) + if assetErr != nil { + continue + } + + if asset.GetProp("symbol").(string) == assetSymbol { + escrowBalance := escrowBalances[i].(float64) + response := map[string]any{ + "walletId": walletId, + "assetSymbol": assetSymbol, + "escrowBalance": escrowBalance, + } + responseJSON, jsonErr := json.Marshal(response) + if jsonErr != nil { + return nil, errors.WrapError(nil, "failed to encode response to JSON format") + } + return responseJSON, nil + } + } + + return nil, errors.NewCCError("Asset not found in wallet", 404) + }, +} + +// GetWalletByOwner retrieves complete wallet details using the owner's public key. +// This operation returns the full wallet state including all balances, escrow balances, +// and asset types held, subject to ownership verification. +// +// Arguments: +// - pubKey: Public key of the wallet owner +// - ownerCertHash: Certificate hash for ownership verification +// +// Process Flow: +// 1. Compute public key hash and lookup wallet UUID via UserDirectory +// 2. Retrieve complete wallet asset from ledger +// 3. Verify owner authorization via certificate hash +// 4. Return full wallet JSON representation +// +// Returns: +// - JSON representation of the complete wallet state +// - Error if wallet not found or authorization fails +// +// Security: Certificate verification ensures only the wallet owner can view +// their complete wallet details, maintaining transaction privacy. +var GetWalletByOwner = transactions.Transaction{ + Tag: "getWalletByOwner", + Label: "Get Wallet By Owner", + Description: "Find wallet by providing wallet UUID directly", + Method: "GET", + Callers: []accesscontrol.Caller{ + { + MSP: "Org1MSP", + OU: "admin", + }, + { + MSP: "Org2MSP", + OU: "admin", + }, + }, + + Args: []transactions.Argument{ + { + Tag: "pubKey", + Label: "Public Key", + DataType: "string", + Required: true, + }, + { + Tag: "ownerCertHash", + Label: "Owner Certificate Hash", + Description: "Certificate hash for authentication", + DataType: "string", + Required: true, + }, + }, + + Routine: func(stub *sw.StubWrapper, req map[string]any) ([]byte, errors.ICCError) { + pubKey, _ := req["pubKey"].(string) + ownerCertHash, _ := req["ownerCertHash"].(string) + + // Lookup wallet using publicKeyHash property + hash := sha256.Sum256([]byte(pubKey)) + pubKeyHash := hex.EncodeToString(hash[:]) + + userDirKey, err := assets.NewKey(map[string]any{ + "@assetType": "userdir", + "publicKeyHash": pubKeyHash, + }) + if err != nil { + return nil, errors.NewCCError(fmt.Sprintf("Seller's Key cannot be found from user dir: %v", err), 404) + } + + userDir, err := userDirKey.Get(stub) + if err != nil { + return nil, errors.NewCCError("Buyer wallet not found. Buyer must create wallet first.", 404) + } + walletUuid := userDir.GetProp("walletUUID").(string) + + // Get wallet directly + walletKey := assets.Key{"@key": "wallet:" + walletUuid} + wallet, err := walletKey.Get(stub) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "Wallet not found", 404) + } + + // Verify ownership + if wallet.GetProp("ownerCertHash").(string) != ownerCertHash { + return nil, errors.NewCCError("Unauthorized: Certificate hash mismatch", 403) + } + + responseJSON, jsonErr := json.Marshal(wallet) + if jsonErr != nil { + return nil, errors.WrapError(nil, "failed to encode wallet to JSON format") + } + + return responseJSON, nil + }, +} diff --git a/samples/chaincode/confidential-escrow/chaincode/transactions/wallet_test.go b/samples/chaincode/confidential-escrow/chaincode/transactions/wallet_test.go new file mode 100644 index 000000000..3bcbb1dad --- /dev/null +++ b/samples/chaincode/confidential-escrow/chaincode/transactions/wallet_test.go @@ -0,0 +1,672 @@ +package transactions + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "strings" + "testing" + + "github.com/hyperledger-labs/cc-tools/assets" + "github.com/hyperledger-labs/cc-tools/errors" + asset "github.com/hyperledger/fabric-private-chaincode/samples/chaincode/confidential-escrow/chaincode/assets" + "github.com/hyperledger/fabric-private-chaincode/samples/chaincode/confidential-escrow/chaincode/testutils" +) + +// TestMain runs before all tests to initialize cc-tools +func TestMain(m *testing.M) { + // Initialize asset types + assetTypeList := []assets.AssetType{ + asset.Wallet, + asset.DigitalAssetToken, + asset.UserDirectory, + asset.Escrow, + } + assets.InitAssetList(assetTypeList) + + // Run tests + m.Run() +} + +func asICCError(err errors.ICCError) errors.ICCError { + return err +} + +// ============================================================================ +// CreateWallet Tests +// ============================================================================ + +func TestCreateWallet_Success(t *testing.T) { + // Create a fresh mock blockchain stub for this test + wrapper, mockStub := testutils.NewMockStubWrapper() + + // Define the input arguments for creating a wallet + args := map[string]any{ + "walletId": "alice-savings", // User-friendly nickname + "ownerPubKey": "alice-public-key-12345", // Alice's public key + "ownerCertHash": "alice-cert-hash-xyz", // Alice's certificate hash + } + + // Execute the CreateWallet transaction + response, err := CreateWallet.Routine(wrapper.StubWrapper, args) + testutils.AssertNoError(t, err, "wallet creation should succeed") + + // Parse the response to check wallet properties + var createdWallet map[string]any + if err := json.Unmarshal(response, &createdWallet); err != nil { + t.Fatalf("Failed to parse wallet response: %v", err) + } + + // Verify the nickname was stored correctly + testutils.AssertEqual(t, "alice-savings", createdWallet["walletId"], "walletId mismatch") + + // Verify the public key was stored correctly + testutils.AssertEqual(t, "alice-public-key-12345", createdWallet["ownerPubKey"], "owner Pub key mismatch") + + // Verify the certificate hash was stored correctly + testutils.AssertEqual(t, "alice-cert-hash-xyz", createdWallet["ownerCertHash"], "cert hash mismatch") + + // Verify that a UUID was generated (in the @key field) + walletKey, exists := createdWallet["@key"].(string) + if !exists { + t.Fatal("Expected wallet to have a @key field with UUID") + } + + // The key should be in format "wallet:" + if len(walletKey) < 8 || walletKey[:7] != "wallet:" { + t.Errorf("Expected key format 'wallet:', got: %s", walletKey) + } + + // Extract the UUID portion (everything after "wallet:") + walletUUID := walletKey[7:] + + // Verify the wallet was actually saved to the mock ledger + _, exists = mockStub.State[walletKey] + if !exists { + t.Errorf("Expected wallet to be saved with key '%s'", walletKey) + } + + var userDirBytes []byte + var userDirKey string + for key, value := range mockStub.State { + if strings.HasPrefix(key, "userdir:") { + userDirKey = key + userDirBytes = value + break + } + } + + if userDirBytes == nil { + t.Fatalf("Expected UserDirectory entry to exist: %v", userDirKey) + } + + var userDir map[string]any + if err := json.Unmarshal(userDirBytes, &userDir); err != nil { + t.Fatalf("Failed to parse UserDirectory: %v", err) + } + + // Verify it has the correct publicKeyHash property + hash := sha256.Sum256([]byte("alice-public-key-12345")) + expectedPubKeyHash := hex.EncodeToString(hash[:]) + testutils.AssertEqual(t, expectedPubKeyHash, userDir["publicKeyHash"], "publicKeyHash mismatch") + + testutils.AssertEqual(t, walletUUID, userDir["walletUUID"], "UserDir has different walletUUID") + + // Verify UserDirectory contains the correct wallet UUID + if err := json.Unmarshal(userDirBytes, &userDir); err != nil { + t.Fatalf("Failed to parse UserDirectory: %v", err) + } + + testutils.AssertEqual(t, walletUUID, userDir["walletUUID"], "UserDir has different walletUUID") + + // Verify empty balance arrays were initialized + balances, ok := createdWallet["balances"].([]any) + if !ok || len(balances) != 0 { + t.Errorf("Expected empty balances array, got: %v", createdWallet["balances"]) + } + + escrowBalances, ok := createdWallet["escrowBalances"].([]any) + if !ok || len(escrowBalances) != 0 { + t.Errorf("Expected empty escrowBalances array, got: %v", createdWallet["escrowBalances"]) + } + + t.Log("✓ Wallet created successfully with all expected properties") +} + +// ============================================================================ +// GetBalance Tests +// ============================================================================ + +func TestGetBalance_Success(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup: Create digital asset + err := fixtures.CreateMockDigitalAsset( + mockStub, + fixtures.AssetID, + fixtures.AssetSymbol, + fixtures.AssetName, + fixtures.IssuerCertHash, + 1000.0, + ) + if err != nil { + t.Fatalf("Failed to create mock digital asset: %v", err) + } + + // Setup: Create buyer wallet with balance + err = fixtures.CreateMockWallet( + mockStub, + fixtures.BuyerPubKey, + fixtures.BuyerCertHash, + fixtures.BuyerWalletID, + fixtures.BuyerWalletUUID, + fixtures.AssetID, + 150.0, // balance + 50.0, // escrow balance + ) + if err != nil { + t.Fatalf("Failed to create mock wallet: %v", err) + } + + // Setup: Create user directory + err = fixtures.CreateMockUserDir( + mockStub, + fixtures.BuyerPubKeyHash, + fixtures.BuyerWalletUUID, + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create mock user directory: %v", err) + } + + // t.Log("Mock state keys:") + // for key := range mockStub.State { + // t.Logf(" %q", key) + // } + + // Execute GetBalance + args := map[string]any{ + "pubKey": fixtures.BuyerPubKey, + "assetSymbol": fixtures.AssetSymbol, + "ownerCertHash": fixtures.BuyerCertHash, + } + + response, txErr := GetBalance.Routine(wrapper.StubWrapper, args) + if txErr != nil { + t.Fatalf("GetBalance should succeed: Expected no error, got: %v", txErr) + } + + // Verify response + var result map[string]any + if err := json.Unmarshal(response, &result); err != nil { + t.Fatalf("Failed to parse response: %v", err) + } + + testutils.AssertEqual(t, fixtures.BuyerWalletUUID, result["walletId"], "walletId mismatch") + testutils.AssertEqual(t, fixtures.AssetSymbol, result["assetSymbol"], "assetSymbol mismatch") + testutils.AssertEqual(t, 150.0, result["balance"], "balance mismatch") + + t.Log("✓ GetBalance returned correct balance") +} + +func TestGetBalance_WalletNotFound(t *testing.T) { + wrapper, _ := testutils.NewMockStubWrapper() + + args := map[string]any{ + "pubKey": "non-existent-pubkey", + "assetSymbol": "TST", + "ownerCertHash": "some-cert-hash", + } + + _, err := GetBalance.Routine(wrapper.StubWrapper, args) + testutils.AssertError(t, err, "should fail when wallet not found") + testutils.AssertErrorStatus(t, err, 404, "should return 404 status") +} + +func TestGetBalance_UnauthorizedAccess(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup wallet and user directory + err := fixtures.CreateMockDigitalAsset( + mockStub, + fixtures.AssetID, + fixtures.AssetSymbol, + fixtures.AssetName, + fixtures.IssuerCertHash, + 1000.0, + ) + if err != nil { + t.Fatalf("Failed to create mock digital asset: %v", err) + } + + err = fixtures.CreateMockWallet( + mockStub, + fixtures.BuyerPubKey, + fixtures.BuyerCertHash, + fixtures.BuyerWalletID, + fixtures.BuyerWalletUUID, + fixtures.AssetID, + 150.0, + 50.0, + ) + if err != nil { + t.Fatalf("Failed to create mock wallet: %v", err) + } + + err = fixtures.CreateMockUserDir( + mockStub, + fixtures.BuyerPubKeyHash, + fixtures.BuyerWalletUUID, + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create mock user directory: %v", err) + } + + // Try to access with wrong certificate hash + args := map[string]any{ + "pubKey": fixtures.BuyerPubKey, + "assetSymbol": fixtures.AssetSymbol, + "ownerCertHash": "wrong-cert-hash", // Wrong certificate + } + + _, txErr := GetBalance.Routine(wrapper.StubWrapper, args) + testutils.AssertError(t, txErr, "should fail with wrong certificate") + testutils.AssertErrorStatus(t, txErr, 403, "should return 403 Unauthorized") +} + +func TestGetBalance_AssetNotInWallet(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup wallet with one asset + err := fixtures.CreateMockDigitalAsset( + mockStub, + fixtures.AssetID, + fixtures.AssetSymbol, + fixtures.AssetName, + fixtures.IssuerCertHash, + 1000.0, + ) + if err != nil { + t.Fatalf("Failed to create mock digital asset: %v", err) + } + + err = fixtures.CreateMockWallet( + mockStub, + fixtures.BuyerPubKey, + fixtures.BuyerCertHash, + fixtures.BuyerWalletID, + fixtures.BuyerWalletUUID, + fixtures.AssetID, + 150.0, + 50.0, + ) + if err != nil { + t.Fatalf("Failed to create mock wallet: %v", err) + } + + err = fixtures.CreateMockUserDir( + mockStub, + fixtures.BuyerPubKeyHash, + fixtures.BuyerWalletUUID, + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create mock user directory: %v", err) + } + + // Query for a different asset + args := map[string]any{ + "pubKey": fixtures.BuyerPubKey, + "assetSymbol": "NOTFOUND", // Asset not in wallet + "ownerCertHash": fixtures.BuyerCertHash, + } + + _, txErr := GetBalance.Routine(wrapper.StubWrapper, args) + testutils.AssertError(t, txErr, "should fail when asset not found in wallet") + testutils.AssertErrorStatus(t, txErr, 404, "should return 404 status") +} + +// ============================================================================ +// GetEscrowBalance Tests +// ============================================================================ + +func TestGetEscrowBalance_Success(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup: Create digital asset + err := fixtures.CreateMockDigitalAsset( + mockStub, + fixtures.AssetID, + fixtures.AssetSymbol, + fixtures.AssetName, + fixtures.IssuerCertHash, + 1000.0, + ) + if err != nil { + t.Fatalf("Failed to create mock digital asset: %v", err) + } + + // Setup: Create buyer wallet with escrow balance + err = fixtures.CreateMockWallet( + mockStub, + fixtures.BuyerPubKey, + fixtures.BuyerCertHash, + fixtures.BuyerWalletID, + fixtures.BuyerWalletUUID, + fixtures.AssetID, + 150.0, // balance + 75.0, // escrow balance + ) + if err != nil { + t.Fatalf("Failed to create mock wallet: %v", err) + } + + // Setup: Create user directory + err = fixtures.CreateMockUserDir( + mockStub, + fixtures.BuyerPubKeyHash, + fixtures.BuyerWalletUUID, + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create mock user directory: %v", err) + } + + // Execute GetEscrowBalance + args := map[string]any{ + "pubKey": fixtures.BuyerPubKey, + "assetSymbol": fixtures.AssetSymbol, + "ownerCertHash": fixtures.BuyerCertHash, + } + + response, txErr := GetEscrowBalance.Routine(wrapper.StubWrapper, args) + testutils.AssertNoError(t, txErr, "GetEscrowBalance should succeed") + + // Verify response + var result map[string]any + if err := json.Unmarshal(response, &result); err != nil { + t.Fatalf("Failed to parse response: %v", err) + } + + testutils.AssertEqual(t, fixtures.BuyerWalletUUID, result["walletId"], "walletId mismatch") + testutils.AssertEqual(t, fixtures.AssetSymbol, result["assetSymbol"], "assetSymbol mismatch") + testutils.AssertEqual(t, 75.0, result["escrowBalance"], "escrowBalance mismatch") + + t.Log("✓ GetEscrowBalance returned correct escrow balance") +} + +func TestGetEscrowBalance_WalletNotFound(t *testing.T) { + wrapper, _ := testutils.NewMockStubWrapper() + + args := map[string]any{ + "pubKey": "non-existent-pubkey", + "assetSymbol": "TST", + "ownerCertHash": "some-cert-hash", + } + + _, err := GetEscrowBalance.Routine(wrapper.StubWrapper, args) + testutils.AssertError(t, err, "should fail when wallet not found") + testutils.AssertErrorStatus(t, err, 404, "should return 404 status") +} + +func TestGetEscrowBalance_UnauthorizedAccess(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup + err := fixtures.CreateMockDigitalAsset( + mockStub, + fixtures.AssetID, + fixtures.AssetSymbol, + fixtures.AssetName, + fixtures.IssuerCertHash, + 1000.0, + ) + if err != nil { + t.Fatalf("Failed to create mock digital asset: %v", err) + } + + err = fixtures.CreateMockWallet( + mockStub, + fixtures.BuyerPubKey, + fixtures.BuyerCertHash, + fixtures.BuyerWalletID, + fixtures.BuyerWalletUUID, + fixtures.AssetID, + 150.0, + 75.0, + ) + if err != nil { + t.Fatalf("Failed to create mock wallet: %v", err) + } + + err = fixtures.CreateMockUserDir( + mockStub, + fixtures.BuyerPubKeyHash, + fixtures.BuyerWalletUUID, + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create mock user directory: %v", err) + } + + // Try to access with wrong certificate hash + args := map[string]any{ + "pubKey": fixtures.BuyerPubKey, + "assetSymbol": fixtures.AssetSymbol, + "ownerCertHash": "wrong-cert-hash", + } + + _, txErr := GetEscrowBalance.Routine(wrapper.StubWrapper, args) + testutils.AssertError(t, txErr, "should fail with wrong certificate") + testutils.AssertErrorStatus(t, txErr, 403, "should return 403 Unauthorized") +} + +func TestGetEscrowBalance_AssetNotInWallet(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup + err := fixtures.CreateMockDigitalAsset( + mockStub, + fixtures.AssetID, + fixtures.AssetSymbol, + fixtures.AssetName, + fixtures.IssuerCertHash, + 1000.0, + ) + if err != nil { + t.Fatalf("Failed to create mock digital asset: %v", err) + } + + err = fixtures.CreateMockWallet( + mockStub, + fixtures.BuyerPubKey, + fixtures.BuyerCertHash, + fixtures.BuyerWalletID, + fixtures.BuyerWalletUUID, + fixtures.AssetID, + 150.0, + 75.0, + ) + if err != nil { + t.Fatalf("Failed to create mock wallet: %v", err) + } + + err = fixtures.CreateMockUserDir( + mockStub, + fixtures.BuyerPubKeyHash, + fixtures.BuyerWalletUUID, + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create mock user directory: %v", err) + } + + // Query for non-existent asset + args := map[string]any{ + "pubKey": fixtures.BuyerPubKey, + "assetSymbol": "NOTFOUND", + "ownerCertHash": fixtures.BuyerCertHash, + } + + _, txErr := GetEscrowBalance.Routine(wrapper.StubWrapper, args) + testutils.AssertError(t, txErr, "should fail when asset not found") + testutils.AssertErrorStatus(t, txErr, 404, "should return 404 status") +} + +// ============================================================================ +// GetWalletByOwner Tests +// ============================================================================ + +func TestGetWalletByOwner_Success(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup: Create digital asset + err := fixtures.CreateMockDigitalAsset( + mockStub, + fixtures.AssetID, + fixtures.AssetSymbol, + fixtures.AssetName, + fixtures.IssuerCertHash, + 1000.0, + ) + if err != nil { + t.Fatalf("Failed to create mock digital asset: %v", err) + } + + // Setup: Create buyer wallet + err = fixtures.CreateMockWallet( + mockStub, + fixtures.BuyerPubKey, + fixtures.BuyerCertHash, + fixtures.BuyerWalletID, + fixtures.BuyerWalletUUID, + fixtures.AssetID, + 150.0, + 50.0, + ) + if err != nil { + t.Fatalf("Failed to create mock wallet: %v", err) + } + + // Setup: Create user directory + err = fixtures.CreateMockUserDir( + mockStub, + fixtures.BuyerPubKeyHash, + fixtures.BuyerWalletUUID, + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create mock user directory: %v", err) + } + + // Execute GetWalletByOwner + args := map[string]any{ + "pubKey": fixtures.BuyerPubKey, + "ownerCertHash": fixtures.BuyerCertHash, + } + + response, txErr := GetWalletByOwner.Routine(wrapper.StubWrapper, args) + testutils.AssertNoError(t, txErr, "GetWalletByOwner should succeed") + + // Verify response contains complete wallet + var wallet map[string]any + if err := json.Unmarshal(response, &wallet); err != nil { + t.Fatalf("Failed to parse wallet: %v", err) + } + + testutils.AssertEqual(t, fixtures.BuyerWalletID, wallet["walletId"], "walletId mismatch") + testutils.AssertEqual(t, fixtures.BuyerPubKey, wallet["ownerPubKey"], "ownerPubKey mismatch") + testutils.AssertEqual(t, fixtures.BuyerCertHash, wallet["ownerCertHash"], "ownerCertHash mismatch") + + // Verify balances exist + balances, ok := wallet["balances"].([]any) + if !ok { + t.Fatal("Expected balances array") + } + if len(balances) != 1 { + t.Errorf("Expected 1 balance entry, got %d", len(balances)) + } + + escrowBalances, ok := wallet["escrowBalances"].([]any) + if !ok { + t.Fatal("Expected escrowBalances array") + } + if len(escrowBalances) != 1 { + t.Errorf("Expected 1 escrow balance entry, got %d", len(escrowBalances)) + } + + t.Log("✓ GetWalletByOwner returned complete wallet") +} + +func TestGetWalletByOwner_WalletNotFound(t *testing.T) { + wrapper, _ := testutils.NewMockStubWrapper() + + args := map[string]any{ + "pubKey": "non-existent-pubkey", + "ownerCertHash": "some-cert-hash", + } + + _, err := GetWalletByOwner.Routine(wrapper.StubWrapper, args) + testutils.AssertError(t, err, "should fail when wallet not found") + testutils.AssertErrorStatus(t, err, 404, "should return 404 status") +} + +func TestGetWalletByOwner_UnauthorizedAccess(t *testing.T) { + wrapper, mockStub := testutils.NewMockStubWrapper() + fixtures := testutils.NewTestFixtures() + + // Setup + err := fixtures.CreateMockDigitalAsset( + mockStub, + fixtures.AssetID, + fixtures.AssetSymbol, + fixtures.AssetName, + fixtures.IssuerCertHash, + 1000.0, + ) + if err != nil { + t.Fatalf("Failed to create mock digital asset: %v", err) + } + + err = fixtures.CreateMockWallet( + mockStub, + fixtures.BuyerPubKey, + fixtures.BuyerCertHash, + fixtures.BuyerWalletID, + fixtures.BuyerWalletUUID, + fixtures.AssetID, + 150.0, + 50.0, + ) + if err != nil { + t.Fatalf("Failed to create mock wallet: %v", err) + } + + err = fixtures.CreateMockUserDir( + mockStub, + fixtures.BuyerPubKeyHash, + fixtures.BuyerWalletUUID, + fixtures.BuyerCertHash, + ) + if err != nil { + t.Fatalf("Failed to create mock user directory: %v", err) + } + + // Try to access with wrong certificate + args := map[string]any{ + "pubKey": fixtures.BuyerPubKey, + "ownerCertHash": "wrong-cert-hash", + } + + _, txErr := GetWalletByOwner.Routine(wrapper.StubWrapper, args) + testutils.AssertError(t, txErr, "should fail with wrong certificate") + testutils.AssertErrorStatus(t, txErr, 403, "should return 403 Unauthorized") +} diff --git a/samples/chaincode/confidential-escrow/chaincode/utils.go b/samples/chaincode/confidential-escrow/chaincode/utils.go new file mode 100644 index 000000000..4b1cfd4ef --- /dev/null +++ b/samples/chaincode/confidential-escrow/chaincode/utils.go @@ -0,0 +1,14 @@ +package chaincode + +import ( + "fmt" + "os" +) + +// GenerateCollection handles collection generation (if needed) +func GenerateCollection(orgs []string) { + fmt.Println("Collection generation called with orgs:", orgs) + fmt.Println("Collection generation not implemented yet") + // Exit after generating (like in cc-tools-demo) + os.Exit(0) +} diff --git a/samples/chaincode/confidential-escrow/confidential-escrow-compose.yaml b/samples/chaincode/confidential-escrow/confidential-escrow-compose.yaml new file mode 100644 index 000000000..e1904ab4c --- /dev/null +++ b/samples/chaincode/confidential-escrow/confidential-escrow-compose.yaml @@ -0,0 +1,12 @@ +services: + # org1 + ecc.peer0.org1.example.com: + environment: + - RUN_CCAAS=true + - FPC_ENABLED=true + + # org2 + ecc.peer0.org2.example.com: + environment: + - RUN_CCAAS=true + - FPC_ENABLED=true diff --git a/samples/chaincode/confidential-escrow/confidentialEscrowEnclave.json b/samples/chaincode/confidential-escrow/confidentialEscrowEnclave.json new file mode 100644 index 000000000..1ca911b98 --- /dev/null +++ b/samples/chaincode/confidential-escrow/confidentialEscrowEnclave.json @@ -0,0 +1,32 @@ +{ + "exe": "ecc", + "key": "private.pem", + "debug": true, + "heapSize": 512, + "productID": 1, + "securityVersion": 1, + "mounts": null, + "files": null, + "env": [ + { + "name": "CHAINCODE_SERVER_ADDRESS", + "fromHost": true + }, + { + "name": "CHAINCODE_PKG_ID", + "fromHost": true + }, + { + "name": "FPC_ENABLED", + "fromHost": true + }, + { + "name": "RUN_CCAAS", + "fromHost": true + }, + { + "name": "FABRIC_LOGGING_SPEC", + "fromHost": true + } + ] +} diff --git a/samples/chaincode/confidential-escrow/go.mod b/samples/chaincode/confidential-escrow/go.mod new file mode 100644 index 000000000..e64228a2d --- /dev/null +++ b/samples/chaincode/confidential-escrow/go.mod @@ -0,0 +1,35 @@ +module github.com/hyperledger/fabric-private-chaincode/samples/chaincode/confidential-escrow + +go 1.24.2 + +require ( + github.com/hyperledger-labs/cc-tools v1.0.2 + github.com/hyperledger/fabric-chaincode-go v0.0.0-20230228194215-b84622ba6a7a + github.com/hyperledger/fabric-private-chaincode v0.0.0-00010101000000-000000000000 + github.com/hyperledger/fabric-protos-go v0.3.0 +) + +require ( + github.com/Shopify/sarama v0.0.0-00010101000000-000000000000 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/uuid v1.3.1 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/hyperledger/fabric v2.1.1+incompatible // indirect + github.com/miekg/pkcs11 v1.1.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/stretchr/testify v1.10.0 // indirect + github.com/sykesm/zap-logfmt v0.0.4 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.25.0 // indirect + golang.org/x/crypto v0.38.0 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect + google.golang.org/grpc v1.59.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect +) + +replace github.com/hyperledger/fabric-private-chaincode => ../../../ + +replace github.com/Shopify/sarama => github.com/IBM/sarama v1.45.2 diff --git a/samples/chaincode/confidential-escrow/go.sum b/samples/chaincode/confidential-escrow/go.sum new file mode 100644 index 000000000..7f6192a75 --- /dev/null +++ b/samples/chaincode/confidential-escrow/go.sum @@ -0,0 +1,164 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/IBM/sarama v1.45.2 h1:8m8LcMCu3REcwpa7fCP6v2fuPuzVwXDAM2DOv3CBrKw= +github.com/IBM/sarama v1.45.2/go.mod h1:ppaoTcVdGv186/z6MEKsMm70A5fwJfRTpstI37kVn3Y= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= +github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA= +github.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= +github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws= +github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0= +github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hyperledger-labs/cc-tools v1.0.2 h1:PqQr06BMT/82B7DH5JrHko9C9hejLXHE1rgSpb53Mok= +github.com/hyperledger-labs/cc-tools v1.0.2/go.mod h1:NQyK1wndA/L5EeKqzhLlLGrsfSQJbsvjxbaFiaE6XCI= +github.com/hyperledger/fabric v2.1.1+incompatible h1:cYYRv3vVg4kA6DmrixLxwn1nwBEUuYda8DsMwlaMKbY= +github.com/hyperledger/fabric v2.1.1+incompatible/go.mod h1:tGFAOCT696D3rG0Vofd2dyWYLySHlh0aQjf7Q1HAju0= +github.com/hyperledger/fabric-amcl v0.0.0-20230602173724-9e02669dceb2 h1:B1Nt8hKb//KvgGRprk0h1t4lCnwhE9/ryb1WqfZbV+M= +github.com/hyperledger/fabric-amcl v0.0.0-20230602173724-9e02669dceb2/go.mod h1:X+DIyUsaTmalOpmpQfIvFZjKHQedrURQ5t4YqquX7lE= +github.com/hyperledger/fabric-chaincode-go v0.0.0-20230228194215-b84622ba6a7a h1:HwSCxEeiBthwcazcAykGATQ36oG9M+HEQvGLvB7aLvA= +github.com/hyperledger/fabric-chaincode-go v0.0.0-20230228194215-b84622ba6a7a/go.mod h1:TDSu9gxURldEnaGSFbH1eMlfSQBWQcMQfnDBcpQv5lU= +github.com/hyperledger/fabric-protos-go v0.3.0 h1:MXxy44WTMENOh5TI8+PCK2x6pMj47Go2vFRKDHB2PZs= +github.com/hyperledger/fabric-protos-go v0.3.0/go.mod h1:WWnyWP40P2roPmmvxsUXSvVI/CF6vwY1K1UFidnKBys= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +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/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs= +github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/gomega v1.27.8 h1:gegWiwZjBsf2DgiSbf5hpokZ98JVDMcWkUiigk6/KXc= +github.com/onsi/gomega v1.27.8/go.mod h1:2J8vzI/s+2shY9XHRApDkdgPo1TKT7P2u6fXeJKFnNQ= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= +github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= +github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.10.1 h1:nuJZuYpG7gTj/XqiUwg8bA0cp1+M2mC3J4g5luUYBKk= +github.com/spf13/viper v1.10.1/go.mod h1:IGlFPqhNAPKRxohIzWpI5QEy4kuI7tcl5WvR+8qy1rU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/sykesm/zap-logfmt v0.0.4 h1:U2WzRvmIWG1wDLCFY3sz8UeEmsdHQjHFNlIdmroVFaI= +github.com/sykesm/zap-logfmt v0.0.4/go.mod h1:AuBd9xQjAe3URrWT1BBDk2v2onAZHkZkWRMiYZXiZWA= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= +go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.12.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c= +go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b h1:ZlWIi1wSK56/8hn4QcBp/j9M7Gt3U/3hZw3mC7vDICo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:swOH3j0KzcDDgGUWr+SNpyTen5YrXjS3eyPzFYKc6lc= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +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/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/samples/chaincode/confidential-escrow/main.go b/samples/chaincode/confidential-escrow/main.go new file mode 100644 index 000000000..11167313c --- /dev/null +++ b/samples/chaincode/confidential-escrow/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "flag" + "log" + + cc "github.com/hyperledger/fabric-private-chaincode/samples/chaincode/confidential-escrow/chaincode" +) + +// main is the entry point for the Confidential Escrow chaincode. +// It handles command-line flags for collection generation and starts the chaincode server. +// +// Flags: +// +// -g: Enable collection generation mode +// -orgs: List of organization names for collection configuration +// +// When collection generation is enabled, the application generates +// collection configurations and exits. Otherwise, it initializes +// and starts the chaincode service. +func main() { + // Handle collection generation flag + genFlag := flag.Bool("g", false, "Enable collection generation") + flag.Bool("orgs", false, "List of orgs to generate collection for") + flag.Parse() + if *genFlag { + listOrgs := flag.Args() + cc.GenerateCollection(listOrgs) + return + } + + log.Printf("Starting Confidential Escrow Chaincode v1.0.0") + + // Setup CC-Tools components + err := cc.SetupCC() + if err != nil { + log.Printf("Error setting up chaincode: %s", err) + return + } + + // Start chaincode + err = cc.StartChaincode() + if err != nil { + log.Printf("Error starting chaincode: %s", err) + } +} diff --git a/samples/chaincode/confidential-escrow/multi_user_dashboard.sh b/samples/chaincode/confidential-escrow/multi_user_dashboard.sh new file mode 100755 index 000000000..75d2867b7 --- /dev/null +++ b/samples/chaincode/confidential-escrow/multi_user_dashboard.sh @@ -0,0 +1,1341 @@ +#!/bin/bash + +# FPC Multi-User Testing System +# Description: Interactive testing system for Alice, Bob, and Monitor + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +MAGENTA='\033[0;35m' +NC='\033[0m' + +# Global variables +STATE_DIR="/tmp/fpc_test_state" +RUNNING_IN_DOCKER="false" +MAIN_SCRIPT_SOURCED="false" +USER_MODE="" +USER_NAME="" +USER_ORG="" +CERT_HASH="" + +# State files +ACTIVITY_LOG="$STATE_DIR/activity.log" +ALICE_STATE="$STATE_DIR/alice.state" +BOB_STATE="$STATE_DIR/bob.state" +SHARED_STATE="$STATE_DIR/shared.state" + +# Logging functions +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +log_activity() { + local timestamp=$(date '+%H:%M:%S') + local user="$1" + local action="$2" + echo "[$timestamp] $user: $action" >>"$ACTIVITY_LOG" +} + +# Initialize state directory and files +init_state() { + mkdir -p "$STATE_DIR" + touch "$ACTIVITY_LOG" "$ALICE_STATE" "$BOB_STATE" "$SHARED_STATE" + + # Initialize shared state if empty + if [ ! -s "$SHARED_STATE" ]; then + cat >"$SHARED_STATE" <<'EOF' +DIGITAL_ASSET_UUID="" +DIGITAL_ASSET_JSON="" +ESCROWS=() +SYSTEM_INITIALIZED="false" +EOF + fi +} + +# Source state files +load_state() { + if [ -f "$SHARED_STATE" ]; then + source "$SHARED_STATE" + fi + + if [ -n "$USER_MODE" ] && [ -f "${STATE_DIR}/${USER_MODE}.state" ]; then + source "${STATE_DIR}/${USER_MODE}.state" + fi +} + +# Save user-specific state +save_user_state() { + cat >"${STATE_DIR}/${USER_MODE}.state" <"$SHARED_STATE" <' | sed 's/^> //' | grep -o "\"@key\":\"${asset_type}:[^\"]*\"" | cut -d':' -f3 | tr -d '"' +} + +# Extract only the asset UUID, not the full JSON with metadata +extract_asset_uuid() { + local output="$1" + echo "$output" | grep '^>' | sed 's/^> //' | grep -o '"@key":"digitalAsset:[^"]*"' | cut -d':' -f3 | tr -d '"' +} + +# Extract balance from response +extract_balance() { + local output="$1" + echo "$output" | grep -o '"balance":[0-9]*' | head -1 | cut -d':' -f2 +} + +extract_escrow_balance() { + local output="$1" + echo "$output" | grep -o '"escrowBalance":[0-9]*' | head -1 | cut -d":" -f2 +} + +# Run fpcclient command with error handling +# Run fpcclient command with proper error handling +run_fpcclient() { + local cmd_type="$1" + local function_name="$2" + local args="$3" + local desc="$4" + + log_info "$desc" + echo -e "${CYAN}Command: ./fpcclient $cmd_type $function_name${NC}" + echo -e "${CYAN}Payload: $args${NC}" + echo + + local output + if [ "$cmd_type" = "invoke" ]; then + output=$(./fpcclient invoke "$function_name" "$args" 2>&1) + else + output=$(./fpcclient query "$function_name" "$args" 2>&1) + fi + + local exit_code=$? + + echo -e "${YELLOW}════════ FPCCLIENT OUTPUT START ════════${NC}" + echo "$output" + echo -e "${YELLOW}════════ FPCCLIENT OUTPUT END ══════════${NC}" + echo + + if [ $exit_code -eq 0 ]; then + log_success "$desc - COMPLETED" + return 0 + else + log_error "$desc - FAILED (Exit Code: $exit_code)" + return 1 + fi +} + +# Dashboard functions +show_dashboard() { + clear + echo -e "${CYAN}╔══════════════════════════════════════════════════════════════╗${NC}" + echo -e "${CYAN}║${NC} ${YELLOW}FPC MULTI-USER TEST SYSTEM${NC} ${CYAN} ║${NC}" + echo -e "${CYAN}╠══════════════════════════════════════════════════════════════╣${NC}" + + if [ "$USER_MODE" = "monitor" ]; then + show_monitor_dashboard + else + show_user_dashboard + fi + + echo -e "${CYAN}╚══════════════════════════════════════════════════════════════╝${NC}" +} + +show_user_dashboard() { + load_state + + local wallet_display="${WALLET_UUID}" + [ -n "$WALLET_UUID" ] && wallet_display="${wallet_display}" || wallet_display="Not created" + + echo -e "${CYAN}${NC} ${GREEN}User:${NC} $USER_NAME ($USER_ORG) ${CYAN}${NC}" + echo -e "${CYAN}${NC} ${GREEN}Wallet UUID:${NC} ${wallet_display} ${CYAN}${NC}" + echo -e "${CYAN}${NC} ${GREEN}Balance:${NC} ${WALLET_BALANCE:-0} CBDC ${CYAN}${NC}" + echo -e "${CYAN}${NC} ${GREEN}Escrow Balance:${NC} ${ESCROW_BALANCE:-0} CBDC ${CYAN}${NC}" + echo -e "${CYAN}╠══════════════════════════════════════════════════════════════╣${NC}" + + # Show recent activity + echo -e "${CYAN}${NC} ${YELLOW}Recent Activity:${NC} ${CYAN}${NC}" + if [ -f "$ACTIVITY_LOG" ] && [ -s "$ACTIVITY_LOG" ]; then + tail -n 3 "$ACTIVITY_LOG" | while IFS= read -r line; do + printf "${CYAN}${NC} %-58s ${CYAN}${NC}\n" "$line" + done + else + printf "${CYAN}${NC} %-58s ${CYAN}${NC}\n" "No activity yet" + fi +} + +show_monitor_dashboard() { + load_state + + echo -e "${CYAN} ${YELLOW}SYSTEM MONITOR${NC} " + echo -e "${CYAN}╠══════════════════════════════════════════════════════════════╣${NC}" + + # Digital Asset - Show full UUID on same line, no restriction + if [ -n "$DIGITAL_ASSET_UUID" ]; then + printf " ${GREEN}Digital Asset:${NC} ${DIM}%s${NC}\n" "$DIGITAL_ASSET_UUID" + else + printf " ${GREEN}Digital Asset:${NC} %s\n" "Not created" + fi + + echo -e "${CYAN}╠══════════════════════════════════════════════════════════════╣${NC}" + + # Users Status Header + echo -e " ${YELLOW}Users Status:${NC} " + + # Alice Status + if [ -f "$ALICE_STATE" ]; then + source "$ALICE_STATE" + echo -e " " + printf " ${MAGENTA}%-8s${NC} %-49s\n" "Alice:" "" + if [ -n "$WALLET_UUID" ]; then + printf " ${DIM}Wallet:${NC} %-47s\n" "$WALLET_UUID" + else + printf " Wallet: %-47s\n" "Not created" + fi + printf " Balance: ${GREEN}%-47s${NC}\n" "${WALLET_BALANCE:-0} CBDC" + else + echo -e " " + echo -e " ${MAGENTA}Alice:${NC} Not active " + fi + + # Bob Status + if [ -f "$BOB_STATE" ]; then + source "$BOB_STATE" + echo -e " " + printf " ${MAGENTA}%-8s${NC} %-49s\n" "Bob:" "" + if [ -n "$WALLET_UUID" ]; then + printf " ${DIM}Wallet:${NC} %-47s\n" "$WALLET_UUID" + else + printf " Wallet: %-47s\n" "Not created" + fi + printf " Balance: ${GREEN}%-47s${NC}\n" "${WALLET_BALANCE:-0} CBDC" + else + echo -e " " + echo -e " ${MAGENTA}Bob:${NC} Not active " + fi + + echo -e " " + echo -e "${CYAN}╠══════════════════════════════════════════════════════════════╣${NC}" + + # Activity Log Header + echo -e " ${YELLOW}Live Activity Log:${NC} " + echo -e " " + + if [ -f "$ACTIVITY_LOG" ] && [ -s "$ACTIVITY_LOG" ]; then + tail -n 5 "$ACTIVITY_LOG" | while IFS= read -r line; do + # Print the entire line without any truncation or wrapping + printf " %s\n" "$line" + done + else + printf " %s\n" "No activity yet" + fi + + echo -e " " +} + +show_main_menu() { + echo + echo -e "${YELLOW}═══ MAIN MENU ═══${NC}" + # echo "1. Setup System (Initialize Digital Asset)" + echo "1. Wallet Operations" + echo "2. Token Operations" + echo "3. Escrow Operations" + echo "4. Refresh Dashboard" + echo "0. Exit" + echo +} + +show_wallet_menu() { + echo + echo -e "${YELLOW}═══ WALLET OPERATIONS ═══${NC}" + echo "1. Create Wallet" + echo "2. Check Balance" + echo "3. Query My Wallet Details" + echo "0. Back to Main Menu" + echo +} + +show_token_menu() { + echo + echo -e "${YELLOW}═══ TOKEN OPERATIONS ═══${NC}" + # echo "1. Mint Tokens" + echo "1. Transfer Tokens" + echo "2. Burn Tokens" + echo "0. Back to Main Menu" + echo +} + +show_escrow_menu() { + echo + echo -e "${YELLOW}═══ ESCROW OPERATIONS ═══${NC}" + echo "1. Create Escrow" + echo "2. Release Escrow" + echo "3. Refund Escrow" + echo "4. Check Escrow Balance" + echo "5. Query Escrow Details" + echo "0. Back to Main Menu" + echo +} + +show_query_menu() { + echo + echo -e "${YELLOW}═══ QUERY OPERATIONS ═══${NC}" + echo "1. Query My Wallet" + echo "2. Query Digital Asset" + echo "3. Get System Schema" + echo "4. Get My Balance" + echo "5. Get Wallet By Owner" + echo "0. Back to Main Menu" + echo +} + +show_monitor_menu() { + echo + echo -e "${YELLOW}═══ MONITOR MENU ═══${NC}" + echo "1. Refresh Dashboard" + echo "2. View Full Activity Log" + echo "3. Clear Activity Log" + echo "4. Export System State" + echo "5. View Alice State" + echo "6. View Bob State" + echo "0. Exit" + echo +} + +# Wallet operations +create_wallet() { + load_state + + if [ -n "$WALLET_UUID" ]; then + log_info "Wallet already exists: ${WALLET_UUID}" + return 0 + fi + + # Create digital asset if not exists + if [ "$SYSTEM_INITIALIZED" != "true" ]; then + log_info "Digital asset not found. Creating automatically..." + local output + if output=$(run_fpcclient "invoke" "createDigitalAsset" '{ + "name": "CBDC", + "symbol": "CBDC", + "decimals": 2, + "totalSupply": 1000000, + "owner": "central_bank", + "issuerHash": "sha256:central_bank_cert" + }' "Creating digital asset CBDC"); then + DIGITAL_ASSET_UUID=$(extract_uuid "$output" "digitalAsset") + DIGITAL_ASSET_JSON=$(echo "$output" | grep '^>' | sed 's/^> //') + SYSTEM_INITIALIZED="true" + save_shared_state + log_activity "$USER_NAME" "Created digital asset CBDC (UUID: ${DIGITAL_ASSET_UUID})" + log_success "Digital asset created automatically!" + else + echo "$output" + log_error "Failed to create digital asset" + return 1 + fi + fi + + local wallet_id="${USER_MODE}-wallet-$(date +%s)" + + local json_payload="{ + \"walletId\": \"$wallet_id\", + \"ownerPubKey\": \"${USER_NAME}_public_key\", + \"ownerCertHash\": \"$CERT_HASH\" + }" + + local output + if output=$(run_fpcclient "invoke" "createWallet" "$json_payload" "Creating wallet for $USER_NAME"); then + echo "$output" + + WALLET_UUID=$(extract_uuid "$output" "wallet") + + if [ -z "$WALLET_UUID" ]; then + log_error "Failed to extract wallet UUID from response" + return 1 + fi + + WALLET_ID="$wallet_id" + WALLET_BALANCE=0 + ESCROW_BALANCE=0 + save_user_state + log_activity "$USER_NAME" "Created wallet: ${WALLET_UUID}" + log_success "Wallet created successfully!" + + # Auto-mint 10000 CBDC + log_info "Auto-funding wallet with 10000 CBDC..." + if output=$(run_fpcclient "invoke" "mintTokens" "{ + \"assetId\": \"$DIGITAL_ASSET_UUID\", + \"pubKey\": \"${USER_MODE}_public_key\", + \"amount\": 10000, + \"issuerCertHash\": \"sha256:central_bank_cert\" + }" "Minting 10000 CBDC tokens"); then + WALLET_BALANCE=10000 + save_user_state + log_activity "$USER_NAME" "Auto-funded wallet with 10000 CBDC" + log_success "Wallet funded with 10000 CBDC!" + else + echo "$output" + log_error "Failed to auto-fund wallet" + fi + else + echo "$output" + log_error "Failed to create wallet" + return 1 + fi +} + +check_balance() { + load_state + + if [ -z "$WALLET_UUID" ]; then + log_error "Please create a wallet first!" + return 1 + fi + + local output + if output=$(run_fpcclient "query" "getBalance" "{ + \"pubKey\": \"${USER_MODE}_public_key\", + \"assetSymbol\": \"CBDC\", + \"ownerCertHash\": \"$CERT_HASH\" + }" "Checking balance for $USER_NAME"); then + local balance=$(extract_balance "$output") + WALLET_BALANCE=${balance:-0} + save_user_state + log_success "Current balance: $WALLET_BALANCE CBDC" + else + log_error "Failed to check balance" + echo "$output" + return 1 + fi +} + +query_wallet() { + load_state + + if [ -z "$WALLET_UUID" ]; then + log_error "Please create a wallet first!" + return 1 + fi + + run_fpcclient "query" "getWalletByOwner" "{ + \"pubKey\": \"${USER_MODE}_public_key\", + \"ownerCertHash\": \"$CERT_HASH\" + }" "Querying wallet details" +} + +mint_tokens() { + load_state + + if [ -z "$WALLET_UUID" ] || [ -z "$DIGITAL_ASSET_UUID" ]; then + log_error "Please create wallet and setup system first!" + return 1 + fi + + echo -n "Enter amount to mint: " + read amount + + if ! [[ "$amount" =~ ^[0-9]+$ ]] || [ "$amount" -le 0 ]; then + log_error "Invalid amount. Please enter a positive number." + return 1 + fi + + local output + if output=$(run_fpcclient "invoke" "mintTokens" "{ + \"pubKey\": \"${USER_MODE}_public_key\", + \"assetId\": \"$DIGITAL_ASSET_UUID\", + \"amount\": $amount, + \"issuerCertHash\": \"sha256:central_bank_cert\" + }" "Minting $amount CBDC tokens"); then + log_activity "$USER_NAME" "Minted $amount CBDC tokens" + check_balance + log_success "$amount tokens minted successfully!" + else + log_error "Failed to mint tokens" + return 1 + fi +} + +transfer_tokens() { + load_state + + if [ -z "$WALLET_UUID" ]; then + log_error "Please create a wallet first!" + return 1 + fi + + # Get other user's wallet + local other_user_state + local other_user + + if [ "$USER_MODE" = "alice" ]; then + other_user_state="$BOB_STATE" + other_user="bob" + else + other_user_state="$ALICE_STATE" + other_user="alice" + fi + + if [ ! -f "$other_user_state" ] || [ ! -s "$other_user_state" ]; then + log_error "$other_user hasn't created a wallet yet!" + return 1 + fi + + echo -n "Enter amount to transfer to $other_user: " + read amount + + if ! [[ "$amount" =~ ^[0-9]+$ ]] || [ "$amount" -le 0 ]; then + log_error "Invalid amount. Please enter a positive number." + return 1 + fi + + local output + if output=$(run_fpcclient "invoke" "transferTokens" "{ + \"fromPubKey\": \"${USER_MODE}_public_key\", + \"toPubKey\": \"${other_user}_public_key\", + \"assetId\": \"$DIGITAL_ASSET_UUID\", + \"amount\": $amount, + \"senderCertHash\": \"$CERT_HASH\" + }" "Transferring $amount CBDC to $other_user"); then + log_activity "$USER_NAME" "Transferred $amount CBDC to $other_user" + check_balance + log_success "$amount tokens transferred successfully to $other_user!" + else + echo "$output" + log_error "Failed to transfer tokens" + return 1 + fi +} + +burn_tokens() { + load_state + + if [ -z "$WALLET_UUID" ] || [ -z "$DIGITAL_ASSET_UUID" ]; then + log_error "Please create wallet first!" + return 1 + fi + + echo -n "Enter amount to burn: " + read amount + + if ! [[ "$amount" =~ ^[0-9]+$ ]] || [ "$amount" -le 0 ]; then + log_error "Invalid amount. Please enter a positive number." + return 1 + fi + + local output + if output=$(run_fpcclient "invoke" "burnTokens" "{ + \"pubKey\": \"${USER_MODE}_public_key\", + \"assetId\": \"$DIGITAL_ASSET_UUID\", + \"walletUUID\": \"$WALLET_UUID\", + \"amount\": $amount, + \"issuerCertHash\": \"sha256:central_bank_cert\" + }" "Burning $amount CBDC tokens"); then + log_activity "$USER_NAME" "Burned $amount CBDC tokens" + check_balance + log_success "$amount tokens burned successfully!" + else + log_error "Failed to burn tokens" + return 1 + fi +} + +# Escrow operations +create_escrow() { + load_state + + if [ -z "$WALLET_UUID" ] || [ -z "$DIGITAL_ASSET_JSON" ]; then + log_error "Please create wallet and setup system first!" + return 1 + fi + + local other_user + local other_cert_hash + + if [ "$USER_MODE" = "alice" ]; then + other_user="bob" + other_cert_hash="sha256:bob_cert" + other_user_state="$BOB_STATE" + else + other_user="alice" + other_cert_hash="sha256:alice_cert" + other_user_state="$ALICE_STATE" + fi + + echo -n "Enter parcel ID: " + read parcel_id + + if [ -z "$parcel_id" ]; then + log_error "Parcel ID cannot be empty!" + return 1 + fi + + echo -n "Enter escrow amount: " + read amount + + if ! [[ "$amount" =~ ^[0-9]+$ ]] || [ "$amount" -le 0 ]; then + log_error "Invalid amount. Please enter a positive number." + return 1 + fi + + echo -n "Enter secret for escrow: " + read -s secret + echo + + if [ -z "$secret" ]; then + log_error "Secret cannot be empty!" + return 1 + fi + + local escrow_id="${USER_MODE}-escrow-$(date +%s)" + local buyer_wallet="$WALLET_UUID" + + local output + if output=$(run_fpcclient "invoke" "createAndLockEscrow" "{ + \"escrowId\": \"$escrow_id\", + \"buyerPubKey\": \"${USER_MODE}_public_key\", + \"sellerPubKey\": \"${other_user}_public_key\", + \"amount\": $amount, + \"assetType\": $DIGITAL_ASSET_JSON, + \"parcelId\": \"$parcel_id\", + \"secret\": \"$secret\", + \"buyerCertHash\": \"$CERT_HASH\" + }" "Creating escrow for parcel $parcel_id"); then + local escrow_uuid=$(extract_uuid "$output" "escrow") + LAST_ESCROW_UUID="$escrow_uuid" + LAST_ESCROW_SECRET="$secret" + LAST_PARCEL_ID="$parcel_id" + save_user_state + log_activity "$USER_NAME" "Created escrow for parcel $parcel_id with $other_user: ${escrow_uuid}" + check_balance + log_success "Escrow created and funds locked successfully!" + echo "Escrow UUID: $escrow_uuid" + echo "Parcel ID: $parcel_id" + echo "Remember your secret!" + else + echo "$output" + log_error "Failed to create escrow" + return 1 + fi +} + +release_escrow() { + load_state + + if [ -z "$WALLET_UUID" ]; then + log_error "Please create a wallet first!" + return 1 + fi + + echo -n "Enter escrow UUID to release: " + read escrow_uuid + + echo -n "Enter parcel ID: " + read parcel_id + + echo -n "Enter secret provided by buyer: " + read -s secret + echo + + local output + if output=$(run_fpcclient "invoke" "releaseEscrow" "{ + \"escrowUUID\": \"$escrow_uuid\", + \"secret\": \"$secret\", + \"parcelId\": \"$parcel_id\", + \"sellerCertHash\": \"$CERT_HASH\" + }" "Releasing escrow"); then + log_activity "$USER_NAME" "Released escrow ${escrow_uuid}" + check_balance + log_success "Escrow released! Funds transferred to your wallet." + else + echo "$output" + log_error "Failed to release escrow" + return 1 + fi +} + +refund_escrow() { + load_state + + if [ -z "$WALLET_UUID" ]; then + log_error "Please create a wallet first!" + return 1 + fi + + if [ -z "$LAST_ESCROW_UUID" ]; then + echo -n "Enter escrow UUID to refund: " + read escrow_uuid + else + echo "Last escrow: ${LAST_ESCROW_UUID}" + echo -n "Use this escrow? (y/n): " + read use_last + + if [ "$use_last" = "y" ] || [ "$use_last" = "Y" ]; then + escrow_uuid="$LAST_ESCROW_UUID" + else + echo -n "Enter escrow UUID: " + read escrow_uuid + fi + fi + + local output + if output=$(run_fpcclient "invoke" "refundEscrow" "{ + \"escrowUUID\": \"$escrow_uuid\", + \"buyerPubKey\": \"${USER_MODE}_public_key\", + \"buyerCertHash\": \"$CERT_HASH\" + }" "Refunding escrow"); then + log_activity "$USER_NAME" "Refunded escrow ${escrow_uuid}" + check_balance + log_success "Escrow refunded! Funds returned to your wallet." + else + echo "$output" + log_error "Failed to refund escrow" + return 1 + fi +} + +check_escrow_balance() { + load_state + + if [ -z "$WALLET_UUID" ]; then + log_error "Please create a wallet first!" + return 1 + fi + + local output + if output=$(run_fpcclient "query" "getEscrowBalance" "{ + \"pubKey\": \"${USER_MODE}_public_key\", + \"assetSymbol\": \"CBDC\", + \"ownerCertHash\": \"$CERT_HASH\" + }" "Checking escrow balance"); then + echo "$output" + local balance=$(extract_escrow_balance "$output") + ESCROW_BALANCE=${balance:-0} + save_user_state + log_success "Current escrow balance: $ESCROW_BALANCE CBDC" + else + log_error "Failed to check escrow balance" + return 1 + fi +} + +query_escrow() { + load_state + + if [ -z "$LAST_ESCROW_UUID" ]; then + echo -n "Enter escrow UUID to query: " + read escrow_uuid + else + echo "Last escrow: ${LAST_ESCROW_UUID}" + echo -n "Use this escrow? (y/n): " + read use_last + + if [ "$use_last" = "y" ] || [ "$use_last" = "Y" ]; then + escrow_uuid="$LAST_ESCROW_UUID" + else + echo -n "Enter escrow UUID: " + read escrow_uuid + fi + fi + + run_fpcclient "query" "readEscrow" "{\"uuid\": \"$escrow_uuid\"}" "Querying escrow details" +} + +# Query operations +query_digital_asset() { + load_state + + if [ -z "$DIGITAL_ASSET_UUID" ]; then + log_error "Digital asset not created yet!" + return 1 + fi + + run_fpcclient "query" "readDigitalAsset" "{\"uuid\": \"$DIGITAL_ASSET_UUID\"}" "Querying digital asset" +} + +get_schema() { + run_fpcclient "invoke" "getSchema" '{}' "Getting system schema" +} + +# Monitor operations +run_monitor() { + local last_activity_hash="" + local last_alice_hash="" + local last_bob_hash="" + + while true; do + # Calculate hashes of current state + local current_activity_hash=$([ -f "$ACTIVITY_LOG" ] && md5sum "$ACTIVITY_LOG" | cut -d' ' -f1 || echo "") + local current_alice_hash=$([ -f "$ALICE_STATE" ] && md5sum "$ALICE_STATE" | cut -d' ' -f1 || echo "") + local current_bob_hash=$([ -f "$BOB_STATE" ] && md5sum "$BOB_STATE" | cut -d' ' -f1 || echo "") + + # Check if anything changed + if [ "$current_activity_hash" != "$last_activity_hash" ] || + [ "$current_alice_hash" != "$last_alice_hash" ] || + [ "$current_bob_hash" != "$last_bob_hash" ]; then + + show_dashboard + echo + echo -e "${YELLOW}[Auto-refreshing... Press Ctrl+C to exit]${NC}" + + # Update hashes + last_activity_hash="$current_activity_hash" + last_alice_hash="$current_alice_hash" + last_bob_hash="$current_bob_hash" + fi + + sleep 2 # Refresh every 2 seconds + done +} + +clear_activity_log() { + echo -n "Are you sure you want to clear the activity log? (y/N): " + read confirm + + if [ "$confirm" = "y" ] || [ "$confirm" = "Y" ]; then + >"$ACTIVITY_LOG" + log_success "Activity log cleared!" + else + log_info "Activity log not cleared." + fi +} + +export_system_state() { + local export_file="/tmp/fpc_system_export_$(date +%Y%m%d_%H%M%S).txt" + + { + echo "==========================================" + echo " FPC System State Export" + echo "==========================================" + echo "Export Time: $(date)" + echo + echo "==========================================" + echo " Shared State" + echo "==========================================" + if [ -f "$SHARED_STATE" ]; then + cat "$SHARED_STATE" + else + echo "No shared state" + fi + echo + echo "==========================================" + echo " Alice State" + echo "==========================================" + if [ -f "$ALICE_STATE" ] && [ -s "$ALICE_STATE" ]; then + cat "$ALICE_STATE" + else + echo "Alice not active" + fi + echo + echo "==========================================" + echo " Bob State" + echo "==========================================" + if [ -f "$BOB_STATE" ] && [ -s "$BOB_STATE" ]; then + cat "$BOB_STATE" + else + echo "Bob not active" + fi + echo + echo "==========================================" + echo " Activity Log" + echo "==========================================" + if [ -f "$ACTIVITY_LOG" ] && [ -s "$ACTIVITY_LOG" ]; then + cat "$ACTIVITY_LOG" + else + echo "No activity logged" + fi + echo + echo "==========================================" + } >"$export_file" + + log_success "System state exported to: $export_file" +} + +view_user_state() { + local user="$1" + local state_file="${STATE_DIR}/${user}.state" + + clear + echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}" + echo -e "${CYAN} ${user^^} STATE ${NC}" + echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}" + echo + + if [ -f "$state_file" ] && [ -s "$state_file" ]; then + cat "$state_file" + else + echo "$user is not active yet." + fi + + echo + echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}" +} + +# User interaction loops +run_user_interface() { + while true; do + show_dashboard + show_main_menu + read -p "Choose option: " choice + + case $choice in + 1) handle_wallet_operations ;; + 2) handle_token_operations ;; + 3) handle_escrow_operations ;; + 4) + sleep 1 + continue + ;; + 0) exit 0 ;; + *) log_error "Invalid option" ;; + esac + + if [ $choice -ne 6 ]; then + read -p "Press Enter to continue..." + fi + done +} + +handle_wallet_operations() { + while true; do + show_wallet_menu + read -p "Choose option: " choice + + case $choice in + 1) create_wallet ;; + 2) check_balance ;; + 3) query_wallet ;; + 0) return ;; + *) log_error "Invalid option" ;; + esac + + if [ $choice -ne 0 ]; then + read -p "Press Enter to continue..." + fi + done +} + +handle_token_operations() { + while true; do + show_token_menu + read -p "Choose option: " choice + + case $choice in + # 1) mint_tokens ;; + 1) transfer_tokens ;; + 2) burn_tokens ;; + 0) return ;; + *) log_error "Invalid option" ;; + esac + + if [ $choice -ne 0 ]; then + read -p "Press Enter to continue..." + fi + done +} + +handle_escrow_operations() { + while true; do + show_escrow_menu + read -p "Choose option: " choice + + case $choice in + 1) create_escrow ;; + # 2) verify_escrow_condition ;; + 2) release_escrow ;; + 3) refund_escrow ;; + 4) check_escrow_balance ;; + 5) query_escrow ;; + 0) return ;; + *) log_error "Invalid option" ;; + esac + + if [ $choice -ne 0 ]; then + read -p "Press Enter to continue..." + fi + done +} + +handle_query_operations() { + while true; do + show_query_menu + read -p "Choose option: " choice + + case $choice in + 1) query_wallet ;; + 2) query_digital_asset ;; + 3) get_schema ;; + 4) check_balance ;; + 0) return ;; + *) log_error "Invalid option" ;; + esac + + if [ $choice -ne 0 ]; then + read -p "Press Enter to continue..." + fi + done +} + +# Main Execution +show_startup_menu() { + clear + echo -e "${CYAN}╔══════════════════════════════════════════════════════════════╗${NC}" + echo -e "${CYAN}║${NC} ${YELLOW}FPC SETUP & TEST SYSTEM${NC} ${CYAN}║${NC}" + echo -e "${CYAN}╠══════════════════════════════════════════════════════════════╣${NC}" + + # Dynamic environment display with proper spacing + local env_text + if [ "$RUNNING_IN_DOCKER" = "true" ]; then + env_text="Docker Container" + else + env_text="Host System " # Padded to match "Docker Container" length + fi + echo -e "${CYAN}║${NC} Environment: ${env_text} ${CYAN}║${NC}" + + echo -e "${CYAN}╠══════════════════════════════════════════════════════════════╣${NC}" + echo -e "${CYAN}║${NC} ${CYAN}║${NC}" + echo -e "${CYAN}║${NC} ${YELLOW}SETUP OPTIONS:${NC} ${CYAN}║${NC}" + echo -e "${CYAN}║${NC} 1. Full Setup (ERCC + Network + Install) ${CYAN}║${NC}" + echo -e "${CYAN}║${NC} 2. Quick Setup (Skip ERCC build) ${CYAN}║${NC}" + echo -e "${CYAN}║${NC} ${CYAN}║${NC}" + echo -e "${CYAN}║${NC} ${YELLOW}MULTI-USER TEST:${NC} ${CYAN}║${NC}" + echo -e "${CYAN}║${NC} 3. Run as Alice (Org1MSP) ${CYAN}║${NC}" + echo -e "${CYAN}║${NC} 4. Run as Bob (Org2MSP) ${CYAN}║${NC}" + echo -e "${CYAN}║${NC} 5. Run as Monitor (Read-only) ${CYAN}║${NC}" + echo -e "${CYAN}║${NC} ${CYAN}║${NC}" + echo -e "${CYAN}║${NC} ${YELLOW}UTILITIES:${NC} ${CYAN}║${NC}" + echo -e "${CYAN}║${NC} 6. Reset System (Clear all state) ${CYAN}║${NC}" + echo -e "${CYAN}║${NC} 7. Run All Tests (Original test script) ${CYAN}║${NC}" + echo -e "${CYAN}║${NC} ${CYAN}║${NC}" + echo -e "${CYAN}║${NC} 0. Exit ${CYAN}║${NC}" + echo -e "${CYAN}║${NC} ${CYAN}║${NC}" + echo -e "${CYAN}╚══════════════════════════════════════════════════════════════╝${NC}" + echo +} + +reset_system() { + echo -n "Are you sure you want to reset the entire system? (y/N): " + read confirm + + if [ "$confirm" = "y" ] || [ "$confirm" = "Y" ]; then + rm -rf "$STATE_DIR" + rm -rf "tmp/fpc_enclave_initialized" + # rm -f "/tmp/fpc_enclave_alice_initialized" + # rm -f "/tmp/fpc_enclave_bob_initialized" + init_state + log_success "System reset complete!" + sleep 2 + else + log_info "Reset cancelled." + sleep 1 + fi +} + +########### +# MAIN.SH # +########### + +# Source main.sh for setup functions +source_main_script() { + local main_script="$FPC_PATH/samples/chaincode/confidential-escrow/test.sh" + + if [ -f "$main_script" ]; then + # Source only the functions, don't execute main + source "$main_script" + log_info "Loaded setup functions from test.sh" + else + log_error "test.sh not found at: $main_script" + exit 1 + fi +} + +# Detect if running inside Docker container +detect_environment() { + if [ -f "/.dockerenv" ] || grep -q docker /proc/1/cgroup 2>/dev/null; then + RUNNING_IN_DOCKER="true" + log_info "Detected: Running inside Docker container" + else + RUNNING_IN_DOCKER="false" + log_info "Detected: Running on host system" + fi +} + +######### +# SETUP # +######### + +# Wrapper functions that call test.sh functions +do_full_setup() { + if [ "$MAIN_SCRIPT_SOURCED" = "false" ]; then + source_main_script + MAIN_SCRIPT_SOURCED="true" + fi + + log_info "=== RUNNING FULL SETUP ===" + build_ercc + build_chaincode + initial_setup + setup_network + install_fpc + start_ercc + log_success "Full setup completed!" +} + +do_quick_setup() { + if [ "$MAIN_SCRIPT_SOURCED" = "false" ]; then + source_main_script + MAIN_SCRIPT_SOURCED="true" + fi + + log_info "=== RUNNING QUICK SETUP ===" + build_chaincode + setup_network + install_fpc + start_ercc + log_success "Quick setup completed!" +} + +run_original_tests() { + if [ "$MAIN_SCRIPT_SOURCED" = "false" ]; then + source_main_script + MAIN_SCRIPT_SOURCED="true" + fi + + log_info "=== RUNNING ORIGINAL TEST SUITE ===" + + # Check if enclave is initialized + local enclave_marker="/tmp/fpc_enclave_initialized" + local env_file="$FPC_PATH/samples/chaincode/confidential-escrow/.env" + + if [ ! -f "$enclave_marker" ]; then + log_info "Enclave not initialized. Setting up docker environment..." + setup_docker "$env_file" + touch "$enclave_marker" + log_success "Docker environment initialized!" + else + log_info "Enclave already initialized, sourcing environment..." + source "$env_file" + fi + + run_tests + + log_success "Original tests completed!" +} + +main() { + check_fpc_path + detect_environment + init_state + + # If argument provided, use it directly + if [ $# -gt 0 ]; then + case "$1" in + "full") + source_main_script + MAIN_SCRIPT_SOURCED="true" + do_full_setup + ;; + "quick") + source_main_script + MAIN_SCRIPT_SOURCED="true" + do_quick_setup + ;; + "test-all") + source_main_script + MAIN_SCRIPT_SOURCED="true" + run_original_tests + ;; + "alice" | "bob") + setup_user_env "$1" + run_user_interface + ;; + "monitor") + USER_MODE="monitor" + run_monitor + ;; + "reset") + reset_system + ;; + *) + echo "Usage: $0 [full|quick|docker-env|alice|bob|monitor|reset|test-all]" + echo + echo "Setup Options:" + echo " full - Full setup (ERCC + Network + Install)" + echo " quick - Quick setup (Skip ERCC build)" + echo + echo "Multi-User Options:" + echo " alice - Run as Alice (Org1MSP)" + echo " bob - Run as Bob (Org2MSP)" + echo " monitor - Run as Monitor (read-only)" + echo + echo "Utilities:" + echo " reset - Reset system state" + echo " test-all - Run original test suite" + exit 1 + ;; + esac + return + fi + + # Interactive mode with integrated menu + while true; do + show_startup_menu + read -p "Choose option (0-7): " choice + + case $choice in + 1 | 2 | 7) + # Source test.sh once for setup operations + if [ "$MAIN_SCRIPT_SOURCED" = "false" ]; then + source_main_script + MAIN_SCRIPT_SOURCED="true" + fi + ;; + esac + + case $choice in + 1) + do_full_setup + read -p "Press Enter to continue..." + ;; + 2) + do_quick_setup + read -p "Press Enter to continue..." + ;; + 3) + setup_user_env "alice" + run_user_interface + ;; + 4) + setup_user_env "bob" + run_user_interface + ;; + 5) + USER_MODE="monitor" + run_monitor + ;; + 6) + reset_system + ;; + 7) + run_original_tests + read -p "Press Enter to continue..." + ;; + 0) + exit 0 + ;; + *) + log_error "Invalid option" + sleep 1 + ;; + esac + done +} + +main "$@" diff --git a/samples/chaincode/confidential-escrow/test.sh b/samples/chaincode/confidential-escrow/test.sh new file mode 100755 index 000000000..52efe6b0a --- /dev/null +++ b/samples/chaincode/confidential-escrow/test.sh @@ -0,0 +1,500 @@ +#!/bin/bash + +# Combined FPC Setup and Test Script +# Description: One script to rule them all + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +# Logging functions +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Check if FPC_PATH is set +check_fpc_path() { + if [ -z "$FPC_PATH" ]; then + log_error "FPC_PATH is not set. Please export FPC_PATH=/path/to/your/fpc" + exit 1 + fi + log_info "Using FPC_PATH: $FPC_PATH" +} + +# Source environment variables +source_env() { + local env_file="${1:-}" + + # Get the directory where the script is located + SCRIPT_DIR="$FPC_PATH/samples/chaincode/confidential-escrow/" + + # If no argument provided, use default .env + if [ -z "$env_file" ]; then + env_file="$SCRIPT_DIR/.env" + fi + + if [ -f "$env_file" ]; then + source "$env_file" + log_info "Environment variables loaded from $(basename $env_file)" + else + log_error "Environment file not found: $env_file" + exit 1 + fi +} + +# Function to run commands with error handling +run_cmd() { + local cmd="$1" + local desc="$2" + + log_info "$desc" + echo "Running: $cmd" + + if eval "$cmd"; then + log_success "$desc - COMPLETED" + else + log_error "$desc - FAILED" + exit 1 + fi +} + +# Build ERCC (one time requirement) +build_ercc() { + log_info "=== BUILDING ERCC ===" + run_cmd "GOOS=linux make -C $FPC_PATH/ercc build docker" "Building ERCC" +} + +# Build chaincode +build_chaincode() { + log_info "=== BUILDING CHAINCODE ===" + run_cmd "GOOS=linux make -C $FPC_PATH/samples/chaincode/confidential-escrow with_go docker" "Building chaincode" +} + +# One-time setup (run only once) +initial_setup() { + log_info "=== INITIAL SETUP (One-time only) ===" + cd $FPC_PATH/samples/deployment/test-network + run_cmd "./setup.sh" "Running initial setup" +} + +# Setup test network +setup_network() { + log_info "=== SETTING UP NETWORK ===" + cd $FPC_PATH/samples/deployment/test-network/fabric-samples/test-network + + run_cmd "./network.sh down" "Bringing down network" + run_cmd "./network.sh up -ca" "Starting network" + run_cmd "./network.sh createChannel -c mychannel" "Creating channel" +} + +# Install FPC +install_fpc() { + log_info "=== INSTALLING FPC ===" + export CC_ID=confidential-escrow + export CC_PATH="$FPC_PATH/samples/chaincode/confidential-escrow/" + export CC_VER=$(cat "$FPC_PATH/samples/chaincode/confidential-escrow/mrenclave") + + cd $FPC_PATH/samples/deployment/test-network + run_cmd "./installFPC.sh" "Installing FPC" +} + +# Start ERCC-ECC +start_ercc() { + log_info "=== STARTING ERCC-ECC ===" + export EXTRA_COMPOSE_FILE="$FPC_PATH/samples/chaincode/confidential-escrow/confidential-escrow-compose.yaml" + cd $FPC_PATH/samples/deployment/test-network + run_cmd "make ercc-ecc-start" "Starting ERCC-ECC" +} + +# Setup docker environment +setup_docker() { + local env_file="${1:-}" + + log_info "=== SETTING UP DOCKER ENVIRONMENT ===" + + # Source the appropriate environment file + if [ -n "$env_file" ]; then + source_env "$env_file" + else + source_env # Use default .env + fi + + cd $FPC_PATH/samples/deployment/test-network + run_cmd "./update-connection.sh" "Updating connections" + run_cmd "./update-external-connection.sh" "Updating external connections" + + cd $FPC_PATH/samples/application/simple-cli-go + run_cmd "./fpcclient init $CORE_PEER_ID" "Initializing enclave" + log_success "Docker environment ready!" +} + +# Test functions +test_schema() { + log_info "Getting schema..." + cd $FPC_PATH/samples/application/simple-cli-go + ./fpcclient invoke getSchema +} + +test_debug() { + log_info "Running debug test..." + cd $FPC_PATH/samples/application/simple-cli-go + ./fpcclient invoke debugTest '{}' +} + +WALLET_UUID="" +WALLET1_PUBKEY="" +WALLET1_CERT="" +WALLET2_UUID="" +WALLET2_PUBKEY="" +WALLET2_CERT="" +ISSUER_CERT_HASH="" +ESCROW_UUID="" +DIGITAL_ASSET_UUID="" +DIGITAL_ASSET_JSON="" +TOKEN_SYMBOL="" + +store_asset_data() { + local output="$1" + DIGITAL_ASSET_JSON=$(echo "$output" | grep '^>' | sed 's/^> //') + DIGITAL_ASSET_UUID=$(echo "$DIGITAL_ASSET_JSON" | grep -o '"@key":"digitalAsset:[^"]*"' | cut -d':' -f3 | tr -d '"') +} + +store_wallet_data() { + local output="$1" + WALLET_UUID=$(echo "$output" | grep '^>' | sed 's/^> //' | grep -o '"@key":"wallet:[^"]*"' | cut -d':' -f3 | tr -d '"') +} + +store_escrow_data() { + local output="$1" + ESCROW_UUID=$(echo "$output" | grep '^>' | sed 's/^> //' | grep -o '"@key":"escrow:[^"]*"' | cut -d':' -f3 | tr -d '"') +} + +test_create_asset() { + log_info "Creating digital asset..." + cd $FPC_PATH/samples/application/simple-cli-go + local timestamp=$(date +%s) + local issuer_hash="sha256:$(echo -n "issuer_${timestamp}" | sha256sum | cut -d' ' -f1)" + local token_symbol="CBDC${timestamp}" + ISSUER_CERT_HASH="$issuer_hash" + TOKEN_SYMBOL="$token_symbol" + + local output=$(./fpcclient invoke createDigitalAsset '{ + "name": "", + "symbol": "'"$token_symbol"'", + "decimals": 2, + "totalSupply": 1000000, + "owner": "central_bank", + "issuerHash": "'"${issuer_hash}"'" + }' 2>&1) + echo "$output" + store_asset_data "$output" +} + +test_create_wallet() { + log_info "Creating wallet..." + cd $FPC_PATH/samples/application/simple-cli-go + local timestamp=$(date +%s) + local wallet_id="wallet-${timestamp}" + local owner_key="PsychoPunkSage_pubkey_${timestamp}" + local cert_hash="sha256:$(echo -n "${owner_key}" | sha256sum | cut -d' ' -f1)" + + WALLET1_PUBKEY="$owner_key" + WALLET1_CERT="$cert_hash" + + local output=$(./fpcclient invoke createWallet "{ + \"walletId\": \"${wallet_id}\", + \"ownerPubKey\": \"${owner_key}\", + \"ownerCertHash\": \"${cert_hash}\" + }" 2>&1) + echo "$output" + echo "DIGITIAL ASSET JSON:> $DIGITAL_ASSET_JSON" + store_wallet_data "$output" +} + +test_create_wallet2() { + log_info "Creating second wallet..." + cd $FPC_PATH/samples/application/simple-cli-go + local timestamp=$(date +%s) + local wallet_id="wallet-${timestamp}-2" + local owner_key="Abhinav_Prakash_pubkey_${timestamp}" + local cert_hash="sha256:$(echo -n "${owner_key}" | sha256sum | cut -d' ' -f1)" + + WALLET2_PUBKEY="$owner_key" + WALLET2_CERT="$cert_hash" + + local output=$(./fpcclient invoke createWallet "{ + \"walletId\": \"${wallet_id}\", + \"ownerPubKey\": \"${owner_key}\", + \"ownerCertHash\": \"${cert_hash}\" + }" 2>&1) + echo "$output" + echo "DIGITIAL ASSET JSON:> $DIGITAL_ASSET_JSON" + WALLET2_UUID=$(echo "$output" | grep '^>' | sed 's/^> //' | grep -o '"@key":"wallet:[^"]*"' | cut -d':' -f3 | tr -d '"') +} + +test_create_and_lock_escrow() { + log_info "Creating escrow and locking funds..." + cd $FPC_PATH/samples/application/simple-cli-go + local timestamp=$(date +%s) + local escrow_id="escrow-${timestamp}" + local parcel_id="parcel-${timestamp}" + local secret="secret-${timestamp}" + + ESCROW_SECRET="$secret" + ESCROW_PARCEL="$parcel_id" + + local output=$(./fpcclient invoke createAndLockEscrow "{ + \"escrowId\": \"$escrow_id\", + \"buyerPubKey\": \"$WALLET1_PUBKEY\", + \"sellerPubKey\": \"$WALLET2_PUBKEY\", + \"amount\": 20, + \"assetType\": $DIGITAL_ASSET_JSON, + \"parcelId\": \"$parcel_id\", + \"secret\": \"$secret\", + \"buyerCertHash\": \"$WALLET1_CERT\" + }" 2>&1) + echo "$output" + store_escrow_data "$output" +} + +test_query_asset() { + log_info "Querying digital asset..." + cd $FPC_PATH/samples/application/simple-cli-go + log_info $DIGITAL_ASSET_UUID + log_info $DIGITAL_ASSET_JSON + ./fpcclient query readDigitalAsset "{\"uuid\": \"$DIGITAL_ASSET_UUID\"}" +} + +test_query_wallet() { + log_info "Querying wallet..." + cd $FPC_PATH/samples/application/simple-cli-go + ./fpcclient query getWalletByOwner "{ + \"pubKey\": \"$WALLET1_PUBKEY\", + \"ownerCertHash\": \"$WALLET1_CERT\" + }" +} + +test_get_balance() { + log_info "Testing getBalance transaction" + + # Test getting balance for $TOKEN_SYMBOL in wallet1 + cd $FPC_PATH/samples/application/simple-cli-go + ./fpcclient query getBalance "{ + \"pubKey\": \"$WALLET1_PUBKEY\", + \"assetSymbol\": \"$TOKEN_SYMBOL\", + \"ownerCertHash\": \"$WALLET1_CERT\" + }" +} + +test_mint_tokens() { + log_info "Testing mintTokens transaction" + cd $FPC_PATH/samples/application/simple-cli-go + ./fpcclient invoke mintTokens "{ + \"assetId\": \"$DIGITAL_ASSET_UUID\", + \"pubKey\": \"$WALLET2_PUBKEY\", + \"amount\": 100, + \"issuerCertHash\": \"$ISSUER_CERT_HASH\" + }" + + ./fpcclient invoke mintTokens "{ + \"assetId\": \"$DIGITAL_ASSET_UUID\", + \"pubKey\": \"$WALLET1_PUBKEY\", + \"amount\": 100, + \"issuerCertHash\": \"$ISSUER_CERT_HASH\" + }" +} + +test_transfer_tokens() { + log_info "Testing transferTokens transaction" + cd $FPC_PATH/samples/application/simple-cli-go + ./fpcclient invoke transferTokens "{ + \"fromPubKey\": \"$WALLET2_PUBKEY\", + \"toPubKey\": \"$WALLET1_PUBKEY\", + \"assetId\": \"$DIGITAL_ASSET_UUID\", + \"amount\": 50, + \"senderCertHash\": \"$WALLET2_CERT\" + }" +} + +test_burn_tokens() { + log_info "Testing burnTokens transaction" + cd $FPC_PATH/samples/application/simple-cli-go + ./fpcclient invoke burnTokens "{ + \"assetId\": \"$DIGITAL_ASSET_UUID\", + \"pubKey\": \"$WALLET1_PUBKEY\", + \"amount\": 25, + \"issuerCertHash\": \"$ISSUER_CERT_HASH\" + }" +} + +test_get_escrow_balance() { + log_info "Testing getEscrowBalance transaction" + cd $FPC_PATH/samples/application/simple-cli-go + ./fpcclient query getEscrowBalance "{ + \"pubKey\": \"$WALLET1_PUBKEY\", + \"assetSymbol\": \"$TOKEN_SYMBOL\", + \"ownerCertHash\": \"$WALLET1_CERT\" + }" +} + +test_release_escrow() { + log_info "Testing releaseEscrow transaction" + cd $FPC_PATH/samples/application/simple-cli-go + ./fpcclient invoke releaseEscrow "{ + \"escrowUUID\": \"$ESCROW_UUID\", + \"secret\": \"$ESCROW_SECRET\", + \"parcelId\": \"$ESCROW_PARCEL\", + \"sellerCertHash\": \"$WALLET2_CERT\" + }" +} + +test_refund_escrow() { + log_info "Testing refundEscrow transaction (on new escrow)" + cd $FPC_PATH/samples/application/simple-cli-go + # Create another escrow for refund test + local output=$(./fpcclient invoke createAndLockEscrow "{ + \"escrowId\": \"escrow-222\", + \"buyerPubKey\": \"$WALLET1_PUBKEY\", + \"sellerPubKey\": \"$WALLET2_PUBKEY\", + \"amount\": 20, + \"assetType\": $DIGITAL_ASSET_JSON, + \"parcelId\": \"$ESCROW_PARCEL\", + \"secret\": \"$ESCROW_SECRET\", + \"buyerCertHash\": \"$WALLET1_CERT\" + }" 2>&1) + + local REFUND_ESCROW_UUID=$(echo "$output" | grep '^>' | sed 's/^> //' | grep -o '"@key":"escrow:[^"]*"' | cut -d':' -f3 | tr -d '"') + + # Now refund + ./fpcclient invoke refundEscrow "{ + \"escrowUUID\": \"$REFUND_ESCROW_UUID\", + \"buyerPubKey\": \"$WALLET1_PUBKEY\", + \"buyerCertHash\": \"$WALLET1_CERT\" + }" +} + +test_query_escrow() { + log_info "Querying escrow..." + cd $FPC_PATH/samples/application/simple-cli-go + ./fpcclient query readEscrow "{\"uuid\": \"$ESCROW_UUID\"}" +} + +# Batch operations +run_tests() { + log_info "=== RUNNING TESTS ===" + source_env + test_schema + test_debug + test_create_asset + test_create_wallet + test_create_wallet2 + test_mint_tokens + test_create_and_lock_escrow + test_query_asset + test_query_wallet + test_query_escrow + test_get_balance + test_transfer_tokens + test_get_balance + test_burn_tokens + test_get_balance + test_query_wallet + + # New escrow tests + log_info "=== ESCROW SYSTEM TESTS ===" + test_get_balance + test_get_escrow_balance + test_release_escrow + test_get_balance + test_get_escrow_balance + test_create_and_lock_escrow + test_get_balance + test_get_escrow_balance + test_refund_escrow + test_get_balance + test_get_escrow_balance + + log_success "=== TESTS COMPLETED ===" +} + +# Main menu +show_menu() { + echo + echo "=== FPC CONTROL SCRIPT ===" + echo "SETUP OPTIONS:" + echo "1. Full Setup (ERCC + Network + Install)" + echo "2. Quick Setup (Skip ERCC build)" + echo "3. Setup Docker Environment" + echo + echo "TEST OPTIONS:" + echo "4. Run All Tests" + echo + echo "0. Exit" + echo +} + +# Main execution +main() { + check_fpc_path + + case "${1:-menu}" in + "full") + build_ercc + build_chaincode + initial_setup + setup_network + install_fpc + start_ercc + ;; + "quick") + build_chaincode + setup_network + install_fpc + start_ercc + ;; + "docker") + setup_docker + ;; + "test-all") + run_tests + ;; + "menu") + while true; do + show_menu + read -p "Choose an option (0-8): " choice + case $choice in + 1) main "full" ;; + 2) main "quick" ;; + 3) main "docker" ;; + 4) main "test-all" ;; + 0) exit 0 ;; + *) log_error "Invalid option" ;; + esac + echo + read -p "Press Enter to continue..." + done + ;; + *) + echo "Usage: $0 [full|quick|chaincode|docker|clean|test-basic|test-query|test-all|menu]" + exit 1 + ;; + esac +} + +# Only run main if script is executed directly, not sourced +if [ "${BASH_SOURCE[0]}" = "${0}" ]; then + main "$@" +fi diff --git a/samples/chaincode/confidential-escrow/utils/crypto.go b/samples/chaincode/confidential-escrow/utils/crypto.go new file mode 100644 index 000000000..4308f251c --- /dev/null +++ b/samples/chaincode/confidential-escrow/utils/crypto.go @@ -0,0 +1,43 @@ +package utils + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" +) + +// Deal eveyting in bytes to keep things generic and simple +func SHA256Hash(data []byte) string { + hash := sha256.Sum256(data) + return hex.EncodeToString(hash[:]) +} + +// Hash/secret Verification +func VerifyHash(data []byte, expected string) bool { + return SHA256Hash(data) == expected +} + +func HashCertificate(cert []byte) string { + return SHA256Hash(cert) +} + +func VerifySecret(secret string, expected string) bool { + return VerifyHash([]byte(secret), expected) +} + +// CreateCompositeKey creates a deterministic composite key +// Useful for creating unique identifiers +func CreateCompositeKey(components ...string) string { + var combined string + for i, component := range components { + if i > 0 { + combined += ":" + } + combined += component + } + return SHA256Hash([]byte(combined)) +} + +func LogHashOperation(operation string, input string, output string) { + fmt.Printf("[CRYPTO] %s: %s -> %s\n", operation, input[:min(len(input), 10)]+"...", output[:16]+"...") +} diff --git a/samples/chaincode/confidential-escrow/utils/external.go b/samples/chaincode/confidential-escrow/utils/external.go new file mode 100644 index 000000000..7bac69599 --- /dev/null +++ b/samples/chaincode/confidential-escrow/utils/external.go @@ -0,0 +1,9 @@ +package utils + +import ( + "github.com/hyperledger-labs/cc-tools/errors" +) + +func GenerateUUID() (string, errors.ICCError) { + return "todo", nil +} diff --git a/samples/chaincode/confidential-escrow/utils/validate.go b/samples/chaincode/confidential-escrow/utils/validate.go new file mode 100644 index 000000000..da1c7b3bd --- /dev/null +++ b/samples/chaincode/confidential-escrow/utils/validate.go @@ -0,0 +1,4 @@ +package utils + +// will think of it. +// mainly created for field validation..