support nested wallets

This commit is contained in:
Craig Raw 2022-03-02 13:34:10 +02:00
parent 956f59880e
commit 0734757a17
12 changed files with 352 additions and 107 deletions

View file

@ -361,6 +361,10 @@ public class PaymentCode {
return new PaymentCode(strPaymentCode, pubkey, chain); return new PaymentCode(strPaymentCode, pubkey, chain);
} }
public String toAbbreviatedString() {
return strPaymentCode.substring(0, 8) + "..." + strPaymentCode.substring(strPaymentCode.length() - 3);
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if(this == o) { if(this == o) {

View file

@ -18,6 +18,7 @@ import java.util.*;
import static com.sparrowwallet.drongo.psbt.PSBTEntry.*; import static com.sparrowwallet.drongo.psbt.PSBTEntry.*;
import static com.sparrowwallet.drongo.psbt.PSBTInput.*; import static com.sparrowwallet.drongo.psbt.PSBTInput.*;
import static com.sparrowwallet.drongo.psbt.PSBTOutput.*; import static com.sparrowwallet.drongo.psbt.PSBTOutput.*;
import static com.sparrowwallet.drongo.wallet.Wallet.addDummySpendingInput;
public class PSBT { public class PSBT {
public static final byte PSBT_GLOBAL_UNSIGNED_TX = 0x00; public static final byte PSBT_GLOBAL_UNSIGNED_TX = 0x00;
@ -89,12 +90,16 @@ public class PSBT {
this.version = version; this.version = version;
} }
boolean alwaysIncludeWitnessUtxo = wallet.getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().alwaysIncludeNonWitnessUtxo());
int inputIndex = 0; int inputIndex = 0;
for(Iterator<Map.Entry<BlockTransactionHashIndex, WalletNode>> iter = walletTransaction.getSelectedUtxos().entrySet().iterator(); iter.hasNext(); inputIndex++) { for(Iterator<Map.Entry<BlockTransactionHashIndex, WalletNode>> iter = walletTransaction.getSelectedUtxos().entrySet().iterator(); iter.hasNext(); inputIndex++) {
Map.Entry<BlockTransactionHashIndex, WalletNode> utxoEntry = iter.next(); Map.Entry<BlockTransactionHashIndex, WalletNode> utxoEntry = iter.next();
Transaction utxo = wallet.getTransactions().get(utxoEntry.getKey().getHash()).getTransaction();
WalletNode walletNode = utxoEntry.getValue();
Wallet signingWallet = walletNode.getWallet();
boolean alwaysIncludeWitnessUtxo = signingWallet.getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().alwaysIncludeNonWitnessUtxo());
Transaction utxo = signingWallet.getTransactions().get(utxoEntry.getKey().getHash()).getTransaction();
int utxoIndex = (int)utxoEntry.getKey().getIndex(); int utxoIndex = (int)utxoEntry.getKey().getIndex();
TransactionOutput utxoOutput = utxo.getOutputs().get(utxoIndex); TransactionOutput utxoOutput = utxo.getOutputs().get(utxoIndex);
@ -112,17 +117,16 @@ public class PSBT {
Map<ECKey, KeyDerivation> derivedPublicKeys = new LinkedHashMap<>(); Map<ECKey, KeyDerivation> derivedPublicKeys = new LinkedHashMap<>();
ECKey tapInternalKey = null; ECKey tapInternalKey = null;
for(Keystore keystore : wallet.getKeystores()) { for(Keystore keystore : signingWallet.getKeystores()) {
WalletNode walletNode = utxoEntry.getValue(); derivedPublicKeys.put(signingWallet.getScriptType().getOutputKey(keystore.getPubKey(walletNode)), keystore.getKeyDerivation().extend(walletNode.getDerivation()));
derivedPublicKeys.put(wallet.getScriptType().getOutputKey(keystore.getPubKey(walletNode)), keystore.getKeyDerivation().extend(walletNode.getDerivation()));
//TODO: Implement Musig for multisig wallets //TODO: Implement Musig for multisig wallets
if(wallet.getScriptType() == ScriptType.P2TR) { if(signingWallet.getScriptType() == ScriptType.P2TR) {
tapInternalKey = keystore.getPubKey(walletNode); tapInternalKey = keystore.getPubKey(walletNode);
} }
} }
PSBTInput psbtInput = new PSBTInput(this, wallet.getScriptType(), transaction, inputIndex, utxo, utxoIndex, redeemScript, witnessScript, derivedPublicKeys, Collections.emptyMap(), tapInternalKey, alwaysIncludeWitnessUtxo); PSBTInput psbtInput = new PSBTInput(this, signingWallet.getScriptType(), transaction, inputIndex, utxo, utxoIndex, redeemScript, witnessScript, derivedPublicKeys, Collections.emptyMap(), tapInternalKey, alwaysIncludeWitnessUtxo);
psbtInputs.add(psbtInput); psbtInputs.add(psbtInput);
} }
@ -132,7 +136,7 @@ public class PSBT {
Address address = txOutput.getScript().getToAddresses()[0]; Address address = txOutput.getScript().getToAddresses()[0];
if(walletTransaction.getPayments().stream().anyMatch(payment -> payment.getAddress().equals(address))) { if(walletTransaction.getPayments().stream().anyMatch(payment -> payment.getAddress().equals(address))) {
outputNodes.add(wallet.getWalletAddresses().getOrDefault(address, null)); outputNodes.add(wallet.getWalletAddresses().getOrDefault(address, null));
} else if(walletTransaction.getChangeMap().keySet().stream().anyMatch(changeNode -> wallet.getAddress(changeNode).equals(address))) { } else if(walletTransaction.getChangeMap().keySet().stream().anyMatch(changeNode -> changeNode.getAddress().equals(address))) {
outputNodes.add(wallet.getWalletAddresses().getOrDefault(address, null)); outputNodes.add(wallet.getWalletAddresses().getOrDefault(address, null));
} }
} catch(NonStandardScriptException e) { } catch(NonStandardScriptException e) {
@ -151,7 +155,7 @@ public class PSBT {
//Construct dummy transaction to spend the UTXO created by this wallet's txOutput //Construct dummy transaction to spend the UTXO created by this wallet's txOutput
Transaction transaction = new Transaction(); Transaction transaction = new Transaction();
TransactionInput spendingInput = wallet.addDummySpendingInput(transaction, outputNode, txOutput); TransactionInput spendingInput = addDummySpendingInput(transaction, outputNode, txOutput);
Script redeemScript = null; Script redeemScript = null;
if(ScriptType.P2SH.isScriptType(txOutput.getScript())) { if(ScriptType.P2SH.isScriptType(txOutput.getScript())) {
@ -164,7 +168,7 @@ public class PSBT {
} }
Map<ECKey, KeyDerivation> derivedPublicKeys = new LinkedHashMap<>(); Map<ECKey, KeyDerivation> derivedPublicKeys = new LinkedHashMap<>();
for(Keystore keystore : wallet.getKeystores()) { for(Keystore keystore : outputNode.getWallet().getKeystores()) {
derivedPublicKeys.put(keystore.getPubKey(outputNode), keystore.getKeyDerivation().extend(outputNode.getDerivation())); derivedPublicKeys.put(keystore.getPubKey(outputNode), keystore.getKeyDerivation().extend(outputNode.getDerivation()));
} }

View file

@ -102,11 +102,7 @@ public class PSBTInput {
log.debug(" Transaction input references txid: " + input.getOutpoint().getHash() + " vout " + input.getOutpoint().getIndex() + " with script " + input.getScriptSig()); log.debug(" Transaction input references txid: " + input.getOutpoint().getHash() + " vout " + input.getOutpoint().getIndex() + " with script " + input.getScriptSig());
} }
for(TransactionOutput output: nonWitnessTx.getOutputs()) { for(TransactionOutput output: nonWitnessTx.getOutputs()) {
try { log.debug(" Transaction output value: " + output.getValue() + (output.getScript().getToAddress() != null ? " to address " + output.getScript().getToAddress() : "") + " with script hex " + Utils.bytesToHex(output.getScript().getProgram()) + " to script " + output.getScript());
log.debug(" Transaction output value: " + output.getValue() + " to addresses " + Arrays.asList(output.getScript().getToAddresses()) + " with script hex " + Utils.bytesToHex(output.getScript().getProgram()) + " to script " + output.getScript());
} catch(NonStandardScriptException e) {
log.error("Unknown script type", e);
}
} }
break; break;
case PSBT_IN_WITNESS_UTXO: case PSBT_IN_WITNESS_UTXO:

View file

@ -12,7 +12,7 @@ public class CoinbaseUtxoFilter implements UtxoFilter {
@Override @Override
public boolean isEligible(BlockTransactionHashIndex candidate) { public boolean isEligible(BlockTransactionHashIndex candidate) {
//Disallow immature coinbase outputs //Disallow immature coinbase outputs
BlockTransaction blockTransaction = wallet.getTransactions().get(candidate.getHash()); BlockTransaction blockTransaction = wallet.getWalletTransaction(candidate.getHash());
if(blockTransaction != null && blockTransaction.getTransaction() != null && blockTransaction.getTransaction().isCoinBase() if(blockTransaction != null && blockTransaction.getTransaction() != null && blockTransaction.getTransaction().isCoinBase()
&& wallet.getStoredBlockHeight() != null && candidate.getConfirmations(wallet.getStoredBlockHeight()) < Transaction.COINBASE_MATURITY_THRESHOLD) { && wallet.getStoredBlockHeight() != null && candidate.getConfirmations(wallet.getStoredBlockHeight()) < Transaction.COINBASE_MATURITY_THRESHOLD) {
return false; return false;

View file

@ -1,5 +1,7 @@
package com.sparrowwallet.drongo.wallet; package com.sparrowwallet.drongo.wallet;
import com.sparrowwallet.drongo.protocol.ScriptType;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -7,6 +9,7 @@ import static com.sparrowwallet.drongo.protocol.Transaction.WITNESS_SCALE_FACTOR
public class OutputGroup { public class OutputGroup {
private final List<BlockTransactionHashIndex> utxos = new ArrayList<>(); private final List<BlockTransactionHashIndex> utxos = new ArrayList<>();
private final ScriptType scriptType;
private final int walletBlockHeight; private final int walletBlockHeight;
private final long inputWeightUnits; private final long inputWeightUnits;
private final double feeRate; private final double feeRate;
@ -18,7 +21,8 @@ public class OutputGroup {
private int depth = Integer.MAX_VALUE; private int depth = Integer.MAX_VALUE;
private boolean allInputsFromWallet = true; private boolean allInputsFromWallet = true;
public OutputGroup(int walletBlockHeight, long inputWeightUnits, double feeRate, double longTermFeeRate) { public OutputGroup(ScriptType scriptType, int walletBlockHeight, long inputWeightUnits, double feeRate, double longTermFeeRate) {
this.scriptType = scriptType;
this.walletBlockHeight = walletBlockHeight; this.walletBlockHeight = walletBlockHeight;
this.inputWeightUnits = inputWeightUnits; this.inputWeightUnits = inputWeightUnits;
this.feeRate = feeRate; this.feeRate = feeRate;
@ -48,6 +52,10 @@ public class OutputGroup {
return utxos; return utxos;
} }
public ScriptType getScriptType() {
return scriptType;
}
public long getValue() { public long getValue() {
return value; return value;
} }

View file

@ -1,15 +1,19 @@
package com.sparrowwallet.drongo.wallet; package com.sparrowwallet.drongo.wallet;
import com.sparrowwallet.drongo.protocol.ScriptType;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class StonewallUtxoSelector implements UtxoSelector { public class StonewallUtxoSelector implements UtxoSelector {
private final ScriptType preferredScriptType;
private final long noInputsFee; private final long noInputsFee;
//Use the same seed so the UTXO selection is deterministic //Use the same seed so the UTXO selection is deterministic
private final Random random = new Random(42); private final Random random = new Random(42);
public StonewallUtxoSelector(long noInputsFee) { public StonewallUtxoSelector(ScriptType preferredScriptType, long noInputsFee) {
this.preferredScriptType = preferredScriptType;
this.noInputsFee = noInputsFee; this.noInputsFee = noInputsFee;
} }
@ -17,6 +21,16 @@ public class StonewallUtxoSelector implements UtxoSelector {
public List<Collection<BlockTransactionHashIndex>> selectSets(long targetValue, Collection<OutputGroup> candidates) { public List<Collection<BlockTransactionHashIndex>> selectSets(long targetValue, Collection<OutputGroup> candidates) {
long actualTargetValue = targetValue + noInputsFee; long actualTargetValue = targetValue + noInputsFee;
List<OutputGroup> preferredCandidates = candidates.stream().filter(outputGroup -> outputGroup.getScriptType().equals(preferredScriptType)).collect(Collectors.toList());
List<Collection<BlockTransactionHashIndex>> preferredSets = selectSets(targetValue, preferredCandidates, actualTargetValue);
if(!preferredSets.isEmpty()) {
return preferredSets;
}
return selectSets(targetValue, candidates, actualTargetValue);
}
private List<Collection<BlockTransactionHashIndex>> selectSets(long targetValue, Collection<OutputGroup> candidates, long actualTargetValue) {
for(int i = 0; i < 10; i++) { for(int i = 0; i < 10; i++) {
List<OutputGroup> randomized = new ArrayList<>(candidates); List<OutputGroup> randomized = new ArrayList<>(candidates);
Collections.shuffle(randomized, random); Collections.shuffle(randomized, random);

View file

@ -117,6 +117,7 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
childWallet.purposeNodes.clear(); childWallet.purposeNodes.clear();
childWallet.transactions.clear(); childWallet.transactions.clear();
childWallet.detachedLabels.clear(); childWallet.detachedLabels.clear();
childWallet.childWallets.clear();
childWallet.storedBlockHeight = null; childWallet.storedBlockHeight = null;
childWallet.gapLimit = standardAccount.getMinimumGapLimit(); childWallet.gapLimit = standardAccount.getMinimumGapLimit();
childWallet.birthDate = null; childWallet.birthDate = null;
@ -156,12 +157,14 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
public Wallet getChildWallet(StandardAccount account) { public Wallet getChildWallet(StandardAccount account) {
for(Wallet childWallet : getChildWallets()) { for(Wallet childWallet : getChildWallets()) {
if(!childWallet.isNested()) {
for(Keystore keystore : childWallet.getKeystores()) { for(Keystore keystore : childWallet.getKeystores()) {
if(keystore.getKeyDerivation().getDerivation().get(keystore.getKeyDerivation().getDerivation().size() - 1).equals(account.getChildNumber())) { if(keystore.getKeyDerivation().getDerivation().get(keystore.getKeyDerivation().getDerivation().size() - 1).equals(account.getChildNumber())) {
return childWallet; return childWallet;
} }
} }
} }
}
return null; return null;
} }
@ -229,7 +232,12 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
List<Wallet> allWallets = new ArrayList<>(); List<Wallet> allWallets = new ArrayList<>();
Wallet masterWallet = isMasterWallet() ? this : getMasterWallet(); Wallet masterWallet = isMasterWallet() ? this : getMasterWallet();
allWallets.add(masterWallet); allWallets.add(masterWallet);
allWallets.addAll(masterWallet.getChildWallets()); for(Wallet childWallet : getChildWallets()) {
if(!childWallet.isNested()) {
allWallets.add(childWallet);
}
}
return allWallets; return allWallets;
} }
@ -274,7 +282,7 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
Address notificationAddress = externalPaymentCode.getNotificationAddress(); Address notificationAddress = externalPaymentCode.getNotificationAddress();
for(Map.Entry<BlockTransactionHashIndex, WalletNode> txoEntry : getWalletTxos().entrySet()) { for(Map.Entry<BlockTransactionHashIndex, WalletNode> txoEntry : getWalletTxos().entrySet()) {
if(txoEntry.getKey().isSpent()) { if(txoEntry.getKey().isSpent()) {
BlockTransaction blockTransaction = transactions.get(txoEntry.getKey().getSpentBy().getHash()); BlockTransaction blockTransaction = getWalletTransaction(txoEntry.getKey().getSpentBy().getHash());
if(blockTransaction != null) { if(blockTransaction != null) {
for(TransactionOutput txOutput : blockTransaction.getTransaction().getOutputs()) { for(TransactionOutput txOutput : blockTransaction.getTransaction().getOutputs()) {
if(notificationAddress.equals(txOutput.getScript().getToAddress())) { if(notificationAddress.equals(txOutput.getScript().getToAddress())) {
@ -293,6 +301,10 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
return Collections.emptyMap(); return Collections.emptyMap();
} }
public boolean isNested() {
return isBip47();
}
public boolean isBip47() { public boolean isBip47() {
return !isMasterWallet() && getKeystores().size() == 1 && getKeystores().get(0).getSource() == KeystoreSource.SW_PAYMENT_CODE; return !isMasterWallet() && getKeystores().size() == 1 && getKeystores().get(0).getSource() == KeystoreSource.SW_PAYMENT_CODE;
} }
@ -329,8 +341,10 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
Set<StandardAccount> whirlpoolAccounts = new HashSet<>(Set.of(StandardAccount.WHIRLPOOL_PREMIX, StandardAccount.WHIRLPOOL_POSTMIX, StandardAccount.WHIRLPOOL_BADBANK)); Set<StandardAccount> whirlpoolAccounts = new HashSet<>(Set.of(StandardAccount.WHIRLPOOL_PREMIX, StandardAccount.WHIRLPOOL_POSTMIX, StandardAccount.WHIRLPOOL_BADBANK));
for(Wallet childWallet : getChildWallets()) { for(Wallet childWallet : getChildWallets()) {
if(!childWallet.isNested()) {
whirlpoolAccounts.remove(childWallet.getStandardAccountType()); whirlpoolAccounts.remove(childWallet.getStandardAccountType());
} }
}
return whirlpoolAccounts.isEmpty(); return whirlpoolAccounts.isEmpty();
} }
@ -525,7 +539,7 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
WalletNode purposeNode; WalletNode purposeNode;
Optional<WalletNode> optionalPurposeNode = purposeNodes.stream().filter(node -> node.getKeyPurpose().equals(keyPurpose)).findFirst(); Optional<WalletNode> optionalPurposeNode = purposeNodes.stream().filter(node -> node.getKeyPurpose().equals(keyPurpose)).findFirst();
if(optionalPurposeNode.isEmpty()) { if(optionalPurposeNode.isEmpty()) {
purposeNode = new WalletNode(keyPurpose); purposeNode = new WalletNode(this, keyPurpose);
purposeNodes.add(purposeNode); purposeNodes.add(purposeNode);
} else { } else {
purposeNode = optionalPurposeNode.get(); purposeNode = optionalPurposeNode.get();
@ -598,10 +612,10 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
public Address getAddress(WalletNode node) { public Address getAddress(WalletNode node) {
if(policyType == PolicyType.SINGLE) { if(policyType == PolicyType.SINGLE) {
ECKey pubKey = getPubKey(node); ECKey pubKey = node.getPubKey();
return scriptType.getAddress(pubKey); return scriptType.getAddress(pubKey);
} else if(policyType == PolicyType.MULTI) { } else if(policyType == PolicyType.MULTI) {
List<ECKey> pubKeys = getPubKeys(node); List<ECKey> pubKeys = node.getPubKeys();
Script script = ScriptType.MULTISIG.getOutputScript(defaultPolicy.getNumSignaturesRequired(), pubKeys); Script script = ScriptType.MULTISIG.getOutputScript(defaultPolicy.getNumSignaturesRequired(), pubKeys);
return scriptType.getAddress(script); return scriptType.getAddress(script);
} else { } else {
@ -611,10 +625,10 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
public Script getOutputScript(WalletNode node) { public Script getOutputScript(WalletNode node) {
if(policyType == PolicyType.SINGLE) { if(policyType == PolicyType.SINGLE) {
ECKey pubKey = getPubKey(node); ECKey pubKey = node.getPubKey();
return scriptType.getOutputScript(pubKey); return scriptType.getOutputScript(pubKey);
} else if(policyType == PolicyType.MULTI) { } else if(policyType == PolicyType.MULTI) {
List<ECKey> pubKeys = getPubKeys(node); List<ECKey> pubKeys = node.getPubKeys();
Script script = ScriptType.MULTISIG.getOutputScript(defaultPolicy.getNumSignaturesRequired(), pubKeys); Script script = ScriptType.MULTISIG.getOutputScript(defaultPolicy.getNumSignaturesRequired(), pubKeys);
return scriptType.getOutputScript(script); return scriptType.getOutputScript(script);
} else { } else {
@ -624,10 +638,10 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
public String getOutputDescriptor(WalletNode node) { public String getOutputDescriptor(WalletNode node) {
if(policyType == PolicyType.SINGLE) { if(policyType == PolicyType.SINGLE) {
ECKey pubKey = getPubKey(node); ECKey pubKey = node.getPubKey();
return scriptType.getOutputDescriptor(pubKey); return scriptType.getOutputDescriptor(pubKey);
} else if(policyType == PolicyType.MULTI) { } else if(policyType == PolicyType.MULTI) {
List<ECKey> pubKeys = getPubKeys(node); List<ECKey> pubKeys = node.getPubKeys();
Script script = ScriptType.MULTISIG.getOutputScript(defaultPolicy.getNumSignaturesRequired(), pubKeys); Script script = ScriptType.MULTISIG.getOutputScript(defaultPolicy.getNumSignaturesRequired(), pubKeys);
return scriptType.getOutputDescriptor(script); return scriptType.getOutputDescriptor(script);
} else { } else {
@ -662,12 +676,20 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
getWalletAddresses(walletAddresses, getNode(keyPurpose)); getWalletAddresses(walletAddresses, getNode(keyPurpose));
} }
for(Wallet childWallet : getChildWallets()) {
if(childWallet.isNested()) {
for(KeyPurpose keyPurpose : childWallet.getWalletKeyPurposes()) {
getWalletAddresses(walletAddresses, childWallet.getNode(keyPurpose));
}
}
}
return walletAddresses; return walletAddresses;
} }
private void getWalletAddresses(Map<Address, WalletNode> walletAddresses, WalletNode purposeNode) { private void getWalletAddresses(Map<Address, WalletNode> walletAddresses, WalletNode purposeNode) {
for(WalletNode addressNode : purposeNode.getChildren()) { for(WalletNode addressNode : purposeNode.getChildren()) {
walletAddresses.put(getAddress(addressNode), addressNode); walletAddresses.put(addressNode.getAddress(), addressNode);
} }
} }
@ -692,12 +714,23 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
for(KeyPurpose keyPurpose : keyPurposes) { for(KeyPurpose keyPurpose : keyPurposes) {
getWalletOutputScripts(walletOutputScripts, getNode(keyPurpose)); getWalletOutputScripts(walletOutputScripts, getNode(keyPurpose));
} }
for(Wallet childWallet : getChildWallets()) {
if(childWallet.isNested()) {
for(KeyPurpose keyPurpose : childWallet.getWalletKeyPurposes()) {
if(keyPurposes.contains(keyPurpose)) {
getWalletOutputScripts(walletOutputScripts, childWallet.getNode(keyPurpose));
}
}
}
}
return walletOutputScripts; return walletOutputScripts;
} }
private void getWalletOutputScripts(Map<Script, WalletNode> walletOutputScripts, WalletNode purposeNode) { private void getWalletOutputScripts(Map<Script, WalletNode> walletOutputScripts, WalletNode purposeNode) {
for(WalletNode addressNode : purposeNode.getChildren()) { for(WalletNode addressNode : purposeNode.getChildren()) {
walletOutputScripts.put(getOutputScript(addressNode), addressNode); walletOutputScripts.put(addressNode.getOutputScript(), addressNode);
} }
} }
@ -719,6 +752,14 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
getWalletTxos(walletTxos, getNode(keyPurpose)); getWalletTxos(walletTxos, getNode(keyPurpose));
} }
for(Wallet childWallet : getChildWallets()) {
if(childWallet.isNested()) {
for(KeyPurpose keyPurpose : childWallet.getWalletKeyPurposes()) {
getWalletTxos(walletTxos, childWallet.getNode(keyPurpose));
}
}
}
return walletTxos; return walletTxos;
} }
@ -740,6 +781,14 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
getWalletUtxos(walletUtxos, getNode(keyPurpose), includeSpentMempoolOutputs); getWalletUtxos(walletUtxos, getNode(keyPurpose), includeSpentMempoolOutputs);
} }
for(Wallet childWallet : getChildWallets()) {
if(childWallet.isNested()) {
for(KeyPurpose keyPurpose : childWallet.getWalletKeyPurposes()) {
getWalletUtxos(walletUtxos, childWallet.getNode(keyPurpose), includeSpentMempoolOutputs);
}
}
}
return walletUtxos; return walletUtxos;
} }
@ -751,6 +800,51 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
} }
} }
public boolean hasTransactions() {
if(!transactions.isEmpty()) {
return true;
}
for(Wallet childWallet : getChildWallets()) {
if(childWallet.isNested()) {
if(!childWallet.transactions.isEmpty()) {
return true;
}
}
}
return false;
}
public BlockTransaction getWalletTransaction(Sha256Hash txid) {
BlockTransaction blockTransaction = transactions.get(txid);
if(blockTransaction != null) {
return blockTransaction;
}
for(Wallet childWallet : getChildWallets()) {
if(childWallet.isNested()) {
blockTransaction = childWallet.transactions.get(txid);
if(blockTransaction != null) {
return blockTransaction;
}
}
}
return null;
}
public Map<Sha256Hash, BlockTransaction> getWalletTransactions() {
Map<Sha256Hash, BlockTransaction> allTransactions = new HashMap<>(transactions);
for(Wallet childWallet : getChildWallets()) {
if(childWallet.isNested()) {
allTransactions.putAll(childWallet.transactions);
}
}
return allTransactions;
}
/** /**
* Determines the dust threshold for creating a new change output in this wallet. * Determines the dust threshold for creating a new change output in this wallet.
* *
@ -828,15 +922,15 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
WalletNode receiveNode = getFreshNode(KeyPurpose.RECEIVE); WalletNode receiveNode = getFreshNode(KeyPurpose.RECEIVE);
Transaction transaction = new Transaction(); Transaction transaction = new Transaction();
TransactionOutput prevTxOut = transaction.addOutput(1L, getAddress(receiveNode)); TransactionOutput prevTxOut = transaction.addOutput(1L, receiveNode.getAddress());
TransactionInput txInput = null; TransactionInput txInput = null;
if(getPolicyType().equals(PolicyType.SINGLE)) { if(getPolicyType().equals(PolicyType.SINGLE)) {
ECKey pubKey = getPubKey(receiveNode); ECKey pubKey = receiveNode.getPubKey();
TransactionSignature signature = TransactionSignature.dummy(getScriptType().getSignatureType()); TransactionSignature signature = TransactionSignature.dummy(getScriptType().getSignatureType());
txInput = getScriptType().addSpendingInput(transaction, prevTxOut, pubKey, signature); txInput = getScriptType().addSpendingInput(transaction, prevTxOut, pubKey, signature);
} else if(getPolicyType().equals(PolicyType.MULTI)) { } else if(getPolicyType().equals(PolicyType.MULTI)) {
List<ECKey> pubKeys = getPubKeys(receiveNode); List<ECKey> pubKeys = receiveNode.getPubKeys();
int threshold = getDefaultPolicy().getNumSignaturesRequired(); int threshold = getDefaultPolicy().getNumSignaturesRequired();
Map<ECKey, TransactionSignature> pubKeySignatures = new TreeMap<>(new ECKey.LexicographicECKeyComparator()); Map<ECKey, TransactionSignature> pubKeySignatures = new TreeMap<>(new ECKey.LexicographicECKeyComparator());
for(int i = 0; i < pubKeys.size(); i++) { for(int i = 0; i < pubKeys.size(); i++) {
@ -856,7 +950,7 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
public long getCostOfChange(double feeRate, double longTermFeeRate) { public long getCostOfChange(double feeRate, double longTermFeeRate) {
WalletNode changeNode = getFreshNode(KeyPurpose.CHANGE); WalletNode changeNode = getFreshNode(KeyPurpose.CHANGE);
TransactionOutput changeOutput = new TransactionOutput(new Transaction(), 1L, getOutputScript(changeNode)); TransactionOutput changeOutput = new TransactionOutput(new Transaction(), 1L, changeNode.getOutputScript());
return getFee(changeOutput, feeRate, longTermFeeRate); return getFee(changeOutput, feeRate, longTermFeeRate);
} }
@ -898,7 +992,7 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
//Add inputs //Add inputs
for(Map.Entry<BlockTransactionHashIndex, WalletNode> selectedUtxo : selectedUtxos.entrySet()) { for(Map.Entry<BlockTransactionHashIndex, WalletNode> selectedUtxo : selectedUtxos.entrySet()) {
Transaction prevTx = getTransactions().get(selectedUtxo.getKey().getHash()).getTransaction(); Transaction prevTx = getWalletTransaction(selectedUtxo.getKey().getHash()).getTransaction();
TransactionOutput prevTxOut = prevTx.getOutputs().get((int)selectedUtxo.getKey().getIndex()); TransactionOutput prevTxOut = prevTx.getOutputs().get((int)selectedUtxo.getKey().getIndex());
TransactionInput txInput = addDummySpendingInput(transaction, selectedUtxo.getValue(), prevTxOut); TransactionInput txInput = addDummySpendingInput(transaction, selectedUtxo.getValue(), prevTxOut);
@ -913,7 +1007,7 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
for(int i = 1; i < numSets; i+=2) { for(int i = 1; i < numSets; i+=2) {
WalletNode mixNode = getFreshNode(getChangeKeyPurpose()); WalletNode mixNode = getFreshNode(getChangeKeyPurpose());
txExcludedChangeNodes.add(mixNode); txExcludedChangeNodes.add(mixNode);
Payment fakeMixPayment = new Payment(getAddress(mixNode), ".." + mixNode + " (Fake Mix)", totalPaymentAmount, false); Payment fakeMixPayment = new Payment(mixNode.getAddress(), ".." + mixNode + " (Fake Mix)", totalPaymentAmount, false);
fakeMixPayment.setType(Payment.Type.FAKE_MIX); fakeMixPayment.setType(Payment.Type.FAKE_MIX);
txPayments.add(fakeMixPayment); txPayments.add(fakeMixPayment);
} }
@ -972,7 +1066,7 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
while(txExcludedChangeNodes.contains(changeNode)) { while(txExcludedChangeNodes.contains(changeNode)) {
changeNode = getFreshNode(getChangeKeyPurpose(), changeNode); changeNode = getFreshNode(getChangeKeyPurpose(), changeNode);
} }
TransactionOutput changeOutput = new TransactionOutput(transaction, setChangeAmts.iterator().next(), getOutputScript(changeNode)); TransactionOutput changeOutput = new TransactionOutput(transaction, setChangeAmts.iterator().next(), changeNode.getOutputScript());
double changeVSize = noChangeVSize + changeOutput.getLength() * numSets; double changeVSize = noChangeVSize + changeOutput.getLength() * numSets;
long changeFeeRequiredAmt = (fee == null ? (long)Math.floor(feeRate * changeVSize) : fee); long changeFeeRequiredAmt = (fee == null ? (long)Math.floor(feeRate * changeVSize) : fee);
changeFeeRequiredAmt = (fee == null && feeRate == Transaction.DEFAULT_MIN_RELAY_FEE ? changeFeeRequiredAmt + 1 : changeFeeRequiredAmt); changeFeeRequiredAmt = (fee == null && feeRate == Transaction.DEFAULT_MIN_RELAY_FEE ? changeFeeRequiredAmt + 1 : changeFeeRequiredAmt);
@ -984,7 +1078,7 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
Map<WalletNode, Long> changeMap = new LinkedHashMap<>(); Map<WalletNode, Long> changeMap = new LinkedHashMap<>();
setChangeAmts = getSetChangeAmounts(selectedUtxoSets, totalPaymentAmount, changeFeeRequiredAmt); setChangeAmts = getSetChangeAmounts(selectedUtxoSets, totalPaymentAmount, changeFeeRequiredAmt);
for(Long setChangeAmt : setChangeAmts) { for(Long setChangeAmt : setChangeAmts) {
transaction.addOutput(setChangeAmt, getOutputScript(changeNode)); transaction.addOutput(setChangeAmt, changeNode.getOutputScript());
changeMap.put(changeNode, setChangeAmt); changeMap.put(changeNode, setChangeAmt);
changeNode = getFreshNode(getChangeKeyPurpose(), changeNode); changeNode = getFreshNode(getChangeKeyPurpose(), changeNode);
} }
@ -1041,20 +1135,21 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
return changeAmts; return changeAmts;
} }
public TransactionInput addDummySpendingInput(Transaction transaction, WalletNode walletNode, TransactionOutput prevTxOut) { public static TransactionInput addDummySpendingInput(Transaction transaction, WalletNode walletNode, TransactionOutput prevTxOut) {
if(getPolicyType().equals(PolicyType.SINGLE)) { Wallet signingWallet = walletNode.getWallet();
ECKey pubKey = getPubKey(walletNode); if(signingWallet.getPolicyType().equals(PolicyType.SINGLE)) {
return getScriptType().addSpendingInput(transaction, prevTxOut, pubKey, TransactionSignature.dummy(getScriptType().getSignatureType())); ECKey pubKey = walletNode.getPubKey();
} else if(getPolicyType().equals(PolicyType.MULTI)) { return signingWallet.getScriptType().addSpendingInput(transaction, prevTxOut, pubKey, TransactionSignature.dummy(signingWallet.getScriptType().getSignatureType()));
List<ECKey> pubKeys = getPubKeys(walletNode); } else if(signingWallet.getPolicyType().equals(PolicyType.MULTI)) {
int threshold = getDefaultPolicy().getNumSignaturesRequired(); List<ECKey> pubKeys = walletNode.getPubKeys();
int threshold = signingWallet.getDefaultPolicy().getNumSignaturesRequired();
Map<ECKey, TransactionSignature> pubKeySignatures = new TreeMap<>(new ECKey.LexicographicECKeyComparator()); Map<ECKey, TransactionSignature> pubKeySignatures = new TreeMap<>(new ECKey.LexicographicECKeyComparator());
for(int i = 0; i < pubKeys.size(); i++) { for(int i = 0; i < pubKeys.size(); i++) {
pubKeySignatures.put(pubKeys.get(i), i < threshold ? TransactionSignature.dummy(getScriptType().getSignatureType()) : null); pubKeySignatures.put(pubKeys.get(i), i < threshold ? TransactionSignature.dummy(signingWallet.getScriptType().getSignatureType()) : null);
} }
return getScriptType().addMultisigSpendingInput(transaction, prevTxOut, threshold, pubKeySignatures); return signingWallet.getScriptType().addMultisigSpendingInput(transaction, prevTxOut, threshold, pubKeySignatures);
} else { } else {
throw new UnsupportedOperationException("Cannot create transaction for policy type " + getPolicyType()); throw new UnsupportedOperationException("Cannot create transaction for policy type " + signingWallet.getPolicyType());
} }
} }
@ -1104,14 +1199,23 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
private List<OutputGroup> getGroupedUtxos(List<UtxoFilter> utxoFilters, double feeRate, double longTermFeeRate, boolean groupByAddress, boolean includeSpentMempoolOutputs) { private List<OutputGroup> getGroupedUtxos(List<UtxoFilter> utxoFilters, double feeRate, double longTermFeeRate, boolean groupByAddress, boolean includeSpentMempoolOutputs) {
List<OutputGroup> outputGroups = new ArrayList<>(); List<OutputGroup> outputGroups = new ArrayList<>();
Map<Sha256Hash, BlockTransaction> walletTransactions = getWalletTransactions();
for(KeyPurpose keyPurpose : getWalletKeyPurposes()) { for(KeyPurpose keyPurpose : getWalletKeyPurposes()) {
getGroupedUtxos(outputGroups, getNode(keyPurpose), utxoFilters, feeRate, longTermFeeRate, groupByAddress, includeSpentMempoolOutputs); getGroupedUtxos(outputGroups, getNode(keyPurpose), utxoFilters, walletTransactions, feeRate, longTermFeeRate, groupByAddress, includeSpentMempoolOutputs);
}
for(Wallet childWallet : getChildWallets()) {
if(childWallet.isNested()) {
for(KeyPurpose keyPurpose : childWallet.getWalletKeyPurposes()) {
childWallet.getGroupedUtxos(outputGroups, childWallet.getNode(keyPurpose), utxoFilters, walletTransactions, feeRate, longTermFeeRate, groupByAddress, includeSpentMempoolOutputs);
}
}
} }
return outputGroups; return outputGroups;
} }
private void getGroupedUtxos(List<OutputGroup> outputGroups, WalletNode purposeNode, List<UtxoFilter> utxoFilters, double feeRate, double longTermFeeRate, boolean groupByAddress, boolean includeSpentMempoolOutputs) { private void getGroupedUtxos(List<OutputGroup> outputGroups, WalletNode purposeNode, List<UtxoFilter> utxoFilters, Map<Sha256Hash, BlockTransaction> walletTransactions, double feeRate, double longTermFeeRate, boolean groupByAddress, boolean includeSpentMempoolOutputs) {
for(WalletNode addressNode : purposeNode.getChildren()) { for(WalletNode addressNode : purposeNode.getChildren()) {
OutputGroup outputGroup = null; OutputGroup outputGroup = null;
for(BlockTransactionHashIndex utxo : addressNode.getUnspentTransactionOutputs(includeSpentMempoolOutputs)) { for(BlockTransactionHashIndex utxo : addressNode.getUnspentTransactionOutputs(includeSpentMempoolOutputs)) {
@ -1121,11 +1225,11 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
} }
if(outputGroup == null || !groupByAddress) { if(outputGroup == null || !groupByAddress) {
outputGroup = new OutputGroup(getStoredBlockHeight(), getInputWeightUnits(), feeRate, longTermFeeRate); outputGroup = new OutputGroup(addressNode.getWallet().getScriptType(), getStoredBlockHeight(), getInputWeightUnits(), feeRate, longTermFeeRate);
outputGroups.add(outputGroup); outputGroups.add(outputGroup);
} }
outputGroup.add(utxo, allInputsFromWallet(utxo.getHash())); outputGroup.add(utxo, allInputsFromWallet(walletTransactions, utxo.getHash()));
} }
} }
} }
@ -1137,7 +1241,12 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
* @return Whether the transaction was created entirely from inputs that reference outputs that belong to this wallet * @return Whether the transaction was created entirely from inputs that reference outputs that belong to this wallet
*/ */
public boolean allInputsFromWallet(Sha256Hash txId) { public boolean allInputsFromWallet(Sha256Hash txId) {
BlockTransaction utxoBlkTx = getTransactions().get(txId); Map<Sha256Hash, BlockTransaction> allTransactions = getWalletTransactions();
return allInputsFromWallet(allTransactions, txId);
}
private boolean allInputsFromWallet(Map<Sha256Hash, BlockTransaction> walletTransactions, Sha256Hash txId) {
BlockTransaction utxoBlkTx = walletTransactions.get(txId);
if(utxoBlkTx == null) { if(utxoBlkTx == null) {
//Provided txId was not a wallet transaction //Provided txId was not a wallet transaction
return false; return false;
@ -1145,7 +1254,7 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
for(int i = 0; i < utxoBlkTx.getTransaction().getInputs().size(); i++) { for(int i = 0; i < utxoBlkTx.getTransaction().getInputs().size(); i++) {
TransactionInput utxoTxInput = utxoBlkTx.getTransaction().getInputs().get(i); TransactionInput utxoTxInput = utxoBlkTx.getTransaction().getInputs().get(i);
BlockTransaction prevBlkTx = getTransactions().get(utxoTxInput.getOutpoint().getHash()); BlockTransaction prevBlkTx = walletTransactions.get(utxoTxInput.getOutpoint().getHash());
if(prevBlkTx == null) { if(prevBlkTx == null) {
return false; return false;
} }
@ -1171,13 +1280,13 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
*/ */
public long getMaxSpendable(List<Address> paymentAddresses, double feeRate) { public long getMaxSpendable(List<Address> paymentAddresses, double feeRate) {
long maxInputValue = 0; long maxInputValue = 0;
int inputWeightUnits = getInputWeightUnits();
long minInputValue = (long)Math.ceil(feeRate * inputWeightUnits / WITNESS_SCALE_FACTOR);
Transaction transaction = new Transaction(); Transaction transaction = new Transaction();
for(Map.Entry<BlockTransactionHashIndex, WalletNode> utxo : getWalletUtxos().entrySet()) { for(Map.Entry<BlockTransactionHashIndex, WalletNode> utxo : getWalletUtxos().entrySet()) {
int inputWeightUnits = utxo.getValue().getWallet().getInputWeightUnits();
long minInputValue = (long)Math.ceil(feeRate * inputWeightUnits / WITNESS_SCALE_FACTOR);
if(utxo.getKey().getValue() > minInputValue) { if(utxo.getKey().getValue() > minInputValue) {
Transaction prevTx = getTransactions().get(utxo.getKey().getHash()).getTransaction(); Transaction prevTx = getWalletTransaction(utxo.getKey().getHash()).getTransaction();
TransactionOutput prevTxOut = prevTx.getOutputs().get((int)utxo.getKey().getIndex()); TransactionOutput prevTxOut = prevTx.getOutputs().get((int)utxo.getKey().getIndex());
addDummySpendingInput(transaction, utxo.getValue(), prevTxOut); addDummySpendingInput(transaction, utxo.getValue(), prevTxOut);
maxInputValue += utxo.getKey().getValue(); maxInputValue += utxo.getKey().getValue();
@ -1207,7 +1316,7 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
Map<Script, WalletNode> walletOutputScripts = getWalletOutputScripts(); Map<Script, WalletNode> walletOutputScripts = getWalletOutputScripts();
for(TransactionInput txInput : transaction.getInputs()) { for(TransactionInput txInput : transaction.getInputs()) {
BlockTransaction blockTransaction = transactions.get(txInput.getOutpoint().getHash()); BlockTransaction blockTransaction = getWalletTransaction(txInput.getOutpoint().getHash());
if(blockTransaction != null && blockTransaction.getTransaction().getOutputs().size() > txInput.getOutpoint().getIndex()) { if(blockTransaction != null && blockTransaction.getTransaction().getOutputs().size() > txInput.getOutpoint().getIndex()) {
TransactionOutput utxo = blockTransaction.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex()); TransactionOutput utxo = blockTransaction.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex());
@ -1236,20 +1345,22 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
for(TransactionInput txInput : signingNodes.keySet()) { for(TransactionInput txInput : signingNodes.keySet()) {
WalletNode walletNode = signingNodes.get(txInput); WalletNode walletNode = signingNodes.get(txInput);
Map<ECKey, Keystore> keystoreKeysForNode = getKeystores().stream().collect(Collectors.toMap(keystore -> getScriptType().getOutputKey(keystore.getPubKey(walletNode)), Function.identity(), Wallet signingWallet = walletNode.getWallet();
Map<ECKey, Keystore> keystoreKeysForNode = signingWallet.getKeystores().stream()
.collect(Collectors.toMap(keystore -> signingWallet.getScriptType().getOutputKey(keystore.getPubKey(walletNode)), Function.identity(),
(u, v) -> { throw new IllegalStateException("Duplicate keys from different keystores for node " + walletNode); }, (u, v) -> { throw new IllegalStateException("Duplicate keys from different keystores for node " + walletNode); },
LinkedHashMap::new)); LinkedHashMap::new));
Map<ECKey, TransactionSignature> keySignatureMap = new LinkedHashMap<>(); Map<ECKey, TransactionSignature> keySignatureMap = new LinkedHashMap<>();
BlockTransaction blockTransaction = transactions.get(txInput.getOutpoint().getHash()); BlockTransaction blockTransaction = signingWallet.transactions.get(txInput.getOutpoint().getHash());
if(blockTransaction != null && blockTransaction.getTransaction().getOutputs().size() > txInput.getOutpoint().getIndex()) { if(blockTransaction != null && blockTransaction.getTransaction().getOutputs().size() > txInput.getOutpoint().getIndex()) {
TransactionOutput spentTxo = blockTransaction.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex()); TransactionOutput spentTxo = blockTransaction.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex());
Script signingScript = getSigningScript(txInput, spentTxo); Script signingScript = getSigningScript(txInput, spentTxo);
Sha256Hash hash; Sha256Hash hash;
if(getScriptType() == P2TR) { if(signingWallet.getScriptType() == P2TR) {
List<TransactionOutput> spentOutputs = transaction.getInputs().stream().map(input -> transactions.get(input.getOutpoint().getHash()).getTransaction().getOutputs().get((int)input.getOutpoint().getIndex())).collect(Collectors.toList()); List<TransactionOutput> spentOutputs = transaction.getInputs().stream().map(input -> signingWallet.transactions.get(input.getOutpoint().getHash()).getTransaction().getOutputs().get((int)input.getOutpoint().getIndex())).collect(Collectors.toList());
hash = transaction.hashForTaprootSignature(spentOutputs, txInput.getIndex(), !P2TR.isScriptType(signingScript), signingScript, SigHash.ALL_TAPROOT, null); hash = transaction.hashForTaprootSignature(spentOutputs, txInput.getIndex(), !P2TR.isScriptType(signingScript), signingScript, SigHash.ALL_TAPROOT, null);
} else if(txInput.hasWitness()) { } else if(txInput.hasWitness()) {
hash = transaction.hashForWitnessSignature(txInput.getIndex(), signingScript, spentTxo.getValue(), SigHash.ALL); hash = transaction.hashForWitnessSignature(txInput.getIndex(), signingScript, spentTxo.getValue(), SigHash.ALL);
@ -1336,7 +1447,9 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
for(PSBTInput psbtInput : signingNodes.keySet()) { for(PSBTInput psbtInput : signingNodes.keySet()) {
WalletNode walletNode = signingNodes.get(psbtInput); WalletNode walletNode = signingNodes.get(psbtInput);
Map<ECKey, Keystore> keystoreKeysForNode = getKeystores().stream().collect(Collectors.toMap(keystore -> getScriptType().getOutputKey(keystore.getPubKey(walletNode)), Function.identity(), Wallet signingWallet = walletNode.getWallet();
Map<ECKey, Keystore> keystoreKeysForNode = signingWallet.getKeystores().stream()
.collect(Collectors.toMap(keystore -> signingWallet.getScriptType().getOutputKey(keystore.getPubKey(walletNode)), Function.identity(),
(u, v) -> { throw new IllegalStateException("Duplicate keys from different keystores for node " + walletNode); }, (u, v) -> { throw new IllegalStateException("Duplicate keys from different keystores for node " + walletNode); },
LinkedHashMap::new)); LinkedHashMap::new));
@ -1362,10 +1475,11 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
public void sign(PSBT psbt) throws MnemonicException { public void sign(PSBT psbt) throws MnemonicException {
Map<PSBTInput, WalletNode> signingNodes = getSigningNodes(psbt); Map<PSBTInput, WalletNode> signingNodes = getSigningNodes(psbt);
for(Keystore keystore : getKeystores()) {
if(keystore.hasPrivateKey()) {
for(Map.Entry<PSBTInput, WalletNode> signingEntry : signingNodes.entrySet()) { for(Map.Entry<PSBTInput, WalletNode> signingEntry : signingNodes.entrySet()) {
ECKey privKey = getScriptType().getOutputKey(keystore.getKey(signingEntry.getValue())); Wallet signingWallet = signingEntry.getValue().getWallet();
for(Keystore keystore : signingWallet.getKeystores()) {
if(keystore.hasPrivateKey()) {
ECKey privKey = signingWallet.getScriptType().getOutputKey(keystore.getKey(signingEntry.getValue()));
PSBTInput psbtInput = signingEntry.getKey(); PSBTInput psbtInput = signingEntry.getKey();
if(!psbtInput.isSigned()) { if(!psbtInput.isSigned()) {
@ -1410,15 +1524,15 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
TransactionInput finalizedTxInput; TransactionInput finalizedTxInput;
if(getPolicyType().equals(PolicyType.SINGLE)) { if(getPolicyType().equals(PolicyType.SINGLE)) {
ECKey pubKey = getPubKey(signingNode); ECKey pubKey = signingNode.getPubKey();
TransactionSignature transactionSignature = psbtInput.isTaproot() ? psbtInput.getTapKeyPathSignature() : psbtInput.getPartialSignature(pubKey); TransactionSignature transactionSignature = psbtInput.isTaproot() ? psbtInput.getTapKeyPathSignature() : psbtInput.getPartialSignature(pubKey);
if(transactionSignature == null) { if(transactionSignature == null) {
throw new IllegalArgumentException("Pubkey of partial signature does not match wallet pubkey"); throw new IllegalArgumentException("Pubkey of partial signature does not match wallet pubkey");
} }
finalizedTxInput = getScriptType().addSpendingInput(transaction, utxo, pubKey, transactionSignature); finalizedTxInput = signingNode.getWallet().getScriptType().addSpendingInput(transaction, utxo, pubKey, transactionSignature);
} else if(getPolicyType().equals(PolicyType.MULTI)) { } else if(getPolicyType().equals(PolicyType.MULTI)) {
List<ECKey> pubKeys = getPubKeys(signingNode); List<ECKey> pubKeys = signingNode.getPubKeys();
Map<ECKey, TransactionSignature> pubKeySignatures = new TreeMap<>(new ECKey.LexicographicECKeyComparator()); Map<ECKey, TransactionSignature> pubKeySignatures = new TreeMap<>(new ECKey.LexicographicECKeyComparator());
for(ECKey pubKey : pubKeys) { for(ECKey pubKey : pubKeys) {
@ -1430,7 +1544,7 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
throw new IllegalArgumentException("Pubkeys of partial signatures do not match wallet pubkeys"); throw new IllegalArgumentException("Pubkeys of partial signatures do not match wallet pubkeys");
} }
finalizedTxInput = getScriptType().addMultisigSpendingInput(transaction, utxo, threshold, pubKeySignatures); finalizedTxInput = signingNode.getWallet().getScriptType().addMultisigSpendingInput(transaction, utxo, threshold, pubKeySignatures);
} else { } else {
throw new UnsupportedOperationException("Cannot finalise PSBT for policy type " + getPolicyType()); throw new UnsupportedOperationException("Cannot finalise PSBT for policy type " + getPolicyType());
} }
@ -1471,6 +1585,12 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
transactions.clear(); transactions.clear();
storedBlockHeight = 0; storedBlockHeight = 0;
for(Wallet childWallet : getChildWallets()) {
if(childWallet.isNested()) {
childWallet.clearHistory();
}
}
} }
private Map<String, String> getDetachedLabels(boolean includeAddresses) { private Map<String, String> getDetachedLabels(boolean includeAddresses) {
@ -1484,7 +1604,7 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
for(WalletNode purposeNode : purposeNodes) { for(WalletNode purposeNode : purposeNodes) {
for(WalletNode addressNode : purposeNode.getChildren()) { for(WalletNode addressNode : purposeNode.getChildren()) {
if(includeAddresses && addressNode.getLabel() != null && !addressNode.getLabel().isEmpty()) { if(includeAddresses && addressNode.getLabel() != null && !addressNode.getLabel().isEmpty()) {
labels.put(getAddress(addressNode).toString(), addressNode.getLabel()); labels.put(addressNode.getAddress().toString(), addressNode.getLabel());
} }
for(BlockTransactionHashIndex output : addressNode.getTransactionOutputs()) { for(BlockTransactionHashIndex output : addressNode.getTransactionOutputs()) {
@ -1637,7 +1757,7 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
copy.getKeystores().add(keystore.copy()); copy.getKeystores().add(keystore.copy());
} }
for(WalletNode node : purposeNodes) { for(WalletNode node : purposeNodes) {
copy.purposeNodes.add(node.copy()); copy.purposeNodes.add(node.copy(copy));
} }
for(Sha256Hash hash : transactions.keySet()) { for(Sha256Hash hash : transactions.keySet()) {
copy.transactions.put(hash, transactions.get(hash)); copy.transactions.put(hash, transactions.get(hash));
@ -1684,6 +1804,12 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
} }
} }
for(Wallet childWallet : getChildWallets()) {
if(childWallet.isNested() && childWallet.isEncrypted()) {
return true;
}
}
return false; return false;
} }
@ -1691,24 +1817,48 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
for(Keystore keystore : keystores) { for(Keystore keystore : keystores) {
keystore.encrypt(key); keystore.encrypt(key);
} }
for(Wallet childWallet : getChildWallets()) {
if(childWallet.isNested()) {
childWallet.encrypt(key);
}
}
} }
public void decrypt(CharSequence password) { public void decrypt(CharSequence password) {
for(Keystore keystore : keystores) { for(Keystore keystore : keystores) {
keystore.decrypt(password); keystore.decrypt(password);
} }
for(Wallet childWallet : getChildWallets()) {
if(childWallet.isNested()) {
childWallet.decrypt(password);
}
}
} }
public void decrypt(Key key) { public void decrypt(Key key) {
for(Keystore keystore : keystores) { for(Keystore keystore : keystores) {
keystore.decrypt(key); keystore.decrypt(key);
} }
for(Wallet childWallet : getChildWallets()) {
if(childWallet.isNested()) {
childWallet.decrypt(key);
}
}
} }
public void clearPrivate() { public void clearPrivate() {
for(Keystore keystore : keystores) { for(Keystore keystore : keystores) {
keystore.clearPrivate(); keystore.clearPrivate();
} }
for(Wallet childWallet : getChildWallets()) {
if(childWallet.isNested()) {
childWallet.clearPrivate();
}
}
} }
@Override @Override

View file

@ -2,7 +2,10 @@ package com.sparrowwallet.drongo.wallet;
import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.crypto.ChildNumber; import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.Script;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -13,29 +16,53 @@ public class WalletNode extends Persistable implements Comparable<WalletNode> {
private TreeSet<WalletNode> children = new TreeSet<>(); private TreeSet<WalletNode> children = new TreeSet<>();
private TreeSet<BlockTransactionHashIndex> transactionOutputs = new TreeSet<>(); private TreeSet<BlockTransactionHashIndex> transactionOutputs = new TreeSet<>();
private transient Wallet wallet;
private transient KeyPurpose keyPurpose; private transient KeyPurpose keyPurpose;
private transient int index = -1; private transient int index = -1;
private transient List<ChildNumber> derivation; private transient List<ChildNumber> derivation;
//Cache pubkeys for BIP47 wallets to avoid time-consuming ECDH calculations
private transient ECKey cachedPubKey;
//Note use of this constructor must be followed by setting the wallet field
public WalletNode(String derivationPath) { public WalletNode(String derivationPath) {
this.derivationPath = derivationPath; this.derivationPath = derivationPath;
parseDerivation(); parseDerivation();
} }
public WalletNode(KeyPurpose keyPurpose) { public WalletNode(Wallet wallet, String derivationPath) {
this.wallet = wallet;
this.derivationPath = derivationPath;
parseDerivation();
}
public WalletNode(Wallet wallet, KeyPurpose keyPurpose) {
this.wallet = wallet;
this.derivation = List.of(keyPurpose.getPathIndex()); this.derivation = List.of(keyPurpose.getPathIndex());
this.derivationPath = KeyDerivation.writePath(derivation); this.derivationPath = KeyDerivation.writePath(derivation);
this.keyPurpose = keyPurpose; this.keyPurpose = keyPurpose;
this.index = keyPurpose.getPathIndex().num(); this.index = keyPurpose.getPathIndex().num();
} }
public WalletNode(KeyPurpose keyPurpose, int index) { public WalletNode(Wallet wallet, KeyPurpose keyPurpose, int index) {
this.wallet = wallet;
this.derivation = List.of(keyPurpose.getPathIndex(), new ChildNumber(index)); this.derivation = List.of(keyPurpose.getPathIndex(), new ChildNumber(index));
this.derivationPath = KeyDerivation.writePath(derivation); this.derivationPath = KeyDerivation.writePath(derivation);
this.keyPurpose = keyPurpose; this.keyPurpose = keyPurpose;
this.index = index; this.index = index;
} }
public Wallet getWallet() {
return wallet;
}
public void setWallet(Wallet wallet) {
this.wallet = wallet;
for(WalletNode childNode : getChildren()) {
childNode.setWallet(wallet);
}
}
public String getDerivationPath() { public String getDerivationPath() {
return derivationPath; return derivationPath;
} }
@ -154,7 +181,7 @@ public class WalletNode extends Persistable implements Comparable<WalletNode> {
Set<WalletNode> newNodes = fillToIndex(index); Set<WalletNode> newNodes = fillToIndex(index);
if(!wallet.getDetachedLabels().isEmpty() && wallet.isValid()) { if(!wallet.getDetachedLabels().isEmpty() && wallet.isValid()) {
for(WalletNode newNode : newNodes) { for(WalletNode newNode : newNodes) {
String label = wallet.getDetachedLabels().remove(wallet.getAddress(newNode).toString()); String label = wallet.getDetachedLabels().remove(newNode.getAddress().toString());
if(label != null && (newNode.getLabel() == null || newNode.getLabel().isEmpty())) { if(label != null && (newNode.getLabel() == null || newNode.getLabel().isEmpty())) {
newNode.setLabel(label); newNode.setLabel(label);
} }
@ -167,7 +194,7 @@ public class WalletNode extends Persistable implements Comparable<WalletNode> {
public synchronized Set<WalletNode> fillToIndex(int index) { public synchronized Set<WalletNode> fillToIndex(int index) {
Set<WalletNode> newNodes = new TreeSet<>(); Set<WalletNode> newNodes = new TreeSet<>();
for(int i = 0; i <= index; i++) { for(int i = 0; i <= index; i++) {
WalletNode node = new WalletNode(getKeyPurpose(), i); WalletNode node = new WalletNode(wallet, getKeyPurpose(), i);
if(children.add(node)) { if(children.add(node)) {
newNodes.add(node); newNodes.add(node);
} }
@ -190,6 +217,35 @@ public class WalletNode extends Persistable implements Comparable<WalletNode> {
return highestNode == null ? null : highestNode.index; return highestNode == null ? null : highestNode.index;
} }
public ECKey getPubKey() {
if(cachedPubKey != null) {
return cachedPubKey;
}
if(wallet.isBip47()) {
cachedPubKey = wallet.getPubKey(this);
return cachedPubKey;
}
return wallet.getPubKey(this);
}
public List<ECKey> getPubKeys() {
return wallet.getPubKeys(this);
}
public Address getAddress() {
return wallet.getAddress(this);
}
public Script getOutputScript() {
return wallet.getOutputScript(this);
}
public String getOutputDescriptor() {
return wallet.getOutputDescriptor(this);
}
@Override @Override
public String toString() { public String toString() {
return derivationPath.replace("m", ".."); return derivationPath.replace("m", "..");
@ -200,12 +256,12 @@ public class WalletNode extends Persistable implements Comparable<WalletNode> {
if (this == o) return true; if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
WalletNode node = (WalletNode) o; WalletNode node = (WalletNode) o;
return derivationPath.equals(node.derivationPath); return Objects.equals(wallet, node.wallet) && derivationPath.equals(node.derivationPath);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return derivationPath.hashCode(); return Objects.hash(wallet, derivationPath);
} }
@Override @Override
@ -232,13 +288,13 @@ public class WalletNode extends Persistable implements Comparable<WalletNode> {
} }
} }
public WalletNode copy() { public WalletNode copy(Wallet walletCopy) {
WalletNode copy = new WalletNode(derivationPath); WalletNode copy = new WalletNode(walletCopy, derivationPath);
copy.setId(getId()); copy.setId(getId());
copy.setLabel(label); copy.setLabel(label);
for(WalletNode child : getChildren()) { for(WalletNode child : getChildren()) {
copy.children.add(child.copy()); copy.children.add(child.copy(walletCopy));
} }
for(BlockTransactionHashIndex txo : getTransactionOutputs()) { for(BlockTransactionHashIndex txo : getTransactionOutputs()) {

View file

@ -79,7 +79,7 @@ public class WalletTransaction {
} }
public Address getChangeAddress(WalletNode changeNode) { public Address getChangeAddress(WalletNode changeNode) {
return getWallet().getAddress(changeNode); return changeNode.getAddress();
} }
public long getFee() { public long getFee() {

View file

@ -126,15 +126,15 @@ public class PaymentCodeTest {
Assert.assertEquals("1JDdmqFLhpzcUwPeinhJbUPw4Co3aWLyzW", paymentCodeAlice.getNotificationAddress().toString()); Assert.assertEquals("1JDdmqFLhpzcUwPeinhJbUPw4Co3aWLyzW", paymentCodeAlice.getNotificationAddress().toString());
WalletNode sendNode0 = aliceBip47Wallet.getFreshNode(KeyPurpose.SEND); WalletNode sendNode0 = aliceBip47Wallet.getFreshNode(KeyPurpose.SEND);
Address address0 = aliceBip47Wallet.getAddress(sendNode0); Address address0 = sendNode0.getAddress();
Assert.assertEquals("141fi7TY3h936vRUKh1qfUZr8rSBuYbVBK", address0.toString()); Assert.assertEquals("141fi7TY3h936vRUKh1qfUZr8rSBuYbVBK", address0.toString());
WalletNode sendNode1 = aliceBip47Wallet.getFreshNode(KeyPurpose.SEND, sendNode0); WalletNode sendNode1 = aliceBip47Wallet.getFreshNode(KeyPurpose.SEND, sendNode0);
Address address1 = aliceBip47Wallet.getAddress(sendNode1); Address address1 = sendNode1.getAddress();
Assert.assertEquals("12u3Uued2fuko2nY4SoSFGCoGLCBUGPkk6", address1.toString()); Assert.assertEquals("12u3Uued2fuko2nY4SoSFGCoGLCBUGPkk6", address1.toString());
WalletNode sendNode2 = aliceBip47Wallet.getFreshNode(KeyPurpose.SEND, sendNode1); WalletNode sendNode2 = aliceBip47Wallet.getFreshNode(KeyPurpose.SEND, sendNode1);
Address address2 = aliceBip47Wallet.getAddress(sendNode2); Address address2 = sendNode2.getAddress();
Assert.assertEquals("1FsBVhT5dQutGwaPePTYMe5qvYqqjxyftc", address2.toString()); Assert.assertEquals("1FsBVhT5dQutGwaPePTYMe5qvYqqjxyftc", address2.toString());
DeterministicSeed bobSeed = new DeterministicSeed("reward upper indicate eight swift arch injury crystal super wrestle already dentist", "", 0, DeterministicSeed.Type.BIP39); DeterministicSeed bobSeed = new DeterministicSeed("reward upper indicate eight swift arch injury crystal super wrestle already dentist", "", 0, DeterministicSeed.Type.BIP39);
@ -149,15 +149,15 @@ public class PaymentCodeTest {
Assert.assertEquals("1ChvUUvht2hUQufHBXF8NgLhW8SwE2ecGV", paymentCodeBob.getNotificationAddress().toString()); Assert.assertEquals("1ChvUUvht2hUQufHBXF8NgLhW8SwE2ecGV", paymentCodeBob.getNotificationAddress().toString());
WalletNode receiveNode0 = bobBip47Wallet.getFreshNode(KeyPurpose.RECEIVE); WalletNode receiveNode0 = bobBip47Wallet.getFreshNode(KeyPurpose.RECEIVE);
Address receiveAddress0 = bobBip47Wallet.getAddress(receiveNode0); Address receiveAddress0 = receiveNode0.getAddress();
Assert.assertEquals("141fi7TY3h936vRUKh1qfUZr8rSBuYbVBK", receiveAddress0.toString()); Assert.assertEquals("141fi7TY3h936vRUKh1qfUZr8rSBuYbVBK", receiveAddress0.toString());
WalletNode receiveNode1 = bobBip47Wallet.getFreshNode(KeyPurpose.RECEIVE, receiveNode0); WalletNode receiveNode1 = bobBip47Wallet.getFreshNode(KeyPurpose.RECEIVE, receiveNode0);
Address receiveAddress1 = bobBip47Wallet.getAddress(receiveNode1); Address receiveAddress1 = receiveNode1.getAddress();
Assert.assertEquals("12u3Uued2fuko2nY4SoSFGCoGLCBUGPkk6", receiveAddress1.toString()); Assert.assertEquals("12u3Uued2fuko2nY4SoSFGCoGLCBUGPkk6", receiveAddress1.toString());
WalletNode receiveNode2 = bobBip47Wallet.getFreshNode(KeyPurpose.RECEIVE, receiveNode1); WalletNode receiveNode2 = bobBip47Wallet.getFreshNode(KeyPurpose.RECEIVE, receiveNode1);
Address receiveAddress2 = bobBip47Wallet.getAddress(receiveNode2); Address receiveAddress2 = receiveNode2.getAddress();
Assert.assertEquals("1FsBVhT5dQutGwaPePTYMe5qvYqqjxyftc", receiveAddress2.toString()); Assert.assertEquals("1FsBVhT5dQutGwaPePTYMe5qvYqqjxyftc", receiveAddress2.toString());
ECKey privKey0 = bobWallet.getKeystores().get(0).getKey(receiveNode0); ECKey privKey0 = bobWallet.getKeystores().get(0).getKey(receiveNode0);

View file

@ -22,7 +22,7 @@ public class ECKeyTest {
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2PKH, wallet.getKeystores(), 1)); wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2PKH, wallet.getKeystores(), 1));
WalletNode firstReceive = wallet.getNode(KeyPurpose.RECEIVE).getChildren().iterator().next(); WalletNode firstReceive = wallet.getNode(KeyPurpose.RECEIVE).getChildren().iterator().next();
Address address = wallet.getAddress(firstReceive); Address address = firstReceive.getAddress();
Assert.assertEquals("14JmU9a7SzieZNEtBnsZo688rt3mGrw6hr", address.toString()); Assert.assertEquals("14JmU9a7SzieZNEtBnsZo688rt3mGrw6hr", address.toString());
ECKey privKey = keystore.getKey(firstReceive); ECKey privKey = keystore.getKey(firstReceive);

View file

@ -104,8 +104,10 @@ public class WalletTest {
wallet.getKeystores().add(keystore); wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2PKH, wallet.getKeystores(), 1)); wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2PKH, wallet.getKeystores(), 1));
Assert.assertEquals("12kTQjuWDp7Uu6PwY6CsS1KLTt3d1DBHZa", wallet.getAddress(new WalletNode(KeyPurpose.RECEIVE, 0)).toString()); WalletNode receive0 = new WalletNode(wallet, KeyPurpose.RECEIVE, 0);
Assert.assertEquals("1HbQwQCitHQxVtP39isXmUdHx7hQCZovrK", wallet.getAddress(new WalletNode(KeyPurpose.RECEIVE, 1)).toString()); Assert.assertEquals("12kTQjuWDp7Uu6PwY6CsS1KLTt3d1DBHZa", receive0.getAddress().toString());
WalletNode receive1 = new WalletNode(wallet, KeyPurpose.RECEIVE, 1);
Assert.assertEquals("1HbQwQCitHQxVtP39isXmUdHx7hQCZovrK", receive1.getAddress().toString());
} }
@Test @Test
@ -119,8 +121,10 @@ public class WalletTest {
wallet.getKeystores().add(keystore); wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2SH_P2WPKH, wallet.getKeystores(), 1)); wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2SH_P2WPKH, wallet.getKeystores(), 1));
Assert.assertEquals("3NZLE4TntsjtcZ5MbrfxwtYo9meBVybVQj", wallet.getAddress(new WalletNode(KeyPurpose.RECEIVE, 0)).toString()); WalletNode receive0 = new WalletNode(wallet, KeyPurpose.RECEIVE, 0);
Assert.assertEquals("32YBBuRsp8XTeLx4T6BmD2L4nANGaNDkSg", wallet.getAddress(new WalletNode(KeyPurpose.RECEIVE, 1)).toString()); Assert.assertEquals("3NZLE4TntsjtcZ5MbrfxwtYo9meBVybVQj", receive0.getAddress().toString());
WalletNode receive1 = new WalletNode(wallet, KeyPurpose.RECEIVE, 1);
Assert.assertEquals("32YBBuRsp8XTeLx4T6BmD2L4nANGaNDkSg", receive1.getAddress().toString());
} }
@Test @Test
@ -134,8 +138,10 @@ public class WalletTest {
wallet.getKeystores().add(keystore); wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2WPKH, wallet.getKeystores(), 1)); wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2WPKH, wallet.getKeystores(), 1));
Assert.assertEquals("bc1quvxdut936uswuxwxrk6nvjmgwxh463r0fjwn55", wallet.getAddress(new WalletNode(KeyPurpose.RECEIVE, 0)).toString()); WalletNode receive0 = new WalletNode(wallet, KeyPurpose.RECEIVE, 0);
Assert.assertEquals("bc1q95j2862dz7mqpraw6qdjc70gumyu5z7adgq9x9", wallet.getAddress(new WalletNode(KeyPurpose.RECEIVE, 1)).toString()); Assert.assertEquals("bc1quvxdut936uswuxwxrk6nvjmgwxh463r0fjwn55", receive0.getAddress().toString());
WalletNode receive1 = new WalletNode(wallet, KeyPurpose.RECEIVE, 1);
Assert.assertEquals("bc1q95j2862dz7mqpraw6qdjc70gumyu5z7adgq9x9", receive1.getAddress().toString());
} }
@Test @Test
@ -159,8 +165,10 @@ public class WalletTest {
wallet.getKeystores().add(keystore2); wallet.getKeystores().add(keystore2);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI, ScriptType.P2SH, wallet.getKeystores(), 2)); wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI, ScriptType.P2SH, wallet.getKeystores(), 2));
Assert.assertEquals("38kq6yz4VcYymTExQPY3gppbz38mtPLveK", wallet.getAddress(new WalletNode(KeyPurpose.RECEIVE, 0)).toString()); WalletNode receive0 = new WalletNode(wallet, KeyPurpose.RECEIVE, 0);
Assert.assertEquals("3EdKaNsnjBTBggWcSMRyVju6GbHWy68mAH", wallet.getAddress(new WalletNode(KeyPurpose.CHANGE, 1)).toString()); Assert.assertEquals("38kq6yz4VcYymTExQPY3gppbz38mtPLveK", receive0.getAddress().toString());
WalletNode receive1 = new WalletNode(wallet, KeyPurpose.CHANGE, 1);
Assert.assertEquals("3EdKaNsnjBTBggWcSMRyVju6GbHWy68mAH", receive1.getAddress().toString());
} }
@Test @Test
@ -184,8 +192,10 @@ public class WalletTest {
wallet.getKeystores().add(keystore2); wallet.getKeystores().add(keystore2);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI, ScriptType.P2SH_P2WSH, wallet.getKeystores(), 2)); wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI, ScriptType.P2SH_P2WSH, wallet.getKeystores(), 2));
Assert.assertEquals("3Mw8xqAHh8g3eBvh7q1UEUmoexqdXDK9Tf", wallet.getAddress(new WalletNode(KeyPurpose.RECEIVE, 0)).toString()); WalletNode receive0 = new WalletNode(wallet, KeyPurpose.RECEIVE, 0);
Assert.assertEquals("35dFo1ivJ8jyHpyf42MWvnYf5LBU8Siren", wallet.getAddress(new WalletNode(KeyPurpose.CHANGE, 1)).toString()); Assert.assertEquals("3Mw8xqAHh8g3eBvh7q1UEUmoexqdXDK9Tf", receive0.getAddress().toString());
WalletNode receive1 = new WalletNode(wallet, KeyPurpose.CHANGE, 1);
Assert.assertEquals("35dFo1ivJ8jyHpyf42MWvnYf5LBU8Siren", receive1.getAddress().toString());
} }
@Test @Test
@ -209,8 +219,10 @@ public class WalletTest {
wallet.getKeystores().add(keystore2); wallet.getKeystores().add(keystore2);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI, ScriptType.P2WSH, wallet.getKeystores(), 2)); wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI, ScriptType.P2WSH, wallet.getKeystores(), 2));
Assert.assertEquals("bc1q20e4vm656h5lvmngz9ztz6hjzftvh39yzngqhuqzk8qzj7tqnzaqgclrwc", wallet.getAddress(new WalletNode(KeyPurpose.RECEIVE, 0)).toString()); WalletNode receive0 = new WalletNode(wallet, KeyPurpose.RECEIVE, 0);
Assert.assertEquals("bc1q2epdx7dplwaas2jucfrzmxm8350rqh68hs6vqreysku80ye44mfqla85f2", wallet.getAddress(new WalletNode(KeyPurpose.CHANGE, 1)).toString()); Assert.assertEquals("bc1q20e4vm656h5lvmngz9ztz6hjzftvh39yzngqhuqzk8qzj7tqnzaqgclrwc", receive0.getAddress().toString());
WalletNode receive1 = new WalletNode(wallet, KeyPurpose.CHANGE, 1);
Assert.assertEquals("bc1q2epdx7dplwaas2jucfrzmxm8350rqh68hs6vqreysku80ye44mfqla85f2", receive1.getAddress().toString());
} }
@Test @Test
@ -227,6 +239,7 @@ public class WalletTest {
List<ChildNumber> derivation = List.of(keystore.getExtendedPublicKey().getKeyChildNumber(), new ChildNumber(0)); List<ChildNumber> derivation = List.of(keystore.getExtendedPublicKey().getKeyChildNumber(), new ChildNumber(0));
Assert.assertEquals("027ecc656f4b91b92881b6f07cf876cd2e42b20df7acc4df54fc3315fbb2d13e1c", Utils.bytesToHex(extendedKey.getKey(derivation).getPubKey())); Assert.assertEquals("027ecc656f4b91b92881b6f07cf876cd2e42b20df7acc4df54fc3315fbb2d13e1c", Utils.bytesToHex(extendedKey.getKey(derivation).getPubKey()));
Assert.assertEquals("bc1qarzeu6ncapyvjzdeayjq8vnzp6uvcn4eaeuuqq", wallet.getAddress(new WalletNode(KeyPurpose.RECEIVE, 0)).toString()); WalletNode receive0 = new WalletNode(wallet, KeyPurpose.RECEIVE, 0);
Assert.assertEquals("bc1qarzeu6ncapyvjzdeayjq8vnzp6uvcn4eaeuuqq", receive0.getAddress().toString());
} }
} }