From 0734757a177627600a63cb3347804ea126b0d417 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Wed, 2 Mar 2022 13:34:10 +0200 Subject: [PATCH] support nested wallets --- .../drongo/bip47/PaymentCode.java | 4 + .../com/sparrowwallet/drongo/psbt/PSBT.java | 26 +- .../sparrowwallet/drongo/psbt/PSBTInput.java | 6 +- .../drongo/wallet/CoinbaseUtxoFilter.java | 2 +- .../drongo/wallet/OutputGroup.java | 10 +- .../drongo/wallet/StonewallUtxoSelector.java | 16 +- .../sparrowwallet/drongo/wallet/Wallet.java | 266 ++++++++++++++---- .../drongo/wallet/WalletNode.java | 74 ++++- .../drongo/wallet/WalletTransaction.java | 2 +- .../drongo/bip47/PaymentCodeTest.java | 12 +- .../drongo/crypto/ECKeyTest.java | 2 +- .../drongo/wallet/WalletTest.java | 39 ++- 12 files changed, 352 insertions(+), 107 deletions(-) diff --git a/src/main/java/com/sparrowwallet/drongo/bip47/PaymentCode.java b/src/main/java/com/sparrowwallet/drongo/bip47/PaymentCode.java index 2cd9579..c2a2966 100644 --- a/src/main/java/com/sparrowwallet/drongo/bip47/PaymentCode.java +++ b/src/main/java/com/sparrowwallet/drongo/bip47/PaymentCode.java @@ -361,6 +361,10 @@ public class PaymentCode { return new PaymentCode(strPaymentCode, pubkey, chain); } + public String toAbbreviatedString() { + return strPaymentCode.substring(0, 8) + "..." + strPaymentCode.substring(strPaymentCode.length() - 3); + } + @Override public boolean equals(Object o) { if(this == o) { diff --git a/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java b/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java index 51c6f39..cf97b56 100644 --- a/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java +++ b/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java @@ -18,6 +18,7 @@ import java.util.*; import static com.sparrowwallet.drongo.psbt.PSBTEntry.*; import static com.sparrowwallet.drongo.psbt.PSBTInput.*; import static com.sparrowwallet.drongo.psbt.PSBTOutput.*; +import static com.sparrowwallet.drongo.wallet.Wallet.addDummySpendingInput; public class PSBT { public static final byte PSBT_GLOBAL_UNSIGNED_TX = 0x00; @@ -89,12 +90,16 @@ public class PSBT { this.version = version; } - boolean alwaysIncludeWitnessUtxo = wallet.getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().alwaysIncludeNonWitnessUtxo()); - int inputIndex = 0; for(Iterator> iter = walletTransaction.getSelectedUtxos().entrySet().iterator(); iter.hasNext(); inputIndex++) { Map.Entry 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(); TransactionOutput utxoOutput = utxo.getOutputs().get(utxoIndex); @@ -112,17 +117,16 @@ public class PSBT { Map derivedPublicKeys = new LinkedHashMap<>(); ECKey tapInternalKey = null; - for(Keystore keystore : wallet.getKeystores()) { - WalletNode walletNode = utxoEntry.getValue(); - derivedPublicKeys.put(wallet.getScriptType().getOutputKey(keystore.getPubKey(walletNode)), keystore.getKeyDerivation().extend(walletNode.getDerivation())); + for(Keystore keystore : signingWallet.getKeystores()) { + derivedPublicKeys.put(signingWallet.getScriptType().getOutputKey(keystore.getPubKey(walletNode)), keystore.getKeyDerivation().extend(walletNode.getDerivation())); //TODO: Implement Musig for multisig wallets - if(wallet.getScriptType() == ScriptType.P2TR) { + if(signingWallet.getScriptType() == ScriptType.P2TR) { 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); } @@ -132,7 +136,7 @@ public class PSBT { Address address = txOutput.getScript().getToAddresses()[0]; if(walletTransaction.getPayments().stream().anyMatch(payment -> payment.getAddress().equals(address))) { 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)); } } catch(NonStandardScriptException e) { @@ -151,7 +155,7 @@ public class PSBT { //Construct dummy transaction to spend the UTXO created by this wallet's txOutput Transaction transaction = new Transaction(); - TransactionInput spendingInput = wallet.addDummySpendingInput(transaction, outputNode, txOutput); + TransactionInput spendingInput = addDummySpendingInput(transaction, outputNode, txOutput); Script redeemScript = null; if(ScriptType.P2SH.isScriptType(txOutput.getScript())) { @@ -164,7 +168,7 @@ public class PSBT { } Map derivedPublicKeys = new LinkedHashMap<>(); - for(Keystore keystore : wallet.getKeystores()) { + for(Keystore keystore : outputNode.getWallet().getKeystores()) { derivedPublicKeys.put(keystore.getPubKey(outputNode), keystore.getKeyDerivation().extend(outputNode.getDerivation())); } diff --git a/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java b/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java index 4d89317..7b67a63 100644 --- a/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java +++ b/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java @@ -102,11 +102,7 @@ public class PSBTInput { log.debug(" Transaction input references txid: " + input.getOutpoint().getHash() + " vout " + input.getOutpoint().getIndex() + " with script " + input.getScriptSig()); } for(TransactionOutput output: nonWitnessTx.getOutputs()) { - try { - 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); - } + 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()); } break; case PSBT_IN_WITNESS_UTXO: diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/CoinbaseUtxoFilter.java b/src/main/java/com/sparrowwallet/drongo/wallet/CoinbaseUtxoFilter.java index 92051c8..8f079f6 100644 --- a/src/main/java/com/sparrowwallet/drongo/wallet/CoinbaseUtxoFilter.java +++ b/src/main/java/com/sparrowwallet/drongo/wallet/CoinbaseUtxoFilter.java @@ -12,7 +12,7 @@ public class CoinbaseUtxoFilter implements UtxoFilter { @Override public boolean isEligible(BlockTransactionHashIndex candidate) { //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() && wallet.getStoredBlockHeight() != null && candidate.getConfirmations(wallet.getStoredBlockHeight()) < Transaction.COINBASE_MATURITY_THRESHOLD) { return false; diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/OutputGroup.java b/src/main/java/com/sparrowwallet/drongo/wallet/OutputGroup.java index 42f74d9..b66e928 100644 --- a/src/main/java/com/sparrowwallet/drongo/wallet/OutputGroup.java +++ b/src/main/java/com/sparrowwallet/drongo/wallet/OutputGroup.java @@ -1,5 +1,7 @@ package com.sparrowwallet.drongo.wallet; +import com.sparrowwallet.drongo.protocol.ScriptType; + import java.util.ArrayList; import java.util.List; @@ -7,6 +9,7 @@ import static com.sparrowwallet.drongo.protocol.Transaction.WITNESS_SCALE_FACTOR public class OutputGroup { private final List utxos = new ArrayList<>(); + private final ScriptType scriptType; private final int walletBlockHeight; private final long inputWeightUnits; private final double feeRate; @@ -18,7 +21,8 @@ public class OutputGroup { private int depth = Integer.MAX_VALUE; 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.inputWeightUnits = inputWeightUnits; this.feeRate = feeRate; @@ -48,6 +52,10 @@ public class OutputGroup { return utxos; } + public ScriptType getScriptType() { + return scriptType; + } + public long getValue() { return value; } diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/StonewallUtxoSelector.java b/src/main/java/com/sparrowwallet/drongo/wallet/StonewallUtxoSelector.java index 7ef194d..64b2f64 100644 --- a/src/main/java/com/sparrowwallet/drongo/wallet/StonewallUtxoSelector.java +++ b/src/main/java/com/sparrowwallet/drongo/wallet/StonewallUtxoSelector.java @@ -1,15 +1,19 @@ package com.sparrowwallet.drongo.wallet; +import com.sparrowwallet.drongo.protocol.ScriptType; + import java.util.*; import java.util.stream.Collectors; public class StonewallUtxoSelector implements UtxoSelector { + private final ScriptType preferredScriptType; private final long noInputsFee; //Use the same seed so the UTXO selection is deterministic private final Random random = new Random(42); - public StonewallUtxoSelector(long noInputsFee) { + public StonewallUtxoSelector(ScriptType preferredScriptType, long noInputsFee) { + this.preferredScriptType = preferredScriptType; this.noInputsFee = noInputsFee; } @@ -17,6 +21,16 @@ public class StonewallUtxoSelector implements UtxoSelector { public List> selectSets(long targetValue, Collection candidates) { long actualTargetValue = targetValue + noInputsFee; + List preferredCandidates = candidates.stream().filter(outputGroup -> outputGroup.getScriptType().equals(preferredScriptType)).collect(Collectors.toList()); + List> preferredSets = selectSets(targetValue, preferredCandidates, actualTargetValue); + if(!preferredSets.isEmpty()) { + return preferredSets; + } + + return selectSets(targetValue, candidates, actualTargetValue); + } + + private List> selectSets(long targetValue, Collection candidates, long actualTargetValue) { for(int i = 0; i < 10; i++) { List randomized = new ArrayList<>(candidates); Collections.shuffle(randomized, random); diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java b/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java index 6e2ea79..584a0d0 100644 --- a/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java +++ b/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java @@ -117,6 +117,7 @@ public class Wallet extends Persistable implements Comparable { childWallet.purposeNodes.clear(); childWallet.transactions.clear(); childWallet.detachedLabels.clear(); + childWallet.childWallets.clear(); childWallet.storedBlockHeight = null; childWallet.gapLimit = standardAccount.getMinimumGapLimit(); childWallet.birthDate = null; @@ -156,9 +157,11 @@ public class Wallet extends Persistable implements Comparable { public Wallet getChildWallet(StandardAccount account) { for(Wallet childWallet : getChildWallets()) { - for(Keystore keystore : childWallet.getKeystores()) { - if(keystore.getKeyDerivation().getDerivation().get(keystore.getKeyDerivation().getDerivation().size() - 1).equals(account.getChildNumber())) { - return childWallet; + if(!childWallet.isNested()) { + for(Keystore keystore : childWallet.getKeystores()) { + if(keystore.getKeyDerivation().getDerivation().get(keystore.getKeyDerivation().getDerivation().size() - 1).equals(account.getChildNumber())) { + return childWallet; + } } } } @@ -229,7 +232,12 @@ public class Wallet extends Persistable implements Comparable { List allWallets = new ArrayList<>(); Wallet masterWallet = isMasterWallet() ? this : getMasterWallet(); allWallets.add(masterWallet); - allWallets.addAll(masterWallet.getChildWallets()); + for(Wallet childWallet : getChildWallets()) { + if(!childWallet.isNested()) { + allWallets.add(childWallet); + } + } + return allWallets; } @@ -274,7 +282,7 @@ public class Wallet extends Persistable implements Comparable { Address notificationAddress = externalPaymentCode.getNotificationAddress(); for(Map.Entry txoEntry : getWalletTxos().entrySet()) { if(txoEntry.getKey().isSpent()) { - BlockTransaction blockTransaction = transactions.get(txoEntry.getKey().getSpentBy().getHash()); + BlockTransaction blockTransaction = getWalletTransaction(txoEntry.getKey().getSpentBy().getHash()); if(blockTransaction != null) { for(TransactionOutput txOutput : blockTransaction.getTransaction().getOutputs()) { if(notificationAddress.equals(txOutput.getScript().getToAddress())) { @@ -293,6 +301,10 @@ public class Wallet extends Persistable implements Comparable { return Collections.emptyMap(); } + public boolean isNested() { + return isBip47(); + } + public boolean isBip47() { return !isMasterWallet() && getKeystores().size() == 1 && getKeystores().get(0).getSource() == KeystoreSource.SW_PAYMENT_CODE; } @@ -329,7 +341,9 @@ public class Wallet extends Persistable implements Comparable { Set whirlpoolAccounts = new HashSet<>(Set.of(StandardAccount.WHIRLPOOL_PREMIX, StandardAccount.WHIRLPOOL_POSTMIX, StandardAccount.WHIRLPOOL_BADBANK)); for(Wallet childWallet : getChildWallets()) { - whirlpoolAccounts.remove(childWallet.getStandardAccountType()); + if(!childWallet.isNested()) { + whirlpoolAccounts.remove(childWallet.getStandardAccountType()); + } } return whirlpoolAccounts.isEmpty(); @@ -525,7 +539,7 @@ public class Wallet extends Persistable implements Comparable { WalletNode purposeNode; Optional optionalPurposeNode = purposeNodes.stream().filter(node -> node.getKeyPurpose().equals(keyPurpose)).findFirst(); if(optionalPurposeNode.isEmpty()) { - purposeNode = new WalletNode(keyPurpose); + purposeNode = new WalletNode(this, keyPurpose); purposeNodes.add(purposeNode); } else { purposeNode = optionalPurposeNode.get(); @@ -598,10 +612,10 @@ public class Wallet extends Persistable implements Comparable { public Address getAddress(WalletNode node) { if(policyType == PolicyType.SINGLE) { - ECKey pubKey = getPubKey(node); + ECKey pubKey = node.getPubKey(); return scriptType.getAddress(pubKey); } else if(policyType == PolicyType.MULTI) { - List pubKeys = getPubKeys(node); + List pubKeys = node.getPubKeys(); Script script = ScriptType.MULTISIG.getOutputScript(defaultPolicy.getNumSignaturesRequired(), pubKeys); return scriptType.getAddress(script); } else { @@ -611,10 +625,10 @@ public class Wallet extends Persistable implements Comparable { public Script getOutputScript(WalletNode node) { if(policyType == PolicyType.SINGLE) { - ECKey pubKey = getPubKey(node); + ECKey pubKey = node.getPubKey(); return scriptType.getOutputScript(pubKey); } else if(policyType == PolicyType.MULTI) { - List pubKeys = getPubKeys(node); + List pubKeys = node.getPubKeys(); Script script = ScriptType.MULTISIG.getOutputScript(defaultPolicy.getNumSignaturesRequired(), pubKeys); return scriptType.getOutputScript(script); } else { @@ -624,10 +638,10 @@ public class Wallet extends Persistable implements Comparable { public String getOutputDescriptor(WalletNode node) { if(policyType == PolicyType.SINGLE) { - ECKey pubKey = getPubKey(node); + ECKey pubKey = node.getPubKey(); return scriptType.getOutputDescriptor(pubKey); } else if(policyType == PolicyType.MULTI) { - List pubKeys = getPubKeys(node); + List pubKeys = node.getPubKeys(); Script script = ScriptType.MULTISIG.getOutputScript(defaultPolicy.getNumSignaturesRequired(), pubKeys); return scriptType.getOutputDescriptor(script); } else { @@ -662,12 +676,20 @@ public class Wallet extends Persistable implements Comparable { getWalletAddresses(walletAddresses, getNode(keyPurpose)); } + for(Wallet childWallet : getChildWallets()) { + if(childWallet.isNested()) { + for(KeyPurpose keyPurpose : childWallet.getWalletKeyPurposes()) { + getWalletAddresses(walletAddresses, childWallet.getNode(keyPurpose)); + } + } + } + return walletAddresses; } private void getWalletAddresses(Map walletAddresses, WalletNode purposeNode) { 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 { for(KeyPurpose keyPurpose : keyPurposes) { 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; } private void getWalletOutputScripts(Map walletOutputScripts, WalletNode purposeNode) { 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 { getWalletTxos(walletTxos, getNode(keyPurpose)); } + for(Wallet childWallet : getChildWallets()) { + if(childWallet.isNested()) { + for(KeyPurpose keyPurpose : childWallet.getWalletKeyPurposes()) { + getWalletTxos(walletTxos, childWallet.getNode(keyPurpose)); + } + } + } + return walletTxos; } @@ -740,6 +781,14 @@ public class Wallet extends Persistable implements Comparable { 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; } @@ -751,6 +800,51 @@ public class Wallet extends Persistable implements Comparable { } } + 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 getWalletTransactions() { + Map 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. * @@ -828,15 +922,15 @@ public class Wallet extends Persistable implements Comparable { WalletNode receiveNode = getFreshNode(KeyPurpose.RECEIVE); Transaction transaction = new Transaction(); - TransactionOutput prevTxOut = transaction.addOutput(1L, getAddress(receiveNode)); + TransactionOutput prevTxOut = transaction.addOutput(1L, receiveNode.getAddress()); TransactionInput txInput = null; if(getPolicyType().equals(PolicyType.SINGLE)) { - ECKey pubKey = getPubKey(receiveNode); + ECKey pubKey = receiveNode.getPubKey(); TransactionSignature signature = TransactionSignature.dummy(getScriptType().getSignatureType()); txInput = getScriptType().addSpendingInput(transaction, prevTxOut, pubKey, signature); } else if(getPolicyType().equals(PolicyType.MULTI)) { - List pubKeys = getPubKeys(receiveNode); + List pubKeys = receiveNode.getPubKeys(); int threshold = getDefaultPolicy().getNumSignaturesRequired(); Map pubKeySignatures = new TreeMap<>(new ECKey.LexicographicECKeyComparator()); for(int i = 0; i < pubKeys.size(); i++) { @@ -856,7 +950,7 @@ public class Wallet extends Persistable implements Comparable { public long getCostOfChange(double feeRate, double longTermFeeRate) { 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); } @@ -898,7 +992,7 @@ public class Wallet extends Persistable implements Comparable { //Add inputs for(Map.Entry 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()); TransactionInput txInput = addDummySpendingInput(transaction, selectedUtxo.getValue(), prevTxOut); @@ -913,7 +1007,7 @@ public class Wallet extends Persistable implements Comparable { for(int i = 1; i < numSets; i+=2) { WalletNode mixNode = getFreshNode(getChangeKeyPurpose()); 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); txPayments.add(fakeMixPayment); } @@ -972,7 +1066,7 @@ public class Wallet extends Persistable implements Comparable { while(txExcludedChangeNodes.contains(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; long changeFeeRequiredAmt = (fee == null ? (long)Math.floor(feeRate * changeVSize) : fee); changeFeeRequiredAmt = (fee == null && feeRate == Transaction.DEFAULT_MIN_RELAY_FEE ? changeFeeRequiredAmt + 1 : changeFeeRequiredAmt); @@ -984,7 +1078,7 @@ public class Wallet extends Persistable implements Comparable { Map changeMap = new LinkedHashMap<>(); setChangeAmts = getSetChangeAmounts(selectedUtxoSets, totalPaymentAmount, changeFeeRequiredAmt); for(Long setChangeAmt : setChangeAmts) { - transaction.addOutput(setChangeAmt, getOutputScript(changeNode)); + transaction.addOutput(setChangeAmt, changeNode.getOutputScript()); changeMap.put(changeNode, setChangeAmt); changeNode = getFreshNode(getChangeKeyPurpose(), changeNode); } @@ -1041,20 +1135,21 @@ public class Wallet extends Persistable implements Comparable { return changeAmts; } - public TransactionInput addDummySpendingInput(Transaction transaction, WalletNode walletNode, TransactionOutput prevTxOut) { - if(getPolicyType().equals(PolicyType.SINGLE)) { - ECKey pubKey = getPubKey(walletNode); - return getScriptType().addSpendingInput(transaction, prevTxOut, pubKey, TransactionSignature.dummy(getScriptType().getSignatureType())); - } else if(getPolicyType().equals(PolicyType.MULTI)) { - List pubKeys = getPubKeys(walletNode); - int threshold = getDefaultPolicy().getNumSignaturesRequired(); + public static TransactionInput addDummySpendingInput(Transaction transaction, WalletNode walletNode, TransactionOutput prevTxOut) { + Wallet signingWallet = walletNode.getWallet(); + if(signingWallet.getPolicyType().equals(PolicyType.SINGLE)) { + ECKey pubKey = walletNode.getPubKey(); + return signingWallet.getScriptType().addSpendingInput(transaction, prevTxOut, pubKey, TransactionSignature.dummy(signingWallet.getScriptType().getSignatureType())); + } else if(signingWallet.getPolicyType().equals(PolicyType.MULTI)) { + List pubKeys = walletNode.getPubKeys(); + int threshold = signingWallet.getDefaultPolicy().getNumSignaturesRequired(); Map pubKeySignatures = new TreeMap<>(new ECKey.LexicographicECKeyComparator()); 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 { - 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 { private List getGroupedUtxos(List utxoFilters, double feeRate, double longTermFeeRate, boolean groupByAddress, boolean includeSpentMempoolOutputs) { List outputGroups = new ArrayList<>(); + Map walletTransactions = getWalletTransactions(); 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; } - private void getGroupedUtxos(List outputGroups, WalletNode purposeNode, List utxoFilters, double feeRate, double longTermFeeRate, boolean groupByAddress, boolean includeSpentMempoolOutputs) { + private void getGroupedUtxos(List outputGroups, WalletNode purposeNode, List utxoFilters, Map walletTransactions, double feeRate, double longTermFeeRate, boolean groupByAddress, boolean includeSpentMempoolOutputs) { for(WalletNode addressNode : purposeNode.getChildren()) { OutputGroup outputGroup = null; for(BlockTransactionHashIndex utxo : addressNode.getUnspentTransactionOutputs(includeSpentMempoolOutputs)) { @@ -1121,11 +1225,11 @@ public class Wallet extends Persistable implements Comparable { } if(outputGroup == null || !groupByAddress) { - outputGroup = new OutputGroup(getStoredBlockHeight(), getInputWeightUnits(), feeRate, longTermFeeRate); + outputGroup = new OutputGroup(addressNode.getWallet().getScriptType(), getStoredBlockHeight(), getInputWeightUnits(), feeRate, longTermFeeRate); 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 { * @return Whether the transaction was created entirely from inputs that reference outputs that belong to this wallet */ public boolean allInputsFromWallet(Sha256Hash txId) { - BlockTransaction utxoBlkTx = getTransactions().get(txId); + Map allTransactions = getWalletTransactions(); + return allInputsFromWallet(allTransactions, txId); + } + + private boolean allInputsFromWallet(Map walletTransactions, Sha256Hash txId) { + BlockTransaction utxoBlkTx = walletTransactions.get(txId); if(utxoBlkTx == null) { //Provided txId was not a wallet transaction return false; @@ -1145,7 +1254,7 @@ public class Wallet extends Persistable implements Comparable { for(int i = 0; i < utxoBlkTx.getTransaction().getInputs().size(); 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) { return false; } @@ -1171,13 +1280,13 @@ public class Wallet extends Persistable implements Comparable { */ public long getMaxSpendable(List
paymentAddresses, double feeRate) { long maxInputValue = 0; - int inputWeightUnits = getInputWeightUnits(); - long minInputValue = (long)Math.ceil(feeRate * inputWeightUnits / WITNESS_SCALE_FACTOR); Transaction transaction = new Transaction(); for(Map.Entry utxo : getWalletUtxos().entrySet()) { + int inputWeightUnits = utxo.getValue().getWallet().getInputWeightUnits(); + long minInputValue = (long)Math.ceil(feeRate * inputWeightUnits / WITNESS_SCALE_FACTOR); 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()); addDummySpendingInput(transaction, utxo.getValue(), prevTxOut); maxInputValue += utxo.getKey().getValue(); @@ -1207,7 +1316,7 @@ public class Wallet extends Persistable implements Comparable { Map walletOutputScripts = getWalletOutputScripts(); 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()) { TransactionOutput utxo = blockTransaction.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex()); @@ -1236,20 +1345,22 @@ public class Wallet extends Persistable implements Comparable { for(TransactionInput txInput : signingNodes.keySet()) { WalletNode walletNode = signingNodes.get(txInput); - Map keystoreKeysForNode = getKeystores().stream().collect(Collectors.toMap(keystore -> getScriptType().getOutputKey(keystore.getPubKey(walletNode)), Function.identity(), + Wallet signingWallet = walletNode.getWallet(); + Map 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); }, LinkedHashMap::new)); Map 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()) { TransactionOutput spentTxo = blockTransaction.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex()); Script signingScript = getSigningScript(txInput, spentTxo); Sha256Hash hash; - if(getScriptType() == P2TR) { - List spentOutputs = transaction.getInputs().stream().map(input -> transactions.get(input.getOutpoint().getHash()).getTransaction().getOutputs().get((int)input.getOutpoint().getIndex())).collect(Collectors.toList()); + if(signingWallet.getScriptType() == P2TR) { + List 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); } else if(txInput.hasWitness()) { hash = transaction.hashForWitnessSignature(txInput.getIndex(), signingScript, spentTxo.getValue(), SigHash.ALL); @@ -1336,7 +1447,9 @@ public class Wallet extends Persistable implements Comparable { for(PSBTInput psbtInput : signingNodes.keySet()) { WalletNode walletNode = signingNodes.get(psbtInput); - Map keystoreKeysForNode = getKeystores().stream().collect(Collectors.toMap(keystore -> getScriptType().getOutputKey(keystore.getPubKey(walletNode)), Function.identity(), + Wallet signingWallet = walletNode.getWallet(); + Map 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); }, LinkedHashMap::new)); @@ -1362,10 +1475,11 @@ public class Wallet extends Persistable implements Comparable { public void sign(PSBT psbt) throws MnemonicException { Map signingNodes = getSigningNodes(psbt); - for(Keystore keystore : getKeystores()) { - if(keystore.hasPrivateKey()) { - for(Map.Entry signingEntry : signingNodes.entrySet()) { - ECKey privKey = getScriptType().getOutputKey(keystore.getKey(signingEntry.getValue())); + for(Map.Entry signingEntry : signingNodes.entrySet()) { + 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(); if(!psbtInput.isSigned()) { @@ -1410,15 +1524,15 @@ public class Wallet extends Persistable implements Comparable { TransactionInput finalizedTxInput; if(getPolicyType().equals(PolicyType.SINGLE)) { - ECKey pubKey = getPubKey(signingNode); + ECKey pubKey = signingNode.getPubKey(); TransactionSignature transactionSignature = psbtInput.isTaproot() ? psbtInput.getTapKeyPathSignature() : psbtInput.getPartialSignature(pubKey); if(transactionSignature == null) { 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)) { - List pubKeys = getPubKeys(signingNode); + List pubKeys = signingNode.getPubKeys(); Map pubKeySignatures = new TreeMap<>(new ECKey.LexicographicECKeyComparator()); for(ECKey pubKey : pubKeys) { @@ -1430,7 +1544,7 @@ public class Wallet extends Persistable implements Comparable { 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 { throw new UnsupportedOperationException("Cannot finalise PSBT for policy type " + getPolicyType()); } @@ -1471,6 +1585,12 @@ public class Wallet extends Persistable implements Comparable { transactions.clear(); storedBlockHeight = 0; + + for(Wallet childWallet : getChildWallets()) { + if(childWallet.isNested()) { + childWallet.clearHistory(); + } + } } private Map getDetachedLabels(boolean includeAddresses) { @@ -1484,7 +1604,7 @@ public class Wallet extends Persistable implements Comparable { for(WalletNode purposeNode : purposeNodes) { for(WalletNode addressNode : purposeNode.getChildren()) { 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()) { @@ -1637,7 +1757,7 @@ public class Wallet extends Persistable implements Comparable { copy.getKeystores().add(keystore.copy()); } for(WalletNode node : purposeNodes) { - copy.purposeNodes.add(node.copy()); + copy.purposeNodes.add(node.copy(copy)); } for(Sha256Hash hash : transactions.keySet()) { copy.transactions.put(hash, transactions.get(hash)); @@ -1684,6 +1804,12 @@ public class Wallet extends Persistable implements Comparable { } } + for(Wallet childWallet : getChildWallets()) { + if(childWallet.isNested() && childWallet.isEncrypted()) { + return true; + } + } + return false; } @@ -1691,24 +1817,48 @@ public class Wallet extends Persistable implements Comparable { for(Keystore keystore : keystores) { keystore.encrypt(key); } + + for(Wallet childWallet : getChildWallets()) { + if(childWallet.isNested()) { + childWallet.encrypt(key); + } + } } public void decrypt(CharSequence password) { for(Keystore keystore : keystores) { keystore.decrypt(password); } + + for(Wallet childWallet : getChildWallets()) { + if(childWallet.isNested()) { + childWallet.decrypt(password); + } + } } public void decrypt(Key key) { for(Keystore keystore : keystores) { keystore.decrypt(key); } + + for(Wallet childWallet : getChildWallets()) { + if(childWallet.isNested()) { + childWallet.decrypt(key); + } + } } public void clearPrivate() { for(Keystore keystore : keystores) { keystore.clearPrivate(); } + + for(Wallet childWallet : getChildWallets()) { + if(childWallet.isNested()) { + childWallet.clearPrivate(); + } + } } @Override diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/WalletNode.java b/src/main/java/com/sparrowwallet/drongo/wallet/WalletNode.java index 2394116..ae1374a 100644 --- a/src/main/java/com/sparrowwallet/drongo/wallet/WalletNode.java +++ b/src/main/java/com/sparrowwallet/drongo/wallet/WalletNode.java @@ -2,7 +2,10 @@ package com.sparrowwallet.drongo.wallet; import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.KeyPurpose; +import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.crypto.ChildNumber; +import com.sparrowwallet.drongo.crypto.ECKey; +import com.sparrowwallet.drongo.protocol.Script; import java.util.*; import java.util.stream.Collectors; @@ -13,29 +16,53 @@ public class WalletNode extends Persistable implements Comparable { private TreeSet children = new TreeSet<>(); private TreeSet transactionOutputs = new TreeSet<>(); + private transient Wallet wallet; private transient KeyPurpose keyPurpose; private transient int index = -1; private transient List 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) { this.derivationPath = derivationPath; 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.derivationPath = KeyDerivation.writePath(derivation); this.keyPurpose = keyPurpose; 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.derivationPath = KeyDerivation.writePath(derivation); this.keyPurpose = keyPurpose; 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() { return derivationPath; } @@ -154,7 +181,7 @@ public class WalletNode extends Persistable implements Comparable { Set newNodes = fillToIndex(index); if(!wallet.getDetachedLabels().isEmpty() && wallet.isValid()) { 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())) { newNode.setLabel(label); } @@ -167,7 +194,7 @@ public class WalletNode extends Persistable implements Comparable { public synchronized Set fillToIndex(int index) { Set newNodes = new TreeSet<>(); for(int i = 0; i <= index; i++) { - WalletNode node = new WalletNode(getKeyPurpose(), i); + WalletNode node = new WalletNode(wallet, getKeyPurpose(), i); if(children.add(node)) { newNodes.add(node); } @@ -190,6 +217,35 @@ public class WalletNode extends Persistable implements Comparable { 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 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 public String toString() { return derivationPath.replace("m", ".."); @@ -200,12 +256,12 @@ public class WalletNode extends Persistable implements Comparable { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; WalletNode node = (WalletNode) o; - return derivationPath.equals(node.derivationPath); + return Objects.equals(wallet, node.wallet) && derivationPath.equals(node.derivationPath); } @Override public int hashCode() { - return derivationPath.hashCode(); + return Objects.hash(wallet, derivationPath); } @Override @@ -232,13 +288,13 @@ public class WalletNode extends Persistable implements Comparable { } } - public WalletNode copy() { - WalletNode copy = new WalletNode(derivationPath); + public WalletNode copy(Wallet walletCopy) { + WalletNode copy = new WalletNode(walletCopy, derivationPath); copy.setId(getId()); copy.setLabel(label); for(WalletNode child : getChildren()) { - copy.children.add(child.copy()); + copy.children.add(child.copy(walletCopy)); } for(BlockTransactionHashIndex txo : getTransactionOutputs()) { diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/WalletTransaction.java b/src/main/java/com/sparrowwallet/drongo/wallet/WalletTransaction.java index 36b0282..8248fdf 100644 --- a/src/main/java/com/sparrowwallet/drongo/wallet/WalletTransaction.java +++ b/src/main/java/com/sparrowwallet/drongo/wallet/WalletTransaction.java @@ -79,7 +79,7 @@ public class WalletTransaction { } public Address getChangeAddress(WalletNode changeNode) { - return getWallet().getAddress(changeNode); + return changeNode.getAddress(); } public long getFee() { diff --git a/src/test/java/com/sparrowwallet/drongo/bip47/PaymentCodeTest.java b/src/test/java/com/sparrowwallet/drongo/bip47/PaymentCodeTest.java index e4cd501..31c05a9 100644 --- a/src/test/java/com/sparrowwallet/drongo/bip47/PaymentCodeTest.java +++ b/src/test/java/com/sparrowwallet/drongo/bip47/PaymentCodeTest.java @@ -126,15 +126,15 @@ public class PaymentCodeTest { Assert.assertEquals("1JDdmqFLhpzcUwPeinhJbUPw4Co3aWLyzW", paymentCodeAlice.getNotificationAddress().toString()); WalletNode sendNode0 = aliceBip47Wallet.getFreshNode(KeyPurpose.SEND); - Address address0 = aliceBip47Wallet.getAddress(sendNode0); + Address address0 = sendNode0.getAddress(); Assert.assertEquals("141fi7TY3h936vRUKh1qfUZr8rSBuYbVBK", address0.toString()); WalletNode sendNode1 = aliceBip47Wallet.getFreshNode(KeyPurpose.SEND, sendNode0); - Address address1 = aliceBip47Wallet.getAddress(sendNode1); + Address address1 = sendNode1.getAddress(); Assert.assertEquals("12u3Uued2fuko2nY4SoSFGCoGLCBUGPkk6", address1.toString()); WalletNode sendNode2 = aliceBip47Wallet.getFreshNode(KeyPurpose.SEND, sendNode1); - Address address2 = aliceBip47Wallet.getAddress(sendNode2); + Address address2 = sendNode2.getAddress(); 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); @@ -149,15 +149,15 @@ public class PaymentCodeTest { Assert.assertEquals("1ChvUUvht2hUQufHBXF8NgLhW8SwE2ecGV", paymentCodeBob.getNotificationAddress().toString()); WalletNode receiveNode0 = bobBip47Wallet.getFreshNode(KeyPurpose.RECEIVE); - Address receiveAddress0 = bobBip47Wallet.getAddress(receiveNode0); + Address receiveAddress0 = receiveNode0.getAddress(); Assert.assertEquals("141fi7TY3h936vRUKh1qfUZr8rSBuYbVBK", receiveAddress0.toString()); WalletNode receiveNode1 = bobBip47Wallet.getFreshNode(KeyPurpose.RECEIVE, receiveNode0); - Address receiveAddress1 = bobBip47Wallet.getAddress(receiveNode1); + Address receiveAddress1 = receiveNode1.getAddress(); Assert.assertEquals("12u3Uued2fuko2nY4SoSFGCoGLCBUGPkk6", receiveAddress1.toString()); WalletNode receiveNode2 = bobBip47Wallet.getFreshNode(KeyPurpose.RECEIVE, receiveNode1); - Address receiveAddress2 = bobBip47Wallet.getAddress(receiveNode2); + Address receiveAddress2 = receiveNode2.getAddress(); Assert.assertEquals("1FsBVhT5dQutGwaPePTYMe5qvYqqjxyftc", receiveAddress2.toString()); ECKey privKey0 = bobWallet.getKeystores().get(0).getKey(receiveNode0); diff --git a/src/test/java/com/sparrowwallet/drongo/crypto/ECKeyTest.java b/src/test/java/com/sparrowwallet/drongo/crypto/ECKeyTest.java index 8fb8808..3897aca 100644 --- a/src/test/java/com/sparrowwallet/drongo/crypto/ECKeyTest.java +++ b/src/test/java/com/sparrowwallet/drongo/crypto/ECKeyTest.java @@ -22,7 +22,7 @@ public class ECKeyTest { wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2PKH, wallet.getKeystores(), 1)); WalletNode firstReceive = wallet.getNode(KeyPurpose.RECEIVE).getChildren().iterator().next(); - Address address = wallet.getAddress(firstReceive); + Address address = firstReceive.getAddress(); Assert.assertEquals("14JmU9a7SzieZNEtBnsZo688rt3mGrw6hr", address.toString()); ECKey privKey = keystore.getKey(firstReceive); diff --git a/src/test/java/com/sparrowwallet/drongo/wallet/WalletTest.java b/src/test/java/com/sparrowwallet/drongo/wallet/WalletTest.java index d9f23e1..2dc3155 100644 --- a/src/test/java/com/sparrowwallet/drongo/wallet/WalletTest.java +++ b/src/test/java/com/sparrowwallet/drongo/wallet/WalletTest.java @@ -104,8 +104,10 @@ public class WalletTest { wallet.getKeystores().add(keystore); wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2PKH, wallet.getKeystores(), 1)); - Assert.assertEquals("12kTQjuWDp7Uu6PwY6CsS1KLTt3d1DBHZa", wallet.getAddress(new WalletNode(KeyPurpose.RECEIVE, 0)).toString()); - Assert.assertEquals("1HbQwQCitHQxVtP39isXmUdHx7hQCZovrK", wallet.getAddress(new WalletNode(KeyPurpose.RECEIVE, 1)).toString()); + WalletNode receive0 = new WalletNode(wallet, KeyPurpose.RECEIVE, 0); + Assert.assertEquals("12kTQjuWDp7Uu6PwY6CsS1KLTt3d1DBHZa", receive0.getAddress().toString()); + WalletNode receive1 = new WalletNode(wallet, KeyPurpose.RECEIVE, 1); + Assert.assertEquals("1HbQwQCitHQxVtP39isXmUdHx7hQCZovrK", receive1.getAddress().toString()); } @Test @@ -119,8 +121,10 @@ public class WalletTest { wallet.getKeystores().add(keystore); wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2SH_P2WPKH, wallet.getKeystores(), 1)); - Assert.assertEquals("3NZLE4TntsjtcZ5MbrfxwtYo9meBVybVQj", wallet.getAddress(new WalletNode(KeyPurpose.RECEIVE, 0)).toString()); - Assert.assertEquals("32YBBuRsp8XTeLx4T6BmD2L4nANGaNDkSg", wallet.getAddress(new WalletNode(KeyPurpose.RECEIVE, 1)).toString()); + WalletNode receive0 = new WalletNode(wallet, KeyPurpose.RECEIVE, 0); + Assert.assertEquals("3NZLE4TntsjtcZ5MbrfxwtYo9meBVybVQj", receive0.getAddress().toString()); + WalletNode receive1 = new WalletNode(wallet, KeyPurpose.RECEIVE, 1); + Assert.assertEquals("32YBBuRsp8XTeLx4T6BmD2L4nANGaNDkSg", receive1.getAddress().toString()); } @Test @@ -134,8 +138,10 @@ public class WalletTest { wallet.getKeystores().add(keystore); wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2WPKH, wallet.getKeystores(), 1)); - Assert.assertEquals("bc1quvxdut936uswuxwxrk6nvjmgwxh463r0fjwn55", wallet.getAddress(new WalletNode(KeyPurpose.RECEIVE, 0)).toString()); - Assert.assertEquals("bc1q95j2862dz7mqpraw6qdjc70gumyu5z7adgq9x9", wallet.getAddress(new WalletNode(KeyPurpose.RECEIVE, 1)).toString()); + WalletNode receive0 = new WalletNode(wallet, KeyPurpose.RECEIVE, 0); + Assert.assertEquals("bc1quvxdut936uswuxwxrk6nvjmgwxh463r0fjwn55", receive0.getAddress().toString()); + WalletNode receive1 = new WalletNode(wallet, KeyPurpose.RECEIVE, 1); + Assert.assertEquals("bc1q95j2862dz7mqpraw6qdjc70gumyu5z7adgq9x9", receive1.getAddress().toString()); } @Test @@ -159,8 +165,10 @@ public class WalletTest { wallet.getKeystores().add(keystore2); wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI, ScriptType.P2SH, wallet.getKeystores(), 2)); - Assert.assertEquals("38kq6yz4VcYymTExQPY3gppbz38mtPLveK", wallet.getAddress(new WalletNode(KeyPurpose.RECEIVE, 0)).toString()); - Assert.assertEquals("3EdKaNsnjBTBggWcSMRyVju6GbHWy68mAH", wallet.getAddress(new WalletNode(KeyPurpose.CHANGE, 1)).toString()); + WalletNode receive0 = new WalletNode(wallet, KeyPurpose.RECEIVE, 0); + Assert.assertEquals("38kq6yz4VcYymTExQPY3gppbz38mtPLveK", receive0.getAddress().toString()); + WalletNode receive1 = new WalletNode(wallet, KeyPurpose.CHANGE, 1); + Assert.assertEquals("3EdKaNsnjBTBggWcSMRyVju6GbHWy68mAH", receive1.getAddress().toString()); } @Test @@ -184,8 +192,10 @@ public class WalletTest { wallet.getKeystores().add(keystore2); wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI, ScriptType.P2SH_P2WSH, wallet.getKeystores(), 2)); - Assert.assertEquals("3Mw8xqAHh8g3eBvh7q1UEUmoexqdXDK9Tf", wallet.getAddress(new WalletNode(KeyPurpose.RECEIVE, 0)).toString()); - Assert.assertEquals("35dFo1ivJ8jyHpyf42MWvnYf5LBU8Siren", wallet.getAddress(new WalletNode(KeyPurpose.CHANGE, 1)).toString()); + WalletNode receive0 = new WalletNode(wallet, KeyPurpose.RECEIVE, 0); + Assert.assertEquals("3Mw8xqAHh8g3eBvh7q1UEUmoexqdXDK9Tf", receive0.getAddress().toString()); + WalletNode receive1 = new WalletNode(wallet, KeyPurpose.CHANGE, 1); + Assert.assertEquals("35dFo1ivJ8jyHpyf42MWvnYf5LBU8Siren", receive1.getAddress().toString()); } @Test @@ -209,8 +219,10 @@ public class WalletTest { wallet.getKeystores().add(keystore2); wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI, ScriptType.P2WSH, wallet.getKeystores(), 2)); - Assert.assertEquals("bc1q20e4vm656h5lvmngz9ztz6hjzftvh39yzngqhuqzk8qzj7tqnzaqgclrwc", wallet.getAddress(new WalletNode(KeyPurpose.RECEIVE, 0)).toString()); - Assert.assertEquals("bc1q2epdx7dplwaas2jucfrzmxm8350rqh68hs6vqreysku80ye44mfqla85f2", wallet.getAddress(new WalletNode(KeyPurpose.CHANGE, 1)).toString()); + WalletNode receive0 = new WalletNode(wallet, KeyPurpose.RECEIVE, 0); + Assert.assertEquals("bc1q20e4vm656h5lvmngz9ztz6hjzftvh39yzngqhuqzk8qzj7tqnzaqgclrwc", receive0.getAddress().toString()); + WalletNode receive1 = new WalletNode(wallet, KeyPurpose.CHANGE, 1); + Assert.assertEquals("bc1q2epdx7dplwaas2jucfrzmxm8350rqh68hs6vqreysku80ye44mfqla85f2", receive1.getAddress().toString()); } @Test @@ -227,6 +239,7 @@ public class WalletTest { List derivation = List.of(keystore.getExtendedPublicKey().getKeyChildNumber(), new ChildNumber(0)); 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()); } }