Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73d4fd5049 | ||
|
|
a94380e882 | ||
|
|
e4dd4950bf | ||
|
|
26ce1b3469 | ||
|
|
ebce34f3d1 | ||
|
|
f28e00b97e | ||
|
|
25770c2426 | ||
|
|
799cac7b1f | ||
|
|
c265fd1969 | ||
|
|
890f0476b1 | ||
|
|
4d93381124 | ||
|
|
364909cfa3 | ||
|
|
38f0068411 | ||
|
|
8885e48ed9 | ||
|
|
31ce3ce68a | ||
|
|
b0d0514617 | ||
|
|
d7d23f9b58 | ||
|
|
3fdf093a26 | ||
|
|
74c298fd93 | ||
|
|
4298bfb053 | ||
|
|
231eb13cee | ||
|
|
52470ee6d8 | ||
|
|
853949675e | ||
|
|
098afebbe0 | ||
|
|
63c0a6d6e2 | ||
|
|
77c305f90b | ||
|
|
276f8b4148 | ||
|
|
b3c92617c9 | ||
|
|
58635801fc | ||
|
|
8c32bb3903 | ||
|
|
55a2c86a83 | ||
|
|
345e018eb9 | ||
|
|
45d2dee764 | ||
|
|
250bc84060 |
@@ -30,6 +30,9 @@ jobs:
|
||||
- name: Package tar distribution
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: ./gradlew packageTarDistribution
|
||||
- name: Repackage deb distribution
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: ./repackage.sh
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -43,6 +46,9 @@ jobs:
|
||||
- name: Package headless tar distribution
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: ./gradlew -Djava.awt.headless=true packageTarDistribution
|
||||
- name: Repackage headless deb distribution
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: ./repackage.sh
|
||||
- name: Upload Headless Artifact
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -3,7 +3,7 @@ plugins {
|
||||
id 'org-openjfx-javafxplugin'
|
||||
id 'org.beryx.jlink' version '3.1.1'
|
||||
id 'org.gradlex.extra-java-module-info' version '1.9'
|
||||
id 'io.matthewnelson.kmp.tor.resource-filterjar' version '408.16.2'
|
||||
id 'io.matthewnelson.kmp.tor.resource-filterjar' version '408.16.3'
|
||||
}
|
||||
|
||||
def os = org.gradle.internal.os.OperatingSystem.current()
|
||||
@@ -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.0'
|
||||
version '2.2.3'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
@@ -74,9 +74,9 @@ 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.2")
|
||||
implementation("io.matthewnelson.kmp-tor:resource-exec-tor-gpl:408.16.3")
|
||||
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.10.1') {
|
||||
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
|
||||
}
|
||||
|
||||
@@ -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.1.3"
|
||||
GIT_TAG="2.2.2"
|
||||
```
|
||||
|
||||
The project can then be initially cloned as follows:
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
set -e # Exit on any error
|
||||
|
||||
# Define paths
|
||||
BUILD_DIR="build"
|
||||
JPACKAGE_DIR="$BUILD_DIR/jpackage"
|
||||
TEMP_DIR="$BUILD_DIR/repackage"
|
||||
|
||||
# Find the .deb file in build/jpackage (assuming there is only one)
|
||||
DEB_FILE=$(find "$JPACKAGE_DIR" -type f -name "*.deb" -print -quit)
|
||||
|
||||
# Check if a .deb file was found
|
||||
if [ -z "$DEB_FILE" ]; then
|
||||
echo "Error: No .deb file found in $JPACKAGE_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract the filename from the path for later use
|
||||
DEB_FILENAME=$(basename "$DEB_FILE")
|
||||
|
||||
echo "Found .deb file: $DEB_FILENAME"
|
||||
|
||||
# Create a temp directory inside build to avoid file conflicts
|
||||
mkdir -p "$TEMP_DIR"
|
||||
cd "$TEMP_DIR"
|
||||
|
||||
# Extract the .deb file contents
|
||||
ar x "../../$DEB_FILE"
|
||||
|
||||
# Decompress zst files to tar
|
||||
unzstd control.tar.zst
|
||||
unzstd data.tar.zst
|
||||
|
||||
# Compress tar files to xz
|
||||
xz -c control.tar > control.tar.xz
|
||||
xz -c data.tar > data.tar.xz
|
||||
|
||||
# Remove the original .deb file
|
||||
rm "../../$DEB_FILE"
|
||||
|
||||
# Create the new .deb file with xz compression in the original location
|
||||
ar cr "../../$DEB_FILE" debian-binary control.tar.xz data.tar.xz
|
||||
|
||||
# Clean up temp files
|
||||
cd ../..
|
||||
rm -rf "$TEMP_DIR"
|
||||
|
||||
echo "Repackaging complete: $DEB_FILENAME"
|
||||
@@ -21,7 +21,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.2.0</string>
|
||||
<string>2.2.3</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
||||
|
||||
@@ -1422,6 +1422,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);
|
||||
@@ -1437,7 +1441,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;
|
||||
@@ -3107,6 +3111,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.0";
|
||||
public static final String APP_VERSION = "2.2.3";
|
||||
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";
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.sparrowwallet.sparrow.control;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.sparrow.BlockSummary;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.net.FeeRatesSource;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
@@ -15,6 +17,7 @@ import javafx.scene.text.FontWeight;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import javafx.util.Duration;
|
||||
import org.girod.javafx.svgimage.SVGImage;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
@@ -26,12 +29,13 @@ public class BlockCube extends Group {
|
||||
public static final double CUBE_SIZE = 60;
|
||||
|
||||
private final IntegerProperty weightProperty = new SimpleIntegerProperty(0);
|
||||
private final DoubleProperty medianFeeProperty = new SimpleDoubleProperty(-1.0d);
|
||||
private final DoubleProperty medianFeeProperty = new SimpleDoubleProperty(-Double.MAX_VALUE);
|
||||
private final IntegerProperty heightProperty = new SimpleIntegerProperty(0);
|
||||
private final IntegerProperty txCountProperty = new SimpleIntegerProperty(0);
|
||||
private final LongProperty timestampProperty = new SimpleLongProperty(System.currentTimeMillis());
|
||||
private final StringProperty elapsedProperty = new SimpleStringProperty("");
|
||||
private final BooleanProperty confirmedProperty = new SimpleBooleanProperty(false);
|
||||
private final ObjectProperty<FeeRatesSource> feeRatesSource = new SimpleObjectProperty<>(null);
|
||||
|
||||
private Polygon front;
|
||||
private Rectangle unusedArea;
|
||||
@@ -43,21 +47,27 @@ public class BlockCube extends Group {
|
||||
private final TextFlow medianFeeTextFlow = new TextFlow();
|
||||
private final Text txCountText = new Text();
|
||||
private final Text elapsedText = new Text();
|
||||
private final Group feeRateIcon = new 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);
|
||||
|
||||
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
|
||||
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
|
||||
this.feeRatesSource.set(feeRatesSource);
|
||||
|
||||
this.weightProperty.addListener((_, _, _) -> {
|
||||
if(front != null) {
|
||||
updateFill();
|
||||
}
|
||||
});
|
||||
this.medianFeeProperty.addListener((_, _, newValue) -> {
|
||||
medianFeeText.setText("~" + Math.round(Math.max(newValue.doubleValue(), 1.0d)));
|
||||
unitsText.setText(" s/vb");
|
||||
medianFeeText.setText(newValue.doubleValue() < 0.0d ? "" : "~" + Math.round(Math.max(newValue.doubleValue(), 1.0d)));
|
||||
unitsText.setText(newValue.doubleValue() < 0.0d ? "" : " s/vb");
|
||||
double medianFeeWidth = TextUtils.computeTextWidth(medianFeeText.getFont(), medianFeeText.getText(), 0.0d);
|
||||
double unitsWidth = TextUtils.computeTextWidth(unitsText.getFont(), unitsText.getText(), 0.0d);
|
||||
medianFeeTextFlow.setTranslateX((CUBE_SIZE - (medianFeeText.getLayoutBounds().getWidth() + unitsWidth)) / 2);
|
||||
medianFeeTextFlow.setTranslateX((CUBE_SIZE - (medianFeeWidth + unitsWidth)) / 2);
|
||||
});
|
||||
this.txCountProperty.addListener((_, _, newValue) -> {
|
||||
txCountText.setText(newValue.intValue() == 0 ? "" : newValue + " txes");
|
||||
@@ -79,6 +89,11 @@ public class BlockCube extends Group {
|
||||
updateFill();
|
||||
}
|
||||
});
|
||||
this.feeRatesSource.addListener((_, _, _) -> {
|
||||
if(front != null) {
|
||||
updateFill();
|
||||
}
|
||||
});
|
||||
this.medianFeeText.textProperty().addListener((_, _, _) -> {
|
||||
pulse();
|
||||
});
|
||||
@@ -145,12 +160,15 @@ public class BlockCube extends Group {
|
||||
txCountText.setX((CUBE_SIZE - txCountText.getLayoutBounds().getWidth()) / 2);
|
||||
txCountText.setY(34);
|
||||
|
||||
feeRateIcon.setTranslateX(((CUBE_SIZE * 0.7) - 14) / 2);
|
||||
feeRateIcon.setTranslateY(-36);
|
||||
|
||||
elapsedText.getStyleClass().add("block-text");
|
||||
elapsedText.setFont(new Font(10));
|
||||
elapsedText.setX((CUBE_SIZE - elapsedText.getLayoutBounds().getWidth()) / 2);
|
||||
elapsedText.setY(50);
|
||||
|
||||
getChildren().addAll(frontFaceGroup, top, left, heightText, medianFeeTextFlow, txCountText, elapsedText);
|
||||
getChildren().addAll(frontFaceGroup, top, left, heightText, medianFeeTextFlow, txCountText, feeRateIcon, elapsedText);
|
||||
}
|
||||
|
||||
private void updateFill() {
|
||||
@@ -167,6 +185,7 @@ public class BlockCube extends Group {
|
||||
usedArea.setHeight(CUBE_SIZE - startYAbsolute);
|
||||
usedArea.setVisible(true);
|
||||
heightText.setVisible(true);
|
||||
feeRateIcon.getChildren().clear();
|
||||
} else {
|
||||
getStyleClass().removeAll("block-confirmed");
|
||||
if(!getStyleClass().contains("block-unconfirmed")) {
|
||||
@@ -175,6 +194,16 @@ public class BlockCube extends Group {
|
||||
usedArea.setVisible(false);
|
||||
unusedArea.setStyle("-fx-fill: " + getFeeRateStyleName() + ";");
|
||||
heightText.setVisible(false);
|
||||
if(feeRatesSource.get() != null) {
|
||||
SVGImage svgImage = feeRatesSource.get().getSVGImage();
|
||||
if(svgImage != null) {
|
||||
feeRateIcon.getChildren().setAll(feeRatesSource.get().getSVGImage());
|
||||
} else {
|
||||
feeRateIcon.getChildren().clear();
|
||||
}
|
||||
} else {
|
||||
feeRateIcon.getChildren().clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,8 +353,20 @@ public class BlockCube extends Group {
|
||||
confirmedProperty.set(confirmed);
|
||||
}
|
||||
|
||||
public FeeRatesSource getFeeRatesSource() {
|
||||
return feeRatesSource.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<FeeRatesSource> feeRatesSourceProperty() {
|
||||
return feeRatesSource;
|
||||
}
|
||||
|
||||
public void setFeeRatesSource(FeeRatesSource feeRatesSource) {
|
||||
this.feeRatesSource.set(feeRatesSource);
|
||||
}
|
||||
|
||||
public static BlockCube fromBlockSummary(BlockSummary blockSummary) {
|
||||
return new BlockCube(blockSummary.getWeight().orElse(0), blockSummary.getMedianFee().orElse(1.0d), blockSummary.getHeight(),
|
||||
return new BlockCube(blockSummary.getWeight().orElse(0), blockSummary.getMedianFee().orElse(-1.0d), blockSummary.getHeight(),
|
||||
blockSummary.getTransactionCount().orElse(0), blockSummary.getTimestamp().getTime(), true);
|
||||
}
|
||||
}
|
||||
@@ -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(() -> {
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.BlockSummary;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.net.FeeRatesSource;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
|
||||
import javafx.animation.TranslateTransition;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.shape.Line;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
@@ -14,8 +17,12 @@ import javafx.util.Duration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppServices.TARGET_BLOCKS_RANGE;
|
||||
import static com.sparrowwallet.sparrow.control.BlockCube.CUBE_SIZE;
|
||||
|
||||
public class RecentBlocksView extends Pane {
|
||||
private static final double CUBE_SPACING = 100;
|
||||
private static final double ANIMATION_DURATION_MILLIS = 1000;
|
||||
@@ -24,6 +31,7 @@ public class RecentBlocksView extends Pane {
|
||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||
|
||||
private final ObjectProperty<List<BlockCube>> cubesProperty = new SimpleObjectProperty<>(new ArrayList<>());
|
||||
private final Tooltip tooltip = new Tooltip();
|
||||
|
||||
public RecentBlocksView() {
|
||||
cubesProperty.addListener((_, _, newValue) -> {
|
||||
@@ -41,6 +49,18 @@ public class RecentBlocksView extends Pane {
|
||||
cube.setElapsed(BlockCube.getElapsed(cube.getTimestamp()));
|
||||
}
|
||||
}));
|
||||
|
||||
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
|
||||
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
|
||||
updateFeeRatesSource(feeRatesSource);
|
||||
Tooltip.install(this, tooltip);
|
||||
}
|
||||
|
||||
public void updateFeeRatesSource(FeeRatesSource feeRatesSource) {
|
||||
tooltip.setText("Fee rate estimate from " + feeRatesSource.getDescription());
|
||||
if(getCubes() != null && !getCubes().isEmpty()) {
|
||||
getCubes().getFirst().setFeeRatesSource(feeRatesSource);
|
||||
}
|
||||
}
|
||||
|
||||
public void drawView() {
|
||||
@@ -54,7 +74,7 @@ public class RecentBlocksView extends Pane {
|
||||
}
|
||||
|
||||
private void createSeparator() {
|
||||
Line separator = new Line(SEPARATOR_X, -9, SEPARATOR_X, 80);
|
||||
Line separator = new Line(SEPARATOR_X, -9, SEPARATOR_X, CUBE_SIZE);
|
||||
separator.getStyleClass().add("blocks-separator");
|
||||
separator.getStrokeDashArray().addAll(5.0, 5.0); // Create dotted line pattern
|
||||
separator.setStrokeWidth(1.0);
|
||||
@@ -73,14 +93,14 @@ public class RecentBlocksView extends Pane {
|
||||
if(latestTip > knownTip) {
|
||||
addNewBlock(latestBlocks, currentFeeRate);
|
||||
} else {
|
||||
for(int i = 1; i < getCubes().size() && i < latestBlocks.size(); i++) {
|
||||
for(int i = 1; i < getCubes().size() && i <= latestBlocks.size(); i++) {
|
||||
BlockCube blockCube = getCubes().get(i);
|
||||
BlockSummary latestBlock = latestBlocks.get(i);
|
||||
BlockSummary latestBlock = latestBlocks.get(i - 1);
|
||||
blockCube.setConfirmed(true);
|
||||
blockCube.setHeight(latestBlock.getHeight());
|
||||
blockCube.setTimestamp(latestBlock.getTimestamp().getTime());
|
||||
blockCube.setWeight(latestBlock.getWeight().orElse(0));
|
||||
blockCube.setMedianFee(latestBlock.getMedianFee().orElse(0.0d));
|
||||
blockCube.setMedianFee(latestBlock.getMedianFee().orElse(-1.0d));
|
||||
blockCube.setTxCount(latestBlock.getTransactionCount().orElse(0));
|
||||
}
|
||||
updateFeeRate(currentFeeRate);
|
||||
@@ -88,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;
|
||||
}
|
||||
@@ -100,7 +120,7 @@ public class RecentBlocksView extends Pane {
|
||||
blockCube.setHeight(latestBlock.getHeight());
|
||||
blockCube.setTimestamp(latestBlock.getTimestamp().getTime());
|
||||
blockCube.setWeight(latestBlock.getWeight().orElse(0));
|
||||
blockCube.setMedianFee(latestBlock.getMedianFee().orElse(0.0d));
|
||||
blockCube.setMedianFee(latestBlock.getMedianFee().orElse(-1.0d));
|
||||
blockCube.setTxCount(latestBlock.getTransactionCount().orElse(0));
|
||||
}
|
||||
|
||||
@@ -120,6 +140,14 @@ public class RecentBlocksView extends Pane {
|
||||
}
|
||||
}
|
||||
|
||||
public void updateFeeRate(Map<Integer, Double> targetBlockFeeRates) {
|
||||
int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1);
|
||||
if(targetBlockFeeRates.get(defaultTarget) != null) {
|
||||
Double defaultRate = targetBlockFeeRates.get(defaultTarget);
|
||||
updateFeeRate(defaultRate);
|
||||
}
|
||||
}
|
||||
|
||||
public void updateFeeRate(Double currentFeeRate) {
|
||||
if(!getCubes().isEmpty()) {
|
||||
BlockCube firstCube = getCubes().getFirst();
|
||||
|
||||
@@ -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;
|
||||
@@ -347,6 +350,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() {
|
||||
@@ -549,6 +571,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);
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.io.Server;
|
||||
import org.girod.javafx.svgimage.SVGImage;
|
||||
import org.girod.javafx.svgimage.SVGLoader;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.Locale;
|
||||
|
||||
public enum BlockExplorer {
|
||||
MEMPOOL_SPACE("https://mempool.space"),
|
||||
BLOCKSTREAM_INFO("https://blockstream.info"),
|
||||
NONE("http://none");
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(BlockExplorer.class);
|
||||
|
||||
private final Server server;
|
||||
|
||||
BlockExplorer(String url) {
|
||||
@@ -16,4 +26,17 @@ public enum BlockExplorer {
|
||||
public Server getServer() {
|
||||
return server;
|
||||
}
|
||||
|
||||
public static SVGImage getSVGImage(Server server) {
|
||||
try {
|
||||
URL url = AppServices.class.getResource("/image/blockexplorer/" + server.getHost().toLowerCase(Locale.ROOT) + "-icon.svg");
|
||||
if(url != null) {
|
||||
return SVGLoader.load(url);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.error("Could not load block explorer image for " + server.getHost());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -261,7 +265,7 @@ public class ElectrumServer {
|
||||
return 0;
|
||||
});
|
||||
|
||||
return txos.stream().map(txo -> new ScriptHashTx(txo.getHeight(), txo.getHashAsString(), txo.getFee())).toList();
|
||||
return txos.stream().map(txo -> new ScriptHashTx(txo.getHeight(), txo.getHashAsString(), txo.getFee() == null ? 0 : txo.getFee())).toList();
|
||||
}
|
||||
|
||||
private static String getScriptHashStatus(List<ScriptHashTx> scriptHashTxes) {
|
||||
@@ -439,7 +443,7 @@ public class ElectrumServer {
|
||||
blkTx.getTransaction().getInputs().stream().map(txInput -> getPrevOutput(wallet, txInput))
|
||||
.filter(Objects::nonNull).map(ElectrumServer::getScriptHash).anyMatch(scriptHash::equals)) {
|
||||
List<ScriptHashTx> scriptHashTxes = new ArrayList<>(getScriptHashes(scriptHash, node));
|
||||
scriptHashTxes.add(new ScriptHashTx(0, txid.toString(), blkTx.getFee()));
|
||||
scriptHashTxes.add(new ScriptHashTx(0, txid.toString(), blkTx.getFee() == null ? 0 : blkTx.getFee()));
|
||||
|
||||
String status = getScriptHashStatus(scriptHashTxes);
|
||||
if(Objects.equals(status, statuses.getLast())) {
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -9,9 +9,12 @@ import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import org.apache.commons.lang3.time.DateUtils;
|
||||
import org.girod.javafx.svgimage.SVGImage;
|
||||
import org.girod.javafx.svgimage.SVGLoader;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.URL;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.Instant;
|
||||
@@ -297,6 +300,19 @@ public enum ExchangeSource {
|
||||
return name;
|
||||
}
|
||||
|
||||
public SVGImage getSVGImage() {
|
||||
try {
|
||||
URL url = AppServices.class.getResource("/image/exchangesource/" + name.toLowerCase(Locale.ROOT) + "-icon.svg");
|
||||
if(url != null) {
|
||||
return SVGLoader.load(url);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.error("Could not load exchange source image for " + name);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static class CurrenciesService extends Service<List<Currency>> {
|
||||
private final ExchangeSource exchangeSource;
|
||||
|
||||
|
||||
@@ -7,9 +7,12 @@ import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.BlockSummary;
|
||||
import org.girod.javafx.svgimage.SVGImage;
|
||||
import org.girod.javafx.svgimage.SVGLoader;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
|
||||
public enum FeeRatesSource {
|
||||
@@ -31,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());
|
||||
@@ -127,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");
|
||||
}
|
||||
@@ -196,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);
|
||||
@@ -275,6 +312,27 @@ public enum FeeRatesSource {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return switch(this) {
|
||||
case ELECTRUM_SERVER -> "server";
|
||||
case MINIMUM -> "settings";
|
||||
default -> getName().toLowerCase(Locale.ROOT);
|
||||
};
|
||||
}
|
||||
|
||||
public SVGImage getSVGImage() {
|
||||
try {
|
||||
URL url = AppServices.class.getResource("/image/feeratesource/" + getDescription() + "-icon.svg");
|
||||
if(url != null) {
|
||||
return SVGLoader.load(url);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.error("Could not load fee rates source image for " + name);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected record ThreeTierRates(Double fastestFee, Double halfHourFee, Double hourFee, Double minimumFee) {}
|
||||
|
||||
private record OxtRates(OxtRatesData[] data) {}
|
||||
@@ -285,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());
|
||||
|
||||
@@ -87,6 +87,8 @@ public class GeneralSettingsController extends SettingsDetailController {
|
||||
config.setFeeRatesSource(feeRatesSource.getValue());
|
||||
}
|
||||
|
||||
feeRatesSource.setCellFactory(_ -> new FeeRatesSourceListCell());
|
||||
feeRatesSource.setButtonCell(feeRatesSource.getCellFactory().call(null));
|
||||
feeRatesSource.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
config.setFeeRatesSource(newValue);
|
||||
EventManager.get().post(new FeeRatesSourceChangedEvent(newValue));
|
||||
@@ -96,25 +98,8 @@ public class GeneralSettingsController extends SettingsDetailController {
|
||||
currenciesLoadWarning.setVisible(false);
|
||||
|
||||
blockExplorers.setItems(getBlockExplorerList());
|
||||
blockExplorers.setConverter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(Server server) {
|
||||
if(server == null || server == BlockExplorer.NONE.getServer()) {
|
||||
return "None";
|
||||
}
|
||||
|
||||
if(server == CUSTOM_BLOCK_EXPLORER) {
|
||||
return "Custom...";
|
||||
}
|
||||
|
||||
return server.getHost();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Server fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
blockExplorers.setCellFactory(_ -> new BlockExplorerListCell());
|
||||
blockExplorers.setButtonCell(blockExplorers.getCellFactory().call(null));
|
||||
blockExplorers.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue != null) {
|
||||
if(newValue == CUSTOM_BLOCK_EXPLORER) {
|
||||
@@ -258,14 +243,50 @@ public class GeneralSettingsController extends SettingsDetailController {
|
||||
fiatCurrency.valueProperty().addListener(fiatCurrencyListener);
|
||||
}
|
||||
|
||||
private static class FeeRatesSourceListCell extends ListCell<FeeRatesSource> {
|
||||
@Override
|
||||
protected void updateItem(FeeRatesSource item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if(empty || item == null) {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
} else {
|
||||
setText(item.toString());
|
||||
setGraphic(item.getSVGImage());
|
||||
setGraphicTextGap(8.0d);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class BlockExplorerListCell extends ListCell<Server> {
|
||||
@Override
|
||||
protected void updateItem(Server server, boolean empty) {
|
||||
super.updateItem(server, empty);
|
||||
if(empty || server == null || server == BlockExplorer.NONE.getServer()) {
|
||||
setText("None");
|
||||
setGraphic(null);
|
||||
} else if(server == CUSTOM_BLOCK_EXPLORER) {
|
||||
setText("Custom...");
|
||||
setGraphic(null);
|
||||
} else {
|
||||
setText(server.getHost());
|
||||
setGraphic(BlockExplorer.getSVGImage(server));
|
||||
setGraphicTextGap(8.0d);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class ExchangeSourceButtonCell extends ListCell<ExchangeSource> {
|
||||
@Override
|
||||
protected void updateItem(ExchangeSource exchangeSource, boolean empty) {
|
||||
super.updateItem(exchangeSource, empty);
|
||||
if(exchangeSource == null || empty) {
|
||||
setText("");
|
||||
setGraphic(null);
|
||||
} else {
|
||||
setText(exchangeSource.getName());
|
||||
setGraphic(exchangeSource.getSVGImage());
|
||||
setGraphicTextGap(8.0d);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -276,12 +297,15 @@ public class GeneralSettingsController extends SettingsDetailController {
|
||||
super.updateItem(exchangeSource, empty);
|
||||
if(exchangeSource == null || empty) {
|
||||
setText("");
|
||||
setGraphic(null);
|
||||
} else {
|
||||
String text = exchangeSource.getName();
|
||||
if(exchangeSource.getDescription() != null && !exchangeSource.getDescription().isEmpty()) {
|
||||
text += " (" + exchangeSource.getDescription() + ")";
|
||||
}
|
||||
setText(text);
|
||||
setGraphic(exchangeSource.getSVGImage());
|
||||
setGraphicTextGap(8.0d);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
@@ -1412,6 +1436,12 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
}
|
||||
feeRange.updateTrackHighlight();
|
||||
|
||||
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())) {
|
||||
setDefaultFeeRate();
|
||||
@@ -1434,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
|
||||
@@ -1600,6 +1630,11 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void feeRateSourceChanged(FeeRatesSourceChangedEvent event) {
|
||||
recentBlocksView.updateFeeRatesSource(event.getFeeRateSource());
|
||||
}
|
||||
|
||||
private class PrivacyAnalysisTooltip extends VBox {
|
||||
private final List<Label> analysisLabels = new ArrayList<>();
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="14px" height="14px" viewBox="0 0 14 14" version="1.1">
|
||||
<g id="surface1">
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(54.11765%,54.11765%,54.11765%);fill-opacity:1;" d="M 9.695312 9.898438 C 9.050781 10.582031 8.230469 10.972656 7.386719 11.074219 L 6.589844 12.925781 C 8.144531 13.027344 9.738281 12.453125 10.929688 11.199219 C 13.007812 9.007812 13.121094 5.527344 11.269531 3.195312 L 10.027344 4.5 C 11.203125 6.109375 11.09375 8.429688 9.695312 9.902344 Z M 9.695312 9.898438 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(68.235295%,68.235295%,68.235295%);fill-opacity:1;" d="M 3.148438 6.117188 C 3.304688 5.375 3.652344 4.675781 4.195312 4.101562 C 5.605469 2.609375 7.84375 2.507812 9.367188 3.792969 L 10.605469 2.488281 C 8.394531 0.488281 5.058594 0.59375 2.964844 2.800781 C 1.902344 3.921875 1.355469 5.375 1.320312 6.839844 Z M 3.148438 6.117188 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 1.691406 7.960938 L 4.695312 9 L 5.511719 12.414062 L 8.371094 5.546875 Z M 1.691406 7.960938 "/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="14px" height="14px" viewBox="0 0 14 14" version="1.1">
|
||||
<defs>
|
||||
<linearGradient id="linear0" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="1" y2="0" gradientTransform="matrix(0,10.538675,-10.538675,0,6.999808,4.639967)">
|
||||
<stop offset="0" style="stop-color:rgb(42.941176%,42.941176%,42.941176%);stop-opacity:0.8;"/>
|
||||
<stop offset="0.998812" style="stop-color:rgb(96.078431%,96.078431%,96.078431%);stop-opacity:0.8;"/>
|
||||
</linearGradient>
|
||||
<filter id="alpha" filterUnits="objectBoundingBox" x="0%" y="0%" width="100%" height="100%">
|
||||
<feColorMatrix type="matrix" in="SourceGraphic" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
|
||||
</filter>
|
||||
<mask id="mask0">
|
||||
<g filter="url(#alpha)">
|
||||
<rect x="0" y="0" width="14" height="14" style="fill:rgb(0%,0%,0%);fill-opacity:0.301961;stroke:none;"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="clip1">
|
||||
<rect x="0" y="0" width="15" height="15"/>
|
||||
</clipPath>
|
||||
<g id="surface5" clip-path="url(#clip1)">
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:0.6;" d="M 11.160156 11.003906 C 11.160156 11.171875 11.046875 11.304688 10.90625 11.304688 L 9.492188 11.304688 C 9.351562 11.304688 9.238281 11.171875 9.238281 11.003906 L 9.238281 2.996094 C 9.238281 2.828125 9.351562 2.695312 9.492188 2.695312 L 10.90625 2.695312 C 11.046875 2.695312 11.160156 2.828125 11.160156 2.996094 Z M 11.160156 11.003906 "/>
|
||||
</g>
|
||||
</defs>
|
||||
<g id="surface1">
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(20.392157%,20.392157%,20.392157%);fill-opacity:0.9;" d="M 12.453125 11.101562 C 12.453125 11.855469 11.847656 12.460938 11.09375 12.460938 L 2.898438 12.460938 C 2.148438 12.460938 1.542969 11.855469 1.542969 11.101562 L 1.542969 2.898438 C 1.542969 2.144531 2.152344 1.539062 2.898438 1.539062 L 11.09375 1.539062 C 11.847656 1.539062 12.453125 2.144531 12.453125 2.898438 Z M 12.453125 11.101562 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear0);" d="M 1.542969 7.011719 L 1.542969 11.101562 C 1.542969 11.855469 2.152344 12.460938 2.90625 12.460938 L 11.09375 12.460938 C 11.847656 12.460938 12.457031 11.855469 12.457031 11.101562 L 12.457031 7.011719 Z M 1.542969 7.011719 "/>
|
||||
<use xlink:href="#surface5" mask="url(#mask0)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="14px" height="14px" viewBox="0 0 14 14" version="1.1">
|
||||
<g id="surface1">
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:#7a7a7a;fill-opacity:1;" d="M 6.996094 1.132812 C 10.234375 1.132812 12.859375 3.757812 12.859375 6.996094 C 12.859375 10.234375 10.234375 12.859375 6.996094 12.859375 C 3.757812 12.859375 1.132812 10.234375 1.132812 6.996094 C 1.132812 3.757812 3.757812 1.132812 6.996094 1.132812 Z M 6.996094 1.132812 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 6.996094 9.054688 C 5.859375 9.054688 4.9375 8.132812 4.9375 6.996094 C 4.9375 5.859375 5.859375 4.933594 6.996094 4.933594 C 8.015625 4.933594 8.863281 5.675781 9.027344 6.652344 L 11.101562 6.652344 C 10.925781 4.535156 9.15625 2.871094 6.996094 2.871094 C 4.71875 2.871094 2.871094 4.71875 2.871094 6.996094 C 2.871094 9.269531 4.71875 11.117188 6.996094 11.117188 C 9.15625 11.117188 10.925781 9.453125 11.101562 7.339844 L 9.023438 7.339844 C 8.863281 8.3125 8.015625 9.054688 6.996094 9.054688 Z M 6.996094 9.054688 "/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="14px" height="14px" viewBox="0 0 14 14" version="1.1">
|
||||
<defs>
|
||||
<clipPath id="clip1">
|
||||
<path d="M 1.398438 1.339844 L 12.601562 1.339844 L 12.601562 12.660156 L 1.398438 12.660156 Z M 1.398438 1.339844 "/>
|
||||
</clipPath>
|
||||
<clipPath id="clip2">
|
||||
<path d="M 2 3 L 12 3 L 12 12.660156 L 2 12.660156 Z M 2 3 "/>
|
||||
</clipPath>
|
||||
<clipPath id="clip3">
|
||||
<path d="M 2 6 L 12 6 L 12 12.660156 L 2 12.660156 Z M 2 6 "/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g id="surface1">
|
||||
<g clip-path="url(#clip1)" clip-rule="nonzero">
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(77.254903%,77.254903%,77.254903%);fill-opacity:1;" d="M 12.601562 6.957031 C 12.613281 10.070312 10.117188 12.605469 7.027344 12.621094 C 3.933594 12.636719 1.414062 10.121094 1.398438 7.007812 C 1.386719 3.890625 3.882812 1.355469 6.976562 1.339844 C 10.066406 1.328125 12.585938 3.839844 12.601562 6.957031 Z M 12.601562 6.957031 "/>
|
||||
</g>
|
||||
<g clip-path="url(#clip2)" clip-rule="nonzero">
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(33.725491%,33.725491%,33.725491%);fill-opacity:1;" d="M 9.839844 4.960938 C 9.433594 4.84375 9.011719 4.675781 8.585938 4.507812 C 8.5625 4.398438 8.464844 4.265625 8.273438 4.101562 C 7.996094 3.855469 7.472656 3.863281 7.019531 3.96875 C 6.519531 3.851562 6.027344 3.808594 5.554688 3.925781 C 1.683594 5 3.878906 7.621094 2.457031 10.253906 C 2.65625 10.6875 4.890625 12.761719 7.992188 12.53125 C 7.992188 12.53125 6.914062 9.921875 9.347656 8.667969 C 11.324219 7.648438 12.75 5.761719 9.839844 4.960938 Z M 9.839844 4.960938 "/>
|
||||
</g>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 7.289062 5.589844 C 7.289062 6.195312 6.804688 6.679688 6.207031 6.679688 C 5.609375 6.679688 5.125 6.195312 5.125 5.589844 C 5.125 4.988281 5.609375 4.5 6.207031 4.5 C 6.804688 4.5 7.289062 4.988281 7.289062 5.589844 Z M 7.289062 5.589844 "/>
|
||||
<g clip-path="url(#clip3)" clip-rule="nonzero">
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(33.725491%,33.725491%,33.725491%);fill-opacity:1;" d="M 11.195312 7.160156 C 10.316406 7.78125 9.320312 8.253906 7.90625 8.253906 C 7.242188 8.253906 7.109375 7.546875 6.671875 7.894531 C 6.445312 8.074219 5.648438 8.472656 5.015625 8.445312 C 4.375 8.414062 3.359375 8.039062 3.070312 6.679688 C 2.957031 8.039062 2.902344 9.042969 2.394531 10.1875 C 3.636719 12.019531 5.808594 12.929688 7.992188 12.53125 C 7.757812 10.882812 9.191406 9.265625 9.996094 8.4375 C 10.304688 8.125 10.890625 7.613281 11.195312 7.160156 Z M 11.195312 7.160156 "/>
|
||||
</g>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(5.098039%,7.058824%,9.019608%);fill-opacity:1;" d="M 6.027344 5.582031 C 6.027344 5.09375 6.3125 4.695312 6.664062 4.695312 C 7.011719 4.695312 7.296875 5.09375 7.296875 5.582031 C 7.296875 6.074219 7.011719 6.472656 6.664062 6.472656 C 6.3125 6.472656 6.027344 6.074219 6.027344 5.582031 Z M 6.027344 5.582031 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(27.450982%,27.450982%,27.450982%);fill-opacity:1;" d="M 6.917969 3.96875 C 7.261719 4.105469 8.527344 4.527344 9.078125 4.695312 C 8.515625 3.765625 7.667969 3.820312 6.917969 3.96875 Z M 6.917969 3.96875 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 6.664062 5.582031 L 5.898438 5.074219 L 5.898438 6.089844 Z M 6.664062 5.582031 "/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="14px" height="14px" viewBox="0 0 14 14" version="1.1">
|
||||
<defs>
|
||||
<linearGradient id="linear0" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="1" y2="0" gradientTransform="matrix(0,10.538675,-10.538675,0,6.999808,4.639967)">
|
||||
<stop offset="0" style="stop-color:rgb(42.941176%,42.941176%,42.941176%);stop-opacity:0.8;"/>
|
||||
<stop offset="0.998812" style="stop-color:rgb(96.078431%,96.078431%,96.078431%);stop-opacity:0.8;"/>
|
||||
</linearGradient>
|
||||
<filter id="alpha" filterUnits="objectBoundingBox" x="0%" y="0%" width="100%" height="100%">
|
||||
<feColorMatrix type="matrix" in="SourceGraphic" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
|
||||
</filter>
|
||||
<mask id="mask0">
|
||||
<g filter="url(#alpha)">
|
||||
<rect x="0" y="0" width="14" height="14" style="fill:rgb(0%,0%,0%);fill-opacity:0.301961;stroke:none;"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="clip1">
|
||||
<rect x="0" y="0" width="15" height="15"/>
|
||||
</clipPath>
|
||||
<g id="surface5" clip-path="url(#clip1)">
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:0.6;" d="M 11.160156 11.003906 C 11.160156 11.171875 11.046875 11.304688 10.90625 11.304688 L 9.492188 11.304688 C 9.351562 11.304688 9.238281 11.171875 9.238281 11.003906 L 9.238281 2.996094 C 9.238281 2.828125 9.351562 2.695312 9.492188 2.695312 L 10.90625 2.695312 C 11.046875 2.695312 11.160156 2.828125 11.160156 2.996094 Z M 11.160156 11.003906 "/>
|
||||
</g>
|
||||
</defs>
|
||||
<g id="surface1">
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(20.392157%,20.392157%,20.392157%);fill-opacity:0.9;" d="M 12.453125 11.101562 C 12.453125 11.855469 11.847656 12.460938 11.09375 12.460938 L 2.898438 12.460938 C 2.148438 12.460938 1.542969 11.855469 1.542969 11.101562 L 1.542969 2.898438 C 1.542969 2.144531 2.152344 1.539062 2.898438 1.539062 L 11.09375 1.539062 C 11.847656 1.539062 12.453125 2.144531 12.453125 2.898438 Z M 12.453125 11.101562 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear0);" d="M 1.542969 7.011719 L 1.542969 11.101562 C 1.542969 11.855469 2.152344 12.460938 2.90625 12.460938 L 11.09375 12.460938 C 11.847656 12.460938 12.457031 11.855469 12.457031 11.101562 L 12.457031 7.011719 Z M 1.542969 7.011719 "/>
|
||||
<use xlink:href="#surface5" mask="url(#mask0)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="14px" height="14px" viewBox="0 0 14 14" version="1.1">
|
||||
<defs>
|
||||
<linearGradient id="linear0" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="1" y2="0" gradientTransform="matrix(0,10.538675,-10.538675,0,6.999808,4.639967)">
|
||||
<stop offset="0" style="stop-color:rgb(42.941176%,42.941176%,42.941176%);stop-opacity:0.8;"/>
|
||||
<stop offset="0.998812" style="stop-color:rgb(96.078431%,96.078431%,96.078431%);stop-opacity:0.8;"/>
|
||||
</linearGradient>
|
||||
<filter id="alpha" filterUnits="objectBoundingBox" x="0%" y="0%" width="100%" height="100%">
|
||||
<feColorMatrix type="matrix" in="SourceGraphic" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
|
||||
</filter>
|
||||
<mask id="mask0">
|
||||
<g filter="url(#alpha)">
|
||||
<rect x="0" y="0" width="14" height="14" style="fill:rgb(0%,0%,0%);fill-opacity:0.301961;stroke:none;"/>
|
||||
</g>
|
||||
</mask>
|
||||
<clipPath id="clip1">
|
||||
<rect x="0" y="0" width="15" height="15"/>
|
||||
</clipPath>
|
||||
<g id="surface5" clip-path="url(#clip1)">
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:0.6;" d="M 11.160156 11.003906 C 11.160156 11.171875 11.046875 11.304688 10.90625 11.304688 L 9.492188 11.304688 C 9.351562 11.304688 9.238281 11.171875 9.238281 11.003906 L 9.238281 2.996094 C 9.238281 2.828125 9.351562 2.695312 9.492188 2.695312 L 10.90625 2.695312 C 11.046875 2.695312 11.160156 2.828125 11.160156 2.996094 Z M 11.160156 11.003906 "/>
|
||||
</g>
|
||||
</defs>
|
||||
<g id="surface1">
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(20.392157%,20.392157%,20.392157%);fill-opacity:0.9;" d="M 12.453125 11.101562 C 12.453125 11.855469 11.847656 12.460938 11.09375 12.460938 L 2.898438 12.460938 C 2.148438 12.460938 1.542969 11.855469 1.542969 11.101562 L 1.542969 2.898438 C 1.542969 2.144531 2.152344 1.539062 2.898438 1.539062 L 11.09375 1.539062 C 11.847656 1.539062 12.453125 2.144531 12.453125 2.898438 Z M 12.453125 11.101562 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear0);" d="M 1.542969 7.011719 L 1.542969 11.101562 C 1.542969 11.855469 2.152344 12.460938 2.90625 12.460938 L 11.09375 12.460938 C 11.847656 12.460938 12.457031 11.855469 12.457031 11.101562 L 12.457031 7.011719 Z M 1.542969 7.011719 "/>
|
||||
<use xlink:href="#surface5" mask="url(#mask0)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="14px" height="14px" viewBox="0 0 14 14" version="1.1">
|
||||
<g id="surface1">
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:#a8a8a8;fill-opacity:1;" d="M 11.898438 4.898438 L 2.101562 4.898438 C 1.710938 4.898438 1.398438 4.585938 1.398438 4.199219 L 1.398438 2.800781 C 1.398438 2.414062 1.710938 2.101562 2.101562 2.101562 L 11.898438 2.101562 C 12.289062 2.101562 12.601562 2.414062 12.601562 2.800781 L 12.601562 4.199219 C 12.601562 4.585938 12.289062 4.898438 11.898438 4.898438 Z M 10.851562 2.976562 C 10.558594 2.976562 10.324219 3.210938 10.324219 3.5 C 10.324219 3.789062 10.558594 4.023438 10.851562 4.023438 C 11.140625 4.023438 11.375 3.789062 11.375 3.5 C 11.375 3.210938 11.140625 2.976562 10.851562 2.976562 Z M 9.449219 2.976562 C 9.160156 2.976562 8.925781 3.210938 8.925781 3.5 C 8.925781 3.789062 9.160156 4.023438 9.449219 4.023438 C 9.742188 4.023438 9.976562 3.789062 9.976562 3.5 C 9.976562 3.210938 9.742188 2.976562 9.449219 2.976562 Z M 11.898438 8.398438 L 2.101562 8.398438 C 1.710938 8.398438 1.398438 8.085938 1.398438 7.699219 L 1.398438 6.300781 C 1.398438 5.914062 1.710938 5.601562 2.101562 5.601562 L 11.898438 5.601562 C 12.289062 5.601562 12.601562 5.914062 12.601562 6.300781 L 12.601562 7.699219 C 12.601562 8.085938 12.289062 8.398438 11.898438 8.398438 Z M 10.851562 6.476562 C 10.558594 6.476562 10.324219 6.710938 10.324219 7 C 10.324219 7.289062 10.558594 7.523438 10.851562 7.523438 C 11.140625 7.523438 11.375 7.289062 11.375 7 C 11.375 6.710938 11.140625 6.476562 10.851562 6.476562 Z M 9.449219 6.476562 C 9.160156 6.476562 8.925781 6.710938 8.925781 7 C 8.925781 7.289062 9.160156 7.523438 9.449219 7.523438 C 9.742188 7.523438 9.976562 7.289062 9.976562 7 C 9.976562 6.710938 9.742188 6.476562 9.449219 6.476562 Z M 11.898438 11.898438 L 2.101562 11.898438 C 1.710938 11.898438 1.398438 11.585938 1.398438 11.199219 L 1.398438 9.800781 C 1.398438 9.414062 1.710938 9.101562 2.101562 9.101562 L 11.898438 9.101562 C 12.289062 9.101562 12.601562 9.414062 12.601562 9.800781 L 12.601562 11.199219 C 12.601562 11.585938 12.289062 11.898438 11.898438 11.898438 Z M 10.851562 9.976562 C 10.558594 9.976562 10.324219 10.210938 10.324219 10.5 C 10.324219 10.789062 10.558594 11.023438 10.851562 11.023438 C 11.140625 11.023438 11.375 10.789062 11.375 10.5 C 11.375 10.210938 11.140625 9.976562 10.851562 9.976562 Z M 9.449219 9.976562 C 9.160156 9.976562 8.925781 10.210938 8.925781 10.5 C 8.925781 10.789062 9.160156 11.023438 9.449219 11.023438 C 9.742188 11.023438 9.976562 10.789062 9.976562 10.5 C 9.976562 10.210938 9.742188 9.976562 9.449219 9.976562 Z M 9.449219 9.976562 "/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="14px" height="14px" viewBox="0 0 14 14" version="1.1">
|
||||
<g id="surface1">
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:#a8a8a8;fill-opacity:1;" d="M 12.363281 10.054688 L 9.800781 7.492188 C 9.296875 6.988281 8.539062 6.890625 7.933594 7.191406 L 5.601562 4.859375 L 5.601562 3.5 L 2.800781 1.398438 L 1.402344 2.800781 L 3.5 5.597656 L 4.859375 5.597656 L 7.191406 7.929688 C 6.894531 8.539062 6.988281 9.292969 7.496094 9.800781 L 10.058594 12.359375 C 10.375 12.679688 10.890625 12.679688 11.210938 12.359375 L 12.363281 11.207031 C 12.679688 10.890625 12.679688 10.371094 12.363281 10.054688 Z M 8.65625 6.320312 C 9.277344 6.320312 9.859375 6.5625 10.296875 7 L 10.71875 7.425781 C 11.066406 7.273438 11.394531 7.0625 11.675781 6.777344 C 12.488281 5.96875 12.765625 4.824219 12.507812 3.789062 C 12.457031 3.589844 12.210938 3.523438 12.066406 3.667969 L 10.441406 5.296875 L 8.953125 5.046875 L 8.707031 3.5625 L 10.335938 1.933594 C 10.480469 1.789062 10.410156 1.542969 10.210938 1.492188 C 9.171875 1.238281 8.03125 1.511719 7.222656 2.324219 C 6.597656 2.945312 6.304688 3.769531 6.320312 4.589844 L 8.117188 6.386719 C 8.292969 6.34375 8.476562 6.320312 8.65625 6.320312 Z M 6.382812 8.113281 L 5.144531 6.875 L 1.808594 10.210938 C 1.261719 10.757812 1.261719 11.644531 1.808594 12.191406 C 2.355469 12.738281 3.242188 12.738281 3.789062 12.191406 L 6.492188 9.488281 C 6.328125 9.050781 6.277344 8.578125 6.382812 8.113281 Z M 2.800781 11.722656 C 2.511719 11.722656 2.277344 11.488281 2.277344 11.199219 C 2.277344 10.910156 2.511719 10.675781 2.800781 10.675781 C 3.089844 10.675781 3.324219 10.910156 3.324219 11.199219 C 3.324219 11.488281 3.089844 11.722656 2.800781 11.722656 Z M 2.800781 11.722656 "/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -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 |