Initial commit: Spiderman - Hyperledger Fabric Asset Transfer REST API with complete features

main
huyinsong 3 days ago
commit f374898c6e
  1. 56
      .gitignore
  2. 213
      CONFIG_GUIDE.md
  3. 53
      Dockerfile
  4. 130
      Makefile
  5. 552
      README.md
  6. 152
      cmd/server/main.go
  7. 62
      config.env.example
  8. 29
      docker-compose.yml
  9. 740
      docs/docs.go
  10. 720
      docs/swagger.json
  11. 468
      docs/swagger.yaml
  12. 35
      go.mod
  13. 125
      go.sum
  14. 572
      internal/api/handler.go
  15. 570
      internal/fabric/client.go
  16. 82
      internal/logger/logger.go
  17. 125
      internal/middleware/middleware.go
  18. 133
      pkg/config/config.go
  19. 82
      pkg/models/asset.go
  20. 193
      pkg/models/asset_test.go
  21. 14
      pkg/models/blockchain.go
  22. 20
      pkg/models/response.go
  23. 27
      pkg/models/transaction.go

56
.gitignore vendored

@ -0,0 +1,56 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go compiler
*.out
# Go workspace file
go.work
# Build artifacts
bin/
build/
dist/
# Test coverage
*.out
coverage.html
# Environment configuration files
.env
config.env
# IDE and editor files
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Logs
*.log
# Docker
.dockerignore
# Node.js (if you have any frontend components)
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*

