Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recipient API created #35

Merged
merged 12 commits into from
May 16, 2024
11 changes: 11 additions & 0 deletions src/ProjectOrigin.Stamp.Server/Database/IUnitOfWork.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using ProjectOrigin.Stamp.Server.Repositories;

namespace ProjectOrigin.Stamp.Server.Database;

public interface IUnitOfWork
{
void Commit();
void Rollback();

IRecipientRepository RecipientRepository { get; }
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System.Data;
using Microsoft.Extensions.Options;
using Npgsql;
using ProjectOrigin.Stamp.Server.Database.Postgres;

namespace ProjectOrigin.Stamp.Server.Database.Postgres;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS Recipients (
tnickelsen marked this conversation as resolved.
Show resolved Hide resolved
id uuid NOT NULL PRIMARY KEY,
wallet_endpoint_reference_version integer NOT NULL,
wallet_endpoint_reference_endpoint VARCHAR(512) NOT NULL,
wallet_endpoint_reference_public_key bytea NOT NULL
);
Empty file.
104 changes: 104 additions & 0 deletions src/ProjectOrigin.Stamp.Server/Database/UnitOfWork.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.Data;
using ProjectOrigin.Stamp.Server.Repositories;

namespace ProjectOrigin.Stamp.Server.Database;

public class UnitOfWork : IUnitOfWork, IDisposable
{
private IRecipientRepository _recipientRepository = null!;

public IRecipientRepository RecipientRepository
{
get
{
return _recipientRepository ??= new RecipientRepository(_lazyTransaction.Value.Connection ?? throw new InvalidOperationException("Transaction is null."));
}
}

private readonly Dictionary<Type, object> _repositories = new Dictionary<Type, object>();
private readonly Lazy<IDbConnection> _lazyConnection;
private Lazy<IDbTransaction> _lazyTransaction;
private bool _disposed = false;

public UnitOfWork(IDbConnectionFactory connectionFactory)
{
_lazyConnection = new Lazy<IDbConnection>(() =>
{
var connection = connectionFactory.CreateConnection();
connection.Open();
return connection;
});

_lazyTransaction = new Lazy<IDbTransaction>(_lazyConnection.Value.BeginTransaction);
}

public void Commit()
{
if (!_lazyTransaction.IsValueCreated)
return;

try
{
_lazyTransaction.Value.Commit();
}
catch
{
_lazyTransaction.Value.Rollback();
throw;
}
finally
{
ResetUnitOfWork();
}
}

public void Rollback()
{
if (!_lazyTransaction.IsValueCreated)
return;

_lazyTransaction.Value.Rollback();

ResetUnitOfWork();
}

private void ResetUnitOfWork()
{
if (_lazyTransaction.IsValueCreated)
_lazyTransaction.Value.Dispose();

_lazyTransaction = new Lazy<IDbTransaction>(_lazyConnection.Value.BeginTransaction);

_repositories.Clear();
}

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

~UnitOfWork() => Dispose(false);

protected virtual void Dispose(bool disposing)
{
if (disposing && !_disposed)
{
_disposed = true;

if (_lazyTransaction.IsValueCreated)
{
_lazyTransaction.Value.Dispose();
}

if (_lazyConnection.IsValueCreated)
{
_lazyConnection.Value.Dispose();
}
}

_repositories.Clear();
}
}
12 changes: 12 additions & 0 deletions src/ProjectOrigin.Stamp.Server/Models/Recipient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System;
using ProjectOrigin.HierarchicalDeterministicKeys.Interfaces;

namespace ProjectOrigin.Stamp.Server.Models;

public class Recipient
{
public required Guid Id { get; init; }
public required int WalletEndpointReferenceVersion { get; init; }
public required string WalletEndpointReferenceEndpoint { get; init; }
public required IHDPublicKey WalletEndpointReferencePublicKey { get; init; }
}
12 changes: 12 additions & 0 deletions src/ProjectOrigin.Stamp.Server/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"profiles": {
"ProjectOrigin.Stamp.Server": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:57881;http://localhost:57882"
}
}
}
46 changes: 46 additions & 0 deletions src/ProjectOrigin.Stamp.Server/Repositories/RecipientRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System;
using System.Data;
using System.Threading.Tasks;
using Dapper;
using ProjectOrigin.Stamp.Server.Models;

namespace ProjectOrigin.Stamp.Server.Repositories;

public interface IRecipientRepository
{
Task<int> Create(Recipient recipient);
Task<Recipient?> Get(Guid id);
}

