From cc961b4eebba045458cef46e41d66b304e7d534c Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Mon, 14 Nov 2022 11:00:26 +0200 Subject: [PATCH] all walletconfig for wallet scope configuration variables --- build.gradle | 5 + drongo | 2 +- .../sparrowwallet/sparrow/AppController.java | 80 +++++++-- .../sparrowwallet/sparrow/SparrowDesktop.java | 3 + .../sparrow/control/PayNymAvatar.java | 3 + .../sparrow/control/WalletIcon.java | 167 ++++++++++++++++++ .../sparrow/event/PayNymImageLoadedEvent.java | 22 +++ .../event/WalletConfigChangedEvent.java | 9 + .../sparrowwallet/sparrow/io/ImageUtils.java | 56 ++++++ .../sparrow/io/db/DbPersistence.java | 14 ++ .../sparrow/io/db/WalletConfigDao.java | 36 ++++ .../sparrow/io/db/WalletConfigMapper.java | 21 +++ .../sparrow/io/db/WalletDao.java | 10 +- .../sparrow/paynym/PayNymController.java | 29 ++- .../soroban/CounterpartyController.java | 8 +- .../sparrow/soroban/InitiatorController.java | 6 +- .../sparrow/soroban/SorobanController.java | 28 +++ .../sparrow/wallet/PaymentController.java | 18 +- .../sparrow/wallet/WalletForm.java | 23 +++ src/main/java/module-info.java | 1 + .../sparrow/sql/V8__WalletConfig.sql | 1 + src/main/resources/image/bitbox02-icon.png | Bin 0 -> 550 bytes src/main/resources/image/bitbox02-icon@2x.png | Bin 0 -> 1277 bytes src/main/resources/image/coldcard-icon.png | Bin 0 -> 287 bytes src/main/resources/image/coldcard-icon@2x.png | Bin 0 -> 741 bytes src/main/resources/image/ledger-icon.png | Bin 0 -> 1613 bytes src/main/resources/image/ledger-icon@2x.png | Bin 0 -> 1875 bytes src/main/resources/image/passport-icon.png | Bin 0 -> 558 bytes src/main/resources/image/passport-icon@2x.png | Bin 0 -> 1487 bytes src/main/resources/image/seedsigner-icon.png | Bin 0 -> 1771 bytes .../resources/image/seedsigner-icon@2x.png | Bin 0 -> 2174 bytes src/main/resources/image/trezor-icon.png | Bin 0 -> 1605 bytes src/main/resources/image/trezor-icon@2x.png | Bin 0 -> 1969 bytes 33 files changed, 517 insertions(+), 25 deletions(-) create mode 100644 src/main/java/com/sparrowwallet/sparrow/control/WalletIcon.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/event/PayNymImageLoadedEvent.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/event/WalletConfigChangedEvent.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/io/ImageUtils.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/io/db/WalletConfigDao.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/io/db/WalletConfigMapper.java create mode 100644 src/main/resources/com/sparrowwallet/sparrow/sql/V8__WalletConfig.sql create mode 100644 src/main/resources/image/bitbox02-icon.png create mode 100644 src/main/resources/image/bitbox02-icon@2x.png create mode 100644 src/main/resources/image/coldcard-icon.png create mode 100644 src/main/resources/image/coldcard-icon@2x.png create mode 100644 src/main/resources/image/ledger-icon.png create mode 100644 src/main/resources/image/ledger-icon@2x.png create mode 100644 src/main/resources/image/passport-icon.png create mode 100644 src/main/resources/image/passport-icon@2x.png create mode 100644 src/main/resources/image/seedsigner-icon.png create mode 100644 src/main/resources/image/seedsigner-icon@2x.png create mode 100644 src/main/resources/image/trezor-icon.png create mode 100644 src/main/resources/image/trezor-icon@2x.png diff --git a/build.gradle b/build.gradle index ecf20db6..fdc0659b 100644 --- a/build.gradle +++ b/build.gradle @@ -110,6 +110,7 @@ dependencies { implementation('net.sourceforge.streamsupport:streamsupport:1.7.0') implementation('com.github.librepdf:openpdf:1.3.27') implementation('com.googlecode.lanterna:lanterna:3.1.1') + implementation('net.coobird:thumbnailator:0.4.18') testImplementation('junit:junit:4.12') } @@ -579,6 +580,10 @@ extraJavaModuleInfo { module('jcip-annotations-1.0.jar', 'net.jcip.annotations', '1.0') { exports('net.jcip.annotations') } + module('thumbnailator-0.4.18.jar', 'net.coobird.thumbnailator', '0.4.18') { + exports('net.coobird.thumbnailator') + requires('java.desktop') + } module("netlayer-jpms-${osName}${targetName}-0.6.8.jar", 'netlayer.jpms', '0.6.8') { exports('org.berndpruenster.netlayer.tor') requires('com.github.ravn.jsocks') diff --git a/drongo b/drongo index 7c34ec7c..fa18ec9d 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit 7c34ec7c3b818634b7819f1b27a5f9c57f5554c8 +Subproject commit fa18ec9d458bb17221fb01b6be9f4eceb354a156 diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index 4a03ef2d..4344e888 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -1463,11 +1463,10 @@ public class AppController implements Initializable { wallet.setName(name); } Tab tab = new Tab(""); - Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WALLET); - glyph.setFontSize(10.0); - glyph.setOpacity(TAB_LABEL_GRAPHIC_OPACITY_ACTIVE); + WalletIcon walletIcon = new WalletIcon(storage, wallet); + walletIcon.setOpacity(TAB_LABEL_GRAPHIC_OPACITY_ACTIVE); Label tabLabel = new Label(name); - tabLabel.setGraphic(glyph); + tabLabel.setGraphic(walletIcon); tabLabel.setGraphicTextGap(5.0); tab.setGraphic(tabLabel); tab.setClosable(true); @@ -1856,17 +1855,61 @@ public class AppController implements Initializable { contextMenu.getItems().addAll(new SeparatorMenuItem(), close, closeOthers, closeAll); - if(tab.getUserData() instanceof WalletTabData) { + if(tab.getUserData() instanceof WalletTabData walletTabData) { + Menu walletIcon = new Menu("Wallet Icon"); + MenuItem custom = new MenuItem("Custom..."); + custom.setOnAction(event -> { + setCustomIcon(walletTabData.getWallet()); + }); + MenuItem reset = new MenuItem("Reset"); + reset.setOnAction(event -> { + resetIcon(walletTabData.getWalletForm()); + }); + walletIcon.getItems().addAll(custom, reset); + MenuItem delete = new MenuItem("Delete..."); delete.setOnAction(event -> { - deleteWallet(getSelectedWalletForm()); + deleteWallet(walletTabData.getWalletForm()); }); - contextMenu.getItems().addAll(new SeparatorMenuItem(), delete); + contextMenu.getItems().addAll(new SeparatorMenuItem(), walletIcon, delete); } return contextMenu; } + private void setCustomIcon(Wallet wallet) { + Stage window = new Stage(); + + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Open Image"); + fileChooser.getExtensionFilters().addAll( + new FileChooser.ExtensionFilter("All Files", org.controlsfx.tools.Platform.getCurrent().equals(org.controlsfx.tools.Platform.UNIX) ? "*" : "*.*"), + new FileChooser.ExtensionFilter("Images", "*.png", "*.jpg", "*.jpeg", "*.gif") + ); + + AppServices.moveToActiveWindowScreen(window, 800, 450); + File file = fileChooser.showOpenDialog(window); + if(file != null) { + try { + byte[] iconData = ImageUtils.resize(file, WalletIcon.SAVE_WIDTH, WalletIcon.SAVE_HEIGHT); + WalletConfig walletConfig = wallet.getMasterWalletConfig(); + walletConfig.setIconData(iconData, true); + EventManager.get().post(new WalletConfigChangedEvent(wallet)); + } catch(Exception e) { + log.error("Error creating custom wallet icon", e); + showErrorDialog("Error creating custom wallet icon", e.getMessage()); + } + } + } + + private void resetIcon(WalletForm walletForm) { + Wallet masterWallet = walletForm.getMasterWallet(); + if(masterWallet.getWalletConfig() != null && masterWallet.getWalletConfig().isUserIcon()) { + masterWallet.getWalletConfig().setIconData(null, false); + EventManager.get().post(new WalletConfigChangedEvent(masterWallet)); + } + } + private void deleteWallet(WalletForm selectedWalletForm) { Optional optButtonType = AppServices.showWarningDialog("Delete " + selectedWalletForm.getWallet().getMasterName() + "?", "The wallet file and any backups will be deleted. Are you sure?", ButtonType.NO, ButtonType.YES); if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) { @@ -2071,8 +2114,8 @@ public class AppController implements Initializable { private void tabLabelAddFailure(Tab tab) { Label tabLabel = (Label)tab.getGraphic(); - if(!tabLabel.getStyleClass().contains("failure")) { - tabLabel.getGraphic().getStyleClass().add("failure"); + WalletIcon walletIcon = (WalletIcon)tabLabel.getGraphic(); + if(walletIcon.addFailure()) { tabLabel.setTooltip(new Tooltip("Error loading transaction history from server")); } } @@ -2105,7 +2148,8 @@ public class AppController implements Initializable { private void tabLabelRemoveFailure(Tab tab) { Label tabLabel = (Label)tab.getGraphic(); - tabLabel.getGraphic().getStyleClass().remove("failure"); + WalletIcon walletIcon = (WalletIcon)tabLabel.getGraphic(); + walletIcon.removeFailure(); tabLabel.setTooltip(null); } @@ -2222,6 +2266,9 @@ public class AppController implements Initializable { Tab masterTab = subTabs.getTabs().stream().filter(tab -> ((WalletTabData)tab.getUserData()).getWallet().isMasterWallet()).findFirst().orElse(subTabs.getTabs().get(0)); Label masterLabel = (Label)masterTab.getGraphic(); masterLabel.setText(event.getWallet().getLabel() != null ? event.getWallet().getLabel() : event.getWallet().getAutomaticName()); + Label tabLabel = (Label)walletTab.getGraphic(); + WalletIcon walletIcon = (WalletIcon)tabLabel.getGraphic(); + walletIcon.setWallet(event.getWallet()); } } } @@ -2749,4 +2796,17 @@ public class AppController implements Initializable { lockAllWallets.setDisable(false); } } + + @Subscribe + public void walletConfigChanged(WalletConfigChangedEvent event) { + for(Tab tab : tabs.getTabs()) { + if(tab.getUserData() instanceof WalletTabData walletTabData) { + if(walletTabData.getWallet() == event.getWallet()) { + Label tabLabel = (Label)tab.getGraphic(); + WalletIcon walletIcon = (WalletIcon)tabLabel.getGraphic(); + walletIcon.refresh(); + } + } + } + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/SparrowDesktop.java b/src/main/java/com/sparrowwallet/sparrow/SparrowDesktop.java index 2936e45f..a5a3cd20 100644 --- a/src/main/java/com/sparrowwallet/sparrow/SparrowDesktop.java +++ b/src/main/java/com/sparrowwallet/sparrow/SparrowDesktop.java @@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow; import com.sparrowwallet.drongo.Network; import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.sparrow.control.WalletIcon; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands; import com.sparrowwallet.sparrow.io.Config; @@ -18,6 +19,7 @@ import org.controlsfx.tools.Platform; import org.slf4j.LoggerFactory; import java.io.File; +import java.net.URL; import java.util.*; import java.util.stream.Collectors; @@ -43,6 +45,7 @@ public class SparrowDesktop extends Application { GlyphFontRegistry.register(new FontAwesome5()); GlyphFontRegistry.register(new FontAwesome5Brands()); Font.loadFont(AppServices.class.getResourceAsStream("/font/RobotoMono-Regular.ttf"), 13); + URL.setURLStreamHandlerFactory(protocol -> WalletIcon.PROTOCOL.equals(protocol) ? new WalletIcon.WalletIconStreamHandler() : null); AppServices.initialize(this); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/PayNymAvatar.java b/src/main/java/com/sparrowwallet/sparrow/control/PayNymAvatar.java index 12910a1a..af15118a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/PayNymAvatar.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/PayNymAvatar.java @@ -2,6 +2,8 @@ package com.sparrowwallet.sparrow.control; import com.sparrowwallet.drongo.bip47.PaymentCode; import com.sparrowwallet.sparrow.AppServices; +import com.sparrowwallet.sparrow.EventManager; +import com.sparrowwallet.sparrow.event.PayNymImageLoadedEvent; import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.paynym.PayNymService; import javafx.beans.property.ObjectProperty; @@ -47,6 +49,7 @@ public class PayNymAvatar extends StackPane { }); payNymAvatarService.setOnSucceeded(successEvent -> { setImage(payNymAvatarService.getValue()); + EventManager.get().post(new PayNymImageLoadedEvent(paymentCode, payNymAvatarService.getValue())); }); payNymAvatarService.setOnFailed(failedEvent -> { log.debug("Error loading PayNym avatar", failedEvent.getSource().getException()); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WalletIcon.java b/src/main/java/com/sparrowwallet/sparrow/control/WalletIcon.java new file mode 100644 index 00000000..ca07cb94 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/WalletIcon.java @@ -0,0 +1,167 @@ +package com.sparrowwallet.sparrow.control; + +import com.sparrowwallet.drongo.wallet.*; +import com.sparrowwallet.sparrow.AppServices; +import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; +import com.sparrowwallet.sparrow.io.ImageUtils; +import com.sparrowwallet.sparrow.io.Storage; +import javafx.application.Platform; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.geometry.Pos; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.ImagePattern; +import javafx.scene.shape.Circle; +import org.controlsfx.glyphfont.Glyph; + +import java.io.*; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; + +public class WalletIcon extends StackPane { + public static final String PROTOCOL = "walleticon"; + private static final String QUERY = "icon"; + public static final int WIDTH = 15; + public static final int HEIGHT = 15; + public static final int SAVE_WIDTH = WIDTH * 2; + public static final int SAVE_HEIGHT = HEIGHT * 2; + + private final Storage storage; + private final ObjectProperty walletProperty = new SimpleObjectProperty<>(); + + public WalletIcon(Storage storage, Wallet wallet) { + super(); + this.storage = storage; + setPrefSize(WIDTH, HEIGHT); + walletProperty.addListener((observable, oldValue, newValue) -> { + refresh(); + }); + walletProperty.set(wallet); + } + + public void refresh() { + Wallet wallet = getWallet(); + + getChildren().clear(); + if(wallet.getWalletConfig() != null && wallet.getWalletConfig().getIconData() != null) { + String walletId = storage.getWalletId(wallet); + if(AppServices.get().getWallet(walletId) != null) { + addWalletIcon(walletId); + } else { + Platform.runLater(() -> addWalletIcon(walletId)); + } + } else if(wallet.getKeystores().size() == 1) { + Keystore keystore = wallet.getKeystores().get(0); + if(keystore.getSource() == KeystoreSource.HW_USB || keystore.getSource() == KeystoreSource.HW_AIRGAPPED) { + WalletModel walletModel = keystore.getWalletModel(); + + Image image = null; + try { + image = new Image("image/" + walletModel.getType() + "-icon.png", 15, 15, true, true); + } catch(Exception e) { + //ignore + } + + if(image == null) { + try { + image = new Image("image/" + walletModel.getType() + ".png", 15, 15, true, true); + } catch(Exception e) { + //ignore + } + } + + if(image != null && !image.isError()) { + ImageView imageView = new ImageView(image); + getChildren().add(imageView); + } + } + } + + if(getChildren().isEmpty()) { + Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WALLET); + glyph.setFontSize(10.0); + getChildren().add(glyph); + } + } + + private void addWalletIcon(String walletId) { + Image image = new Image(PROTOCOL + ":" + walletId + "?" + QUERY, WIDTH, HEIGHT, true, false); + getChildren().clear(); + Circle circle = new Circle(getPrefWidth() / 2,getPrefHeight() / 2,getPrefWidth() / 2); + circle.setFill(new ImagePattern(image)); + getChildren().add(circle); + } + + public boolean addFailure() { + if(getChildren().stream().noneMatch(node -> node.getStyleClass().contains("failure"))) { + Glyph failGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE); + failGlyph.setFontSize(10); + failGlyph.getStyleClass().add("failure"); + getChildren().add(failGlyph); + StackPane.setAlignment(failGlyph, Pos.TOP_RIGHT); + failGlyph.setTranslateX(5); + failGlyph.setTranslateY(-4); + return true; + } + + return false; + } + + public void removeFailure() { + getChildren().removeIf(node -> node.getStyleClass().contains("failure")); + } + + public Wallet getWallet() { + return walletProperty.get(); + } + + public ObjectProperty walletProperty() { + return walletProperty; + } + + public void setWallet(Wallet wallet) { + this.walletProperty.set(wallet); + } + + public static class WalletIconStreamHandler extends URLStreamHandler { + @Override + protected URLConnection openConnection(URL url) throws IOException { + return new URLConnection(url) { + @Override + public void connect() throws IOException { + //Nothing required + } + + @Override + public InputStream getInputStream() throws IOException { + String walletId = url.getPath(); + String query = url.getQuery(); + + Wallet wallet = AppServices.get().getWallet(walletId); + if(wallet == null) { + throw new IOException("Cannot find wallet for wallet id " + walletId); + } + + if(query.startsWith(QUERY)) { + if(wallet.getWalletConfig() == null || wallet.getWalletConfig().getIconData() == null) { + throw new IOException("No icon data for " + walletId); + } + + ByteArrayInputStream bais = new ByteArrayInputStream(wallet.getWalletConfig().getIconData()); + if(query.endsWith("@2x")) { + return bais; + } else { + return ImageUtils.resize(bais, WalletIcon.WIDTH, WalletIcon.HEIGHT); + } + } + + throw new MalformedURLException("Cannot load url " + url); + } + }; + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/PayNymImageLoadedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/PayNymImageLoadedEvent.java new file mode 100644 index 00000000..00d696ef --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/PayNymImageLoadedEvent.java @@ -0,0 +1,22 @@ +package com.sparrowwallet.sparrow.event; + +import com.sparrowwallet.drongo.bip47.PaymentCode; +import javafx.scene.image.Image; + +public class PayNymImageLoadedEvent { + private final PaymentCode paymentCode; + private final Image image; + + public PayNymImageLoadedEvent(PaymentCode paymentCode, Image image) { + this.paymentCode = paymentCode; + this.image = image; + } + + public PaymentCode getPaymentCode() { + return paymentCode; + } + + public Image getImage() { + return image; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/WalletConfigChangedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/WalletConfigChangedEvent.java new file mode 100644 index 00000000..9b1da3fe --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/WalletConfigChangedEvent.java @@ -0,0 +1,9 @@ +package com.sparrowwallet.sparrow.event; + +import com.sparrowwallet.drongo.wallet.Wallet; + +public class WalletConfigChangedEvent extends WalletChangedEvent { + public WalletConfigChangedEvent(Wallet wallet) { + super(wallet); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/ImageUtils.java b/src/main/java/com/sparrowwallet/sparrow/io/ImageUtils.java new file mode 100644 index 00000000..e85c9537 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/io/ImageUtils.java @@ -0,0 +1,56 @@ +package com.sparrowwallet.sparrow.io; + +import javafx.embed.swing.SwingFXUtils; +import javafx.scene.image.Image; +import net.coobird.thumbnailator.Thumbnails; + +import java.awt.image.BufferedImage; +import java.io.*; + +public class ImageUtils { + public static byte[] resize(Image image, int width, int height) { + return resize(SwingFXUtils.fromFXImage(image, null), width, height); + } + + public static byte[] resize(BufferedImage image, int width, int height) { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + resize(image, baos, width, height); + return baos.toByteArray(); + } catch(IOException e) { + throw new RuntimeException(e); + } + } + + public static void resize(BufferedImage image, OutputStream outputStream, int width, int height) throws IOException { + resize(Thumbnails.of(image), outputStream, width, height); + } + + public static byte[] resize(File file, int width, int height) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + resize(file, baos, width, height); + return baos.toByteArray(); + } + + public static void resize(File file, OutputStream outputStream, int width, int height) throws IOException { + resize(Thumbnails.of(file), outputStream, width, height); + } + + public static InputStream resize(InputStream inputStream, int width, int height) { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + resize(inputStream, baos, width, height); + return new ByteArrayInputStream(baos.toByteArray()); + } catch(IOException e) { + throw new RuntimeException(e); + } + } + + public static void resize(InputStream inputStream, OutputStream outputStream, int width, int height) throws IOException { + resize(Thumbnails.of(inputStream), outputStream, width, height); + } + + private static void resize(Thumbnails.Builder builder, OutputStream outputStream, int width, int height) throws IOException { + builder.size(width, height).outputFormat("png").outputQuality(1).toOutputStream(outputStream); + } +} 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 ff8e9da5..7e590199 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java @@ -315,6 +315,11 @@ public class DbPersistence implements Persistence { } } + if(dirtyPersistables.walletConfig) { + WalletConfigDao walletConfigDao = handle.attach(WalletConfigDao.class); + walletConfigDao.addOrUpdate(wallet, wallet.getWalletConfig()); + } + if(dirtyPersistables.mixConfig) { MixConfigDao mixConfigDao = handle.attach(MixConfigDao.class); mixConfigDao.addOrUpdate(wallet, wallet.getMixConfig()); @@ -745,6 +750,13 @@ public class DbPersistence implements Persistence { } } + @Subscribe + public void walletConfigChanged(WalletConfigChangedEvent event) { + if(persistsFor(event.getWallet()) && event.getWallet().getWalletConfig() != null) { + updateExecutor.execute(() -> dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).walletConfig = true); + } + } + @Subscribe public void walletMixConfigChanged(WalletMixConfigChangedEvent event) { if(persistsFor(event.getWallet()) && event.getWallet().getMixConfig() != null) { @@ -793,6 +805,7 @@ public class DbPersistence implements Persistence { public Integer watchLast = null; public final List labelEntries = new ArrayList<>(); public final List utxoStatuses = new ArrayList<>(); + public boolean walletConfig; public boolean mixConfig; public final Map changedUtxoMixes = new HashMap<>(); public final Map removedUtxoMixes = new HashMap<>(); @@ -812,6 +825,7 @@ public class DbPersistence implements Persistence { "\nAddress labels:" + labelEntries.stream().filter(entry -> entry instanceof NodeEntry).map(entry -> ((NodeEntry)entry).getNode().toString() + " " + entry.getLabel()).collect(Collectors.toList()) + "\nUTXO labels:" + labelEntries.stream().filter(entry -> entry instanceof HashIndexEntry).map(entry -> ((HashIndexEntry)entry).getHashIndex().toString()).collect(Collectors.toList()) + "\nUTXO statuses:" + utxoStatuses + + "\nWallet config:" + walletConfig + "\nMix config:" + mixConfig + "\nUTXO mixes changed:" + changedUtxoMixes + "\nUTXO mixes removed:" + removedUtxoMixes + diff --git a/src/main/java/com/sparrowwallet/sparrow/io/db/WalletConfigDao.java b/src/main/java/com/sparrowwallet/sparrow/io/db/WalletConfigDao.java new file mode 100644 index 00000000..fc750cfd --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/io/db/WalletConfigDao.java @@ -0,0 +1,36 @@ +package com.sparrowwallet.sparrow.io.db; + +import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.drongo.wallet.WalletConfig; +import org.jdbi.v3.sqlobject.config.RegisterRowMapper; +import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys; +import org.jdbi.v3.sqlobject.statement.SqlQuery; +import org.jdbi.v3.sqlobject.statement.SqlUpdate; + +public interface WalletConfigDao { + @SqlQuery("select id, iconData, userIcon, usePayNym from walletConfig where wallet = ?") + @RegisterRowMapper(WalletConfigMapper.class) + WalletConfig getForWalletId(Long id); + + @SqlUpdate("insert into walletConfig (iconData, userIcon, usePayNym, wallet) values (?, ?, ?, ?)") + @GetGeneratedKeys("id") + long insertWalletConfig(byte[] iconData, boolean userIcon, boolean usePayNym, long wallet); + + @SqlUpdate("update walletConfig set iconData = ?, userIcon = ?, usePayNym = ?, wallet = ? where id = ?") + void updateWalletConfig(byte[] iconData, boolean userIcon, boolean usePayNym, long wallet, long id); + + default void addWalletConfig(Wallet wallet) { + if(wallet.getWalletConfig() != null) { + addOrUpdate(wallet, wallet.getWalletConfig()); + } + } + + default void addOrUpdate(Wallet wallet, WalletConfig walletConfig) { + if(walletConfig.getId() == null) { + long id = insertWalletConfig(walletConfig.getIconData(), walletConfig.isUserIcon(), walletConfig.isUsePayNym(), wallet.getId()); + walletConfig.setId(id); + } else { + updateWalletConfig(walletConfig.getIconData(), walletConfig.isUserIcon(), walletConfig.isUsePayNym(), wallet.getId(), walletConfig.getId()); + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/db/WalletConfigMapper.java b/src/main/java/com/sparrowwallet/sparrow/io/db/WalletConfigMapper.java new file mode 100644 index 00000000..d172495c --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/io/db/WalletConfigMapper.java @@ -0,0 +1,21 @@ +package com.sparrowwallet.sparrow.io.db; + +import com.sparrowwallet.drongo.wallet.WalletConfig; +import org.jdbi.v3.core.mapper.RowMapper; +import org.jdbi.v3.core.statement.StatementContext; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class WalletConfigMapper implements RowMapper { + @Override + public WalletConfig map(ResultSet rs, StatementContext ctx) throws SQLException { + byte[] iconData = rs.getBytes("iconData"); + boolean userIcon = rs.getBoolean("userIcon"); + boolean usePayNym = rs.getBoolean("usePayNym"); + + WalletConfig walletConfig = new WalletConfig(iconData, userIcon, usePayNym); + walletConfig.setId(rs.getLong("id")); + return walletConfig; + } +} 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 fc53e237..82a20882 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/db/WalletDao.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/db/WalletDao.java @@ -1,10 +1,7 @@ package com.sparrowwallet.sparrow.io.db; import com.sparrowwallet.drongo.protocol.Sha256Hash; -import com.sparrowwallet.drongo.wallet.BlockTransaction; -import com.sparrowwallet.drongo.wallet.UtxoMixData; -import com.sparrowwallet.drongo.wallet.Wallet; -import com.sparrowwallet.drongo.wallet.WalletNode; +import com.sparrowwallet.drongo.wallet.*; import org.jdbi.v3.sqlobject.CreateSqlObject; import org.jdbi.v3.sqlobject.config.RegisterRowMapper; import org.jdbi.v3.sqlobject.customizer.Bind; @@ -33,6 +30,9 @@ public interface WalletDao { @CreateSqlObject DetachedLabelDao createDetachedLabelDao(); + @CreateSqlObject + WalletConfigDao createWalletConfigDao(); + @CreateSqlObject MixConfigDao createMixConfigDao(); @@ -118,6 +118,7 @@ public interface WalletDao { Map detachedLabels = createDetachedLabelDao().getAll(); wallet.getDetachedLabels().putAll(detachedLabels); + wallet.setWalletConfig(createWalletConfigDao().getForWalletId(wallet.getId())); wallet.setMixConfig(createMixConfigDao().getForWalletId(wallet.getId())); Map utxoMixes = createUtxoMixDataDao().getForWalletId(wallet.getId()); @@ -136,6 +137,7 @@ public interface WalletDao { createWalletNodeDao().addWalletNodes(wallet); createBlockTransactionDao().addBlockTransactions(wallet); createDetachedLabelDao().clearAndAddAll(wallet); + createWalletConfigDao().addWalletConfig(wallet); createMixConfigDao().addMixConfig(wallet); createUtxoMixDataDao().addUtxoMixData(wallet); } finally { diff --git a/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymController.java b/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymController.java index 538afb22..0f23027d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymController.java +++ b/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymController.java @@ -165,7 +165,7 @@ public class PayNymController { followersList.setSelectionModel(new NoSelectionModel<>()); followersList.setFocusTraversable(false); - if(Config.get().isUsePayNym() && AppServices.isConnected() && masterWallet.hasPaymentCode()) { + if(isUsePayNym(masterWallet) && AppServices.isConnected() && masterWallet.hasPaymentCode()) { refresh(); } else { payNymName.setVisible(false); @@ -260,9 +260,9 @@ public class PayNymController { } public void retrievePayNym(ActionEvent event) { - Config.get().setUsePayNym(true); PayNymService payNymService = AppServices.getPayNymService(); Wallet masterWallet = getMasterWallet(); + setUsePayNym(masterWallet, true); payNymService.createPayNym(masterWallet).subscribe(createMap -> { payNymName.setText((String)createMap.get("nymName")); payNymAvatar.setPaymentCode(masterWallet.getPaymentCode()); @@ -630,6 +630,31 @@ public class PayNymController { return wallet.isMasterWallet() ? wallet : wallet.getMasterWallet(); } + protected boolean isUsePayNym(Wallet wallet) { + //TODO: Remove config setting + boolean usePayNym = Config.get().isUsePayNym(); + if(usePayNym && wallet != null) { + setUsePayNym(wallet, true); + } + + return usePayNym; + } + + protected void setUsePayNym(Wallet wallet, boolean usePayNym) { + //TODO: Remove config setting + if(Config.get().isUsePayNym() != usePayNym) { + Config.get().setUsePayNym(usePayNym); + } + + if(wallet != null) { + WalletConfig walletConfig = wallet.getMasterWalletConfig(); + if(walletConfig.isUsePayNym() != usePayNym) { + walletConfig.setUsePayNym(usePayNym); + EventManager.get().post(new WalletConfigChangedEvent(wallet.isMasterWallet() ? wallet : wallet.getMasterWallet())); + } + } + } + public boolean isSelectLinkedOnly() { return selectLinkedOnly; } diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/CounterpartyController.java b/src/main/java/com/sparrowwallet/sparrow/soroban/CounterpartyController.java index 8f4d16a4..6a342ad1 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/CounterpartyController.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/CounterpartyController.java @@ -174,7 +174,7 @@ public class CounterpartyController extends SorobanController { payNymAvatar.visibleProperty().bind(payNym.visibleProperty()); payNymButton.managedProperty().bind(payNymButton.visibleProperty()); payNymButton.visibleProperty().bind(payNym.visibleProperty().not()); - if(Config.get().isUsePayNym()) { + if(isUsePayNym(wallet)) { retrievePayNym(null); } else { payNym.setVisible(false); @@ -270,7 +270,7 @@ public class CounterpartyController extends SorobanController { private void updateMixPartner(PaymentCode paymentCodeInitiator, CahootsType cahootsType) { String code = paymentCodeInitiator.toString(); mixingPartner.setText(code.substring(0, 12) + "..." + code.substring(code.length() - 5)); - if(Config.get().isUsePayNym()) { + if(isUsePayNym(wallet)) { mixPartnerAvatar.setPaymentCode(paymentCodeInitiator); AppServices.getPayNymService().getPayNym(paymentCodeInitiator.toString()).subscribe(payNym -> { mixingPartner.setText(payNym.nymName()); @@ -351,7 +351,7 @@ public class CounterpartyController extends SorobanController { } private void followPaymentCode(PaymentCode paymentCodeInitiator) { - if(Config.get().isUsePayNym()) { + if(isUsePayNym(wallet)) { PayNymService payNymService = AppServices.getPayNymService(); payNymService.getAuthToken(wallet, new HashMap<>()).subscribe(authToken -> { String signature = payNymService.getSignature(wallet, authToken); @@ -393,7 +393,7 @@ public class CounterpartyController extends SorobanController { } public void retrievePayNym(ActionEvent event) { - Config.get().setUsePayNym(true); + setUsePayNym(wallet, true); PayNymService payNymService = AppServices.getPayNymService(); payNymService.createPayNym(wallet).subscribe(createMap -> { diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java b/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java index bfa6e855..ac294af5 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java @@ -158,7 +158,7 @@ public class InitiatorController extends SorobanController { if(newValue != null) { if(newValue.startsWith("P") && newValue.contains("...") && newValue.length() == 20 && counterpartyPaymentCode.get() != null) { //Assumed valid payment code - } else if(Config.get().isUsePayNym() && PAYNYM_REGEX.matcher(newValue).matches()) { + } else if(isUsePayNym(wallet) && PAYNYM_REGEX.matcher(newValue).matches()) { if(!newValue.equals(counterpartyPayNymName.get())) { searchPayNyms(newValue); } @@ -233,7 +233,7 @@ public class InitiatorController extends SorobanController { payNymFollowers.prefWidthProperty().bind(counterparty.widthProperty()); payNymFollowers.valueProperty().addListener((observable, oldValue, payNym) -> { if(payNym == FIND_FOLLOWERS) { - Config.get().setUsePayNym(true); + setUsePayNym(wallet, true); setPayNymFollowers(); } else if(payNym != null) { counterpartyPayNymName.set(payNym.nymName()); @@ -349,7 +349,7 @@ public class InitiatorController extends SorobanController { payNymFollowers.setItems(FXCollections.observableList(followerPayNyms)); }, error -> { if(error.getMessage().endsWith("404")) { - Config.get().setUsePayNym(false); + setUsePayNym(masterWallet, false); AppServices.showErrorDialog("Could not retrieve PayNym", "This wallet does not have an associated PayNym or any followers yet. You can retrieve the PayNym using the Find PayNym button."); } else { log.warn("Could not retrieve followers: ", error); diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/SorobanController.java b/src/main/java/com/sparrowwallet/sparrow/soroban/SorobanController.java index f0e75a08..648645a3 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/SorobanController.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/SorobanController.java @@ -10,7 +10,10 @@ import com.sparrowwallet.drongo.protocol.TransactionOutput; import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.psbt.PSBTParseException; import com.sparrowwallet.drongo.wallet.*; +import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.control.TransactionDiagram; +import com.sparrowwallet.sparrow.event.WalletConfigChangedEvent; +import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.net.ElectrumServer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -126,4 +129,29 @@ public class SorobanController { Taskbar.getTaskbar().requestUserAttention(true, false); } } + + protected boolean isUsePayNym(Wallet wallet) { + //TODO: Remove config setting + boolean usePayNym = Config.get().isUsePayNym(); + if(usePayNym && wallet != null) { + setUsePayNym(wallet, true); + } + + return usePayNym; + } + + protected void setUsePayNym(Wallet wallet, boolean usePayNym) { + //TODO: Remove config setting + if(Config.get().isUsePayNym() != usePayNym) { + Config.get().setUsePayNym(usePayNym); + } + + if(wallet != null) { + WalletConfig walletConfig = wallet.getMasterWalletConfig(); + if(walletConfig.isUsePayNym() != usePayNym) { + walletConfig.setUsePayNym(usePayNym); + EventManager.get().post(new WalletConfigChangedEvent(wallet.isMasterWallet() ? wallet : wallet.getMasterWallet())); + } + } + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java index b70d8978..5ce2ac7e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java @@ -22,6 +22,7 @@ import com.sparrowwallet.sparrow.control.*; import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.io.Config; +import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.net.ExchangeSource; import com.sparrowwallet.sparrow.paynym.PayNym; import com.sparrowwallet.sparrow.paynym.PayNymAddress; @@ -39,6 +40,7 @@ import javafx.collections.ListChangeListener; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.Initializable; +import javafx.scene.Node; import javafx.scene.control.*; import org.controlsfx.glyphfont.Glyph; import org.controlsfx.validation.ValidationResult; @@ -176,7 +178,7 @@ public class PaymentController extends WalletFormController implements Initializ setGraphic(null); } else { setText(wallet.getFullDisplayName() + (wallet == sendController.getWalletForm().getWallet() ? " (Consolidation)" : "")); - setGraphic(wallet == payNymWallet ? getPayNymGlyph() : null); + setGraphic(getOpenWalletIcon(wallet)); } } }); @@ -325,6 +327,20 @@ public class PaymentController extends WalletFormController implements Initializ openWallets.setItems(FXCollections.observableList(openWalletList)); } + private Node getOpenWalletIcon(Wallet wallet) { + if(wallet == payNymWallet) { + return getPayNymGlyph(); + } + + Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet(); + Storage storage = AppServices.get().getOpenWallets().get(masterWallet); + if(storage != null) { + return new WalletIcon(storage, masterWallet); + } + + return null; + } + private void addValidation(ValidationSupport validationSupport) { this.validationSupport = validationSupport; diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java index 0430a35f..dd459276 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java @@ -7,7 +7,9 @@ import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.WalletTabData; +import com.sparrowwallet.sparrow.control.WalletIcon; import com.sparrowwallet.sparrow.event.*; +import com.sparrowwallet.sparrow.io.ImageUtils; import com.sparrowwallet.sparrow.io.StorageException; import com.sparrowwallet.sparrow.net.AllHistoryChangedException; import com.sparrowwallet.sparrow.net.ElectrumServer; @@ -561,6 +563,13 @@ public class WalletForm { } } + @Subscribe + public void walletConfigChanged(WalletConfigChangedEvent event) { + if(event.getWallet() == wallet) { + Platform.runLater(() -> EventManager.get().post(new WalletDataChangedEvent(wallet))); + } + } + @Subscribe public void walletMixConfigChanged(WalletMixConfigChangedEvent event) { if(event.getWallet() == wallet) { @@ -660,4 +669,18 @@ public class WalletForm { } } } + + @Subscribe + public void payNymImageLoaded(PayNymImageLoadedEvent event) { + if(wallet.isMasterWallet() && wallet.hasPaymentCode() && event.getPaymentCode().equals(wallet.getPaymentCode())) { + WalletConfig walletConfig = wallet.getMasterWalletConfig(); + if(!walletConfig.isUserIcon()) { + byte[] iconData = ImageUtils.resize(event.getImage(), WalletIcon.SAVE_WIDTH, WalletIcon.SAVE_HEIGHT); + if(walletConfig.getIconData() == null || !Arrays.equals(walletConfig.getIconData(), iconData)) { + walletConfig.setIconData(iconData, false); + EventManager.get().post(new WalletConfigChangedEvent(wallet)); + } + } + } + } } diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 305ce10e..e7e53e55 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -46,4 +46,5 @@ open module com.sparrowwallet.sparrow { requires co.nstant.in.cbor; requires com.github.librepdf.openpdf; requires com.googlecode.lanterna; + requires net.coobird.thumbnailator; } \ No newline at end of file diff --git a/src/main/resources/com/sparrowwallet/sparrow/sql/V8__WalletConfig.sql b/src/main/resources/com/sparrowwallet/sparrow/sql/V8__WalletConfig.sql new file mode 100644 index 00000000..b2df69ad --- /dev/null +++ b/src/main/resources/com/sparrowwallet/sparrow/sql/V8__WalletConfig.sql @@ -0,0 +1 @@ +create table walletConfig (id identity not null, iconData varbinary(4096), userIcon boolean, usePayNym boolean, wallet bigint not null); \ No newline at end of file diff --git a/src/main/resources/image/bitbox02-icon.png b/src/main/resources/image/bitbox02-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e143f6626af1f2910d709d883947849574f6e094 GIT binary patch literal 550 zcmV+>0@?kEP)%$ zhftTmK|n$!C~4Z%fL@G)2~|#WUi8vCuX$gmhHw!D7oX|N!}HWK5%ZVNj zdpj1pbbWgIhA%P_nNM6DKPnbqHQia(ZDcZ;Ywzo|b%bz%=Z9+L-Q9;nf{@g8J>m2D zzV&CZ8@4QqcN_;WMmEQBgb?!6wryOkR(W@}2dC3%3;^Ibj)f2c3KRo$0U?ADg=0d9 z^Cy@907RqFlLiN4j#5gyyoclC@%RAT8%$9YzhztLN@f3n(Kb$tg80cF4Aef<>&YLc z8G62*y*x8>)7ja1`DUP5(=_?upt7ue(IS#0L1g5-GMAdm2mFE89LvF5Rh`<~uRQK= z?yb!*3@3_G^K3Z0xw5jFEffl8HlA)ImX_`=HyVxL*0Zg};bHM&Ka5U}a=CmipI5Gw zN_$~deK%!x%t=Csz_DyS7z%C6vRsOd$;RyLt?ak|``i)(9-o*;xQ o0HE&G2Sh2ws;YYa8vIxN0wbxz^i~q~GXMYp07*qoM6N<$f=6QijsO4v literal 0 HcmV?d00001 diff --git a/src/main/resources/image/bitbox02-icon@2x.png b/src/main/resources/image/bitbox02-icon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..b70914ff8a77c16e34f13733fe264d41a2cd1162 GIT binary patch literal 1277 zcmVBf9*Ef-rjqU59^2zE30u2CUTzc&F}oq z=bZ1ozjJ=TL;K%bb#=9vd}ikKnon!q^?JSK0O0j{w;nro>}_Ut%rut(AP@++4rIVCD)UGNuEIY1SD4Mutah3oiclzQPa$SXU;qFC1`$DsNp~h-LKOg*l!gQulgZ8q7)WS7 zokt>(83dT?)1!4wFW&t4WUx9DmzO4X|C%i<29-uZevk(GSi{%~> zMF6CEy1xLmI2fWfRtT=t8?dtQUX@H0@QF-DsBz6;#WEci~mwju>%ilbE;=~u*nc1EUnzZ7E zhKABjn>OzA2mH%Ip^&0!8YD?#0J^U0QryrdveQl0^*D&gkfby*FfbrrZf#xLd9!QH zndauI+S=OBD=RB&QU-T+cD(Y__4X&(1W1rcMDb`eD#pjh(;W^6?6YS-0suDKtPELJ zFf=q|6+-BeB*k?@Pa7T{l7hj$rB3Ifasa7=4b!BFHqG~k<>SiE=%+-~>v z++61g0C2h7$BT=fcKiK*u2xmmIy5wBHBHvj)6>V9*%pt-lm5I@f=xq@hQnbfvXY*e znT4#(tXOXD!ZS{%vv&Xf{Vf2%%uXU|1@P>VBS(t6yZ_i3=n1?U4EEVnwcj>2HVV@; zqf^CC3TDv!{3ZEtE_B|>%F1qVxw609y}POl0DL~*nv*BLerv#Z~J-ei)+8$ zym|At04_H*HGSB6rFC0>P%Z1|?8vuRC@UFw%EQ^%)cC=TYd1y?9XeD`M5D}Xsjsgu zYq`*}wY$6fsiDEaIgv;NvMj@Hv&R-KTGX*>)yi+Gs^0sGhz6LM_Uzfcv$)vv_?9hO z-boqE%nAU_=kvMFpFdx6v#VpHKM;5#916?PXcWv0A~F+0f+SgjZI%sL+1bD5=jVUF zqIgAP*=uE804zi_ma^1D6pch8&z(DW_OnYrUMlqa{fNb4kYzb4%aYYFO{-~`!eX_+ zA}od$)8f6MURmu|3xdJEeTHG|D99_=wS4*VQ^}-GdJ#|*rETfbWtFj*Rx)?4W234D zovNxT;qWMAS=JOqv1pp6kByGXilSI@7v^IAyagk9uDtIGA1iEhI2^5s%fyuJE~PQ% z^Z6EEym)c_t>1omy(iGKvbVPvgM)*x+wI87$w78@&hPF#_op2qTHDs nr_7wz-Pi3*Ot6Rc-?qO1byy&(1w!OG00000NkvXXu0mjf=7w>; literal 0 HcmV?d00001 diff --git a/src/main/resources/image/coldcard-icon.png b/src/main/resources/image/coldcard-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2a5b2a4f7562e170b7eb2c339f5c18f75114df61 GIT binary patch literal 287 zcmV+)0pR|LP)EfuR&VU>hr_;iBjY z4IE$(`2?eR@-A+$gH;SBr-t(~ax>Hx zHnAS+GZykmubBEH*9ygTJVlS;2ut{k!6UvfTt*(iTujbzoc<2A$1{=MuX6uy&Lf!D l9?ehADjm9tOT6gb_ytHxEx?e#Px=4=002ovPDHLkV1oI0c(VWi literal 0 HcmV?d00001 diff --git a/src/main/resources/image/coldcard-icon@2x.png b/src/main/resources/image/coldcard-icon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0a8dd88e8b7ee304a754789c6f0a45fb4fffa1b0 GIT binary patch literal 741 zcmVuQIOJy@TCVq5D`IwjL4UI zQwS;v6167p7sMMT9=muKV@4dund8G+;_%abMZR%FHy1gLl zS7Utw+R&4G2Ijp7y=cg_hq>NZ;rlT6^q~cpaHbldv3ddT;Z-!_9lVUca1&!OBmK@} zG|ISx2k6YTcD#k_7@24MQwh>g06Yz+Fb9*d93yZW^Dza>@IT(ptp337IFvpgWY7z7 z0<)??ihysY<0sgGKk+6$Ptc7RhE z7rQYEck}A2@V|%O@H38}Rn+~t!1ADBqL#0tE1zG=?<4YkK!I<649Vx=LIcl;xJX>n zg>aT-R_hY*B0du0St^(qjj0%iMYw`_LRb^=KEBF*`=19q3p<61+fl;Tg5rLn42Q7| zH!uiIndONLuvv7*xA-9g*jEWsB(BH7ltjHsxFzV`Dq3qx26iX!4Vh6#zTeILmkZ9X znQ?n&wk;1>AZpct^;jYX%rE!??`8lWi$SqkP~DA{I3P;#6xBuI>cUJx;b)>(yYVU3 z;@5mWCfe+$4Ct8X^EP~s%{VJsuv`sNOVtAfrT^t6Gw%|0?ZkM|^OLY54?ij<-xV>* zE-Ef(P2f&J=Y%}yhG?PF_!6(AZwp=%L#0_vuEC;QZ)8wAF|-<>D4JG5-$`u18li=f zsM7`P!8!aZgf>qoeRyU)6WcS;4h8*QB(7F5K+a`mhw!6N_%$JGZ|Q*>T7^B zh^Yf2V01ucMQToNVo83HLO@Zzf{~tyo*7V{WdaM>3=trE(*j0>{R^1j=J79JMzDeE z7>o>zjI0a|fk?s7*vi1d%D{*r=`q)JpcH3;M`SSrgPt-7Ggd6MFJoX}-jo>G?WUP)qwZeFo6#1NP{E~&-IMVSR9nfZANAafIw@=Hr>m6Sjh z!2!gbsTG+BoAQdG-U511A0(r1sAr&$O&id&aQC6;sz@xrsi`D04ToJI8HhG;U_jhx z19E{CIKm@cGILY&ih=&IGc>gUI}Jku;W;FY5x6uWNg!ziddteWC>4~vLh|!-?69Ra z8+~+D*bGQ@Ni0dV1EnxsLqi}6fu%5Q10yQ~10+KrD$&gF1tyu4#F9iTrWjfR6D+bR zC~A?+0R^g6L{MsReoiSUDcBhsfPuath_cbgkVn^x&>EVTSz>1d)q*CBt}7BDkJUzG zd33%0MVYCLyWDE?cR&0n+gdCm8l=ku!lMN;F`{#oweG-o{Yd5_!9dbY*l&FpuK@T_VoE}wT^zr{vZ2P>eqHhPRmpc7cZWXv3k`l*07ld*=ui2>lZ%$ zQEqK+qhf2?%dWj%Z+`zfvhA((jmBk}%(wbA3qsaA*lf|!embFP-SqIvo=}(59r2e; zOe1%v>@7O)as8{QmDeGInkH%Q($dO5ZJD#$Rb&DlG*&Rrd33tGl70G{wZ)%GC#h&| z4BFajzichH(32Yb4@)1+JYIcR{-AwR@w1qo&b{_(KLV#EJdID=vcYfp=7#l!s+Ntv dN;saU#V`mX>0 literal 0 HcmV?d00001 diff --git a/src/main/resources/image/ledger-icon@2x.png b/src/main/resources/image/ledger-icon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a46bcf1985dd4875e1a857bfd3149a92de1e29c4 GIT binary patch literal 1875 zcmeAS@N?(olHy`uVBq!ia0vp^av;pX1|+Qw)-3{3jKx9jP7LeL$-D$|j-^I;ruq6Z zXaU(A3~Y=-49p-UK*+!-#lQ+?GcbfPO2gT4j2ciiOh7e;3_y}W6o}K>GZ|Q*>T7^B zh^Yf2V01ucMQToNVo83HLO@Zzf{~tyo*7V{WdaM>3=trE(*j0>{R^1j=E*H!MzDeE z7>o>zjI0a|fk?s7*vi1d%D{-BcJnuoJ2(qGB8wRq^pruEv0|xx83P0Jrp%Ctk_cZP ztK|G#y~LFKq*T3%+yanE3^o;3KxS@gNuokUZcbjYRfVk*ScMgk4HDK@QUEI{$+lIB z@C{IK&M!(;Fw-;8Gf=YQQczH^DN0GR3UYCSY6tRcl`=|73as??%gf94%8m8%i_-NC zEiEne4UF`SjC6r2bc-wVN)jt{^NN)rhQQ2mNi9w;$}A|!%+FH*nVXoDUs__Tqy(}E z4j}GKt;j^!lvfP(7SMzGAQ^o_Jp+Ag+JK&gyAMTIMPdO?O(mIWIP3z+K(v7a1L8&- zkPEE95gzH1nVXtd4D^qkv4IWPX&4d+&mn1yz@-sM0!bs#TUO3Rsi5Q)lAoVrhb^_) z=%cH`WCTXy14WZT{i$T@f=z~%%DoeP4rJL)atj8=0DB-BUAN zrpKm~ZxjqU;>A5t<%~+CLGxsvNAKmo>1{r+^-WCw zqggk77RoY_NE1Bsf$=~Avzwm|Uliw%A6<(i8+r064<@?oFU(HqEu)95{$ZToJDG@W~ z(w}E8KHnJoh-v%7YpbjpL*tslp2_?(pUe=hsNJ^t%Bz=kmFWyIDclEdt)JQav|XYx z+PI7N(*T6B~p#YF^}A^YW^~OpYo!BaP~`{Tk&=#uv9s^i{JlRLcHrys3X` z?g56m4?jGad^1u+M>2ozjwOc+PoC>}neTG#OQt(}AA9!bXU)Fl64U?Af3dbd+I(e*R(4A1#XDX7C$HD- z-+wd5%=_@e54&Pg78?oPe=0Wh#g(kBpMS(oi!!@bKJP(xg;sX@O3llmOmf;$-G7a5 zgo(H=XQ=MTQLF1;Ufx^lzTn1}<4JAbR6ntaIJQp{p7D=OX3@b5?D3~l6eHPZA3oaa z`{#h$p7VBXL8n=k^Q8O`d&+*oYU;{{Esxlz*Z1Ax%Gu=EdhvMN#)v!5nD!g~Yq{5& zxFcFa#HMQhuJyl}{vX-bx<6iTy8q_u_P5w>+I_067q+|_U&5|B=}pwCuBb)K$0nRw wxmQy^DUD^N(Xun2wlXb>Uzf_-x1Fh;;W@)a%Xe2Lw}9#ePgg&ebxsLQ0Qj_l_y7O^ literal 0 HcmV?d00001 diff --git a/src/main/resources/image/passport-icon.png b/src/main/resources/image/passport-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..24cb3fbb348476ecac54a76eb5810814842d56ae GIT binary patch literal 558 zcmV+}0@3}6P)i~!=k$9KLRK zyRG`Lu6UkD8;yq4Znv>otqxq*rQd6{9|x}c+iA5v{rBgyb90-BaMK_7hlmJ5{HrrY zfJ;0~B>MShz5Zm3mnNHKS+2=kd61Nn86gA+M4*HKr4)i-xHA~^Uryr5l#hr=*VoqW w0FsM{M>(K$hf(_NU8z(igou&I0#4Na0#V+x{&uGli2wiq07*qoM6N<$f_O#=@c;k- literal 0 HcmV?d00001 diff --git a/src/main/resources/image/passport-icon@2x.png b/src/main/resources/image/passport-icon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..999695593d134663074cc7bd11248696f7874d64 GIT binary patch literal 1487 zcmV;=1u*)FP)P%Dl3xgzP9

VuyJu`h3(3F!`N14e z>^1kl0W$y#0Eb2n9cQ-RU{+5$+Ifr=_4}V>Pp{=`)kl&8myF18SrmUMt(j%fe_H%- z@vj>;G=Oq9<7z)>yWOtTg6a>5>ztSwdQ)gUa!rKFc^9yBIvr0+^D8ENixndX zgFKF7xSp2^3wsv)LR(QIsgLy=$n$ zO655w{;@z124NA$u@i^RhaCGpQ=`?!^Rqu=Q)>UNdUfdpf+@Id2M-h-%yv#&oJ{aGZ#S+xYcST zRs?C5{o>lSYabmucFgH^yJe@-L8H+~>-Bm+iXz+TbgY11D2+iasySg03bV2T4425C z-O2eBSOi}K0j}ptSgpc!T`G$F&u`Aoz6Jni&z>~^kR*x3ahxSdGH5oN>roViC#O!% zW?A}2-}h0Cs>X9&0sss7>b~I7(b0f_FA@>_icf*>Q(5H8YyJLD0AO-*(w&)^u>deK zG9p7mLuryEgE)={aU7?I4m8fl}^7$ixO)@rrB)|$U_{`~val)q$V1isHo zYX}f2r5byJnb{JNINE{ZXqD&1wU0jT{Tu+g-7aHW3GDTH5{6;cY&HkYW^)k7@u1mk zt}}Dx%g2xZq2KRct@st?I8H%CAR^fl3}A_g?r5!HN|UEq{ zo6SM3R!fs486-(Ea2%(&aN)u#G5s{pa=4D;Fww_a>*YPcw{G1Ukm3cc6pA9hID2LG zA^=2DBpYKl8!ap>P^D7I9mmPE)|uydx#xL#C}G;|cDtbM5wX6Gu8LM~@yI2Y?yO>{i_X03uQmkw8TJ9W4+Mb^3`X#wU(+-tV;A&+iKb zfKI0~G|?JA^T2@*sUxk{)EC>E*Km>r?du*6v z5I_Vb0xJd2b-~Oi^5WHPcJIsTU5~|HuV)?^9lflG>lMFpJPbo<$AQ*Lv{F!7lhztq zYdDSr$92W8R45EXL{S8NZ}0v#x8_GnOH0{CFzy5I6>0(?=gyr|A1p4OXQjWZmHIq0 z-}BsB$cD>A1ZD;^!x#f&%)5mte|P!Hm6x|UXLbYZrLC*a>C>klS3=LQQco$y1c;kN zqyZ5TDMjRtwf4HPwp$F$rMKtaeis0yr>FI9`~N}UEzdVQQ&UrsV@v|(h7gSiK*D@W pL{{|j@=|wx{_buI|4jd%`Zqy7K&e}O3_kz>002ovPDHLkV1kpb*WLgC literal 0 HcmV?d00001 diff --git a/src/main/resources/image/seedsigner-icon.png b/src/main/resources/image/seedsigner-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a108ee51e67ae1f2d4b726b2e850a10f7d1b2fc GIT binary patch literal 1771 zcmeAS@N?(olHy`uVBq!ia0vp^{2o>z46O{ztPCv_42?hx zBTEJb<^_xhH!Wa-o6Em|8Nmiwuz%+SJ0Qhb;1OBOz`!jG!i)^F=14Fwu(V``M3hAM z`dB6B=jtV<VstT4fPE4v1tQ(7VbV2T@{H1I5m}Irs1#)B!g31N;2H4 zg3_WKa6qIa>!;?V=BDPA6a)1c>cjmH^qvjK1 zoRlJ5GJz%*14GTu&=jf%T>`7dNL(6`B#<-$1JBC2C>4}&L-O-;?69R{8+~+DXa@M^ zr(~v8x+IpQ+JREMu7R1Zp+yKR#Y40ssX^BbOp+;yC5bToM!JTEKonwVY-I?{I)>T? zz^r2cH3eBMk~yGAvkFQr&d(_YrL@qz%n~~z8+~lT=(?Qqa|?=6i@`w+3~8_yWHEI0 z{zaLbOEe-;*)21@alMt#B_UGy3yb|`0u~A66>i>z)%wLu~kk?tUa%(^*C#!(a zMs~yjS@!Jgxihn8zHz$$-Nt9a%1YrC|EyZ2MO8BTKfZW1`=;`%8SSjX z_rLq=p0=8v8ky>+Rw&~v(bmu%X}#~Fzp?&di?VyKj~2O{1g(pzVorg@s>62X^lOl8+S-ausSv@%WN>bUdo#qxi5je zUqn)V!%^`Gf7-xYB!w7;iUotgYcIi33^+x_h?=Nw& literal 0 HcmV?d00001 diff --git a/src/main/resources/image/seedsigner-icon@2x.png b/src/main/resources/image/seedsigner-icon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..b7d162a8699580f43b0cbc0bff4344cf9658b946 GIT binary patch literal 2174 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/image/trezor-icon@2x.png b/src/main/resources/image/trezor-icon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..e61e973b7334452f5d521d19b046f44ae59bef54 GIT binary patch literal 1969 zcmeAS@N?(olHy`uVBq!ia0vp^av;pX1|+Qw)-3{3jKx9jP7LeL$-D$|j-^I;ruq6Z zXaU(A3~Y=-49p-UK*+!-#lQ+?GcbfPO2gT4j2ciiOh7e;3_y}W6o}K>GZ|Q*>T7^B zh^Yf2V01ucMQToNVo83HLO@Zzf{~tyo*7V{WdaM>3=trE(*j0>{R^1j=E*H!MzDeE z7>o>zjI0a|fk?s7$ja2z%D{j@yXYV{P>Qp_BeIx*K~EWk87r3BmoYFfZ^{gbD2ed( zu}aR*)k{ptPfFFR$SnYw#9&il1!U%?mLw`vE(HYzo1&C7s~{IQsCFRFRw<*Tq`*pFzr4I$uiRKKzbIYb z(9+UU-@r)U$VeBcLbtdwuOzWTH?LS3VhGF}m(=3qqRfJl%=|nBkhzIT`K2YcN=hJ$ z-~i&z)QU`mO?kyoZvj2150cS0)HBe>rVZ#>xcg9aRU{VR)Krq0hQlt93`83^Fd%NU z0lB~m9O02JnYpQX#X$eq85`JuorWQS@EnrH2wWPGB#<-$y=CQGlnP2-A^G_^cGyyz zjXt_6YzCydB$lMwfl`>Rp&<~3z*3mDfe|pBAsGTuiDrf`Fv+AOmLy`BVr*q-U}a>1 zYzm56By&K4Y84TbTAZI#3Q7vVpaTPaLl9-7k0Fn)7ojyYFSEqX2&x557+qH+LLRG) z$nxlV{fjbFfk_(dT0^LH$YN0SHu|7cjg-nESqLl&%tLluz~F`DM?0uy-kKfO|xm#fJ$vNdwPY)j7Q~CMc_p|fw z-jtP;yj6dz@k0Eo88dy-LbAA9o#rmv88h!Gm%GgIlag(PA`5kMH{J|eUAj9)&wlCs z3v(N`6ORx$kGMjS*`~d%wP!WxKeZjraC8f8l*BNe{Fu z7(Xd6T}t2=E`I&>SMeXQfLYhNCZBv#74}PD+1iA*t8-IJ*SAP+0 zsdgu4oI14XU;@vhw?AedusISe;QyF?^{lic^Aj_(p64u<)ySD(azSD0+AWTEzyB@Y z(zxQvwvN5i4HF)FzjL@8WvJ6FoFbHFHe2?yO}=7U!*tiVJP2 z_0_x+TFSl2GfDJjoQv2xPZr;Mg|qiH*H8Q?99wF;`_>_W`jXi4ZR)4rKS=LW>))Z2 zn(6WV&NQvClhR!4FS+q8yLc~4PHLx7XuR>_xyBJwdEPFcb?5QNiaSkzwaKi=WCI8$%=-3hXanf8mM zGoPM0`mE!5r|q%*r^8=M%1_Myu}Qkt*L}z48;^W8cf4V4mza>eqyK}j9Y_3!skd)? oZV`$PlUY-xYIFEj{ch&{3=Hbu;@&^#1y#EYp00i_>zopr0D)hgH~;_u literal 0 HcmV?d00001