mirror of
https://github.com/sparrowwallet/drongo.git
synced 2024-12-27 02:26:44 +00:00
tx creation algorithm
This commit is contained in:
parent
3ee7cd11eb
commit
ccf7de9f62
6 changed files with 261 additions and 19 deletions
|
@ -13,6 +13,7 @@ import java.util.stream.Collectors;
|
||||||
import static com.sparrowwallet.drongo.policy.PolicyType.*;
|
import static com.sparrowwallet.drongo.policy.PolicyType.*;
|
||||||
import static com.sparrowwallet.drongo.protocol.Script.decodeFromOpN;
|
import static com.sparrowwallet.drongo.protocol.Script.decodeFromOpN;
|
||||||
import static com.sparrowwallet.drongo.protocol.ScriptOpCodes.*;
|
import static com.sparrowwallet.drongo.protocol.ScriptOpCodes.*;
|
||||||
|
import static com.sparrowwallet.drongo.protocol.Transaction.WITNESS_SCALE_FACTOR;
|
||||||
|
|
||||||
public enum ScriptType {
|
public enum ScriptType {
|
||||||
P2PK("P2PK", "m/44'/0'/0'") {
|
P2PK("P2PK", "m/44'/0'/0'") {
|
||||||
|
@ -1030,6 +1031,10 @@ public enum ScriptType {
|
||||||
|
|
||||||
public static final ScriptType[] SINGLE_HASH_TYPES = {P2PKH, P2SH, P2SH_P2WPKH, P2SH_P2WSH, P2WPKH, P2WSH};
|
public static final ScriptType[] SINGLE_HASH_TYPES = {P2PKH, P2SH, P2SH_P2WPKH, P2SH_P2WSH, P2WPKH, P2WSH};
|
||||||
|
|
||||||
|
public static final ScriptType[] NON_WITNESS_TYPES = {P2PK, P2PKH, P2SH};
|
||||||
|
|
||||||
|
public static final ScriptType[] WITNESS_TYPES = {P2SH_P2WPKH, P2SH_P2WSH, P2WPKH, P2WSH};
|
||||||
|
|
||||||
public static List<ScriptType> getScriptTypesForPolicyType(PolicyType policyType) {
|
public static List<ScriptType> getScriptTypesForPolicyType(PolicyType policyType) {
|
||||||
return Arrays.stream(values()).filter(scriptType -> scriptType.isAllowed(policyType)).collect(Collectors.toList());
|
return Arrays.stream(values()).filter(scriptType -> scriptType.isAllowed(policyType)).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
@ -1044,6 +1049,30 @@ public enum ScriptType {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the dust threshold for this script type.
|
||||||
|
*
|
||||||
|
* @param output The output under consideration
|
||||||
|
* @param feeRate The fee rate at which the fee required will be calculated
|
||||||
|
* @return the minimum viable value than the provided output must have in order to not be dust
|
||||||
|
*/
|
||||||
|
public long getDustThreshold(TransactionOutput output, Double feeRate) {
|
||||||
|
//Start with length of output
|
||||||
|
int totalLength = output.getLength();
|
||||||
|
if(Arrays.asList(WITNESS_TYPES).contains(this)) {
|
||||||
|
//Add length of spending input with 75% discount to script size
|
||||||
|
totalLength += (32 + 4 + 1 + (107 / WITNESS_SCALE_FACTOR) + 4);
|
||||||
|
} else if(Arrays.asList(NON_WITNESS_TYPES).contains(this)) {
|
||||||
|
//Add length of spending input with no discount
|
||||||
|
totalLength += (32 + 4 + 1 + 107 + 4);
|
||||||
|
} else {
|
||||||
|
throw new UnsupportedOperationException("Cannot determine dust threshold for script type " + this.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
//Return fee rate in sats/vbyte multiplied by the calculated total byte length
|
||||||
|
return (long)(feeRate * totalLength);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return name;
|
return name;
|
||||||
|
|
|
@ -21,6 +21,8 @@ public class Transaction extends ChildMessage {
|
||||||
public static final long MAX_BITCOIN = 21 * 1000 * 1000L;
|
public static final long MAX_BITCOIN = 21 * 1000 * 1000L;
|
||||||
public static final long SATOSHIS_PER_BITCOIN = 100 * 1000 * 1000L;
|
public static final long SATOSHIS_PER_BITCOIN = 100 * 1000 * 1000L;
|
||||||
public static final long MAX_BLOCK_LOCKTIME = 500000000L;
|
public static final long MAX_BLOCK_LOCKTIME = 500000000L;
|
||||||
|
public static final int WITNESS_SCALE_FACTOR = 4;
|
||||||
|
public static final double DEFAULT_DISCARD_FEE_RATE = 10000d / 1000;
|
||||||
|
|
||||||
private long version;
|
private long version;
|
||||||
private long locktime;
|
private long locktime;
|
||||||
|
@ -147,6 +149,18 @@ public class Transaction extends ChildMessage {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] bitcoinSerialize() {
|
||||||
|
try {
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
bitcoinSerializeToStream(outputStream);
|
||||||
|
return outputStream.toByteArray();
|
||||||
|
} catch (IOException e) {
|
||||||
|
//can't happen
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public void bitcoinSerializeToStream(OutputStream stream) throws IOException {
|
public void bitcoinSerializeToStream(OutputStream stream) throws IOException {
|
||||||
boolean useSegwit = isSegwit();
|
boolean useSegwit = isSegwit();
|
||||||
bitcoinSerializeToStream(stream, useSegwit);
|
bitcoinSerializeToStream(stream, useSegwit);
|
||||||
|
@ -253,19 +267,19 @@ public class Transaction extends ChildMessage {
|
||||||
int wu = 0;
|
int wu = 0;
|
||||||
|
|
||||||
// version
|
// version
|
||||||
wu += 4*4;
|
wu += 4 * WITNESS_SCALE_FACTOR;
|
||||||
// marker, flag
|
// marker, flag
|
||||||
if(isSegwit()) {
|
if(isSegwit()) {
|
||||||
wu += 2;
|
wu += 2;
|
||||||
}
|
}
|
||||||
// txin_count, txins
|
// txin_count, txins
|
||||||
wu += new VarInt(inputs.size()).getSizeInBytes() * 4;
|
wu += new VarInt(inputs.size()).getSizeInBytes() * WITNESS_SCALE_FACTOR;
|
||||||
for (TransactionInput in : inputs)
|
for (TransactionInput in : inputs)
|
||||||
wu += in.length * 4;
|
wu += in.length * WITNESS_SCALE_FACTOR;
|
||||||
// txout_count, txouts
|
// txout_count, txouts
|
||||||
wu += new VarInt(outputs.size()).getSizeInBytes() * 4;
|
wu += new VarInt(outputs.size()).getSizeInBytes() * WITNESS_SCALE_FACTOR;
|
||||||
for (TransactionOutput out : outputs)
|
for (TransactionOutput out : outputs)
|
||||||
wu += out.length * 4;
|
wu += out.length * WITNESS_SCALE_FACTOR;
|
||||||
// script_witnesses
|
// script_witnesses
|
||||||
if(isSegwit()) {
|
if(isSegwit()) {
|
||||||
for (TransactionInput in : inputs) {
|
for (TransactionInput in : inputs) {
|
||||||
|
@ -275,9 +289,9 @@ public class Transaction extends ChildMessage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// lock_time
|
// lock_time
|
||||||
wu += 4*4;
|
wu += 4 * WITNESS_SCALE_FACTOR;
|
||||||
|
|
||||||
return (int)Math.ceil((double)wu / 4.0);
|
return (int)Math.ceil((double)wu / (double)WITNESS_SCALE_FACTOR);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<TransactionInput> getInputs() {
|
public List<TransactionInput> getInputs() {
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.sparrowwallet.drongo.wallet;
|
||||||
|
|
||||||
|
public class InsufficientFundsException extends Exception {
|
||||||
|
public InsufficientFundsException() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public InsufficientFundsException(String msg) {
|
||||||
|
super(msg);
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,9 @@ public class PriorityUtxoSelector implements UtxoSelector {
|
||||||
List<BlockTransactionHashIndex> sorted = candidates.stream().filter(ref -> ref.getHeight() != 0).collect(Collectors.toList());
|
List<BlockTransactionHashIndex> sorted = candidates.stream().filter(ref -> ref.getHeight() != 0).collect(Collectors.toList());
|
||||||
sort(sorted);
|
sort(sorted);
|
||||||
|
|
||||||
|
//Testing only: remove
|
||||||
|
Collections.reverse(sorted);
|
||||||
|
|
||||||
long total = 0;
|
long total = 0;
|
||||||
for(BlockTransactionHashIndex reference : sorted) {
|
for(BlockTransactionHashIndex reference : sorted) {
|
||||||
if(total > targetValue) {
|
if(total > targetValue) {
|
||||||
|
|
|
@ -151,17 +151,45 @@ public class Wallet {
|
||||||
throw new IllegalStateException("Could not fill nodes to index " + index);
|
throw new IllegalStateException("Could not fill nodes to index " + index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ECKey getPubKey(WalletNode node) {
|
||||||
|
return getPubKey(node.getKeyPurpose(), node.getIndex());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ECKey getPubKey(KeyPurpose keyPurpose, int index) {
|
||||||
|
if(policyType == PolicyType.MULTI) {
|
||||||
|
throw new IllegalStateException("Attempting to retrieve a single key for a multisig policy wallet");
|
||||||
|
} else if(policyType == PolicyType.CUSTOM) {
|
||||||
|
throw new UnsupportedOperationException("Cannot determine a public key for a custom policy");
|
||||||
|
}
|
||||||
|
|
||||||
|
Keystore keystore = getKeystores().get(0);
|
||||||
|
return keystore.getKey(keyPurpose, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ECKey> getPubKeys(WalletNode node) {
|
||||||
|
return getPubKeys(node.getKeyPurpose(), node.getIndex());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ECKey> getPubKeys(KeyPurpose keyPurpose, int index) {
|
||||||
|
if(policyType == PolicyType.SINGLE) {
|
||||||
|
throw new IllegalStateException("Attempting to retrieve multiple keys for a singlesig policy wallet");
|
||||||
|
} else if(policyType == PolicyType.CUSTOM) {
|
||||||
|
throw new UnsupportedOperationException("Cannot determine public keys for a custom policy");
|
||||||
|
}
|
||||||
|
|
||||||
|
return getKeystores().stream().map(keystore -> keystore.getKey(keyPurpose, index)).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
public Address getAddress(WalletNode node) {
|
public Address getAddress(WalletNode node) {
|
||||||
return getAddress(node.getKeyPurpose(), node.getIndex());
|
return getAddress(node.getKeyPurpose(), node.getIndex());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Address getAddress(KeyPurpose keyPurpose, int index) {
|
public Address getAddress(KeyPurpose keyPurpose, int index) {
|
||||||
if(policyType == PolicyType.SINGLE) {
|
if(policyType == PolicyType.SINGLE) {
|
||||||
Keystore keystore = getKeystores().get(0);
|
ECKey pubKey = getPubKey(keyPurpose, index);
|
||||||
DeterministicKey key = keystore.getKey(keyPurpose, index);
|
return scriptType.getAddress(pubKey);
|
||||||
return scriptType.getAddress(key);
|
|
||||||
} else if(policyType == PolicyType.MULTI) {
|
} else if(policyType == PolicyType.MULTI) {
|
||||||
List<ECKey> pubKeys = getKeystores().stream().map(keystore -> keystore.getKey(keyPurpose, index)).collect(Collectors.toList());
|
List<ECKey> pubKeys = getPubKeys(keyPurpose, index);
|
||||||
Script script = ScriptType.MULTISIG.getOutputScript(defaultPolicy.getNumSignaturesRequired(), pubKeys);
|
Script script = ScriptType.MULTISIG.getOutputScript(defaultPolicy.getNumSignaturesRequired(), pubKeys);
|
||||||
return scriptType.getAddress(script);
|
return scriptType.getAddress(script);
|
||||||
} else {
|
} else {
|
||||||
|
@ -175,11 +203,10 @@ public class Wallet {
|
||||||
|
|
||||||
public Script getOutputScript(KeyPurpose keyPurpose, int index) {
|
public Script getOutputScript(KeyPurpose keyPurpose, int index) {
|
||||||
if(policyType == PolicyType.SINGLE) {
|
if(policyType == PolicyType.SINGLE) {
|
||||||
Keystore keystore = getKeystores().get(0);
|
ECKey pubKey = getPubKey(keyPurpose, index);
|
||||||
DeterministicKey key = keystore.getKey(keyPurpose, index);
|
return scriptType.getOutputScript(pubKey);
|
||||||
return scriptType.getOutputScript(key);
|
|
||||||
} else if(policyType == PolicyType.MULTI) {
|
} else if(policyType == PolicyType.MULTI) {
|
||||||
List<ECKey> pubKeys = getKeystores().stream().map(keystore -> keystore.getKey(keyPurpose, index)).collect(Collectors.toList());
|
List<ECKey> pubKeys = getPubKeys(keyPurpose, index);
|
||||||
Script script = ScriptType.MULTISIG.getOutputScript(defaultPolicy.getNumSignaturesRequired(), pubKeys);
|
Script script = ScriptType.MULTISIG.getOutputScript(defaultPolicy.getNumSignaturesRequired(), pubKeys);
|
||||||
return scriptType.getOutputScript(script);
|
return scriptType.getOutputScript(script);
|
||||||
} else {
|
} else {
|
||||||
|
@ -193,11 +220,10 @@ public class Wallet {
|
||||||
|
|
||||||
public String getOutputDescriptor(KeyPurpose keyPurpose, int index) {
|
public String getOutputDescriptor(KeyPurpose keyPurpose, int index) {
|
||||||
if(policyType == PolicyType.SINGLE) {
|
if(policyType == PolicyType.SINGLE) {
|
||||||
Keystore keystore = getKeystores().get(0);
|
ECKey pubKey = getPubKey(keyPurpose, index);
|
||||||
DeterministicKey key = keystore.getKey(keyPurpose, index);
|
return scriptType.getOutputDescriptor(pubKey);
|
||||||
return scriptType.getOutputDescriptor(key);
|
|
||||||
} else if(policyType == PolicyType.MULTI) {
|
} else if(policyType == PolicyType.MULTI) {
|
||||||
List<ECKey> pubKeys = getKeystores().stream().map(keystore -> keystore.getKey(keyPurpose, index)).collect(Collectors.toList());
|
List<ECKey> pubKeys = getPubKeys(keyPurpose, index);
|
||||||
Script script = ScriptType.MULTISIG.getOutputScript(defaultPolicy.getNumSignaturesRequired(), pubKeys);
|
Script script = ScriptType.MULTISIG.getOutputScript(defaultPolicy.getNumSignaturesRequired(), pubKeys);
|
||||||
return scriptType.getOutputDescriptor(script);
|
return scriptType.getOutputDescriptor(script);
|
||||||
} else {
|
} else {
|
||||||
|
@ -222,6 +248,91 @@ public class Wallet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public WalletTransaction createWalletTransaction(List<UtxoSelector> utxoSelectors, Address recipientAddress, long recipientAmount, double feeRate) throws InsufficientFundsException {
|
||||||
|
long valueRequiredAmt = recipientAmount;
|
||||||
|
|
||||||
|
while(true) {
|
||||||
|
Map<BlockTransactionHashIndex, WalletNode> selectedUtxos = selectInputs(utxoSelectors, valueRequiredAmt);
|
||||||
|
long totalSelectedAmt = selectedUtxos.keySet().stream().mapToLong(BlockTransactionHashIndex::getValue).sum();
|
||||||
|
|
||||||
|
//Add inputs
|
||||||
|
Transaction transaction = new Transaction();
|
||||||
|
for(Map.Entry<BlockTransactionHashIndex, WalletNode> selectedUtxo : selectedUtxos.entrySet()) {
|
||||||
|
Transaction prevTx = getTransactions().get(selectedUtxo.getKey().getHash()).getTransaction();
|
||||||
|
TransactionOutput prevTxOut = prevTx.getOutputs().get((int)selectedUtxo.getKey().getIndex());
|
||||||
|
|
||||||
|
if(getPolicyType().equals(PolicyType.SINGLE)) {
|
||||||
|
ECKey pubKey = getPubKey(selectedUtxo.getValue());
|
||||||
|
TransactionSignature signature = TransactionSignature.dummy();
|
||||||
|
getScriptType().addSpendingInput(transaction, prevTxOut, pubKey, signature);
|
||||||
|
} else if(getPolicyType().equals(PolicyType.MULTI)) {
|
||||||
|
List<ECKey> pubKeys = getPubKeys(selectedUtxo.getValue());
|
||||||
|
int threshold = getDefaultPolicy().getNumSignaturesRequired();
|
||||||
|
List<TransactionSignature> signatures = new ArrayList<>(threshold);
|
||||||
|
for(int i = 0; i < threshold; i++) {
|
||||||
|
signatures.add(TransactionSignature.dummy());
|
||||||
|
}
|
||||||
|
getScriptType().addMultisigSpendingInput(transaction, prevTxOut, threshold, pubKeys, signatures);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Add recipient output
|
||||||
|
transaction.addOutput(recipientAmount, recipientAddress);
|
||||||
|
int noChangeVSize = transaction.getVirtualSize();
|
||||||
|
long noChangeFeeRequiredAmt = (long)(feeRate * noChangeVSize);
|
||||||
|
|
||||||
|
//Calculate what is left over from selected utxos after paying recipient
|
||||||
|
long differenceAmt = totalSelectedAmt - recipientAmount;
|
||||||
|
|
||||||
|
//If insufficient fee, increase value required from inputs to include the fee and try again
|
||||||
|
if(differenceAmt < noChangeFeeRequiredAmt) {
|
||||||
|
valueRequiredAmt = totalSelectedAmt + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Determine if a change output is required by checking if its value is greater than its dust threshold
|
||||||
|
long changeAmt = differenceAmt - noChangeFeeRequiredAmt;
|
||||||
|
WalletNode changeNode = getFreshNode(KeyPurpose.CHANGE);
|
||||||
|
TransactionOutput changeOutput = new TransactionOutput(transaction, changeAmt, getOutputScript(changeNode));
|
||||||
|
long dustThreshold = getScriptType().getDustThreshold(changeOutput, Transaction.DEFAULT_DISCARD_FEE_RATE);
|
||||||
|
if(changeAmt > dustThreshold) {
|
||||||
|
//Change output is required, determine new fee once change output has been added
|
||||||
|
int changeVSize = noChangeVSize + changeOutput.getLength();
|
||||||
|
long changeFeeRequiredAmt = (long)(feeRate * changeVSize);
|
||||||
|
|
||||||
|
//Recalculate the change amount with the new fee
|
||||||
|
changeAmt = differenceAmt - changeFeeRequiredAmt;
|
||||||
|
if(changeAmt < dustThreshold) {
|
||||||
|
//The new fee has meant that the change output is now dust. We pay too high a fee without change, but change is dust when added. Increase value required from inputs and try again
|
||||||
|
valueRequiredAmt = totalSelectedAmt + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Add change output
|
||||||
|
transaction.addOutput(changeAmt, getOutputScript(changeNode));
|
||||||
|
|
||||||
|
return new WalletTransaction(this, transaction, selectedUtxos, recipientAddress, recipientAmount, changeNode, changeAmt, changeFeeRequiredAmt);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WalletTransaction(this, transaction, selectedUtxos, recipientAddress, recipientAmount, differenceAmt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<BlockTransactionHashIndex, WalletNode> selectInputs(List<UtxoSelector> utxoSelectors, Long targetValue) throws InsufficientFundsException {
|
||||||
|
Map<BlockTransactionHashIndex, WalletNode> utxos = getWalletUtxos();
|
||||||
|
|
||||||
|
for(UtxoSelector utxoSelector : utxoSelectors) {
|
||||||
|
Collection<BlockTransactionHashIndex> selectedInputs = utxoSelector.select(targetValue, utxos.keySet());
|
||||||
|
long total = selectedInputs.stream().mapToLong(BlockTransactionHashIndex::getValue).sum();
|
||||||
|
if(total > targetValue) {
|
||||||
|
utxos.keySet().retainAll(selectedInputs);
|
||||||
|
return utxos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InsufficientFundsException("Not enough combined value in UTXOs for output value " + targetValue);
|
||||||
|
}
|
||||||
|
|
||||||
public void clearNodes() {
|
public void clearNodes() {
|
||||||
purposeNodes.clear();
|
purposeNodes.clear();
|
||||||
transactions.clear();
|
transactions.clear();
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
package com.sparrowwallet.drongo.wallet;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
|
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
|
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WalletTransaction contains a draft transaction along with associated metadata. The draft transaction has empty signatures but is otherwise complete.
|
||||||
|
* This object represents an intermediate step before the transaction is signed or a PSBT is created from it.
|
||||||
|
*/
|
||||||
|
public class WalletTransaction {
|
||||||
|
private final Wallet wallet;
|
||||||
|
private final Transaction transaction;
|
||||||
|
private final Map<BlockTransactionHashIndex, WalletNode> selectedUtxos;
|
||||||
|
private final Address recipientAddress;
|
||||||
|
private final long recipientAmount;
|
||||||
|
private final WalletNode changeNode;
|
||||||
|
private final long changeAmount;
|
||||||
|
private final long fee;
|
||||||
|
|
||||||
|
public WalletTransaction(Wallet wallet, Transaction transaction, Map<BlockTransactionHashIndex, WalletNode> selectedUtxos, Address recipientAddress, long recipientAmount, long fee) {
|
||||||
|
this(wallet, transaction, selectedUtxos, recipientAddress, recipientAmount, null, 0L, fee);
|
||||||
|
}
|
||||||
|
|
||||||
|
public WalletTransaction(Wallet wallet, Transaction transaction, Map<BlockTransactionHashIndex, WalletNode> selectedUtxos, Address recipientAddress, long recipientAmount, WalletNode changeNode, long changeAmount, long fee) {
|
||||||
|
this.wallet = wallet;
|
||||||
|
this.transaction = transaction;
|
||||||
|
this.selectedUtxos = selectedUtxos;
|
||||||
|
this.recipientAddress = recipientAddress;
|
||||||
|
this.recipientAmount = recipientAmount;
|
||||||
|
this.changeNode = changeNode;
|
||||||
|
this.changeAmount = changeAmount;
|
||||||
|
this.fee = fee;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PSBT createPSBT() {
|
||||||
|
//TODO: Create PSBT
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Wallet getWallet() {
|
||||||
|
return wallet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Transaction getTransaction() {
|
||||||
|
return transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<BlockTransactionHashIndex, WalletNode> getSelectedUtxos() {
|
||||||
|
return selectedUtxos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Address getRecipientAddress() {
|
||||||
|
return recipientAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getRecipientAmount() {
|
||||||
|
return recipientAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public WalletNode getChangeNode() {
|
||||||
|
return changeNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getChangeAmount() {
|
||||||
|
return changeAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getFee() {
|
||||||
|
return fee;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue