/// <summary> /// Purchase SIFT from one of our accounts. /// </summary> /// <param name="address"> /// The account to use for purchase. /// </param> /// <param name="quantity"> /// The quantity to purchase. /// </param> /// <returns> /// Details of the purchase result indicating whether a success or failure happened. /// </returns> public async Task <SiftPurchaseResponse> PurchaseSift(string address, uint quantity) { try { // Ensure we have the balance decimal purchaseCostWei = WeiPerSift * quantity; EthereumAccount account = Accounts.FirstOrDefault(acc => acc.Address == address); if (account == null) { return(new SiftPurchaseResponse(SiftPurchaseFailureType.UnknownAccount, "The specified account address is not known.")); } if (account.BalanceWei < purchaseCostWei) { return(new SiftPurchaseResponse(SiftPurchaseFailureType.InsufficientFunds, "The specified account does not have sufficient funds.")); } // Determine the gas cost for this TransactionGasInfo gasInfo = await CalculateGasCostForEtherSend(address, IcoContractAddress, purchaseCostWei); decimal gasPrice = gasInfo.GasPrice; decimal gasCost = gasInfo.GasCost; decimal gas = gasInfo.Gas; decimal remainingGasMoney = account.BalanceWei - purchaseCostWei; if (remainingGasMoney < gasCost) { return(new SiftPurchaseResponse(SiftPurchaseFailureType.InsufficientGas, "You do not have enough gas for this transaction.")); } byte gasMultiple = 25; decimal maximumGasCost; while ((maximumGasCost = gasCost * gasMultiple) * gasPrice > remainingGasMoney) { gasMultiple--; } // Prompt to unlock account Logger.ApplicationInstance.Debug("Asking user confirmation for sending " + purchaseCostWei + " from " + address + " to " + IcoContractAddress); Func <TransactionUnlockViewModel> fnc = new Func <TransactionUnlockViewModel>(() => { TransactionUnlockViewModel vm = new TransactionUnlockViewModel(gasCost, gasMultiple, gas, address, IcoContractAddress, purchaseCostWei); TransactionUnlockWindow window = new TransactionUnlockWindow { DataContext = vm }; window.ShowDialog(); return(vm); }); TransactionUnlockViewModel viewModel = Application.Current.Dispatcher.Invoke(fnc); if (viewModel.WasCancelled) { return(new SiftPurchaseResponse(SiftPurchaseFailureType.UserCancelled, "User cancelled transaction on confirmation screen.")); } // Unlock the account SiftDialog dialog = SiftDialog.ShowButtonless("Sending Transaction", "Please wait whilst the transaction to buy SIFT is sent to the Ethereum network, this should only take a few seconds...", true); Logger.ApplicationInstance.Debug("Attempting to unlock " + address); if (!await _web3.Personal.UnlockAccount.SendRequestAsync(address, viewModel.Password == null ? string.Empty : viewModel.Password.ToString(), 120)) { return(new SiftPurchaseResponse(SiftPurchaseFailureType.UnlockError, "Unable to unlock account with the supplied password.")); } // Send and mine the transaction Logger.ApplicationInstance.Debug("Account unlocked, sending " + purchaseCostWei + " from " + address + " to " + IcoContractAddress); TransactionInput transactionInput = new TransactionInput { From = address, To = IcoContractAddress, Value = new HexBigInteger(new BigInteger(purchaseCostWei)), GasPrice = new HexBigInteger(new BigInteger(viewModel.SelectedGasMultiplier * gasCost)), Gas = new HexBigInteger(new BigInteger(viewModel.Gas)) }; Logger.ApplicationInstance.Info("Sending transaction for " + purchaseCostWei + " to " + IcoContractAddress + " from " + address); string transactionHash = await _web3.Eth.Transactions.SendTransaction.SendRequestAsync(transactionInput); Action act = dialog.Close; if (dialog.Dispatcher.CheckAccess()) { act(); } else { dialog.Dispatcher.Invoke(act); } // Return the response of the transaction hash return(new SiftPurchaseResponse(transactionHash)); } catch (Exception ex) { Logger.ApplicationInstance.Error("Error purchasing SIFT", ex); if (ex.Message.Contains("personal_unlockAccount method not implemented")) { return(new SiftPurchaseResponse(SiftPurchaseFailureType.MissingRpcPersonal, "Your geth installation doesn't have the personal RPC enabled, please enable it to continue.")); } else if (ex.Message.Contains("could not decrypt key with given passphrase")) { return(new SiftPurchaseResponse(SiftPurchaseFailureType.PasswordInvalid, "The password you supplied was incorrect")); } return(new SiftPurchaseResponse(SiftPurchaseFailureType.Unknown, ex.ToString())); } }
/// <summary> /// This method provides the main entry point for the background thread that keeps ethereum wallet information synchronised. /// </summary> private void NetworkCheckThreadEntry() { Logger.ApplicationInstance.Info("Network status thread started"); bool connectedYet = false; DateTime nextContractCheckTime = DateTime.UtcNow; while (_isAlive) { try { // Get a list of all accounts currently know about by our wallet string[] addresses = _web3.Eth.Accounts.SendRequestAsync().Result; // First remove any existing accounts that do not exist in our new list if (addresses != null) { EthereumAccount[] removedAccounts = Accounts.Where(ac => !addresses.Contains(ac.Address)).ToArray(); foreach (EthereumAccount removedAccount in removedAccounts) { Accounts.Remove(removedAccount); } // Process each of these accounts foreach (string address in addresses) { // Get the current balance for this account decimal balance = decimal.Parse(_web3.Eth.GetBalance.SendRequestAsync(address).Result.Value.ToString()); // And check the SIFT balance ulong siftBalance = _tokenService.GetBalanceOfAsync <ulong>(address).Result; // See if we have an existing account - if not we'll need to create a new one EthereumAccount existingAccount = Accounts.FirstOrDefault(ac => ac.Address == address); if (existingAccount == null) { Accounts.Add(new EthereumAccount(address, balance, siftBalance)); Logger.ApplicationInstance.Info("Found new account " + address + " with " + balance + " ETH / " + siftBalance + " SIFT"); } else { if (existingAccount.BalanceWei != balance) { existingAccount.BalanceWei = balance; Logger.ApplicationInstance.Info("Account " + address + " now has " + balance + " ETH"); } if (existingAccount.SiftBalance != siftBalance) { existingAccount.SiftBalance = siftBalance; Logger.ApplicationInstance.Info("Account " + address + " now has " + siftBalance + " SIFT"); } } } } // Let's get the block number and check if we're syncing to just set some basics bits and bobs up BlockNumber = ulong.Parse(_web3.Eth.Blocks.GetBlockNumber.SendRequestAsync().Result.Value.ToString()); IsSyncing = _web3.Eth.Syncing.SendRequestAsync().Result.IsSyncing; // Ask the contract our status but first check we've got the right contract if (DateTime.UtcNow >= nextContractCheckTime) { // First check the ICO contract Logger.ApplicationInstance.Debug("Checking ICO contract version"); decimal icoContractVersion = decimal.Parse(_icoContract.GetFunction("contractVersion").CallAsync <ulong>().Result.ToString()); if (icoContractVersion == IcoContractVersion) { // Get the phase of the ICO bool isIcoPhase = _icoContract.GetFunction("icoPhase").CallAsync <bool>().Result; ContractPhase = isIcoPhase ? ContractPhase.Ico : ContractPhase.Trading; // Check if the ICO is abandoned IsIcoAbandoned = _icoContract.GetFunction("icoAbandoned").CallAsync <bool>().Result; // Check the ICO dates IcoStartDate = DateFromTimestamp(_icoContract.GetFunction("icoStartTime").CallAsync <ulong>().Result); IcoEndDate = DateFromTimestamp(_icoContract.GetFunction("icoEndTime").CallAsync <ulong>().Result); // Whilst we're here we also want to check the gas cost to send DefaultGasInfo = CalculateGasCostForEtherSend("0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", IcoContractAddress, 1000000000000000000m).Result; Logger.ApplicationInstance.Error("Default gas calculating for sending as " + DefaultGasInfo.Gas + " at " + DefaultGasInfo.GasPrice + " = " + DefaultGasInfo.GasCost); } else { ContractPhase = ContractPhase.Unknown; Logger.ApplicationInstance.Error("ICO contract mismatch - expected " + IcoContractVersion + " but got " + icoContractVersion + " at " + IcoContractAddress); } // Now check SIFT contract Logger.ApplicationInstance.Debug("Checking SIFT contract version"); decimal siftContractVersion = decimal.Parse(_siftContract.GetFunction("contractVersion").CallAsync <ulong>().Result.ToString()); if (siftContractVersion != SiftContractVersion) { ContractPhase = ContractPhase.Unknown; Logger.ApplicationInstance.Error("SIFT contract mismatch - expected " + SiftContractVersion + " but got " + siftContractVersion + " at " + SiftContractAddress); } // Now check the SIFT contract nextContractCheckTime = DateTime.UtcNow.AddSeconds(10); } // If valid contract check total supply if (ContractPhase != ContractPhase.Unknown) { Logger.ApplicationInstance.Debug("Checking total supply and shareholding"); TotalSupply = _tokenService.GetTotalSupplyAsync <ulong>().Result; foreach (EthereumAccount account in Accounts) { account.ShareholdingPercentage = TotalSupply == 0 ? 0 : (decimal)account.SiftBalance / TotalSupply * 100; } } // Signify a successful loop LastChecksSuccessful = true; if (!connectedYet) { connectedYet = true; Logger.ApplicationInstance.Info("Completed first successful network check, we should be good to go"); } } catch (Exception ex) { LastChecksSuccessful = false; Logger.ApplicationInstance.Error("There was an unexpected error updating Ethereum network information", ex); } // Wait to try again DateTime nextTime = DateTime.UtcNow.AddSeconds(3); while (DateTime.UtcNow < nextTime && _isAlive) { Thread.Sleep(100); } } }