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
|
- name: Package tar distribution
|
||||||
if: ${{ runner.os == 'Linux' }}
|
if: ${{ runner.os == 'Linux' }}
|
||||||
run: ./gradlew packageTarDistribution
|
run: ./gradlew packageTarDistribution
|
||||||
- name: Upload Artifacts
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: Sparrow Build - ${{ runner.os }} ${{ runner.arch }}
|
name: Sparrow Build - ${{ runner.os }} ${{ runner.arch }}
|
||||||
|
|
@ -43,9 +43,6 @@ jobs:
|
||||||
- name: Package headless tar distribution
|
- name: Package headless tar distribution
|
||||||
if: ${{ runner.os == 'Linux' }}
|
if: ${{ runner.os == 'Linux' }}
|
||||||
run: ./gradlew -Djava.awt.headless=true packageTarDistribution
|
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
|
- name: Upload Headless Artifact
|
||||||
if: ${{ runner.os == 'Linux' }}
|
if: ${{ runner.os == 'Linux' }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|
|
||||||
62
build.gradle
62
build.gradle
|
|
@ -3,10 +3,9 @@ plugins {
|
||||||
id 'org-openjfx-javafxplugin'
|
id 'org-openjfx-javafxplugin'
|
||||||
id 'org.beryx.jlink' version '3.1.1'
|
id 'org.beryx.jlink' version '3.1.1'
|
||||||
id 'org.gradlex.extra-java-module-info' version '1.9'
|
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 os = org.gradle.internal.os.OperatingSystem.current()
|
||||||
def osName = os.getFamilyName()
|
def osName = os.getFamilyName()
|
||||||
if(os.macOsX) {
|
if(os.macOsX) {
|
||||||
|
|
@ -20,8 +19,8 @@ if(System.getProperty("os.arch") == "aarch64") {
|
||||||
}
|
}
|
||||||
def headless = "true".equals(System.getProperty("java.awt.headless"))
|
def headless = "true".equals(System.getProperty("java.awt.headless"))
|
||||||
|
|
||||||
group "com.sparrowwallet"
|
group 'com.sparrowwallet'
|
||||||
version "${sparrowVersion}"
|
version '2.1.4'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
|
@ -77,7 +76,7 @@ dependencies {
|
||||||
implementation('co.nstant.in:cbor:0.9')
|
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-5')
|
||||||
implementation("io.matthewnelson.kmp-tor:runtime:2.2.1")
|
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') {
|
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.10.1') {
|
||||||
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
|
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
|
||||||
}
|
}
|
||||||
|
|
@ -239,7 +238,7 @@ jlink {
|
||||||
jpackage {
|
jpackage {
|
||||||
imageName = "Sparrow"
|
imageName = "Sparrow"
|
||||||
installerName = "Sparrow"
|
installerName = "Sparrow"
|
||||||
appVersion = "${sparrowVersion}"
|
appVersion = "${version}"
|
||||||
skipInstaller = os.macOsX || properties.skipInstallers
|
skipInstaller = os.macOsX || properties.skipInstallers
|
||||||
imageOptions = []
|
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']
|
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(os.linux) {
|
||||||
if(headless) {
|
if(headless) {
|
||||||
installerOptions = ['--license-file', 'LICENSE', '--resource-dir', "src/main/deploy/package/linux-headless/${osArch}"]
|
installerName = "sparrowserver"
|
||||||
|
installerOptions = ['--license-file', 'LICENSE']
|
||||||
} else {
|
} 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/']
|
imageOptions += ['--icon', 'src/main/deploy/package/linux/Sparrow.png', '--resource-dir', 'src/main/deploy/package/linux/']
|
||||||
}
|
}
|
||||||
if(os.macOsX) {
|
if(os.macOsX) {
|
||||||
|
|
@ -272,6 +273,7 @@ jlink {
|
||||||
|
|
||||||
if(os.linux) {
|
if(os.linux) {
|
||||||
tasks.jlink.finalizedBy('addUserWritePermission', 'copyUdevRules')
|
tasks.jlink.finalizedBy('addUserWritePermission', 'copyUdevRules')
|
||||||
|
tasks.jpackageImage.finalizedBy('prepareResourceDir')
|
||||||
} else {
|
} else {
|
||||||
tasks.jlink.finalizedBy('addUserWritePermission')
|
tasks.jlink.finalizedBy('addUserWritePermission')
|
||||||
}
|
}
|
||||||
|
|
@ -290,12 +292,42 @@ tasks.register('copyUdevRules', Copy) {
|
||||||
include('*')
|
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) {
|
tasks.register('removeGroupWritePermission', Exec) {
|
||||||
commandLine 'chmod', '-R', 'g-w', "$buildDir/jpackage/Sparrow"
|
commandLine 'chmod', '-R', 'g-w', "$buildDir/jpackage/Sparrow"
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register('packageZipDistribution', Zip) {
|
tasks.register('packageZipDistribution', Zip) {
|
||||||
archiveFileName = "Sparrow-${sparrowVersion}.zip"
|
archiveFileName = "Sparrow-${version}.zip"
|
||||||
destinationDirectory = file("$buildDir/jpackage")
|
destinationDirectory = file("$buildDir/jpackage")
|
||||||
preserveFileTimestamps = os.macOsX
|
preserveFileTimestamps = os.macOsX
|
||||||
from("$buildDir/jpackage/") {
|
from("$buildDir/jpackage/") {
|
||||||
|
|
@ -306,7 +338,7 @@ tasks.register('packageZipDistribution', Zip) {
|
||||||
|
|
||||||
tasks.register('packageTarDistribution', Tar) {
|
tasks.register('packageTarDistribution', Tar) {
|
||||||
dependsOn removeGroupWritePermission
|
dependsOn removeGroupWritePermission
|
||||||
archiveFileName = "sparrow-${sparrowVersion}-${releaseArch}.tar.gz"
|
archiveFileName = "sparrow${headless ? 'server': 'wallet'}-${version}-${releaseArch}.tar.gz"
|
||||||
destinationDirectory = file("$buildDir/jpackage")
|
destinationDirectory = file("$buildDir/jpackage")
|
||||||
compression = Compression.GZIP
|
compression = Compression.GZIP
|
||||||
from("$buildDir/jpackage/") {
|
from("$buildDir/jpackage/") {
|
||||||
|
|
@ -460,10 +492,6 @@ extraJavaModuleInfo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String torOs = os.macOsX ? "macos" : (os.windows ? "mingw" : "linux-libc")
|
kmpTorResourceFilterJar {
|
||||||
filterInfo {
|
keepTorCompilation("current","current")
|
||||||
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/')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -20,9 +20,5 @@ gradlePlugin {
|
||||||
id = "org-openjfx-javafxplugin"
|
id = "org-openjfx-javafxplugin"
|
||||||
implementationClass = "org.openjfx.gradle.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]
|
[Desktop Entry]
|
||||||
Name=Sparrow
|
Name=Sparrow
|
||||||
Comment=Sparrow
|
Comment=Sparrow
|
||||||
Exec=/opt/sparrow/bin/Sparrow %U
|
Exec=/opt/sparrowwallet/bin/Sparrow %U
|
||||||
Icon=/opt/sparrow/lib/Sparrow.png
|
Icon=/opt/sparrowwallet/lib/Sparrow.png
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Type=Application
|
Type=Application
|
||||||
Categories=Finance;Network;
|
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
|
#!/bin/sh
|
||||||
# postinst script for sparrow
|
# postinst script for sparrowwallet
|
||||||
#
|
#
|
||||||
# see: dh_installdeb(1)
|
# see: dh_installdeb(1)
|
||||||
|
|
||||||
|
|
@ -22,9 +22,9 @@ package_type=deb
|
||||||
|
|
||||||
case "$1" in
|
case "$1" in
|
||||||
configure)
|
configure)
|
||||||
xdg-desktop-menu install /opt/sparrow/lib/sparrow-Sparrow.desktop
|
xdg-desktop-menu install /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop
|
||||||
xdg-mime install /opt/sparrow/lib/sparrow-Sparrow-MimeInfo.xml
|
xdg-mime install /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml
|
||||||
install -D -m 644 /opt/sparrow/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
|
install -D -m 644 /opt/sparrowwallet/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
|
||||||
if ! getent group plugdev > /dev/null; then
|
if ! getent group plugdev > /dev/null; then
|
||||||
groupadd plugdev
|
groupadd plugdev
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,20 @@
|
||||||
Summary: Sparrow
|
Summary: Sparrow
|
||||||
Name: sparrow
|
Name: sparrowwallet
|
||||||
Version: 2.1.4
|
Version: ${version}
|
||||||
Release: 1
|
Release: 1
|
||||||
License: ASL 2.0
|
License: ASL 2.0
|
||||||
Vendor: Unknown
|
Vendor: Unknown
|
||||||
|
|
||||||
%if "x" != "x"
|
%if "x" != "x"
|
||||||
URL:
|
URL: https://sparrowwallet.com
|
||||||
%endif
|
%endif
|
||||||
|
|
||||||
%if "x/opt" != "x"
|
%if "x/opt" != "x"
|
||||||
Prefix: /opt
|
Prefix: /opt
|
||||||
%endif
|
%endif
|
||||||
|
|
||||||
Provides: sparrow
|
Provides: sparrowwallet
|
||||||
|
Obsoletes: sparrow <= 2.1.4
|
||||||
|
|
||||||
%if "xutils" != "x"
|
%if "xutils" != "x"
|
||||||
Group: utils
|
Group: utils
|
||||||
|
|
@ -50,8 +51,8 @@ Sparrow Wallet
|
||||||
|
|
||||||
%install
|
%install
|
||||||
rm -rf %{buildroot}
|
rm -rf %{buildroot}
|
||||||
install -d -m 755 %{buildroot}/opt/sparrow
|
install -d -m 755 %{buildroot}/opt/sparrowwallet
|
||||||
cp -r %{_sourcedir}/opt/sparrow/* %{buildroot}/opt/sparrow
|
cp -r %{_sourcedir}/opt/sparrowwallet/* %{buildroot}/opt/sparrowwallet
|
||||||
if [ "$(echo %{_sourcedir}/lib/systemd/system/*.service)" != '%{_sourcedir}/lib/systemd/system/*.service' ]; then
|
if [ "$(echo %{_sourcedir}/lib/systemd/system/*.service)" != '%{_sourcedir}/lib/systemd/system/*.service' ]; then
|
||||||
install -d -m 755 %{buildroot}/lib/systemd/system
|
install -d -m 755 %{buildroot}/lib/systemd/system
|
||||||
cp %{_sourcedir}/lib/systemd/system/*.service %{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
|
%post
|
||||||
package_type=rpm
|
package_type=rpm
|
||||||
xdg-desktop-menu install /opt/sparrow/lib/sparrow-Sparrow.desktop
|
xdg-desktop-menu install /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop
|
||||||
xdg-mime install /opt/sparrow/lib/sparrow-Sparrow-MimeInfo.xml
|
xdg-mime install /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml
|
||||||
install -D -m 644 /opt/sparrow/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
|
install -D -m 644 /opt/sparrowwallet/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
|
||||||
if ! getent group plugdev > /dev/null; then
|
if ! getent group plugdev > /dev/null; then
|
||||||
groupadd plugdev
|
groupadd plugdev
|
||||||
fi
|
fi
|
||||||
|
|
@ -251,9 +252,9 @@ desktop_trace ()
|
||||||
echo "$@"
|
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/sparrowwallet/lib/sparrowwallet-Sparrow.desktop xdg-desktop-menu uninstall /opt/sparrowwallet/lib/sparrowwallet-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/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml xdg-mime uninstall /opt/sparrowwallet/lib/sparrowwallet-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 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
|
%clean
|
||||||
|
|
@ -612,16 +612,16 @@ public class AppController implements Initializable {
|
||||||
|
|
||||||
public void installUdevRules(ActionEvent event) {
|
public void installUdevRules(ActionEvent event) {
|
||||||
String commands = """
|
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 control --reload
|
||||||
sudo udevadm trigger
|
sudo udevadm trigger
|
||||||
sudo groupadd -f plugdev
|
sudo groupadd -f plugdev
|
||||||
sudo usermod -aG plugdev `whoami`
|
sudo usermod -aG plugdev `whoami`
|
||||||
""";
|
""";
|
||||||
String home = System.getProperty(JPACKAGE_APP_PATH);
|
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", "");
|
home = home.replace("bin/Sparrow", "");
|
||||||
commands = commands.replace("/opt/sparrow/", home);
|
commands = commands.replace("/opt/sparrowwallet/", home);
|
||||||
}
|
}
|
||||||
|
|
||||||
TextAreaDialog dialog = new TextAreaDialog(commands, false);
|
TextAreaDialog dialog = new TextAreaDialog(commands, false);
|
||||||
|
|
|
||||||
|
|
@ -305,12 +305,6 @@ public class AppServices {
|
||||||
if(event != null) {
|
if(event != null) {
|
||||||
EventManager.get().post(event);
|
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 -> {
|
connectionService.setOnFailed(failEvent -> {
|
||||||
//Close connection here to create a new transport next time we try
|
//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) {
|
private void fetchBlockSummaries(List<NewBlockEvent> newBlockEvents) {
|
||||||
if(isConnected()) {
|
if(isConnected()) {
|
||||||
ElectrumServer.BlockSummaryService blockSummaryService = new ElectrumServer.BlockSummaryService(newBlockEvents);
|
ElectrumServer.BlockSummaryService blockSummaryService = new ElectrumServer.BlockSummaryService(newBlockEvents);
|
||||||
|
|
@ -1216,6 +1217,12 @@ public class AppServices {
|
||||||
latestBlockHeader = event.getBlockHeader();
|
latestBlockHeader = event.getBlockHeader();
|
||||||
Config.get().addRecentServer();
|
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)) {
|
if(!blockSummaries.containsKey(currentBlockHeight)) {
|
||||||
fetchBlockSummaries(Collections.emptyList());
|
fetchBlockSummaries(Collections.emptyList());
|
||||||
}
|
}
|
||||||
|
|
@ -1259,10 +1266,8 @@ public class AppServices {
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void feeRateSourceChanged(FeeRatesSourceChangedEvent event) {
|
public void feeRateSourceChanged(FeeRatesSourceChangedEvent event) {
|
||||||
//Perform once-off fee rates retrieval to immediately change displayed rates
|
//Perform once-off fee rates retrieval to immediately change displayed rates
|
||||||
if(feeRatesService != null && !feeRatesService.isRunning() && Config.get().getMode() != Mode.OFFLINE) {
|
fetchFeeRates();
|
||||||
feeRatesService = createFeeRatesService();
|
fetchBlockSummaries(Collections.emptyList());
|
||||||
feeRatesService.start();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
|
|
|
||||||
|
|
@ -5,21 +5,23 @@ import java.time.temporal.ChronoUnit;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public class BlockSummary {
|
public class BlockSummary implements Comparable<BlockSummary> {
|
||||||
private final Integer height;
|
private final Integer height;
|
||||||
private final Date timestamp;
|
private final Date timestamp;
|
||||||
private final Double medianFee;
|
private final Double medianFee;
|
||||||
private final Integer transactionCount;
|
private final Integer transactionCount;
|
||||||
|
private final Integer weight;
|
||||||
|
|
||||||
public BlockSummary(Integer height, Date timestamp) {
|
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.height = height;
|
||||||
this.timestamp = timestamp;
|
this.timestamp = timestamp;
|
||||||
this.medianFee = medianFee;
|
this.medianFee = medianFee;
|
||||||
this.transactionCount = transactionCount;
|
this.transactionCount = transactionCount;
|
||||||
|
this.weight = weight;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Integer getHeight() {
|
public Integer getHeight() {
|
||||||
|
|
@ -38,6 +40,10 @@ public class BlockSummary {
|
||||||
return transactionCount == null ? Optional.empty() : Optional.of(transactionCount);
|
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) {
|
private static long calculateElapsedSeconds(long timestampUtc) {
|
||||||
Instant timestampInstant = Instant.ofEpochMilli(timestampUtc);
|
Instant timestampInstant = Instant.ofEpochMilli(timestampUtc);
|
||||||
Instant nowInstant = Instant.now();
|
Instant nowInstant = Instant.now();
|
||||||
|
|
@ -62,4 +68,9 @@ public class BlockSummary {
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return getElapsed() + ":" + getMedianFee();
|
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.protocol.TransactionOutput;
|
||||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||||
import com.sparrowwallet.drongo.wallet.*;
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
import com.sparrowwallet.sparrow.UnitFormat;
|
import com.sparrowwallet.sparrow.*;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
|
||||||
import com.sparrowwallet.sparrow.Theme;
|
|
||||||
import com.sparrowwallet.sparrow.event.ExcludeUtxoEvent;
|
import com.sparrowwallet.sparrow.event.ExcludeUtxoEvent;
|
||||||
import com.sparrowwallet.sparrow.event.ReplaceChangeAddressEvent;
|
import com.sparrowwallet.sparrow.event.ReplaceChangeAddressEvent;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
||||||
import com.sparrowwallet.sparrow.io.Config;
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
|
import com.sparrowwallet.sparrow.net.ExchangeSource;
|
||||||
import com.sparrowwallet.sparrow.wallet.OptimizationStrategy;
|
import com.sparrowwallet.sparrow.wallet.OptimizationStrategy;
|
||||||
import javafx.beans.property.BooleanProperty;
|
import javafx.beans.property.BooleanProperty;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
|
@ -229,6 +227,13 @@ public class TransactionDiagram extends GridPane {
|
||||||
getChildren().clear();
|
getChildren().clear();
|
||||||
getChildren().addAll(inputsTypePane, inputsPane, inputsLinesPane, txPane, outputsLinesPane, outputsPane);
|
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) {
|
if(contextMenu == null) {
|
||||||
contextMenu = new ContextMenu();
|
contextMenu = new ContextMenu();
|
||||||
MenuItem menuItem = new MenuItem("Save as Image...");
|
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) {
|
private Pane getOutputsLines(List<Payment> displayedPayments) {
|
||||||
VBox pane = new VBox();
|
VBox pane = new VBox();
|
||||||
Group group = new Group();
|
Group group = new Group();
|
||||||
|
|
@ -839,6 +848,33 @@ public class TransactionDiagram extends GridPane {
|
||||||
return txPane;
|
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() {
|
private void saveAsImage() {
|
||||||
Stage window = new Stage();
|
Stage window = new Stage();
|
||||||
FileChooser fileChooser = new FileChooser();
|
FileChooser fileChooser = new FileChooser();
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import javafx.scene.control.*;
|
||||||
import javafx.scene.image.Image;
|
import javafx.scene.image.Image;
|
||||||
import javafx.scene.image.ImageView;
|
import javafx.scene.image.ImageView;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.Priority;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
@ -103,6 +104,7 @@ public class WalletSummaryDialog extends Dialog<Void> {
|
||||||
vBox.getChildren().add(table);
|
vBox.getChildren().add(table);
|
||||||
|
|
||||||
hBox.getChildren().add(vBox);
|
hBox.getChildren().add(vBox);
|
||||||
|
HBox.setHgrow(vBox, Priority.ALWAYS);
|
||||||
|
|
||||||
Wallet balanceWallet;
|
Wallet balanceWallet;
|
||||||
if(allOpenWallets) {
|
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.Transport;
|
||||||
import com.github.arteam.simplejsonrpc.client.exception.JsonRpcBatchException;
|
import com.github.arteam.simplejsonrpc.client.exception.JsonRpcBatchException;
|
||||||
import com.github.arteam.simplejsonrpc.client.exception.JsonRpcException;
|
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.protocol.Sha256Hash;
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
|
|
@ -162,12 +161,12 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
|
||||||
try {
|
try {
|
||||||
return batchRequest.execute();
|
return batchRequest.execute();
|
||||||
} catch(JsonRpcBatchException e) {
|
} 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));
|
Map<String, Boolean> unsubscribedScriptHashes = scriptHashes.stream().collect(Collectors.toMap(s -> s, _ -> true));
|
||||||
unsubscribedScriptHashes.keySet().removeIf(scriptHash -> e.getErrors().containsKey(scriptHash));
|
unsubscribedScriptHashes.keySet().removeIf(scriptHash -> e.getErrors().containsKey(scriptHash));
|
||||||
return unsubscribedScriptHashes;
|
return unsubscribedScriptHashes;
|
||||||
} catch(Exception e) {
|
} 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();
|
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
|
@Override
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public Map<String, String> getTransactions(Transport transport, Wallet wallet, Set<String> txids) {
|
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 Server coreElectrumServer;
|
||||||
|
|
||||||
|
private static ServerCapability serverCapability;
|
||||||
|
|
||||||
private static final Pattern RPC_WALLET_LOADING_PATTERN = Pattern.compile(".*\"(Wallet loading failed[:.][^\"]*)\".*");
|
private static final Pattern RPC_WALLET_LOADING_PATTERN = Pattern.compile(".*\"(Wallet loading failed[:.][^\"]*)\".*");
|
||||||
|
|
||||||
private static synchronized CloseableTransport getTransport() throws ServerException {
|
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 {
|
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 = Config.get().getFeeRatesSource();
|
||||||
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
|
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
|
||||||
|
|
||||||
|
|
@ -1010,7 +1027,7 @@ public class ElectrumServer {
|
||||||
if(current == null) {
|
if(current == null) {
|
||||||
return Collections.emptyMap();
|
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());
|
.mapToObj(i -> new BlockTransaction(null, i, null, null, null)).collect(Collectors.toSet());
|
||||||
Map<Integer, BlockHeader> blockHeaders = getBlockHeaders(null, references);
|
Map<Integer, BlockHeader> blockHeaders = getBlockHeaders(null, references);
|
||||||
return blockHeaders.keySet().stream()
|
return blockHeaders.keySet().stream()
|
||||||
|
|
@ -1219,7 +1236,7 @@ public class ElectrumServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
if(server.startsWith("cormorant")) {
|
if(server.startsWith("cormorant")) {
|
||||||
return new ServerCapability(true);
|
return new ServerCapability(true, false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(server.startsWith("electrs/")) {
|
if(server.startsWith("electrs/")) {
|
||||||
|
|
@ -1405,7 +1422,7 @@ public class ElectrumServer {
|
||||||
firstCall = false;
|
firstCall = false;
|
||||||
|
|
||||||
//If electrumx is detected, we can upgrade to batched RPC. Electrs/EPS do not support batching.
|
//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()) {
|
if(serverCapability.supportsBatching()) {
|
||||||
log.debug("Upgrading to batched JSON-RPC");
|
log.debug("Upgrading to batched JSON-RPC");
|
||||||
electrumServerRpc = new BatchedElectrumServerRpc(electrumServerRpc.getIdCounterValue(), serverCapability.getMaxTargetBlocks());
|
electrumServerRpc = new BatchedElectrumServerRpc(electrumServerRpc.getIdCounterValue(), serverCapability.getMaxTargetBlocks());
|
||||||
|
|
@ -1945,13 +1962,18 @@ public class ElectrumServer {
|
||||||
|
|
||||||
if(startHeight == 0 || totalBlocks > 1 || startHeight > maxHeight + 1) {
|
if(startHeight == 0 || totalBlocks > 1 || startHeight > maxHeight + 1) {
|
||||||
if(isBlockstorm(totalBlocks)) {
|
if(isBlockstorm(totalBlocks)) {
|
||||||
for(int height = maxHeight + 1; height < endHeight; height++) {
|
int start = Math.max(maxHeight + 1, endHeight - 15);
|
||||||
blockSummaryMap.put(height, new BlockSummary(height, new Date()));
|
for(int height = start; height <= endHeight; height++) {
|
||||||
|
blockSummaryMap.put(height, new BlockSummary(height, new Date(), 1.0d, 0, 0));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
blockSummaryMap.putAll(electrumServer.getRecentBlockSummaryMap());
|
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) {
|
for(NewBlockEvent event : newBlockEvents) {
|
||||||
blockSummaryMap.putAll(electrumServer.getBlockSummaryMap(event.getHeight(), event.getBlockHeader()));
|
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, 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, String> getTransactions(Transport transport, Wallet wallet, Set<String> txids);
|
||||||
|
|
||||||
Map<String, VerboseTransaction> getVerboseTransactions(Transport transport, Set<String> txids, String scriptHash);
|
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() {
|
public Double getMedianFee() {
|
||||||
return extras == null ? null : extras.medianFee();
|
return extras == null ? null : extras.medianFee();
|
||||||
}
|
}
|
||||||
|
|
@ -294,7 +294,7 @@ public enum FeeRatesSource {
|
||||||
if(height == null || timestamp == null) {
|
if(height == null || timestamp == null) {
|
||||||
throw new IllegalStateException("Height = " + height + ", timestamp = " + timestamp + ": both must be specified");
|
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.BlockTransaction;
|
||||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
|
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
|
||||||
|
|
||||||
class ScriptHashTx {
|
public class ScriptHashTx {
|
||||||
public static final ScriptHashTx ERROR_TX = new ScriptHashTx() {
|
public static final ScriptHashTx ERROR_TX = new ScriptHashTx() {
|
||||||
@Override
|
@Override
|
||||||
public BlockTransactionHash getBlockchainTransactionHash() {
|
public BlockTransactionHash getBlockchainTransactionHash() {
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,29 @@ import com.sparrowwallet.sparrow.AppServices;
|
||||||
public class ServerCapability {
|
public class ServerCapability {
|
||||||
private final boolean supportsBatching;
|
private final boolean supportsBatching;
|
||||||
private final int maxTargetBlocks;
|
private final int maxTargetBlocks;
|
||||||
|
private final boolean supportsRecentMempool;
|
||||||
|
private final boolean supportsBlockStats;
|
||||||
|
|
||||||
public ServerCapability(boolean supportsBatching) {
|
public ServerCapability(boolean supportsBatching) {
|
||||||
this.supportsBatching = supportsBatching;
|
this(supportsBatching, AppServices.TARGET_BLOCKS_RANGE.getLast());
|
||||||
this.maxTargetBlocks = AppServices.TARGET_BLOCKS_RANGE.getLast();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServerCapability(boolean supportsBatching, int maxTargetBlocks) {
|
public ServerCapability(boolean supportsBatching, int maxTargetBlocks) {
|
||||||
this.supportsBatching = supportsBatching;
|
this.supportsBatching = supportsBatching;
|
||||||
this.maxTargetBlocks = maxTargetBlocks;
|
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() {
|
public boolean supportsBatching() {
|
||||||
|
|
@ -23,4 +37,12 @@ public class ServerCapability {
|
||||||
public int getMaxTargetBlocks() {
|
public int getMaxTargetBlocks() {
|
||||||
return maxTargetBlocks;
|
return maxTargetBlocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean supportsRecentMempool() {
|
||||||
|
return supportsRecentMempool;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean supportsBlockStats() {
|
||||||
|
return supportsBlockStats;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,29 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
|
||||||
return result;
|
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
|
@Override
|
||||||
public Map<String, String> getTransactions(Transport transport, Wallet wallet, Set<String> txids) {
|
public Map<String, String> getTransactions(Transport transport, Wallet wallet, Set<String> txids) {
|
||||||
JsonRpcClient client = new JsonRpcClient(transport);
|
JsonRpcClient client = new JsonRpcClient(transport);
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import com.sparrowwallet.sparrow.AppServices;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
class VerboseTransaction {
|
public class VerboseTransaction {
|
||||||
public String blockhash;
|
public String blockhash;
|
||||||
public long blocktime;
|
public long blocktime;
|
||||||
public int confirmations;
|
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.JsonRpcParam;
|
||||||
import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcService;
|
import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcService;
|
||||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||||
|
import com.sparrowwallet.sparrow.net.BlockStats;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
@ -48,6 +49,9 @@ public interface BitcoindClientService {
|
||||||
@JsonRpcMethod("getblockheader")
|
@JsonRpcMethod("getblockheader")
|
||||||
VerboseBlockHeader getBlockHeader(@JsonRpcParam("blockhash") String blockhash);
|
VerboseBlockHeader getBlockHeader(@JsonRpcParam("blockhash") String blockhash);
|
||||||
|
|
||||||
|
@JsonRpcMethod("getblockstats")
|
||||||
|
BlockStats getBlockStats(@JsonRpcParam("blockhash") int hash_or_height);
|
||||||
|
|
||||||
@JsonRpcMethod("getrawtransaction")
|
@JsonRpcMethod("getrawtransaction")
|
||||||
Object getRawTransaction(@JsonRpcParam("txid") String txid, @JsonRpcParam("verbose") boolean verbose);
|
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.SparrowWallet;
|
||||||
import com.sparrowwallet.sparrow.event.MempoolEntriesInitializedEvent;
|
import com.sparrowwallet.sparrow.event.MempoolEntriesInitializedEvent;
|
||||||
import com.sparrowwallet.drongo.Version;
|
import com.sparrowwallet.drongo.Version;
|
||||||
|
import com.sparrowwallet.sparrow.net.BlockStats;
|
||||||
import com.sparrowwallet.sparrow.net.cormorant.Cormorant;
|
import com.sparrowwallet.sparrow.net.cormorant.Cormorant;
|
||||||
import com.sparrowwallet.sparrow.net.cormorant.bitcoind.*;
|
import com.sparrowwallet.sparrow.net.cormorant.bitcoind.*;
|
||||||
import com.sparrowwallet.sparrow.net.cormorant.index.TxEntry;
|
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")
|
@JsonRpcMethod("blockchain.transaction.get")
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public Object getTransaction(@JsonRpcParam("tx_hash") String tx_hash, @JsonRpcParam("verbose") @JsonRpcOptional boolean verbose) throws BitcoindIOException, TransactionNotFoundException {
|
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;
|
package com.sparrowwallet.sparrow.wallet;
|
||||||
|
|
||||||
public enum FeeRatesSelection {
|
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;
|
private final String name;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,7 @@ import com.sparrowwallet.drongo.crypto.ECKey;
|
||||||
import com.sparrowwallet.drongo.protocol.*;
|
import com.sparrowwallet.drongo.protocol.*;
|
||||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||||
import com.sparrowwallet.drongo.wallet.*;
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
import com.sparrowwallet.sparrow.UnitFormat;
|
import com.sparrowwallet.sparrow.*;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
|
||||||
import com.sparrowwallet.sparrow.CurrencyRate;
|
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
|
||||||
import com.sparrowwallet.sparrow.control.*;
|
import com.sparrowwallet.sparrow.control.*;
|
||||||
import com.sparrowwallet.sparrow.event.*;
|
import com.sparrowwallet.sparrow.event.*;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
|
|
@ -28,6 +25,7 @@ import com.sparrowwallet.sparrow.paynym.PayNymService;
|
||||||
import javafx.animation.KeyFrame;
|
import javafx.animation.KeyFrame;
|
||||||
import javafx.animation.Timeline;
|
import javafx.animation.Timeline;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.property.*;
|
import javafx.beans.property.*;
|
||||||
import javafx.beans.value.ChangeListener;
|
import javafx.beans.value.ChangeListener;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
|
|
@ -78,6 +76,9 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
@FXML
|
@FXML
|
||||||
private ToggleButton mempoolSizeToggle;
|
private ToggleButton mempoolSizeToggle;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ToggleButton recentBlocksToggle;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private Field targetBlocksField;
|
private Field targetBlocksField;
|
||||||
|
|
||||||
|
|
@ -117,6 +118,9 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
@FXML
|
@FXML
|
||||||
private MempoolSizeFeeRatesChart mempoolSizeFeeRatesChart;
|
private MempoolSizeFeeRatesChart mempoolSizeFeeRatesChart;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private RecentBlocksView recentBlocksView;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private TransactionDiagram transactionDiagram;
|
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<BlockTransaction> replacedTransactionProperty = new SimpleObjectProperty<>(null);
|
||||||
|
|
||||||
|
private final ObjectProperty<FeeRatesSelection> feeRatesSelectionProperty = new SimpleObjectProperty<>(null);
|
||||||
|
|
||||||
private final List<byte[]> opReturnsList = new ArrayList<>();
|
private final List<byte[]> opReturnsList = new ArrayList<>();
|
||||||
|
|
||||||
private final Set<WalletNode> excludedChangeNodes = new HashSet<>();
|
private final Set<WalletNode> excludedChangeNodes = new HashSet<>();
|
||||||
|
|
@ -299,6 +305,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
feeRange.valueProperty().addListener(feeRangeListener);
|
feeRange.valueProperty().addListener(feeRangeListener);
|
||||||
|
|
||||||
blockTargetFeeRatesChart.managedProperty().bind(blockTargetFeeRatesChart.visibleProperty());
|
blockTargetFeeRatesChart.managedProperty().bind(blockTargetFeeRatesChart.visibleProperty());
|
||||||
|
blockTargetFeeRatesChart.visibleProperty().bind(Bindings.equal(feeRatesSelectionProperty, FeeRatesSelection.BLOCK_TARGET));
|
||||||
blockTargetFeeRatesChart.initialize();
|
blockTargetFeeRatesChart.initialize();
|
||||||
Map<Integer, Double> targetBlocksFeeRates = getTargetBlocksFeeRates();
|
Map<Integer, Double> targetBlocksFeeRates = getTargetBlocksFeeRates();
|
||||||
if(targetBlocksFeeRates != null) {
|
if(targetBlocksFeeRates != null) {
|
||||||
|
|
@ -308,20 +315,41 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
}
|
}
|
||||||
|
|
||||||
mempoolSizeFeeRatesChart.managedProperty().bind(mempoolSizeFeeRatesChart.visibleProperty());
|
mempoolSizeFeeRatesChart.managedProperty().bind(mempoolSizeFeeRatesChart.visibleProperty());
|
||||||
mempoolSizeFeeRatesChart.visibleProperty().bind(blockTargetFeeRatesChart.visibleProperty().not());
|
mempoolSizeFeeRatesChart.visibleProperty().bind(Bindings.equal(feeRatesSelectionProperty, FeeRatesSelection.MEMPOOL_SIZE));
|
||||||
mempoolSizeFeeRatesChart.initialize();
|
mempoolSizeFeeRatesChart.initialize();
|
||||||
Map<Date, Set<MempoolRateSize>> mempoolHistogram = getMempoolHistogram();
|
Map<Date, Set<MempoolRateSize>> mempoolHistogram = getMempoolHistogram();
|
||||||
if(mempoolHistogram != null) {
|
if(mempoolHistogram != null) {
|
||||||
mempoolSizeFeeRatesChart.update(mempoolHistogram);
|
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 = Config.get().getFeeRatesSelection();
|
||||||
feeRatesSelection = (feeRatesSelection == null ? FeeRatesSelection.MEMPOOL_SIZE : feeRatesSelection);
|
feeRatesSelection = (feeRatesSelection == null ? FeeRatesSelection.RECENT_BLOCKS : feeRatesSelection);
|
||||||
cpfpFeeRate.managedProperty().bind(cpfpFeeRate.visibleProperty());
|
cpfpFeeRate.managedProperty().bind(cpfpFeeRate.visibleProperty());
|
||||||
cpfpFeeRate.setVisible(false);
|
cpfpFeeRate.setVisible(false);
|
||||||
setDefaultFeeRate();
|
setDefaultFeeRate();
|
||||||
updateFeeRateSelection(feeRatesSelection);
|
feeRatesSelectionProperty.set(feeRatesSelection);
|
||||||
feeSelectionToggleGroup.selectToggle(feeRatesSelection == FeeRatesSelection.BLOCK_TARGET ? targetBlocksToggle : mempoolSizeToggle);
|
feeSelectionToggleGroup.selectToggle(feeRatesSelection == FeeRatesSelection.BLOCK_TARGET ? targetBlocksToggle :
|
||||||
|
(feeRatesSelection == FeeRatesSelection.MEMPOOL_SIZE ? mempoolSizeToggle : recentBlocksToggle));
|
||||||
feeSelectionToggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
|
feeSelectionToggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
if(newValue != null) {
|
if(newValue != null) {
|
||||||
FeeRatesSelection newFeeRatesSelection = (FeeRatesSelection)newValue.getUserData();
|
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()));
|
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() {
|
private void setDefaultFeeRate() {
|
||||||
int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1);
|
int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1);
|
||||||
int index = TARGET_BLOCKS_RANGE.indexOf(defaultTarget);
|
int index = TARGET_BLOCKS_RANGE.indexOf(defaultTarget);
|
||||||
Double defaultRate = getTargetBlocksFeeRates().get(defaultTarget);
|
Double defaultRate = getTargetBlocksFeeRates().get(defaultTarget);
|
||||||
targetBlocks.setValue(index);
|
targetBlocks.setValue(index);
|
||||||
blockTargetFeeRatesChart.select(defaultTarget);
|
blockTargetFeeRatesChart.select(defaultTarget);
|
||||||
|
recentBlocksView.updateFeeRate(defaultRate);
|
||||||
setFeeRangeRate(defaultRate);
|
setFeeRangeRate(defaultRate);
|
||||||
setFeeRate(getFeeRangeRate());
|
setFeeRate(getFeeRangeRate());
|
||||||
if(Network.get().equals(Network.MAINNET) && defaultRate == getFallbackFeeRate()) {
|
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) {
|
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);
|
fiatFeeAmount.set(currencyRate, amount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1411,10 +1428,15 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void feeRateSelectionChanged(FeeRatesSelectionChangedEvent event) {
|
public void feeRateSelectionChanged(FeeRatesSelectionChangedEvent event) {
|
||||||
if(event.getWallet() == getWalletForm().getWallet()) {
|
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
|
@Subscribe
|
||||||
public void spendUtxos(SpendUtxoEvent event) {
|
public void spendUtxos(SpendUtxoEvent event) {
|
||||||
if((event.getUtxos() == null || !event.getUtxos().isEmpty()) && event.getWallet().equals(getWalletForm().getWallet())) {
|
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) {
|
if(event.getExchangeSource() == ExchangeSource.NONE) {
|
||||||
fiatFeeAmount.setCurrency(null);
|
fiatFeeAmount.setCurrency(null);
|
||||||
fiatFeeAmount.setBtcRate(0.0);
|
fiatFeeAmount.setBtcRate(0.0);
|
||||||
|
if(paymentTabs.getTabs().size() > 1) {
|
||||||
|
updateTransaction();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void exchangeRatesUpdated(ExchangeRatesUpdatedEvent event) {
|
public void exchangeRatesUpdated(ExchangeRatesUpdatedEvent event) {
|
||||||
setFiatFeeAmount(event.getCurrencyRate(), getFeeValueSats());
|
setFiatFeeAmount(event.getCurrencyRate(), getFeeValueSats());
|
||||||
|
if(paymentTabs.getTabs().size() > 1) {
|
||||||
|
updateTransaction();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
|
|
@ -1597,7 +1625,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
} else if(payjoinPresent) {
|
} else if(payjoinPresent) {
|
||||||
addLabel("Cannot fake coinjoin due to payjoin", getInfoGlyph());
|
addLabel("Cannot fake coinjoin due to payjoin", getInfoGlyph());
|
||||||
} else {
|
} else {
|
||||||
if(utxoSelectorProperty().get() != null) {
|
if(utxoSelectorProperty().get() != null && !(utxoSelectorProperty().get() instanceof MaxUtxoSelector)) {
|
||||||
addLabel("Cannot fake coinjoin due to coin control", getInfoGlyph());
|
addLabel("Cannot fake coinjoin due to coin control", getInfoGlyph());
|
||||||
} else {
|
} else {
|
||||||
addLabel("Cannot fake coinjoin due to insufficient funds", getInfoGlyph());
|
addLabel("Cannot fake coinjoin due to insufficient funds", getInfoGlyph());
|
||||||
|
|
|
||||||
|
|
@ -343,4 +343,32 @@ HorizontalHeaderColumn > TableColumnHeader.column-header.table-column{
|
||||||
|
|
||||||
#grid .spreadsheet-cell.selection {
|
#grid .spreadsheet-cell.selection {
|
||||||
-fx-text-fill: -fx-base;
|
-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;
|
-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 {
|
.vsizeChart {
|
||||||
VSIZE1-2_COLOR: rgb(216, 27, 96);
|
VSIZE1-2_COLOR: rgb(216, 27, 96);
|
||||||
VSIZE2-3_COLOR: rgb(142, 36, 170);
|
VSIZE2-3_COLOR: rgb(142, 36, 170);
|
||||||
|
|
@ -164,3 +224,45 @@
|
||||||
VSIZE600-700_COLOR: rgb(51, 105, 30);
|
VSIZE600-700_COLOR: rgb(51, 105, 30);
|
||||||
VSIZE700-800_COLOR: rgb(130, 119, 23);
|
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.wallet.OptimizationStrategy?>
|
||||||
<?import com.sparrowwallet.sparrow.control.HelpLabel?>
|
<?import com.sparrowwallet.sparrow.control.HelpLabel?>
|
||||||
<?import com.sparrowwallet.sparrow.control.FeeRangeSlider?>
|
<?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">
|
<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>
|
<center>
|
||||||
|
|
@ -80,6 +81,14 @@
|
||||||
<FeeRatesSelection fx:constant="MEMPOOL_SIZE"/>
|
<FeeRatesSelection fx:constant="MEMPOOL_SIZE"/>
|
||||||
</userData>
|
</userData>
|
||||||
</ToggleButton>
|
</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>
|
</buttons>
|
||||||
</SegmentedButton>
|
</SegmentedButton>
|
||||||
</HBox>
|
</HBox>
|
||||||
|
|
@ -140,6 +149,7 @@
|
||||||
<NumberAxis side="LEFT" />
|
<NumberAxis side="LEFT" />
|
||||||
</yAxis>
|
</yAxis>
|
||||||
</MempoolSizeFeeRatesChart>
|
</MempoolSizeFeeRatesChart>
|
||||||
|
<RecentBlocksView fx:id="recentBlocksView" styleClass="feeRatesChart" AnchorPane.topAnchor="10" AnchorPane.leftAnchor="74" translateY="30" minHeight="135"/>
|
||||||
</AnchorPane>
|
</AnchorPane>
|
||||||
</GridPane>
|
</GridPane>
|
||||||
<AnchorPane>
|
<AnchorPane>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue