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

Put a limit to founder if target amount was not reached #227

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
78 changes: 78 additions & 0 deletions src/Angor.Test/ProtocolNew/InvestmentIntegrationsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -635,5 +635,83 @@ public void InvestorTransaction_NoPenalty_Test(int stageIndex)
investorInvTrx.Outputs.AsCoins().Where(c => c.Amount > 0));
}
}

[Fact]
public void SpendInvestorReleaseTest()
{
var network = Networks.Bitcoin.Testnet();

var words = new WalletWords { Words = new Mnemonic(Wordlist.English, WordCount.Twelve).ToString() };

// Create the investor params
var investorKey = new Key();
var investorChangeKey = new Key();

var funderKey = _derivationOperations.DeriveFounderKey(words, 1);
var angorKey = _derivationOperations.DeriveAngorKey(funderKey, angorRootKey);
var founderRecoveryKey = _derivationOperations.DeriveFounderRecoveryKey(words, 1);
var funderPrivateKey = _derivationOperations.DeriveFounderPrivateKey(words, 1);
var founderRecoveryPrivateKey = _derivationOperations.DeriveFounderRecoveryPrivateKey(words, 1);

var investorContext = new InvestorContext
{
ProjectInfo = new ProjectInfo
{
TargetAmount = 3,
StartDate = DateTime.UtcNow,
ExpiryDate = DateTime.UtcNow.AddDays(5),
PenaltyDays = 5,
Stages = new List<Stage>
{
new() { AmountToRelease = 1, ReleaseDate = DateTime.UtcNow.AddDays(1) },
new() { AmountToRelease = 1, ReleaseDate = DateTime.UtcNow.AddDays(2) },
new() { AmountToRelease = 1, ReleaseDate = DateTime.UtcNow.AddDays(3) }
},
FounderKey = funderKey,
FounderRecoveryKey = founderRecoveryKey,
ProjectIdentifier = angorKey,
ProjectSeeders = new ProjectSeeders()
},
InvestorKey = Encoders.Hex.EncodeData(investorKey.PubKey.ToBytes()),
ChangeAddress = investorChangeKey.PubKey.GetSegwitAddress(network).ToString()
};

var investorReleaseKey = new Key();
var investorReleasePubKey = Encoders.Hex.EncodeData(investorReleaseKey.PubKey.ToBytes());

// Create the investment transaction
var investmentTransaction = _investorTransactionActions.CreateInvestmentTransaction(investorContext.ProjectInfo, investorContext.InvestorKey,
Money.Coins(investorContext.ProjectInfo.TargetAmount).Satoshi);

investorContext.TransactionHex = investmentTransaction.ToHex();

// Build the release transaction
var releaseTransaction = _investorTransactionActions.BuildReleaseInvestorFundsTransaction(investorContext.ProjectInfo, investmentTransaction, investorReleasePubKey);

// Sign the release transaction
var founderSignatures = _founderTransactionActions.SignInvestorRecoveryTransactions(investorContext.ProjectInfo,
investmentTransaction.ToHex(), releaseTransaction,
Encoders.Hex.EncodeData(founderRecoveryPrivateKey.ToBytes()));

var signedReleaseTransaction = _investorTransactionActions.AddSignaturesToReleaseFundsTransaction(investorContext.ProjectInfo,
investmentTransaction, founderSignatures, Encoders.Hex.EncodeData(investorKey.ToBytes()), investorReleasePubKey);

// Validate the signatures
var sigCheckResult = _investorTransactionActions.CheckInvestorReleaseSignatures(investorContext.ProjectInfo, investmentTransaction, founderSignatures, investorReleasePubKey);
Assert.True(sigCheckResult, "Failed to validate the founder's signatures");

List<Coin> coins = new();
foreach (var indexedTxOut in investmentTransaction.Outputs.AsIndexedOutputs().Where(w => !w.TxOut.ScriptPubKey.IsUnspendable))
{
coins.Add(new Blockcore.NBitcoin.Coin(indexedTxOut));
coins.Add(new Blockcore.NBitcoin.Coin(Blockcore.NBitcoin.uint256.Zero, 0, new Blockcore.NBitcoin.Money(1000),
new Script("4a8a3d6bb78a5ec5bf2c599eeb1ea522677c4b10132e554d78abecd7561e4b42"))); // Adding fee inputs
}

signedReleaseTransaction.Inputs.Add(new Blockcore.Consensus.TransactionInfo.TxIn(
new Blockcore.Consensus.TransactionInfo.OutPoint(Blockcore.NBitcoin.uint256.Zero, 0), null)); // Add fee to the transaction

TransactionValidation.ThanTheTransactionHasNoErrors(signedReleaseTransaction, coins);
}
}
}
191 changes: 191 additions & 0 deletions src/Angor/Client/Pages/Release.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
@page "/release/{ProjectIdentifier}"
@using Angor.Shared
@using Angor.Client.Storage
@using Angor.Shared.Models
@using Angor.Shared.ProtocolNew
@using Angor.Client.Models
@using Angor.Shared.Services
@using Blockcore.Consensus.TransactionInfo

@inject IJSRuntime JS

@inject ILogger<Signatures> Logger
@inject IDerivationOperations DerivationOperations
@inject IClientStorage Storage;
@inject ISignService SignService
@inject IInvestorTransactionActions InvestorTransactionActions
@inject IFounderTransactionActions FounderTransactionActions
@inject ISerializer serializer
@inject IEncryptionService encryption

@inherits BaseComponent

<NotificationComponent @ref="notificationComponent" />
<PasswordComponent @ref="passwordComponent" />

@if (!hasWallet)
{
NavigationManager.NavigateTo($"/wallet");
return;
}
<div class="row">
<div class="card card-body">
<div class="d-flex align-items-center justify-content-between">
<div class="d-flex col-auto my-auto align-items-center">
<span class="user-select-none">
<Icon IconName="signature" Height="42" Width="42" />
</span>
<div class="h-100 ms-3 flex-grow-1">
<h5 class="mb-0 font-weight-bolder">
Release funds
</h5>

</div>
</div>
</div>
<p class="mb-0 font-weight-normal text-sm d-flex mt-4">
Project ID:
<span id="transactionID" class="text-dynamic ms-1">@ProjectIdentifier</span>
</p>
</div>
</div>

<div class="row mt-4">
<div class="col">
<div class="card">
<div class="card-header pb-0 p-3">
<div class="row">
<div class="col-6 d-flex align-items-center">
<h6 class="mb-0">Approved Signatures</h6>
</div>
</div>
</div>
<div class="card-body">
<div class="table-responsive form-control">
<table class="table align-items-center mb-0">
<thead>
<tr>
<th class="text-uppercase text-xxs font-weight-bolder opacity-7">Investor Public Key</th>
<th class="text-uppercase text-xxs font-weight-bolder opacity-7">Time Approved</th>
<th class="text-uppercase text-xxs font-weight-bolder opacity-7">Status</th>
</tr>
</thead>
<tbody>
@foreach (var approval in approvalsDictionary)
{
<tr>
<td>@approval.Key</td>
<td>@approval.Value.TimeApproved.ToString("g")</td>
<td>@approval.Value.EventId</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>


@code {
[Parameter]
public string ProjectIdentifier { get; set; }

public FounderProject FounderProject { get; set; }

List<(Transaction Transaction, string TrxId)> transactions = new();
Dictionary<string, (DateTime TimeApproved, string EventId)> approvalsDictionary = new();


bool messagesReceived;
bool scanedForApprovals;

private bool isLoading = false;
private bool refreshButtonSpinner = false;

protected override async Task OnInitializedAsync()
{
Logger.LogDebug("OnInitializedAsync");
if (hasWallet)
{
FounderProject = Storage.GetFounderProjects()
.FirstOrDefault(_ => _.ProjectInfo.ProjectIdentifier == ProjectIdentifier)
?? throw new ArgumentException("The project was not found, try to scan in the founder page");

await FetchApprovedSignatures(FounderProject);
}
Logger.LogDebug("End of OnInitializedAsync");
}

private async Task FetchApprovedSignatures(FounderProject founderProject)
{
// Fetch all approvals
SignService.LookupInvestmentRequestApprovals(
founderProject.ProjectInfo.NostrPubKey,
(investorNostrPubKey, timeApproved, reqEventId) =>
{
approvalsDictionary[investorNostrPubKey] = (timeApproved, reqEventId);
},
async () =>
{
// After fetching approvals, look up all release signatures
foreach (var investorNostrPubKey in approvalsDictionary.Keys.ToList())
{
SignService.LookupReleaseSigs(
investorNostrPubKey,
founderProject.ProjectInfo.NostrPubKey,
approvalsDictionary[investorNostrPubKey].TimeApproved,
approvalsDictionary[investorNostrPubKey].EventId,
async (releaseSigContent) =>
{
// Mark the item in the dictionary as found
approvalsDictionary[investorNostrPubKey] = (approvalsDictionary[investorNostrPubKey].TimeApproved, "Found");
});
}

// Log the results
foreach (var approval in approvalsDictionary)
{
Logger.LogDebug($"Investor: {approval.Key}, TimeApproved: {approval.Value.TimeApproved}, Status: {approval.Value.EventId}");
}

StateHasChanged();
});
}

protected override async Task OnAfterRenderAsync(bool firstRender)
{
Logger.LogDebug("OnAfterRenderAsync");
// await FetchSignaturesCheckPassword();
}

private void ReleaseFundsToInvestors(MouseEventArgs e)
{
foreach (var transaction in transactions)
{
// todo: in the initial communication between the founder and investor the investor should provide the address to send the funds on a release.
string releaseAddress = string.Empty;

var sigs = SignReleaseOfFunds(transaction.Transaction, FounderProject.ProjectInfo, null, releaseAddress);

// publish the sigs to the investors pubkey on nostr under a specific tag an investor will know it is a release of funds sig response
}
}


private SignatureInfo SignReleaseOfFunds(Transaction investorTrx, ProjectInfo info, string founderSigningPrivateKey, string investorReleaseAddress)
{
var transactionHex = investorTrx.ToHex();

// build sigs
var recoveryTrx = InvestorTransactionActions.BuildReleaseInvestorFundsTransaction(info, investorTrx, investorReleaseAddress);
var sig = FounderTransactionActions.SignInvestorRecoveryTransactions(info, transactionHex, recoveryTrx, founderSigningPrivateKey);

// todo: make a similar method for release funds to investors
//if (!_investorTransactionActions.CheckInvestorRecoverySignatures(info, investorTrx, sig))
// throw new InvalidOperationException();

return sig;
}
}
33 changes: 27 additions & 6 deletions src/Angor/Client/Pages/Spend.razor
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
@inject IDerivationOperations _derivationOperations
@inject IWalletOperations _WalletOperations
@inject IFounderTransactionActions _founderTransactionActions
@inject IInvestorTransactionActions _investorTransactionActions
@inject ILogger<Recover> Logger;
@inject IClipboardService _clipboardService

Expand Down Expand Up @@ -62,10 +63,6 @@
</div>
</div>





<div class="animate-fade-in">

@if (firstTimeRefreshSpinner && refreshSpinner)
Expand All @@ -78,6 +75,15 @@
}
else
{
@if (!targetInvestmentReached && projectDateStarted)
{
<div class="alert alert-warning" role="alert">
<h4 class="alert-heading">Target Not Reached</h4>
<p>The target investment amount has not been reached, as founder you can only release the funds back to the investors.</p>
<button class="btn btn-primary" @onclick="ReleaseFundsToInvestors">Release Funds to Investors</button>
</div>
}

<div class="card card-body slide-up">
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="card-title d-flex align-items-center">
Expand Down Expand Up @@ -479,6 +485,9 @@
int totalStages;
TimeSpan? timeUntilNextStage;

bool targetInvestmentReached = false;
bool projectDateStarted = false;

private ProjectInfo project;

private FeeData feeData = new();
Expand Down Expand Up @@ -512,8 +521,6 @@

protected override async Task OnInitializedAsync()
{


project = storage.GetFounderProjects().FirstOrDefault(p => p.ProjectInfo.ProjectIdentifier == ProjectId)?.ProjectInfo;

firstTimeRefreshSpinner = true;
Expand Down Expand Up @@ -555,6 +562,9 @@
currentWithdrawableAmount += availableInvestedAmount;
}
}

