Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
4c33d1d
Put a limit to founder if amount was not reached
dangershony Dec 18, 2024
0d8e68d
merge main
dangershony Dec 23, 2024
98d87a5
Prepare the methods that will be used to release funds (WIP)
dangershony Dec 23, 2024
4aaecaf
merge master
dangershony Jan 7, 2025
370aaf0
mrege master fixes
dangershony Jan 7, 2025
ddbd786
move the box inside the spinner section
dangershony Jan 9, 2025
7e20013
create a release page
dangershony Jan 9, 2025
10ce92f
lookup release sigs
dangershony Jan 10, 2025
4765868
Add the code so sign and validate the release trx and also add a test…
dangershony Jan 13, 2025
cc31c27
Filter events by subject
dangershony Jan 14, 2025
118c85a
Write the code to sign the release sigs
dangershony Jan 15, 2025
da0d7d0
Sending a json type as encrypted data for the recovery request to fou…
dangershony Jan 16, 2025
322d717
show investor they have a pending release
dangershony Jan 17, 2025
8883765
Create the release page and implement investor release funds
dangershony Jan 20, 2025
24a5e7e
All code is done ready to be test
dangershony Jan 23, 2025
c6f4d7a
Add handling of nostr events in an async event
dangershony Jan 29, 2025
487131e
check if a message was already decrypted
dangershony Jan 29, 2025
3fb58a1
flow now works end to end
dangershony Jan 31, 2025
7145346
Merge branch 'main' into limit-if-target-was-not-reached
dangershony Feb 3, 2025
7dd1df5
add missing braces
dangershony Feb 3, 2025
bcdfea9
Act on review
dangershony Feb 5, 2025
7a4f9ae
Delete the extra `SignatureReleaseItem` class
dangershony Feb 5, 2025
f22d320
merge master
dangershony Feb 6, 2025
494f1a5
Act on review
dangershony Feb 6, 2025
bc1da68
Fix some tests that incorrectly used TargetAmount as coins instead of…
dangershony Feb 6, 2025
f699da1
Some more fixes to the amount param in various places
dangershony Feb 7, 2025
4cf8d6c
Removing the TargetInvestmentReached param
dangershony Feb 11, 2025
3026a3f
act on review
dangershony Feb 12, 2025
8c6af84
Rename to UnfundedReleaseAddress
dangershony Feb 12, 2025
ebee0a1
Write test to call a relay subscription
dangershony Feb 12, 2025
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; }

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

/// <summary>
/// The address to release the funds to if the project did not reach the target.
/// This will be used by the founder when signing the release outputs
/// </summary>
public string ReleaseAddress { get; set; }

/// <summary>
/// The trxid of an unfunded project that the investor has released the funds without a penalty
/// </summary>
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
47 changes: 45 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,24 @@
<strong class="text-danger">Pending</strong>
}
</div>

<div class="mb-3 d-flex justify-content-between">
<span>Founder Released Funds</span>
@if (hasInvestmentReleaseRequests)
{
@if (project.ReleaseTransactionId == null)
{
<a href=@($"/release/{project.ProjectInfo.ProjectIdentifier}") class="btn btn-sm btn-border ms-2" title="Pending">
<strong class="text-danger">Release funds</strong>
</a>
}
else
{
<strong class="text-primary">Funds released</strong>
}
}
</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 +280,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 +307,7 @@

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

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

private void CheckSignatureFromFounder()
{
foreach (var project in projects)
Expand All @@ -361,6 +389,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
Loading