status-im/status-go

View on GitHub
services/wallet/thirdparty/rarible/types.go

Summary

Maintainability
A
0 mins
Test Coverage
C
72%
package rarible

import (
    "encoding/json"
    "fmt"
    "math/big"
    "strconv"
    "strings"

    "github.com/ethereum/go-ethereum/common"

    "github.com/status-im/status-go/services/wallet/bigint"
    walletCommon "github.com/status-im/status-go/services/wallet/common"
    "github.com/status-im/status-go/services/wallet/thirdparty"

    "golang.org/x/text/cases"
    "golang.org/x/text/language"
)

const RaribleID = "rarible"

const (
    ethereumString = "ETHEREUM"
    arbitrumString = "ARBITRUM"
)

func chainStringToChainID(chainString string, isMainnet bool) walletCommon.ChainID {
    chainID := walletCommon.UnknownChainID
    switch chainString {
    case ethereumString:
        if isMainnet {
            chainID = walletCommon.EthereumMainnet
        } else {
            chainID = walletCommon.EthereumSepolia
        }
    case arbitrumString:
        if isMainnet {
            chainID = walletCommon.ArbitrumMainnet
        } else {
            chainID = walletCommon.ArbitrumSepolia
        }
    }
    return walletCommon.ChainID(chainID)
}

func chainIDToChainString(chainID walletCommon.ChainID) string {
    chainString := ""
    switch uint64(chainID) {
    case walletCommon.EthereumMainnet, walletCommon.EthereumSepolia:
        chainString = ethereumString
    case walletCommon.ArbitrumMainnet, walletCommon.ArbitrumSepolia:
        chainString = arbitrumString
    }
    return chainString
}

func raribleToContractType(contractType string) walletCommon.ContractType {
    switch contractType {
    case "CRYPTO_PUNKS", "ERC721":
        return walletCommon.ContractTypeERC721
    case "ERC1155":
        return walletCommon.ContractTypeERC1155
    default:
        return walletCommon.ContractTypeUnknown
    }
}

func raribleContractIDToUniqueID(contractID string, isMainnet bool) (thirdparty.ContractID, error) {
    ret := thirdparty.ContractID{}

    parts := strings.Split(contractID, ":")
    if len(parts) != 2 {
        return ret, fmt.Errorf("invalid rarible contract id string %s", contractID)
    }

    ret.ChainID = chainStringToChainID(parts[0], isMainnet)
    if uint64(ret.ChainID) == walletCommon.UnknownChainID {
        return ret, fmt.Errorf("unknown rarible chainID in contract id string %s", contractID)
    }
    ret.Address = common.HexToAddress(parts[1])

    return ret, nil
}

func raribleCollectibleIDToUniqueID(collectibleID string, isMainnet bool) (thirdparty.CollectibleUniqueID, error) {
    ret := thirdparty.CollectibleUniqueID{}

    parts := strings.Split(collectibleID, ":")
    if len(parts) != 3 {
        return ret, fmt.Errorf("invalid rarible collectible id string %s", collectibleID)
    }

    ret.ContractID.ChainID = chainStringToChainID(parts[0], isMainnet)
    if uint64(ret.ContractID.ChainID) == walletCommon.UnknownChainID {
        return ret, fmt.Errorf("unknown rarible chainID in collectible id string %s", collectibleID)
    }
    ret.ContractID.Address = common.HexToAddress(parts[1])
    tokenID, ok := big.NewInt(0).SetString(parts[2], 10)
    if !ok {
        return ret, fmt.Errorf("invalid rarible tokenID %s", collectibleID)
    }
    ret.TokenID = &bigint.BigInt{
        Int: tokenID,
    }

    return ret, nil
}

type BatchTokenIDs struct {
    IDs []string `json:"ids"`
}

type CollectibleFilterFullTextField = string

const (
    CollectibleFilterFullTextFieldName        = "NAME"
    CollectibleFilterFullTextFieldDescription = "DESCRIPTION"
)

type CollectibleFilterFullText struct {
    Text   string                           `json:"text"`
    Fields []CollectibleFilterFullTextField `json:"fields"`
}

type CollectibleFilter struct {
    Blockchains []string                  `json:"blockchains"`
    Collections []string                  `json:"collections,omitempty"`
    Deleted     bool                      `json:"deleted"`
    FullText    CollectibleFilterFullText `json:"fullText"`
}

type CollectibleFilterContainerSort = string

