github.com/status-im/status-go@v1.1.0/services/wallet/thirdparty/rarible/types.go (about)

     1  package rarible
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"math/big"
     7  	"strconv"
     8  	"strings"
     9  
    10  	"github.com/ethereum/go-ethereum/common"
    11  
    12  	"github.com/status-im/status-go/services/wallet/bigint"
    13  	walletCommon "github.com/status-im/status-go/services/wallet/common"
    14  	"github.com/status-im/status-go/services/wallet/thirdparty"
    15  
    16  	"golang.org/x/text/cases"
    17  	"golang.org/x/text/language"
    18  )
    19  
    20  const RaribleID = "rarible"
    21  
    22  const (
    23  	ethereumString = "ETHEREUM"
    24  	arbitrumString = "ARBITRUM"
    25  )
    26  
    27  func chainStringToChainID(chainString string, isMainnet bool) walletCommon.ChainID {
    28  	chainID := walletCommon.UnknownChainID
    29  	switch chainString {
    30  	case ethereumString:
    31  		if isMainnet {
    32  			chainID = walletCommon.EthereumMainnet
    33  		} else {
    34  			chainID = walletCommon.EthereumSepolia
    35  		}
    36  	case arbitrumString:
    37  		if isMainnet {
    38  			chainID = walletCommon.ArbitrumMainnet
    39  		} else {
    40  			chainID = walletCommon.ArbitrumSepolia
    41  		}
    42  	}
    43  	return walletCommon.ChainID(chainID)
    44  }
    45  
    46  func chainIDToChainString(chainID walletCommon.ChainID) string {
    47  	chainString := ""
    48  	switch uint64(chainID) {
    49  	case walletCommon.EthereumMainnet, walletCommon.EthereumSepolia:
    50  		chainString = ethereumString
    51  	case walletCommon.ArbitrumMainnet, walletCommon.ArbitrumSepolia:
    52  		chainString = arbitrumString
    53  	}
    54  	return chainString
    55  }
    56  
    57  func raribleToContractType(contractType string) walletCommon.ContractType {
    58  	switch contractType {
    59  	case "CRYPTO_PUNKS", "ERC721":
    60  		return walletCommon.ContractTypeERC721
    61  	case "ERC1155":
    62  		return walletCommon.ContractTypeERC1155
    63  	default:
    64  		return walletCommon.ContractTypeUnknown
    65  	}
    66  }
    67  
    68  func raribleContractIDToUniqueID(contractID string, isMainnet bool) (thirdparty.ContractID, error) {
    69  	ret := thirdparty.ContractID{}
    70  
    71  	parts := strings.Split(contractID, ":")
    72  	if len(parts) != 2 {
    73  		return ret, fmt.Errorf("invalid rarible contract id string %s", contractID)
    74  	}
    75  
    76  	ret.ChainID = chainStringToChainID(parts[0], isMainnet)
    77  	if uint64(ret.ChainID) == walletCommon.UnknownChainID {
    78  		return ret, fmt.Errorf("unknown rarible chainID in contract id string %s", contractID)
    79  	}
    80  	ret.Address = common.HexToAddress(parts[1])
    81  
    82  	return ret, nil
    83  }
    84  
    85  func raribleCollectibleIDToUniqueID(collectibleID string, isMainnet bool) (thirdparty.CollectibleUniqueID, error) {
    86  	ret := thirdparty.CollectibleUniqueID{}
    87  
    88  	parts := strings.Split(collectibleID, ":")
    89  	if len(parts) != 3 {
    90  		return ret, fmt.Errorf("invalid rarible collectible id string %s", collectibleID)
    91  	}
    92  
    93  	ret.ContractID.ChainID = chainStringToChainID(parts[0], isMainnet)
    94  	if uint64(ret.ContractID.ChainID) == walletCommon.UnknownChainID {
    95  		return ret, fmt.Errorf("unknown rarible chainID in collectible id string %s", collectibleID)
    96  	}
    97  	ret.ContractID.Address = common.HexToAddress(parts[1])
    98  	tokenID, ok := big.NewInt(0).SetString(parts[2], 10)
    99  	if !ok {
   100  		return ret, fmt.Errorf("invalid rarible tokenID %s", collectibleID)
   101  	}
   102  	ret.TokenID = &bigint.BigInt{
   103  		Int: tokenID,
   104  	}
   105  
   106  	return ret, nil
   107  }
   108  
   109  type BatchTokenIDs struct {
   110  	IDs []string `json:"ids"`
   111  }
   112  
   113  type CollectibleFilterFullTextField = string
   114  
   115  const (
   116  	CollectibleFilterFullTextFieldName        = "NAME"
   117  	CollectibleFilterFullTextFieldDescription = "DESCRIPTION"
   118  )
   119  
   120  type CollectibleFilterFullText struct {
   121  	Text   string                           `json:"text"`
   122  	Fields []CollectibleFilterFullTextField `json:"fields"`
   123  }
   124  
   125  type CollectibleFilter struct {
   126  	Blockchains []string                  `json:"blockchains"`
   127  	Collections []string                  `json:"collections,omitempty"`
   128  	Deleted     bool                      `json:"deleted"`
   129  	FullText    CollectibleFilterFullText `json:"fullText"`
   130  }
   131  
   132  type CollectibleFilterContainerSort = string
   133  
   134  const (
   135  	CollectibleFilterContainerSortRelevance = "RELEVANCE"
   136  	CollectibleFilterContainerSortLatest    = "LATEST"
   137  	CollectibleFilterContainerSortEarliest  = "EARLIEST"
   138  )
   139  
   140  type CollectibleFilterContainer struct {
   141  	Limit  int                            `json:"size"`
   142  	Cursor string                         `json:"continuation"`
   143  	Filter CollectibleFilter              `json:"filter"`
   144  	Sort   CollectibleFilterContainerSort `json:"sort"`
   145  }
   146  
   147  type CollectionFilter struct {
   148  	Blockchains []string `json:"blockchains"`
   149  	Text        string   `json:"text"`
   150  }
   151  
   152  type CollectionFilterContainer struct {
   153  	Limit  int              `json:"size"`
   154  	Cursor string           `json:"continuation"`
   155  	Filter CollectionFilter `json:"filter"`
   156  }
   157  
   158  type CollectiblesContainer struct {
   159  	Continuation string        `json:"continuation"`
   160  	Collectibles []Collectible `json:"items"`
   161  }
   162  
   163  type Collectible struct {
   164  	ID         string              `json:"id"`
   165  	Blockchain string              `json:"blockchain"`
   166  	Collection string              `json:"collection"`
   167  	Contract   string              `json:"contract"`
   168  	TokenID    *bigint.BigInt      `json:"tokenId"`
   169  	Metadata   CollectibleMetadata `json:"meta"`
   170  }
   171  
   172  type CollectibleMetadata struct {
   173  	Name            string      `json:"name"`
   174  	Description     string      `json:"description"`
   175  	ExternalURI     string      `json:"externalUri"`
   176  	OriginalMetaURI string      `json:"originalMetaUri"`
   177  	Attributes      []Attribute `json:"attributes"`
   178  	Contents        []Content   `json:"content"`
   179  }
   180  
   181  type Attribute struct {
   182  	Key   string         `json:"key"`
   183  	Value AttributeValue `json:"value"`
   184  }
   185  
   186  type AttributeValue string
   187  
   188  func (st *AttributeValue) UnmarshalJSON(b []byte) error {
   189  	var item interface{}
   190  	if err := json.Unmarshal(b, &item); err != nil {
   191  		return err
   192  	}
   193  
   194  	switch v := item.(type) {
   195  	case float64:
   196  		*st = AttributeValue(strconv.FormatFloat(v, 'f', 2, 64))
   197  	case int:
   198  		*st = AttributeValue(strconv.Itoa(v))
   199  	case string:
   200  		*st = AttributeValue(v)
   201  	}
   202  	return nil
   203  }
   204  
   205  type CollectionsContainer struct {
   206  	Continuation string       `json:"continuation"`
   207  	Collections  []Collection `json:"collections"`
   208  }
   209  
   210  type Collection struct {
   211  	ID           string             `json:"id"`
   212  	Blockchain   string             `json:"blockchain"`
   213  	ContractType string             `json:"type"`
   214  	Name         string             `json:"name"`
   215  	Metadata     CollectionMetadata `json:"meta"`
   216  }
   217  
   218  type CollectionMetadata struct {
   219  	Name        string    `json:"name"`
   220  	Description string    `json:"description"`
   221  	Contents    []Content `json:"content"`
   222  }
   223  
   224  type Content struct {
   225  	Type           string `json:"@type"`
   226  	URL            string `json:"url"`
   227  	Representation string `json:"representation"`
   228  	Available      bool   `json:"available"`
   229  }
   230  
   231  type ContractOwnershipContainer struct {
   232  	Continuation string              `json:"continuation"`
   233  	Ownerships   []ContractOwnership `json:"ownerships"`
   234  }
   235  
   236  type ContractOwnership struct {
   237  	ID         string         `json:"id"`
   238  	Blockchain string         `json:"blockchain"`
   239  	ItemID     string         `json:"itemId"`
   240  	Contract   string         `json:"contract"`
   241  	Collection string         `json:"collection"`
   242  	TokenID    *bigint.BigInt `json:"tokenId"`
   243  	Owner      string         `json:"owner"`
   244  	Value      *bigint.BigInt `json:"value"`
   245  }
   246  
   247  func raribleContractOwnershipsToCommon(raribleOwnerships []ContractOwnership) []thirdparty.CollectibleOwner {
   248  	balancesPerOwner := make(map[common.Address][]thirdparty.TokenBalance)
   249  	for _, raribleOwnership := range raribleOwnerships {
   250  		owner := common.HexToAddress(raribleOwnership.Owner)
   251  		if _, ok := balancesPerOwner[owner]; !ok {
   252  			balancesPerOwner[owner] = make([]thirdparty.TokenBalance, 0)
   253  		}
   254  
   255  		balance := thirdparty.TokenBalance{
   256  			TokenID: raribleOwnership.TokenID,
   257  			Balance: raribleOwnership.Value,
   258  		}
   259  		balancesPerOwner[owner] = append(balancesPerOwner[owner], balance)
   260  	}
   261  
   262  	ret := make([]thirdparty.CollectibleOwner, 0, len(balancesPerOwner))
   263  	for owner, balances := range balancesPerOwner {
   264  		ret = append(ret, thirdparty.CollectibleOwner{
   265  			OwnerAddress:  owner,
   266  			TokenBalances: balances,
   267  		})
   268  	}
   269  
   270  	return ret
   271  }
   272  
   273  func raribleToCollectibleTraits(attributes []Attribute) []thirdparty.CollectibleTrait {
   274  	ret := make([]thirdparty.CollectibleTrait, 0, len(attributes))
   275  	caser := cases.Title(language.Und, cases.NoLower)
   276  	for _, orig := range attributes {
   277  		dest := thirdparty.CollectibleTrait{
   278  			TraitType: orig.Key,
   279  			Value:     caser.String(string(orig.Value)),
   280  		}
   281  
   282  		ret = append(ret, dest)
   283  	}
   284  	return ret
   285  }
   286  
   287  func raribleToCollectiblesData(l []Collectible, isMainnet bool) []thirdparty.FullCollectibleData {
   288  	ret := make([]thirdparty.FullCollectibleData, 0, len(l))
   289  	for _, c := range l {
   290  		id, err := raribleCollectibleIDToUniqueID(c.ID, isMainnet)
   291  		if err != nil {
   292  			continue
   293  		}
   294  		item := c.toCommon(id)
   295  		ret = append(ret, item)
   296  	}
   297  	return ret
   298  }
   299  
   300  func raribleToCollectionsData(l []Collection, isMainnet bool) []thirdparty.CollectionData {
   301  	ret := make([]thirdparty.CollectionData, 0, len(l))
   302  	for _, c := range l {
   303  		id, err := raribleContractIDToUniqueID(c.ID, isMainnet)
   304  		if err != nil {
   305  			continue
   306  		}
   307  		item := c.toCommon(id)
   308  		ret = append(ret, item)
   309  	}
   310  	return ret
   311  }
   312  
   313  func (c *Collection) toCommon(id thirdparty.ContractID) thirdparty.CollectionData {
   314  	ret := thirdparty.CollectionData{
   315  		ID:           id,
   316  		ContractType: raribleToContractType(c.ContractType),
   317  		Provider:     RaribleID,
   318  		Name:         c.Metadata.Name,
   319  		Slug:         "", /* Missing from the API for now */
   320  		ImageURL:     getImageURL(c.Metadata.Contents),
   321  		Traits:       make(map[string]thirdparty.CollectionTrait, 0), /* Missing from the API for now */
   322  	}
   323  	return ret
   324  }
   325  
   326  func contentTypeValue(contentType string, includeOriginal bool) int {
   327  	ret := -1
   328  
   329  	switch contentType {
   330  	case "PREVIEW":
   331  		ret = 1
   332  	case "PORTRAIT":
   333  		ret = 2
   334  	case "BIG":
   335  		ret = 3
   336  	case "ORIGINAL":
   337  		if includeOriginal {
   338  			ret = 4
   339  		}
   340  	}
   341  
   342  	return ret
   343  }
   344  
   345  func isNewContentBigger(current string, new string, includeOriginal bool) bool {
   346  	currentValue := contentTypeValue(current, includeOriginal)
   347  	newValue := contentTypeValue(new, includeOriginal)
   348  
   349  	return newValue > currentValue
   350  }
   351  
   352  func getBiggestContentURL(contents []Content, contentType string, includeOriginal bool) string {
   353  	ret := Content{
   354  		Type:           "",
   355  		URL:            "",
   356  		Representation: "",
   357  		Available:      false,
   358  	}
   359  
   360  	for _, content := range contents {
   361  		if content.Type == contentType {
   362  			if isNewContentBigger(ret.Representation, content.Representation, includeOriginal) {
   363  				ret = content
   364  			}
   365  		}
   366  	}
   367  
   368  	return ret.URL
   369  }
   370  
   371  func getAnimationURL(contents []Content) string {
   372  	// Try to get the biggest content of type "VIDEO"
   373  	ret := getBiggestContentURL(contents, "VIDEO", true)
   374  
   375  	// If empty, try to get the biggest content of type "IMAGE", including the "ORIGINAL" representation
   376  	if ret == "" {
   377  		ret = getBiggestContentURL(contents, "IMAGE", true)
   378  	}
   379  
   380  	return ret
   381  }
   382  
   383  func getImageURL(contents []Content) string {
   384  	// Get the biggest content of type "IMAGE", excluding the "ORIGINAL" representation
   385  	ret := getBiggestContentURL(contents, "IMAGE", false)
   386  
   387  	// If empty, allow the "ORIGINAL" representation
   388  	if ret == "" {
   389  		ret = getBiggestContentURL(contents, "IMAGE", true)
   390  	}
   391  
   392  	return ret
   393  }
   394  
   395  func (c *Collectible) toCollectibleData(id thirdparty.CollectibleUniqueID) thirdparty.CollectibleData {
   396  	imageURL := getImageURL(c.Metadata.Contents)
   397  	animationURL := getAnimationURL(c.Metadata.Contents)
   398  
   399  	if animationURL == "" {
   400  		animationURL = imageURL
   401  	}
   402  
   403  	return thirdparty.CollectibleData{
   404  		ID:           id,
   405  		ContractType: walletCommon.ContractTypeUnknown, // Rarible doesn't provide the contract type with the collectible
   406  		Provider:     RaribleID,
   407  		Name:         c.Metadata.Name,
   408  		Description:  c.Metadata.Description,
   409  		Permalink:    c.Metadata.ExternalURI,
   410  		ImageURL:     imageURL,
   411  		AnimationURL: animationURL,
   412  		Traits:       raribleToCollectibleTraits(c.Metadata.Attributes),
   413  		TokenURI:     c.Metadata.OriginalMetaURI,
   414  	}
   415  }
   416  
   417  func (c *Collectible) toCommon(id thirdparty.CollectibleUniqueID) thirdparty.FullCollectibleData {
   418  	return thirdparty.FullCollectibleData{
   419  		CollectibleData: c.toCollectibleData(id),
   420  		CollectionData:  nil,
   421  	}
   422  }