feat: fetch collectibles balances
This commit is contained in:
parent
c7533a7dab
commit
71377a50d7
|
@ -164,7 +164,7 @@ type loadOwnedCollectiblesCommand struct {
|
|||
ownedCollectiblesChangeCh chan<- OwnedCollectiblesChange
|
||||
|
||||
// Not to be set by the caller
|
||||
partialOwnership []thirdparty.CollectibleUniqueID
|
||||
partialOwnership []thirdparty.CollectibleIDBalance
|
||||
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)
|
||||
for _, id := range ownership {
|
||||
balance := thirdparty.TokenBalance{
|
||||
TokenID: id.TokenID,
|
||||
Balance: &bigint.BigInt{Int: big.NewInt(1)},
|
||||
for _, idBalance := range ownership {
|
||||
balanceBigInt := idBalance.Balance
|
||||
if balanceBigInt == nil {
|
||||
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
|
||||
}
|
||||
|
@ -295,9 +299,6 @@ func (c *loadOwnedCollectiblesCommand) Run(parent context.Context) (err error) {
|
|||
// 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 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)
|
||||
|
||||
updateMessage.Removed, updateMessage.Updated, updateMessage.Added, err = c.ownershipDB.Update(c.chainID, c.account, balances, start.Unix())
|
||||
|
|
|
@ -17,6 +17,7 @@ import (
|
|||
"github.com/ethereum/go-ethereum/event"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"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/server"
|
||||
"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
|
||||
}
|
||||
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) {
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,6 @@ import (
|
|||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"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/eth-node/types"
|
||||
"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) {
|
||||
client, err := r.s.rpcClient.EthClient(network.ChainID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tokenID, success := new(big.Int).SetString(token.Symbol, 10)
|
||||
if !success {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return caller.BalanceOf(&bind.CallOpts{
|
||||
Context: ctx,
|
||||
}, account, tokenID)
|
||||
if len(balances) != 1 || balances[0] == nil {
|
||||
return nil, errors.New("invalid ERC1155 balance fetch response")
|
||||
}
|
||||
|
||||
return balances[0].Int, nil
|
||||
}
|
||||
|
||||
func (r *Router) suggestedRoutes(
|
||||
|
|
|
@ -43,6 +43,9 @@ func TestUnmarshallOwnedCollectibles(t *testing.T) {
|
|||
expectedTokenID0, _ := big.NewInt(0).SetString("50659039041325838222074459099120411190538227963344971355684955900852972814336", 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{
|
||||
{
|
||||
CollectibleData: thirdparty.CollectibleData{
|
||||
|
@ -77,6 +80,9 @@ func TestUnmarshallOwnedCollectibles(t *testing.T) {
|
|||
ImageURL: "",
|
||||
Traits: make(map[string]thirdparty.CollectionTrait),
|
||||
},
|
||||
AccountBalance: &bigint.BigInt{
|
||||
Int: expectedBalance0,
|
||||
},
|
||||
},
|
||||
{
|
||||
CollectibleData: thirdparty.CollectibleData{
|
||||
|
@ -132,6 +138,9 @@ func TestUnmarshallOwnedCollectibles(t *testing.T) {
|
|||
ImageURL: "https://raw.seadn.io/files/e7765f13c4658f514d0efc008ae7f300.png",
|
||||
Traits: make(map[string]thirdparty.CollectionTrait),
|
||||
},
|
||||
AccountBalance: &bigint.BigInt{
|
||||
Int: expectedBalance1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -87,7 +87,7 @@ const ownedCollectiblesJSON = `{
|
|||
},
|
||||
"owners": null,
|
||||
"timeLastUpdated": "2024-01-03T19:11:04.681Z",
|
||||
"balance": "1",
|
||||
"balance": "15",
|
||||
"acquiredAt": {
|
||||
"blockTimestamp": null,
|
||||
"blockNumber": null
|
||||
|
|
|
@ -133,6 +133,7 @@ type Asset struct {
|
|||
Image Image `json:"image"`
|
||||
Raw Raw `json:"raw"`
|
||||
TokenURI string `json:"tokenUri"`
|
||||
Balance *bigint.BigInt `json:"balance,omitempty"`
|
||||
}
|
||||
|
||||
type OwnedNFTList struct {
|
||||
|
@ -216,6 +217,7 @@ func (c *Asset) toCommon(id thirdparty.CollectibleUniqueID) thirdparty.FullColle
|
|||
return thirdparty.FullCollectibleData{
|
||||
CollectibleData: c.toCollectiblesData(id),
|
||||
CollectionData: &contractData,
|
||||
AccountBalance: c.Balance,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -97,6 +97,45 @@ func GroupContractIDsByChainID(ids []ContractID) map[w_common.ChainID][]Contract
|
|||
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 {
|
||||
Min float64 `json:"min"`
|
||||
Max float64 `json:"max"`
|
||||
|
@ -153,7 +192,8 @@ type FullCollectibleData struct {
|
|||
CollectionData *CollectionData
|
||||
CommunityInfo *CommunityInfo
|
||||
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 {
|
||||
|
@ -163,29 +203,38 @@ type CollectiblesContainer[T any] struct {
|
|||
Provider string
|
||||
}
|
||||
|
||||
type CollectibleOwnershipContainer CollectiblesContainer[CollectibleUniqueID]
|
||||
type CollectibleOwnershipContainer CollectiblesContainer[CollectibleIDBalance]
|
||||
type CollectionDataContainer CollectiblesContainer[CollectionData]
|
||||
type CollectibleDataContainer CollectiblesContainer[CollectibleData]
|
||||
type FullCollectibleDataContainer CollectiblesContainer[FullCollectibleData]
|
||||
|
||||
// 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 {
|
||||
ret := make([]CollectibleUniqueID, 0, len(items))
|
||||
func collectibleItemsToBalances(items []FullCollectibleData) []CollectibleIDBalance {
|
||||
ret := make([]CollectibleIDBalance, 0, len(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
|
||||
}
|
||||
|
||||
func (c *FullCollectibleDataContainer) ToOwnershipContainer() CollectibleOwnershipContainer {
|
||||
return CollectibleOwnershipContainer{
|
||||
Items: collectibleItemsToIDs(c.Items),
|
||||
Items: collectibleItemsToBalances(c.Items),
|
||||
NextCursor: c.NextCursor,
|
||||
PreviousCursor: c.PreviousCursor,
|
||||
Provider: c.Provider,
|
||||
}
|
||||
}
|
||||
|
||||
type CollectibleIDBalance struct {
|
||||
ID CollectibleUniqueID `json:"id"`
|
||||
Balance *bigint.BigInt `json:"balance"`
|
||||
}
|
||||
|
||||
type TokenBalance struct {
|
||||
TokenID *bigint.BigInt `json:"tokenId"`
|
||||
Balance *bigint.BigInt `json:"balance"`
|
||||
|
|
Loading…
Reference in New Issue