feat: fetch collectibles balances

This commit is contained in:
Dario Gabriel Lipicar 2024-03-05 15:57:02 -03:00 committed by dlipicar
parent c7533a7dab
commit 71377a50d7
7 changed files with 178 additions and 27 deletions

View File

@ -164,7 +164,7 @@ type loadOwnedCollectiblesCommand struct {
ownedCollectiblesChangeCh chan<- OwnedCollectiblesChange ownedCollectiblesChangeCh chan<- OwnedCollectiblesChange
// Not to be set by the caller // Not to be set by the caller
partialOwnership []thirdparty.CollectibleUniqueID partialOwnership []thirdparty.CollectibleIDBalance
err error err error
} }
@ -200,14 +200,18 @@ func (c *loadOwnedCollectiblesCommand) triggerEvent(eventType walletevent.EventT
}) })
} }
func ownedTokensToTokenBalancesPerContractAddress(ownership []thirdparty.CollectibleUniqueID) thirdparty.TokenBalancesPerContractAddress { func ownedTokensToTokenBalancesPerContractAddress(ownership []thirdparty.CollectibleIDBalance) thirdparty.TokenBalancesPerContractAddress {
ret := make(thirdparty.TokenBalancesPerContractAddress) ret := make(thirdparty.TokenBalancesPerContractAddress)
for _, id := range ownership { for _, idBalance := range ownership {
balance := thirdparty.TokenBalance{ balanceBigInt := idBalance.Balance
TokenID: id.TokenID, if balanceBigInt == nil {
Balance: &bigint.BigInt{Int: big.NewInt(1)}, balanceBigInt = &bigint.BigInt{Int: big.NewInt(1)}
} }
ret[id.ContractID.Address] = append(ret[id.ContractID.Address], balance) balance := thirdparty.TokenBalance{
TokenID: idBalance.ID.TokenID,
Balance: balanceBigInt,
}
ret[idBalance.ID.ContractID.Address] = append(ret[idBalance.ID.ContractID.Address], balance)
} }
return ret return ret
} }
@ -295,9 +299,6 @@ func (c *loadOwnedCollectiblesCommand) Run(parent context.Context) (err error) {
// Normally, update the DB once we've finished fetching // Normally, update the DB once we've finished fetching
// If this is the first fetch, make partial updates to the client to get a better UX // If this is the first fetch, make partial updates to the client to get a better UX
if initialFetch || finished { if initialFetch || finished {
// Token balances should come from the providers. For now we assume all balances are 1, which
// is only valid for ERC721.
// TODO (#13025): Fetch balances from the providers.
balances := ownedTokensToTokenBalancesPerContractAddress(c.partialOwnership) balances := ownedTokensToTokenBalancesPerContractAddress(c.partialOwnership)
updateMessage.Removed, updateMessage.Updated, updateMessage.Added, err = c.ownershipDB.Update(c.chainID, c.account, balances, start.Unix()) updateMessage.Removed, updateMessage.Updated, updateMessage.Added, err = c.ownershipDB.Update(c.chainID, c.account, balances, start.Unix())

View File

@ -17,6 +17,7 @@ import (
"github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/contracts/community-tokens/collectibles" "github.com/status-im/status-go/contracts/community-tokens/collectibles"
"github.com/status-im/status-go/contracts/ierc1155"
"github.com/status-im/status-go/rpc" "github.com/status-im/status-go/rpc"
"github.com/status-im/status-go/server" "github.com/status-im/status-go/server"
"github.com/status-im/status-go/services/wallet/async" "github.com/status-im/status-go/services/wallet/async"
@ -315,6 +316,85 @@ func (o *Manager) FetchAllAssetsByOwner(ctx context.Context, chainID walletCommo
} }
return nil, ErrNoProvidersAvailableForChainID return nil, ErrNoProvidersAvailableForChainID
} }
func (o *Manager) FetchERC1155Balances(ctx context.Context, owner common.Address, chainID walletCommon.ChainID, contractAddress common.Address, tokenIDs []*bigint.BigInt) ([]*bigint.BigInt, error) {
if len(tokenIDs) == 0 {
return nil, nil
}
backend, err := o.rpcClient.EthClient(uint64(chainID))
if err != nil {
return nil, err
}
caller, err := ierc1155.NewIerc1155Caller(contractAddress, backend)
if err != nil {
return nil, err
}
owners := make([]common.Address, len(tokenIDs))
ids := make([]*big.Int, len(tokenIDs))
for i, tokenID := range tokenIDs {
owners[i] = owner
ids[i] = tokenID.Int
}
balances, err := caller.BalanceOfBatch(&bind.CallOpts{
Context: ctx,
}, owners, ids)
if err != nil {
return nil, err
}
bigIntBalances := make([]*bigint.BigInt, len(balances))
for i, balance := range balances {
bigIntBalances[i] = &bigint.BigInt{Int: balance}
}
return bigIntBalances, err
}
func (o *Manager) fillMissingBalances(ctx context.Context, owner common.Address, collectibles []*thirdparty.FullCollectibleData) {
collectiblesByChainIDAndContractAddress := thirdparty.GroupCollectiblesByChainIDAndContractAddress(collectibles)
for chainID, collectiblesByContract := range collectiblesByChainIDAndContractAddress {
for contractAddress, contractCollectibles := range collectiblesByContract {
collectiblesToFetchPerTokenID := make(map[string]*thirdparty.FullCollectibleData)
for _, collectible := range contractCollectibles {
if collectible.AccountBalance == nil {
switch getContractType(*collectible) {
case walletCommon.ContractTypeERC1155:
collectiblesToFetchPerTokenID[collectible.CollectibleData.ID.TokenID.String()] = collectible
default:
// Any other type of collectible is non-fungible, balance is 1
collectible.AccountBalance = &bigint.BigInt{Int: big.NewInt(1)}
}
}
}
if len(collectiblesToFetchPerTokenID) == 0 {
continue
}
tokenIDs := make([]*bigint.BigInt, 0, len(collectiblesToFetchPerTokenID))
for _, c := range collectiblesToFetchPerTokenID {
tokenIDs = append(tokenIDs, c.CollectibleData.ID.TokenID)
}
balances, err := o.FetchERC1155Balances(ctx, owner, chainID, contractAddress, tokenIDs)
if err != nil {
log.Error("FetchERC1155Balances failed", "chainID", chainID, "contractAddress", contractAddress, "err", err)
continue
}
for i := range balances {
collectible := collectiblesToFetchPerTokenID[tokenIDs[i].String()]
collectible.AccountBalance = balances[i]
}
}
}
}
func (o *Manager) FetchCollectibleOwnershipByOwner(ctx context.Context, chainID walletCommon.ChainID, owner common.Address, cursor string, limit int, providerID string) (*thirdparty.CollectibleOwnershipContainer, error) { func (o *Manager) FetchCollectibleOwnershipByOwner(ctx context.Context, chainID walletCommon.ChainID, owner common.Address, cursor string, limit int, providerID string) (*thirdparty.CollectibleOwnershipContainer, error) {
// We don't yet have an API that will return only Ownership data // We don't yet have an API that will return only Ownership data
@ -324,7 +404,15 @@ func (o *Manager) FetchCollectibleOwnershipByOwner(ctx context.Context, chainID
return nil, err return nil, err
} }
// Some providers do not give us the balances for ERC1155 tokens, so we need to fetch them separately.
collectibles := make([]*thirdparty.FullCollectibleData, 0, len(assetContainer.Items))
for i := range assetContainer.Items {
collectibles = append(collectibles, &assetContainer.Items[i])
}
o.fillMissingBalances(ctx, owner, collectibles)
ret := assetContainer.ToOwnershipContainer() ret := assetContainer.ToOwnershipContainer()
return &ret, nil return &ret, nil
} }

