mirror of
https://github.com/sparrowwallet/drongo.git
synced 2024-11-02 18:26:43 +00:00
support nested wallets
This commit is contained in:
parent
956f59880e
commit
0734757a17
12 changed files with 352 additions and 107 deletions
|
@ -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) {
|
||||
|
|
|
@ -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<Map.Entry<BlockTransactionHashIndex, WalletNode>> iter = walletTransaction.getSelectedUtxos().entrySet().iterator(); iter.hasNext(); inputIndex++) {
|
||||
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();
|
||||
TransactionOutput utxoOutput = utxo.getOutputs().get(utxoIndex);
|
||||
|
||||
|
@ -112,17 +117,16 @@ public class PSBT {
|
|||
|
||||
Map<ECKey, KeyDerivation> 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<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()));
|
||||
}
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<BlockTransactionHashIndex> 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;
|
||||
}
|
||||
|
|
|
@ -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<Collection<BlockTransactionHashIndex>> selectSets(long targetValue, Collection<OutputGroup> candidates) {
|
||||
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++) {
|
||||
List<OutputGroup> randomized = new ArrayList<>(candidates);
|
||||
Collections.shuffle(randomized, random);
|
||||
|
|
|
@ -117,6 +117,7 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
|||
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<Wallet> {
|
|||
|
||||
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<Wallet> {
|
|||
List<Wallet> 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<Wallet> {
|
|||
Address notificationAddress = externalPaymentCode.getNotificationAddress();
|
||||
for(Map.Entry<BlockTransactionHashIndex, WalletNode> 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<Wallet> {
|
|||
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<Wallet> {
|
|||
|
||||
Set<StandardAccount> 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<Wallet> {
|
|||
WalletNode purposeNode;
|
||||
Optional<WalletNode> 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<Wallet> {
|
|||
|
||||
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<ECKey> pubKeys = getPubKeys(node);
|
||||
List<ECKey> 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<Wallet> {
|
|||
|
||||
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<ECKey> pubKeys = getPubKeys(node);
|
||||
List<ECKey> 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<Wallet> {
|
|||
|
||||
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<ECKey> pubKeys = getPubKeys(node);
|
||||
List<ECKey> 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<Wallet> {
|
|||
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<Address, WalletNode> 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<Wallet> {
|
|||
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<Script, WalletNode> 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<Wallet> {
|
|||
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<Wallet> {
|
|||
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<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.
|
||||
*
|
||||
|
@ -828,15 +922,15 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
|||
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<ECKey> pubKeys = getPubKeys(receiveNode);
|
||||
List<ECKey> pubKeys = receiveNode.getPubKeys();
|
||||
int threshold = getDefaultPolicy().getNumSignaturesRequired();
|
||||
Map<ECKey, TransactionSignature> 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<Wallet> {
|
|||
|
||||
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<Wallet> {
|
|||
|
||||
//Add inputs
|
||||
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());
|
||||
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) {
|
||||
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<Wallet> {
|
|||
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<Wallet> {
|
|||
Map<WalletNode, Long> 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<Wallet> {
|
|||
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<ECKey> 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<ECKey> pubKeys = walletNode.getPubKeys();
|
||||
int threshold = signingWallet.getDefaultPolicy().getNumSignaturesRequired();
|
||||
Map<ECKey, TransactionSignature> 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<Wallet> {
|
|||
|
||||
private List<OutputGroup> getGroupedUtxos(List<UtxoFilter> utxoFilters, double feeRate, double longTermFeeRate, boolean groupByAddress, boolean includeSpentMempoolOutputs) {
|
||||
List<OutputGroup> outputGroups = new ArrayList<>();
|
||||
Map<Sha256Hash, BlockTransaction> 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<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()) {
|
||||
OutputGroup outputGroup = null;
|
||||
for(BlockTransactionHashIndex utxo : addressNode.getUnspentTransactionOutputs(includeSpentMempoolOutputs)) {
|
||||
|
@ -1121,11 +1225,11 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
|||
}
|
||||
|
||||
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<Wallet> {
|
|||
* @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<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) {
|
||||
//Provided txId was not a wallet transaction
|
||||
return false;
|
||||
|
@ -1145,7 +1254,7 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
|||
|
||||
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<Wallet> {
|
|||
*/
|
||||
public long getMaxSpendable(List<Address> 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<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) {
|
||||
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<Wallet> {
|
|||
Map<Script, WalletNode> 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<Wallet> {
|
|||
|
||||
for(TransactionInput txInput : signingNodes.keySet()) {
|
||||
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); },
|
||||
LinkedHashMap::new));
|
||||
|
||||
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()) {
|
||||
TransactionOutput spentTxo = blockTransaction.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex());
|
||||
|
||||
Script signingScript = getSigningScript(txInput, spentTxo);
|
||||
Sha256Hash hash;
|
||||
if(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());
|
||||
if(signingWallet.getScriptType() == P2TR) {
|
||||
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);
|
||||
} 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<Wallet> {
|
|||
|
||||
for(PSBTInput psbtInput : signingNodes.keySet()) {
|
||||
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); },
|
||||
LinkedHashMap::new));
|
||||
|
||||
|
@ -1362,10 +1475,11 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
|||
|
||||
public void sign(PSBT psbt) throws MnemonicException {
|
||||
Map<PSBTInput, WalletNode> signingNodes = getSigningNodes(psbt);
|
||||
for(Keystore keystore : getKeystores()) {
|
||||
if(keystore.hasPrivateKey()) {
|
||||
for(Map.Entry<PSBTInput, WalletNode> signingEntry : signingNodes.entrySet()) {
|
||||
ECKey privKey = getScriptType().getOutputKey(keystore.getKey(signingEntry.getValue()));
|
||||
for(Map.Entry<PSBTInput, WalletNode> 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<Wallet> {
|
|||
|
||||
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<ECKey> pubKeys = getPubKeys(signingNode);
|
||||
List<ECKey> pubKeys = signingNode.getPubKeys();
|
||||
|
||||
Map<ECKey, TransactionSignature> pubKeySignatures = new TreeMap<>(new ECKey.LexicographicECKeyComparator());
|
||||
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");
|
||||
}
|
||||
|
||||
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<Wallet> {
|
|||
|
||||
transactions.clear();
|
||||
storedBlockHeight = 0;
|
||||
|
||||
for(Wallet childWallet : getChildWallets()) {
|
||||
if(childWallet.isNested()) {
|
||||
childWallet.clearHistory();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 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<Wallet> {
|
|||
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<Wallet> {
|
|||
}
|
||||
}
|
||||
|
||||
for(Wallet childWallet : getChildWallets()) {
|
||||
if(childWallet.isNested() && childWallet.isEncrypted()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -1691,24 +1817,48 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
|||
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
|
||||
|
|
|
@ -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<WalletNode> {
|
|||
private TreeSet<WalletNode> children = new TreeSet<>();
|
||||
private TreeSet<BlockTransactionHashIndex> transactionOutputs = new TreeSet<>();
|
||||
|
||||
private transient Wallet wallet;
|
||||
private transient KeyPurpose keyPurpose;
|
||||
private transient int index = -1;
|
||||
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) {
|
||||
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<WalletNode> {
|
|||
Set<WalletNode> 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<WalletNode> {
|
|||
public synchronized Set<WalletNode> fillToIndex(int index) {
|
||||
Set<WalletNode> 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<WalletNode> {
|
|||
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
|
||||
public String toString() {
|
||||
return derivationPath.replace("m", "..");
|
||||
|
@ -200,12 +256,12 @@ public class WalletNode extends Persistable implements Comparable<WalletNode> {
|
|||
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<WalletNode> {
|
|||
}
|
||||
}
|
||||
|
||||
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()) {
|
||||
|
|
|
@ -79,7 +79,7 @@ public class WalletTransaction {
|
|||
}
|
||||
|
||||
public Address getChangeAddress(WalletNode changeNode) {
|
||||
return getWallet().getAddress(changeNode);
|
||||
return changeNode.getAddress();
|
||||
}
|
||||
|
||||
public long getFee() {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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<ChildNumber> 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());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue