Generated
Vendored
+459
@@ -0,0 +1,459 @@
|
||||
package alchemy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
|
||||
walletCommon "github.com/status-im/status-go/services/wallet/common"
|
||||
"github.com/status-im/status-go/services/wallet/connection"
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty"
|
||||
)
|
||||
|
||||
const nftMetadataBatchLimit = 100
|
||||
const contractMetadataBatchLimit = 100
|
||||
|
||||
func getBaseURL(chainID walletCommon.ChainID) (string, error) {
|
||||
switch uint64(chainID) {
|
||||
case walletCommon.EthereumMainnet:
|
||||
return "https://eth-mainnet.g.alchemy.com", nil
|
||||
case walletCommon.EthereumGoerli:
|
||||
return "https://eth-goerli.g.alchemy.com", nil
|
||||
case walletCommon.EthereumSepolia:
|
||||
return "https://eth-sepolia.g.alchemy.com", nil
|
||||
case walletCommon.OptimismMainnet:
|
||||
return "https://opt-mainnet.g.alchemy.com", nil
|
||||
case walletCommon.OptimismGoerli:
|
||||
return "https://opt-goerli.g.alchemy.com", nil
|
||||
case walletCommon.OptimismSepolia:
|
||||
return "https://opt-sepolia.g.alchemy.com", nil
|
||||
case walletCommon.ArbitrumMainnet:
|
||||
return "https://arb-mainnet.g.alchemy.com", nil
|
||||
case walletCommon.ArbitrumGoerli:
|
||||
return "https://arb-goerli.g.alchemy.com", nil
|
||||
case walletCommon.ArbitrumSepolia:
|
||||
return "https://arb-sepolia.g.alchemy.com", nil
|
||||
}
|
||||
|
||||
return "", thirdparty.ErrChainIDNotSupported
|
||||
}
|
||||
|
||||
func (o *Client) ID() string {
|
||||
return AlchemyID
|
||||
}
|
||||
|
||||
func (o *Client) IsChainSupported(chainID walletCommon.ChainID) bool {
|
||||
_, err := getBaseURL(chainID)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (o *Client) IsConnected() bool {
|
||||
return o.connectionStatus.IsConnected()
|
||||
}
|
||||
|
||||
func getAPIKeySubpath(apiKey string) string {
|
||||
if apiKey == "" {
|
||||
return "demo"
|
||||
}
|
||||
return apiKey
|
||||
}
|
||||
|
||||
func getNFTBaseURL(chainID walletCommon.ChainID, apiKey string) (string, error) {
|
||||
baseURL, err := getBaseURL(chainID)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s/nft/v3/%s", baseURL, getAPIKeySubpath(apiKey)), nil
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
thirdparty.CollectibleContractOwnershipProvider
|
||||
client *http.Client
|
||||
apiKeys map[uint64]string
|
||||
connectionStatus *connection.Status
|
||||
}
|
||||
|
||||
func NewClient(apiKeys map[uint64]string) *Client {
|
||||
for _, chainID := range walletCommon.AllChainIDs() {
|
||||
if apiKeys[uint64(chainID)] == "" {
|
||||
log.Warn("Alchemy API key not available for", "chainID", chainID)
|
||||
}
|
||||
}
|
||||
|
||||
return &Client{
|
||||
client: &http.Client{Timeout: time.Minute},
|
||||
apiKeys: apiKeys,
|
||||
connectionStatus: connection.NewStatus(),
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Client) doQuery(ctx context.Context, url string) (*http.Response, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return o.doWithRetries(req)
|
||||
}
|
||||
|
||||
func (o *Client) doPostWithJSON(ctx context.Context, url string, payload any) (*http.Response, error) {
|
||||
payloadJSON, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
payloadString := string(payloadJSON)
|
||||
payloadReader := strings.NewReader(payloadString)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", url, payloadReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Add("accept", "application/json")
|
||||
req.Header.Add("content-type", "application/json")
|
||||
|
||||
return o.doWithRetries(req)
|
||||
}
|
||||
|
||||
func (o *Client) doWithRetries(req *http.Request) (*http.Response, error) {
|
||||
b := backoff.ExponentialBackOff{
|
||||
InitialInterval: time.Millisecond * 1000,
|
||||
RandomizationFactor: 0.1,
|
||||
Multiplier: 1.5,
|
||||
MaxInterval: time.Second * 32,
|
||||
MaxElapsedTime: time.Second * 128,
|
||||
Clock: backoff.SystemClock,
|
||||
}
|
||||
b.Reset()
|
||||
|
||||
op := func() (*http.Response, error) {
|
||||
resp, err := o.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, backoff.Permanent(err)
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
err = fmt.Errorf("unsuccessful request: %d %s", resp.StatusCode, http.StatusText(resp.StatusCode))
|
||||
if resp.StatusCode == http.StatusTooManyRequests {
|
||||
return nil, err
|
||||
}
|
||||
return nil, backoff.Permanent(err)
|
||||
}
|
||||
|
||||
return backoff.RetryWithData(op, &b)
|
||||
}
|
||||
|
||||
func (o *Client) FetchCollectibleOwnersByContractAddress(ctx context.Context, chainID walletCommon.ChainID, contractAddress common.Address) (*thirdparty.CollectibleContractOwnership, error) {
|
||||
ownership := thirdparty.CollectibleContractOwnership{
|
||||
ContractAddress: contractAddress,
|
||||
Owners: make([]thirdparty.CollectibleOwner, 0),
|
||||
}
|
||||
|
||||
queryParams := url.Values{
|
||||
"contractAddress": {contractAddress.String()},
|
||||
"withTokenBalances": {"true"},
|
||||
}
|
||||
|
||||
baseURL, err := getNFTBaseURL(chainID, o.apiKeys[uint64(chainID)])
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for {
|
||||
url := fmt.Sprintf("%s/getOwnersForContract?%s", baseURL, queryParams.Encode())
|
||||
|
||||
resp, err := o.doQuery(ctx, url)
|
||||
if err != nil {
|
||||
if ctx.Err() == nil {
|
||||
o.connectionStatus.SetIsConnected(false)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
o.connectionStatus.SetIsConnected(true)
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var alchemyOwnership CollectibleContractOwnership
|
||||
err = json.Unmarshal(body, &alchemyOwnership)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ownership.Owners = append(ownership.Owners, alchemyCollectibleOwnersToCommon(alchemyOwnership.Owners)...)
|
||||
|
||||
if alchemyOwnership.PageKey == "" {
|
||||
break
|
||||
}
|
||||
|
||||
queryParams["pageKey"] = []string{alchemyOwnership.PageKey}
|
||||
}
|
||||
|
||||
return &ownership, nil
|
||||
}
|
||||
|
||||
func (o *Client) FetchAllAssetsByOwner(ctx context.Context, chainID walletCommon.ChainID, owner common.Address, cursor string, limit int) (*thirdparty.FullCollectibleDataContainer, error) {
|
||||
queryParams := url.Values{}
|
||||
|
||||
return o.fetchOwnedAssets(ctx, chainID, owner, queryParams, cursor, limit)
|
||||
}
|
||||
|
||||
func (o *Client) FetchAllAssetsByOwnerAndContractAddress(ctx context.Context, chainID walletCommon.ChainID, owner common.Address, contractAddresses []common.Address, cursor string, limit int) (*thirdparty.FullCollectibleDataContainer, error) {
|
||||
queryParams := url.Values{}
|
||||
|
||||
for _, contractAddress := range contractAddresses {
|
||||
queryParams.Add("contractAddresses", contractAddress.String())
|
||||
}
|
||||
|
||||
return o.fetchOwnedAssets(ctx, chainID, owner, queryParams, cursor, limit)
|
||||
}
|
||||
|
||||
func (o *Client) fetchOwnedAssets(ctx context.Context, chainID walletCommon.ChainID, owner common.Address, queryParams url.Values, cursor string, limit int) (*thirdparty.FullCollectibleDataContainer, error) {
|
||||
assets := new(thirdparty.FullCollectibleDataContainer)
|
||||
|
||||
queryParams["owner"] = []string{owner.String()}
|
||||
queryParams["withMetadata"] = []string{"true"}
|
||||
|
||||
if len(cursor) > 0 {
|
||||
queryParams["pageKey"] = []string{cursor}
|
||||
assets.PreviousCursor = cursor
|
||||
}
|
||||
assets.Provider = o.ID()
|
||||
|
||||
baseURL, err := getNFTBaseURL(chainID, o.apiKeys[uint64(chainID)])
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for {
|
||||
url := fmt.Sprintf("%s/getNFTsForOwner?%s", baseURL, queryParams.Encode())
|
||||
|
||||
resp, err := o.doQuery(ctx, url)
|
||||
if err != nil {
|
||||
if ctx.Err() == nil {
|
||||
o.connectionStatus.SetIsConnected(false)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
o.connectionStatus.SetIsConnected(true)
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if Json is not returned there must be an error
|
||||
if !json.Valid(body) {
|
||||
return nil, fmt.Errorf("invalid json: %s", string(body))
|
||||
}
|
||||
|
||||
container := OwnedNFTList{}
|
||||
err = json.Unmarshal(body, &container)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
assets.Items = append(assets.Items, alchemyToCollectiblesData(chainID, container.OwnedNFTs)...)
|
||||
assets.NextCursor = container.PageKey
|
||||
|
||||
if len(assets.NextCursor) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
queryParams["cursor"] = []string{assets.NextCursor}
|
||||
|
||||
if limit != thirdparty.FetchNoLimit && len(assets.Items) >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return assets, nil
|
||||
}
|
||||
|
||||
func getCollectibleUniqueIDBatches(ids []thirdparty.CollectibleUniqueID) []BatchTokenIDs {
|
||||
batches := make([]BatchTokenIDs, 0)
|
||||
|
||||
for startIdx := 0; startIdx < len(ids); startIdx += nftMetadataBatchLimit {
|
||||
endIdx := startIdx + nftMetadataBatchLimit
|
||||
if endIdx > len(ids) {
|
||||
endIdx = len(ids)
|
||||
}
|
||||
|
||||
pageIDs := ids[startIdx:endIdx]
|
||||
|
||||
batchIDs := BatchTokenIDs{
|
||||
IDs: make([]TokenID, 0, len(pageIDs)),
|
||||
}
|
||||
for _, id := range pageIDs {
|
||||
batchID := TokenID{
|
||||
ContractAddress: id.ContractID.Address,
|
||||
TokenID: id.TokenID,
|
||||
}
|
||||
batchIDs.IDs = append(batchIDs.IDs, batchID)
|
||||
}
|
||||
|
||||
batches = append(batches, batchIDs)
|
||||
}
|
||||
|
||||
return batches
|
||||
}
|
||||
|
||||
func (o *Client) fetchAssetsByBatchTokenIDs(ctx context.Context, chainID walletCommon.ChainID, batchIDs BatchTokenIDs) ([]thirdparty.FullCollectibleData, error) {
|
||||
baseURL, err := getNFTBaseURL(chainID, o.apiKeys[uint64(chainID)])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/getNFTMetadataBatch", baseURL)
|
||||
|
||||
resp, err := o.doPostWithJSON(ctx, url, batchIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if Json is not returned there must be an error
|
||||
if !json.Valid(body) {
|
||||
return nil, fmt.Errorf("invalid json: %s", string(body))
|
||||
}
|
||||
|
||||
assets := NFTList{}
|
||||
err = json.Unmarshal(body, &assets)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret := alchemyToCollectiblesData(chainID, assets.NFTs)
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (o *Client) FetchAssetsByCollectibleUniqueID(ctx context.Context, uniqueIDs []thirdparty.CollectibleUniqueID) ([]thirdparty.FullCollectibleData, error) {
|
||||
ret := make([]thirdparty.FullCollectibleData, 0, len(uniqueIDs))
|
||||
|
||||
idsPerChainID := thirdparty.GroupCollectibleUIDsByChainID(uniqueIDs)
|
||||
|
||||
for chainID, ids := range idsPerChainID {
|
||||
batches := getCollectibleUniqueIDBatches(ids)
|
||||
for _, batch := range batches {
|
||||
assets, err := o.fetchAssetsByBatchTokenIDs(ctx, chainID, batch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret = append(ret, assets...)
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func getContractAddressBatches(ids []thirdparty.ContractID) []BatchContractAddresses {
|
||||
batches := make([]BatchContractAddresses, 0)
|
||||
|
||||
for startIdx := 0; startIdx < len(ids); startIdx += contractMetadataBatchLimit {
|
||||
endIdx := startIdx + contractMetadataBatchLimit
|
||||
if endIdx > len(ids) {
|
||||
endIdx = len(ids)
|
||||
}
|
||||
|
||||
pageIDs := ids[startIdx:endIdx]
|
||||
|
||||
batchIDs := BatchContractAddresses{
|
||||
Addresses: make([]common.Address, 0, len(pageIDs)),
|
||||
}
|
||||
for _, id := range pageIDs {
|
||||
batchIDs.Addresses = append(batchIDs.Addresses, id.Address)
|
||||
}
|
||||
|
||||
batches = append(batches, batchIDs)
|
||||
}
|
||||
|
||||
return batches
|
||||
}
|
||||
|
||||
func (o *Client) fetchCollectionsDataByBatchContractAddresses(ctx context.Context, chainID walletCommon.ChainID, batchAddresses BatchContractAddresses) ([]thirdparty.CollectionData, error) {
|
||||
baseURL, err := getNFTBaseURL(chainID, o.apiKeys[uint64(chainID)])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/getContractMetadataBatch", baseURL)
|
||||
|
||||
resp, err := o.doPostWithJSON(ctx, url, batchAddresses)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if Json is not returned there must be an error
|
||||
if !json.Valid(body) {
|
||||
return nil, fmt.Errorf("invalid json: %s", string(body))
|
||||
}
|
||||
|
||||
collections := ContractList{}
|
||||
err = json.Unmarshal(body, &collections)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret := alchemyToCollectionsData(chainID, collections.Contracts)
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (o *Client) FetchCollectionsDataByContractID(ctx context.Context, contractIDs []thirdparty.ContractID) ([]thirdparty.CollectionData, error) {
|
||||
ret := make([]thirdparty.CollectionData, 0, len(contractIDs))
|
||||
|
||||
idsPerChainID := thirdparty.GroupContractIDsByChainID(contractIDs)
|
||||
|
||||
for chainID, ids := range idsPerChainID {
|
||||
batches := getContractAddressBatches(ids)
|
||||
for _, batch := range batches {
|
||||
contractsData, err := o.fetchCollectionsDataByBatchContractAddresses(ctx, chainID, batch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret = append(ret, contractsData...)
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
Generated
Vendored
+197
@@ -0,0 +1,197 @@
|
||||
// nolint: misspell
|
||||
package alchemy
|
||||
|
||||
const collectionJSON = `{
|
||||
"address": "0x06012c8cf97BEaD5deAe237070F9587f8E7A266d",
|
||||
"name": "CryptoKitties",
|
||||
"symbol": "CK",
|
||||
"totalSupply": "2023564",
|
||||
"tokenType": "ERC721",
|
||||
"contractDeployer": "0xba52c75764d6F594735dc735Be7F1830CDf58dDf",
|
||||
"deployedBlockNumber": 4605167,
|
||||
"openSeaMetadata": {
|
||||
"floorPrice": 0.003,
|
||||
"collectionName": "CryptoKitties",
|
||||
"collectionSlug": "cryptokitties",
|
||||
"safelistRequestStatus": "verified",
|
||||
"imageUrl": "https://i.seadn.io/gae/C272ZRW1RGGef9vKMePFSCeKc1Lw6U40wl9ofNVxzUxFdj84hH9xJRQNf-7wgs7W8qw8RWe-1ybKp-VKuU5D-tg?w=500&auto=format",
|
||||
"description": "CryptoKitties is a game centered around breedable, collectible, and oh-so-adorable creatures we call CryptoKitties! Each cat is one-of-a-kind and 100% owned by you; it cannot be replicated, taken away, or destroyed.",
|
||||
"externalUrl": null,
|
||||
"twitterUsername": "CryptoKitties",
|
||||
"discordUrl": "https://discord.gg/cryptokitties",
|
||||
"bannerImageUrl": "https://i.seadn.io/gcs/static/banners/cryptokitties-banner2.png?w=500&auto=format",
|
||||
"lastIngestedAt": "2024-02-05T23:13:45.000Z"
|
||||
}
|
||||
}`
|
||||
|
||||
const ownedCollectiblesJSON = `{
|
||||
"ownedNfts": [
|
||||
{
|
||||
"contract": {
|
||||
"address": "0x2b1870752208935fDA32AB6A016C01a27877CF12",
|
||||
"name": null,
|
||||
"symbol": null,
|
||||
"totalSupply": null,
|
||||
"tokenType": "ERC1155",
|
||||
"contractDeployer": "0x5Df2E003CEcb0ebf69cBd8F7Fbb6F44F690331F2",
|
||||
"deployedBlockNumber": 8088320,
|
||||
"openSeaMetadata": {
|
||||
"floorPrice": 0,
|
||||
"collectionName": "Unidentified contract - SkdH8ZQtyB",
|
||||
"collectionSlug": "unidentified-contract-skdh8zqtyb",
|
||||
"safelistRequestStatus": "not_requested",
|
||||
"imageUrl": null,
|
||||
"description": null,
|
||||
"externalUrl": null,
|
||||
"twitterUsername": null,
|
||||
"discordUrl": null,
|
||||
"bannerImageUrl": null,
|
||||
"lastIngestedAt": "2024-01-28T06:17:57.000Z"
|
||||
},
|
||||
"isSpam": null,
|
||||
"spamClassifications": []
|
||||
},
|
||||
"tokenId": "50659039041325838222074459099120411190538227963344971355684955900852972814336",
|
||||
"tokenType": "ERC1155",
|
||||
"name": "HODL",
|
||||
"description": "The enemy king sent a single message, written on a parchment stained by blood.\n“You are advised to submit without further delay, for if I bring my army into your land, I will destroy your hodlings, slay your people, and burn your city to ashes.”\nHodlers of ENJ sent a single word as reply:\n“If.”\nThe battle that followed does not come around too often, a battle that began every legend told about the warriors that gained eternal glory. \nThe battle that followed seemed like a lost one from the very beginning. \nThe enemy army was revealed at dawn, illuminated by the rising Sun.The ground shook as countless hordes marched towards a small band of men armed with shields, spears and swords.\nThe hodlers were outnumbered, one thousand to one. \nFear, doubt and uncertainty did not reach their hearts and minds - for they were born for this. \nEach hodler was bred for warfare, instructed in bloodshed, groomed to become a poet of death. \nA philosopher of war, blood and glory. \nEach man was forged into an invincible soldier that had a single driving force during each battle.\nStand your ground - at all costs. \nAs the swarm of enemies approached, the king yelled, asking his men: \n“Hodlers! What is your profession?”\n“HODL! HODL! HODL! HODL!!! HODL!!!!!” they replied, hitting spears against their shields. \nAn endless stream of arrows fell from the heavens only moments later, blocking out the Sun so they could fight in the shade. They emerged from the darkness without even a single scratch, protected by their legendary Enjin shields. \nWave after wave, their enemies rushed towards their doom, as they were met with cold tips of thrusting spears and sharp edges of crimson swords.\nAgainst all odds, the wall of men and steel held against the never-ending, shilling swarm. \nWhat was left of the enemy army retreated, fleeing in absolute panic and indisputable terror.\nBathed in blood, the ENJ hodlers were victorious.\nTheir story will be told for thousands of years, immortalized with divine blocks and chains.\n* * *\n“HODL” was minted in 2018 for our amazing community of epic Enjin HODLers. We are extremely grateful for the trust you've put in us and the products we're making - and the mission we're trying to accomplish, and hope you’ll love this token of our appreciation. ",
|
||||
"tokenUri": "https://cdn.enjin.io/mint/meta/70000000000001b2.json",
|
||||
"image": {
|
||||
"cachedUrl": "https://nft-cdn.alchemy.com/eth-mainnet/c5c93ffa8146ade7d3694c0f28463f0c",
|
||||
"thumbnailUrl": "https://res.cloudinary.com/alchemyapi/image/upload/thumbnailv2/eth-mainnet/c5c93ffa8146ade7d3694c0f28463f0c",
|
||||
"pngUrl": "https://res.cloudinary.com/alchemyapi/image/upload/convert-png/eth-mainnet/c5c93ffa8146ade7d3694c0f28463f0c",
|
||||
"contentType": "image/jpeg",
|
||||
"size": 256926,
|
||||
"originalUrl": "https://cdn.enjin.io/mint/image/70000000000001b2.jpg"
|
||||
},
|
||||
"raw": {
|
||||
"tokenUri": "https://cdn.enjin.io/mint/meta/70000000000001b2.json",
|
||||
"metadata": {
|
||||
"name": "HODL",
|
||||
"description": "The enemy king sent a single message, written on a parchment stained by blood.\n“You are advised to submit without further delay, for if I bring my army into your land, I will destroy your hodlings, slay your people, and burn your city to ashes.”\nHodlers of ENJ sent a single word as reply:\n“If.”\nThe battle that followed does not come around too often, a battle that began every legend told about the warriors that gained eternal glory. \nThe battle that followed seemed like a lost one from the very beginning. \nThe enemy army was revealed at dawn, illuminated by the rising Sun.The ground shook as countless hordes marched towards a small band of men armed with shields, spears and swords.\nThe hodlers were outnumbered, one thousand to one. \nFear, doubt and uncertainty did not reach their hearts and minds - for they were born for this. \nEach hodler was bred for warfare, instructed in bloodshed, groomed to become a poet of death. \nA philosopher of war, blood and glory. \nEach man was forged into an invincible soldier that had a single driving force during each battle.\nStand your ground - at all costs. \nAs the swarm of enemies approached, the king yelled, asking his men: \n“Hodlers! What is your profession?”\n“HODL! HODL! HODL! HODL!!! HODL!!!!!” they replied, hitting spears against their shields. \nAn endless stream of arrows fell from the heavens only moments later, blocking out the Sun so they could fight in the shade. They emerged from the darkness without even a single scratch, protected by their legendary Enjin shields. \nWave after wave, their enemies rushed towards their doom, as they were met with cold tips of thrusting spears and sharp edges of crimson swords.\nAgainst all odds, the wall of men and steel held against the never-ending, shilling swarm. \nWhat was left of the enemy army retreated, fleeing in absolute panic and indisputable terror.\nBathed in blood, the ENJ hodlers were victorious.\nTheir story will be told for thousands of years, immortalized with divine blocks and chains.\n* * *\n“HODL” was minted in 2018 for our amazing community of epic Enjin HODLers. We are extremely grateful for the trust you've put in us and the products we're making - and the mission we're trying to accomplish, and hope you’ll love this token of our appreciation. ",
|
||||
"image": "https://cdn.enjin.io/mint/image/70000000000001b2.jpg"
|
||||
},
|
||||
"error": null
|
||||
},
|
||||
"collection": {
|
||||
"name": "Unidentified contract - SkdH8ZQtyB",
|
||||
"slug": "unidentified-contract-skdh8zqtyb",
|
||||
"externalUrl": null,
|
||||
"bannerImageUrl": null
|
||||
},
|
||||
"mint": {
|
||||
"mintAddress": null,
|
||||
"blockNumber": null,
|
||||
"timestamp": null,
|
||||
"transactionHash": null
|
||||
},
|
||||
"owners": null,
|
||||
"timeLastUpdated": "2024-01-03T19:11:04.681Z",
|
||||
"balance": "1",
|
||||
"acquiredAt": {
|
||||
"blockTimestamp": null,
|
||||
"blockNumber": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"contract": {
|
||||
"address": "0x3f6B1585AfeFc56433C8d28AA89dbc77af59278f",
|
||||
"name": "Simpson Punk",
|
||||
"symbol": "SIMPUNK",
|
||||
"totalSupply": "1789",
|
||||
"tokenType": "ERC721",
|
||||
"contractDeployer": "0xa74E02F671e00eeFbf4e13D9D89B397523653E67",
|
||||
"deployedBlockNumber": 18607457,
|
||||
"openSeaMetadata": {
|
||||
"floorPrice": 0.00069,
|
||||
"collectionName": "SimpsonPunks",
|
||||
"collectionSlug": "simpsonpunkseth",
|
||||
"safelistRequestStatus": "not_requested",
|
||||
"imageUrl": "https://raw.seadn.io/files/e7765f13c4658f514d0efc008ae7f300.png",
|
||||
"description": "1,789 SimpsonPunks entered the Blockchain????",
|
||||
"externalUrl": null,
|
||||
"twitterUsername": "SimpsonPunksETH",
|
||||
"discordUrl": null,
|
||||
"bannerImageUrl": null,
|
||||
"lastIngestedAt": "2024-01-31T00:06:00.000Z"
|
||||
},
|
||||
"isSpam": null,
|
||||
"spamClassifications": []
|
||||
},
|
||||
"tokenId": "900",
|
||||
"tokenType": "ERC721",
|
||||
"name": "#900",
|
||||
"description": "5,555 SimpsonPunks entered the Ethereum Blockchain🍩",
|
||||
"tokenUri": "https://alchemy.mypinata.cloud/ipfs/bafybeidqbmbglapk2bkffa4o2ws5jhxnhlbdeqh7k6tk62pukse3xhvv2e/900.json",
|
||||
"image": {
|
||||
"cachedUrl": "https://nft-cdn.alchemy.com/eth-mainnet/52accf48dc609088738b15808fe07e8c",
|
||||
"thumbnailUrl": "https://res.cloudinary.com/alchemyapi/image/upload/thumbnailv2/eth-mainnet/52accf48dc609088738b15808fe07e8c",
|
||||
"pngUrl": "https://res.cloudinary.com/alchemyapi/image/upload/convert-png/eth-mainnet/52accf48dc609088738b15808fe07e8c",
|
||||
"contentType": "image/png",
|
||||
"size": 21206,
|
||||
"originalUrl": "https://ipfs.io/ipfs/bafybeib2metombffkovfzbjhljpcmobb5wqazsbpenz2drysgm7und47ym/900.png"
|
||||
},
|
||||
"raw": {
|
||||
"tokenUri": "ipfs://bafybeidqbmbglapk2bkffa4o2ws5jhxnhlbdeqh7k6tk62pukse3xhvv2e/900.json",
|
||||
"metadata": {
|
||||
"date": 1700514890266,
|
||||
"image": "ipfs://bafybeib2metombffkovfzbjhljpcmobb5wqazsbpenz2drysgm7und47ym/900.png",
|
||||
"name": "#900",
|
||||
"description": "5,555 SimpsonPunks entered the Ethereum Blockchain🍩",
|
||||
"edition": 900,
|
||||
"attributes": [
|
||||
{
|
||||
"value": "Background",
|
||||
"trait_type": "layers"
|
||||
},
|
||||
{
|
||||
"value": "Monkey",
|
||||
"trait_type": "Face"
|
||||
},
|
||||
{
|
||||
"value": "Sweatband Blue",
|
||||
"trait_type": "Head"
|
||||
},
|
||||
{
|
||||
"value": "Thin Full",
|
||||
"trait_type": "Facial Hair"
|
||||
},
|
||||
{
|
||||
"value": "Burger",
|
||||
"trait_type": "Mouth"
|
||||
}
|
||||
],
|
||||
"imageHash": "c1055d7f183405a5ec934bf6d83ac653740795cbf40031e3b34e957a352b471c",
|
||||
"compiler": "HashLips Art Engine - NFTChef fork"
|
||||
},
|
||||
"error": null
|
||||
},
|
||||
"collection": {
|
||||
"name": "SimpsonPunks",
|
||||
"slug": "simpsonpunkseth",
|
||||
"externalUrl": null,
|
||||
"bannerImageUrl": null
|
||||
},
|
||||
"mint": {
|
||||
"mintAddress": null,
|
||||
"blockNumber": null,
|
||||
"timestamp": null,
|
||||
"transactionHash": null
|
||||
},
|
||||
"owners": null,
|
||||
"timeLastUpdated": "2024-01-03T21:02:33.333Z",
|
||||
"balance": "1",
|
||||
"acquiredAt": {
|
||||
"blockTimestamp": null,
|
||||
"blockNumber": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"totalCount": 53,
|
||||
"validAt": {
|
||||
"blockNumber": 19169748,
|
||||
"blockHash": "0x95badbc2af9d0fa4ecfb614e6f24910e027b3e0a5c6fa4c32521ff5bc7754693",
|
||||
"blockTimestamp": "2024-02-06T14:20:11Z"
|
||||
},
|
||||
"pageKey": "MHgyYjE4NzA3NTIyMDg5MzVmZGEzMmFiNmEwMTZjMDFhMjc4NzdjZjEyOjB4NzgwMDAwMDAwMDAwMDIyZTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDpmYWxzZQ=="
|
||||
}`
|
||||
Generated
Vendored
+249
@@ -0,0 +1,249 @@
|
||||
package alchemy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"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 AlchemyID = "alchemy"
|
||||
|
||||
type TokenBalance struct {
|
||||
TokenID *bigint.BigInt `json:"tokenId"`
|
||||
Balance *bigint.BigInt `json:"balance"`
|
||||
}
|
||||
|
||||
type CollectibleOwner struct {
|
||||
OwnerAddress common.Address `json:"ownerAddress"`
|
||||
TokenBalances []TokenBalance `json:"tokenBalances"`
|
||||
}
|
||||
|
||||
type CollectibleContractOwnership struct {
|
||||
Owners []CollectibleOwner `json:"owners"`
|
||||
PageKey string `json:"pageKey"`
|
||||
}
|
||||
|
||||
func alchemyCollectibleOwnersToCommon(alchemyOwners []CollectibleOwner) []thirdparty.CollectibleOwner {
|
||||
owners := make([]thirdparty.CollectibleOwner, 0, len(alchemyOwners))
|
||||
for _, alchemyOwner := range alchemyOwners {
|
||||
balances := make([]thirdparty.TokenBalance, 0, len(alchemyOwner.TokenBalances))
|
||||
|
||||
for _, alchemyBalance := range alchemyOwner.TokenBalances {
|
||||
balances = append(balances, thirdparty.TokenBalance{
|
||||
TokenID: &bigint.BigInt{Int: alchemyBalance.TokenID.Int},
|
||||
Balance: alchemyBalance.Balance,
|
||||
})
|
||||
}
|
||||
owner := thirdparty.CollectibleOwner{
|
||||
OwnerAddress: alchemyOwner.OwnerAddress,
|
||||
TokenBalances: balances,
|
||||
}
|
||||
|
||||
owners = append(owners, owner)
|
||||
}
|
||||
return owners
|
||||
}
|
||||
|
||||
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 Attribute struct {
|
||||
TraitType string `json:"trait_type"`
|
||||
Value AttributeValue `json:"value"`
|
||||
}
|
||||
|
||||
type RawMetadata struct {
|
||||
Attributes []Attribute `json:"attributes"`
|
||||
}
|
||||
|
||||
type RawFull struct {
|
||||
RawMetadata RawMetadata `json:"metadata"`
|
||||
}
|
||||
|
||||
type Raw struct {
|
||||
RawMetadata interface{} `json:"metadata"`
|
||||
}
|
||||
|
||||
func (r *Raw) UnmarshalJSON(b []byte) error {
|
||||
raw := RawFull{
|
||||
RawMetadata{
|
||||
Attributes: make([]Attribute, 0),
|
||||
},
|
||||
}
|
||||
|
||||
// Field structure is not known in advance
|
||||
_ = json.Unmarshal(b, &raw)
|
||||
|
||||
r.RawMetadata = raw.RawMetadata
|
||||
return nil
|
||||
}
|
||||
|
||||
type OpenSeaMetadata struct {
|
||||
ImageURL string `json:"imageUrl"`
|
||||
}
|
||||
|
||||
type Contract struct {
|
||||
Address common.Address `json:"address"`
|
||||
Name string `json:"name"`
|
||||
Symbol string `json:"symbol"`
|
||||
TokenType string `json:"tokenType"`
|
||||
OpenSeaMetadata OpenSeaMetadata `json:"openseaMetadata"`
|
||||
}
|
||||
|
||||
type ContractList struct {
|
||||
Contracts []Contract `json:"contracts"`
|
||||
}
|
||||
|
||||
type Image struct {
|
||||
ImageURL string `json:"pngUrl"`
|
||||
CachedAnimationURL string `json:"cachedUrl"`
|
||||
OriginalAnimationURL string `json:"originalUrl"`
|
||||
}
|
||||
|
||||
type Asset struct {
|
||||
Contract Contract `json:"contract"`
|
||||
TokenID *bigint.BigInt `json:"tokenId"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Image Image `json:"image"`
|
||||
Raw Raw `json:"raw"`
|
||||
TokenURI string `json:"tokenUri"`
|
||||
}
|
||||
|
||||
type OwnedNFTList struct {
|
||||
OwnedNFTs []Asset `json:"ownedNfts"`
|
||||
TotalCount *bigint.BigInt `json:"totalCount"`
|
||||
PageKey string `json:"pageKey"`
|
||||
}
|
||||
|
||||
type NFTList struct {
|
||||
NFTs []Asset `json:"nfts"`
|
||||
}
|
||||
|
||||
type BatchContractAddresses struct {
|
||||
Addresses []common.Address `json:"contractAddresses"`
|
||||
}
|
||||
|
||||
type BatchTokenIDs struct {
|
||||
IDs []TokenID `json:"tokens"`
|
||||
}
|
||||
|
||||
type TokenID struct {
|
||||
ContractAddress common.Address `json:"contractAddress"`
|
||||
TokenID *bigint.BigInt `json:"tokenId"`
|
||||
}
|
||||
|
||||
func alchemyToCollectibleTraits(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: strings.Replace(orig.TraitType, "_", " ", 1),
|
||||
Value: caser.String(string(orig.Value)),
|
||||
}
|
||||
|
||||
ret = append(ret, dest)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func alchemyToContractType(tokenType string) walletCommon.ContractType {
|
||||
switch tokenType {
|
||||
case "ERC721":
|
||||
return walletCommon.ContractTypeERC721
|
||||
case "ERC1155":
|
||||
return walletCommon.ContractTypeERC1155
|
||||
default:
|
||||
return walletCommon.ContractTypeUnknown
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Contract) toCollectionData(id thirdparty.ContractID) thirdparty.CollectionData {
|
||||
ret := thirdparty.CollectionData{
|
||||
ID: id,
|
||||
ContractType: alchemyToContractType(c.TokenType),
|
||||
Provider: AlchemyID,
|
||||
Name: c.Name,
|
||||
ImageURL: c.OpenSeaMetadata.ImageURL,
|
||||
Traits: make(map[string]thirdparty.CollectionTrait, 0),
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (c *Asset) toCollectiblesData(id thirdparty.CollectibleUniqueID) thirdparty.CollectibleData {
|
||||
rawMetadata := c.Raw.RawMetadata.(RawMetadata)
|
||||
|
||||
return thirdparty.CollectibleData{
|
||||
ID: id,
|
||||
ContractType: alchemyToContractType(c.Contract.TokenType),
|
||||
Provider: AlchemyID,
|
||||
Name: c.Name,
|
||||
Description: c.Description,
|
||||
ImageURL: c.Image.ImageURL,
|
||||
AnimationURL: c.Image.CachedAnimationURL,
|
||||
Traits: alchemyToCollectibleTraits(rawMetadata.Attributes),
|
||||
TokenURI: c.TokenURI,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Asset) toCommon(id thirdparty.CollectibleUniqueID) thirdparty.FullCollectibleData {
|
||||
contractData := c.Contract.toCollectionData(id.ContractID)
|
||||
return thirdparty.FullCollectibleData{
|
||||
CollectibleData: c.toCollectiblesData(id),
|
||||
CollectionData: &contractData,
|
||||
}
|
||||
}
|
||||
|
||||
func alchemyToCollectiblesData(chainID walletCommon.ChainID, l []Asset) []thirdparty.FullCollectibleData {
|
||||
ret := make([]thirdparty.FullCollectibleData, 0, len(l))
|
||||
for _, asset := range l {
|
||||
id := thirdparty.CollectibleUniqueID{
|
||||
ContractID: thirdparty.ContractID{
|
||||
ChainID: chainID,
|
||||
Address: asset.Contract.Address,
|
||||
},
|
||||
TokenID: asset.TokenID,
|
||||
}
|
||||
item := asset.toCommon(id)
|
||||
ret = append(ret, item)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func alchemyToCollectionsData(chainID walletCommon.ChainID, l []Contract) []thirdparty.CollectionData {
|
||||
ret := make([]thirdparty.CollectionData, 0, len(l))
|
||||
for _, contract := range l {
|
||||
id := thirdparty.ContractID{
|
||||
ChainID: chainID,
|
||||
Address: contract.Address,
|
||||
}
|
||||
item := contract.toCollectionData(id)
|
||||
ret = append(ret, item)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
Generated
Vendored
+308
@@ -0,0 +1,308 @@
|
||||
package coingecko
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty"
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty/utils"
|
||||
)
|
||||
|
||||
var coinGeckoMapping = map[string]string{
|
||||
"STT": "status",
|
||||
"SNT": "status",
|
||||
"ETH": "ethereum",
|
||||
"AST": "airswap",
|
||||
"AMB": "",
|
||||
"ABT": "arcblock",
|
||||
"ATM": "",
|
||||
"BNB": "binancecoin",
|
||||
"BLT": "bloom",
|
||||
"CDT": "",
|
||||
"COMP": "compound-coin",
|
||||
"EDG": "edgeless",
|
||||
"ELF": "",
|
||||
"ENG": "enigma",
|
||||
"EOS": "eos",
|
||||
"GEN": "daostack",
|
||||
"MANA": "decentraland-wormhole",
|
||||
"LEND": "ethlend",
|
||||
"LRC": "loopring",
|
||||
"MET": "metronome",
|
||||
"POLY": "polymath",
|
||||
"PPT": "populous",
|
||||
"SAN": "santiment-network-token",
|
||||
"DNT": "district0x",
|
||||
"SPN": "sapien",
|
||||
"USDS": "stableusd",
|
||||
"STX": "stox",
|
||||
"SUB": "substratum",
|
||||
"PAY": "tenx",
|
||||
"GRT": "the-graph",
|
||||
"TNT": "tierion",
|
||||
"TRX": "tron",
|
||||
"TGT": "",
|
||||
"RARE": "superrare",
|
||||
"UNI": "uniswap",
|
||||
"USDC": "usd-coin",
|
||||
"USDP": "paxos-standard",
|
||||
"VRS": "",
|
||||
"TIME": "",
|
||||
}
|
||||
|
||||
const baseURL = "https://api.coingecko.com/api/v3/"
|
||||
|
||||
type HistoricalPriceContainer struct {
|
||||
Prices [][]float64 `json:"prices"`
|
||||
}
|
||||
type GeckoMarketValues struct {
|
||||
ID string `json:"id"`
|
||||
Symbol string `json:"symbol"`
|
||||
Name string `json:"name"`
|
||||
MarketCap float64 `json:"market_cap"`
|
||||
High24h float64 `json:"high_24h"`
|
||||
Low24h float64 `json:"low_24h"`
|
||||
PriceChange24h float64 `json:"price_change_24h"`
|
||||
PriceChangePercentage24h float64 `json:"price_change_percentage_24h"`
|
||||
PriceChangePercentage1hInCurrency float64 `json:"price_change_percentage_1h_in_currency"`
|
||||
}
|
||||
|
||||
type GeckoToken struct {
|
||||
ID string `json:"id"`
|
||||
Symbol string `json:"symbol"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
client *http.Client
|
||||
tokens map[string]GeckoToken
|
||||
tokensURL string
|
||||
fetchTokensMutex sync.Mutex
|
||||
}
|
||||
|
||||
func NewClient() *Client {
|
||||
return &Client{client: &http.Client{Timeout: time.Minute}, tokens: make(map[string]GeckoToken), tokensURL: fmt.Sprintf("%scoins/list", baseURL)}
|
||||
}
|
||||
|
||||
func (c *Client) DoQuery(url string) (*http.Response, error) {
|
||||
resp, err := c.client.Get(url)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func mapTokensToSymbols(tokens []GeckoToken, tokenMap map[string]GeckoToken) {
|
||||
for _, token := range tokens {
|
||||
if id, ok := coinGeckoMapping[strings.ToUpper(token.Symbol)]; ok {
|
||||
if id != token.ID {
|
||||
continue
|
||||
}
|
||||
}
|
||||
tokenMap[strings.ToUpper(token.Symbol)] = token
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) getTokens() (map[string]GeckoToken, error) {
|
||||
c.fetchTokensMutex.Lock()
|
||||
defer c.fetchTokensMutex.Unlock()
|
||||
|
||||
if len(c.tokens) > 0 {
|
||||
return c.tokens, nil
|
||||
}
|
||||
|
||||
resp, err := c.DoQuery(c.tokensURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tokens []GeckoToken
|
||||
err = json.Unmarshal(body, &tokens)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mapTokensToSymbols(tokens, c.tokens)
|
||||
|
||||
return c.tokens, nil
|
||||
}
|
||||
|
||||
func (c *Client) mapSymbolsToIds(symbols []string) ([]string, error) {
|
||||
tokens, err := c.getTokens()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids := make([]string, 0)
|
||||
for _, symbol := range utils.RenameSymbols(symbols) {
|
||||
if token, ok := tokens[symbol]; ok {
|
||||
ids = append(ids, token.ID)
|
||||
}
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func (c *Client) getIDFromSymbol(symbol string) (string, error) {
|
||||
tokens, err := c.getTokens()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tokens[strings.ToUpper(symbol)].ID, nil
|
||||
}
|
||||
|
||||
func (c *Client) FetchPrices(symbols []string, currencies []string) (map[string]map[string]float64, error) {
|
||||
ids, err := c.mapSymbolsToIds(symbols)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
url := fmt.Sprintf("%ssimple/price?ids=%s&vs_currencies=%s", baseURL, strings.Join(ids, ","), strings.Join(currencies, ","))
|
||||
resp, err := c.DoQuery(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prices := make(map[string]map[string]float64)
|
||||
err = json.Unmarshal(body, &prices)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[string]map[string]float64)
|
||||
for _, symbol := range symbols {
|
||||
result[symbol] = map[string]float64{}
|
||||
id, err := c.getIDFromSymbol(utils.GetRealSymbol(symbol))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, currency := range currencies {
|
||||
result[symbol][currency] = prices[id][strings.ToLower(currency)]
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *Client) FetchTokenDetails(symbols []string) (map[string]thirdparty.TokenDetails, error) {
|
||||
tokens, err := c.getTokens()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make(map[string]thirdparty.TokenDetails)
|
||||
for _, symbol := range symbols {
|
||||
if value, ok := tokens[utils.GetRealSymbol(symbol)]; ok {
|
||||
result[symbol] = thirdparty.TokenDetails{
|
||||
ID: value.ID,
|
||||
Name: value.Name,
|
||||
Symbol: symbol,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *Client) FetchTokenMarketValues(symbols []string, currency string) (map[string]thirdparty.TokenMarketValues, error) {
|
||||
ids, err := c.mapSymbolsToIds(symbols)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
url := fmt.Sprintf("%scoins/markets?ids=%s&vs_currency=%s&order=market_cap_desc&per_page=250&page=1&sparkline=false&price_change_percentage=%s", baseURL, strings.Join(ids, ","), currency, "1h%2C24h")
|
||||
|
||||
resp, err := c.DoQuery(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var marketValues []GeckoMarketValues
|
||||
err = json.Unmarshal(body, &marketValues)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[string]thirdparty.TokenMarketValues)
|
||||
for _, symbol := range symbols {
|
||||
id, err := c.getIDFromSymbol(utils.GetRealSymbol(symbol))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, marketValue := range marketValues {
|
||||
if id != marketValue.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
result[symbol] = thirdparty.TokenMarketValues{
|
||||
MKTCAP: marketValue.MarketCap,
|
||||
HIGHDAY: marketValue.High24h,
|
||||
LOWDAY: marketValue.Low24h,
|
||||
CHANGEPCTHOUR: marketValue.PriceChangePercentage1hInCurrency,
|
||||
CHANGEPCTDAY: marketValue.PriceChangePercentage24h,
|
||||
CHANGEPCT24HOUR: marketValue.PriceChangePercentage24h,
|
||||
CHANGE24HOUR: marketValue.PriceChange24h,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *Client) FetchHistoricalHourlyPrices(symbol string, currency string, limit int, aggregate int) ([]thirdparty.HistoricalPrice, error) {
|
||||
return []thirdparty.HistoricalPrice{}, nil
|
||||
}
|
||||
|
||||
func (c *Client) FetchHistoricalDailyPrices(symbol string, currency string, limit int, allData bool, aggregate int) ([]thirdparty.HistoricalPrice, error) {
|
||||
id, err := c.getIDFromSymbol(utils.GetRealSymbol(symbol))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%scoins/%s/market_chart?vs_currency=%s&days=30", baseURL, id, currency)
|
||||
resp, err := c.DoQuery(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var container HistoricalPriceContainer
|
||||
err = json.Unmarshal(body, &container)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]thirdparty.HistoricalPrice, 0)
|
||||
for _, price := range container.Prices {
|
||||
result = append(result, thirdparty.HistoricalPrice{
|
||||
Timestamp: int64(price[0]),
|
||||
Value: price[1],
|
||||
})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
Generated
Vendored
+230
@@ -0,0 +1,230 @@
|
||||
package thirdparty
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/status-im/status-go/protocol/communities/token"
|
||||
"github.com/status-im/status-go/services/wallet/bigint"
|
||||
w_common "github.com/status-im/status-go/services/wallet/common"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrChainIDNotSupported = errors.New("chainID not supported")
|
||||
ErrEndpointNotSupported = errors.New("endpoint not supported")
|
||||
)
|
||||
|
||||
const FetchNoLimit = 0
|
||||
const FetchFromStartCursor = ""
|
||||
const FetchFromAnyProvider = ""
|
||||
|
||||
type CollectibleProvider interface {
|
||||
ID() string
|
||||
IsChainSupported(chainID w_common.ChainID) bool
|
||||
IsConnected() bool
|
||||
}
|
||||
|
||||
type ContractID struct {
|
||||
ChainID w_common.ChainID `json:"chainID"`
|
||||
Address common.Address `json:"address"`
|
||||
}
|
||||
|
||||
func (k *ContractID) HashKey() string {
|
||||
return fmt.Sprintf("%d+%s", k.ChainID, k.Address.String())
|
||||
}
|
||||
|
||||
type CollectibleUniqueID struct {
|
||||
ContractID ContractID `json:"contractID"`
|
||||
TokenID *bigint.BigInt `json:"tokenID"`
|
||||
}
|
||||
|
||||
func (k *CollectibleUniqueID) HashKey() string {
|
||||
return fmt.Sprintf("%s+%s", k.ContractID.HashKey(), k.TokenID.String())
|
||||
}
|
||||
|
||||
func (k *CollectibleUniqueID) Same(other *CollectibleUniqueID) bool {
|
||||
return k.ContractID.ChainID == other.ContractID.ChainID && k.ContractID.Address == other.ContractID.Address && k.TokenID.Cmp(other.TokenID.Int) == 0
|
||||
}
|
||||
|
||||
func RowsToCollectibles(rows *sql.Rows) ([]CollectibleUniqueID, error) {
|
||||
var ids []CollectibleUniqueID
|
||||
for rows.Next() {
|
||||
id := CollectibleUniqueID{
|
||||
TokenID: &bigint.BigInt{Int: big.NewInt(0)},
|
||||
}
|
||||
err := rows.Scan(
|
||||
&id.ContractID.ChainID,
|
||||
&id.ContractID.Address,
|
||||
(*bigint.SQLBigIntBytes)(id.TokenID.Int),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ids = append(ids, id)
|
||||
}
|
||||
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func GroupCollectibleUIDsByChainID(uids []CollectibleUniqueID) map[w_common.ChainID][]CollectibleUniqueID {
|
||||
ret := make(map[w_common.ChainID][]CollectibleUniqueID)
|
||||
|
||||
for _, uid := range uids {
|
||||
if _, ok := ret[uid.ContractID.ChainID]; !ok {
|
||||
ret[uid.ContractID.ChainID] = make([]CollectibleUniqueID, 0, len(uids))
|
||||
}
|
||||
ret[uid.ContractID.ChainID] = append(ret[uid.ContractID.ChainID], uid)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func GroupContractIDsByChainID(ids []ContractID) map[w_common.ChainID][]ContractID {
|
||||
ret := make(map[w_common.ChainID][]ContractID)
|
||||
|
||||
for _, id := range ids {
|
||||
if _, ok := ret[id.ChainID]; !ok {
|
||||
ret[id.ChainID] = make([]ContractID, 0, len(ids))
|
||||
}
|
||||
ret[id.ChainID] = append(ret[id.ChainID], id)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
type CollectionTrait struct {
|
||||
Min float64 `json:"min"`
|
||||
Max float64 `json:"max"`
|
||||
}
|
||||
|
||||
// Collection info
|
||||
type CollectionData struct {
|
||||
ID ContractID `json:"id"`
|
||||
ContractType w_common.ContractType `json:"contract_type"`
|
||||
CommunityID string `json:"community_id"`
|
||||
Provider string `json:"provider"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
ImageURL string `json:"image_url"`
|
||||
ImagePayload []byte
|
||||
Traits map[string]CollectionTrait `json:"traits"`
|
||||
}
|
||||
|
||||
type CollectibleTrait struct {
|
||||
TraitType string `json:"trait_type"`
|
||||
Value string `json:"value"`
|
||||
DisplayType string `json:"display_type"`
|
||||
MaxValue string `json:"max_value"`
|
||||
}
|
||||
|
||||
// Collectible info
|
||||
type CollectibleData struct {
|
||||
ID CollectibleUniqueID `json:"id"`
|
||||
ContractType w_common.ContractType `json:"contract_type"`
|
||||
CommunityID string `json:"community_id"`
|
||||
Provider string `json:"provider"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Permalink string `json:"permalink"`
|
||||
ImageURL string `json:"image_url"`
|
||||
ImagePayload []byte
|
||||
AnimationURL string `json:"animation_url"`
|
||||
AnimationMediaType string `json:"animation_media_type"`
|
||||
Traits []CollectibleTrait `json:"traits"`
|
||||
BackgroundColor string `json:"background_color"`
|
||||
TokenURI string `json:"token_uri"`
|
||||
}
|
||||
|
||||
// Community-related collectible info. Present only for collectibles minted in a community.
|
||||
type CollectibleCommunityInfo struct {
|
||||
PrivilegesLevel token.PrivilegesLevel `json:"privileges_level"`
|
||||
}
|
||||
|
||||
// Combined Collection+Collectible info returned by the CollectibleProvider
|
||||
// Some providers may not return the CollectionData in the same API call, so it's optional
|
||||
type FullCollectibleData struct {
|
||||
CollectibleData CollectibleData
|
||||
CollectionData *CollectionData
|
||||
CommunityInfo *CommunityInfo
|
||||
CollectibleCommunityInfo *CollectibleCommunityInfo
|
||||
Ownership []AccountBalance
|
||||
}
|
||||
|
||||
type CollectiblesContainer[T any] struct {
|
||||
Items []T
|
||||
NextCursor string
|
||||
PreviousCursor string
|
||||
Provider string
|
||||
}
|
||||
|
||||
type CollectibleOwnershipContainer CollectiblesContainer[CollectibleUniqueID]
|
||||
type CollectionDataContainer CollectiblesContainer[CollectionData]
|
||||
type CollectibleDataContainer CollectiblesContainer[CollectibleData]
|
||||
type FullCollectibleDataContainer CollectiblesContainer[FullCollectibleData]
|
||||
|
||||
// Tried to find a way to make this generic, but couldn't, so the code below is duplicated somewhere else
|
||||
func collectibleItemsToIDs(items []FullCollectibleData) []CollectibleUniqueID {
|
||||
ret := make([]CollectibleUniqueID, 0, len(items))
|
||||
for _, item := range items {
|
||||
ret = append(ret, item.CollectibleData.ID)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (c *FullCollectibleDataContainer) ToOwnershipContainer() CollectibleOwnershipContainer {
|
||||
return CollectibleOwnershipContainer{
|
||||
Items: collectibleItemsToIDs(c.Items),
|
||||
NextCursor: c.NextCursor,
|
||||
PreviousCursor: c.PreviousCursor,
|
||||
Provider: c.Provider,
|
||||
}
|
||||
}
|
||||
|
||||
type TokenBalance struct {
|
||||
TokenID *bigint.BigInt `json:"tokenId"`
|
||||
Balance *bigint.BigInt `json:"balance"`
|
||||
}
|
||||
|
||||
type TokenBalancesPerContractAddress = map[common.Address][]TokenBalance
|
||||
|
||||
type CollectibleOwner struct {
|
||||
OwnerAddress common.Address `json:"ownerAddress"`
|
||||
TokenBalances []TokenBalance `json:"tokenBalances"`
|
||||
}
|
||||
|
||||
type CollectibleContractOwnership struct {
|
||||
ContractAddress common.Address `json:"contractAddress"`
|
||||
Owners []CollectibleOwner `json:"owners"`
|
||||
}
|
||||
|
||||
type AccountBalance struct {
|
||||
Address common.Address `json:"address"`
|
||||
Balance *bigint.BigInt `json:"balance"`
|
||||
TxTimestamp int64 `json:"txTimestamp"`
|
||||
}
|
||||
|
||||
type CollectibleContractOwnershipProvider interface {
|
||||
CollectibleProvider
|
||||
FetchCollectibleOwnersByContractAddress(ctx context.Context, chainID w_common.ChainID, contractAddress common.Address) (*CollectibleContractOwnership, error)
|
||||
}
|
||||
|
||||
type CollectibleAccountOwnershipProvider interface {
|
||||
CollectibleProvider
|
||||
FetchAllAssetsByOwner(ctx context.Context, chainID w_common.ChainID, owner common.Address, cursor string, limit int) (*FullCollectibleDataContainer, error)
|
||||
FetchAllAssetsByOwnerAndContractAddress(ctx context.Context, chainID w_common.ChainID, owner common.Address, contractAddresses []common.Address, cursor string, limit int) (*FullCollectibleDataContainer, error)
|
||||
}
|
||||
|
||||
type CollectibleDataProvider interface {
|
||||
CollectibleProvider
|
||||
FetchAssetsByCollectibleUniqueID(ctx context.Context, uniqueIDs []CollectibleUniqueID) ([]FullCollectibleData, error)
|
||||
}
|
||||
|
||||
type CollectionDataProvider interface {
|
||||
CollectibleProvider
|
||||
FetchCollectionsDataByContractID(ctx context.Context, ids []ContractID) ([]CollectionData, error)
|
||||
}
|
||||
Generated
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
package thirdparty
|
||||
|
||||
// Community-related info used by the wallet, cached in the wallet db.
|
||||
type CommunityInfo struct {
|
||||
CommunityName string `json:"community_name"`
|
||||
CommunityColor string `json:"community_color"`
|
||||
CommunityImage string `json:"community_image"`
|
||||
CommunityImagePayload []byte
|
||||
}
|
||||
|
||||
type CommunityInfoProvider interface {
|
||||
FetchCommunityInfo(communityID string) (*CommunityInfo, error)
|
||||
|
||||
// Collectible-related methods
|
||||
GetCommunityID(tokenURI string) string
|
||||
FillCollectibleMetadata(collectible *FullCollectibleData) error
|
||||
}
|
||||
Generated
Vendored
+196
@@ -0,0 +1,196 @@
|
||||
package cryptocompare
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty"
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty/utils"
|
||||
)
|
||||
|
||||
const baseURL = "https://min-api.cryptocompare.com"
|
||||
|
||||
type HistoricalPricesContainer struct {
|
||||
Aggregated bool `json:"Aggregated"`
|
||||
TimeFrom int64 `json:"TimeFrom"`
|
||||
TimeTo int64 `json:"TimeTo"`
|
||||
HistoricalData []thirdparty.HistoricalPrice `json:"Data"`
|
||||
}
|
||||
|
||||
type HistoricalPricesData struct {
|
||||
Data HistoricalPricesContainer `json:"Data"`
|
||||
}
|
||||
|
||||
type TokenDetailsContainer struct {
|
||||
Data map[string]thirdparty.TokenDetails `json:"Data"`
|
||||
}
|
||||
|
||||
type MarketValuesContainer struct {
|
||||
Raw map[string]map[string]thirdparty.TokenMarketValues `json:"Raw"`
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewClient() *Client {
|
||||
return &Client{client: &http.Client{Timeout: time.Minute}}
|
||||
}
|
||||
|
||||
func (c *Client) DoQuery(url string) (*http.Response, error) {
|
||||
resp, err := c.client.Get(url)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *Client) FetchPrices(symbols []string, currencies []string) (map[string]map[string]float64, error) {
|
||||
chunks := utils.ChunkSymbols(symbols, 60)
|
||||
result := make(map[string]map[string]float64)
|
||||
realCurrencies := utils.RenameSymbols(currencies)
|
||||
for _, smbls := range chunks {
|
||||
realSymbols := utils.RenameSymbols(smbls)
|
||||
url := fmt.Sprintf("%s/data/pricemulti?fsyms=%s&tsyms=%s&extraParams=Status.im", baseURL, strings.Join(realSymbols, ","), strings.Join(realCurrencies, ","))
|
||||
resp, err := c.DoQuery(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prices := make(map[string]map[string]float64)
|
||||
err = json.Unmarshal(body, &prices)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, symbol := range smbls {
|
||||
result[symbol] = map[string]float64{}
|
||||
for _, currency := range currencies {
|
||||
result[symbol][currency] = prices[utils.GetRealSymbol(symbol)][utils.GetRealSymbol(currency)]
|
||||
}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *Client) FetchTokenDetails(symbols []string) (map[string]thirdparty.TokenDetails, error) {
|
||||
url := fmt.Sprintf("%s/data/all/coinlist", baseURL)
|
||||
resp, err := c.DoQuery(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
container := TokenDetailsContainer{}
|
||||
err = json.Unmarshal(body, &container)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tokenDetails := make(map[string]thirdparty.TokenDetails)
|
||||
|
||||
for _, symbol := range symbols {
|
||||
tokenDetails[symbol] = container.Data[utils.GetRealSymbol(symbol)]
|
||||
}
|
||||
|
||||
return tokenDetails, nil
|
||||
}
|
||||
|
||||
func (c *Client) FetchTokenMarketValues(symbols []string, currency string) (map[string]thirdparty.TokenMarketValues, error) {
|
||||
chunks := utils.ChunkSymbols(symbols)
|
||||
realCurrency := utils.GetRealSymbol(currency)
|
||||
item := map[string]thirdparty.TokenMarketValues{}
|
||||
for _, smbls := range chunks {
|
||||
realSymbols := utils.RenameSymbols(smbls)
|
||||
url := fmt.Sprintf("%s/data/pricemultifull?fsyms=%s&tsyms=%s&extraParams=Status.im", baseURL, strings.Join(realSymbols, ","), realCurrency)
|
||||
resp, err := c.DoQuery(url)
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
|
||||
container := MarketValuesContainer{}
|
||||
err = json.Unmarshal(body, &container)
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
|
||||
for _, symbol := range smbls {
|
||||
item[symbol] = container.Raw[utils.GetRealSymbol(symbol)][utils.GetRealSymbol(currency)]
|
||||
}
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (c *Client) FetchHistoricalHourlyPrices(symbol string, currency string, limit int, aggregate int) ([]thirdparty.HistoricalPrice, error) {
|
||||
item := []thirdparty.HistoricalPrice{}
|
||||
|
||||
url := fmt.Sprintf("%s/data/v2/histohour?fsym=%s&tsym=%s&aggregate=%d&limit=%d&extraParams=Status.im", baseURL, utils.GetRealSymbol(symbol), currency, aggregate, limit)
|
||||
resp, err := c.DoQuery(url)
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
|
||||
container := HistoricalPricesData{}
|
||||
err = json.Unmarshal(body, &container)
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
|
||||
item = container.Data.HistoricalData
|
||||
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (c *Client) FetchHistoricalDailyPrices(symbol string, currency string, limit int, allData bool, aggregate int) ([]thirdparty.HistoricalPrice, error) {
|
||||
item := []thirdparty.HistoricalPrice{}
|
||||
|
||||
url := fmt.Sprintf("%s/data/v2/histoday?fsym=%s&tsym=%s&aggregate=%d&limit=%d&allData=%v&extraParams=Status.im", baseURL, utils.GetRealSymbol(symbol), currency, aggregate, limit, allData)
|
||||
resp, err := c.DoQuery(url)
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
|
||||
container := HistoricalPricesData{}
|
||||
err = json.Unmarshal(body, &container)
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
|
||||
item = container.Data.HistoricalData
|
||||
|
||||
return item, nil
|
||||
}
|
||||
Generated
Vendored
+118
@@ -0,0 +1,118 @@
|
||||
package fourbyte
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty"
|
||||
)
|
||||
|
||||
type Signature struct {
|
||||
ID int `json:"id"`
|
||||
Text string `json:"text_signature"`
|
||||
}
|
||||
|
||||
type ByID []Signature
|
||||
|
||||
func (s ByID) Len() int { return len(s) }
|
||||
func (s ByID) Less(i, j int) bool { return s[i].ID > s[j].ID }
|
||||
func (s ByID) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
|
||||
type SignatureList struct {
|
||||
Count int `json:"count"`
|
||||
Results []Signature `json:"results"`
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
Client *http.Client
|
||||
URL string
|
||||
}
|
||||
|
||||
func NewClient() *Client {
|
||||
return &Client{Client: &http.Client{Timeout: time.Minute}, URL: "https://www.4byte.directory"}
|
||||
}
|
||||
|
||||
func (c *Client) DoQuery(url string) (*http.Response, error) {
|
||||
resp, err := c.Client.Get(url)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *Client) Run(data string) (*thirdparty.DataParsed, error) {
|
||||
if len(data) < 10 || !strings.HasPrefix(data, "0x") {
|
||||
return nil, errors.New("input is badly formatted")
|
||||
}
|
||||
methodSigData := data[2:10]
|
||||
url := fmt.Sprintf("%s/api/v1/signatures/?hex_signature=%s", c.URL, methodSigData)
|
||||
resp, err := c.DoQuery(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var signatures SignatureList
|
||||
err = json.Unmarshal(body, &signatures)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if signatures.Count == 0 {
|
||||
return nil, err
|
||||
}
|
||||
rgx := regexp.MustCompile(`\((.*?)\)`)
|
||||
results := signatures.Results
|
||||
sort.Sort(ByID(results))
|
||||
for _, signature := range results {
|
||||
id := fmt.Sprintf("0x%x", signature.ID)
|
||||
name := strings.Split(signature.Text, "(")[0]
|
||||
rs := rgx.FindStringSubmatch(signature.Text)
|
||||
inputsMapString := make(map[string]string)
|
||||
if len(rs[1]) > 0 {
|
||||
inputs := make([]string, 0)
|
||||
rawInputs := strings.Split(rs[1], ",")
|
||||
for index, typ := range rawInputs {
|
||||
if index == len(rawInputs)-1 && typ == "bytes" {
|
||||
continue
|
||||
}
|
||||
inputs = append(inputs, fmt.Sprintf("{\"name\":\"%d\",\"type\":\"%s\"}", index, typ))
|
||||
}
|
||||
functionABI := fmt.Sprintf("[{\"constant\":true,\"inputs\":[%s],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\", \"name\": \"%s\"}], ", strings.Join(inputs, ","), name)
|
||||
contractABI, err := abi.JSON(strings.NewReader(functionABI))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
method := contractABI.Methods[name]
|
||||
inputsMap := make(map[string]interface{})
|
||||
if err := method.Inputs.UnpackIntoMap(inputsMap, []byte(data[10:])); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for key, value := range inputsMap {
|
||||
inputsMapString[key] = fmt.Sprintf("%v", value)
|
||||
}
|
||||
}
|
||||
|
||||
return &thirdparty.DataParsed{
|
||||
Name: name,
|
||||
ID: id,
|
||||
Signature: signature.Text,
|
||||
Inputs: inputsMapString,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("couldn't find a corresponding signature")
|
||||
}
|
||||
Generated
Vendored
+105
@@ -0,0 +1,105 @@
|
||||
package fourbytegithub
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty"
|
||||
)
|
||||
|
||||
type Signature struct {
|
||||
ID int `json:"id"`
|
||||
Text string `json:"text_signature"`
|
||||
}
|
||||
|
||||
type ByID []Signature
|
||||
|
||||
func (s ByID) Len() int { return len(s) }
|
||||
func (s ByID) Less(i, j int) bool { return s[i].ID > s[j].ID }
|
||||
func (s ByID) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
|
||||
type SignatureList struct {
|
||||
Count int `json:"count"`
|
||||
Results []Signature `json:"results"`
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
Client *http.Client
|
||||
URL string
|
||||
}
|
||||
|
||||
func NewClient() *Client {
|
||||
return &Client{Client: &http.Client{Timeout: time.Minute}, URL: "https://raw.githubusercontent.com"}
|
||||
}
|
||||
|
||||
func (c *Client) DoQuery(url string) (*http.Response, error) {
|
||||
resp, err := c.Client.Get(url)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *Client) Run(data string) (*thirdparty.DataParsed, error) {
|
||||
if len(data) < 10 || !strings.HasPrefix(data, "0x") {
|
||||
return nil, errors.New("input is badly formatted")
|
||||
}
|
||||
methodSigData := data[2:10]
|
||||
url := fmt.Sprintf("%s/ethereum-lists/4bytes/master/signatures/%s", c.URL, methodSigData)
|
||||
resp, err := c.DoQuery(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signature := string(body)
|
||||
rgx := regexp.MustCompile(`\((.*?)\)`)
|
||||
|
||||
id := fmt.Sprintf("0x%s", methodSigData)
|
||||
name := strings.Split(signature, "(")[0]
|
||||
rs := rgx.FindStringSubmatch(signature)
|
||||
inputsMapString := make(map[string]string)
|
||||
|
||||
if len(rs[1]) > 0 {
|
||||
inputs := make([]string, 0)
|
||||
rawInputs := strings.Split(rs[1], ",")
|
||||
for index, typ := range rawInputs {
|
||||
if index == len(rawInputs)-1 && typ == "bytes" {
|
||||
continue
|
||||
}
|
||||
inputs = append(inputs, fmt.Sprintf("{\"name\":\"%d\",\"type\":\"%s\"}", index, typ))
|
||||
}
|
||||
|
||||
functionABI := fmt.Sprintf("[{\"constant\":true,\"inputs\":[%s],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\", \"name\": \"%s\"}], ", strings.Join(inputs, ","), name)
|
||||
contractABI, err := abi.JSON(strings.NewReader(functionABI))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
method := contractABI.Methods[name]
|
||||
inputsMap := make(map[string]interface{})
|
||||
if err := method.Inputs.UnpackIntoMap(inputsMap, []byte(data[10:])); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for key, value := range inputsMap {
|
||||
inputsMapString[key] = fmt.Sprintf("%v", value)
|
||||
}
|
||||
}
|
||||
|
||||
return &thirdparty.DataParsed{
|
||||
Name: name,
|
||||
ID: id,
|
||||
Signature: signature,
|
||||
Inputs: inputsMapString,
|
||||
}, nil
|
||||
}
|
||||
Generated
Vendored
+318
@@ -0,0 +1,318 @@
|
||||
package opensea
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
|
||||
walletCommon "github.com/status-im/status-go/services/wallet/common"
|
||||
"github.com/status-im/status-go/services/wallet/connection"
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty"
|
||||
)
|
||||
|
||||
const assetLimitV2 = 50
|
||||
|
||||
func getV2BaseURL(chainID walletCommon.ChainID) (string, error) {
|
||||
switch uint64(chainID) {
|
||||
case walletCommon.EthereumMainnet, walletCommon.ArbitrumMainnet, walletCommon.OptimismMainnet:
|
||||
return "https://api.opensea.io/v2", nil
|
||||
case walletCommon.EthereumSepolia, walletCommon.ArbitrumSepolia, walletCommon.OptimismSepolia:
|
||||
return "https://testnets-api.opensea.io/v2", nil
|
||||
}
|
||||
|
||||
return "", thirdparty.ErrChainIDNotSupported
|
||||
}
|
||||
|
||||
func (o *ClientV2) ID() string {
|
||||
return OpenseaV2ID
|
||||
}
|
||||
|
||||
func (o *ClientV2) IsChainSupported(chainID walletCommon.ChainID) bool {
|
||||
_, err := getV2BaseURL(chainID)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (o *ClientV2) IsConnected() bool {
|
||||
return o.connectionStatus.IsConnected()
|
||||
}
|
||||
|
||||
func getV2URL(chainID walletCommon.ChainID, path string) (string, error) {
|
||||
baseURL, err := getV2BaseURL(chainID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s/%s", baseURL, path), nil
|
||||
}
|
||||
|
||||
type ClientV2 struct {
|
||||
client *HTTPClient
|
||||
apiKey string
|
||||
connectionStatus *connection.Status
|
||||
urlGetter urlGetter
|
||||
}
|
||||
|
||||
// new opensea v2 client.
|
||||
func NewClientV2(apiKey string, httpClient *HTTPClient) *ClientV2 {
|
||||
if apiKey == "" {
|
||||
log.Warn("OpenseaV2 API key not available")
|
||||
}
|
||||
|
||||
return &ClientV2{
|
||||
client: httpClient,
|
||||
apiKey: apiKey,
|
||||
connectionStatus: connection.NewStatus(),
|
||||
urlGetter: getV2URL,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *ClientV2) FetchAllAssetsByOwnerAndContractAddress(ctx context.Context, chainID walletCommon.ChainID, owner common.Address, contractAddresses []common.Address, cursor string, limit int) (*thirdparty.FullCollectibleDataContainer, error) {
|
||||
// No dedicated endpoint to filter owned assets by contract address.
|
||||
// Will probably be available at some point, for now do the filtering ourselves.
|
||||
assets := new(thirdparty.FullCollectibleDataContainer)
|
||||
|
||||
// Build map for more efficient contract address check
|
||||
contractHashMap := make(map[string]bool)
|
||||
for _, contractAddress := range contractAddresses {
|
||||
contractID := thirdparty.ContractID{
|
||||
ChainID: chainID,
|
||||
Address: contractAddress,
|
||||
}
|
||||
contractHashMap[contractID.HashKey()] = true
|
||||
}
|
||||
|
||||
assets.PreviousCursor = cursor
|
||||
assets.NextCursor = cursor
|
||||
assets.Provider = o.ID()
|
||||
|
||||
for {
|
||||
assetsPage, err := o.FetchAllAssetsByOwner(ctx, chainID, owner, assets.NextCursor, assetLimitV2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, asset := range assetsPage.Items {
|
||||
if contractHashMap[asset.CollectibleData.ID.ContractID.HashKey()] {
|
||||
assets.Items = append(assets.Items, asset)
|
||||
}
|
||||
}
|
||||
|
||||
assets.NextCursor = assetsPage.NextCursor
|
||||
|
||||
if assets.NextCursor == "" {
|
||||
break
|
||||
}
|
||||
|
||||
if limit > thirdparty.FetchNoLimit && len(assets.Items) >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return assets, nil
|
||||
}
|
||||
|
||||
func (o *ClientV2) FetchAllAssetsByOwner(ctx context.Context, chainID walletCommon.ChainID, owner common.Address, cursor string, limit int) (*thirdparty.FullCollectibleDataContainer, error) {
|
||||
pathParams := []string{
|
||||
"chain", chainIDToChainString(chainID),
|
||||
"account", owner.String(),
|
||||
"nfts",
|
||||
}
|
||||
|
||||
queryParams := url.Values{}
|
||||
|
||||
return o.fetchAssets(ctx, chainID, pathParams, queryParams, limit, cursor)
|
||||
}
|
||||
|
||||
func (o *ClientV2) FetchAssetsByCollectibleUniqueID(ctx context.Context, uniqueIDs []thirdparty.CollectibleUniqueID) ([]thirdparty.FullCollectibleData, error) {
|
||||
return o.fetchDetailedAssets(ctx, uniqueIDs)
|
||||
}
|
||||
|
||||
func (o *ClientV2) fetchAssets(ctx context.Context, chainID walletCommon.ChainID, pathParams []string, queryParams url.Values, limit int, cursor string) (*thirdparty.FullCollectibleDataContainer, error) {
|
||||
assets := new(thirdparty.FullCollectibleDataContainer)
|
||||
|
||||
tmpLimit := assetLimitV2
|
||||
if limit > thirdparty.FetchNoLimit && limit < tmpLimit {
|
||||
tmpLimit = limit
|
||||
}
|
||||
queryParams["limit"] = []string{strconv.Itoa(tmpLimit)}
|
||||
|
||||
assets.PreviousCursor = cursor
|
||||
if cursor != "" {
|
||||
queryParams["next"] = []string{cursor}
|
||||
}
|
||||
assets.Provider = o.ID()
|
||||
|
||||
for {
|
||||
path := fmt.Sprintf("%s?%s", strings.Join(pathParams, "/"), queryParams.Encode())
|
||||
url, err := o.urlGetter(chainID, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := o.client.doGetRequest(ctx, url, o.apiKey)
|
||||
if err != nil {
|
||||
o.connectionStatus.SetIsConnected(false)
|
||||
return nil, err
|
||||
}
|
||||
o.connectionStatus.SetIsConnected(true)
|
||||
|
||||
// If body is empty, it means the account has no collectibles for this chain.
|
||||
// (Workaround implemented in http_client.go)
|
||||
if body == nil {
|
||||
assets.NextCursor = ""
|
||||
break
|
||||
}
|
||||
|
||||
// if Json is not returned there must be an error
|
||||
if !json.Valid(body) {
|
||||
return nil, fmt.Errorf("invalid json: %s", string(body))
|
||||
}
|
||||
|
||||
container := NFTContainer{}
|
||||
err = json.Unmarshal(body, &container)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, asset := range container.NFTs {
|
||||
assets.Items = append(assets.Items, asset.toCommon(chainID))
|
||||
}
|
||||
assets.NextCursor = container.NextCursor
|
||||
|
||||
if assets.NextCursor == "" {
|
||||
break
|
||||
}
|
||||
|
||||
queryParams["next"] = []string{assets.NextCursor}
|
||||
|
||||
if limit > thirdparty.FetchNoLimit && len(assets.Items) >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return assets, nil
|
||||
}
|
||||
|
||||
func (o *ClientV2) fetchDetailedAssets(ctx context.Context, uniqueIDs []thirdparty.CollectibleUniqueID) ([]thirdparty.FullCollectibleData, error) {
|
||||
assets := make([]thirdparty.FullCollectibleData, 0, len(uniqueIDs))
|
||||
|
||||
for _, id := range uniqueIDs {
|
||||
path := fmt.Sprintf("chain/%s/contract/%s/nfts/%s", chainIDToChainString(id.ContractID.ChainID), id.ContractID.Address.String(), id.TokenID.String())
|
||||
url, err := o.urlGetter(id.ContractID.ChainID, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := o.client.doGetRequest(ctx, url, o.apiKey)
|
||||
if err != nil {
|
||||
if ctx.Err() == nil {
|
||||
o.connectionStatus.SetIsConnected(false)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
o.connectionStatus.SetIsConnected(true)
|
||||
|
||||
// if Json is not returned there must be an error
|
||||
if !json.Valid(body) {
|
||||
return nil, fmt.Errorf("invalid json: %s", string(body))
|
||||
}
|
||||
|
||||
nftContainer := DetailedNFTContainer{}
|
||||
err = json.Unmarshal(body, &nftContainer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
assets = append(assets, nftContainer.NFT.toCommon(id.ContractID.ChainID))
|
||||
}
|
||||
|
||||
return assets, nil
|
||||
}
|
||||
|
||||
func (o *ClientV2) fetchContractDataByContractID(ctx context.Context, id thirdparty.ContractID) (*ContractData, error) {
|
||||
path := fmt.Sprintf("chain/%s/contract/%s", chainIDToChainString(id.ChainID), id.Address.String())
|
||||
url, err := o.urlGetter(id.ChainID, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := o.client.doGetRequest(ctx, url, o.apiKey)
|
||||
if err != nil {
|
||||
if ctx.Err() == nil {
|
||||
o.connectionStatus.SetIsConnected(false)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
o.connectionStatus.SetIsConnected(true)
|
||||
|
||||
// if Json is not returned there must be an error
|
||||
if !json.Valid(body) {
|
||||
return nil, fmt.Errorf("invalid json: %s", string(body))
|
||||
}
|
||||
|
||||
contract := ContractData{}
|
||||
err = json.Unmarshal(body, &contract)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &contract, nil
|
||||
}
|
||||
|
||||
func (o *ClientV2) fetchCollectionDataBySlug(ctx context.Context, chainID walletCommon.ChainID, slug string) (*CollectionData, error) {
|
||||
path := fmt.Sprintf("collections/%s", slug)
|
||||
url, err := o.urlGetter(chainID, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := o.client.doGetRequest(ctx, url, o.apiKey)
|
||||
if err != nil {
|
||||
o.connectionStatus.SetIsConnected(false)
|
||||
return nil, err
|
||||
}
|
||||
o.connectionStatus.SetIsConnected(true)
|
||||
|
||||
// if Json is not returned there must be an error
|
||||
if !json.Valid(body) {
|
||||
return nil, fmt.Errorf("invalid json: %s", string(body))
|
||||
}
|
||||
|
||||
collection := CollectionData{}
|
||||
err = json.Unmarshal(body, &collection)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &collection, nil
|
||||
}
|
||||
|
||||
func (o *ClientV2) FetchCollectionsDataByContractID(ctx context.Context, contractIDs []thirdparty.ContractID) ([]thirdparty.CollectionData, error) {
|
||||
ret := make([]thirdparty.CollectionData, 0, len(contractIDs))
|
||||
|
||||
for _, id := range contractIDs {
|
||||
contractData, err := o.fetchContractDataByContractID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if contractData == nil || contractData.Collection == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
collectionData, err := o.fetchCollectionDataBySlug(ctx, id.ChainID, contractData.Collection)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret = append(ret, collectionData.toCommon(id, contractData.ContractStandard))
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
Generated
Vendored
+96
@@ -0,0 +1,96 @@
|
||||
package opensea
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
const requestTimeout = 5 * time.Second
|
||||
const getRequestRetryMaxCount = 15
|
||||
const getRequestWaitTime = 300 * time.Millisecond
|
||||
|
||||
type HTTPClient struct {
|
||||
client *http.Client
|
||||
getRequestLock sync.RWMutex
|
||||
}
|
||||
|
||||
func NewHTTPClient() *HTTPClient {
|
||||
return &HTTPClient{
|
||||
client: &http.Client{
|
||||
Timeout: requestTimeout,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (o *HTTPClient) doGetRequest(ctx context.Context, url string, apiKey string) ([]byte, error) {
|
||||
// Ensure only one thread makes a request at a time
|
||||
o.getRequestLock.Lock()
|
||||
defer o.getRequestLock.Unlock()
|
||||
|
||||
retryCount := 0
|
||||
statusCode := http.StatusOK
|
||||
|
||||
// Try to do the request without an apiKey first
|
||||
tmpAPIKey := ""
|
||||
|
||||
for {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:96.0) Gecko/20100101 Firefox/96.0")
|
||||
if len(tmpAPIKey) > 0 {
|
||||
req.Header.Set("X-API-KEY", tmpAPIKey)
|
||||
}
|
||||
|
||||
resp, err := o.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
log.Error("failed to close opensea request body", "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
statusCode = resp.StatusCode
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
return body, err
|
||||
case http.StatusBadRequest:
|
||||
// The OpenSea v2 API will return error 400 if the account holds no collectibles on
|
||||
// the requested chain. This shouldn't be treated as an error, return an empty body.
|
||||
return nil, nil
|
||||
case http.StatusTooManyRequests:
|
||||
if retryCount < getRequestRetryMaxCount {
|
||||
// sleep and retry
|
||||
time.Sleep(getRequestWaitTime)
|
||||
retryCount++
|
||||
continue
|
||||
}
|
||||
// break and error
|
||||
case http.StatusForbidden:
|
||||
// Request requires an apiKey, set it and retry
|
||||
if tmpAPIKey == "" && apiKey != "" {
|
||||
tmpAPIKey = apiKey
|
||||
// sleep and retry
|
||||
time.Sleep(getRequestWaitTime)
|
||||
continue
|
||||
}
|
||||
// break and error
|
||||
default:
|
||||
// break and error
|
||||
}
|
||||
break
|
||||
}
|
||||
return nil, fmt.Errorf("unsuccessful request: %d %s", statusCode, http.StatusText(statusCode))
|
||||
}
|
||||
Generated
Vendored
+238
@@ -0,0 +1,238 @@
|
||||
package opensea
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"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 (
|
||||
OpenseaV2ID = "openseaV2"
|
||||
ethereumMainnetString = "ethereum"
|
||||
arbitrumMainnetString = "arbitrum"
|
||||
optimismMainnetString = "optimism"
|
||||
ethereumSepoliaString = "sepolia"
|
||||
arbitrumSepoliaString = "arbitrum_sepolia"
|
||||
optimismSepoliaString = "optimism_sepolia"
|
||||
)
|
||||
|
||||
type urlGetter func(walletCommon.ChainID, string) (string, error)
|
||||
|
||||
func chainIDToChainString(chainID walletCommon.ChainID) string {
|
||||
chainString := ""
|
||||
switch uint64(chainID) {
|
||||
case walletCommon.EthereumMainnet:
|
||||
chainString = ethereumMainnetString
|
||||
case walletCommon.ArbitrumMainnet:
|
||||
chainString = arbitrumMainnetString
|
||||
case walletCommon.OptimismMainnet:
|
||||
chainString = optimismMainnetString
|
||||
case walletCommon.EthereumSepolia:
|
||||
chainString = ethereumSepoliaString
|
||||
case walletCommon.ArbitrumSepolia:
|
||||
chainString = arbitrumSepoliaString
|
||||
case walletCommon.OptimismSepolia:
|
||||
chainString = optimismSepoliaString
|
||||
}
|
||||
return chainString
|
||||
}
|
||||
|
||||
func openseaToContractType(contractType string) walletCommon.ContractType {
|
||||
switch contractType {
|
||||
case "cryptopunks", "erc721":
|
||||
return walletCommon.ContractTypeERC721
|
||||
case "erc1155":
|
||||
return walletCommon.ContractTypeERC1155
|
||||
default:
|
||||
return walletCommon.ContractTypeUnknown
|
||||
}
|
||||
}
|
||||
|
||||
type NFTContainer struct {
|
||||
NFTs []NFT `json:"nfts"`
|
||||
NextCursor string `json:"next"`
|
||||
}
|
||||
|
||||
type NFT struct {
|
||||
TokenID *bigint.BigInt `json:"identifier"`
|
||||
Collection string `json:"collection"`
|
||||
Contract common.Address `json:"contract"`
|
||||
TokenStandard string `json:"token_standard"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
ImageURL string `json:"image_url"`
|
||||
MetadataURL string `json:"metadata_url"`
|
||||
}
|
||||
|
||||
type DetailedNFTContainer struct {
|
||||
NFT DetailedNFT `json:"nft"`
|
||||
}
|
||||
|
||||
type DetailedNFT struct {
|
||||
TokenID *bigint.BigInt `json:"identifier"`
|
||||
Collection string `json:"collection"`
|
||||
Contract common.Address `json:"contract"`
|
||||
TokenStandard string `json:"token_standard"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
ImageURL string `json:"image_url"`
|
||||
AnimationURL string `json:"animation_url"`
|
||||
MetadataURL string `json:"metadata_url"`
|
||||
Owners []OwnerV2 `json:"owners"`
|
||||
Traits []TraitV2 `json:"traits"`
|
||||
}
|
||||
|
||||
type OwnerV2 struct {
|
||||
Address common.Address `json:"address"`
|
||||
Quantity *bigint.BigInt `json:"quantity"`
|
||||
}
|
||||
|
||||
type TraitValue string
|
||||
|
||||
func (st *TraitValue) UnmarshalJSON(b []byte) error {
|
||||
var item interface{}
|
||||
if err := json.Unmarshal(b, &item); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch v := item.(type) {
|
||||
case float64:
|
||||
*st = TraitValue(strconv.FormatFloat(v, 'f', 2, 64))
|
||||
case int:
|
||||
*st = TraitValue(strconv.Itoa(v))
|
||||
case string:
|
||||
*st = TraitValue(v)
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type TraitV2 struct {
|
||||
TraitType string `json:"trait_type"`
|
||||
DisplayType string `json:"display_type"`
|
||||
MaxValue string `json:"max_value"`
|
||||
TraitCount int `json:"trait_count"`
|
||||
Order string `json:"order"`
|
||||
Value TraitValue `json:"value"`
|
||||
}
|
||||
|
||||
type ContractData struct {
|
||||
Address common.Address `json:"address"`
|
||||
Chain string `json:"chain"`
|
||||
Collection string `json:"collection"`
|
||||
ContractStandard string `json:"contract_standard"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type ContractID struct {
|
||||
Address common.Address `json:"address"`
|
||||
Chain string `json:"chain"`
|
||||
}
|
||||
|
||||
type CollectionData struct {
|
||||
Collection string `json:"collection"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Owner common.Address `json:"owner"`
|
||||
ImageURL string `json:"image_url"`
|
||||
Contracts []ContractID `json:"contracts"`
|
||||
}
|
||||
|
||||
func (c *NFT) id(chainID walletCommon.ChainID) thirdparty.CollectibleUniqueID {
|
||||
return thirdparty.CollectibleUniqueID{
|
||||
ContractID: thirdparty.ContractID{
|
||||
ChainID: chainID,
|
||||
Address: c.Contract,
|
||||
},
|
||||
TokenID: c.TokenID,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *NFT) toCollectiblesData(chainID walletCommon.ChainID) thirdparty.CollectibleData {
|
||||
return thirdparty.CollectibleData{
|
||||
ID: c.id(chainID),
|
||||
ContractType: openseaToContractType(c.TokenStandard),
|
||||
Provider: OpenseaV2ID,
|
||||
Name: c.Name,
|
||||
Description: c.Description,
|
||||
ImageURL: c.ImageURL,
|
||||
AnimationURL: c.ImageURL,
|
||||
Traits: make([]thirdparty.CollectibleTrait, 0),
|
||||
TokenURI: c.MetadataURL,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *NFT) toCommon(chainID walletCommon.ChainID) thirdparty.FullCollectibleData {
|
||||
return thirdparty.FullCollectibleData{
|
||||
CollectibleData: c.toCollectiblesData(chainID),
|
||||
CollectionData: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func openseaV2ToCollectibleTraits(traits []TraitV2) []thirdparty.CollectibleTrait {
|
||||
ret := make([]thirdparty.CollectibleTrait, 0, len(traits))
|
||||
caser := cases.Title(language.Und, cases.NoLower)
|
||||
for _, orig := range traits {
|
||||
dest := thirdparty.CollectibleTrait{
|
||||
TraitType: strings.Replace(orig.TraitType, "_", " ", 1),
|
||||
Value: caser.String(string(orig.Value)),
|
||||
DisplayType: orig.DisplayType,
|
||||
MaxValue: orig.MaxValue,
|
||||
}
|
||||
|
||||
ret = append(ret, dest)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (c *DetailedNFT) id(chainID walletCommon.ChainID) thirdparty.CollectibleUniqueID {
|
||||
return thirdparty.CollectibleUniqueID{
|
||||
ContractID: thirdparty.ContractID{
|
||||
ChainID: chainID,
|
||||
Address: c.Contract,
|
||||
},
|
||||
TokenID: c.TokenID,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *DetailedNFT) toCollectiblesData(chainID walletCommon.ChainID) thirdparty.CollectibleData {
|
||||
return thirdparty.CollectibleData{
|
||||
ID: c.id(chainID),
|
||||
ContractType: openseaToContractType(c.TokenStandard),
|
||||
Provider: OpenseaV2ID,
|
||||
Name: c.Name,
|
||||
Description: c.Description,
|
||||
ImageURL: c.ImageURL,
|
||||
AnimationURL: c.AnimationURL,
|
||||
Traits: openseaV2ToCollectibleTraits(c.Traits),
|
||||
TokenURI: c.MetadataURL,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *DetailedNFT) toCommon(chainID walletCommon.ChainID) thirdparty.FullCollectibleData {
|
||||
return thirdparty.FullCollectibleData{
|
||||
CollectibleData: c.toCollectiblesData(chainID),
|
||||
CollectionData: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CollectionData) toCommon(id thirdparty.ContractID, tokenStandard string) thirdparty.CollectionData {
|
||||
ret := thirdparty.CollectionData{
|
||||
ID: id,
|
||||
ContractType: openseaToContractType(tokenStandard),
|
||||
Provider: OpenseaV2ID,
|
||||
Name: c.Name,
|
||||
Slug: c.Collection,
|
||||
ImageURL: c.ImageURL,
|
||||
}
|
||||
return ret
|
||||
}
|
||||
Generated
Vendored
+428
@@ -0,0 +1,428 @@
|
||||
package rarible
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
|
||||
walletCommon "github.com/status-im/status-go/services/wallet/common"
|
||||
"github.com/status-im/status-go/services/wallet/connection"
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty"
|
||||
)
|
||||
|
||||
const ownedNFTLimit = 100
|
||||
const collectionOwnershipLimit = 50
|
||||
const nftMetadataBatchLimit = 50
|
||||
|
||||
func (o *Client) ID() string {
|
||||
return RaribleID
|
||||
}
|
||||
|
||||
func (o *Client) IsChainSupported(chainID walletCommon.ChainID) bool {
|
||||
_, err := getBaseURL(chainID)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (o *Client) IsConnected() bool {
|
||||
return o.connectionStatus.IsConnected()
|
||||
}
|
||||
|
||||
func getBaseURL(chainID walletCommon.ChainID) (string, error) {
|
||||
switch uint64(chainID) {
|
||||
case walletCommon.EthereumMainnet, walletCommon.ArbitrumMainnet:
|
||||
return "https://api.rarible.org", nil
|
||||
case walletCommon.EthereumGoerli, walletCommon.ArbitrumSepolia:
|
||||
return "https://testnet-api.rarible.org", nil
|
||||
}
|
||||
|
||||
return "", thirdparty.ErrChainIDNotSupported
|
||||
}
|
||||
|
||||
func getItemBaseURL(chainID walletCommon.ChainID) (string, error) {
|
||||
baseURL, err := getBaseURL(chainID)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s/v0.1/items", baseURL), nil
|
||||
}
|
||||
|
||||
func getOwnershipBaseURL(chainID walletCommon.ChainID) (string, error) {
|
||||
baseURL, err := getBaseURL(chainID)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s/v0.1/ownerships", baseURL), nil
|
||||
}
|
||||
|
||||
func getCollectionBaseURL(chainID walletCommon.ChainID) (string, error) {
|
||||
baseURL, err := getBaseURL(chainID)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s/v0.1/collections", baseURL), nil
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
thirdparty.CollectibleContractOwnershipProvider
|
||||
client *http.Client
|
||||
mainnetAPIKey string
|
||||
testnetAPIKey string
|
||||
connectionStatus *connection.Status
|
||||
}
|
||||
|
||||
func NewClient(mainnetAPIKey string, testnetAPIKey string) *Client {
|
||||
if mainnetAPIKey == "" {
|
||||
log.Warn("Rarible API key not available for Mainnet")
|
||||
}
|
||||
|
||||
if testnetAPIKey == "" {
|
||||
log.Warn("Rarible API key not available for Testnet")
|
||||
}
|
||||
|
||||
return &Client{
|
||||
client: &http.Client{Timeout: time.Minute},
|
||||
mainnetAPIKey: mainnetAPIKey,
|
||||
testnetAPIKey: testnetAPIKey,
|
||||
connectionStatus: connection.NewStatus(),
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Client) getAPIKey(chainID walletCommon.ChainID) string {
|
||||
if chainID.IsMainnet() {
|
||||
return o.mainnetAPIKey
|
||||
}
|
||||
return o.testnetAPIKey
|
||||
}
|
||||
|
||||
func (o *Client) doQuery(ctx context.Context, url string, apiKey string) (*http.Response, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("content-type", "application/json")
|
||||
|
||||
return o.doWithRetries(req, apiKey)
|
||||
}
|
||||
|
||||
func (o *Client) doPostWithJSON(ctx context.Context, url string, payload any, apiKey string) (*http.Response, error) {
|
||||
payloadJSON, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
payloadString := string(payloadJSON)
|
||||
payloadReader := strings.NewReader(payloadString)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", url, payloadReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Add("accept", "application/json")
|
||||
req.Header.Add("content-type", "application/json")
|
||||
|
||||
return o.doWithRetries(req, apiKey)
|
||||
}
|
||||
|
||||
func (o *Client) doWithRetries(req *http.Request, apiKey string) (*http.Response, error) {
|
||||
b := backoff.ExponentialBackOff{
|
||||
InitialInterval: time.Millisecond * 1000,
|
||||
RandomizationFactor: 0.1,
|
||||
Multiplier: 1.5,
|
||||
MaxInterval: time.Second * 32,
|
||||
MaxElapsedTime: time.Second * 128,
|
||||
Clock: backoff.SystemClock,
|
||||
}
|
||||
b.Reset()
|
||||
|
||||
req.Header.Set("X-API-KEY", apiKey)
|
||||
|
||||
op := func() (*http.Response, error) {
|
||||
resp, err := o.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, backoff.Permanent(err)
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
err = fmt.Errorf("unsuccessful request: %d %s", resp.StatusCode, http.StatusText(resp.StatusCode))
|
||||
if resp.StatusCode == http.StatusTooManyRequests {
|
||||
return nil, err
|
||||
}
|
||||
return nil, backoff.Permanent(err)
|
||||
}
|
||||
|
||||
return backoff.RetryWithData(op, &b)
|
||||
}
|
||||
|
||||
func (o *Client) FetchCollectibleOwnersByContractAddress(ctx context.Context, chainID walletCommon.ChainID, contractAddress common.Address) (*thirdparty.CollectibleContractOwnership, error) {
|
||||
ownership := thirdparty.CollectibleContractOwnership{
|
||||
ContractAddress: contractAddress,
|
||||
Owners: make([]thirdparty.CollectibleOwner, 0),
|
||||
}
|
||||
|
||||
queryParams := url.Values{
|
||||
"collection": {fmt.Sprintf("%s:%s", chainIDToChainString(chainID), contractAddress.String())},
|
||||
"size": {strconv.Itoa(collectionOwnershipLimit)},
|
||||
}
|
||||
|
||||
baseURL, err := getOwnershipBaseURL(chainID)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for {
|
||||
url := fmt.Sprintf("%s/byCollection?%s", baseURL, queryParams.Encode())
|
||||
|
||||
resp, err := o.doQuery(ctx, url, o.getAPIKey(chainID))
|
||||
if err != nil {
|
||||
o.connectionStatus.SetIsConnected(false)
|
||||
return nil, err
|
||||
}
|
||||
o.connectionStatus.SetIsConnected(true)
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var raribleOwnership ContractOwnershipContainer
|
||||
err = json.Unmarshal(body, &raribleOwnership)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ownership.Owners = append(ownership.Owners, raribleContractOwnershipsToCommon(raribleOwnership.Ownerships)...)
|
||||
|
||||
if raribleOwnership.Continuation == "" {
|
||||
break
|
||||
}
|
||||
|
||||
queryParams["continuation"] = []string{raribleOwnership.Continuation}
|
||||
}
|
||||
|
||||
return &ownership, nil
|
||||
}
|
||||
|
||||
func (o *Client) FetchAllAssetsByOwner(ctx context.Context, chainID walletCommon.ChainID, owner common.Address, cursor string, limit int) (*thirdparty.FullCollectibleDataContainer, error) {
|
||||
assets := new(thirdparty.FullCollectibleDataContainer)
|
||||
|
||||
queryParams := url.Values{
|
||||
"owner": {fmt.Sprintf("%s:%s", ethereumString, owner.String())},
|
||||
"blockchains": {chainIDToChainString(chainID)},
|
||||
}
|
||||
|
||||
tmpLimit := ownedNFTLimit
|
||||
if limit > thirdparty.FetchNoLimit && limit < tmpLimit {
|
||||
tmpLimit = limit
|
||||
}
|
||||
queryParams["size"] = []string{strconv.Itoa(tmpLimit)}
|
||||
|
||||
if len(cursor) > 0 {
|
||||
queryParams["continuation"] = []string{cursor}
|
||||
assets.PreviousCursor = cursor
|
||||
}
|
||||
assets.Provider = o.ID()
|
||||
|
||||
baseURL, err := getItemBaseURL(chainID)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for {
|
||||
url := fmt.Sprintf("%s/byOwner?%s", baseURL, queryParams.Encode())
|
||||
|
||||
resp, err := o.doQuery(ctx, url, o.getAPIKey(chainID))
|
||||
if err != nil {
|
||||
o.connectionStatus.SetIsConnected(false)
|
||||
return nil, err
|
||||
}
|
||||
o.connectionStatus.SetIsConnected(true)
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if Json is not returned there must be an error
|
||||
if !json.Valid(body) {
|
||||
return nil, fmt.Errorf("invalid json: %s", string(body))
|
||||
}
|
||||
|
||||
var container CollectiblesContainer
|
||||
err = json.Unmarshal(body, &container)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
assets.Items = append(assets.Items, raribleToCollectiblesData(container.Collectibles, chainID.IsMainnet())...)
|
||||
assets.NextCursor = container.Continuation
|
||||
|
||||
if len(assets.NextCursor) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
queryParams["continuation"] = []string{assets.NextCursor}
|
||||
|
||||
if limit != thirdparty.FetchNoLimit && len(assets.Items) >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return assets, nil
|
||||
}
|
||||
|
||||
func (o *Client) FetchAllAssetsByOwnerAndContractAddress(ctx context.Context, chainID walletCommon.ChainID, owner common.Address, contractAddresses []common.Address, cursor string, limit int) (*thirdparty.FullCollectibleDataContainer, error) {
|
||||
return nil, thirdparty.ErrEndpointNotSupported
|
||||
}
|
||||
|
||||
func getCollectibleUniqueIDBatches(ids []thirdparty.CollectibleUniqueID) []BatchTokenIDs {
|
||||
batches := make([]BatchTokenIDs, 0)
|
||||
|
||||
for startIdx := 0; startIdx < len(ids); startIdx += nftMetadataBatchLimit {
|
||||
endIdx := startIdx + nftMetadataBatchLimit
|
||||
if endIdx > len(ids) {
|
||||
endIdx = len(ids)
|
||||
}
|
||||
|
||||
pageIDs := ids[startIdx:endIdx]
|
||||
|
||||
batchIDs := BatchTokenIDs{
|
||||
IDs: make([]string, 0, len(pageIDs)),
|
||||
}
|
||||
for _, id := range pageIDs {
|
||||
batchID := fmt.Sprintf("%s:%s:%s", chainIDToChainString(id.ContractID.ChainID), id.ContractID.Address.String(), id.TokenID.String())
|
||||
batchIDs.IDs = append(batchIDs.IDs, batchID)
|
||||
}
|
||||
|
||||
batches = append(batches, batchIDs)
|
||||
}
|
||||
|
||||
return batches
|
||||
}
|
||||
|
||||
func (o *Client) fetchAssetsByBatchTokenIDs(ctx context.Context, chainID walletCommon.ChainID, batchIDs BatchTokenIDs) ([]thirdparty.FullCollectibleData, error) {
|
||||
baseURL, err := getItemBaseURL(chainID)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/byIds", baseURL)
|
||||
|
||||
resp, err := o.doPostWithJSON(ctx, url, batchIDs, o.getAPIKey(chainID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if Json is not returned there must be an error
|
||||
if !json.Valid(body) {
|
||||
return nil, fmt.Errorf("invalid json: %s", string(body))
|
||||
}
|
||||
|
||||
var assets CollectiblesContainer
|
||||
err = json.Unmarshal(body, &assets)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret := raribleToCollectiblesData(assets.Collectibles, chainID.IsMainnet())
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (o *Client) FetchAssetsByCollectibleUniqueID(ctx context.Context, uniqueIDs []thirdparty.CollectibleUniqueID) ([]thirdparty.FullCollectibleData, error) {
|
||||
ret := make([]thirdparty.FullCollectibleData, 0, len(uniqueIDs))
|
||||
|
||||
idsPerChainID := thirdparty.GroupCollectibleUIDsByChainID(uniqueIDs)
|
||||
|
||||
for chainID, ids := range idsPerChainID {
|
||||
batches := getCollectibleUniqueIDBatches(ids)
|
||||
for _, batch := range batches {
|
||||
assets, err := o.fetchAssetsByBatchTokenIDs(ctx, chainID, batch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret = append(ret, assets...)
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (o *Client) FetchCollectionsDataByContractID(ctx context.Context, contractIDs []thirdparty.ContractID) ([]thirdparty.CollectionData, error) {
|
||||
ret := make([]thirdparty.CollectionData, 0, len(contractIDs))
|
||||
|
||||
for _, contractID := range contractIDs {
|
||||
baseURL, err := getCollectionBaseURL(contractID.ChainID)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/%s:%s", baseURL, chainIDToChainString(contractID.ChainID), contractID.Address.String())
|
||||
|
||||
resp, err := o.doQuery(ctx, url, o.getAPIKey(contractID.ChainID))
|
||||
if err != nil {
|
||||
o.connectionStatus.SetIsConnected(false)
|
||||
return nil, err
|
||||
}
|
||||
o.connectionStatus.SetIsConnected(true)
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if Json is not returned there must be an error
|
||||
if !json.Valid(body) {
|
||||
return nil, fmt.Errorf("invalid json: %s", string(body))
|
||||
}
|
||||
|
||||
var collection Collection
|
||||
err = json.Unmarshal(body, &collection)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret = append(ret, collection.toCommon(contractID))
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
Generated
Vendored
+227
@@ -0,0 +1,227 @@
|
||||
package rarible
|
||||
|
||||
const collectionJSON = `{
|
||||
"id": "ETHEREUM:0x06012c8cf97bead5deae237070f9587f8e7a266d",
|
||||
"blockchain": "ETHEREUM",
|
||||
"structure": "REGULAR",
|
||||
"type": "ERC721",
|
||||
"status": "CONFIRMED",
|
||||
"name": "CryptoKitties",
|
||||
"symbol": "CK",
|
||||
"features": [],
|
||||
"minters": [],
|
||||
"meta": {
|
||||
"name": "CryptoKitties",
|
||||
"description": "CryptoKitties is a game centered around breedable, collectible, and oh-so-adorable creatures we call CryptoKitties! Each cat is one-of-a-kind and 100% owned by you; it cannot be replicated, taken away, or destroyed.",
|
||||
"tags": [],
|
||||
"genres": [],
|
||||
"externalUri": "https://www.cryptokitties.co/",
|
||||
"content": [
|
||||
{
|
||||
"@type": "IMAGE",
|
||||
"url": "https://i.seadn.io/gae/C272ZRW1RGGef9vKMePFSCeKc1Lw6U40wl9ofNVxzUxFdj84hH9xJRQNf-7wgs7W8qw8RWe-1ybKp-VKuU5D-tg?w=500&auto=format",
|
||||
"representation": "ORIGINAL",
|
||||
"mimeType": "image/png",
|
||||
"size": 7584,
|
||||
"available": true,
|
||||
"width": 250,
|
||||
"height": 246
|
||||
}
|
||||
],
|
||||
"externalLink": "https://www.cryptokitties.co/",
|
||||
"sellerFeeBasisPoints": 250
|
||||
},
|
||||
"originOrders": [],
|
||||
"self": false,
|
||||
"scam": false
|
||||
}`
|
||||
|
||||
const ownedCollectiblesJSON = `{
|
||||
"continuation": "1603836283000_ETHEREUM:0xd07dc4262bcdbf85190c01c996b4c06a461d2430:63879:0x4765273c477c2dc484da4f1984639e943adccfeb",
|
||||
"items": [
|
||||
{
|
||||
"id": "ETHEREUM:0xb66a603f4cfe17e3d27b87a8bfcad319856518b8:32292934596187112148346015918544186536963932779440027682601542850818403729416",
|
||||
"blockchain": "ETHEREUM",
|
||||
"collection": "ETHEREUM:0xb66a603f4cfe17e3d27b87a8bfcad319856518b8",
|
||||
"contract": "ETHEREUM:0xb66a603f4cfe17e3d27b87a8bfcad319856518b8",
|
||||
"tokenId": "32292934596187112148346015918544186536963932779440027682601542850818403729416",
|
||||
"creators": [
|
||||
{
|
||||
"account": "ETHEREUM:0x4765273c477c2dc484da4f1984639e943adccfeb",
|
||||
"value": 10000
|
||||
}
|
||||
],
|
||||
"lazySupply": "0",
|
||||
"pending": [],
|
||||
"mintedAt": "2023-06-01T10:03:23Z",
|
||||
"lastUpdatedAt": "2023-06-01T10:03:38.923Z",
|
||||
"supply": "100",
|
||||
"meta": {
|
||||
"name": "Rariversary #002",
|
||||
"description": "Today marks your Second Rariversary! Can you believe it’s already been two years? Time flies when you’re having fun! Thank you for everything you contribute!",
|
||||
"createdAt": "2023-06-01T10:03:23Z",
|
||||
"tags": [],
|
||||
"genres": [],
|
||||
"externalUri": "https://rarible.com/token/0xb66a603f4cfe17e3d27b87a8bfcad319856518b8:32292934596187112148346015918544186536963932779440027682601542850818403729416",
|
||||
"originalMetaUri": "ipfs://ipfs/bafkreialxjfvfkn43jluxmilfg3d3ojnomtqg634nuowqq2syx4odqrx5m",
|
||||
"attributes": [
|
||||
{
|
||||
"key": "Theme",
|
||||
"value": "luv u"
|
||||
},
|
||||
{
|
||||
"key": "Gift for",
|
||||
"value": "Rariversary"
|
||||
},
|
||||
{
|
||||
"key": "Year",
|
||||
"value": "2"
|
||||
}
|
||||
],
|
||||
"content": [
|
||||
{
|
||||
"@type": "IMAGE",
|
||||
"url": "https://ipfs.raribleuserdata.com/ipfs/bafybeibpqyrvdkw7ypajsmsvjiz2mhytv7fyyfa6n35tfui7e473dxnyom/image.png",
|
||||
"representation": "ORIGINAL",
|
||||
"mimeType": "image/png",
|
||||
"size": 4675851,
|
||||
"available": true,
|
||||
"width": 2000,
|
||||
"height": 2000
|
||||
},
|
||||
{
|
||||
"@type": "IMAGE",
|
||||
"url": "https://lh3.googleusercontent.com/03DCIWuHtWUG5zIPAkdBjPAucg-BNu-917hsY1LRyEtG9pMcYSwIv5n_jZoK4bvMjNbw9MEC3AZA29kje83fCf2XwG6WegOv0JU=s1000",
|
||||
"representation": "BIG",
|
||||
"mimeType": "image/png",
|
||||
"size": 1216435,
|
||||
"available": true,
|
||||
"width": 1000,
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"@type": "IMAGE",
|
||||
"url": "https://lh3.googleusercontent.com/03DCIWuHtWUG5zIPAkdBjPAucg-BNu-917hsY1LRyEtG9pMcYSwIv5n_jZoK4bvMjNbw9MEC3AZA29kje83fCf2XwG6WegOv0JU=k-w1200-s2400-rj",
|
||||
"representation": "PORTRAIT",
|
||||
"mimeType": "image/jpeg",
|
||||
"size": 191288,
|
||||
"available": true,
|
||||
"width": 1200,
|
||||
"height": 1200
|
||||
},
|
||||
{
|
||||
"@type": "IMAGE",
|
||||
"url": "https://lh3.googleusercontent.com/03DCIWuHtWUG5zIPAkdBjPAucg-BNu-917hsY1LRyEtG9pMcYSwIv5n_jZoK4bvMjNbw9MEC3AZA29kje83fCf2XwG6WegOv0JU=s250",
|
||||
"representation": "PREVIEW",
|
||||
"mimeType": "image/png",
|
||||
"size": 96841,
|
||||
"available": true,
|
||||
"width": 250,
|
||||
"height": 250
|
||||
}
|
||||
]
|
||||
},
|
||||
"deleted": false,
|
||||
"originOrders": [],
|
||||
"ammOrders": {
|
||||
"ids": []
|
||||
},
|
||||
"auctions": [],
|
||||
"totalStock": "0",
|
||||
"sellers": 0,
|
||||
"suspicious": false
|
||||
},
|
||||
{
|
||||
"id": "ETHEREUM:0xb66a603f4cfe17e3d27b87a8bfcad319856518b8:32292934596187112148346015918544186536963932779440027682601542850818403729414",
|
||||
"blockchain": "ETHEREUM",
|
||||
"collection": "ETHEREUM:0xb66a603f4cfe17e3d27b87a8bfcad319856518b8",
|
||||
"contract": "ETHEREUM:0xb66a603f4cfe17e3d27b87a8bfcad319856518b8",
|
||||
"tokenId": "32292934596187112148346015918544186536963932779440027682601542850818403729414",
|
||||
"creators": [
|
||||
{
|
||||
"account": "ETHEREUM:0x4765273c477c2dc484da4f1984639e943adccfeb",
|
||||
"value": 10000
|
||||
}
|
||||
],
|
||||
"lazySupply": "0",
|
||||
"pending": [],
|
||||
"mintedAt": "2023-06-01T09:43:35Z",
|
||||
"lastUpdatedAt": "2023-06-01T09:43:41.498Z",
|
||||
"supply": "100",
|
||||
"meta": {
|
||||
"name": "Rariversary #003",
|
||||
"description": "Today marks your Third Rariversary! Can you believe it’s already been three years? Time flies when you’re having fun! We’ve loved working with you these years and can’t wait to see what the next few years bring. Thank you for everything you contribute!",
|
||||
"createdAt": "2023-06-01T09:43:35Z",
|
||||
"tags": [],
|
||||
"genres": [],
|
||||
"externalUri": "https://rarible.com/token/0xb66a603f4cfe17e3d27b87a8bfcad319856518b8:32292934596187112148346015918544186536963932779440027682601542850818403729414",
|
||||
"originalMetaUri": "ipfs://ipfs/bafkreifeaueluerp33pjevz56f3ioxv63z73zuvm4wku5k6sobvala4phe",
|
||||
"attributes": [
|
||||
{
|
||||
"key": "Theme",
|
||||
"value": "LFG"
|
||||
},
|
||||
{
|
||||
"key": "Gift for",
|
||||
"value": "Rariversary"
|
||||
},
|
||||
{
|
||||
"key": "Year",
|
||||
"value": "3"
|
||||
}
|
||||
],
|
||||
"content": [
|
||||
{
|
||||
"@type": "IMAGE",
|
||||
"url": "https://ipfs.raribleuserdata.com/ipfs/bafybeicsr36faeleunc5pzkqyf57pwm66vir4xhdkvv6cnkkznoyewqt7u/image.png",
|
||||
"representation": "ORIGINAL",
|
||||
"mimeType": "image/png",
|
||||
"size": 3742351,
|
||||
"available": true,
|
||||
"width": 2000,
|
||||
"height": 2000
|
||||
},
|
||||
{
|
||||
"@type": "IMAGE",
|
||||
"url": "https://lh3.googleusercontent.com/SimzYIBjaTFt3BTBXFGOOvAqfw_etV0Pbe2pen-IvwF7L8DOysNca7qBdj3Dt5n_HWsse5vDLD7FZ7o5XdEivRvBtUybI1mXZEBQ=s1000",
|
||||
"representation": "BIG",
|
||||
"mimeType": "image/png",
|
||||
"size": 988277,
|
||||
"available": true,
|
||||
"width": 1000,
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"@type": "IMAGE",
|
||||
"url": "https://lh3.googleusercontent.com/SimzYIBjaTFt3BTBXFGOOvAqfw_etV0Pbe2pen-IvwF7L8DOysNca7qBdj3Dt5n_HWsse5vDLD7FZ7o5XdEivRvBtUybI1mXZEBQ=k-w1200-s2400-rj",
|
||||
"representation": "PORTRAIT",
|
||||
"mimeType": "image/jpeg",
|
||||
"size": 224410,
|
||||
"available": true,
|
||||
"width": 1200,
|
||||
"height": 1200
|
||||
},
|
||||
{
|
||||
"@type": "IMAGE",
|
||||
"url": "https://lh3.googleusercontent.com/SimzYIBjaTFt3BTBXFGOOvAqfw_etV0Pbe2pen-IvwF7L8DOysNca7qBdj3Dt5n_HWsse5vDLD7FZ7o5XdEivRvBtUybI1mXZEBQ=s250",
|
||||
"representation": "PREVIEW",
|
||||
"mimeType": "image/png",
|
||||
"size": 68280,
|
||||
"available": true,
|
||||
"width": 250,
|
||||
"height": 250
|
||||
}
|
||||
]
|
||||
},
|
||||
"deleted": false,
|
||||
"originOrders": [],
|
||||
"ammOrders": {
|
||||
"ids": []
|
||||
},
|
||||
"auctions": [],
|
||||
"totalStock": "0",
|
||||
"sellers": 0,
|
||||
"suspicious": false
|
||||
}
|
||||
]
|
||||
}`
|
||||
Generated
Vendored
+359
@@ -0,0 +1,359 @@
|
||||
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.EthereumGoerli
|
||||
}
|
||||
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.EthereumGoerli:
|
||||
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 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 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 (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,
|
||||
}
|
||||
}
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
package thirdparty
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"math/rand"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/status-im/status-go/protocol/communities/token"
|
||||
"github.com/status-im/status-go/services/wallet/bigint"
|
||||
w_common "github.com/status-im/status-go/services/wallet/common"
|
||||
)
|
||||
|
||||
func generateContractType(seed int) w_common.ContractType {
|
||||
if seed%2 == 0 {
|
||||
return w_common.ContractTypeERC721
|
||||
}
|
||||
return w_common.ContractTypeERC1155
|
||||
}
|
||||
|
||||
func GenerateTestCollectiblesData(count int) (result []CollectibleData) {
|
||||
base := rand.Intn(100) // nolint: gosec
|
||||
|
||||
result = make([]CollectibleData, 0, count)
|
||||
for i := base; i < count+base; i++ {
|
||||
bigI := big.NewInt(int64(i))
|
||||
newCollectible := CollectibleData{
|
||||
ID: CollectibleUniqueID{
|
||||
ContractID: ContractID{
|
||||
ChainID: w_common.ChainID(i % 4),
|
||||
Address: common.BigToAddress(bigI),
|
||||
},
|
||||
TokenID: &bigint.BigInt{Int: bigI},
|
||||
},
|
||||
ContractType: generateContractType(i),
|
||||
Provider: fmt.Sprintf("provider-%d", i),
|
||||
Name: fmt.Sprintf("name-%d", i),
|
||||
Description: fmt.Sprintf("description-%d", i),
|
||||
Permalink: fmt.Sprintf("permalink-%d", i),
|
||||
ImageURL: fmt.Sprintf("imageurl-%d", i),
|
||||
ImagePayload: []byte(fmt.Sprintf("imagepayload-%d", i)),
|
||||
AnimationURL: fmt.Sprintf("animationurl-%d", i),
|
||||
AnimationMediaType: fmt.Sprintf("animationmediatype-%d", i),
|
||||
Traits: []CollectibleTrait{
|
||||
{
|
||||
TraitType: fmt.Sprintf("traittype-%d", i),
|
||||
Value: fmt.Sprintf("traitvalue-%d", i),
|
||||
DisplayType: fmt.Sprintf("displaytype-%d", i),
|
||||
MaxValue: fmt.Sprintf("maxvalue-%d", i),
|
||||
},
|
||||
{
|
||||
TraitType: fmt.Sprintf("traittype-%d", i),
|
||||
Value: fmt.Sprintf("traitvalue-%d", i),
|
||||
DisplayType: fmt.Sprintf("displaytype-%d", i),
|
||||
MaxValue: fmt.Sprintf("maxvalue-%d", i),
|
||||
},
|
||||
{
|
||||
TraitType: fmt.Sprintf("traittype-%d", i),
|
||||
Value: fmt.Sprintf("traitvalue-%d", i),
|
||||
DisplayType: fmt.Sprintf("displaytype-%d", i),
|
||||
MaxValue: fmt.Sprintf("maxvalue-%d", i),
|
||||
},
|
||||
},
|
||||
BackgroundColor: fmt.Sprintf("backgroundcolor-%d", i),
|
||||
TokenURI: fmt.Sprintf("tokenuri-%d", i),
|
||||
CommunityID: fmt.Sprintf("communityid-%d", i%5),
|
||||
}
|
||||
result = append(result, newCollectible)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func GenerateTestCollectiblesCommunityData(count int) []CollectibleCommunityInfo {
|
||||
base := rand.Intn(100) // nolint: gosec
|
||||
|
||||
result := make([]CollectibleCommunityInfo, 0, count)
|
||||
for i := base; i < count+base; i++ {
|
||||
newCommunityInfo := CollectibleCommunityInfo{
|
||||
PrivilegesLevel: token.PrivilegesLevel(i) % (token.CommunityLevel + 1),
|
||||
}
|
||||
result = append(result, newCommunityInfo)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func GenerateTestCollectiblesOwnership(count int) []AccountBalance {
|
||||
base := rand.Intn(100) // nolint: gosec
|
||||
|
||||
ret := make([]AccountBalance, 0, count)
|
||||
for i := base; i < count+base; i++ {
|
||||
ret = append(ret, AccountBalance{
|
||||
Address: common.HexToAddress(fmt.Sprintf("0x%x", i)),
|
||||
Balance: &bigint.BigInt{Int: big.NewInt(int64(i))},
|
||||
})
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func GenerateTestCollectionsData(count int) (result []CollectionData) {
|
||||
base := rand.Intn(100) // nolint: gosec
|
||||
|
||||
result = make([]CollectionData, 0, count)
|
||||
for i := base; i < count+base; i++ {
|
||||
bigI := big.NewInt(int64(count))
|
||||
traits := make(map[string]CollectionTrait)
|
||||
for j := 0; j < 3; j++ {
|
||||
traits[fmt.Sprintf("traittype-%d", j)] = CollectionTrait{
|
||||
Min: float64(i+j) / 2,
|
||||
Max: float64(i+j) * 2,
|
||||
}
|
||||
}
|
||||
|
||||
newCollection := CollectionData{
|
||||
ID: ContractID{
|
||||
ChainID: w_common.ChainID(i),
|
||||
Address: common.BigToAddress(bigI),
|
||||
},
|
||||
ContractType: generateContractType(i),
|
||||
Provider: fmt.Sprintf("provider-%d", i),
|
||||
Name: fmt.Sprintf("name-%d", i),
|
||||
Slug: fmt.Sprintf("slug-%d", i),
|
||||
ImageURL: fmt.Sprintf("imageurl-%d", i),
|
||||
ImagePayload: []byte(fmt.Sprintf("imagepayload-%d", i)),
|
||||
Traits: traits,
|
||||
CommunityID: fmt.Sprintf("community-%d", i),
|
||||
}
|
||||
result = append(result, newCollection)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func GenerateTestCommunityInfo(count int) map[string]CommunityInfo {
|
||||
base := rand.Intn(100) // nolint: gosec
|
||||
|
||||
result := make(map[string]CommunityInfo)
|
||||
for i := base; i < count+base; i++ {
|
||||
communityID := fmt.Sprintf("communityid-%d", i)
|
||||
newCommunity := CommunityInfo{
|
||||
CommunityName: fmt.Sprintf("communityname-%d", i),
|
||||
CommunityColor: fmt.Sprintf("communitycolor-%d", i),
|
||||
CommunityImage: fmt.Sprintf("communityimage-%d", i),
|
||||
CommunityImagePayload: []byte(fmt.Sprintf("communityimagepayload-%d", i)),
|
||||
}
|
||||
result[communityID] = newCommunity
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func GenerateTestFullCollectiblesData(count int) []FullCollectibleData {
|
||||
collectiblesData := GenerateTestCollectiblesData(count)
|
||||
collectionsData := GenerateTestCollectionsData(count)
|
||||
communityInfoMap := GenerateTestCommunityInfo(count)
|
||||
communityInfo := make([]CommunityInfo, 0, count)
|
||||
for _, info := range communityInfoMap {
|
||||
communityInfo = append(communityInfo, info)
|
||||
}
|
||||
communityData := GenerateTestCollectiblesCommunityData(count)
|
||||
|
||||
ret := make([]FullCollectibleData, 0, count)
|
||||
for i := 0; i < count; i++ {
|
||||
// Ensure consistent ContracType
|
||||
collectionsData[i].ContractType = collectiblesData[i].ContractType
|
||||
|
||||
ret = append(ret, FullCollectibleData{
|
||||
CollectibleData: collectiblesData[i],
|
||||
CollectionData: &collectionsData[i],
|
||||
CommunityInfo: &communityInfo[i],
|
||||
CollectibleCommunityInfo: &communityData[i],
|
||||
Ownership: GenerateTestCollectiblesOwnership(rand.Intn(5) + 1), // nolint: gosec
|
||||
})
|
||||
}
|
||||
return ret
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package thirdparty
|
||||
|
||||
type HistoricalPrice struct {
|
||||
Timestamp int64 `json:"time"`
|
||||
Value float64 `json:"close"`
|
||||
}
|
||||
|
||||
type TokenMarketValues struct {
|
||||
MKTCAP float64 `json:"MKTCAP"`
|
||||
HIGHDAY float64 `json:"HIGHDAY"`
|
||||
LOWDAY float64 `json:"LOWDAY"`
|
||||
CHANGEPCTHOUR float64 `json:"CHANGEPCTHOUR"`
|
||||
CHANGEPCTDAY float64 `json:"CHANGEPCTDAY"`
|
||||
CHANGEPCT24HOUR float64 `json:"CHANGEPCT24HOUR"`
|
||||
CHANGE24HOUR float64 `json:"CHANGE24HOUR"`
|
||||
}
|
||||
|
||||
type TokenDetails struct {
|
||||
ID string `json:"Id"`
|
||||
Name string `json:"Name"`
|
||||
Symbol string `json:"Symbol"`
|
||||
Description string `json:"Description"`
|
||||
TotalCoinsMined float64 `json:"TotalCoinsMined"`
|
||||
AssetLaunchDate string `json:"AssetLaunchDate"`
|
||||
AssetWhitepaperURL string `json:"AssetWhitepaperUrl"`
|
||||
AssetWebsiteURL string `json:"AssetWebsiteUrl"`
|
||||
BuiltOn string `json:"BuiltOn"`
|
||||
SmartContractAddress string `json:"SmartContractAddress"`
|
||||
}
|
||||
|
||||
type MarketDataProvider interface {
|
||||
FetchPrices(symbols []string, currencies []string) (map[string]map[string]float64, error)
|
||||
FetchHistoricalDailyPrices(symbol string, currency string, limit int, allData bool, aggregate int) ([]HistoricalPrice, error)
|
||||
FetchHistoricalHourlyPrices(symbol string, currency string, limit int, aggregate int) ([]HistoricalPrice, error)
|
||||
FetchTokenMarketValues(symbols []string, currency string) (map[string]TokenMarketValues, error)
|
||||
FetchTokenDetails(symbols []string) (map[string]TokenDetails, error)
|
||||
}
|
||||
|
||||
type DataParsed struct {
|
||||
Name string `json:"name"`
|
||||
ID string `json:"id"`
|
||||
Inputs map[string]string `json:"inputs"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
type DecoderProvider interface {
|
||||
Run(data string) (*DataParsed, error)
|
||||
}
|
||||
Generated
Vendored
+41
@@ -0,0 +1,41 @@
|
||||
package utils
|
||||
|
||||
import "strings"
|
||||
|
||||
var renameMapping = map[string]string{
|
||||
"STT": "SNT",
|
||||
}
|
||||
|
||||
func RenameSymbols(symbols []string) (renames []string) {
|
||||
for _, symbol := range symbols {
|
||||
renames = append(renames, GetRealSymbol(symbol))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func GetRealSymbol(symbol string) string {
|
||||
if val, ok := renameMapping[strings.ToUpper(symbol)]; ok {
|
||||
return val
|
||||
}
|
||||
return strings.ToUpper(symbol)
|
||||
}
|
||||
|
||||
func ChunkSymbols(symbols []string, chunkSizeOptional ...int) [][]string {
|
||||
var chunks [][]string
|
||||
chunkSize := 100
|
||||
if len(chunkSizeOptional) > 0 {
|
||||
chunkSize = chunkSizeOptional[0]
|
||||
}
|
||||
|
||||
for i := 0; i < len(symbols); i += chunkSize {
|
||||
end := i + chunkSize
|
||||
|
||||
if end > len(symbols) {
|
||||
end = len(symbols)
|
||||
}
|
||||
|
||||
chunks = append(chunks, symbols[i:end])
|
||||
}
|
||||
|
||||
return chunks
|
||||
}
|
||||
Reference in New Issue
Block a user