Check ownership of collectibles in community permissions

This adds an additional check for collectibles when community
permissions are validated.

Specifically this uses opensea to request all NFTs given an
owner wallet and a list of contract addresses (collectibles).
This commit is contained in:
Pascal Precht 2023-03-27 11:35:03 +02:00 committed by Follow the white rabbit
parent 48e16317a7
commit 9267ad46c5
5 changed files with 186 additions and 54 deletions

View File

@ -218,6 +218,7 @@ func main() {
protocol.WithPushNotificationServerConfig(&config.PushNotificationServerConfig), protocol.WithPushNotificationServerConfig(&config.PushNotificationServerConfig),
protocol.WithDatabase(db), protocol.WithDatabase(db),
protocol.WithTorrentConfig(&config.TorrentConfig), protocol.WithTorrentConfig(&config.TorrentConfig),
protocol.WithWalletConfig(&config.WalletConfig),
protocol.WithRPCClient(backend.StatusNode().RPCClient()), protocol.WithRPCClient(backend.StatusNode().RPCClient()),
} }

View File

@ -37,6 +37,7 @@ import (
"github.com/status-im/status-go/protocol/protobuf" "github.com/status-im/status-go/protocol/protobuf"
"github.com/status-im/status-go/protocol/requests" "github.com/status-im/status-go/protocol/requests"
"github.com/status-im/status-go/protocol/transport" "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/token" "github.com/status-im/status-go/services/wallet/token"
"github.com/status-im/status-go/signal" "github.com/status-im/status-go/signal"
) )
@ -72,6 +73,7 @@ type Manager struct {
quit chan struct{} quit chan struct{}
torrentConfig *params.TorrentConfig torrentConfig *params.TorrentConfig
torrentClient *torrent.Client torrentClient *torrent.Client
walletConfig *params.WalletConfig
historyArchiveTasksWaitGroup sync.WaitGroup historyArchiveTasksWaitGroup sync.WaitGroup
historyArchiveTasks map[string]chan struct{} historyArchiveTasks map[string]chan struct{}
periodicMemberPermissionsTasks map[string]chan struct{} periodicMemberPermissionsTasks map[string]chan struct{}
@ -102,6 +104,7 @@ func (t *HistoryArchiveDownloadTask) Cancel() {
type managerOptions struct { type managerOptions struct {
accountsManager *account.GethManager accountsManager *account.GethManager
tokenManager *token.Manager tokenManager *token.Manager
walletConfig *params.WalletConfig
} }
type ManagerOption func(*managerOptions) type ManagerOption func(*managerOptions)
@ -118,6 +121,12 @@ func WithTokenManager(tokenManager *token.Manager) ManagerOption {
} }
} }
func WithWalletConfig(walletConfig *params.WalletConfig) ManagerOption {
return func(opts *managerOptions) {
opts.walletConfig = walletConfig
}
}
func NewManager(identity *ecdsa.PrivateKey, db *sql.DB, encryptor *encryption.Protocol, logger *zap.Logger, verifier *ens.Verifier, transport *transport.Transport, torrentConfig *params.TorrentConfig, opts ...ManagerOption) (*Manager, error) { func NewManager(identity *ecdsa.PrivateKey, db *sql.DB, encryptor *encryption.Protocol, logger *zap.Logger, verifier *ens.Verifier, transport *transport.Transport, torrentConfig *params.TorrentConfig, opts ...ManagerOption) (*Manager, error) {
if identity == nil { if identity == nil {
return nil, errors.New("empty identity") return nil, errors.New("empty identity")
@ -166,6 +175,10 @@ func NewManager(identity *ecdsa.PrivateKey, db *sql.DB, encryptor *encryption.Pr
manager.tokenManager = managerConfig.tokenManager manager.tokenManager = managerConfig.tokenManager
} }
if managerConfig.walletConfig != nil {
manager.walletConfig = managerConfig.walletConfig
}
if verifier != nil { if verifier != nil {
sub := verifier.Subscribe() sub := verifier.Subscribe()
@ -1450,15 +1463,80 @@ func (m *Manager) HandleCommunityRequestToJoin(signer *ecdsa.PublicKey, request
} }
func (m *Manager) checkPermissionToJoin(permissions []*protobuf.CommunityTokenPermission, walletAddresses []gethcommon.Address) (bool, error) { func (m *Manager) checkPermissionToJoin(permissions []*protobuf.CommunityTokenPermission, walletAddresses []gethcommon.Address) (bool, error) {
tokenAddresses, addressToSymbolMap := getTokenAddressesFromPermissions(permissions)
balances, err := m.getAccumulatedTokenBalances(walletAddresses, tokenAddresses, addressToSymbolMap) erc20TokenRequirements, erc721TokenRequirements := extractTokenRequirements(permissions)
// find owned ERC721 tokens required by community's permissions
ownedERC721Tokens, err := m.getOwnedERC721Tokens(walletAddresses, erc721TokenRequirements)
if err != nil {
return false, err
}
// find owned ERC20 token balances required by community's permissions
ownedERC20Tokens, err := m.getAccumulatedTokenBalances(walletAddresses, erc20TokenRequirements)
if err != nil { if err != nil {
return false, err return false, err
} }
hasPermission := false hasPermission := false
for _, tokenPermission := range permissions { for _, tokenPermission := range permissions {
if checkTokenCriteria(tokenPermission.TokenCriteria, balances) { permissionRequirementsMet := true
// There can be multiple token requirements per permission.
// If only one is not met, the entire permission is marked
// as not fulfilled
for _, tokenRequirement := range tokenPermission.TokenCriteria {
tokenRequirementMet := false
// check NFTs
if tokenRequirement.Type == protobuf.CommunityTokenType_ERC721 {
if len(ownedERC721Tokens) == 0 {
continue
}
contractAddressesLoop:
for chainID, address := range tokenRequirement.ContractAddresses {
addr := strings.ToLower(address)
if _, exists := ownedERC721Tokens[chainID][addr]; !exists {
continue
}
if len(tokenRequirement.TokenIds) == 0 {
// no NFT with specific tokenId needs to be owned,
tokenRequirementMet = true
break contractAddressesLoop
}
tokenIDsLoop:
for _, tokenID := range tokenRequirement.TokenIds {
tokenIDBigInt := new(big.Int).SetUint64(tokenID)
for _, asset := range ownedERC721Tokens[chainID][addr] {
if asset.TokenID.Cmp(tokenIDBigInt) == 0 {
tokenRequirementMet = true
break tokenIDsLoop
}
}
}
}
} else if tokenRequirement.Type == protobuf.CommunityTokenType_ERC20 {
if len(ownedERC20Tokens) == 0 {
continue
}
amount, _ := strconv.ParseFloat(tokenRequirement.Amount, 32)
if ownedERC20Tokens[tokenRequirement.Symbol].Cmp(big.NewFloat(amount)) != -1 {
tokenRequirementMet = true
}
}
if !tokenRequirementMet {
permissionRequirementsMet = false
}
}
// multiple permissions are treated as logical OR, meaning
// if only one of them is fulfilled, the user gets permission
// to join and we can stop early
if permissionRequirementsMet {
hasPermission = true hasPermission = true
break break
} }
@ -1467,25 +1545,91 @@ func (m *Manager) checkPermissionToJoin(permissions []*protobuf.CommunityTokenPe
return hasPermission, nil return hasPermission, nil
} }
func checkTokenCriteria(tokenCriteria []*protobuf.TokenCriteria, balances map[string]*big.Float) bool { func extractTokenRequirements(permissions []*protobuf.CommunityTokenPermission) (map[uint64]map[string]*protobuf.TokenCriteria, map[uint64]map[string]*protobuf.TokenCriteria) {
result := true erc20TokenRequirementsByChain := make(map[uint64]map[string]*protobuf.TokenCriteria)
hasERC20 := false erc721TokenRequirementsByChain := make(map[uint64]map[string]*protobuf.TokenCriteria)
for _, tokenRequirement := range tokenCriteria { for _, tokenPermission := range permissions {
// we gotta check for whether there are ERC20 token criteria for _, tokenRequirement := range tokenPermission.TokenCriteria {
// in the first place, if we don't we'll return a false positive isERC721 := tokenRequirement.Type == protobuf.CommunityTokenType_ERC721
if tokenRequirement.Type == protobuf.CommunityTokenType_ERC20 { isERC20 := tokenRequirement.Type == protobuf.CommunityTokenType_ERC20
hasERC20 = true for chainID, contractAddress := range tokenRequirement.ContractAddresses {
amount, _ := strconv.ParseFloat(tokenRequirement.Amount, 32)
if balances[tokenRequirement.Symbol].Cmp(big.NewFloat(amount)) == -1 { _, existsERC721 := erc721TokenRequirementsByChain[chainID]
result = false
break if isERC721 && !existsERC721 {
erc721TokenRequirementsByChain[chainID] = make(map[string]*protobuf.TokenCriteria)
}
_, existsERC20 := erc20TokenRequirementsByChain[chainID]
if isERC20 && !existsERC20 {
erc20TokenRequirementsByChain[chainID] = make(map[string]*protobuf.TokenCriteria)
}
_, existsERC721 = erc721TokenRequirementsByChain[chainID][contractAddress]
if isERC721 && !existsERC721 {
erc721TokenRequirementsByChain[chainID][strings.ToLower(contractAddress)] = tokenRequirement
}
_, existsERC20 = erc20TokenRequirementsByChain[chainID][contractAddress]
if isERC20 && !existsERC20 {
erc20TokenRequirementsByChain[chainID][strings.ToLower(contractAddress)] = tokenRequirement
}
} }
} }
} }
return hasERC20 && result return erc20TokenRequirementsByChain, erc721TokenRequirementsByChain
} }
func (m *Manager) getAccumulatedTokenBalances(accounts []gethcommon.Address, tokenAddresses []gethcommon.Address, addressToToken map[gethcommon.Address]tokenData) (map[string]*big.Float, error) { func (m *Manager) getOwnedERC721Tokens(walletAddresses []gethcommon.Address, tokenRequirements map[uint64]map[string]*protobuf.TokenCriteria) (map[uint64]map[string][]opensea.Asset, error) {
if m.walletConfig == nil || len(m.walletConfig.OpenseaAPIKey) == 0 {
return nil, errors.New("no api key for opensea")
}
ownedERC721Tokens := make(map[uint64]map[string][]opensea.Asset)
for chainID, erc721Tokens := range tokenRequirements {
client, err := opensea.NewOpenseaClient(chainID, m.walletConfig.OpenseaAPIKey)
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[string][]opensea.Asset)
}
for _, owner := range walletAddresses {
assets, err := client.FetchAllAssetsByOwnerAndContractAddress(owner, contractAddresses, "", 5)
if err != nil {
m.logger.Info("couldn't fetch owner assets", zap.Error(err))
return nil, err
}
for _, asset := range assets.Assets {
if _, exists := ownedERC721Tokens[chainID][asset.Contract.Address]; !exists {
ownedERC721Tokens[chainID][asset.Contract.Address] = make([]opensea.Asset, 0)
}
ownedERC721Tokens[chainID][asset.Contract.Address] = append(ownedERC721Tokens[chainID][asset.Contract.Address], asset)
}
}
}
return ownedERC721Tokens, nil
}
func (m *Manager) getAccumulatedTokenBalances(accounts []gethcommon.Address, tokenRequirements map[uint64]map[string]*protobuf.TokenCriteria) (map[string]*big.Float, error) {
tokenAddresses := make([]gethcommon.Address, 0)
for _, tokens := range tokenRequirements {
for contractAddress := range tokens {
tokenAddresses = append(tokenAddresses, gethcommon.HexToAddress(contractAddress))
}
}
networks, err := m.tokenManager.RPCClient.NetworkManager.Get(false) networks, err := m.tokenManager.RPCClient.NetworkManager.Get(false)
if err != nil { if err != nil {
return nil, err return nil, err
@ -1507,52 +1651,26 @@ func (m *Manager) getAccumulatedTokenBalances(accounts []gethcommon.Address, tok
} }
accumulatedBalances := make(map[string]*big.Float) accumulatedBalances := make(map[string]*big.Float)
for _, accounts := range balancesByChain { for chainID, accounts := range balancesByChain {
for _, contracts := range accounts { for _, contracts := range accounts {
for contract, value := range contracts { for contract, value := range contracts {
if _, exists := accumulatedBalances[addressToToken[contract].Symbol]; !exists { if token, exists := tokenRequirements[chainID][contract.Hex()]; exists {
accumulatedBalances[addressToToken[contract].Symbol] = new(big.Float) if _, exists := accumulatedBalances[token.Symbol]; !exists {
accumulatedBalances[token.Symbol] = new(big.Float)
}
balance := new(big.Float).Quo(
new(big.Float).SetInt(value.ToInt()),
big.NewFloat(math.Pow(10, float64(token.Decimals))),
)
prevBalance := accumulatedBalances[token.Symbol]
accumulatedBalances[token.Symbol].Add(prevBalance, balance)
} }
balance := new(big.Float).Quo(
new(big.Float).SetInt(value.ToInt()),
big.NewFloat(math.Pow(10, float64(addressToToken[contract].Decimals))),
)
prevBalance := accumulatedBalances[addressToToken[contract].Symbol]
accumulatedBalances[addressToToken[contract].Symbol].Add(prevBalance, balance)
} }
} }
} }
return accumulatedBalances, nil return accumulatedBalances, nil
} }
type tokenData struct {
Symbol string
Decimals int
}
func getTokenAddressesFromPermissions(tokenPermissions []*protobuf.CommunityTokenPermission) ([]gethcommon.Address, map[gethcommon.Address]tokenData) {
set := make(map[gethcommon.Address]bool)
addressToToken := make(map[gethcommon.Address]tokenData)
for _, tokenPermission := range tokenPermissions {
for _, token := range tokenPermission.TokenCriteria {
if token.Type == protobuf.CommunityTokenType_ERC20 {
for _, contractAddress := range token.ContractAddresses {
set[gethcommon.HexToAddress(contractAddress)] = true
addressToToken[gethcommon.HexToAddress(contractAddress)] = tokenData{
Symbol: token.Symbol,
Decimals: int(token.Decimals),
}
}
}
}
}
tokenAddresses := make([]gethcommon.Address, 0)
for tokenAddress := range set {
tokenAddresses = append(tokenAddresses, tokenAddress)
}
return tokenAddresses, addressToToken
}
func (m *Manager) HandleCommunityRequestToJoinResponse(signer *ecdsa.PublicKey, request *protobuf.CommunityRequestToJoinResponse) (*RequestToJoin, error) { func (m *Manager) HandleCommunityRequestToJoinResponse(signer *ecdsa.PublicKey, request *protobuf.CommunityRequestToJoinResponse) (*RequestToJoin, error) {
pkString := common.PubkeyToHex(&m.identity.PublicKey) pkString := common.PubkeyToHex(&m.identity.PublicKey)

View File

@ -417,6 +417,10 @@ func NewMessenger(
managerOptions = append(managerOptions, communities.WithTokenManager(tokenManager)) managerOptions = append(managerOptions, communities.WithTokenManager(tokenManager))
} }
if c.walletConfig != nil {
managerOptions = append(managerOptions, communities.WithWalletConfig(c.walletConfig))
}
communitiesManager, err := communities.NewManager(identity, database, encryptionProtocol, logger, ensVerifier, transp, c.torrentConfig, managerOptions...) communitiesManager, err := communities.NewManager(identity, database, encryptionProtocol, logger, ensVerifier, transp, c.torrentConfig, managerOptions...)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -83,6 +83,7 @@ type config struct {
clusterConfig params.ClusterConfig clusterConfig params.ClusterConfig
browserDatabase *browsers.Database browserDatabase *browsers.Database
torrentConfig *params.TorrentConfig torrentConfig *params.TorrentConfig
walletConfig *params.WalletConfig
httpServer *server.MediaServer httpServer *server.MediaServer
rpcClient *rpc.Client rpcClient *rpc.Client
@ -309,6 +310,13 @@ func WithRPCClient(r *rpc.Client) Option {
} }
} }
func WithWalletConfig(wc *params.WalletConfig) Option {
return func(c *config) error {
c.walletConfig = wc
return nil
}
}
func WithMessageCSV(enabled bool) Option { func WithMessageCSV(enabled bool) Option {
return func(c *config) error { return func(c *config) error {
c.outputMessagesCSV = enabled c.outputMessagesCSV = enabled

View File

@ -425,6 +425,7 @@ func buildMessengerOptions(
protocol.WithHTTPServer(httpServer), protocol.WithHTTPServer(httpServer),
protocol.WithRPCClient(rpcClient), protocol.WithRPCClient(rpcClient),
protocol.WithMessageCSV(config.OutputMessageCSVEnabled), protocol.WithMessageCSV(config.OutputMessageCSVEnabled),
protocol.WithWalletConfig(&config.WalletConfig),
} }
if config.ShhextConfig.DataSyncEnabled { if config.ShhextConfig.DataSyncEnabled {