public class RecipientRepository : IRecipientRepository
{
private readonly IDbConnection _connection;

public RecipientRepository(IDbConnection connection)
{
_connection = connection;
}

public Task<int> Create(Recipient recipient)
{
return _connection.ExecuteAsync(
@"INSERT INTO recipients (id, wallet_endpoint_reference_version, wallet_endpoint_reference_endpoint, wallet_endpoint_reference_public_key)
VALUES (@Id, @WalletEndpointReferenceVersion, @WalletEndpointReferenceEndpoint, @WalletEndpointReferencePublicKey)",
new
{
recipient.Id,
recipient.WalletEndpointReferenceVersion,
recipient.WalletEndpointReferenceEndpoint,
recipient.WalletEndpointReferencePublicKey
});
}

public Task<Recipient?> Get(Guid id)
{
return _connection.QueryFirstOrDefaultAsync<Recipient>(
@"SELECT id, wallet_endpoint_reference_version, wallet_endpoint_reference_endpoint, wallet_endpoint_reference_public_key
FROM recipients
WHERE id = @Id",
new { Id = id });
}
}
139 changes: 139 additions & 0 deletions src/ProjectOrigin.Stamp.Server/Services/REST/v1/RecipientController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
using Microsoft.AspNetCore.Mvc;
using System;
using System.Threading.Tasks;
using ProjectOrigin.Stamp.Server.Database;
using ProjectOrigin.Stamp.Server.Models;
using ProjectOrigin.HierarchicalDeterministicKeys.Implementations;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using ProjectOrigin.Stamp.Server.Options;

namespace ProjectOrigin.Stamp.Server.Services.REST.v1;

[ApiController]
public class RecipientController : ControllerBase

Check warning on line 14 in src/ProjectOrigin.Stamp.Server/Services/REST/v1/RecipientController.cs

View workflow job for this annotation

GitHub Actions / analyse / sonar-analysis

