Skip to content

Commit

Permalink
Recipient API created (#35)
Browse files Browse the repository at this point in the history
  • Loading branch information
tnickelsen authored May 16, 2024
1 parent 43d15bd commit 02c9d13
Show file tree
Hide file tree
Showing 18 changed files with 769 additions and 1 deletion.
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 (
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 });
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
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;

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

[ApiController]
public class RecipientController : ControllerBase

Check warning on line 12 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="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,
[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(null as string, new CreateRecipientResponse
{
Id = recipient.Id
});
}
}

#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 57 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 65 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; }
}
#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);
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

0 comments on commit 02c9d13

Please sign in to comment.