Initial Commit - html and ts added to support mixer tab. Second stage (which I'm now working on) will include integration to Server through client and any changes to the gui following review.
MODIFY
HiddenWallet.Daemon/Controllers/WalletController.cs
HiddenWallet.Daemon/Controllers/WalletController.cs
@@ -313,5 +313,33 @@ namespace HiddenWallet.Daemon.Controllers
return new ObjectResult(new FailureResponse { Message = ex.Message, Details = ex.ToString() });
}
}
+
+ [Route("tumbler-server")]
+ [HttpGet]
+ public IActionResult TumblerServer()
+ {
+ //return new ObjectResult(new SuccessResponse());
+ //This wil go in WalletWrapper:
+ // public Network Network => _walletJob.Safe.Network;
+ // var addressUri = Network == Network.Main ? Config.TumbleBitServerMainNet : Config.TumbleBitServerTestNet;
+ // var address = "";
+ // if (addressUri != null)
+ // {
+ // address = addressUri.AbsoluteUri;
+ // }
+
+ //Fees:
+ //var satoshiFeePerBytes = (await _walletJob.FeeJob.GetFeePerBytesAsync(FeeType.High, default(CancellationToken)).ConfigureAwait(false)).ToDecimal(MoneyUnit.Satoshi);
+
+ return new ObjectResult(new ChaumianCoinJoin.Models.StatusResponse { Success = true, Address = "To Do: set in WalletWrapper.GetTumblerServerResponseAsync", Phase = ChaumianCoinJoin.TumblerPhase.InputRegistration.ToString(), Denomination = "0.15", AnonymitySet = 12, TimeSpentInInputRegistrationInSeconds = 416, MaximumInputsPerAlices = 4, FeePerInputs = "0.00015 BTC", FeePerOutputs = "0.00015 BTC", NetworkFee = "0.002034 BTC", Version = "1" });
+ //try
+ //{
+ // return new ObjectResult(Global.WalletWrapper.GetTumblerServerResponseAsync().Result);
+ //}
+ //catch (Exception ex)
+ //{
+ // return new ObjectResult(new FailureResponse { Message = ex.Message, Details = ex.ToString() });
+ //}
+ }
}
}

MODIFY
HiddenWallet.Gui/app/html/wallet-content.html
HiddenWallet.Gui/app/html/wallet-content.html
<span class="mdi mdi-tor"></span> Build Transaction
</button>
</div>
+
+<div id="mixer-content">
+ <span class="label label-info" style="font-size: 14px;">Mix coins with ZeroLink</span>
+ <hr />
+
+ <label>Tumbler</label>
+ <input id="tumbler-address" type="text" class="form-control" readonly />
+ <br />
+ <label>Status:</label>
+ <span class="label" style="font-size: 13px; margin-left:11.4em;" id="tumbler-status"></span>
+ <br />
+ <div id="mixer-settings-content">
+ <label>Denomination:</label>
+ <span class="label label-success" style="font-size: 13px; margin-left:96px;" id="tumbler-denomination">1</span>
+ <br />
+ <label>Fee Per Input:</label>
+ <span class="label label-success" style="font-size: 13px; margin-left:101px;" id="tumbler-fee-input">2</span>
+ <br />
+ <label>Fee Per Output:</label>
+ <span class="label label-success" style="font-size: 13px; margin-left:89px;" id="tumbler-fee-output">3</span>
+ <br />
+ <br />
+
+ <label>From:</label>
+ <div class="dropdown">
+ <span id="choose-wallet-dropdown-active" onclick="chooseWalletDropdown()" style="font-size: 15px; margin-left:2.2em;" class="label label-danger dropbtn"><span class="mdi mdi-arrow-down-drop-circle"></span> Select Wallet!</span>
+ <div id="choose-wallet-dropdown" class="dropdown-content">
+ <a id="wallet-dropdown-alice" onclick="chooseWalletDropdown('alice')" href="#">Alice</a>
+ <a id="wallet-dropdown-bob" onclick="chooseWalletDropdown('bob')" href="#">Bob</a>
+ </div>
+ </div>
+ <label>To:</label>
+ <span class="label label-danger" id="tumbling-to-wallet" style="font-size: 15px;">?</span>
+ <br />
+ <br />
+ <div id="not-enough-funds-to-mix" style="font-size: 13px;" class="alert alert-danger">Not enough funds to mix</div>
+ <div id="wallet-selected">
+ <form class="form-inline">
+ <label>Amount:</label>
+ <input style="margin-left:1em;" id="amount-input" class="form-control form-inline" onchange="amountChanged()" onkeypress="document.getElementById('amount-input').stepUp(1); return false;" type="number" size="11" />
+ <label>BTC</label>
+ </form>
+ <hr />
+
+ <label>Estimated Network Fees:</label>
+ <span style="font-size: 13px;" class="label label-success" id="tumbling-network-fees"></span>
+ <br />
+ <br />
+
+ <button id="broadcast-button" class="btn btn-lg btn-success btn-block" onclick="">
+ <span class="mdi mdi-tor"></span> Start Mixing
+ </button>
+ </div>
+ </div>
+ <hr />
+ <table class="table table-striped">
+ <thead>
+ <tr>
+ <th>Phase</th>
+ <th>Anonymity Set</th>
+ <th>Time in input phase</th>
+ </tr>
+ </thead>
+ <tbody id="tumbling-phase-records"></tbody>
+ </table>
+</div>
\ No newline at end of file