Specify the RouteAttribute when an HttpMethodAttribute or RouteAttribute is specified at an action level. (https://rules.sonarsource.com/csharp/RSPEC-6934)

Check warning on line 14 in src/ProjectOrigin.Stamp.Server/Services/REST/v1/RecipientController.cs

View workflow job for this annotation

GitHub Actions / analyse / sonar-analysis

Specify the RouteAttribute when an HttpMethodAttribute or RouteAttribute is specified at an action level. (https://rules.sonarsource.com/csharp/RSPEC-6934)
{
/// <summary>
/// Creates a new recipient
/// </summary>
/// <param name="unitOfWork"></param>
/// <param name="restApiOptions"></param>
/// <param name="request">The create recipient request</param>
/// <response code="201">The recipient was created.</response>
[HttpPost]
[Route("v1/recipients")]
[Produces("application/json")]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<ActionResult<CreateRecipientResponse>> CreateRecipient(
[FromServices] IUnitOfWork unitOfWork,
[FromServices] IOptions<RestApiOptions> restApiOptions,
[FromBody] CreateRecipientRequest request)
{
var recipient = new Recipient
{
Id = Guid.NewGuid(),
WalletEndpointReferenceVersion = request.WalletEndpointReference.Version,
WalletEndpointReferenceEndpoint = request.WalletEndpointReference.Endpoint.ToString(),
WalletEndpointReferencePublicKey = new Secp256k1Algorithm().ImportHDPublicKey(request.WalletEndpointReference.PublicKey)
};

await unitOfWork.RecipientRepository.Create(recipient);

unitOfWork.Commit();

return Created($"{restApiOptions.Value.PathBase}/v1/recipients/{recipient.Id}", new CreateRecipientResponse
{
Id = recipient.Id
});
}

/// <summary>
/// Gets a specific recipient.
/// </summary>
/// <param name="unitOfWork"></param>
/// <param name="recipientId">The ID of the recipient to get.</param>
/// <response code="200">The recipient was found.</response>
/// <response code="404">If the recipient specified is not found.</response>
[HttpGet]
[Route("v1/recipients/{recipientId}")]
[Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(void), StatusCodes.Status404NotFound)]
public async Task<ActionResult<RecipientDto>> GetRecipient(
[FromServices] IUnitOfWork unitOfWork,
[FromRoute] Guid recipientId)
{
var recipient = await unitOfWork.RecipientRepository.Get(recipientId);

if (recipient == null)
{
return NotFound();
}

return Ok(new RecipientDto
{
Id = recipient.Id,
WalletEndpointReference = new WalletEndpointReferenceDto
{
Endpoint = new Uri(recipient.WalletEndpointReferenceEndpoint),
PublicKey = recipient.WalletEndpointReferencePublicKey.Export().ToArray(),
Version = recipient.WalletEndpointReferenceVersion
}
});
}
}

#region Records

/// <summary>
/// Request to create a new recipient.
/// </summary>
public record CreateRecipientRequest
{
/// <summary>
/// The recipient wallet endpoint reference.
/// </summary>
public required WalletEndpointReferenceDto WalletEndpointReference { get; init; }

Check warning on line 96 in src/ProjectOrigin.Stamp.Server/Services/REST/v1/RecipientController.cs

View workflow job for this annotation

GitHub Actions / analyse / sonar-analysis

Property used as input in a controller action should be nullable or annotated with the Required attribute to avoid under-posting. (https://rules.sonarsource.com/csharp/RSPEC-6964)

Check warning on line 96 in src/ProjectOrigin.Stamp.Server/Services/REST/v1/RecipientController.cs

View workflow job for this annotation

GitHub Actions / analyse / sonar-analysis

Property used as input in a controller action should be nullable or annotated with the Required attribute to avoid under-posting. (https://rules.sonarsource.com/csharp/RSPEC-6964)
}

public record WalletEndpointReferenceDto
{
/// <summary>
/// The version of the ReceiveSlice API.
/// </summary>
public required int Version { get; init; }

Check warning on line 104 in src/ProjectOrigin.Stamp.Server/Services/REST/v1/RecipientController.cs

View workflow job for this annotation

GitHub Actions / analyse / sonar-analysis

Property used as input in a controller action should be nullable or annotated with the Required attribute to avoid under-posting. (https://rules.sonarsource.com/csharp/RSPEC-6964)

Check warning on line 104 in src/ProjectOrigin.Stamp.Server/Services/REST/v1/RecipientController.cs

View workflow job for this annotation

GitHub Actions / analyse / sonar-analysis

Property used as input in a controller action should be nullable or annotated with the Required attribute to avoid under-posting. (https://rules.sonarsource.com/csharp/RSPEC-6964)

/// <summary>
/// The url endpoint of where the wallet is hosted.
/// </summary>
public required Uri Endpoint { get; init; }

/// <summary>
/// The public key used to generate sub-public-keys for each slice.
/// </summary>
public required byte[] PublicKey { get; init; }
}

/// <summary>
/// Response to create a recipient.
/// </summary>
public record CreateRecipientResponse
{
/// <summary>
/// The ID of the created recipient.
/// </summary>
public required Guid Id { get; init; }
}

public record RecipientDto
{
/// <summary>
/// The ID of the recipient.
/// </summary>
public required Guid Id { get; init; }
/// <summary>
/// The wallet endpoint reference of the recipient.
/// </summary>
public required WalletEndpointReferenceDto WalletEndpointReference { get; init; }
}
#endregion
1 change: 1 addition & 0 deletions src/ProjectOrigin.Stamp.Server/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ public void ConfigureServices(IServiceCollection services)
o.ConfigureMassTransitTransport(_configuration.GetSection("MessageBroker").GetValid<MessageBrokerOptions>());
});

services.AddScoped<IUnitOfWork, UnitOfWork>();
services.AddSingleton<IDbConnectionFactory, PostgresConnectionFactory>();

services.AddSwaggerGen(options =>
Expand Down
21 changes: 21 additions & 0 deletions src/ProjectOrigin.Stamp.Test/Extensions/HttpClientExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace ProjectOrigin.Stamp.Test.Extensions;

public static class HttpClientExtensions
{
public static async Task<T?> ReadJson<T>(this HttpContent content)
{
var options = GetJsonSerializerOptions();
return await content.ReadFromJsonAsync<T>(options);
}

private static JsonSerializerOptions GetJsonSerializerOptions()
{
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
tnickelsen marked this conversation as resolved.
Show resolved Hide resolved
options.Converters.Add(new JsonStringEnumConverter());
return options;
}
}
16 changes: 16 additions & 0 deletions src/ProjectOrigin.Stamp.Test/ProjectOrigin.Stamp.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,20 @@
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="8.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="Testcontainers.PostgreSql" Version="3.8.0" />
<PackageReference Include="xunit" Version="2.8.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\ProjectOrigin.Stamp.Server\ProjectOrigin.Stamp.Server.csproj" />
</ItemGroup>

</Project>
Loading