@ -0,0 +1,213 @@
# 环境配置指南
## 📋 概述
本项目支持通过环境变量和配置文件来管理应用程序配置。配置系统具有以下特性:
- 🔧 支持从 `.env``config.env` 文件加载配置
- 🌍 环境变量优先级高于配置文件
- 🎯 针对开发和生产环境的不同配置
- 📚 完整的配置文档和示例
## 🚀 快速开始
### 1. 设置配置文件
```bash
# 复制示例配置文件
cp config.env.example config.env
# 或使用Makefile
make env-setup
```
### 2. 编辑配置
编辑 `config.env` 文件,根据您的环境调整配置:
```bash
# 服务器配置
PORT=8080
# Fabric配置
MSP_ID=Org1MSP
CRYPTO_PATH=../../test-network/organizations/peerOrganizations/org1.example.com
CHANNEL_NAME=mychannel
CHAINCODE_NAME=basic
# 日志配置
LOG_LEVEL=info
LOG_FORMAT=text
# 环境配置
ENVIRONMENT=development
```
### 3. 运行应用
```bash
# 开发环境
make run-dev
# 生产环境
make run-prod
# 或直接运行
make run
```
## 📖 配置选项
### 服务器配置
| 变量 | 默认值 | 描述 |
|------|--------|------|
| `PORT` | `8080` | HTTP服务器端口 |
### Hyperledger Fabric配置
| 变量 | 默认值 | 描述 |
|------|--------|------|
| `MSP_ID` | `Org1MSP` | MSP身份ID |
| `CRYPTO_PATH` | `../../test-network/organizations/...` | 加密材料基础路径 |
| `CERT_PATH` | `{CRYPTO_PATH}/users/.../signcerts` | 证书路径 |
| `KEY_PATH` | `{CRYPTO_PATH}/users/.../keystore` | 私钥路径 |
| `TLS_CERT_PATH` | `{CRYPTO_PATH}/peers/.../tls/ca.crt` | TLS证书路径 |
| `PEER_ENDPOINT` | `dns:///localhost:7051` | Peer节点gRPC端点 |
| `GATEWAY_PEER` | `peer0.org1.example.com` | 网关Peer名称 |
| `CHANNEL_NAME` | `mychannel` | 区块链通道名称 |
| `CHAINCODE_NAME` | `basic` | 链码名称 |
### 日志配置
| 变量 | 默认值 | 可选值 | 描述 |
|------|--------|--------|------|
| `LOG_LEVEL` | `info` | `panic`, `fatal`, `error`, `warn`, `info`, `debug`, `trace` | 日志级别 |
| `LOG_FORMAT` | `text` | `text`, `json` | 日志格式 |
### 环境配置
| 变量 | 默认值 | 可选值 | 描述 |
|------|--------|--------|------|
| `ENVIRONMENT` | `development` | `development`, `production` | 运行环境 |
| `API_TIMEOUT` | `30` | 数字 | API超时时间(秒) |
| `DB_TIMEOUT` | `10` | 数字 | 数据库超时时间(秒) |
## 🔧 配置优先级
配置加载优先级(从高到低):
1. **环境变量** - 系统环境变量
2. **config.env** - 项目配置文件
3. **默认值** - 代码中定义的默认值
## 💡 使用示例
### 开发环境配置
```bash
# config.env for development
ENVIRONMENT=development
LOG_LEVEL=debug
LOG_FORMAT=text
PORT=8080
```
### 生产环境配置
```bash
# config.env for production
ENVIRONMENT=production
LOG_LEVEL=warn
LOG_FORMAT=json
PORT=80
```
### Docker环境配置
```bash
# 通过环境变量传递
docker run -p 8080:8080 \
-e ENVIRONMENT=production \
-e LOG_FORMAT=json \
-e LOG_LEVEL=info \
spiderman
```
## 🎯 环境特定功能
### 开发环境
- ✅ Swagger文档可用 (`/swagger/`)
- 📊 详细的调试日志
- 🔧 热重载支持
### 生产环境
- 🚫 Swagger文档关闭
- 📈 JSON格式日志
- ⚡ 优化的性能设置
## 🛠 Makefile命令
```bash
# 环境配置
make env-setup # 设置配置文件
make show-config # 显示当前配置
# 运行命令
make run # 使用配置文件运行
make run-dev # 开发环境运行
make run-prod # 生产环境运行
# Docker命令
make docker-run # Docker运行
make docker-run-dev # Docker开发环境运行
```
## 🔍 配置验证
检查当前配置:
```bash
make show-config
```
运行时配置验证:
```bash
# 启动应用时会显示加载的配置
make run-dev
```
## 🚨 安全注意事项
1. **不要提交包含敏感信息的配置文件到版本控制**
2. **生产环境使用环境变量而不是配置文件**
3. **定期轮换证书和密钥**
4. **使用适当的文件权限保护配置文件**
## 🔧 故障排除
### 常见问题
**Q: 配置文件没有生效?**
A: 检查文件名是否正确(`config.env`),环境变量优先级更高
**Q: Fabric连接失败?**
A: 检查 `CRYPTO_PATH` 和相关证书路径是否正确
**Q: 日志级别没有改变?**
A: 确保 `LOG_LEVEL` 值正确,重启应用程序
### 调试配置
启用调试模式查看配置加载过程:
```bash
LOG_LEVEL=debug make run
```
## 📚 参考资料
- [Go环境变量最佳实践](https://12factor.net/config)
- [Hyperledger Fabric配置文档](https://hyperledger-fabric.readthedocs.io/)
- [项目结构说明](./PROJECT_STRUCTURE.md)

@ -0,0 +1,53 @@
# Build stage
FROM golang:1.23-alpine AS builder
# Install git for go mod download
RUN apk add --no-cache git
# Set working directory
WORKDIR /app
# Copy go mod files
COPY go.mod go.sum ./
# Download dependencies
RUN go mod download
# Copy source code
COPY . .
# Build the application
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o bin/spiderman cmd/server/main.go
# Final stage
FROM alpine:3.18
# Install ca-certificates for HTTPS requests
RUN apk --no-cache add ca-certificates
# Create non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /root/
# Copy the binary from builder stage
COPY --from=builder /app/bin/spiderman .
# Copy any needed configuration files
COPY --from=builder /app/docs ./docs
# Change ownership to non-root user
RUN chown -R appuser:appgroup /root/
# Switch to non-root user
USER appuser
# Expose port
EXPOSE 8080
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
# Run the application
CMD ["./spiderman"]

@ -0,0 +1,130 @@
# Hyperledger Fabric Asset Transfer API Makefile
.PHONY: help build run test clean swagger docker-build docker-run lint fmt vet tidy env-setup
# Default target
help: ## Show this help message
@echo 'Usage: make [target]'
@echo ''
@echo 'Targets:'
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " %-15s %s\n", $$1, $$2}' $(MAKEFILE_LIST)
# Setup environment configuration
env-setup: ## Setup environment configuration files
@echo "Setting up environment configuration..."
@if [ ! -f config.env ]; then \
echo "Creating config.env from example..."; \
cp config.env.example config.env; \
echo "Please edit config.env with your specific configuration"; \
else \
echo "config.env already exists"; \
fi
# Build the application
build: ## Build the application binary
@echo "Building application..."
go build -o bin/asset-transfer-api cmd/server/main.go
# Run the application
run: ## Run the application
@echo "Running application..."
go run cmd/server/main.go
# Run the application in development mode
run-dev: ## Run the application in development mode
@echo "Running application in development mode..."
ENVIRONMENT=development go run cmd/server/main.go
# Run the application in production mode
run-prod: ## Run the application in production mode
@echo "Running application in production mode..."
ENVIRONMENT=production LOG_FORMAT=json go run cmd/server/main.go
# Run tests
test: ## Run tests
@echo "Running tests..."
go test -v ./...
# Run tests with coverage
test-coverage: ## Run tests with coverage
@echo "Running tests with coverage..."
go test -v -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
# Clean build artifacts
clean: ## Clean build artifacts
@echo "Cleaning..."
rm -rf bin/
rm -f coverage.out coverage.html
# Generate swagger documentation
swagger: ## Generate swagger documentation
@echo "Generating swagger documentation..."
swag init -g cmd/server/main.go -o docs
# Build docker image
docker-build: ## Build docker image
@echo "Building docker image..."
docker build -t spiderman .
# Run docker container
docker-run: ## Run docker container
@echo "Running docker container..."
docker run -p 8080:8080 spiderman
# Run docker container in development mode
docker-run-dev: ## Run docker container in development mode
@echo "Running docker container in development mode..."
docker run -p 8080:8080 -e ENVIRONMENT=development spiderman
# Run linting
lint: ## Run golangci-lint
@echo "Running linter..."
golangci-lint run
# Format code
fmt: ## Format Go code
@echo "Formatting code..."
go fmt ./...
# Run go vet
vet: ## Run go vet
@echo "Running go vet..."
go vet ./...
# Tidy dependencies
tidy: ## Tidy go modules
@echo "Tidying dependencies..."
go mod tidy
# Development setup
dev-setup: env-setup deps ## Setup development environment
@echo "Setting up development environment..."
go mod download
go install github.com/swaggo/swag/cmd/swag@latest
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
@echo "Development environment setup complete!"
@echo ""
@echo "Next steps:"
@echo " 1. Edit config.env with your Fabric network configuration"
@echo " 2. Run 'make run-dev' to start in development mode"
@echo " 3. Visit http://localhost:8080/swagger/ for API documentation"
# Check code quality
check: fmt vet lint test ## Run all checks (format, vet, lint, test)
# Install dependencies
deps: ## Download dependencies
@echo "Downloading dependencies..."
go mod download
go mod verify
# Show current configuration
show-config: ## Show current configuration from environment
@echo "Current Configuration:"
@echo "PORT: $(shell echo $${PORT:-8080})"
@echo "ENVIRONMENT: $(shell echo $${ENVIRONMENT:-development})"
@echo "LOG_LEVEL: $(shell echo $${LOG_LEVEL:-info})"
@echo "LOG_FORMAT: $(shell echo $${LOG_FORMAT:-text})"
@echo "CHANNEL_NAME: $(shell echo $${CHANNEL_NAME:-mychannel})"
@echo "CHAINCODE_NAME: $(shell echo $${CHAINCODE_NAME:-basic})"

@ -0,0 +1,552 @@
# Spiderman - Hyperledger Fabric Asset Transfer REST API
这是一个基于Hyperledger Fabric的资产转移REST API服务,代号为Spiderman,提供了完整的RESTful接口来管理区块链上的资产。
## 功能特性
- 初始化账本
- 创建资产
- 查询所有资产
- 根据ID查询资产
- 转移资产所有权
- 更新资产信息
- **完整的Swagger API文档**
- **交互式API测试界面**
## 先决条件
1. Go 1.23.0 或更高版本
2. Hyperledger Fabric网络已运行
3. 正确配置的证书和密钥文件
## 安装和运行
1. 下载依赖项:
```bash
go mod tidy
```
2. 启动服务:
```bash
go run .
```
服务默认在端口8080上运行,可以通过环境变量`PORT`自定义端口。
## API 接口
### 基础URL
```
http://localhost:8080/api/v1
```
### 🔍 Swagger API 文档
**访问地址:** `http://localhost:8080/swagger/index.html`
Swagger提供完整的API文档和交互式测试界面,您可以:
- 查看所有API端点的详细信息
- 在线测试API接口
- 查看请求/响应示例
- 下载API规范文件(JSON/YAML)
### 健康检查
```http
GET /health
```
### 1. 初始化账本
初始化区块链账本,创建初始资产集合。
```http
POST /api/v1/ledger/init
```
**响应示例:**
```json
{
"success": true,
"message": "Ledger initialized successfully"
}
```
### 2. 获取所有资产
查询账本上的所有资产。
```http
GET /api/v1/assets
```
**响应示例:**
```json
{
"success": true,
"data": [
{
"ID": "asset1",
"color": "blue",
"size": "5",
"owner": "Tomoko",
"appraisedValue": "1000"
}
]
}
```
### 3. 创建资产
创建一个新的资产。
```http
POST /api/v1/assets
```
**请求体:**
```json
{
"id": "asset123",
"color": "red",
"size": "10",
"owner": "Alice",
"appraisedValue": "2000"
}
```
**响应示例:**
```json
{
"success": true,
"message": "Asset created successfully",
"data": {
"ID": "asset123",
"color": "red",
"size": "10",
"owner": "Alice",
"appraisedValue": "2000"
}
}
```
### 4. 根据ID查询资产
根据资产ID查询特定资产。
```http
GET /api/v1/assets/{id}
```
**响应示例:**
```json
{
"success": true,
"data": {
"ID": "asset123",
"color": "red",
"size": "10",
"owner": "Alice",
"appraisedValue": "2000"
}
}
```
### 5. 转移资产所有权
转移资产的所有权给新的拥有者。
```http
PUT /api/v1/assets/{id}/transfer
```
**请求体:**
```json
{
"newOwner": "Bob"
}
```
**响应示例:**
```json
{
"success": true,
"message": "Asset asset123 transferred to Bob successfully"
}
```
### 6. 更新资产
更新现有资产的信息。
```http
PUT /api/v1/assets/{id}
```
**请求体:**
```json
{
"color": "green",
"size": "15",
"owner": "Charlie",
"appraisedValue": "3000"
}
```
**响应示例:**
```json
{
"success": true,
"message": "Asset updated successfully",
"data": {
"ID": "asset123",
"color": "green",
"size": "15",
"owner": "Charlie",
"appraisedValue": "3000"
}
}
```
## API 端点
### 健康检查
- `GET /health` - 检查服务健康状态
### 账本操作
- `POST /api/v1/ledger/init` - 初始化账本
### 资产操作
- `GET /api/v1/assets` - 获取所有资产
- `POST /api/v1/assets` - 创建新资产
- `GET /api/v1/assets/{id}` - 根据ID获取资产
- `PUT /api/v1/assets/{id}` - 更新资产信息
- `PUT /api/v1/assets/{id}/transfer` - 转移资产所有权
### 区块链操作
- `GET /api/v1/blockchain/height` - 获取当前区块高度
- `GET /api/v1/blockchain/info` - 获取区块链信息
### 交易操作
- `GET /api/v1/transactions/{txid}` - 根据交易ID查询交易详情
### 使用示例
#### 获取区块高度
```bash
curl -X GET "http://localhost:8080/api/v1/blockchain/height"
```
响应示例:
```json
{
"success": true,
"message": "Block height retrieved successfully",
"data": {
"height": 12345
}
}
```
#### 获取区块链信息
```bash
curl -X GET "http://localhost:8080/api/v1/blockchain/info"
```
响应示例:
```json
{
"success": true,
"message": "Chain information retrieved successfully",
"data": {
"height": 12345,
"chainName": "mychannel"
}
}
```
#### 查询交易详情
```bash
curl -X GET "http://localhost:8080/api/v1/transactions/{txid}"
```
响应示例:
```json
{
"success": true,
"message": "Transaction details retrieved successfully",
"data": {
"transactionId": "d341d13093e72e201f73725b4d93b45b2e261fd7440e3e624f141f6f8d509495",
"blockNumber": 79,
"blockHash": "block-hash-for-d341d130",
"timestamp": "2025-06-12T13:38:14+08:00",
"channelId": "mychannel",
"creatorMspId": "",
"creatorId": "",
"endorsers": null,
"chaincodeId": "",
"function": "",
"arguments": null,
"response": {
"status": 200,
"message": "Transaction completed successfully",
"payload": ""
},
"validationCode": "VALID",
"rawTransaction": {
"hasData": true,
"size": 4822
}
}
}
```
## 错误处理
API使用标准HTTP状态码表示请求结果:
- `200 OK` - 请求成功
- `201 Created` - 资源创建成功
- `400 Bad Request` - 请求参数错误
- `404 Not Found` - 资源未找到
- `500 Internal Server Error` - 服务器内部错误
错误响应格式:
```json
{
"success": false,
"message": "错误描述信息"
}
```
## 配置
服务使用以下环境变量进行配置:
- `PORT` - 服务端口(默认:8080)
- `CHAINCODE_NAME` - 链码名称(默认:basic)
- `CHANNEL_NAME` - 通道名称(默认:mychannel)
## 证书配置
确保以下路径存在正确的证书文件:
- `../../test-network/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/signcerts`
- `../../test-network/organizations/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/keystore`
- `../../test-network/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt`
## 使用示例
使用curl测试API:
```bash
# 健康检查
curl http://localhost:8080/health
# 初始化账本
curl -X POST http://localhost:8080/api/v1/ledger/init
# 创建资产
curl -X POST http://localhost:8080/api/v1/assets \
-H "Content-Type: application/json" \
-d '{
"id": "asset123",
"color": "red",
"size": "10",
"owner": "Alice",
"appraisedValue": "2000"
}'
# 查询所有资产
curl http://localhost:8080/api/v1/assets
# 查询特定资产
curl http://localhost:8080/api/v1/assets/asset123
# 转移资产
curl -X PUT http://localhost:8080/api/v1/assets/asset123/transfer \
-H "Content-Type: application/json" \
-d '{"newOwner": "Bob"}'
```
## 容器化部署
### 使用Docker构建和运行
```bash
# 构建镜像
docker build -t asset-transfer-api .
# 运行容器
docker run -p 8080:8080 asset-transfer-api
```
### 使用Docker Compose
```bash
# 启动服务
docker-compose up -d
# 查看日志
docker-compose logs -f
# 停止服务
docker-compose down
```
## 项目结构
```
.
├── main.go # 主服务入口和路由配置
├── fabric_client.go # Fabric客户端封装
├── api_handler.go # HTTP请求处理器
├── docs/ # Swagger生成的API文档
│ ├── docs.go # Go文档
│ ├── swagger.json # JSON格式API规范
│ └── swagger.yaml # YAML格式API规范
├── go.mod # Go模块配置
├── go.sum # 依赖项校验和
├── Dockerfile # Docker镜像构建文件
├── docker-compose.yml # Docker Compose配置
├── start.sh # 启动脚本
├── test_api.sh # API测试脚本
└── README.md # 项目文档
```
## 📖 Swagger 配置
本项目的 Swagger 文档配置现在支持通过 `config.env` 文件动态设置 host 和 port。
### 配置方式
`config.env` 文件中设置以下参数:
```bash
# Server Configuration
HOST=localhost # Swagger 文档的主机地址
PORT=8888 # Swagger 文档的端口号
```
### 访问地址
启动服务后,Swagger 文档将在以下地址可用:
- Swagger UI: `http://{HOST}:{PORT}/swagger/index.html`
- API 基础路径: `http://{HOST}:{PORT}/api/v1`
例如,使用默认配置时:
- Swagger UI: http://localhost:8888/swagger/index.html
- API 基础路径: http://localhost:8888/api/v1
### 自定义配置示例
如果你需要在不同的主机或端口上运行:
```bash
# config.env
HOST=192.168.1.100
PORT=9090
```
服务启动后会自动更新 Swagger 配置,无需手动重新生成文档。
## 🔧 故障排除
### API访问错误解决方案
如果在使用curl访问API时遇到错误,请按以下步骤排查:
#### 1. JSON反序列化错误
**错误信息**: `cannot unmarshal number into Go struct field Asset.appraisedValue of type string`
**原因**: Hyperledger Fabric链码中的初始数据的`appraisedValue`字段是数字类型,但API代码期望字符串类型。
**解决方案**: 已在`pkg/models/asset.go`中实现自定义JSON反序列化方法:
```go
// 自定义UnmarshalJSON方法处理数字和字符串类型的appraisedValue
func (a *Asset) UnmarshalJSON(data []byte) error {
// 支持string, float64, int, int64类型的自动转换
}
```
#### 2. gRPC连接超时错误
**错误信息**: `gRPC error: context deadline exceeded`
**原因**: Hyperledger Fabric网络没有运行或配置不正确。
**解决方案**:
1. 确保Fabric网络正在运行:
```bash
# 在fabric-samples/test-network目录下
./network.sh up createChannel -ca -c mychannel
./network.sh deployCC -ccn basic -ccp ../asset-transfer-basic/chaincode-go -ccl go
```
2. 检查网络状态:
```bash
docker ps # 应该看到peer, orderer, ca等容器
```
3. 验证配置文件中的路径:
```bash
# config.env中的CRYPTO_PATH是否指向正确的证书目录
CRYPTO_PATH=../../test-network/organizations/peerOrganizations/org1.example.com
```
#### 3. 端口冲突
**错误信息**: `bind: address already in use`
**解决方案**:
```bash
# 查找占用端口的进程
lsof -i :8888
# 停止进程
kill -9 <PID>
```
#### 4. 证书路径错误
**错误信息**: `failed to read certificate file``failed to read private key file`
**解决方案**:
1. 检查证书路径是否存在:
```bash
ls -la ../../test-network/organizations/peerOrganizations/org1.example.com/
```
2. 确保当前目录正确:
```bash
pwd # 应该在项目根目录
```
### 测试API连接
1. **健康检查**:
```bash
curl http://localhost:8888/health
```
2. **初始化账本** (首次使用):
```bash
curl -X POST http://localhost:8888/api/v1/ledger/init
```
3. **获取所有资产**:
```bash
curl http://localhost:8888/api/v1/assets
```
### 常见问题
**Q: API返回空数组`{"success":true,"data":[]}`?**
A: 账本可能没有初始化,运行初始化命令:
```bash
curl -X POST http://localhost:8888/api/v1/ledger/init
```
**Q: Swagger UI无法访问?**
A: 确保在开发环境下运行:
```bash
ENVIRONMENT=development go run cmd/server/main.go
```
**Q: 修改代码后错误依然存在?**
A: 清理缓存并重新启动:
```bash
go clean -cache
go mod tidy
go run cmd/server/main.go
```

@ -0,0 +1,152 @@
// Package main provides the main entry point for the Hyperledger Fabric Asset Transfer REST API
//
// @title Hyperledger Fabric Asset Transfer API
// @version 1.0
// @description RESTful API for managing assets on Hyperledger Fabric blockchain
// @termsOfService http://swagger.io/terms/
//
// @contact.name API Support
// @contact.url http://www.swagger.io/support
// @contact.email support@swagger.io
//
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
//
// @host localhost:8888
// @BasePath /api/v1
//
// @schemes http https
package main
import (
"fmt"
"log"
"net/http"
"spiderman/docs" // Import generated docs
"spiderman/internal/api"
"spiderman/internal/fabric"
"spiderman/internal/logger"
"spiderman/internal/middleware"
"spiderman/pkg/config"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
httpSwagger "github.com/swaggo/http-swagger"
)
func main() {
// Load configuration from environment variables and .env files
cfg := config.LoadConfig()
// Update Swagger configuration with values from config.env
docs.SwaggerInfo.Host = fmt.Sprintf("%s:%s", cfg.Server.Host, cfg.Server.Port)
// Initialize logger
logger.Init(&cfg.Log)
// Log startup information
logger.Logger.WithFields(logrus.Fields{
"environment": cfg.Environment.Environment,
"host": cfg.Server.Host,
"port": cfg.Server.Port,
"swagger_host": docs.SwaggerInfo.Host,
"log_level": cfg.Log.Level,
"log_format": cfg.Log.Format,
}).Info("Starting Hyperledger Fabric Asset Transfer REST API")
// Initialize Fabric client
logger.Logger.Info("Initializing Fabric client...")
fabricClient, err := fabric.NewClient(&cfg.Fabric)
if err != nil {
logger.Logger.WithError(err).Fatal("Failed to initialize Fabric client")
}
defer fabricClient.Close()
// Create API handler
apiHandler := api.NewHandler(fabricClient)
// Setup router with middleware
router := setupRouter(apiHandler, cfg)
// Start server
startServer(router, cfg)
}
func setupRouter(apiHandler *api.Handler, cfg *config.Config) *mux.Router {
router := mux.NewRouter()
// Add middleware in order
router.Use(middleware.Recovery)
router.Use(middleware.CORS)
router.Use(middleware.Logging)
// Swagger documentation route (only in development)
if cfg.IsDevelopment() {
router.PathPrefix("/swagger/").Handler(httpSwagger.WrapHandler)
logger.Logger.Info("Swagger documentation enabled at /swagger/")
}
// API routes
api := router.PathPrefix("/api/v1").Subrouter()
// Health check
router.HandleFunc("/health", apiHandler.HealthCheck).Methods("GET").Name("HealthCheck")
// Ledger operations
api.HandleFunc("/ledger/init", apiHandler.InitLedger).Methods("POST").Name("InitLedger")
// Asset operations with OPTIONS support
api.HandleFunc("/assets", apiHandler.GetAllAssets).Methods("GET").Name("GetAllAssets")
api.HandleFunc("/assets", apiHandler.CreateAsset).Methods("POST").Name("CreateAsset")
api.HandleFunc("/assets", func(w http.ResponseWriter, r *http.Request) {}).Methods("OPTIONS").Name("AssetsOptions")
api.HandleFunc("/assets/{id}", apiHandler.GetAssetByID).Methods("GET").Name("GetAssetByID")
api.HandleFunc("/assets/{id}/transfer", apiHandler.TransferAsset).Methods("PUT").Name("TransferAsset")
api.HandleFunc("/assets/{id}", apiHandler.UpdateAsset).Methods("PUT").Name("UpdateAsset")
api.HandleFunc("/assets/{id}", func(w http.ResponseWriter, r *http.Request) {}).Methods("OPTIONS").Name("AssetByIdOptions")
api.HandleFunc("/assets/{id}/transfer", func(w http.ResponseWriter, r *http.Request) {}).Methods("OPTIONS").Name("TransferAssetOptions")
// Blockchain operations
api.HandleFunc("/blockchain/height", apiHandler.GetBlockHeight).Methods("GET").Name("GetBlockHeight")
api.HandleFunc("/blockchain/info", apiHandler.GetChainInfo).Methods("GET").Name("GetChainInfo")
api.HandleFunc("/blockchain/height", func(w http.ResponseWriter, r *http.Request) {}).Methods("OPTIONS").Name("BlockHeightOptions")
api.HandleFunc("/blockchain/info", func(w http.ResponseWriter, r *http.Request) {}).Methods("OPTIONS").Name("ChainInfoOptions")
// Transaction operations
api.HandleFunc("/transactions/{txid}", apiHandler.GetTransactionByID).Methods("GET").Name("GetTransactionByID")
api.HandleFunc("/transactions/{txid}", func(w http.ResponseWriter, r *http.Request) {}).Methods("OPTIONS").Name("TransactionByIdOptions")
return router
}
func startServer(router *mux.Router, cfg *config.Config) {
host := cfg.Server.Host
port := cfg.Server.Port
logger.Logger.WithFields(logrus.Fields{
"host": host,
"port": port,
"environment": cfg.Environment.Environment,
"swagger_url": fmt.Sprintf("http://%s:%s/swagger/index.html", host, port),
"health_url": fmt.Sprintf("http://%s:%s/health", host, port),
"api_base_url": fmt.Sprintf("http://%s:%s/api/v1", host, port),
}).Info("Server configuration")
fmt.Printf("Starting REST API server on %s:%s\n", host, port)
fmt.Printf("Environment: %s\n", cfg.Environment.Environment)
fmt.Printf("Health check: http://%s:%s/health\n", host, port)
if cfg.IsDevelopment() {
fmt.Printf("Swagger documentation: http://%s:%s/swagger/index.html\n", host, port)
}
logger.Logger.WithFields(logrus.Fields{
"address": ":" + port,
}).Info("Starting HTTP server")
if err := http.ListenAndServe(":"+port, router); err != nil {
log.Fatal("Failed to start server: ", err)
}
}

@ -0,0 +1,62 @@
# Hyperledger Fabric Asset Transfer API Environment Configuration
# Copy this file to config.env and modify the values as needed
# =============================================================================
# Server Configuration
# =============================================================================
HOST=localhost
PORT=8080
# =============================================================================
# Hyperledger Fabric Configuration
# =============================================================================
# MSP (Membership Service Provider) ID
MSP_ID=Org1MSP
# Base path for cryptographic materials
CRYPTO_PATH=../../test-network/organizations/peerOrganizations/org1.example.com
# Certificate path (optional, defaults to CRYPTO_PATH/users/User1@org1.example.com/msp/signcerts)
CERT_PATH=
# Private key path (optional, defaults to CRYPTO_PATH/users/User1@org1.example.com/msp/keystore)
KEY_PATH=
# TLS certificate path (optional, defaults to CRYPTO_PATH/peers/peer0.org1.example.com/tls/ca.crt)
TLS_CERT_PATH=
# Peer endpoint for gRPC connection
PEER_ENDPOINT=dns:///localhost:7051
# Gateway peer name (used for TLS verification)
GATEWAY_PEER=peer0.org1.example.com
# Fabric network channel name
CHANNEL_NAME=mychannel
# Chaincode name deployed on the channel
CHAINCODE_NAME=basic
# =============================================================================
# Logging Configuration
# =============================================================================
# Log level: panic, fatal, error, warn, info, debug, trace
LOG_LEVEL=info
# Log format: text, json
LOG_FORMAT=text
# =============================================================================
# Development Configuration
# =============================================================================
# Set to development for additional debug features
ENVIRONMENT=production
# API request timeout in seconds
API_TIMEOUT=30
# Database connection timeout in seconds (if using database)
DB_TIMEOUT=10

@ -0,0 +1,29 @@
version: '3.8'
services:
asset-transfer-api:
build: .
ports:
- "8080:8080"
environment:
- PORT=8080
- CHAINCODE_NAME=basic
- CHANNEL_NAME=mychannel
- LOG_LEVEL=info
- LOG_FORMAT=json
volumes:
# 挂载Fabric证书目录(根据实际路径调整)
- ../../test-network:/test-network:ro
networks:
- fabric-network
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
networks:
fabric-network:
external: true

@ -0,0 +1,740 @@
// Package docs Code generated by swaggo/swag. DO NOT EDIT
package docs
import "github.com/swaggo/swag"
const docTemplate = `{
"schemes": {{ marshal .Schemes }},
"swagger": "2.0",
"info": {
"description": "{{escape .Description}}",
"title": "{{.Title}}",
"termsOfService": "http://swagger.io/terms/",
"contact": {
"name": "API Support",
"url": "http://www.swagger.io/support",
"email": "support@swagger.io"
},
"license": {
"name": "Apache 2.0",
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
},
"version": "{{.Version}}"
},
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/assets": {
"get": {
"description": "Retrieve all assets from the blockchain ledger",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"assets"
],
"summary": "Get all assets",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/models.Response"
},
{
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Asset"
}
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
},
"post": {
"description": "Create a new asset on the blockchain ledger with auto-generated UUID if ID not provided",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"assets"
],
"summary": "Create a new asset",
"parameters": [
{
"description": "Asset data (ID is optional and will be auto-generated)",
"name": "asset",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.CreateAssetRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"allOf": [
{
"$ref": "#/definitions/models.Response"
},
{
"type": "object",
"properties": {
"data": {
"type": "object",
"properties": {
"asset": {
"$ref": "#/definitions/models.Asset"
},
"transactionId": {
"type": "string"
}
}
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
}
},
"/assets/{id}": {
"get": {
"description": "Retrieve a specific asset by its ID from the blockchain ledger",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"assets"
],
"summary": "Get asset by ID",
"parameters": [
{
"type": "string",
"description": "Asset ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/models.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.Asset"
}
}
}
]
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
},
"put": {
"description": "Update an existing asset's information on the blockchain",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"assets"
],
"summary": "Update an existing asset",
"parameters": [
{
"type": "string",
"description": "Asset ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Updated asset data",
"name": "asset",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.CreateAssetRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/models.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.Asset"
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
}
},
"/assets/{id}/transfer": {
"put": {
"description": "Transfer ownership of an asset to a new owner",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"assets"
],
"summary": "Transfer asset ownership",
"parameters": [
{
"type": "string",
"description": "Asset ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Transfer request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.TransferAssetRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Response"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
}
},
"/blockchain/height": {
"get": {
"description": "Get the current block height of the blockchain",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"blockchain"
],
"summary": "Get block height",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/models.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.BlockHeightResponse"
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
}
},
"/blockchain/info": {
"get": {
"description": "Get detailed information about the blockchain including block height",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"blockchain"
],
"summary": "Get chain information",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/models.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.ChainInfoResponse"
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
}
},
"/health": {
"get": {
"description": "Check the health status of the API",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"health"
],
"summary": "Health check",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.HealthResponse"
}
}
}
}
},
"/ledger/init": {
"post": {
"description": "Initialize the blockchain ledger with a set of assets",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"ledger"
],
"summary": "Initialize ledger",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
}
},
"/transactions/{txid}": {
"get": {
"description": "Retrieve detailed information about a specific transaction by its ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"transactions"
],
"summary": "Get transaction details by ID",
"parameters": [
{
"type": "string",
"description": "Transaction ID",
"name": "txid",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/models.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.TransactionDetail"
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
}
}
},
"definitions": {
"models.Asset": {
"type": "object",
"properties": {
"ID": {
"type": "string",
"example": "asset123"
},
"appraisedValue": {
"type": "string",
"example": "2000"
},
"color": {
"type": "string",
"example": "red"
},
"owner": {
"type": "string",
"example": "Alice"
},
"size": {
"type": "string",
"example": "10"
}
}
},
"models.BlockHeightResponse": {
"type": "object",
"properties": {
"height": {
"type": "integer",
"example": 12345
}
}
},
"models.ChainInfoResponse": {
"type": "object",
"properties": {
"blockHash": {
"type": "string",
"example": "a1b2c3d4e5f6..."
},
"chainName": {
"type": "string",
"example": "mychannel"
},
"height": {
"type": "integer",
"example": 12345
}
}
},
"models.CreateAssetRequest": {
"type": "object",
"properties": {
"appraisedValue": {
"type": "string",
"example": "2000"
},
"color": {
"type": "string",
"example": "red"
},
"id": {
"description": "Optional - will be auto-generated if not provided",
"type": "string",
"example": "asset123"
},
"owner": {
"type": "string",
"example": "Alice"
},
"size": {
"type": "string",
"example": "10"
}
}
},
"models.ErrorResponse": {
"type": "object",
"properties": {
"message": {
"type": "string"
},
"success": {
"type": "boolean"
}
}
},
"models.HealthResponse": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "healthy"
}
}
},
"models.Response": {
"type": "object",
"properties": {
"data": {},
"message": {
"type": "string"
},
"success": {
"type": "boolean"
}
}
},
"models.TransactionDetail": {
"type": "object",
"properties": {
"arguments": {
"type": "array",
"items": {
"type": "string"
},
"example": [
"asset1",
"red",
"10",
"Alice",
"1000"
]
},
"blockHash": {
"type": "string",
"example": "a1b2c3d4e5f6..."
},
"blockNumber": {
"type": "integer",
"example": 123
},
"chaincodeId": {
"type": "string",
"example": "basic"
},
"channelId": {
"type": "string",
"example": "mychannel"
},
"creatorId": {
"type": "string",
"example": "User1@org1.example.com"
},
"creatorMspId": {
"type": "string",
"example": "Org1MSP"
},
"endorsers": {
"type": "array",
"items": {
"type": "string"
},
"example": [
"peer0.org1.example.com",
"peer0.org2.example.com"
]
},
"function": {
"type": "string",
"example": "CreateAsset"
},
"rawTransaction": {
"type": "object",
"additionalProperties": true
},
"response": {
"$ref": "#/definitions/models.TransactionResponse"
},
"timestamp": {
"type": "string",
"example": "2024-01-15T10:30:00Z"
},
"transactionId": {
"type": "string",
"example": "f973e40540e3b629b7dffd0b91b87aa3474bc31de89e11b01f749bbc5e6d5add"
},
"validationCode": {
"type": "string",
"example": "VALID"
}
}
},
"models.TransactionResponse": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "Transaction completed successfully"
},
"payload": {
"type": "string",
"example": ""
},
"status": {
"type": "integer",
"example": 200
}
}
},
"models.TransferAssetRequest": {
"type": "object",
"properties": {
"newOwner": {
"type": "string",
"example": "Bob"
}
}
}
}
}`
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{
Version: "1.0",
Host: "localhost:8888",
BasePath: "/api/v1",
Schemes: []string{"http", "https"},
Title: "Hyperledger Fabric Asset Transfer API",
Description: "RESTful API for managing assets on Hyperledger Fabric blockchain",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,
LeftDelim: "{{",
RightDelim: "}}",
}
func init() {
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
}

@ -0,0 +1,720 @@
{
"schemes": [
"http",
"https"
],
"swagger": "2.0",
"info": {
"description": "RESTful API for managing assets on Hyperledger Fabric blockchain",
"title": "Hyperledger Fabric Asset Transfer API",
"termsOfService": "http://swagger.io/terms/",
"contact": {
"name": "API Support",
"url": "http://www.swagger.io/support",
"email": "support@swagger.io"
},
"license": {
"name": "Apache 2.0",
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
},
"version": "1.0"
},
"host": "localhost:8888",
"basePath": "/api/v1",
"paths": {
"/assets": {
"get": {
"description": "Retrieve all assets from the blockchain ledger",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"assets"
],
"summary": "Get all assets",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/models.Response"
},
{
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Asset"
}
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
},
"post": {
"description": "Create a new asset on the blockchain ledger with auto-generated UUID if ID not provided",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"assets"
],
"summary": "Create a new asset",
"parameters": [
{
"description": "Asset data (ID is optional and will be auto-generated)",
"name": "asset",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.CreateAssetRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"allOf": [
{
"$ref": "#/definitions/models.Response"
},
{
"type": "object",
"properties": {
"data": {
"type": "object",
"properties": {
"asset": {
"$ref": "#/definitions/models.Asset"
},
"transactionId": {
"type": "string"
}
}
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
}
},
"/assets/{id}": {
"get": {
"description": "Retrieve a specific asset by its ID from the blockchain ledger",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"assets"
],
"summary": "Get asset by ID",
"parameters": [
{
"type": "string",
"description": "Asset ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/models.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.Asset"
}
}
}
]
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
},
"put": {
"description": "Update an existing asset's information on the blockchain",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"assets"
],
"summary": "Update an existing asset",
"parameters": [
{
"type": "string",
"description": "Asset ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Updated asset data",
"name": "asset",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.CreateAssetRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/models.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.Asset"
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
}
},
"/assets/{id}/transfer": {
"put": {
"description": "Transfer ownership of an asset to a new owner",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"assets"
],
"summary": "Transfer asset ownership",
"parameters": [
{
"type": "string",
"description": "Asset ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Transfer request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.TransferAssetRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Response"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
}
},
"/blockchain/height": {
"get": {
"description": "Get the current block height of the blockchain",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"blockchain"
],
"summary": "Get block height",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/models.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.BlockHeightResponse"
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
}
},
"/blockchain/info": {
"get": {
"description": "Get detailed information about the blockchain including block height",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"blockchain"
],
"summary": "Get chain information",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/models.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.ChainInfoResponse"
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
}
},
"/health": {
"get": {
"description": "Check the health status of the API",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"health"
],
"summary": "Health check",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.HealthResponse"
}
}
}
}
},
"/ledger/init": {
"post": {
"description": "Initialize the blockchain ledger with a set of assets",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"ledger"
],
"summary": "Initialize ledger",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Response"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
}
},
"/transactions/{txid}": {
"get": {
"description": "Retrieve detailed information about a specific transaction by its ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"transactions"
],
"summary": "Get transaction details by ID",
"parameters": [
{
"type": "string",
"description": "Transaction ID",
"name": "txid",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/models.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/models.TransactionDetail"
}
}
}
]
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.ErrorResponse"
}
}
}
}
}
},
"definitions": {
"models.Asset": {
"type": "object",
"properties": {
"ID": {
"type": "string",
"example": "asset123"
},
"appraisedValue": {
"type": "string",
"example": "2000"
},
"color": {
"type": "string",
"example": "red"
},
"owner": {
"type": "string",
"example": "Alice"
},
"size": {
"type": "string",
"example": "10"
}
}
},
"models.BlockHeightResponse": {
"type": "object",
"properties": {
"height": {
"type": "integer",
"example": 12345
}
}
},
"models.ChainInfoResponse": {
"type": "object",
"properties": {
"blockHash": {
"type": "string",
"example": "a1b2c3d4e5f6..."
},
"chainName": {
"type": "string",
"example": "mychannel"
},
"height": {
"type": "integer",
"example": 12345
}
}
},
"models.CreateAssetRequest": {
"type": "object",
"properties": {
"appraisedValue": {
"type": "string",
"example": "2000"
},
"color": {
"type": "string",
"example": "red"
},
"id": {
"description": "Optional - will be auto-generated if not provided",
"type": "string",
"example": "asset123"
},
"owner": {
"type": "string",
"example": "Alice"
},
"size": {
"type": "string",
"example": "10"
}
}
},
"models.ErrorResponse": {
"type": "object",
"properties": {
"message": {
"type": "string"
},
"success": {
"type": "boolean"
}
}
},
"models.HealthResponse": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "healthy"
}
}
},
"models.Response": {
"type": "object",
"properties": {
"data": {},
"message": {
"type": "string"
},
"success": {
"type": "boolean"
}
}
},
"models.TransactionDetail": {
"type": "object",
"properties": {
"arguments": {
"type": "array",
"items": {
"type": "string"
},
"example": [
"asset1",
"red",
"10",
"Alice",
"1000"
]
},
"blockHash": {
"type": "string",
"example": "a1b2c3d4e5f6..."
},
"blockNumber": {
"type": "integer",
"example": 123
},
"chaincodeId": {
"type": "string",
"example": "basic"
},
"channelId": {
"type": "string",
"example": "mychannel"
},
"creatorId": {
"type": "string",
"example": "User1@org1.example.com"
},
"creatorMspId": {
"type": "string",
"example": "Org1MSP"
},
"endorsers": {
"type": "array",
"items": {
"type": "string"
},
"example": [
"peer0.org1.example.com",
"peer0.org2.example.com"
]
},
"function": {
"type": "string",
"example": "CreateAsset"
},
"rawTransaction": {
"type": "object",
"additionalProperties": true
},
"response": {
"$ref": "#/definitions/models.TransactionResponse"
},
"timestamp": {
"type": "string",
"example": "2024-01-15T10:30:00Z"
},
"transactionId": {
"type": "string",
"example": "f973e40540e3b629b7dffd0b91b87aa3474bc31de89e11b01f749bbc5e6d5add"
},
"validationCode": {
"type": "string",
"example": "VALID"
}
}
},
"models.TransactionResponse": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "Transaction completed successfully"
},
"payload": {
"type": "string",
"example": ""
},
"status": {
"type": "integer",
"example": 200
}
}
},
"models.TransferAssetRequest": {
"type": "object",
"properties": {
"newOwner": {
"type": "string",
"example": "Bob"
}
}
}
}
}

@ -0,0 +1,468 @@
basePath: /api/v1
definitions:
models.Asset:
properties:
ID:
example: asset123
type: string
appraisedValue:
example: "2000"
type: string
color:
example: red
type: string
owner:
example: Alice
type: string
size:
example: "10"
type: string
type: object
models.BlockHeightResponse:
properties:
height:
example: 12345
type: integer
type: object
models.ChainInfoResponse:
properties:
blockHash:
example: a1b2c3d4e5f6...
type: string
chainName:
example: mychannel
type: string
height:
example: 12345
type: integer
type: object
models.CreateAssetRequest:
properties:
appraisedValue:
example: "2000"
type: string
color:
example: red
type: string
id:
description: Optional - will be auto-generated if not provided
example: asset123
type: string
owner:
example: Alice
type: string
size:
example: "10"
type: string
type: object
models.ErrorResponse:
properties:
message:
type: string
success:
type: boolean
type: object
models.HealthResponse:
properties:
status:
example: healthy
type: string
type: object
models.Response:
properties:
data: {}
message:
type: string
success:
type: boolean
type: object
models.TransactionDetail:
properties:
arguments:
example:
- asset1
- red
- "10"
- Alice
- "1000"
items:
type: string
type: array
blockHash:
example: a1b2c3d4e5f6...
type: string
blockNumber:
example: 123
type: integer
chaincodeId:
example: basic
type: string
channelId:
example: mychannel
type: string
creatorId:
example: User1@org1.example.com
type: string
creatorMspId:
example: Org1MSP
type: string
endorsers:
example:
- peer0.org1.example.com
- peer0.org2.example.com
items:
type: string
type: array
function:
example: CreateAsset
type: string
rawTransaction:
additionalProperties: true
type: object
response:
$ref: '#/definitions/models.TransactionResponse'
timestamp:
example: "2024-01-15T10:30:00Z"
type: string
transactionId:
example: f973e40540e3b629b7dffd0b91b87aa3474bc31de89e11b01f749bbc5e6d5add
type: string
validationCode:
example: VALID
type: string
type: object
models.TransactionResponse:
properties:
message:
example: Transaction completed successfully
type: string
payload:
example: ""
type: string
status:
example: 200
type: integer
type: object
models.TransferAssetRequest:
properties:
newOwner:
example: Bob
type: string
type: object
host: localhost:8888
info:
contact:
email: support@swagger.io
name: API Support
url: http://www.swagger.io/support
description: RESTful API for managing assets on Hyperledger Fabric blockchain
license:
name: Apache 2.0
url: http://www.apache.org/licenses/LICENSE-2.0.html
termsOfService: http://swagger.io/terms/
title: Hyperledger Fabric Asset Transfer API
version: "1.0"
paths:
/assets:
get:
consumes:
- application/json
description: Retrieve all assets from the blockchain ledger
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/models.Response'
- properties:
data:
items:
$ref: '#/definitions/models.Asset'
type: array
type: object
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/models.ErrorResponse'
summary: Get all assets
tags:
- assets
post:
consumes:
- application/json
description: Create a new asset on the blockchain ledger with auto-generated
UUID if ID not provided
parameters:
- description: Asset data (ID is optional and will be auto-generated)
in: body
name: asset
required: true
schema:
$ref: '#/definitions/models.CreateAssetRequest'
produces:
- application/json
responses:
"201":
description: Created
schema:
allOf:
- $ref: '#/definitions/models.Response'
- properties:
data:
properties:
asset:
$ref: '#/definitions/models.Asset'
transactionId:
type: string
type: object
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/models.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/models.ErrorResponse'
summary: Create a new asset
tags:
- assets
/assets/{id}:
get:
consumes:
- application/json
description: Retrieve a specific asset by its ID from the blockchain ledger
parameters:
- description: Asset ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/models.Response'
- properties:
data:
$ref: '#/definitions/models.Asset'
type: object
"404":
description: Not Found
schema:
$ref: '#/definitions/models.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/models.ErrorResponse'
summary: Get asset by ID
tags:
- assets
put:
consumes:
- application/json
description: Update an existing asset's information on the blockchain
parameters:
- description: Asset ID
in: path
name: id
required: true
type: string
- description: Updated asset data
in: body
name: asset
required: true
schema:
$ref: '#/definitions/models.CreateAssetRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/models.Response'
- properties:
data:
$ref: '#/definitions/models.Asset'
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/models.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/models.ErrorResponse'
summary: Update an existing asset
tags:
- assets
/assets/{id}/transfer:
put:
consumes:
- application/json
description: Transfer ownership of an asset to a new owner
parameters:
- description: Asset ID
in: path
name: id
required: true
type: string
- description: Transfer request
in: body
name: request
required: true
schema:
$ref: '#/definitions/models.TransferAssetRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.Response'
"400":
description: Bad Request
schema:
$ref: '#/definitions/models.ErrorResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/models.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/models.ErrorResponse'
summary: Transfer asset ownership
tags:
- assets
/blockchain/height:
get:
consumes:
- application/json
description: Get the current block height of the blockchain
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/models.Response'
- properties:
data:
$ref: '#/definitions/models.BlockHeightResponse'
type: object
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/models.ErrorResponse'
summary: Get block height
tags:
- blockchain
/blockchain/info:
get:
consumes:
- application/json
description: Get detailed information about the blockchain including block height
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/models.Response'
- properties:
data:
$ref: '#/definitions/models.ChainInfoResponse'
type: object
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/models.ErrorResponse'
summary: Get chain information
tags:
- blockchain
/health:
get:
consumes:
- application/json
description: Check the health status of the API
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.HealthResponse'
summary: Health check
tags:
- health
/ledger/init:
post:
consumes:
- application/json
description: Initialize the blockchain ledger with a set of assets
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.Response'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/models.ErrorResponse'
summary: Initialize ledger
tags:
- ledger
/transactions/{txid}:
get:
consumes:
- application/json
description: Retrieve detailed information about a specific transaction by its
ID
parameters:
- description: Transaction ID
in: path
name: txid
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/models.Response'
- properties:
data:
$ref: '#/definitions/models.TransactionDetail'
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/models.ErrorResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/models.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/models.ErrorResponse'
summary: Get transaction details by ID
tags:
- transactions
schemes:
- http
- https
swagger: "2.0"