const (
    CollectibleFilterContainerSortRelevance = "RELEVANCE"
    CollectibleFilterContainerSortLatest    = "LATEST"
    CollectibleFilterContainerSortEarliest  = "EARLIEST"
)

type CollectibleFilterContainer struct {
    Limit  int                            `json:"size"`
    Cursor string                         `json:"continuation"`
    Filter CollectibleFilter              `json:"filter"`
    Sort   CollectibleFilterContainerSort `json:"sort"`
}

type CollectionFilter struct {
    Blockchains []string `json:"blockchains"`
    Text        string   `json:"text"`
}

type CollectionFilterContainer struct {
    Limit  int              `json:"size"`
    Cursor string           `json:"continuation"`
    Filter CollectionFilter `json:"filter"`
}

type CollectiblesContainer struct {
    Continuation string        `json:"continuation"`
    Collectibles []Collectible `json:"items"`
}

type Collectible struct {
    ID         string              `json:"id"`
    Blockchain string              `json:"blockchain"`
    Collection string              `json:"collection"`
    Contract   string              `json:"contract"`
    TokenID    *bigint.BigInt      `json:"tokenId"`
    Metadata   CollectibleMetadata `json:"meta"`
}

type CollectibleMetadata struct {
    Name            string      `json:"name"`
    Description     string      `json:"description"`
    ExternalURI     string      `json:"externalUri"`
    OriginalMetaURI string      `json:"originalMetaUri"`
    Attributes      []Attribute `json:"attributes"`
    Contents        []Content   `json:"content"`
}

type Attribute struct {
    Key   string         `json:"key"`
    Value AttributeValue `json:"value"`
}

type AttributeValue string

func (st *AttributeValue) UnmarshalJSON(b []byte) error {
    var item interface{}
    if err := json.Unmarshal(b, &item); err != nil {
        return err
    }

    switch v := item.(type) {
    case float64:
        *st = AttributeValue(strconv.FormatFloat(v, 'f', 2, 64))
    case int:
        *st = AttributeValue(strconv.Itoa(v))
    case string:
        *st = AttributeValue(v)
    }
    return nil
}

type CollectionsContainer struct {
    Continuation string       `json:"continuation"`
    Collections  []Collection `json:"collections"`
}

type Collection struct {
    ID           string             `json:"id"`
    Blockchain   string             `json:"blockchain"`
    ContractType string             `json:"type"`
    Name         string             `json:"name"`
    Metadata     CollectionMetadata `json:"meta"`
}

type CollectionMetadata struct {
    Name        string    `json:"name"`
    Description string    `json:"description"`
    Contents    []Content `json:"content"`
}

type Content struct {
    Type           string `json:"@type"`
    URL            string `json:"url"`
    Representation string `json:"representation"`
    Available      bool   `json:"available"`
}

type ContractOwnershipContainer struct {
    Continuation string              `json:"continuation"`
    Ownerships   []ContractOwnership `json:"ownerships"`
}

type ContractOwnership struct {
    ID         string         `json:"id"`
    Blockchain string         `json:"blockchain"`
    ItemID     string         `json:"itemId"`
    Contract   string         `json:"contract"`
    Collection string         `json:"collection"`
    TokenID    *bigint.BigInt `json:"tokenId"`
    Owner      string         `json:"owner"`
    Value      *bigint.BigInt `json:"value"`
}

func raribleContractOwnershipsToCommon(raribleOwnerships []ContractOwnership) []thirdparty.CollectibleOwner {
    balancesPerOwner := make(map[common.Address][]thirdparty.TokenBalance)
    for _, raribleOwnership := range raribleOwnerships {
        owner := common.HexToAddress(raribleOwnership.Owner)
        if _, ok := balancesPerOwner[owner]; !ok {
            balancesPerOwner[owner] = make([]thirdparty.TokenBalance, 0)
        }

        balance := thirdparty.TokenBalance{
            TokenID: raribleOwnership.TokenID,
            Balance: raribleOwnership.Value,
        }
        balancesPerOwner[owner] = append(balancesPerOwner[owner], balance)
    }

    ret := make([]thirdparty.CollectibleOwner, 0, len(balancesPerOwner))
    for owner, balances := range balancesPerOwner {
        ret = append(ret, thirdparty.CollectibleOwner{
            OwnerAddress:  owner,
            TokenBalances: balances,
        })
    }

    return ret
}

