解析 NFT 元数据,支持 IPFS/Arweave 存储,图片缓存与压缩
┌──────────────┐ ┌─────────────┐ ┌──────────────┐
│ Clients │────▶│ Fiber API │────▶│ Blockchain │
│ (REST/HTTP) │ │ Service │ │ (go-ethereum)│
└──────────────┘ └─────────────┘ └──────────────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Redis │ │ MinIO │ │ IPFS │
│ (Cache) │ │ (Images) │ │ (Gateway)│
└──────────┘ └──────────┘ └──────────┘
1type NFTMetadata struct {2 Name string `json:"name"`3 Description string `json:"description"`4 Image string `json:"image"`5 ExternalURL string `json:"external_url,omitempty"`6 AnimationURL string `json:"animation_url,omitempty"`7 Attributes []NFTAttribute `json:"attributes,omitempty"`8}9 10type NFTAttribute struct {11 TraitType string `json:"trait_type"`12 Value interface{} `json:"value"`13 DisplayType string `json:"display_type,omitempty"`14}15 16func (s *NFTService) GetTokenURI(ctx context.Context, chain, contract string, tokenID *big.Int) (string, error) {17 client := s.clients[chain]18 19 // 尝试 ERC72120 uri, err := s.callTokenURI(ctx, client, contract, tokenID)21 if err != nil {22 // 尝试 ERC115523 uri, err = s.callURI(ctx, client, contract, tokenID)24 }25 26 return uri, err27}1type StorageGateway struct {2 ipfsGateways []string3 arweaveGateways []string4 httpClient *http.Client5}6 7func (g *StorageGateway) ResolveURI(ctx context.Context, uri string) ([]byte, error) {8 switch {9 case strings.HasPrefix(uri, "ipfs://"):10 return g.fetchFromIPFS(ctx, strings.TrimPrefix(uri, "ipfs://"))11 case strings.HasPrefix(uri, "ar://"):12 return g.fetchFromArweave(ctx, strings.TrimPrefix(uri, "ar://"))13 case strings.HasPrefix(uri, "data:"):14 return g.decodeDataURI(uri)15 default:16 return g.fetchHTTP(ctx, uri)17 }18}19 20func (g *StorageGateway) fetchFromIPFS(ctx context.Context, cid string) ([]byte, error) {21 for _, gateway := range g.ipfsGateways {22 url := fmt.Sprintf("%s/ipfs/%s", gateway, cid)23 req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)24 resp, err := g.httpClient.Do(req)25 if err != nil || resp.StatusCode != 200 {26 continue27 }28 defer resp.Body.Close()29 return io.ReadAll(resp.Body)30 }31 return nil, fmt.Errorf("failed to fetch from IPFS: %s", cid)32}1func (s *NFTService) calculateRarity(ctx context.Context, chain, contract string, attrs []NFTAttribute) (*RarityInfo, error) {2 distribution, err := s.getTraitDistribution(ctx, chain, contract)3 if err != nil {4 return nil, err5 }6 7 totalSupply := distribution.TotalSupply8 score := 0.09 traitRarity := make(map[string]float64)10 11 for _, attr := range attrs {12 key := fmt.Sprintf("%s:%v", attr.TraitType, attr.Value)13 count := distribution.Traits[key]14 15 // 稀有度 = 1 / (trait_count / total_supply)16 rarity := float64(totalSupply) / float64(count)17 score += rarity18 traitRarity[attr.TraitType] = rarity19 }20 21 return &RarityInfo{22 Score: score,23 TraitRarity: traitRarity,24 }, nil25}