diff --git a/drongo b/drongo index 7df781c7..698f8b08 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit 7df781c77c6aabf8464cb4eb6fc49cfda0f3c6c0 +Subproject commit 698f8b08a170712a55fd6824a3d37c32b2d0e798 diff --git a/src/main/java/com/sparrowwallet/sparrow/event/WalletSilentPaymentAddressesChangedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/WalletSilentPaymentAddressesChangedEvent.java new file mode 100644 index 00000000..4002ddf9 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/WalletSilentPaymentAddressesChangedEvent.java @@ -0,0 +1,9 @@ +package com.sparrowwallet.sparrow.event; + +import com.sparrowwallet.drongo.wallet.Wallet; + +public class WalletSilentPaymentAddressesChangedEvent extends WalletChangedEvent { + public WalletSilentPaymentAddressesChangedEvent(Wallet wallet) { + super(wallet.resolveMasterWallet()); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java b/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java index 08ee1fe7..11f238a7 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java @@ -337,6 +337,11 @@ public class DbPersistence implements Persistence { walletConfigDao.addOrUpdate(wallet, wallet.getWalletConfig()); } + if(dirtyPersistables.silentPaymentAddresses) { + SilentPaymentAddressDao silentPaymentAddressDao = handle.attach(SilentPaymentAddressDao.class); + silentPaymentAddressDao.clearAndAddAll(wallet); + } + if(dirtyPersistables.walletTable != null) { WalletTableDao walletTableDao = handle.attach(WalletTableDao.class); walletTableDao.addOrUpdate(wallet, dirtyPersistables.walletTable.getTableType(), dirtyPersistables.walletTable); @@ -888,6 +893,13 @@ public class DbPersistence implements Persistence { } } + @Subscribe + public void walletSilentPaymentAddressesChanged(WalletSilentPaymentAddressesChangedEvent event) { + if(persistsFor(event.getWallet())) { + updateExecutor.execute(() -> dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).silentPaymentAddresses = true); + } + } + private static class DirtyPersistables { public boolean deleteAccount; public boolean clearHistory; @@ -906,6 +918,7 @@ public class DbPersistence implements Persistence { public final List labelKeystores = new ArrayList<>(); public final List encryptionKeystores = new ArrayList<>(); public final List registrationKeystores = new ArrayList<>(); + public boolean silentPaymentAddresses; public String toString() { return "Dirty Persistables" + @@ -927,7 +940,8 @@ public class DbPersistence implements Persistence { "\nUTXO mixes removed:" + removedUtxoMixes + "\nKeystore labels:" + labelKeystores.stream().map(Keystore::getLabel).collect(Collectors.toList()) + "\nKeystore encryptions:" + encryptionKeystores.stream().map(Keystore::getLabel).collect(Collectors.toList()) + - "\nKeystore registrations:" + registrationKeystores.stream().map(Keystore::getDeviceRegistration).collect(Collectors.toList()); + "\nKeystore registrations:" + registrationKeystores.stream().map(Keystore::getDeviceRegistration).collect(Collectors.toList()) + + "\nSilent payment addresses:" + silentPaymentAddresses; } } } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/db/SilentPaymentAddressDao.java b/src/main/java/com/sparrowwallet/sparrow/io/db/SilentPaymentAddressDao.java new file mode 100644 index 00000000..a216e715 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/io/db/SilentPaymentAddressDao.java @@ -0,0 +1,40 @@ +package com.sparrowwallet.sparrow.io.db; + +import com.sparrowwallet.drongo.address.Address; +import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress; +import com.sparrowwallet.drongo.wallet.Wallet; +import org.jdbi.v3.sqlobject.config.RegisterRowMapper; +import org.jdbi.v3.sqlobject.statement.SqlBatch; +import org.jdbi.v3.sqlobject.statement.SqlQuery; +import org.jdbi.v3.sqlobject.statement.SqlUpdate; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public interface SilentPaymentAddressDao { + @SqlQuery("select address, silentPaymentAddress from silentPaymentAddress") + @RegisterRowMapper(SilentPaymentAddressMapper.class) + Map getAll(); + + @SqlBatch("insert into silentPaymentAddress (address, silentPaymentAddress) values (?, ?)") + void insertSilentPaymentAddresses(List addresses, List silentPaymentAddresses); + + @SqlUpdate("delete from silentPaymentAddress") + void clear(); + + default void clearAndAddAll(Wallet wallet) { + clear(); + + List addresses = new ArrayList<>(); + List silentPaymentAddresses = new ArrayList<>(); + for(Map.Entry entry : wallet.getSilentPaymentAddresses().entrySet()) { + addresses.add(entry.getKey().getData()); + silentPaymentAddresses.add(entry.getValue().serialize()); + } + + if(!addresses.isEmpty()) { + insertSilentPaymentAddresses(addresses, silentPaymentAddresses); + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/db/SilentPaymentAddressMapper.java b/src/main/java/com/sparrowwallet/sparrow/io/db/SilentPaymentAddressMapper.java new file mode 100644 index 00000000..20cefa70 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/io/db/SilentPaymentAddressMapper.java @@ -0,0 +1,36 @@ +package com.sparrowwallet.sparrow.io.db; + +import com.sparrowwallet.drongo.address.Address; +import com.sparrowwallet.drongo.address.P2TRAddress; +import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress; +import org.jdbi.v3.core.mapper.RowMapper; +import org.jdbi.v3.core.statement.StatementContext; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Map; + +public class SilentPaymentAddressMapper implements RowMapper> { + @Override + public Map.Entry map(ResultSet rs, StatementContext ctx) throws SQLException { + Address address = new P2TRAddress(rs.getBytes("address")); + SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.fromBytes(rs.getBytes("silentPaymentAddress")); + + return new Map.Entry<>() { + @Override + public Address getKey() { + return address; + } + + @Override + public SilentPaymentAddress getValue() { + return silentPaymentAddress; + } + + @Override + public SilentPaymentAddress setValue(SilentPaymentAddress value) { + return null; + } + }; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/db/WalletDao.java b/src/main/java/com/sparrowwallet/sparrow/io/db/WalletDao.java index 36005bf1..60b8ef09 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/db/WalletDao.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/db/WalletDao.java @@ -30,6 +30,9 @@ public interface WalletDao { @CreateSqlObject DetachedLabelDao createDetachedLabelDao(); + @CreateSqlObject + SilentPaymentAddressDao createSilentPaymentAddressDao(); + @CreateSqlObject WalletConfigDao createWalletConfigDao(); @@ -121,6 +124,8 @@ public interface WalletDao { Map detachedLabels = createDetachedLabelDao().getAll(); wallet.getDetachedLabels().putAll(detachedLabels); + wallet.getSilentPaymentAddresses().putAll(createSilentPaymentAddressDao().getAll()); + wallet.setWalletConfig(createWalletConfigDao().getForWalletId(wallet.getId())); Map walletTables = createWalletTableDao().getForWalletId(wallet.getId()); @@ -144,6 +149,7 @@ public interface WalletDao { createWalletNodeDao().addWalletNodes(wallet); createBlockTransactionDao().addBlockTransactions(wallet); createDetachedLabelDao().clearAndAddAll(wallet); + createSilentPaymentAddressDao().clearAndAddAll(wallet); createWalletConfigDao().addWalletConfig(wallet); createWalletTableDao().addWalletTables(wallet); createMixConfigDao().addMixConfig(wallet); diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java index 2c1460cb..c6373acf 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java @@ -578,6 +578,8 @@ public class HeadersController extends TransactionFormController implements Init int threshold = signingWallet.getDefaultPolicy().getNumSignaturesRequired(); signaturesProgressBar.initialize(headersForm.getSignatureKeystoreMap(), threshold); + + learnSilentPaymentAddresses(signingWallet, headersForm.getPsbt()); }); blockchainForm.setDynamicUpdate(this); @@ -1156,6 +1158,11 @@ public class HeadersController extends TransactionFormController implements Init Map signingNodes = unencryptedWallet.getSigningNodes(headersForm.getPsbt()); List silentPayments = unencryptedWallet.computeSilentPaymentOutputs(headersForm.getPsbt(), signingNodes); if(!silentPayments.isEmpty()) { + Wallet signingWallet = headersForm.getSigningWallet(); + for(SilentPayment silentPayment : silentPayments) { + signingWallet.addSilentPaymentAddress(silentPayment.getAddress(), silentPayment.getSilentPaymentAddress()); + } + EventManager.get().post(new WalletSilentPaymentAddressesChangedEvent(signingWallet)); EventManager.get().post(new TransactionOutputsChangedEvent(headersForm.getTransaction())); } unencryptedWallet.sign(signingNodes); @@ -1443,6 +1450,37 @@ public class HeadersController extends TransactionFormController implements Init requestPayjoinPSBTService.start(); } + private void learnSilentPaymentAddresses(Wallet wallet, PSBT psbt) { + if(wallet == null || psbt == null) { + return; + } + + Map pending = new LinkedHashMap<>(); + for(PSBTOutput psbtOutput : psbt.getPsbtOutputs()) { + SilentPaymentAddress spAddress = psbtOutput.getSilentPaymentAddress(); + if(spAddress != null) { + Script script = psbtOutput.getScript(); + Address address = script == null ? null : script.getToAddress(); + if(address == null) { + return; + } + pending.put(address, spAddress); + } + } + + boolean changed = false; + for(Map.Entry entry : pending.entrySet()) { + if(!entry.getValue().equals(wallet.getSilentPaymentAddress(entry.getKey()))) { + wallet.addSilentPaymentAddress(entry.getKey(), entry.getValue()); + changed = true; + } + } + + if(changed) { + EventManager.get().post(new WalletSilentPaymentAddressesChangedEvent(wallet)); + } + } + @Override public void update() { BlockTransaction blockTransaction = headersForm.getBlockTransaction(); @@ -1664,6 +1702,7 @@ public class HeadersController extends TransactionFormController implements Init @Subscribe public void psbtCombined(PSBTCombinedEvent event) { if(event.getPsbt().equals(headersForm.getPsbt())) { + learnSilentPaymentAddresses(headersForm.getSigningWallet(), headersForm.getPsbt()); if(headersForm.getSigningWallet() != null) { updateSignedKeystores(headersForm.getSigningWallet()); } else if(headersForm.getPsbt().isSigned()) { @@ -1678,6 +1717,7 @@ public class HeadersController extends TransactionFormController implements Init @Subscribe public void psbtFinalized(PSBTFinalizedEvent event) { if(event.getPsbt().equals(headersForm.getPsbt())) { + learnSilentPaymentAddresses(headersForm.getSigningWallet(), headersForm.getPsbt()); if(headersForm.getSigningWallet() != null) { updateSignedKeystores(headersForm.getSigningWallet()); } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java index 5b0d4765..20d0b365 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java @@ -1559,6 +1559,11 @@ public class SendController extends WalletFormController implements Initializabl @Subscribe public void excludeUtxo(ExcludeUtxoEvent event) { if(event.getWalletTransaction() == walletTransactionProperty.get()) { + BlockTransaction replacedTransaction = replacedTransactionProperty.get(); + if(replacedTransaction != null && !getWalletForm().getWallet().isSafeToAddInputsOrOutputs(replacedTransaction)) { + AppServices.showErrorDialog("Cannot Exclude Input", "Removing an input from this replacement transaction could break silent payment outputs as the original output script depends on the input set."); + return; + } UtxoSelector utxoSelector = utxoSelectorProperty.get(); if(utxoSelector instanceof MaxUtxoSelector) { Collection utxos = event.getWalletTransaction().getSelectedUtxos().keySet(); diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java index 305a193e..d669ed65 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java @@ -749,6 +749,13 @@ public class WalletForm { } } + @Subscribe + public void walletSilentPaymentAddressesChanged(WalletSilentPaymentAddressesChangedEvent event) { + if(event.getWallet() == wallet) { + Platform.runLater(() -> EventManager.get().post(new WalletDataChangedEvent(wallet))); + } + } + @Subscribe public void walletTabsClosed(WalletTabsClosedEvent event) { for(WalletTabData tabData : event.getClosedWalletTabData()) { diff --git a/src/main/resources/com/sparrowwallet/sparrow/sql/V10__SilentPayments.sql b/src/main/resources/com/sparrowwallet/sparrow/sql/V10__SilentPayments.sql index 9ed678a3..2339656d 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/sql/V10__SilentPayments.sql +++ b/src/main/resources/com/sparrowwallet/sparrow/sql/V10__SilentPayments.sql @@ -1,3 +1,4 @@ alter table wallet add column birthHeight integer after birthDate; alter table walletNode add column silentPaymentTweak varbinary(32) after addressData; -alter table keystore add column silentPaymentScanAddress varbinary(65) after externalPaymentCode; \ No newline at end of file +alter table keystore add column silentPaymentScanAddress varbinary(65) after externalPaymentCode; +create table silentPaymentAddress (address varbinary(32) primary key not null, silentPaymentAddress varbinary(67) not null); \ No newline at end of file