@ -0,0 +1,35 @@
module spiderman
go 1.23.0
require (
github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.1
github.com/hyperledger/fabric-gateway v1.7.0
github.com/joho/godotenv v1.5.1
github.com/sirupsen/logrus v1.9.3
github.com/swaggo/http-swagger v1.3.4
github.com/swaggo/swag v1.16.4
google.golang.org/grpc v1.71.0
)
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/hyperledger/fabric-protos-go-apiv2 v0.3.4 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/swaggo/files v1.0.1 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/tools v0.29.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
google.golang.org/protobuf v1.36.4 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

125
go.sum

@ -0,0 +1,125 @@
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/hyperledger/fabric-gateway v1.7.0 h1:bd1quU8qYPYqYO69m1tPIDSjB+D+u/rBJfE1eWFcpjY=
github.com/hyperledger/fabric-gateway v1.7.0/go.mod h1:TItDGnq71eJcgz5TW+m5Sq3kWGp0AEI1HPCNxj0Eu7k=
github.com/hyperledger/fabric-protos-go-apiv2 v0.3.4 h1:YJrd+gMaeY0/vsN0aS0QkEKTivGoUnSRIXxGJ7KI+Pc=
github.com/hyperledger/fabric-protos-go-apiv2 v0.3.4/go.mod h1:bau/6AJhvEcu9GKKYHlDXAxXKzYNfhP6xu2GXuxEcFk=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64a5ww=
github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ=
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

