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 884a940
Show file tree
Hide file tree
Showing 14 changed files with 387 additions and 39 deletions.
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
119 changes: 119 additions & 0 deletions services/wallet/collectibles/collection_data_db.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,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 +132,13 @@ func setCollectionsData(creator sqlite.StatementCreator, collections []thirdpart
if err != nil {
return err
}

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

Expand Down Expand Up @@ -246,8 +255,118 @@ 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))

_, 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 @@ -48,6 +48,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 +1037,94 @@ 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" {
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
13 changes: 12 additions & 1 deletion services/wallet/thirdparty/alchemy/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,9 @@ func (r *Raw) UnmarshalJSON(b []byte) error {
}

type OpenSeaMetadata struct {
ImageURL string `json:"imageUrl"`
ImageURL string `json:"imageUrl"`
TwitterUsername string `json:"twitterUsername"`
ExternalUrl string `json:"externalUrl"`
}

type Contract struct {
Expand Down Expand Up @@ -184,6 +186,14 @@ func alchemyToContractType(tokenType string) walletCommon.ContractType {
}
}

func (c *Contract) toCollectionSocials() thirdparty.CollectionSocials {
ret := thirdparty.CollectionSocials{
Website: c.OpenSeaMetadata.ExternalUrl,
TwitterHandle: c.OpenSeaMetadata.TwitterUsername,
}
return ret
}

func (c *Contract) toCollectionData(id thirdparty.ContractID) thirdparty.CollectionData {
ret := thirdparty.CollectionData{
ID: id,
Expand All @@ -192,6 +202,7 @@ func (c *Contract) toCollectionData(id thirdparty.ContractID) thirdparty.Collect
Name: c.Name,
ImageURL: c.OpenSeaMetadata.ImageURL,
Traits: make(map[string]thirdparty.CollectionTrait, 0),
Socials: c.toCollectionSocials(),
}
return ret
}
Expand Down

0 comments on commit 884a940

Please sign in to comment.