Skip to content

Commit 813070e

Browse files
noescape00rowandh
authored andcommitted
NFT ids cashing (#750)
* init * logs search * Bugfixes * additional api method and bug fixes * fixes per review
1 parent 3c98e9b commit 813070e

File tree

3 files changed

+294
-3
lines changed

3 files changed

+294
-3
lines changed

src/Stratis.Features.Unity3dApi/Controllers/Unity3dController.cs

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,12 @@ public class Unity3dController : Controller
7171

7272
private readonly ILocalExecutor localExecutor;
7373

74+
private readonly INFTTransferIndexer NFTTransferIndexer;
75+
7476
public Unity3dController(ILoggerFactory loggerFactory, IAddressIndexer addressIndexer,
75-
IBlockStore blockStore, IChainState chainState, Network network, ICoinView coinView, WalletController walletController, ChainIndexer chainIndexer, IStakeChain stakeChain = null,
76-
IContractPrimitiveSerializer primitiveSerializer = null, IStateRepositoryRoot stateRoot = null, IContractAssemblyCache contractAssemblyCache = null,
77+
IBlockStore blockStore, IChainState chainState, Network network, ICoinView coinView, WalletController walletController, ChainIndexer chainIndexer, INFTTransferIndexer NFTTransferIndexer,
78+
IStakeChain stakeChain = null,
79+
IContractPrimitiveSerializer primitiveSerializer = null, IStateRepositoryRoot stateRoot = null, IContractAssemblyCache contractAssemblyCache = null,
7780
IReceiptRepository receiptRepository = null, ISmartContractTransactionService smartContractTransactionService = null, ILocalExecutor localExecutor = null)
7881
{
7982
Guard.NotNull(loggerFactory, nameof(loggerFactory));
@@ -86,6 +89,7 @@ public Unity3dController(ILoggerFactory loggerFactory, IAddressIndexer addressIn
8689
this.walletController = Guard.NotNull(walletController, nameof(walletController));
8790
this.chainIndexer = Guard.NotNull(chainIndexer, nameof(chainIndexer));
8891
this.stakeChain = stakeChain;
92+
this.NFTTransferIndexer = NFTTransferIndexer;
8993

9094
this.primitiveSerializer = primitiveSerializer;
9195
this.stateRoot = stateRoot;
@@ -558,6 +562,42 @@ public async Task<List<ReceiptResponse>> ReceiptSearchAPI([FromQuery] string con
558562
return result;
559563
}
560564

565+
[Route("api/[controller]/watch_nft_contract")]
566+
[HttpGet]
567+
[ProducesResponseType((int)HttpStatusCode.OK)]
568+
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
569+
public void WatchNFTContract([FromQuery] string contractAddress)
570+
{
571+
this.NFTTransferIndexer.WatchNFTContract(contractAddress);
572+
}
573+
574+
[Route("api/[controller]/get_watched_nft_contracts")]
575+
[HttpGet]
576+
[ProducesResponseType((int)HttpStatusCode.OK)]
577+
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
578+
public List<string> GetWatchedNFTContracts()
579+
{
580+
return this.NFTTransferIndexer.GetWatchedNFTContracts();
581+
}
582+
583+
[Route("api/[controller]/get_owned_nfts")]
584+
[HttpGet]
585+
[ProducesResponseType((int)HttpStatusCode.OK)]
586+
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
587+
public OwnedNFTsModel GetOwnedNFTs([FromQuery] string ownerAddress)
588+
{
589+
return this.NFTTransferIndexer.GetOwnedNFTs(ownerAddress);
590+
}
591+
592+
[Route("api/[controller]/get_all_nft_owners_by_contract_address")]
593+
[HttpGet]
594+
[ProducesResponseType((int)HttpStatusCode.OK)]
595+
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
596+
public NFTContractModel GetAllNFTOwnersByContractAddress([FromQuery] string contractAddress)
597+
{
598+
return this.NFTTransferIndexer.GetAllNFTOwnersByContractAddress(contractAddress);
599+
}
600+
561601
/// <summary>
562602
/// If the call is to a property, rewrites the method name to the getter method's name.
563603
/// </summary>
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Runtime.InteropServices;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using LiteDB;
9+
using Microsoft.Extensions.Logging;
10+
using NBitcoin;
11+
using Newtonsoft.Json;
12+
using Stratis.Bitcoin.AsyncWork;
13+
using Stratis.Bitcoin.Configuration;
14+
using Stratis.Bitcoin.Features.BlockStore.AddressIndexing;
15+
using Stratis.Bitcoin.Features.SmartContracts.Models;
16+
using Stratis.Bitcoin.Features.SmartContracts.Wallet;
17+
using FileMode = LiteDB.FileMode;
18+
19+
namespace Stratis.Features.Unity3dApi
20+
{
21+
public interface INFTTransferIndexer : IDisposable
22+
{
23+
/// <summary>Initialized NFT indexer.</summary>
24+
void Initialize();
25+
26+
/// <summary>Adds NFT contract to watch list. Only contracts from the watch list are being indexed.</summary>
27+
void WatchNFTContract(string contractAddress);
28+
29+
/// <summary>Provides a list of all nft contract addresses that are being tracked.</summary>
30+
List<string> GetWatchedNFTContracts();
31+
32+
/// <summary>Provides collection of NFT ids that belong to provided user's address for watched contracts.</summary>
33+
OwnedNFTsModel GetOwnedNFTs(string address);
34+
35+
/// <summary>Returns collection of all users that own nft.</summary>
36+
NFTContractModel GetAllNFTOwnersByContractAddress(string contractAddress);
37+
}
38+
39+
/// <summary>This component maps addresses to NFT Ids they own.</summary>
40+
public class NFTTransferIndexer : INFTTransferIndexer
41+
{
42+
public ChainedHeader IndexerTip { get; private set; }
43+
44+
private const string DatabaseFilename = "NFTTransferIndexer.litedb";
45+
private const string DbOwnedNFTsKey = "OwnedNfts";
46+
private const int SyncBufferBlocks = 50;
47+
48+
private readonly DataFolder dataFolder;
49+
private readonly ILogger logger;
50+
private readonly ChainIndexer chainIndexer;
51+
private readonly IAsyncProvider asyncProvider;
52+
private readonly ISmartContractTransactionService smartContractTransactionService;
53+
54+
private LiteDatabase db;
55+
private LiteCollection<NFTContractModel> NFTContractCollection;
56+
private CancellationTokenSource cancellation;
57+
private Task indexingTask;
58+
59+
public NFTTransferIndexer(DataFolder dataFolder, ILoggerFactory loggerFactory, IAsyncProvider asyncProvider, ChainIndexer chainIndexer, ISmartContractTransactionService smartContractTransactionService)
60+
{
61+
this.dataFolder = dataFolder;
62+
this.cancellation = new CancellationTokenSource();
63+
this.asyncProvider = asyncProvider;
64+
this.chainIndexer = chainIndexer;
65+
this.smartContractTransactionService = smartContractTransactionService;
66+
67+
this.logger = loggerFactory.CreateLogger(this.GetType().FullName);
68+
}
69+
70+
/// <inheritdoc />
71+
public void Initialize()
72+
{
73+
if (this.db != null)
74+
throw new Exception("NFTTransferIndexer already initialized!");
75+
76+
string dbPath = Path.Combine(this.dataFolder.RootPath, DatabaseFilename);
77+
78+
FileMode fileMode = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? FileMode.Exclusive : FileMode.Shared;
79+
this.db = new LiteDatabase(new ConnectionString() { Filename = dbPath, Mode = fileMode });
80+
this.NFTContractCollection = this.db.GetCollection<NFTContractModel>(DbOwnedNFTsKey);
81+
82+
this.indexingTask = Task.Run(async () => await this.IndexNFTsContinuouslyAsync().ConfigureAwait(false));
83+
this.asyncProvider.RegisterTask($"{nameof(AddressIndexer)}.{nameof(this.indexingTask)}", this.indexingTask);
84+
}
85+
86+
/// <inheritdoc />
87+
public void WatchNFTContract(string contractAddress)
88+
{
89+
if (!this.NFTContractCollection.Exists(x => x.ContractAddress == contractAddress))
90+
{
91+
NFTContractModel model = new NFTContractModel()
92+
{
93+
ContractAddress = contractAddress,
94+
LastUpdatedBlock = 0,
95+
OwnedIDsByAddress = new Dictionary<string, List<long>>()
96+
};
97+
98+
this.NFTContractCollection.Upsert(model);
99+
}
100+
}
101+
102+
/// <inheritdoc />
103+
public List<string> GetWatchedNFTContracts()
104+
{
105+
return this.NFTContractCollection.FindAll().Select(x => x.ContractAddress).ToList();
106+
}
107+
108+
/// <inheritdoc />
109+
public OwnedNFTsModel GetOwnedNFTs(string address)
110+
{
111+
List<NFTContractModel> NFTContractModels = this.NFTContractCollection.FindAll().Where(x => x.OwnedIDsByAddress.ContainsKey(address)).ToList();
112+
113+
OwnedNFTsModel output = new OwnedNFTsModel() { OwnedIDsByContractAddress = new Dictionary<string, List<long>>() };
114+
115+
foreach (NFTContractModel contractModel in NFTContractModels)
116+
{
117+
List<long> ids = contractModel.OwnedIDsByAddress[address];
118+
output.OwnedIDsByContractAddress.Add(contractModel.ContractAddress, ids);
119+
}
120+
121+
return output;
122+
}
123+
124+
public NFTContractModel GetAllNFTOwnersByContractAddress(string contractAddress)
125+
{
126+
NFTContractModel currentContract = this.NFTContractCollection.FindOne(x => x.ContractAddress == contractAddress);
127+
return currentContract;
128+
}
129+
130+
private async Task IndexNFTsContinuouslyAsync()
131+
{
132+
await Task.Delay(1);
133+
134+
try
135+
{
136+
while (!this.cancellation.Token.IsCancellationRequested)
137+
{
138+
List<string> contracts = this.NFTContractCollection.FindAll().Select(x => x.ContractAddress).ToList();
139+
140+
foreach (string contractAddr in contracts)
141+
{
142+
if (this.cancellation.Token.IsCancellationRequested)
143+
break;
144+
145+
NFTContractModel currentContract = this.NFTContractCollection.FindOne(x => x.ContractAddress == contractAddr);
146+
147+
ChainedHeader chainTip = this.chainIndexer.Tip;
148+
149+
List<ReceiptResponse> receipts = this.smartContractTransactionService.ReceiptSearch(
150+
contractAddr, "TransferLog", null, currentContract.LastUpdatedBlock, null);
151+
152+
if (receipts == null)
153+
continue;
154+
155+
int lastReceiptHeight = 0;
156+
if (receipts.Any())
157+
lastReceiptHeight = (int)receipts.Last().BlockNumber.Value;
158+
159+
currentContract.LastUpdatedBlock = new List<int>() { chainTip.Height, lastReceiptHeight }.Max();
160+
161+
List<TransferLog> transferLogs = new List<TransferLog>(receipts.Count);
162+
163+
foreach (ReceiptResponse receiptRes in receipts)
164+
{
165+
string jsonLog = Newtonsoft.Json.JsonConvert.SerializeObject(receiptRes.Logs.First().Log);
166+
167+
TransferLog infoObj = JsonConvert.DeserializeObject<TransferLog>(jsonLog);
168+
transferLogs.Add(infoObj);
169+
}
170+
171+
foreach (TransferLog transferInfo in transferLogs)
172+
{
173+
if (currentContract.OwnedIDsByAddress.ContainsKey(transferInfo.From))
174+
{
175+
currentContract.OwnedIDsByAddress[transferInfo.From].Remove(transferInfo.TokenId);
176+
177+
if (currentContract.OwnedIDsByAddress[transferInfo.From].Count == 0)
178+
currentContract.OwnedIDsByAddress.Remove(transferInfo.From);
179+
}
180+
181+
if (!currentContract.OwnedIDsByAddress.ContainsKey(transferInfo.To))
182+
currentContract.OwnedIDsByAddress.Add(transferInfo.To, new List<long>());
183+
184+
currentContract.OwnedIDsByAddress[transferInfo.To].Add(transferInfo.TokenId);
185+
}
186+
187+
this.NFTContractCollection.Upsert(currentContract);
188+
}
189+
190+
try
191+
{
192+
await Task.Delay(TimeSpan.FromSeconds(6), this.cancellation.Token);
193+
}
194+
catch (TaskCanceledException)
195+
{
196+
}
197+
}
198+
}
199+
catch (Exception e)
200+
{
201+
this.logger.LogError(e.ToString());
202+
}
203+
}
204+
205+
public void Dispose()
206+
{
207+
this.cancellation.Cancel();
208+
this.indexingTask?.GetAwaiter().GetResult();
209+
this.db?.Dispose();
210+
}
211+
}
212+
213+
public class NFTContractModel
214+
{
215+
public int Id { get; set; }
216+
217+
public string ContractAddress { get; set; }
218+
219+
// Key is nft owner address, value is list of NFT IDs
220+
public Dictionary<string, List<long>> OwnedIDsByAddress { get; set; }
221+
222+
public int LastUpdatedBlock { get; set; }
223+
}
224+
225+
public class OwnedNFTsModel
226+
{
227+
public Dictionary<string, List<long>> OwnedIDsByContractAddress { get; set; }
228+
}
229+
230+
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.4.3.0 (Newtonsoft.Json v11.0.0.0)")]
231+
public partial class TransferLog
232+
{
233+
[JsonProperty("from", Required = Required.DisallowNull, NullValueHandling = NullValueHandling.Ignore)]
234+
public string From { get; set; }
235+
236+
[JsonProperty("to", Required = Required.DisallowNull, NullValueHandling = NullValueHandling.Ignore)]
237+
public string To { get; set; }
238+
239+
[JsonProperty("tokenId", Required = Required.DisallowNull, NullValueHandling = NullValueHandling.Ignore)]
240+
public long TokenId { get; set; }
241+
}
242+
}

src/Stratis.Features.Unity3dApi/Unity3dApiFeature.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,21 @@ public sealed class Unity3dApiFeature : FullNodeFeature
3434

3535
private readonly ICertificateStore certificateStore;
3636

37+
private readonly INFTTransferIndexer NFTTransferIndexer;
38+
3739
public Unity3dApiFeature(
3840
IFullNodeBuilder fullNodeBuilder,
3941
FullNode fullNode,
4042
Unity3dApiSettings apiSettings,
4143
ILoggerFactory loggerFactory,
42-
ICertificateStore certificateStore)
44+
ICertificateStore certificateStore,
45+
INFTTransferIndexer NFTTransferIndexer)
4346
{
4447
this.fullNodeBuilder = fullNodeBuilder;
4548
this.fullNode = fullNode;
4649
this.apiSettings = apiSettings;
4750
this.certificateStore = certificateStore;
51+
this.NFTTransferIndexer = NFTTransferIndexer;
4852
this.logger = loggerFactory.CreateLogger(this.GetType().FullName);
4953

5054
this.InitializeBeforeBase = true;
@@ -61,6 +65,8 @@ public override Task InitializeAsync()
6165
this.logger.LogInformation("Unity API starting on URL '{0}'.", this.apiSettings.ApiUri);
6266
this.webHost = Program.Initialize(this.fullNodeBuilder.Services, this.fullNode, this.apiSettings, this.certificateStore, new WebHostBuilder());
6367

68+
this.NFTTransferIndexer.Initialize();
69+
6470
if (this.apiSettings.KeepaliveTimer == null)
6571
{
6672
this.logger.LogTrace("(-)[KEEPALIVE_DISABLED]");
@@ -120,6 +126,8 @@ public override void Dispose()
120126
this.webHost.StopAsync(TimeSpan.FromSeconds(ApiStopTimeoutSeconds)).Wait();
121127
this.webHost = null;
122128
}
129+
130+
this.NFTTransferIndexer.Dispose();
123131
}
124132
}
125133

@@ -139,6 +147,7 @@ public static IFullNodeBuilder UseUnity3dApi(this IFullNodeBuilder fullNodeBuild
139147
services.AddSingleton(fullNodeBuilder);
140148
services.AddSingleton<Unity3dApiSettings>();
141149
services.AddSingleton<ICertificateStore, CertificateStore>();
150+
services.AddSingleton<INFTTransferIndexer, NFTTransferIndexer>();
142151

143152
// Controller
144153
services.AddTransient<Unity3dController>();

0 commit comments

Comments
 (0)