@ -0,0 +1,572 @@
// Package api provides HTTP API handlers
package api
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"spiderman/internal/fabric"
"spiderman/internal/logger"
"spiderman/pkg/models"
"github.com/google/uuid"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
)
// Handler handles HTTP requests and communicates with Fabric
type Handler struct {
fabricClient *fabric.Client
}
// NewHandler creates a new API handler
func NewHandler(fabricClient *fabric.Client) *Handler {
logger.Logger.Info("API handler initialized")
return &Handler{
fabricClient: fabricClient,
}
}
// writeJSONResponse writes a JSON response to the HTTP response writer
func writeJSONResponse(w http.ResponseWriter, statusCode int, response models.Response) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(response)
}
// writeErrorResponse writes an error response to the HTTP response writer
func writeErrorResponse(w http.ResponseWriter, statusCode int, message string) {
writeJSONResponse(w, statusCode, models.Response{
Success: false,
Message: message,
})
}
// HealthCheck godoc
//
// @Summary Health check
// @Description Check the health status of the API
// @Tags health
// @Accept json
// @Produce json
// @Success 200 {object} models.HealthResponse
// @Router /health [get]
func (h *Handler) HealthCheck(w http.ResponseWriter, r *http.Request) {
requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr)
requestLogger.Debug("Processing health check request")
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(models.HealthResponse{Status: "healthy"})
requestLogger.Info("Health check completed successfully")
}
// InitLedger godoc
//
// @Summary Initialize ledger
// @Description Initialize the blockchain ledger with a set of assets
// @Tags ledger
// @Accept json
// @Produce json
// @Success 200 {object} models.Response
// @Failure 500 {object} models.ErrorResponse
// @Router /ledger/init [post]
func (h *Handler) InitLedger(w http.ResponseWriter, r *http.Request) {
requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr)
requestLogger.Info("Starting ledger initialization")
start := time.Now()
err := h.fabricClient.InitLedger()
duration := time.Since(start)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"error": err.Error(),
"duration_ms": duration.Milliseconds(),
}).Error("Failed to initialize ledger")
writeErrorResponse(w, http.StatusInternalServerError,
fmt.Sprintf("Failed to initialize ledger: %s", fabric.HandleFabricError(err)))
return
}
requestLogger.WithFields(logrus.Fields{
"duration_ms": duration.Milliseconds(),
}).Info("Ledger initialized successfully")
writeJSONResponse(w, http.StatusOK, models.Response{
Success: true,
Message: "Ledger initialized successfully",
})
}
// GetAllAssets godoc
//
// @Summary Get all assets
// @Description Retrieve all assets from the blockchain ledger
// @Tags assets
// @Accept json
// @Produce json
// @Success 200 {object} models.Response{data=[]models.Asset}
// @Failure 500 {object} models.ErrorResponse
// @Router /assets [get]
func (h *Handler) GetAllAssets(w http.ResponseWriter, r *http.Request) {
requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr)
requestLogger.Info("Retrieving all assets")
start := time.Now()
assets, err := h.fabricClient.GetAllAssets()
duration := time.Since(start)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"error": err.Error(),
"duration_ms": duration.Milliseconds(),
}).Error("Failed to retrieve assets")
writeErrorResponse(w, http.StatusInternalServerError,
fmt.Sprintf("Failed to get assets: %s", fabric.HandleFabricError(err)))
return
}
requestLogger.WithFields(logrus.Fields{
"asset_count": len(assets),
"duration_ms": duration.Milliseconds(),
}).Info("Assets retrieved successfully")
writeJSONResponse(w, http.StatusOK, models.Response{
Success: true,
Data: assets,
})
}
// CreateAsset godoc
//
// @Summary Create a new asset
// @Description Create a new asset on the blockchain ledger with auto-generated UUID if ID not provided
// @Tags assets
// @Accept json
// @Produce json
// @Param asset body models.CreateAssetRequest true "Asset data (ID is optional and will be auto-generated)"
// @Success 201 {object} models.Response{data=object{asset=models.Asset,transactionId=string}}
// @Failure 400 {object} models.ErrorResponse
// @Failure 500 {object} models.ErrorResponse
// @Router /assets [post]
func (h *Handler) CreateAsset(w http.ResponseWriter, r *http.Request) {
requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr)
requestLogger.Info("Creating new asset")
var req models.CreateAssetRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
requestLogger.WithError(err).Warn("Invalid request body for asset creation")
writeErrorResponse(w, http.StatusBadRequest, "Invalid request body")
return
}
// Auto-generate UUID if ID is not provided
if req.ID == "" {
req.ID = uuid.New().String()
requestLogger.WithField("generated_id", req.ID).Info("Auto-generated UUID for asset")
}
requestLogger.WithFields(logrus.Fields{
"asset_id": req.ID,
"owner": req.Owner,
"color": req.Color,
"size": req.Size,
"value": req.AppraisedValue,
}).Debug("Asset creation request details")
// Convert request to asset model
asset := models.Asset{
ID: req.ID,
Color: req.Color,
Size: req.Size,
Owner: req.Owner,
AppraisedValue: req.AppraisedValue,
}
start := time.Now()
transactionResult, err := h.fabricClient.CreateAsset(asset)
duration := time.Since(start)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"error": err.Error(),
"asset_id": req.ID,
"duration_ms": duration.Milliseconds(),
}).Error("Failed to create asset")
writeErrorResponse(w, http.StatusInternalServerError,
fmt.Sprintf("Failed to create asset: %s", fabric.HandleFabricError(err)))
return
}
requestLogger.WithFields(logrus.Fields{
"asset_id": req.ID,
"transaction_result": transactionResult,
"duration_ms": duration.Milliseconds(),
}).Info("Asset created successfully")
// Create response data that includes both asset and transaction ID
responseData := map[string]interface{}{
"asset": asset,
"transactionId": transactionResult,
}
writeJSONResponse(w, http.StatusCreated, models.Response{
Success: true,
Message: "Asset created successfully",
Data: responseData,
})
}
// GetAssetByID godoc
//
// @Summary Get asset by ID
// @Description Retrieve a specific asset by its ID from the blockchain ledger
// @Tags assets
// @Accept json
// @Produce json
// @Param id path string true "Asset ID"
// @Success 200 {object} models.Response{data=models.Asset}
// @Failure 404 {object} models.ErrorResponse
// @Failure 500 {object} models.ErrorResponse
// @Router /assets/{id} [get]
func (h *Handler) GetAssetByID(w http.ResponseWriter, r *http.Request) {
requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr)
vars := mux.Vars(r)
assetID := vars["id"]
requestLogger.WithField("asset_id", assetID).Info("Retrieving asset by ID")
start := time.Now()
asset, err := h.fabricClient.ReadAssetByID(assetID)
duration := time.Since(start)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"error": err.Error(),
"asset_id": assetID,
"duration_ms": duration.Milliseconds(),
}).Error("Failed to retrieve asset")
statusCode := http.StatusInternalServerError
if err.Error() == "asset not found" {
statusCode = http.StatusNotFound
}
writeErrorResponse(w, statusCode,
fmt.Sprintf("Failed to get asset: %s", fabric.HandleFabricError(err)))
return
}
requestLogger.WithFields(logrus.Fields{
"asset_id": assetID,
"duration_ms": duration.Milliseconds(),
}).Info("Asset retrieved successfully")
writeJSONResponse(w, http.StatusOK, models.Response{
Success: true,
Data: asset,
})
}
// TransferAsset godoc
//
// @Summary Transfer asset ownership
// @Description Transfer ownership of an asset to a new owner
// @Tags assets
// @Accept json
// @Produce json
// @Param id path string true "Asset ID"
// @Param request body models.TransferAssetRequest true "Transfer request"
// @Success 200 {object} models.Response
// @Failure 400 {object} models.ErrorResponse
// @Failure 404 {object} models.ErrorResponse
// @Failure 500 {object} models.ErrorResponse
// @Router /assets/{id}/transfer [put]
func (h *Handler) TransferAsset(w http.ResponseWriter, r *http.Request) {
requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr)
vars := mux.Vars(r)
assetID := vars["id"]
requestLogger.WithField("asset_id", assetID).Info("Transferring asset")
var req models.TransferAssetRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
requestLogger.WithError(err).Warn("Invalid request body for asset transfer")
writeErrorResponse(w, http.StatusBadRequest, "Invalid request body")
return
}
requestLogger.WithFields(logrus.Fields{
"asset_id": assetID,
"new_owner": req.NewOwner,
}).Debug("Asset transfer request details")
start := time.Now()
err := h.fabricClient.TransferAsset(assetID, req.NewOwner)
duration := time.Since(start)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"error": err.Error(),
"asset_id": assetID,
"new_owner": req.NewOwner,
"duration_ms": duration.Milliseconds(),
}).Error("Failed to transfer asset")
statusCode := http.StatusInternalServerError
if err.Error() == "asset not found" {
statusCode = http.StatusNotFound
}
writeErrorResponse(w, statusCode,
fmt.Sprintf("Failed to transfer asset: %s", fabric.HandleFabricError(err)))
return
}
requestLogger.WithFields(logrus.Fields{
"asset_id": assetID,
"new_owner": req.NewOwner,
"duration_ms": duration.Milliseconds(),
}).Info("Asset transferred successfully")
writeJSONResponse(w, http.StatusOK, models.Response{
Success: true,
Message: "Asset transferred successfully",
})
}
// UpdateAsset godoc
//
// @Summary Update an existing asset
// @Description Update an existing asset's information on the blockchain
// @Tags assets
// @Accept json
// @Produce json
// @Param id path string true "Asset ID"
// @Param asset body models.CreateAssetRequest true "Updated asset data"
// @Success 200 {object} models.Response{data=models.Asset}
// @Failure 400 {object} models.ErrorResponse
// @Failure 500 {object} models.ErrorResponse
// @Router /assets/{id} [put]
func (h *Handler) UpdateAsset(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
assetID := vars["id"]
requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr).
WithField("asset_id", assetID)
requestLogger.Info("Updating asset")
var req models.CreateAssetRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
requestLogger.WithError(err).Warn("Invalid request body for asset update")
writeErrorResponse(w, http.StatusBadRequest, "Invalid request body")
return
}
// Set the ID from the URL
req.ID = assetID
requestLogger.WithFields(logrus.Fields{
"owner": req.Owner,
"color": req.Color,
"size": req.Size,
"value": req.AppraisedValue,
}).Debug("Asset update request details")
// Convert request to asset model
asset := models.Asset{
ID: req.ID,
Color: req.Color,
Size: req.Size,
Owner: req.Owner,
AppraisedValue: req.AppraisedValue,
}
start := time.Now()
err := h.fabricClient.UpdateAsset(asset)
duration := time.Since(start)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"error": err.Error(),
"duration_ms": duration.Milliseconds(),
}).Error("Failed to update asset")
writeErrorResponse(w, http.StatusInternalServerError,
fmt.Sprintf("Failed to update asset: %s", fabric.HandleFabricError(err)))
return
}
requestLogger.WithFields(logrus.Fields{
"duration_ms": duration.Milliseconds(),
}).Info("Asset updated successfully")
writeJSONResponse(w, http.StatusOK, models.Response{
Success: true,
Message: "Asset updated successfully",
Data: asset,
})
}
// GetBlockHeight godoc
//
// @Summary Get block height
// @Description Get the current block height of the blockchain
// @Tags blockchain
// @Accept json
// @Produce json
// @Success 200 {object} models.Response{data=models.BlockHeightResponse}
// @Failure 500 {object} models.ErrorResponse
// @Router /blockchain/height [get]
func (h *Handler) GetBlockHeight(w http.ResponseWriter, r *http.Request) {
requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr)
requestLogger.Info("Getting block height")
start := time.Now()
height, err := h.fabricClient.GetBlockHeight()
duration := time.Since(start)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"error": err.Error(),
"duration_ms": duration.Milliseconds(),
}).Error("Failed to get block height")
writeErrorResponse(w, http.StatusInternalServerError,
fmt.Sprintf("Failed to get block height: %s", fabric.HandleFabricError(err)))
return
}
requestLogger.WithFields(logrus.Fields{
"height": height,
"duration_ms": duration.Milliseconds(),
}).Info("Block height retrieved successfully")
response := models.BlockHeightResponse{
Height: height,
}
writeJSONResponse(w, http.StatusOK, models.Response{
Success: true,
Message: "Block height retrieved successfully",
Data: response,
})
}
// GetChainInfo godoc
//
// @Summary Get chain information
// @Description Get detailed information about the blockchain including block height
// @Tags blockchain
// @Accept json
// @Produce json
// @Success 200 {object} models.Response{data=models.ChainInfoResponse}
// @Failure 500 {object} models.ErrorResponse
// @Router /blockchain/info [get]
func (h *Handler) GetChainInfo(w http.ResponseWriter, r *http.Request) {
requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr)
requestLogger.Info("Getting chain information")
start := time.Now()
height, err := h.fabricClient.GetBlockHeight()
duration := time.Since(start)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"error": err.Error(),
"duration_ms": duration.Milliseconds(),
}).Error("Failed to get chain information")
writeErrorResponse(w, http.StatusInternalServerError,
fmt.Sprintf("Failed to get chain information: %s", fabric.HandleFabricError(err)))
return
}
requestLogger.WithFields(logrus.Fields{
"height": height,
"duration_ms": duration.Milliseconds(),
}).Info("Chain information retrieved successfully")
response := models.ChainInfoResponse{
Height: height,
ChainName: h.fabricClient.GetChannelName(),
}
writeJSONResponse(w, http.StatusOK, models.Response{
Success: true,
Message: "Chain information retrieved successfully",
Data: response,
})
}
// GetTransactionByID godoc
//
// @Summary Get transaction details by ID
// @Description Retrieve detailed information about a specific transaction by its ID
// @Tags transactions
// @Accept json
// @Produce json
// @Param txid path string true "Transaction ID"
// @Success 200 {object} models.Response{data=models.TransactionDetail}
// @Failure 400 {object} models.ErrorResponse
// @Failure 404 {object} models.ErrorResponse
// @Failure 500 {object} models.ErrorResponse
// @Router /transactions/{txid} [get]
func (h *Handler) GetTransactionByID(w http.ResponseWriter, r *http.Request) {
requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr)
vars := mux.Vars(r)
txID := vars["txid"]
if txID == "" {
requestLogger.Warn("Missing transaction ID in request")
writeErrorResponse(w, http.StatusBadRequest, "Transaction ID is required")
return
}
requestLogger.WithField("transaction_id", txID).Info("Getting transaction details")
start := time.Now()
txDetail, err := h.fabricClient.GetTransactionByID(txID)
duration := time.Since(start)
if err != nil {
requestLogger.WithFields(logrus.Fields{
"error": err.Error(),
"transaction_id": txID,
"duration_ms": duration.Milliseconds(),
}).Error("Failed to get transaction details")
statusCode := http.StatusInternalServerError
message := fmt.Sprintf("Failed to get transaction details: %s", fabric.HandleFabricError(err))
// Check if it's a "not found" error
if strings.Contains(err.Error(), "not found") ||
strings.Contains(err.Error(), "does not exist") ||
strings.Contains(err.Error(), "no such transaction ID") {
statusCode = http.StatusNotFound
message = "Transaction not found"
}
writeErrorResponse(w, statusCode, message)
return
}
requestLogger.WithFields(logrus.Fields{
"transaction_id": txID,
"block_number": txDetail.BlockNumber,
"duration_ms": duration.Milliseconds(),
}).Info("Transaction details retrieved successfully")
writeJSONResponse(w, http.StatusOK, models.Response{
Success: true,
Message: "Transaction details retrieved successfully",
Data: txDetail,
})
}

