diff --git a/UniversalNFT.dev.API/Controllers/NFTController.cs b/UniversalNFT.dev.API/Controllers/NFTController.cs
index dd6481d..209483d 100644
--- a/UniversalNFT.dev.API/Controllers/NFTController.cs
+++ b/UniversalNFT.dev.API/Controllers/NFTController.cs
@@ -7,7 +7,7 @@
namespace UniversalNFT.dev.API.Controllers
{
///
- /// Load and translate metadata for an NFToken on the blockchain and return it in the specified format.
+ /// Load and translate metadata for a specfic NFToken on the blockchain and return it in the specified format.
///
[ApiController]
[Route("v1.0/NFT")]
diff --git a/UniversalNFT.dev.API/Controllers/WalletController.cs b/UniversalNFT.dev.API/Controllers/WalletController.cs
new file mode 100644
index 0000000..0b1696a
--- /dev/null
+++ b/UniversalNFT.dev.API/Controllers/WalletController.cs
@@ -0,0 +1,42 @@
+using Microsoft.AspNetCore.Mvc;
+using Swashbuckle.AspNetCore.Annotations;
+using UniversalNFT.dev.API.Models.API;
+using UniversalNFT.dev.API.Services.NFT;
+using UniversalNFT.dev.API.SwaggerConfig;
+
+namespace UniversalNFT.dev.API.Controllers
+{
+ ///
+ /// Return all NFTs in a specific wallet in our UniversalNFTResponseV1 format.
+ ///
+ [ApiController]
+ [Route("v1.0/Wallet")]
+ public class WalletController : ControllerBase
+ {
+ private readonly INFTService _nftService;
+
+ public WalletController(INFTService nftService)
+ {
+ _nftService = nftService;
+ }
+
+ ///
+ /// Return all NFTs in a wallet in our UniversalNFTResponseV1 format.
+ ///
+ [HttpGet]
+ [SwaggerResponse(200, "The wallet NFTs are loaded and thumbnail cached sucessfully", typeof(IEnumerable))]
+ [SwaggerResponse(404, "The wallet could not be found")]
+ public async Task Get(
+ [SwaggerParameter("The XRPL wallet to load NFTs from", Required = true)]
+ [SwaggerTryItOutDefaultValue("rPpMSFxzjqJ6AGgEZ8kgbQeeo6UJvUkVmb")]
+ string OwnerWalletAddress)
+ {
+ var response = await _nftService.GetAllNFTs(OwnerWalletAddress);
+
+ if (response == null)
+ return NotFound();
+
+ return new JsonResult(response);
+ }
+ }
+}
diff --git a/UniversalNFT.dev.API/Facades/HttpFacade.cs b/UniversalNFT.dev.API/Facades/HttpFacade.cs
index 7f0b4d0..c9938f1 100644
--- a/UniversalNFT.dev.API/Facades/HttpFacade.cs
+++ b/UniversalNFT.dev.API/Facades/HttpFacade.cs
@@ -18,6 +18,5 @@ public class HttpFacade : IHttpFacade
return null;
}
-
}
}
diff --git a/UniversalNFT.dev.API/Facades/IHttpFacade.cs b/UniversalNFT.dev.API/Facades/IHttpFacade.cs
index 9efc92f..f142f3d 100644
--- a/UniversalNFT.dev.API/Facades/IHttpFacade.cs
+++ b/UniversalNFT.dev.API/Facades/IHttpFacade.cs
@@ -1,5 +1,4 @@
-
-namespace UniversalNFT.dev.API.Facades
+namespace UniversalNFT.dev.API.Facades
{
public interface IHttpFacade
{
diff --git a/UniversalNFT.dev.API/Models/API/Artv0Response.cs b/UniversalNFT.dev.API/Models/API/Artv0Response.cs
index 7f3345f..cfbf055 100644
--- a/UniversalNFT.dev.API/Models/API/Artv0Response.cs
+++ b/UniversalNFT.dev.API/Models/API/Artv0Response.cs
@@ -11,7 +11,7 @@ public class Artv0Response
public string nftType { get => "art.v0"; }
[SwaggerSchema("The NFTokenID", Nullable = false)]
- public string name { get; set; }
+ public string name { get; set; }
[SwaggerSchema("The date and time in ISO format this request was last generated on the server e.g. 2023-05-20T15:30:00.0000000+00:00", Nullable = false)]
public string description { get; set; }
diff --git a/UniversalNFT.dev.API/Program.cs b/UniversalNFT.dev.API/Program.cs
index 40a5614..7788214 100644
--- a/UniversalNFT.dev.API/Program.cs
+++ b/UniversalNFT.dev.API/Program.cs
@@ -16,7 +16,8 @@
// Swagger
builder.Services.AddEndpointsApiExplorer();
-builder.Services.AddSwaggerGen(options => {
+builder.Services.AddSwaggerGen(options =>
+{
var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename), includeControllerXmlComments: true);
diff --git a/UniversalNFT.dev.API/Services/AppSettings/XRPLSettings.cs b/UniversalNFT.dev.API/Services/AppSettings/XRPLSettings.cs
index d844a91..1b043b4 100644
--- a/UniversalNFT.dev.API/Services/AppSettings/XRPLSettings.cs
+++ b/UniversalNFT.dev.API/Services/AppSettings/XRPLSettings.cs
@@ -3,5 +3,7 @@
public class XRPLSettings
{
public string? XRPLServerAddress { get; set; }
+
+ public bool EnableDelay { get; set; } = true;
}
}
diff --git a/UniversalNFT.dev.API/Services/NFT/INFTService.cs b/UniversalNFT.dev.API/Services/NFT/INFTService.cs
index e8fa89b..12848ba 100644
--- a/UniversalNFT.dev.API/Services/NFT/INFTService.cs
+++ b/UniversalNFT.dev.API/Services/NFT/INFTService.cs
@@ -7,5 +7,7 @@ public interface INFTService
Task GetNFT(string NFTokenID, string OwnerWalletAddress);
Task GetArtv0(string NFTokenID, string OwnerWalletAddress);
+
+ Task> GetAllNFTs(string OwnerWalletAddress);
}
}
\ No newline at end of file
diff --git a/UniversalNFT.dev.API/Services/NFT/NFTService.cs b/UniversalNFT.dev.API/Services/NFT/NFTService.cs
index 4167b29..c450c9d 100644
--- a/UniversalNFT.dev.API/Services/NFT/NFTService.cs
+++ b/UniversalNFT.dev.API/Services/NFT/NFTService.cs
@@ -27,6 +27,9 @@ public NFTService(
_serverSettings = serverSettings.Value;
}
+ ///
+ /// Load a specific NFT from a given wallet and return it in our format
+ ///
public async Task GetNFT(
string NFTokenID,
string OwnerWalletAddress)
@@ -68,6 +71,53 @@ public async Task GetNFT(
return null;
}
+ ///
+ /// Load all NFTs in a wallet and process them into our format
+ ///
+ public async Task> GetAllNFTs(string OwnerWalletAddress)
+ {
+ var accountNftsResult = new List();
+ try
+ {
+ // Load the NFT from XRPL
+ var accountNfts = await _xrplService.GetAllNFTs(OwnerWalletAddress);
+ if (accountNfts?.Any() != true)
+ return Enumerable.Empty();
+
+ foreach (var accountNft in accountNfts)
+ {
+ var imageUrl = await _rulesEngine.ProcessNFToken(accountNft) ?? string.Empty;
+ imageUrl = IPFSService.NormaliseUrl(imageUrl);
+
+ // We have an imageUrl extracted! Create the thumbnail if it doesn't exist
+ // in our cache.
+ var thumbnailFilename = await _imageService.CreateThumbnail(imageUrl, accountNft.NFTokenID);
+
+ accountNftsResult.Add(new UniversalNFTResponseV1
+ {
+ NFTokenID = accountNft.NFTokenID,
+ OwnerAccount = OwnerWalletAddress,
+ ImageThumbnailCacheUrl = !string.IsNullOrWhiteSpace(thumbnailFilename)
+ ? $"{_serverSettings.ServerExternalDomain}/v1.0/Image?file={thumbnailFilename}"
+ : string.Empty,
+ ImageUrl = imageUrl,
+ Timestamp = DateTime.UtcNow.ToString("O")
+ });
+ }
+
+ return accountNftsResult;
+ }
+ catch (Exception ex)
+ {
+ // Log it if you care
+ }
+
+ return Enumerable.Empty();
+ }
+
+ ///
+ /// Load a specific NFT from a given wallet and return it in art.v0 format
+ ///
public async Task GetArtv0(
string NFTokenID,
string OwnerWalletAddress)
diff --git a/UniversalNFT.dev.API/Services/XRPL/IXRPLService.cs b/UniversalNFT.dev.API/Services/XRPL/IXRPLService.cs
index 958e95e..0b2a1c5 100644
--- a/UniversalNFT.dev.API/Services/XRPL/IXRPLService.cs
+++ b/UniversalNFT.dev.API/Services/XRPL/IXRPLService.cs
@@ -4,6 +4,8 @@ namespace UniversalNFT.dev.API.Services.XRPL
{
public interface IXRPLService
{
+ Task> GetAllNFTs(string ownerAccount);
+
Task GetNFT(string tokenID, string ownerAccount);
}
}
\ No newline at end of file
diff --git a/UniversalNFT.dev.API/Services/XRPL/XRPLService.cs b/UniversalNFT.dev.API/Services/XRPL/XRPLService.cs
index 6cd19d0..048a06b 100644
--- a/UniversalNFT.dev.API/Services/XRPL/XRPLService.cs
+++ b/UniversalNFT.dev.API/Services/XRPL/XRPLService.cs
@@ -83,10 +83,12 @@ public XRPLService(IOptions xrplSettings, ILogger log
]}";
// Lets be kind to the free cluster servers, for better performance
- // host your own Rippled node and remove the next line! :)
- Thread.Sleep(200);
+ // host your own Rippled node and disable with appsettings.json
+ // "XRPLSettings": { "EnableDelay" : false }
+ if (_xrplSettings.EnableDelay)
+ Thread.Sleep(200);
- var seekResponse = await _httpClient.PostAsync(_xrplSettings.XRPLServerAddress,
+ var seekResponse = await _httpClient.PostAsync(_xrplSettings.XRPLServerAddress,
new StringContent(body));
if (seekResponse.IsSuccessStatusCode)
{
@@ -132,4 +134,94 @@ public XRPLService(IOptions xrplSettings, ILogger log
return null;
}
+
+ public async Task> GetAllNFTs(string ownerAccount)
+ {
+ var accountNfts = new List();
+
+ try
+ {
+ // Setup the request body to load account NFTs
+ var body = @"{ ""method"": ""account_nfts"",
+ ""params"": [
+ {
+ ""account"": """ + ownerAccount + @""",
+ ""ledger_index"": ""validated"",
+ ""limit"": 1000
+ }
+ ]}";
+
+ // Load account NFTs from Rippled
+ var response = await _httpClient.PostAsync(_xrplSettings.XRPLServerAddress, new StringContent(body));
+ if (response.IsSuccessStatusCode)
+ {
+ // Parse success response
+ var data = await response.Content.ReadAsStringAsync();
+ var resultObj = JsonSerializer.Deserialize(data);
+
+ foreach (var accountNft in resultObj?.Result?.NFTs ?? Enumerable.Empty())
+ {
+ if (!string.IsNullOrWhiteSpace(accountNft.URI))
+ {
+ var convertedUri = Encoding.UTF8.GetString(HexHelper.StringToByteArray(accountNft.URI));
+ accountNft.URI = IPFSService.NormaliseUrl(convertedUri);
+ }
+ accountNfts.Add(accountNft);
+ }
+
+ // Page if we have to keep going
+ var seek = resultObj?.Result?.Marker;
+ while (!string.IsNullOrWhiteSpace(seek))
+ {
+ body = @"{ ""method"": ""account_nfts"",
+ ""params"": [
+ {
+ ""account"": """ + ownerAccount + @""",
+ ""ledger_index"": ""validated"",
+ ""limit"": 1000,
+ ""marker"": """ + seek + @"""
+ }
+ ]}";
+
+ // Lets be kind to the free cluster servers, for better performance
+ // host your own Rippled node and disable with appsettings.json
+ // "XRPLSettings": { "EnableDelay" : false }
+ if (_xrplSettings.EnableDelay)
+ Thread.Sleep(200);
+
+ var seekResponse = await _httpClient.PostAsync(_xrplSettings.XRPLServerAddress,
+ new StringContent(body));
+ if (seekResponse.IsSuccessStatusCode)
+ {
+ // Parse success response
+ var seekData = await seekResponse.Content.ReadAsStringAsync();
+ var seekResultObj = JsonSerializer.Deserialize(seekData);
+
+ foreach (var seekAccountNft in seekResultObj?.Result?.NFTs ?? Enumerable.Empty())
+ {
+ if (!string.IsNullOrWhiteSpace(seekAccountNft.URI))
+ {
+ var convertedUri = Encoding.UTF8.GetString(HexHelper.StringToByteArray(seekAccountNft.URI));
+ seekAccountNft.URI = IPFSService.NormaliseUrl(convertedUri);
+ }
+ accountNfts.Add(seekAccountNft);
+ }
+
+ seek = seekResultObj?.Result?.Marker;
+ }
+ else
+ {
+ // Don't carry on seeking on error
+ seek = null;
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error getting NFT in XRPLService");
+ }
+
+ return accountNfts;
+ }
}