Skip to content

Commit

Permalink
feat: Add api and functionality to get collection website and twitter…
Browse files Browse the repository at this point in the history
… handle from Alchemy
  • Loading branch information
Khushboo-dev-cpp committed Apr 9, 2024
1 parent e2a4a22 commit f819a23
Show file tree
Hide file tree
Showing 15 changed files with 400 additions and 39 deletions.
9 changes: 9 additions & 0 deletions services/wallet/activity/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ func (m *mockCollectiblesManager) FetchAssetsByCollectibleUniqueID(ctx context.C
return res.([]thirdparty.FullCollectibleData), args.Error(1)
}

func (m *mockCollectiblesManager) FetchCollectibleSocialsAsync(ctx context.Context, uniqueID thirdparty.CollectibleUniqueID) error {
args := m.Called(uniqueID)
res := args.Get(0)
if res == nil {
return args.Error(1)
}
return args.Error(1)
}

// mockTokenManager implements the token.ManagerInterface
type mockTokenManager struct {
mock.Mock
Expand Down
7 changes: 7 additions & 0 deletions services/wallet/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,13 @@ func (api *API) GetCollectiblesByUniqueIDAsync(requestID int32, uniqueIDs []thir
return nil
}

func (api *API) FetchCollectibleSocialsAsync(requestID int32, uniqueID thirdparty.CollectibleUniqueID) error {
log.Debug("wallet.api.FetchCollectibleSocialsAsync", "requestID", requestID, "uniqueID", uniqueID)

api.s.collectibles.FetchCollectibleSocialsAsync(requestID, uniqueID)
return nil
}

func (api *API) GetCollectibleOwnersByContractAddress(ctx context.Context, chainID wcommon.ChainID, contractAddress common.Address) (*thirdparty.CollectibleContractOwnership, error) {
log.Debug("call to GetCollectibleOwnersByContractAddress")
return api.s.collectiblesManager.FetchCollectibleOwnersByContractAddress(ctx, chainID, contractAddress)
Expand Down
123 changes: 123 additions & 0 deletions services/wallet/collectibles/collection_data_db.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"

"github.com/status-im/status-go/services/wallet/thirdparty"
"github.com/status-im/status-go/services/wallet/thirdparty/alchemy"
"github.com/status-im/status-go/sqlite"
)

Expand All @@ -21,6 +22,8 @@ func NewCollectionDataDB(sqlDb *sql.DB) *CollectionDataDB {
const collectionDataColumns = "chain_id, contract_address, provider, name, slug, image_url, image_payload, community_id"
const collectionTraitsColumns = "chain_id, contract_address, trait_type, min, max"
const selectCollectionTraitsColumns = "trait_type, min, max"
const collectionSocialsColumns = "chain_id, contract_address, website, twitter_handle"
const selectCollectionSocialsColumns = "website, twitter_handle"

func rowsToCollectionTraits(rows *sql.Rows) (map[string]thirdparty.CollectionTrait, error) {
traits := make(map[string]thirdparty.CollectionTrait)
Expand Down Expand Up @@ -130,6 +133,13 @@ func setCollectionsData(creator sqlite.StatementCreator, collections []thirdpart
if err != nil {
return err
}

if c.Provider == alchemy.AlchemyID {
err = upsertCollectionSocials(creator, c.ID, c.Socials)
if err != nil {
return err
}
}
}
}

Expand Down Expand Up @@ -246,8 +256,121 @@ func (o *CollectionDataDB) GetData(ids []thirdparty.ContractID) (map[string]thir
return nil, err
}

// Get socials from different table
c.Socials, err = getCollectionSocials(o.db, c.ID)
if err != nil {
return nil, err
}

ret[c.ID.HashKey()] = *c
}
}
return ret, nil
}

func (o *CollectionDataDB) GetSocialsValidForID(id thirdparty.CollectibleUniqueID) bool {
exists, err := o.db.Prepare(`SELECT EXISTS (
SELECT 1 FROM collection_socials_cache
WHERE chain_id=? AND contract_address=?
)`)
if err != nil {
return false
}

row := exists.QueryRow(
id.ContractID.ChainID,
id.ContractID.Address,
)
var found bool
err = row.Scan(&found)
if err != nil {
return false
}

return found
}

func (o *CollectionDataDB) SetCollectionSocialsData(id thirdparty.ContractID, collectionSocials thirdparty.CollectionSocials) (err error) {
tx, err := o.db.Begin()
if err != nil {
return err
}
defer func() {
if err == nil {
err = tx.Commit()
return
}
_ = tx.Rollback()
}()

// Insert new collections socials
err = upsertCollectionSocials(tx, id, collectionSocials)
if err != nil {
return err
}

return
}

func rowsToCollectionSocials(rows *sql.Rows) (thirdparty.CollectionSocials, error) {
socials := thirdparty.CollectionSocials{}
for rows.Next() {
var website string
var twitterHandle string
err := rows.Scan(
&website,
&twitterHandle,
)
if err != nil {
return socials, err
}
if len(website) > 0 {
socials.Website = website
}
if len(twitterHandle) > 0 {
socials.TwitterHandle = twitterHandle
}
}
return socials, nil
}

func getCollectionSocials(creator sqlite.StatementCreator, id thirdparty.ContractID) (thirdparty.CollectionSocials, error) {
// Get socials
selectSocials, err := creator.Prepare(fmt.Sprintf(`SELECT %s
FROM collection_socials_cache
WHERE chain_id = ? AND contract_address = ?`, selectCollectionSocialsColumns))
if err != nil {
return thirdparty.CollectionSocials{}, err
}

rows, err := selectSocials.Query(
id.ChainID,
id.Address,
)
if err != nil {
return thirdparty.CollectionSocials{}, err
}

return rowsToCollectionSocials(rows)
}

func upsertCollectionSocials(creator sqlite.StatementCreator, id thirdparty.ContractID, socials thirdparty.CollectionSocials) error {
// Insert socials
insertSocial, err := creator.Prepare(fmt.Sprintf(`INSERT OR REPLACE INTO collection_socials_cache (%s)
VALUES (?, ?, ?, ?)`, collectionSocialsColumns))
if err != nil {
return err
}

_, err = insertSocial.Exec(
id.ChainID,
id.Address,
socials.Website,
socials.TwitterHandle,
)
if err != nil {
return err
}

return nil
}
92 changes: 92 additions & 0 deletions services/wallet/collectibles/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/status-im/status-go/services/wallet/community"
"github.com/status-im/status-go/services/wallet/connection"
"github.com/status-im/status-go/services/wallet/thirdparty"
"github.com/status-im/status-go/services/wallet/thirdparty/alchemy"
"github.com/status-im/status-go/services/wallet/walletevent"
)