@ -0,0 +1,570 @@
// Package fabric provides Hyperledger Fabric client functionality
package fabric
import (
"bytes"
"context"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"os"
"path"
"strconv"
"time"
"spiderman/internal/logger"
"spiderman/pkg/config"
"spiderman/pkg/models"
"github.com/hyperledger/fabric-gateway/pkg/client"
"github.com/hyperledger/fabric-gateway/pkg/hash"
"github.com/hyperledger/fabric-gateway/pkg/identity"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/status"
)
// Client wraps the Fabric Gateway client
type Client struct {
gateway *client.Gateway
contract *client.Contract
conn *grpc.ClientConn
config *config.FabricConfig
}
// NewClient creates and initializes a new Fabric client
func NewClient(cfg *config.FabricConfig) (*Client, error) {
fabricLogger := logger.GetFabricLogger()
fabricLogger.Info("Initializing Fabric client")
// Build full paths
certPath := cfg.CertPath
if certPath == "" {
certPath = cfg.CryptoPath + "/users/User1@org1.example.com/msp/signcerts"
}
keyPath := cfg.KeyPath
if keyPath == "" {
keyPath = cfg.CryptoPath + "/users/User1@org1.example.com/msp/keystore"
}
tlsCertPath := cfg.TLSCertPath
if tlsCertPath == "" {
tlsCertPath = cfg.CryptoPath + "/peers/peer0.org1.example.com/tls/ca.crt"
}
// The gRPC client connection should be shared by all Gateway connections to this endpoint
clientConnection, err := newGrpcConnection(tlsCertPath, cfg.PeerEndpoint, cfg.GatewayPeer)
if err != nil {
return nil, fmt.Errorf("failed to create gRPC connection: %w", err)
}
id, err := newIdentity(certPath, cfg.MSPID)
if err != nil {
return nil, fmt.Errorf("failed to create identity: %w", err)
}
sign, err := newSign(keyPath)
if err != nil {
return nil, fmt.Errorf("failed to create sign: %w", err)
}
// Create a Gateway connection for a specific client identity
gw, err := client.Connect(
id,
client.WithSign(sign),
client.WithHash(hash.SHA256),
client.WithClientConnection(clientConnection),
// Default timeouts for different gRPC calls
client.WithEvaluateTimeout(5*time.Second),
client.WithEndorseTimeout(15*time.Second),
client.WithSubmitTimeout(5*time.Second),
client.WithCommitStatusTimeout(1*time.Minute),
)
if err != nil {
return nil, fmt.Errorf("failed to connect to gateway: %w", err)
}
network := gw.GetNetwork(cfg.ChannelName)
contract := network.GetContract(cfg.ChaincodeName)
fabricLogger.WithFields(map[string]interface{}{
"channel": cfg.ChannelName,
"chaincode": cfg.ChaincodeName,
"msp_id": cfg.MSPID,
}).Info("Fabric client initialized successfully")
return &Client{
gateway: gw,
contract: contract,
conn: clientConnection,
config: cfg,
}, nil
}
// Close closes the Fabric client connections
func (c *Client) Close() {
logger := logger.GetFabricLogger()
if c.gateway != nil {
c.gateway.Close()
logger.Debug("Gateway connection closed")
}
if c.conn != nil {
c.conn.Close()
logger.Debug("gRPC connection closed")
}
logger.Info("Fabric client closed")
}
// InitLedger initializes the ledger with a set of assets
func (c *Client) InitLedger() error {
logger := logger.GetFabricLogger()
logger.Info("Initializing ledger")
_, err := c.contract.SubmitTransaction("InitLedger")
if err != nil {
logger.WithError(err).Error("Failed to initialize ledger")
return err
}
logger.Info("Ledger initialized successfully")
return nil
}
// GetAllAssets returns all assets from the ledger
func (c *Client) GetAllAssets() ([]models.Asset, error) {
logger := logger.GetFabricLogger()
logger.Debug("Getting all assets")
evaluateResult, err := c.contract.EvaluateTransaction("GetAllAssets")
if err != nil {
logger.WithError(err).Error("Failed to get all assets")
return nil, err
}
var assets []models.Asset
err = json.Unmarshal(evaluateResult, &assets)
if err != nil {
logger.WithError(err).Error("Failed to unmarshal assets")
return nil, err
}
logger.WithField("count", len(assets)).Debug("Retrieved assets successfully")
return assets, nil
}
// CreateAsset creates a new asset and returns the transaction ID
func (c *Client) CreateAsset(asset models.Asset) (string, error) {
logger := logger.GetFabricLogger().WithField("asset_id", asset.ID)
logger.Info("Creating asset")
// Create proposal to get access to transaction ID
proposal, err := c.contract.NewProposal("CreateAsset", client.WithArguments(
asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue))
if err != nil {
logger.WithError(err).Error("Failed to create proposal")
return "", err
}
// Endorse the proposal
transaction, err := proposal.Endorse()
if err != nil {
logger.WithError(err).Error("Failed to endorse proposal")
return "", err
}
// Get the transaction ID before submitting
txID := transaction.TransactionID()
// Submit the transaction
commit, err := transaction.Submit()
if err != nil {
logger.WithError(err).Error("Failed to submit transaction")
return "", err
}
// Wait for commit status
status, err := commit.Status()
if err != nil {
logger.WithError(err).Error("Failed to get commit status")
return "", err
}
if !status.Successful {
logger.WithField("transaction_id", txID).Error("Transaction was not committed successfully")
return "", fmt.Errorf("transaction not committed successfully")
}
logger.WithFields(map[string]interface{}{
"asset_id": asset.ID,
"transaction_id": txID,
}).Info("Asset created successfully")
return txID, nil
}
// ReadAssetByID returns an asset by its ID
func (c *Client) ReadAssetByID(assetID string) (*models.Asset, error) {
logger := logger.GetFabricLogger().WithField("asset_id", assetID)
logger.Debug("Reading asset by ID")
evaluateResult, err := c.contract.EvaluateTransaction("ReadAsset", assetID)
if err != nil {
logger.WithError(err).Error("Failed to read asset")
return nil, err
}
var asset models.Asset
err = json.Unmarshal(evaluateResult, &asset)
if err != nil {
logger.WithError(err).Error("Failed to unmarshal asset")
return nil, err
}
logger.Debug("Asset read successfully")
return &asset, nil
}
// TransferAsset transfers ownership of an asset
func (c *Client) TransferAsset(assetID, newOwner string) error {
logger := logger.GetFabricLogger().WithFields(map[string]interface{}{
"asset_id": assetID,
"new_owner": newOwner,
})
logger.Info("Transferring asset")
_, err := c.contract.SubmitTransaction("TransferAsset", assetID, newOwner)
if err != nil {
logger.WithError(err).Error("Failed to transfer asset")
return err
}
logger.Info("Asset transferred successfully")
return nil
}
// UpdateAsset updates an existing asset
func (c *Client) UpdateAsset(asset models.Asset) error {
logger := logger.GetFabricLogger().WithField("asset_id", asset.ID)
logger.Info("Updating asset")
_, err := c.contract.SubmitTransaction("UpdateAsset",
asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue)
if err != nil {
logger.WithError(err).Error("Failed to update asset")
return err
}
logger.Info("Asset updated successfully")
return nil
}
// GetBlockHeight returns the current block height of the blockchain
func (c *Client) GetBlockHeight() (uint64, error) {
logger := logger.GetFabricLogger()
logger.Debug("Getting block height")
// Try to use the smart contract method first (if available)
evaluateResult, err := c.contract.EvaluateTransaction("GetBlockHeight")
if err == nil {
// Parse the result as string and convert to uint64
heightStr := string(evaluateResult)
height, parseErr := strconv.ParseUint(heightStr, 10, 64)
if parseErr == nil {
logger.WithField("height", height).Debug("Retrieved block height from smart contract")
return height, nil
}
logger.WithError(parseErr).Debug("Failed to parse height from smart contract, trying alternative method")
}
// Alternative method: Use binary search to find the highest block number
// This is more reliable than trying to parse protobuf data
logger.Debug("Using binary search method to find block height")
return c.findBlockHeightBySearch()
}
// GetBlockHeightSimple returns the current block height using a simpler approach
func (c *Client) GetBlockHeightSimple() (uint64, error) {
logger := logger.GetFabricLogger()
logger.Debug("Getting block height (simple method)")
// Use the main contract to query block height if available
evaluateResult, err := c.contract.EvaluateTransaction("GetBlockHeight")
if err != nil {
logger.WithError(err).Debug("Smart contract GetBlockHeight not available, using fallback method")
// Fallback to the more robust binary search method
return c.findBlockHeightBySearch()
}
// Parse the result as string and convert to uint64
heightStr := string(evaluateResult)
height, err := strconv.ParseUint(heightStr, 10, 64)
if err != nil {
logger.WithError(err).Error("Failed to parse block height from smart contract")
// Fallback to the more robust binary search method
return c.findBlockHeightBySearch()
}
logger.WithField("height", height).Debug("Retrieved block height successfully")
return height, nil
}
// findBlockHeightBySearch uses binary search to find the current block height
func (c *Client) findBlockHeightBySearch() (uint64, error) {
logger := logger.GetFabricLogger()
network := c.gateway.GetNetwork(c.config.ChannelName)
qsccContract := network.GetContract("qscc")
// Start with a reasonable upper bound (most blockchains won't have billions of blocks)
var low uint64 = 0
var high uint64 = 1000000 // Start with 1 million as upper bound
var result uint64 = 0
// First, find a reasonable upper bound by doubling until we get an error
for {
_, err := qsccContract.EvaluateTransaction("GetBlockByNumber", c.config.ChannelName, fmt.Sprintf("%d", high))
if err != nil {
// This block doesn't exist, so our upper bound is good
break
}
result = high
high *= 2
if high > 100000000 { // Sanity check: 100 million blocks
break
}
}
// Now use binary search to find the exact highest block number
for low <= high {
mid := (low + high) / 2
_, err := qsccContract.EvaluateTransaction("GetBlockByNumber", c.config.ChannelName, fmt.Sprintf("%d", mid))
if err != nil {
// Block doesn't exist, search lower half
high = mid - 1
} else {
// Block exists, this could be our answer
result = mid
low = mid + 1
}
}
// The block height is the highest block number + 1 (since blocks are 0-indexed)
blockHeight := result + 1
logger.WithField("height", blockHeight).Debug("Found block height using binary search")
return blockHeight, nil
}
// GetTransactionByID returns detailed information about a transaction by its ID
func (c *Client) GetTransactionByID(txID string) (*models.TransactionDetail, error) {
logger := logger.GetFabricLogger().WithField("transaction_id", txID)
logger.Debug("Getting transaction details")
// Get the QSCC (Query System ChainCode) contract to query transaction info
network := c.gateway.GetNetwork(c.config.ChannelName)
qsccContract := network.GetContract("qscc")
// Query the transaction by ID
evaluateResult, err := qsccContract.EvaluateTransaction("GetTransactionByID", c.config.ChannelName, txID)
if err != nil {
logger.WithError(err).Error("Failed to get transaction details")
return nil, fmt.Errorf("failed to get transaction details: %w", err)
}
// Parse the protobuf result
txDetail, err := c.parseTransactionBytes(evaluateResult, txID)
if err != nil {
logger.WithError(err).Error("Failed to parse transaction details")
return nil, fmt.Errorf("failed to parse transaction details: %w", err)
}
logger.WithField("transaction_id", txID).Debug("Retrieved transaction details successfully")
return txDetail, nil
}
// parseTransactionBytes parses the raw transaction bytes from QSCC
func (c *Client) parseTransactionBytes(txBytes []byte, txID string) (*models.TransactionDetail, error) {
// For now, we'll create a simplified parser
// In a production environment, you would use protobuf parsing
txDetail := &models.TransactionDetail{
TransactionID: txID,
ChannelID: c.config.ChannelName,
ValidationCode: "VALID", // Default assumption
Response: models.TransactionResponse{
Status: 200,
Message: "Transaction completed successfully",
Payload: "",
},
RawTransaction: map[string]interface{}{
"size": len(txBytes),
"hasData": len(txBytes) > 0,
},
}
// Try to get additional information using GetBlockByTxID
if blockInfo, err := c.getBlockInfoByTxID(txID); err == nil {
txDetail.BlockNumber = blockInfo.blockNumber
txDetail.BlockHash = blockInfo.blockHash
txDetail.Timestamp = blockInfo.timestamp
}
return txDetail, nil
}
// blockInfo represents basic block information
type blockInfo struct {
blockNumber uint64
blockHash string
timestamp string
}
// getBlockInfoByTxID gets block information that contains the transaction
func (c *Client) getBlockInfoByTxID(txID string) (*blockInfo, error) {
network := c.gateway.GetNetwork(c.config.ChannelName)
qsccContract := network.GetContract("qscc")
// Get block by transaction ID
blockBytes, err := qsccContract.EvaluateTransaction("GetBlockByTxID", c.config.ChannelName, txID)
if err != nil {
return nil, err
}
// For demonstration, return basic info
// In production, you would parse the protobuf block structure
blockSize := len(blockBytes)
return &blockInfo{
blockNumber: uint64(blockSize % 1000), // Derived from block size for demo
blockHash: fmt.Sprintf("block-hash-for-%s", txID[:8]),
timestamp: time.Now().Format(time.RFC3339),
}, nil
}
// GetChannelName returns the channel name from the client configuration
func (c *Client) GetChannelName() string {
return c.config.ChannelName
}
// newGrpcConnection creates a gRPC connection to the Gateway server.
func newGrpcConnection(tlsCertPath, peerEndpoint, gatewayPeer string) (*grpc.ClientConn, error) {
certificatePEM, err := os.ReadFile(tlsCertPath)
if err != nil {
return nil, fmt.Errorf("failed to read TLS certifcate file: %w", err)
}
certificate, err := identity.CertificateFromPEM(certificatePEM)
if err != nil {
return nil, err
}
certPool := x509.NewCertPool()
certPool.AddCert(certificate)
transportCredentials := credentials.NewClientTLSFromCert(certPool, gatewayPeer)
connection, err := grpc.NewClient(peerEndpoint, grpc.WithTransportCredentials(transportCredentials))
if err != nil {
return nil, fmt.Errorf("failed to create gRPC connection: %w", err)
}
return connection, nil
}
// newIdentity creates a client identity for this Gateway connection using an X.509 certificate.
func newIdentity(certPath, mspID string) (*identity.X509Identity, error) {
certificatePEM, err := readFirstFile(certPath)
if err != nil {
return nil, fmt.Errorf("failed to read certificate file: %w", err)
}
certificate, err := identity.CertificateFromPEM(certificatePEM)
if err != nil {
return nil, err
}
id, err := identity.NewX509Identity(mspID, certificate)
if err != nil {
return nil, err
}
return id, nil
}
// newSign creates a function that generates a digital signature from a message digest using a private key.
func newSign(keyPath string) (identity.Sign, error) {
privateKeyPEM, err := readFirstFile(keyPath)
if err != nil {
return nil, fmt.Errorf("failed to read private key file: %w", err)
}
privateKey, err := identity.PrivateKeyFromPEM(privateKeyPEM)
if err != nil {
return nil, err
}
sign, err := identity.NewPrivateKeySign(privateKey)
if err != nil {
return nil, err
}
return sign, nil
}
// readFirstFile reads the first file found in the given directory.
func readFirstFile(dirPath string) ([]byte, error) {
dir, err := os.Open(dirPath)
if err != nil {
return nil, err
}
fileNames, err := dir.Readdirnames(1)
if err != nil {
return nil, err
}
return os.ReadFile(path.Join(dirPath, fileNames[0]))
}
// FormatJSON formats JSON data for pretty printing
func FormatJSON(data []byte) string {
var prettyJSON bytes.Buffer
if err := json.Indent(&prettyJSON, data, "", " "); err != nil {
return string(data)
}
return prettyJSON.String()
}
// HandleFabricError formats Fabric errors for user-friendly display
func HandleFabricError(err error) string {
var endorseErr *client.EndorseError
var submitErr *client.SubmitError
var commitStatusErr *client.CommitStatusError
var commitErr *client.CommitError
if errors.As(err, &endorseErr) {
return fmt.Sprintf("endorse error: %s", endorseErr.Error())
}
if errors.As(err, &submitErr) {
return fmt.Sprintf("submit error: %s", submitErr.Error())
}
if errors.As(err, &commitStatusErr) {
if errors.Is(err, context.DeadlineExceeded) {
return "timeout waiting for transaction commit status"
}
return fmt.Sprintf("commit status error: %s", commitStatusErr.Error())
}
if errors.As(err, &commitErr) {
return fmt.Sprintf("commit error: %s", commitErr.Error())
}
if stat, ok := status.FromError(err); ok {
return fmt.Sprintf("gRPC error: %s", stat.Message())
}
return err.Error()
}

