Skip to content

Commit

Permalink
RC v1.1.0 - Load All NFTs (#6)
Browse files Browse the repository at this point in the history
* Add wallet explorer endpoint

---------

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
  • Loading branch information
jacobpretorius and dependabot[bot] authored May 3, 2024
1 parent 0995c46 commit 0c983af
Show file tree
Hide file tree
Showing 11 changed files with 198 additions and 9 deletions.
2 changes: 1 addition & 1 deletion UniversalNFT.dev.API/Controllers/NFTController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
namespace UniversalNFT.dev.API.Controllers
{
/// <summary>
/// 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.
/// </summary>
[ApiController]
[Route("v1.0/NFT")]
Expand Down
42 changes: 42 additions & 0 deletions UniversalNFT.dev.API/Controllers/WalletController.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Return all NFTs in a specific wallet in our UniversalNFTResponseV1 format.
/// </summary>
[ApiController]
[Route("v1.0/Wallet")]
public class WalletController : ControllerBase
{
private readonly INFTService _nftService;

public WalletController(INFTService nftService)
{
_nftService = nftService;
}

/// <summary>
/// Return all NFTs in a wallet in our UniversalNFTResponseV1 format.
/// </summary>
[HttpGet]
[SwaggerResponse(200, "The wallet NFTs are loaded and thumbnail cached sucessfully", typeof(IEnumerable<UniversalNFTResponseV1>))]
[SwaggerResponse(404, "The wallet could not be found")]
public async Task<IActionResult> 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);
}
}
}
1 change: 0 additions & 1 deletion UniversalNFT.dev.API/Facades/HttpFacade.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,5 @@ public class HttpFacade : IHttpFacade

return null;
}

}
}
3 changes: 1 addition & 2 deletions UniversalNFT.dev.API/Facades/IHttpFacade.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@

namespace UniversalNFT.dev.API.Facades
namespace UniversalNFT.dev.API.Facades
{
public interface IHttpFacade
{
Expand Down
2 changes: 1 addition & 1 deletion UniversalNFT.dev.API/Models/API/Artv0Response.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }

Check warning on line 14 in UniversalNFT.dev.API/Models/API/Artv0Response.cs

View workflow job for this annotation

GitHub Actions / build (Release)

Non-nullable property 'name' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

[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; }

Check warning on line 17 in UniversalNFT.dev.API/Models/API/Artv0Response.cs

View workflow job for this annotation

GitHub Actions / build (Release)

Non-nullable property 'description' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
Expand Down
3 changes: 2 additions & 1 deletion UniversalNFT.dev.API/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
2 changes: 2 additions & 0 deletions UniversalNFT.dev.API/Services/AppSettings/XRPLSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@
public class XRPLSettings
{
public string? XRPLServerAddress { get; set; }

public bool EnableDelay { get; set; } = true;
}
}
2 changes: 2 additions & 0 deletions UniversalNFT.dev.API/Services/NFT/INFTService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@ public interface INFTService
Task<UniversalNFTResponseV1> GetNFT(string NFTokenID, string OwnerWalletAddress);

Task<Artv0Response> GetArtv0(string NFTokenID, string OwnerWalletAddress);

Task<IEnumerable<UniversalNFTResponseV1>> GetAllNFTs(string OwnerWalletAddress);
}
}
50 changes: 50 additions & 0 deletions UniversalNFT.dev.API/Services/NFT/NFTService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ public NFTService(
_serverSettings = serverSettings.Value;
}

/// <summary>
/// Load a specific NFT from a given wallet and return it in our format
/// </summary>
public async Task<UniversalNFTResponseV1> GetNFT(
string NFTokenID,
string OwnerWalletAddress)
Expand Down Expand Up @@ -68,6 +71,53 @@ public async Task<UniversalNFTResponseV1> GetNFT(
return null;
}

/// <summary>
/// Load all NFTs in a wallet and process them into our format
/// </summary>
public async Task<IEnumerable<UniversalNFTResponseV1>> GetAllNFTs(string OwnerWalletAddress)
{
var accountNftsResult = new List<UniversalNFTResponseV1>();
try
{
// Load the NFT from XRPL
var accountNfts = await _xrplService.GetAllNFTs(OwnerWalletAddress);
if (accountNfts?.Any() != true)
return Enumerable.Empty<UniversalNFTResponseV1>();

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<UniversalNFTResponseV1>();
}

/// <summary>
/// Load a specific NFT from a given wallet and return it in art.v0 format
/// </summary>
public async Task<Artv0Response> GetArtv0(
string NFTokenID,
string OwnerWalletAddress)
Expand Down
2 changes: 2 additions & 0 deletions UniversalNFT.dev.API/Services/XRPL/IXRPLService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ namespace UniversalNFT.dev.API.Services.XRPL
{
public interface IXRPLService
{
Task<IEnumerable<RippledAccountNFToken>> GetAllNFTs(string ownerAccount);

Task<RippledAccountNFToken?> GetNFT(string tokenID, string ownerAccount);
}
}
98 changes: 95 additions & 3 deletions UniversalNFT.dev.API/Services/XRPL/XRPLService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,12 @@ public XRPLService(IOptions<XRPLSettings> xrplSettings, ILogger<XRPLService> 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)
{
Expand Down Expand Up @@ -132,4 +134,94 @@ public XRPLService(IOptions<XRPLSettings> xrplSettings, ILogger<XRPLService> log

return null;
}

public async Task<IEnumerable<RippledAccountNFToken>> GetAllNFTs(string ownerAccount)
{
var accountNfts = new List<RippledAccountNFToken>();

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<RippledAccountNFTsResponse>(data);

foreach (var accountNft in resultObj?.Result?.NFTs ?? Enumerable.Empty<RippledAccountNFToken>())
{
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<RippledAccountNFTsResponse>(seekData);

foreach (var seekAccountNft in seekResultObj?.Result?.NFTs ?? Enumerable.Empty<RippledAccountNFToken>())
{
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;
}
}

0 comments on commit 0c983af

Please sign in to comment.