commit
f374898c6e
@ -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,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 |
||||
) |
@ -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…
Reference in new issue