mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2025-11-05 11:56:37 +00:00
Merge branch 'sparrowwallet:master' into master
This commit is contained in:
commit
891a4c3ff4
41 changed files with 1053 additions and 395 deletions
5
.github/workflows/package.yaml
vendored
5
.github/workflows/package.yaml
vendored
|
|
@ -30,7 +30,7 @@ jobs:
|
|||
- name: Package tar distribution
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: ./gradlew packageTarDistribution
|
||||
- name: Upload Artifacts
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Sparrow Build - ${{ runner.os }} ${{ runner.arch }}
|
||||
|
|
@ -43,9 +43,6 @@ jobs:
|
|||
- name: Package headless tar distribution
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: ./gradlew -Djava.awt.headless=true packageTarDistribution
|
||||
- name: Rename Headless Artifacts
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: for f in build/jpackage/sparrow*; do mv -v "$f" "${f/sparrow/sparrow-server}"; done;
|
||||
- name: Upload Headless Artifact
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
|
|
|
|||
62
build.gradle
62
build.gradle
|
|
@ -3,10 +3,9 @@ plugins {
|
|||
id 'org-openjfx-javafxplugin'
|
||||
id 'org.beryx.jlink' version '3.1.1'
|
||||
id 'org.gradlex.extra-java-module-info' version '1.9'
|
||||
id 'com.sparrowwallet.filterjar'
|
||||
id 'io.matthewnelson.kmp.tor.resource-filterjar' version '408.16.2'
|
||||
}
|
||||
|
||||
def sparrowVersion = '2.1.4'
|
||||
def os = org.gradle.internal.os.OperatingSystem.current()
|
||||
def osName = os.getFamilyName()
|
||||
if(os.macOsX) {
|
||||
|
|
@ -20,8 +19,8 @@ if(System.getProperty("os.arch") == "aarch64") {
|
|||
}
|
||||
def headless = "true".equals(System.getProperty("java.awt.headless"))
|
||||
|
||||
group "com.sparrowwallet"
|
||||
version "${sparrowVersion}"
|
||||
group 'com.sparrowwallet'
|
||||
version '2.1.4'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
|
|
@ -77,7 +76,7 @@ dependencies {
|
|||
implementation('co.nstant.in:cbor:0.9')
|
||||
implementation('org.openpnp:openpnp-capture-java:0.0.28-5')
|
||||
implementation("io.matthewnelson.kmp-tor:runtime:2.2.1")
|
||||
implementation("io.matthewnelson.kmp-tor:resource-exec-tor-gpl:408.16.0")
|
||||
implementation("io.matthewnelson.kmp-tor:resource-exec-tor-gpl:408.16.2")
|
||||
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.10.1') {
|
||||
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
|
||||
}
|
||||
|
|
@ -239,7 +238,7 @@ jlink {
|
|||
jpackage {
|
||||
imageName = "Sparrow"
|
||||
installerName = "Sparrow"
|
||||
appVersion = "${sparrowVersion}"
|
||||
appVersion = "${version}"
|
||||
skipInstaller = os.macOsX || properties.skipInstallers
|
||||
imageOptions = []
|
||||
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/asc.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--file-associations', 'src/main/deploy/auth47.properties', '--file-associations', 'src/main/deploy/lightning.properties', '--license-file', 'LICENSE']
|
||||
|
|
@ -250,11 +249,13 @@ jlink {
|
|||
}
|
||||
if(os.linux) {
|
||||
if(headless) {
|
||||
installerOptions = ['--license-file', 'LICENSE', '--resource-dir', "src/main/deploy/package/linux-headless/${osArch}"]
|
||||
installerName = "sparrowserver"
|
||||
installerOptions = ['--license-file', 'LICENSE']
|
||||
} else {
|
||||
installerOptions += ['--resource-dir', 'src/main/deploy/package/linux/', '--linux-shortcut', '--linux-menu-group', 'Sparrow']
|
||||
installerName = "sparrowwallet"
|
||||
installerOptions += ['--linux-shortcut', '--linux-menu-group', 'Sparrow']
|
||||
}
|
||||
installerOptions += ['--linux-app-category', 'utils', '--linux-app-release', '1', '--linux-rpm-license-type', 'ASL 2.0', '--linux-deb-maintainer', 'mail@sparrowwallet.com']
|
||||
installerOptions += ['--resource-dir', layout.buildDirectory.dir('deploy/package').get().asFile.toString(), '--linux-app-category', 'utils', '--linux-app-release', '1', '--linux-rpm-license-type', 'ASL 2.0', '--linux-deb-maintainer', 'mail@sparrowwallet.com']
|
||||
imageOptions += ['--icon', 'src/main/deploy/package/linux/Sparrow.png', '--resource-dir', 'src/main/deploy/package/linux/']
|
||||
}
|
||||
if(os.macOsX) {
|
||||
|
|
@ -272,6 +273,7 @@ jlink {
|
|||
|
||||
if(os.linux) {
|
||||
tasks.jlink.finalizedBy('addUserWritePermission', 'copyUdevRules')
|
||||
tasks.jpackageImage.finalizedBy('prepareResourceDir')
|
||||
} else {
|
||||
tasks.jlink.finalizedBy('addUserWritePermission')
|
||||
}
|
||||
|
|
@ -290,12 +292,42 @@ tasks.register('copyUdevRules', Copy) {
|
|||
include('*')
|
||||
}
|
||||
|
||||
tasks.register('prepareResourceDir', Copy) {
|
||||
from("src/main/deploy/package/linux${headless ? '-headless' : ''}")
|
||||
into(layout.buildDirectory.dir('deploy/package'))
|
||||
include('*')
|
||||
eachFile { file ->
|
||||
if(file.name.equals('control') || file.name.endsWith('.spec')) {
|
||||
filter { line ->
|
||||
if(line.contains('${size}')) {
|
||||
line = line.replace('${size}', getDirectorySize(layout.buildDirectory.dir('jpackage/Sparrow').get().asFile))
|
||||
}
|
||||
return line.replace('${version}', "${version}").replace('${arch}', osArch == 'aarch64' ? 'arm64' : 'amd64')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static def getDirectorySize(File directory) {
|
||||
long size = 0
|
||||
if(directory.isFile()) {
|
||||
size = directory.length()
|
||||
} else if(directory.isDirectory()) {
|
||||
directory.eachFileRecurse { file ->
|
||||
if(file.isFile()) {
|
||||
size += file.length()
|
||||
}
|
||||
}
|
||||
}
|
||||
return Long.toString(size/1024 as long)
|
||||
}
|
||||
|
||||
tasks.register('removeGroupWritePermission', Exec) {
|
||||
commandLine 'chmod', '-R', 'g-w', "$buildDir/jpackage/Sparrow"
|
||||
}
|
||||
|
||||
tasks.register('packageZipDistribution', Zip) {
|
||||
archiveFileName = "Sparrow-${sparrowVersion}.zip"
|
||||
archiveFileName = "Sparrow-${version}.zip"
|
||||
destinationDirectory = file("$buildDir/jpackage")
|
||||
preserveFileTimestamps = os.macOsX
|
||||
from("$buildDir/jpackage/") {
|
||||
|
|
@ -306,7 +338,7 @@ tasks.register('packageZipDistribution', Zip) {
|
|||
|
||||
tasks.register('packageTarDistribution', Tar) {
|
||||
dependsOn removeGroupWritePermission
|
||||
archiveFileName = "sparrow-${sparrowVersion}-${releaseArch}.tar.gz"
|
||||
archiveFileName = "sparrow${headless ? 'server': 'wallet'}-${version}-${releaseArch}.tar.gz"
|
||||
destinationDirectory = file("$buildDir/jpackage")
|
||||
compression = Compression.GZIP
|
||||
from("$buildDir/jpackage/") {
|
||||
|
|
@ -460,10 +492,6 @@ extraJavaModuleInfo {
|
|||
}
|
||||
}
|
||||
|
||||
String torOs = os.macOsX ? "macos" : (os.windows ? "mingw" : "linux-libc")
|
||||
filterInfo {
|
||||
filter('io.matthewnelson.kmp-tor', 'resource-lib-tor-gpl-jvm') {
|
||||
include("io/matthewnelson/kmp/tor/resource/lib/tor/native/${torOs}/${releaseArch}")
|
||||
exclude('io/matthewnelson/kmp/tor/resource/lib/tor/native/')
|
||||
}
|
||||
kmpTorResourceFilterJar {
|
||||
keepTorCompilation("current","current")
|
||||
}
|
||||
|
|
@ -20,9 +20,5 @@ gradlePlugin {
|
|||
id = "org-openjfx-javafxplugin"
|
||||
implementationClass = "org.openjfx.gradle.JavaFXPlugin"
|
||||
}
|
||||
register("com.sparrowwallet.filterjar") {
|
||||
id = "com.sparrowwallet.filterjar"
|
||||
implementationClass = "com.sparrowwallet.filterjar.FilterJarPlugin"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,69 +0,0 @@
|
|||
package com.sparrowwallet.filterjar;
|
||||
|
||||
import org.gradle.api.Action;
|
||||
import org.gradle.api.artifacts.Configuration;
|
||||
import org.gradle.api.artifacts.ConfigurationContainer;
|
||||
import org.gradle.api.attributes.Attribute;
|
||||
import org.gradle.api.model.ObjectFactory;
|
||||
import org.gradle.api.provider.MapProperty;
|
||||
import org.gradle.api.tasks.SourceSet;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public abstract class FilterJarExtension {
|
||||
static Attribute<Boolean> FILTERED_ATTRIBUTE = Attribute.of("filtered", Boolean.class);
|
||||
|
||||
public abstract MapProperty<String, JarFilterConfigImpl> getFilterConfigs();
|
||||
|
||||
@Inject
|
||||
protected abstract ObjectFactory getObjects();
|
||||
|
||||
@Inject
|
||||
protected abstract ConfigurationContainer getConfigurations();
|
||||
|
||||
public void filter(String group, String artifact, Action<? super JarFilterConfigImpl> action) {
|
||||
String name = group + ":" + artifact;
|
||||
JarFilterConfigImpl config = new JarFilterConfigImpl(name, getObjects());
|
||||
config.setGroup(group);
|
||||
config.setArtifact(artifact);
|
||||
action.execute(config);
|
||||
getFilterConfigs().put(name, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate the plugin's functionality for dependencies of all scopes of the given source set
|
||||
* (runtimeClasspath, compileClasspath, annotationProcessor).
|
||||
* Note that the plugin activates the functionality for all source sets by default.
|
||||
* Therefore, this method only has an effect for source sets for which a {@link #deactivate(Configuration)}
|
||||
* has been performed.
|
||||
*
|
||||
* @param sourceSet the Source Set to activate (e.g. sourceSets.test)
|
||||
*/
|
||||
public void activate(SourceSet sourceSet) {
|
||||
Configuration runtimeClasspath = getConfigurations().getByName(sourceSet.getRuntimeClasspathConfigurationName());
|
||||
Configuration compileClasspath = getConfigurations().getByName(sourceSet.getCompileClasspathConfigurationName());
|
||||
Configuration annotationProcessor = getConfigurations().getByName(sourceSet.getAnnotationProcessorConfigurationName());
|
||||
|
||||
activate(runtimeClasspath);
|
||||
activate(compileClasspath);
|
||||
activate(annotationProcessor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate the plugin's functionality for a single resolvable Configuration.
|
||||
*
|
||||
* @param resolvable a resolvable Configuration (e.g. configurations["customClasspath"])
|
||||
*/
|
||||
public void activate(Configuration resolvable) {
|
||||
resolvable.getAttributes().attribute(FILTERED_ATTRIBUTE, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate the plugin's functionality for a single resolvable Configuration.
|
||||
*
|
||||
* @param resolvable a resolvable Configuration (e.g. configurations.annotationProcessor)
|
||||
*/
|
||||
public void deactivate(Configuration resolvable) {
|
||||
resolvable.getAttributes().attribute(FILTERED_ATTRIBUTE, false);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
package com.sparrowwallet.filterjar;
|
||||
|
||||
import org.gradle.api.artifacts.transform.TransformParameters;
|
||||
import org.gradle.api.provider.MapProperty;
|
||||
import org.gradle.api.tasks.Input;
|
||||
|
||||
public interface FilterJarParameters extends TransformParameters {
|
||||
@Input
|
||||
MapProperty<String, JarFilterConfig> getFilterConfigs();
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
package com.sparrowwallet.filterjar;
|
||||
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.plugins.JavaPlugin;
|
||||
import org.gradle.api.tasks.SourceSetContainer;
|
||||
|
||||
import static com.sparrowwallet.filterjar.FilterJarExtension.FILTERED_ATTRIBUTE;
|
||||
|
||||
public class FilterJarPlugin implements Plugin<Project> {
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
// Register the extension
|
||||
FilterJarExtension extension = project.getExtensions().create("filterInfo", FilterJarExtension.class);
|
||||
|
||||
project.getPlugins().withType(JavaPlugin.class).configureEach(_ -> {
|
||||
// By default, activate plugin for all source sets
|
||||
project.getExtensions().getByType(SourceSetContainer.class).all(extension::activate);
|
||||
|
||||
// All jars have a filtered=false attribute by default
|
||||
project.getDependencies().getArtifactTypes().maybeCreate("jar").getAttributes().attribute(FILTERED_ATTRIBUTE, false);
|
||||
|
||||
// Register the transform
|
||||
project.getDependencies().registerTransform(FilterJarTransform.class, transform -> {
|
||||
transform.getFrom().attribute(FILTERED_ATTRIBUTE, false);
|
||||
transform.getTo().attribute(FILTERED_ATTRIBUTE, true);
|
||||
transform.parameters(params -> {
|
||||
params.getFilterConfigs().putAll(extension.getFilterConfigs());
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
package com.sparrowwallet.filterjar;
|
||||
|
||||
|
||||
import org.gradle.api.artifacts.transform.InputArtifact;
|
||||
import org.gradle.api.artifacts.transform.TransformAction;
|
||||
import org.gradle.api.artifacts.transform.TransformOutputs;
|
||||
import org.gradle.api.file.FileSystemLocation;
|
||||
import org.gradle.api.provider.Provider;
|
||||
import org.gradle.api.tasks.PathSensitive;
|
||||
import org.gradle.api.tasks.PathSensitivity;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.HashSet;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.JarOutputStream;
|
||||
import java.nio.file.Files;
|
||||
|
||||
public abstract class FilterJarTransform implements TransformAction<FilterJarParameters> {
|
||||
@InputArtifact
|
||||
@PathSensitive(PathSensitivity.NAME_ONLY)
|
||||
public abstract Provider<FileSystemLocation> getInputArtifact();
|
||||
|
||||
@Override
|
||||
public void transform(TransformOutputs outputs) {
|
||||
File originalJar = getInputArtifact().get().getAsFile();
|
||||
String jarName = originalJar.getName();
|
||||
|
||||
// Get filter configurations from parameters
|
||||
Map<String, JarFilterConfig> filterConfigs = getParameters().getFilterConfigs().get();
|
||||
|
||||
//Inclusions are prioritised ahead of exclusions
|
||||
Set<String> inclusions = new HashSet<>();
|
||||
Set<String> exclusions = new HashSet<>();
|
||||
|
||||
// Check if this JAR matches any configured filters (simplified matching based on artifact name)
|
||||
filterConfigs.forEach((key, config) -> {
|
||||
if(jarName.contains(config.getArtifact())) {
|
||||
inclusions.addAll(config.getInclusions());
|
||||
exclusions.addAll(config.getExclusions());
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
if(!exclusions.isEmpty()) {
|
||||
filterJar(originalJar, getFilterJar(outputs, originalJar), inclusions, exclusions);
|
||||
} else {
|
||||
outputs.file(originalJar);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
throw new RuntimeException("Failed to transform jar: " + jarName, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void filterJar(File inputFile, File outputFile, Set<String> inclusions, Set<String> exclusions) throws Exception {
|
||||
try(JarFile jarFile = new JarFile(inputFile); JarOutputStream jarOut = new JarOutputStream(Files.newOutputStream(outputFile.toPath()))) {
|
||||
jarFile.entries().asIterator().forEachRemaining(entry -> {
|
||||
String entryName = entry.getName();
|
||||
boolean shouldInclude = inclusions.stream().anyMatch(entryName::startsWith);
|
||||
boolean shouldExclude = exclusions.stream().anyMatch(entryName::startsWith);
|
||||
if(shouldInclude || !shouldExclude) {
|
||||
try {
|
||||
jarOut.putNextEntry(new JarEntry(entryName));
|
||||
jarFile.getInputStream(entry).transferTo(jarOut);
|
||||
jarOut.closeEntry();
|
||||
} catch(Exception e) {
|
||||
throw new RuntimeException("Error processing entry: " + entryName, e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private File getFilterJar(TransformOutputs outputs, File originalJar) {
|
||||
return outputs.file(originalJar.getName().substring(0, originalJar.getName().lastIndexOf('.')) + "-filtered.jar");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
package com.sparrowwallet.filterjar;
|
||||
|
||||
import org.gradle.api.tasks.Input;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface JarFilterConfig {
|
||||
@Input
|
||||
String getGroup();
|
||||
|
||||
@Input
|
||||
String getArtifact();
|
||||
|
||||
@Input
|
||||
List<String> getInclusions();
|
||||
|
||||
@Input
|
||||
List<String> getExclusions();
|
||||
}
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
package com.sparrowwallet.filterjar;
|
||||
|
||||
import org.gradle.api.Named;
|
||||
import org.gradle.api.model.ObjectFactory;
|
||||
import javax.inject.Inject;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class JarFilterConfigImpl implements Named, JarFilterConfig, Serializable {
|
||||
private final String name;
|
||||
private String group;
|
||||
private String artifact;
|
||||
private final List<String> inclusions;
|
||||
private final List<String> exclusions;
|
||||
|
||||
@Inject
|
||||
public JarFilterConfigImpl(String name, ObjectFactory objectFactory) {
|
||||
this.name = name;
|
||||
this.inclusions = new ArrayList<>();
|
||||
this.exclusions = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroup() {
|
||||
return group;
|
||||
}
|
||||
|
||||
public void setGroup(String group) {
|
||||
this.group = group;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getArtifact() {
|
||||
return artifact;
|
||||
}
|
||||
|
||||
public void setArtifact(String artifact) {
|
||||
this.artifact = artifact;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getInclusions() {
|
||||
return inclusions;
|
||||
}
|
||||
|
||||
public void include(String path) {
|
||||
inclusions.add(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getExclusions() {
|
||||
return exclusions;
|
||||
}
|
||||
|
||||
public void exclude(String path) {
|
||||
exclusions.add(path);
|
||||
}
|
||||
}
|
||||
2
lark
2
lark
|
|
@ -1 +1 @@
|
|||
Subproject commit d3ed65b89e0b6273eac4e35b266986308a5e83a9
|
||||
Subproject commit 5facb25ede49c30650a8460dc04982650edb397f
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
Package: sparrow
|
||||
Version: 2.1.4-1
|
||||
Section: utils
|
||||
Maintainer: Craig Raw <mail@sparrowwallet.com>
|
||||
Priority: optional
|
||||
Architecture: arm64
|
||||
Provides: sparrow
|
||||
Description: Sparrow
|
||||
Depends: libc6, zlib1g
|
||||
12
src/main/deploy/package/linux-headless/control
Normal file
12
src/main/deploy/package/linux-headless/control
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
Package: sparrowserver
|
||||
Version: ${version}-1
|
||||
Section: utils
|
||||
Maintainer: Craig Raw <mail@sparrowwallet.com>
|
||||
Priority: optional
|
||||
Architecture: ${arch}
|
||||
Conflicts: sparrow (<= 2.1.4)
|
||||
Replaces: sparrow (<= 2.1.4)
|
||||
Provides: sparrowserver
|
||||
Description: Sparrow Server
|
||||
Depends: libc6, zlib1g
|
||||
Installed-Size: ${size}
|
||||
85
src/main/deploy/package/linux-headless/sparrowserver.spec
Executable file
85
src/main/deploy/package/linux-headless/sparrowserver.spec
Executable file
|
|
@ -0,0 +1,85 @@
|
|||
Summary: Sparrow Server
|
||||
Name: sparrowserver
|
||||
Version: ${version}
|
||||
Release: 1
|
||||
License: ASL 2.0
|
||||
Vendor: Unknown
|
||||
|
||||
%if "x" != "x"
|
||||
URL: https://sparrowwallet.com
|
||||
%endif
|
||||
|
||||
%if "x/opt" != "x"
|
||||
Prefix: /opt
|
||||
%endif
|
||||
|
||||
Provides: sparrowserver
|
||||
Obsoletes: sparrow <= 2.1.4
|
||||
|
||||
%if "xutils" != "x"
|
||||
Group: utils
|
||||
%endif
|
||||
|
||||
Autoprov: 0
|
||||
Autoreq: 0
|
||||
|
||||
#comment line below to enable effective jar compression
|
||||
#it could easily get your package size from 40 to 15Mb but
|
||||
#build time will substantially increase and it may require unpack200/system java to install
|
||||
%define __jar_repack %{nil}
|
||||
|
||||
# on RHEL we got unwanted improved debugging enhancements
|
||||
%define _build_id_links none
|
||||
|
||||
%define package_filelist %{_builddir}/%{name}.files
|
||||
%define app_filelist %{_builddir}/%{name}.app.files
|
||||
%define filesystem_filelist %{_builddir}/%{name}.filesystem.files
|
||||
|
||||
%define default_filesystem / /opt /usr /usr/bin /usr/lib /usr/local /usr/local/bin /usr/local/lib
|
||||
|
||||
%description
|
||||
Sparrow Server
|
||||
|
||||
%global __os_install_post %{nil}
|
||||
|
||||
%prep
|
||||
|
||||
%build
|
||||
|
||||
%install
|
||||
rm -rf %{buildroot}
|
||||
install -d -m 755 %{buildroot}/opt/sparrowserver
|
||||
cp -r %{_sourcedir}/opt/sparrowserver/* %{buildroot}/opt/sparrowserver
|
||||
if [ "$(echo %{_sourcedir}/lib/systemd/system/*.service)" != '%{_sourcedir}/lib/systemd/system/*.service' ]; then
|
||||
install -d -m 755 %{buildroot}/lib/systemd/system
|
||||
cp %{_sourcedir}/lib/systemd/system/*.service %{buildroot}/lib/systemd/system
|
||||
fi
|
||||
%if "x%{_rpmdir}/../../LICENSE" != "x"
|
||||
%define license_install_file %{_defaultlicensedir}/%{name}-%{version}/%{basename:%{_rpmdir}/../../LICENSE}
|
||||
install -d -m 755 "%{buildroot}%{dirname:%{license_install_file}}"
|
||||
install -m 644 "%{_rpmdir}/../../LICENSE" "%{buildroot}%{license_install_file}"
|
||||
%endif
|
||||
(cd %{buildroot} && find . -path ./lib/systemd -prune -o -type d -print) | sed -e 's/^\.//' -e '/^$/d' | sort > %{app_filelist}
|
||||
{ rpm -ql filesystem || echo %{default_filesystem}; } | sort > %{filesystem_filelist}
|
||||
comm -23 %{app_filelist} %{filesystem_filelist} > %{package_filelist}
|
||||
sed -i -e 's/.*/%dir "&"/' %{package_filelist}
|
||||
(cd %{buildroot} && find . -not -type d) | sed -e 's/^\.//' -e 's/.*/"&"/' >> %{package_filelist}
|
||||
%if "x%{_rpmdir}/../../LICENSE" != "x"
|
||||
sed -i -e 's|"%{license_install_file}"||' -e '/^$/d' %{package_filelist}
|
||||
%endif
|
||||
|
||||
%files -f %{package_filelist}
|
||||
%if "x%{_rpmdir}/../../LICENSE" != "x"
|
||||
%license "%{license_install_file}"
|
||||
%endif
|
||||
|
||||
%post
|
||||
package_type=rpm
|
||||
|
||||
%pre
|
||||
package_type=rpm
|
||||
|
||||
%preun
|
||||
package_type=rpm
|
||||
|
||||
%clean
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
Package: sparrow
|
||||
Version: 2.1.4-1
|
||||
Section: utils
|
||||
Maintainer: Craig Raw <mail@sparrowwallet.com>
|
||||
Priority: optional
|
||||
Architecture: amd64
|
||||
Provides: sparrow
|
||||
Description: Sparrow
|
||||
Depends: libc6, zlib1g
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
[Desktop Entry]
|
||||
Name=Sparrow
|
||||
Comment=Sparrow
|
||||
Exec=/opt/sparrow/bin/Sparrow %U
|
||||
Icon=/opt/sparrow/lib/Sparrow.png
|
||||
Exec=/opt/sparrowwallet/bin/Sparrow %U
|
||||
Icon=/opt/sparrowwallet/lib/Sparrow.png
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Finance;Network;
|
||||
|
|
|
|||
12
src/main/deploy/package/linux/control
Normal file
12
src/main/deploy/package/linux/control
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
Package: sparrowwallet
|
||||
Version: ${version}-1
|
||||
Section: utils
|
||||
Maintainer: Craig Raw <mail@sparrowwallet.com>
|
||||
Priority: optional
|
||||
Architecture: ${arch}
|
||||
Provides: sparrowwallet
|
||||
Conflicts: sparrow (<= 2.1.4)
|
||||
Replaces: sparrow (<= 2.1.4)
|
||||
Description: Sparrow Wallet
|
||||
Depends: libasound2, libbsd0, libc6, libmd0, libx11-6, libxau6, libxcb1, libxdmcp6, libxext6, libxi6, libxrender1, libxtst6, xdg-utils
|
||||
Installed-Size: ${size}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/sh
|
||||
# postinst script for sparrow
|
||||
# postinst script for sparrowwallet
|
||||
#
|
||||
# see: dh_installdeb(1)
|
||||
|
||||
|
|
@ -22,9 +22,9 @@ package_type=deb
|
|||
|
||||
case "$1" in
|
||||
configure)
|
||||
xdg-desktop-menu install /opt/sparrow/lib/sparrow-Sparrow.desktop
|
||||
xdg-mime install /opt/sparrow/lib/sparrow-Sparrow-MimeInfo.xml
|
||||
install -D -m 644 /opt/sparrow/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
|
||||
xdg-desktop-menu install /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop
|
||||
xdg-mime install /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml
|
||||
install -D -m 644 /opt/sparrowwallet/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
|
||||
if ! getent group plugdev > /dev/null; then
|
||||
groupadd plugdev
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
Summary: Sparrow
|
||||
Name: sparrow
|
||||
Version: 2.1.4
|
||||
Name: sparrowwallet
|
||||
Version: ${version}
|
||||
Release: 1
|
||||
License: ASL 2.0
|
||||
Vendor: Unknown
|
||||
|
||||
%if "x" != "x"
|
||||
URL:
|
||||
URL: https://sparrowwallet.com
|
||||
%endif
|
||||
|
||||
%if "x/opt" != "x"
|
||||
Prefix: /opt
|
||||
%endif
|
||||
|
||||
Provides: sparrow
|
||||
Provides: sparrowwallet
|
||||
Obsoletes: sparrow <= 2.1.4
|
||||
|
||||
%if "xutils" != "x"
|
||||
Group: utils
|
||||
|
|
@ -50,8 +51,8 @@ Sparrow Wallet
|
|||
|
||||
%install
|
||||
rm -rf %{buildroot}
|
||||
install -d -m 755 %{buildroot}/opt/sparrow
|
||||
cp -r %{_sourcedir}/opt/sparrow/* %{buildroot}/opt/sparrow
|
||||
install -d -m 755 %{buildroot}/opt/sparrowwallet
|
||||
cp -r %{_sourcedir}/opt/sparrowwallet/* %{buildroot}/opt/sparrowwallet
|
||||
if [ "$(echo %{_sourcedir}/lib/systemd/system/*.service)" != '%{_sourcedir}/lib/systemd/system/*.service' ]; then
|
||||
install -d -m 755 %{buildroot}/lib/systemd/system
|
||||
cp %{_sourcedir}/lib/systemd/system/*.service %{buildroot}/lib/systemd/system
|
||||
|
|
@ -77,9 +78,9 @@ sed -i -e 's/.*/%dir "&"/' %{package_filelist}
|
|||
|
||||
%post
|
||||
package_type=rpm
|
||||
xdg-desktop-menu install /opt/sparrow/lib/sparrow-Sparrow.desktop
|
||||
xdg-mime install /opt/sparrow/lib/sparrow-Sparrow-MimeInfo.xml
|
||||
install -D -m 644 /opt/sparrow/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
|
||||
xdg-desktop-menu install /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop
|
||||
xdg-mime install /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml
|
||||
install -D -m 644 /opt/sparrowwallet/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
|
||||
if ! getent group plugdev > /dev/null; then
|
||||
groupadd plugdev
|
||||
fi
|
||||
|
|
@ -251,9 +252,9 @@ desktop_trace ()
|
|||
echo "$@"
|
||||
}
|
||||
|
||||
do_if_file_belongs_to_single_package /opt/sparrow/lib/sparrow-Sparrow.desktop xdg-desktop-menu uninstall /opt/sparrow/lib/sparrow-Sparrow.desktop
|
||||
do_if_file_belongs_to_single_package /opt/sparrow/lib/sparrow-Sparrow-MimeInfo.xml xdg-mime uninstall /opt/sparrow/lib/sparrow-Sparrow-MimeInfo.xml
|
||||
do_if_file_belongs_to_single_package /opt/sparrow/lib/sparrow-Sparrow.desktop desktop_uninstall_default_mime_handler sparrow-Sparrow.desktop application/psbt application/bitcoin-transaction application/pgp-signature x-scheme-handler/bitcoin x-scheme-handler/auth47 x-scheme-handler/lightning
|
||||
do_if_file_belongs_to_single_package /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop xdg-desktop-menu uninstall /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop
|
||||
do_if_file_belongs_to_single_package /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml xdg-mime uninstall /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml
|
||||
do_if_file_belongs_to_single_package /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop desktop_uninstall_default_mime_handler sparrowwallet-Sparrow.desktop application/psbt application/bitcoin-transaction application/pgp-signature x-scheme-handler/bitcoin x-scheme-handler/auth47 x-scheme-handler/lightning
|
||||
|
||||
|
||||
%clean
|
||||
|
|
@ -612,16 +612,16 @@ public class AppController implements Initializable {
|
|||
|
||||
public void installUdevRules(ActionEvent event) {
|
||||
String commands = """
|
||||
sudo install -m 644 /opt/sparrow/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
|
||||
sudo install -m 644 /opt/sparrowwallet/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
|
||||
sudo udevadm control --reload
|
||||
sudo udevadm trigger
|
||||
sudo groupadd -f plugdev
|
||||
sudo usermod -aG plugdev `whoami`
|
||||
""";
|
||||
String home = System.getProperty(JPACKAGE_APP_PATH);
|
||||
if(home != null && !home.startsWith("/opt/sparrow") && home.endsWith("bin/Sparrow")) {
|
||||
if(home != null && !home.startsWith("/opt/sparrowwallet") && home.endsWith("bin/Sparrow")) {
|
||||
home = home.replace("bin/Sparrow", "");
|
||||
commands = commands.replace("/opt/sparrow/", home);
|
||||
commands = commands.replace("/opt/sparrowwallet/", home);
|
||||
}
|
||||
|
||||
TextAreaDialog dialog = new TextAreaDialog(commands, false);
|
||||
|
|
|
|||
|
|
@ -305,12 +305,6 @@ public class AppServices {
|
|||
if(event != null) {
|
||||
EventManager.get().post(event);
|
||||
}
|
||||
|
||||
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
|
||||
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
|
||||
if(event instanceof ConnectionEvent && feeRatesSource.supportsNetwork(Network.get()) && feeRatesSource.isExternal()) {
|
||||
EventManager.get().post(new FeeRatesSourceChangedEvent(feeRatesSource));
|
||||
}
|
||||
});
|
||||
connectionService.setOnFailed(failEvent -> {
|
||||
//Close connection here to create a new transport next time we try
|
||||
|
|
@ -494,6 +488,13 @@ public class AppServices {
|
|||
}
|
||||
}
|
||||
|
||||
private void fetchFeeRates() {
|
||||
if(feeRatesService != null && !feeRatesService.isRunning() && Config.get().getMode() != Mode.OFFLINE) {
|
||||
feeRatesService = createFeeRatesService();
|
||||
feeRatesService.start();
|
||||
}
|
||||
}
|
||||
|
||||
private void fetchBlockSummaries(List<NewBlockEvent> newBlockEvents) {
|
||||
if(isConnected()) {
|
||||
ElectrumServer.BlockSummaryService blockSummaryService = new ElectrumServer.BlockSummaryService(newBlockEvents);
|
||||
|
|
@ -1216,6 +1217,12 @@ public class AppServices {
|
|||
latestBlockHeader = event.getBlockHeader();
|
||||
Config.get().addRecentServer();
|
||||
|
||||
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
|
||||
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
|
||||
if(feeRatesSource.supportsNetwork(Network.get()) && feeRatesSource.isExternal()) {
|
||||
fetchFeeRates();
|
||||
}
|
||||
|
||||
if(!blockSummaries.containsKey(currentBlockHeight)) {
|
||||
fetchBlockSummaries(Collections.emptyList());
|
||||
}
|
||||
|
|
@ -1259,10 +1266,8 @@ public class AppServices {
|
|||
@Subscribe
|
||||
public void feeRateSourceChanged(FeeRatesSourceChangedEvent event) {
|
||||
//Perform once-off fee rates retrieval to immediately change displayed rates
|
||||
if(feeRatesService != null && !feeRatesService.isRunning() && Config.get().getMode() != Mode.OFFLINE) {
|
||||
feeRatesService = createFeeRatesService();
|
||||
feeRatesService.start();
|
||||
}
|
||||
fetchFeeRates();
|
||||
fetchBlockSummaries(Collections.emptyList());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
|
|
|
|||
|
|
@ -5,21 +5,23 @@ import java.time.temporal.ChronoUnit;
|
|||
import java.util.Date;
|
||||
import java.util.Optional;
|
||||
|
||||
public class BlockSummary {
|
||||
public class BlockSummary implements Comparable<BlockSummary> {
|
||||
private final Integer height;
|
||||
private final Date timestamp;
|
||||
private final Double medianFee;
|
||||
private final Integer transactionCount;
|
||||
private final Integer weight;
|
||||
|
||||
public BlockSummary(Integer height, Date timestamp) {
|
||||
this(height, timestamp, null, null);
|
||||
this(height, timestamp, null, null, null);
|
||||
}
|
||||
|
||||
public BlockSummary(Integer height, Date timestamp, Double medianFee, Integer transactionCount) {
|
||||
public BlockSummary(Integer height, Date timestamp, Double medianFee, Integer transactionCount, Integer weight) {
|
||||
this.height = height;
|
||||
this.timestamp = timestamp;
|
||||
this.medianFee = medianFee;
|
||||
this.transactionCount = transactionCount;
|
||||
this.weight = weight;
|
||||
}
|
||||
|
||||
public Integer getHeight() {
|
||||
|
|
@ -38,6 +40,10 @@ public class BlockSummary {
|
|||
return transactionCount == null ? Optional.empty() : Optional.of(transactionCount);
|
||||
}
|
||||
|
||||
public Optional<Integer> getWeight() {
|
||||
return weight == null ? Optional.empty() : Optional.of(weight);
|
||||
}
|
||||
|
||||
private static long calculateElapsedSeconds(long timestampUtc) {
|
||||
Instant timestampInstant = Instant.ofEpochMilli(timestampUtc);
|
||||
Instant nowInstant = Instant.now();
|
||||
|
|
@ -62,4 +68,9 @@ public class BlockSummary {
|
|||
public String toString() {
|
||||
return getElapsed() + ":" + getMedianFee();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(BlockSummary o) {
|
||||
return o.height - height;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
331
src/main/java/com/sparrowwallet/sparrow/control/BlockCube.java
Normal file
331
src/main/java/com/sparrowwallet/sparrow/control/BlockCube.java
Normal file
|
|
@ -0,0 +1,331 @@
|
|||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.sparrow.BlockSummary;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.shape.Polygon;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.scene.text.FontWeight;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
|
||||
public class BlockCube extends Group {
|
||||
public static final List<Integer> MEMPOOL_FEE_RATES_INTERVALS = List.of(1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200, 250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000);
|
||||
|
||||
public static final double CUBE_SIZE = 60;
|
||||
|
||||
private final IntegerProperty weightProperty = new SimpleIntegerProperty(0);
|
||||
private final DoubleProperty medianFeeProperty = new SimpleDoubleProperty(-1.0d);
|
||||
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 Polygon front;
|
||||
private Rectangle unusedArea;
|
||||
private Rectangle usedArea;
|
||||
|
||||
private final Text heightText = new Text();
|
||||
private final Text medianFeeText = new Text();
|
||||
private final Text unitsText = new Text();
|
||||
private final TextFlow medianFeeTextFlow = new TextFlow();
|
||||
private final Text txCountText = new Text();
|
||||
private final Text elapsedText = new Text();
|
||||
|
||||
public BlockCube(Integer weight, Double medianFee, Integer height, Integer txCount, Long timestamp, boolean confirmed) {
|
||||
getStyleClass().addAll("block-" + Network.getCanonical().getName(), "block-cube");
|
||||
this.confirmedProperty.set(confirmed);
|
||||
|
||||
this.weightProperty.addListener((_, _, _) -> {
|
||||
if(front != null) {
|
||||
updateFill();
|
||||
}
|
||||
});
|
||||
this.medianFeeProperty.addListener((_, _, newValue) -> {
|
||||
medianFeeText.setText("~" + Math.round(Math.max(newValue.doubleValue(), 1.0d)));
|
||||
unitsText.setText(" s/vb");
|
||||
double unitsWidth = TextUtils.computeTextWidth(unitsText.getFont(), unitsText.getText(), 0.0d);
|
||||
medianFeeTextFlow.setTranslateX((CUBE_SIZE - (medianFeeText.getLayoutBounds().getWidth() + unitsWidth)) / 2);
|
||||
});
|
||||
this.txCountProperty.addListener((_, _, newValue) -> {
|
||||
txCountText.setText(newValue.intValue() == 0 ? "" : newValue + " txes");
|
||||
txCountText.setX((CUBE_SIZE - txCountText.getLayoutBounds().getWidth()) / 2);
|
||||
});
|
||||
this.timestampProperty.addListener((_, _, newValue) -> {
|
||||
elapsedProperty.set(getElapsed(newValue.longValue()));
|
||||
});
|
||||
this.elapsedProperty.addListener((_, _, newValue) -> {
|
||||
elapsedText.setText(isConfirmed() ? newValue : "In ~10m");
|
||||
elapsedText.setX((CUBE_SIZE - elapsedText.getLayoutBounds().getWidth()) / 2);
|
||||
});
|
||||
this.heightProperty.addListener((_, _, newValue) -> {
|
||||
heightText.setText(newValue.intValue() == 0 ? "" : String.valueOf(newValue));
|
||||
heightText.setX(((CUBE_SIZE * 0.7) - heightText.getLayoutBounds().getWidth()) / 2);
|
||||
});
|
||||
this.confirmedProperty.addListener((_, _, _) -> {
|
||||
if(front != null) {
|
||||
updateFill();
|
||||
}
|
||||
});
|
||||
this.medianFeeText.textProperty().addListener((_, _, _) -> {
|
||||
pulse();
|
||||
});
|
||||
|
||||
if(weight != null) {
|
||||
this.weightProperty.set(weight);
|
||||
}
|
||||
if(medianFee != null) {
|
||||
this.medianFeeProperty.set(medianFee);
|
||||
}
|
||||
if(height != null) {
|
||||
this.heightProperty.set(height);
|
||||
}
|
||||
if(txCount != null) {
|
||||
this.txCountProperty.set(txCount);
|
||||
}
|
||||
if(timestamp != null) {
|
||||
this.timestampProperty.set(timestamp);
|
||||
}
|
||||
|
||||
drawCube();
|
||||
}
|
||||
|
||||
private void drawCube() {
|
||||
double depth = CUBE_SIZE * 0.2;
|
||||
double perspective = CUBE_SIZE * 0.04;
|
||||
|
||||
front = new Polygon(0, 0, CUBE_SIZE, 0, CUBE_SIZE, CUBE_SIZE, 0, CUBE_SIZE);
|
||||
front.getStyleClass().add("block-front");
|
||||
front.setFill(null);
|
||||
unusedArea = new Rectangle(0, 0, CUBE_SIZE, CUBE_SIZE);
|
||||
unusedArea.getStyleClass().add("block-unused");
|
||||
usedArea = new Rectangle(0, 0, CUBE_SIZE, CUBE_SIZE);
|
||||
usedArea.getStyleClass().add("block-used");
|
||||
|
||||
Group frontFaceGroup = new Group(front, unusedArea, usedArea);
|
||||
|
||||
Polygon top = new Polygon(0, 0, CUBE_SIZE, 0, CUBE_SIZE - depth - perspective, -depth, -depth, -depth);
|
||||
top.getStyleClass().add("block-top");
|
||||
top.setStroke(null);
|
||||
|
||||
Polygon left = new Polygon(0, 0, -depth, -depth, -depth, CUBE_SIZE - depth - perspective, 0, CUBE_SIZE);
|
||||
left.getStyleClass().add("block-left");
|
||||
left.setStroke(null);
|
||||
|
||||
updateFill();
|
||||
|
||||
heightText.getStyleClass().add("block-height");
|
||||
heightText.setFont(new Font(11));
|
||||
heightText.setX(((CUBE_SIZE * 0.7) - heightText.getLayoutBounds().getWidth()) / 2);
|
||||
heightText.setY(-24);
|
||||
|
||||
medianFeeText.getStyleClass().add("block-text");
|
||||
medianFeeText.setFont(Font.font(null, FontWeight.BOLD, 11));
|
||||
unitsText.getStyleClass().add("block-text");
|
||||
unitsText.setFont(new Font(10));
|
||||
medianFeeTextFlow.getChildren().addAll(medianFeeText, unitsText);
|
||||
medianFeeTextFlow.setTranslateX((CUBE_SIZE - (medianFeeText.getLayoutBounds().getWidth() + unitsText.getLayoutBounds().getWidth())) / 2);
|
||||
medianFeeTextFlow.setTranslateY(7);
|
||||
|
||||
txCountText.getStyleClass().add("block-text");
|
||||
txCountText.setFont(new Font(10));
|
||||
txCountText.setOpacity(0.7);
|
||||
txCountText.setX((CUBE_SIZE - txCountText.getLayoutBounds().getWidth()) / 2);
|
||||
txCountText.setY(34);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private void updateFill() {
|
||||
if(isConfirmed()) {
|
||||
getStyleClass().removeAll("block-unconfirmed");
|
||||
if(!getStyleClass().contains("block-confirmed")) {
|
||||
getStyleClass().add("block-confirmed");
|
||||
}
|
||||
double startY = 1 - weightProperty.doubleValue() / (Transaction.MAX_BLOCK_SIZE_VBYTES * Transaction.WITNESS_SCALE_FACTOR);
|
||||
double startYAbsolute = startY * BlockCube.CUBE_SIZE;
|
||||
unusedArea.setHeight(startYAbsolute);
|
||||
unusedArea.setStyle(null);
|
||||
usedArea.setY(startYAbsolute);
|
||||
usedArea.setHeight(CUBE_SIZE - startYAbsolute);
|
||||
usedArea.setVisible(true);
|
||||
heightText.setVisible(true);
|
||||
} else {
|
||||
getStyleClass().removeAll("block-confirmed");
|
||||
if(!getStyleClass().contains("block-unconfirmed")) {
|
||||
getStyleClass().add("block-unconfirmed");
|
||||
}
|
||||
usedArea.setVisible(false);
|
||||
unusedArea.setStyle("-fx-fill: " + getFeeRateStyleName() + ";");
|
||||
heightText.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void pulse() {
|
||||
if(isConfirmed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(unusedArea != null) {
|
||||
unusedArea.setStyle("-fx-fill: " + getFeeRateStyleName() + ";");
|
||||
}
|
||||
|
||||
Timeline timeline = new Timeline(
|
||||
new KeyFrame(Duration.ZERO, new KeyValue(opacityProperty(), 1.0)),
|
||||
new KeyFrame(Duration.millis(500), new KeyValue(opacityProperty(), 0.7)),
|
||||
new KeyFrame(Duration.millis(1000), new KeyValue(opacityProperty(), 1.0))
|
||||
);
|
||||
|
||||
timeline.setCycleCount(1);
|
||||
timeline.play();
|
||||
}
|
||||
|
||||
private static long calculateElapsedSeconds(long timestampUtc) {
|
||||
Instant timestampInstant = Instant.ofEpochMilli(timestampUtc);
|
||||
Instant nowInstant = Instant.now();
|
||||
return ChronoUnit.SECONDS.between(timestampInstant, nowInstant);
|
||||
}
|
||||
|
||||
public static String getElapsed(long timestampUtc) {
|
||||
long elapsed = calculateElapsedSeconds(timestampUtc);
|
||||
if(elapsed < 60) {
|
||||
return "Just now";
|
||||
} else if(elapsed < 3600) {
|
||||
return Math.round(elapsed / 60f) + "m ago";
|
||||
} else if(elapsed < 86400) {
|
||||
return Math.round(elapsed / 3600f) + "h ago";
|
||||
} else {
|
||||
return Math.round(elapsed / 86400d) + "d ago";
|
||||
}
|
||||
}
|
||||
|
||||
private String getFeeRateStyleName() {
|
||||
double rate = getMedianFee();
|
||||
int[] feeRateInterval = getFeeRateInterval(rate);
|
||||
if(feeRateInterval[1] == Integer.MAX_VALUE) {
|
||||
return "VSIZE2000-2200_COLOR";
|
||||
}
|
||||
int[] nextRateInterval = getFeeRateInterval(rate * 2);
|
||||
String from = "VSIZE" + feeRateInterval[0] + "-" + feeRateInterval[1] + "_COLOR";
|
||||
String to = "VSIZE" + nextRateInterval[0] + "-" + (nextRateInterval[1] == Integer.MAX_VALUE ? "2200" : nextRateInterval[1]) + "_COLOR";
|
||||
return "linear-gradient(from 75% 0% to 100% 0%, " + from + " 0%, " + to + " 100%, " + from +")";
|
||||
}
|
||||
|
||||
private int[] getFeeRateInterval(double medianFee) {
|
||||
for(int i = 0; i < MEMPOOL_FEE_RATES_INTERVALS.size(); i++) {
|
||||
int feeRate = MEMPOOL_FEE_RATES_INTERVALS.get(i);
|
||||
int nextFeeRate = (i == MEMPOOL_FEE_RATES_INTERVALS.size() - 1 ? Integer.MAX_VALUE : MEMPOOL_FEE_RATES_INTERVALS.get(i + 1));
|
||||
if(feeRate <= medianFee && nextFeeRate > medianFee) {
|
||||
return new int[] { feeRate, nextFeeRate };
|
||||
}
|
||||
}
|
||||
|
||||
return new int[] { 1, 2 };
|
||||
}
|
||||
|
||||
public int getWeight() {
|
||||
return weightProperty.get();
|
||||
}
|
||||
|
||||
public IntegerProperty weightProperty() {
|
||||
return weightProperty;
|
||||
}
|
||||
|
||||
public void setWeight(int weight) {
|
||||
weightProperty.set(weight);
|
||||
}
|
||||
|
||||
public double getMedianFee() {
|
||||
return medianFeeProperty.get();
|
||||
}
|
||||
|
||||
public DoubleProperty medianFee() {
|
||||
return medianFeeProperty;
|
||||
}
|
||||
|
||||
public void setMedianFee(double medianFee) {
|
||||
medianFeeProperty.set(medianFee);
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return heightProperty.get();
|
||||
}
|
||||
|
||||
public IntegerProperty heightProperty() {
|
||||
return heightProperty;
|
||||
}
|
||||
|
||||
public void setHeight(int height) {
|
||||
heightProperty.set(height);
|
||||
}
|
||||
|
||||
public int getTxCount() {
|
||||
return txCountProperty.get();
|
||||
}
|
||||
|
||||
public IntegerProperty txCountProperty() {
|
||||
return txCountProperty;
|
||||
}
|
||||
|
||||
public void setTxCount(int txCount) {
|
||||
txCountProperty.set(txCount);
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestampProperty.get();
|
||||
}
|
||||
|
||||
public LongProperty timestampProperty() {
|
||||
return timestampProperty;
|
||||
}
|
||||
|
||||
public void setTimestamp(long timestamp) {
|
||||
timestampProperty.set(timestamp);
|
||||
}
|
||||
|
||||
public String getElapsed() {
|
||||
return elapsedProperty.get();
|
||||
}
|
||||
|
||||
public StringProperty elapsedProperty() {
|
||||
return elapsedProperty;
|
||||
}
|
||||
|
||||
public void setElapsed(String elapsed) {
|
||||
elapsedProperty.set(elapsed);
|
||||
}
|
||||
|
||||
public boolean isConfirmed() {
|
||||
return confirmedProperty.get();
|
||||
}
|
||||
|
||||
public BooleanProperty confirmedProperty() {
|
||||
return confirmedProperty;
|
||||
}
|
||||
|
||||
public void setConfirmed(boolean confirmed) {
|
||||
confirmedProperty.set(confirmed);
|
||||
}
|
||||
|
||||
public static BlockCube fromBlockSummary(BlockSummary blockSummary) {
|
||||
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,150 @@
|
|||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.BlockSummary;
|
||||
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.layout.Pane;
|
||||
import javafx.scene.shape.Line;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class RecentBlocksView extends Pane {
|
||||
private static final double CUBE_SPACING = 100;
|
||||
private static final double ANIMATION_DURATION_MILLIS = 1000;
|
||||
private static final double SEPARATOR_X = 74;
|
||||
|
||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||
|
||||
private final ObjectProperty<List<BlockCube>> cubesProperty = new SimpleObjectProperty<>(new ArrayList<>());
|
||||
|
||||
public RecentBlocksView() {
|
||||
cubesProperty.addListener((_, _, newValue) -> {
|
||||
if(newValue != null && newValue.size() == 3) {
|
||||
drawView();
|
||||
}
|
||||
});
|
||||
|
||||
Rectangle clip = new Rectangle(-20, -40, CUBE_SPACING * 3 - 20, 100);
|
||||
setClip(clip);
|
||||
|
||||
Observable<Long> intervalObservable = Observable.interval(1, TimeUnit.MINUTES);
|
||||
disposables.add(intervalObservable.observeOn(JavaFxScheduler.platform()).subscribe(_ -> {
|
||||
for(BlockCube cube : getCubes()) {
|
||||
cube.setElapsed(BlockCube.getElapsed(cube.getTimestamp()));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public void drawView() {
|
||||
createSeparator();
|
||||
|
||||
for(int i = 0; i < 3; i++) {
|
||||
BlockCube cube = getCubes().get(i);
|
||||
cube.setTranslateX(i * CUBE_SPACING);
|
||||
getChildren().add(cube);
|
||||
}
|
||||
}
|
||||
|
||||
private void createSeparator() {
|
||||
Line separator = new Line(SEPARATOR_X, -9, SEPARATOR_X, 80);
|
||||
separator.getStyleClass().add("blocks-separator");
|
||||
separator.getStrokeDashArray().addAll(5.0, 5.0); // Create dotted line pattern
|
||||
separator.setStrokeWidth(1.0);
|
||||
getChildren().add(separator);
|
||||
}
|
||||
|
||||
public void update(List<BlockSummary> latestBlocks, Double currentFeeRate) {
|
||||
if(getCubes().isEmpty()) {
|
||||
List<BlockCube> cubes = new ArrayList<>();
|
||||
cubes.add(new BlockCube(null, currentFeeRate, null, null, 0L, false));
|
||||
cubes.addAll(latestBlocks.stream().map(BlockCube::fromBlockSummary).limit(2).toList());
|
||||
setCubes(cubes);
|
||||
} else {
|
||||
int knownTip = getCubes().stream().mapToInt(BlockCube::getHeight).max().orElse(0);
|
||||
int latestTip = latestBlocks.stream().mapToInt(BlockSummary::getHeight).max().orElse(0);
|
||||
if(latestTip > knownTip) {
|
||||
addNewBlock(latestBlocks, currentFeeRate);
|
||||
} else {
|
||||
for(int i = 1; i < getCubes().size() && i < latestBlocks.size(); i++) {
|
||||
BlockCube blockCube = getCubes().get(i);
|
||||
BlockSummary latestBlock = latestBlocks.get(i);
|
||||
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.setTxCount(latestBlock.getTransactionCount().orElse(0));
|
||||
}
|
||||
updateFeeRate(currentFeeRate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void addNewBlock(List<BlockSummary> latestBlocks, Double currentFeeRate) {
|
||||
if(getCubes().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for(int i = 0; i < getCubes().size() && i < latestBlocks.size(); i++) {
|
||||
BlockCube blockCube = getCubes().get(i);
|
||||
BlockSummary latestBlock = latestBlocks.get(i);
|
||||
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.setTxCount(latestBlock.getTransactionCount().orElse(0));
|
||||
}
|
||||
|
||||
add(new BlockCube(null, currentFeeRate, null, null, 0L, false));
|
||||
}
|
||||
|
||||
public void add(BlockCube newCube) {
|
||||
newCube.setTranslateX(-CUBE_SPACING);
|
||||
getChildren().add(newCube);
|
||||
getCubes().getFirst().setConfirmed(true);
|
||||
getCubes().addFirst(newCube);
|
||||
animateCubes();
|
||||
if(getCubes().size() > 4) {
|
||||
BlockCube lastCube = getCubes().getLast();
|
||||
getChildren().remove(lastCube);
|
||||
getCubes().remove(lastCube);
|
||||
}
|
||||
}
|
||||
|
||||
public void updateFeeRate(Double currentFeeRate) {
|
||||
if(!getCubes().isEmpty()) {
|
||||
BlockCube firstCube = getCubes().getFirst();
|
||||
firstCube.setMedianFee(currentFeeRate);
|
||||
}
|
||||
}
|
||||
|
||||
private void animateCubes() {
|
||||
for(int i = 0; i < getCubes().size(); i++) {
|
||||
BlockCube cube = getCubes().get(i);
|
||||
TranslateTransition transition = new TranslateTransition(Duration.millis(ANIMATION_DURATION_MILLIS), cube);
|
||||
transition.setToX(i * CUBE_SPACING);
|
||||
transition.play();
|
||||
}
|
||||
}
|
||||
|
||||
public List<BlockCube> getCubes() {
|
||||
return cubesProperty.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<List<BlockCube>> cubesProperty() {
|
||||
return cubesProperty;
|
||||
}
|
||||
|
||||
public void setCubes(List<BlockCube> cubes) {
|
||||
this.cubesProperty.set(cubes);
|
||||
}
|
||||
}
|
||||
|
|
@ -8,15 +8,13 @@ import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
|||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.Theme;
|
||||
import com.sparrowwallet.sparrow.*;
|
||||
import com.sparrowwallet.sparrow.event.ExcludeUtxoEvent;
|
||||
import com.sparrowwallet.sparrow.event.ReplaceChangeAddressEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.net.ExchangeSource;
|
||||
import com.sparrowwallet.sparrow.wallet.OptimizationStrategy;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
|
|
@ -229,6 +227,13 @@ public class TransactionDiagram extends GridPane {
|
|||
getChildren().clear();
|
||||
getChildren().addAll(inputsTypePane, inputsPane, inputsLinesPane, txPane, outputsLinesPane, outputsPane);
|
||||
|
||||
List<Payment> defaultPayments = getDefaultPayments();
|
||||
if(!isFinal() && defaultPayments.size() > 1) {
|
||||
Pane totalsPane = getTotalsPane(defaultPayments);
|
||||
GridPane.setConstraints(totalsPane, 2, 0, 3, 1);
|
||||
getChildren().add(totalsPane);
|
||||
}
|
||||
|
||||
if(contextMenu == null) {
|
||||
contextMenu = new ContextMenu();
|
||||
MenuItem menuItem = new MenuItem("Save as Image...");
|
||||
|
|
@ -616,6 +621,10 @@ public class TransactionDiagram extends GridPane {
|
|||
}
|
||||
}
|
||||
|
||||
private List<Payment> getDefaultPayments() {
|
||||
return walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT).toList();
|
||||
}
|
||||
|
||||
private Pane getOutputsLines(List<Payment> displayedPayments) {
|
||||
VBox pane = new VBox();
|
||||
Group group = new Group();
|
||||
|
|
@ -839,6 +848,33 @@ public class TransactionDiagram extends GridPane {
|
|||
return txPane;
|
||||
}
|
||||
|
||||
private Pane getTotalsPane(List<Payment> defaultPayments) {
|
||||
VBox totalsBox = new VBox();
|
||||
totalsBox.setPadding(new Insets(0, 0, 15, 0));
|
||||
totalsBox.setAlignment(Pos.CENTER);
|
||||
|
||||
long amount = defaultPayments.stream().mapToLong(Payment::getAmount).sum();
|
||||
|
||||
HBox coinLabelBox = new HBox();
|
||||
coinLabelBox.setAlignment(Pos.CENTER);
|
||||
CoinLabel totalCoinLabel = new CoinLabel();
|
||||
totalCoinLabel.setValue(amount);
|
||||
coinLabelBox.getChildren().addAll(totalCoinLabel, new Label(" in "), new Label(Long.toString(defaultPayments.size())), new Label(" payments"));
|
||||
totalsBox.getChildren().addAll(createSpacer(), coinLabelBox);
|
||||
|
||||
CurrencyRate currencyRate = AppServices.getFiatCurrencyExchangeRate();
|
||||
if(currencyRate != null && currencyRate.isAvailable() && Config.get().getExchangeSource() != ExchangeSource.NONE) {
|
||||
HBox fiatLabelBox = new HBox();
|
||||
fiatLabelBox.setAlignment(Pos.CENTER);
|
||||
FiatLabel fiatLabel = new FiatLabel();
|
||||
fiatLabel.set(currencyRate, amount);
|
||||
fiatLabelBox.getChildren().add(fiatLabel);
|
||||
totalsBox.getChildren().add(fiatLabelBox);
|
||||
}
|
||||
|
||||
return totalsBox;
|
||||
}
|
||||
|
||||
private void saveAsImage() {
|
||||
Stage window = new Stage();
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import javafx.scene.control.*;
|
|||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
|
@ -103,6 +104,7 @@ public class WalletSummaryDialog extends Dialog<Void> {
|
|||
vBox.getChildren().add(table);
|
||||
|
||||
hBox.getChildren().add(vBox);
|
||||
HBox.setHgrow(vBox, Priority.ALWAYS);
|
||||
|
||||
Wallet balanceWallet;
|
||||
if(allOpenWallets) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import com.github.arteam.simplejsonrpc.client.JsonRpcClient;
|
|||
import com.github.arteam.simplejsonrpc.client.Transport;
|
||||
import com.github.arteam.simplejsonrpc.client.exception.JsonRpcBatchException;
|
||||
import com.github.arteam.simplejsonrpc.client.exception.JsonRpcException;
|
||||
import com.github.arteam.simplejsonrpc.core.domain.ErrorMessage;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
|
|
@ -162,12 +161,12 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
|
|||
try {
|
||||
return batchRequest.execute();
|
||||
} catch(JsonRpcBatchException e) {
|
||||
log.warn("Failed to unsubscribe from script hashes: " + e.getErrors().keySet(), e);
|
||||
log.info("Failed to unsubscribe from script hashes: " + e.getErrors().keySet(), e);
|
||||
Map<String, Boolean> unsubscribedScriptHashes = scriptHashes.stream().collect(Collectors.toMap(s -> s, _ -> true));
|
||||
unsubscribedScriptHashes.keySet().removeIf(scriptHash -> e.getErrors().containsKey(scriptHash));
|
||||
return unsubscribedScriptHashes;
|
||||
} catch(Exception e) {
|
||||
log.warn("Failed to unsubscribe from script hashes: " + scriptHashes, e);
|
||||
log.info("Failed to unsubscribe from script hashes: " + scriptHashes, e);
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
}
|
||||
|
|
@ -191,6 +190,24 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<Integer, BlockStats> getBlockStats(Transport transport, Set<Integer> blockHeights) {
|
||||
PagedBatchRequestBuilder<Integer, BlockStats> batchRequest = PagedBatchRequestBuilder.create(transport, idCounter).keysType(Integer.class).returnType(BlockStats.class);
|
||||
|
||||
for(Integer height : blockHeights) {
|
||||
batchRequest.add(height, "blockchain.block.stats", height);
|
||||
}
|
||||
|
||||
try {
|
||||
return batchRequest.execute();
|
||||
} catch(JsonRpcBatchException e) {
|
||||
return (Map<Integer, BlockStats>)e.getSuccesses();
|
||||
} catch(Exception e) {
|
||||
throw new ElectrumServerRpcException("Failed to retrieve block stats for block heights: " + blockHeights, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, String> getTransactions(Transport transport, Wallet wallet, Set<String> txids) {
|
||||
|
|
|
|||
14
src/main/java/com/sparrowwallet/sparrow/net/BlockStats.java
Normal file
14
src/main/java/com/sparrowwallet/sparrow/net/BlockStats.java
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.sparrowwallet.sparrow.BlockSummary;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record BlockStats(int height, String blockhash, double[] feerate_percentiles, int total_weight, int txs, long time) {
|
||||
public BlockSummary toBlockSummary() {
|
||||
Double medianFee = feerate_percentiles != null && feerate_percentiles.length > 0 ? feerate_percentiles[feerate_percentiles.length / 2] : null;
|
||||
return new BlockSummary(height, new Date(time * 1000), medianFee, txs, total_weight);
|
||||
}
|
||||
}
|
||||
|
|
@ -82,6 +82,8 @@ public class ElectrumServer {
|
|||
|
||||
private static Server coreElectrumServer;
|
||||
|
||||
private static ServerCapability serverCapability;
|
||||
|
||||
private static final Pattern RPC_WALLET_LOADING_PATTERN = Pattern.compile(".*\"(Wallet loading failed[:.][^\"]*)\".*");
|
||||
|
||||
private static synchronized CloseableTransport getTransport() throws ServerException {
|
||||
|
|
@ -981,6 +983,21 @@ public class ElectrumServer {
|
|||
}
|
||||
|
||||
public Map<Integer, BlockSummary> getBlockSummaryMap(Integer height, BlockHeader blockHeader) throws ServerException {
|
||||
if(serverCapability.supportsBlockStats()) {
|
||||
if(height == null) {
|
||||
Integer current = AppServices.getCurrentBlockHeight();
|
||||
if(current == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Set<Integer> heights = IntStream.range(current - 1, current + 1).boxed().collect(Collectors.toSet());
|
||||
Map<Integer, BlockStats> blockStats = electrumServerRpc.getBlockStats(getTransport(), heights);
|
||||
return blockStats.keySet().stream().collect(Collectors.toMap(java.util.function.Function.identity(), v -> blockStats.get(v).toBlockSummary()));
|
||||
} else {
|
||||
Map<Integer, BlockStats> blockStats = electrumServerRpc.getBlockStats(getTransport(), Set.of(height));
|
||||
return blockStats.keySet().stream().collect(Collectors.toMap(java.util.function.Function.identity(), v -> blockStats.get(v).toBlockSummary()));
|
||||
}
|
||||
}
|
||||
|
||||
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
|
||||
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
|
||||
|
||||
|
|
@ -1010,7 +1027,7 @@ public class ElectrumServer {
|
|||
if(current == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Set<BlockTransactionHash> references = IntStream.range(current - 4, current + 1)
|
||||
Set<BlockTransactionHash> references = IntStream.range(current - 1, current + 1)
|
||||
.mapToObj(i -> new BlockTransaction(null, i, null, null, null)).collect(Collectors.toSet());
|
||||
Map<Integer, BlockHeader> blockHeaders = getBlockHeaders(null, references);
|
||||
return blockHeaders.keySet().stream()
|
||||
|
|
@ -1219,7 +1236,7 @@ public class ElectrumServer {
|
|||
}
|
||||
|
||||
if(server.startsWith("cormorant")) {
|
||||
return new ServerCapability(true);
|
||||
return new ServerCapability(true, false, true);
|
||||
}
|
||||
|
||||
if(server.startsWith("electrs/")) {
|
||||
|
|
@ -1405,7 +1422,7 @@ public class ElectrumServer {
|
|||
firstCall = false;
|
||||
|
||||
//If electrumx is detected, we can upgrade to batched RPC. Electrs/EPS do not support batching.
|
||||
ServerCapability serverCapability = getServerCapability(serverVersion);
|
||||
serverCapability = getServerCapability(serverVersion);
|
||||
if(serverCapability.supportsBatching()) {
|
||||
log.debug("Upgrading to batched JSON-RPC");
|
||||
electrumServerRpc = new BatchedElectrumServerRpc(electrumServerRpc.getIdCounterValue(), serverCapability.getMaxTargetBlocks());
|
||||
|
|
@ -1945,13 +1962,18 @@ public class ElectrumServer {
|
|||
|
||||
if(startHeight == 0 || totalBlocks > 1 || startHeight > maxHeight + 1) {
|
||||
if(isBlockstorm(totalBlocks)) {
|
||||
for(int height = maxHeight + 1; height < endHeight; height++) {
|
||||
blockSummaryMap.put(height, new BlockSummary(height, new Date()));
|
||||
int start = Math.max(maxHeight + 1, endHeight - 15);
|
||||
for(int height = start; height <= endHeight; height++) {
|
||||
blockSummaryMap.put(height, new BlockSummary(height, new Date(), 1.0d, 0, 0));
|
||||
}
|
||||
} else {
|
||||
blockSummaryMap.putAll(electrumServer.getRecentBlockSummaryMap());
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
List<NewBlockEvent> events = new ArrayList<>(newBlockEvents);
|
||||
events.removeIf(event -> blockSummaryMap.containsKey(event.getHeight()));
|
||||
if(!events.isEmpty()) {
|
||||
for(NewBlockEvent event : newBlockEvents) {
|
||||
blockSummaryMap.putAll(electrumServer.getBlockSummaryMap(event.getHeight(), event.getBlockHeader()));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ public interface ElectrumServerRpc {
|
|||
|
||||
Map<Integer, String> getBlockHeaders(Transport transport, Wallet wallet, Set<Integer> blockHeights);
|
||||
|
||||
Map<Integer, BlockStats> getBlockStats(Transport transport, Set<Integer> blockHeights);
|
||||
|
||||
Map<String, String> getTransactions(Transport transport, Wallet wallet, Set<String> txids);
|
||||
|
||||
Map<String, VerboseTransaction> getVerboseTransactions(Transport transport, Set<String> txids, String scriptHash);
|
||||
|
|
|
|||
|
|
@ -285,7 +285,7 @@ public enum FeeRatesSource {
|
|||
}
|
||||
}
|
||||
|
||||
protected record MempoolBlockSummary(String id, Integer height, Long timestamp, Integer tx_count, MempoolBlockSummaryExtras extras) {
|
||||
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();
|
||||
}
|
||||
|
|
@ -294,7 +294,7 @@ public enum FeeRatesSource {
|
|||
if(height == null || timestamp == null) {
|
||||
throw new IllegalStateException("Height = " + height + ", timestamp = " + timestamp + ": both must be specified");
|
||||
}
|
||||
return new BlockSummary(height, new Date(timestamp * 1000), getMedianFee(), tx_count);
|
||||
return new BlockSummary(height, new Date(timestamp * 1000), getMedianFee(), tx_count, weight);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
|||
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
|
||||
|
||||
class ScriptHashTx {
|
||||
public class ScriptHashTx {
|
||||
public static final ScriptHashTx ERROR_TX = new ScriptHashTx() {
|
||||
@Override
|
||||
public BlockTransactionHash getBlockchainTransactionHash() {
|
||||
|
|
|
|||
|
|
@ -5,15 +5,29 @@ import com.sparrowwallet.sparrow.AppServices;
|
|||
public class ServerCapability {
|
||||
private final boolean supportsBatching;
|
||||
private final int maxTargetBlocks;
|
||||
private final boolean supportsRecentMempool;
|
||||
private final boolean supportsBlockStats;
|
||||
|
||||
public ServerCapability(boolean supportsBatching) {
|
||||
this.supportsBatching = supportsBatching;
|
||||
this.maxTargetBlocks = AppServices.TARGET_BLOCKS_RANGE.getLast();
|
||||
this(supportsBatching, AppServices.TARGET_BLOCKS_RANGE.getLast());
|
||||
}
|
||||
|
||||
public ServerCapability(boolean supportsBatching, int maxTargetBlocks) {
|
||||
this.supportsBatching = supportsBatching;
|
||||
this.maxTargetBlocks = maxTargetBlocks;
|
||||
this.supportsRecentMempool = false;
|
||||
this.supportsBlockStats = false;
|
||||
}
|
||||
|
||||
public ServerCapability(boolean supportsBatching, boolean supportsRecentMempool, boolean supportsBlockStats) {
|
||||
this(supportsBatching, AppServices.TARGET_BLOCKS_RANGE.getLast(), supportsRecentMempool, supportsBlockStats);
|
||||
}
|
||||
|
||||
public ServerCapability(boolean supportsBatching, int maxTargetBlocks, boolean supportsRecentMempool, boolean supportsBlockStats) {
|
||||
this.supportsBatching = supportsBatching;
|
||||
this.maxTargetBlocks = maxTargetBlocks;
|
||||
this.supportsRecentMempool = supportsRecentMempool;
|
||||
this.supportsBlockStats = supportsBlockStats;
|
||||
}
|
||||
|
||||
public boolean supportsBatching() {
|
||||
|
|
@ -23,4 +37,12 @@ public class ServerCapability {
|
|||
public int getMaxTargetBlocks() {
|
||||
return maxTargetBlocks;
|
||||
}
|
||||
|
||||
public boolean supportsRecentMempool() {
|
||||
return supportsRecentMempool;
|
||||
}
|
||||
|
||||
public boolean supportsBlockStats() {
|
||||
return supportsBlockStats;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -177,6 +177,29 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
|
|||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Integer, BlockStats> getBlockStats(Transport transport, Set<Integer> blockHeights) {
|
||||
JsonRpcClient client = new JsonRpcClient(transport);
|
||||
|
||||
Map<Integer, BlockStats> result = new LinkedHashMap<>();
|
||||
for(Integer blockHeight : blockHeights) {
|
||||
try {
|
||||
BlockStats blockStats = new RetryLogic<BlockStats>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(() ->
|
||||
client.createRequest().returnAs(BlockStats.class).method("blockchain.block.stats").id(idCounter.incrementAndGet()).params(blockHeight).execute());
|
||||
result.put(blockHeight, blockStats);
|
||||
} catch(ServerException e) {
|
||||
//If there is an error with the server connection, don't keep trying - this may take too long given many blocks
|
||||
throw new ElectrumServerRpcException("Failed to retrieve block stats for block height: " + blockHeight, e);
|
||||
} catch(JsonRpcException e) {
|
||||
log.warn("Failed to retrieve block stats for block height: " + blockHeight + (e.getErrorMessage() != null ? " (" + e.getErrorMessage().getMessage() + ")" : ""));
|
||||
} catch(Exception e) {
|
||||
log.warn("Failed to retrieve block stats for block height: " + blockHeight + " (" + e.getMessage() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getTransactions(Transport transport, Wallet wallet, Set<String> txids) {
|
||||
JsonRpcClient client = new JsonRpcClient(transport);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import com.sparrowwallet.sparrow.AppServices;
|
|||
import java.util.Date;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
class VerboseTransaction {
|
||||
public class VerboseTransaction {
|
||||
public String blockhash;
|
||||
public long blocktime;
|
||||
public int confirmations;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcOptional;
|
|||
import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcParam;
|
||||
import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcService;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.sparrow.net.BlockStats;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
|
@ -48,6 +49,9 @@ public interface BitcoindClientService {
|
|||
@JsonRpcMethod("getblockheader")
|
||||
VerboseBlockHeader getBlockHeader(@JsonRpcParam("blockhash") String blockhash);
|
||||
|
||||
@JsonRpcMethod("getblockstats")
|
||||
BlockStats getBlockStats(@JsonRpcParam("blockhash") int hash_or_height);
|
||||
|
||||
@JsonRpcMethod("getrawtransaction")
|
||||
Object getRawTransaction(@JsonRpcParam("txid") String txid, @JsonRpcParam("verbose") boolean verbose);
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import com.sparrowwallet.sparrow.EventManager;
|
|||
import com.sparrowwallet.sparrow.SparrowWallet;
|
||||
import com.sparrowwallet.sparrow.event.MempoolEntriesInitializedEvent;
|
||||
import com.sparrowwallet.drongo.Version;
|
||||
import com.sparrowwallet.sparrow.net.BlockStats;
|
||||
import com.sparrowwallet.sparrow.net.cormorant.Cormorant;
|
||||
import com.sparrowwallet.sparrow.net.cormorant.bitcoind.*;
|
||||
import com.sparrowwallet.sparrow.net.cormorant.index.TxEntry;
|
||||
|
|
@ -157,6 +158,17 @@ public class ElectrumServerService {
|
|||
}
|
||||
}
|
||||
|
||||
@JsonRpcMethod("blockchain.block.stats")
|
||||
public BlockStats getBlockStats(@JsonRpcParam("height") int height) throws BitcoindIOException, BlockNotFoundException {
|
||||
try {
|
||||
return bitcoindClient.getBitcoindService().getBlockStats(height);
|
||||
} catch(JsonRpcException e) {
|
||||
throw new BlockNotFoundException(e.getErrorMessage());
|
||||
} catch(IllegalStateException e) {
|
||||
throw new BitcoindIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@JsonRpcMethod("blockchain.transaction.get")
|
||||
@SuppressWarnings("unchecked")
|
||||
public Object getTransaction(@JsonRpcParam("tx_hash") String tx_hash, @JsonRpcParam("verbose") @JsonRpcOptional boolean verbose) throws BitcoindIOException, TransactionNotFoundException {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.sparrowwallet.sparrow.wallet;
|
||||
|
||||
public enum FeeRatesSelection {
|
||||
BLOCK_TARGET("Block Target"), MEMPOOL_SIZE("Mempool Size");
|
||||
BLOCK_TARGET("Block Target"), MEMPOOL_SIZE("Mempool Size"), RECENT_BLOCKS("Recent Blocks");
|
||||
|
||||
private final String name;
|
||||
|
||||
|
|
|
|||
|
|
@ -13,10 +13,7 @@ import com.sparrowwallet.drongo.crypto.ECKey;
|
|||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.CurrencyRate;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.*;
|
||||
import com.sparrowwallet.sparrow.control.*;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
|
|
@ -28,6 +25,7 @@ import com.sparrowwallet.sparrow.paynym.PayNymService;
|
|||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
|
@ -78,6 +76,9 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
@FXML
|
||||
private ToggleButton mempoolSizeToggle;
|
||||
|
||||
@FXML
|
||||
private ToggleButton recentBlocksToggle;
|
||||
|
||||
@FXML
|
||||
private Field targetBlocksField;
|
||||
|
||||
|
|
@ -117,6 +118,9 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
@FXML
|
||||
private MempoolSizeFeeRatesChart mempoolSizeFeeRatesChart;
|
||||
|
||||
@FXML
|
||||
private RecentBlocksView recentBlocksView;
|
||||
|
||||
@FXML
|
||||
private TransactionDiagram transactionDiagram;
|
||||
|
||||
|
|
@ -162,6 +166,8 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
|
||||
private final ObjectProperty<BlockTransaction> replacedTransactionProperty = new SimpleObjectProperty<>(null);
|
||||
|
||||
private final ObjectProperty<FeeRatesSelection> feeRatesSelectionProperty = new SimpleObjectProperty<>(null);
|
||||
|
||||
private final List<byte[]> opReturnsList = new ArrayList<>();
|
||||
|
||||
private final Set<WalletNode> excludedChangeNodes = new HashSet<>();
|
||||
|
|
@ -299,6 +305,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
feeRange.valueProperty().addListener(feeRangeListener);
|
||||
|
||||
blockTargetFeeRatesChart.managedProperty().bind(blockTargetFeeRatesChart.visibleProperty());
|
||||
blockTargetFeeRatesChart.visibleProperty().bind(Bindings.equal(feeRatesSelectionProperty, FeeRatesSelection.BLOCK_TARGET));
|
||||
blockTargetFeeRatesChart.initialize();
|
||||
Map<Integer, Double> targetBlocksFeeRates = getTargetBlocksFeeRates();
|
||||
if(targetBlocksFeeRates != null) {
|
||||
|
|
@ -308,20 +315,41 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
}
|
||||
|
||||
mempoolSizeFeeRatesChart.managedProperty().bind(mempoolSizeFeeRatesChart.visibleProperty());
|
||||
mempoolSizeFeeRatesChart.visibleProperty().bind(blockTargetFeeRatesChart.visibleProperty().not());
|
||||
mempoolSizeFeeRatesChart.visibleProperty().bind(Bindings.equal(feeRatesSelectionProperty, FeeRatesSelection.MEMPOOL_SIZE));
|
||||
mempoolSizeFeeRatesChart.initialize();
|
||||
Map<Date, Set<MempoolRateSize>> mempoolHistogram = getMempoolHistogram();
|
||||
if(mempoolHistogram != null) {
|
||||
mempoolSizeFeeRatesChart.update(mempoolHistogram);
|
||||
}
|
||||
|
||||
recentBlocksView.managedProperty().bind(recentBlocksView.visibleProperty());
|
||||
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());
|
||||
}
|
||||
|
||||
feeRatesSelectionProperty.addListener((_, oldValue, newValue) -> {
|
||||
boolean isBlockTargetSelection = (newValue == FeeRatesSelection.BLOCK_TARGET);
|
||||
boolean wasBlockTargetSelection = (oldValue == FeeRatesSelection.BLOCK_TARGET || oldValue == null);
|
||||
targetBlocksField.setVisible(isBlockTargetSelection);
|
||||
if(isBlockTargetSelection) {
|
||||
setTargetBlocks(getTargetBlocks(getFeeRangeRate()));
|
||||
updateTransaction();
|
||||
} else if(wasBlockTargetSelection) {
|
||||
setFeeRangeRate(getTargetBlocksFeeRates().get(getTargetBlocks()));
|
||||
updateTransaction();
|
||||
}
|
||||
});
|
||||
|
||||
FeeRatesSelection feeRatesSelection = Config.get().getFeeRatesSelection();
|
||||
feeRatesSelection = (feeRatesSelection == null ? FeeRatesSelection.MEMPOOL_SIZE : feeRatesSelection);
|
||||
feeRatesSelection = (feeRatesSelection == null ? FeeRatesSelection.RECENT_BLOCKS : feeRatesSelection);
|
||||
cpfpFeeRate.managedProperty().bind(cpfpFeeRate.visibleProperty());
|
||||
cpfpFeeRate.setVisible(false);
|
||||
setDefaultFeeRate();
|
||||
updateFeeRateSelection(feeRatesSelection);
|
||||
feeSelectionToggleGroup.selectToggle(feeRatesSelection == FeeRatesSelection.BLOCK_TARGET ? targetBlocksToggle : mempoolSizeToggle);
|
||||
feeRatesSelectionProperty.set(feeRatesSelection);
|
||||
feeSelectionToggleGroup.selectToggle(feeRatesSelection == FeeRatesSelection.BLOCK_TARGET ? targetBlocksToggle :
|
||||
(feeRatesSelection == FeeRatesSelection.MEMPOOL_SIZE ? mempoolSizeToggle : recentBlocksToggle));
|
||||
feeSelectionToggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue != null) {
|
||||
FeeRatesSelection newFeeRatesSelection = (FeeRatesSelection)newValue.getUserData();
|
||||
|
|
@ -723,24 +751,13 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
return List.of(spentTxoFilter, new FrozenTxoFilter(), new CoinbaseTxoFilter(getWalletForm().getWallet()));
|
||||
}
|
||||
|
||||
private void updateFeeRateSelection(FeeRatesSelection feeRatesSelection) {
|
||||
boolean blockTargetSelection = (feeRatesSelection == FeeRatesSelection.BLOCK_TARGET);
|
||||
targetBlocksField.setVisible(blockTargetSelection);
|
||||
blockTargetFeeRatesChart.setVisible(blockTargetSelection);
|
||||
if(blockTargetSelection) {
|
||||
setTargetBlocks(getTargetBlocks(getFeeRangeRate()));
|
||||
} else {
|
||||
setFeeRangeRate(getTargetBlocksFeeRates().get(getTargetBlocks()));
|
||||
}
|
||||
updateTransaction();
|
||||
}
|
||||
|
||||
private void setDefaultFeeRate() {
|
||||
int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1);
|
||||
int index = TARGET_BLOCKS_RANGE.indexOf(defaultTarget);
|
||||
Double defaultRate = getTargetBlocksFeeRates().get(defaultTarget);
|
||||
targetBlocks.setValue(index);
|
||||
blockTargetFeeRatesChart.select(defaultTarget);
|
||||
recentBlocksView.updateFeeRate(defaultRate);
|
||||
setFeeRangeRate(defaultRate);
|
||||
setFeeRate(getFeeRangeRate());
|
||||
if(Network.get().equals(Network.MAINNET) && defaultRate == getFallbackFeeRate()) {
|
||||
|
|
@ -964,7 +981,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
}
|
||||
|
||||
private void setFiatFeeAmount(CurrencyRate currencyRate, Long amount) {
|
||||
if(amount != null && currencyRate != null && currencyRate.isAvailable()) {
|
||||
if(amount != null && currencyRate != null && currencyRate.isAvailable() && Config.get().getExchangeSource() != ExchangeSource.NONE) {
|
||||
fiatFeeAmount.set(currencyRate, amount);
|
||||
}
|
||||
}
|
||||
|
|
@ -1411,10 +1428,15 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
@Subscribe
|
||||
public void feeRateSelectionChanged(FeeRatesSelectionChangedEvent event) {
|
||||
if(event.getWallet() == getWalletForm().getWallet()) {
|
||||
updateFeeRateSelection(event.getFeeRateSelection());
|
||||
feeRatesSelectionProperty.set(event.getFeeRateSelection());
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void blockSummary(BlockSummaryEvent event) {
|
||||
Platform.runLater(() -> recentBlocksView.update(AppServices.getBlockSummaries().values().stream().sorted().toList(), AppServices.getDefaultFeeRate()));
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void spendUtxos(SpendUtxoEvent event) {
|
||||
if((event.getUtxos() == null || !event.getUtxos().isEmpty()) && event.getWallet().equals(getWalletForm().getWallet())) {
|
||||
|
|
@ -1497,12 +1519,18 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
if(event.getExchangeSource() == ExchangeSource.NONE) {
|
||||
fiatFeeAmount.setCurrency(null);
|
||||
fiatFeeAmount.setBtcRate(0.0);
|
||||
if(paymentTabs.getTabs().size() > 1) {
|
||||
updateTransaction();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void exchangeRatesUpdated(ExchangeRatesUpdatedEvent event) {
|
||||
setFiatFeeAmount(event.getCurrencyRate(), getFeeValueSats());
|
||||
if(paymentTabs.getTabs().size() > 1) {
|
||||
updateTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
|
|
@ -1597,7 +1625,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
} else if(payjoinPresent) {
|
||||
addLabel("Cannot fake coinjoin due to payjoin", getInfoGlyph());
|
||||
} else {
|
||||
if(utxoSelectorProperty().get() != null) {
|
||||
if(utxoSelectorProperty().get() != null && !(utxoSelectorProperty().get() instanceof MaxUtxoSelector)) {
|
||||
addLabel("Cannot fake coinjoin due to coin control", getInfoGlyph());
|
||||
} else {
|
||||
addLabel("Cannot fake coinjoin due to insufficient funds", getInfoGlyph());
|
||||
|
|
|
|||
|
|
@ -343,4 +343,32 @@ HorizontalHeaderColumn > TableColumnHeader.column-header.table-column{
|
|||
|
||||
#grid .spreadsheet-cell.selection {
|
||||
-fx-text-fill: -fx-base;
|
||||
}
|
||||
|
||||
.root .block-height {
|
||||
-fx-fill: derive(lightgray, -20%);
|
||||
}
|
||||
|
||||
.root .blocks-separator {
|
||||
-fx-stroke: derive(lightgray, -20%);
|
||||
}
|
||||
|
||||
.root .block-confirmed .block-unused {
|
||||
-fx-fill: #5a5a65;
|
||||
}
|
||||
|
||||
.root .block-confirmed .block-top {
|
||||
-fx-fill: #474c5e;
|
||||
}
|
||||
|
||||
.root .block-confirmed .block-left {
|
||||
-fx-fill: #3c4055;
|
||||
}
|
||||
|
||||
.root .block-unconfirmed .block-top {
|
||||
-fx-fill: #635b57;
|
||||
}
|
||||
|
||||
.root .block-unconfirmed .block-left {
|
||||
-fx-fill: #4e4846;
|
||||
}
|
||||
|
|
@ -132,6 +132,66 @@
|
|||
-fx-text-fill: -fx-accent;
|
||||
}
|
||||
|
||||
.block-used {
|
||||
-fx-fill: linear-gradient(from 0% 0% to 0% 100%, -top 0%, -bottom 100%);
|
||||
}
|
||||
|
||||
.block-mainnet {
|
||||
-top: rgb(155, 79, 174);
|
||||
-bottom: rgb(77, 96, 154);
|
||||
}
|
||||
|
||||
.block-testnet {
|
||||
-top: rgb(30, 136, 229);
|
||||
-bottom: rgba(57, 73, 171);
|
||||
}
|
||||
|
||||
.block-signet {
|
||||
-top: rgb(136, 14, 79);
|
||||
-bottom: rgb(64, 7, 39);
|
||||
}
|
||||
|
||||
.block-regtest {
|
||||
-top: rgb(0, 137, 123);
|
||||
-bottom: rgb(0, 96, 100);
|
||||
}
|
||||
|
||||
.block-confirmed .block-unused {
|
||||
-fx-fill: #8c8c98;
|
||||
}
|
||||
|
||||
.block-confirmed .block-top {
|
||||
-fx-fill: #696d7c;
|
||||
}
|
||||
|
||||
.block-confirmed .block-left {
|
||||
-fx-fill: #616475;
|
||||
}
|
||||
|
||||
.block-unconfirmed .block-top {
|
||||
-fx-fill: #807976;
|
||||
}
|
||||
|
||||
.block-unconfirmed .block-left {
|
||||
-fx-fill: #6f6a69;
|
||||
}
|
||||
|
||||
.block-unconfirmed .block-unused {
|
||||
-fx-opacity: 70%;
|
||||
}
|
||||
|
||||
.block-height {
|
||||
-fx-fill: derive(-fx-text-background-color, 40%);
|
||||
}
|
||||
|
||||
.block-text {
|
||||
-fx-fill: #ffffff;
|
||||
}
|
||||
|
||||
.blocks-separator {
|
||||
-fx-stroke: -fx-text-background-color;
|
||||
}
|
||||
|
||||
.vsizeChart {
|
||||
VSIZE1-2_COLOR: rgb(216, 27, 96);
|
||||
VSIZE2-3_COLOR: rgb(142, 36, 170);
|
||||
|
|
@ -164,3 +224,45 @@
|
|||
VSIZE600-700_COLOR: rgb(51, 105, 30);
|
||||
VSIZE700-800_COLOR: rgb(130, 119, 23);
|
||||
}
|
||||
|
||||
.block-cube {
|
||||
VSIZE1-2_COLOR: #557d00;
|
||||
VSIZE2-3_COLOR: #5d7d01;
|
||||
VSIZE3-4_COLOR: #637d02;
|
||||
VSIZE4-5_COLOR: #6d7d04;
|
||||
VSIZE5-6_COLOR: #757d05;
|
||||
VSIZE6-8_COLOR: #7d7d06;
|
||||
VSIZE8-10_COLOR: #867d08;
|
||||
VSIZE10-12_COLOR: #8c7d09;
|
||||
VSIZE12-15_COLOR: #957d0b;
|
||||
VSIZE15-20_COLOR: #9b7d0c;
|
||||
VSIZE20-30_COLOR: #a67d0e;
|
||||
VSIZE30-40_COLOR: #aa7d0f;
|
||||
VSIZE40-50_COLOR: #b27d10;
|
||||
VSIZE50-60_COLOR: #bb7d11;
|
||||
VSIZE60-70_COLOR: #bf7d12;
|
||||
VSIZE70-80_COLOR: #bf7815;
|
||||
VSIZE80-90_COLOR: #bf7319;
|
||||
VSIZE90-100_COLOR: #be6c1e;
|
||||
VSIZE100-125_COLOR: #be6820;
|
||||
VSIZE125-150_COLOR: #bd6125;
|
||||
VSIZE150-175_COLOR: #bd5c28;
|
||||
VSIZE175-200_COLOR: #bc552d;
|
||||
VSIZE200-250_COLOR: #bc4f30;
|
||||
VSIZE250-300_COLOR: #bc4a34;
|
||||
VSIZE300-350_COLOR: #bb4339;
|
||||
VSIZE350-400_COLOR: #bb3d3c;
|
||||
VSIZE400-500_COLOR: #bb373f;
|
||||
VSIZE500-600_COLOR: #ba3243;
|
||||
VSIZE600-700_COLOR: #b92b48;
|
||||
VSIZE700-800_COLOR: #b9254b;
|
||||
VSIZE800-900_COLOR: #b8214d;
|
||||
VSIZE900-1000_COLOR: #b71d4f;
|
||||
VSIZE1000-1200_COLOR: #b61951;
|
||||
VSIZE1200-1400_COLOR: #b41453;
|
||||
VSIZE1400-1600_COLOR: #b30e55;
|
||||
VSIZE1600-1800_COLOR: #b10857;
|
||||
VSIZE1800-2000_COLOR: #b00259;
|
||||
VSIZE2000-2200_COLOR: #ae005b;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
<?import com.sparrowwallet.sparrow.wallet.OptimizationStrategy?>
|
||||
<?import com.sparrowwallet.sparrow.control.HelpLabel?>
|
||||
<?import com.sparrowwallet.sparrow.control.FeeRangeSlider?>
|
||||
<?import com.sparrowwallet.sparrow.control.RecentBlocksView?>
|
||||
|
||||
<BorderPane stylesheets="@send.css, @wallet.css, @../script.css, @../general.css" styleClass="wallet-pane" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.wallet.SendController">
|
||||
<center>
|
||||
|
|
@ -80,6 +81,14 @@
|
|||
<FeeRatesSelection fx:constant="MEMPOOL_SIZE"/>
|
||||
</userData>
|
||||
</ToggleButton>
|
||||
<ToggleButton fx:id="recentBlocksToggle" text="Recent Blocks" toggleGroup="$feeSelectionToggleGroup">
|
||||
<tooltip>
|
||||
<Tooltip text="Show recent and upcoming blocks"/>
|
||||
</tooltip>
|
||||
<userData>
|
||||
<FeeRatesSelection fx:constant="RECENT_BLOCKS"/>
|
||||
</userData>
|
||||
</ToggleButton>
|
||||
</buttons>
|
||||
</SegmentedButton>
|
||||
</HBox>
|
||||
|
|
@ -140,6 +149,7 @@
|
|||
<NumberAxis side="LEFT" />
|
||||
</yAxis>
|
||||
</MempoolSizeFeeRatesChart>
|
||||
<RecentBlocksView fx:id="recentBlocksView" styleClass="feeRatesChart" AnchorPane.topAnchor="10" AnchorPane.leftAnchor="74" translateY="30" minHeight="135"/>
|
||||
</AnchorPane>
|
||||
</GridPane>
|
||||
<AnchorPane>
|
||||
|
|
|
|||
Loading…
Reference in a new issue