Add SignTransaction
MODIFY
WalletWasabi.Tests/UnitTests/WabiSabi/Client/ArenaClientTests.cs
WalletWasabi.Tests/UnitTests/WabiSabi/Client/ArenaClientTests.cs
@@ -9,6 +9,7 @@ using WalletWasabi.Helpers;
using WalletWasabi.Tests.Helpers;
using WalletWasabi.WabiSabi.Backend;
using WalletWasabi.WabiSabi.Backend.Banning;
+using WalletWasabi.WabiSabi.Backend.Models;
using WalletWasabi.WabiSabi.Backend.PostRequests;
using WalletWasabi.WabiSabi.Backend.Rounds;
using WalletWasabi.WabiSabi.Client;
@@ -83,5 +84,57 @@ namespace WalletWasabi.Tests.UnitTests.WabiSabi.Client
Assert.Single(apiClient.AmountCredentialClient.Credentials.Valuable, x => x.Amount.ToMoney() == reissuanceAmounts.First());
Assert.Single(apiClient.AmountCredentialClient.Credentials.Valuable, x => x.Amount.ToMoney() == reissuanceAmounts.Last());
}
+
+ [Fact]
+ public async Task SignTransactionAsync()
+ {
+ WabiSabiConfig config = new();
+ Round round = WabiSabiFactory.CreateRound(config);
+
+ using Key key1 = new();
+ Alice alice1 = WabiSabiFactory.CreateAlice(key: key1);
+ round.Alices.Add(alice1);
+
+ using Key key2 = new();
+ Alice alice2 = WabiSabiFactory.CreateAlice(key: key2);
+ round.Alices.Add(alice2);
+
+ var coinjoin = round.Coinjoin;
+ using Arena arena = await WabiSabiFactory.CreateAndStartArenaAsync(config, round);
+
+ var mockRpc = new Mock<IRPCClient>();
+ await using var coordinator = new ArenaRequestHandler(config, new Prison(), arena, mockRpc.Object);
+
+ var rnd = new InsecureRandom();
+ var amountClient = new WabiSabiClient(round.AmountCredentialIssuerParameters, 2, rnd, 4300000000000ul);
+ var weightClient = new WabiSabiClient(round.WeightCredentialIssuerParameters, 2, rnd, 2000ul);
+ var apiClient = new ArenaClient(amountClient, weightClient, coordinator);
+
+ round.SetPhase(Phase.TransactionSigning);
+
+ // No inputs in the CoinJoin.
+ await Assert.ThrowsAsync<ArgumentException>(async () => await apiClient.SignTransactionAsync(round.Id, alice1.Coins.ToArray(), new BitcoinSecret(key1, Network.Main), coinjoin));
+
+ coinjoin.Inputs.Add(alice1.Coins.First().Outpoint);
+
+ // Trying to sign coins those are not in the CoinJoin.
+ await Assert.ThrowsAsync<InvalidOperationException>(async () => await apiClient.SignTransactionAsync(round.Id, alice2.Coins.ToArray(), new BitcoinSecret(key2, Network.Main), coinjoin));
+
+ coinjoin.Inputs.Add(alice2.Coins.First().Outpoint);
+ // Trying to sign coins with the wrong secret.
+ await Assert.ThrowsAsync<InvalidOperationException>(async () => await apiClient.SignTransactionAsync(round.Id, alice1.Coins.ToArray(), new BitcoinSecret(key2, Network.Main), coinjoin));
+
+ // Trying to sign coins with the wrong secret.
+ await Assert.ThrowsAsync<InvalidOperationException>(async () => await apiClient.SignTransactionAsync(round.Id, alice1.Coins.ToArray(), new BitcoinSecret(key2, Network.Main), coinjoin));
+
+ Assert.False(round.Coinjoin.HasWitness);
+
+ // Trying to sign coins with the wrong secret.
+ await apiClient.SignTransactionAsync(round.Id, alice1.Coins.ToArray(), new BitcoinSecret(key1, Network.Main), coinjoin);
+ Assert.False(round.Coinjoin.Inputs.All(i => i.HasWitScript()));
+
+ await apiClient.SignTransactionAsync(round.Id, alice2.Coins.ToArray(), new BitcoinSecret(key2, Network.Main), coinjoin);
+ Assert.True(round.Coinjoin.Inputs.All(i => i.HasWitScript()));
+ }
}
}

MODIFY
WalletWasabi/WabiSabi/Client/ArenaClient.cs
WalletWasabi/WabiSabi/Client/ArenaClient.cs
@@ -94,5 +94,43 @@ namespace WalletWasabi.WabiSabi.Client
AmountCredentialClient.HandleResponse(confirmConnectionResponse.ZeroAmountCredentials, zeroAmountCredentialResponseValidation);
WeightCredentialClient.HandleResponse(confirmConnectionResponse.ZeroWeightCredentials, zeroWeightCredentialResponseValidation);
}
+
+ public async Task SignTransactionAsync(Guid roundId, IEnumerable<ICoin> coinsToSign, BitcoinSecret bitcoinSecret, Transaction unsignedCoinJoin)
+ {
+ if (unsignedCoinJoin.Inputs.Count == 0)
+ {
+ throw new ArgumentException("No inputs to sign.", nameof(unsignedCoinJoin));
+ }
+
+ if (!coinsToSign.Any())
+ {
+ throw new ArgumentException("No coins were provided.", nameof(coinsToSign));
+ }
+
+ var myInputs = coinsToSign.ToDictionary(c => c.Outpoint);
+ var signedCoinJoin = unsignedCoinJoin.Clone();
+ List<InputWitnessPair> signatures = new();
+
+ foreach (var txInput in signedCoinJoin.Inputs.AsIndexedInputs().Where(input => myInputs.ContainsKey(input.PrevOut)))
+ {
+ var coin = myInputs[txInput.PrevOut];
+
+ signedCoinJoin.Sign(bitcoinSecret, coin);
+
+ if (!txInput.VerifyScript(coin, out _))
+ {
+ throw new InvalidOperationException("Witness is missing.");
+ }
+
+ signatures.Add(new InputWitnessPair(txInput.Index, txInput.WitScript));
+ }
+
+ if (myInputs.Count != signatures.Count)
+ {
+ throw new InvalidOperationException("Missing signatures.");
+ }
+
+ await RequestHandler.SignTransactionAsync(new TransactionSignaturesRequest(roundId, signatures)).ConfigureAwait(false);
+ }
}
}

Add SignTransaction