feat: fetch collectibles balances
This commit is contained in:
parent
c7533a7dab
commit
71377a50d7
|
@ -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())
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"`
|
||||||
|
|
Loading…
Reference in New Issue