mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2025-11-05 11:56:37 +00:00
Merge branch 'sparrowwallet:master' into master
This commit is contained in:
commit
4498bad7e3
33 changed files with 514 additions and 102 deletions
|
|
@ -20,7 +20,7 @@ if(System.getProperty("os.arch") == "aarch64") {
|
|||
def headless = "true".equals(System.getProperty("java.awt.headless"))
|
||||
|
||||
group 'com.sparrowwallet'
|
||||
version '2.2.2'
|
||||
version '2.2.4'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
|
|
@ -78,7 +78,7 @@ dependencies {
|
|||
implementation('com.fasterxml.jackson.core:jackson-databind:2.17.2')
|
||||
implementation('com.sparrowwallet:hummingbird:1.7.4')
|
||||
implementation('co.nstant.in:cbor:0.9')
|
||||
implementation('org.openpnp:openpnp-capture-java:0.0.28-5')
|
||||
implementation('org.openpnp:openpnp-capture-java:0.0.28-6')
|
||||
implementation("io.matthewnelson.kmp-tor:runtime:2.2.1")
|
||||
implementation("io.matthewnelson.kmp-tor:resource-exec-tor-gpl:408.16.3")
|
||||
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.10.1') {
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ sudo apt install -y rpm fakeroot binutils
|
|||
First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify:
|
||||
|
||||
```shell
|
||||
GIT_TAG="2.2.1"
|
||||
GIT_TAG="2.2.3"
|
||||
```
|
||||
|
||||
The project can then be initially cloned as follows:
|
||||
|
|
|
|||
2
drongo
2
drongo
|
|
@ -1 +1 @@
|
|||
Subproject commit abb598d3b041a9d0b3d0ba41b5fb9785e2100193
|
||||
Subproject commit 13e1fafbe8892d7005a043ae561e09ed66f7cea6
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.2.2</string>
|
||||
<string>2.2.4</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
||||
|
|
|
|||
|
|
@ -1462,6 +1462,10 @@ public class AppController implements Initializable {
|
|||
}
|
||||
|
||||
public void sendToMany(ActionEvent event) {
|
||||
sendToMany(Collections.emptyList());
|
||||
}
|
||||
|
||||
private void sendToMany(List<Payment> initialPayments) {
|
||||
if(sendToManyDialog != null) {
|
||||
Stage stage = (Stage)sendToManyDialog.getDialogPane().getScene().getWindow();
|
||||
stage.setAlwaysOnTop(true);
|
||||
|
|
@ -1477,7 +1481,7 @@ public class AppController implements Initializable {
|
|||
bitcoinUnit = wallet.getAutoUnit();
|
||||
}
|
||||
|
||||
sendToManyDialog = new SendToManyDialog(bitcoinUnit);
|
||||
sendToManyDialog = new SendToManyDialog(bitcoinUnit, initialPayments);
|
||||
sendToManyDialog.initModality(Modality.NONE);
|
||||
Optional<List<Payment>> optPayments = sendToManyDialog.showAndWait();
|
||||
sendToManyDialog = null;
|
||||
|
|
@ -3147,6 +3151,11 @@ public class AppController implements Initializable {
|
|||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void requestSendToMany(RequestSendToManyEvent event) {
|
||||
sendToMany(event.getPayments());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void functionAction(FunctionActionEvent event) {
|
||||
selectTab(event.getWallet());
|
||||
|
|
|
|||
|
|
@ -136,6 +136,8 @@ public class AppServices {
|
|||
|
||||
private static Map<Integer, Double> targetBlockFeeRates;
|
||||
|
||||
private static Double nextBlockMedianFeeRate;
|
||||
|
||||
private static final TreeMap<Date, Set<MempoolRateSize>> mempoolHistogram = new TreeMap<>();
|
||||
|
||||
private static Double minimumRelayFeeRate;
|
||||
|
|
@ -748,6 +750,10 @@ public class AppServices {
|
|||
return Math.max(minRate, Transaction.DUST_RELAY_TX_FEE);
|
||||
}
|
||||
|
||||
public static Double getNextBlockMedianFeeRate() {
|
||||
return nextBlockMedianFeeRate == null ? getDefaultFeeRate() : nextBlockMedianFeeRate;
|
||||
}
|
||||
|
||||
public static double getFallbackFeeRate() {
|
||||
return Network.get() == Network.MAINNET ? FALLBACK_FEE_RATE : TESTNET_FALLBACK_FEE_RATE;
|
||||
}
|
||||
|
|
@ -1249,11 +1255,13 @@ public class AppServices {
|
|||
if(AppServices.currentBlockHeight != null) {
|
||||
blockSummaries.keySet().removeIf(height -> AppServices.currentBlockHeight - height > 5);
|
||||
}
|
||||
nextBlockMedianFeeRate = event.getNextBlockMedianFeeRate();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void feesUpdated(FeeRatesUpdatedEvent event) {
|
||||
targetBlockFeeRates = event.getTargetBlockFeeRates();
|
||||
nextBlockMedianFeeRate = event.getNextBlockMedianFeeRate();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import java.util.*;
|
|||
public class SparrowWallet {
|
||||
public static final String APP_ID = "sparrow";
|
||||
public static final String APP_NAME = "Sparrow";
|
||||
public static final String APP_VERSION = "2.2.2";
|
||||
public static final String APP_VERSION = "2.2.4";
|
||||
public static final String APP_VERSION_SUFFIX = "";
|
||||
public static final String APP_HOME_PROPERTY = "sparrow.home";
|
||||
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
|
||||
|
|
|
|||
|
|
@ -52,7 +52,10 @@ public class BlockCube extends Group {
|
|||
public BlockCube(Integer weight, Double medianFee, Integer height, Integer txCount, Long timestamp, boolean confirmed) {
|
||||
getStyleClass().addAll("block-" + Network.getCanonical().getName(), "block-cube");
|
||||
this.confirmedProperty.set(confirmed);
|
||||
this.feeRatesSource.set(Config.get().getFeeRatesSource());
|
||||
|
||||
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
|
||||
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
|
||||
this.feeRatesSource.set(feeRatesSource);
|
||||
|
||||
this.weightProperty.addListener((_, _, _) -> {
|
||||
if(front != null) {
|
||||
|
|
@ -198,6 +201,8 @@ public class BlockCube extends Group {
|
|||
} else {
|
||||
feeRateIcon.getChildren().clear();
|
||||
}
|
||||
} else {
|
||||
feeRateIcon.getChildren().clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppServices.getActiveWindow;
|
||||
import static com.sparrowwallet.sparrow.AppServices.setStageIcon;
|
||||
|
||||
public class ConfirmationAlert extends Alert {
|
||||
private final CheckBox dontAskAgain;
|
||||
|
||||
public ConfirmationAlert(String title, String contentText, ButtonType... buttons) {
|
||||
super(AlertType.CONFIRMATION, contentText, buttons);
|
||||
|
||||
initOwner(getActiveWindow());
|
||||
setStageIcon(getDialogPane().getScene().getWindow());
|
||||
getDialogPane().getScene().getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
setTitle(title);
|
||||
setHeaderText(title);
|
||||
|
||||
VBox contentBox = new VBox(20);
|
||||
contentBox.setPadding(new Insets(10, 20, 10, 20));
|
||||
Label contentLabel = new Label(contentText);
|
||||
contentLabel.setWrapText(true);
|
||||
dontAskAgain = new CheckBox("Don't ask again");
|
||||
contentBox.getChildren().addAll(contentLabel, dontAskAgain);
|
||||
|
||||
getDialogPane().setContent(contentBox);
|
||||
}
|
||||
|
||||
public boolean isDontAskAgain() {
|
||||
return dontAskAgain.isSelected();
|
||||
}
|
||||
}
|
||||
|
|
@ -240,6 +240,9 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
|||
setFormatFromScriptType(address.getScriptType());
|
||||
if(wallet != null) {
|
||||
setWalletNodeFromAddress(wallet, address);
|
||||
if(walletNode != null) {
|
||||
setFormatFromScriptType(getSigningScriptType(walletNode));
|
||||
}
|
||||
}
|
||||
} catch(InvalidAddressException e) {
|
||||
//can't happen
|
||||
|
|
@ -273,7 +276,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
|||
}
|
||||
|
||||
if(wallet != null && walletNode != null) {
|
||||
setFormatFromScriptType(wallet.getScriptType());
|
||||
setFormatFromScriptType(getSigningScriptType(walletNode));
|
||||
} else {
|
||||
formatGroup.selectToggle(formatElectrum);
|
||||
}
|
||||
|
|
@ -287,9 +290,13 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
|||
}
|
||||
|
||||
private boolean canSign(Wallet wallet) {
|
||||
return wallet.getKeystores().get(0).hasPrivateKey()
|
||||
|| wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB
|
||||
|| wallet.getKeystores().get(0).getWalletModel().isCard();
|
||||
return wallet.getKeystores().getFirst().hasPrivateKey()
|
||||
|| wallet.getKeystores().getFirst().getSource() == KeystoreSource.HW_USB
|
||||
|| wallet.getKeystores().getFirst().getWalletModel().isCard();
|
||||
}
|
||||
|
||||
private boolean canSignBip322(Wallet wallet) {
|
||||
return wallet.getKeystores().getFirst().hasPrivateKey();
|
||||
}
|
||||
|
||||
private Address getAddress()throws InvalidAddressException {
|
||||
|
|
@ -313,6 +320,11 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
|||
walletNode = wallet.getWalletAddresses().get(address);
|
||||
}
|
||||
|
||||
private ScriptType getSigningScriptType(WalletNode walletNode) {
|
||||
ScriptType scriptType = walletNode.getWallet().getScriptType();
|
||||
return canSign(walletNode.getWallet()) && !canSignBip322(walletNode.getWallet()) ? ScriptType.P2PKH : scriptType;
|
||||
}
|
||||
|
||||
private void setFormatFromScriptType(ScriptType scriptType) {
|
||||
formatElectrum.setDisable(scriptType == ScriptType.P2TR);
|
||||
formatTrezor.setDisable(scriptType == ScriptType.P2TR || scriptType == ScriptType.P2PKH);
|
||||
|
|
@ -345,7 +357,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
|||
|
||||
//Note we can expect a single keystore due to the check in the constructor
|
||||
Wallet signingWallet = walletNode.getWallet();
|
||||
if(signingWallet.getKeystores().get(0).hasPrivateKey()) {
|
||||
if(signingWallet.getKeystores().getFirst().hasPrivateKey()) {
|
||||
if(signingWallet.isEncrypted()) {
|
||||
EventManager.get().post(new RequestOpenWalletsEvent());
|
||||
} else {
|
||||
|
|
@ -358,7 +370,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
|||
|
||||
private void signUnencryptedKeystore(Wallet decryptedWallet) {
|
||||
try {
|
||||
Keystore keystore = decryptedWallet.getKeystores().get(0);
|
||||
Keystore keystore = decryptedWallet.getKeystores().getFirst();
|
||||
ECKey privKey = keystore.getKey(walletNode);
|
||||
String signatureText;
|
||||
if(isBip322()) {
|
||||
|
|
@ -378,8 +390,8 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
|||
}
|
||||
|
||||
private void signDeviceKeystore(Wallet deviceWallet) {
|
||||
List<String> fingerprints = List.of(deviceWallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint());
|
||||
KeyDerivation fullDerivation = deviceWallet.getKeystores().get(0).getKeyDerivation().extend(walletNode.getDerivation());
|
||||
List<String> fingerprints = List.of(deviceWallet.getKeystores().getFirst().getKeyDerivation().getMasterFingerprint());
|
||||
KeyDerivation fullDerivation = deviceWallet.getKeystores().getFirst().getKeyDerivation().extend(walletNode.getDerivation());
|
||||
DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(fingerprints, deviceWallet, message.getText().trim(), fullDerivation);
|
||||
deviceSignMessageDialog.initOwner(getDialogPane().getScene().getWindow());
|
||||
Optional<String> optSignature = deviceSignMessageDialog.showAndWait();
|
||||
|
|
|
|||
|
|
@ -122,19 +122,21 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
|||
if(percentComplete.get() <= 0.0) {
|
||||
Platform.runLater(() -> percentComplete.set(opening ? 0.0 : -1.0));
|
||||
}
|
||||
});
|
||||
|
||||
if(opening) {
|
||||
webcamService.openedProperty().addListener((_, _, opened) -> {
|
||||
if(opened) {
|
||||
Platform.runLater(() -> {
|
||||
try {
|
||||
postOpenUpdate = true;
|
||||
List<CaptureDevice> newDevices = new ArrayList<>(webcamService.getDevices());
|
||||
List<CaptureDevice> newDevices = new ArrayList<>(webcamService.getAvailableDevices());
|
||||
newDevices.removeAll(foundDevices);
|
||||
foundDevices.addAll(newDevices);
|
||||
foundDevices.removeIf(device -> !webcamService.getDevices().contains(device));
|
||||
|
||||
if(Config.get().getWebcamDevice() != null && webcamDeviceProperty.get() == null) {
|
||||
if(webcamService.getDevice() != null) {
|
||||
for(CaptureDevice device : foundDevices) {
|
||||
if(device.getName().equals(Config.get().getWebcamDevice())) {
|
||||
if(device.equals(webcamService.getDevice())) {
|
||||
webcamDeviceProperty.set(device);
|
||||
}
|
||||
}
|
||||
|
|
@ -146,10 +148,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
|||
postOpenUpdate = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
webcamService.closedProperty().addListener((_, _, closed) -> {
|
||||
if(closed && webcamResolutionProperty.get() != null) {
|
||||
} else if(webcamResolutionProperty.get() != null) {
|
||||
webcamService.setResolution(webcamResolutionProperty.get());
|
||||
webcamService.setDevice(webcamDeviceProperty.get());
|
||||
Platform.runLater(() -> {
|
||||
|
|
|
|||
|
|
@ -50,7 +50,9 @@ public class RecentBlocksView extends Pane {
|
|||
}
|
||||
}));
|
||||
|
||||
updateFeeRatesSource(Config.get().getFeeRatesSource());
|
||||
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
|
||||
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
|
||||
updateFeeRatesSource(feeRatesSource);
|
||||
Tooltip.install(this, tooltip);
|
||||
}
|
||||
|
||||
|
|
@ -106,7 +108,7 @@ public class RecentBlocksView extends Pane {
|
|||
}
|
||||
}
|
||||
|
||||
public void addNewBlock(List<BlockSummary> latestBlocks, Double currentFeeRate) {
|
||||
private void addNewBlock(List<BlockSummary> latestBlocks, Double currentFeeRate) {
|
||||
if(getCubes().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -140,8 +142,10 @@ public class RecentBlocksView extends Pane {
|
|||
|
||||
public void updateFeeRate(Map<Integer, Double> targetBlockFeeRates) {
|
||||
int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1);
|
||||
Double defaultRate = targetBlockFeeRates.get(defaultTarget);
|
||||
updateFeeRate(defaultRate);
|
||||
if(targetBlockFeeRates.get(defaultTarget) != null) {
|
||||
Double defaultRate = targetBlockFeeRates.get(defaultTarget);
|
||||
updateFeeRate(defaultRate);
|
||||
}
|
||||
}
|
||||
|
||||
public void updateFeeRate(Double currentFeeRate) {
|
||||
|
|
|
|||
|
|
@ -13,8 +13,6 @@ import javafx.collections.FXCollections;
|
|||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.FileChooser;
|
||||
|
|
@ -34,7 +32,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
|||
private final SpreadsheetView spreadsheetView;
|
||||
public static final AddressCellType ADDRESS = new AddressCellType();
|
||||
|
||||
public SendToManyDialog(BitcoinUnit bitcoinUnit) {
|
||||
public SendToManyDialog(BitcoinUnit bitcoinUnit, List<Payment> payments) {
|
||||
this.bitcoinUnit = bitcoinUnit;
|
||||
|
||||
final DialogPane dialogPane = new SendToManyDialogPane();
|
||||
|
|
@ -44,7 +42,8 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
|||
dialogPane.setHeaderText("Send to many recipients by specifying addresses and amounts.\nOnly the first row's label is necessary.");
|
||||
dialogPane.setGraphic(new DialogImage(DialogImage.Type.SPARROW));
|
||||
|
||||
List<Payment> initialPayments = IntStream.range(0, 100).mapToObj(i -> new Payment(null, null, -1, false)).collect(Collectors.toList());
|
||||
List<Payment> initialPayments = IntStream.range(0, 100)
|
||||
.mapToObj(i -> i < payments.size() ? payments.get(i) : new Payment(null, null, -1, false)).collect(Collectors.toList());
|
||||
Grid grid = getGrid(initialPayments);
|
||||
|
||||
spreadsheetView = new SpreadsheetView(grid) {
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ public enum WebcamPixelFormat {
|
|||
//Only V4L2 formats defined in linux/videodev2.h are required here, declared in order of priority for supported formats
|
||||
PIX_FMT_RGB24("RGB3", true),
|
||||
PIX_FMT_YUYV("YUYV", true),
|
||||
PIX_FMT_MJPG("MJPG", true),
|
||||
PIX_FMT_NV12("NV12", false);
|
||||
PIX_FMT_NV12("NV12", true),
|
||||
PIX_FMT_MJPG("MJPG", true);
|
||||
|
||||
private final String name;
|
||||
private final boolean supported;
|
||||
|
|
@ -25,6 +25,14 @@ public enum WebcamPixelFormat {
|
|||
return supported;
|
||||
}
|
||||
|
||||
public int getFourCC() {
|
||||
char a = name.charAt(0);
|
||||
char b = name.charAt(1);
|
||||
char c = name.charAt(2);
|
||||
char d = name.charAt(3);
|
||||
return ((int) a) | ((int) b << 8) | ((int) c << 16) | ((int) d << 24);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ import java.awt.image.BufferedImage;
|
|||
import java.awt.image.WritableRaster;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
|
@ -34,13 +37,18 @@ import java.util.stream.Stream;
|
|||
public class WebcamService extends ScheduledService<Image> {
|
||||
private static final Logger log = LoggerFactory.getLogger(WebcamService.class);
|
||||
|
||||
private final Semaphore taskSemaphore = new Semaphore(1);
|
||||
private final AtomicBoolean cancelRequested = new AtomicBoolean(false);
|
||||
private final AtomicBoolean captureClosed = new AtomicBoolean(false);
|
||||
|
||||
private List<CaptureDevice> devices;
|
||||
private List<CaptureDevice> availableDevices;
|
||||
private Set<WebcamResolution> resolutions;
|
||||
|
||||
private WebcamResolution resolution;
|
||||
private CaptureDevice device;
|
||||
private final BooleanProperty opening = new SimpleBooleanProperty(false);
|
||||
private final BooleanProperty closed = new SimpleBooleanProperty(false);
|
||||
private final BooleanProperty opened = new SimpleBooleanProperty(false);
|
||||
|
||||
private final ObjectProperty<Result> resultProperty = new SimpleObjectProperty<>(null);
|
||||
|
||||
|
|
@ -105,24 +113,36 @@ public class WebcamService extends ScheduledService<Image> {
|
|||
return new Task<>() {
|
||||
@Override
|
||||
protected Image call() throws Exception {
|
||||
if(cancelRequested.get() || isCancelled() || captureClosed.get()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if(!taskSemaphore.tryAcquire()) {
|
||||
log.warn("Skipped execution of webcam capture task, another task is running");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
if(stream == null) {
|
||||
if(devices == null) {
|
||||
devices = capture.getDevices();
|
||||
availableDevices = new ArrayList<>(devices);
|
||||
|
||||
if(devices.isEmpty()) {
|
||||
throw new UnsupportedOperationException("No cameras available");
|
||||
}
|
||||
}
|
||||
|
||||
CaptureDevice selectedDevice = devices.stream().filter(d -> !d.getFormats().isEmpty()).findFirst().orElse(devices.getFirst());
|
||||
while(stream == null && !availableDevices.isEmpty()) {
|
||||
CaptureDevice selectedDevice = availableDevices.stream().filter(d -> !d.getFormats().isEmpty()).findFirst().orElse(availableDevices.getFirst());
|
||||
|
||||
if(device != null) {
|
||||
for(CaptureDevice webcam : devices) {
|
||||
for(CaptureDevice webcam : availableDevices) {
|
||||
if(webcam.getName().equals(device.getName())) {
|
||||
selectedDevice = webcam;
|
||||
}
|
||||
}
|
||||
} else if(Config.get().getWebcamDevice() != null) {
|
||||
for(CaptureDevice webcam : devices) {
|
||||
for(CaptureDevice webcam : availableDevices) {
|
||||
if(webcam.getName().equals(Config.get().getWebcamDevice())) {
|
||||
selectedDevice = webcam;
|
||||
}
|
||||
|
|
@ -163,22 +183,35 @@ public class WebcamService extends ScheduledService<Image> {
|
|||
}
|
||||
}
|
||||
|
||||
//On Linux, formats not defined in WebcamPixelFormat are unsupported
|
||||
if(OsType.getCurrent() == OsType.UNIX && WebcamPixelFormat.fromFourCC(format.getFormatInfo().fourcc) == null) {
|
||||
log.warn("Unsupported camera pixel format " + WebcamPixelFormat.fourCCToString(format.getFormatInfo().fourcc));
|
||||
}
|
||||
|
||||
if(log.isDebugEnabled()) {
|
||||
log.debug("Opening capture stream on " + device + " with format " + format.getFormatInfo().width + "x" + format.getFormatInfo().height + " (" + WebcamPixelFormat.fourCCToString(format.getFormatInfo().fourcc) + ")");
|
||||
log.debug("Opening capture stream on " + device + " with format " + format.getFormatInfo().width + "x" + format.getFormatInfo().height + " (" + WebcamPixelFormat.fourCCToString(format.getFormatInfo().fourcc) + ")");
|
||||
}
|
||||
|
||||
opening.set(true);
|
||||
stream = device.openStream(format);
|
||||
opening.set(false);
|
||||
closed.set(false);
|
||||
|
||||
try {
|
||||
zoomLimits = stream.getPropertyLimits(CaptureProperty.Zoom);
|
||||
} catch(Throwable e) {
|
||||
log.debug("Error getting zoom limits on " + device + ", assuming no zoom function");
|
||||
}
|
||||
|
||||
if(stream == null) {
|
||||
availableDevices.remove(device);
|
||||
}
|
||||
}
|
||||
|
||||
if(stream == null) {
|
||||
throw new UnsupportedOperationException("No usable cameras available, tried " + devices);
|
||||
}
|
||||
|
||||
opened.set(true);
|
||||
BufferedImage originalImage = stream.capture();
|
||||
CroppedDimension cropped = getCroppedDimension(originalImage);
|
||||
BufferedImage croppedImage = originalImage.getSubimage(cropped.x, cropped.y, cropped.length, cropped.length);
|
||||
|
|
@ -195,6 +228,7 @@ public class WebcamService extends ScheduledService<Image> {
|
|||
return image;
|
||||
} finally {
|
||||
opening.set(false);
|
||||
taskSemaphore.release();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -204,21 +238,38 @@ public class WebcamService extends ScheduledService<Image> {
|
|||
public void reset() {
|
||||
stream = null;
|
||||
zoomLimits = null;
|
||||
cancelRequested.set(false);
|
||||
super.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cancel() {
|
||||
if(stream != null) {
|
||||
stream.close();
|
||||
closed.set(true);
|
||||
cancelRequested.set(true);
|
||||
boolean cancelled = super.cancel();
|
||||
|
||||
try {
|
||||
if(taskSemaphore.tryAcquire(1, TimeUnit.SECONDS)) {
|
||||
taskSemaphore.release();
|
||||
} else {
|
||||
log.error("Timed out waiting for task semaphore to be available to cancel, cancelling anyway");
|
||||
}
|
||||
} catch(InterruptedException e) {
|
||||
log.error("Interrupted while waiting for task semaphore to be available to cancel, cancelling anyway");
|
||||
}
|
||||
|
||||
return super.cancel();
|
||||
if(stream != null) {
|
||||
stream.close();
|
||||
opened.set(false);
|
||||
}
|
||||
|
||||
return cancelled;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
capture.close();
|
||||
public synchronized void close() {
|
||||
if(!captureClosed.get()) {
|
||||
captureClosed.set(true);
|
||||
capture.close();
|
||||
}
|
||||
}
|
||||
|
||||
public PropertyLimits getZoomLimits() {
|
||||
|
|
@ -336,6 +387,10 @@ public class WebcamService extends ScheduledService<Image> {
|
|||
return devices;
|
||||
}
|
||||
|
||||
public List<CaptureDevice> getAvailableDevices() {
|
||||
return availableDevices;
|
||||
}
|
||||
|
||||
public Set<WebcamResolution> getResolutions() {
|
||||
return resolutions;
|
||||
}
|
||||
|
|
@ -376,8 +431,12 @@ public class WebcamService extends ScheduledService<Image> {
|
|||
return opening;
|
||||
}
|
||||
|
||||
public BooleanProperty closedProperty() {
|
||||
return closed;
|
||||
public BooleanProperty openedProperty() {
|
||||
return opened;
|
||||
}
|
||||
|
||||
public boolean getCancelRequested() {
|
||||
return cancelRequested.get();
|
||||
}
|
||||
|
||||
public static <T extends Enum<T>> T getNearestEnum(T target) {
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ public class WebcamView {
|
|||
});
|
||||
|
||||
service.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue != null) {
|
||||
if(newValue != null && !service.getCancelRequested()) {
|
||||
imageProperty.set(newValue);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,12 +6,18 @@ import java.util.Map;
|
|||
|
||||
public class BlockSummaryEvent {
|
||||
private final Map<Integer, BlockSummary> blockSummaryMap;
|
||||
private final Double nextBlockMedianFeeRate;
|
||||
|
||||
public BlockSummaryEvent(Map<Integer, BlockSummary> blockSummaryMap) {
|
||||
public BlockSummaryEvent(Map<Integer, BlockSummary> blockSummaryMap, Double nextBlockMedianFeeRate) {
|
||||
this.blockSummaryMap = blockSummaryMap;
|
||||
this.nextBlockMedianFeeRate = nextBlockMedianFeeRate;
|
||||
}
|
||||
|
||||
public Map<Integer, BlockSummary> getBlockSummaryMap() {
|
||||
return blockSummaryMap;
|
||||
}
|
||||
|
||||
public Double getNextBlockMedianFeeRate() {
|
||||
return nextBlockMedianFeeRate;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,13 +7,23 @@ import java.util.Set;
|
|||
|
||||
public class FeeRatesUpdatedEvent extends MempoolRateSizesUpdatedEvent {
|
||||
private final Map<Integer, Double> targetBlockFeeRates;
|
||||
private final Double nextBlockMedianFeeRate;
|
||||
|
||||
public FeeRatesUpdatedEvent(Map<Integer, Double> targetBlockFeeRates, Set<MempoolRateSize> mempoolRateSizes) {
|
||||
this(targetBlockFeeRates, mempoolRateSizes, null);
|
||||
}
|
||||
|
||||
public FeeRatesUpdatedEvent(Map<Integer, Double> targetBlockFeeRates, Set<MempoolRateSize> mempoolRateSizes, Double nextBlockMedianFeeRate) {
|
||||
super(mempoolRateSizes);
|
||||
this.targetBlockFeeRates = targetBlockFeeRates;
|
||||
this.nextBlockMedianFeeRate = nextBlockMedianFeeRate;
|
||||
}
|
||||
|
||||
public Map<Integer, Double> getTargetBlockFeeRates() {
|
||||
return targetBlockFeeRates;
|
||||
}
|
||||
|
||||
public Double getNextBlockMedianFeeRate() {
|
||||
return nextBlockMedianFeeRate;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Payment;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class RequestSendToManyEvent {
|
||||
private final List<Payment> payments;
|
||||
|
||||
public RequestSendToManyEvent(List<Payment> payments) {
|
||||
this.payments = payments;
|
||||
}
|
||||
|
||||
public List<Payment> getPayments() {
|
||||
return payments;
|
||||
}
|
||||
}
|
||||
|
|
@ -14,9 +14,16 @@ import java.util.List;
|
|||
*/
|
||||
public class WalletNodeHistoryChangedEvent {
|
||||
private final String scriptHash;
|
||||
private final String status;
|
||||
|
||||
public WalletNodeHistoryChangedEvent(String scriptHash) {
|
||||
this.scriptHash = scriptHash;
|
||||
this.status = null;
|
||||
}
|
||||
|
||||
public WalletNodeHistoryChangedEvent(String scriptHash, String status) {
|
||||
this.scriptHash = scriptHash;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public WalletNode getWalletNode(Wallet wallet) {
|
||||
|
|
@ -70,4 +77,8 @@ public class WalletNodeHistoryChangedEvent {
|
|||
public String getScriptHash() {
|
||||
return scriptHash;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,8 @@ public class Config {
|
|||
private boolean showDeprecatedImportExport = false;
|
||||
private boolean signBsmsExports = false;
|
||||
private boolean preventSleep = false;
|
||||
private Boolean connectToBroadcast;
|
||||
private Boolean suggestSendToMany;
|
||||
private List<File> recentWalletFiles;
|
||||
private Integer keyDerivationPeriod;
|
||||
private long dustAttackThreshold = DUST_ATTACK_THRESHOLD_SATS;
|
||||
|
|
@ -69,6 +71,7 @@ public class Config {
|
|||
private File coreDataDir;
|
||||
private String coreAuth;
|
||||
private boolean useLegacyCoreWallet;
|
||||
private boolean legacyServer;
|
||||
private Server electrumServer;
|
||||
private List<Server> recentElectrumServers;
|
||||
private File electrumServerCert;
|
||||
|
|
@ -355,6 +358,25 @@ public class Config {
|
|||
|
||||
public void setPreventSleep(boolean preventSleep) {
|
||||
this.preventSleep = preventSleep;
|
||||
flush();
|
||||
}
|
||||
|
||||
public Boolean getConnectToBroadcast() {
|
||||
return connectToBroadcast;
|
||||
}
|
||||
|
||||
public void setConnectToBroadcast(Boolean connectToBroadcast) {
|
||||
this.connectToBroadcast = connectToBroadcast;
|
||||
flush();
|
||||
}
|
||||
|
||||
public Boolean getSuggestSendToMany() {
|
||||
return suggestSendToMany;
|
||||
}
|
||||
|
||||
public void setSuggestSendToMany(Boolean suggestSendToMany) {
|
||||
this.suggestSendToMany = suggestSendToMany;
|
||||
flush();
|
||||
}
|
||||
|
||||
public List<File> getRecentWalletFiles() {
|
||||
|
|
@ -557,6 +579,15 @@ public class Config {
|
|||
flush();
|
||||
}
|
||||
|
||||
public boolean isLegacyServer() {
|
||||
return legacyServer;
|
||||
}
|
||||
|
||||
public void setLegacyServer(boolean legacyServer) {
|
||||
this.legacyServer = legacyServer;
|
||||
flush();
|
||||
}
|
||||
|
||||
public Server getElectrumServer() {
|
||||
return electrumServer;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ public class Descriptor implements WalletImport, WalletExport {
|
|||
} else if(line.startsWith("#")) {
|
||||
continue;
|
||||
} else {
|
||||
paragraph.append(line);
|
||||
paragraph.append(line.replaceFirst("^.+:", "").trim());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,8 @@ public class ElectrumPersonalServer implements WalletExport {
|
|||
try {
|
||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
|
||||
writer.write("# Electrum Personal Server configuration file fragments\n");
|
||||
writer.write("# Copy the lines below into the relevant sections in your EPS config.ini file\n\n");
|
||||
writer.write("# First close Sparrow and edit your config file in Sparrow home to set \"legacyServer\": true\n");
|
||||
writer.write("# Then copy the lines below into the relevant sections in your EPS config.ini file\n\n");
|
||||
writer.write("# Copy into [master-public-keys] section\n");
|
||||
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
|
||||
writeWalletXpub(masterWallet, writer);
|
||||
|
|
|
|||
|
|
@ -76,6 +76,10 @@ public class ElectrumServer {
|
|||
|
||||
private static final Set<String> sameHeightTxioScriptHashes = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private final static Map<String, Integer> subscribedRecent = new ConcurrentHashMap<>();
|
||||
|
||||
private final static Map<String, String> broadcastRecent = new ConcurrentHashMap<>();
|
||||
|
||||
private static ElectrumServerRpc electrumServerRpc = new SimpleElectrumServerRpc();
|
||||
|
||||
private static Cormorant cormorant;
|
||||
|
|
@ -936,6 +940,20 @@ public class ElectrumServer {
|
|||
return targetBlocksFeeRatesSats;
|
||||
}
|
||||
|
||||
public Double getNextBlockMedianFeeRate() {
|
||||
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
|
||||
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
|
||||
if(feeRatesSource.supportsNetwork(Network.get())) {
|
||||
try {
|
||||
return feeRatesSource.getNextBlockMedianFeeRate();
|
||||
} catch(Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Map<Integer, Double> getDefaultFeeEstimates(List<Integer> targetBlocks) throws ServerException {
|
||||
try {
|
||||
Map<Integer, Double> targetBlocksFeeRatesBtcKb = electrumServerRpc.getFeeEstimates(getTransport(), targetBlocks);
|
||||
|
|
@ -1048,6 +1066,11 @@ public class ElectrumServer {
|
|||
List<BlockTransactionHash> recentTransactions = feeRatesSource.getRecentMempoolTransactions();
|
||||
Map<BlockTransactionHash, Transaction> setReferences = new HashMap<>();
|
||||
setReferences.put(recentTransactions.getFirst(), null);
|
||||
if(recentTransactions.size() > 1) {
|
||||
Random random = new Random();
|
||||
int halfSize = recentTransactions.size() / 2;
|
||||
setReferences.put(recentTransactions.get(halfSize == 1 ? 1 : random.nextInt(halfSize) + 1), null);
|
||||
}
|
||||
Map<Sha256Hash, BlockTransaction> transactions = getTransactions(null, setReferences, Collections.emptyMap());
|
||||
return transactions.values().stream().filter(blxTx -> blxTx.getTransaction() != null).toList();
|
||||
} catch(Exception e) {
|
||||
|
|
@ -1232,11 +1255,11 @@ public class ElectrumServer {
|
|||
if(!serverVersion.isEmpty()) {
|
||||
String server = serverVersion.getFirst().toLowerCase(Locale.ROOT);
|
||||
if(server.contains("electrumx")) {
|
||||
return new ServerCapability(true);
|
||||
return new ServerCapability(true, true);
|
||||
}
|
||||
|
||||
if(server.startsWith("cormorant")) {
|
||||
return new ServerCapability(true, false, true);
|
||||
return new ServerCapability(true, false, true, false);
|
||||
}
|
||||
|
||||
if(server.startsWith("electrs/")) {
|
||||
|
|
@ -1248,7 +1271,7 @@ public class ElectrumServer {
|
|||
try {
|
||||
Version version = new Version(electrsVersion);
|
||||
if(version.compareTo(ELECTRS_MIN_BATCHING_VERSION) >= 0) {
|
||||
return new ServerCapability(true);
|
||||
return new ServerCapability(true, true);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
|
|
@ -1264,7 +1287,7 @@ public class ElectrumServer {
|
|||
try {
|
||||
Version version = new Version(fulcrumVersion);
|
||||
if(version.compareTo(FULCRUM_MIN_BATCHING_VERSION) >= 0) {
|
||||
return new ServerCapability(true);
|
||||
return new ServerCapability(true, true);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
|
|
@ -1283,15 +1306,19 @@ public class ElectrumServer {
|
|||
Version version = new Version(mempoolElectrsVersion);
|
||||
if(version.compareTo(MEMPOOL_ELECTRS_MIN_BATCHING_VERSION) > 0 ||
|
||||
(version.compareTo(MEMPOOL_ELECTRS_MIN_BATCHING_VERSION) == 0 && (!mempoolElectrsSuffix.contains("dev") || mempoolElectrsSuffix.contains("dev-249848d")))) {
|
||||
return new ServerCapability(true, 25);
|
||||
return new ServerCapability(true, 25, false);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
|
||||
if(server.startsWith("electrumpersonalserver")) {
|
||||
return new ServerCapability(false, false);
|
||||
}
|
||||
}
|
||||
|
||||
return new ServerCapability(false);
|
||||
return new ServerCapability(false, true);
|
||||
}
|
||||
|
||||
public static class ServerVersionService extends Service<List<String>> {
|
||||
|
|
@ -1456,8 +1483,9 @@ public class ElectrumServer {
|
|||
if(elapsed > FEE_RATES_PERIOD) {
|
||||
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE, false);
|
||||
Set<MempoolRateSize> mempoolRateSizes = electrumServer.getMempoolRateSizes();
|
||||
Double nextBlockMedianFeeRate = electrumServer.getNextBlockMedianFeeRate();
|
||||
feeRatesRetrievedAt = System.currentTimeMillis();
|
||||
return new FeeRatesUpdatedEvent(blockTargetFeeRates, mempoolRateSizes);
|
||||
return new FeeRatesUpdatedEvent(blockTargetFeeRates, mempoolRateSizes, nextBlockMedianFeeRate);
|
||||
}
|
||||
} else {
|
||||
closeConnection();
|
||||
|
|
@ -1583,6 +1611,31 @@ public class ElectrumServer {
|
|||
Set<MempoolRateSize> mempoolRateSizes = electrumServer.getMempoolRateSizes();
|
||||
EventManager.get().post(new MempoolRateSizesUpdatedEvent(mempoolRateSizes));
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletNodeHistoryChanged(WalletNodeHistoryChangedEvent event) {
|
||||
String status = broadcastRecent.remove(event.getScriptHash());
|
||||
if(status != null && status.equals(event.getStatus())) {
|
||||
Map<String, String> subscribeScriptHashes = new HashMap<>();
|
||||
Random random = new Random();
|
||||
int subscriptions = random.nextInt(2) + 1;
|
||||
for(int i = 0; i < subscriptions; i++) {
|
||||
byte[] randomScriptHashBytes = new byte[32];
|
||||
random.nextBytes(randomScriptHashBytes);
|
||||
String randomScriptHash = Utils.bytesToHex(randomScriptHashBytes);
|
||||
if(!subscribedScriptHashes.containsKey(randomScriptHash)) {
|
||||
subscribeScriptHashes.put("m/" + subscribeScriptHashes.size(), randomScriptHash);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
electrumServerRpc.subscribeScriptHashes(transport, null, subscribeScriptHashes);
|
||||
subscribeScriptHashes.values().forEach(scriptHash -> subscribedRecent.put(scriptHash, AppServices.getCurrentBlockHeight()));
|
||||
} catch(ElectrumServerRpcException e) {
|
||||
log.debug("Error subscribing to recent mempool transaction outputs", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class ReadRunnable implements Runnable {
|
||||
|
|
@ -1935,7 +1988,8 @@ public class ElectrumServer {
|
|||
protected FeeRatesUpdatedEvent call() throws ServerException {
|
||||
ElectrumServer electrumServer = new ElectrumServer();
|
||||
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE, false);
|
||||
return new FeeRatesUpdatedEvent(blockTargetFeeRates, null);
|
||||
Double nextBlockMedianFeeRate = electrumServer.getNextBlockMedianFeeRate();
|
||||
return new FeeRatesUpdatedEvent(blockTargetFeeRates, null, nextBlockMedianFeeRate);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -1982,10 +2036,14 @@ public class ElectrumServer {
|
|||
Config config = Config.get();
|
||||
if(!isBlockstorm(totalBlocks) && !AppServices.isUsingProxy() && config.getServer().getProtocol().equals(Protocol.SSL)
|
||||
&& (config.getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER || config.getServerType() == ServerType.ELECTRUM_SERVER)) {
|
||||
subscribeRecent(electrumServer);
|
||||
subscribeRecent(electrumServer, AppServices.getCurrentBlockHeight() == null ? endHeight : AppServices.getCurrentBlockHeight());
|
||||
}
|
||||
|
||||
return new BlockSummaryEvent(blockSummaryMap);
|
||||
Double nextBlockMedianFeeRate = null;
|
||||
if(!isBlockstorm(totalBlocks)) {
|
||||
nextBlockMedianFeeRate = electrumServer.getNextBlockMedianFeeRate();
|
||||
}
|
||||
return new BlockSummaryEvent(blockSummaryMap, nextBlockMedianFeeRate);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -1994,43 +2052,72 @@ public class ElectrumServer {
|
|||
return Network.get() != Network.MAINNET && totalBlocks > 2;
|
||||
}
|
||||
|
||||
private final static Set<String> subscribedRecent = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||
|
||||
private void subscribeRecent(ElectrumServer electrumServer) {
|
||||
Set<String> unsubscribeScriptHashes = new HashSet<>(subscribedRecent);
|
||||
private void subscribeRecent(ElectrumServer electrumServer, int currentHeight) {
|
||||
Set<String> unsubscribeScriptHashes = subscribedRecent.entrySet().stream().filter(entry -> entry.getValue() == null || entry.getValue() <= currentHeight - 3)
|
||||
.map(Map.Entry::getKey).collect(Collectors.toSet());
|
||||
unsubscribeScriptHashes.removeIf(subscribedScriptHashes::containsKey);
|
||||
electrumServerRpc.unsubscribeScriptHashes(transport, unsubscribeScriptHashes);
|
||||
subscribedRecent.removeAll(unsubscribeScriptHashes);
|
||||
if(!unsubscribeScriptHashes.isEmpty() && serverCapability.supportsUnsubscribe()) {
|
||||
electrumServerRpc.unsubscribeScriptHashes(transport, unsubscribeScriptHashes);
|
||||
}
|
||||
subscribedRecent.keySet().removeAll(unsubscribeScriptHashes);
|
||||
broadcastRecent.keySet().removeAll(unsubscribeScriptHashes);
|
||||
|
||||
Map<String, String> subscribeScriptHashes = new HashMap<>();
|
||||
List<BlockTransaction> recentTransactions = electrumServer.getRecentMempoolTransactions();
|
||||
for(BlockTransaction blkTx : recentTransactions) {
|
||||
for(int i = 0; i < blkTx.getTransaction().getOutputs().size() && subscribeScriptHashes.size() < 10; i++) {
|
||||
for(int i = 0; i < blkTx.getTransaction().getOutputs().size(); i++) {
|
||||
TransactionOutput txOutput = blkTx.getTransaction().getOutputs().get(i);
|
||||
String scriptHash = getScriptHash(txOutput);
|
||||
if(!subscribedScriptHashes.containsKey(scriptHash)) {
|
||||
subscribeScriptHashes.put("m/" + i, getScriptHash(txOutput));
|
||||
subscribeScriptHashes.put("m/" + subscribeScriptHashes.size(), scriptHash);
|
||||
}
|
||||
if(Math.random() < 0.1d) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!subscribeScriptHashes.isEmpty()) {
|
||||
Random random = new Random();
|
||||
int additionalRandomScriptHashes = random.nextInt(8);
|
||||
for(int i = 0; i < additionalRandomScriptHashes; i++) {
|
||||
byte[] randomScriptHashBytes = new byte[32];
|
||||
random.nextBytes(randomScriptHashBytes);
|
||||
String randomScriptHash = Utils.bytesToHex(randomScriptHashBytes);
|
||||
if(!subscribedScriptHashes.containsKey(randomScriptHash)) {
|
||||
subscribeScriptHashes.put("m/" + subscribeScriptHashes.size(), randomScriptHash);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
electrumServerRpc.subscribeScriptHashes(transport, null, subscribeScriptHashes);
|
||||
subscribedRecent.addAll(subscribeScriptHashes.values());
|
||||
subscribeScriptHashes.values().forEach(scriptHash -> subscribedRecent.put(scriptHash, currentHeight));
|
||||
} catch(ElectrumServerRpcException e) {
|
||||
log.debug("Error subscribing to recent mempool transactions", e);
|
||||
}
|
||||
}
|
||||
|
||||
if(!recentTransactions.isEmpty()) {
|
||||
broadcastRecent(electrumServer, recentTransactions);
|
||||
}
|
||||
}
|
||||
|
||||
private void broadcastRecent(ElectrumServer electrumServer, List<BlockTransaction> recentTransactions) {
|
||||
ScheduledService<Void> broadcastService = new ScheduledService<>() {
|
||||
@Override
|
||||
protected Task<Void> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected Void call() throws Exception {
|
||||
for(BlockTransaction blkTx : recentTransactions) {
|
||||
electrumServer.broadcastTransaction(blkTx.getTransaction());
|
||||
if(!recentTransactions.isEmpty()) {
|
||||
Random random = new Random();
|
||||
if(random.nextBoolean()) {
|
||||
BlockTransaction blkTx = recentTransactions.get(random.nextInt(recentTransactions.size()));
|
||||
String scriptHash = getScriptHash(blkTx.getTransaction().getOutputs().getFirst());
|
||||
String status = getScriptHashStatus(List.of(new ScriptHashTx(0, blkTx.getHashAsString(), blkTx.getFee())));
|
||||
broadcastRecent.put(scriptHash, status);
|
||||
electrumServer.broadcastTransaction(blkTx.getTransaction());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,12 @@ public enum FeeRatesSource {
|
|||
return getThreeTierFeeRates(this, defaultblockTargetFeeRates, url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double getNextBlockMedianFeeRate() throws Exception {
|
||||
String url = getApiUrl() + "v1/fees/mempool-blocks";
|
||||
return requestNextBlockMedianFeeRate(this, url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockSummary getBlockSummary(Sha256Hash blockId) throws Exception {
|
||||
String url = getApiUrl() + "v1/block/" + Utils.bytesToHex(blockId.getReversedBytes());
|
||||
|
|
@ -130,6 +136,10 @@ public enum FeeRatesSource {
|
|||
|
||||
public abstract Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates);
|
||||
|
||||
public Double getNextBlockMedianFeeRate() throws Exception {
|
||||
throw new UnsupportedOperationException(name + " does not support retrieving the next block median fee rate");
|
||||
}
|
||||
|
||||
public BlockSummary getBlockSummary(Sha256Hash blockId) throws Exception {
|
||||
throw new UnsupportedOperationException(name + " does not support block summaries");
|
||||
}
|
||||
|
|
@ -199,6 +209,30 @@ public enum FeeRatesSource {
|
|||
return httpClientService.requestJson(url, ThreeTierRates.class, null);
|
||||
}
|
||||
|
||||
protected static Double requestNextBlockMedianFeeRate(FeeRatesSource feeRatesSource, String url) throws Exception {
|
||||
if(log.isInfoEnabled()) {
|
||||
log.info("Requesting next block median fee rate from " + url);
|
||||
}
|
||||
|
||||
HttpClientService httpClientService = AppServices.getHttpClientService();
|
||||
try {
|
||||
MempoolBlock[] mempoolBlocks = feeRatesSource.requestMempoolBlocks(url, httpClientService);
|
||||
return mempoolBlocks.length > 0 ? mempoolBlocks[0].medianFee : null;
|
||||
} catch (Exception e) {
|
||||
if(log.isDebugEnabled()) {
|
||||
log.warn("Error retrieving next block median fee rate from " + url, e);
|
||||
} else {
|
||||
log.warn("Error retrieving next block median fee rate from " + url + " (" + e.getMessage() + ")");
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
protected MempoolBlock[] requestMempoolBlocks(String url, HttpClientService httpClientService) throws Exception {
|
||||
return httpClientService.requestJson(url, MempoolBlock[].class, null);
|
||||
}
|
||||
|
||||
protected static BlockSummary requestBlockSummary(FeeRatesSource feeRatesSource, String url) throws Exception {
|
||||
if(log.isInfoEnabled()) {
|
||||
log.info("Requesting block summary from " + url);
|
||||
|
|
@ -309,6 +343,8 @@ public enum FeeRatesSource {
|
|||
}
|
||||
}
|
||||
|
||||
protected record MempoolBlock(Integer nTx, Double medianFee) {}
|
||||
|
||||
protected record MempoolBlockSummary(String id, Integer height, Long timestamp, Integer tx_count, Integer weight, MempoolBlockSummaryExtras extras) {
|
||||
public Double getMedianFee() {
|
||||
return extras == null ? null : extras.medianFee();
|
||||
|
|
|
|||
|
|
@ -7,27 +7,30 @@ public class ServerCapability {
|
|||
private final int maxTargetBlocks;
|
||||
private final boolean supportsRecentMempool;
|
||||
private final boolean supportsBlockStats;
|
||||
private final boolean supportsUnsubscribe;
|
||||
|
||||
public ServerCapability(boolean supportsBatching) {
|
||||
this(supportsBatching, AppServices.TARGET_BLOCKS_RANGE.getLast());
|
||||
public ServerCapability(boolean supportsBatching, boolean supportsUnsubscribe) {
|
||||
this(supportsBatching, AppServices.TARGET_BLOCKS_RANGE.getLast(), supportsUnsubscribe);
|
||||
}
|
||||
|
||||
public ServerCapability(boolean supportsBatching, int maxTargetBlocks) {
|
||||
public ServerCapability(boolean supportsBatching, int maxTargetBlocks, boolean supportsUnsubscribe) {
|
||||
this.supportsBatching = supportsBatching;
|
||||
this.maxTargetBlocks = maxTargetBlocks;
|
||||
this.supportsRecentMempool = false;
|
||||
this.supportsBlockStats = false;
|
||||
this.supportsUnsubscribe = supportsUnsubscribe;
|
||||
}
|
||||
|
||||
public ServerCapability(boolean supportsBatching, boolean supportsRecentMempool, boolean supportsBlockStats) {
|
||||
this(supportsBatching, AppServices.TARGET_BLOCKS_RANGE.getLast(), supportsRecentMempool, supportsBlockStats);
|
||||
public ServerCapability(boolean supportsBatching, boolean supportsRecentMempool, boolean supportsBlockStats, boolean supportsUnsubscribe) {
|
||||
this(supportsBatching, AppServices.TARGET_BLOCKS_RANGE.getLast(), supportsRecentMempool, supportsBlockStats, supportsUnsubscribe);
|
||||
}
|
||||
|
||||
public ServerCapability(boolean supportsBatching, int maxTargetBlocks, boolean supportsRecentMempool, boolean supportsBlockStats) {
|
||||
public ServerCapability(boolean supportsBatching, int maxTargetBlocks, boolean supportsRecentMempool, boolean supportsBlockStats, boolean supportsUnsubscribe) {
|
||||
this.supportsBatching = supportsBatching;
|
||||
this.maxTargetBlocks = maxTargetBlocks;
|
||||
this.supportsRecentMempool = supportsRecentMempool;
|
||||
this.supportsBlockStats = supportsBlockStats;
|
||||
this.supportsUnsubscribe = supportsUnsubscribe;
|
||||
}
|
||||
|
||||
public boolean supportsBatching() {
|
||||
|
|
@ -45,4 +48,8 @@ public class ServerCapability {
|
|||
public boolean supportsBlockStats() {
|
||||
return supportsBlockStats;
|
||||
}
|
||||
|
||||
public boolean supportsUnsubscribe() {
|
||||
return supportsUnsubscribe;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import com.sparrowwallet.drongo.wallet.Wallet;
|
|||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
|
@ -38,16 +39,32 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
|
|||
|
||||
@Override
|
||||
public List<String> getServerVersion(Transport transport, String clientName, String[] supportedVersions) {
|
||||
if(Config.get().getServerType() == ServerType.ELECTRUM_SERVER && Config.get().isLegacyServer()) {
|
||||
return getLegacyServerVersion(transport, clientName);
|
||||
}
|
||||
|
||||
try {
|
||||
JsonRpcClient client = new JsonRpcClient(transport);
|
||||
//Using 1.4 as the version number as EPS tries to parse this number to a float :(
|
||||
return new RetryLogic<List<String>>(MAX_RETRIES, RETRY_DELAY, IllegalStateException.class).getResult(() ->
|
||||
client.createRequest().returnAsList(String.class).method("server.version").id(idCounter.incrementAndGet()).params(clientName, "1.4").execute());
|
||||
client.createRequest().returnAsList(String.class).method("server.version").id(idCounter.incrementAndGet()).params(clientName, supportedVersions).execute());
|
||||
} catch(JsonRpcException e) {
|
||||
return getLegacyServerVersion(transport, clientName);
|
||||
} catch(Exception e) {
|
||||
throw new ElectrumServerRpcException("Error getting server version", e);
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> getLegacyServerVersion(Transport transport, String clientName) {
|
||||
try {
|
||||
//Fallback to using 1.4 as the version number as EPS tries to parse this number to a float :(
|
||||
JsonRpcClient client = new JsonRpcClient(transport);
|
||||
return new RetryLogic<List<String>>(MAX_RETRIES, RETRY_DELAY, IllegalStateException.class).getResult(() ->
|
||||
client.createRequest().returnAsList(String.class).method("server.version").id(idCounter.incrementAndGet()).params(clientName, "1.4").execute());
|
||||
} catch(Exception ex) {
|
||||
throw new ElectrumServerRpcException("Error getting legacy server version", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServerBanner(Transport transport) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -38,6 +38,6 @@ public class SubscriptionService {
|
|||
existingStatuses.add(status);
|
||||
}
|
||||
|
||||
Platform.runLater(() -> EventManager.get().post(new WalletNodeHistoryChangedEvent(scriptHash)));
|
||||
Platform.runLater(() -> EventManager.get().post(new WalletNodeHistoryChangedEvent(scriptHash, status)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,10 +35,11 @@ public class ElectrumServerService {
|
|||
}
|
||||
|
||||
@JsonRpcMethod("server.version")
|
||||
public List<String> getServerVersion(@JsonRpcParam("client_name") String clientName, @JsonRpcParam("protocol_version") String protocolVersion) throws UnsupportedVersionException {
|
||||
Version clientVersion = new Version(protocolVersion);
|
||||
public List<String> getServerVersion(@JsonRpcParam("client_name") String clientName, @JsonRpcParam("protocol_version") String[] protocolVersion) throws UnsupportedVersionException {
|
||||
String version = protocolVersion.length > 1 ? protocolVersion[1] : protocolVersion[0];
|
||||
Version clientVersion = new Version(version);
|
||||
if(clientVersion.compareTo(VERSION) < 0) {
|
||||
throw new UnsupportedVersionException(protocolVersion);
|
||||
throw new UnsupportedVersionException(version);
|
||||
}
|
||||
|
||||
return List.of(Cormorant.SERVER_NAME + " " + SparrowWallet.APP_VERSION, VERSION.get());
|
||||
|
|
|
|||
|
|
@ -1559,6 +1559,23 @@ public class HeadersController extends TransactionFormController implements Init
|
|||
|
||||
signButtonBox.setVisible(false);
|
||||
broadcastButtonBox.setVisible(true);
|
||||
|
||||
if(Config.get().hasServer() && !AppServices.isConnected() && !AppServices.isConnecting()) {
|
||||
if(Config.get().getConnectToBroadcast() == null) {
|
||||
Platform.runLater(() -> {
|
||||
ConfirmationAlert confirmationAlert = new ConfirmationAlert("Connect to broadcast?", "Connect to the configured server to broadcast the transaction?", ButtonType.NO, ButtonType.YES);
|
||||
Optional<ButtonType> optType = confirmationAlert.showAndWait();
|
||||
if(confirmationAlert.isDontAskAgain() && optType.isPresent()) {
|
||||
Config.get().setConnectToBroadcast(optType.get() == ButtonType.YES);
|
||||
}
|
||||
if(optType.isPresent() && optType.get() == ButtonType.YES) {
|
||||
EventManager.get().post(new RequestConnectEvent());
|
||||
}
|
||||
});
|
||||
} else if(Config.get().getConnectToBroadcast()) {
|
||||
Platform.runLater(() -> EventManager.get().post(new RequestConnectEvent()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -326,7 +326,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
recentBlocksView.visibleProperty().bind(Bindings.equal(feeRatesSelectionProperty, FeeRatesSelection.RECENT_BLOCKS));
|
||||
List<BlockSummary> blockSummaries = AppServices.getBlockSummaries().values().stream().sorted().toList();
|
||||
if(!blockSummaries.isEmpty()) {
|
||||
recentBlocksView.update(blockSummaries, AppServices.getDefaultFeeRate());
|
||||
recentBlocksView.update(blockSummaries, AppServices.getNextBlockMedianFeeRate());
|
||||
}
|
||||
|
||||
feeRatesSelectionProperty.addListener((_, oldValue, newValue) -> {
|
||||
|
|
@ -491,11 +491,35 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
validationSupport.setErrorDecorationEnabled(false);
|
||||
}
|
||||
|
||||
public Tab addPaymentTab() {
|
||||
public void addPaymentTab() {
|
||||
if(Config.get().getSuggestSendToMany() == null && openSendToMany()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Tab tab = getPaymentTab();
|
||||
paymentTabs.getTabs().add(tab);
|
||||
paymentTabs.getSelectionModel().select(tab);
|
||||
return tab;
|
||||
}
|
||||
|
||||
private boolean openSendToMany() {
|
||||
try {
|
||||
List<Payment> payments = getPayments();
|
||||
if(payments.size() == 3) {
|
||||
ConfirmationAlert confirmationAlert = new ConfirmationAlert("Open Send To Many?", "Open the Tools > Send To Many dialog to add multiple payments?", ButtonType.NO, ButtonType.YES);
|
||||
Optional<ButtonType> optType = confirmationAlert.showAndWait();
|
||||
if(confirmationAlert.isDontAskAgain() && optType.isPresent()) {
|
||||
Config.get().setSuggestSendToMany(optType.get() == ButtonType.YES);
|
||||
}
|
||||
if(optType.isPresent() && optType.get() == ButtonType.YES) {
|
||||
Platform.runLater(() -> EventManager.get().post(new RequestSendToManyEvent(payments)));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public Tab getPaymentTab() {
|
||||
|
|
@ -1205,7 +1229,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
|
||||
public void broadcastNotification(Wallet decryptedWallet) {
|
||||
try {
|
||||
PaymentCode paymentCode = decryptedWallet.getPaymentCode();
|
||||
PaymentCode paymentCode = decryptedWallet.isMasterWallet() ? decryptedWallet.getPaymentCode() : decryptedWallet.getMasterWallet().getPaymentCode();
|
||||
PaymentCode externalPaymentCode = paymentCodeProperty.get();
|
||||
WalletTransaction walletTransaction = walletTransactionProperty.get();
|
||||
WalletNode input0Node = walletTransaction.getSelectedUtxos().entrySet().iterator().next().getValue();
|
||||
|
|
@ -1411,7 +1435,12 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
setFeeRatePriority(getFeeRangeRate());
|
||||
}
|
||||
feeRange.updateTrackHighlight();
|
||||
recentBlocksView.updateFeeRate(event.getTargetBlockFeeRates());
|
||||
|
||||
if(event.getNextBlockMedianFeeRate() != null) {
|
||||
recentBlocksView.updateFeeRate(event.getNextBlockMedianFeeRate());
|
||||
} else {
|
||||
recentBlocksView.updateFeeRate(event.getTargetBlockFeeRates());
|
||||
}
|
||||
|
||||
if(updateDefaultFeeRate) {
|
||||
if(getFeeRate() != null && Long.valueOf((long)getFallbackFeeRate()).equals(getFeeRate().longValue())) {
|
||||
|
|
@ -1435,7 +1464,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
|
||||
@Subscribe
|
||||
public void blockSummary(BlockSummaryEvent event) {
|
||||
Platform.runLater(() -> recentBlocksView.update(AppServices.getBlockSummaries().values().stream().sorted().toList(), AppServices.getDefaultFeeRate()));
|
||||
Platform.runLater(() -> recentBlocksView.update(AppServices.getBlockSummaries().values().stream().sorted().toList(), AppServices.getNextBlockMedianFeeRate()));
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@
|
|||
<defs/>
|
||||
<g id="Layer-1">
|
||||
<g opacity="1">
|
||||
<path d="M11.1092 6.38238C11.7874 6.49541 12.6924 6.75737 13.0291 7.43079C13.3676 8.10768 12.7646 8.86191 12.0528 8.81338C11.6576 8.78644 11.4097 8.41273 11.1551 8.15812C10.9809 10.1088 10.2877 12.136 8.44235 13.0988C7.90851 13.3773 7.15886 13.6243 6.55521 13.4329C5.9407 13.2381 5.84716 12.4781 5.33643 12.1486C4.98351 11.9209 4.55155 11.9498 4.15042 11.9324C3.96346 11.9243 3.77148 11.9124 3.60655 11.8145C3.06601 11.4935 3.37994 10.7658 3.09545 10.3139C2.99212 10.1498 2.79588 10.0734 2.64333 9.96663C2.3666 9.77293 1.83235 9.33348 1.87667 8.96409C1.90244 8.74941 2.25019 8.74007 2.38778 8.72164C2.54701 8.70032 2.70699 8.68477 2.86611 8.66267C3.25833 8.6082 3.72578 8.49071 3.9866 8.16468C3.30258 8.77269 2.31035 8.12428 2.44675 7.26042C2.54818 6.618 3.3035 6.29432 3.849 6.13993C4.17298 6.04824 4.52084 5.97983 4.85809 6.02199C4.85809 5.54542 4.74612 5.08577 4.70738 4.61318C4.66874 4.14168 4.74205 3.65871 4.91051 3.21749C5.67321 1.21995 8.32474 0.966847 9.7201 2.44428C10.2289 2.98306 10.4928 3.66365 10.703 4.36419C10.9014 5.02542 11.0222 5.69867 11.1092 6.38238Z" fill="#989898" fill-opacity="0.996078" fill-rule="nonzero" opacity="1" stroke="none"/>
|
||||
<path d="M7.57741 4.35763C7.52047 4.27222 7.50984 4.17594 7.43325 4.09553C7.20899 3.86005 6.91345 4.08501 6.88939 4.35763C6.86774 4.60298 7.01074 4.97283 7.31531 4.92771C7.35869 4.92128 7.43451 4.88713 7.46602 4.85563C7.53563 4.78601 7.5612 4.67147 7.58396 4.58042C7.62201 5.15107 7.1347 5.78519 6.53555 5.4257C6.19393 5.22073 6.0003 4.77722 5.98514 4.3904C5.97127 4.03683 6.06143 3.5982 6.39795 3.40751C6.97483 3.08061 7.57741 3.82267 7.57741 4.35763Z" fill="#fefefe" fill-rule="nonzero" opacity="1" stroke="none"/>
|
||||
<path d="M9.70699 4.25279C9.67353 4.1524 9.63791 4.03833 9.55629 3.96448C9.24722 3.68485 8.99444 4.04682 9.01897 4.35763C9.03907 4.61216 9.30486 4.96596 9.57594 4.73768C9.66318 4.66422 9.68613 4.55355 9.71355 4.44937C9.75056 4.9675 9.30222 5.61975 8.73066 5.33397C8.34543 5.14135 8.13656 4.68734 8.11472 4.27245C8.10412 4.071 8.12355 3.87066 8.1999 3.68272C8.24175 3.57971 8.29741 3.4913 8.37027 3.40751C8.91987 2.77547 9.70699 3.64998 9.70699 4.25279Z" fill="#fefefe" fill-rule="nonzero" opacity="1" stroke="none"/>
|
||||
<path d="M9.28763 3.86619C9.63393 3.81072 9.79871 4.27401 9.68734 4.57387C9.64772 4.68053 9.56254 4.79847 9.43834 4.80976C8.96026 4.85322 8.82813 3.93979 9.28763 3.86619Z" fill="#000000" fill-rule="nonzero" opacity="1" stroke="none"/>
|
||||
<path d="M7.1777 3.99069C7.52459 3.9464 7.66131 4.42013 7.55775 4.70492C7.52103 4.80592 7.43318 4.92354 7.31531 4.93426C6.8277 4.97859 6.6978 4.05196 7.1777 3.99069Z" fill="#000000" fill-rule="nonzero" opacity="1" stroke="none"/>
|
||||
<path d="M10.8078 6.47573C11.4294 6.57932 12.2588 6.8194 12.5674 7.43658C12.8776 8.05695 12.325 8.74819 11.6726 8.70371C11.3104 8.67902 11.0832 8.33652 10.8499 8.10317C10.6902 9.89095 10.0549 11.7489 8.36366 12.6312C7.8744 12.8865 7.18736 13.1129 6.63412 12.9374C6.07093 12.7589 5.9852 12.0624 5.51712 11.7604C5.19368 11.5517 4.79779 11.5782 4.43016 11.5623C4.25881 11.5548 4.08287 11.5439 3.93171 11.4542C3.43631 11.16 3.72402 10.4931 3.46329 10.0789C3.36859 9.92852 3.18874 9.8585 3.04893 9.76065C2.79531 9.58313 2.30568 9.18038 2.3463 8.84184C2.36991 8.64508 2.68862 8.63652 2.81472 8.61963C2.96065 8.60009 3.10727 8.58584 3.25311 8.56559C3.61257 8.51567 4.04098 8.40799 4.28002 8.10919C3.65313 8.66642 2.74376 8.07216 2.86877 7.28044C2.96173 6.69167 3.65397 6.39502 4.15391 6.25353C4.45084 6.16949 4.76965 6.1068 5.07873 6.14544C5.07873 5.70867 4.97611 5.2874 4.94061 4.85428C4.90519 4.42216 4.97238 3.97952 5.12677 3.57515C5.82578 1.74442 8.25587 1.51246 9.5347 2.86651C10.001 3.36029 10.2429 3.98405 10.4355 4.62608C10.6174 5.23209 10.7281 5.84912 10.8078 6.47573Z" fill="#989898" fill-opacity="0.996078" fill-rule="nonzero" opacity="1" stroke="none"/>
|
||||
<path d="M7.57096 4.62007C7.51877 4.54179 7.50903 4.45355 7.43883 4.37986C7.2333 4.16404 6.96244 4.37022 6.94039 4.62007C6.92055 4.84493 7.05161 5.18389 7.33074 5.14254C7.3705 5.13665 7.43999 5.10535 7.46887 5.07648C7.53266 5.01268 7.5561 4.9077 7.57696 4.82425C7.61183 5.34725 7.16522 5.92841 6.6161 5.59894C6.30301 5.41109 6.12555 5.00462 6.11166 4.6501C6.09895 4.32606 6.18158 3.92406 6.48999 3.7493C7.0187 3.4497 7.57096 4.12979 7.57096 4.62007Z" fill="#fefefe" fill-rule="nonzero" opacity="1" stroke="none"/>
|
||||
<path d="M9.52269 4.52399C9.49202 4.43198 9.45938 4.32744 9.38457 4.25975C9.10132 4.00348 8.86965 4.33522 8.89213 4.62007C8.91055 4.85334 9.15414 5.1776 9.40258 4.96838C9.48254 4.90106 9.50357 4.79963 9.5287 4.70415C9.56262 5.17901 9.15172 5.77679 8.62789 5.51487C8.27484 5.33834 8.08341 4.92225 8.06339 4.542C8.05368 4.35738 8.07149 4.17377 8.14146 4.00152C8.17981 3.90712 8.23083 3.82609 8.2976 3.7493C8.8013 3.17004 9.52269 3.97152 9.52269 4.52399Z" fill="#fefefe" fill-rule="nonzero" opacity="1" stroke="none"/>
|
||||
<path d="M9.13835 4.16967C9.45573 4.11883 9.60675 4.54343 9.50468 4.81825C9.46837 4.916 9.3903 5.0241 9.27648 5.03444C8.83832 5.07427 8.71722 4.23713 9.13835 4.16967Z" fill="#000000" fill-rule="nonzero" opacity="1" stroke="none"/>
|
||||
<path d="M7.20463 4.28377C7.52255 4.24318 7.64785 4.67735 7.55294 4.93836C7.51928 5.03092 7.43877 5.13872 7.33074 5.14855C6.88386 5.18917 6.7648 4.33993 7.20463 4.28377Z" fill="#000000" fill-rule="nonzero" opacity="1" stroke="none"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
|
@ -5,11 +5,11 @@
|
|||
<defs/>
|
||||
<g id="Layer-1">
|
||||
<g opacity="1">
|
||||
<path d="M11.1099 6.38217C11.7882 6.49522 12.6934 6.75723 13.0302 7.43078C13.3687 8.10779 12.7656 8.86217 12.0537 8.81363C11.6584 8.78668 11.4105 8.41291 11.1558 8.15825C10.9816 10.1093 10.2882 12.1368 8.44253 13.0998C7.90859 13.3784 7.15879 13.6255 6.55503 13.4341C5.94041 13.2392 5.84685 12.4791 5.33602 12.1495C4.98304 11.9218 4.55099 11.9507 4.14978 11.9332C3.96279 11.9251 3.77077 11.9132 3.60582 11.8153C3.06517 11.4943 3.37916 10.7664 3.09462 10.3145C2.99126 10.1503 2.79498 10.0739 2.64241 9.9671C2.36563 9.77336 1.83127 9.33383 1.87561 8.96437C1.90138 8.74965 2.2492 8.74031 2.38681 8.72188C2.54607 8.70055 2.70608 8.685 2.86523 8.66289C3.25752 8.60841 3.72506 8.4909 3.98594 8.1648C3.30179 8.77293 2.30937 8.1244 2.44579 7.26038C2.54725 6.61783 3.3027 6.29409 3.84831 6.13967C4.17235 6.04797 4.52028 5.97954 4.85759 6.02171C4.85759 5.54505 4.7456 5.08532 4.70686 4.61264C4.6682 4.14104 4.74153 3.65798 4.91002 3.21668C5.67286 1.21876 8.3249 0.965609 9.72052 2.44333C10.2295 2.98221 10.4934 3.66293 10.7036 4.36359C10.902 5.02495 11.0229 5.69833 11.1099 6.38217Z" fill="#242424" fill-opacity="0.996078" fill-rule="nonzero" opacity="1" stroke="none"/>
|
||||
<path d="M7.57742 4.35704C7.52047 4.27161 7.50984 4.17531 7.43324 4.09489C7.20893 3.85937 6.91333 4.08436 6.88927 4.35704C6.86762 4.60243 7.01064 4.97235 7.31527 4.92722C7.35866 4.92079 7.4345 4.88664 7.46601 4.85513C7.53564 4.7855 7.56121 4.67093 7.58398 4.57987C7.62203 5.15062 7.13463 5.78487 6.53537 5.42531C6.19368 5.2203 6.00002 4.7767 5.98485 4.38981C5.97098 4.03617 6.06116 3.59746 6.39774 3.40674C6.97473 3.07977 7.57742 3.82198 7.57742 4.35704Z" fill="#fefefe" fill-rule="nonzero" opacity="1" stroke="none"/>
|
||||
<path d="M9.70741 4.25218C9.67394 4.15177 9.63832 4.03767 9.55667 3.96381C9.24755 3.68413 8.99472 4.04616 9.01926 4.35704C9.03936 4.61161 9.3052 4.96548 9.57634 4.73716C9.66359 4.66368 9.68655 4.55299 9.71397 4.44879C9.75098 4.96702 9.30256 5.61939 8.73089 5.33356C8.34559 5.1409 8.13668 4.68681 8.11484 4.27184C8.10423 4.07035 8.12367 3.86997 8.20004 3.682C8.24189 3.57897 8.29756 3.49054 8.37044 3.40674C8.92014 2.77458 9.70741 3.64925 9.70741 4.25218Z" fill="#fefefe" fill-rule="nonzero" opacity="1" stroke="none"/>
|
||||
<path d="M9.28797 3.8655C9.63434 3.81002 9.79915 4.2734 9.68775 4.57331C9.64813 4.67999 9.56293 4.79796 9.43871 4.80925C8.96053 4.85272 8.82838 3.93912 9.28797 3.8655Z" fill="#000000" fill-rule="nonzero" opacity="1" stroke="none"/>
|
||||
<path d="M7.17764 3.99002C7.5246 3.94573 7.66134 4.41955 7.55776 4.70439C7.52103 4.80541 7.43317 4.92306 7.31527 4.93377C6.82757 4.97811 6.69764 4.05131 7.17764 3.99002Z" fill="#000000" fill-rule="nonzero" opacity="1" stroke="none"/>
|
||||
<path d="M10.8091 6.47531C11.4309 6.57894 12.2606 6.81912 12.5694 7.43654C12.8797 8.05714 12.3268 8.74865 11.6742 8.70416C11.3119 8.67945 11.0846 8.33683 10.8512 8.10339C10.6915 9.89186 10.0559 11.7504 8.36399 12.6332C7.87454 12.8885 7.18722 13.1151 6.63377 12.9396C6.07037 12.7609 5.98461 12.0642 5.51634 11.762C5.19278 11.5533 4.79673 11.5798 4.42895 11.5638C4.25754 11.5563 4.08153 11.5454 3.93032 11.4557C3.43472 11.1614 3.72255 10.4942 3.46172 10.08C3.36697 9.92944 3.18705 9.85941 3.04719 9.76151C2.79348 9.58391 2.30365 9.18101 2.34429 8.84234C2.36791 8.64551 2.68675 8.63695 2.81289 8.62005C2.95888 8.6005 3.10556 8.58625 3.25144 8.56598C3.61105 8.51604 4.03962 8.40832 4.27877 8.10939C3.65163 8.66685 2.7419 8.07236 2.86696 7.28034C2.95996 6.69133 3.65246 6.39457 4.1526 6.25302C4.44964 6.16896 4.76858 6.10623 5.07778 6.14489C5.07778 5.70795 4.97512 5.28653 4.93961 4.85324C4.90417 4.42094 4.97139 3.97813 5.12584 3.5736C5.82511 1.74217 8.25616 1.51012 9.53548 2.8647C10.002 3.35867 10.244 3.98267 10.4366 4.62494C10.6185 5.23119 10.7293 5.84846 10.8091 6.47531Z" fill="#242424" fill-opacity="0.996078" fill-rule="nonzero" opacity="1" stroke="none"/>
|
||||
<path d="M7.57097 4.61894C7.51876 4.54063 7.50902 4.45235 7.4388 4.37863C7.23318 4.16274 6.96221 4.36898 6.94016 4.61894C6.92031 4.84388 7.05142 5.18297 7.33066 5.1416C7.37044 5.13571 7.43996 5.10441 7.46884 5.07552C7.53267 5.01169 7.55611 4.90667 7.57698 4.8232C7.61186 5.34639 7.16507 5.92779 6.61575 5.59819C6.30253 5.41026 6.12501 5.00363 6.11111 4.64898C6.09839 4.32481 6.18106 3.92265 6.48959 3.74783C7.0185 3.4481 7.57097 4.12846 7.57097 4.61894Z" fill="#fefefe" fill-rule="nonzero" opacity="1" stroke="none"/>
|
||||
<path d="M9.52346 4.52282C9.49278 4.43077 9.46013 4.32618 9.38528 4.25848C9.10192 4.0021 8.87016 4.33396 8.89266 4.61894C8.91108 4.85229 9.15477 5.17668 9.40331 4.96738C9.48329 4.90002 9.50434 4.79856 9.52948 4.70304C9.5634 5.17809 9.15235 5.77609 8.62832 5.51408C8.27512 5.33748 8.08362 4.92123 8.0636 4.54084C8.05388 4.35614 8.0717 4.17245 8.1417 4.00015C8.18007 3.9057 8.2311 3.82464 8.2979 3.74783C8.8018 3.16834 9.52346 3.97013 9.52346 4.52282Z" fill="#fefefe" fill-rule="nonzero" opacity="1" stroke="none"/>
|
||||
<path d="M9.13897 4.16836C9.45648 4.1175 9.60756 4.54227 9.50544 4.81719C9.46912 4.91498 9.39102 5.02312 9.27715 5.03346C8.83882 5.07331 8.71768 4.23584 9.13897 4.16836Z" fill="#000000" fill-rule="nonzero" opacity="1" stroke="none"/>
|
||||
<path d="M7.2045 4.2825C7.52255 4.2419 7.64789 4.67624 7.55294 4.93734C7.51927 5.02994 7.43874 5.13779 7.33066 5.14761C6.8836 5.18825 6.7645 4.33868 7.2045 4.2825Z" fill="#000000" fill-rule="nonzero" opacity="1" stroke="none"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
Loading…
Reference in a new issue