func raribleToCollectibleTraits(attributes []Attribute) []thirdparty.CollectibleTrait {
    ret := make([]thirdparty.CollectibleTrait, 0, len(attributes))
    caser := cases.Title(language.Und, cases.NoLower)
    for _, orig := range attributes {
        dest := thirdparty.CollectibleTrait{
            TraitType: orig.Key,
            Value:     caser.String(string(orig.Value)),
        }

        ret = append(ret, dest)
    }
    return ret
}

func raribleToCollectiblesData(l []Collectible, isMainnet bool) []thirdparty.FullCollectibleData {
    ret := make([]thirdparty.FullCollectibleData, 0, len(l))
    for _, c := range l {
        id, err := raribleCollectibleIDToUniqueID(c.ID, isMainnet)
        if err != nil {
            continue
        }
        item := c.toCommon(id)
        ret = append(ret, item)
    }
    return ret
}

func raribleToCollectionsData(l []Collection, isMainnet bool) []thirdparty.CollectionData {
    ret := make([]thirdparty.CollectionData, 0, len(l))
    for _, c := range l {
        id, err := raribleContractIDToUniqueID(c.ID, isMainnet)
        if err != nil {
            continue
        }
        item := c.toCommon(id)
        ret = append(ret, item)
    }
    return ret
}

func (c *Collection) toCommon(id thirdparty.ContractID) thirdparty.CollectionData {
    ret := thirdparty.CollectionData{
        ID:           id,
        ContractType: raribleToContractType(c.ContractType),
        Provider:     RaribleID,
        Name:         c.Metadata.Name,
        Slug:         "", /* Missing from the API for now */
        ImageURL:     getImageURL(c.Metadata.Contents),
        Traits:       make(map[string]thirdparty.CollectionTrait, 0), /* Missing from the API for now */
    }
    return ret
}

func contentTypeValue(contentType string, includeOriginal bool) int {
    ret := -1

    switch contentType {
    case "PREVIEW":
        ret = 1
    case "PORTRAIT":
        ret = 2
    case "BIG":
        ret = 3
    case "ORIGINAL":
        if includeOriginal {
            ret = 4
        }
    }

    return ret
}

func isNewContentBigger(current string, new string, includeOriginal bool) bool {
    currentValue := contentTypeValue(current, includeOriginal)
    newValue := contentTypeValue(new, includeOriginal)

    return newValue > currentValue
}

func getBiggestContentURL(contents []Content, contentType string, includeOriginal bool) string {
    ret := Content{
        Type:           "",
        URL:            "",
        Representation: "",
        Available:      false,
    }

    for _, content := range contents {
        if content.Type == contentType {
            if isNewContentBigger(ret.Representation, content.Representation, includeOriginal) {
                ret = content
            }
        }
    }

    return ret.URL
}

func getAnimationURL(contents []Content) string {
    // Try to get the biggest content of type "VIDEO"
    ret := getBiggestContentURL(contents, "VIDEO", true)

    // If empty, try to get the biggest content of type "IMAGE", including the "ORIGINAL" representation
    if ret == "" {
        ret = getBiggestContentURL(contents, "IMAGE", true)
    }

    return ret
}

func getImageURL(contents []Content) string {
    // Get the biggest content of type "IMAGE", excluding the "ORIGINAL" representation
    ret := getBiggestContentURL(contents, "IMAGE", false)

    // If empty, allow the "ORIGINAL" representation
    if ret == "" {
        ret = getBiggestContentURL(contents, "IMAGE", true)
    }

    return ret
}

func (c *Collectible) toCollectibleData(id thirdparty.CollectibleUniqueID) thirdparty.CollectibleData {
    imageURL := getImageURL(c.Metadata.Contents)
    animationURL := getAnimationURL(c.Metadata.Contents)

    if animationURL == "" {
        animationURL = imageURL
    }

    return thirdparty.CollectibleData{
        ID:           id,
        ContractType: walletCommon.ContractTypeUnknown, // Rarible doesn't provide the contract type with the collectible
        Provider:     RaribleID,
        Name:         c.Metadata.Name,
        Description:  c.Metadata.Description,
        Permalink:    c.Metadata.ExternalURI,
        ImageURL:     imageURL,
        AnimationURL: animationURL,
        Traits:       raribleToCollectibleTraits(c.Metadata.Attributes),
        TokenURI:     c.Metadata.OriginalMetaURI,
    }
}

func (c *Collectible) toCommon(id thirdparty.CollectibleUniqueID) thirdparty.FullCollectibleData {
    return thirdparty.FullCollectibleData{
        CollectibleData: c.toCollectibleData(id),
        CollectionData:  nil,
    }
}