feat: implemented multi chain collectible ownership provider

This commit is contained in:
Dario Gabriel Lipicar 2023-07-13 14:26:17 -03:00 committed by dlipicar
parent 0919a87588
commit 1f379aec1f
6 changed files with 181 additions and 77 deletions

View File

@ -27,7 +27,6 @@ import (
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/event"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
@ -41,7 +40,7 @@ import (
"github.com/status-im/status-go/protocol/transport"
"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/opensea"
"github.com/status-im/status-go/services/wallet/thirdparty"
"github.com/status-im/status-go/services/wallet/token"
"github.com/status-im/status-go/signal"
)
@ -71,11 +70,11 @@ type Manager struct {
identity *ecdsa.PrivateKey
accountsManager account.Manager
tokenManager TokenManager
collectiblesManager CollectiblesManager
logger *zap.Logger
stdoutLogger *zap.Logger
transport *transport.Transport
quit chan struct{}
openseaClientBuilder openseaClientBuilder
torrentConfig *params.TorrentConfig
torrentClient *torrent.Client
walletConfig *params.WalletConfig
@ -87,21 +86,6 @@ type Manager struct {
stopped bool
}
type openseaClient interface {
FetchAllAssetsByOwnerAndContractAddress(owner gethcommon.Address, contractAddresses []gethcommon.Address, cursor string, limit int) (*opensea.AssetContainer, error)
}
type openseaClientBuilder interface {
NewOpenseaClient(uint64, string, *event.Feed) (openseaClient, error)
}
type defaultOpenseaBuilder struct {
}
func (b *defaultOpenseaBuilder) NewOpenseaClient(chainID uint64, apiKey string, feed *event.Feed) (openseaClient, error) {
return opensea.NewOpenseaClient(chainID, apiKey, nil)
}
type HistoryArchiveDownloadTask struct {
CancelChan chan struct{}
Waiter sync.WaitGroup
@ -123,10 +107,10 @@ func (t *HistoryArchiveDownloadTask) Cancel() {
}
type managerOptions struct {
accountsManager account.Manager
tokenManager TokenManager
walletConfig *params.WalletConfig
openseaClientBuilder openseaClientBuilder
accountsManager account.Manager
tokenManager TokenManager
collectiblesManager CollectiblesManager
walletConfig *params.WalletConfig
}
type TokenManager interface {
@ -157,6 +141,10 @@ func (m *DefaultTokenManager) GetAllChainIDs() ([]uint64, error) {
return chainIDs, nil
}
type CollectiblesManager interface {
FetchBalancesByOwnerAndContractAddress(chainID uint64, ownerAddress gethcommon.Address, contractAddresses []gethcommon.Address) (thirdparty.TokenBalancesPerContractAddress, error)
}
func (m *DefaultTokenManager) GetBalancesByChain(ctx context.Context, accounts, tokenAddresses []gethcommon.Address, chainIDs []uint64) (BalancesByChain, error) {
clients, err := m.tokenManager.RPCClient.EthClients(chainIDs)
if err != nil {
@ -175,9 +163,9 @@ func WithAccountManager(accountsManager account.Manager) ManagerOption {
}
}
func WithOpenseaClientBuilder(builder openseaClientBuilder) ManagerOption {
func WithCollectiblesManager(collectiblesManager CollectiblesManager) ManagerOption {
return func(opts *managerOptions) {
opts.openseaClientBuilder = builder
opts.collectiblesManager = collectiblesManager
}
}
@ -235,6 +223,10 @@ func NewManager(identity *ecdsa.PrivateKey, db *sql.DB, encryptor *encryption.Pr
manager.accountsManager = managerConfig.accountsManager
}
if managerConfig.collectiblesManager != nil {
manager.collectiblesManager = managerConfig.collectiblesManager
}
if managerConfig.tokenManager != nil {
manager.tokenManager = managerConfig.tokenManager
}
@ -243,12 +235,6 @@ func NewManager(identity *ecdsa.PrivateKey, db *sql.DB, encryptor *encryption.Pr
manager.walletConfig = managerConfig.walletConfig
}
if managerConfig.openseaClientBuilder != nil {
manager.openseaClientBuilder = managerConfig.openseaClientBuilder
} else {
manager.openseaClientBuilder = &defaultOpenseaBuilder{}
}
if verifier != nil {
sub := verifier.Subscribe()
@ -2002,8 +1988,9 @@ func (m *Manager) checkPermissions(permissions []*protobuf.CommunityTokenPermiss
continue
}
if _, exists := ownedERC721Tokens[chainID][account][strings.ToLower(address)]; exists {
tokenBalances := ownedERC721Tokens[chainID][account][gethcommon.HexToAddress(address)]
if len(tokenBalances) > 0 {
// 'account' owns some TokenID owned from contract 'address'
if _, exists := accountsChainIDsCombinations[account]; !exists {
accountsChainIDsCombinations[account] = make(map[uint64]bool)
}
@ -2019,8 +2006,8 @@ func (m *Manager) checkPermissions(permissions []*protobuf.CommunityTokenPermiss
for _, tokenID := range tokenRequirement.TokenIds {
tokenIDBigInt := new(big.Int).SetUint64(tokenID)
for _, asset := range ownedERC721Tokens[chainID][account][strings.ToLower(address)] {
if asset.TokenID.Cmp(tokenIDBigInt) == 0 {
for _, asset := range tokenBalances {
if asset.TokenID.Cmp(tokenIDBigInt) == 0 && asset.Balance.Sign() > 0 {
tokenRequirementMet = true
accountsChainIDsCombinations[account][chainID] = true
break tokenIDsLoop
@ -2143,15 +2130,14 @@ func (m *Manager) checkPermissions(permissions []*protobuf.CommunityTokenPermiss
return response, nil
}
type CollectiblesByChain = map[uint64]map[gethcommon.Address]map[string][]opensea.Asset
type CollectiblesByChain = map[uint64]map[gethcommon.Address]thirdparty.TokenBalancesPerContractAddress
func (m *Manager) GetOwnedERC721Tokens(walletAddresses []gethcommon.Address, tokenRequirements map[uint64]map[string]*protobuf.TokenCriteria, chainIDs []uint64) (CollectiblesByChain, error) {
if m.walletConfig == nil || m.walletConfig.OpenseaAPIKey == "" {
return nil, errors.New("no opensea client")
if m.collectiblesManager == nil {
return nil, errors.New("no collectibles manager")
}
ownedERC721Tokens := make(map[uint64]map[gethcommon.Address]map[string][]opensea.Asset)
ownedERC721Tokens := make(CollectiblesByChain)
for chainID, erc721Tokens := range tokenRequirements {
@ -2166,41 +2152,22 @@ func (m *Manager) GetOwnedERC721Tokens(walletAddresses []gethcommon.Address, tok
continue
}
client, err := m.openseaClientBuilder.NewOpenseaClient(chainID, m.walletConfig.OpenseaAPIKey, nil)
if err != nil {
return nil, err
}
contractAddresses := make([]gethcommon.Address, 0)
for contractAddress := range erc721Tokens {
contractAddresses = append(contractAddresses, gethcommon.HexToAddress(contractAddress))
}
if _, exists := ownedERC721Tokens[chainID]; !exists {
ownedERC721Tokens[chainID] = make(map[gethcommon.Address]map[string][]opensea.Asset)
ownedERC721Tokens[chainID] = make(map[gethcommon.Address]thirdparty.TokenBalancesPerContractAddress)
}
for _, owner := range walletAddresses {
assets, err := client.FetchAllAssetsByOwnerAndContractAddress(owner, contractAddresses, "", 5)
balances, err := m.collectiblesManager.FetchBalancesByOwnerAndContractAddress(chainID, owner, contractAddresses)
if err != nil {
m.logger.Info("couldn't fetch owner assets", zap.Error(err))
return nil, err
}
if len(assets.Assets) == 0 {
continue
}
if _, exists := ownedERC721Tokens[chainID][owner]; !exists {
ownedERC721Tokens[chainID][owner] = make(map[string][]opensea.Asset, 0)
}
for _, asset := range assets.Assets {
if _, exists := ownedERC721Tokens[chainID][owner][asset.Contract.Address]; !exists {
ownedERC721Tokens[chainID][owner][asset.Contract.Address] = make([]opensea.Asset, 0)
}
ownedERC721Tokens[chainID][owner][asset.Contract.Address] = append(ownedERC721Tokens[chainID][owner][asset.Contract.Address], asset)
}
ownedERC721Tokens[chainID][owner] = balances
}
}
return ownedERC721Tokens, nil

View File

@ -14,14 +14,14 @@ import (
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/event"
"github.com/status-im/status-go/appdatabase"
"github.com/status-im/status-go/eth-node/types"
userimages "github.com/status-im/status-go/images"
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/protocol/requests"
"github.com/status-im/status-go/protocol/transport"
"github.com/status-im/status-go/services/wallet/thirdparty/opensea"
"github.com/status-im/status-go/services/wallet/bigint"
"github.com/status-im/status-go/services/wallet/thirdparty"
"github.com/golang/protobuf/proto"
_ "github.com/mutecomm/go-sqlcipher/v4" // require go-sqlcipher that overrides default implementation
@ -63,6 +63,17 @@ func intToBig(n int64) *hexutil.Big {
return (*hexutil.Big)(big.NewInt(n))
}
func uintToDecBig(n uint64) *bigint.BigInt {
return &bigint.BigInt{Int: big.NewInt(int64(n))}
}
func tokenBalance(tokenID uint64, balance uint64) thirdparty.TokenBalance {
return thirdparty.TokenBalance{
TokenID: uintToDecBig(tokenID),
Balance: uintToDecBig(balance),
}
}
func (s *ManagerSuite) getHistoryTasksCount() int {
// sync.Map doesn't have a Len function, so we need to count manually
count := 0
@ -73,11 +84,26 @@ func (s *ManagerSuite) getHistoryTasksCount() int {
return count
}
type openseaClientTestBuilder struct {
type testCollectiblesManager struct {
response map[uint64]map[gethcommon.Address]thirdparty.TokenBalancesPerContractAddress
}
func (b *openseaClientTestBuilder) NewOpenseaClient(chainID uint64, apiKey string, feed *event.Feed) (openseaClient, error) {
return opensea.NewOpenseaClient(chainID, apiKey, nil)
func (m *testCollectiblesManager) setResponse(chainID uint64, walletAddress gethcommon.Address, contractAddress gethcommon.Address, balances []thirdparty.TokenBalance) {
if m.response == nil {
m.response = make(map[uint64]map[gethcommon.Address]thirdparty.TokenBalancesPerContractAddress)
}
if m.response[chainID] == nil {
m.response[chainID] = make(map[gethcommon.Address]thirdparty.TokenBalancesPerContractAddress)
}
if m.response[chainID][walletAddress] == nil {
m.response[chainID][walletAddress] = make(thirdparty.TokenBalancesPerContractAddress)
}
m.response[chainID][walletAddress][contractAddress] = balances
}
func (m *testCollectiblesManager) FetchBalancesByOwnerAndContractAddress(chainID uint64, ownerAddress gethcommon.Address, contractAddresses []gethcommon.Address) (thirdparty.TokenBalancesPerContractAddress, error) {
return m.response[chainID][ownerAddress], nil
}
type testTokenManager struct {
@ -110,7 +136,7 @@ func (m *testTokenManager) GetBalancesByChain(ctx context.Context, accounts, tok
return m.response, nil
}
func (s *ManagerSuite) setupManagerForTokenPermissions() (*Manager, *testTokenManager) {
func (s *ManagerSuite) setupManagerForTokenPermissions() (*Manager, *testCollectiblesManager, *testTokenManager) {
db, err := appdatabase.InitializeDB(sqlite.InMemoryPath, "", sqlite.ReducedKDFIterationsNumber)
s.NoError(err, "creating sqlite db instance")
err = sqlite.Migrate(db)
@ -120,13 +146,14 @@ func (s *ManagerSuite) setupManagerForTokenPermissions() (*Manager, *testTokenMa
s.Require().NoError(err)
s.Require().NoError(err)
cm := &testCollectiblesManager{}
tm := &testTokenManager{}
options := []ManagerOption{
WithWalletConfig(&params.WalletConfig{
OpenseaAPIKey: "some-key",
}),
WithOpenseaClientBuilder(&openseaClientTestBuilder{}),
WithCollectiblesManager(cm),
WithTokenManager(tm),
}
@ -134,11 +161,11 @@ func (s *ManagerSuite) setupManagerForTokenPermissions() (*Manager, *testTokenMa
s.Require().NoError(err)
s.Require().NoError(m.Start())
return m, tm
return m, cm, tm
}
func (s *ManagerSuite) TestRetrieveTokens() {
m, tm := s.setupManagerForTokenPermissions()
m, _, tm := s.setupManagerForTokenPermissions()
var chainID uint64 = 5
contractAddresses := make(map[uint64]string)
@ -185,6 +212,56 @@ func (s *ManagerSuite) TestRetrieveTokens() {
s.Require().False(resp.Satisfied)
}
func (s *ManagerSuite) TestRetrieveCollectibles() {
m, cm, _ := s.setupManagerForTokenPermissions()
var chainID uint64 = 5
contractAddresses := make(map[uint64]string)
contractAddresses[chainID] = "0x3d6afaa395c31fcd391fe3d562e75fe9e8ec7e6a"
tokenID := uint64(10)
var tokenBalances []thirdparty.TokenBalance
var tokenCriteria = []*protobuf.TokenCriteria{
&protobuf.TokenCriteria{
ContractAddresses: contractAddresses,
TokenIds: []uint64{tokenID},
Type: protobuf.CommunityTokenType_ERC721,
},
}
var permissions = []*protobuf.CommunityTokenPermission{
&protobuf.CommunityTokenPermission{
Id: "some-id",
Type: protobuf.CommunityTokenPermission_BECOME_MEMBER,
TokenCriteria: tokenCriteria,
},
}
accountChainIDsCombination := []*AccountChainIDsCombination{
&AccountChainIDsCombination{
Address: gethcommon.HexToAddress("0xD6b912e09E797D291E8D0eA3D3D17F8000e01c32"),
ChainIDs: []uint64{chainID},
},
}
// Set response to exactly the right one
tokenBalances = []thirdparty.TokenBalance{tokenBalance(tokenID, 1)}
cm.setResponse(chainID, accountChainIDsCombination[0].Address, gethcommon.HexToAddress(contractAddresses[chainID]), tokenBalances)
resp, err := m.checkPermissionToJoin(permissions, accountChainIDsCombination, false)
s.Require().NoError(err)
s.Require().NotNil(resp)
s.Require().True(resp.Satisfied)
// Set balances to 0
tokenBalances = []thirdparty.TokenBalance{}
cm.setResponse(chainID, accountChainIDsCombination[0].Address, gethcommon.HexToAddress(contractAddresses[chainID]), tokenBalances)
resp, err = m.checkPermissionToJoin(permissions, accountChainIDsCombination, false)
s.Require().NoError(err)
s.Require().NotNil(resp)
s.Require().False(resp.Satisfied)
}
func (s *ManagerSuite) TestCreateCommunity() {
request := &requests.CreateCommunity{
@ -812,7 +889,7 @@ func (s *ManagerSuite) TestUnseedHistoryArchiveTorrent() {
func (s *ManagerSuite) TestCheckChannelPermissions_NoPermissions() {
m, tm := s.setupManagerForTokenPermissions()
m, _, tm := s.setupManagerForTokenPermissions()
var chainID uint64 = 5
contractAddresses := make(map[uint64]string)
@ -841,7 +918,7 @@ func (s *ManagerSuite) TestCheckChannelPermissions_NoPermissions() {
func (s *ManagerSuite) TestCheckChannelPermissions_ViewOnlyPermissions() {
m, tm := s.setupManagerForTokenPermissions()
m, _, tm := s.setupManagerForTokenPermissions()
var chainID uint64 = 5
contractAddresses := make(map[uint64]string)
@ -899,7 +976,7 @@ func (s *ManagerSuite) TestCheckChannelPermissions_ViewOnlyPermissions() {
func (s *ManagerSuite) TestCheckChannelPermissions_ViewAndPostPermissions() {
m, tm := s.setupManagerForTokenPermissions()
m, _, tm := s.setupManagerForTokenPermissions()
var chainID uint64 = 5
contractAddresses := make(map[uint64]string)
@ -958,7 +1035,7 @@ func (s *ManagerSuite) TestCheckChannelPermissions_ViewAndPostPermissions() {
func (s *ManagerSuite) TestCheckChannelPermissions_ViewAndPostPermissionsCombination() {
m, tm := s.setupManagerForTokenPermissions()
m, _, tm := s.setupManagerForTokenPermissions()
var chainID uint64 = 5
contractAddresses := make(map[uint64]string)
@ -1032,7 +1109,7 @@ func (s *ManagerSuite) TestCheckChannelPermissions_ViewAndPostPermissionsCombina
func (s *ManagerSuite) TestCheckAllChannelsPermissions_EmptyPermissions() {
m, _ := s.setupManagerForTokenPermissions()
m, _, _ := s.setupManagerForTokenPermissions()
createRequest := &requests.CreateCommunity{
Name: "channel permission community",
@ -1079,7 +1156,7 @@ func (s *ManagerSuite) TestCheckAllChannelsPermissions_EmptyPermissions() {
func (s *ManagerSuite) TestCheckAllChannelsPermissions() {
m, tm := s.setupManagerForTokenPermissions()
m, _, tm := s.setupManagerForTokenPermissions()
var chatID1 string
var chatID2 string

View File

@ -422,10 +422,19 @@ func NewMessenger(
ensVerifier := ens.New(node, logger, transp, database, c.verifyENSURL, c.verifyENSContractAddress)
var walletAPI *wallet.API
if c.walletService != nil {
walletAPI = wallet.NewAPI(c.walletService)
}
managerOptions := []communities.ManagerOption{
communities.WithAccountManager(accountsManager),
}
if walletAPI != nil {
managerOptions = append(managerOptions, communities.WithCollectiblesManager(walletAPI))
}
if c.tokenManager != nil {
managerOptions = append(managerOptions, communities.WithTokenManager(c.tokenManager))
} else if c.rpcClient != nil {
@ -545,7 +554,7 @@ func NewMessenger(
messenger.mentionsManager = NewMentionManager(messenger)
if c.walletService != nil {
messenger.walletAPI = wallet.NewAPI(c.walletService)
messenger.walletAPI = walletAPI
}
if c.outputMessagesCSV {

View File

@ -338,6 +338,11 @@ func (api *API) GetCollectibleOwnersByContractAddress(chainID uint64, contractAd
return api.s.collectiblesManager.FetchNFTOwnersByContractAddress(chainID, contractAddress)
}
func (api *API) FetchBalancesByOwnerAndContractAddress(chainID uint64, ownerAddress common.Address, contractAddresses []common.Address) (thirdparty.TokenBalancesPerContractAddress, error) {
log.Debug("call to FetchBalancesByOwnerAndContractAddress")
return api.s.collectiblesManager.FetchBalancesByOwnerAndContractAddress(chainID, ownerAddress, contractAddresses)
}
func (api *API) AddEthereumChain(ctx context.Context, network params.Network) error {
log.Debug("call to AddEthereumChain")
return api.s.rpcClient.NetworkManager.Upsert(&network)

View File

@ -3,6 +3,7 @@ package collectibles
import (
"context"
"fmt"
"math/big"
"strings"
"sync"
"time"
@ -14,6 +15,7 @@ import (
"github.com/ethereum/go-ethereum/event"
"github.com/status-im/status-go/contracts/collectibles"
"github.com/status-im/status-go/rpc"
"github.com/status-im/status-go/services/wallet/bigint"
"github.com/status-im/status-go/services/wallet/thirdparty"
"github.com/status-im/status-go/services/wallet/thirdparty/opensea"
)
@ -121,6 +123,48 @@ func (o *Manager) FetchAllAssetsByOwnerAndCollection(chainID uint64, owner commo
return assetContainer, nil
}
// Need to combine different providers to support all needed ChainIDs
func (o *Manager) FetchBalancesByOwnerAndContractAddress(chainID uint64, ownerAddress common.Address, contractAddresses []common.Address) (thirdparty.TokenBalancesPerContractAddress, error) {
ret := make(thirdparty.TokenBalancesPerContractAddress)
for _, contractAddress := range contractAddresses {
ret[contractAddress] = make([]thirdparty.TokenBalance, 0)
}
// Try with more direct endpoint first (OpenSea)
assetsContainer, err := o.FetchAllAssetsByOwnerAndContractAddress(chainID, ownerAddress, contractAddresses, "", 0)
if err == opensea.ErrChainIDNotSupported {
// Use contract ownership providers
for _, contractAddress := range contractAddresses {
ownership, err := o.FetchNFTOwnersByContractAddress(chainID, contractAddress)
if err != nil {
return nil, err
}
for _, nftOwner := range ownership.Owners {
if nftOwner.OwnerAddress == ownerAddress {
ret[contractAddress] = nftOwner.TokenBalances
break
}
}
}
} else if err == nil {
// OpenSea could provide
for _, asset := range assetsContainer.Assets {
contractAddress := common.HexToAddress(asset.Contract.Address)
balance := thirdparty.TokenBalance{
TokenID: asset.TokenID,
Balance: &bigint.BigInt{Int: big.NewInt(1)},
}
ret[contractAddress] = append(ret[contractAddress], balance)
}
} else {
// OpenSea could have provided, but returned error
return nil, err
}
return ret, nil
}
func (o *Manager) FetchAllAssetsByOwnerAndContractAddress(chainID uint64, owner common.Address, contractAddresses []common.Address, cursor string, limit int) (*opensea.AssetContainer, error) {
client, err := opensea.NewOpenseaClient(chainID, o.openseaAPIKey, o.walletFeed)
if err != nil {

View File

@ -67,6 +67,8 @@ type TokenBalance struct {
Balance *bigint.BigInt `json:"balance"`
}
type TokenBalancesPerContractAddress = map[common.Address][]TokenBalance
type NFTOwner struct {
OwnerAddress common.Address `json:"ownerAddress"`
TokenBalances []TokenBalance `json:"tokenBalances"`