Documentation
¶
Overview ¶
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.
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.
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.
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.
Index ¶
Constants ¶
This section is empty.
Variables ¶
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) 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) 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) } 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) 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) } 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()) } 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 }, }
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:
- Resolve wallet UUID from public key hash
- Verify issuer authorization
- Validate sufficient balance in target wallet
- Deduct tokens from wallet balance
- 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 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] 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) 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) 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) } 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 } } 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) } currentBalance := balances[assetIndex].(float64) currentEscrowBalance := escrowBalances[assetIndex].(float64) balances[assetIndex] = currentBalance - amount escrowBalances[assetIndex] = currentEscrowBalance + amount 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()) } conditionData := secret + parcelId conditionHash := sha256.Sum256([]byte(conditionData)) conditionValue := hex.EncodeToString(conditionHash[:]) 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 }, }
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:
- Validate both buyer and seller wallets exist
- Verify buyer authorization via certificate hash
- Check buyer has sufficient available balance
- Move tokens from available balance to escrow balance
- Compute condition hash: SHA256(secret + parcelId)
- 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 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 }, }
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 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 }, }
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 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.Put(stub) if err != nil { return nil, errors.WrapErrorWithStatus(err, "Error saving wallet on blockchain", err.Status()) } walletUUID := strings.Split(walletAsset.GetProp("@key").(string), ":")[1] userDirMap := make(map[string]any) userDirMap["@assetType"] = "userdir" userDirMap["publicKeyHash"] = pubKeyHash 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 }, }
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:
- Create wallet with empty balance arrays
- Compute SHA-256 hash of owner's public key
- Extract wallet UUID from created asset
- 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 DebugTest = transactions.Transaction{ Tag: "debugTest", Label: "Debug Test", Description: "Test transaction with no access control", Method: "GET", Args: []transactions.Argument{}, Routine: func(stub *sw.StubWrapper, req map[string]interface{}) ([]byte, errors.ICCError) { return []byte("Debug test successful"), nil }, }
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) 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) 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()) } if walletAsset.GetProp("ownerCertHash").(string) != ownerCertHash { return nil, errors.NewCCError("Unauthorized: Certificate hash mismatch", 403) } 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 } 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) }, }
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:
- Compute public key hash and lookup wallet UUID via UserDirectory
- Retrieve wallet from ledger
- Verify owner authorization via certificate hash
- Iterate through wallet's asset types to find matching symbol
- 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 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: "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) 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) 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()) } if walletAsset.GetProp("ownerCertHash").(string) != ownerCertHash { return nil, errors.NewCCError("Unauthorized: Certificate hash mismatch", 403) } 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 } 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) }, }
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:
- Resolve wallet UUID from public key hash
- Retrieve wallet and verify ownership
- Find asset index by matching symbol
- 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 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) 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) walletKey := assets.Key{"@key": "wallet:" + walletUuid} wallet, err := walletKey.Get(stub) if err != nil { return nil, errors.WrapErrorWithStatus(err, "Wallet not found", 404) } 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 }, }
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:
- Compute public key hash and lookup wallet UUID via UserDirectory
- Retrieve complete wallet asset from ledger
- Verify owner authorization via certificate hash
- 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 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) 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) 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) } 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) 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) } 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()) } 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 }, }
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:
- Resolve wallet UUID from public key hash via UserDirectory
- Verify issuer authorization against stored issuerHash
- Update or initialize wallet balance for the asset type
- 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 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{ { 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 }, }
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 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 }, }
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 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()) } 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 }, }
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 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: "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) buyerPubKey, _ := req["buyerPubKey"].(string) buyerCertHash, _ := req["buyerCertHash"].(string) escrowKey := assets.Key{"@key": "escrow:" + escrowUUID} escrowAsset, err := escrowKey.Get(stub) if err != nil { return nil, errors.WrapErrorWithStatus(err, "Escrow not found", 404) } 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) if escrowAsset.GetProp("status").(string) != "Active" { return nil, errors.NewCCError("Escrow is not active", 400) } buyerWalletKey := assets.Key{"@key": "wallet:" + buyerWalletUUID} 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) } assetType := escrowAsset.GetProp("assetType").(map[string]any) assetId := strings.Split(assetType["@key"].(string), ":")[1] amount := escrowAsset.GetProp("amount").(float64) 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) } buyerEscrowBalances[buyerAssetIndex] = buyerEscrowBalances[buyerAssetIndex].(float64) - amount buyerBalances[buyerAssetIndex] = buyerBalances[buyerAssetIndex].(float64) + amount 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()) } 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 }, }
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:
- Retrieve escrow contract and verify "Active" status
- Resolve buyer wallet from public key hash
- Verify buyer authorization via certificate hash
- Move tokens from escrow balance back to available balance
- 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 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) escrowKey := assets.Key{"@key": "escrow:" + escrowUUID} escrowAsset, err := escrowKey.Get(stub) if err != nil { return nil, errors.WrapErrorWithStatus(err, "Escrow not found", 404) } if escrowAsset.GetProp("status").(string) != "Active" { return nil, errors.NewCCError("Escrow is not active", 400) } if escrowAsset.GetProp("parcelId").(string) != parcelId { return nil, errors.NewCCError("Invalid parcel ID", 403) } 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) } 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) } if sellerWallet.GetProp("ownerCertHash").(string) != sellerCertHash { return nil, errors.NewCCError("Unauthorized: Not the seller", 403) } 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) } assetType := escrowAsset.GetProp("assetType").(map[string]any) assetId := strings.Split(assetType["@key"].(string), ":")[1] amount := escrowAsset.GetProp("amount").(float64) 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 for i, assetRef := range buyerAssets { refAssetId := strings.Split(assetRef.(map[string]any)["@key"].(string), ":")[1] if refAssetId == assetId { buyerAssetIndex = i break } } 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 } buyerEscrowBalances[buyerAssetIndex] = buyerEscrowBalances[buyerAssetIndex].(float64) - amount sellerBalances[sellerAssetIndex] = sellerBalances[sellerAssetIndex].(float64) + amount 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()) } 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()) } 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 }, }
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:
- Retrieve escrow contract and verify "Active" status
- Verify parcelId matches escrow record
- Validate secret by computing SHA256(secret + parcelId)
- Verify seller authorization via certificate hash
- Deduct from buyer's escrow balance
- Add to seller's available balance (initialize if needed)
- 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 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) 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) 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()) } if fromWalletAsset.GetProp("ownerCertHash").(string) != senderCertHash { return nil, errors.NewCCError("Unauthorized: Sender certificate mismatch", 403) } 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) 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()) } 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) } 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 !toAssetFound { toAssetTypes = append(toAssetTypes, map[string]any{ "@key": "digitalAsset:" + assetId, }) toBalances = append(toBalances, amount) toEscrowBalances = append(toEscrowBalances, 0.0) } 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()) } 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 }, }
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:
- Resolve both wallet UUIDs from public key hashes
- Verify sender authorization
- Validate sufficient available balance (not escrowed)
- Deduct from source wallet
- Add to destination wallet (initialize asset entry if needed)
- 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 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) escrowKey := assets.Key{"@key": "escrow:" + escrowId} escrowAsset, err := escrowKey.Get(stub) if err != nil { return nil, errors.WrapErrorWithStatus(err, "Error reading escrow", err.Status()) } currentStatus := escrowAsset.GetProp("status").(string) if currentStatus != "Active" { return nil, errors.NewCCError("Escrow is not active", 400) } storedCondition := escrowAsset.GetProp("conditionValue").(string) hasher := sha256.New() hasher.Write([]byte(secret + parcelId)) computedHash := hex.EncodeToString(hasher.Sum(nil)) if computedHash != storedCondition { return nil, errors.NewCCError("Condition verification failed: hash mismatch", 403) } 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()) } 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 }, }
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:
- Retrieve escrow contract from ledger
- Verify escrow status is "Active"
- Compute SHA256(secret + parcelId)
- Compare computed hash with stored conditionValue
- 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.
Functions ¶
This section is empty.
Types ¶
This section is empty.