support mixing to multisig wallets

This commit is contained in:
Craig Raw 2021-09-02 17:14:01 +02:00
parent b6f047d382
commit a42761981c
5 changed files with 92 additions and 15 deletions

View file

@ -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')

View file

@ -44,7 +44,6 @@ public class MixToController implements Initializable {
List<Wallet> 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);

View file

@ -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<ExtendedKey.Header> 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);
}

View file

@ -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();
}
}

View file

@ -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()));