Expand All @@ -48,6 +49,7 @@ var (

type ManagerInterface interface {
FetchAssetsByCollectibleUniqueID(ctx context.Context, uniqueIDs []thirdparty.CollectibleUniqueID, asyncFetch bool) ([]thirdparty.FullCollectibleData, error)
FetchCollectibleSocialsAsync(ctx context.Context, uniqueID thirdparty.CollectibleUniqueID) error
}

type Manager struct {
Expand Down Expand Up @@ -1036,3 +1038,93 @@ func (o *Manager) SearchCollections(ctx context.Context, chainID walletCommon.Ch
}
return nil, ErrNoProvidersAvailableForChainID
}

func (o *Manager) FetchCollectibleSocialsAsync(ctx context.Context, uniqueID thirdparty.CollectibleUniqueID) error {
hasValidSocials := o.collectionsDataDB.GetSocialsValidForID(uniqueID)

if !hasValidSocials {
// Atomic group stores the error from the first failed command and stops other commands on error
group := async.NewAtomicGroup(ctx)
group.Add(func(ctx context.Context) error {
defer o.checkConnectionStatus(uniqueID.ContractID.ChainID)

socials, err := o.fetchSocialsForCollectibleUniqueID(ctx, uniqueID.ContractID.ChainID, uniqueID)
if err != nil {
log.Debug("FetchCollectibleSocials failed for", "chainID", uniqueID.ContractID.ChainID, "uniqueID", uniqueID, "err", err)
return err
}

socialsMessage := CollectibleSocialsMessage{
ID: uniqueID,
Socials: socials,
}

err = o.collectionsDataDB.SetCollectionSocialsData(uniqueID.ContractID, socials)
if err != nil {
log.Error("Error saving socials to DB: %v", err)
return nil
}

payload, err := json.Marshal(socialsMessage)
if err != nil {
log.Error("Error marshaling response: %v", err)
return nil
}

event := walletevent.Event{
Type: EventGetCollectibleSocialsDone,
Message: string(payload),
}

o.feed.Send(event)
return err
})

group.Wait()
return group.Error()
}
return nil
}

func (o *Manager) fetchSocialsForCollectibleUniqueID(ctx context.Context, chainID walletCommon.ChainID, idToFetch thirdparty.CollectibleUniqueID) (thirdparty.CollectionSocials, error) {
socials := thirdparty.CollectionSocials{}
providerFound := false
cmdRes := circuitbreaker.CommandResult{}
for _, provider := range o.providers.CollectibleDataProviders {
if provider.ID() == alchemy.AlchemyID {
cmd := circuitbreaker.Command{}
if !provider.IsChainSupported(chainID) {
continue
}

// Some provider was found for the chainID
providerFound = true

provider := provider
cmd.Add(circuitbreaker.NewFunctor(func() ([]any, error) {
socials, err := provider.FetchCollectibleSocialsByUniqueID(ctx, idToFetch)
return []any{socials}, err
}))

cmdRes = o.getCircuitBreaker(chainID).Execute(cmd)

if len(cmdRes.Result()) > 0 {
// Only update website if non empty website is found
if len(cmdRes.Result()[0].(string)) > 0 {
socials.Website = cmdRes.Result()[0].(string)
}
// Only update twitterHandle if non empty twitterHandle is found
if len(cmdRes.Result()[1].(string)) > 0 {
socials.TwitterHandle = cmdRes.Result()[1].(string)
}
}
break
}
}

if !providerFound {
return thirdparty.CollectionSocials{}, ErrNoProvidersAvailableForChainID // lets not stop the group if no providers are available for the chain
}

return socials, cmdRes.Error()
}
16 changes: 16 additions & 0 deletions services/wallet/collectibles/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const (

EventOwnedCollectiblesFilteringDone walletevent.EventType = "wallet-owned-collectibles-filtering-done"
EventGetCollectiblesDetailsDone walletevent.EventType = "wallet-get-collectibles-details-done"
EventGetCollectibleSocialsDone walletevent.EventType = "wallet-get-collectible-socials-done"
)

type OwnershipUpdateMessage struct {
Expand All @@ -43,6 +44,11 @@ type OwnershipUpdateMessage struct {
Removed []thirdparty.CollectibleUniqueID `json:"removed"`
}

type CollectibleSocialsMessage struct {
ID thirdparty.CollectibleUniqueID `json:"id"`
Socials thirdparty.CollectionSocials `json:"socials"`
}

type CollectibleDataType byte

const (
Expand Down Expand Up @@ -80,6 +86,10 @@ var (
ID: 2,
Policy: async.ReplacementPolicyCancelOld,
}
fetchCollectibleSocialsTask = async.TaskType{
ID: 3,
Policy: async.ReplacementPolicyCancelOld,
}
)

type Service struct {
Expand Down Expand Up @@ -547,3 +557,9 @@ func (s *Service) notifyCommunityCollectiblesReceived(ownedCollectibles OwnedCol
Message: string(encodedMessage),
})
}

func (s *Service) FetchCollectibleSocialsAsync(requestID int32, uniqueID thirdparty.CollectibleUniqueID) {
s.scheduler.Enqueue(requestID, fetchCollectibleSocialsTask, func(ctx context.Context) (interface{}, error) {
return "", s.manager.FetchCollectibleSocialsAsync(ctx, uniqueID)
}, func(result interface{}, taskType async.TaskType, err error) {})
}
20 changes: 17 additions & 3 deletions services/wallet/collectibles/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,15 @@ type CollectibleData struct {
}

type CollectionData struct {
Name string `json:"name"`
Slug string `json:"slug"`
ImageURL string `json:"image_url"`
Name string `json:"name"`
Slug string `json:"slug"`
ImageURL string `json:"image_url"`
Socials CollectionSocials `json:"socials"`
}

type CollectionSocials struct {
Website string `json:"website"`
TwitterHandle string `json:"twitter_handle"`
}

type CommunityData struct {
Expand Down Expand Up @@ -94,6 +100,10 @@ func fullCollectibleDataToHeader(c thirdparty.FullCollectibleData) Collectible {
Slug: c.CollectionData.Slug,
ImageURL: c.CollectionData.ImageURL,
}
ret.CollectionData.Socials = CollectionSocials{
Website: c.CollectionData.Socials.Website,
TwitterHandle: c.CollectionData.Socials.TwitterHandle,
}
}
if c.CollectibleData.CommunityID != "" {
communityData := communityInfoToData(c.CollectibleData.CommunityID, c.CommunityInfo, c.CollectibleCommunityInfo)
Expand Down Expand Up @@ -136,6 +146,10 @@ func fullCollectibleDataToDetails(c thirdparty.FullCollectibleData) Collectible
Slug: c.CollectionData.Slug,
ImageURL: c.CollectionData.ImageURL,
}
ret.CollectionData.Socials = CollectionSocials{
Website: c.CollectionData.Socials.Website,
TwitterHandle: c.CollectionData.Socials.TwitterHandle,
}
}
if c.CollectibleData.CommunityID != "" {
communityData := communityInfoToData(c.CollectibleData.CommunityID, c.CommunityInfo, c.CollectibleCommunityInfo)
Expand Down
11 changes: 11 additions & 0 deletions services/wallet/thirdparty/alchemy/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,17 @@ func (o *Client) FetchAssetsByCollectibleUniqueID(ctx context.Context, uniqueIDs
return ret, nil
}

func (o *Client) FetchCollectibleSocialsByUniqueID(ctx context.Context, uniqueID thirdparty.CollectibleUniqueID) (thirdparty.CollectionSocials, error) {
resp, err := o.FetchCollectionsDataByContractID(ctx, []thirdparty.ContractID{uniqueID.ContractID})
if err != nil {
return thirdparty.CollectionSocials{}, err
}
if len(resp) > 0 {
return resp[0].Socials, nil
}
return thirdparty.CollectionSocials{}, nil
}

func getContractAddressBatches(ids []thirdparty.ContractID) []BatchContractAddresses {
batches := make([]BatchContractAddresses, 0)

Expand Down

0 comments on commit f819a23

Please sign in to comment.