Skip to content

Commit 930ed7f

Browse files
authored
Implement Funded section. Stage 2. (block-core#677)
1 parent 90b6d67 commit 930ed7f

File tree

230 files changed

+3164
-2683
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

230 files changed

+3164
-2683
lines changed

src/Angor/Avalonia/Angor.Sdk/Funding/Investor/InvestedProjectDto.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ public class InvestedProjectDto
1818
public Amount InRecovery { get; set; }
1919
public InvestmentStatus InvestmentStatus { get; set; }
2020
public string InvestmentId { get; set; }
21+
public DateTimeOffset? RequestedOn { get; set; }
2122
}
2223

2324
public enum FounderStatus
2425
{
2526
Invalid,
2627
Requested,
2728
Approved
28-
}
29+
}

src/Angor/Avalonia/Angor.Sdk/Funding/Investor/Operations/GetInvestments.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,10 @@ public async Task<Result<GetInvestmentsResponse>> Handle(GetInvestmentsRequest r
7777
Investment = new Amount(investment?.TotalAmount ?? 0),
7878
InvestmentId = investment?.TransactionId ?? string.Empty,
7979
Raised = new Amount(stats.stats?.AmountInvested ?? 0),
80-
InRecovery = new Amount(stats.stats?.AmountInPenalties ?? 0)
80+
InRecovery = new Amount(stats.stats?.AmountInPenalties ?? 0),
81+
RequestedOn = investmentRecord.RequestEventTime.HasValue
82+
? new DateTimeOffset(investmentRecord.RequestEventTime.Value)
83+
: null
8184
};
8285

8386
if (investment != null)
@@ -142,6 +145,7 @@ public async Task<Result<GetInvestmentsResponse>> Handle(GetInvestmentsRequest r
142145
dto.FounderStatus = Handshake.Status == InvestmentRequestStatus.Approved
143146
? FounderStatus.Approved
144147
: FounderStatus.Requested;
148+
dto.RequestedOn = new DateTimeOffset(Handshake.RequestCreated);
145149

146150
dto.InvestmentStatus = DetermineInvestmentStatus(Handshake);
147151

@@ -186,4 +190,3 @@ private static InvestmentStatus DetermineInvestmentStatus(InvestmentHandshake Ha
186190
}
187191

188192

189-

src/Angor/Avalonia/AngorApp.Model/Common/RefreshableCollection.cs

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@ namespace AngorApp.Model.Common;
1111

1212
public static class RefreshableCollection
1313
{
14-
public static RefreshableCollection<TItem, TKey> Create<TItem, TKey>(Func<Task<Result<IEnumerable<TItem>>>> getItems, Func<TItem, TKey> getKey)
14+
public static RefreshableCollection<TItem, TKey> Create<TItem, TKey>(
15+
Func<Task<Result<IEnumerable<TItem>>>> getItems,
16+
Func<TItem, TKey> getKey,
17+
Func<TItem, IComparable>? sortBy = null)
1518
where TItem : notnull
1619
where TKey : notnull
1720
{
18-
return new RefreshableCollection<TItem, TKey>(getItems, getKey);
21+
return new RefreshableCollection<TItem, TKey>(getItems, getKey, sortBy);
1922
}
2023
}
2124

@@ -25,24 +28,42 @@ public class RefreshableCollection<TItem, TKey> : IDisposable
2528
{
2629
private readonly CompositeDisposable disposable = new();
2730

28-
public RefreshableCollection(Func<Task<Result<IEnumerable<TItem>>>> getItems, Func<TItem, TKey> getKey)
31+
public RefreshableCollection(
32+
Func<Task<Result<IEnumerable<TItem>>>> getItems,
33+
Func<TItem, TKey> getKey,
34+
Func<TItem, IComparable>? sortBy = null)
2935
{
3036
Refresh = ReactiveCommand.CreateFromTask(getItems).Enhance().DisposeWith(disposable);
3137

3238
var updates = Refresh.Successes()
3339
.EditDiff(getKey)
3440
.Publish();
3541

36-
updates
37-
.DisposeMany()
38-
.Bind(out var items)
39-
.Subscribe()
40-
.DisposeWith(disposable);
42+
if (sortBy is null)
43+
{
44+
updates
45+
.DisposeMany()
46+
.Bind(out var items)
47+
.Subscribe()
48+
.DisposeWith(disposable);
49+
50+
Items = items;
51+
}
52+
else
53+
{
54+
updates
55+
.DisposeMany()
56+
.SortBy(sortBy)
57+
.Bind(out var items)
58+
.Subscribe()
59+
.DisposeWith(disposable);
60+
61+
Items = items;
62+
}
4163

4264
updates.Connect().DisposeWith(disposable);
4365

4466
Changes = updates;
45-
Items = items;
4667
}
4768

4869
public ReadOnlyObservableCollection<TItem> Items { get; }

src/Angor/Avalonia/AngorApp.Model/Contracts/Projects/IProject.cs

Lines changed: 0 additions & 20 deletions
This file was deleted.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using Angor.Sdk.Funding.Investor;
2+
using AngorApp.Model.Funded.Shared.Model;
3+
using AngorApp.Model.ProjectsV2;
4+
using AngorApp.Model.ProjectsV2.FundProject;
5+
using AngorApp.Model.Shared.Services;
6+
7+
namespace AngorApp.Model.Funded.Fund.Model
8+
{
9+
public class FundFunded : FundedBase, IFundFunded
10+
{
11+
public FundFunded(
12+
IFundProject project,
13+
IFundInvestorData investorData,
14+
INotificationService notificationService,
15+
ITransactionDraftPreviewer draftPreviewer,
16+
IInvestmentAppService appService,
17+
IWalletContext walletContext
18+
) : base(project, investorData, notificationService, draftPreviewer, appService, walletContext)
19+
{
20+
}
21+
22+
public new IFundProject Project => (IFundProject)base.Project;
23+
public new IFundInvestorData InvestorData => (IFundInvestorData)base.InvestorData;
24+
25+
IProject IFunded.Project => base.Project;
26+
IInvestorData IFunded.InvestorData => base.InvestorData;
27+
}
28+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using AngorApp.Model.Funded.Fund.Samples;
2+
using AngorApp.Model.Funded.Shared.Model;
3+
using AngorApp.Model.ProjectsV2;
4+
using AngorApp.Model.ProjectsV2.FundProject;
5+
6+
namespace AngorApp.Model.Funded.Fund.Model
7+
{
8+
public class FundFundedSample : IFundFunded
9+
{
10+
public FundFundedSample() : this(new FundInvestorDataSample())
11+
{
12+
}
13+
14+
public FundFundedSample(IFundInvestorData investorData)
15+
{
16+
InvestorData = investorData;
17+
}
18+
19+
public IFundProject Project { get; } = new FundProjectSample();
20+
public IFundInvestorData InvestorData { get; }
21+
22+
public IEnhancedCommand<Result> CancelApproval { get; } = EnhancedCommand.CreateWithResult(Result.Success);
23+
public IEnhancedCommand<Result> CancelInvestment { get; } = EnhancedCommand.CreateWithResult(Result.Success);
24+
public IEnhancedCommand<Result> ConfirmInvestment { get; } = EnhancedCommand.CreateWithResult(Result.Success);
25+
public IEnhancedCommand<Result> OpenChat { get; } = EnhancedCommand.CreateWithResult(Result.Success);
26+
public IEnhancedCommand<Result> RecoverFunds { get; } = EnhancedCommand.CreateWithResult(Result.Success);
27+
28+
IProject IFunded.Project => Project;
29+
IInvestorData IFunded.InvestorData => InvestorData;
30+
}
31+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
using System.Reactive.Disposables;
2+
using System.Reactive.Subjects;
3+
using Angor.Sdk.Funding.Founder;
4+
using Angor.Sdk.Funding.Investor;
5+
using Angor.Sdk.Funding.Investor.Operations;
6+
using Angor.Sdk.Funding.Shared;
7+
using AngorApp.Model.Funded.Shared.Model;
8+
using AngorApp.Model.Shared.Services;
9+
using Zafiro.CSharpFunctionalExtensions;
10+
11+
namespace AngorApp.Model.Funded.Fund.Model
12+
{
13+
public sealed class FundInvestorData : IFundInvestorData, IDisposable
14+
{
15+
private readonly IInvestmentAppService investmentAppService;
16+
private readonly IWalletContext walletContext;
17+
private readonly CompositeDisposable disposables = new();
18+
private readonly BehaviorSubject<InvestmentStatus> status;
19+
private readonly BehaviorSubject<RecoveryState> recovery = new(RecoveryState.None);
20+
21+
public FundInvestorData(InvestedProjectDto dto, IInvestmentAppService investmentAppService, IWalletContext walletContext)
22+
{
23+
this.investmentAppService = investmentAppService;
24+
this.walletContext = walletContext;
25+
26+
InvestmentId = dto.InvestmentId;
27+
ProjectId = dto.Id;
28+
29+
Amount = new AmountUI(dto.Investment.Sats);
30+
InvestedOn = dto.RequestedOn ?? DateTimeOffset.MinValue;
31+
status = new BehaviorSubject<InvestmentStatus>(dto.InvestmentStatus);
32+
Status = status;
33+
Recovery = recovery;
34+
35+
var refresh = EnhancedCommand.CreateWithResult(DoRefresh).DisposeWith(disposables);
36+
refresh.Successes()
37+
.Subscribe(Update)
38+
.DisposeWith(disposables);
39+
Refresh = refresh;
40+
}
41+
42+
public IAmountUI Amount { get; }
43+
public DateTimeOffset InvestedOn { get; private set; }
44+
public IEnhancedCommand Refresh { get; }
45+
public string InvestmentId { get; }
46+
public string ProjectId { get; }
47+
public IObservable<InvestmentStatus> Status { get; }
48+
public IObservable<RecoveryState> Recovery { get; }
49+
50+
private async Task<Result<(InvestedProjectDto Dto, RecoveryState Recovery)>> DoRefresh()
51+
{
52+
return await walletContext
53+
.Require()
54+
.Bind(async wallet =>
55+
{
56+
var dto = await investmentAppService
57+
.GetInvestments(new GetInvestments.GetInvestmentsRequest(wallet.Id))
58+
.Bind(response => response.Projects
59+
.TryFirst(project => project.Id == ProjectId)
60+
.ToResult($"Investment not found: {ProjectId}"));
61+
62+
if (dto.IsFailure)
63+
return Result.Failure<(InvestedProjectDto, RecoveryState)>(dto.Error);
64+
65+
var recoveryState = RecoveryState.None;
66+
67+
if (dto.Value.InvestmentStatus == InvestmentStatus.Invested)
68+
{
69+
var recoveryResult = await investmentAppService.GetRecoveryStatus(
70+
new GetRecoveryStatus.GetRecoveryStatusRequest(wallet.Id, new ProjectId(ProjectId)));
71+
72+
if (recoveryResult.IsSuccess)
73+
{
74+
var r = recoveryResult.Value.RecoveryData;
75+
recoveryState = new RecoveryState(r.HasUnspentItems, r.HasItemsInPenalty, r.HasReleaseSignatures, r.EndOfProject, r.IsAboveThreshold);
76+
}
77+
}
78+
79+
return Result.Success((dto.Value, recoveryState));
80+
});
81+
}
82+
83+
private void Update((InvestedProjectDto Dto, RecoveryState Recovery) result)
84+
{
85+
InvestedOn = result.Dto.RequestedOn ?? DateTimeOffset.MinValue;
86+
status.OnNext(result.Dto.InvestmentStatus);
87+
recovery.OnNext(result.Recovery);
88+
}
89+
90+
public void Dispose()
91+
{
92+
disposables.Dispose();
93+
status.Dispose();
94+
recovery.Dispose();
95+
}
96+
}
97+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using Angor.Sdk.Funding.Founder;
2+
using AngorApp.Model.Funded.Shared.Model;
3+
4+
namespace AngorApp.Model.Funded.Fund.Model
5+
{
6+
public class FundInvestorDataSample : IFundInvestorData
7+
{
8+
public FundInvestorDataSample(InvestmentStatus status = InvestmentStatus.Invested)
9+
{
10+
Status = Observable.Return(status);
11+
Recovery = Observable.Return(new RecoveryState(true, false, false, false, true));
12+
}
13+
14+
public IAmountUI Amount { get; } = AmountUI.FromBtc(0.5m);
15+
public DateTimeOffset InvestedOn { get; } = DateTimeOffset.Now;
16+
public IEnhancedCommand Refresh { get; } = EnhancedCommand.Create(() => { });
17+
public string InvestmentId { get; } = "funding-preview-id";
18+
public string ProjectId { get; }
19+
public IObservable<InvestmentStatus> Status { get; }
20+
public IObservable<RecoveryState> Recovery { get; }
21+
}
22+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using AngorApp.Model.Funded.Shared.Model;
2+
using AngorApp.Model.ProjectsV2.FundProject;
3+
4+
namespace AngorApp.Model.Funded.Fund.Model
5+
{
6+
public interface IFundFunded : IFunded
7+
{
8+
new IFundProject Project { get; }
9+
new IFundInvestorData InvestorData { get; }
10+
}
11+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using AngorApp.Model.Funded.Shared.Model;
2+
3+
namespace AngorApp.Model.Funded.Fund.Model
4+
{
5+
public interface IFundInvestorData : IInvestorData
6+
{
7+
}
8+
}

0 commit comments

Comments
 (0)