8000 Add Founder projects to Avalonia app by SuperJMN · Pull Request #366 · block-core/angor · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Add Founder projects to Avalonia app #366

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

Merged
merged 12 commits into from
May 13, 2025
Merged
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Angor.Contexts.Funding.Projects.Infrastructure.Interfaces;
using Angor.Contexts.Funding.Projects.Domain;
using Angor.Contexts.Funding.Projects.Infrastructure.Interfaces;
using Angor.Contexts.Funding.Shared;
using Angor.Contexts.Funding.Tests.TestDoubles;
using Angor.Shared;
Expand All @@ -17,14 +18,26 @@ public async Task Get_latest_projects()
var result = await sut.Latest();
Assert.NotEmpty(result);
}

[Fact]
public async Task Get_founder_projects()
{
var sut = CreateSut();

var projectId = new ProjectId("angor1qkmmqqktfhe79wxp20555cdp5gfardr4s26wr00");
var result = await sut.GetFounderProjects(Guid.NewGuid());

Assert.NotEmpty(result.Value);
}


private IProjectAppService CreateSut()
{
var serviceCollection = new ServiceCollection();

var logger = new LoggerConfiguration().WriteTo.TestOutput(output).CreateLogger();
FundingContextServices.Register(serviceCollection, logger);
serviceCollection.AddSingleton<ISeedwordsProvider>(sp => new TestingSeedwordsProvider("print foil moment average quarter keep amateur shell tray roof acoustic where", "", sp.GetRequiredService<IDerivationOperations>()));
serviceCollection.AddSingleton<ISeedwordsProvider>(sp => new TestingSeedwordsProvider("oven suggest panda hip orange cheap kite focus cross never tornado forget", "", sp.GetRequiredService<IDerivationOperations>()));

var serviceProvider = serviceCollection.BuildServiceProvider();
var projectAppService = serviceProvider.GetService<IProjectAppService>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Angor.Contests.CrossCutting;
using Angor.Contexts.Funding.Projects.Application.Dtos;
using Angor.Contexts.Funding.Projects.Domain;
using Angor.Contexts.Funding.Projects.Infrastructure.Impl;
using Angor.Contexts.Funding.Shared;
using Angor.Shared;
using CSharpFunctionalExtensions;
using MediatR;
using Zafiro.CSharpFunctionalExtensions;

namespace Angor.Contexts.Funding.Founder.Operations;

public static class GetFounderProjects
{
public class GetFounderProjectsHandler(
IProjectRepository projectRepository,
ISeedwordsProvider seedwordsProvider,
IDerivationOperations derivationOperations,
INetworkConfiguration networkConfiguration) : IRequestHandler<GetFounderProjectsRequest, Result<IEnumerable<ProjectDto>>>
{
public Task<Result<IEnumerable<ProjectDto>>> Handle(GetFounderProjectsRequest request, CancellationToken cancellationToken)
{
return GetProjectIds(request)
.Traverse(projectRepository.TryGet)
.Map(projectMaybes => projectMaybes.Values())
.MapEach(project => project.ToDto());
}

private Task<Result<IEnumerable<ProjectId>>> GetProjectIds(GetFounderProjectsRequest request)
{
return seedwordsProvider.GetSensitiveData(request.WalletId)
.Map(p => p.ToWalletWords())
.Map(words => derivationOperations.DeriveProjectKeys(words, networkConfiguration.GetAngorKey()))
.Map(collection => collection.Keys.AsEnumerable())
.MapEach(keys => keys.ProjectIdentifier)
.MapEach(fk => new ProjectId(fk));
}
}

public record GetFounderProjectsRequest(Guid WalletId) : IRequest<Result<IEnumerable<ProjectDto>>>;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
using Angor.Contexts.Funding.Projects.Domain;
using Angor.Shared;
using Angor.Shared.Models;
Expand All @@ -25,22 +26,25 @@ public class GetPendingInvestmentsHandler(IProjectRepository projectRepository,
{
public async Task<Result<IEnumerable<PendingInvestmentDto>>> Handle(GetPendingInvestmentsRequest request, CancellationToken cancellationToken)
{
var project = await projectRepository.Get(request.ProjectId);
if (project.IsFailure)
var projectResult = await projectRepository.Get(request.ProjectId);
if (projectResult.IsFailure)
{
return Result.Failure<IEnumerable<PendingInvestmentDto>>(project.Error);
return Result.Failure<IEnumerable<PendingInvestmentDto>>(projectResult.Error);
}

var nostrPubKey = project.Value.NostrPubKey;
var nostrPubKey = projectResult.Value.NostrPubKey;
var investingMessages = InvestmentMessages(nostrPubKey);
var pendingInvestmentResults = await investingMessages.SelectMany(nostrMessage => DecryptInvestmentMessage(request.WalletId, project, nostrMessage)).ToList();
var pendingInvestmentResults = await investingMessages
.SelectMany(nostrMessage => DecryptInvestmentMessage(request.WalletId, projectResult.Value, nostrMessage))
.ToList();

return pendingInvestmentResults.Combine();
}

private Task<Result<PendingInvestmentDto>> DecryptInvestmentMessage(Guid walletId, Result<Project> project, NostrMessage nostrMessage)
private Task<Result<PendingInvestmentDto>> DecryptInvestmentMessage(Guid walletId, Pro 6D40 ject project, NostrMessage nostrMessage)
{
return from decrypted in nostrDecrypter.Decrypt(walletId, project.Value.Id, nostrMessage)
return
from decrypted in nostrDecrypter.Decrypt(walletId, project.Id, nostrMessage)
from signRecoveryRequest in Result.Try(() => serializer.Deserialize<SignRecoveryRequest>(decrypted))
select new PendingInvestmentDto(nostrMessage.Created, GetAmount(signRecoveryRequest), nostrMessage.InvestorNostrPubKey);
}
Expand Down Expand Up @@ -69,5 +73,7 @@ private decimal GetAmount(SignRecoveryRequest signRecoveryRequest)
}
}

public record PendingInvestmentDto(DateTime Created, decimal Amount, string InvestorNostrPubKey);
public record PendingInvestmentDto(DateTime Created, decimal Amount, string InvestorNostrPubKey)
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ public interface IProjectRepository
{
Task<Result<Project>> Get(ProjectId id);
Task<IList<Project>> Latest();


Task<Result<Maybe<Project>>> TryGet(ProjectId projectId);
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
using Angor.Contests.CrossCutting;
using Angor.Contexts.Funding.Founder.Operations;
using Angor.Contexts.Funding.Projects.Application.Dtos;
using Angor.Contexts.Funding.Projects.Domain;
using Angor.Contexts.Funding.Projects.Infrastructure.Interfaces;
using CSharpFunctionalExtensions;
using MediatR;
using Zafiro.CSharpFunctionalExtensions;

namespace Angor.Contexts.Funding.Projects.Infrastructure.Impl;

public class ProjectAppService(
IProjectRepository projectRepository)
IProjectRepository projectRepository, IMediator mediator)
: IProjectAppService
{
[MemoizeTimed]
Expand All @@ -23,4 +25,9 @@ public Task<Maybe<ProjectDto>> FindById(ProjectId projectId)
{
return projectRepository.Get(projectId).Map(project1 => project1.ToDto()).AsMaybe();
}

public Task<Result<IEnumerable<ProjectDto>>> GetFounderProjects(Guid walletId)
{
return mediator.Send(new GetFounderProjects.GetFounderProjectsRequest(walletId));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,19 @@ public class ProjectRepository(
{
public Task<Result<Project>> Get(ProjectId id)
{
return FindById(id);
return TryGet(id).Bind(maybe => maybe.ToResult("Project not found"));
}

public Task<IList<Project>> Latest()
{
return ProjectsFrom(indexerService.GetLatest()).ToList().ToTask();
}

public async Task<Result<Project>> FindById(ProjectId projectId)
public Task<Result<Maybe<Project>>> TryGet(ProjectId projectId)
{
var project = (await indexerService.GetProjectByIdAsync(projectId.Value)).AsMaybe();
return await project.Map(async data => await ProjectsFrom(new[] { data }.ToObservable()).FirstAsync()).ToResult($"Project {projectId} not Found");
return Result.Try(() => indexerService.GetProjectByIdAsync(projectId.Value))
.Map(data => data.AsMaybe())
.Map(maybe => maybe.Map(async data => await ProjectsFrom(new[]{ data}.ToObservable())));
}

private IObservable<Project> ProjectsFrom(IObservable<ProjectIndexerData> projectIndexerDatas)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ public interface IProjectAppService
{
Task<IList<ProjectDto>> Latest();
Task<Maybe<ProjectDto>> FindById(ProjectId projectId);
Task<Result<IEnumerable<ProjectDto>>> GetFounderProjects(Guid walletId);
}
7 changes: 7 additions & 0 deletions src/Angor/Avalonia/AngorApp/AngorApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@
<ProjectReference Include="..\Angor.Contexts.Wallet\Angor.Contexts.Wallet.csproj" />
<ProjectReference Include="..\Angor.Contexts.Funding\Angor.Contexts.Funding.csproj" />
</ItemGroup>

<ItemGroup>
<Compile Update="Sections\Founder\Details\FounderProjectDetailsView.axaml.cs">
<DependentUpon>FounderProjectDetailsView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup>


</Project>
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using Angor.Contexts.Funding.Projects.Application.Dtos;
using AngorApp.Sections.Browse;
using AngorApp.Sections.Founder;
using AngorApp.Sections.Founder.Details;
using AngorApp.Sections.Home;
using AngorApp.Sections.Portfolio;
using AngorApp.Sections.Shell;
Expand All @@ -16,9 +18,11 @@ public static IServiceCollection Register(this IServiceCollection services)
.AddTransient<Lazy<IMainViewModel>>(sp => new Lazy<IMainViewModel>(sp.GetRequiredService<IMainViewModel>))
.AddTransient<IHomeSectionViewModel, HomeSectionViewModel>()
.AddTransient<IWalletSectionViewModel, WalletSectionViewModel>()
.AddTransient<IBrowseSectionViewModel, BrowseSectionViewModel>()
.AddScoped<IBrowseSectionViewModel, BrowseSectionViewModel>()
.AddTransient<IPortfolioSectionViewModel, PortfolioSectionViewModel>()
.AddTransient<IFounderSectionViewModel, FounderSectionViewModel>()
.AddScoped<Func<ProjectDto, IFounderProjectViewModel>>(provider => dto => ActivatorUtilities.CreateInstance<FounderProjectViewModel>(provider, dto))
.AddScoped<Func<ProjectDto, IFounderProjectDetailsViewModel>>(provider => dto => ActivatorUtilities.CreateInstance<FounderProjectDetailsViewModel>(provider, dto))
.AddTransient<IMainViewModel, MainViewModel>();
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
using System.Windows.Input;
using Angor.Contexts.Funding.Projects.Infrastructure.Interfaces;
using Angor.Contexts.Wallet.Application;
using AngorApp.Features.Invest;
using AngorApp.Sections.Browse.Details;
using AngorApp.Sections.Browse.ProjectLookup;
using AngorApp.UI.Services;
using ReactiveUI.SourceGenerators;
using Zafiro.Avalonia.Controls.Navigation;
using Zafiro.Reactive;
using Zafiro.UI.Navigation;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:c="clr-namespace:Zafiro.Avalonia.Controls;assembly=Zafiro.Avalonia"
xmlns:sdg="clr-namespace:Zafiro.Avalonia.Controls.SlimDataGrid;assembly=Zafiro.Avalonia"
xmlns:angorApp="clr-namespace:AngorApp"
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
xmlns:controls="clr-namespace:AngorApp.UI.Controls"
xmlns:pt="https://github.com/projektanker/icons.avalonia"
xmlns:details="clr-namespace:AngorApp.Sections.Founder.Details"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="AngorApp.Sections.Founder.Details.FounderProjectDetailsView" x:DataType="details:IFounderProjectDetailsViewModel">

<Design.DataContext>
<details:FounderProjectDetailsViewModelDesign BannerUrl="https://images-assets.nasa.gov/image/PIA05062/PIA05062~thumb.jpg" />
</Design.DataContext>

<Interaction.Behaviors>
<DataContextChangedTrigger>
<InvokeCommandAction Command="{Binding LoadPendingInvestments}" />
</DataContextChangedTrigger>
</Interaction.Behaviors>

<StackPanel Spacing="10">
<controls:Pane Padding="0" CornerRadius="20">
<StackPanel>
<asyncImageLoader:AdvancedImage CornerRadius="20 20 0 0" Height="200" Stretch="UniformToFill" Source="{Binding BannerUrl}" />
<StackPanel Margin="20" Spacing="10">
<TextBlock Classes="SizeBig" Text="{Binding Name}" />
<TextBlock TextWrapping="Wrap" Text="{Binding ShortDescription}" />
</StackPanel>
</StackPanel>
</controls:Pane>
<controls:Pane Title="Investments pending approval" IsTitleVisible="True">
<controls:Pane.TitleRightContent>
<Button pt:Attached.Icon="fa-rotate-right" Command="{Binding LoadPendingInvestments}" />
</controls:Pane.TitleRightContent>
<c:Loading IsLoading="{Binding LoadPendingInvestments.IsExecuting^}">
<sdg:SlimDataGrid Margin="0 10" ItemsSource="{Binding PendingInvestments}">
<sdg:SlimDataGrid.Columns>
<sdg:Column Header="Amount" Binding="{Binding Amount}" />
<sdg:Column Header="NPub" Binding="{Binding InvestorNostrPubKey}" />
<sdg:Column Header="Date" Binding="{Binding Created}" />
</sdg:SlimDataGrid.Columns>
</sdg:SlimDataGrid>
</c:Loading>
</controls:Pane>
</StackPanel>
</UserControl>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace AngorApp.Sections.Founder.Details;

public partial class FounderProjectDetailsView : UserControl
{
public FounderProjectDetailsView()
{
InitializeComponent();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Angor.Contexts.Funding.Founder.Operations;
using Angor.Contexts.Funding.Investor;
using Angor.Contexts.Funding.Projects.Application.Dtos;
using AngorApp.UI.Services;
using DynamicData;
using Zafiro.CSharpFunctionalExtensions;
using Zafiro.UI;

namespace AngorApp.Sections.Founder.Details;

public class FounderProjectDetailsViewModel : IFounderProjectDetailsViewModel
{
private readonly ProjectDto projectDto;

public FounderProjectDetailsViewModel(ProjectDto projectDto, IInvestmentAppService investmentAppService, UIServices uiServices)
{
this.projectDto = projectDto;
LoadPendingInvestments = ReactiveCommand.CreateFromTask(token =>
{
return uiServices.WalletRoot.GetDefaultWalletAndActivate()
.Bind(maybe => maybe.ToResult("You need to create a wallet first"))
.Bind(wallet => investmentAppService.GetPendingInvestments(wallet.Id.Value, projectDto.Id));
});

LoadPendingInvestments.HandleErrorsWith(uiServices.NotificationService, "Failed to get pending investments");

LoadPendingInvestments.Successes()
.EditDiff(dto => dto)
.Bind(out var pendingInvestments)
.Subscribe();

PendingInvestments = pendingInvestments;
}

public IEnumerable<GetPendingInvestments.PendingInvestmentDto> PendingInvestments { get; }

public ReactiveCommand<Unit, Result<IEnumerable<GetPendingInvestments.PendingInvestmentDto>>> LoadPendingInvestments { get; }
public Uri? BannerUrl => projectDto.Banner;
public string ShortDescription => projectDto.ShortDescription;
public string Name => projectDto.Name;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Angor.Contexts.Funding.Founder.Operations;

namespace AngorApp.Sections.Founder.Details;

public class FounderProjectDetailsViewModelDesign : IFounderProjectDetailsViewModel
{
public string Name { get; } = "Test";

public IEnumerable<GetPendingInvestments.PendingInvestmentDto> PendingInvestments { get; } = new List<GetPendingInvestments.PendingInvestmentDto>()
{
new(DateTime.Now, 1234, "nostr pub key"),
new(DateTime.Now.AddHours(-2), 1233, "nostr pub key"),
new(DateTime.Now.AddHours(-4), 1235, "nostr pub key"),
};

public ReactiveCommand<Unit, Result<IEnumerable<GetPendingInvestments.PendingInvestmentDto>>> LoadPendingInvestments { get; }
public Uri? BannerUrl { get; set; }
public string ShortDescription { get; } = "Short description, Bitcoin ONLY.";
}
5B27
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Angor.Contexts.Funding.Founder.Operations;

namespace AngorApp.Sections.Founder.Details;

public interface IFounderProjectDetailsViewModel
{
public string Name { get; }
public IEnumerable<GetPendingInvestments.PendingInvestmentDto> PendingInvestments { get; }
ReactiveCommand<Unit, Result<IEnumerable<GetPendingInvestments.PendingInvestmentDto>>> LoadPendingInvestments { get; }
public Uri? BannerUrl { get; }
public string ShortDescription { get; }
}
Loading
0