Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m28s
CI/CD Pipeline / Security Scanning (push) Successful in 2m34s
CI/CD Pipeline / Lint and Format (push) Failing after 54s
CI/CD Pipeline / Terraform Validation (push) Failing after 25s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 27s
Validation / validate-genesis (push) Successful in 31s
Validation / validate-terraform (push) Failing after 28s
Validation / validate-kubernetes (push) Failing after 12s
Validation / validate-smart-contracts (push) Failing after 13s
Validation / validate-security (push) Failing after 1m22s
Validation / validate-documentation (push) Failing after 20s
Use a shared getClientID helper so SolaceNet capability checks and transfer authorization handle identity errors consistently. Co-authored-by: Cursor <cursoragent@cursor.com>
414 lines
12 KiB
Go
414 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/hyperledger/fabric-contract-api-go/contractapi"
|
|
)
|
|
|
|
// TokenizedAssetContract provides functions for managing tokenized assets
|
|
type TokenizedAssetContract struct {
|
|
contractapi.Contract
|
|
}
|
|
|
|
// TokenizedAsset represents a tokenized asset on Fabric
|
|
type TokenizedAsset struct {
|
|
TokenID string `json:"tokenId"`
|
|
UnderlyingAsset string `json:"underlyingAsset"`
|
|
Amount string `json:"amount"`
|
|
Issuer string `json:"issuer"`
|
|
BackingReserve string `json:"backingReserve"`
|
|
Status string `json:"status"` // minted, transferred, redeemed
|
|
RegulatoryFlags map[string]interface{} `json:"regulatoryFlags"`
|
|
CreatedAt string `json:"createdAt"`
|
|
UpdatedAt string `json:"updatedAt"`
|
|
}
|
|
|
|
// MintRequest represents a request to mint tokenized assets
|
|
type MintRequest struct {
|
|
TokenID string `json:"tokenId"`
|
|
UnderlyingAsset string `json:"underlyingAsset"`
|
|
Amount string `json:"amount"`
|
|
Issuer string `json:"issuer"`
|
|
ReserveProof string `json:"reserveProof"`
|
|
RegulatoryFlags map[string]interface{} `json:"regulatoryFlags"`
|
|
}
|
|
|
|
// TransferRequest represents a request to transfer tokenized assets
|
|
type TransferRequest struct {
|
|
TokenID string `json:"tokenId"`
|
|
From string `json:"from"`
|
|
To string `json:"to"`
|
|
Amount string `json:"amount"`
|
|
Regulatory map[string]interface{} `json:"regulatory"`
|
|
}
|
|
|
|
// RedemptionRequest represents a request to redeem tokenized assets
|
|
type RedemptionRequest struct {
|
|
TokenID string `json:"tokenId"`
|
|
Redeemer string `json:"redeemer"`
|
|
Amount string `json:"amount"`
|
|
RedemptionProof string `json:"redemptionProof"`
|
|
}
|
|
|
|
// InitLedger initializes the ledger with sample data (for testing)
|
|
func (s *TokenizedAssetContract) InitLedger(ctx contractapi.TransactionContextInterface) error {
|
|
assets := []TokenizedAsset{
|
|
{
|
|
TokenID: "EUR-T-2025-001",
|
|
UnderlyingAsset: "EUR",
|
|
Amount: "1000000.00",
|
|
Issuer: "DBIS",
|
|
BackingReserve: "1:1",
|
|
Status: "minted",
|
|
RegulatoryFlags: map[string]interface{}{
|
|
"kyc": true,
|
|
"aml": true,
|
|
"regulatoryApproval": true,
|
|
},
|
|
CreatedAt: time.Now().Format(time.RFC3339),
|
|
UpdatedAt: time.Now().Format(time.RFC3339),
|
|
},
|
|
}
|
|
|
|
for _, asset := range assets {
|
|
assetJSON, err := json.Marshal(asset)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = ctx.GetStub().PutState(asset.TokenID, assetJSON)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to put asset to world state: %v", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// MintToken mints a new tokenized asset after reserve verification
|
|
func (s *TokenizedAssetContract) MintToken(ctx contractapi.TransactionContextInterface, requestJSON string) error {
|
|
var request MintRequest
|
|
err := json.Unmarshal([]byte(requestJSON), &request)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to unmarshal mint request: %v", err)
|
|
}
|
|
|
|
// Check if token already exists
|
|
existing, err := ctx.GetStub().GetState(request.TokenID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read from world state: %v", err)
|
|
}
|
|
if existing != nil {
|
|
return fmt.Errorf("token %s already exists", request.TokenID)
|
|
}
|
|
|
|
// Verify reserve proof (in production, this would call reserve manager chaincode)
|
|
// For now, we assume reserve proof is valid if provided
|
|
if request.ReserveProof == "" {
|
|
return fmt.Errorf("reserve proof is required")
|
|
}
|
|
|
|
// Check SolaceNet capability (would integrate with SolaceNet service)
|
|
// This is a placeholder - in production, call SolaceNet API
|
|
callerID, err := getClientID(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
canMint, err := s.checkSolaceNetCapability(ctx, callerID, "tokenization.mint")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to check SolaceNet capability: %v", err)
|
|
}
|
|
if !canMint {
|
|
return fmt.Errorf("client %s does not have tokenization.mint capability", callerID)
|
|
}
|
|
|
|
// Create tokenized asset
|
|
asset := TokenizedAsset{
|
|
TokenID: request.TokenID,
|
|
UnderlyingAsset: request.UnderlyingAsset,
|
|
Amount: request.Amount,
|
|
Issuer: request.Issuer,
|
|
BackingReserve: "1:1",
|
|
Status: "minted",
|
|
RegulatoryFlags: request.RegulatoryFlags,
|
|
CreatedAt: time.Now().Format(time.RFC3339),
|
|
UpdatedAt: time.Now().Format(time.RFC3339),
|
|
}
|
|
|
|
assetJSON, err := json.Marshal(asset)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = ctx.GetStub().PutState(request.TokenID, assetJSON)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to put asset to world state: %v", err)
|
|
}
|
|
|
|
// Emit event
|
|
eventPayload := fmt.Sprintf(`{"tokenId":"%s","action":"mint","amount":"%s","issuer":"%s"}`,
|
|
request.TokenID, request.Amount, request.Issuer)
|
|
err = ctx.GetStub().SetEvent("TokenMinted", []byte(eventPayload))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to emit event: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// TransferToken transfers tokenized assets with regulatory checks
|
|
func (s *TokenizedAssetContract) TransferToken(ctx contractapi.TransactionContextInterface, requestJSON string) error {
|
|
var request TransferRequest
|
|
err := json.Unmarshal([]byte(requestJSON), &request)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to unmarshal transfer request: %v", err)
|
|
}
|
|
|
|
// Get token
|
|
assetJSON, err := ctx.GetStub().GetState(request.TokenID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read token from world state: %v", err)
|
|
}
|
|
if assetJSON == nil {
|
|
return fmt.Errorf("token %s does not exist", request.TokenID)
|
|
}
|
|
|
|
var asset TokenizedAsset
|
|
err = json.Unmarshal(assetJSON, &asset)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Verify sender has permission
|
|
callerID, err := getClientID(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if asset.Issuer != callerID && request.From != callerID {
|
|
return fmt.Errorf("client %s is not authorized to transfer this token", callerID)
|
|
}
|
|
|
|
// Check SolaceNet capability
|
|
canTransfer, err := s.checkSolaceNetCapability(ctx, callerID, "tokenization.transfer")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to check SolaceNet capability: %v", err)
|
|
}
|
|
if !canTransfer {
|
|
return fmt.Errorf("client %s does not have tokenization.transfer capability", callerID)
|
|
}
|
|
|
|
// Verify amounts (simplified - in production, use proper decimal handling)
|
|
requestAmount, err := strconv.ParseFloat(request.Amount, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid amount: %v", err)
|
|
}
|
|
|
|
currentAmount, err := strconv.ParseFloat(asset.Amount, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid current amount: %v", err)
|
|
}
|
|
|
|
if requestAmount > currentAmount {
|
|
return fmt.Errorf("insufficient balance: requested %f, available %f", requestAmount, currentAmount)
|
|
}
|
|
|
|
// Update token
|
|
newAmount := currentAmount - requestAmount
|
|
asset.Amount = fmt.Sprintf("%.2f", newAmount)
|
|
asset.Status = "transferred"
|
|
asset.UpdatedAt = time.Now().Format(time.RFC3339)
|
|
|
|
// Merge regulatory flags
|
|
for k, v := range request.Regulatory {
|
|
asset.RegulatoryFlags[k] = v
|
|
}
|
|
|
|
updatedJSON, err := json.Marshal(asset)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = ctx.GetStub().PutState(request.TokenID, updatedJSON)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update asset in world state: %v", err)
|
|
}
|
|
|
|
// Emit event
|
|
eventPayload := fmt.Sprintf(`{"tokenId":"%s","action":"transfer","from":"%s","to":"%s","amount":"%s"}`,
|
|
request.TokenID, request.From, request.To, request.Amount)
|
|
err = ctx.GetStub().SetEvent("TokenTransferred", []byte(eventPayload))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to emit event: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RedeemToken redeems tokenized assets back to underlying asset
|
|
func (s *TokenizedAssetContract) RedeemToken(ctx contractapi.TransactionContextInterface, requestJSON string) error {
|
|
var request RedemptionRequest
|
|
err := json.Unmarshal([]byte(requestJSON), &request)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to unmarshal redemption request: %v", err)
|
|
}
|
|
|
|
// Get token
|
|
assetJSON, err := ctx.GetStub().GetState(request.TokenID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read token from world state: %v", err)
|
|
}
|
|
if assetJSON == nil {
|
|
return fmt.Errorf("token %s does not exist", request.TokenID)
|
|
}
|
|
|
|
var asset TokenizedAsset
|
|
err = json.Unmarshal(assetJSON, &asset)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Verify redemption proof
|
|
if request.RedemptionProof == "" {
|
|
return fmt.Errorf("redemption proof is required")
|
|
}
|
|
|
|
// Check SolaceNet capability
|
|
callerID, err := getClientID(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
canRedeem, err := s.checkSolaceNetCapability(ctx, callerID, "tokenization.redeem")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to check SolaceNet capability: %v", err)
|
|
}
|
|
if !canRedeem {
|
|
return fmt.Errorf("client %s does not have tokenization.redeem capability", callerID)
|
|
}
|
|
|
|
// Verify amounts
|
|
requestAmount, err := strconv.ParseFloat(request.Amount, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid amount: %v", err)
|
|
}
|
|
|
|
currentAmount, err := strconv.ParseFloat(asset.Amount, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid current amount: %v", err)
|
|
}
|
|
|
|
if requestAmount > currentAmount {
|
|
return fmt.Errorf("insufficient balance: requested %f, available %f", requestAmount, currentAmount)
|
|
}
|
|
|
|
// Update token
|
|
newAmount := currentAmount - requestAmount
|
|
asset.Amount = fmt.Sprintf("%.2f", newAmount)
|
|
asset.Status = "redeemed"
|
|
asset.UpdatedAt = time.Now().Format(time.RFC3339)
|
|
|
|
updatedJSON, err := json.Marshal(asset)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = ctx.GetStub().PutState(request.TokenID, updatedJSON)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update asset in world state: %v", err)
|
|
}
|
|
|
|
// Emit event
|
|
eventPayload := fmt.Sprintf(`{"tokenId":"%s","action":"redeem","redeemer":"%s","amount":"%s"}`,
|
|
request.TokenID, request.Redeemer, request.Amount)
|
|
err = ctx.GetStub().SetEvent("TokenRedeemed", []byte(eventPayload))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to emit event: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetToken returns the tokenized asset details
|
|
func (s *TokenizedAssetContract) GetToken(ctx contractapi.TransactionContextInterface, tokenID string) (*TokenizedAsset, error) {
|
|
assetJSON, err := ctx.GetStub().GetState(tokenID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read from world state: %v", err)
|
|
}
|
|
if assetJSON == nil {
|
|
return nil, fmt.Errorf("token %s does not exist", tokenID)
|
|
}
|
|
|
|
var asset TokenizedAsset
|
|
err = json.Unmarshal(assetJSON, &asset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &asset, nil
|
|
}
|
|
|
|
// GetAllTokens returns all tokenized assets (with pagination support)
|
|
func (s *TokenizedAssetContract) GetAllTokens(ctx contractapi.TransactionContextInterface) ([]*TokenizedAsset, error) {
|
|
resultsIterator, err := ctx.GetStub().GetStateByRange("", "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resultsIterator.Close()
|
|
|
|
var assets []*TokenizedAsset
|
|
for resultsIterator.HasNext() {
|
|
queryResponse, err := resultsIterator.Next()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var asset TokenizedAsset
|
|
err = json.Unmarshal(queryResponse.Value, &asset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
assets = append(assets, &asset)
|
|
}
|
|
|
|
return assets, nil
|
|
}
|
|
|
|
// getClientID returns the submitting client's MSP identity ID.
|
|
func getClientID(ctx contractapi.TransactionContextInterface) (string, error) {
|
|
id, err := ctx.GetClientIdentity().GetID()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get client identity: %v", err)
|
|
}
|
|
return id, nil
|
|
}
|
|
|
|
// checkSolaceNetCapability checks if a client has a SolaceNet capability
|
|
// In production, this would call SolaceNet API or use chaincode-to-chaincode invocation
|
|
func (s *TokenizedAssetContract) checkSolaceNetCapability(ctx contractapi.TransactionContextInterface, clientID, capability string) (bool, error) {
|
|
// Placeholder implementation — parameters reserved for SolaceNet integration.
|
|
_ = ctx
|
|
_ = clientID
|
|
_ = capability
|
|
|
|
// In production, this would:
|
|
// 1. Call SolaceNet API via external service
|
|
// 2. Or use chaincode-to-chaincode invocation if SolaceNet is on same network
|
|
// 3. Or use Cacti to bridge to SolaceNet service
|
|
return true, nil
|
|
}
|
|
|
|
func main() {
|
|
chaincode, err := contractapi.NewChaincode(&TokenizedAssetContract{})
|
|
if err != nil {
|
|
fmt.Printf("Error creating tokenized asset chaincode: %v", err)
|
|
return
|
|
}
|
|
|
|
if err := chaincode.Start(); err != nil {
|
|
fmt.Printf("Error starting tokenized asset chaincode: %v", err)
|
|
}
|
|
}
|