transactions

package
v0.0.0-...-059df17 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 9, 2026 License: Apache-2.0, MIT Imports: 12 Imported by: 0

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

View Source
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:

  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.

View Source
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:

  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.

View Source
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.

View Source
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.

View Source
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:

  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.

View Source
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
	},
}
View Source
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:

  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.

View Source
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:

  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
View Source
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:

  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.

View Source
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:

  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.

View Source
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.

View Source
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
View Source
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.

View Source
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:

  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.

View Source
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:

  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.

View Source
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:

  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.

View Source
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:

  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.

Functions

This section is empty.

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL