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 15 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}
}
25 changes: 24 additions & 1 deletion src/Angor/Client/Models/FounderProject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,30 @@ public class FounderProject : Project

public string ProjectInfoEventId { get; set; }
public bool NostrProfileCreated { get; set; }



/// <summary>
/// The total amount of the project that has been invested in,
/// This parameter will only be set once the founder went to the spend page
/// and was able to calaulate to total amount of funds that have been invested in the project.
///
/// The intention is to use this parameter to know if the founder should be forced to release
/// the funds back to the investor by sending signature of a trx that spend coins to the investors address
/// </summary>
public decimal? TotalAvailableInvestedAmount { get; set; }

public DateTime? ReleaseSignaturesTime { get; set; }

public bool TargetInvestmentReached()
{
return TotalAvailableInvestedAmount >= ProjectInfo.TargetAmount;
}

public bool ProjectDateStarted()
{
return DateTime.UtcNow > ProjectInfo.StartDate;
}

public bool NostrMetadataCreated()
{
return !string.IsNullOrEmpty(Metadata?.Name);
Expand Down
2 changes: 2 additions & 0 deletions src/Angor/Client/Models/InvestmentState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ public class InvestmentState
public string ProjectIdentifier { get; set; }
public string InvestmentTransactionHash { get; set; }
public string investorPubKey { get; set; }
public string ReleaseAddress { get; set; }

}
3 changes: 3 additions & 0 deletions src/Angor/Client/Models/InvestorProject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ public class InvestorProject : Project
public string InvestorPublicKey { get; set; }
public string InvestorNPub { get; set; }

public string ReleaseAddress { get; set; }
public string ReleaseTransactionId { get; set; }

public bool WaitingForFounderResponse()
{
return ReceivedFounderSignatures() == false && SignaturesInfo?.TimeOfSignatureRequest != null;
Expand Down
38 changes: 26 additions & 12 deletions src/Angor/Client/Pages/Invest.razor
Original file line number Diff line number Diff line change
Expand Up @@ -853,25 +853,30 @@ else

var words = await passwordComponent.GetWalletAsync();

var nostrPrivateKey = _derivationOperations.DeriveProjectNostrInvestorPrivateKey(words, project.ProjectInfo.ProjectIdentifier);
var investorNostrPrivateKey = _derivationOperations.DeriveProjectNostrInvestorPrivateKey(words, project.ProjectInfo.ProjectIdentifier);
var nostrPrivateKeyHex = Encoders.Hex.EncodeData(investorNostrPrivateKey.ToBytes());

var nostrPrivateKeyHex = Encoders.Hex.EncodeData(nostrPrivateKey.ToBytes());
var releaseAddress = accountInfo.GetNextReceiveAddress();

SignRecoveryRequest signRecoveryRequest = new()
{
ProjectIdentifier = investorProject.ProjectInfo.ProjectIdentifier,
InvestmentTransactionHex = strippedInvestmentTransaction.ToHex(network.Consensus.ConsensusFactory),
ReleaseAddress = releaseAddress
};

var sigJson = serializer.Serialize(signRecoveryRequest);

var encryptedContent = await encryption.EncryptNostrContentAsync(
nostrPrivateKeyHex, investorProject.ProjectInfo.NostrPubKey,
strippedInvestmentTransaction.ToHex(network.Consensus.ConsensusFactory));
sigJson);

var investmentSigsRequest = _SignService.RequestInvestmentSigs(new SignRecoveryRequest
{
ProjectIdentifier = investorProject.ProjectInfo.ProjectIdentifier,
EncryptedContent = encryptedContent,
NostrPubKey = investorProject.ProjectInfo.NostrPubKey,
InvestorNostrPrivateKey = nostrPrivateKeyHex
});
var investmentSigsRequest = _SignService.RequestInvestmentSigs(encryptedContent, nostrPrivateKeyHex, investorProject.ProjectInfo.NostrPubKey);

investorProject.SignaturesInfo!.TimeOfSignatureRequest = investmentSigsRequest.eventTime;
investorProject.SignaturesInfo!.SignatureRequestEventId = investmentSigsRequest.eventId;
investorProject.InvestorNPub = NostrPrivateKey.FromHex(nostrPrivateKeyHex).DerivePublicKey().Hex;
investorProject.ReleaseAddress = releaseAddress;

storage.AddInvestmentProject(investorProject);

Expand Down Expand Up @@ -989,7 +994,6 @@ else

storage.UpdateInvestmentProject(investorProject);

await SaveInvestmentsListToNostrAsync();

var accountInfo = storage.GetAccountInfo(network.Name);
var unspentInfo = SessionStorage.GetUnconfirmedInboundFunds();
Expand All @@ -1006,6 +1010,9 @@ else

notificationComponent.ShowNotificationMessage("Invested in project", 5);

// this will publish to Nostr so we call it last.
await SaveInvestmentsListToNostrAsync();

NavigationManager.NavigateTo($"/view/{project.ProjectInfo.ProjectIdentifier}");
}
catch (Exception e)
Expand All @@ -1030,7 +1037,14 @@ else
{
ProjectIdentifiers = storage.GetInvestmentProjects()
.Where(x => x.InvestedInProject())
.Select(x => new InvestmentState { ProjectIdentifier = x.ProjectInfo.ProjectIdentifier, investorPubKey = x.InvestorPublicKey, InvestmentTransactionHash = x.TransactionId })
.Select(x => new InvestmentState
{
ProjectIdentifier = x.ProjectInfo.ProjectIdentifier,
investorPubKey = x.InvestorPublicKey,
InvestmentTransactionHash = x.TransactionId,
ReleaseAddress = x.ReleaseAddress,

})
.ToList()
};

Expand Down
37 changes: 35 additions & 2 deletions src/Angor/Client/Pages/Investor.razor
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,10 @@
Stats.TryGetValue(project.ProjectInfo.ProjectIdentifier, out var stats);
var nostrPubKey = project.ProjectInfo.NostrPubKey;
investmentRequestsMap.TryGetValue(nostrPubKey, out bool hasInvestmentRequests);
releaseRequestsMap.TryGetValue(nostrPubKey, out bool hasInvestmentReleaseRequests);

<div class="col">



<div class="card card-body">
<div class="d-flex align-items-center">
<span class="user-select-none">
Expand Down Expand Up @@ -230,6 +229,12 @@
<strong class="text-danger">Pending</strong>
}
</div>
@if (hasInvestmentReleaseRequests)
{
<div class="mb-3 d-flex justify-content-between">
<span>Founder Released Funds</span>
</div>
}
<div class="card-footer">
<a href=@($"/view/{project.ProjectInfo.ProjectIdentifier}") class="btn btn-border w-100">
View Project
Expand Down Expand Up @@ -263,6 +268,7 @@
long TotalInRecovery = 0;

private Dictionary<string, bool> investmentRequestsMap = new Dictionary<string, bool>();
private Dictionary<string, bool> releaseRequestsMap = new Dictionary<string, bool>();

public Dictionary<string, ProjectStats> Stats = new();

Expand All @@ -289,6 +295,7 @@

var refreshTask = RefreshBalance();
CheckSignatureFromFounder();
CheckReleaseFromFounder();
await refreshTask;
}
}
Expand Down Expand Up @@ -345,6 +352,17 @@
return Task.CompletedTask;
}

private Task HandleReleaseSignatureReceivedAsync(string nostrPubKey, string signatureContent)
{
if (releaseRequestsMap.ContainsKey(nostrPubKey))
{
releaseRequestsMap[nostrPubKey] = true;
StateHasChanged();
}

return Task.CompletedTask;
}

private void CheckSignatureFromFounder()
{
foreach (var project in projects)
Expand All @@ -361,6 +379,21 @@
}
}

private void CheckReleaseFromFounder()
{
foreach (var project in projects)
{
releaseRequestsMap[project.ProjectInfo.NostrPubKey] = false;

_SignService.LookupReleaseSigs(
project.InvestorNPub,
project.ProjectInfo.NostrPubKey,
null,
project.SignaturesInfo.SignatureRequestEventId,
signatureContent => HandleReleaseSignatureReceivedAsync(project.ProjectInfo.NostrPubKey, signatureContent)
);
}
}

private async Task RefreshBalance()
{
Expand Down
21 changes: 4 additions & 17 deletions src/Angor/Client/Pages/Recover.razor
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
@inject IWalletOperations _WalletOperations
@inject IInvestorTransactionActions _InvestorTransactionActions
@inject ILogger<Recover> Logger;
@inject IWalletUIService _walletUIService;

@inherits BaseComponent

Expand Down Expand Up @@ -609,20 +610,6 @@
StageInfo.EndOfProject = project.ProjectInfo.ExpiryDate < DateTime.Now;
}

private void AddTransactionToPending(Transaction transaction)
{
var accountInfo = storage.GetAccountInfo(network.Name);
var unconfirmedInbound = _cacheStorage.GetUnconfirmedInboundFunds();
var unconfirmedOutbound = _cacheStorage.GetUnconfirmedOutboundFunds();

unconfirmedInbound.AddRange(_WalletOperations.UpdateAccountUnconfirmedInfoWithSpentTransaction(accountInfo, transaction));
unconfirmedOutbound.AddRange(transaction.Inputs.Select(_ => new Outpoint(_.PrevOut.Hash.ToString(), (int)_.PrevOut.N)));

storage.SetAccountInfo(network.Name, accountInfo);
_cacheStorage.SetUnconfirmedInboundFunds(unconfirmedInbound);
_cacheStorage.SetUnconfirmedOutboundFunds(unconfirmedOutbound);
}

private async Task PrepareToRecoverCoinsCheckPassword()
{
if (!passwordComponent.HasPassword())
Expand Down Expand Up @@ -748,7 +735,7 @@

storage.UpdateInvestmentProject(project);

AddTransactionToPending(recoveryTransaction.Transaction);
_walletUIService.AddTransactionToPending(recoveryTransaction.Transaction);

notificationComponent.ShowNotificationMessage("Done", 1);

Expand Down Expand Up @@ -877,7 +864,7 @@
project.RecoveryReleaseTransactionId = releaseRecoveryTransaction.Transaction.GetHash().ToString();
storage.UpdateInvestmentProject(project);

AddTransactionToPending(releaseRecoveryTransaction.Transaction);
_walletUIService.AddTransactionToPending(releaseRecoveryTransaction.Transaction);

notificationComponent.ShowNotificationMessage("Done", 1);

Expand Down Expand Up @@ -1002,7 +989,7 @@
project.EndOfProjectTransactionId = endOfProjectTransaction.Transaction.GetHash().ToString();
storage.UpdateInvestmentProject(project);

AddTransactionToPending(endOfProjectTransaction.Transaction);
_walletUIService.AddTransactionToPending(endOfProjectTransaction.Transaction);

notificationComponent.ShowNotificationMessage("Done", 1);
}
Expand Down
Loading
Loading