use of com.sparrowwallet.drongo.protocol.TransactionInput in project sparrow by sparrowwallet.
the class Payjoin method getSingleInputFee.
private long getSingleInputFee() {
Transaction transaction = psbt.extractTransaction();
double feeRate = psbt.getFee().doubleValue() / transaction.getVirtualSize();
int vSize = 68;
if (transaction.getInputs().size() > 0) {
TransactionInput input = transaction.getInputs().get(0);
vSize = input.getLength() * Transaction.WITNESS_SCALE_FACTOR;
vSize += input.getWitness() != null ? input.getWitness().getLength() : 0;
vSize = (int) Math.ceil((double) vSize / Transaction.WITNESS_SCALE_FACTOR);
}
return (long) (vSize * feeRate);
}
use of com.sparrowwallet.drongo.protocol.TransactionInput in project sparrow by sparrowwallet.
the class OutputController method updateSpent.
private void updateSpent(List<BlockTransaction> outputTransactions) {
int outputIndex = outputForm.getIndex();
if (outputIndex < outputForm.getMaxOutputFetched()) {
spent.setText("Unspent");
} else {
spent.setText("Unknown");
}
if (outputIndex >= 0 && outputIndex < outputTransactions.size()) {
BlockTransaction outputBlockTransaction = outputTransactions.get(outputIndex);
if (outputBlockTransaction != null) {
spent.setText("Spent");
if (outputBlockTransaction == ElectrumServer.UNFETCHABLE_BLOCK_TRANSACTION) {
spent.setText("Spent (Spending transaction history too large to fetch)");
return;
}
for (int i = 0; i < outputBlockTransaction.getTransaction().getInputs().size(); i++) {
TransactionInput input = outputBlockTransaction.getTransaction().getInputs().get(i);
if (input.getOutpoint().getHash().equals(outputForm.getTransaction().getTxId()) && input.getOutpoint().getIndex() == outputIndex) {
spentField.setVisible(false);
spentByField.setVisible(true);
final Integer inputIndex = i;
spentBy.setText(outputBlockTransaction.getHash().toString() + ":" + inputIndex);
spentBy.setOnAction(event -> {
EventManager.get().post(new ViewTransactionEvent(spentBy.getScene().getWindow(), outputBlockTransaction, TransactionView.INPUT, inputIndex));
});
spentBy.setContextMenu(new TransactionReferenceContextMenu(spentBy.getText()));
}
}
}
}
}
use of com.sparrowwallet.drongo.protocol.TransactionInput in project sparrow by sparrowwallet.
the class SparrowDataSource method fetchWalletResponse.
@Override
protected WalletResponse fetchWalletResponse() throws Exception {
WalletResponse walletResponse = new WalletResponse();
walletResponse.wallet = new WalletResponse.Wallet();
Map<Sha256Hash, BlockTransaction> allTransactions = new HashMap<>();
Map<Sha256Hash, String> allTransactionsZpubs = new HashMap<>();
List<WalletResponse.Address> addresses = new ArrayList<>();
List<WalletResponse.Tx> txes = new ArrayList<>();
List<UnspentOutput> unspentOutputs = new ArrayList<>();
int storedBlockHeight = 0;
String[] zpubs = getWalletSupplier().getPubs(true);
for (String zpub : zpubs) {
Wallet wallet = getWallet(zpub);
if (wallet == null) {
log.debug("No wallet for " + zpub + " found");
continue;
}
Map<Sha256Hash, BlockTransaction> walletTransactions = wallet.getWalletTransactions();
allTransactions.putAll(walletTransactions);
walletTransactions.keySet().forEach(txid -> allTransactionsZpubs.put(txid, zpub));
if (wallet.getStoredBlockHeight() != null) {
storedBlockHeight = Math.max(storedBlockHeight, wallet.getStoredBlockHeight());
}
WalletResponse.Address address = new WalletResponse.Address();
List<ExtendedKey.Header> headers = ExtendedKey.Header.getHeaders(Network.get());
ExtendedKey.Header header = headers.stream().filter(head -> head.getDefaultScriptType().equals(wallet.getScriptType()) && !head.isPrivateKey()).findFirst().orElse(ExtendedKey.Header.xpub);
address.address = wallet.getKeystores().get(0).getExtendedPublicKey().toString(header);
int receiveIndex = wallet.getNode(KeyPurpose.RECEIVE).getHighestUsedIndex() == null ? 0 : wallet.getNode(KeyPurpose.RECEIVE).getHighestUsedIndex() + 1;
address.account_index = wallet.getMixConfig() != null ? Math.max(receiveIndex, wallet.getMixConfig().getReceiveIndex()) : receiveIndex;
int changeIndex = wallet.getNode(KeyPurpose.CHANGE).getHighestUsedIndex() == null ? 0 : wallet.getNode(KeyPurpose.CHANGE).getHighestUsedIndex() + 1;
address.change_index = wallet.getMixConfig() != null ? Math.max(changeIndex, wallet.getMixConfig().getChangeIndex()) : changeIndex;
address.n_tx = walletTransactions.size();
addresses.add(address);
for (Map.Entry<BlockTransactionHashIndex, WalletNode> utxo : wallet.getWalletUtxos().entrySet()) {
BlockTransaction blockTransaction = wallet.getWalletTransaction(utxo.getKey().getHash());
if (blockTransaction != null && utxo.getKey().getStatus() != Status.FROZEN) {
unspentOutputs.add(Whirlpool.getUnspentOutput(utxo.getValue(), blockTransaction, (int) utxo.getKey().getIndex()));
}
}
}
for (BlockTransaction blockTransaction : allTransactions.values()) {
WalletResponse.Tx tx = new WalletResponse.Tx();
tx.block_height = blockTransaction.getHeight();
tx.hash = blockTransaction.getHashAsString();
tx.locktime = blockTransaction.getTransaction().getLocktime();
tx.version = (int) blockTransaction.getTransaction().getVersion();
tx.inputs = new WalletResponse.TxInput[blockTransaction.getTransaction().getInputs().size()];
for (int i = 0; i < blockTransaction.getTransaction().getInputs().size(); i++) {
TransactionInput txInput = blockTransaction.getTransaction().getInputs().get(i);
tx.inputs[i] = new WalletResponse.TxInput();
tx.inputs[i].vin = txInput.getIndex();
tx.inputs[i].sequence = txInput.getSequenceNumber();
if (allTransactionsZpubs.containsKey(txInput.getOutpoint().getHash())) {
tx.inputs[i].prev_out = new WalletResponse.TxOut();
tx.inputs[i].prev_out.txid = txInput.getOutpoint().getHash().toString();
tx.inputs[i].prev_out.vout = (int) txInput.getOutpoint().getIndex();
BlockTransaction spentTransaction = allTransactions.get(txInput.getOutpoint().getHash());
if (spentTransaction != null) {
TransactionOutput spentOutput = spentTransaction.getTransaction().getOutputs().get((int) txInput.getOutpoint().getIndex());
tx.inputs[i].prev_out.value = spentOutput.getValue();
}
tx.inputs[i].prev_out.xpub = new UnspentOutput.Xpub();
tx.inputs[i].prev_out.xpub.m = allTransactionsZpubs.get(txInput.getOutpoint().getHash());
}
}
tx.out = new WalletResponse.TxOutput[blockTransaction.getTransaction().getOutputs().size()];
for (int i = 0; i < blockTransaction.getTransaction().getOutputs().size(); i++) {
TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get(i);
tx.out[i] = new WalletResponse.TxOutput();
tx.out[i].n = txOutput.getIndex();
tx.out[i].value = txOutput.getValue();
tx.out[i].xpub = new UnspentOutput.Xpub();
tx.out[i].xpub.m = allTransactionsZpubs.get(blockTransaction.getHash());
}
txes.add(tx);
}
walletResponse.addresses = addresses.toArray(new WalletResponse.Address[0]);
walletResponse.txs = txes.toArray(new WalletResponse.Tx[0]);
walletResponse.unspent_outputs = unspentOutputs.toArray(new UnspentOutput[0]);
walletResponse.info = new WalletResponse.Info();
walletResponse.info.latest_block = new WalletResponse.InfoBlock();
walletResponse.info.latest_block.height = AppServices.getCurrentBlockHeight() == null ? storedBlockHeight : AppServices.getCurrentBlockHeight();
walletResponse.info.latest_block.hash = Sha256Hash.ZERO_HASH.toString();
walletResponse.info.latest_block.time = AppServices.getLatestBlockHeader() == null ? 1 : AppServices.getLatestBlockHeader().getTime();
walletResponse.info.fees = new LinkedHashMap<>();
for (MinerFeeTarget target : MinerFeeTarget.values()) {
walletResponse.info.fees.put(target.getValue(), getMinerFeeSupplier().getFee(target));
}
return walletResponse;
}
use of com.sparrowwallet.drongo.protocol.TransactionInput in project sparrow by sparrowwallet.
the class Payjoin method checkProposal.
private void checkProposal(PSBT original, PSBT proposal, int changeOutputIndex, long maxAdditionalFeeContribution, boolean allowOutputSubstitution) throws PayjoinReceiverException {
Queue<Map.Entry<TransactionInput, PSBTInput>> originalInputs = new ArrayDeque<>();
for (int i = 0; i < original.getPsbtInputs().size(); i++) {
originalInputs.add(Map.entry(original.getTransaction().getInputs().get(i), original.getPsbtInputs().get(i)));
}
Queue<Map.Entry<TransactionOutput, PSBTOutput>> originalOutputs = new ArrayDeque<>();
for (int i = 0; i < original.getPsbtOutputs().size(); i++) {
originalOutputs.add(Map.entry(original.getTransaction().getOutputs().get(i), original.getPsbtOutputs().get(i)));
}
// Checking that the PSBT of the receiver is clean
if (!proposal.getExtendedPublicKeys().isEmpty()) {
throw new PayjoinReceiverException("Global xpubs should not be included in the receiver's PSBT");
}
Transaction originalTx = original.getTransaction();
Transaction proposalTx = proposal.getTransaction();
// Verify that the transaction version, and nLockTime are unchanged.
if (proposalTx.getVersion() != originalTx.getVersion()) {
throw new PayjoinReceiverException("The proposal PSBT changed the transaction version");
}
if (proposalTx.getLocktime() != originalTx.getLocktime()) {
throw new PayjoinReceiverException("The proposal PSBT changed the nLocktime");
}
Set<Long> sequences = new HashSet<>();
// For each inputs in the proposal:
for (PSBTInput proposedPSBTInput : proposal.getPsbtInputs()) {
if (!proposedPSBTInput.getDerivedPublicKeys().isEmpty()) {
throw new PayjoinReceiverException("The receiver added keypaths to an input");
}
if (!proposedPSBTInput.getPartialSignatures().isEmpty()) {
throw new PayjoinReceiverException("The receiver added partial signatures to an input");
}
TransactionInput proposedTxIn = proposedPSBTInput.getInput();
boolean isOriginalInput = originalInputs.size() > 0 && originalInputs.peek().getKey().getOutpoint().equals(proposedTxIn.getOutpoint());
if (isOriginalInput) {
Map.Entry<TransactionInput, PSBTInput> originalInput = originalInputs.remove();
TransactionInput originalTxIn = originalInput.getKey();
// Verify that sequence is unchanged.
if (originalTxIn.getSequenceNumber() != proposedTxIn.getSequenceNumber()) {
throw new PayjoinReceiverException("The proposed transaction input modified the sequence of one of the original inputs");
}
// Verify the PSBT input is not finalized
if (proposedPSBTInput.isFinalized()) {
throw new PayjoinReceiverException("The receiver finalized one of the original inputs");
}
// Verify that non_witness_utxo and witness_utxo are not specified.
if (proposedPSBTInput.getNonWitnessUtxo() != null || proposedPSBTInput.getWitnessUtxo() != null) {
throw new PayjoinReceiverException("The receiver added non_witness_utxo or witness_utxo to one of the original inputs");
}
sequences.add(proposedTxIn.getSequenceNumber());
PSBTInput originalPSBTInput = originalInput.getValue();
// Fill up the info from the original PSBT input so we can sign and get fees.
proposedPSBTInput.setNonWitnessUtxo(originalPSBTInput.getNonWitnessUtxo());
proposedPSBTInput.setWitnessUtxo(originalPSBTInput.getWitnessUtxo());
// We fill up information we had on the signed PSBT, so we can sign it.
proposedPSBTInput.getDerivedPublicKeys().putAll(originalPSBTInput.getDerivedPublicKeys());
proposedPSBTInput.getProprietary().putAll(originalPSBTInput.getProprietary());
proposedPSBTInput.setRedeemScript(originalPSBTInput.getFinalScriptSig().getFirstNestedScript());
proposedPSBTInput.setWitnessScript(originalPSBTInput.getFinalScriptWitness().getWitnessScript());
proposedPSBTInput.setSigHash(originalPSBTInput.getSigHash());
} else {
// Verify the PSBT input is finalized
if (!proposedPSBTInput.isFinalized()) {
throw new PayjoinReceiverException("The receiver did not finalize one of their inputs");
}
// Verify that non_witness_utxo or witness_utxo are filled in.
if (proposedPSBTInput.getNonWitnessUtxo() == null && proposedPSBTInput.getWitnessUtxo() == null) {
throw new PayjoinReceiverException("The receiver did not specify non_witness_utxo or witness_utxo for one of their inputs");
}
sequences.add(proposedTxIn.getSequenceNumber());
// Verify that the payjoin proposal did not introduced mixed inputs' type.
if (wallet.getScriptType() != proposedPSBTInput.getScriptType()) {
throw new PayjoinReceiverException("Proposal script type of " + proposedPSBTInput.getScriptType() + " did not match wallet script type of " + wallet.getScriptType());
}
}
}
// Verify that all of sender's inputs from the original PSBT are in the proposal.
if (!originalInputs.isEmpty()) {
throw new PayjoinReceiverException("Some of the original inputs are not included in the proposal");
}
// Verify that the payjoin proposal did not introduced mixed inputs' sequence.
if (sequences.size() != 1) {
throw new PayjoinReceiverException("Mixed sequences detected in the proposal");
}
Long newFee = proposal.getFee();
long additionalFee = newFee - original.getFee();
if (additionalFee < 0) {
throw new PayjoinReceiverException("The receiver decreased absolute fee");
}
TransactionOutput changeOutput = (changeOutputIndex > -1 ? originalTx.getOutputs().get(changeOutputIndex) : null);
// For each outputs in the proposal:
for (int i = 0; i < proposal.getPsbtOutputs().size(); i++) {
PSBTOutput proposedPSBTOutput = proposal.getPsbtOutputs().get(i);
// Verify that no keypaths is in the PSBT output
if (!proposedPSBTOutput.getDerivedPublicKeys().isEmpty()) {
throw new PayjoinReceiverException("The receiver added keypaths to an output");
}
TransactionOutput proposedTxOut = proposalTx.getOutputs().get(i);
boolean isOriginalOutput = originalOutputs.size() > 0 && originalOutputs.peek().getKey().getScript().equals(proposedTxOut.getScript());
if (isOriginalOutput) {
Map.Entry<TransactionOutput, PSBTOutput> originalOutput = originalOutputs.remove();
if (originalOutput.getKey() == changeOutput) {
var actualContribution = changeOutput.getValue() - proposedTxOut.getValue();
// The amount that was subtracted from the output's value is less than or equal to maxadditionalfeecontribution
if (actualContribution > maxAdditionalFeeContribution) {
throw new PayjoinReceiverException("The actual contribution is more than maxadditionalfeecontribution");
}
// Make sure the actual contribution is only paying fee
if (actualContribution > additionalFee) {
throw new PayjoinReceiverException("The actual contribution is not only paying fee");
}
// Make sure the actual contribution is only paying for fee incurred by additional inputs
int additionalInputsCount = proposalTx.getInputs().size() - originalTx.getInputs().size();
if (actualContribution > getSingleInputFee() * additionalInputsCount) {
throw new PayjoinReceiverException("The actual contribution is not only paying for additional inputs");
}
} else if (allowOutputSubstitution && originalOutput.getKey().getScript().equals(payjoinURI.getAddress().getOutputScript())) {
// That's the payment output, the receiver may have changed it.
} else {
if (originalOutput.getKey().getValue() > proposedTxOut.getValue()) {
throw new PayjoinReceiverException("The receiver decreased the value of one of the outputs");
}
}
PSBTOutput originalPSBTOutput = originalOutput.getValue();
// We fill up information we had on the signed PSBT, so we can sign it.
proposedPSBTOutput.getDerivedPublicKeys().putAll(originalPSBTOutput.getDerivedPublicKeys());
proposedPSBTOutput.getProprietary().putAll(originalPSBTOutput.getProprietary());
proposedPSBTOutput.setRedeemScript(originalPSBTOutput.getRedeemScript());
proposedPSBTOutput.setWitnessScript(originalPSBTOutput.getWitnessScript());
}
}
// Verify that all of sender's outputs from the original PSBT are in the proposal.
if (!originalOutputs.isEmpty()) {
// The payment output may have been substituted
if (!allowOutputSubstitution || originalOutputs.size() != 1 || !originalOutputs.remove().getKey().getScript().equals(payjoinURI.getAddress().getOutputScript())) {
throw new PayjoinReceiverException("Some of our outputs are not included in the proposal");
}
}
// Add global pubkey map for signing
proposal.getExtendedPublicKeys().putAll(psbt.getExtendedPublicKeys());
proposal.getGlobalProprietary().putAll(psbt.getGlobalProprietary());
}
use of com.sparrowwallet.drongo.protocol.TransactionInput in project sparrow by sparrowwallet.
the class SorobanController method getWalletTransaction.
private WalletTransaction getWalletTransaction(Wallet wallet, WalletTransaction walletTransaction, Transaction transaction, Map<Sha256Hash, BlockTransaction> inputTransactions) {
Map<BlockTransactionHashIndex, WalletNode> allWalletUtxos = wallet.getWalletTxos();
Map<BlockTransactionHashIndex, WalletNode> walletUtxos = new LinkedHashMap<>();
Map<BlockTransactionHashIndex, WalletNode> externalUtxos = new LinkedHashMap<>();
for (TransactionInput txInput : transaction.getInputs()) {
Optional<BlockTransactionHashIndex> optWalletUtxo = allWalletUtxos.keySet().stream().filter(txo -> txo.getHash().equals(txInput.getOutpoint().getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()).findFirst();
if (optWalletUtxo.isPresent()) {
walletUtxos.put(optWalletUtxo.get(), allWalletUtxos.get(optWalletUtxo.get()));
} else {
BlockTransactionHashIndex externalUtxo;
if (inputTransactions != null && inputTransactions.containsKey(txInput.getOutpoint().getHash())) {
BlockTransaction blockTransaction = inputTransactions.get(txInput.getOutpoint().getHash());
TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get((int) txInput.getOutpoint().getIndex());
externalUtxo = new BlockTransactionHashIndex(blockTransaction.getHash(), blockTransaction.getHeight(), blockTransaction.getDate(), blockTransaction.getFee(), txInput.getOutpoint().getIndex(), txOutput.getValue());
} else {
externalUtxo = new BlockTransactionHashIndex(txInput.getOutpoint().getHash(), 0, null, null, txInput.getOutpoint().getIndex(), 0);
}
externalUtxos.put(externalUtxo, null);
}
}
List<Map<BlockTransactionHashIndex, WalletNode>> selectedUtxoSets = new ArrayList<>();
selectedUtxoSets.add(walletUtxos);
selectedUtxoSets.add(externalUtxos);
Map<Address, WalletNode> walletAddresses = wallet.getWalletAddresses();
List<Payment> payments = new ArrayList<>();
Map<WalletNode, Long> changeMap = new LinkedHashMap<>();
for (TransactionOutput txOutput : transaction.getOutputs()) {
Address address = txOutput.getScript().getToAddress();
if (address != null) {
Optional<Payment> optPayment = walletTransaction == null ? Optional.empty() : walletTransaction.getPayments().stream().filter(payment -> payment.getAddress().equals(address) && payment.getAmount() == txOutput.getValue()).findFirst();
if (optPayment.isPresent()) {
payments.add(optPayment.get());
} else if (walletAddresses.containsKey(address) && walletAddresses.get(address).getKeyPurpose() == KeyPurpose.CHANGE) {
changeMap.put(walletAddresses.get(address), txOutput.getValue());
} else {
Payment payment = new Payment(address, null, txOutput.getValue(), false);
if (transaction.getOutputs().stream().anyMatch(txo -> txo != txOutput && txo.getValue() == txOutput.getValue())) {
payment.setType(Payment.Type.MIX);
}
payments.add(payment);
}
}
}
long fee = calculateFee(walletTransaction, selectedUtxoSets, transaction);
return new WalletTransaction(wallet, transaction, Collections.emptyList(), selectedUtxoSets, payments, changeMap, fee, inputTransactions);
}
Aggregations