targetInvestmentReached = totalAvailableInvestedAmount >= project.TargetAmount;
projectDateStarted = DateTime.UtcNow > project.StartDate;
}

protected override async Task OnAfterRenderAsync(bool firstRender)
Expand Down Expand Up @@ -699,6 +709,12 @@

protected async Task ClaimCoinsCheckPassword(int stageId)
{
if (!targetInvestmentReached)
{
notificationComponent.ShowErrorMessage("Target investment amount has not been reached, you can only release the funds back to the investors");
return;
}

if (passwordComponent.HasPassword())
{
await ClaimCoins(stageId);
Expand Down Expand Up @@ -968,4 +984,9 @@
{
showRawTransactionModal = isVisible;
}

private void ReleaseFundsToInvestors(MouseEventArgs e)
{
NavigationManager.NavigateTo($"/release/{ProjectId}");
}
}
7 changes: 6 additions & 1 deletion src/Angor/Shared/Models/SignRecoveryRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ public class SignRecoveryRequest

public string InvestorNostrPrivateKey { get; set; }
public string NostrPubKey { get; set; }


/// <summary>
/// An address that will be used to release the funds to the investor in case the target amount is not reached.
/// </summary>
public string ReleaseAddress{ get; set; }

public string InvestmentTransaction { get; set; }

public string EncryptedContent { get; set; }
Expand Down
2 changes: 0 additions & 2 deletions src/Angor/Shared/ProtocolNew/FounderTransactionActions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,6 @@ public SignatureInfo SignInvestorRecoveryTransactions(ProjectInfo projectInfo, s

SignatureInfo info = new SignatureInfo { ProjectIdentifier = projectInfo.ProjectIdentifier };

AssemblyLogger.LogAssemblyVersion(key.GetType(), _logger);

// todo: david change to Enumerable.Range
for (var stageIndex = 0; stageIndex < projectInfo.Stages.Count; stageIndex++)
{
Expand Down
Loading
Loading