Skip to content

Commit

Permalink
Feature/fetch real data for wallet hardcoded wallet address (#39)
Browse files Browse the repository at this point in the history
* Rename a test and make another test less flaky by waiting longer for UI to update

* Stubbed out NftContentFetcher - specifically the constructor

* Rename a test and make another test less flaky by waiting longer for UI to update

* Ripped out Nethereum. Prepare dependencies for package exporting

* Wrap editor scripts in #if UNITY_EDITOR preprocessor defines so that they don't interfere with the build process

* Remove unneeded code

* Removed unneeded file

* Make UI in sample scene scale based on the screen size

* ContentFetcher implementation - including implementations for NftContentFetcher and TokenContentFetcher, which are essentially wrappers for ContentFetcher

* Fetch real fungible tokens.

* Added some logging to requests. Added error handling for HTTP requests sent by Indexer. Fixed error with content fetching by removing an unsupported Chain

* Fix tests - were checking for mock fetchers in the wrong place

* Fetch real nfts prototype

* Move SpriteFetcher.cs

* Finish fetching tokens for a chain first, then start processing collections asynchronously

* Retry failed requests to indexer due to rate limits. Fix some bugs

* Cleaner logging

* Move to next chain when receiving errors fetching from indexer - handles case when the indexer is down for a given chain. This logs a warning and points to the indexer status page as a first thing to check

* More informative logging

* Fix some bugs with fetching nfts. Uncaught exceptions were causing the async messages to abort silently

* Feed up fetch speed by adding nfts and tokens (fetching their images as well) to the lists in parallel

* Conducted trials with different number of tokens fetched at once to figure out an ideal starting place for devs to work with

* Fix broken UI tests

* Fixed bug with MockTransactionDetailsFetcher that was causing theUnity to freeze (infinite loop) when clicking on any of the wallet elements and trying to open their info page

* Destroy grid children on awake as opposed to everytime we open the walletpage

* Refresh ContentFetcher when re-opening the WalletPage

* Fixed bug where MockTransactionDetailsFetcher wasn't fetching transactions correctly

* Use my thread-safe implementation of DelayTask as opposed to the default Task.Delay

* Add basic end to end test for token and nft fetching and wallet page population. Fix memory leaks associated with web requests

* Don't swallow error when converting JSON objects

* Clean up ContentFetcher implementation

* Don't display NFTs that have the default sprite as their image in the wallet - i.e. those where we couldn't fetch the image

* Add icons in preparation for merge
  • Loading branch information
BellringerQuinn authored Oct 26, 2023
1 parent 3d9b411 commit 64b231f
Show file tree
Hide file tree
Showing 33 changed files with 6,820 additions and 17,569 deletions.
2,758 changes: 2,477 additions & 281 deletions Assets/SequenceExamples/Prefabs/WalletPanel.prefab

Large diffs are not rendered by default.

20,691 changes: 3,454 additions & 17,237 deletions Assets/SequenceExamples/Scenes/Demo.unity

Large diffs are not rendered by default.

369 changes: 369 additions & 0 deletions Assets/SequenceExamples/Scripts/ContentFetcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,369 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Threading.Tasks;
using Sequence.Utils;
using UnityEngine;
using UnityEngine.Networking;
using Vector2 = UnityEngine.Vector2;

namespace Sequence.Demo
{
public class ContentFetcher : IContentFetcher
{
private Queue<TokenElement> _tokenQueue = new Queue<TokenElement>();
private Queue<NftElement> _nftQueue = new Queue<NftElement>();
private List<Chain> _includeChains;
private List<IIndexer> _indexers;
private Address _address;
private bool _more = true;
private bool _isFetching = false;
private Queue<TokenBalance>[] _collectionsToProcess;

private int _chainIndex;

public ContentFetcher(Address address, params Chain[] includeChains)
{
_address = address;

_includeChains = includeChains.ConvertToList();

_indexers = new List<IIndexer>();
int chains = _includeChains.Count;
for (int i = 0; i < chains; i++)
{
_indexers.Add(new ChainIndexer((int)_includeChains[i]));
}

_collectionsToProcess = new Queue<TokenBalance>[chains];
for (int i = 0; i < chains; i++)
{
_collectionsToProcess[i] = new Queue<TokenBalance>();
}
}

public event Action<FetchContentResult> OnContentFetch;
public event Action<CollectionProcessingResult> OnCollectionProcessing;

public async Task FetchContent(int pageSize)
{
_isFetching = true;
_chainIndex = 0;
int pageNumber = 0;
int indexers = _indexers.Count;
Debug.Log("Fetching content...");
while (_more)
{
GetTokenBalancesArgs args = new GetTokenBalancesArgs(
_address,
true,
new Page { page = pageNumber, pageSize = pageSize });
GetTokenBalancesReturn balances = await _indexers[_chainIndex].GetTokenBalances(args);
if (balances == null)
{
Debug.LogWarning(
$"Received an error from indexer when fetching token balances with args: {args}\nCheck chain status here: https://status.sequence.info/");

pageNumber = 0;
IncrementChainIndex(indexers);

continue;
}
Page returnedPage = balances.page;
AddTokensToQueues(balances.balances, _chainIndex);
if (returnedPage.more)
{
pageNumber = returnedPage.page;
}
else
{
pageNumber = 0;
IncrementChainIndex(indexers);
}
OnContentFetch?.Invoke(new FetchContentResult(balances.balances, _more));
}

for (int i = 0; i < indexers; i++)
{
await ProcessCollectionsFromChain(i, pageSize);
}
}

private void IncrementChainIndex(int indexers)
{
_chainIndex++;
if (_chainIndex >= indexers)
{
Debug.Log("No more chains to fetch from.");
_more = false;
}
else
{
Debug.Log($"Moving to next chain... {(Chain)(int)_indexers[_chainIndex].GetChainID()}");
}
}

private async Task AddTokensToQueues(TokenBalance[] tokenBalances, int indexerIndex)
{
int items = tokenBalances.Length;
for (int i = 0; i < items; i++)
{
if (tokenBalances[i].IsToken())
{
TokenElement token = await BuildTokenElement(tokenBalances[i]);
if (token != null)
{
_tokenQueue.Enqueue(token);
}
}else if (tokenBalances[i].IsNft())
{
_collectionsToProcess[indexerIndex].Enqueue(tokenBalances[i]);
}
}
}

private async Task<TokenElement> BuildTokenElement(TokenBalance tokenBalance)
{
Sprite tokenIconSprite = await FetchIconSprite(tokenBalance);

ContractInfo contractInfo = tokenBalance.contractInfo;
if (contractInfo == null)
{
Debug.LogWarning($"No contractInfo found for given token: {tokenBalance}");
return null;
}

BigInteger balance = tokenBalance.balance / (BigInteger)Math.Pow(10, (int)contractInfo.decimals);

try
{
return new TokenElement(tokenBalance.contractAddress, tokenIconSprite, contractInfo.name,
(Chain)(int)contractInfo.chainId, (uint)balance, contractInfo.symbol,
new MockCurrencyConverter()); // Todo replace MockCurrencyConverter with real implementation
}
catch (Exception e)
{
Debug.LogError($"Failed to build token element for token: {tokenBalance}\nError: {e.Message}");
return null;
}
}

private async Task ProcessCollectionsFromChain(int chainIndex, int pageSize)
{
Queue<TokenBalance> toProcess = _collectionsToProcess[chainIndex];
while (toProcess.TryDequeue(out TokenBalance tokenBalance))
{
Debug.Log($"Processing collections from {(Chain)(int)_indexers[chainIndex].GetChainID()}. Collections to process: {_collectionsToProcess[chainIndex].Count}");
await ProcessCollection(tokenBalance, _indexers[chainIndex], pageSize);
}
}

private async Task ProcessCollection(TokenBalance tokenBalance, IIndexer indexer, int pageSize)
{
bool more = true;
int pageNumber = 0;
int nftsFound = 0;
while (more)
{
GetTokenBalancesReturn balances = await indexer.GetTokenBalances(
new GetTokenBalancesArgs(
_address,
tokenBalance.contractAddress,
true,
new Page { page = pageNumber, pageSize = pageSize }));
if (balances == null)
{
Debug.LogError($"Failed to finish processing collection: {tokenBalance}");
break;
}
Page returnedPage = balances.page;
if (returnedPage.more)
{
pageNumber = returnedPage.page;
}
else
{
more = false;
}

nftsFound += balances.balances.Length;

OnCollectionProcessing?.Invoke(new CollectionProcessingResult(balances.balances, more));
AddNftsToQueue(balances.balances);
}
}

private async Task AddNftsToQueue(TokenBalance[] tokenBalances)
{
int items = tokenBalances.Length;
for (int i = 0; i < items; i++)
{
if (tokenBalances[i].IsNft())
{
NftElement nft = await BuildNftElement(tokenBalances[i]);
if (nft != null)
{
_nftQueue.Enqueue(nft);
}
}
else
{
Debug.LogError($"Only ERC721/ERC1155s should be provided to this method! Given {tokenBalances[i]}");
}
}
}

private async Task<NftElement> BuildNftElement(TokenBalance tokenBalance)
{
Sprite collectionIconSprite = await FetchIconSprite(tokenBalance);
Sprite nftIconSprite = await FetchNftImageSprite(tokenBalance);

ContractInfo contractInfo = tokenBalance.contractInfo;
if (contractInfo == null)
{
Debug.LogWarning($"No contractInfo found for given token: {tokenBalance}");
return null;
}

TokenMetadata metadata = tokenBalance.tokenMetadata;
if (metadata == null)
{
Debug.LogWarning($"No metadata found for given token: {tokenBalance}");
return null;
}

try
{
BigInteger balance = tokenBalance.balance;
if (contractInfo.decimals != 0)
{
balance = tokenBalance.balance / (BigInteger)Math.Pow(10, (int)contractInfo.decimals);
}
return new NftElement(new Address(tokenBalance.contractAddress), nftIconSprite, metadata.name,
collectionIconSprite, contractInfo.name, metadata.tokenId, (Chain)(int)contractInfo.chainId,
(uint)balance, 1,
new MockCurrencyConverter()); // Todo replace MockCurrencyConverter with real implementation
// Todo figure out ethValue
}
catch (Exception e)
{
Debug.LogWarning($"Failed to build NFT element for token: {tokenBalance}\nError: {e.Message}");
return null;
}
}

private async Task<Sprite> FetchIconSprite(TokenBalance tokenBalance)
{
string metadataUrl = "";
ContractInfo contractInfo = tokenBalance.contractInfo;
if (contractInfo != null && contractInfo.logoURI != null && contractInfo.logoURI.Length > 0)
{
metadataUrl = contractInfo.logoURI;
}
else
{
Debug.LogWarning($"No metadata URL found for given token: {tokenBalance}");
}

Sprite iconSprite = await SpriteFetcher.Fetch(metadataUrl);
return iconSprite;
}

private async Task<Sprite> FetchNftImageSprite(TokenBalance tokenBalance)
{
string metadataUrl = "";
TokenMetadata metadata = tokenBalance.tokenMetadata;
if (metadata != null && metadata.image != null && metadata.image.Length > 0)
{
metadataUrl = metadata.image;
}
else
{
Debug.Log($"No metadata URL found for given token: {tokenBalance}");
}

Sprite iconSprite = await SpriteFetcher.Fetch(metadataUrl);
return iconSprite;
}

public async Task<FetchTokenContentResult> FetchTokenContent(int maxToFetch)
{
int tokensFetched = _tokenQueue.Count;
while (tokensFetched < maxToFetch && _more)
{
if (!_isFetching)
{
FetchContent(maxToFetch);
}
await Task.Yield();
tokensFetched = _tokenQueue.Count;
}

TokenElement[] tokens = new TokenElement[tokensFetched];
for (int i = 0; i < tokensFetched; i++)
{
tokens[i] = _tokenQueue.Dequeue();
}

return new FetchTokenContentResult(tokens, _more || _tokenQueue.Count > 0);
}

public async Task<FetchNftContentResult> FetchNftContent(int maxToFetch)
{
int nftsFetched = _nftQueue.Count;
while (nftsFetched < maxToFetch && (_more || CollectionsLeftToProcess()))
{
if (!_isFetching)
{
FetchContent(maxToFetch);
}
await Task.Yield();
nftsFetched = _nftQueue.Count;
}
NftElement[] nfts = new NftElement[nftsFetched];
for (int i = 0; i < nftsFetched; i++)
{
nfts[i] = _nftQueue.Dequeue();
}

return new FetchNftContentResult(nfts, _more || _nftQueue.Count > 0 || CollectionsLeftToProcess());
}

public Address GetAddress()
{
return _address;
}

public void RefreshTokens()
{
_tokenQueue = new Queue<TokenElement>();
_more = true;
_isFetching = false;
}

public void RefreshNfts()
{
_nftQueue = new Queue<NftElement>();
int chains = _includeChains.Count;
_collectionsToProcess = new Queue<TokenBalance>[chains];
for (int i = 0; i < chains; i++)
{
_collectionsToProcess[i] = new Queue<TokenBalance>();
}
}

private bool CollectionsLeftToProcess()
{
int chains = _collectionsToProcess.Length;
for (int i = 0; i < chains; i++)
{
if (_collectionsToProcess[i].Count > 0)
{
return true;
}
}

return false;
}
}
}
3 changes: 3 additions & 0 deletions Assets/SequenceExamples/Scripts/ContentFetcher.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 64b231f

Please sign in to comment.