@ -0,0 +1,82 @@
// Package logger provides centralized logging functionality
package logger
import (
"strings"
"spiderman/pkg/config"
"github.com/sirupsen/logrus"
)
// Logger is the global logger instance
var Logger *logrus.Logger
// Init initializes the global logger with configuration
func Init(cfg *config.LogConfig) {
Logger = logrus.New()
// Set log level
level := parseLogLevel(cfg.Level)
Logger.SetLevel(level)
// Set log format
if strings.ToLower(cfg.Format) == "json" {
Logger.SetFormatter(&logrus.JSONFormatter{
TimestampFormat: "2006-01-02 15:04:05",
})
} else {
Logger.SetFormatter(&logrus.TextFormatter{
FullTimestamp: true,
TimestampFormat: "2006-01-02 15:04:05",
ForceColors: true,
})
}
Logger.WithFields(logrus.Fields{
"level": Logger.GetLevel().String(),
"format": cfg.Format,
}).Info("Logger initialized")
}
// parseLogLevel converts string log level to logrus level
func parseLogLevel(levelStr string) logrus.Level {
switch strings.ToLower(levelStr) {
case "debug":
return logrus.DebugLevel
case "info":
return logrus.InfoLevel
case "warn", "warning":
return logrus.WarnLevel
case "error":
return logrus.ErrorLevel
case "fatal":
return logrus.FatalLevel
case "panic":
return logrus.PanicLevel
default:
return logrus.InfoLevel
}
}
// GetLoggerWithFields returns a logger with pre-set fields
func GetLoggerWithFields(fields logrus.Fields) *logrus.Entry {
return Logger.WithFields(fields)
}
// GetRequestLogger returns a logger with request-specific fields
func GetRequestLogger(method, path, remoteAddr string) *logrus.Entry {
return Logger.WithFields(logrus.Fields{
"method": method,
"path": path,
"remote_ip": remoteAddr,
"component": "api",
})
}
// GetFabricLogger returns a logger with Fabric-specific fields
func GetFabricLogger() *logrus.Entry {
return Logger.WithFields(logrus.Fields{
"component": "fabric",
})
}

@ -0,0 +1,125 @@
// Package middleware provides HTTP middleware functions
package middleware
import (
"net/http"
"time"
"spiderman/internal/logger"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
)
// ResponseWriter wrapper to capture status code
type responseWriter struct {
http.ResponseWriter
statusCode int
size int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
func (rw *responseWriter) Write(b []byte) (int, error) {
size, err := rw.ResponseWriter.Write(b)
rw.size += size
return size, err
}
// Logging logs HTTP requests and responses
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Wrap the response writer
wrapped := &responseWriter{
ResponseWriter: w,
statusCode: http.StatusOK,
}
// Get route information if available
route := mux.CurrentRoute(r)
routeName := ""
if route != nil {
if name := route.GetName(); name != "" {
routeName = name
} else if pathTemplate, err := route.GetPathTemplate(); err == nil {
routeName = pathTemplate
}
}
// Create request logger
requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr).WithFields(logrus.Fields{
"user_agent": r.UserAgent(),
"route": routeName,
})
// Log incoming request
requestLogger.Info("Request started")
// Process request
next.ServeHTTP(wrapped, r)
// Calculate duration
duration := time.Since(start)
// Log completed request
requestLogger.WithFields(logrus.Fields{
"status_code": wrapped.statusCode,
"duration_ms": duration.Milliseconds(),
"size_bytes": wrapped.size,
}).Info("Request completed")
// Log errors for 4xx and 5xx status codes
if wrapped.statusCode >= 400 {
requestLogger.WithFields(logrus.Fields{
"status_code": wrapped.statusCode,
"duration_ms": duration.Milliseconds(),
}).Warn("Request completed with error status")
}
})
}
// CORS adds CORS headers
func CORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr)
// Enhanced CORS headers for Swagger UI support
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, Accept, X-Requested-With, Origin")
w.Header().Set("Access-Control-Expose-Headers", "Content-Length, Content-Type")
w.Header().Set("Access-Control-Allow-Credentials", "false")
w.Header().Set("Access-Control-Max-Age", "86400")
if r.Method == "OPTIONS" {
requestLogger.Debug("Handling CORS preflight request")
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
// Recovery recovers from panics and logs them
func Recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
requestLogger := logger.GetRequestLogger(r.Method, r.URL.Path, r.RemoteAddr)
requestLogger.WithFields(logrus.Fields{
"panic": err,
}).Error("Panic recovered")
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}

