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 }