Skip to content

Commit

Permalink
feat(api)_: add api and functionality to get collection website and t…
Browse files Browse the repository at this point in the history
…witter handle from alchemy
  • Loading branch information
Khushboo-dev-cpp committed May 3, 2024
1 parent 8a0b7dd commit 7d189f5
Show file tree
Hide file tree
Showing 18 changed files with 418 additions and 50 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 @@ -42,6 +42,15 @@ func (m *mockCollectiblesManager) FetchAssetsByCollectibleUniqueID(ctx context.C
return res.([]thirdparty.FullCollectibleData), args.Error(1)
}

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

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

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

return api.s.collectiblesManager.FetchCollectionSocialsAsync(uniqueID)
}

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
129 changes: 129 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, provider, 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 @@ -98,6 +100,28 @@ func upsertCollectionTraits(creator sqlite.StatementCreator, id thirdparty.Contr
return nil
}

func hasSocialsForID(creator sqlite.StatementCreator, contractID thirdparty.ContractID) (bool, error) {
exists, err := creator.Prepare(`SELECT EXISTS (
SELECT 1 FROM collection_socials_cache
WHERE chain_id=? AND contract_address=?
)`)
if err != nil {
return false, err
}

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

return found, nil
}

func setCollectionsData(creator sqlite.StatementCreator, collections []thirdparty.CollectionData, allowUpdate bool) error {
insertCollection, err := creator.Prepare(fmt.Sprintf(`%s INTO collection_data_cache (%s)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, insertStatement(allowUpdate), collectionDataColumns))
Expand Down Expand Up @@ -130,6 +154,13 @@ func setCollectionsData(creator sqlite.StatementCreator, collections []thirdpart
if err != nil {
return err
}

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

Expand Down Expand Up @@ -246,8 +277,106 @@ 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) HasValidSocialsForID(contractID thirdparty.ContractID) (bool, error) {
return hasSocialsForID(o.db, contractID)
}

func (o *CollectionDataDB) SetCollectionSocialsData(provider string, 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
hasSocials, err := hasSocialsForID(tx, id)
if err != nil {
return err
}

if !hasSocials && collectionSocials != nil {
err = upsertCollectionSocials(tx, provider, id, collectionSocials)
if err != nil {
return err
}
}

return
}

func rowsToCollectionSocials(rows *sql.Rows) (*thirdparty.CollectionSocials, error) {
var website string
var twitterHandle string
for rows.Next() {
err := rows.Scan(
&website,
&twitterHandle,
)
if err != nil {
return nil, err
}
}
return &thirdparty.CollectionSocials{
Website: website,
TwitterHandle: twitterHandle}, 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 nil, err
}

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

return rowsToCollectionSocials(rows)
}

func upsertCollectionSocials(creator sqlite.StatementCreator, provider string, 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,
provider,
socials.Website,
socials.TwitterHandle,
)
if err != nil {
return err
}

return nil
}
46 changes: 46 additions & 0 deletions services/wallet/collectibles/collection_data_db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,49 @@ func TestUpdateCollectionsData(t *testing.T) {
require.Equal(t, c0, loadedMap[c0.ID.HashKey()])
require.Equal(t, c1, loadedMap[c1.ID.HashKey()])
}

func TestCollectionSocialsData(t *testing.T) {
db, cleanDB := setupCollectionDataDBTest(t)
defer cleanDB()

data := thirdparty.GenerateTestCollectionsData(10)

ids := make([]thirdparty.ContractID, 0, len(data))
for _, collection := range data {
ids = append(ids, collection.ID)
}

err := db.SetData(data, true)
require.NoError(t, err)

// Check for loaded data
loadedMap, err := db.GetData(ids)
require.NoError(t, err)
require.Equal(t, len(data), len(loadedMap))

// Valid check for ID should return false as it was not set initially
valid, err := db.HasValidSocialsForID(data[0].ID)
require.NoError(t, err)
require.False(t, valid)

// Now we'll try to set socials data for the first item
socials := &thirdparty.CollectionSocials{
Website: "new-website",
TwitterHandle: "newTwitterHandle",
}
err = db.SetCollectionSocialsData("alchemy", data[0].ID, socials)
require.NoError(t, err)

// Valid check for ID should return true as it was now set
valid, err = db.HasValidSocialsForID(data[0].ID)
require.NoError(t, err)
require.True(t, valid)

// Check the loaded data again for socials
loadedMap, err = db.GetData(ids)
require.NoError(t, err)
require.Equal(t, len(data), len(loadedMap))

require.Equal(t, socials.Website, loadedMap[data[0].ID.HashKey()].Socials.Website)
require.Equal(t, socials.TwitterHandle, loadedMap[data[0].ID.HashKey()].Socials.TwitterHandle)
}
76 changes: 76 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)
FetchCollectionSocialsAsync(uniqueID thirdparty.CollectibleUniqueID) error
}

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

func (o *Manager) FetchCollectionSocialsAsync(uniqueID thirdparty.CollectibleUniqueID) error {
hasValidSocials, err := o.collectionsDataDB.HasValidSocialsForID(uniqueID.ContractID)
if err != nil {
log.Debug("FetchCollectionSocialsAsync failed for", "chainID", uniqueID.ContractID.ChainID, "uniqueID", uniqueID, "err", err)
return err
}

if !hasValidSocials {
go func() {
defer o.checkConnectionStatus(uniqueID.ContractID.ChainID)

provider, socials, err := o.fetchSocialsForCollectibleUniqueID(context.Background(), uniqueID.ContractID.ChainID, uniqueID)
if err != nil || socials == nil {
log.Debug("FetchCollectionSocialsAsync failed for", "chainID", uniqueID.ContractID.ChainID, "uniqueID", uniqueID, "err", err)
return
}

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

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

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

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

o.feed.Send(event)
}()

}
return nil
}

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

provider := provider
cmd.Add(circuitbreaker.NewFunctor(func() ([]interface{}, error) {
socials, err := provider.FetchCollectibleSocialsByUniqueID(ctx, idToFetch)
if err != nil {
log.Error("FetchCollectibleSocialsByUniqueID failed for", "provider", provider.ID(), "chainID", chainID, "err", err)
}
return []interface{}{provider.ID(), socials}, err
}))
}

if cmd.IsEmpty() {
return "", nil, ErrNoProvidersAvailableForChainID // lets not stop the group if no providers are available for the chain
}

cmdRes := o.getCircuitBreaker(chainID).Execute(cmd)
if cmdRes.Error() != nil {
log.Error("fetchSocialsForCollectibleUniqueID failed for", "chainID", chainID, "err", cmdRes.Error())
return "", nil, cmdRes.Error()
}
return cmdRes.Result()[0].(string), cmdRes.Result()[1].(*thirdparty.CollectionSocials), cmdRes.Error()
}
6 changes: 6 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

0 comments on commit 7d189f5

Please sign in to comment.