View File

@ -16,7 +16,6 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/status-im/status-go/contracts" "github.com/status-im/status-go/contracts"
"github.com/status-im/status-go/contracts/ierc1155"
"github.com/status-im/status-go/contracts/ierc20" "github.com/status-im/status-go/contracts/ierc20"
"github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/params" "github.com/status-im/status-go/params"
@ -525,24 +524,27 @@ func (r *Router) getBalance(ctx context.Context, network *params.Network, token
} }
func (r *Router) getERC1155Balance(ctx context.Context, network *params.Network, token *token.Token, account common.Address) (*big.Int, error) { func (r *Router) getERC1155Balance(ctx context.Context, network *params.Network, token *token.Token, account common.Address) (*big.Int, error) {
client, err := r.s.rpcClient.EthClient(network.ChainID)
if err != nil {
return nil, err
}
tokenID, success := new(big.Int).SetString(token.Symbol, 10) tokenID, success := new(big.Int).SetString(token.Symbol, 10)
if !success { if !success {
return nil, errors.New("failed to convert token symbol to big.Int") return nil, errors.New("failed to convert token symbol to big.Int")
} }
caller, err := ierc1155.NewIerc1155Caller(token.Address, client) balances, err := r.s.collectiblesManager.FetchERC1155Balances(
ctx,
account,
walletCommon.ChainID(network.ChainID),
token.Address,
[]*bigint.BigInt{&bigint.BigInt{Int: tokenID}},
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return caller.BalanceOf(&bind.CallOpts{ if len(balances) != 1 || balances[0] == nil {
Context: ctx, return nil, errors.New("invalid ERC1155 balance fetch response")
}, account, tokenID) }
return balances[0].Int, nil
} }
func (r *Router) suggestedRoutes( func (r *Router) suggestedRoutes(

View File

@ -43,6 +43,9 @@ func TestUnmarshallOwnedCollectibles(t *testing.T) {
expectedTokenID0, _ := big.NewInt(0).SetString("50659039041325838222074459099120411190538227963344971355684955900852972814336", 10) expectedTokenID0, _ := big.NewInt(0).SetString("50659039041325838222074459099120411190538227963344971355684955900852972814336", 10)
expectedTokenID1, _ := big.NewInt(0).SetString("900", 10) expectedTokenID1, _ := big.NewInt(0).SetString("900", 10)
expectedBalance0, _ := big.NewInt(0).SetString("15", 10)
expectedBalance1, _ := big.NewInt(0).SetString("1", 10)
expectedCollectiblesData := []thirdparty.FullCollectibleData{ expectedCollectiblesData := []thirdparty.FullCollectibleData{
{ {
CollectibleData: thirdparty.CollectibleData{ CollectibleData: thirdparty.CollectibleData{
@ -77,6 +80,9 @@ func TestUnmarshallOwnedCollectibles(t *testing.T) {
ImageURL: "", ImageURL: "",
Traits: make(map[string]thirdparty.CollectionTrait), Traits: make(map[string]thirdparty.CollectionTrait),
}, },
AccountBalance: &bigint.BigInt{
Int: expectedBalance0,
},
}, },
{ {
CollectibleData: thirdparty.CollectibleData{ CollectibleData: thirdparty.CollectibleData{
@ -132,6 +138,9 @@ func TestUnmarshallOwnedCollectibles(t *testing.T) {
ImageURL: "https://raw.seadn.io/files/e7765f13c4658f514d0efc008ae7f300.png", ImageURL: "https://raw.seadn.io/files/e7765f13c4658f514d0efc008ae7f300.png",
Traits: make(map[string]thirdparty.CollectionTrait), Traits: make(map[string]thirdparty.CollectionTrait),
}, },
AccountBalance: &bigint.BigInt{
Int: expectedBalance1,
},
}, },
} }

View File

@ -87,7 +87,7 @@ const ownedCollectiblesJSON = `{
}, },
"owners": null, "owners": null,
"timeLastUpdated": "2024-01-03T19:11:04.681Z", "timeLastUpdated": "2024-01-03T19:11:04.681Z",
"balance": "1", "balance": "15",
"acquiredAt": { "acquiredAt": {
"blockTimestamp": null, "blockTimestamp": null,
"blockNumber": null "blockNumber": null

View File

@ -133,6 +133,7 @@ type Asset struct {
Image Image `json:"image"` Image Image `json:"image"`
Raw Raw `json:"raw"` Raw Raw `json:"raw"`
TokenURI string `json:"tokenUri"` TokenURI string `json:"tokenUri"`
Balance *bigint.BigInt `json:"balance,omitempty"`
} }
type OwnedNFTList struct { type OwnedNFTList struct {
@ -216,6 +217,7 @@ func (c *Asset) toCommon(id thirdparty.CollectibleUniqueID) thirdparty.FullColle
return thirdparty.FullCollectibleData{ return thirdparty.FullCollectibleData{
CollectibleData: c.toCollectiblesData(id), CollectibleData: c.toCollectiblesData(id),
CollectionData: &contractData, CollectionData: &contractData,
AccountBalance: c.Balance,
} }
} }

View File

@ -97,6 +97,45 @@ func GroupContractIDsByChainID(ids []ContractID) map[w_common.ChainID][]Contract
return ret return ret
} }
func GroupCollectiblesByChainID(collectibles []*FullCollectibleData) map[w_common.ChainID][]*FullCollectibleData {
ret := make(map[w_common.ChainID][]*FullCollectibleData)
for i, collectible := range collectibles {
chainID := collectible.CollectibleData.ID.ContractID.ChainID
if _, ok := ret[chainID]; !ok {
ret[chainID] = make([]*FullCollectibleData, 0, len(collectibles))
}
ret[chainID] = append(ret[chainID], collectibles[i])
}
return ret
}
func GroupCollectiblesByContractAddress(collectibles []*FullCollectibleData) map[common.Address][]*FullCollectibleData {
ret := make(map[common.Address][]*FullCollectibleData)
for i, collectible := range collectibles {
contractAddress := collectible.CollectibleData.ID.ContractID.Address
if _, ok := ret[contractAddress]; !ok {
ret[contractAddress] = make([]*FullCollectibleData, 0, len(collectibles))
}
ret[contractAddress] = append(ret[contractAddress], collectibles[i])
}
return ret
}
func GroupCollectiblesByChainIDAndContractAddress(collectibles []*FullCollectibleData) map[w_common.ChainID]map[common.Address][]*FullCollectibleData {
ret := make(map[w_common.ChainID]map[common.Address][]*FullCollectibleData)
collectiblesByChainID := GroupCollectiblesByChainID(collectibles)
for chainID, chainCollectibles := range collectiblesByChainID {
ret[chainID] = GroupCollectiblesByContractAddress(chainCollectibles)
}
return ret
}
type CollectionTrait struct { type CollectionTrait struct {
Min float64 `json:"min"` Min float64 `json:"min"`
Max float64 `json:"max"` Max float64 `json:"max"`
@ -153,7 +192,8 @@ type FullCollectibleData struct {
CollectionData *CollectionData CollectionData *CollectionData
CommunityInfo *CommunityInfo CommunityInfo *CommunityInfo
CollectibleCommunityInfo *CollectibleCommunityInfo CollectibleCommunityInfo *CollectibleCommunityInfo
Ownership []AccountBalance Ownership []AccountBalance // This is a list of all the owners of the collectible
AccountBalance *bigint.BigInt // This is the balance of the collectible for the requested account
} }
type CollectiblesContainer[T any] struct { type CollectiblesContainer[T any] struct {
@ -163,29 +203,38 @@ type CollectiblesContainer[T any] struct {
Provider string Provider string
} }
type CollectibleOwnershipContainer CollectiblesContainer[CollectibleUniqueID] type CollectibleOwnershipContainer CollectiblesContainer[CollectibleIDBalance]
type CollectionDataContainer CollectiblesContainer[CollectionData] type CollectionDataContainer CollectiblesContainer[CollectionData]
type CollectibleDataContainer CollectiblesContainer[CollectibleData] type CollectibleDataContainer CollectiblesContainer[CollectibleData]
type FullCollectibleDataContainer CollectiblesContainer[FullCollectibleData] type FullCollectibleDataContainer CollectiblesContainer[FullCollectibleData]
// Tried to find a way to make this generic, but couldn't, so the code below is duplicated somewhere else // Tried to find a way to make this generic, but couldn't, so the code below is duplicated somewhere else
func collectibleItemsToIDs(items []FullCollectibleData) []CollectibleUniqueID { func collectibleItemsToBalances(items []FullCollectibleData) []CollectibleIDBalance {
ret := make([]CollectibleUniqueID, 0, len(items)) ret := make([]CollectibleIDBalance, 0, len(items))
for _, item := range items { for _, item := range items {
ret = append(ret, item.CollectibleData.ID) balance := CollectibleIDBalance{
ID: item.CollectibleData.ID,
Balance: item.AccountBalance,
}
ret = append(ret, balance)
} }
return ret return ret
} }
func (c *FullCollectibleDataContainer) ToOwnershipContainer() CollectibleOwnershipContainer { func (c *FullCollectibleDataContainer) ToOwnershipContainer() CollectibleOwnershipContainer {
return CollectibleOwnershipContainer{ return CollectibleOwnershipContainer{
Items: collectibleItemsToIDs(c.Items), Items: collectibleItemsToBalances(c.Items),
NextCursor: c.NextCursor, NextCursor: c.NextCursor,
PreviousCursor: c.PreviousCursor, PreviousCursor: c.PreviousCursor,
Provider: c.Provider, Provider: c.Provider,
} }
} }
type CollectibleIDBalance struct {
ID CollectibleUniqueID `json:"id"`
Balance *bigint.BigInt `json:"balance"`
}
type TokenBalance struct { type TokenBalance struct {
TokenID *bigint.BigInt `json:"tokenId"` TokenID *bigint.BigInt `json:"tokenId"`
Balance *bigint.BigInt `json:"balance"` Balance *bigint.BigInt `json:"balance"`