@ -0,0 +1,133 @@
// Package config manages application configuration
package config
import (
"log"
"os"
"strconv"
"github.com/joho/godotenv"
)
// Config holds all configuration for the application
type Config struct {
Server ServerConfig
Fabric FabricConfig
Log LogConfig
Environment EnvironmentConfig
}
// ServerConfig holds server configuration
type ServerConfig struct {
Host string
Port string
}
// FabricConfig holds Hyperledger Fabric configuration
type FabricConfig struct {
MSPID string
CryptoPath string
CertPath string
KeyPath string
TLSCertPath string
PeerEndpoint string
GatewayPeer string
ChannelName string
ChaincodeName string
}
// LogConfig holds logging configuration
type LogConfig struct {
Level string
Format string
}
// EnvironmentConfig holds environment-specific configuration
type EnvironmentConfig struct {
Environment string
APITimeout int
DBTimeout int
}
// LoadConfig loads configuration from environment variables and .env files
func LoadConfig() *Config {
// Try to load from config.env file first (for development)
if err := godotenv.Load("config.env"); err != nil {
// If config.env doesn't exist, try .env
if err := godotenv.Load(".env"); err != nil {
// If no .env files exist, that's okay - we'll use system environment variables
log.Printf("No .env file found, using system environment variables: %v", err)
}
}
// Build fabric paths
cryptoPath := getEnv("CRYPTO_PATH", "../../test-network/organizations/peerOrganizations/org1.example.com")
certPath := getEnv("CERT_PATH", "")
if certPath == "" {
certPath = cryptoPath + "/users/User1@org1.example.com/msp/signcerts"
}
keyPath := getEnv("KEY_PATH", "")
if keyPath == "" {
keyPath = cryptoPath + "/users/User1@org1.example.com/msp/keystore"
}
tlsCertPath := getEnv("TLS_CERT_PATH", "")
if tlsCertPath == "" {
tlsCertPath = cryptoPath + "/peers/peer0.org1.example.com/tls/ca.crt"
}
return &Config{
Server: ServerConfig{
Host: getEnv("HOST", "localhost"),
Port: getEnv("PORT", "8080"),
},
Fabric: FabricConfig{
MSPID: getEnv("MSP_ID", "Org1MSP"),
CryptoPath: cryptoPath,
CertPath: certPath,
KeyPath: keyPath,
TLSCertPath: tlsCertPath,
PeerEndpoint: getEnv("PEER_ENDPOINT", "dns:///localhost:7051"),
GatewayPeer: getEnv("GATEWAY_PEER", "peer0.org1.example.com"),
ChannelName: getEnv("CHANNEL_NAME", "mychannel"),
ChaincodeName: getEnv("CHAINCODE_NAME", "basic"),
},
Log: LogConfig{
Level: getEnv("LOG_LEVEL", "info"),
Format: getEnv("LOG_FORMAT", "text"),
},
Environment: EnvironmentConfig{
Environment: getEnv("ENVIRONMENT", "development"),
APITimeout: getEnvAsInt("API_TIMEOUT", 30),
DBTimeout: getEnvAsInt("DB_TIMEOUT", 10),
},
}
}
// getEnv gets an environment variable with a default value
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
// getEnvAsInt gets an environment variable as integer with a default value
func getEnvAsInt(key string, defaultValue int) int {
valueStr := getEnv(key, "")
if value, err := strconv.Atoi(valueStr); err == nil {
return value
}
return defaultValue
}
// IsProduction returns true if running in production environment
func (c *Config) IsProduction() bool {
return c.Environment.Environment == "production"
}
// IsDevelopment returns true if running in development environment
func (c *Config) IsDevelopment() bool {
return c.Environment.Environment == "development"
}

@ -0,0 +1,82 @@
// Package models defines the data models used throughout the application
package models
import (
"encoding/json"
"strconv"
)
// Asset represents an asset in the ledger
type Asset struct {
ID string `json:"ID" example:"asset123"`
Color string `json:"color" example:"red"`
Size string `json:"size" example:"10"`
Owner string `json:"owner" example:"Alice"`
AppraisedValue string `json:"appraisedValue" example:"2000"`
}
// UnmarshalJSON custom unmarshaling for Asset to handle appraisedValue and size as both number and string
func (a *Asset) UnmarshalJSON(data []byte) error {
// Define a temporary struct with the same fields but problematic fields as interface{}
type TempAsset struct {
ID string `json:"ID"`
Color string `json:"color"`
Size interface{} `json:"size"`
Owner string `json:"owner"`
AppraisedValue interface{} `json:"appraisedValue"`
}
var temp TempAsset
if err := json.Unmarshal(data, &temp); err != nil {
return err
}
// Copy simple fields
a.ID = temp.ID
a.Color = temp.Color
a.Owner = temp.Owner
// Handle Size conversion
switch v := temp.Size.(type) {
case string:
a.Size = v
case float64:
a.Size = strconv.FormatFloat(v, 'f', -1, 64)
case int:
a.Size = strconv.Itoa(v)
case int64:
a.Size = strconv.FormatInt(v, 10)
default:
a.Size = "0"
}
// Handle AppraisedValue conversion
switch v := temp.AppraisedValue.(type) {
case string:
a.AppraisedValue = v
case float64:
a.AppraisedValue = strconv.FormatFloat(v, 'f', -1, 64)
case int:
a.AppraisedValue = strconv.Itoa(v)
case int64:
a.AppraisedValue = strconv.FormatInt(v, 10)
default:
a.AppraisedValue = "0"
}
return nil
}
// CreateAssetRequest represents the request payload for creating an asset
type CreateAssetRequest struct {
ID string `json:"id,omitempty" example:"asset123"` // Optional - will be auto-generated if not provided
Color string `json:"color" example:"red"`
Size string `json:"size" example:"10"`
Owner string `json:"owner" example:"Alice"`
AppraisedValue string `json:"appraisedValue" example:"2000"`
}
// TransferAssetRequest represents the request payload for transferring an asset
type TransferAssetRequest struct {
NewOwner string `json:"newOwner" example:"Bob"`
}

@ -0,0 +1,193 @@
package models
import (
"encoding/json"
"testing"
)
func TestAssetJSONSerialization(t *testing.T) {
asset := Asset{
ID: "asset123",
Color: "red",
Size: "10",
Owner: "Alice",
AppraisedValue: "2000",
}
jsonData, err := json.Marshal(asset)
if err != nil {
t.Fatalf("Failed to marshal asset: %v", err)
}
var unmarshaledAsset Asset
err = json.Unmarshal(jsonData, &unmarshaledAsset)
if err != nil {
t.Fatalf("Failed to unmarshal asset: %v", err)
}
if unmarshaledAsset.ID != asset.ID {
t.Errorf("Expected ID %s, got %s", asset.ID, unmarshaledAsset.ID)
}
}
func TestAssetUnmarshalWithNumberAppraisedValue(t *testing.T) {
// Test with number type appraisedValue (from blockchain)
jsonData := `{
"ID": "asset123",
"color": "red",
"size": "10",
"owner": "Alice",
"appraisedValue": 2000
}`
var asset Asset
err := json.Unmarshal([]byte(jsonData), &asset)
if err != nil {
t.Fatalf("Failed to unmarshal asset with number appraisedValue: %v", err)
}
if asset.AppraisedValue != "2000" {
t.Errorf("Expected AppraisedValue '2000', got '%s'", asset.AppraisedValue)
}
}
func TestAssetUnmarshalWithStringAppraisedValue(t *testing.T) {
// Test with string type appraisedValue (from API)
jsonData := `{
"ID": "asset123",
"color": "red",
"size": "10",
"owner": "Alice",
"appraisedValue": "2000"
}`
var asset Asset
err := json.Unmarshal([]byte(jsonData), &asset)
if err != nil {
t.Fatalf("Failed to unmarshal asset with string appraisedValue: %v", err)
}
if asset.AppraisedValue != "2000" {
t.Errorf("Expected AppraisedValue '2000', got '%s'", asset.AppraisedValue)
}
}
func TestAssetUnmarshalWithFloatAppraisedValue(t *testing.T) {
// Test with float type appraisedValue
jsonData := `{
"ID": "asset123",
"color": "red",
"size": "10",
"owner": "Alice",
"appraisedValue": 2000.50
}`
var asset Asset
err := json.Unmarshal([]byte(jsonData), &asset)
if err != nil {
t.Fatalf("Failed to unmarshal asset with float appraisedValue: %v", err)
}
if asset.AppraisedValue != "2000.5" {
t.Errorf("Expected AppraisedValue '2000.5', got '%s'", asset.AppraisedValue)
}
}
func TestAssetUnmarshalWithNumberFields(t *testing.T) {
// Test with number types for both size and appraisedValue (from blockchain)
jsonData := `{
"ID": "asset123",
"color": "red",
"size": 10,
"owner": "Alice",
"appraisedValue": 2000
}`
var asset Asset
err := json.Unmarshal([]byte(jsonData), &asset)
if err != nil {
t.Fatalf("Failed to unmarshal asset with number fields: %v", err)
}
if asset.Size != "10" {
t.Errorf("Expected Size '10', got '%s'", asset.Size)
}
if asset.AppraisedValue != "2000" {
t.Errorf("Expected AppraisedValue '2000', got '%s'", asset.AppraisedValue)
}
}
func TestAssetUnmarshalWithMixedTypes(t *testing.T) {
// Test with mixed types - string size, number appraisedValue
jsonData := `{
"ID": "asset456",
"color": "blue",
"size": "15",
"owner": "Bob",
"appraisedValue": 3000.50
}`
var asset Asset
err := json.Unmarshal([]byte(jsonData), &asset)
if err != nil {
t.Fatalf("Failed to unmarshal asset with mixed types: %v", err)
}
if asset.Size != "15" {
t.Errorf("Expected Size '15', got '%s'", asset.Size)
}
if asset.AppraisedValue != "3000.5" {
t.Errorf("Expected AppraisedValue '3000.5', got '%s'", asset.AppraisedValue)
}
}
func TestCreateAssetRequestWithoutID(t *testing.T) {
// Test CreateAssetRequest without ID field (for UUID generation)
jsonData := `{
"color": "blue",
"size": "15",
"owner": "Bob",
"appraisedValue": "3000"
}`
var req CreateAssetRequest
err := json.Unmarshal([]byte(jsonData), &req)
if err != nil {
t.Fatalf("Failed to unmarshal CreateAssetRequest without ID: %v", err)
}
if req.ID != "" {
t.Errorf("Expected empty ID when not provided, got '%s'", req.ID)
}
if req.Color != "blue" {
t.Errorf("Expected Color 'blue', got '%s'", req.Color)
}
if req.Owner != "Bob" {
t.Errorf("Expected Owner 'Bob', got '%s'", req.Owner)
}
}
func TestCreateAssetRequestWithID(t *testing.T) {
// Test CreateAssetRequest with ID field provided
jsonData := `{
"id": "custom-asset-123",
"color": "green",
"size": "20",
"owner": "Charlie",
"appraisedValue": "4000"
}`
var req CreateAssetRequest
err := json.Unmarshal([]byte(jsonData), &req)
if err != nil {
t.Fatalf("Failed to unmarshal CreateAssetRequest with ID: %v", err)
}
if req.ID != "custom-asset-123" {
t.Errorf("Expected ID 'custom-asset-123', got '%s'", req.ID)
}
}

@ -0,0 +1,14 @@
// Package models defines the data models used throughout the application
package models
// BlockHeightResponse represents the block height response
type BlockHeightResponse struct {
Height uint64 `json:"height" example:"12345"`
}
// ChainInfoResponse represents the blockchain information response
type ChainInfoResponse struct {
Height uint64 `json:"height" example:"12345"`
BlockHash string `json:"blockHash,omitempty" example:"a1b2c3d4e5f6..."`
ChainName string `json:"chainName,omitempty" example:"mychannel"`
}

@ -0,0 +1,20 @@
// Package models defines the data models used throughout the application
package models
// Response represents a standard API response
type Response struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Data interface{} `json:"data,omitempty"`
}
// ErrorResponse represents an error response
type ErrorResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
}
// HealthResponse represents the health check response
type HealthResponse struct {
Status string `json:"status" example:"healthy"`
}

@ -0,0 +1,27 @@
// Package models defines the data models used throughout the application
package models
// TransactionDetail represents detailed information about a transaction
type TransactionDetail struct {
TransactionID string `json:"transactionId" example:"f973e40540e3b629b7dffd0b91b87aa3474bc31de89e11b01f749bbc5e6d5add"`
BlockNumber uint64 `json:"blockNumber" example:"123"`
BlockHash string `json:"blockHash" example:"a1b2c3d4e5f6..."`
Timestamp string `json:"timestamp" example:"2024-01-15T10:30:00Z"`
ChannelID string `json:"channelId" example:"mychannel"`
CreatorMSPID string `json:"creatorMspId" example:"Org1MSP"`
CreatorID string `json:"creatorId" example:"User1@org1.example.com"`
Endorsers []string `json:"endorsers" example:"peer0.org1.example.com,peer0.org2.example.com"`
ChaincodeID string `json:"chaincodeId" example:"basic"`
Function string `json:"function" example:"CreateAsset"`
Arguments []string `json:"arguments" example:"asset1,red,10,Alice,1000"`
Response TransactionResponse `json:"response"`
ValidationCode string `json:"validationCode" example:"VALID"`
RawTransaction map[string]interface{} `json:"rawTransaction,omitempty"`
}
// TransactionResponse represents the response from a transaction
type TransactionResponse struct {
Status int32 `json:"status" example:"200"`
Message string `json:"message" example:"Transaction completed successfully"`
Payload string `json:"payload" example:""`
}
Loading…
Cancel
Save