diff --git a/build.gradle b/build.gradle index 0031a39f..9ea9c7b5 100644 --- a/build.gradle +++ b/build.gradle @@ -91,7 +91,7 @@ dependencies { implementation('org.slf4j:jul-to-slf4j:1.7.30') { exclude group: 'org.slf4j' } - implementation('com.sparrowwallet.nightjar:nightjar:0.2.12-SNAPSHOT') + implementation('com.sparrowwallet.nightjar:nightjar:0.2.13-SNAPSHOT') testImplementation('junit:junit:4.12') } @@ -387,7 +387,7 @@ extraJavaModuleInfo { module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') { exports('co.nstant.in.cbor') } - module('nightjar-0.2.12-SNAPSHOT.jar', 'com.sparrowwallet.nightjar', '0.2.12-SNAPSHOT') { + module('nightjar-0.2.13-SNAPSHOT.jar', 'com.sparrowwallet.nightjar', '0.2.13-SNAPSHOT') { requires('com.google.common') requires('net.sourceforge.streamsupport') requires('org.slf4j') @@ -401,6 +401,7 @@ extraJavaModuleInfo { exports('com.samourai.wallet.api.backend.beans') exports('com.samourai.wallet.client.indexHandler') exports('com.samourai.wallet.hd') + exports('com.samourai.wallet.util') exports('com.samourai.whirlpool.client.event') exports('com.samourai.whirlpool.client.wallet') exports('com.samourai.whirlpool.client.wallet.beans') diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/MixToController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/MixToController.java index 6811af68..3070cc9b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/MixToController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/MixToController.java @@ -44,7 +44,6 @@ public class MixToController implements Initializable { List destinationWallets = AppServices.get().getOpenWallets().keySet().stream().filter(openWallet -> openWallet.isValid() && openWallet != wallet && openWallet != wallet.getMasterWallet() - && openWallet.getPolicyType().equals(PolicyType.SINGLE) && !StandardAccount.WHIRLPOOL_ACCOUNTS.contains(openWallet.getStandardAccountType())).collect(Collectors.toList()); allWallets.addAll(destinationWallets); diff --git a/src/main/java/com/sparrowwallet/sparrow/whirlpool/Whirlpool.java b/src/main/java/com/sparrowwallet/sparrow/whirlpool/Whirlpool.java index 3923c542..e1e9deb7 100644 --- a/src/main/java/com/sparrowwallet/sparrow/whirlpool/Whirlpool.java +++ b/src/main/java/com/sparrowwallet/sparrow/whirlpool/Whirlpool.java @@ -7,6 +7,7 @@ import com.samourai.wallet.api.backend.beans.UnspentOutput; import com.samourai.wallet.hd.HD_Wallet; import com.samourai.wallet.hd.HD_WalletFactoryGeneric; import com.samourai.whirlpool.client.event.*; +import com.samourai.whirlpool.client.mix.handler.IPostmixHandler; import com.samourai.whirlpool.client.tx0.*; import com.samourai.whirlpool.client.wallet.WhirlpoolEventService; import com.samourai.whirlpool.client.wallet.WhirlpoolWallet; @@ -41,6 +42,7 @@ import com.sparrowwallet.sparrow.wallet.UtxoEntry; import com.sparrowwallet.sparrow.whirlpool.dataPersister.SparrowDataPersister; import com.sparrowwallet.sparrow.whirlpool.dataSource.SparrowDataSource; import com.sparrowwallet.sparrow.whirlpool.dataSource.SparrowMinerFeeSupplier; +import com.sparrowwallet.sparrow.whirlpool.dataSource.SparrowPostmixHandler; import javafx.application.Platform; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; @@ -365,18 +367,12 @@ public class Whirlpool { throw new IllegalStateException("Cannot find mix to wallet with id " + mixToWalletId); } - if(mixToWallet.getPolicyType() != PolicyType.SINGLE) { - throw new IllegalStateException("Only single signature mix to wallets are currently supported"); - } - - List headers = ExtendedKey.Header.getHeaders(Network.get()); - ExtendedKey.Header header = headers.stream().filter(head -> head.getDefaultScriptType().equals(mixToWallet.getScriptType()) && !head.isPrivateKey()).findFirst().orElse(ExtendedKey.Header.xpub); - ExtendedKey extPubKey = mixToWallet.getKeystores().get(0).getExtendedPublicKey(); - String xpub = extPubKey.toString(header); Integer highestUsedIndex = mixToWallet.getNode(KeyPurpose.RECEIVE).getHighestUsedIndex(); + int startIndex = highestUsedIndex == null ? 0 : highestUsedIndex + 1; int mixes = minMixes == null ? DEFAULT_MIXTO_MIN_MIXES : minMixes; - ExternalDestination externalDestination = new ExternalDestination(xpub, 0, highestUsedIndex == null ? 0 : highestUsedIndex + 1, mixes, DEFAULT_MIXTO_RANDOM_FACTOR); + IPostmixHandler postmixHandler = new SparrowPostmixHandler(whirlpoolWalletService, mixToWallet, KeyPurpose.RECEIVE, startIndex); + ExternalDestination externalDestination = new ExternalDestination(postmixHandler, 0, startIndex, mixes, DEFAULT_MIXTO_RANDOM_FACTOR); config.setExternalDestination(externalDestination); } diff --git a/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowPostmixHandler.java b/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowPostmixHandler.java new file mode 100644 index 00000000..0e1c0474 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowPostmixHandler.java @@ -0,0 +1,75 @@ +package com.sparrowwallet.sparrow.whirlpool.dataSource; + +import com.samourai.wallet.client.indexHandler.IIndexHandler; +import com.samourai.wallet.util.XPubUtil; +import com.samourai.whirlpool.client.mix.handler.DestinationType; +import com.samourai.whirlpool.client.mix.handler.IPostmixHandler; +import com.samourai.whirlpool.client.mix.handler.MixDestination; +import com.samourai.whirlpool.client.wallet.WhirlpoolWalletService; +import com.sparrowwallet.drongo.KeyPurpose; +import com.sparrowwallet.drongo.address.Address; +import com.sparrowwallet.drongo.wallet.Wallet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SparrowPostmixHandler implements IPostmixHandler { + private static final Logger log = LoggerFactory.getLogger(SparrowPostmixHandler.class); + + private final WhirlpoolWalletService whirlpoolWalletService; + private final Wallet wallet; + private final KeyPurpose keyPurpose; + private final int startIndex; + + protected MixDestination destination; + + public SparrowPostmixHandler(WhirlpoolWalletService whirlpoolWalletService, Wallet wallet, KeyPurpose keyPurpose, int startIndex) { + this.whirlpoolWalletService = whirlpoolWalletService; + this.wallet = wallet; + this.keyPurpose = keyPurpose; + this.startIndex = startIndex; + } + + public Wallet getWallet() { + return wallet; + } + + protected MixDestination computeNextDestination() throws Exception { + // index + int index = Math.max(getIndexHandler().getAndIncrementUnconfirmed(), startIndex); + + // address + Address address = wallet.getAddress(keyPurpose, index); + String path = XPubUtil.getInstance().getPath(index, keyPurpose.getPathIndex().num()); + + log.info("Mixing to external xPub -> receiveAddress=" + address + ", path=" + path); + return new MixDestination(DestinationType.XPUB, index, address.toString(), path); + } + + @Override + public MixDestination getDestination() { + return destination; // may be NULL + } + + public final MixDestination computeDestination() throws Exception { + // use "unconfirmed" index to avoid huge index gaps on multiple mix failures + this.destination = computeNextDestination(); + return destination; + } + + @Override + public void onMixFail() { + if(destination != null) { + getIndexHandler().cancelUnconfirmed(destination.getIndex()); + } + } + + @Override + public void onRegisterOutput() { + // confirm receive address even when REGISTER_OUTPUT fails, to avoid 'ouput already registered' + getIndexHandler().confirmUnconfirmed(destination.getIndex()); + } + + private IIndexHandler getIndexHandler() { + return whirlpoolWalletService.whirlpoolWallet().getWalletStateSupplier().getIndexHandlerExternal(); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowWalletStateSupplier.java b/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowWalletStateSupplier.java index c7fe1740..dfab6617 100644 --- a/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowWalletStateSupplier.java +++ b/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowWalletStateSupplier.java @@ -47,7 +47,7 @@ public class SparrowWalletStateSupplier implements WalletStateSupplier { indexHandler = new SparrowIndexHandler(wallet, walletNode, 0); indexHandlerWallets.put(key, indexHandler); } - + return indexHandler; } @@ -58,9 +58,15 @@ public class SparrowWalletStateSupplier implements WalletStateSupplier { } if(externalIndexHandler == null) { - Wallet externalWallet = SparrowDataSource.getWallet(externalDestination.getXpub()); + Wallet externalWallet = null; + if(externalDestination.getPostmixHandler() instanceof SparrowPostmixHandler sparrowPostmixHandler) { + externalWallet = sparrowPostmixHandler.getWallet(); + } else if(externalDestination.getXpub() != null) { + externalWallet = SparrowDataSource.getWallet(externalDestination.getXpub()); + } + if(externalWallet == null) { - throw new IllegalStateException("Cannot find wallet for external destination xpub " + externalDestination.getXpub()); + throw new IllegalStateException("Cannot find wallet for external destination " + externalDestination); } KeyPurpose keyPurpose = KeyPurpose.fromChildNumber(new ChildNumber(externalDestination.getChain()));