MODIFY
HiddenWallet.Gui/app/html/wallet-phase-tabs.html
HiddenWallet.Gui/app/html/wallet-phase-tabs.html
<ul class="nav nav-tabs">
<li class="active"><a>Alice Wallet</a></li>
<li><a href="javascript:walletPhaseShow('bob')">Bob Wallet</a></li>
+ <li><a href="javascript:walletPhaseShow('mixer')">Mixer</a></li>
</ul>
</nav>
</div>
<ul class="nav nav-tabs">
<li><a href="javascript:walletPhaseShow('alice')">Alice Wallet</a></li>
<li class="active"><a>Bob Wallet</a></li>
+ <li><a href="javascript:walletPhaseShow('mixer')">Mixer</a></li>
+ </ul>
+ </nav>
+</div>
+
+<div class="masthead" id="mixer-active">
+ <nav>
+ <ul class="nav nav-tabs">
+ <li><a href="javascript:walletPhaseShow('alice')">Alice Wallet</a></li>
+ <li><a href="javascript:walletPhaseShow('bob')">Bob Wallet</a></li>
+ <li class="active"><a>Mixer</a></li>
</ul>
</nav>
</div>
\ No newline at end of file

MODIFY
HiddenWallet.Gui/app/js/default.ts
HiddenWallet.Gui/app/js/default.ts
@@ -91,7 +91,7 @@ document.body.addEventListener('contextmenu', (mouseEvent: MouseEvent) => {
window.onclick = function (mouseEvent: MouseEvent) {
let eventTarget: HTMLElement = mouseEvent.target as HTMLElement;
- if (eventTarget.className !== 'dropbtn') {
+ if (eventTarget.className.indexOf('dropbtn') == -1 ) {
var dropdowns: HTMLCollectionOf<Element> = document.getElementsByClassName("dropdown-content");

MODIFY
HiddenWallet.Gui/app/js/wallet-phase.ts
HiddenWallet.Gui/app/js/wallet-phase.ts
@@ -6,9 +6,12 @@ function walletPhaseShow(tabItem: string = "") {
let hideButton: HTMLElement = document.getElementById("hide-btn");
let alice: HTMLElement = tabsFrame.contentWindow.document.getElementById("alice-active");
let bob: HTMLElement = tabsFrame.contentWindow.document.getElementById("bob-active");
+ let mixer: HTMLElement = tabsFrame.contentWindow.document.getElementById("mixer-active");
let tabs: HTMLElement = document.getElementById("tabs");
let before: HTMLElement = document.getElementById("before-menu-br");
let after: HTMLElement = document.getElementById("after-menu-hr");
+ let menu: HTMLElement = document.getElementById("menu");
+ let balances: HTMLElement = document.getElementById("balances");
hideButton.hidden = false;
@@ -38,6 +41,18 @@ function walletPhaseShow(tabItem: string = "") {
after.style.display = "block";
walletShow('receive');
}
+ else if (tabItem === "mixer") {
+ content.setAttribute("style", "max-height: 430");
+ tabs.innerHTML = mixer.outerHTML;
+ let hintTextArray = ["ZeroLink cannot steal your coins, nor deanonymize you", "Losing internet connection while mixing is dangerous if the Tumbler is malicous"];
+ let randomHint = hintTextArray[Math.floor(Math.random() * hintTextArray.length)];
+ writeHint(randomHint);
+ menu.innerHTML = "";
+ balances.innerHTML = "";
+ before.style.display = "block";
+ after.style.display = "none";
+ mixerShow();
+ }
else if (tabItem === "") {
storage.get('lastAccount', function (error, data) {
if (error) {
@@ -77,6 +92,189 @@ function walletShow(menuItem: string) {
updateWalletContent();
}
+/* When the user clicks on the button,
+toggle between hiding and showing the dropdown content */
+function chooseWalletDropdown(aliceBob: string = "") {
+ let chooseWallet: HTMLElement = document.getElementById("choose-wallet-dropdown");
+ let chooseWalletActive: HTMLElement = document.getElementById("choose-wallet-dropdown-active");
+ let tumblingTo: HTMLElement = document.getElementById("tumbling-to-wallet");
+ let notEnoughFunds: HTMLElement = document.getElementById("not-enough-funds-to-mix");
+ let walletSelected: HTMLElement = document.getElementById("wallet-selected");
+ let amount: HTMLInputElement = document.getElementById("amount-input") as HTMLInputElement;
+ let tumblingFees: HTMLElement = document.getElementById("tumbling-network-fees");
+
+ let resp: any;
+
+ if (aliceBob === "") {
+ chooseWallet.classList.toggle("show");
+ }
+ else if (aliceBob === "alice") {
+ chooseWalletActive.innerHTML = "Alice";
+ tumblingTo.innerHTML = "Bob";
+ resp = httpGetWallet("balances/alice");
+ }
+ else if (aliceBob === "bob") {
+ chooseWalletActive.innerHTML = "Bob";
+ tumblingTo.innerHTML = "Alice";
+ resp = httpGetWallet("balances/bob");
+ }
+ if (aliceBob === "bob" || aliceBob === "alice") {
+ chooseWalletActive.classList.remove("label-danger");
+ chooseWalletActive.classList.add("label-success");
+ tumblingTo.classList.remove("label-danger");
+ tumblingTo.classList.add("label-success");
+
+ //todo - next 2 lines comment out and fake a value - might not be needed
+ //let maximumMixed: number = resp.Available - (resp.Available * (tumblerFeePercent / 100));
+ let maximumMixed: number = 1;
+
+ if (maximumMixed < tumblerDenomination) {
+ notEnoughFunds.style.removeProperty("display");
+ walletSelected.style.display = "none";
+ }
+ else {
+ var times = Math.floor(maximumMixed / tumblerDenomination);
+ //todo - commented out below - might not be needed:
+ //maximumMixed -= (estCycleFee * times);
+ walletSelected.style.removeProperty("display");
+ notEnoughFunds.style.display = "none";
+
+ //todo - we shouldn't be able to change this: needs to be fixed not stepped input:
+ amount.step = String(tumblerDenomination);
+ amount.min = String(tumblerDenomination);
+ amount.max = String(tumblerDenomination);
+ amount.value = String(tumblerDenomination);
+ //todo - commented out might not be needed:
+ //tumblingFees.innerText = estCycleFee + " BTC";
+ }
+ }
+}
+
+function amountChanged() {
+ let amount: HTMLInputElement = document.getElementById("amount-input") as HTMLInputElement;
+ let tumblingFees: HTMLElement = document.getElementById("tumbling-network-fees");
+
+ //todo - commented out below for now - might not be needed
+ //let cycleCount: number = Number(amount.value) / tumblerDenomination;
+ //tumblingFees.innerText = round(cycleCount * estCycleFee, 8) + " BTC";
+}
+
+function round(value: number, precision: number) {
+ let multiplier: number = Math.pow(10, precision || 0);
+ return Math.round(value * multiplier) / multiplier;
+}
+
+let tumblerAddress: string;
+let tumblerPhase: string;
+let tumblerDenomination: number;
+let anonymitySet: number;
+let timeSpentInputRegistration: number;
+let maxInputsPerAlice: number;
+let feePerInputs: string;
+let feePerOutputs: string;
+let version: number;
+let networkFees: string;
+let tumblerStatus: string;
+
+function mixerShow() {
+ let contentElem: HTMLElement = document.getElementById("content");
+ let walletContentFrame: HTMLIFrameElement = <HTMLIFrameElement>document.getElementById("wallet-content-frame");
+ let mixerContentElem: HTMLElement = walletContentFrame.contentWindow.document.getElementById("mixer-content");
+
+ let resp: any;
+
+ contentElem.innerHTML = mixerContentElem.outerHTML;
+
+ let tumblerAddressElem: HTMLInputElement = document.getElementById("tumbler-address") as HTMLInputElement;
+ let tumblerStatusElem: HTMLElement = document.getElementById("tumbler-status");
+ let mixerSettingsContentElem: HTMLElement = document.getElementById("mixer-settings-content");
+ let denominationElem: HTMLElement = document.getElementById("tumbler-denomination");
+ let anonSetElem: HTMLElement = document.getElementById("anonymity-set");
+ let inputPhaseTimeElem: HTMLElement = document.getElementById("input-phase-time");
+ let feeInputElem: HTMLElement = document.getElementById("tumbler-fee-input");
+ let feeOutputElem: HTMLElement = document.getElementById("tumbler-fee-output");
+ let networkFeesElem: HTMLElement = document.getElementById("tumbling-network-fees");
+ let walletSelectedElem: HTMLElement = document.getElementById("wallet-selected");
+ let notEnoughFundsElem: HTMLElement = document.getElementById("not-enough-funds-to-mix");
+
+ resp = httpGetWallet("tumbler-server");
+
+ //TODO - handle resp.Success - show or hide resp.*?
+ //todo - return Status in tumbler-server - online/offline?
+ if (resp.Success) {
+ tumblerStatus = "ONLINE";
+ }
+ else {
+ tumblerStatus = "OFFLINE";
+ }
+
+ tumblerAddress = resp.Address;
+ //todo - helper to convert 0 etc to human readable:
+ tumblerPhase = resp.Phase;
+ tumblerDenomination = resp.Denomination;
+ anonymitySet = resp.AnonymitySet;
+ timeSpentInputRegistration = resp.TimeSpentInInputRegistrationInSeconds;
+ maxInputsPerAlice = resp.MaximumInputsPerAlices;
+ feePerInputs = resp.FeePerInputs;
+ feePerOutputs = resp.FeePerOutputs;
+ networkFees = resp.NetworkFee;
+ version = resp.Version;
+ //todo - version check and error.
+
+ tumblerAddressElem.value = tumblerAddress;
+
+ tumblerStatusElem.innerText = tumblerStatus;
+
+ if (walletState.toUpperCase() === "SyncingHeaders".toUpperCase()
+ || walletState.toUpperCase() === "NotStarted".toUpperCase()
+ || (Number(blocksLeftToSync) !== 0 && Number(blocksLeftToSync) !== 1 && Number(blocksLeftToSync) !== 2)) {
+ tumblerStatusElem.innerText = "Waiting for wallet to sync...";
+ tumblerStatusElem.classList.add("label-danger");
+ mixerSettingsContentElem.style.display = "none";
+ }
+ else if (tumblerStatus != "online".toUpperCase()) {
+ tumblerStatusElem.classList.add("label-danger");
+ mixerSettingsContentElem.style.display = "none";
+ }
+ else {
+ tumblerStatusElem.classList.add("label-success");
+
+ denominationElem.innerText = tumblerDenomination.toString() + " BTC";
+ feeInputElem.innerText = feePerInputs;
+ feeOutputElem.innerText = feePerOutputs;
+ networkFeesElem.innerText = networkFees;
+ //document.getElementById("tumbler-fee").innerText = resp.FeePercent + " %";
+ //document.getElementById("tumbling-time").innerText = Math.round((resp.CycleLengthMinutes / 60) * 10) / 10 + " hours";
+
+ walletSelectedElem.style.display = "none";
+ notEnoughFundsElem.style.display = "none";
+
+ //Although we are just displyaing one row for now we might want to add a row for each phase as we progress through them
+ //so it has been built to handle this in the future:
+
+ let phaseRecords: HTMLElement = document.getElementById("tumbling-phase-records");
+
+ //for (phase in phases) {
+ let trNode: HTMLElement = document.createElement("tr");
+
+ let tdNodePhase: HTMLElement = document.createElement("td");
+ tdNodePhase.innerText = "Input Registration";
+
+ let tdNodeAnonSet: HTMLElement = document.createElement("td");
+ tdNodeAnonSet.innerText = String(anonymitySet);
+
+ let tdNodeTime: HTMLElement = document.createElement("td");
+ tdNodeTime.innerText = String(timeSpentInputRegistration);
+
+ trNode.appendChild(tdNodePhase);
+ trNode.appendChild(tdNodeAnonSet);
+ trNode.appendChild(tdNodeTime);
+ phaseRecords.appendChild(trNode);
+ //}
+ }
+}
+
+
function updateWalletContent() {
let walletContentFrame: HTMLIFrameElement = <HTMLIFrameElement>document.getElementById("wallet-content-frame");

MODIFY
HiddenWallet/ChaumianCoinJoin/Models/StatusResponse.cs
HiddenWallet/ChaumianCoinJoin/Models/StatusResponse.cs
@@ -9,6 +9,7 @@ namespace HiddenWallet.ChaumianCoinJoin.Models
public class StatusResponse : BaseResponse
{
public StatusResponse() => Success = true;
+ public string Address { get; set; }
public string Phase { get; set; }
public string Denomination { get; set; }
public int AnonymitySet { get; set; }
@@ -16,6 +17,7 @@ namespace HiddenWallet.ChaumianCoinJoin.Models
public int MaximumInputsPerAlices { get; set; }
public string FeePerInputs { get; set; }
public string FeePerOutputs { get; set; }
+ public string NetworkFee { get; set; }
public string Version { get; set; }
}
}

Initial Commit - html and ts added to support mixer tab. Second stage (which I'm now working on) will include integration to Server through client and any changes to the gui following review.