39 changed files with 1905 additions and 86 deletions
@ -1 +1 @@ |
|||
Subproject commit 8fd14ce1338328efb6cea4e847766515b5b217a0 |
|||
Subproject commit eab42c0f0580452968f579bba3904bfc6480ae0f |
@ -0,0 +1,25 @@ |
|||
package com.sparrowwallet.sparrow.control; |
|||
|
|||
import com.sparrowwallet.sparrow.AppServices; |
|||
import javafx.concurrent.Worker; |
|||
import javafx.scene.control.DialogPane; |
|||
import javafx.scene.image.Image; |
|||
import javafx.scene.image.ImageView; |
|||
import org.controlsfx.dialog.ProgressDialog; |
|||
|
|||
public class ServiceProgressDialog extends ProgressDialog { |
|||
public ServiceProgressDialog(String title, String header, String imagePath, Worker<?> worker) { |
|||
super(worker); |
|||
|
|||
final DialogPane dialogPane = getDialogPane(); |
|||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm()); |
|||
AppServices.setStageIcon(dialogPane.getScene().getWindow()); |
|||
|
|||
setTitle(title); |
|||
setHeaderText(header); |
|||
|
|||
dialogPane.getStyleClass().remove("progress-dialog"); |
|||
Image image = new Image(imagePath); |
|||
dialogPane.setGraphic(new ImageView(image)); |
|||
} |
|||
} |
@ -0,0 +1,27 @@ |
|||
package com.sparrowwallet.sparrow.event; |
|||
|
|||
import com.sparrowwallet.drongo.wallet.Wallet; |
|||
import com.sparrowwallet.sparrow.io.Storage; |
|||
|
|||
public class ChildWalletAddedEvent extends WalletChangedEvent { |
|||
private final Storage storage; |
|||
private final Wallet childWallet; |
|||
|
|||
public ChildWalletAddedEvent(Storage storage, Wallet masterWallet, Wallet childWallet) { |
|||
super(masterWallet); |
|||
this.storage = storage; |
|||
this.childWallet = childWallet; |
|||
} |
|||
|
|||
public Storage getStorage() { |
|||
return storage; |
|||
} |
|||
|
|||
public Wallet getChildWallet() { |
|||
return childWallet; |
|||
} |
|||
|
|||
public String getMasterWalletId() { |
|||
return storage.getWalletId(getWallet()); |
|||
} |
|||
} |
@ -0,0 +1,109 @@ |
|||
package com.sparrowwallet.sparrow.net; |
|||
|
|||
/* |
|||
* Copyright 2002-2019 the original author or authors. |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* https://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
|
|||
import java.net.InetAddress; |
|||
import java.net.UnknownHostException; |
|||
|
|||
/** |
|||
* Matches a request based on IP Address or subnet mask matching against the remote |
|||
* address. |
|||
* <p> |
|||
* Both IPv6 and IPv4 addresses are supported, but a matcher which is configured with an |
|||
* IPv4 address will never match a request which returns an IPv6 address, and vice-versa. |
|||
* |
|||
* @author Luke Taylor |
|||
* @since 3.0.2 |
|||
* |
|||
* Slightly modified by omidzk to have zero dependency to any frameworks other than the JRE. |
|||
*/ |
|||
public final class IpAddressMatcher { |
|||
private static final IpAddressMatcher LOCAL_RANGE_1 = new IpAddressMatcher("10.0.0.0/8"); |
|||
private static final IpAddressMatcher LOCAL_RANGE_2 = new IpAddressMatcher("172.16.0.0/12"); |
|||
private static final IpAddressMatcher LOCAL_RANGE_3 = new IpAddressMatcher("192.168.0.0/16"); |
|||
|
|||
private final int nMaskBits; |
|||
private final InetAddress requiredAddress; |
|||
|
|||
/** |
|||
* Takes a specific IP address or a range specified using the IP/Netmask (e.g. |
|||
* 192.168.1.0/24 or 202.24.0.0/14). |
|||
* |
|||
* @param ipAddress the address or range of addresses from which the request must |
|||
* come. |
|||
*/ |
|||
public IpAddressMatcher(String ipAddress) { |
|||
|
|||
if (ipAddress.indexOf('/') > 0) { |
|||
String[] addressAndMask = ipAddress.split("/"); |
|||
ipAddress = addressAndMask[0]; |
|||
nMaskBits = Integer.parseInt(addressAndMask[1]); |
|||
} |
|||
else { |
|||
nMaskBits = -1; |
|||
} |
|||
requiredAddress = parseAddress(ipAddress); |
|||
assert (requiredAddress.getAddress().length * 8 >= nMaskBits) : |
|||
String.format("IP address %s is too short for bitmask of length %d", |
|||
ipAddress, nMaskBits); |
|||
} |
|||
|
|||
public boolean matches(String address) { |
|||
InetAddress remoteAddress = parseAddress(address); |
|||
|
|||
if (!requiredAddress.getClass().equals(remoteAddress.getClass())) { |
|||
return false; |
|||
} |
|||
|
|||
if (nMaskBits < 0) { |
|||
return remoteAddress.equals(requiredAddress); |
|||
} |
|||
|
|||
byte[] remAddr = remoteAddress.getAddress(); |
|||
byte[] reqAddr = requiredAddress.getAddress(); |
|||
|
|||
int nMaskFullBytes = nMaskBits / 8; |
|||
byte finalByte = (byte) (0xFF00 >> (nMaskBits & 0x07)); |
|||
|
|||
// System.out.println("Mask is " + new sun.misc.HexDumpEncoder().encode(mask));
|
|||
|
|||
for (int i = 0; i < nMaskFullBytes; i++) { |
|||
if (remAddr[i] != reqAddr[i]) { |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
if (finalByte != 0) { |
|||
return (remAddr[nMaskFullBytes] & finalByte) == (reqAddr[nMaskFullBytes] & finalByte); |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
private InetAddress parseAddress(String address) { |
|||
try { |
|||
return InetAddress.getByName(address); |
|||
} |
|||
catch (UnknownHostException e) { |
|||
throw new IllegalArgumentException("Failed to parse address" + address, e); |
|||
} |
|||
} |
|||
|
|||
public static boolean isLocalNetworkAddress(String address) { |
|||
return LOCAL_RANGE_1.matches(address) || LOCAL_RANGE_2.matches(address) || LOCAL_RANGE_3.matches(address); |
|||
} |
|||
} |
@ -0,0 +1,277 @@ |
|||
package com.sparrowwallet.sparrow.whirlpool; |
|||
|
|||
import com.samourai.wallet.api.backend.BackendApi; |
|||
import com.samourai.wallet.api.backend.MinerFee; |
|||
import com.samourai.wallet.api.backend.MinerFeeTarget; |
|||
import com.samourai.wallet.api.backend.beans.*; |
|||
import com.sparrowwallet.drongo.ExtendedKey; |
|||
import com.sparrowwallet.drongo.KeyPurpose; |
|||
import com.sparrowwallet.drongo.Network; |
|||
import com.sparrowwallet.drongo.Utils; |
|||
import com.sparrowwallet.drongo.address.Address; |
|||
import com.sparrowwallet.drongo.protocol.*; |
|||
import com.sparrowwallet.drongo.wallet.BlockTransaction; |
|||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex; |
|||
import com.sparrowwallet.drongo.wallet.Wallet; |
|||
import com.sparrowwallet.drongo.wallet.WalletNode; |
|||
import com.sparrowwallet.sparrow.AppServices; |
|||
import com.sparrowwallet.sparrow.net.ElectrumServer; |
|||
import org.slf4j.Logger; |
|||
import org.slf4j.LoggerFactory; |
|||
|
|||
import java.util.*; |
|||
|
|||
@SuppressWarnings("deprecation") |
|||
public class SparrowBackendApi extends BackendApi { |
|||
private static final Logger log = LoggerFactory.getLogger(SparrowBackendApi.class); |
|||
private static final int FALLBACK_FEE_RATE = 75; |
|||
|
|||
public SparrowBackendApi() { |
|||
super(null, null); |
|||
} |
|||
|
|||
@Override |
|||
public TxsResponse fetchTxs(String[] zpubs, int page, int count) throws Exception { |
|||
List<TxsResponse.Tx> txes = new ArrayList<>(); |
|||
|
|||
for(String zpub : zpubs) { |
|||
Wallet wallet = getWallet(zpub); |
|||
if(wallet == null) { |
|||
log.debug("No wallet for " + zpub + " found"); |
|||
continue; |
|||
} |
|||
|
|||
for(BlockTransaction blockTransaction : wallet.getTransactions().values()) { |
|||
TxsResponse.Tx tx = new TxsResponse.Tx(); |
|||
tx.block_height = blockTransaction.getHeight(); |
|||
tx.hash = blockTransaction.getHashAsString(); |
|||
tx.locktime = blockTransaction.getTransaction().getLocktime(); |
|||
tx.time = blockTransaction.getDate().getTime(); |
|||
tx.version = (int)blockTransaction.getTransaction().getVersion(); |
|||
|
|||
tx.inputs = new TxsResponse.TxInput[blockTransaction.getTransaction().getInputs().size()]; |
|||
for(int i = 0; i < blockTransaction.getTransaction().getInputs().size(); i++) { |
|||
TransactionInput txInput = blockTransaction.getTransaction().getInputs().get(i); |
|||
tx.inputs[i] = new TxsResponse.TxInput(); |
|||
tx.inputs[i].vin = txInput.getIndex(); |
|||
tx.inputs[i].sequence = txInput.getSequenceNumber(); |
|||
tx.inputs[i].prev_out = new TxsResponse.TxOut(); |
|||
tx.inputs[i].prev_out.txid = txInput.getOutpoint().getHash().toString(); |
|||
tx.inputs[i].prev_out.vout = (int)txInput.getOutpoint().getIndex(); |
|||
|
|||
BlockTransaction spentTransaction = wallet.getTransactions().get(txInput.getOutpoint().getHash()); |
|||
if(spentTransaction != null) { |
|||
TransactionOutput spentOutput = spentTransaction.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex()); |
|||
tx.inputs[i].prev_out.value = spentOutput.getValue(); |
|||
Address[] addresses = spentOutput.getScript().getToAddresses(); |
|||
if(addresses.length > 0) { |
|||
tx.inputs[i].prev_out.addr = addresses[0].toString(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
tx.out = new TxsResponse.TxOutput[blockTransaction.getTransaction().getOutputs().size()]; |
|||
for(int i = 0; i < blockTransaction.getTransaction().getOutputs().size(); i++) { |
|||
TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get(i); |
|||
tx.out[i].n = txOutput.getIndex(); |
|||
tx.out[i].value = txOutput.getValue(); |
|||
Address[] addresses = txOutput.getScript().getToAddresses(); |
|||
if(addresses.length > 0) { |
|||
tx.out[i].addr = addresses[0].toString(); |
|||
} |
|||
} |
|||
|
|||
txes.add(tx); |
|||
} |
|||
} |
|||
|
|||
List<TxsResponse.Tx> pageTxes; |
|||
if(txes.size() < count) { |
|||
pageTxes = txes; |
|||
} else { |
|||
pageTxes = txes.subList(page * count, Math.min((page * count) + count, txes.size())); |
|||
} |
|||
|
|||
TxsResponse txsResponse = new TxsResponse(); |
|||
txsResponse.n_tx = txes.size(); |
|||
txsResponse.page = page; |
|||
txsResponse.n_tx_page = pageTxes.size(); |
|||
txsResponse.txs = pageTxes.toArray(new TxsResponse.Tx[0]); |
|||
|
|||
return txsResponse; |
|||
} |
|||
|
|||
@Override |
|||
public WalletResponse fetchWallet(String[] zpubs) throws Exception { |
|||
WalletResponse walletResponse = new WalletResponse(); |
|||
walletResponse.wallet = new WalletResponse.Wallet(); |
|||
|
|||
Map<Sha256Hash, BlockTransaction> allTransactions = new HashMap<>(); |
|||
Map<Sha256Hash, String> allTransactionsZpubs = new HashMap<>(); |
|||
List<WalletResponse.Address> addresses = new ArrayList<>(); |
|||
List<WalletResponse.Tx> txes = new ArrayList<>(); |
|||
List<UnspentOutput> unspentOutputs = new ArrayList<>(); |
|||
int storedBlockHeight = 0; |
|||
|
|||
for(String zpub : zpubs) { |
|||
Wallet wallet = getWallet(zpub); |
|||
if(wallet == null) { |
|||
log.debug("No wallet for " + zpub + " found"); |
|||
continue; |
|||
} |
|||
|
|||
allTransactions.putAll(wallet.getTransactions()); |
|||
wallet.getTransactions().keySet().forEach(txid -> allTransactionsZpubs.put(txid, zpub)); |
|||
if(wallet.getStoredBlockHeight() != null) { |
|||
storedBlockHeight = Math.max(storedBlockHeight, wallet.getStoredBlockHeight()); |
|||
} |
|||
|
|||
WalletResponse.Address address = new WalletResponse.Address(); |
|||
List<ExtendedKey.Header> headers = ExtendedKey.Header.getHeaders(Network.get()); |
|||
ExtendedKey.Header header = headers.stream().filter(head -> head.getDefaultScriptType().equals(wallet.getScriptType()) && !head.isPrivateKey()).findFirst().orElse(ExtendedKey.Header.xpub); |
|||
address.address = wallet.getKeystores().get(0).getExtendedPublicKey().toString(header); |
|||
address.account_index = wallet.getNode(KeyPurpose.RECEIVE).getHighestUsedIndex() == null ? 0 : wallet.getNode(KeyPurpose.RECEIVE).getHighestUsedIndex() + 1; |
|||
address.change_index = wallet.getNode(KeyPurpose.CHANGE).getHighestUsedIndex() == null ? 0 : wallet.getNode(KeyPurpose.CHANGE).getHighestUsedIndex() + 1; |
|||
address.n_tx = wallet.getTransactions().size(); |
|||
addresses.add(address); |
|||
|
|||
for(Map.Entry<BlockTransactionHashIndex, WalletNode> utxo : wallet.getWalletUtxos().entrySet()) { |
|||
BlockTransaction blockTransaction = wallet.getTransactions().get(utxo.getKey().getHash()); |
|||
if(blockTransaction != null) { |
|||
unspentOutputs.add(Whirlpool.getUnspentOutput(wallet, utxo.getValue(), blockTransaction, (int)utxo.getKey().getIndex())); |
|||
} |
|||
} |
|||
} |
|||
|
|||
for(BlockTransaction blockTransaction : allTransactions.values()) { |
|||
WalletResponse.Tx tx = new WalletResponse.Tx(); |
|||
tx.block_height = blockTransaction.getHeight(); |
|||
tx.hash = blockTransaction.getHashAsString(); |
|||
tx.locktime = blockTransaction.getTransaction().getLocktime(); |
|||
tx.version = (int)blockTransaction.getTransaction().getVersion(); |
|||
|
|||
tx.inputs = new WalletResponse.TxInput[blockTransaction.getTransaction().getInputs().size()]; |
|||
for(int i = 0; i < blockTransaction.getTransaction().getInputs().size(); i++) { |
|||
TransactionInput txInput = blockTransaction.getTransaction().getInputs().get(i); |
|||
tx.inputs[i] = new WalletResponse.TxInput(); |
|||
tx.inputs[i].vin = txInput.getIndex(); |
|||
tx.inputs[i].sequence = txInput.getSequenceNumber(); |
|||
if(allTransactionsZpubs.containsKey(txInput.getOutpoint().getHash())) { |
|||
tx.inputs[i].prev_out = new WalletResponse.TxOut(); |
|||
tx.inputs[i].prev_out.txid = txInput.getOutpoint().getHash().toString(); |
|||
tx.inputs[i].prev_out.vout = (int)txInput.getOutpoint().getIndex(); |
|||
|
|||
BlockTransaction spentTransaction = allTransactions.get(txInput.getOutpoint().getHash()); |
|||
if(spentTransaction != null) { |
|||
TransactionOutput spentOutput = spentTransaction.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex()); |
|||
tx.inputs[i].prev_out.value = spentOutput.getValue(); |
|||
} |
|||
|
|||
tx.inputs[i].prev_out.xpub = new UnspentOutput.Xpub(); |
|||
tx.inputs[i].prev_out.xpub.m = allTransactionsZpubs.get(txInput.getOutpoint().getHash()); |
|||
} |
|||
} |
|||
|
|||
tx.out = new WalletResponse.TxOutput[blockTransaction.getTransaction().getOutputs().size()]; |
|||
for(int i = 0; i < blockTransaction.getTransaction().getOutputs().size(); i++) { |
|||
TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get(i); |
|||
tx.out[i] = new WalletResponse.TxOutput(); |
|||
tx.out[i].n = txOutput.getIndex(); |
|||
tx.out[i].value = txOutput.getValue(); |
|||
tx.out[i].xpub = new UnspentOutput.Xpub(); |
|||
tx.out[i].xpub.m = allTransactionsZpubs.get(blockTransaction.getHash()); |
|||
} |
|||
|
|||
txes.add(tx); |
|||
} |
|||
|
|||
walletResponse.addresses = addresses.toArray(new WalletResponse.Address[0]); |
|||
walletResponse.txs = txes.toArray(new WalletResponse.Tx[0]); |
|||
walletResponse.unspent_outputs = unspentOutputs.toArray(new UnspentOutput[0]); |
|||
|
|||
walletResponse.info = new WalletResponse.Info(); |
|||
walletResponse.info.latest_block = new WalletResponse.InfoBlock(); |
|||
walletResponse.info.latest_block.height = AppServices.getCurrentBlockHeight() == null ? storedBlockHeight : AppServices.getCurrentBlockHeight(); |
|||
walletResponse.info.latest_block.hash = Sha256Hash.ZERO_HASH.toString(); |
|||
walletResponse.info.latest_block.time = AppServices.getLatestBlockHeader() == null ? 1 : AppServices.getLatestBlockHeader().getTime(); |
|||
|
|||
walletResponse.info.fees = new LinkedHashMap<>(); |
|||
for(MinerFeeTarget target : MinerFeeTarget.values()) { |
|||
walletResponse.info.fees.put(target.getValue(), AppServices.getTargetBlockFeeRates() == null ? FALLBACK_FEE_RATE : getMinimumFeeForTarget(Integer.parseInt(target.getValue()))); |
|||
} |
|||
|
|||
return walletResponse; |
|||
} |
|||
|
|||
@Override |
|||
public MinerFee fetchMinerFee() throws Exception { |
|||
Map<String, Integer> fees = new LinkedHashMap<>(); |
|||
for(MinerFeeTarget target : MinerFeeTarget.values()) { |
|||
fees.put(target.getValue(), AppServices.getTargetBlockFeeRates() == null ? FALLBACK_FEE_RATE : getMinimumFeeForTarget(Integer.parseInt(target.getValue()))); |
|||
} |
|||
|
|||
return new MinerFee(fees); |
|||
} |
|||
|
|||
@Override |
|||
public void pushTx(String txHex) throws Exception { |
|||
Transaction transaction = new Transaction(Utils.hexToBytes(txHex)); |
|||
ElectrumServer electrumServer = new ElectrumServer(); |
|||
electrumServer.broadcastTransactionPrivately(transaction); |
|||
} |
|||
|
|||
@Override |
|||
public boolean testConnectivity() { |
|||
return AppServices.isConnected(); |
|||
} |
|||
|
|||
private Integer getMinimumFeeForTarget(int targetBlocks) { |
|||
List<Map.Entry<Integer, Double>> feeRates = new ArrayList<>(AppServices.getTargetBlockFeeRates().entrySet()); |
|||
Collections.reverse(feeRates); |
|||
for(Map.Entry<Integer, Double> feeRate : feeRates) { |
|||
if(feeRate.getKey() <= targetBlocks) { |
|||
return feeRate.getValue().intValue(); |
|||
} |
|||
} |
|||
|
|||
return feeRates.get(0).getValue().intValue(); |
|||
} |
|||
|
|||
@Override |
|||
public void initBip84(String zpub) throws Exception { |
|||
//nothing required
|
|||
} |
|||
|
|||
private Wallet getWallet(String zpub) { |
|||
return AppServices.get().getOpenWallets().keySet().stream() |
|||
.filter(wallet -> { |
|||
List<ExtendedKey.Header> headers = ExtendedKey.Header.getHeaders(Network.get()); |
|||
ExtendedKey.Header header = headers.stream().filter(head -> head.getDefaultScriptType().equals(wallet.getScriptType()) && !head.isPrivateKey()).findFirst().orElse(ExtendedKey.Header.xpub); |
|||
ExtendedKey.Header p2pkhHeader = headers.stream().filter(head -> head.getDefaultScriptType().equals(ScriptType.P2PKH) && !head.isPrivateKey()).findFirst().orElse(ExtendedKey.Header.xpub); |
|||
ExtendedKey extPubKey = wallet.getKeystores().get(0).getExtendedPublicKey(); |
|||
return extPubKey.toString(header).equals(zpub) || extPubKey.toString(p2pkhHeader).equals(zpub); |
|||
}) |
|||
.findFirst() |
|||
.orElse(null); |
|||
} |
|||
|
|||
@Override |
|||
public List<UnspentOutput> fetchUtxos(String zpub) throws Exception { |
|||
throw new UnsupportedOperationException(); |
|||
} |
|||
|
|||
@Override |
|||
public List<UnspentOutput> fetchUtxos(String[] zpubs) throws Exception { |
|||
throw new UnsupportedOperationException(); |
|||
} |
|||
|
|||
@Override |
|||
public Map<String, MultiAddrResponse.Address> fetchAddresses(String[] zpubs) throws Exception { |
|||
throw new UnsupportedOperationException(); |
|||
} |
|||
|
|||
@Override |
|||
public MultiAddrResponse.Address fetchAddress(String zpub) throws Exception { |
|||
throw new UnsupportedOperationException(); |
|||
} |
|||
} |
@ -0,0 +1,11 @@ |
|||
package com.sparrowwallet.sparrow.whirlpool; |
|||
|
|||
import com.samourai.wallet.api.backend.MinerFee; |
|||
import com.samourai.whirlpool.client.wallet.data.minerFee.MinerFeeSupplier; |
|||
|
|||
public class SparrowMinerFeeSupplier extends MinerFeeSupplier { |
|||
public SparrowMinerFeeSupplier(int feeMin, int feeMax, int feeFallback, MinerFee currentMinerFee) { |
|||
super(feeMin, feeMax, feeFallback); |
|||
setValue(currentMinerFee); |
|||
} |
|||
} |
@ -0,0 +1,361 @@ |
|||
package com.sparrowwallet.sparrow.whirlpool; |
|||
|
|||
import com.google.common.eventbus.Subscribe; |
|||
import com.google.common.net.HostAndPort; |
|||
import com.samourai.tor.client.TorClientService; |
|||
import com.samourai.wallet.api.backend.BackendApi; |
|||
import com.samourai.wallet.api.backend.beans.UnspentOutput; |
|||
import com.samourai.wallet.hd.HD_Wallet; |
|||
import com.samourai.wallet.hd.java.HD_WalletFactoryJava; |
|||
import com.samourai.whirlpool.client.event.*; |
|||
import com.samourai.whirlpool.client.tx0.*; |
|||
import com.samourai.whirlpool.client.wallet.WhirlpoolEventService; |
|||
import com.samourai.whirlpool.client.wallet.WhirlpoolWallet; |
|||
import com.samourai.whirlpool.client.wallet.WhirlpoolWalletConfig; |
|||
import com.samourai.whirlpool.client.wallet.WhirlpoolWalletService; |
|||
import com.samourai.whirlpool.client.wallet.beans.*; |
|||
import com.samourai.whirlpool.client.wallet.data.pool.PoolData; |
|||
import com.samourai.whirlpool.client.wallet.data.utxo.UtxoSupplier; |
|||
import com.samourai.whirlpool.client.whirlpool.ServerApi; |
|||
import com.samourai.whirlpool.client.whirlpool.beans.Pool; |
|||
import com.sparrowwallet.drongo.ExtendedKey; |
|||
import com.sparrowwallet.drongo.Network; |
|||
import com.sparrowwallet.drongo.Utils; |
|||
import com.sparrowwallet.drongo.protocol.ScriptType; |
|||
import com.sparrowwallet.drongo.protocol.Sha256Hash; |
|||
import com.sparrowwallet.drongo.protocol.Transaction; |
|||
import com.sparrowwallet.drongo.protocol.TransactionOutput; |
|||
import com.sparrowwallet.drongo.wallet.*; |
|||
import com.sparrowwallet.nightjar.http.JavaHttpClientService; |
|||
import com.sparrowwallet.nightjar.stomp.JavaStompClientService; |
|||
import com.sparrowwallet.nightjar.tor.WhirlpoolTorClientService; |
|||
import com.sparrowwallet.sparrow.AppServices; |
|||
import com.sparrowwallet.sparrow.wallet.UtxoEntry; |
|||
import javafx.concurrent.Service; |
|||
import javafx.concurrent.Task; |
|||
import org.slf4j.Logger; |
|||
import org.slf4j.LoggerFactory; |
|||
|
|||
import java.util.*; |
|||
import java.util.stream.Collectors; |
|||
|
|||
public class Whirlpool { |
|||
private static final Logger log = LoggerFactory.getLogger(Whirlpool.class); |
|||
|
|||
private final HostAndPort torProxy; |
|||
private final WhirlpoolServer whirlpoolServer; |
|||
private final JavaHttpClientService httpClientService; |
|||
private final JavaStompClientService stompClientService; |
|||
private final TorClientService torClientService; |
|||
private final WhirlpoolWalletService whirlpoolWalletService; |
|||
private final WhirlpoolWalletConfig config; |
|||
private HD_Wallet hdWallet; |
|||
|
|||
public Whirlpool(Network network, HostAndPort torProxy, String sCode, int maxClients, int clientDelay) { |
|||
this.torProxy = torProxy; |
|||
this.whirlpoolServer = WhirlpoolServer.valueOf(network.getName().toUpperCase()); |
|||
this.httpClientService = new JavaHttpClientService(torProxy); |
|||
this.stompClientService = new JavaStompClientService(httpClientService); |
|||
this.torClientService = new WhirlpoolTorClientService(); |
|||
this.whirlpoolWalletService = new WhirlpoolWalletService(); |
|||
this.config = computeWhirlpoolWalletConfig(sCode, maxClients, clientDelay); |
|||
|
|||
WhirlpoolEventService.getInstance().register(this); |
|||
} |
|||
|
|||
private WhirlpoolWalletConfig computeWhirlpoolWalletConfig(String sCode, int maxClients, int clientDelay) { |
|||
boolean onion = (torProxy != null); |
|||
String serverUrl = whirlpoolServer.getServerUrl(onion); |
|||
|
|||
ServerApi serverApi = new ServerApi(serverUrl, httpClientService); |
|||
BackendApi backendApi = new SparrowBackendApi(); |
|||
|
|||
WhirlpoolWalletConfig whirlpoolWalletConfig = new WhirlpoolWalletConfig(httpClientService, stompClientService, torClientService, serverApi, whirlpoolServer, false, backendApi); |
|||
whirlpoolWalletConfig.setScode(sCode); |
|||
|
|||
return whirlpoolWalletConfig; |
|||
} |
|||
|
|||
public Collection<Pool> getPools() throws Exception { |
|||
Tx0ParamService tx0ParamService = getTx0ParamService(); |
|||
PoolData poolData = new PoolData(config.getServerApi().fetchPools(), tx0ParamService); |
|||
return poolData.getPools(); |
|||
} |
|||
|
|||
public Tx0Preview getTx0Preview(Pool pool, Collection<UnspentOutput> utxos) throws Exception { |
|||
Tx0Config tx0Config = new Tx0Config(); |
|||
tx0Config.setChangeWallet(WhirlpoolAccount.BADBANK); |
|||
Tx0FeeTarget tx0FeeTarget = Tx0FeeTarget.BLOCKS_4; |
|||
Tx0FeeTarget mixFeeTarget = Tx0FeeTarget.BLOCKS_4; |
|||
|
|||
Tx0ParamService tx0ParamService = getTx0ParamService(); |
|||
|
|||
Tx0Service tx0Service = new Tx0Service(config); |
|||
return tx0Service.tx0Preview(utxos, tx0Config, tx0ParamService.getTx0Param(pool, tx0FeeTarget, mixFeeTarget)); |
|||
} |
|||
|
|||
public Tx0 broadcastTx0(Pool pool, Collection<BlockTransactionHashIndex> utxos) throws Exception { |
|||
WhirlpoolWallet whirlpoolWallet = getWhirlpoolWallet(); |
|||
whirlpoolWallet.start(); |
|||
UtxoSupplier utxoSupplier = whirlpoolWallet.getUtxoSupplier(); |
|||
List<WhirlpoolUtxo> whirlpoolUtxos = utxos.stream().map(ref -> utxoSupplier.findUtxo(ref.getHashAsString(), (int)ref.getIndex())).filter(Objects::nonNull).collect(Collectors.toList()); |
|||
|
|||
if(whirlpoolUtxos.size() != utxos.size()) { |
|||
throw new IllegalStateException("Failed to find UTXOs in Whirlpool wallet"); |
|||
} |
|||
|
|||
Tx0Config tx0Config = new Tx0Config(); |
|||
tx0Config.setChangeWallet(WhirlpoolAccount.BADBANK); |
|||
Tx0FeeTarget tx0FeeTarget = Tx0FeeTarget.BLOCKS_4; |
|||
Tx0FeeTarget mixFeeTarget = Tx0FeeTarget.BLOCKS_4; |
|||
|
|||
return whirlpoolWallet.tx0(whirlpoolUtxos, pool, tx0Config, tx0FeeTarget, mixFeeTarget); |
|||
} |
|||
|
|||
private Tx0ParamService getTx0ParamService() { |
|||
try { |
|||
SparrowMinerFeeSupplier minerFeeSupplier = new SparrowMinerFeeSupplier(config.getFeeMin(), config.getFeeMax(), config.getFeeFallback(), config.getBackendApi().fetchMinerFee()); |
|||
return new Tx0ParamService(minerFeeSupplier, config); |
|||
} catch(Exception e) { |
|||
log.error("Error fetching miner fees", e); |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
public void setHDWallet(Wallet wallet) { |
|||
if(wallet.isEncrypted()) { |
|||
throw new IllegalStateException("Wallet cannot be encrypted"); |
|||
} |
|||
|
|||
try { |
|||
Keystore keystore = wallet.getKeystores().get(0); |
|||
ScriptType scriptType = wallet.getScriptType(); |
|||
int purpose = scriptType.getDefaultDerivation().get(0).num(); |
|||
List<String> words = keystore.getSeed().getMnemonicCode(); |
|||
String passphrase = keystore.getSeed().getPassphrase().asString(); |
|||
HD_WalletFactoryJava hdWalletFactory = HD_WalletFactoryJava.getInstance(); |
|||
byte[] seed = hdWalletFactory.computeSeedFromWords(words); |
|||
hdWallet = new HD_Wallet(purpose, words, whirlpoolServer, seed, passphrase, 1); |
|||
} catch(Exception e) { |
|||
throw new IllegalStateException("Could not create Whirlpool HD wallet ", e); |
|||
} |
|||
} |
|||
|
|||
public WhirlpoolWallet getWhirlpoolWallet() throws WhirlpoolException { |
|||
if(whirlpoolWalletService.whirlpoolWallet() != null) { |
|||
return whirlpoolWalletService.whirlpoolWallet(); |
|||
} |
|||
|
|||
if(hdWallet == null) { |
|||
throw new IllegalStateException("Whirlpool HD wallet not added"); |
|||
} |
|||
|
|||
try { |
|||
return whirlpoolWalletService.openWallet(config, Utils.hexToBytes(hdWallet.getSeedHex()), hdWallet.getPassphrase()); |
|||
} catch(Exception e) { |
|||
throw new WhirlpoolException("Could not create whirlpool wallet ", e); |
|||
} |
|||
} |
|||
|
|||
public HostAndPort getTorProxy() { |
|||
return torProxy; |
|||
} |
|||
|
|||
public boolean hasWallet() { |
|||
return hdWallet != null; |
|||
} |
|||
|
|||
public boolean isStarted() { |
|||
if(whirlpoolWalletService.whirlpoolWallet() == null) { |
|||
return false; |
|||
} |
|||
|
|||
return whirlpoolWalletService.whirlpoolWallet().isStarted(); |
|||
} |
|||
|
|||
public void shutdown() { |
|||
whirlpoolWalletService.closeWallet(); |
|||
httpClientService.shutdown(); |
|||
} |
|||
|
|||
public static UnspentOutput getUnspentOutput(Wallet wallet, WalletNode node, BlockTransaction blockTransaction, int index) { |
|||
TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get(index); |
|||
|
|||
UnspentOutput out = new UnspentOutput(); |
|||
out.tx_hash = txOutput.getHash().toString(); |
|||
out.tx_output_n = txOutput.getIndex(); |
|||
out.value = txOutput.getValue(); |
|||
out.script = Utils.bytesToHex(txOutput.getScriptBytes()); |
|||
|
|||
try { |
|||
out.addr = txOutput.getScript().getToAddresses()[0].toString(); |
|||
} catch(Exception e) { |
|||
//ignore
|
|||
} |
|||
|
|||
Transaction transaction = (Transaction)txOutput.getParent(); |
|||
out.tx_version = (int)transaction.getVersion(); |
|||
out.tx_locktime = transaction.getLocktime(); |
|||
if(AppServices.getCurrentBlockHeight() != null) { |
|||
out.confirmations = blockTransaction.getConfirmations(AppServices.getCurrentBlockHeight()); |
|||
} |
|||
|
|||
if(wallet.getKeystores().size() != 1) { |
|||
throw new IllegalStateException("Cannot mix outputs from a wallet with multiple keystores"); |
|||
} |
|||
|
|||
UnspentOutput.Xpub xpub = new UnspentOutput.Xpub(); |
|||
List<ExtendedKey.Header> headers = ExtendedKey.Header.getHeaders(Network.get()); |
|||
ExtendedKey.Header header = headers.stream().filter(head -> head.getDefaultScriptType().equals(wallet.getScriptType()) && !head.isPrivateKey()).findFirst().orElse(ExtendedKey.Header.xpub); |
|||
xpub.m = wallet.getKeystores().get(0).getExtendedPublicKey().toString(header); |
|||
xpub.path = node.getDerivationPath().toUpperCase(); |
|||
|
|||
out.xpub = xpub; |
|||
|
|||
return out; |
|||
} |
|||
|
|||
public String getScode() { |
|||
return config.getScode(); |
|||
} |
|||
|
|||
public void setScode(String scode) { |
|||
config.setScode(scode); |
|||
} |
|||
|
|||
@Subscribe |
|||
public void onMixFail(MixFailEvent e) { |
|||
log.info("Mix failed for utxo " + e.getWhirlpoolUtxo().getUtxo().tx_hash + ":" + e.getWhirlpoolUtxo().getUtxo().tx_output_n); |
|||
} |
|||
|
|||
@Subscribe |
|||
public void onMixSuccess(MixSuccessEvent e) { |
|||
log.info("Mix success, new utxo " + e.getMixSuccess().getReceiveUtxo().getHash() + ":" + e.getMixSuccess().getReceiveUtxo().getIndex()); |
|||
} |
|||
|
|||
@Subscribe |
|||
public void onWalletStart(WalletStartEvent e) { |
|||
log.info("Wallet started"); |
|||
} |
|||
|
|||
@Subscribe |
|||
public void onWalletStop(WalletStopEvent e) { |
|||
log.info("Wallet stopped"); |
|||
} |
|||
|
|||
public static class PoolsService extends Service<Collection<Pool>> { |
|||
private final Whirlpool whirlpool; |
|||
|
|||
public PoolsService(Whirlpool whirlpool) { |
|||
this.whirlpool = whirlpool; |
|||
} |
|||
|
|||
@Override |
|||
protected Task<Collection<Pool>> createTask() { |
|||
return new Task<>() { |
|||
protected Collection<Pool> call() throws Exception { |
|||
return whirlpool.getPools(); |
|||
} |
|||
}; |
|||
} |
|||
} |
|||
|
|||
public static class Tx0PreviewService extends Service<Tx0Preview> { |
|||
private final Whirlpool whirlpool; |
|||
private final Wallet wallet; |
|||
private final Pool pool; |
|||
private final List<UtxoEntry> utxoEntries; |
|||
|
|||
public Tx0PreviewService(Whirlpool whirlpool, Wallet wallet, Pool pool, List<UtxoEntry> utxoEntries) { |
|||
this.whirlpool = whirlpool; |
|||
this.wallet = wallet; |
|||
this.pool = pool; |
|||
this.utxoEntries = utxoEntries; |
|||
} |
|||
|
|||
@Override |
|||
protected Task<Tx0Preview> createTask() { |
|||
return new Task<>() { |
|||
protected Tx0Preview call() throws Exception { |
|||
updateProgress(-1, 1); |
|||
updateMessage("Fetching premix transaction..."); |
|||
|
|||
Collection<UnspentOutput> utxos = utxoEntries.stream().map(utxoEntry -> Whirlpool.getUnspentOutput(wallet, utxoEntry.getNode(), utxoEntry.getBlockTransaction(), (int)utxoEntry.getHashIndex().getIndex())).collect(Collectors.toList()); |
|||
return whirlpool.getTx0Preview(pool, utxos); |
|||
} |
|||
}; |
|||
} |
|||
} |
|||
|
|||
public static class Tx0BroadcastService extends Service<Sha256Hash> { |
|||
private final Whirlpool whirlpool; |
|||
private final Pool pool; |
|||
private final Collection<BlockTransactionHashIndex> utxos; |
|||
|
|||
public Tx0BroadcastService(Whirlpool whirlpool, Pool pool, Collection<BlockTransactionHashIndex> utxos) { |
|||
this.whirlpool = whirlpool; |
|||
this.pool = pool; |
|||
this.utxos = utxos; |
|||
} |
|||
|
|||
@Override |
|||
protected Task<Sha256Hash> createTask() { |
|||
return new Task<>() { |
|||
protected Sha256Hash call() throws Exception { |
|||
updateProgress(-1, 1); |
|||
updateMessage("Broadcasting premix transaction..."); |
|||
|
|||
Tx0 tx0 = whirlpool.broadcastTx0(pool, utxos); |
|||
return Sha256Hash.wrap(tx0.getTxid()); |
|||
} |
|||
}; |
|||
} |
|||
} |
|||
|
|||
public static class StartupService extends Service<WhirlpoolWallet> { |
|||
private final Whirlpool whirlpool; |
|||
|
|||
public StartupService(Whirlpool whirlpool) { |
|||
this.whirlpool = whirlpool; |
|||
} |
|||
|
|||
@Override |
|||
protected Task<WhirlpoolWallet> createTask() { |
|||
return new Task<>() { |
|||
protected WhirlpoolWallet call() throws Exception { |
|||
updateProgress(-1, 1); |
|||
updateMessage("Starting Whirlpool..."); |
|||
|
|||
WhirlpoolWallet whirlpoolWallet = whirlpool.getWhirlpoolWallet(); |
|||
if(AppServices.onlineProperty().get()) { |
|||
whirlpoolWallet.start(); |
|||
} |
|||
|
|||
return whirlpoolWallet; |
|||
} |
|||
}; |
|||
} |
|||
} |
|||
|
|||
public static class ShutdownService extends Service<Boolean> { |
|||
private final Whirlpool whirlpool; |
|||
|
|||
public ShutdownService(Whirlpool whirlpool) { |
|||
this.whirlpool = whirlpool; |
|||
} |
|||
|
|||
@Override |
|||
protected Task<Boolean> createTask() { |
|||
return new Task<>() { |
|||
protected Boolean call() throws Exception { |
|||
updateProgress(-1, 1); |
|||
updateMessage("Disconnecting from Whirlpool..."); |
|||
|
|||
whirlpool.shutdown(); |
|||
return true; |
|||
} |
|||
}; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,253 @@ |
|||
package com.sparrowwallet.sparrow.whirlpool; |
|||
|
|||
import com.samourai.whirlpool.client.tx0.Tx0Preview; |
|||
import com.samourai.whirlpool.client.whirlpool.beans.Pool; |
|||
import com.sparrowwallet.drongo.BitcoinUnit; |
|||
import com.sparrowwallet.drongo.protocol.Transaction; |
|||
import com.sparrowwallet.drongo.wallet.Wallet; |
|||
import com.sparrowwallet.sparrow.AppServices; |
|||
import com.sparrowwallet.sparrow.control.CoinLabel; |
|||
import com.sparrowwallet.sparrow.io.Config; |
|||
import com.sparrowwallet.sparrow.wallet.Entry; |
|||
import com.sparrowwallet.sparrow.wallet.UtxoEntry; |
|||
import javafx.beans.property.ObjectProperty; |
|||
import javafx.beans.property.SimpleObjectProperty; |
|||
import javafx.collections.FXCollections; |
|||
import javafx.fxml.FXML; |
|||
import javafx.scene.control.*; |
|||
import javafx.scene.layout.HBox; |
|||
import javafx.scene.layout.VBox; |
|||
import javafx.util.StringConverter; |
|||
|
|||
import java.util.*; |
|||
|
|||
public class WhirlpoolController { |
|||
@FXML |
|||
private VBox whirlpoolBox; |
|||
|
|||
@FXML |
|||
private VBox step1; |
|||
|
|||
@FXML |
|||
private VBox step2; |
|||
|
|||
@FXML |
|||
private VBox step3; |
|||
|
|||
@FXML |
|||
private VBox step4; |
|||
|
|||
@FXML |
|||
private TextField scode; |
|||
|
|||
@FXML |
|||
private ComboBox<Pool> pool; |
|||
|
|||
@FXML |
|||
private VBox selectedPool; |
|||
|
|||
@FXML |
|||
private CoinLabel poolFee; |
|||
|
|||
@FXML |
|||
private Label poolInsufficient; |
|||
|
|||
@FXML |
|||
private Label poolAnonset; |
|||
|
|||
@FXML |
|||
private HBox discountFeeBox; |
|||
|
|||
@FXML |
|||
private HBox nbOutputsBox; |
|||
|
|||
@FXML |
|||
private Label nbOutputsLoading; |
|||
|
|||
@FXML |
|||
private Label nbOutputs; |
|||
|
|||
@FXML |
|||
private CoinLabel discountFee; |
|||
|
|||
private String walletId; |
|||
private Wallet wallet; |
|||
private List<UtxoEntry> utxoEntries; |
|||
private final ObjectProperty<Tx0Preview> tx0PreviewProperty = new SimpleObjectProperty<>(null); |
|||
|
|||
public void initializeView(String walletId, Wallet wallet, List<UtxoEntry> utxoEntries) { |
|||
this.walletId = walletId; |
|||
this.wallet = wallet; |
|||
this.utxoEntries = utxoEntries; |
|||
|
|||
step1.managedProperty().bind(step1.visibleProperty()); |
|||
step2.managedProperty().bind(step2.visibleProperty()); |
|||
step3.managedProperty().bind(step3.visibleProperty()); |
|||
step4.managedProperty().bind(step4.visibleProperty()); |
|||
|
|||
step2.setVisible(false); |
|||
step3.setVisible(false); |
|||
step4.setVisible(false); |
|||
|
|||
scode.setText(Config.get().getScode() == null ? "" : Config.get().getScode()); |
|||
scode.textProperty().addListener((observable, oldValue, newValue) -> { |
|||
Config.get().setScode(newValue); |
|||
}); |
|||
|
|||
pool.setConverter(new StringConverter<Pool>() { |
|||
@Override |
|||
public String toString(Pool pool) { |
|||
return pool == null ? "Fetching pools..." : pool.getPoolId().replace("btc", " BTC"); |
|||
} |
|||
|
|||
@Override |
|||
public Pool fromString(String string) { |
|||
return null; |
|||
} |
|||
}); |
|||
|
|||
pool.valueProperty().addListener((observable, oldValue, newValue) -> { |
|||
if(newValue == null) { |
|||
selectedPool.setVisible(false); |
|||
} else { |
|||
poolFee.setValue(newValue.getFeeValue()); |
|||
poolAnonset.setText(newValue.getMixAnonymitySet() + " UTXOs"); |
|||
selectedPool.setVisible(true); |
|||
fetchTx0Preview(newValue); |
|||
} |
|||
}); |
|||
|
|||
step4.visibleProperty().addListener((observable, oldValue, newValue) -> { |
|||
if(newValue && pool.getItems().isEmpty()) { |
|||
fetchPools(); |
|||
} |
|||
}); |
|||
|
|||
selectedPool.managedProperty().bind(selectedPool.visibleProperty()); |
|||
selectedPool.setVisible(false); |
|||
pool.managedProperty().bind(pool.visibleProperty()); |
|||
poolInsufficient.managedProperty().bind(poolInsufficient.visibleProperty()); |
|||
poolInsufficient.visibleProperty().bind(pool.visibleProperty().not()); |
|||
discountFeeBox.managedProperty().bind(discountFeeBox.visibleProperty()); |
|||
discountFeeBox.setVisible(false); |
|||
nbOutputsBox.managedProperty().bind(nbOutputsBox.visibleProperty()); |
|||
nbOutputsBox.setVisible(false); |
|||
nbOutputsLoading.managedProperty().bind(nbOutputsLoading.visibleProperty()); |
|||
nbOutputs.managedProperty().bind(nbOutputs.visibleProperty()); |
|||
nbOutputsLoading.visibleProperty().bind(nbOutputs.visibleProperty().not()); |
|||
nbOutputs.setVisible(false); |
|||
} |
|||
|
|||
public boolean next() { |
|||
if(step1.isVisible()) { |
|||
step1.setVisible(false); |
|||
step2.setVisible(true); |
|||
return true; |
|||
} |
|||
|
|||
if(step2.isVisible()) { |
|||
step2.setVisible(false); |
|||
step3.setVisible(true); |
|||
return true; |
|||
} |
|||
|
|||
if(step3.isVisible()) { |
|||
step3.setVisible(false); |
|||
step4.setVisible(true); |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
public boolean back() { |
|||
if(step2.isVisible()) { |
|||
step2.setVisible(false); |
|||
step1.setVisible(true); |
|||
return false; |
|||
} |
|||
|
|||
if(step3.isVisible()) { |
|||
step3.setVisible(false); |
|||
step2.setVisible(true); |
|||
return true; |
|||
} |
|||
|
|||
if(step4.isVisible()) { |
|||
step4.setVisible(false); |
|||
step3.setVisible(true); |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
private void fetchPools() { |
|||
long totalUtxoValue = utxoEntries.stream().mapToLong(Entry::getValue).sum(); |
|||
Whirlpool.PoolsService poolsService = new Whirlpool.PoolsService(AppServices.get().getWhirlpool(walletId)); |
|||
poolsService.setOnSucceeded(workerStateEvent -> { |
|||
List<Pool> availablePools = poolsService.getValue().stream().filter(pool1 -> totalUtxoValue >= (pool1.getPremixValueMin() + pool1.getFeeValue())).toList(); |
|||
if(availablePools.isEmpty()) { |
|||
pool.setVisible(false); |
|||
OptionalLong optMinValue = poolsService.getValue().stream().mapToLong(Pool::getMustMixBalanceMin).min(); |
|||
if(optMinValue.isPresent()) { |
|||
String satsValue = String.format(Locale.ENGLISH, "%,d", optMinValue.getAsLong()) + " sats"; |
|||
String btcValue = CoinLabel.BTC_FORMAT.format((double)optMinValue.getAsLong() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC"; |
|||
poolInsufficient.setText("No available pools. Select a value over " + (Config.get().getBitcoinUnit() == BitcoinUnit.BTC ? btcValue : satsValue) + "."); |
|||
} |
|||
} else { |
|||
pool.setDisable(false); |
|||
pool.setItems(FXCollections.observableList(availablePools)); |
|||
pool.getSelectionModel().select(0); |
|||
} |
|||
}); |
|||
poolsService.setOnFailed(workerStateEvent -> { |
|||
Throwable exception = workerStateEvent.getSource().getException(); |
|||
while(exception.getCause() != null) { |
|||
exception = exception.getCause(); |
|||
} |
|||
|
|||
Optional<ButtonType> optButton = AppServices.showErrorDialog("Error fetching pools", exception.getMessage(), ButtonType.CANCEL, new ButtonType("Retry", ButtonBar.ButtonData.APPLY)); |
|||
if(optButton.isPresent()) { |
|||
if(optButton.get().getButtonData().equals(ButtonBar.ButtonData.APPLY)) { |
|||
fetchPools(); |
|||
} else { |
|||
pool.setDisable(true); |
|||
} |
|||
} |
|||
}); |
|||
poolsService.start(); |
|||
} |
|||
|
|||
private void fetchTx0Preview(Pool pool) { |
|||
Whirlpool whirlpool = AppServices.get().getWhirlpool(walletId); |
|||
whirlpool.setScode(Config.get().getScode()); |
|||
|
|||
Whirlpool.Tx0PreviewService tx0PreviewService = new Whirlpool.Tx0PreviewService(whirlpool, wallet, pool, utxoEntries); |
|||
tx0PreviewService.setOnRunning(workerStateEvent -> { |
|||
nbOutputsBox.setVisible(true); |
|||
nbOutputsLoading.setText("Calculating..."); |
|||
}); |
|||
tx0PreviewService.setOnSucceeded(workerStateEvent -> { |
|||
Tx0Preview tx0Preview = tx0PreviewService.getValue(); |
|||
discountFeeBox.setVisible(tx0Preview.getPool().getFeeValue() != tx0Preview.getTx0Data().getFeeValue()); |
|||
discountFee.setValue(tx0Preview.getTx0Data().getFeeValue()); |
|||
nbOutputsBox.setVisible(true); |
|||
nbOutputs.setText(tx0Preview.getNbPremix() + " UTXOs"); |
|||
nbOutputs.setVisible(true); |
|||
tx0PreviewProperty.set(tx0Preview); |
|||
}); |
|||
tx0PreviewService.setOnFailed(workerStateEvent -> { |
|||
Throwable exception = workerStateEvent.getSource().getException(); |
|||
while(exception.getCause() != null) { |
|||
exception = exception.getCause(); |
|||
} |
|||
|
|||
nbOutputsLoading.setText("Error fetching fee: " + exception.getMessage()); |
|||
}); |
|||
tx0PreviewService.start(); |
|||
} |
|||
|
|||
public ObjectProperty<Tx0Preview> getTx0PreviewProperty() { |
|||
return tx0PreviewProperty; |
|||
} |
|||
} |
@ -0,0 +1,81 @@ |
|||
package com.sparrowwallet.sparrow.whirlpool; |
|||
|
|||
import com.samourai.whirlpool.client.tx0.Tx0Preview; |
|||
import com.samourai.whirlpool.client.whirlpool.beans.Pool; |
|||
import com.sparrowwallet.drongo.wallet.Wallet; |
|||
import com.sparrowwallet.sparrow.AppServices; |
|||
import com.sparrowwallet.sparrow.wallet.UtxoEntry; |
|||
import javafx.beans.value.ChangeListener; |
|||
import javafx.beans.value.ObservableValue; |
|||
import javafx.event.ActionEvent; |
|||
import javafx.fxml.FXMLLoader; |
|||
import javafx.scene.control.*; |
|||
|
|||
import java.io.IOException; |
|||
import java.util.List; |
|||
|
|||
public class WhirlpoolDialog extends Dialog<Tx0Preview> { |
|||
public WhirlpoolDialog(String walletId, Wallet wallet, List<UtxoEntry> utxoEntries) { |
|||
final DialogPane dialogPane = getDialogPane(); |
|||
AppServices.setStageIcon(dialogPane.getScene().getWindow()); |
|||
AppServices.onEscapePressed(dialogPane.getScene(), this::close); |
|||
|
|||
try { |
|||
FXMLLoader whirlpoolLoader = new FXMLLoader(AppServices.class.getResource("whirlpool/whirlpool.fxml")); |
|||
dialogPane.setContent(whirlpoolLoader.load()); |
|||
WhirlpoolController whirlpoolController = whirlpoolLoader.getController(); |
|||
whirlpoolController.initializeView(walletId, wallet, utxoEntries); |
|||
|
|||
dialogPane.setPrefWidth(600); |
|||
dialogPane.setPrefHeight(520); |
|||
AppServices.moveToActiveWindowScreen(this); |
|||
|
|||
dialogPane.getStylesheets().add(AppServices.class.getResource("whirlpool/whirlpool.css").toExternalForm()); |
|||
|
|||
final ButtonType nextButtonType = new javafx.scene.control.ButtonType("Next", ButtonBar.ButtonData.OK_DONE); |
|||
final ButtonType backButtonType = new javafx.scene.control.ButtonType("Back", ButtonBar.ButtonData.LEFT); |
|||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE); |
|||
final ButtonType previewButtonType = new javafx.scene.control.ButtonType("Preview Premix", ButtonBar.ButtonData.APPLY); |
|||
dialogPane.getButtonTypes().addAll(nextButtonType, backButtonType, cancelButtonType, previewButtonType); |
|||
|
|||
Button nextButton = (Button)dialogPane.lookupButton(nextButtonType); |
|||
Button backButton = (Button)dialogPane.lookupButton(backButtonType); |
|||
Button previewButton = (Button)dialogPane.lookupButton(previewButtonType); |
|||
previewButton.setDisable(true); |
|||
whirlpoolController.getTx0PreviewProperty().addListener(new ChangeListener<Tx0Preview>() { |
|||
@Override |
|||
public void changed(ObservableValue<? extends Tx0Preview> observable, Tx0Preview oldValue, Tx0Preview newValue) { |
|||
previewButton.setDisable(newValue == null); |
|||
} |
|||
}); |
|||
|
|||
nextButton.managedProperty().bind(nextButton.visibleProperty()); |
|||
backButton.managedProperty().bind(backButton.visibleProperty()); |
|||
previewButton.managedProperty().bind(previewButton.visibleProperty()); |
|||
|
|||
backButton.setDisable(true); |
|||
previewButton.visibleProperty().bind(nextButton.visibleProperty().not()); |
|||
|
|||
nextButton.addEventFilter(ActionEvent.ACTION, event -> { |
|||
if(!whirlpoolController.next()) { |
|||
nextButton.setVisible(false); |
|||
previewButton.setDefaultButton(true); |
|||
} |
|||
backButton.setDisable(false); |
|||
event.consume(); |
|||
}); |
|||
|
|||
backButton.addEventFilter(ActionEvent.ACTION, event -> { |
|||
nextButton.setVisible(true); |
|||
if(!whirlpoolController.back()) { |
|||
backButton.setDisable(true); |
|||
} |
|||
event.consume(); |
|||
}); |
|||
|
|||
setResultConverter(dialogButton -> dialogButton.getButtonData().equals(ButtonBar.ButtonData.APPLY) ? whirlpoolController.getTx0PreviewProperty().get() : null); |
|||
} catch(IOException e) { |
|||
throw new RuntimeException(e); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,11 @@ |
|||
package com.sparrowwallet.sparrow.whirlpool; |
|||
|
|||
public class WhirlpoolException extends Exception { |
|||
public WhirlpoolException(String message) { |
|||
super(message); |
|||
} |
|||
|
|||
public WhirlpoolException(String message, Throwable cause) { |
|||
super(message, cause); |
|||
} |
|||
} |
@ -0,0 +1,42 @@ |
|||
.whirlpool-pane { |
|||
-fx-padding: 0; |
|||
} |
|||
|
|||
.title-area { |
|||
-fx-background-color: -fx-control-inner-background; |
|||
-fx-padding: 10 25 10 25; |
|||
-fx-border-width: 0px 0px 1px 0px; |
|||
-fx-border-color: #e5e5e6; |
|||
} |
|||
|
|||
#whirlpoolBox, .button-bar { |
|||
-fx-padding: 10 25 25 25; |
|||
} |
|||
|
|||
.button-bar .container { |
|||
-fx-padding: 0 0 15px 0; |
|||
} |
|||
|
|||
.title-label { |
|||
-fx-font-size: 24px; |
|||
} |
|||
|
|||
.title-text { |
|||
-fx-font-size: 20px; |
|||
-fx-padding: 0 0 15px 0; |
|||
-fx-graphic-text-gap: 10px; |
|||
} |
|||
|
|||
.content-text { |
|||
-fx-font-size: 16px; |
|||
-fx-text-fill: derive(-fx-text-base-color, 15%); |
|||
} |
|||
|
|||
.field-box { |
|||
-fx-pref-height: 30px; |
|||
-fx-alignment: CENTER_LEFT; |
|||
} |
|||
|
|||
.field-label { |
|||
-fx-pref-width: 120px; |
|||
} |
@ -0,0 +1,99 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
|
|||
<?import java.lang.*?> |
|||
<?import java.util.*?> |
|||
<?import javafx.scene.*?> |
|||
<?import javafx.scene.control.*?> |
|||
<?import javafx.scene.layout.*?> |
|||
|
|||
<?import org.controlsfx.glyphfont.Glyph?> |
|||
<?import javafx.scene.image.ImageView?> |
|||
<?import javafx.scene.image.Image?> |
|||
<?import javafx.geometry.Insets?> |
|||
<?import com.sparrowwallet.sparrow.control.CoinLabel?> |
|||
<StackPane prefHeight="460.0" prefWidth="600.0" stylesheets="@whirlpool.css, @../general.css" styleClass="whirlpool-pane" fx:controller="com.sparrowwallet.sparrow.whirlpool.WhirlpoolController" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml"> |
|||
<VBox spacing="20"> |
|||
<HBox styleClass="title-area"> |
|||
<HBox alignment="CENTER_LEFT"> |
|||
<Label fx:id="title" text="Whirlpool Configuration" styleClass="title-label" /> |
|||
</HBox> |
|||
<Region HBox.hgrow="ALWAYS"/> |
|||
<ImageView AnchorPane.rightAnchor="0"> |
|||
<Image url="/image/whirlpool.png" requestedWidth="50" requestedHeight="50" smooth="false" /> |
|||
</ImageView> |
|||
</HBox> |
|||
<VBox fx:id="whirlpoolBox" styleClass="content-area" spacing="20" prefHeight="370"> |
|||
<VBox fx:id="step1" spacing="15"> |
|||
<Label text="Introduction" styleClass="title-text"> |
|||
<graphic> |
|||
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="RANDOM" styleClass="title-icon" /> |
|||
</graphic> |
|||
</Label> |
|||
<Label text="Mixing (CoinJoin) is provided in Sparrow through the Samourai Whirlpool coordinator. " wrapText="true" styleClass="content-text" /> |
|||
<Label text="Sparrow contains a Whirlpool client, which communicates with the coordinator using blinded inputs. As such, the privacy of your UTXOs is unchanged when using this service. If you are using Tor to connect to your server, or have configured a Tor proxy, communication with the coordinator will be over Tor." wrapText="true" styleClass="content-text" /> |
|||
<Label text="The fees for using the Whirlpool service are deducted from the UTXOs that you mix. These fees include the Whirlpool fee, and the miner fees required for the transactions. All fees are displayed for review before mixing begins." wrapText="true" styleClass="content-text" /> |
|||
</VBox> |
|||
<VBox fx:id="step2" spacing="15"> |
|||
<Label text="Premix, Postmix and Badbank" styleClass="title-text"> |
|||
<graphic> |
|||
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="RANDOM" styleClass="title-icon" /> |
|||
</graphic> |
|||
</Label> |
|||
<Label text="Initiating your first CoinJoin in Sparrow will add three new wallets to your existing wallet: Premix, Postmix and Badbank." wrapText="true" styleClass="content-text" /> |
|||
<Label text="Premix contains UTXOs that have been split from your deposit UTXOs into equal amounts, waiting for their first mixing round. Postmix contains UTXOs that have been through at least one mixing round. Badbank contains any change from your premix transaction." wrapText="true" styleClass="content-text" /> |
|||
<Label text="Click on the tabs at the right of the wallet to use these wallets. Note that they will have reduced functionality (for example they will not display receiving addresses)." wrapText="true" styleClass="content-text" /> |
|||
</VBox> |
|||
<VBox fx:id="step3" spacing="15"> |
|||
<Label text="Configure Whirlpool" styleClass="title-text"> |
|||
<graphic> |
|||
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="RANDOM" styleClass="title-icon" /> |
|||
</graphic> |
|||
</Label> |
|||
<Label text="Configure Whirlpool using the fields below. You can enter a Samourai SCODE for reduced cost mixing." wrapText="true" styleClass="content-text" /> |
|||
<HBox styleClass="field-box"> |
|||
<padding> |
|||
<Insets top="20" /> |
|||
</padding> |
|||
<Label text="SCODE:" styleClass="field-label" /> |
|||
<TextField fx:id="scode" /> |
|||
</HBox> |
|||
</VBox> |
|||
<VBox fx:id="step4" spacing="15"> |
|||
<Label text="Select Pool" styleClass="title-text"> |
|||
<graphic> |
|||
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="RANDOM" styleClass="title-icon" /> |
|||
</graphic> |
|||
</Label> |
|||
<Label text="Choose which pool to use below. You will then be able to preview your premix transaction." wrapText="true" styleClass="content-text" /> |
|||
<HBox spacing="20" alignment="CENTER_LEFT"> |
|||
<padding> |
|||
<Insets top="20" bottom="5" /> |
|||
</padding> |
|||
<Label text="Pool:" prefWidth="100"/> |
|||
<ComboBox fx:id="pool" /> |
|||
<Label fx:id="poolInsufficient" text="No available pools." styleClass="failure"/> |
|||
</HBox> |
|||
<VBox fx:id="selectedPool" spacing="15"> |
|||
<HBox styleClass="field-box"> |
|||
<Label text="Anonset:" styleClass="field-label" /> |
|||
<Label fx:id="poolAnonset" /> |
|||
</HBox> |
|||
<HBox styleClass="field-box"> |
|||
<Label text="Pool Fee:" styleClass="field-label" /> |
|||
<CoinLabel fx:id="poolFee" /> |
|||
<HBox fx:id="discountFeeBox" alignment="CENTER_LEFT"> |
|||
<Label text=" (discounted to " /> |
|||
<CoinLabel fx:id="discountFee" /> |
|||
<Label text=")" /> |
|||
</HBox> |
|||
</HBox> |
|||
<HBox fx:id="nbOutputsBox" styleClass="field-box"> |
|||
<Label text="Premix Outputs:" styleClass="field-label" /> |
|||
<Label fx:id="nbOutputsLoading" text="Calculating..." /> |
|||
<Label fx:id="nbOutputs" /> |
|||
</HBox> |
|||
</VBox> |
|||
</VBox> |
|||
</VBox> |
|||
</VBox> |
|||
</StackPane> |
After Width: | Height: | Size: 1008 B |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 3.7 KiB |
Loading…
Reference in new issue