Revise cancellation
MODIFY
HiddenWallet.Daemon/Controllers/TumblerController.cs
HiddenWallet.Daemon/Controllers/TumblerController.cs
@@ -44,14 +44,28 @@ namespace HiddenWallet.Daemon.Controllers
coinJoinService.StatusResponse = await coinJoinService.TumblerClient.GetStatusAsync(CancellationToken.None);
}
- if(coinJoinService.TumblingInProcess)
+ return new ObjectResult(new SuccessResponse());
+ }
+ }
+ catch (Exception ex)
{
- return new ObjectResult(new ConnectionResponse { IsMixOngoing = true });
+ return new ObjectResult(new FailureResponse { Message = ex.Message, Details = ex.ToString() });
}
- else
+ }
+
+ [Route("ongoing-mix")]
+ [HttpGet]
+ public IActionResult OngoingMix()
+ {
+ try
+ {
+ if (IsMixOngoing)
{
- return new ObjectResult(new ConnectionResponse { IsMixOngoing = false });
+ return new ObjectResult(new YesNoResponse { Value = true });
}
+ else
+ {
+ return new ObjectResult(new YesNoResponse { Value = false });
}
}
catch (Exception ex)
@@ -60,10 +74,15 @@ namespace HiddenWallet.Daemon.Controllers
}
}
+ // asp.net core brainfart, must keep these static
+ private static bool IsMixOngoing { get; set; }
+ private static CancellationTokenSource CancelMixSource { get; set; } = null;
[Route("tumble")]
[HttpPost]
public async Task<IActionResult> TumbleAsync([FromBody]TumbleRequest request)
{
+ List<uint256> txIds = new List<uint256>();
+ IsMixOngoing = true;
try
{
if (request == null || request.From == null || request.To == null || request.RoundCount == 0)
@@ -77,16 +96,19 @@ namespace HiddenWallet.Daemon.Controllers
var getTo = Global.WalletWrapper.GetAccount(request.To, out SafeAccount toAccount);
if (getTo != null) return new ObjectResult(getTo);
+ CancelMixSource = new CancellationTokenSource();
+
for (int i = 0; i < request.RoundCount; i++)
{
IEnumerable<Script> unusedOutputs = await Global.WalletWrapper.WalletJob.GetUnusedScriptPubKeysAsync(AddressType.Pay2WitnessPublicKeyHash, toAccount, HdPathType.NonHardened);
BitcoinAddress activeOutput = unusedOutputs.RandomElement().GetDestinationAddress(Global.WalletWrapper.Network); // TODO: this is sub-optimal, it'd be better to not which had been already registered and not reregister it
BitcoinWitPubKeyAddress bech32 = new BitcoinWitPubKeyAddress(activeOutput.ToString(), Global.WalletWrapper.Network);
- uint256 txid = await Global.WalletWrapper.WalletJob.CoinJoinService.TumbleAsync(fromAccount, bech32, CancellationToken.None);
- while(!Global.WalletWrapper.WalletJob.MemPoolJob.Transactions.Contains(txid))
+ uint256 txid = await Global.WalletWrapper.WalletJob.CoinJoinService.TumbleAsync(fromAccount, bech32, CancelMixSource.Token);
+ txIds.Add(txid);
+ if(CancelMixSource.Token.IsCancellationRequested)
{
- await Task.Delay(100);
+ return new ObjectResult(new FailureResponse { Message = "Mixing was cancelled", Details = string.Join(Environment.NewLine, txIds.Select(a => a.ToString())) });
}
}
@@ -94,11 +116,16 @@ namespace HiddenWallet.Daemon.Controllers
}
catch(OperationCanceledException)
{
- return new ObjectResult(new FailureResponse { Message = "Mixing was cancelled" });
+ return new ObjectResult(new FailureResponse { Message = "Mixing was cancelled", Details = string.Join(Environment.NewLine, txIds.Select(a => a.ToString())) });
}
catch (Exception ex)
{
- return new ObjectResult(new FailureResponse { Message = ex.Message, Details = ex.ToString() });
+ return new ObjectResult(new FailureResponse { Message = ex.Message, Details = ex.ToString() + Environment.NewLine + Environment.NewLine + string.Join(Environment.NewLine, txIds.Select(a => a.ToString())) });
+ }
+ finally
+ {
+ CancelMixSource?.Dispose();
+ IsMixOngoing = false;
}
}
@@ -108,8 +135,11 @@ namespace HiddenWallet.Daemon.Controllers
{
try
{
- CoinJoinService coinJoinService = Global.WalletWrapper.WalletJob.CoinJoinService;
- await coinJoinService.CancelMixAsync(CancellationToken.None);
+ CancelMixSource?.Cancel();
+ while(IsMixOngoing)
+ {
+ await Task.Delay(100);
+ }
return new ObjectResult(new SuccessResponse());
}
catch (Exception ex)

MODIFY
HiddenWallet.FullSpvWallet/ChaumianCoinJoin/CoinJoinService.cs
HiddenWallet.FullSpvWallet/ChaumianCoinJoin/CoinJoinService.cs
@@ -138,18 +138,14 @@ namespace HiddenWallet.FullSpvWallet.ChaumianCoinJoin
#region Tumbling
- private CancellationTokenSource CancelTumbling { get; set; }
/// <summary>
- /// Participates one tumbling round
+ /// Participates in one tumbling round
/// </summary>
- public async Task<uint256> TumbleAsync(SafeAccount from, BitcoinWitPubKeyAddress to, CancellationToken ctsToken)
+ /// <returns>returns txid of cj, null if didn't arrived</returns>
+ public async Task<uint256> TumbleAsync(SafeAccount from, BitcoinWitPubKeyAddress to, CancellationToken cancel)
{
try
{
- CancelTumbling?.Dispose();
- CancelTumbling = CancellationTokenSource.CreateLinkedTokenSource(ctsToken);
- var cancel = CancelTumbling.Token;
-
CoinJoin = null;
TumblingException = null;
@@ -180,16 +176,7 @@ namespace HiddenWallet.FullSpvWallet.ChaumianCoinJoin
}
}
- StatusResponse status;
- try
- {
- status = await TumblerClient.GetStatusAsync(cancel);
- }
- catch (TorException)
- {
- // ToDo: fix it in DotNetTor, this happens when the tumbler goes offline, then comes back up, the already established connection cannot be reused
- status = await TumblerClient.GetStatusAsync(cancel);
- }
+ StatusResponse status = await TumblerClient.GetStatusAsync(cancel);
StatusResponse = status;
Phase = TumblerPhaseHelpers.GetTumblerPhase(status.Phase);
@@ -209,7 +196,8 @@ namespace HiddenWallet.FullSpvWallet.ChaumianCoinJoin
while (TumblingInProcess)
{
if (TumblerConnection == null) throw new HttpRequestException("Server went offline");
- await Task.Delay(100, cancel);
+ if (Phase == TumblerPhase.InputRegistration) cancel.ThrowIfCancellationRequested();
+ await Task.Delay(100);
}
// if tumbling
@@ -219,8 +207,31 @@ namespace HiddenWallet.FullSpvWallet.ChaumianCoinJoin
}
else
{
- return CoinJoin?.GetHash();
+ // wait for cj to arrive
+ for (int i = 0; i < 81; i++) // 1min default timeout of signing phase + 21 sec
+ {
+ await Task.Delay(1000);
+ var arrived = WalletJob.MemPoolJob.Transactions.Contains(CoinJoin.GetHash());
+ if (arrived)
+ {
+ Debug.WriteLine("Transaction is successfully propagated on the network.");
+ return CoinJoin.GetHash();
+ }
+ }
+ return null;
+ }
+ }
+ catch(OperationCanceledException)
+ {
+ try
+ {
+ await CancelMixAsync(CancellationToken.None);
+ }
+ catch
+ {
+
}
+ throw;
}
finally
{
@@ -410,8 +421,6 @@ namespace HiddenWallet.FullSpvWallet.ChaumianCoinJoin
{
var request = new DisconnectionRequest { UniqueId = UniqueAliceId };
await TumblerClient.PostDisconnectionAsync(request, cancel);
-
- CancelTumbling?.Cancel();
}
}
@@ -448,7 +457,6 @@ namespace HiddenWallet.FullSpvWallet.ChaumianCoinJoin
}
TumblerClient?.Dispose();
- CancelTumbling?.Dispose();
}
catch (Exception)
{

MODIFY
HiddenWallet.Gui/app/html/wallet-content.html
HiddenWallet.Gui/app/html/wallet-content.html
</div>
</div>
<button id="cancel-mixing-button" style="display:none" class="btn btn-lg btn-danger btn-block" onclick="cancelMix()">
- Cancel Ongoing Mixing
+ Stop Mixing
</button>
</div>
</div>
\ No newline at end of file

MODIFY
HiddenWallet.Gui/app/js/wallet-phase.ts
HiddenWallet.Gui/app/js/wallet-phase.ts
@@ -153,27 +153,7 @@ function round(value: number, precision: number) {
}
function mixerShow() {
- httpGetTumblerAsync("connection", function (json)
- {
- try {
- if (json.Success) {
- let cancelMixingButton: HTMLElement = document.getElementById("cancel-mixing-button");
- let mixingInputElements: HTMLElement = document.getElementById("mixing-input-elements");
- if (json.IsMixOngoing)
- {
- mixingInputElements.style.display = "none";
- cancelMixingButton.style.display = "inline";
- }
- else
- {
- mixingInputElements.style.display = "inline";
- cancelMixingButton.style.display = "none";
- }
- }
- } catch (err) {
-
- }
- }); // makes sure the tumbler is initialized
+ httpGetTumblerAsync("connection", function (json) { }); // makes sure status response is acquired at least one and subscribed to the tumbler
let walletContentFrame: HTMLIFrameElement = <HTMLIFrameElement>document.getElementById("wallet-content-frame");
let mixerContentElem: HTMLElement = walletContentFrame.contentWindow.document.getElementById("mixer-content");
@@ -189,6 +169,18 @@ function mixerShow() {
let mixerHrs: HTMLCollectionOf<Element> = document.getElementsByClassName("mixer-hr");
+ let cancelMixingButton: HTMLElement = document.getElementById("cancel-mixing-button");
+ let mixingInputElements: HTMLElement = document.getElementById("mixing-input-elements");
+ let isMixOngoing: boolean = httpGetTumbler("ongoing-mix", true).Value;
+ if (isMixOngoing) {
+ mixingInputElements.style.display = "none";
+ cancelMixingButton.style.display = "inline";
+ }
+ else {
+ mixingInputElements.style.display = "inline";
+ cancelMixingButton.style.display = "none";
+ }
+
if (isTumblerOnline === false) {
tumblerStatusElem.classList.add("label-danger");
tumblerStatusElem.style.display = "inline";
@@ -308,15 +300,17 @@ function mix() {
function cancelMix()
{
- if (tumblerPhase !== "InputRegistration")
- {
- alert("Cancelling mix is only allowed in InputRegistration phase");
- return;
- }
+ let containerElement: Element = document.getElementsByClassName("container").item(0);
+ let cancelButton: HTMLElement = document.getElementById("cancel-mixing-button");
+
+ containerElement.setAttribute("style", "pointer-events:none;");
+
+ cancelButton.innerHTML = '<span class="glyphicon glyphicon-cog spinning"></span> Stopping...';
+
+ httpGetTumblerAsync("cancel-mix", function (json) {
try {
- let json = httpGetTumbler("cancel-mix");
if (json.Success === false) {
- let alertMessage: string = `Couldn't build the tansaction: ${json.Message}`;
+ let alertMessage: string = `Couldn't cancel the mix: ${json.Message}`;
if (!isBlank(json.Details)) {
alertMessage = `${alertMessage}
@@ -328,11 +322,13 @@ function cancelMix()
alert(alertMessage);
}
mixerShow();
- } catch (e) {
- alert("Couldn't cancel the mix. Reason:" +
+ } catch (err) {
- e);
}
+
+ cancelButton.innerHTML = '<span class="mdi mdi-tor"></span> Stop Mixing';
+ containerElement.setAttribute("style", "pointer-events:all;");
+ });
}
function updateWalletContent() {

MODIFY
HiddenWallet/ChaumianCoinJoin/ChaumianTumblerClient.cs
HiddenWallet/ChaumianCoinJoin/ChaumianTumblerClient.cs
@@ -37,8 +37,16 @@ namespace HiddenWallet.ChaumianCoinJoin
{
using (await _asyncLock.LockAsync())
{
- HttpResponseMessage response =
- await HttpClient.GetAsync("", HttpCompletionOption.ResponseContentRead, cancel);
+ HttpResponseMessage response;
+ try
+ {
+ response = await HttpClient.GetAsync("", HttpCompletionOption.ResponseContentRead, cancel);
+ }
+ catch
+ {
+ await Task.Delay(3000);
+ response = await HttpClient.GetAsync("", HttpCompletionOption.ResponseContentRead, cancel);
+ }
if (!response.IsSuccessStatusCode) throw new HttpRequestException(response.StatusCode.ToString());
string responseString = await response.Content.ReadAsStringAsync();
@@ -50,8 +58,18 @@ namespace HiddenWallet.ChaumianCoinJoin
{
using (await _asyncLock.LockAsync())
{
- HttpResponseMessage response =
+ HttpResponseMessage response;
+ try
+ {
+ response =
await HttpClient.GetAsync("status", HttpCompletionOption.ResponseContentRead, cancel);
+ }
+ catch
+ {
+ await Task.Delay(3000);
+ response =
+ await HttpClient.GetAsync("status", HttpCompletionOption.ResponseContentRead, cancel);
+ }
if (!response.IsSuccessStatusCode) throw new HttpRequestException(response.StatusCode.ToString());
string responseString = await response.Content.ReadAsStringAsync();
@@ -71,8 +89,18 @@ namespace HiddenWallet.ChaumianCoinJoin
Encoding.UTF8,
"application/json");
- HttpResponseMessage response =
+ HttpResponseMessage response;
+ try
+ {
+ response =
+ await HttpClient.PostAsync("inputs", content, cancel);
+ }
+ catch
+ {
+ await Task.Delay(3000);
+ response =
await HttpClient.PostAsync("inputs", content, cancel);
+ }
if (!response.IsSuccessStatusCode) throw new HttpRequestException(response.StatusCode.ToString());
string responseString = await response.Content.ReadAsStringAsync();
@@ -92,8 +120,18 @@ namespace HiddenWallet.ChaumianCoinJoin
Encoding.UTF8,
"application/json");
- HttpResponseMessage response =
+ HttpResponseMessage response;
+ try
+ {
+ response =
await HttpClient.PostAsync("connection-confirmation", content, cancel);
+ }
+ catch
+ {
+ await Task.Delay(3000);
+ response =
+ await HttpClient.PostAsync("connection-confirmation", content, cancel);
+ }
if (!response.IsSuccessStatusCode) throw new HttpRequestException(response.StatusCode.ToString());
string responseString = await response.Content.ReadAsStringAsync();
@@ -113,8 +151,18 @@ namespace HiddenWallet.ChaumianCoinJoin
Encoding.UTF8,
"application/json");
- HttpResponseMessage response =
+ HttpResponseMessage response;
+ try
+ {
+ response =
+ await HttpClient.PostAsync("disconnection", content, cancel);
+ }
+ catch
+ {
+ await Task.Delay(3000);
+ response =
await HttpClient.PostAsync("disconnection", content, cancel);
+ }
if (!response.IsSuccessStatusCode) throw new HttpRequestException(response.StatusCode.ToString());
string responseString = await response.Content.ReadAsStringAsync();
@@ -132,8 +180,18 @@ namespace HiddenWallet.ChaumianCoinJoin
Encoding.UTF8,
"application/json");
- HttpResponseMessage response =
+ HttpResponseMessage response;
+ try
+ {
+ response =
+ await HttpClient.PostAsync("output", content, cancel);
+ }
+ catch
+ {
+ await Task.Delay(3000);
+ response =
await HttpClient.PostAsync("output", content, cancel);
+ }
if (!response.IsSuccessStatusCode) throw new HttpRequestException(response.StatusCode.ToString());
string responseString = await response.Content.ReadAsStringAsync();
@@ -151,8 +209,18 @@ namespace HiddenWallet.ChaumianCoinJoin
Encoding.UTF8,
"application/json");
- HttpResponseMessage response =
+ HttpResponseMessage response;
+ try
+ {
+ response =
+ await HttpClient.PostAsync("coinjoin", content, cancel);
+ }
+ catch
+ {
+ await Task.Delay(3000);
+ response =
await HttpClient.PostAsync("coinjoin", content, cancel);
+ }
if (!response.IsSuccessStatusCode) throw new HttpRequestException(response.StatusCode.ToString());
string responseString = await response.Content.ReadAsStringAsync();
@@ -172,8 +240,18 @@ namespace HiddenWallet.ChaumianCoinJoin
Encoding.UTF8,
"application/json");
- HttpResponseMessage response =
+ HttpResponseMessage response;
+ try
+ {
+ response =
+ await HttpClient.PostAsync("signature", content, cancel);
+ }
+ catch
+ {
+ await Task.Delay(3000);
+ response =
await HttpClient.PostAsync("signature", content, cancel);
+ }
if (!response.IsSuccessStatusCode) throw new HttpRequestException(response.StatusCode.ToString());
string responseString = await response.Content.ReadAsStringAsync();
@@ -185,8 +263,18 @@ namespace HiddenWallet.ChaumianCoinJoin
{
using (await _asyncLock.LockAsync())
{
- HttpResponseMessage response =
+ HttpResponseMessage response;
+ try
+ {
+ response =
await HttpClient.GetAsync("input-registration-status", HttpCompletionOption.ResponseContentRead, cancel);
+ }
+ catch
+ {
+ await Task.Delay(3000);
+ response =
+ await HttpClient.GetAsync("input-registration-status", HttpCompletionOption.ResponseContentRead, cancel);
+ }
if (!response.IsSuccessStatusCode) throw new HttpRequestException(response.StatusCode.ToString());
string responseString = await response.Content.ReadAsStringAsync();

Revise cancellation