From 406122ca7c705e56f7bcafea0e0379d0655c5edf Mon Sep 17 00:00:00 2001 From: tryiou Date: Thu, 11 Dec 2025 23:07:40 +0100 Subject: [PATCH 01/73] Migrate from Gradle to Maven build system - Replace Gradle with Maven for build automation - Update CI/CD workflows to use Maven wrapper (mvnw) - Add Maven wrapper configuration files - Update README with Maven build instructions - Clean up Gradle-specific files (.gradle, .project) - Update .gitignore for Maven artifacts --- .github/workflows/build.yml | 56 +- .gitignore | 35 +- .mvn/wrapper/maven-wrapper.properties | 3 + .project | 34 - README.md | 59 +- build.gradle | 118 - ci/WaitForShutdownTime.ps1 | 12 - ci/windows-build.cmd | 59 - contrib/netty-reflection.json | 98 - contrib/resource-config.json | 12 - gradle.properties | 9 - gradle/wrapper/gradle-wrapper.jar | Bin 59821 -> 0 bytes gradle/wrapper/gradle-wrapper.properties | 6 - gradlew | 234 -- gradlew.bat | 89 - mvnw | 295 ++ mvnw.cmd | 189 + nativeImageWindows.bat | 27 +- pom.xml | 468 +++ src/main/java/io/cloudchains/app/App.java | 242 +- .../io/cloudchains/app/console/ArgMenu.java | 2 +- .../cloudchains/app/console/ConsoleMenu.java | 20 +- .../io/cloudchains/app/crypto/KeyHandler.java | 21 +- .../io/cloudchains/app/crypto/LoginUtils.java | 40 +- .../net/ActiveCoinChangedEventListener.java | 2 +- .../io/cloudchains/app/net/CoinInstance.java | 1794 +++++----- .../io/cloudchains/app/net/CoinTicker.java | 48 +- .../cloudchains/app/net/CoinTickerUtils.java | 110 +- .../app/net/api/JSONRPCController.java | 50 +- .../app/net/api/JSONRPCMasterServer.java | 93 +- .../app/net/api/JSONRPCServer.java | 88 +- .../app/net/api/http/client/EXRServer.java | 75 +- .../net/api/http/client/EXRServerPool.java | 105 +- .../app/net/api/http/client/EXRWrapper.java | 54 +- .../app/net/api/http/client/HTTPClient.java | 116 +- .../api/http/master/HTTPServerHandler.java | 554 ++- .../http/master/HTTPServerInitializer.java | 25 +- .../api/http/server/HTTPServerHandler.java | 3121 ++++++++--------- .../http/server/HTTPServerInitializer.java | 30 +- .../alqocoin/AlqocoinNetworkParameters.java | 183 +- .../bitbay/BitbayNetworkParameters.java | 183 +- .../BitcoinCashNetworkParameters.java | 183 +- .../blocknet/BlocknetBlockingClient.java | 180 +- .../BlocknetBlockingClientManager.java | 3 +- .../blocknet/BlocknetNetworkParameters.java | 320 +- .../blocknet/BlocknetPacketHeader.java | 77 +- .../blocknet/BlocknetParameters.java | 8 +- .../net/protocols/blocknet/BlocknetPeer.java | 1357 +++---- .../protocols/blocknet/BlocknetPeerGroup.java | 11 +- .../blocknet/BlocknetSerializer.java | 394 +-- .../BlocknetTestnet5NetworkParameters.java | 292 +- .../net/protocols/blocknet/BlocknetUtils.java | 84 +- ...ocknetOnBlocksDownloadedEventListener.java | 2 +- ...cknetOnXRouterMessageReceivedListener.java | 2 +- .../BlocknetPeerConnectedEventListener.java | 2 +- ...BlocknetPeerDisconnectedEventListener.java | 2 +- ...ocknetPreMessageReceivedEventListener.java | 2 +- .../dashcoin/DashcoinNetworkParameters.java | 183 +- .../digibyte/DigibyteNetworkParameters.java | 183 +- .../dogecoin/DogecoinNetworkParameters.java | 183 +- .../litecoin/LitecoinNetworkParameters.java | 191 +- .../phorecoin/PhorecoinNetworkParameters.java | 183 +- .../protocols/pivx/PivxNetworkParameters.java | 183 +- .../poliscoin/PoliscoinNetworkParameters.java | 183 +- .../ravencoin/RavencoinNetworkParameters.java | 183 +- .../syscoin/SyscoinNetworkParameters.java | 183 +- .../TrezarcoinNetworkParameters.java | 183 +- .../UnobtaniumNetworkParameters.java | 21 +- .../substitutions/ApacheSubstitutions.java | 32 - .../net/substitutions/NettySubstitutions.java | 18 - .../app/net/xrouter/XRouterCommandUtils.java | 70 +- .../app/net/xrouter/XRouterFeeUtils.java | 136 +- .../XRouterInitialMessagesSentListener.java | 2 +- .../app/net/xrouter/XRouterMessage.java | 842 ++--- .../net/xrouter/XRouterMessageSerializer.java | 230 +- .../app/net/xrouter/XRouterPacketHeader.java | 250 +- .../app/net/xrouter/XRouterPacketManager.java | 388 +- .../cloudchains/app/util/AddressBalance.java | 248 +- .../app/util/AddressDiscoveryService.java | 88 +- .../io/cloudchains/app/util/CCLogger.java | 2 - .../io/cloudchains/app/util/ConfigHelper.java | 456 +-- .../io/cloudchains/app/util/DetectOS.java | 6 +- .../java/io/cloudchains/app/util/UTXO.java | 140 +- .../java/io/cloudchains/app/util/Utility.java | 2 +- .../app/util/XRouterConfiguration.java | 401 ++- .../background/BackgroundTimerThread.java | 246 +- .../app/util/history/Transaction.java | 13 +- .../cloudchains/app/wallet/WalletHelper.java | 418 +-- .../resources/config/netty-reflection.json | 8 + .../resources/config/resource-config.json | 7 + src/main/resources/simplelogger.properties | 6 + src/test/java/TestWallet.java | 2026 +++++------ 92 files changed, 9918 insertions(+), 9654 deletions(-) create mode 100644 .mvn/wrapper/maven-wrapper.properties delete mode 100644 .project delete mode 100644 build.gradle delete mode 100644 ci/WaitForShutdownTime.ps1 delete mode 100644 ci/windows-build.cmd delete mode 100644 contrib/netty-reflection.json delete mode 100644 contrib/resource-config.json delete mode 100644 gradle.properties delete mode 100644 gradle/wrapper/gradle-wrapper.jar delete mode 100644 gradle/wrapper/gradle-wrapper.properties delete mode 100755 gradlew delete mode 100644 gradlew.bat create mode 100755 mvnw create mode 100644 mvnw.cmd create mode 100644 pom.xml delete mode 100644 src/main/java/io/cloudchains/app/net/substitutions/ApacheSubstitutions.java delete mode 100644 src/main/java/io/cloudchains/app/net/substitutions/NettySubstitutions.java create mode 100644 src/main/resources/config/netty-reflection.json create mode 100644 src/main/resources/config/resource-config.json create mode 100644 src/main/resources/simplelogger.properties diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1a9cebd..6754bd9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,7 +24,7 @@ jobs: uses: actions/checkout@v4 if: github.event.pull_request.draft == false - # - name: Setup GraalVM JDK 21 via SDKMAN (FOR LOCAL TESTING WITH ACT) + # - name: Setup GraalVM JDK 21 via SDKMAN (FOR TESTING WITH ACT) # shell: bash # run: | # curl -s "https://get.sdkman.io" | bash @@ -37,41 +37,41 @@ jobs: uses: graalvm/setup-graalvm@v1 with: java-version: '21' - distribution: 'graalvm-community' + distribution: 'graalvm-community' - name: Verify Java installation run: | java -version - native-image --version + native-image --version - - name: Make gradlew executable - run: chmod +x gradlew + - name: Make Maven wrapper executable + run: chmod +x mvnw - name: Build native image - run: ./gradlew nativeCompile --info + run: ./mvnw clean package -Pnative -DskipTests - name: Make daemon executable - run: chmod +x build/native/nativeCompile/xlite-daemon + run: chmod +x target/xlite-daemon - name: Rename executable - run: mv build/native/nativeCompile/xlite-daemon build/native/nativeCompile/xlite-daemon-linux64 + run: mv target/xlite-daemon target/xlite-daemon-linux64 - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: artifacts-linux path: | - build/native/nativeCompile/xlite-daemon-linux64 + target/xlite-daemon-linux64 - name: Create release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 if: startsWith(github.ref, 'refs/tags/v') with: name: XLite Daemon ${{ github.ref_name}} prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') || contains(github.ref_name, 'rc') }} generate_release_notes: true files: | - build/native/nativeCompile/xlite-daemon-linux64 + target/xlite-daemon-linux64 build_mac: runs-on: macos-15-intel @@ -84,41 +84,41 @@ jobs: uses: graalvm/setup-graalvm@v1 with: java-version: '21' - distribution: 'graalvm-community' + distribution: 'graalvm-community' - name: Verify Java installation run: | java -version - native-image --version + native-image --version - - name: Make gradlew executable - run: chmod +x gradlew + - name: Make Maven wrapper executable + run: chmod +x mvnw - name: Build native image - run: ./gradlew nativeCompile --info + run: ./mvnw clean package -Pnative -DskipTests - name: Make daemon executable - run: chmod +x build/native/nativeCompile/xlite-daemon + run: chmod +x target/xlite-daemon - name: Rename executable - run: mv build/native/nativeCompile/xlite-daemon build/native/nativeCompile/xlite-daemon-osx64 + run: mv target/xlite-daemon target/xlite-daemon-osx64 - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: artifacts-mac path: | - build/native/nativeCompile/xlite-daemon-osx64 + target/xlite-daemon-osx64 - name: Create release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 if: startsWith(github.ref, 'refs/tags/v') with: name: XLite Daemon ${{ github.ref_name}} prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') || contains(github.ref_name, 'rc') }} generate_release_notes: true files: | - build/native/nativeCompile/xlite-daemon-osx64 + target/xlite-daemon-osx64 build_win: runs-on: windows-2022 @@ -131,7 +131,7 @@ jobs: uses: graalvm/setup-graalvm@v1 with: java-version: '21' - distribution: 'graalvm-community' + distribution: 'graalvm-community' - name: Verify Java installation run: | @@ -142,27 +142,27 @@ jobs: run: choco install -y windows-sdk-10.0 - name: Install Visual Studio Build Tools & sdk - run: choco install -y visualstudio2022-workload-vctools windows-sdk-10.0 + run: choco install -y visualstudio2022-workload-vctools - name: Build native image - run: ./gradlew.bat nativeCompile --info + run: mvnw.cmd clean package -Pnative -DskipTests - name: Rename executable - run: ren build\native\nativeCompile\xlite-daemon.exe xlite-daemon-win64.exe + run: Rename-Item target/xlite-daemon.exe xlite-daemon-win64.exe - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: artifacts-win path: | - build\native\nativeCompile\xlite-daemon-win64.exe + target\xlite-daemon-win64.exe - name: Create release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 if: startsWith(github.ref, 'refs/tags/v') with: name: XLite Daemon ${{ github.ref_name}} prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') || contains(github.ref_name, 'rc') }} generate_release_notes: true files: | - build\native\nativeCompile\xlite-daemon-win64.exe \ No newline at end of file + target\xlite-daemon-win64.exe \ No newline at end of file diff --git a/.gitignore b/.gitignore index cf1ea04..fd0578a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,41 @@ +# Lock files and build artifacts .lock/ -.gradle/ /bin/ /build/ /builds/ .classpath .history/ .vscode/ -.gradle/ .env .aider* -.idea* \ No newline at end of file +.idea* + +# Maven-specific ignores +target/ +dependency-reduced-pom.xml +*.jar +*.war +*.ear +*.zip +*.tar.gz +*.rar + +# Compiled class files +*.class + +# Log files +logs/ +*.log +log/ + +# IDE output directories +out/ + +# OS files +.DS_Store +Thumbs.db + +# Temporary files +*.tmp +*.temp +*~ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..c0bcafe --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip diff --git a/.project b/.project deleted file mode 100644 index 41a5c27..0000000 --- a/.project +++ /dev/null @@ -1,34 +0,0 @@ - - - cc-daemon - Project cc-daemon created by Buildship. - - - - - org.eclipse.jdt.core.javabuilder - - - - - org.eclipse.buildship.core.gradleprojectbuilder - - - - - - org.eclipse.jdt.core.javanature - org.eclipse.buildship.core.gradleprojectnature - - - - 1684135176474 - - 30 - - org.eclipse.core.resources.regexFilterMatcher - node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ - - - - diff --git a/README.md b/README.md index 720df77..bf1c56b 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ # Xlite Wallet Backend -The Xlite Wallet Backend is a Java-based project that serves as the backend infrastructure for the Xlite wallet application. It is built using Java with JDK version 21 and utilizes Gradle for build automation. The project incorporates the org.bitcoinj library version 0.14.7 for Bitcoin-related functionality. +The Xlite Wallet Backend is a Java-based project that serves as the backend infrastructure for the Xlite wallet application. It is built using Java with JDK version 21 and utilizes Maven for build automation. The project incorporates the org.bitcoinj library version 0.14.7 for Bitcoin-related functionality. ## Table of Contents - [Project Overview](#project-overview) - [Prerequisites](#prerequisites) - [Getting Started](#getting-started) +- [Maven Build Commands](#maven-build-commands) - [Usage](#usage) - [Configuration](#configuration) - [Contributing](#contributing) @@ -21,6 +22,7 @@ Provide a brief description of the Xlite Wallet Backend project. Explain its pur List the prerequisites required to set up and run the Xlite Wallet Backend. Include the following: - JDK 21: Install the Java Development Kit version 21 or a compatible version. +- Maven 3.8.6 or higher: Install Apache Maven for build automation. ## Getting Started @@ -34,18 +36,59 @@ git clone https://github.com/blocknetdx/xlite-daemon ``` cd xlite-daemon -# mac/linux: -chmod +x gradlew -./gradlew nativeImage +# Make Maven wrapper executable (mac/linux): +chmod +x mvnw -# windows: -nativeImageWindows.bat +# Build native image: +./mvnw clean package -Pnative + +# Or for faster build without tests: +./mvnw clean package -Pnative-fast ``` 3. Configuration: If any configuration files or settings need to be modified, provide instructions on how to set them up. -4. Run the application: +4. Run the application: +``` +./target/xlite-daemon ``` -binary to find in build/graal/ folder + +## Maven Build Commands + +### Basic Maven Operations + +```bash +# Clean and compile +mvn clean compile + +# Run tests +mvn test + +# Package JAR (without native compilation) +mvn package -DskipTests + +# Build native image +mvn clean package -Pnative + +# Run application +mvn exec:java + +# Skip tests for faster builds +mvn clean package -Pnative-fast +``` + +### Profile-Specific Commands + +- **native**: Full native image compilation with all optimizations +- **native-fast**: Faster native compilation with reduced optimizations for development + +### Common Maven Goals + +- `mvn clean`: Remove build artifacts +- `mvn compile`: Compile source code +- `mvn test`: Run unit tests +- `mvn package`: Package compiled code into distributable format +- `mvn install`: Install package into local repository +- `mvn dependency:tree`: Display dependency tree ``` ## Usage diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 123e51d..0000000 --- a/build.gradle +++ /dev/null @@ -1,118 +0,0 @@ -plugins { - id 'application' - id 'org.graalvm.buildtools.native' version '0.11.3' - id 'com.gradleup.shadow' version '9.3.0' -} - -java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } -} - -application { - mainClass = 'io.cloudchains.app.App' -} - -ext { - bitcoinjVersion = '0.14.7' - nettyVersion = '4.1.115.Final' - junitVersion = '5.11.3' - slf4jVersion = '2.0.16' - gsonVersion = '2.13.2' -} - -version = "0.5.14" -group = 'io.cloudchains' - -repositories { - mavenCentral() - gradlePluginPortal() -} - -run { - if (project.hasProperty('args')) { - args project.args.split("=") - } - standardInput = System.in -} - -shadowJar { - // mergeServiceFiles() -} - -test { - useJUnitPlatform() -} - - -graalvmNative { - binaries { - main { - buildArgs.add('--no-fallback') - imageName = 'xlite-daemon' - mainClass = 'io.cloudchains.app.App' - buildArgs.addAll([ - '--enable-native-access=ALL-UNNAMED', - '--enable-url-protocols=http,https', - '--features=com.oracle.svm.thirdparty.gson.GsonFeature', - '--features=com.oracle.svm.polyglot.groovy.GroovyIndyInterfaceFeature', - '--allow-incomplete-classpath', - '--verbose', - '--no-fallback', - '--no-server' - ]) - - // Build-time initialization - buildArgs.addAll([ - '--initialize-at-build-time=com.google.common.base.Charsets', - '--initialize-at-build-time=com.google.common.math.IntMath$1', - '--initialize-at-build-time=com.google.common.math.IntMath', - '--initialize-at-build-time=com.google.common.base.Charsets$1', - '--initialize-at-build-time=com.google.common.base.StandardCharsets', - '--initialize-at-build-time=io.netty,org.apache.commons.logging,org.slf4j', - '--initialize-at-build-time=org.slf4j.helpers.NOPLogger', - '--initialize-at-build-time=org.slf4j.helpers.NOPLoggerFactory', - '--initialize-at-build-time=org.slf4j.helpers.SubstituteLoggerFactory', - '--initialize-at-build-time=org.slf4j.helpers.Util', - '--initialize-at-build-time=org.slf4j.helpers.NOP_FallbackServiceProvider', - '--initialize-at-build-time=org.slf4j.nop.NOPServiceProvider', - '--initialize-at-build-time=org.slf4j.helpers.SubstituteServiceProvider', - '--initialize-at-build-time=org.bitcoinj.core.Utils', - '--initialize-at-build-time=org.bitcoinj.core.Sha256Hash', - '--initialize-at-build-time=org.bitcoinj.crypto.MnemonicCode', - '--initialize-at-build-time=com.google.common.io.BaseEncoding', - '--initialize-at-build-time=com.google.common.io.BaseEncoding$Base16Encoding', - '--initialize-at-build-time=com.google.common.io.BaseEncoding$Alphabet' - ]) - - // Runtime initialization - buildArgs.addAll([ - '--initialize-at-run-time=io.netty.util.internal.logging.Log4JLogger', - '--initialize-at-run-time=io.netty.handler.codec.http.HttpObjectEncoder', - '--initialize-at-run-time=io.netty.handler.codec.http2.DefaultHttp2FrameWriter', - '--initialize-at-run-time=io.netty.handler.codec.http2.Http2CodecUtil' - ]) - } - } -} - -dependencies { - implementation "org.bitcoinj:bitcoinj-core:${bitcoinjVersion}" - implementation "org.bitcoinj:orchid:1.2.1" - implementation 'net.jcip:jcip-annotations:1.0' - implementation "org.json:json:20250517" - implementation "org.apache.httpcomponents:httpclient:4.5.14" - implementation "com.google.code.gson:gson:${gsonVersion}" - implementation "commons-logging:commons-logging:1.2" - implementation "org.slf4j:slf4j-nop:${slf4jVersion}" - implementation "io.netty:netty-all:${nettyVersion}" - implementation "com.google.code.findbugs:jsr305:3.0.2" - compileOnly "org.graalvm.nativeimage:svm:21.+" - - testImplementation platform("org.junit:junit-bom:${junitVersion}") - testImplementation "org.junit.jupiter:junit-jupiter-api:${junitVersion}" - testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junitVersion}" - testImplementation "org.junit.jupiter:junit-jupiter:${junitVersion}" - testImplementation "org.junit.platform:junit-platform-launcher" -} diff --git a/ci/WaitForShutdownTime.ps1 b/ci/WaitForShutdownTime.ps1 deleted file mode 100644 index 0d8cc58..0000000 --- a/ci/WaitForShutdownTime.ps1 +++ /dev/null @@ -1,12 +0,0 @@ -while ($true) { - $os = Get-WmiObject win32_operatingsystem -ComputerName localhost -ErrorAction SilentlyContinue - $uptime = ((get-date) - ($os.ConvertToDateTime($os.lastbootuptime))) - $totalUptime = ($uptime.Days * 1440) + ($uptime.Hours * 60) + ($uptime.Minutes) - Write-Output Uptime: $totalUptime - - if ($totalUptime -gt 50) { - Stop-Computer -ComputerName localhost - }; - - Start-Sleep 30; -} diff --git a/ci/windows-build.cmd b/ci/windows-build.cmd deleted file mode 100644 index 4b7a869..0000000 --- a/ci/windows-build.cmd +++ /dev/null @@ -1,59 +0,0 @@ -REM GRAALVM 20.1.0 IS REQUIRED - INSTALL FROM -REM https://github.com/graalvm/graalvm-ce-dev-builds/releases/download/20.1.0-dev-20200212_0349/graalvm-ce-java8-windows-amd64-20.1.0-dev.zip - -choco install -y windows-sdk-7.1 kb2519277 -choco install -y vcredist2010 - - -SET NATIVE_IMAGE=%1 -SET CC_PACKAGED_JAR=%2 -SET CC_DIR=%3 - -echo Initializing Microsoft SDK 7.1 environment -call "C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\SetEnv.cmd" - -call %NATIVE_IMAGE% -jar %CC_PACKAGED_JAR% ^ - -H:Name=Cloudchains-SPV ^ - -H:Class=io.cloudchains.app.App ^ - -H:+JNI ^ - -H:+UseServiceLoaderFeature ^ - -H:ReflectionConfigurationFiles=contrib/netty-reflection.json ^ - -H:ReflectionConfigurationResources=META-INF/native-image/io.netty/transport/reflection-config.json ^ - -H:ResourceConfigurationFiles=contrib/resource-config.json ^ - -H:IncludeResources='.*/wordlist/english.txt$' ^ - -H:Log=registerResource ^ - --no-fallback ^ - --no-server ^ - -da ^ - --enable-url-protocols=http,https ^ - --initialize-at-build-time=io.netty ^ - --initialize-at-build-time=com.google.common ^ - --initialize-at-build-time=org.apache.commons.logging ^ - --initialize-at-build-time=org.slf4j.LoggerFactory ^ - --initialize-at-build-time=org.slf4j.impl.StaticLoggerBinder ^ - --initialize-at-build-time=org.slf4j.helpers.NOPLogger ^ - --initialize-at-build-time=org.slf4j.helpers.NOPLoggerFactory ^ - --initialize-at-build-time=org.slf4j.helpers.SubstituteLoggerFactory ^ - --initialize-at-build-time=org.slf4j.helpers.Util ^ - --initialize-at-build-time=org.bitcoinj.core.Utils ^ - --initialize-at-build-time=org.bitcoinj.core.Sha256Hash ^ - --initialize-at-build-time=org.bitcoinj.crypto.MnemonicCode ^ - --initialize-at-run-time=io.netty.util.internal.logging.Log4JLogger ^ - --initialize-at-run-time=io.netty.handler.codec.http.HttpObjectEncoder ^ - --initialize-at-run-time=io.netty.handler.codec.http2.DefaultHttp2FrameWriter ^ - --initialize-at-run-time=io.netty.handler.codec.http2.Http2CodecUtil ^ - --initialize-at-run-time=io.netty.buffer.PooledByteBufAllocator ^ - --initialize-at-run-time=io.netty.buffer.ByteBufAllocator ^ - --initialize-at-run-time=io.netty.buffer.ByteBufUtil ^ - --initialize-at-run-time=io.netty.buffer.AbstractReferenceCountedByteBuf ^ - --initialize-at-run-time=io.netty.handler.codec.http2.Http2CodecUtil ^ - --initialize-at-run-time=io.netty.handler.codec.http2.Http2ClientUpgradeCodec ^ - --initialize-at-run-time=io.netty.handler.codec.http2.Http2ConnectionHandler ^ - --initialize-at-run-time=io.netty.handler.codec.http2.DefaultHttp2FrameWriter ^ - --initialize-at-run-time=io.netty.util.AbstractReferenceCounted ^ - --initialize-at-run-time=io.netty.handler.codec.http.HttpObjectEncoder ^ - --initialize-at-run-time=io.netty.handler.codec.http.websocketx.WebSocket00FrameEncoder ^ - --initialize-at-run-time=io.netty.handler.codec.http.websocketx.extensions.compression.DeflateDecoder ^ - --initialize-at-run-time=io.netty.handler.ssl.util.ThreadLocalInsecureRandom ^ - --allow-incomplete-classpath ^ - --verbose diff --git a/contrib/netty-reflection.json b/contrib/netty-reflection.json deleted file mode 100644 index d2af5dc..0000000 --- a/contrib/netty-reflection.json +++ /dev/null @@ -1,98 +0,0 @@ -[ - { - "name": "io.netty.channel.socket.nio.NioServerSocketChannel", - "methods": [ - { "name": "", "parameterTypes": [] } - ] - }, - { - "name" : "io.cloudchains.app.net.CoinInstance", - "allDeclaredConstructors" : true, - "allPublicConstructors" : true, - "allDeclaredMethods" : true, - "allPublicMethods" : true, - "allDeclaredClasses" : true, - "allPublicClasses" : true - }, - { - "name" : "io.cloudchains.app.net.api.JSONRPCController", - "allDeclaredConstructors" : true, - "allPublicConstructors" : true, - "allDeclaredMethods" : true, - "allPublicMethods" : true, - "allDeclaredClasses" : true, - "allPublicClasses" : true - }, - { - "name" : "io.cloudchains.app.net.api.http.HTTPServerHandler", - "allDeclaredConstructors" : true, - "allPublicConstructors" : true, - "allDeclaredMethods" : true, - "allPublicMethods" : true, - "allDeclaredClasses" : true, - "allPublicClasses" : true - }, - { - "name" : "java.lang.String", - "allDeclaredConstructors" : true, - "allPublicConstructors" : true, - "allDeclaredMethods" : true, - "allPublicMethods" : true, - "allDeclaredClasses" : true, - "allPublicClasses" : true - }, - { - "name" : "org.bitcoinj.script.Script", - "allDeclaredConstructors" : true, - "allPublicConstructors" : true, - "allDeclaredMethods" : true, - "allPublicMethods" : true, - "allDeclaredClasses" : true, - "allPublicClasses" : true - }, - { - "name" : "org.bitcoinj.script.Script$ScriptType", - "allDeclaredConstructors" : true, - "allPublicConstructors" : true, - "allDeclaredMethods" : true, - "allPublicMethods" : true, - "allDeclaredClasses" : true, - "allPublicClasses" : true - }, - { - "name" : "org.bitcoinj.wallet.DeterministicSeed", - "allDeclaredConstructors" : true, - "allPublicConstructors" : true, - "allDeclaredMethods" : true, - "allPublicMethods" : true, - "allDeclaredClasses" : true, - "allPublicClasses" : true - }, - { - "name" : "com.google.common.primitives.UnsignedBytes$LexicographicalComparatorHolder$UnsafeComparator", - "allDeclaredConstructors" : true, - "allPublicConstructors" : true, - "allDeclaredMethods" : true, - "allPublicMethods" : true, - "allDeclaredClasses" : true, - "allPublicClasses" : true - }, - { - "name" : "org.apache.commons.logging.LogFactory", - "allDeclaredConstructors" : true, - "allPublicConstructors" : true, - "allDeclaredMethods" : true, - "allPublicMethods" : true, - "allDeclaredClasses" : true, - "allPublicClasses" : true - }, - { - "name" : "org.apache.commons.logging.impl.LogFactoryImpl", - "allDeclaredConstructors" : true, - "allPublicConstructors" : true, - "allDeclaredMethods" : true, - "allPublicMethods" : true, - "allDeclaredClasses" : true, - "allPublicClasses" : true - } -] \ No newline at end of file diff --git a/contrib/resource-config.json b/contrib/resource-config.json deleted file mode 100644 index a146d7a..0000000 --- a/contrib/resource-config.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "resources":[ - {"pattern":"META-INF/services/jdk.vm.ci.hotspot.HotSpotJVMCIBackendFactory"}, - {"pattern":"META-INF/services/jdk.vm.ci.services.JVMCIServiceLocator"}, - {"pattern":"mozilla/public-suffix-list.txt"}, - {"pattern":"org/apache/http/client/version.properties"}, - {"pattern":"org/bitcoinj/crypto/mnemonic/wordlist/english.txt"}, - {"pattern":"org/slf4j/impl/StaticLoggerBinder.class"}, - {"pattern":"sun/net/idn/uidna.spp"}, - {"pattern":"sun/text/resources/unorm.icu"} - ] -} diff --git a/gradle.properties b/gradle.properties deleted file mode 100644 index d7377e6..0000000 --- a/gradle.properties +++ /dev/null @@ -1,9 +0,0 @@ -# Gradle performance improvements -org.gradle.parallel=true -org.gradle.caching=true -org.gradle.configureondemand=true -org.gradle.daemon=true -org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m -XX:+UseG1GC - -# Java compatibility -kotlin.code.style=official \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 41d9927a4d4fb3f96a785543079b8df6723c946b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 59821 zcma&NV|1p`(k7gaZQHhOJ9%QKV?D8LCmq{1JGRYE(y=?XJw0>InKkE~^UnAEs2gk5 zUVGPCwX3dOb!}xiFmPB95NK!+5D<~S0s;d1zn&lrfAn7 zC?Nb-LFlib|DTEqB8oDS5&$(u1<5;wsY!V`2F7^=IR@I9so5q~=3i_(hqqG<9SbL8Q(LqDrz+aNtGYWGJ2;p*{a-^;C>BfGzkz_@fPsK8{pTT~_VzB$E`P@> z7+V1WF2+tSW=`ZRj3&0m&d#x_lfXq`bb-Y-SC-O{dkN2EVM7@!n|{s+2=xSEMtW7( zz~A!cBpDMpQu{FP=y;sO4Le}Z)I$wuFwpugEY3vEGfVAHGqZ-<{vaMv-5_^uO%a{n zE_Zw46^M|0*dZ`;t%^3C19hr=8FvVdDp1>SY>KvG!UfD`O_@weQH~;~W=fXK_!Yc> z`EY^PDJ&C&7LC;CgQJeXH2 zjfM}2(1i5Syj)Jj4EaRyiIl#@&lC5xD{8hS4Wko7>J)6AYPC-(ROpVE-;|Z&u(o=X z2j!*>XJ|>Lo+8T?PQm;SH_St1wxQPz)b)Z^C(KDEN$|-6{A>P7r4J1R-=R7|FX*@! zmA{Ja?XE;AvisJy6;cr9Q5ovphdXR{gE_7EF`ji;n|RokAJ30Zo5;|v!xtJr+}qbW zY!NI6_Wk#6pWFX~t$rAUWi?bAOv-oL6N#1>C~S|7_e4 zF}b9(&a*gHk+4@J26&xpiWYf2HN>P;4p|TD4f586umA2t@cO1=Fx+qd@1Ae#Le>{-?m!PnbuF->g3u)7(n^llJfVI%Q2rMvetfV5 z6g|sGf}pV)3_`$QiKQnqQ<&ghOWz4_{`rA1+7*M0X{y(+?$|{n zs;FEW>YzUWg{sO*+D2l6&qd+$JJP_1Tm;To<@ZE%5iug8vCN3yH{!6u5Hm=#3HJ6J zmS(4nG@PI^7l6AW+cWAo9sFmE`VRcM`sP7X$^vQY(NBqBYU8B|n-PrZdNv8?K?kUTT3|IE`-A8V*eEM2=u*kDhhKsmVPWGns z8QvBk=BPjvu!QLtlF0qW(k+4i+?H&L*qf262G#fks9}D5-L{yiaD10~a;-j!p!>5K zl@Lh+(9D{ePo_S4F&QXv|q_yT`GIPEWNHDD8KEcF*2DdZD;=J6u z|8ICSoT~5Wd!>g%2ovFh`!lTZhAwpIbtchDc{$N%<~e$E<7GWsD42UdJh1fD($89f2on`W`9XZJmr*7lRjAA8K0!(t8-u>2H*xn5cy1EG{J;w;Q-H8Yyx+WW(qoZZM7p(KQx^2-yI6Sw?k<=lVOVwYn zY*eDm%~=|`c{tUupZ^oNwIr!o9T;H3Fr|>NE#By8SvHb&#;cyBmY1LwdXqZwi;qn8 zK+&z{{95(SOPXAl%EdJ3jC5yV^|^}nOT@M0)|$iOcq8G{#*OH7=DlfOb; z#tRO#tcrc*yQB5!{l5AF3(U4>e}nEvkoE_XCX=a3&A6Atwnr&`r&f2d%lDr8f?hBB zr1dKNypE$CFbT9I?n){q<1zHmY>C=5>9_phi79pLJG)f=#dKdQ7We8emMjwR*qIMF zE_P-T*$hX#FUa%bjv4Vm=;oxxv`B*`weqUn}K=^TXjJG=UxdFMSj-QV6fu~;- z|IsUq`#|73M%Yn;VHJUbt<0UHRzbaF{X@76=8*-IRx~bYgSf*H(t?KH=?D@wk*E{| z2@U%jKlmf~C^YxD=|&H?(g~R9-jzEb^y|N5d`p#2-@?BUcHys({pUz4Zto7XwKq2X zSB~|KQGgv_Mh@M!*{nl~2~VV_te&E7K39|WYH zCxfd|v_4!h$Ps2@atm+gj14Ru)DhivY&(e_`eA)!O1>nkGq|F-#-6oo5|XKEfF4hR z%{U%ar7Z8~B!foCd_VRHr;Z1c0Et~y8>ZyVVo9>LLi(qb^bxVkbq-Jq9IF7!FT`(- zTMrf6I*|SIznJLRtlP)_7tQ>J`Um>@pP=TSfaPB(bto$G1C zx#z0$=zNpP-~R);kM4O)9Mqn@5Myv5MmmXOJln312kq#_94)bpSd%fcEo7cD#&|<` zrcal$(1Xv(nDEquG#`{&9Ci~W)-zd_HbH-@2F6+|a4v}P!w!Q*h$#Zu+EcZeY>u&?hn#DCfC zVuye5@Ygr+T)0O2R1*Hvlt>%rez)P2wS}N-i{~IQItGZkp&aeY^;>^m7JT|O^{`78 z$KaK0quwcajja;LU%N|{`2o&QH@u%jtH+j!haGj;*ZCR*`UgOXWE>qpXqHc?g&vA& zt-?_g8k%ZS|D;()0Lf!>7KzTSo-8hUh%OA~i76HKRLudaNiwo*E9HxmzN4y>YpZNO zUE%Q|H_R_UmX=*f=2g=xyP)l-DP}kB@PX|(Ye$NOGN{h+fI6HVw`~Cd0cKqO;s6aiYLy7sl~%gs`~XaL z^KrZ9QeRA{O*#iNmB7_P!=*^pZiJ5O@iE&X2UmUCPz!)`2G3)5;H?d~3#P|)O(OQ_ zua+ZzwWGkWflk4j^Lb=x56M75_p9M*Q50#(+!aT01y80x#rs9##!;b-BH?2Fu&vx} za%4!~GAEDsB54X9wCF~juV@aU}fp_(a<`Ig0Pip8IjpRe#BR?-niYcz@jI+QY zBU9!8dAfq@%p;FX)X=E7?B=qJJNXlJ&7FBsz;4&|*z{^kEE!XbA)(G_O6I9GVzMAF z8)+Un(6od`W7O!!M=0Z)AJuNyN8q>jNaOdC-zAZ31$Iq%{c_SYZe+(~_R`a@ zOFiE*&*o5XG;~UjsuW*ja-0}}rJdd@^VnQD!z2O~+k-OSF%?hqcFPa4e{mV1UOY#J zTf!PM=KMNAzbf(+|AL%K~$ahX0Ol zbAxKu3;v#P{Qia{_WzHl`!@!8c#62XSegM{tW1nu?Ee{sQq(t{0TSq67YfG;KrZ$n z*$S-+R2G?aa*6kRiTvVxqgUhJ{ASSgtepG3hb<3hlM|r>Hr~v_DQ>|Nc%&)r0A9go z&F3Ao!PWKVq~aWOzLQIy&R*xo>}{UTr}?`)KS&2$3NR@a+>+hqK*6r6Uu-H};ZG^| zfq_Vl%YE1*uGwtJ>H*Y(Q9E6kOfLJRlrDNv`N;jnag&f<4#UErM0ECf$8DASxMFF& zK=mZgu)xBz6lXJ~WZR7OYw;4&?v3Kk-QTs;v1r%XhgzSWVf|`Sre2XGdJb}l1!a~z zP92YjnfI7OnF@4~g*LF>G9IZ5c+tifpcm6#m)+BmnZ1kz+pM8iUhwag`_gqr(bnpy zl-noA2L@2+?*7`ZO{P7&UL~ahldjl`r3=HIdo~Hq#d+&Q;)LHZ4&5zuDNug@9-uk; z<2&m#0Um`s=B}_}9s&70Tv_~Va@WJ$n~s`7tVxi^s&_nPI0`QX=JnItlOu*Tn;T@> zXsVNAHd&K?*u~a@u8MWX17VaWuE0=6B93P2IQ{S$-WmT+Yp!9eA>@n~=s>?uDQ4*X zC(SxlKap@0R^z1p9C(VKM>nX8-|84nvIQJ-;9ei0qs{}X>?f%&E#%-)Bpv_p;s4R+ z;PMpG5*rvN&l;i{^~&wKnEhT!S!LQ>udPzta#Hc9)S8EUHK=%x+z@iq!O{)*XM}aI zBJE)vokFFXTeG<2Pq}5Na+kKnu?Ch|YoxdPb&Z{07nq!yzj0=xjzZj@3XvwLF0}Pa zn;x^HW504NNfLY~w!}5>`z=e{nzGB>t4ntE>R}r7*hJF3OoEx}&6LvZz4``m{AZxC zz6V+^73YbuY>6i9ulu)2`ozP(XBY5n$!kiAE_Vf4}Ih)tlOjgF3HW|DF+q-jI_0p%6Voc^e;g28* z;Sr4X{n(X7eEnACWRGNsHqQ_OfWhAHwnSQ87@PvPcpa!xr9`9+{QRn;bh^jgO8q@v zLekO@-cdc&eOKsvXs-eMCH8Y{*~3Iy!+CANy+(WXYS&6XB$&1+tB?!qcL@@) zS7XQ|5=o1fr8yM7r1AyAD~c@Mo`^i~hjx{N17%pDX?j@2bdBEbxY}YZxz!h#)q^1x zpc_RnoC3`V?L|G2R1QbR6pI{Am?yW?4Gy`G-xBYfebXvZ=(nTD7u?OEw>;vQICdPJBmi~;xhVV zisVvnE!bxI5|@IIlDRolo_^tc1{m)XTbIX^<{TQfsUA1Wv(KjJED^nj`r!JjEA%MaEGqPB z9YVt~ol3%e`PaqjZt&-)Fl^NeGmZ)nbL;92cOeLM2H*r-zA@d->H5T_8_;Jut0Q_G zBM2((-VHy2&eNkztIpHk&1H3M3@&wvvU9+$RO%fSEa_d5-qZ!<`-5?L9lQ1@AEpo* z3}Zz~R6&^i9KfRM8WGc6fTFD%PGdruE}`X$tP_*A)_7(uI5{k|LYc-WY*%GJ6JMmw zNBT%^E#IhekpA(i zcB$!EB}#>{^=G%rQ~2;gbObT9PQ{~aVx_W6?(j@)S$&Ja1s}aLT%A*mP}NiG5G93- z_DaRGP77PzLv0s32{UFm##C2LsU!w{vHdKTM1X)}W%OyZ&{3d^2Zu-zw?fT=+zi*q z^fu6CXQ!i?=ljsqSUzw>g#PMk>(^#ejrYp(C)7+@Z1=Mw$Rw!l8c9}+$Uz;9NUO(kCd#A1DX4Lbis0k; z?~pO(;@I6Ajp}PL;&`3+;OVkr3A^dQ(j?`by@A!qQam@_5(w6fG>PvhO`#P(y~2ue zW1BH_GqUY&>PggMhhi@8kAY;XWmj>y1M@c`0v+l~l0&~Kd8ZSg5#46wTLPo*Aom-5 z>qRXyWl}Yda=e@hJ%`x=?I42(B0lRiR~w>n6p8SHN~B6Y>W(MOxLpv>aB)E<1oEcw z%X;#DJpeDaD;CJRLX%u!t23F|cv0ZaE183LXxMq*uWn)cD_ zp!@i5zsmcxb!5uhp^@>U;K>$B|8U@3$65CmhuLlZ2(lF#hHq-<<+7ZN9m3-hFAPgA zKi;jMBa*59ficc#TRbH_l`2r>z(Bm_XEY}rAwyp~c8L>{A<0@Q)j*uXns^q5z~>KI z)43=nMhcU1ZaF;CaBo>hl6;@(2#9yXZ7_BwS4u>gN%SBS<;j{{+p}tbD8y_DFu1#0 zx)h&?`_`=ti_6L>VDH3>PPAc@?wg=Omdoip5j-2{$T;E9m)o2noyFW$5dXb{9CZ?c z);zf3U526r3Fl+{82!z)aHkZV6GM@%OKJB5mS~JcDjieFaVn}}M5rtPnHQVw0Stn- zEHs_gqfT8(0b-5ZCk1%1{QQaY3%b>wU z7lyE?lYGuPmB6jnMI6s$1uxN{Tf_n7H~nKu+h7=%60WK-C&kEIq_d4`wU(*~rJsW< zo^D$-(b0~uNVgC+$J3MUK)(>6*k?92mLgpod{Pd?{os+yHr&t+9ZgM*9;dCQBzE!V zk6e6)9U6Bq$^_`E1xd}d;5O8^6?@bK>QB&7l{vAy^P6FOEO^l7wK4K=lLA45gQ3$X z=$N{GR1{cxO)j;ZxKI*1kZIT9p>%FhoFbRK;M(m&bL?SaN zzkZS9xMf={o@gpG%wE857u@9dq>UKvbaM1SNtMA9EFOp7$BjJQVkIm$wU?-yOOs{i z1^(E(WwZZG{_#aIzfpGc@g5-AtK^?Q&vY#CtVpfLbW?g0{BEX4Vlk(`AO1{-D@31J zce}#=$?Gq+FZG-SD^z)-;wQg9`qEO}Dvo+S9*PUB*JcU)@S;UVIpN7rOqXmEIerWo zP_lk!@RQvyds&zF$Rt>N#_=!?5{XI`Dbo0<@>fIVgcU*9Y+ z)}K(Y&fdgve3ruT{WCNs$XtParmvV;rjr&R(V&_#?ob1LzO0RW3?8_kSw)bjom#0; zeNllfz(HlOJw012B}rgCUF5o|Xp#HLC~of%lg+!pr(g^n;wCX@Yk~SQOss!j9f(KL zDiI1h#k{po=Irl)8N*KU*6*n)A8&i9Wf#7;HUR^5*6+Bzh;I*1cICa|`&`e{pgrdc zs}ita0AXb$c6{tu&hxmT0faMG0GFc)unG8tssRJd%&?^62!_h_kn^HU_kBgp$bSew zqu)M3jTn;)tipv9Wt4Ll#1bmO2n?^)t^ZPxjveoOuK89$oy4(8Ujw{nd*Rs*<+xFi z{k*9v%sl?wS{aBSMMWdazhs0#gX9Has=pi?DhG&_0|cIyRG7c`OBiVG6W#JjYf7-n zIQU*Jc+SYnI8oG^Q8So9SP_-w;Y00$p5+LZ{l+81>v7|qa#Cn->312n=YQd$PaVz8 zL*s?ZU*t-RxoR~4I7e^c!8TA4g>w@R5F4JnEWJpy>|m5la2b#F4d*uoz!m=i1;`L` zB(f>1fAd~;*wf%GEbE8`EA>IO9o6TdgbIC%+en!}(C5PGYqS0{pa?PD)5?ds=j9{w za9^@WBXMZ|D&(yfc~)tnrDd#*;u;0?8=lh4%b-lFPR3ItwVJp};HMdEw#SXg>f-zU zEiaj5H=jzRSy(sWVd%hnLZE{SUj~$xk&TfheSch#23)YTcjrB+IVe0jJqsdz__n{- zC~7L`DG}-Dgrinzf7Jr)e&^tdQ}8v7F+~eF*<`~Vph=MIB|YxNEtLo1jXt#9#UG5` zQ$OSk`u!US+Z!=>dGL>%i#uV<5*F?pivBH@@1idFrzVAzttp5~>Y?D0LV;8Yv`wAa{hewVjlhhBM z_mJhU9yWz9Jexg@G~dq6EW5^nDXe(sU^5{}qbd0*yW2Xq6G37f8{{X&Z>G~dUGDFu zgmsDDZZ5ZmtiBw58CERFPrEG>*)*`_B75!MDsOoK`T1aJ4GZ1avI?Z3OX|Hg?P(xy zSPgO$alKZuXd=pHP6UZy0G>#BFm(np+dekv0l6gd=36FijlT8^kI5; zw?Z*FPsibF2d9T$_L@uX9iw*>y_w9HSh8c=Rm}f>%W+8OS=Hj_wsH-^actull3c@!z@R4NQ4qpytnwMaY z)>!;FUeY?h2N9tD(othc7Q=(dF zZAX&Y1ac1~0n(z}!9{J2kPPnru1?qteJPvA2m!@3Zh%+f1VQt~@leK^$&ZudOpS!+ zw#L0usf!?Df1tB?9=zPZ@q2sG!A#9 zKZL`2cs%|Jf}wG=_rJkwh|5Idb;&}z)JQuMVCZSH9kkG%zvQO01wBN)c4Q`*xnto3 zi7TscilQ>t_SLij{@Fepen*a(`upw#RJAx|JYYXvP1v8f)dTHv9pc3ZUwx!0tOH?c z^Hn=gfjUyo!;+3vZhxNE?LJgP`qYJ`J)umMXT@b z{nU(a^xFfofcxfHN-!Jn*{Dp5NZ&i9#9r{)s^lUFCzs5LQL9~HgxvmU#W|iNs0<3O z%Y2FEgvts4t({%lfX1uJ$w{JwfpV|HsO{ZDl2|Q$-Q?UJd`@SLBsMKGjFFrJ(s?t^ z2Llf`deAe@YaGJf)k2e&ryg*m8R|pcjct@rOXa=64#V9!sp=6tC#~QvYh&M~zmJ;% zr*A}V)Ka^3JE!1pcF5G}b&jdrt;bM^+J;G^#R08x@{|ZWy|547&L|k6)HLG|sN<~o z?y`%kbfRN_vc}pwS!Zr}*q6DG7;be0qmxn)eOcD%s3Wk`=@GM>U3ojhAW&WRppi0e zudTj{ufwO~H7izZJmLJD3uPHtjAJvo6H=)&SJ_2%qRRECN#HEU_RGa(Pefk*HIvOH zW7{=Tt(Q(LZ6&WX_Z9vpen}jqge|wCCaLYpiw@f_%9+-!l{kYi&gT@Cj#D*&rz1%e z@*b1W13bN8^j7IpAi$>`_0c!aVzLe*01DY-AcvwE;kW}=Z{3RJLR|O~^iOS(dNEnL zJJ?Dv^ab++s2v!4Oa_WFDLc4fMspglkh;+vzg)4;LS{%CR*>VwyP4>1Tly+!fA-k? z6$bg!*>wKtg!qGO6GQ=cAmM_RC&hKg$~(m2LdP{{*M+*OVf07P$OHp*4SSj9H;)1p z^b1_4p4@C;8G7cBCB6XC{i@vTB3#55iRBZiml^jc4sYnepCKUD+~k}TiuA;HWC6V3 zV{L5uUAU9CdoU+qsFszEwp;@d^!6XnX~KI|!o|=r?qhs`(-Y{GfO4^d6?8BC0xonf zKtZc1C@dNu$~+p#m%JW*J7alfz^$x`U~)1{c7svkIgQ3~RK2LZ5;2TAx=H<4AjC8{ z;)}8OfkZy7pSzVsdX|wzLe=SLg$W1+`Isf=o&}npxWdVR(i8Rr{uzE516a@28VhVr zVgZ3L&X(Q}J0R2{V(}bbNwCDD5K)<5h9CLM*~!xmGTl{Mq$@;~+|U*O#nc^oHnFOy z9Kz%AS*=iTBY_bSZAAY6wXCI?EaE>8^}WF@|}O@I#i69ljjWQPBJVk zQ_rt#J56_wGXiyItvAShJpLEMtW_)V5JZAuK#BAp6bV3K;IkS zK0AL(3ia99!vUPL#j>?<>mA~Q!mC@F-9I$9Z!96ZCSJO8FDz1SP3gF~m`1c#y!efq8QN}eHd+BHwtm%M5586jlU8&e!CmOC z^N_{YV$1`II$~cTxt*dV{-yp61nUuX5z?N8GNBuZZR}Uy_Y3_~@Y3db#~-&0TX644OuG^D3w_`?Yci{gTaPWST8`LdE)HK5OYv>a=6B%R zw|}>ngvSTE1rh`#1Rey0?LXTq;bCIy>TKm^CTV4BCSqdpx1pzC3^ca*S3fUBbKMzF z6X%OSdtt50)yJw*V_HE`hnBA)1yVN3Ruq3l@lY;%Bu+Q&hYLf_Z@fCUVQY-h4M3)- zE_G|moU)Ne0TMjhg?tscN7#ME6!Rb+y#Kd&-`!9gZ06o3I-VX1d4b1O=bpRG-tDK0 zSEa9y46s7QI%LmhbU3P`RO?w#FDM(}k8T`&>OCU3xD=s5N7}w$GntXF;?jdVfg5w9OR8VPxp5{uw zD+_;Gb}@7Vo_d3UV7PS65%_pBUeEwX_Hwfe2e6Qmyq$%0i8Ewn%F7i%=CNEV)Qg`r|&+$ zP6^Vl(MmgvFq`Zb715wYD>a#si;o+b4j^VuhuN>+sNOq6Qc~Y;Y=T&!Q4>(&^>Z6* zwliz!_16EDLTT;v$@W(s7s0s zi*%p>q#t)`S4j=Ox_IcjcllyT38C4hr&mlr6qX-c;qVa~k$MG;UqdnzKX0wo0Xe-_)b zrHu1&21O$y5828UIHI@N;}J@-9cpxob}zqO#!U%Q*ybZ?BH#~^fOT_|8&xAs_rX24 z^nqn{UWqR?MlY~klh)#Rz-*%&e~9agOg*fIN`P&v!@gcO25Mec23}PhzImkdwVT|@ zFR9dYYmf&HiUF4xO9@t#u=uTBS@k*97Z!&hu@|xQnQDkLd!*N`!0JN7{EUoH%OD85 z@aQ2(w-N)1_M{;FV)C#(a4p!ofIA3XG(XZ2E#%j_(=`IWlJAHWkYM2&(+yY|^2TB0 z>wfC-+I}`)LFOJ%KeBb1?eNxGKeq?AI_eBE!M~$wYR~bB)J3=WvVlT8ZlF2EzIFZt zkaeyj#vmBTGkIL9mM3cEz@Yf>j=82+KgvJ-u_{bBOxE5zoRNQW3+Ahx+eMGem|8xo zL3ORKxY_R{k=f~M5oi-Z>5fgqjEtzC&xJEDQ@`<)*Gh3UsftBJno-y5Je^!D?Im{j za*I>RQ=IvU@5WKsIr?kC$DT+2bgR>8rOf3mtXeMVB~sm%X7W5`s=Tp>FR544tuQ>9qLt|aUSv^io&z93luW$_OYE^sf8DB?gx z4&k;dHMWph>Z{iuhhFJr+PCZ#SiZ9e5xM$A#0yPtVC>yk&_b9I676n|oAH?VeTe*1 z@tDK}QM-%J^3Ns6=_vh*I8hE?+=6n9nUU`}EX|;Mkr?6@NXy8&B0i6h?7%D=%M*Er zivG61Wk7e=v;<%t*G+HKBqz{;0Biv7F+WxGirONRxJij zon5~(a`UR%uUzfEma99QGbIxD(d}~oa|exU5Y27#4k@N|=hE%Y?Y3H%rcT zHmNO#ZJ7nPHRG#y-(-FSzaZ2S{`itkdYY^ZUvyw<7yMBkNG+>$Rfm{iN!gz7eASN9-B3g%LIEyRev|3)kSl;JL zX7MaUL_@~4ot3$woD0UA49)wUeu7#lj77M4ar8+myvO$B5LZS$!-ZXw3w;l#0anYz zDc_RQ0Ome}_i+o~H=CkzEa&r~M$1GC!-~WBiHiDq9Sdg{m|G?o7g`R%f(Zvby5q4; z=cvn`M>RFO%i_S@h3^#3wImmWI4}2x4skPNL9Am{c!WxR_spQX3+;fo!y(&~Palyjt~Xo0uy6d%sX&I`e>zv6CRSm)rc^w!;Y6iVBb3x@Y=`hl9jft zXm5vilB4IhImY5b->x{!MIdCermpyLbsalx8;hIUia%*+WEo4<2yZ6`OyG1Wp%1s$ zh<|KrHMv~XJ9dC8&EXJ`t3ETz>a|zLMx|MyJE54RU(@?K&p2d#x?eJC*WKO9^d17# zdTTKx-Os3k%^=58Sz|J28aCJ}X2-?YV3T7ee?*FoDLOC214J4|^*EX`?cy%+7Kb3(@0@!Q?p zk>>6dWjF~y(eyRPqjXqDOT`4^Qv-%G#Zb2G?&LS-EmO|ixxt79JZlMgd^~j)7XYQ; z62rGGXA=gLfgy{M-%1gR87hbhxq-fL)GSfEAm{yLQP!~m-{4i_jG*JsvUdqAkoc#q6Yd&>=;4udAh#?xa2L z7mFvCjz(hN7eV&cyFb%(U*30H@bQ8-b7mkm!=wh2|;+_4vo=tyHPQ0hL=NR`jbsSiBWtG ztMPPBgHj(JTK#0VcP36Z`?P|AN~ybm=jNbU=^3dK=|rLE+40>w+MWQW%4gJ`>K!^- zx4kM*XZLd(E4WsolMCRsdvTGC=37FofIyCZCj{v3{wqy4OXX-dZl@g`Dv>p2`l|H^ zS_@(8)7gA62{Qfft>vx71stILMuyV4uKb7BbCstG@|e*KWl{P1$=1xg(7E8MRRCWQ1g)>|QPAZot~|FYz_J0T+r zTWTB3AatKyUsTXR7{Uu) z$1J5SSqoJWt(@@L5a)#Q6bj$KvuC->J-q1!nYS6K5&e7vNdtj- zj9;qwbODLgIcObqNRGs1l{8>&7W?BbDd!87=@YD75B2ep?IY|gE~t)$`?XJ45MG@2 zz|H}f?qtEb_p^Xs$4{?nA=Qko3Lc~WrAS`M%9N60FKqL7XI+v_5H-UDiCbRm`fEmv z$pMVH*#@wQqml~MZe+)e4Ts3Gl^!Z0W3y$;|9hI?9(iw29b7en0>Kt2pjFXk@!@-g zTb4}Kw!@u|V!wzk0|qM*zj$*-*}e*ZXs#Y<6E_!BR}3^YtjI_byo{F+w9H9?f%mnBh(uE~!Um7)tgp2Ye;XYdVD95qt1I-fc@X zXHM)BfJ?^g(s3K|{N8B^hamrWAW|zis$`6|iA>M-`0f+vq(FLWgC&KnBDsM)_ez1# zPCTfN8{s^K`_bum2i5SWOn)B7JB0tzH5blC?|x;N{|@ch(8Uy-O{B2)OsfB$q0@FR z27m3YkcVi$KL;;4I*S;Z#6VfZcZFn!D2Npv5pio)sz-`_H*#}ROd7*y4i(y(YlH<4 zh4MmqBe^QV_$)VvzWgMXFy`M(vzyR2u!xx&%&{^*AcVLrGa8J9ycbynjKR~G6zC0e zlEU>zt7yQtMhz>XMnz>ewXS#{Bulz$6HETn?qD5v3td>`qGD;Y8&RmkvN=24=^6Q@DYY zxMt}uh2cSToMkkIWo1_Lp^FOn$+47JXJ*#q=JaeiIBUHEw#IiXz8cStEsw{UYCA5v_%cF@#m^Y!=+qttuH4u}r6gMvO4EAvjBURtLf& z6k!C|OU@hv_!*qear3KJ?VzVXDKqvKRtugefa7^^MSWl0fXXZR$Xb!b6`eY4A1#pk zAVoZvb_4dZ{f~M8fk3o?{xno^znH1t;;E6K#9?erW~7cs%EV|h^K>@&3Im}c7nm%Y zbLozFrwM&tSNp|46)OhP%MJ(5PydzR>8)X%i3!^L%3HCoCF#Y0#9vPI5l&MK*_ z6G8Y>$`~c)VvQle_4L_AewDGh@!bKkJeEs_NTz(yilnM!t}7jz>fmJb89jQo6~)%% z@GNIJ@AShd&K%UdQ5vR#yT<-goR+D@Tg;PuvcZ*2AzSWN&wW$Xc+~vW)pww~O|6hL zBxX?hOyA~S;3rAEfI&jmMT4f!-eVm%n^KF_QT=>!A<5tgXgi~VNBXqsFI(iI$Tu3x0L{<_-%|HMG4Cn?Xs zq~fvBhu;SDOCD7K5(l&i7Py-;Czx5byV*3y%#-Of9rtz?M_owXc2}$OIY~)EZ&2?r zLQ(onz~I7U!w?B%LtfDz)*X=CscqH!UE=mO?d&oYvtj|(u)^yomS;Cd>Men|#2yuD zg&tf(*iSHyo;^A03p&_j*QXay9d}qZ0CgU@rnFNDIT5xLhC5_tlugv()+w%`7;ICf z>;<#L4m@{1}Og76*e zHWFm~;n@B1GqO8s%=qu)+^MR|jp(ULUOi~v;wE8SB6^mK@adSb=o+A_>Itjn13AF& zDZe+wUF9G!JFv|dpj1#d+}BO~s*QTe3381TxA%Q>P*J#z%( z5*8N^QWxgF73^cTKkkvgvIzf*cLEyyKw)Wf{#$n{uS#(rAA~>TS#!asqQ2m_izXe3 z7$Oh=rR;sdmVx3G)s}eImsb<@r2~5?vcw*Q4LU~FFh!y4r*>~S7slAE6)W3Up2OHr z2R)+O<0kKo<3+5vB}v!lB*`%}gFldc+79iahqEx#&Im@NCQU$@PyCZbcTt?K{;o@4 z312O9GB)?X&wAB}*-NEU zn@6`)G`FhT8O^=Cz3y+XtbwO{5+{4-&?z!esFts-C zypwgI^4#tZ74KC+_IW|E@kMI=1pSJkvg$9G3Va(!reMnJ$kcMiZ=30dTJ%(Ws>eUf z;|l--TFDqL!PZbLc_O(XP0QornpP;!)hdT#Ts7tZ9fcQeH&rhP_1L|Z_ha#JOroe^qcsLi`+AoBWHPM7}gD z+mHuPXd14M?nkp|nu9G8hPk;3=JXE-a204Fg!BK|$MX`k-qPeD$2OOqvF;C(l8wm13?>i(pz7kRyYm zM$IEzf`$}B%ezr!$(UO#uWExn%nTCTIZzq&8@i8sP#6r8 z*QMUzZV(LEWZb)wbmf|Li;UpiP;PlTQ(X4zreD`|`RG!7_wc6J^MFD!A=#K*ze>Jg z?9v?p(M=fg_VB0+c?!M$L>5FIfD(KD5ku*djwCp+5GVIs9^=}kM2RFsxx0_5DE%BF zykxwjWvs=rbi4xKIt!z$&v(`msFrl4n>a%NO_4`iSyb!UiAE&mDa+apc zPe)#!ToRW~rqi2e1bdO1RLN5*uUM@{S`KLJhhY-@TvC&5D(c?a(2$mW-&N%h5IfEM zdFI6`6KJiJQIHvFiG-34^BtO3%*$(-Ht_JU*(KddiUYoM{coadlG&LVvke&*p>Cac z^BPy2Zteiq1@ulw0e)e*ot7@A$RJui0$l^{lsCt%R;$){>zuRv9#w@;m=#d%%TJmm zC#%eFOoy$V)|3*d<OC1iP+4R7D z8FE$E8l2Y?(o-i6wG=BKBh0-I?i3WF%hqdD7VCd;vpk|LFP!Et8$@voH>l>U8BY`Q zC*G;&y6|!p=7`G$*+hxCv!@^#+QD3m>^azyZoLS^;o_|plQaj-wx^ zRV&$HcY~p)2|Zqp0SYU?W3zV87s6JP-@D~$t0 zvd;-YL~JWc*8mtHz_s(cXus#XYJc5zdC=&!4MeZ;N3TQ>^I|Pd=HPjVP*j^45rs(n zzB{U4-44=oQ4rNN6@>qYVMH4|GmMIz#z@3UW-1_y#eNa+Q%(41oJ5i(DzvMO^%|?L z^r_+MZtw0DZ0=BT-@?hUtA)Ijk~Kh-N8?~X5%KnRH7cb!?Yrd8gtiEo!v{sGrQk{X zvV>h{8-DqTyuAxIE(hb}jMVtga$;FIrrKm>ye5t%M;p!jcH1(Bbux>4D#MVhgZGd> z=c=nVb%^9T?iDgM&9G(mV5xShc-lBLi*6RShenDqB%`-2;I*;IHg6>#ovKQ$M}dDb z<$USN%LMqa5_5DR7g7@(oAoQ%!~<1KSQr$rmS{UFQJs5&qBhgTEM_Y7|0Wv?fbP`z z)`8~=v;B)+>Jh`V*|$dTxKe`HTBkho^-!!K#@i{9FLn-XqX&fQcGsEAXp)BV7(`Lk zC{4&+Pe-0&<)C0kAa(MTnb|L;ZB5i|b#L1o;J)+?SV8T*U9$Vxhy}dm3%!A}SK9l_6(#5(e*>8|;4gNKk7o_%m_ zEaS=Z(ewk}hBJ>v`jtR=$pm_Wq3d&DU+6`BACU4%qdhH1o^m8hT2&j<4Z8!v=rMCk z-I*?48{2H*&+r<{2?wp$kh@L@=rj8c`EaS~J>W?)trc?zP&4bsNagS4yafuDoXpi5`!{BVqJ1$ZC3`pf$`LIZ(`0&Ik+!_Xa=NJW`R2 zd#Ntgwz`JVwC4A61$FZ&kP)-{T|rGO59`h#1enAa`cWxRR8bKVvvN6jBzAYePrc&5 z+*zr3en|LYB2>qJp479rEALk5d*X-dfKn6|kuNm;2-U2+P3_rma!nWjZQ-y*q3JS? zBE}zE-!1ZBR~G%v!$l#dZ*$UV4$7q}xct}=on+Ba8{b>Y9h*f-GW0D0o#vJ0%ALg( ztG2+AjWlG#d;myA(i&dh8Gp?y9HD@`CTaDAy?c&0unZ%*LbLIg4;m{Kc?)ws3^>M+ zt5>R)%KIJV*MRUg{0$#nW=Lj{#8?dD$yhjBOrAeR#4$H_Dc(eyA4dNjZEz1Xk+Bqt zB&pPl+?R{w8GPv%VI`x`IFOj320F1=cV4aq0(*()Tx!VVxCjua;)t}gTr=b?zY+U! zkb}xjXZ?hMJN{Hjw?w&?gz8Ow`htX z@}WG*_4<%ff8(!S6bf3)p+8h2!Rory>@aob$gY#fYJ=LiW0`+~l7GI%EX_=8 z{(;0&lJ%9)M9{;wty=XvHbIx|-$g4HFij`J$-z~`mW)*IK^MWVN+*>uTNqaDmi!M8 zurj6DGd)g1g(f`A-K^v)3KSOEoZXImXT06apJum-dO_%oR)z6Bam-QC&CNWh7kLOE zcxLdVjYLNO2V?IXWa-ys30Jbxw(Xm?U1{4kDs9`gZQHh8X{*w9=H&Zz&-6RL?uq#R zxN+k~JaL|gdsdvY_u6}}MHC?a@ElFeipA1Lud#M~)pp2SnG#K{a@tSpvXM;A8gz9> zRVDV5T1%%!LsNRDOw~LIuiAiKcj<%7WpgjP7G6mMU1#pFo6a-1>0I5ZdhxnkMX&#L z=Vm}?SDlb_LArobqpnU!WLQE*yVGWgs^4RRy4rrJwoUUWoA~ZJUx$mK>J6}7{CyC4 zv=8W)kKl7TmAnM%m;anEDPv5tzT{A{ON9#FPYF6c=QIc*OrPp96tiY&^Qs+#A1H>Y z<{XtWt2eDwuqM zQ_BI#UIP;2-olOL4LsZ`vTPv-eILtuB7oWosoSefWdM}BcP>iH^HmimR`G`|+9waCO z&M375o@;_My(qYvPNz;N8FBZaoaw3$b#x`yTBJLc8iIP z--la{bzK>YPP|@Mke!{Km{vT8Z4|#An*f=EmL34?!GJfHaDS#41j~8c5KGKmj!GTh&QIH+DjEI*BdbSS2~6VTt}t zhAwNQNT6%c{G`If3?|~Fp7iwee(LaUS)X9@I29cIb61} z$@YBq4hSplr&liE@ye!y&7+7n$fb+8nS~co#^n@oCjCwuKD61x$5|0ShDxhQES5MP z(gH|FO-s6#$++AxnkQR!3YMgKcF)!&aqr^a3^{gAVT`(tY9@tqgY7@ z>>ul3LYy`R({OY7*^Mf}UgJl(N7yyo$ag;RIpYHa_^HKx?DD`%Vf1D0s^ zjk#OCM5oSzuEz(7X`5u~C-Y~n4B}_3*`5B&8tEdND@&h;H{R`o%IFpIJ4~Kw!kUjehGT8W!CD7?d8sg_$KKp%@*dW)#fI1#R<}kvzBVpaog_2&W%c_jJfP` z6)wE+$3+Hdn^4G}(ymPyasc1<*a7s2yL%=3LgtZLXGuA^jdM^{`KDb%%}lr|ONDsl zy~~jEuK|XJ2y<`R{^F)Gx7DJVMvpT>gF<4O%$cbsJqK1;v@GKXm*9l3*~8^_xj*Gs z=Z#2VQ6`H@^~#5Pv##@CddHfm;lbxiQnqy7AYEH(35pTg^;u&J2xs-F#jGLuDw2%z z`a>=0sVMM+oKx4%OnC9zWdbpq*#5^yM;og*EQKpv`^n~-mO_vj=EgFxYnga(7jO?G z`^C87B4-jfB_RgN2FP|IrjOi;W9AM1qS}9W@&1a9Us>PKFQ9~YE!I~wTbl!m3$Th? z)~GjFxmhyyGxN}t*G#1^KGVXm#o(K0xJyverPe}mS=QgJ$#D}emQDw+dHyPu^&Uv> z4O=3gK*HLFZPBY|!VGq60Of6QrAdj`nj1h!$?&a;Hgaj{oo{l0P3TzpJK_q_eW8Ng zP6QF}1{V;xlolCs?pGegPoCSxx@bshb#3ng4Fkp4!7B0=&+1%187izf@}tvsjZ6{m z4;K>sR5rm97HJrJ`w}Y`-MZN$Wv2N%X4KW(N$v2@R1RkRJH2q1Ozs0H`@ zd5)X-{!{<+4Nyd=hQ8Wm3CCd}ujm*a?L79ztfT7@&(?B|!pU5&%9Rl!`i;suAg0+A zxb&UYpo-z}u6CLIndtH~C|yz&!OV_I*L;H#C7ie_5uB1fNRyH*<^d=ww=gxvE%P$p zRHKI{^{nQlB9nLhp9yj-so1is{4^`{Xd>Jl&;dX;J)#- z=fmE5GiV?-&3kcjM1+XG7&tSq;q9Oi4NUuRrIpoyp*Fn&nVNFdUuGQ_g)g>VzXGdneB7`;!aTUE$t* z5iH+8XPxrYl)vFo~+vmcU-2) zq!6R(T0SsoDnB>Mmvr^k*{34_BAK+I=DAGu){p)(ndZqOFT%%^_y;X(w3q-L``N<6 zw9=M zoQ8Lyp>L_j$T20UUUCzYn2-xdN}{e@$8-3vLDN?GbfJ>7*qky{n!wC#1NcYQr~d51 zy;H!am=EI#*S&TCuP{FA3CO)b0AAiN*tLnDbvKwxtMw-l;G2T@EGH)YU?-B`+Y=!$ zypvDn@5V1Tr~y~U0s$ee2+CL3xm_BmxD3w}d_Pd@S%ft#v~_j;6sC6cy%E|dJy@wj z`+(YSh2CrXMxI;yVy*=O@DE2~i5$>nuzZ$wYHs$y`TAtB-ck4fQ!B8a;M=CxY^Nf{ z+UQhn0jopOzvbl(uZZ1R-(IFaprC$9hYK~b=57@ zAJ8*pH%|Tjotzu5(oxZyCQ{5MAw+6L4)NI!9H&XM$Eui-DIoDa@GpNI=I4}m>Hr^r zZjT?xDOea}7cq+TP#wK1p3}sbMK{BV%(h`?R#zNGIP+7u@dV5#zyMau+w}VC1uQ@p zrFUjrJAx6+9%pMhv(IOT52}Dq{B9njh_R`>&j&5Sbub&r*hf4es)_^FTYdDX$8NRk zMi=%I`)hN@N9>X&Gu2RmjKVsUbU>TRUM`gwd?CrL*0zxu-g#uNNnnicYw=kZ{7Vz3 zULaFQ)H=7%Lm5|Z#k?<{ux{o4T{v-e zTLj?F(_qp{FXUzOfJxEyKO15Nr!LQYHF&^jMMBs z`P-}WCyUYIv>K`~)oP$Z85zZr4gw>%aug1V1A)1H(r!8l&5J?ia1x_}Wh)FXTxZUE zs=kI}Ix2cK%Bi_Hc4?mF^m`sr6m8M(n?E+k7Tm^Gn}Kf= zfnqoyVU^*yLypz?s+-XV5(*oOBwn-uhwco5b(@B(hD|vtT8y7#W{>RomA_KchB&Cd zcFNAD9mmqR<341sq+j+2Ra}N5-3wx5IZqg6Wmi6CNO#pLvYPGNER}Q8+PjvIJ42|n zc5r@T*p)R^U=d{cT2AszQcC6SkWiE|hdK)m{7ul^mU+ED1R8G#)#X}A9JSP_ubF5p z8Xxcl;jlGjPwow^p+-f_-a~S;$lztguPE6SceeUCfmRo=Qg zKHTY*O_ z;pXl@z&7hniVYVbGgp+Nj#XP^Aln2T!D*{(Td8h{8Dc?C)KFfjPybiC`Va?Rf)X>y z;5?B{bAhPtbmOMUsAy2Y0RNDQ3K`v`gq)#ns_C&ec-)6cq)d^{5938T`Sr@|7nLl; zcyewuiSUh7Z}q8iIJ@$)L3)m)(D|MbJm_h&tj^;iNk%7K-YR}+J|S?KR|29K?z-$c z<+C4uA43yfSWBv*%z=-0lI{ev`C6JxJ};A5N;lmoR(g{4cjCEn33 z-ef#x^uc%cM-f^_+*dzE?U;5EtEe;&8EOK^K}xITa?GH`tz2F9N$O5;)`Uof4~l+t z#n_M(KkcVP*yMYlk_~5h89o zlf#^qjYG8Wovx+f%x7M7_>@r7xaXa2uXb?_*=QOEe_>ErS(v5-i)mrT3&^`Oqr4c9 zDjP_6T&NQMD`{l#K&sHTm@;}ed_sQ88X3y`ON<=$<8Qq{dOPA&WAc2>EQ+U8%>yWR zK%(whl8tB;{C)yRw|@Gn4%RhT=bbpgMZ6erACc>l5^p)9tR`(2W-D*?Ph6;2=Fr|G- zdF^R&aCqyxqWy#P7#G8>+aUG`pP*ow93N=A?pA=aW0^^+?~#zRWcf_zlKL8q8-80n zqGUm=S8+%4_LA7qrV4Eq{FHm9#9X15%ld`@UKyR7uc1X*>Ebr0+2yCye6b?i=r{MPoqnTnYnq z^?HWgl+G&@OcVx4$(y;{m^TkB5Tnhx2O%yPI=r*4H2f_6Gfyasq&PN^W{#)_Gu7e= zVHBQ8R5W6j;N6P3O(jsRU;hkmLG(Xs_8=F&xh@`*|l{~0OjUVlgm z7opltSHg7Mb%mYamGs*v1-#iW^QMT**f+Nq*AzIvFT~Ur3KTD26OhIw1WQsL(6nGg znHUo-4e15cXBIiyqN};5ydNYJ6zznECVVR44%(P0oW!yQ!YH)FPY?^k{IrtrLo7Zo`?sg%%oMP9E^+H@JLXicr zi?eoI?LODRPcMLl90MH32rf8btf69)ZE~&4d%(&D{C45egC6bF-XQ;6QKkbmqW>_H z{86XDZvjiN2wr&ZPfi;^SM6W+IP0);50m>qBhzx+docpBkkiY@2bSvtPVj~E`CfEu zhQG5G>~J@dni5M5Jmv7GD&@%UR`k3ru-W$$onI259jM&nZ)*d3QFF?Mu?{`+nVzkx z=R*_VH=;yeU?9TzQ3dP)q;P)4sAo&k;{*Eky1+Z!10J<(cJC3zY9>bP=znA=<-0RR zMnt#<9^X7BQ0wKVBV{}oaV=?JA=>R0$az^XE%4WZcA^Em>`m_obQyKbmf-GA;!S-z zK5+y5{xbkdA?2NgZ0MQYF-cfOwV0?3Tzh8tcBE{u%Uy?Ky4^tn^>X}p>4&S(L7amF zpWEio8VBNeZ=l!%RY>oVGOtZh7<>v3?`NcHlYDPUBRzgg z0OXEivCkw<>F(>1x@Zk=IbSOn+frQ^+jI*&qdtf4bbydk-jgVmLAd?5ImK+Sigh?X zgaGUlbf^b-MH2@QbqCawa$H1Vb+uhu{zUG9268pa{5>O&Vq8__Xk5LXDaR1z$g;s~;+Ae82wq#l;wo08tX(9uUX6NJWq1vZLh3QbP$# zL`udY|Qp*4ER`_;$%)2 zmcJLj|FD`(;ts0bD{}Ghq6UAVpEm#>j`S$wHi0-D_|)bEZ}#6) zIiqH7Co;TB`<6KrZi1SF9=lO+>-_3=Hm%Rr7|Zu-EzWLSF{9d(H1v*|UZDWiiqX3} zmx~oQ6%9~$=KjPV_ejzz7aPSvTo+3@-a(OCCoF_u#2dHY&I?`nk zQ@t8#epxAv@t=RUM09u?qnPr6=Y5Pj;^4=7GJ`2)Oq~H)2V)M1sC^S;w?hOB|0zXT zQdf8$)jslO>Q}(4RQ$DPUF#QUJm-k9ysZFEGi9xN*_KqCs9Ng(&<;XONBDe1Joku? z*W!lx(i&gvfXZ4U(AE@)c0FI2UqrFLOO$&Yic|`L;Vyy-kcm49hJ^Mj^H9uY8Fdm2 z?=U1U_5GE_JT;Tx$2#I3rAAs(q@oebIK=19a$N?HNQ4jw0ljtyGJ#D}z3^^Y=hf^Bb--297h6LQxi0-`TB|QY2QPg92TAq$cEQdWE ze)ltSTVMYe0K4wte6;^tE+^>|a>Hit_3QDlFo!3Jd`GQYTwlR#{<^MzG zK!vW&))~RTKq4u29bc<+VOcg7fdorq-kwHaaCQe6tLB{|gW1_W_KtgOD0^$^|`V4C# z*D_S9Dt_DIxpjk3my5cBFdiYaq||#0&0&%_LEN}BOxkb3v*d$4L|S|z z!cZZmfe~_Y`46v=zul=aixZTQCOzb(jx>8&a%S%!(;x{M2!*$od2!Pwfs>RZ-a%GOZdO88rS)ZW~{$656GgW)$Q=@!x;&Nn~!K)lr4gF*%qVO=hlodHA@2)keS2 zC}7O=_64#g&=zY?(zhzFO3)f5=+`dpuyM!Q)zS&otpYB@hhn$lm*iK2DRt+#1n|L%zjM}nB*$uAY^2JIw zV_P)*HCVq%F))^)iaZD#R9n^{sAxBZ?Yvi1SVc*`;8|F2X%bz^+s=yS&AXjysDny)YaU5RMotF-tt~FndTK ziRve_5b!``^ZRLG_ks}y_ye0PKyKQSsQCJuK5()b2ThnKPFU?An4;dK>)T^4J+XjD zEUsW~H?Q&l%K4<1f5^?|?lyCQe(O3?!~OU{_Wxs#|Ff8?a_WPQUKvP7?>1()Cy6oLeA zjEF^d#$6Wb${opCc^%%DjOjll%N2=GeS6D-w=Ap$Ux2+0v#s#Z&s6K*)_h{KFfgKjzO17@p1nKcC4NIgt+3t}&}F z@cV; zZ1r#~?R@ZdSwbFNV(fFl2lWI(Zf#nxa<6f!nBZD>*K)nI&Fun@ngq@Ge!N$O< zySt*mY&0moUXNPe~Fg=%gIu)tJ;asscQ!-AujR@VJBRoNZNk;z4hs4T>Ud!y=1NwGs-k zlTNeBOe}=)Epw=}+dfX;kZ32h$t&7q%Xqdt-&tlYEWc>>c3(hVylsG{Ybh_M8>Cz0ZT_6B|3!_(RwEJus9{;u-mq zW|!`{BCtnao4;kCT8cr@yeV~#rf76=%QQs(J{>Mj?>aISwp3{^BjBO zLV>XSRK+o=oVDBnbv?Y@iK)MiFSl{5HLN@k%SQZ}yhPiu_2jrnI?Kk?HtCv>wN$OM zSe#}2@He9bDZ27hX_fZey=64#SNU#1~=icK`D>a;V-&Km>V6ZdVNj7d2 z-NmAoOQm_aIZ2lXpJhlUeJ95eZt~4_S zIfrDs)S$4UjyxKSaTi#9KGs2P zfSD>(y~r+bU4*#|r`q+be_dopJzKK5JNJ#rR978ikHyJKD>SD@^Bk$~D0*U38Y*IpYcH>aaMdZq|YzQ-Ixd(_KZK!+VL@MWGl zG!k=<%Y-KeqK%``uhx}0#X^@wS+mX@6Ul@90#nmYaKh}?uw>U;GS4fn3|X%AcV@iY z8v+ePk)HxSQ7ZYDtlYj#zJ?5uJ8CeCg3efmc#|a%2=u>+vrGGRg$S@^mk~0f;mIu! zWMA13H1<@hSOVE*o0S5D8y=}RiL#jQpUq42D}vW$z*)VB*FB%C?wl%(3>ANaY)bO@ zW$VFutemwy5Q*&*9HJ603;mJJkB$qp6yxNOY0o_4*y?2`qbN{m&*l{)YMG_QHXXa2 z+hTmlA;=mYwg{Bfusl zyF&}ib2J;#q5tN^e)D62fWW*Lv;Rnb3GO-JVtYG0CgR4jGujFo$Waw zSNLhc{>P~>{KVZE1Vl1!z)|HFuN@J7{`xIp_)6>*5Z27BHg6QIgqLqDJTmKDM+ON* zK0Fh=EG`q13l z+m--9UH0{ZGQ%j=OLO8G2WM*tgfY}bV~>3Grcrpehjj z6Xe<$gNJyD8td3EhkHjpKk}7?k55Tu7?#;5`Qcm~ki;BeOlNr+#PK{kjV>qfE?1No zMA07}b>}Dv!uaS8Hym0TgzxBxh$*RX+Fab6Gm02!mr6u}f$_G4C|^GSXJMniy^b`G z74OC=83m0G7L_dS99qv3a0BU({t$zHQsB-RI_jn1^uK9ka_%aQuE2+~J2o!7`735Z zb?+sTe}Gd??VEkz|KAPMfj(1b{om89p5GIJ^#Aics_6DD%WnNGWAW`I<7jT|Af|8g zZA0^)`p8i#oBvX2|I&`HC8Pn&0>jRuMF4i0s=}2NYLmgkZb=0w9tvpnGiU-gTUQhJ zR6o4W6ZWONuBZAiN77#7;TR1^RKE(>>OL>YU`Yy_;5oj<*}ac99DI(qGCtn6`949f ziMpY4k>$aVfffm{dNH=-=rMg|u?&GIToq-u;@1-W&B2(UOhC-O2N5_px&cF-C^tWp zXvChm9@GXEcxd;+Q6}u;TKy}$JF$B`Ty?|Y3tP$N@Rtoy(*05Wj-Ks32|2y2ZM>bM zi8v8E1os!yorR!FSeP)QxtjIKh=F1ElfR8U7StE#Ika;h{q?b?Q+>%78z^>gTU5+> zxQ$a^rECmETF@Jl8fg>MApu>btHGJ*Q99(tMqsZcG+dZ6Yikx7@V09jWCiQH&nnAv zY)4iR$Ro223F+c3Q%KPyP9^iyzZsP%R%-i^MKxmXQHnW6#6n7%VD{gG$E;7*g86G< zu$h=RN_L2(YHO3@`B<^L(q@^W_0#U%mLC9Q^XEo3LTp*~(I%?P_klu-c~WJxY1zTI z^PqntLIEmdtK~E-v8yc&%U+jVxW5VuA{VMA4Ru1sk#*Srj0Pk#tZuXxkS=5H9?8eb z)t38?JNdP@#xb*yn=<*_pK9^lx%;&yH6XkD6-JXgdddZty8@Mfr9UpGE!I<37ZHUe z_Rd+LKsNH^O)+NW8Ni-V%`@J_QGKA9ZCAMSnsN>Ych9VW zCE7R_1FVy}r@MlkbxZ*TRIGXu`ema##OkqCM9{wkWQJg^%3H${!vUT&vv2250jAWN zw=h)C!b2s`QbWhBMSIYmWqZ_~ReRW;)U#@C&ThctSd_V!=HA=kdGO-Hl57an|M1XC?~3f0{7pyjWY}0mChU z2Fj2(B*r(UpCKm-#(2(ZJD#Y|Or*Vc5VyLpJ8gO1;fCm@EM~{DqpJS5FaZ5%|ALw) zyumBl!i@T57I4ITCFmdbxhaOYud}i!0YkdiNRaQ%5$T5>*HRBhyB~<%-5nj*b8=i= z(8g(LA50%0Zi_eQe}Xypk|bt5e6X{aI^jU2*c?!p*$bGk=?t z+17R){lx~Z{!B34Zip~|A;8l@%*Gc}kT|kC0*Ny$&fI3@%M! zqk_zvN}7bM`x@jqFOtaxI?*^Im5ix@=`QEv;__i;Tek-&7kGm6yP17QANVL>*d0B=4>i^;HKb$k8?DYFMr38IX4azK zBbwjF%$>PqXhJh=*7{zH5=+gi$!nc%SqFZlwRm zmpctOjZh3bwt!Oc>qVJhWQf>`HTwMH2ibK^eE*j!&Z`-bs8=A`Yvnb^?p;5+U=Fb8 z@h>j_3hhazd$y^Z-bt%3%E3vica%nYnLxW+4+?w{%|M_=w^04U{a6^22>M_?{@mXP zS|Qjcn4&F%WN7Z?u&I3fU(UQVw4msFehxR*80dSb=a&UG4zDQp&?r2UGPy@G?0FbY zVUQ?uU9-c;f9z06$O5FO1TOn|P{pLcDGP?rfdt`&uw|(Pm@$n+A?)8 zP$nG(VG&aRU*(_5z#{+yVnntu`6tEq>%9~n^*ao}`F6ph_@6_8|AfAXtFfWee_14` zKKURYV}4}=UJmxv7{RSz5QlwZtzbYQs0;t3?kx*7S%nf-aY&lJ@h?-BAn%~0&&@j) zQd_6TUOLXErJ`A3vE?DJIbLE;s~s%eVt(%fMzUq^UfZV9c?YuhO&6pwKt>j(=2CkgTNEq7&c zfeGN+%5DS@b9HO>zsoRXv@}(EiA|t5LPi}*R3?(-=iASADny<{D0WiQG>*-BSROk4vI6%$R>q64J&v-T+(D<_(b!LD z9GL;DV;;N3!pZYg23mcg81tx>7)=e%f|i{6Mx0GczVpc}{}Mg(W_^=Wh0Rp+xXgX` z@hw|5=Je&nz^Xa>>vclstYt;8c2PY)87Ap;z&S&`yRN>yQVV#K{4&diVR7Rm;S{6m z6<+;jwbm`==`JuC6--u6W7A@o4&ZpJV%5+H)}toy0afF*!)AaG5=pz_i9}@OG%?$O z2cec6#@=%xE3K8;^ps<2{t4SnqH+#607gAHP-G4^+PBiC1s>MXf&bQ|Pa;WBIiErV z?3VFpR9JFl9(W$7p3#xe(Bd?Z93Uu~jHJFo7U3K_x4Ej-=N#=a@f;kPV$>;hiN9i9 z<6elJl?bLI$o=|d6jlihA4~bG;Fm2eEnlGxZL`#H%Cdes>uJfMJ4>@1SGGeQ81DwxGxy7L5 zm05Ik*WpSgZvHh@Wpv|2i|Y#FG?Y$hbRM5ZF0Z7FB3cY0+ei#km9mDSPI}^!<<`vr zuv$SPg2vU{wa)6&QMY)h1hbbxvR2cc_6WcWR`SH& z&KuUQcgu}!iW2Wqvp~|&&LSec9>t(UR_|f$;f-fC&tSO-^-eE0B~Frttnf+XN(#T) z^PsuFV#(pE#6ztaI8(;ywN%CtZh?w&;_)w_s@{JiA-SMjf&pQk+Bw<}f@Q8-xCQMwfaf zMgHsAPU=>>Kw~uDFS(IVRN{$ak(SV(hrO!UqhJ?l{lNnA1>U24!=>|q_p404Xd>M# z7?lh^C&-IfeIr`Dri9If+bc%oU0?|Rh8)%BND5;_9@9tuM)h5Kcw6}$Ca7H_n)nOf0pd`boCXItb`o11 zb`)@}l6I_h>n+;`g+b^RkYs7;voBz&Gv6FLmyvY|2pS)z#P;t8k;lS>49a$XeVDc4 z(tx2Pe3N%Gd(!wM`E7WRBZy)~vh_vRGt&esDa0NCua)rH#_39*H0!gIXpd>~{rGx+ zJKAeXAZ-z5n=mMVqlM5Km;b;B&KSJlScD8n?2t}kS4Wf9@MjIZSJ2R?&=zQn zs_`=+5J$47&mP4s{Y{TU=~O_LzSrXvEP6W?^pz<#Y*6Fxg@$yUGp31d(h+4x>xpb< zH+R639oDST6F*0iH<9NHC^Ep*8D4-%p2^n-kD6YEI<6GYta6-I;V^ZH3n5}syTD=P z3b6z=jBsdP=FlXcUe@I|%=tY4J_2j!EVNEzph_42iO3yfir|Dh>nFl&Lu9!;`!zJB zCis9?_(%DI?$CA(00pkzw^Up`O;>AnPc(uE$C^a9868t$m?5Q)CR%!crI$YZpiYK6m= z!jv}82He`QKF;10{9@roL2Q7CF)OeY{~dBp>J~X#c-Z~{YLAxNmn~kWQW|2u!Yq00 zl5LKbzl39sVCTpm9eDW_T>Z{x@s6#RH|P zA~_lYas7B@SqI`N=>x50Vj@S)QxouKC(f6Aj zz}7e5e*5n?j@GO;mCYEo^Jp_*BmLt3!N)(T>f#L$XHQWzZEVlJo(>qH@7;c%fy zS-jm^Adju9Sm8rOKTxfTU^!&bg2R!7C_-t+#mKb_K?0R72%26ASF;JWA_prJ8_SVW zOSC7C&CpSrgfXRp8r)QK34g<~!1|poTS7F;)NseFsbwO$YfzEeG3oo!qe#iSxQ2S# z1=Fxc9J;2)pCab-9o-m8%BLjf(*mk#JJX3k9}S7Oq)dV0jG)SOMbw7V^Z<5Q0Cy$< z^U0QUVd4(96W03OA1j|x%{sd&BRqIERDb6W{u1p1{J(a;fd6lnWzjeS`d?L3-0#o7 z{Qv&L7!Tm`9|}u=|IbwS_jgH(_V@o`S*R(-XC$O)DVwF~B&5c~m!zl14ydT6sK+Ly zn+}2hQ4RTC^8YvrQ~vk$f9u=pTN{5H_yTOcza9SVE&nt_{`ZC8zkmFji=UyD`G4~f zUfSTR=Kju>6u+y&|Bylb*W&^P|8fvEbQH3+w*DrKq|9xMzq2OiZyM=;(?>~4+O|jn zC_Et05oc>e%}w4ye2Fm%RIR??VvofwZS-}BL@X=_4jdHp}FlMhW_IW?Zh`4$z*Wr!IzQHa3^?1|);~VaWmsIcmc6 zJs{k0YW}OpkfdoTtr4?9F6IX6$!>hhA+^y_y@vvA_Gr7u8T+i-< zDX(~W5W{8mfbbM-en&U%{mINU#Q8GA`byo)iLF7rMVU#wXXY`a3ji3m{4;x53216i z`zA8ap?>_}`tQj7-%$K78uR}R$|@C2)qgop$}o=g(jOv0ishl!E(R73N=i0~%S)6+ z1xFP7|H0yt3Z_Re*_#C2m3_X{=zi1C&3CM7e?9-Y5lCtAlA%RFG9PDD=Quw1dfYnZ zdUL)#+m`hKx@PT`r;mIx_RQ6Txbti+&;xQorP;$H=R2r)gPMO9>l+!p*Mt04VH$$M zSLwJ81IFjQ5N!S#;MyBD^IS`2n04kuYbZ2~4%3%tp0jn^**BZQ05ELp zY%yntZ=52s6U5Y93Aao)v~M3y?6h7mZcVGp63pK*d&!TRjW99rUU;@s#3kYB76Bs$|LRwkH>L!0Xe zE=dz1o}phhnOVYZFsajQsRA^}IYZnk9Wehvo>gHPA=TPI?2A`plIm8=F1%QiHx*Zn zi)*Y@)$aXW0v1J|#+R2=$ysooHZ&NoA|Wa}htd`=Eud!(HD7JlT8ug|yeBZmpry(W z)pS>^1$N#nuo3PnK*>Thmaxz4pLcY?PP2r3AlhJ7jw(TI8V#c}>Ym;$iPaw+83L+* z!_QWpYs{UWYcl0u z(&(bT0Q*S_uUX9$jC;Vk%oUXw=A-1I+!c18ij1CiUlP@pfP9}CHAVm{!P6AEJ(7Dn z?}u#}g`Q?`*|*_0Rrnu8{l4PP?yCI28qC~&zlwgLH2AkfQt1?B#3AOQjW&10%@@)Q zDG?`6$8?Nz(-sChL8mRs#3z^uOA>~G=ZIG*mgUibWmgd{a|Tn4nkRK9O^37E(()Q% zPR0#M4e2Q-)>}RSt1^UOCGuv?dn|IT3#oW_$S(YR+jxAzxCD_L25p_dt|^>g+6Kgj zJhC8n)@wY;Y7JI6?wjU$MQU|_Gw*FIC)x~^Eq1k41BjLmr}U>6#_wxP0-2Ka?uK14u5M-lAFSX$K1K{WH!M1&q}((MWWUp#Uhl#n_yT5dFs4X`>vmM& z*1!p0lACUVqp&sZG1GWATvZEENs^0_7Ymwem~PlFN3hTHVBv(sDuP;+8iH07a)s(# z%a7+p1QM)YkS7>kbo${k2N1&*%jFP*7UABJ2d||c!eSXWM*<4(_uD7;1XFDod@cT$ zP>IC%^fbC${^QrUXy$f)yBwY^g@}}kngZKa1US!lAa+D=G4wklukaY8AEW%GL zh40pnuv*6D>9`_e14@wWD^o#JvxYVG-~P)+<)0fW zP()DuJN?O*3+Ab!CP-tGr8S4;JN-Ye^9D%(%8d{vb_pK#S1z)nZzE^ezD&%L6nYbZ z*62>?u)xQe(Akd=e?vZbyb5)MMNS?RheZDHU?HK<9;PBHdC~r{MvF__%T)-9ifM#cR#2~BjVJYbA>xbPyl9yNX zX)iFVvv-lfm`d?tbfh^j*A|nw)RszyD<#e>llO8X zou=q3$1|M@Ob;F|o4H0554`&y9T&QTa3{yn=w0BLN~l;XhoslF-$4KGNUdRe?-lcV zS4_WmftU*XpP}*wFM^oKT!D%_$HMT#V*j;9weoOq0mjbl1271$F)`Q(C z76*PAw3_TE{vntIkd=|(zw)j^!@j ^tV@s0U~V+mu)vv`xgL$Z9NQLnuRdZ;95D|1)!0Aybwv}XCE#xz1k?ZC zxAU)v@!$Sm*?)t2mWrkevNFbILU9&znoek=d7jn*k+~ptQ)6z`h6e4B&g?Q;IK+aH z)X(BH`n2DOS1#{AJD-a?uL)@Vl+`B=6X3gF(BCm>Q(9+?IMX%?CqgpsvK+b_de%Q> zj-GtHKf!t@p2;Gu*~#}kF@Q2HMevg~?0{^cPxCRh!gdg7MXsS}BLtG_a0IY0G1DVm z2F&O-$Dzzc#M~iN`!j38gAn`6*~h~AP=s_gy2-#LMFoNZ0<3q+=q)a|4}ur7F#><%j1lnr=F42Mbti zi-LYs85K{%NP8wE1*r4Mm+ZuZ8qjovmB;f##!E*M{*A(4^~vg!bblYi1M@7tq^L8- zH7tf_70iWXqcSQgENGdEjvLiSLicUi3l0H*sx=K!!HLxDg^K|s1G}6Tam|KBV>%YeU)Q>zxQe;ddnDTWJZ~^g-kNeycQ?u242mZs`i8cP)9qW`cwqk)Jf?Re0=SD=2z;Gafh(^X-=WJ$i7Z9$Pao56bTwb+?p>L3bi9 zP|qi@;H^1iT+qnNHBp~X>dd=Us6v#FPDTQLb9KTk%z{&OWmkx3uY(c6JYyK3w|z#Q zMY%FPv%ZNg#w^NaW6lZBU+}Znwc|KF(+X0RO~Q6*O{T-P*fi@5cPGLnzWMSyoOPe3 z(J;R#q}3?z5Ve%crTPZQFLTW81cNY-finw!LH9wr$(C)p_@v?(y#b-R^Pv!}_#7t+A?pHEUMY zoQZIwSETTKeS!W{H$lyB1^!jn4gTD{_mgG?#l1Hx2h^HrpCXo95f3utP-b&%w80F} zXFs@Jp$lbIL64@gc?k*gJ;OForPaapOH7zNMB60FdNP<*9<@hEXJk9Rt=XhHR-5_$Ck-R?+1py&J3Y9^sBBZuj?GwSzua;C@9)@JZpaI zE?x6{H8@j9P06%K_m%9#nnp0Li;QAt{jf-7X%Pd2jHoI4As-9!UR=h6Rjc z!3{UPWiSeLG&>1V5RlM@;5HhQW_&-wL2?%k@dvRS<+@B6Yaj*NG>qE5L*w~1ATP$D zmWu6(OE=*EHqy{($~U4zjxAwpPn42_%bdH9dMphiUU|) z*+V@lHaf%*GcXP079>vy5na3h^>X=n;xc;VFx)`AJEk zYZFlS#Nc-GIHc}j06;cOU@ zAD7Egkw<2a8TOcfO9jCp4U4oI*`|jpbqMWo(={gG3BjuM3QTGDG`%y|xithFck}0J zG}N#LyhCr$IYP`#;}tdm-7^9=72+CBfBsOZ0lI=LC_a%U@(t3J_I1t(UdiJ^@NubM zvvA0mGvTC%{fj53M^|Ywv$KbW;n8B-x{9}Z!K6v-tw&Xe_D2{7tX?eVk$sA*0826( zuGz!K7$O#;K;1w<38Tjegl)PmRso`fc&>fAT5s z7hzQe-_`lx`}2=c)jz6;yn(~F6#M@z_7@Z(@GWbIAo6A2&;aFf&>CVHpqoPh5#~=G zav`rZ3mSL2qwNL+Pg>aQv;%V&41e|YU$!fQ9Ksle!XZERpjAowHtX zi#0lnw{(zmk&}t`iFEMmx-y7FWaE*vA{Hh&>ieZg{5u0-3@a8BY)Z47E`j-H$dadu zIP|PXw1gjO@%aSz*O{GqZs_{ke|&S6hV{-dPkl*V|3U4LpqhG0eVdqfeNX28hrafI zE13WOsRE|o?24#`gQJs@v*EwL{@3>Ffa;knvI4@VEG2I>t-L(KRS0ShZ9N!bwXa}e zI0}@2#PwFA&Y9o}>6(ZaSaz>kw{U=@;d{|dYJ~lyjh~@bBL>n}#@KjvXUOhrZ`DbnAtf5bz3LD@0RpmAyC-4cgu<7rZo&C3~A_jA*0)v|Ctcdu} zt@c7nQ6hSDC@76c4hI&*v|5A0Mj4eQ4kVb0$5j^*$@psB zdouR@B?l6E%a-9%i(*YWUAhxTQ(b@z&Z#jmIb9`8bZ3Um3UW!@w4%t0#nxsc;*YrG z@x$D9Yj3EiA(-@|IIzi@!E$N)j?gedGJpW!7wr*7zKZwIFa>j|cy<(1`VV_GzWN=1 zc%OO)o*RRobvTZE<9n1s$#V+~5u8ZwmDaysD^&^cxynksn!_ypmx)Mg^8$jXu5lMo zK3K_8GJh#+7HA1rO2AM8cK(#sXd2e?%3h2D9GD7!hxOEKJZK&T`ZS0e*c9c36Y-6yz2D0>Kvqy(EuiQtUQH^~M*HY!$e z20PGLb2Xq{3Ceg^sn+99K6w)TkprP)YyNU(+^PGU8}4&Vdw*u;(`Bw!Um76gL_aMT z>*82nmA8Tp;~hwi0d3S{vCwD};P(%AVaBr=yJ zqB?DktZ#)_VFh_X69lAHQw(ZNE~ZRo2fZOIP;N6fD)J*3u^YGdgwO(HnI4pb$H#9) zizJ<>qI*a6{+z=j+SibowDLKYI*Je2Y>~=*fL@i*f&8**s~4l&B&}$~nwhtbOTr=G zFx>{y6)dpJPqv={_@*!q0=jgw3^j`qi@!wiWiT_$1`SPUgaG&9z9u9=m5C8`GpMaM zyMRSv2llS4F}L?233!)f?mvcYIZ~U z7mPng^=p)@Z*Fp9owSYA`Fe4OjLiJ`rdM`-U(&z1B1`S`ufK_#T@_BvenxDQU`deH$X5eMVO=;I4EJjh6?kkG2oc6AYF6|(t)L0$ukG}Zn=c+R`Oq;nC)W^ z{ek!A?!nCsfd_5>d&ozG%OJmhmnCOtARwOq&p!FzWl7M))YjqK8|;6sOAc$w2%k|E z`^~kpT!j+Y1lvE0B)mc$Ez_4Rq~df#vC-FmW;n#7E)>@kMA6K30!MdiC19qYFnxQ* z?BKegU_6T37%s`~Gi2^ewVbciy-m5%1P3$88r^`xN-+VdhhyUj4Kzg2 zlKZ|FLUHiJCZL8&<=e=F2A!j@3D@_VN%z?J;uw9MquL`V*f^kYTrpoWZ6iFq00uO+ zD~Zwrs!e4cqGedAtYxZ76Bq3Ur>-h(m1~@{x@^*YExmS*vw9!Suxjlaxyk9P#xaZK z)|opA2v#h=O*T42z>Mub2O3Okd3GL86KZM2zlfbS z{Vps`OO&3efvt->OOSpMx~i7J@GsRtoOfQ%vo&jZ6^?7VhBMbPUo-V^Znt%-4k{I# z8&X)=KY{3lXlQg4^FH^{jw0%t#2%skLNMJ}hvvyd>?_AO#MtdvH;M^Y?OUWU6BdMX zJ(h;PM9mlo@i)lWX&#E@d4h zj4Z0Czj{+ipPeW$Qtz_A52HA<4$F9Qe4CiNQSNE2Q-d1OPObk4?7-&`={{yod5Iy3kB=PK3%0oYSr`Gca120>CHbC#SqE*ivL2R(YmI1A|nAT?JmK*2qj_3p#?0h)$#ixdmP?UejCg9%AS2 z8I(=_QP(a(s)re5bu-kcNQc-&2{QZ%KE*`NBx|v%K2?bK@Ihz_e<5Y(o(gQ-h+s&+ zjpV>uj~?rfJ!UW5Mop~ro^|FP3Z`@B6A=@f{Wn78cm`)3&VJ!QE+P9&$;3SDNH>hI z_88;?|LHr%1kTX0t*xzG-6BU=LRpJFZucRBQ<^zy?O5iH$t>o}C}Fc+kM1EZu$hm% zTTFKrJkXmCylFgrA;QAA(fX5Sia5TNo z?=Ujz7$Q?P%kM$RKqRQisOexvV&L+bolR%`u`k;~!o(HqgzV9I6w9|g*5SVZN6+kT9H$-3@%h%k7BBnB zPn+wmPYNG)V2Jv`&$LoI*6d0EO^&Nh`E* z&1V^!!Szd`8_uf%OK?fuj~! z%p9QLJ?V*T^)72<6p1ONqpmD?Wm((40>W?rhjCDOz?#Ei^sXRt|GM3ULLnoa8cABQ zA)gCqJ%Q5J%D&nJqypG-OX1`JLT+d`R^|0KtfGQU+jw79la&$GHTjKF>*8BI z0}l6TC@XB6`>7<&{6WX2kX4k+0SaI`$I8{{mMHB}tVo*(&H2SmZLmW* z+P8N>(r}tR?f!O)?)df>HIu>$U~e~tflVmwk*+B1;TuqJ+q_^`jwGwCbCgSevBqj$ z<`Fj*izeO)_~fq%wZ0Jfvi6<3v{Afz;l5C^C7!i^(W>%5!R=Ic7nm(0gJ~9NOvHyA zqWH2-6w^YmOy(DY{VrN6ErvZREuUMko@lVbdLDq*{A+_%F>!@6Z)X9kR1VI1+Ler+ zLUPtth=u~23=CqZoAbQ`uGE_91kR(8Ie$mq1p`q|ilkJ`Y-ob_=Nl(RF=o7k{47*I)F%_XMBz9uwRH8q1o$TkV@8Pwl zzi`^7i;K6Ak7o58a_D-V0AWp;H8pSjbEs$4BxoJkkC6UF@QNL)0$NU;Wv0*5 z0Ld;6tm7eR%u=`hnUb)gjHbE2cP?qpo3f4w%5qM0J*W_Kl6&z4YKX?iD@=McR!gTyhpGGYj!ljQm@2GL^J70`q~4CzPv@sz`s80FgiuxjAZ zLq61rHv1O>>w1qOEbVBwGu4%LGS!!muKHJ#JjfT>g`aSn>83Af<9gM3XBdY)Yql|{ zUds}u*;5wuus)D>HmexkC?;R&*Z`yB4;k;4T*(823M&52{pOd1yXvPJ3PPK{Zs>6w zztXy*HSH0scZHn7qIsZ8y-zftJ*uIW;%&-Ka0ExdpijI&xInDg-Bv-Q#Islcbz+R! zq|xz?3}G5W@*7jSd`Hv9q^5N*yN=4?Lh=LXS^5KJC=j|AJ5Y(f_fC-c4YQNtvAvn|(uP9@5Co{dL z?7|=jqTzD8>(6Wr&(XYUEzT~-VVErf@|KeFpKjh=v51iDYN_`Kg&XLOIG;ZI8*U$@ zKig{dy?1H}UbW%3jp@7EVSD>6c%#abQ^YfcO(`)*HuvNc|j( zyUbYozBR15$nNU$0ZAE%ivo4viW?@EprUZr6oX=4Sc!-WvrpJdF`3SwopKPyX~F>L zJ>N>v=_plttTSUq6bYu({&rkq)d94m5n~Sk_MO*gY*tlkPFd2m=Pi>MK)ObVV@Sgs zmXMNMvvcAuz+<$GLR2!j4w&;{)HEkxl{$B^*)lUKIn&p5_huD6+%WDoH4`p}9mkw$ zXCPw6Y7tc%rn$o_vy>%UNBC`0@+Ih-#T05AT)ooKt?94^ROI5;6m2pIM@@tdT=&WP z{u09xEVdD}{(3v}8AYUyT82;LV%P%TaJa%f)c36?=90z>Dzk5mF2}Gs0jYCmufihid8(VFcZWs8#59;JCn{!tHu5kSBbm zL`F{COgE01gg-qcP2Lt~M9}mALg@i?TZp&i9ZM^G<3`WSDh}+Ceb3Q!QecJ|N;Xrs z{wH{D8wQ2+mEfBX#M8)-32+~q4MRVr1UaSPtw}`iwx@x=1Xv-?UT{t}w}W(J&WKAC zrZ%hssvf*T!rs}}#atryn?LB=>0U%PLwA9IQZt$$UYrSw`7++}WR7tfE~*Qg)vRrM zT;(1>Zzka?wIIz8vfrG86oc^rjM@P7^i8D~b(S23AoKYj9HBC(6kq9g`1gN@|9^xO z{~h zbxGMHqGZ@eJ17bgES?HQnwp|G#7I>@p~o2zxWkgZUYSUeB*KT{1Q z*J3xZdWt`eBsA}7(bAHNcMPZf_BZC(WUR5B8wUQa=UV^e21>|yp+uop;$+#JwXD!> zunhJVCIKgaol0AM_AwJNl}_k&q|uD?aTE@{Q*&hxZ=k_>jcwp}KwG6mb5J*pV@K+- zj*`r0WuEU_8O=m&1!|rj9FG7ad<2px63;Gl z9lJrXx$~mPnuiqIH&n$jSt*ReG}1_?r4x&iV#3e_z+B4QbhHwdjiGu^J3vcazPi`| zaty}NFSWe=TDry*a*4XB)F;KDI$5i9!!(5p@5ra4*iW;FlGFV0P;OZXF!HCQ!oLm1 zsK+rY-FnJ?+yTBd0}{*Y6su|hul)wJ>RNQ{eau*;wWM{vWM`d0dTC-}Vwx6@cd#P? zx$Qyk^2*+_ZnMC}q0)+hE-q)PKoox#;pc%DNJ&D5+if6X4j~p$A7-s&AjDkSEV)aM z(<3UOw*&f)+^5F0Mpzw3zB1ZHl*B?C~Cx) zuNg*>5RM9F5{EpU@a2E7hAE`m<89wbQ2Lz&?Egu-^sglNXG5Q;{9n(%&*kEb0vApd zRHrY@22=pkFN81%x)~acZeu`yvK zovAVJNykgxqkEr^hZksHkpxm>2I8FTu2%+XLs@?ym0n;;A~X>i32{g6NOB@o4lk8{ zB}7Z2MNAJi>9u=y%s4QUXaNdt@SlAZr54!S6^ETWoik6gw=k-itu_}Yl_M9!l+Rbv z(S&WD`{_|SE@@(|Wp7bq1Zq}mc4JAG?mr2WN~6}~u`7M_F@J9`sr0frzxfuqSF~mA z$m$(TWAuCIE99yLSwi%R)8geQhs;6VBlRhJb(4Cx zu)QIF%_W9+21xI45U>JknBRaZ9nYkgAcK6~E|Zxo!B&z9zQhjsi^fgwZI%K@rYbMq znWBXg1uCZ+ljGJrsW7@x3h2 z;kn!J!bwCeOrBx;oPkZ}FeP%wExyf4=XMp)N8*lct~SyfK~4^-75EZFpHYO5AnuRM z!>u?>Vj3+j=uiHc<=cD~JWRphDSwxFaINB42-{@ZJTWe85>-RcQ&U%?wK)vjz z5u5fJYkck##j(bP7W0*RdW#BmAIK`D3=(U~?b`cJ&U2jHj}?w6 z_4BM)#EoJ6)2?pcR4AqBd)qAUn@RtNQq})FIQoBK4ie+GB(Vih2D|Ds>RJo2zE~C- z7mI)7p)5(-O6JRh6a@VZ5~piVC+Xv=O-)=0eTMSJsRE^c1@bPQWlr}E31VqO-%739 zdcmE{`1m;5LH8w|7euK>>>U#Iod8l1yivC>;YWsg=z#07E%cU9x1yw#3l6AcIm%79 zGi^zH6rM#CZMow(S(8dcOq#5$kbHnQV6s?MRsU3et!!YK5H?OV9vf2qy-UHCn>}2d zTwI(A_fzmmCtE@10yAGgU7R&|Fl$unZJ_^0BgCEDE6(B*SzfkapE9#0N6adc>}dtH zJ#nt^F~@JMJg4=Pv}OdUHyPt-<<9Z&c0@H@^4U?KwZM&6q0XjXc$>K3c&3iXLD9_%(?)?2kmZ=Ykb;)M`Tw=%_d=e@9eheGG zk0<`4so}r={C{zr|6+_1mA_=a56(XyJq||g6Es1E6%fPg#l{r+vk9;)r6VB7D84nu zE0Z1EIxH{Y@}hT+|#$0xn+CdMy6Uhh80eK~nfMEIpM z`|G1v!USmx81nY8XkhEOSWto}pc#{Ut#`Pqb}9j$FpzkQ7`0<-@5D_!mrLah98Mpr zz(R7;ZcaR-$aKqUaO!j z=7QT;Bu0cvYBi+LDfE_WZ`e@YaE_8CCxoRc?Y_!Xjnz~Gl|aYjN2&NtT5v4#q3od2 zkCQZHe#bn(5P#J**Fj4Py%SaaAKJsmV6}F_6Z7V&n6QAu8UQ#9{gkq+tB=VF_Q6~^ zf(hXvhJ#tC(eYm6g|I>;55Lq-;yY*COpTp4?J}hGQ42MIVI9CgEC{3hYw#CZfFKVG zgD(steIg8veyqX%pYMoulq zMUmbj8I`t>mC`!kZ@A>@PYXy*@NprM@e}W2Q+s?XIRM-U1FHVLM~c60(yz1<46-*j zW*FjTnBh$EzI|B|MRU11^McTPIGVJrzozlv$1nah_|t4~u}Ht^S1@V8r@IXAkN;lH z_s|WHlN90k4X}*#neR5bX%}?;G`X!1#U~@X6bbhgDYKJK17~oFF0&-UB#()c$&V<0 z7o~Pfye$P@$)Lj%T;axz+G1L_YQ*#(qO zQND$QTz(~8EF1c3<%;>dAiD$>8j@7WS$G_+ktE|Z?Cx<}HJb=!aChR&4z ziD&FwsiZ)wxS4k6KTLn>d~!DJ^78yb>?Trmx;GLHrbCBy|Bip<@sWdAfP0I~;(Ybr zoc-@j?wA!$ zIP0m3;LZy+>dl#&Ymws@7|{i1+OFLYf@+8+)w}n?mHUBCqg2=-Hb_sBb?=q))N7Ej zDIL9%@xQFOA!(EQmchHiDN%Omrr;WvlPIN5gW;u#ByV)x2aiOd2smy&;vA2+V!u|D zc~K(OVI8} z0t|e0OQ7h23e01O;%SJ}Q#yeDh`|jZR7j-mL(T4E;{w^}2hzmf_6PF|`gWVj{I?^2T3MBK>{?nMXed4kgNox2DP!jvP9v`;pa6AV)OD zDt*Vd-x7s{-;E?E5}3p-V;Y#dB-@c5vTWfS7<=>E+tN$ME`Z7K$px@!%{5{uV`cH80|IzU! zDs9=$%75P^QKCRQ`mW7$q9U?mU@vrFMvx)NNDrI(uk>xwO;^($EUvqVev#{W&GdtR z0ew;Iwa}(-5D28zABlC{WnN{heSY5Eq5Fc=TN^9X#R}0z53!xP85#@;2E=&oNYHyo z46~#Sf!1M1X!rh}ioe`>G2SkPH{5nCoP`GT@}rH;-LP1Q7U_ypw4+lwsqiBql80aA zJE<(88yw$`xzNiSnU(hsyJqHGac<}{Av)x9lQ=&py9djsh0uc}6QkmKN3{P!TEy;P zzLDVQj4>+0r<9B0owxBt5Uz`!M_VSS|{(?`_e+qD9b=vZHoo6>?u;!IP zM7sqoyP>kWY|=v06gkhaGRUrO8n@zE?Yh8$om@8%=1}*!2wdIWsbrCg@;6HfF?TEN z+B_xtSvT6H3in#8e~jvD7eE|LTQhO_>3b823&O_l$R$CFvP@3~)L7;_A}JpgN@ax{ z2d9Ra)~Yh%75wsmHK8e87yAn-ZMiLo6#=<&PgdFsJw1bby-j&3%&4=9dQFltFR(VB z@=6XmyNN4yr^^o$ON8d{PQ=!OX17^CrdM~7D-;ZrC!||<+FEOxI_WI3 zCA<35va%4v>gcEX-@h8esj=a4szW7x z{0g$hwoWRQG$yK{@3mqd-jYiVofJE!Wok1*nV7Gm&Ssq#hFuvj1sRyHg(6PFA5U*Q z8Rx>-blOs=lb`qa{zFy&n4xY;sd$fE+<3EI##W$P9M{B3c3Si9gw^jlPU-JqD~Cye z;wr=XkV7BSv#6}DrsXWFJ3eUNrc%7{=^sP>rp)BWKA9<}^R9g!0q7yWlh;gr_TEOD|#BmGq<@IV;ue zg+D2}cjpp+dPf&Q(36sFU&K8}hA85U61faW&{lB`9HUl-WWCG|<1XANN3JVAkRYvr5U z4q6;!G*MTdSUt*Mi=z_y3B1A9j-@aK{lNvxK%p23>M&=KTCgR!Ee8c?DAO2_R?Bkaqr6^BSP!8dHXxj%N1l+V$_%vzHjq zvu7p@%Nl6;>y*S}M!B=pz=aqUV#`;h%M0rUHfcog>kv3UZAEB*g7Er@t6CF8kHDmK zTjO@rejA^ULqn!`LwrEwOVmHx^;g|5PHm#B6~YD=gjJ!043F+&#_;D*mz%Q60=L9O zve|$gU&~As5^uz@2-BfQ!bW)Khn}G+Wyjw-19qI#oB(RSNydn0t~;tAmK!P-d{b-@ z@E5|cdgOS#!>%#Rj6ynkMvaW@37E>@hJP^82zk8VXx|3mR^JCcWdA|t{0nPmYFOxN z55#^-rlqobcr==<)bi?E?SPymF*a5oDDeSdO0gx?#KMoOd&G(2O@*W)HgX6y_aa6i zMCl^~`{@UR`nMQE`>n_{_aY5nA}vqU8mt8H`oa=g0SyiLd~BxAj2~l$zRSDHxvDs; zI4>+M$W`HbJ|g&P+$!U7-PHX4RAcR0szJ*(e-417=bO2q{492SWrqDK+L3#ChUHtz z*@MP)e^%@>_&#Yk^1|tv@j4%3T)diEXATx4K*hcO`sY$jk#jN5WD<=C3nvuVs zRh||qDHnc~;Kf59zr0;c7VkVSUPD%NnnJC_l3F^#f_rDu8l}l8qcAz0FFa)EAt32I zUy_JLIhU_J^l~FRH&6-iv zSpG2PRqzDdMWft>Zc(c)#tb%wgmWN%>IOPmZi-noqS!^Ft zb81pRcQi`X#UhWK70hy4tGW1mz|+vI8c*h@fFGJtW3r>qV>1Z0r|L>7I3un^gcep$ zAAWfZHRvB|E*kktY$qQP_$YG60C z@X~tTQjB3%@`uz!qxtxF+LE!+=nrS^07hn`EgAp!h|r03h7B!$#OZW#ACD+M;-5J!W+{h z|6I;5cNnE(Y863%1(oH}_FTW})8zYb$7czPg~Szk1+_NTm6SJ0MS_|oSz%e(S~P-& zSFp;!k?uFayytV$8HPwuyELSXOs^27XvK-DOx-Dl!P|28DK6iX>p#Yb%3`A&CG0X2 zS43FjN%IB}q(!hC$fG}yl1y9W&W&I@KTg6@K^kpH8=yFuP+vI^+59|3%Zqnb5lTDAykf9S#X`3N(X^SpdMyWQGOQRjhiwlj!0W-yD<3aEj^ z&X%=?`6lCy~?`&WSWt?U~EKFcCG_RJ(Qp7j=$I%H8t)Z@6Vj zA#>1f@EYiS8MRHZphpMA_5`znM=pzUpBPO)pXGYpQ6gkine{ z6u_o!P@Q+NKJ}k!_X7u|qfpAyIJb$_#3@wJ<1SE2Edkfk9C!0t%}8Yio09^F`YGzp zaJHGk*-ffsn85@)%4@`;Fv^8q(-Wk7r=Q8pT&hD`5(f?M{gfzGbbwh8(}G#|#fDuk z7v1W)5H9wkorE0ZZjL0Q1=NRGY>zwgfm81DdoaVwNH;or{{e zSyybt)m<=zXoA^RALYG-2touH|L*BLvmm9cdMmn+KGopyR@4*=&0 z&4g|FLoreZOhRmh=)R0bg~T2(8V_q7~42-zvb)+y959OAv!V$u(O z3)%Es0M@CRFmG{5sovIq4%8Ahjk#*5w{+)+MWQoJI_r$HxL5km1#6(e@{lK3Udc~n z0@g`g$s?VrnQJ$!oPnb?IHh-1qA`Rz$)Ai<6w$-MJW-gKNvOhL+XMbE7&mFt`x1KY z>k4(!KbbpZ`>`K@1J<(#vVbjx@Z@(6Q}MF#Mnbr-f55)vXj=^j+#)=s+ThMaV~E`B z8V=|W_fZWDwiso8tNMTNse)RNBGi=gVwgg%bOg8>mbRN%7^Um-7oj4=6`$|(K7!+t^90a{$1 z8Z>}<#!bm%ZEFQ{X(yBZMc>lCz0f1I2w9SquGh<9<=AO&g6BZte6hn>Qmvv;Rt)*c zJfTr2=~EnGD8P$v3R|&1RCl&7)b+`=QGapiPbLg_pxm`+HZurtFZ;wZ=`Vk*do~$wBxoW&=j0OTbQ=Q%S8XJ%~qoa3Ea|au5 zo}_(P;=!y z-AjFrERh%8la!z6Fn@lR?^E~H12D? z8#ht=1F;7@o4$Q8GDj;sSC%Jfn01xgL&%F2wG1|5ikb^qHv&9hT8w83+yv&BQXOQy zMVJSBL(Ky~p)gU3#%|blG?I zR9rP^zUbs7rOA0X52Ao=GRt@C&zlyjNLv-}9?*x{y(`509qhCV*B47f2hLrGl^<@S zuRGR!KwHei?!CM10pBKpDIoBNyRuO*>3FU?HjipIE#B~y3FSfOsMfj~F9PNr*H?0o zHyYB^G(YyNh{SxcE(Y-`x5jFMKb~HO*m+R%rq|ic4fzJ#USpTm;X7K+E%xsT_3VHK ze?*uc4-FsILUH;kL>_okY(w`VU*8+l>o>JmiU#?2^`>arnsl#)*R&nf_%>A+qwl%o z{l(u)M?DK1^mf260_oteV3#E_>6Y4!_hhVDM8AI6MM2V*^_M^sQ0dmHu11fy^kOqX zqzps-c5efIKWG`=Es(9&S@K@)ZjA{lj3ea7_MBPk(|hBFRjHVMN!sNUkrB;(cTP)T97M$ z0Dtc&UXSec<+q?y>5=)}S~{Z@ua;1xt@=T5I7{`Z=z_X*no8s>mY;>BvEXK%b`a6(DTS6t&b!vf_z#HM{Uoy z_5fiB(zpkF{})ruka$iX*~pq1ZxD?q68dIoIZSVls9kFGsTwvr4{T_LidcWtt$u{k zJlW7moRaH6+A5hW&;;2O#$oKyEN8kx z`LmG)Wfq4ykh+q{I3|RfVpkR&QH_x;t41UwxzRFXt^E2B$domKT@|nNW`EHwyj>&< zJatrLQ=_3X%vd%nHh^z@vIk(<5%IRAa&Hjzw`TSyVMLV^L$N5Kk_i3ey6byDt)F^U zuM+Ub4*8+XZpnnPUSBgu^ijLtQD>}K;eDpe1bNOh=fvIfk`&B61+S8ND<(KC%>y&? z>opCnY*r5M+!UrWKxv0_QvTlJc>X#AaI^xoaRXL}t5Ej_Z$y*|w*$6D+A?Lw-CO-$ zitm^{2Ct82-<0IW)0KMNvJHgBrdsIR0v~=H?n6^}l{D``Me90`^o|q!olsF?UX3YS zq^6Vu>Ijm>>PaZI8G@<^NGw{Cx&%|PwYrfwR!gX_%AR=L3BFsf8LxI|K^J}deh0Zd zV?$3r--FEX`#INxsOG6_=!v)DI>0q|BxT)z-G6kzA01M?rba+G_mwNMQD1mbVbNTW zmBi*{s_v_Ft9m2Avg!^78(QFu&n6mbRJ2bAv!b;%yo{g*9l2)>tsZJOOp}U~8VUH`}$8p_}t*XIOehezolNa-a2x0BS})Y9}& z*TPgua{Ewn-=wVrmJUeU39EKx+%w%=ixQWKDLpwaNJs65#6o7Ln7~~X+p_o2BR1g~ zVCfxLzxA{HlWAI6^H;`juI=&r1jQrUv_q0Z1Ja-tjdktrrP>GOC*#p?*xfQU5MqjM zsBe!9lh(u8)w$e@Z|>aUHI5o;MGw*|Myiz3-f0;pHg~Q#%*Kx8MxH%AluVXjG2C$) zWL-K63@Q`#y9_k_+}eR(x4~dp7oV-ek0H>Igy8p#i4GN{>#v=pFYUQT(g&b$OeTy- zX_#FDgNF8XyfGY6R!>inYn8IR2RDa&O!(6NIHrC0H+Qpam1bNa=(`SRKjixBTtm&e z`j9porEci!zdlg1RI0Jw#b(_Tb@RQK1Zxr_%7SUeH6=TrXt3J@js`4iDD0=I zoHhK~I7^W8^Rcp~Yaf>2wVe|Hh1bXa_A{oZ9eG$he;_xYvTbTD#moBy zY57-f2Ef1TP^lBi&p5_s7WGG9|0T}dlfxOxXvScJO1Cnq`c`~{Dp;{;l<-KkCDE+p zmexJkd}zCgE{eF=)K``-qC~IT6GcRog_)!X?fK^F8UDz$(zFUrwuR$qro5>qqn>+Z z%<5>;_*3pZ8QM|yv9CAtrAx;($>4l^_$_-L*&?(77!-=zvnCVW&kUcZMb6;2!83si z518Y%R*A3JZ8Is|kUCMu`!vxDgaWjs7^0j(iTaS4HhQ)ldR=r)_7vYFUr%THE}cPF z{0H45FJ5MQW^+W>P+eEX2kLp3zzFe*-pFVAdDZRybv?H|>`9f$AKVjFWJ=wegO7hO zOIYCtd?Vj{EYLT*^gl35|HbMX|NAEUf2ra9dy1=O;figB>La=~eA^#>O6n4?EMugV zbbt{Dbfef5l^(;}5kZ@!XaWwF8z0vUr6r|+QN*|WpF z^*osUHzOnE$lHuWYO$G7>}Y)bY0^9UY4eDV`E{s+{}Z$O$2*lMEYl zTA`ki(<0(Yrm~}15V-E^e2W6`*`%ydED-3G@$UFm6$ZtLx z+av`BhsHcAWqdxPWfu2*%{}|Sptax4_=NpDMeWy$* zZM6__s`enB$~0aT1BU^2k`J9F%+n+lL_|8JklWOCVYt*0%o*j4w1CsB_H^tVpYT_LLyKuyk=CV6~1M<7~^FylL*+AIFf3h>J=x$ygY-BG}4LJ z8XxYPY!v7dO3PVwEoY=`)6krokmR^|Mg5ztX_^#QR}ibr^X-|_St#rtv3gukh0(#A=};NPlNz57ZDFJ9hf#NP50zS)+Fo=StX)i@ zWS?W}i6LjB>kAB~lupAPyIjFb)izFgRq*iS*(Jt509jNr3r72{Gj`5DGoj;J&k5G@Rm!dJ($ox>SbxR)fc zz|Phug;~A7!p@?|mMva@rWuf2fSDK_ZxN3vVmlYz>rrf?LpiNs)^z!y{As@`55JC~ zS*GD3#N-ptY!2<613UelAJ;M4EEI$dm)`8#n$|o{ce^dlyoUY3bsy2hgnj-;ovubb zg2h1rZA6Ot}K_cpYBpIuF&CyK~5R0Wv;kG|3A^8K3nk{rw$Be8u@aos#qvKQKJyVU$cX6biw&Ep#+q7upFX z%qo&`WZ){<%zh@BTl{MO@v9#;t+cb7so0Uz49Fmo1e4>y!vUyIHadguZS0T7-x#_drMXz*16*c zymR0u^`ZQpXN}2ofegbpSedL%F9aypdQcrzjzPlBW0j zMlPzC&ePZ@Cq!?d%9oQNEg0`rHALm8l#lUdXMVEqDvb(AID~H(?H9z!e9G98fG@IzhajKr)3{L_Clu1(Bwg`RM!-(MOuZi zbeDsj9I3(~EITsE=3Z)a|l_rn8W92U0DB70gF7YYfO0j!)h?QobY1lSR>0 z_TVw@$eP~3k8r9;%g%RlZzCJ2%f}DvY`rsZ$;ak&^~-`i%B%+O!pnADeVyV!dHj|} zzOj#q4eRx9Q8c2Z7vy9L&fGLj+3_?fp}+8o`Xpwyi(81H|7P8#65%FIS*lOi={o&v z4NV$xu7az4Nb50dRGZv<tdZCx4Ek<_o3!mAT} zL5l*|K3Qr-)W8paaG z&R6{ped_4e2cy}ejD0!dt{*PaC*^L@eB%(1Fmc%Y#4)~!jF#lCGfj#E??4LG-T;!M z>Uha}f;W>ib_ZL-I7-v9KZQls^G!-JmL^w;=^}?!RXK;m4$#MwI2AH-l7M2-0 zVMK8k^+4+>2S0k^N_40EDa#`7c;2!&3-o6MHsnBfRnq@>E@)=hDulVq-g5SQWDWbt zj6H5?QS2gRZ^Zvbs~cW|8jagJV|;^zqC0e=D1oUsQPJ3MCb+eRGw(XgIY9y8v_tXq z9$(xWntWpx_Uronmvho{JfyYdV{L1N$^s^|-Nj`Ll`lUsiWTjm&8fadUGMXreJGw$ zQ**m+Tj|(XG}DyUKY~2?&9&n6SJ@9VKa9Hcayv{ar^pNr0WHy zP$bQv&8O!vd;GoT!pLwod-42qB^`m!b7nP@YTX}^+1hzA$}LSLh}Ln|?`%8xGMazw z8WT!LoYJ-Aq3=2p6ZSP~uMgSSWv3f`&-I06tU}WhZsA^6nr&r17hjQIZE>^pk=yZ% z06}dfR$85MjWJPq)T?OO(RxoaF+E#4{Z7)i9}Xsb;Nf+dzig61HO;@JX1Lf9)R5j9)Oi6vPL{H z&UQ9ln=$Q8jnh6-t;`hKM6pHftdd?$=1Aq16jty4-TF~`Gx=C&R242uxP{Y@Q~%O3 z*(16@x+vJsbW@^3tzY=-5MHi#(kB};CU%Ep`mVY1j$MAPpYJBB3x$ue`%t}wZ-@CG z(lBv36{2HMjxT)2$n%(UtHo{iW9>4HX4>)%k8QNnzIQYXrm-^M%#Qk%9odbUrZDz1YPdY`2Z4w~p!5tb^m(mUfk}kZ9+EsmenQ)5iwiaulcy zCJ#2o4Dz?@%)aAKfVXYMF;3t@aqNh2tBBlBkCdj`F31b=h93y(46zQ-YK@+zX5qM9 z&=KkN&3@Ptp*>UD$^q-WpG|9O)HBXz{D>p!`a36aPKkgz7uxEo0J>-o+4HHVD9!Hn z${LD0d{tuGsW*wvZoHc8mJroAs(3!FK@~<}Pz1+vY|Gw}Lwfxp{4DhgiQ_SSlV)E| zZWZxYZLu2EB1=g_y@(ieCQC_1?WNA0J0*}eMZfxCCs>oL;?kHdfMcKB+A)Qull$v( z2x6(38utR^-(?DG>d1GyU()8>ih3ud0@r&I$`ZSS<*1n6(76=OmP>r_JuNCdS|-8U zxGKXL1)Lc2kWY@`_kVBt^%7t9FyLVYX(g%a6>j=yURS1!V<9ieT$$5R+yT!I>}jI5 z?fem|T=Jq;BfZmsvqz_Ud*m5;&xE66*o*S22vf-L+MosmUPPA}~wy`kntf8rIeP-m;;{`xe}9E~G7J!PYoVH_$q~NzQab?F8vWUja5BJ!T5%5IpyqI#Dkps0B;gQ*z?c#N>spFw|wRE$gY?y4wQbJ zku2sVLh({KQz6e0yo+X!rV#8n8<;bHWd{ZLL_(*9Oi)&*`LBdGWz>h zx+p`Wi00u#V$f=CcMmEmgFjw+KnbK3`mbaKfoCsB{;Q^oJgj*LWnd_(dk9Kcssbj` z?*g8l`%{*LuY!Ls*|Tm`1Gv-tRparW8q4AK(5pfJFY5>@qO( zcY>pt*na>LlB^&O@YBDnWLE$x7>pMdSmb-?qMh79eB+Wa{)$%}^kX@Z3g>fytppz! zl%>pMD(Yw+5=!UgYHLD69JiJ;YhiGeEyZM$Au{ff;i zCBbNQfO{d!b7z^F732XX&qhEsJA1UZtJjJEIPyDq+F`LeAUU_4`%2aTX#3NG3%W8u zC!7OvlB?QJ4s2#Ok^_8SKcu&pBd}L?vLRT8Kow#xARt`5&Cg=ygYuz>>c z4)+Vv$;<$l=is&E{k&4Lf-Lzq#BHuWc;wDfm4Fbd5Sr!40s{UpKT$kzmUi{V0t1yp zPOf%H8ynE$x@dQ_!+ISaI}#%72UcYm7~|D*(Fp8xiFAj$CmQ4oH3C+Q8W=Y_9Sp|B z+k<%5=y{eW=YvTivV(*KvC?qxo)xqcEU9(Te=?ITts~;xA0Jph-vpd4@Zw#?r2!`? zB3#XtIY^wxrpjJv&(7Xjvm>$TIg2ZC&+^j(gT0R|&4cb)=92-2Hti1`& z=+M;*O%_j3>9zW|3h{0Tfh5i)Fa;clGNJpPRcUmgErzC{B+zACiPHbff3SmsCZ&X; zp=tgI=zW-t(5sXFL8;ITHw0?5FL3+*z5F-KcLN130l=jAU6%F=DClRPrzO|zY+HD`zlZ-)JT}X?2g!o zxg4Ld-mx6&*-N0-MQ(z+zJo8c`B39gf{-h2vqH<=^T&o1Dgd>4BnVht+JwLcrjJl1 zsP!8`>3-rSls07q2i1hScM&x0lQyBbk(U=#3hI7Bkh*kj6H*&^p+J?OMiT_3*vw5R zEl&p|QQHZq6f~TlAeDGy(^BC0vUK?V&#ezC0*#R-h}_8Cw8-*${mVfHssathC8%VA zUE^Qd!;Rvym%|f@?-!sEj|73Vg8!$$zj_QBZAOraF5HCFKl=(Ac|_p%-P;6z<2WSf zz(9jF2x7ZR{w+p)ETCW06PVt0YnZ>gW9^sr&~`%a_7j-Ful~*4=o|&TM@k@Px2z>^ t{*Ed16F~3V5p+(suF-++X8+nHtT~NSfJ>UC3v)>lEpV}<+rIR_{{yMcG_L>v diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index cd4b7aa..0000000 --- a/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip -networkTimeout=10000 -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew deleted file mode 100755 index 1b6c787..0000000 --- a/gradlew +++ /dev/null @@ -1,234 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" -APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat deleted file mode 100644 index 107acd3..0000000 --- a/gradlew.bat +++ /dev/null @@ -1,89 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/mvnw b/mvnw new file mode 100755 index 0000000..bd8896b --- /dev/null +++ b/mvnw @@ -0,0 +1,295 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..5761d94 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/nativeImageWindows.bat b/nativeImageWindows.bat index c2cb081..936f3f9 100644 --- a/nativeImageWindows.bat +++ b/nativeImageWindows.bat @@ -135,10 +135,23 @@ SET "PATH=%SDK_BIN_PATH%;%PATH%" SET "INCLUDE=%SDK_INCLUDE_PATH%;%INCLUDE%" SET "LIB=%SDK_LIB_PATH%;%LIB%" -echo Building XLite Daemon with nativeCompile... +echo Building XLite Daemon with Maven native profile... + +REM Check if Maven wrapper exists, if not use mvn directly +if exist mvnw.cmd ( + echo Using Maven wrapper (mvnw.cmd)... + set "MAVEN_CMD=mvnw.cmd" +) else if exist mvnw.bat ( + echo Using Maven wrapper (mvnw.bat)... + set "MAVEN_CMD=mvnw.bat" +) else ( + echo Maven wrapper not found, using system mvn command... + set "MAVEN_CMD=mvn" +) -REM Build the project using Gradle nativeCompile task -gradlew.bat clean nativeCompile --info +REM Build the project using Maven with native profile +echo Running: %MAVEN_CMD% clean compile exec:java -Pnative -DskipTests +%MAVEN_CMD% clean compile exec:java -Pnative -DskipTests if %ERRORLEVEL% NEQ 0 ( echo Build failed! @@ -146,7 +159,7 @@ if %ERRORLEVEL% NEQ 0 ( ) REM Additional check for build output -if not exist build\native\nativeCompile\xlite-daemon.exe ( +if not exist target\xlite-daemon.exe ( echo Build completed but native image not found at expected location exit /b 1 ) @@ -154,9 +167,9 @@ if not exist build\native\nativeCompile\xlite-daemon.exe ( echo Build completed successfully! REM Rename the output file -if exist build\native\nativeCompile\xlite-daemon.exe ( - ren build\native\nativeCompile\xlite-daemon.exe xlite-daemon-win64.exe - echo Native image created: build\native\nativeCompile\xlite-daemon-win64.exe +if exist target\xlite-daemon.exe ( + ren target\xlite-daemon.exe xlite-daemon-win64.exe + echo Native image created: target\xlite-daemon-win64.exe ) else ( echo Error: Native image not found at expected location exit /b 1 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..45beb89 --- /dev/null +++ b/pom.xml @@ -0,0 +1,468 @@ + + + 4.0.0 + + + io.cloudchains + xlite-daemon + 0.5.14 + jar + + XLite Daemon + XLite Daemon - Multi-cryptocurrency wallet daemon + https://github.com/blocknetdx/xlite-daemon + + + + UTF-8 + 21 + 21 + 21 + 21 + + + 0.14.7 + 4.2.7.Final + 5.11.3 + 2.13.2 + + + 0.11.3 + 3.13.0 + 3.6.0 + 3.5.0 + + + + + + + + org.junit + junit-bom + ${junit.version} + pom + import + + + + + + + + org.bitcoinj + bitcoinj-core + ${bitcoinj.version} + + + commons-logging + commons-logging + + + org.apache.httpcomponents + httpclient + + + org.apache.httpcomponents + httpcore + + + + org.slf4j + slf4j-api + + + + + + + com.google.code.gson + gson + ${gson.version} + + + + + org.apache.httpcomponents + httpclient + 4.5.14 + + + + + org.slf4j + slf4j-api + 1.7.36 + + + + + org.slf4j + slf4j-nop + 1.7.36 + + + + + org.json + json + 20250517 + + + + + io.netty + netty-common + ${netty.version} + + + io.netty + netty-buffer + ${netty.version} + + + io.netty + netty-transport + ${netty.version} + + + io.netty + netty-handler + ${netty.version} + + + io.netty + netty-codec-http + ${netty.version} + + + + + com.google.code.findbugs + jsr305 + 3.0.2 + + + + + org.junit.jupiter + junit-jupiter-api + test + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.2 + + + **/*Test*.java + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven.compiler.plugin.version} + + ${java.version} + ${java.version} + ${java.version} + + -Xlint:unchecked + -Xlint:deprecation + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.5.0 + + + enforce-versions + + enforce + + + + + 21 + + + 3.8.6 + + + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + ${maven.shade.plugin.version} + + + + shade + + package + + true + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + META-INF/DEPENDENCIES + META-INF/LICENSE.txt + META-INF/NOTICE.txt + META-INF/MANIFEST.MF + + META-INF/versions/*/module-info.class + META-INF/versions/9/module-info + META-INF/versions/11/module-info + + META-INF/io.netty.versions.properties + + + + + + simplelogger.properties + + + + + + + + org.bitcoinj:bitcoinj-core + com.madgag.spongycastle:core + com.google.protobuf:protobuf-java + com.google.guava:guava + net.jcip:jcip-annotations + com.lambdaworks:scrypt + com.squareup.okhttp:okhttp + com.squareup.okio:okio + org.bitcoinj:orchid + com.google.code.gson:gson + com.google.errorprone:error_prone_annotations + org.apache.httpcomponents:httpclient + commons-logging:commons-logging + commons-codec:commons-codec + org.json:json + io.netty:netty-common + io.netty:netty-buffer + io.netty:netty-transport + io.netty:netty-resolver + io.netty:netty-handler + io.netty:netty-transport-native-unix-common + io.netty:netty-codec-base + io.netty:netty-codec-http + io.netty:netty-codec-compression + com.google.code.findbugs:jsr305 + + + + org.apache.httpcomponents:httpcore + org.slf4j:slf4j-api + + + + + + + + + + org.graalvm.buildtools + native-maven-plugin + ${native.maven.plugin.version} + + + io.cloudchains.app.App + + + xlite-daemon + + + false + true + + + + --enable-url-protocols=http,https + --enable-native-access=ALL-UNNAMED + --strict-image-heap + -march=native + -H:+ReportExceptionStackTraces + + + --initialize-at-run-time=io.netty + --initialize-at-run-time=io.netty.util.internal + --initialize-at-run-time=io.netty.channel + --initialize-at-run-time=io.netty.channel.nio + --initialize-at-run-time=io.netty.buffer + --initialize-at-run-time=io.netty.handler + --initialize-at-run-time=io.netty.bootstrap + --initialize-at-run-time=org.apache.httpcomponents + --initialize-at-run-time=org.json + --initialize-at-run-time=org.slf4j + --initialize-at-run-time=org.slf4j.impl + + + --initialize-at-build-time=com.google.common.base.Charsets + --initialize-at-build-time=com.google.common.base.CharMatcher + --initialize-at-build-time=com.google.common.base.CharMatcher$8 + --initialize-at-build-time=com.google.common.math.IntMath + --initialize-at-build-time=com.google.common.base.StandardCharsets + --initialize-at-build-time=com.google.common.base.Joiner + --initialize-at-build-time=com.google.common.io.BaseEncoding + --initialize-at-build-time=com.google.common.io.BaseEncoding$StandardBaseEncoding + --initialize-at-build-time=com.google.common.io.BaseEncoding$Alphabet + --initialize-at-build-time=org.apache.commons.logging + --initialize-at-build-time=org.slf4j.helpers.NOPLogger + --initialize-at-build-time=org.bitcoinj.core.Utils + --initialize-at-build-time=org.bitcoinj.core.Sha256Hash + --initialize-at-build-time=org.bitcoinj.crypto.MnemonicCode + --initialize-at-build-time=io.cloudchains.app.util + --initialize-at-build-time=io.cloudchains.app.crypto + --initialize-at-build-time=io.cloudchains.app.console + --initialize-at-build-time=io.cloudchains.app.net.api + --initialize-at-build-time=io.cloudchains.app.net.protocols + --initialize-at-build-time=io.cloudchains.app.net.xrouter + --initialize-at-build-time=io.cloudchains.app.net.api.http + + + + --report-unsupported-elements-at-runtime + + + + + build-native + + compile-no-fork + + package + + + + + + + org.openrewrite.maven + rewrite-maven-plugin + 6.25.0 + + + + + org.openrewrite.maven.cleanup.DependencyManagementDependencyRequiresVersion + org.openrewrite.maven.cleanup.ExplicitPluginGroupId + org.openrewrite.maven.cleanup.ExplicitPluginVersion + org.openrewrite.maven.cleanup.PrefixlessExpressions + org.openrewrite.maven.RemoveDuplicateDependencies + org.openrewrite.maven.RemoveDuplicatePluginDeclarations + org.openrewrite.maven.RemoveRedundantDependencyVersions + org.openrewrite.maven.RemoveRedundantProperties + org.openrewrite.maven.RemoveUnusedProperties + org.openrewrite.maven.OrderPomElements + org.openrewrite.maven.ModernizeObsoletePoms + org.openrewrite.maven.BestPractices + + + org.openrewrite.java.RemoveUnusedImports + org.openrewrite.java.format.AutoFormat + org.openrewrite.java.OrderImports + org.openrewrite.java.format.RemoveTrailingWhitespace + org.openrewrite.java.RemoveObjectsIsNull + org.openrewrite.java.ShortenFullyQualifiedTypeReferences + org.openrewrite.java.SimplifySingleElementAnnotation + + + + + + + + + true + + + + + + + + cleanup-code + process-classes + + run + + + + + + + + org.codehaus.mojo + exec-maven-plugin + ${exec.maven.plugin.version} + + io.cloudchains.app.App + true + + + java.util.logging.config.file + logging.properties + + + + + + + + + + + + native + + + + org.graalvm.buildtools + native-maven-plugin + 0.11.3 + + false + + + + + + + + + native-fast + + true + + + + \ No newline at end of file diff --git a/src/main/java/io/cloudchains/app/App.java b/src/main/java/io/cloudchains/app/App.java index 6bc7315..741bc90 100644 --- a/src/main/java/io/cloudchains/app/App.java +++ b/src/main/java/io/cloudchains/app/App.java @@ -1,141 +1,141 @@ package io.cloudchains.app; -import io.cloudchains.app.console.*; +import io.cloudchains.app.console.ConsoleMenu; import io.cloudchains.app.net.api.JSONRPCController; import io.cloudchains.app.net.api.JSONRPCMasterServer; -import io.cloudchains.app.net.api.http.client.HTTPClient; -import io.cloudchains.app.net.api.http.client.EXRWrapper; import io.cloudchains.app.net.api.http.client.EXRServerPool; +import io.cloudchains.app.net.api.http.client.HTTPClient; import io.cloudchains.app.util.CCLogger; import java.io.File; import java.io.IOException; import java.io.OutputStream; +import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Date; import java.util.logging.*; public class App { - private final static LogManager LOGMANAGER = LogManager.getLogManager(); - private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); - - private static final boolean isLoggingEnabled = false; - // DEBUG ENDPOINT - public static String BASE_URL = "https://xliterevp.mywire.org/"; - // "http://xl-dae-prox.airdns.org:42111/"; - // DEBUG ENDPOINT - public static String EXR_ENDPOINT = null; - public static EXRServerPool exrServerPool = null; - public static HTTPClient feeUpdateHttpClient = new HTTPClient(2); - public static HTTPClient heightUpdateHttpClient = new HTTPClient(2); - public static JSONRPCMasterServer masterRPC = JSONRPCController.getMasterServer(); - public static ConsoleMenu console = null; - - static { - // Check for EXR_ENDPOINT environment variable - String exrEndpoint = System.getenv("EXR_ENDPOINT"); - if (exrEndpoint != null && !exrEndpoint.isEmpty()) { - EXR_ENDPOINT = exrEndpoint; - exrServerPool = new EXRServerPool(EXR_ENDPOINT); - LOGGER.log(Level.INFO, "[app] EXR mode enabled with " + exrServerPool.getServerCount() + " servers: " + EXR_ENDPOINT); - } - } - - public static void main(String[] args) { - CCLogger.setLogging(isLoggingEnabled); - LOGGER.setLevel(Level.INFO); - LOGGER.setUseParentHandlers(false); + private final static LogManager LOGMANAGER = LogManager.getLogManager(); + private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); + + private static final boolean isLoggingEnabled = false; + // DEBUG ENDPOINT + public static String BASE_URL = "https://xliterevp.mywire.org/"; + // "http://xl-dae-prox.airdns.org:42111/"; + // DEBUG ENDPOINT + public static String EXR_ENDPOINT = null; + public static EXRServerPool exrServerPool = null; + public static HTTPClient feeUpdateHttpClient = new HTTPClient(2); + public static HTTPClient heightUpdateHttpClient = new HTTPClient(2); + public static JSONRPCMasterServer masterRPC = JSONRPCController.getMasterServer(); + public static ConsoleMenu console = null; + + static { + // Check for EXR_ENDPOINT environment variable + String exrEndpoint = System.getenv("EXR_ENDPOINT"); + if (exrEndpoint != null && !exrEndpoint.isEmpty()) { + EXR_ENDPOINT = exrEndpoint; + exrServerPool = new EXRServerPool(EXR_ENDPOINT); + LOGGER.log(Level.INFO, "[app] EXR mode enabled with " + exrServerPool.getServerCount() + " servers: " + EXR_ENDPOINT); + } + } + + public static void main(String[] args) { + CCLogger.setLogging(isLoggingEnabled); + LOGGER.setLevel(Level.INFO); + LOGGER.setUseParentHandlers(false); Runtime.getRuntime().addShutdownHook(new Thread(App::shutdown)); - try { - String userHomeDir; - String OS = (System.getProperty("os.name")).toLowerCase(); - - if (OS.contains("win")) { - userHomeDir = System.getenv("AppData"); - } else if (OS.contains("nix") || OS.contains("nux") || OS.contains("aix")) { - userHomeDir = System.getProperty("user.home") + File.separator + ".config"; - } else if (OS.contains("mac")) { - userHomeDir = System.getProperty("user.home") + File.separator + "Library" + File.separator + "Application Support"; - } else { - userHomeDir = System.getProperty("user.home") + File.separator + ".config"; - } - - DateTimeFormatter timeStampPattern = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - Handler fileHandler = new FileHandler( - userHomeDir + - File.separator + - "CloudChains" + - File.separator + - "error-" + - timeStampPattern.format(java.time.LocalDateTime.now()) + - ".log", + try { + String userHomeDir; + String OS = (System.getProperty("os.name")).toLowerCase(); + + if (OS.contains("win")) { + userHomeDir = System.getenv("AppData"); + } else if (OS.contains("nix") || OS.contains("nux") || OS.contains("aix")) { + userHomeDir = System.getProperty("user.home") + File.separator + ".config"; + } else if (OS.contains("mac")) { + userHomeDir = System.getProperty("user.home") + File.separator + "Library" + File.separator + "Application Support"; + } else { + userHomeDir = System.getProperty("user.home") + File.separator + ".config"; + } + + DateTimeFormatter timeStampPattern = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + Handler fileHandler = new FileHandler( + userHomeDir + + File.separator + + "CloudChains" + + File.separator + + "error-" + + timeStampPattern.format(LocalDateTime.now()) + + ".log", true - ); - - fileHandler.setFormatter(new SimpleFormatter() { - private static final String format = "[%1$tF %1$tT] [%2$-7s] %3$s %n"; - - @Override - public synchronized String format(LogRecord lr) { - return String.format(format, - new Date(lr.getMillis()), - lr.getLevel().getLocalizedName(), - lr.getMessage() - ); - } - }); - fileHandler.setLevel(Level.INFO); - - LOGGER.addHandler(fileHandler); - - } catch (IOException e) { - // TODO Auto-generated catch block - } - - ConsoleHandler consoleHandler = new ConsoleHandler (){ - @Override - protected synchronized void setOutputStream(OutputStream out) throws SecurityException { - super.setOutputStream(System.out); - } - }; - consoleHandler.setLevel(Level.FINE); - - LOGGER.addHandler(consoleHandler); - - console = new ConsoleMenu(args); - console.init(); - } - - public static void shutdown() { - if (masterRPC.isAlive()) { - System.out.println("Shutting down..."); - } - - if (feeUpdateHttpClient != null) { - feeUpdateHttpClient.close(); - } - - if (heightUpdateHttpClient != null) { - heightUpdateHttpClient.close(); - } - - if (exrServerPool != null) { - exrServerPool.close(); - } - - if (masterRPC != null) { - masterRPC.deinit(); - } - - if (console != null) { - console.deinit(); - } - - for (Handler handler : LOGGER.getHandlers()) { - LOGGER.removeHandler(handler); - handler.close(); - } - } + ); + + fileHandler.setFormatter(new SimpleFormatter() { + private static final String format = "[%1$tF %1$tT] [%2$-7s] %3$s %n"; + + @Override + public synchronized String format(LogRecord lr) { + return String.format(format, + new Date(lr.getMillis()), + lr.getLevel().getLocalizedName(), + lr.getMessage() + ); + } + }); + fileHandler.setLevel(Level.INFO); + + LOGGER.addHandler(fileHandler); + + } catch (IOException e) { + // TODO Auto-generated catch block + } + + ConsoleHandler consoleHandler = new ConsoleHandler(){ + @Override + protected synchronized void setOutputStream(OutputStream out) throws SecurityException { + super.setOutputStream(System.out); + } + }; + consoleHandler.setLevel(Level.FINE); + + LOGGER.addHandler(consoleHandler); + + console = new ConsoleMenu(args); + console.init(); + } + + public static void shutdown() { + if (masterRPC.isAlive()) { + System.out.println("Shutting down..."); + } + + if (feeUpdateHttpClient != null) { + feeUpdateHttpClient.close(); + } + + if (heightUpdateHttpClient != null) { + heightUpdateHttpClient.close(); + } + + if (exrServerPool != null) { + exrServerPool.close(); + } + + if (masterRPC != null) { + masterRPC.deinit(); + } + + if (console != null) { + console.deinit(); + } + + for (Handler handler : LOGGER.getHandlers()) { + LOGGER.removeHandler(handler); + handler.close(); + } + } } diff --git a/src/main/java/io/cloudchains/app/console/ArgMenu.java b/src/main/java/io/cloudchains/app/console/ArgMenu.java index 3657a2e..fd7b74d 100644 --- a/src/main/java/io/cloudchains/app/console/ArgMenu.java +++ b/src/main/java/io/cloudchains/app/console/ArgMenu.java @@ -69,7 +69,7 @@ public void init() { System.out.println("Exiting..."); System.exit(0); } - break; + break; default: throw new IllegalStateException("Unexpected value: " + selection); } diff --git a/src/main/java/io/cloudchains/app/console/ConsoleMenu.java b/src/main/java/io/cloudchains/app/console/ConsoleMenu.java index 2b33c31..dfa8abc 100644 --- a/src/main/java/io/cloudchains/app/console/ConsoleMenu.java +++ b/src/main/java/io/cloudchains/app/console/ConsoleMenu.java @@ -7,12 +7,9 @@ import io.cloudchains.app.net.CoinInstance; import io.cloudchains.app.net.CoinTicker; import io.cloudchains.app.net.CoinTickerUtils; -import io.cloudchains.app.net.api.http.client.EXRWrapper; import io.cloudchains.app.net.api.http.client.EXRServerPool; -import io.cloudchains.app.net.protocols.blocknet.BlocknetNetworkParameters; import io.cloudchains.app.util.ConfigHelper; import io.cloudchains.app.util.background.BackgroundTimerThread; -import org.bitcoinj.core.Context; import java.security.SecureRandom; import java.util.Base64; @@ -239,7 +236,7 @@ public void init() { case "--help": displayHelp(); return; // Exit after displaying help - } + } } } @@ -340,7 +337,6 @@ private void completeLogin(String entropy, String userMnemonic, boolean isMnemon App.masterRPC.start(); backgroundTimerThread = new BackgroundTimerThread(); (new Thread(backgroundTimerThread)).start(); - // Start EXR capability probing after wallet is decrypted if (App.exrServerPool != null) { App.exrServerPool.probeAllCapabilities(); @@ -368,13 +364,13 @@ private String generateRandomString(int length) { } /** - * Reads the password from args or from stdin if password is not specified. - * @param input Stdin - * @param args Program arguments - * @param argPos Current arg position - * @param msg Message to display on stdin - * @return Password - */ + * Reads the password from args or from stdin if password is not specified. + * @param input Stdin + * @param args Program arguments + * @param argPos Current arg position + * @param msg Message to display on stdin + * @return Password + */ private String readPassword(Scanner input, String[] args, int argPos, String msg) { if (msg.isEmpty()) msg = "Password:\n"; diff --git a/src/main/java/io/cloudchains/app/crypto/KeyHandler.java b/src/main/java/io/cloudchains/app/crypto/KeyHandler.java index 1741d78..da38e56 100644 --- a/src/main/java/io/cloudchains/app/crypto/KeyHandler.java +++ b/src/main/java/io/cloudchains/app/crypto/KeyHandler.java @@ -7,7 +7,6 @@ import org.bitcoinj.crypto.MnemonicCode; import org.bitcoinj.crypto.MnemonicException; import org.bitcoinj.wallet.DeterministicSeed; -import org.bitcoinj.wallet.UnreadableWalletException; import javax.crypto.Cipher; import javax.crypto.SecretKey; @@ -17,8 +16,6 @@ import java.io.*; import java.security.SecureRandom; import java.text.SimpleDateFormat; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; import java.util.*; import java.util.logging.Level; import java.util.logging.LogManager; @@ -125,7 +122,7 @@ public static boolean importFromMnemonic(List mnemonicList, String passp return false; } - DeterministicSeed seed = new DeterministicSeed(entropy , "", System.currentTimeMillis() / 1000); + DeterministicSeed seed = new DeterministicSeed(entropy, "", System.currentTimeMillis() / 1000); String mnemonic = Joiner.on(" ").join(Objects.requireNonNull(seed.getMnemonicCode())); @@ -189,7 +186,6 @@ private static boolean writeInitialData(File keyFile, String mnemonic, String pa byte[] mnemonicByte = mnemonic.getBytes(); String encryptedSeed = encryptBaseSeed(passphrase, mnemonicByte, salt); - // Print mnemonic to console // System.out.println(mnemonic + "\n"); @@ -214,32 +210,31 @@ public static List getMnemonicFromString(String mnemonic) { return Arrays.asList(mnemonic.split(" ")); } - public static int calculatePasswordStrength(String password){ + public static int calculatePasswordStrength(String password) { // Password must be greater than 8 characters, contain at least one digit, one lowercase letter, one uppercase letter and one special character. int totalScore = 0; - if( password.length() < 8 ) - return 0; - else if( password.length() >= 10 ) + if (password.length() < 8) + return 0;else if (password.length() >= 10) totalScore += 2; else totalScore += 1; //if it contains one digit, add 2 to total score - if( password.matches("(?=.*[0-9]).*") ) + if (password.matches("(?=.*[0-9]).*")) totalScore += 2; //if it contains one lower case letter, add 2 to total score - if( password.matches("(?=.*[a-z]).*") ) + if (password.matches("(?=.*[a-z]).*")) totalScore += 2; //if it contains one upper case letter, add 2 to total score - if( password.matches("(?=.*[A-Z]).*") ) + if (password.matches("(?=.*[A-Z]).*")) totalScore += 2; //if it contains one special character, add 2 to total score - if( password.matches("(?=.*[~!@#$%^&*()_-]).*") ) + if (password.matches("(?=.*[~!@#$%^&*()_-]).*")) totalScore += 2; return totalScore; diff --git a/src/main/java/io/cloudchains/app/crypto/LoginUtils.java b/src/main/java/io/cloudchains/app/crypto/LoginUtils.java index a7430e8..89ff3ee 100644 --- a/src/main/java/io/cloudchains/app/crypto/LoginUtils.java +++ b/src/main/java/io/cloudchains/app/crypto/LoginUtils.java @@ -6,13 +6,13 @@ import java.util.logging.Logger; public class LoginUtils { - private final static LogManager LOGMANAGER = LogManager.getLogManager(); - private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); + private final static LogManager LOGMANAGER = LogManager.getLogManager(); + private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); - private static String toSha256(String message) { - try { - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - digest.update((message).getBytes()); + private static String toSha256(String message) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + digest.update((message).getBytes()); byte[] hash = digest.digest(); StringBuilder hex = new StringBuilder(); for (byte b : hash) { @@ -22,22 +22,22 @@ private static String toSha256(String message) { hex.append(Integer.toHexString(0xFF & b)); } return hex.toString(); - } catch (Exception e) { - LOGGER.log(Level.FINER, "Error while hashing message with SHA256!"); - e.printStackTrace(); - } - return null; - } + } catch (Exception e) { + LOGGER.log(Level.FINER, "Error while hashing message with SHA256!"); + e.printStackTrace(); + } + return null; + } - public static String loginToEntropy(String password) { - String shaPassword = toSha256(password); + public static String loginToEntropy(String password) { + String shaPassword = toSha256(password); - if (shaPassword == null) { - LOGGER.log(Level.FINER, "Password hashing failed"); - return null; - } + if (shaPassword == null) { + LOGGER.log(Level.FINER, "Password hashing failed"); + return null; + } - return shaPassword; - } + return shaPassword; + } } diff --git a/src/main/java/io/cloudchains/app/net/ActiveCoinChangedEventListener.java b/src/main/java/io/cloudchains/app/net/ActiveCoinChangedEventListener.java index c55092b..821cc6a 100644 --- a/src/main/java/io/cloudchains/app/net/ActiveCoinChangedEventListener.java +++ b/src/main/java/io/cloudchains/app/net/ActiveCoinChangedEventListener.java @@ -2,6 +2,6 @@ public interface ActiveCoinChangedEventListener { - void onActiveCoinChanged(CoinInstance newActiveCoin); + void onActiveCoinChanged(CoinInstance newActiveCoin); } diff --git a/src/main/java/io/cloudchains/app/net/CoinInstance.java b/src/main/java/io/cloudchains/app/net/CoinInstance.java index a8fe647..287a5e2 100644 --- a/src/main/java/io/cloudchains/app/net/CoinInstance.java +++ b/src/main/java/io/cloudchains/app/net/CoinInstance.java @@ -1,12 +1,11 @@ package io.cloudchains.app.net; import com.google.common.base.Joiner; -import com.google.common.base.Preconditions; import com.google.common.util.concurrent.AtomicDouble; import com.google.gson.JsonArray; import com.google.gson.JsonObject; +import com.subgraph.orchid.encoders.Hex; //import com.subgraph.orchid.encoders.Hex; -import io.cloudchains.app.App; import io.cloudchains.app.Version; import io.cloudchains.app.crypto.KeyHandler; import io.cloudchains.app.net.api.JSONRPCController; @@ -27,7 +26,6 @@ import io.cloudchains.app.net.protocols.unobtanium.UnobtaniumNetworkParameters; //import io.cloudchains.app.net.protocols.trezarcoin.TrezarcoinNetworkParameters; import io.cloudchains.app.net.xrouter.XRouterMessage; -import io.cloudchains.app.net.xrouter.XRouterMessageSerializer; import io.cloudchains.app.net.xrouter.XRouterPacketManager; import io.cloudchains.app.util.AddressBalance; import io.cloudchains.app.util.AddressDiscoveryService; @@ -38,8 +36,6 @@ import io.cloudchains.app.wallet.WalletHelper; import org.bitcoinj.core.*; import org.bitcoinj.params.MainNetParams; -import org.bitcoinj.store.BlockStoreException; -import org.bitcoinj.store.SPVBlockStore; import org.bitcoinj.utils.BtcFormat; import org.bitcoinj.utils.ListenerRegistration; import org.bitcoinj.utils.MonetaryFormat; @@ -48,9 +44,7 @@ import org.bitcoinj.wallet.Wallet; import org.json.JSONArray; -import java.io.File; import java.math.BigDecimal; -import java.nio.charset.StandardCharsets; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; @@ -58,7 +52,6 @@ import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; -import com.subgraph.orchid.encoders.Hex; public class CoinInstance { public static class CoinError { @@ -67,72 +60,75 @@ public enum CoinErrorCode { } private final CoinErrorCode code; private final String msg; + public CoinError(String msg, CoinErrorCode code) { this.msg = msg; this.code = code; } + public CoinErrorCode getCode() { return this.code; } + public String getMessage() { return this.msg; } } - private final static LogManager LOGMANAGER = LogManager.getLogManager(); - private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); - - private static final int MINIMUM_UTXO_UPDATE_INTERVAL = 500; - - private static final int FORWARD_ADDRESS_COUNT = 0; - - private static ArrayList coinInstances = new ArrayList<>(); - private static CoinInstance activeCurrency; - private static CoinTicker activeBlocknetNetwork = null; - private static CopyOnWriteArrayList> activeCoinChangedListeners = new CopyOnWriteArrayList<>(); - private static HashMap blockCounts = new HashMap<>(); - private static HashMap relayFees = new HashMap<>(); - - private ConfigHelper configHelper; - private WalletHelper walletHelper = null; - private CoinTicker ticker; - private ConcurrentHashMap transactionList = new ConcurrentHashMap<>(); - private ArrayList addressKeyPairs = new ArrayList<>(); - private ArrayList transactionObservableList = new ArrayList<>(); - private BlocknetPeerGroup blocknetPeerGroup; - private BlocknetParameters blocknetNetworkParameters; - private NetworkParameters networkParameters; - private Wallet wallet; - private BlockChain chain; - private boolean hasXRouter = false; - private KeyHandler keyHandler; - private XRouterPacketManager xRouterPacketManager = null; - private int rpcPort = -1; - private boolean testnet = false; - private JSONRPCServer coinRPCServer = null; - private long lastUtxoUpdate = 0; - private int updateFailures = 0; - private int generatedAddressCount; - private AddressDiscoveryService discoveryService = null; - private static boolean addressDiscoveryEnabled = true; - - private CoinInstance(CoinTicker ticker) { - this.ticker = ticker; - this.configHelper = new ConfigHelper(CoinTickerUtils.tickerToString(getTicker())); - - addBlockCount(ticker, 0); - - if (isBlocknetNetwork()) - setActiveCurrency(this); - } + private final static LogManager LOGMANAGER = LogManager.getLogManager(); + private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); + + private static final int MINIMUM_UTXO_UPDATE_INTERVAL = 500; + + private static final int FORWARD_ADDRESS_COUNT = 0; + + private static ArrayList coinInstances = new ArrayList<>(); + private static CoinInstance activeCurrency; + private static CoinTicker activeBlocknetNetwork = null; + private static CopyOnWriteArrayList> activeCoinChangedListeners = new CopyOnWriteArrayList<>(); + private static HashMap blockCounts = new HashMap<>(); + private static HashMap relayFees = new HashMap<>(); + + private ConfigHelper configHelper; + private WalletHelper walletHelper = null; + private CoinTicker ticker; + private ConcurrentHashMap transactionList = new ConcurrentHashMap<>(); + private ArrayList addressKeyPairs = new ArrayList<>(); + private ArrayList transactionObservableList = new ArrayList<>(); + private BlocknetPeerGroup blocknetPeerGroup; + private BlocknetParameters blocknetNetworkParameters; + private NetworkParameters networkParameters; + private Wallet wallet; + private BlockChain chain; + private boolean hasXRouter = false; + private KeyHandler keyHandler; + private XRouterPacketManager xRouterPacketManager = null; + private int rpcPort = -1; + private boolean testnet = false; + private JSONRPCServer coinRPCServer = null; + private long lastUtxoUpdate = 0; + private int updateFailures = 0; + private int generatedAddressCount; + private AddressDiscoveryService discoveryService = null; + private static boolean addressDiscoveryEnabled = true; + + private CoinInstance(CoinTicker ticker) { + this.ticker = ticker; + this.configHelper = new ConfigHelper(CoinTickerUtils.tickerToString(getTicker())); + + addBlockCount(ticker, 0); + + if (isBlocknetNetwork()) + setActiveCurrency(this); + } /** - * Return mnemonic seed from wallet stored on disk. Correct passphrase required. - * Returns empty string on error or failure to retrieve mnemonic (or if mnemonic - * doesn't exist). - * @param pw String - * @return String - */ + * Return mnemonic seed from wallet stored on disk. Correct passphrase required. + * Returns empty string on error or failure to retrieve mnemonic (or if mnemonic + * doesn't exist). + * @param pw String + * @return String + */ public static String getMnemonicForPw(String pw) { if (!KeyHandler.existsBaseECKeyFromLocal()) return ""; @@ -143,133 +139,134 @@ public static String getMnemonicForPw(String pw) { return Joiner.on(" ").join(seed); } - public static int getBlockCountByTicker(CoinTicker ticker) { - if (!blockCounts.containsKey(ticker)) { - return -1; - } - - return blockCounts.get(ticker).get(); - } - - public static double getRelayFeeByTicker(CoinTicker ticker) { - if (!relayFees.containsKey(ticker)) { - return -1; - } - - return relayFees.get(ticker).get(); - } - - public static ArrayList getCoinInstances() { - return coinInstances; - } - - public boolean isBlocknetNetwork() { - return getTicker() == CoinTicker.BLOCKNET || getTicker() == CoinTicker.BLOCKNET_TESTNET5; - } - - public static void setActiveCurrency(CoinInstance newActiveCurrency) { - activeCurrency = newActiveCurrency; - - for (ListenerRegistration registration : activeCoinChangedListeners) { - if (registration.executor == Threading.SAME_THREAD) { - registration.listener.onActiveCoinChanged(activeCurrency); - } - } - } - - public XRouterPacketManager getXRouterPacketManager() { - return xRouterPacketManager; - } - - public static void addActiveCoinChangedListener(ActiveCoinChangedEventListener listener) { - activeCoinChangedListeners.add(new ListenerRegistration<>(listener, Threading.SAME_THREAD)); - } - - public static CoinInstance getActiveCurrency() { - return activeCurrency; - } - - public AddressBalance getAddress(String addressB58) { - for (AddressBalance address : addressKeyPairs) { - if (address.getAddress().toBase58().equals(addressB58)) - return address; - } - - return null; - } - - public AddressBalance generateAddress(boolean updateConfig) { - AddressBalance addressKeyPair = getWalletHelper().generateAddress(); - Address address = addressKeyPair.getAddress(); - DumpedPrivateKey privateKey = addressKeyPair.getPrivateKey(); - addressKeyPairs.add(addressKeyPair); - LOGGER.log(Level.FINER, "[wallet] DEBUG: Generated new address, have " + addressKeyPairs.size() + ": " + address.toBase58() + ", private key: " + privateKey.toBase58() + " (hex: " + privateKey.getKey().getPrivateKeyAsHex() + ")"); - - if (updateConfig) { - configHelper.setAddressCount(configHelper.getAddressCount() + 1); - configHelper.writeConfig(); - generatedAddressCount = configHelper.getAddressCount(); - } - - return addressKeyPair; - } - - public void importPrivateKey(String privKey) { - AddressBalance addressKeyPair = getWalletHelper().generateFromPrivateKey(privKey); - AddressBalance addrExists = addressKeyPairs.stream() - .filter(e -> e.getAddress().equals(addressKeyPair.getAddress())).findAny().orElse(null); - - if (addrExists == null) - addressKeyPairs.add(addressKeyPair); - } - - public CoinTicker getTicker() { - return ticker; - } - - private static CoinInstance getInstanceByTicker(CoinTicker ticker) { - for (CoinInstance instance : coinInstances) { - if (instance.getTicker() == ticker) { - return instance; - } - } - - return null; - } - - public static CoinTicker getActiveBlocknetNetwork() { - return activeBlocknetNetwork; - } - - public static CoinInstance getInstance(CoinTicker ticker) { - CoinInstance instance = getInstanceByTicker(ticker); - if (getActiveBlocknetNetwork() != null && (ticker == CoinTicker.BLOCKNET || ticker == CoinTicker.BLOCKNET_TESTNET5)) { - return getInstanceByTicker(activeBlocknetNetwork); - } - - if (ticker == CoinTicker.BLOCKNET || ticker == CoinTicker.BLOCKNET_TESTNET5) { - activeBlocknetNetwork = ticker; - LOGGER.log(Level.FINER, "[coin] Initialized active Blocknet network: " + ticker.toString()); - LOGGER.log(Level.FINER, "[coin] All subsequent calls to this function requesting a Blocknet network will return the above regardless of testnet or mainnet status."); - } - - if (instance == null) { - instance = new CoinInstance(ticker); - if (ticker == CoinTicker.BLOCKNET) - coinInstances.add(0, instance); - else - coinInstances.add(instance); - } - - return instance; - } + + public static int getBlockCountByTicker(CoinTicker ticker) { + if (!blockCounts.containsKey(ticker)) { + return -1; + } + + return blockCounts.get(ticker).get(); + } + + public static double getRelayFeeByTicker(CoinTicker ticker) { + if (!relayFees.containsKey(ticker)) { + return -1; + } + + return relayFees.get(ticker).get(); + } + + public static ArrayList getCoinInstances() { + return coinInstances; + } + + public boolean isBlocknetNetwork() { + return getTicker() == CoinTicker.BLOCKNET || getTicker() == CoinTicker.BLOCKNET_TESTNET5; + } + + public static void setActiveCurrency(CoinInstance newActiveCurrency) { + activeCurrency = newActiveCurrency; + + for (ListenerRegistration registration : activeCoinChangedListeners) { + if (registration.executor == Threading.SAME_THREAD) { + registration.listener.onActiveCoinChanged(activeCurrency); + } + } + } + + public XRouterPacketManager getXRouterPacketManager() { + return xRouterPacketManager; + } + + public static void addActiveCoinChangedListener(ActiveCoinChangedEventListener listener) { + activeCoinChangedListeners.add(new ListenerRegistration<>(listener, Threading.SAME_THREAD)); + } + + public static CoinInstance getActiveCurrency() { + return activeCurrency; + } + + public AddressBalance getAddress(String addressB58) { + for (AddressBalance address : addressKeyPairs) { + if (address.getAddress().toBase58().equals(addressB58)) + return address; + } + + return null; + } + + public AddressBalance generateAddress(boolean updateConfig) { + AddressBalance addressKeyPair = getWalletHelper().generateAddress(); + Address address = addressKeyPair.getAddress(); + DumpedPrivateKey privateKey = addressKeyPair.getPrivateKey(); + addressKeyPairs.add(addressKeyPair); + LOGGER.log(Level.FINER, "[wallet] DEBUG: Generated new address, have " + addressKeyPairs.size() + ": " + address.toBase58() + ", private key: " + privateKey.toBase58() + " (hex: " + privateKey.getKey().getPrivateKeyAsHex() + ")"); + + if (updateConfig) { + configHelper.setAddressCount(configHelper.getAddressCount() + 1); + configHelper.writeConfig(); + generatedAddressCount = configHelper.getAddressCount(); + } + + return addressKeyPair; + } + + public void importPrivateKey(String privKey) { + AddressBalance addressKeyPair = getWalletHelper().generateFromPrivateKey(privKey); + AddressBalance addrExists = addressKeyPairs.stream() + .filter(e -> e.getAddress().equals(addressKeyPair.getAddress())).findAny().orElse(null); + + if (addrExists == null) + addressKeyPairs.add(addressKeyPair); + } + + public CoinTicker getTicker() { + return ticker; + } + + private static CoinInstance getInstanceByTicker(CoinTicker ticker) { + for (CoinInstance instance : coinInstances) { + if (instance.getTicker() == ticker) { + return instance; + } + } + + return null; + } + + public static CoinTicker getActiveBlocknetNetwork() { + return activeBlocknetNetwork; + } + + public static CoinInstance getInstance(CoinTicker ticker) { + CoinInstance instance = getInstanceByTicker(ticker); + if (getActiveBlocknetNetwork() != null && (ticker == CoinTicker.BLOCKNET || ticker == CoinTicker.BLOCKNET_TESTNET5)) { + return getInstanceByTicker(activeBlocknetNetwork); + } + + if (ticker == CoinTicker.BLOCKNET || ticker == CoinTicker.BLOCKNET_TESTNET5) { + activeBlocknetNetwork = ticker; + LOGGER.log(Level.FINER, "[coin] Initialized active Blocknet network: " + ticker.toString()); + LOGGER.log(Level.FINER, "[coin] All subsequent calls to this function requesting a Blocknet network will return the above regardless of testnet or mainnet status."); + } + + if (instance == null) { + instance = new CoinInstance(ticker); + if (ticker == CoinTicker.BLOCKNET) + coinInstances.add(0, instance); + else + coinInstances.add(instance); + } + + return instance; + } /** - * Change the password. Recreates the wallet file and encrypts with new password. - * @param oldPassword + * Change the password. Recreates the wallet file and encrypts with new password. + * @param oldPassword * @param newPassword * @return Error or null - */ + */ public static CoinError changePassword(String oldPassword, String newPassword) { if (!KeyHandler.existsBaseECKeyFromLocal()) { LOGGER.log(Level.FINER, "[wallet] Unable to change the password: Wallet not found on disk"); @@ -297,221 +294,220 @@ public static CoinError changePassword(String oldPassword, String newPassword) { return null; } - public NetworkParameters getNetworkParameters() { - return networkParameters; - } - - private BlocknetParameters getBlocknetNetworkParameters() { - return blocknetNetworkParameters; - } - - public boolean hasXRouter() { - return hasXRouter; - } - - public void deinit() { - if (getTicker() == getActiveBlocknetNetwork() && blocknetPeerGroup != null) { - blocknetPeerGroup.stop(); - } - - if (coinRPCServer != null) { - try { - coinRPCServer.deinit(); - coinRPCServer.join(); - } catch (Exception e) { - LOGGER.log(Level.FINER, "[coin] ERROR: Error while deinitializing coin RPC server!"); - e.printStackTrace(); - } - } - } - - public CoinError init(String pw, String userMnemonic, boolean isMnemonic) { - return init(pw, userMnemonic, isMnemonic, false); - } - - public CoinError init(String pw, String userMnemonic, boolean isMnemonic, boolean xliteRPC) { - switch (ticker) { - case BLOCKNET: { - LOGGER.log(Level.FINER, "[coin] Initializing for Blocknet main network."); - blocknetNetworkParameters = new BlocknetNetworkParameters(); - networkParameters = blocknetNetworkParameters; - hasXRouter = true; - rpcPort = 41419; - break; - } - case BLOCKNET_TESTNET5: { - LOGGER.log(Level.FINER, "[coin] Initializing for Blocknet test network v5."); - blocknetNetworkParameters = new BlocknetTestnet5NetworkParameters(); - networkParameters = blocknetNetworkParameters; - hasXRouter = true; - rpcPort = 41419; - testnet = true; - break; - } - case BITCOIN: { - LOGGER.log(Level.FINER, "[coin] Initializing for Bitcoin main network."); - networkParameters = MainNetParams.get(); - rpcPort = 8332; - break; - } - // case BITCOIN_CASH: { - // LOGGER.log(Level.FINER, "[coin] Initializing for BitcoinCash main network."); - // networkParameters = new BitcoinCashNetworkParameters(); - // rpcPort = 48332; - // break; - // } - case LITECOIN: { - LOGGER.log(Level.FINER, "[coin] Initializing for Litecoin main network."); - networkParameters = new LitecoinNetworkParameters(); - rpcPort = 9332; - break; - } - case DASHCOIN: { - LOGGER.log(Level.FINER, "[coin] Initializing for Dashcoin main network."); - networkParameters = new DashcoinNetworkParameters(); - rpcPort = 9998; - break; - } - // case DIGIBYTE: { - // LOGGER.log(Level.FINER, "[coin] Initializing for Digibyte main network."); - // networkParameters = new DigibyteNetworkParameters(); - // rpcPort = 14022; - // break; - // } - case DOGECOIN: { - LOGGER.log(Level.FINER, "[coin] Initializing for Dogecoin main network."); - networkParameters = new DogecoinNetworkParameters(); - rpcPort = 22555; - break; - } - case SYSCOIN: { - LOGGER.log(Level.FINER, "[coin] Initializing for Syscoin main network."); - networkParameters = new SyscoinNetworkParameters(); - rpcPort = 8370; - break; - } - // case TREZARCOIN: { - // networkParameters = new TrezarcoinNetworkParameters(); - // rpcPort = 17299; - // break; - // } - // case BITBAY: { - // networkParameters = new BitbayNetworkParameters(); - // rpcPort = 19915; - // break; - // } - case PIVX: { - LOGGER.log(Level.FINER, "[coin] Initializing for Pivx main network."); - networkParameters = new PivxNetworkParameters(); - rpcPort = 9951; - break; - } - case UNOBTANIUM: { - LOGGER.log(Level.FINER, "[coin] Initializing for Unobtanium main network."); - networkParameters = new UnobtaniumNetworkParameters(); - rpcPort = 65111; - break; - } - // case ALQOCOIN: { - // LOGGER.log(Level.FINER, "[coin] Initializing for Alqo main network."); - // networkParameters = new AlqocoinNetworkParameters(); - // rpcPort = 55000; - // break; - // } - // case POLISCOIN: { - // LOGGER.log(Level.FINER, "[coin] Initializing for Polis main network."); - // networkParameters = new PoliscoinNetworkParameters(); - // rpcPort = 24127; - // break; - // } - // case PHORECOIN: { - // LOGGER.log(Level.FINER, "[coin] Initializing for Phore main network."); - // networkParameters = new PhorecoinNetworkParameters(); - // rpcPort = 11772; - // break; - // } - // case RAVENCOIN: { - // LOGGER.log(Level.FINER, "[coin] Initializing for Ravencoin main network."); - // networkParameters = new RavencoinNetworkParameters(); - // rpcPort = 8766; - // break; - // } - default: { - LOGGER.log(Level.FINER, "[coin] ERROR: Invalid/unsupported network: " + ticker.toString()); - return new CoinError("Unsupported coin", CoinError.CoinErrorCode.UNSUPPORTEDCOIN); - } - } - - if (xliteRPC) { - rpcPort = rpcPort + 1; - - configHelper.setRpcPort(rpcPort); - configHelper.writeConfig(); - } - - Context.propagate(new Context(networkParameters)); - - List baseSeed; - boolean existsOnDisk = false; - - if (isMnemonic) { - baseSeed = Arrays.asList(pw.split(" ")); - } else { - if (KeyHandler.existsBaseECKeyFromLocal()) - existsOnDisk = true; - else if (userMnemonic != null) { - if (!KeyHandler.importFromMnemonic(Arrays.asList(new String(userMnemonic).split(" ")), pw)) { - LOGGER.log(Level.FINER, "[wallet] Unable to create wallet from mnemonic"); - return new CoinError("Unable to create wallet from mnemonic", CoinError.CoinErrorCode.BADMNEMONIC); - } - } - - baseSeed = KeyHandler.getBaseSeed(pw); - } - - if (baseSeed == null) { - LOGGER.log(Level.FINER, "[wallet] Possible Bad password: Unable to import or create base seed!"); - return new CoinError("Bad password", CoinError.CoinErrorCode.BADPASSWORD); - } - - // In-memory wallet only - DeterministicSeed seed = new DeterministicSeed(baseSeed, null, "", System.currentTimeMillis() / 1000); - wallet = Wallet.fromSeed(networkParameters, seed); - if (isBlocknetNetwork()) { - String mnemonic = getMnemonic(); - // LOGGER.log(Level.FINE, "[wallet] Mnemonic = " + mnemonic); - } - - // RUN ADDRESS DISCOVERY ONLY DURING WALLET INITIALIZATION - // This ensures discovery runs once at wallet startup in ANY case - if (addressDiscoveryEnabled) { - LOGGER.log(Level.INFO, "[coin] Running address discovery"); - runAddressDiscovery(); - } else { - LOGGER.log(Level.INFO, "[coin] Address discovery disabled"); - } - - // Make sure wallet addresses are available - generateForwardAddresses(true); - - if (configHelper.getRpcPort() == -1000) { - configHelper.setRpcPort(rpcPort); - configHelper.writeConfig(); - } else { - rpcPort = configHelper.getRpcPort(); - } - - if (configHelper.isRpcEnabled() && configHelper.validAuth() && rpcPort != -1) { - coinRPCServer = JSONRPCController.getRPCServer(this); - LOGGER.log(Level.INFO, "[rpc] Starting JSON-RPC server for coin " + CoinTickerUtils.tickerToString(getTicker()) + " on port " + getRPCPort()); - - if (coinRPCServer.isAlive()) - coinRPCServer.deinit(); - - coinRPCServer.start(); - } - - // Blocknet Network / XRouter not used (Dec 10) + public NetworkParameters getNetworkParameters() { + return networkParameters; + } + + private BlocknetParameters getBlocknetNetworkParameters() { + return blocknetNetworkParameters; + } + + public boolean hasXRouter() { + return hasXRouter; + } + + public void deinit() { + if (getTicker() == getActiveBlocknetNetwork() && blocknetPeerGroup != null) { + blocknetPeerGroup.stop(); + } + + if (coinRPCServer != null) { + try { + coinRPCServer.deinit(); + coinRPCServer.join(); + } catch (Exception e) { + LOGGER.log(Level.FINER, "[coin] ERROR: Error while deinitializing coin RPC server!"); + e.printStackTrace(); + } + } + } + + public CoinError init(String pw, String userMnemonic, boolean isMnemonic) { + return init(pw, userMnemonic, isMnemonic, false); + } + + public CoinError init(String pw, String userMnemonic, boolean isMnemonic, boolean xliteRPC) { + switch (ticker) { + case BLOCKNET: { + LOGGER.log(Level.FINER, "[coin] Initializing for Blocknet main network."); + blocknetNetworkParameters = new BlocknetNetworkParameters(); + networkParameters = blocknetNetworkParameters; + hasXRouter = true; + rpcPort = 41419; + break; + } + case BLOCKNET_TESTNET5: { + LOGGER.log(Level.FINER, "[coin] Initializing for Blocknet test network v5."); + blocknetNetworkParameters = new BlocknetTestnet5NetworkParameters(); + networkParameters = blocknetNetworkParameters; + hasXRouter = true; + rpcPort = 41419; + testnet = true; + break; + } + case BITCOIN: { + LOGGER.log(Level.FINER, "[coin] Initializing for Bitcoin main network."); + networkParameters = MainNetParams.get(); + rpcPort = 8332; + break; + } + // case BITCOIN_CASH: { + // LOGGER.log(Level.FINER, "[coin] Initializing for BitcoinCash main network."); + // networkParameters = new BitcoinCashNetworkParameters(); + // rpcPort = 48332; + // break; + // } + case LITECOIN: { + LOGGER.log(Level.FINER, "[coin] Initializing for Litecoin main network."); + networkParameters = new LitecoinNetworkParameters(); + rpcPort = 9332; + break; + } + case DASHCOIN: { + LOGGER.log(Level.FINER, "[coin] Initializing for Dashcoin main network."); + networkParameters = new DashcoinNetworkParameters(); + rpcPort = 9998; + break; + } + // case DIGIBYTE: { + // LOGGER.log(Level.FINER, "[coin] Initializing for Digibyte main network."); + // networkParameters = new DigibyteNetworkParameters(); + // rpcPort = 14022; + // break; + // } + case DOGECOIN: { + LOGGER.log(Level.FINER, "[coin] Initializing for Dogecoin main network."); + networkParameters = new DogecoinNetworkParameters(); + rpcPort = 22555; + break; + } + case SYSCOIN: { + LOGGER.log(Level.FINER, "[coin] Initializing for Syscoin main network."); + networkParameters = new SyscoinNetworkParameters(); + rpcPort = 8370; + break; + } + // case TREZARCOIN: { + // networkParameters = new TrezarcoinNetworkParameters(); + // rpcPort = 17299; + // break; + // } + // case BITBAY: { + // networkParameters = new BitbayNetworkParameters(); + // rpcPort = 19915; + // break; + // } + case PIVX: { + LOGGER.log(Level.FINER, "[coin] Initializing for Pivx main network."); + networkParameters = new PivxNetworkParameters(); + rpcPort = 9951; + break; + } + case UNOBTANIUM: { + LOGGER.log(Level.FINER, "[coin] Initializing for Unobtanium main network."); + networkParameters = new UnobtaniumNetworkParameters(); + rpcPort = 65111; + break; + } + // case ALQOCOIN: { + // LOGGER.log(Level.FINER, "[coin] Initializing for Alqo main network."); + // networkParameters = new AlqocoinNetworkParameters(); + // rpcPort = 55000; + // break; + // } + // case POLISCOIN: { + // LOGGER.log(Level.FINER, "[coin] Initializing for Polis main network."); + // networkParameters = new PoliscoinNetworkParameters(); + // rpcPort = 24127; + // break; + // } + // case PHORECOIN: { + // LOGGER.log(Level.FINER, "[coin] Initializing for Phore main network."); + // networkParameters = new PhorecoinNetworkParameters(); + // rpcPort = 11772; + // break; + // } + // case RAVENCOIN: { + // LOGGER.log(Level.FINER, "[coin] Initializing for Ravencoin main network."); + // networkParameters = new RavencoinNetworkParameters(); + // rpcPort = 8766; + // break; + // } + default: { + LOGGER.log(Level.FINER, "[coin] ERROR: Invalid/unsupported network: " + ticker.toString()); + return new CoinError("Unsupported coin", CoinError.CoinErrorCode.UNSUPPORTEDCOIN); + } + } + + if (xliteRPC) { + rpcPort = rpcPort + 1; + + configHelper.setRpcPort(rpcPort); + configHelper.writeConfig(); + } + + Context.propagate(new Context(networkParameters)); + + List baseSeed; + boolean existsOnDisk = false; + + if (isMnemonic) { + baseSeed = Arrays.asList(pw.split(" ")); + } else { + if (KeyHandler.existsBaseECKeyFromLocal()) + existsOnDisk = true;else if (userMnemonic != null) { + if (!KeyHandler.importFromMnemonic(Arrays.asList(new String(userMnemonic).split(" ")), pw)) { + LOGGER.log(Level.FINER, "[wallet] Unable to create wallet from mnemonic"); + return new CoinError("Unable to create wallet from mnemonic", CoinError.CoinErrorCode.BADMNEMONIC); + } + } + + baseSeed = KeyHandler.getBaseSeed(pw); + } + + if (baseSeed == null) { + LOGGER.log(Level.FINER, "[wallet] Possible Bad password: Unable to import or create base seed!"); + return new CoinError("Bad password", CoinError.CoinErrorCode.BADPASSWORD); + } + + // In-memory wallet only + DeterministicSeed seed = new DeterministicSeed(baseSeed, null, "", System.currentTimeMillis() / 1000); + wallet = Wallet.fromSeed(networkParameters, seed); + if (isBlocknetNetwork()) { + String mnemonic = getMnemonic(); + // LOGGER.log(Level.FINE, "[wallet] Mnemonic = " + mnemonic); + } + + // RUN ADDRESS DISCOVERY ONLY DURING WALLET INITIALIZATION + // This ensures discovery runs once at wallet startup in ANY case + if (addressDiscoveryEnabled) { + LOGGER.log(Level.INFO, "[coin] Running address discovery"); + runAddressDiscovery(); + } else { + LOGGER.log(Level.INFO, "[coin] Address discovery disabled"); + } + + // Make sure wallet addresses are available + generateForwardAddresses(true); + + if (configHelper.getRpcPort() == -1000) { + configHelper.setRpcPort(rpcPort); + configHelper.writeConfig(); + } else { + rpcPort = configHelper.getRpcPort(); + } + + if (configHelper.isRpcEnabled() && configHelper.validAuth() && rpcPort != -1) { + coinRPCServer = JSONRPCController.getRPCServer(this); + LOGGER.log(Level.INFO, "[rpc] Starting JSON-RPC server for coin " + CoinTickerUtils.tickerToString(getTicker()) + " on port " + getRPCPort()); + + if (coinRPCServer.isAlive()) + coinRPCServer.deinit(); + + coinRPCServer.start(); + } + + // Blocknet Network / XRouter not used (Dec 10) // if (isBlocknetNetwork() && hasXRouter()) { // XRouterMessageSerializer xRouterMessageSerializer = (getBlocknetNetworkParameters()).getXRouterMessageSerializer(false); // xRouterPacketManager = new XRouterPacketManager(xRouterMessageSerializer, blocknetNetworkParameters); @@ -553,518 +549,518 @@ else if (userMnemonic != null) { // connectToBlocknetNetwork(); // } - return null; - } - - private void generateForwardAddresses(boolean fromStartup) { - int configAddressCount = configHelper.getAddressCount(); - boolean updateConfig = false; - if (configAddressCount < FORWARD_ADDRESS_COUNT) { // minimum starting addresses - configAddressCount = FORWARD_ADDRESS_COUNT; - updateConfig = true; - } - - LOGGER.log(Level.FINER, "[wallet] Generating " + configAddressCount + " forward addresses for network " + getTicker().toString() + "."); - - // Ensure that internal HD wallet pointer matches the count we're expecting. - // Required because wallet doesn't remember last HD wallet address prior to - // reboot. - if (fromStartup) { - for (int i = 0; i < generatedAddressCount; i++) { - getWalletHelper().generateAddress(); - } - } - for (int i = generatedAddressCount; i < configAddressCount; i++) { - generateAddress(false); - } - generatedAddressCount = configAddressCount; - - if (updateConfig) { - configHelper.setAddressCount(configAddressCount); - configHelper.writeConfig(); - } - } - - private void connectToBlocknetNetwork() { - try { - blocknetPeerGroup.start(); - } catch (Exception e) { - LOGGER.log(Level.FINER, "Error while initializing blocking client object!"); - e.printStackTrace(); - return; - } - - LOGGER.log(Level.FINER, "[coin] This network is connecting/connected."); - } - - public Wallet getWallet() { - return wallet; - } - - public String getMnemonic() { - return Joiner.on(" ").join(Objects.requireNonNull(getWallet().getKeyChainSeed().getMnemonicCode())); - } - - public double getAllBalances() { - double balance = 0; - - for (AddressBalance inst : getAddressKeyPairs()) { - balance += inst.getBalanceProp(); - } - - return balance; - } - - public String getAllBalancesFormatted() { - BtcFormat f = BtcFormat.getInstance(BtcFormat.COIN_SCALE); - return f.format(Coin.valueOf((long) (getAllBalances() * Coin.COIN.value))); - } - - public void sendXrGetTransaction(BlocknetPeer blocknetPeer, String txid) { - String currentCurrency = CoinTickerUtils.tickerToString(getTicker()); - - HashMap body = new HashMap<>(); - body.put("currency", currentCurrency); - body.put("txid", txid); - - getInstance(activeBlocknetNetwork).sendXrMessage(blocknetPeer, "xrGetTransaction", body); - } - - public void sendXrGetBlockCount(BlocknetPeer blocknetPeer) { - String currentCurrency = CoinTickerUtils.tickerToString(getTicker()); - - HashMap body = new HashMap<>(); - body.put("currency", currentCurrency); - - getInstance(activeBlocknetNetwork).sendXrMessage(blocknetPeer, "xrGetBlockCount", body); - } - - public void sendXrGetUtxos(BlocknetPeer blocknetPeer) { - if (System.currentTimeMillis() - lastUtxoUpdate < MINIMUM_UTXO_UPDATE_INTERVAL) { - LOGGER.log(Level.FINER, "[coin] Aborting UTXO checking as the list was updated less than 1 second ago."); - return; - } - - String currentCurrency = CoinTickerUtils.tickerToString(getTicker()); - - HashMap body = new HashMap<>(); - body.put("currency", currentCurrency); - body.put("command", "xrmgetutxos"); - body.put("params", getInstance(CoinTickerUtils.stringToTicker(currentCurrency)).getUTXOParams()); - - getInstance(activeBlocknetNetwork).sendXrMessage(blocknetPeer, "xrService", body); - } - - public String sendXrMessage(BlocknetPeer blocknetPeer, String command, HashMap params) { - return sendXrMessage(blocknetPeer, UUID.randomUUID().toString(), command, params); - } - - public String sendXrMessage(BlocknetPeer blocknetPeer, String uuid, String command, HashMap params) { - XRouterMessage message = null; - - if (blocknetPeer == null || !blocknetPeer.getHaveConfig().get()) { - LOGGER.log(Level.FINER, "[sendXrMessage] Config not received yet"); - return null; - } - - switch (command) { - case "xrGetBlockCount": { - String currency = (String) params.get("currency"); - - message = getXRouterPacketManager().getXrGetBlockCount( - blocknetPeer, - uuid, - currency, - keyHandler.getBaseECKey(), - keyHandler.getPublicKey()); - break; - } - case "xrService": { - String xrCustomCmd = (String) params.get("command"); - - ArrayList paramsList = (ArrayList) params.get("params"); - - message = getXRouterPacketManager().getXrService( - blocknetPeer, - uuid, - xrCustomCmd, - paramsList, - keyHandler.getBaseECKey(), - keyHandler.getPublicKey()); - break; - } - case "xrSendTransaction": { - String feePayment = (String) params.get("feetx"); - String transaction = (String) params.get("transaction"); - String currency = (String) params.get("currency"); - - message = getXRouterPacketManager().getXrSendTransaction( - blocknetPeer, - uuid, - feePayment, - currency, - transaction, - keyHandler.getBaseECKey(), - keyHandler.getPublicKey()); - break; - } - case "xrGetBlockHash": { - String feePayment = (String) params.get("feetx"); - String blockIndex = (String) params.get("blockIndex"); - String currency = (String) params.get("currency"); - - message = getXRouterPacketManager().getXrGetBlockHash( - blocknetPeer, - uuid, - feePayment, - currency, - blockIndex, - keyHandler.getBaseECKey(), - keyHandler.getPublicKey()); - break; - } - case "xrGetBlock": { - String feePayment = (String) params.get("feetx"); - String blockHash = (String) params.get("blockHash"); - String currency = (String) params.get("currency"); - - message = getXRouterPacketManager().getXrGetBlock( - blocknetPeer, - uuid, - feePayment, - currency, - blockHash, - keyHandler.getBaseECKey(), - keyHandler.getPublicKey()); - break; - } - case "xrGetTransaction": { - String txid = (String) params.get("txid"); - String currency = (String) params.get("currency"); - - message = getXRouterPacketManager().getXrGetTransaction( - blocknetPeer, - uuid, - currency, - txid, - keyHandler.getBaseECKey(), - keyHandler.getPublicKey()); - break; - } - case "xrGetConfig": { - message = getXRouterPacketManager().getXrGetConfig( - blocknetPeer, - uuid, - "self", - keyHandler.getBaseECKey(), - keyHandler.getPublicKey()); - break; - } - default: { - LOGGER.log(Level.FINER, "[coin] ERROR: Unknown XRouter Message! Command: " + command); - uuid = null; - break; - } - } - - if (message != null) - blocknetPeerGroup.sendMessage(blocknetPeer, message); - - return uuid; - } - - public JsonArray getAllUTXOS() { - MonetaryFormat PLAIN_FORMAT = MonetaryFormat.BTC.minDecimals(8).repeatOptionalDecimals(1, 0).noCode(); - - JsonArray unspentTxsJSON = new JsonArray(); - for (AddressBalance addressBalance : getAddressKeyPairs()) { - for (UTXO utxo : addressBalance.getUtxos()) { - if (utxo.isSpent()) - continue; - - org.bitcoinj.core.UTXO bUtxo = utxo.createUTXO(); - - JsonObject utxoJSON = new JsonObject(); - utxoJSON.addProperty("txid", bUtxo.getHash().toString()); - utxoJSON.addProperty("vout", bUtxo.getIndex()); - utxoJSON.addProperty("address", bUtxo.getAddress()); - - Monetary monetary = new Monetary() { - @Override - public int smallestUnitExponent() { - return 8; - } - - @Override - public long getValue() { - return utxo.getValue(); - } - - @Override - public int signum() { - if (this.getValue() == 0) - return 0; - return this.getValue() < 0 ? -1 : 1; - } - }; - - BigDecimal amountDecimal = new BigDecimal(PLAIN_FORMAT.format(monetary).toString()); - - utxoJSON.addProperty("amount", amountDecimal); - utxoJSON.addProperty("scriptPubKey", new String(Hex.encode(bUtxo.getScript().getProgram()))); - utxoJSON.addProperty("spendable", true); - - int totalBlocks = CoinInstance.getBlockCountByTicker(getTicker()); - int confirmations = (totalBlocks - bUtxo.getHeight()) + 1; - if (bUtxo.getHeight() == 0) - confirmations = 0; - - utxoJSON.addProperty("confirmations", confirmations); - - unspentTxsJSON.add(utxoJSON); - } - } - - return unspentTxsJSON; - } - - public JsonArray getAllTransactions() { - JsonArray transactionsJSON = new JsonArray(); - for (Transaction tx : transactionList.values()) { - JsonObject txJSON = new JsonObject(); - txJSON.addProperty("category", tx.getCategory()); - txJSON.addProperty("txid", tx.getTxid()); - txJSON.addProperty("blockhash", tx.getBlockhash()); - txJSON.addProperty("vout", tx.getVout()); - txJSON.addProperty("address", tx.getAddress()); - - txJSON.addProperty("amount", tx.getValue()); - txJSON.addProperty("fee", tx.getFee()); - txJSON.addProperty("trusted", false); - txJSON.addProperty("blocktime", tx.getBlocktime()); - txJSON.addProperty("time", tx.getBlocktime()); - - txJSON.addProperty("confirmations", tx.getConfirmations()); - - transactionsJSON.add(txJSON); - } - - - return transactionsJSON; - } - - public ArrayList getUTXOParams() { - ArrayList params = new ArrayList<>(); - params.add(CoinTickerUtils.tickerToString(getTicker())); - - JSONArray utxoAddresses = new JSONArray(); - for (AddressBalance addressBalance : getAddressKeyPairs()) { - utxoAddresses.put(addressBalance.getAddress().toBase58()); - } - - params.add(utxoAddresses.toString()); - - return params; - } - - public AddressBalance getAddressBalance(String address) { - for (AddressBalance addressBalance : getAddressKeyPairs()) { - if (addressBalance.getAddress().toBase58().equals(address)) { - return addressBalance; - } - } - - return null; - } - - public ConfigHelper getConfigHelper() { - return this.configHelper; - } - - public WalletHelper getWalletHelper() { - if (this.walletHelper == null) - this.walletHelper = new WalletHelper(this); - - return this.walletHelper; - } - - public void addBlockCount(CoinTicker ticker, Integer blockCount) { - if (blockCounts.containsKey(ticker)) { - if (blockCounts.get(ticker).get() > blockCount) - return; - - blockCounts.get(ticker).set(blockCount); - return; - } - - blockCounts.put(ticker, new AtomicInteger(blockCount)); - } + return null; + } + + private void generateForwardAddresses(boolean fromStartup) { + int configAddressCount = configHelper.getAddressCount(); + boolean updateConfig = false; + if (configAddressCount < FORWARD_ADDRESS_COUNT) { // minimum starting addresses + configAddressCount = FORWARD_ADDRESS_COUNT; + updateConfig = true; + } + + LOGGER.log(Level.FINER, "[wallet] Generating " + configAddressCount + " forward addresses for network " + getTicker().toString() + "."); + + // Ensure that internal HD wallet pointer matches the count we're expecting. + // Required because wallet doesn't remember last HD wallet address prior to + // reboot. + if (fromStartup) { + for (int i = 0; i < generatedAddressCount; i++) { + getWalletHelper().generateAddress(); + } + } + for (int i = generatedAddressCount; i < configAddressCount; i++) { + generateAddress(false); + } + generatedAddressCount = configAddressCount; + + if (updateConfig) { + configHelper.setAddressCount(configAddressCount); + configHelper.writeConfig(); + } + } + + private void connectToBlocknetNetwork() { + try { + blocknetPeerGroup.start(); + } catch (Exception e) { + LOGGER.log(Level.FINER, "Error while initializing blocking client object!"); + e.printStackTrace(); + return; + } + + LOGGER.log(Level.FINER, "[coin] This network is connecting/connected."); + } + + public Wallet getWallet() { + return wallet; + } + + public String getMnemonic() { + return Joiner.on(" ").join(Objects.requireNonNull(getWallet().getKeyChainSeed().getMnemonicCode())); + } + + public double getAllBalances() { + double balance = 0; + + for (AddressBalance inst : getAddressKeyPairs()) { + balance += inst.getBalanceProp(); + } + + return balance; + } + + public String getAllBalancesFormatted() { + BtcFormat f = BtcFormat.getInstance(BtcFormat.COIN_SCALE); + return f.format(Coin.valueOf((long) (getAllBalances() * Coin.COIN.value))); + } + + public void sendXrGetTransaction(BlocknetPeer blocknetPeer, String txid) { + String currentCurrency = CoinTickerUtils.tickerToString(getTicker()); - public void addRelayFee(CoinTicker ticker, Double relayFee) { - if (relayFees.containsKey(ticker)) { - relayFees.get(ticker).set(relayFee); - return; - } + HashMap body = new HashMap<>(); + body.put("currency", currentCurrency); + body.put("txid", txid); - configHelper.setFee(relayFee); + getInstance(activeBlocknetNetwork).sendXrMessage(blocknetPeer, "xrGetTransaction", body); + } + + public void sendXrGetBlockCount(BlocknetPeer blocknetPeer) { + String currentCurrency = CoinTickerUtils.tickerToString(getTicker()); + + HashMap body = new HashMap<>(); + body.put("currency", currentCurrency); + + getInstance(activeBlocknetNetwork).sendXrMessage(blocknetPeer, "xrGetBlockCount", body); + } + + public void sendXrGetUtxos(BlocknetPeer blocknetPeer) { + if (System.currentTimeMillis() - lastUtxoUpdate < MINIMUM_UTXO_UPDATE_INTERVAL) { + LOGGER.log(Level.FINER, "[coin] Aborting UTXO checking as the list was updated less than 1 second ago."); + return; + } + + String currentCurrency = CoinTickerUtils.tickerToString(getTicker()); + + HashMap body = new HashMap<>(); + body.put("currency", currentCurrency); + body.put("command", "xrmgetutxos"); + body.put("params", getInstance(CoinTickerUtils.stringToTicker(currentCurrency)).getUTXOParams()); + + getInstance(activeBlocknetNetwork).sendXrMessage(blocknetPeer, "xrService", body); + } + + public String sendXrMessage(BlocknetPeer blocknetPeer, String command, HashMap params) { + return sendXrMessage(blocknetPeer, UUID.randomUUID().toString(), command, params); + } + + public String sendXrMessage(BlocknetPeer blocknetPeer, String uuid, String command, HashMap params) { + XRouterMessage message = null; + + if (blocknetPeer == null || !blocknetPeer.getHaveConfig().get()) { + LOGGER.log(Level.FINER, "[sendXrMessage] Config not received yet"); + return null; + } + + switch (command) { + case "xrGetBlockCount": { + String currency = (String) params.get("currency"); + + message = getXRouterPacketManager().getXrGetBlockCount( + blocknetPeer, + uuid, + currency, + keyHandler.getBaseECKey(), + keyHandler.getPublicKey()); + break; + } + case "xrService": { + String xrCustomCmd = (String) params.get("command"); + + ArrayList paramsList = (ArrayList) params.get("params"); + + message = getXRouterPacketManager().getXrService( + blocknetPeer, + uuid, + xrCustomCmd, + paramsList, + keyHandler.getBaseECKey(), + keyHandler.getPublicKey()); + break; + } + case "xrSendTransaction": { + String feePayment = (String) params.get("feetx"); + String transaction = (String) params.get("transaction"); + String currency = (String) params.get("currency"); + + message = getXRouterPacketManager().getXrSendTransaction( + blocknetPeer, + uuid, + feePayment, + currency, + transaction, + keyHandler.getBaseECKey(), + keyHandler.getPublicKey()); + break; + } + case "xrGetBlockHash": { + String feePayment = (String) params.get("feetx"); + String blockIndex = (String) params.get("blockIndex"); + String currency = (String) params.get("currency"); + + message = getXRouterPacketManager().getXrGetBlockHash( + blocknetPeer, + uuid, + feePayment, + currency, + blockIndex, + keyHandler.getBaseECKey(), + keyHandler.getPublicKey()); + break; + } + case "xrGetBlock": { + String feePayment = (String) params.get("feetx"); + String blockHash = (String) params.get("blockHash"); + String currency = (String) params.get("currency"); + + message = getXRouterPacketManager().getXrGetBlock( + blocknetPeer, + uuid, + feePayment, + currency, + blockHash, + keyHandler.getBaseECKey(), + keyHandler.getPublicKey()); + break; + } + case "xrGetTransaction": { + String txid = (String) params.get("txid"); + String currency = (String) params.get("currency"); + + message = getXRouterPacketManager().getXrGetTransaction( + blocknetPeer, + uuid, + currency, + txid, + keyHandler.getBaseECKey(), + keyHandler.getPublicKey()); + break; + } + case "xrGetConfig": { + message = getXRouterPacketManager().getXrGetConfig( + blocknetPeer, + uuid, + "self", + keyHandler.getBaseECKey(), + keyHandler.getPublicKey()); + break; + } + default: { + LOGGER.log(Level.FINER, "[coin] ERROR: Unknown XRouter Message! Command: " + command); + uuid = null; + break; + } + } + + if (message != null) + blocknetPeerGroup.sendMessage(blocknetPeer, message); + + return uuid; + } + + public JsonArray getAllUTXOS() { + MonetaryFormat PLAIN_FORMAT = MonetaryFormat.BTC.minDecimals(8).repeatOptionalDecimals(1, 0).noCode(); + + JsonArray unspentTxsJSON = new JsonArray(); + for (AddressBalance addressBalance : getAddressKeyPairs()) { + for (UTXO utxo : addressBalance.getUtxos()) { + if (utxo.isSpent()) + continue; + + org.bitcoinj.core.UTXO bUtxo = utxo.createUTXO(); + + JsonObject utxoJSON = new JsonObject(); + utxoJSON.addProperty("txid", bUtxo.getHash().toString()); + utxoJSON.addProperty("vout", bUtxo.getIndex()); + utxoJSON.addProperty("address", bUtxo.getAddress()); + + Monetary monetary = new Monetary() { + @Override + public int smallestUnitExponent() { + return 8; + } + + @Override + public long getValue() { + return utxo.getValue(); + } + + @Override + public int signum() { + if (this.getValue() == 0) + return 0; + return this.getValue() < 0 ? -1 : 1; + } + }; + + BigDecimal amountDecimal = new BigDecimal(PLAIN_FORMAT.format(monetary).toString()); + + utxoJSON.addProperty("amount", amountDecimal); + utxoJSON.addProperty("scriptPubKey", new String(Hex.encode(bUtxo.getScript().getProgram()))); + utxoJSON.addProperty("spendable", true); + + int totalBlocks = CoinInstance.getBlockCountByTicker(getTicker()); + int confirmations = (totalBlocks - bUtxo.getHeight()) + 1; + if (bUtxo.getHeight() == 0) + confirmations = 0; + + utxoJSON.addProperty("confirmations", confirmations); + + unspentTxsJSON.add(utxoJSON); + } + } + + return unspentTxsJSON; + } + + public JsonArray getAllTransactions() { + JsonArray transactionsJSON = new JsonArray(); + for (Transaction tx : transactionList.values()) { + JsonObject txJSON = new JsonObject(); + txJSON.addProperty("category", tx.getCategory()); + txJSON.addProperty("txid", tx.getTxid()); + txJSON.addProperty("blockhash", tx.getBlockhash()); + txJSON.addProperty("vout", tx.getVout()); + txJSON.addProperty("address", tx.getAddress()); + + txJSON.addProperty("amount", tx.getValue()); + txJSON.addProperty("fee", tx.getFee()); + txJSON.addProperty("trusted", false); + txJSON.addProperty("blocktime", tx.getBlocktime()); + txJSON.addProperty("time", tx.getBlocktime()); + + txJSON.addProperty("confirmations", tx.getConfirmations()); + + transactionsJSON.add(txJSON); + } + + + return transactionsJSON; + } + + public ArrayList getUTXOParams() { + ArrayList params = new ArrayList<>(); + params.add(CoinTickerUtils.tickerToString(getTicker())); + + JSONArray utxoAddresses = new JSONArray(); + for (AddressBalance addressBalance : getAddressKeyPairs()) { + utxoAddresses.put(addressBalance.getAddress().toBase58()); + } + + params.add(utxoAddresses.toString()); + + return params; + } + + public AddressBalance getAddressBalance(String address) { + for (AddressBalance addressBalance : getAddressKeyPairs()) { + if (addressBalance.getAddress().toBase58().equals(address)) { + return addressBalance; + } + } + + return null; + } + + public ConfigHelper getConfigHelper() { + return this.configHelper; + } + + public WalletHelper getWalletHelper() { + if (this.walletHelper == null) + this.walletHelper = new WalletHelper(this); + + return this.walletHelper; + } + + public void addBlockCount(CoinTicker ticker, Integer blockCount) { + if (blockCounts.containsKey(ticker)) { + if (blockCounts.get(ticker).get() > blockCount) + return; + + blockCounts.get(ticker).set(blockCount); + return; + } + + blockCounts.put(ticker, new AtomicInteger(blockCount)); + } + + public void addRelayFee(CoinTicker ticker, Double relayFee) { + if (relayFees.containsKey(ticker)) { + relayFees.get(ticker).set(relayFee); + return; + } + + configHelper.setFee(relayFee); - relayFees.put(ticker, new AtomicDouble(relayFee)); - } - - public void addCloudTransaction(CloudTransaction cloudTransaction) { - if (transactionObservableList.isEmpty()) { - transactionObservableList.add(cloudTransaction); - return; - } + relayFees.put(ticker, new AtomicDouble(relayFee)); + } + + public void addCloudTransaction(CloudTransaction cloudTransaction) { + if (transactionObservableList.isEmpty()) { + transactionObservableList.add(cloudTransaction); + return; + } - CloudTransaction tx = transactionObservableList.stream() - .filter(e -> e.getTxHash().equals(cloudTransaction.getTxHash())).findAny().orElse(null); + CloudTransaction tx = transactionObservableList.stream() + .filter(e -> e.getTxHash().equals(cloudTransaction.getTxHash())).findAny().orElse(null); - if (tx == null) { - transactionObservableList.add(cloudTransaction); - } - } + if (tx == null) { + transactionObservableList.add(cloudTransaction); + } + } - public void processUtxos(List utxoList) { - // first lets clear UTXOs out of each address - for (UTXO utxo : utxoList) { - AddressBalance addressBalance = getAddress(utxo.getAddress()); - addressBalance.clearUtxos(); - } + public void processUtxos(List utxoList) { + // first lets clear UTXOs out of each address + for (UTXO utxo : utxoList) { + AddressBalance addressBalance = getAddress(utxo.getAddress()); + addressBalance.clearUtxos(); + } - // now let's add them back - for (UTXO utxo : utxoList) { - AddressBalance addressBalance = getAddress(utxo.getAddress()); + // now let's add them back + for (UTXO utxo : utxoList) { + AddressBalance addressBalance = getAddress(utxo.getAddress()); - if (addressBalance == null) { - LOGGER.log(Level.FINER, "[utxo-parser] Warning: Encountered non-tracked address in reply: " + utxo.getAddress()); - continue; - } + if (addressBalance == null) { + LOGGER.log(Level.FINER, "[utxo-parser] Warning: Encountered non-tracked address in reply: " + utxo.getAddress()); + continue; + } - boolean isNewUtxo = addressBalance.addUtxo(utxo); + boolean isNewUtxo = addressBalance.addUtxo(utxo); - if (isNewUtxo) { - addCloudTransaction(new CloudTransaction(utxo)); - LOGGER.log(Level.FINER, "[utxo-parser] Added new UTXO, address: " + utxo.getAddress() + " value: " + utxo.getAmount()); - } - } + if (isNewUtxo) { + addCloudTransaction(new CloudTransaction(utxo)); + LOGGER.log(Level.FINER, "[utxo-parser] Added new UTXO, address: " + utxo.getAddress() + " value: " + utxo.getAmount()); + } + } - setLastUtxoUpdate(System.currentTimeMillis()); - } + setLastUtxoUpdate(System.currentTimeMillis()); + } - public void processHistoryTxs(List transactions) { - for (Transaction tx : transactions) + public void processHistoryTxs(List transactions) { + for (Transaction tx : transactions) transactionList.put(tx.uid(), tx); - } - - private void setLastUtxoUpdate(long newUtxoTime) { - lastUtxoUpdate = newUtxoTime; - } - - public void reloadConfig() { - this.configHelper.loadConfig(); - rpcPort = configHelper.getRpcPort(); - if (configHelper.getAddressCount() != generatedAddressCount) - generateForwardAddresses(false); - - if (coinRPCServer == null) - return; // no rpc available, skip - - JSONRPCController.removeRPCServer(this); - - coinRPCServer = JSONRPCController.getRPCServer(this); - - LOGGER.log(Level.INFO, "[rpc] Starting JSON-RPC server for coin " + CoinTickerUtils.tickerToString(getTicker()) + " on port " + getRPCPort()); - coinRPCServer.start(); - } - - public KeyHandler getKeyHandler() { - return keyHandler; - } - - public BlocknetPeerGroup getBlocknetPeerGroup() { - return blocknetPeerGroup; - } - - public BlocknetPeer getBestBlocknetPeer(String currency) { - if (blocknetPeerGroup == null) - return null; - - return blocknetPeerGroup.getBestBlocknetPeer(currency); - } - - public ArrayList getAddressKeyPairs() { - return addressKeyPairs; - } - - public ArrayList getTransactionList() { - return transactionObservableList; - } - - public static AtomicInteger getBlockCount(CoinTicker ticker) { - return blockCounts.putIfAbsent(ticker, new AtomicInteger(0)); - } - - public static HashMap getBlockCounts() { - return blockCounts; - } - - public int getRPCPort() { - return rpcPort; - } - - public boolean isTestnet() { - return testnet; - } - - public static String getVersionString() { - return Version.SUBVERSION; - } - - public void incrementUpdateFailures() { - updateFailures += 1; - } - - public void resetUpdateFailures() { - updateFailures = 0; - } - - public void runAddressDiscovery() { - String currency = CoinTickerUtils.tickerToString(this.getTicker()); - - if (discoveryService == null) { - discoveryService = new AddressDiscoveryService(this); - LOGGER.log(Level.INFO, "[coin-" + currency + "] AddressDiscoveryService created"); - } - - int discoveredCount = discoveryService.discoverAddressCount(); - int currentCount = configHelper.getAddressCount(); - - if (discoveredCount > currentCount) { - LOGGER.log(Level.INFO, "[coin-" + currency + "] Address discovery found " + - discoveredCount + " addresses (was " + currentCount + ")"); - - // Update config and generate missing addresses - configHelper.setAddressCount(discoveredCount); - configHelper.writeConfig(); - - LOGGER.log(Level.INFO, "[coin-" + currency + "] Updated address count to " + - discoveredCount); - } else { - LOGGER.log(Level.INFO, "[coin-" + currency + "] No new addresses discovered, " + - "keeping current count: " + currentCount); - } - } - - public boolean isInstanceRunning() { - return updateFailures < 5; - } - - public static void setAddressDiscoveryEnabled(boolean enabled) { - addressDiscoveryEnabled = enabled; - } - - public static boolean isAddressDiscoveryEnabled() { - return addressDiscoveryEnabled; - } + } + + private void setLastUtxoUpdate(long newUtxoTime) { + lastUtxoUpdate = newUtxoTime; + } + + public void reloadConfig() { + this.configHelper.loadConfig(); + rpcPort = configHelper.getRpcPort(); + if (configHelper.getAddressCount() != generatedAddressCount) + generateForwardAddresses(false); + + if (coinRPCServer == null) + return; // no rpc available, skip + + JSONRPCController.removeRPCServer(this); + + coinRPCServer = JSONRPCController.getRPCServer(this); + + LOGGER.log(Level.INFO, "[rpc] Starting JSON-RPC server for coin " + CoinTickerUtils.tickerToString(getTicker()) + " on port " + getRPCPort()); + coinRPCServer.start(); + } + + public KeyHandler getKeyHandler() { + return keyHandler; + } + + public BlocknetPeerGroup getBlocknetPeerGroup() { + return blocknetPeerGroup; + } + + public BlocknetPeer getBestBlocknetPeer(String currency) { + if (blocknetPeerGroup == null) + return null; + + return blocknetPeerGroup.getBestBlocknetPeer(currency); + } + + public ArrayList getAddressKeyPairs() { + return addressKeyPairs; + } + + public ArrayList getTransactionList() { + return transactionObservableList; + } + + public static AtomicInteger getBlockCount(CoinTicker ticker) { + return blockCounts.putIfAbsent(ticker, new AtomicInteger(0)); + } + + public static HashMap getBlockCounts() { + return blockCounts; + } + + public int getRPCPort() { + return rpcPort; + } + + public boolean isTestnet() { + return testnet; + } + + public static String getVersionString() { + return Version.SUBVERSION; + } + + public void incrementUpdateFailures() { + updateFailures += 1; + } + + public void resetUpdateFailures() { + updateFailures = 0; + } + + public void runAddressDiscovery() { + String currency = CoinTickerUtils.tickerToString(this.getTicker()); + + if (discoveryService == null) { + discoveryService = new AddressDiscoveryService(this); + LOGGER.log(Level.INFO, "[coin-" + currency + "] AddressDiscoveryService created"); + } + + int discoveredCount = discoveryService.discoverAddressCount(); + int currentCount = configHelper.getAddressCount(); + + if (discoveredCount > currentCount) { + LOGGER.log(Level.INFO, "[coin-" + currency + "] Address discovery found " + + discoveredCount + " addresses (was " + currentCount + ")"); + + // Update config and generate missing addresses + configHelper.setAddressCount(discoveredCount); + configHelper.writeConfig(); + + LOGGER.log(Level.INFO, "[coin-" + currency + "] Updated address count to " + + discoveredCount); + } else { + LOGGER.log(Level.INFO, "[coin-" + currency + "] No new addresses discovered, " + + "keeping current count: " + currentCount); + } + } + + public boolean isInstanceRunning() { + return updateFailures < 5; + } + + public static void setAddressDiscoveryEnabled(boolean enabled) { + addressDiscoveryEnabled = enabled; + } + + public static boolean isAddressDiscoveryEnabled() { + return addressDiscoveryEnabled; + } } diff --git a/src/main/java/io/cloudchains/app/net/CoinTicker.java b/src/main/java/io/cloudchains/app/net/CoinTicker.java index 391cc84..80b7cb4 100644 --- a/src/main/java/io/cloudchains/app/net/CoinTicker.java +++ b/src/main/java/io/cloudchains/app/net/CoinTicker.java @@ -4,44 +4,44 @@ import java.util.List; public enum CoinTicker { - BLOCKNET, - BLOCKNET_TESTNET5, + BLOCKNET, + BLOCKNET_TESTNET5, - BITCOIN, - BITCOIN_CASH, - LITECOIN, - DASHCOIN, - DIGIBYTE, - DOGECOIN, - TREZARCOIN, - SYSCOIN, - PIVX, + BITCOIN, + BITCOIN_CASH, + LITECOIN, + DASHCOIN, + DIGIBYTE, + DOGECOIN, + TREZARCOIN, + SYSCOIN, + PIVX, ALQOCOIN, POLISCOIN, PHORECOIN, RAVENCOIN, - BITBAY, + BITBAY, UNOBTANIUM ; /** - * List of supported coins. - * @return Supported coins - */ + * List of supported coins. + * @return Supported coins + */ public static List coins() { return Arrays.asList( - BLOCKNET, - BLOCKNET_TESTNET5, - BITCOIN, + BLOCKNET, + BLOCKNET_TESTNET5, + BITCOIN, // BITCOIN_CASH, - not support on backend - LITECOIN, - DASHCOIN, + LITECOIN, + DASHCOIN, // DIGIBYTE, - not support on backend - DOGECOIN, + DOGECOIN, // TREZARCOIN, - not support on backend - SYSCOIN, - PIVX, - UNOBTANIUM + SYSCOIN, + PIVX, + UNOBTANIUM // ALQOCOIN, - not support on backend // POLISCOIN, - not support on backend // PHORECOIN, - not support on backend diff --git a/src/main/java/io/cloudchains/app/net/CoinTickerUtils.java b/src/main/java/io/cloudchains/app/net/CoinTickerUtils.java index bfe13af..2e4a218 100644 --- a/src/main/java/io/cloudchains/app/net/CoinTickerUtils.java +++ b/src/main/java/io/cloudchains/app/net/CoinTickerUtils.java @@ -7,19 +7,19 @@ import java.util.Set; public class CoinTickerUtils { - private static HashBiMap tickers; + private static HashBiMap tickers; - static { - tickers = HashBiMap.create(); + static { + tickers = HashBiMap.create(); - tickers.put(CoinTicker.BLOCKNET, "BLOCK"); - tickers.put(CoinTicker.BLOCKNET_TESTNET5, "TBLOCK"); - tickers.put(CoinTicker.BITCOIN, "BTC"); - tickers.put(CoinTicker.LITECOIN, "LTC"); - tickers.put(CoinTicker.DASHCOIN, "DASH"); - tickers.put(CoinTicker.DOGECOIN, "DOGE"); - tickers.put(CoinTicker.SYSCOIN, "SYS"); - tickers.put(CoinTicker.PIVX, "PIVX"); + tickers.put(CoinTicker.BLOCKNET, "BLOCK"); + tickers.put(CoinTicker.BLOCKNET_TESTNET5, "TBLOCK"); + tickers.put(CoinTicker.BITCOIN, "BTC"); + tickers.put(CoinTicker.LITECOIN, "LTC"); + tickers.put(CoinTicker.DASHCOIN, "DASH"); + tickers.put(CoinTicker.DOGECOIN, "DOGE"); + tickers.put(CoinTicker.SYSCOIN, "SYS"); + tickers.put(CoinTicker.PIVX, "PIVX"); // TODO Temporarily disable until supported // tickers.put(CoinTicker.DIGIBYTE, "DGB"); @@ -30,33 +30,33 @@ public class CoinTickerUtils { // TODO Temporarily disable PHORE and POLIS until supported // tickers.put(CoinTicker.POLISCOIN, "POLIS"); // tickers.put(CoinTicker.PHORECOIN, "PHR"); - tickers.put(CoinTicker.TREZARCOIN, "TZC"); - tickers.put(CoinTicker.BITBAY, "BAY"); - tickers.put(CoinTicker.UNOBTANIUM, "UNO"); - - } - - public static String tickerToString(CoinTicker ticker) { - return tickers.get(ticker); - } - - public static CoinTicker stringToTicker(String string) { - return tickers.inverse().get(string); - } - - public static Set getNetworkTickers() { - return new HashSet<>(Arrays.asList(CoinTicker.BLOCKNET, CoinTicker.BLOCKNET_TESTNET5)); - } - - public static CoinTicker[] getActiveTickers() { - return new CoinTicker[] { - CoinTicker.BLOCKNET, - CoinTicker.BITCOIN, - CoinTicker.LITECOIN, - CoinTicker.DASHCOIN, - CoinTicker.DOGECOIN, - CoinTicker.SYSCOIN, - CoinTicker.PIVX, + tickers.put(CoinTicker.TREZARCOIN, "TZC"); + tickers.put(CoinTicker.BITBAY, "BAY"); + tickers.put(CoinTicker.UNOBTANIUM, "UNO"); + + } + + public static String tickerToString(CoinTicker ticker) { + return tickers.get(ticker); + } + + public static CoinTicker stringToTicker(String string) { + return tickers.inverse().get(string); + } + + public static Set getNetworkTickers() { + return new HashSet<>(Arrays.asList(CoinTicker.BLOCKNET, CoinTicker.BLOCKNET_TESTNET5)); + } + + public static CoinTicker[] getActiveTickers() { + return new CoinTicker[]{ + CoinTicker.BLOCKNET, + CoinTicker.BITCOIN, + CoinTicker.LITECOIN, + CoinTicker.DASHCOIN, + CoinTicker.DOGECOIN, + CoinTicker.SYSCOIN, + CoinTicker.PIVX, // TODO Temporarily disable until supported // CoinTicker.DIGIBYTE, @@ -67,22 +67,22 @@ public static CoinTicker[] getActiveTickers() { // TODO Temporarily disable PHORE and POLIS until supported // CoinTicker.POLISCOIN, // CoinTicker.PHORECOIN, - CoinTicker.TREZARCOIN, - CoinTicker.BITBAY, - CoinTicker.UNOBTANIUM, + CoinTicker.TREZARCOIN, + CoinTicker.BITBAY, + CoinTicker.UNOBTANIUM, }; - } - - public static boolean tickerExists(String string) { - return tickers.inverse().containsKey(string); - } - - public static boolean isActiveTicker(CoinTicker ticker) { - for (CoinTicker t : getActiveTickers()) { - if (ticker == t) { - return true; - } - } - return false; - } + } + + public static boolean tickerExists(String string) { + return tickers.inverse().containsKey(string); + } + + public static boolean isActiveTicker(CoinTicker ticker) { + for (CoinTicker t : getActiveTickers()) { + if (ticker == t) { + return true; + } + } + return false; + } } diff --git a/src/main/java/io/cloudchains/app/net/api/JSONRPCController.java b/src/main/java/io/cloudchains/app/net/api/JSONRPCController.java index 4cbfd06..d84fd61 100644 --- a/src/main/java/io/cloudchains/app/net/api/JSONRPCController.java +++ b/src/main/java/io/cloudchains/app/net/api/JSONRPCController.java @@ -7,37 +7,37 @@ public class JSONRPCController { - private static final HashMap servers = new HashMap<>(); - private static JSONRPCMasterServer masterServer = new JSONRPCMasterServer(new ConfigHelper("master").getMasterRpcPort()); + private static final HashMap servers = new HashMap<>(); + private static JSONRPCMasterServer masterServer = new JSONRPCMasterServer(new ConfigHelper("master").getMasterRpcPort()); - public static JSONRPCMasterServer getMasterServer() { - return masterServer; - } + public static JSONRPCMasterServer getMasterServer() { + return masterServer; + } - public static JSONRPCServer getRPCServer(CoinInstance coinInstance) { - if (coinInstance == null || coinInstance.getRPCPort() == -1) { - throw new IllegalArgumentException("Bad coin instance"); - } + public static JSONRPCServer getRPCServer(CoinInstance coinInstance) { + if (coinInstance == null || coinInstance.getRPCPort() == -1) { + throw new IllegalArgumentException("Bad coin instance"); + } - if (!servers.containsKey(coinInstance)) { - servers.put(coinInstance, new JSONRPCServer(coinInstance, coinInstance.getRPCPort())); - } + if (!servers.containsKey(coinInstance)) { + servers.put(coinInstance, new JSONRPCServer(coinInstance, coinInstance.getRPCPort())); + } - return servers.get(coinInstance); - } + return servers.get(coinInstance); + } - public static void removeRPCServer(CoinInstance coinInstance) { - if (coinInstance == null || coinInstance.getRPCPort() == -1) { - throw new IllegalArgumentException("Bad coin instance"); - } + public static void removeRPCServer(CoinInstance coinInstance) { + if (coinInstance == null || coinInstance.getRPCPort() == -1) { + throw new IllegalArgumentException("Bad coin instance"); + } - JSONRPCServer server = servers.get(coinInstance); - if (server == null) - return; + JSONRPCServer server = servers.get(coinInstance); + if (server == null) + return; - if (server.isAlive()) - server.deinit(); + if (server.isAlive()) + server.deinit(); - servers.remove(coinInstance); - } + servers.remove(coinInstance); + } } diff --git a/src/main/java/io/cloudchains/app/net/api/JSONRPCMasterServer.java b/src/main/java/io/cloudchains/app/net/api/JSONRPCMasterServer.java index a301328..6326c8c 100644 --- a/src/main/java/io/cloudchains/app/net/api/JSONRPCMasterServer.java +++ b/src/main/java/io/cloudchains/app/net/api/JSONRPCMasterServer.java @@ -1,6 +1,5 @@ package io.cloudchains.app.net.api; -import io.cloudchains.app.App; import io.cloudchains.app.net.api.http.master.HTTPServerInitializer; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.PooledByteBufAllocator; @@ -15,50 +14,50 @@ import java.util.logging.Logger; public class JSONRPCMasterServer extends Thread { - private final static LogManager LOGMANAGER = LogManager.getLogManager(); - private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); - - private final int port; - private boolean stopping = false; - - private Channel channel; - - JSONRPCMasterServer(int port) { - this.port = port; - } - - public void run() { - EventLoopGroup workerGroup = new NioEventLoopGroup(2); - try { - LOGGER.log(Level.INFO, "[rpc] Starting master RPC server on port " + port + "."); - - ServerBootstrap bootstrap = new ServerBootstrap(); - bootstrap.group(workerGroup) - .option(ChannelOption.SO_BACKLOG, 128) - .option(ChannelOption.SO_REUSEADDR, true) - .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) - .channel(NioServerSocketChannel.class) - .childHandler(new HTTPServerInitializer()); - - channel = bootstrap.bind(port).sync().channel(); - - channel.closeFuture().sync(); - } catch (Exception e) { - if (!stopping) { - LOGGER.log(Level.FINER, "[json-rpc-server] ERROR: Error during server operation! (master RPC)"); - e.printStackTrace(); - } - } - } - - public void deinit() { - stopping = true; - LOGGER.log(Level.FINER, "[json-rpc-server] Interrupting server."); - - if (channel != null && channel.isOpen()) { - channel.close(); - } else { - LOGGER.log(Level.FINER, "[json-rpc-server] Channel is null or not open during deinitialization."); - } - } + private final static LogManager LOGMANAGER = LogManager.getLogManager(); + private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); + + private final int port; + private boolean stopping = false; + + private Channel channel; + + JSONRPCMasterServer(int port) { + this.port = port; + } + + public void run() { + EventLoopGroup workerGroup = new NioEventLoopGroup(2); + try { + LOGGER.log(Level.INFO, "[rpc] Starting master RPC server on port " + port + "."); + + ServerBootstrap bootstrap = new ServerBootstrap(); + bootstrap.group(workerGroup) + .option(ChannelOption.SO_BACKLOG, 128) + .option(ChannelOption.SO_REUSEADDR, true) + .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) + .channel(NioServerSocketChannel.class) + .childHandler(new HTTPServerInitializer()); + + channel = bootstrap.bind(port).sync().channel(); + + channel.closeFuture().sync(); + } catch (Exception e) { + if (!stopping) { + LOGGER.log(Level.FINER, "[json-rpc-server] ERROR: Error during server operation! (master RPC)"); + e.printStackTrace(); + } + } + } + + public void deinit() { + stopping = true; + LOGGER.log(Level.FINER, "[json-rpc-server] Interrupting server."); + + if (channel != null && channel.isOpen()) { + channel.close(); + } else { + LOGGER.log(Level.FINER, "[json-rpc-server] Channel is null or not open during deinitialization."); + } + } } diff --git a/src/main/java/io/cloudchains/app/net/api/JSONRPCServer.java b/src/main/java/io/cloudchains/app/net/api/JSONRPCServer.java index 4469092..d5f3a9e 100644 --- a/src/main/java/io/cloudchains/app/net/api/JSONRPCServer.java +++ b/src/main/java/io/cloudchains/app/net/api/JSONRPCServer.java @@ -16,48 +16,48 @@ import java.util.logging.Logger; public class JSONRPCServer extends Thread { - private final static LogManager LOGMANAGER = LogManager.getLogManager(); - private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); - - private final CoinInstance coin; - private final int port; - private boolean stopping = false; - - private Channel channel; - - JSONRPCServer(CoinInstance coin, int port) { - this.coin = coin; - this.port = port; - } - - public void run() { - EventLoopGroup workerGroup = new NioEventLoopGroup(5); - try { - ServerBootstrap bootstrap = new ServerBootstrap(); - bootstrap.group(workerGroup) - .option(ChannelOption.SO_BACKLOG, 128) - .option(ChannelOption.SO_REUSEADDR, true) - .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) - .channel(NioServerSocketChannel.class) - .childHandler(new HTTPServerInitializer(coin)); - - channel = bootstrap.bind(port).sync().channel(); - - LOGGER.log(Level.FINER, "[rpc] Starting RPC server for " + CoinTickerUtils.tickerToString(coin.getTicker()) + " on port " + port + "."); - - channel.closeFuture().sync(); - } catch (Exception e) { - if (!stopping) { - LOGGER.log(Level.FINER, "[json-rpc-server] ERROR: Error during server operation! (" + CoinTickerUtils.tickerToString(coin.getTicker()) + ")"); - e.printStackTrace(); - } - } - } - - public void deinit() { - stopping = true; - LOGGER.log(Level.FINER, "[json-rpc-server] Interrupting server."); - - channel.close(); - } + private final static LogManager LOGMANAGER = LogManager.getLogManager(); + private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); + + private final CoinInstance coin; + private final int port; + private boolean stopping = false; + + private Channel channel; + + JSONRPCServer(CoinInstance coin, int port) { + this.coin = coin; + this.port = port; + } + + public void run() { + EventLoopGroup workerGroup = new NioEventLoopGroup(5); + try { + ServerBootstrap bootstrap = new ServerBootstrap(); + bootstrap.group(workerGroup) + .option(ChannelOption.SO_BACKLOG, 128) + .option(ChannelOption.SO_REUSEADDR, true) + .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) + .channel(NioServerSocketChannel.class) + .childHandler(new HTTPServerInitializer(coin)); + + channel = bootstrap.bind(port).sync().channel(); + + LOGGER.log(Level.FINER, "[rpc] Starting RPC server for " + CoinTickerUtils.tickerToString(coin.getTicker()) + " on port " + port + "."); + + channel.closeFuture().sync(); + } catch (Exception e) { + if (!stopping) { + LOGGER.log(Level.FINER, "[json-rpc-server] ERROR: Error during server operation! (" + CoinTickerUtils.tickerToString(coin.getTicker()) + ")"); + e.printStackTrace(); + } + } + } + + public void deinit() { + stopping = true; + LOGGER.log(Level.FINER, "[json-rpc-server] Interrupting server."); + + channel.close(); + } } diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/EXRServer.java b/src/main/java/io/cloudchains/app/net/api/http/client/EXRServer.java index a0b581a..d8680d6 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/client/EXRServer.java +++ b/src/main/java/io/cloudchains/app/net/api/http/client/EXRServer.java @@ -1,13 +1,13 @@ package io.cloudchains.app.net.api.http.client; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import io.cloudchains.app.net.CoinTicker; -import org.apache.http.client.config.RequestConfig; -import java.io.IOException; -import java.net.URI; +import io.cloudchains.app.net.CoinTickerUtils; + import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -15,18 +15,16 @@ public class EXRServer { private final static LogManager LOGMANAGER = LogManager.getLogManager(); private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); - private final String endpoint; private final EXRWrapper wrapper; private volatile boolean healthy; private long lastHealthCheck; - private final java.util.Set supportedCoins; + private final Set supportedCoins; private volatile boolean capabilitiesProbed; - // Add constants for configuration private static final int HEALTH_CHECK_INTERVAL_MS = 5000; private static final int CAPABILITY_PROBE_TIMEOUT_MS = 30000; - + public EXRServer(String endpoint) { // Store endpoint with trailing slash for consistency if (endpoint.endsWith("/")) { @@ -34,44 +32,37 @@ public EXRServer(String endpoint) { } else { this.endpoint = endpoint + "/"; } - // EXRWrapper expects endpoint without trailing slash String wrapperEndpoint = this.endpoint.endsWith("/") ? - this.endpoint.substring(0, this.endpoint.length() - 1) : this.endpoint; - + this.endpoint.substring(0, this.endpoint.length() - 1) : this.endpoint; this.wrapper = new EXRWrapper(wrapperEndpoint); this.healthy = true; this.lastHealthCheck = 0; - this.supportedCoins = new java.util.HashSet<>(); + this.supportedCoins = new HashSet<>(); this.capabilitiesProbed = false; } - + public boolean probeCapabilities() { if (capabilitiesProbed) { return true; } - if (!isHealthy()) { return false; } - try { // CALL HEIGHTS ONCE - not per coin - com.google.gson.JsonObject result = wrapper.executeGet("heights"); + JsonObject result = wrapper.executeGet("heights"); if (result != null && result.has("result")) { - com.google.gson.JsonObject heights = result.getAsJsonObject("result"); - + JsonObject heights = result.getAsJsonObject("result"); // Extract ALL supported coins from single response // Only include coins that have non-null values (null means not supported) for (String coinName : heights.keySet()) { - com.google.gson.JsonElement heightValue = heights.get(coinName); - + JsonElement heightValue = heights.get(coinName); if (heightValue.isJsonNull()) { continue; // Skip unsupported coins (null values) } - try { - io.cloudchains.app.net.CoinTicker coin = io.cloudchains.app.net.CoinTickerUtils.stringToTicker(coinName); + CoinTicker coin = CoinTickerUtils.stringToTicker(coinName); if (coin != null) { supportedCoins.add(coin); } @@ -80,26 +71,25 @@ public boolean probeCapabilities() { } } } - + capabilitiesProbed = true; LOGGER.log(Level.INFO, "[exr-server] Probed capabilities for " + endpoint + ", supports: " + supportedCoins.size() + " coins: " + - supportedCoins.stream().map(io.cloudchains.app.net.CoinTickerUtils::tickerToString) - .reduce((a, b) -> a + ", " + b).orElse("none")); + supportedCoins.stream().map(CoinTickerUtils::tickerToString) + .reduce((a, b) -> a + ", " + b).orElse("none")); return !supportedCoins.isEmpty(); } catch (Exception e) { LOGGER.log(Level.WARNING, "[exr-server] Failed to probe capabilities for " + endpoint, e); return false; } } - + public boolean isHealthy() { long now = System.currentTimeMillis(); if (now - lastHealthCheck < HEALTH_CHECK_INTERVAL_MS) { return healthy; } - try { - com.google.gson.JsonObject result = wrapper.executeGet("heights"); + JsonObject result = wrapper.executeGet("heights"); healthy = result != null && !result.isJsonNull(); } catch (Exception e) { healthy = false; @@ -108,38 +98,37 @@ public boolean isHealthy() { lastHealthCheck = now; return healthy; } - - public com.google.gson.JsonObject execute(String method, List params) { + + public JsonObject execute(String method, List params) { if (!isHealthy()) { return null; } - // Server selection should have already filtered by coin support // Remove redundant capability check to avoid race conditions return wrapper.execute(method, params); } - - public com.google.gson.JsonObject executeGet(String method) { + + public JsonObject executeGet(String method) { return isHealthy() ? wrapper.executeGet(method) : null; } - + public void close() { wrapper.close(); } - - public String getEndpoint() { - return endpoint; + + public String getEndpoint() { + return endpoint; } - + public boolean isCapabilitiesProbed() { return capabilitiesProbed; } - - public java.util.Set getSupportedCoins() { - return new java.util.HashSet<>(supportedCoins); + + public Set getSupportedCoins() { + return new HashSet<>(supportedCoins); } - - public boolean hasCapability(io.cloudchains.app.net.CoinTicker coin) { + + public boolean hasCapability(CoinTicker coin) { return supportedCoins.contains(coin); } } \ No newline at end of file diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/EXRServerPool.java b/src/main/java/io/cloudchains/app/net/api/http/client/EXRServerPool.java index a9b2dad..af099f6 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/client/EXRServerPool.java +++ b/src/main/java/io/cloudchains/app/net/api/http/client/EXRServerPool.java @@ -2,6 +2,7 @@ import io.cloudchains.app.net.CoinTicker; import io.cloudchains.app.net.CoinTickerUtils; + import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; @@ -13,47 +14,39 @@ public class EXRServerPool { private final static LogManager LOGMANAGER = LogManager.getLogManager(); private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); - private final List servers; private final AtomicInteger currentIndex; private final Map> coinToServersMap; private final Map endpointToServerMap; private volatile boolean capabilitiesProbed; - // Add synchronization lock for thread-safe map updates private final Object mapUpdateLock = new Object(); - + public EXRServerPool(String endpoints) { this.servers = new CopyOnWriteArrayList<>(); this.currentIndex = new AtomicInteger(0); this.coinToServersMap = new ConcurrentHashMap<>(); this.endpointToServerMap = new ConcurrentHashMap<>(); this.capabilitiesProbed = false; - initializeServers(endpoints); } - // Add constants for configuration private static final int CAPABILITY_PROBE_TIMEOUT_MS = 30000; - + public void startCapabilityProbing() { if (capabilitiesProbed || servers.isEmpty()) { return; } - LOGGER.log(Level.INFO, "[exr-pool] Starting capability probing for " + servers.size() + " servers"); - // Start capability probing in background new Thread(this::probeAllCapabilities, "EXR-Capability-Prober").start(); } - + public void probeAllCapabilities() { if (capabilitiesProbed) { return; } - LOGGER.log(Level.INFO, "[exr-pool] Starting capability probing for " + servers.size() + " servers"); - // Probe each server concurrently List probeThreads = new ArrayList<>(); for (EXRServer server : servers) { @@ -67,7 +60,6 @@ public void probeAllCapabilities() { probeThreads.add(t); t.start(); } - // Wait for all probes to complete for (Thread t : probeThreads) { try { @@ -77,49 +69,45 @@ public void probeAllCapabilities() { LOGGER.log(Level.WARNING, "[exr-pool] Capability probing interrupted", e); } } - // Build coin-to-servers mapping with synchronization synchronized (mapUpdateLock) { coinToServersMap.clear(); for (EXRServer server : servers) { - for (io.cloudchains.app.net.CoinTicker coin : server.getSupportedCoins()) { + for (CoinTicker coin : server.getSupportedCoins()) { coinToServersMap.computeIfAbsent(coin, k -> new ArrayList<>()).add(server); } } } - capabilitiesProbed = true; logCapabilityResults(); LOGGER.log(Level.INFO, "[exr-pool] Capability probing completed"); } - + private void logCapabilityResults() { LOGGER.log(Level.INFO, "[exr-pool] === EXR Server Capabilities ==="); for (EXRServer server : servers) { if (server.isCapabilitiesProbed()) { String supportedCoins = server.getSupportedCoins().stream() - .map(coin -> io.cloudchains.app.net.CoinTickerUtils.tickerToString(coin)) - .reduce((a, b) -> a + ", " + b).orElse("none"); + .map(coin -> CoinTickerUtils.tickerToString(coin)) + .reduce((a, b) -> a + ", " + b).orElse("none"); LOGGER.log(Level.INFO, "[exr-pool] " + server.getEndpoint() + " supports: " + supportedCoins); } else { LOGGER.log(Level.WARNING, "[exr-pool] " + server.getEndpoint() + " capability probe failed"); } } - LOGGER.log(Level.INFO, "[exr-pool] === Coin Distribution ==="); - for (io.cloudchains.app.net.CoinTicker coin : io.cloudchains.app.net.CoinTicker.coins()) { + for (CoinTicker coin : CoinTicker.coins()) { List supportingServers = coinToServersMap.get(coin); if (supportingServers != null && !supportingServers.isEmpty()) { - LOGGER.log(Level.INFO, "[exr-pool] " + io.cloudchains.app.net.CoinTickerUtils.tickerToString(coin) + " supported by " + supportingServers.size() + " servers"); + LOGGER.log(Level.INFO, "[exr-pool] " + CoinTickerUtils.tickerToString(coin) + " supported by " + supportingServers.size() + " servers"); } } } - + private void initializeServers(String endpoints) { if (endpoints == null || endpoints.trim().isEmpty()) { return; } - String[] endpointArray = endpoints.split(","); for (String endpoint : endpointArray) { String trimmed = endpoint.trim(); @@ -130,18 +118,16 @@ private void initializeServers(String endpoints) { LOGGER.log(Level.INFO, "[exr-pool] Added EXR server: " + trimmed); } } - if (!servers.isEmpty()) { LOGGER.log(Level.INFO, "[exr-pool] Created pool with " + servers.size() + " servers"); } } - + public EXRServer selectServer() { if (servers.isEmpty()) { LOGGER.log(Level.WARNING, "[exr-pool] No servers available for selection"); return null; } - // Try round-robin through healthy servers int start = currentIndex.getAndIncrement() % servers.size(); for (int i = 0; i < servers.size(); i++) { @@ -155,7 +141,7 @@ public EXRServer selectServer() { LOGGER.log(Level.WARNING, "[exr-pool] No healthy servers available"); return null; // All servers unhealthy } - + /** * Extract health filtering logic * @param supportingServers List of servers that support the coin @@ -172,54 +158,48 @@ private List getHealthySupportingServers(List supportingSe } /** - * Extract server selection logic - * @param healthyServers List of healthy servers - * @param coin The coin to select a server for - * @return Selected server or null if none available - */ - private EXRServer selectFromHealthyServers(List healthyServers, io.cloudchains.app.net.CoinTicker coin) { + * Extract server selection logic + * @param healthyServers List of healthy servers + * @param coin The coin to select a server for + * @return Selected server or null if none available + */ + private EXRServer selectFromHealthyServers(List healthyServers, CoinTicker coin) { if (healthyServers.isEmpty()) { LOGGER.log(Level.SEVERE, "[exr-pool] NO HEALTHY EXR SERVERS FOR COIN: " + - io.cloudchains.app.net.CoinTickerUtils.tickerToString(coin)); + CoinTickerUtils.tickerToString(coin)); return null; } - int index = currentIndex.getAndIncrement() % healthyServers.size(); EXRServer selectedServer = healthyServers.get(index); - // Double-check that the selected server actually supports the coin if (!selectedServer.hasCapability(coin)) { LOGGER.log(Level.SEVERE, "[exr-pool] CRITICAL ERROR: Selected server " + - selectedServer.getEndpoint() + " does NOT support coin " + - io.cloudchains.app.net.CoinTickerUtils.tickerToString(coin)); + selectedServer.getEndpoint() + " does NOT support coin " + + CoinTickerUtils.tickerToString(coin)); return null; } - return selectedServer; } /** - * Select a server for a specific coin with proper error handling - * @param coin The coin to select a server for - * @return Selected server or null if none available - */ - public EXRServer selectServerForCoin(io.cloudchains.app.net.CoinTicker coin) { + * Select a server for a specific coin with proper error handling + * @param coin The coin to select a server for + * @return Selected server or null if none available + */ + public EXRServer selectServerForCoin(CoinTicker coin) { if (!capabilitiesProbed) { return null; // Wait for probing to complete } - List supportingServers = coinToServersMap.get(coin); if (supportingServers == null || supportingServers.isEmpty()) { LOGGER.log(Level.SEVERE, "[exr-pool] NO EXR SERVERS SUPPORT COIN: " + - io.cloudchains.app.net.CoinTickerUtils.tickerToString(coin)); + CoinTickerUtils.tickerToString(coin)); return null; // FAIL - NO FALLBACK TO BASE_URL } - List healthyServers = getHealthySupportingServers(supportingServers); return selectFromHealthyServers(healthyServers, coin); } - - + public Set getSupportedCoins() { Set supported = new HashSet<>(); for (EXRServer server : servers) { @@ -227,16 +207,15 @@ public Set getSupportedCoins() { } return supported; } - - public boolean hasServerForCoin(io.cloudchains.app.net.CoinTicker coin) { + + public boolean hasServerForCoin(CoinTicker coin) { if (!capabilitiesProbed) { return !servers.isEmpty(); // Assume at least one server supports it } - List supportingServers = coinToServersMap.get(coin); return supportingServers != null && !supportingServers.isEmpty(); } - + public void close() { for (EXRServer server : servers) { server.close(); @@ -245,19 +224,19 @@ public void close() { coinToServersMap.clear(); endpointToServerMap.clear(); } - - public List getServers() { - return new ArrayList<>(servers); + + public List getServers() { + return new ArrayList<>(servers); } - - public boolean isCapabilitiesProbed() { - return capabilitiesProbed; + + public boolean isCapabilitiesProbed() { + return capabilitiesProbed; } - - public int getServerCount() { - return servers.size(); + + public int getServerCount() { + return servers.size(); } - + public String getStatus() { int healthyCount = 0; for (EXRServer server : servers) { diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/EXRWrapper.java b/src/main/java/io/cloudchains/app/net/api/http/client/EXRWrapper.java index 4c5d4ff..7215777 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/client/EXRWrapper.java +++ b/src/main/java/io/cloudchains/app/net/api/http/client/EXRWrapper.java @@ -1,18 +1,18 @@ package io.cloudchains.app.net.api.http.client; import com.google.gson.Gson; -import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; +import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; -import org.apache.http.client.config.RequestConfig; import java.io.IOException; import java.net.URI; @@ -24,59 +24,55 @@ public class EXRWrapper { private final static LogManager LOGMANAGER = LogManager.getLogManager(); private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); - private final String exrEndpoint; private final CloseableHttpClient client; private final Gson gson; - // Constants for configuration private static final int HTTP_TIMEOUT_MS = 30000; private static final String LOG_TAG = "[exr]"; - + public EXRWrapper(String exrEndpoint) { this.exrEndpoint = exrEndpoint; this.gson = new Gson(); - // Configure HTTP client with timeouts RequestConfig config = RequestConfig.custom() .setConnectTimeout(30000) .setConnectionRequestTimeout(30000) .setSocketTimeout(30000) .build(); - this.client = HttpClients.custom() .setDefaultRequestConfig(config) .build(); } - + /** * Execute an HTTP request and return the response body. * @param request The HTTP request to execute * @param operation Description of the operation for logging * @return Response body string or null on error */ - private String executeHttpRequest(org.apache.http.client.methods.HttpRequestBase request, String operation) { - org.apache.http.HttpResponse response = null; + private String executeHttpRequest(HttpRequestBase request, String operation) { + HttpResponse response = null; try { response = client.execute(request); if (validateResponse(response)) { - org.apache.http.HttpEntity entity = response.getEntity(); - String responseBody = org.apache.http.util.EntityUtils.toString(entity); - org.apache.http.util.EntityUtils.consume(entity); + HttpEntity entity = response.getEntity(); + String responseBody = EntityUtils.toString(entity); + EntityUtils.consume(entity); return responseBody; } else { LOGGER.log(Level.WARNING, LOG_TAG + " " + operation + " failed for endpoint: " + exrEndpoint); return null; } - } catch (java.io.IOException e) { + } catch (IOException e) { LOGGER.log(Level.WARNING, LOG_TAG + " " + operation + " failed for endpoint: " + exrEndpoint, e); return null; } finally { request.reset(); - + } } - + /** * Process response JSON and handle wrapping for different response types. * @param responseBody The raw response body @@ -84,7 +80,6 @@ private String executeHttpRequest(org.apache.http.client.methods.HttpRequestBase */ private JsonObject processResponse(String responseBody) { JsonElement responseElement = gson.fromJson(responseBody, JsonElement.class); - if (responseElement.isJsonObject()) { return responseElement.getAsJsonObject(); } else { @@ -94,7 +89,7 @@ private JsonObject processResponse(String responseBody) { return wrapperObj; } } - + /** * Execute a POST request to an EXR endpoint. * Transforms the method and params into EXR format. @@ -103,43 +98,40 @@ private JsonObject processResponse(String responseBody) { * @param params The parameters as a List of Objects * @return JsonObject response or null on error */ - public com.google.gson.JsonObject execute(String method, List params) { + public JsonObject execute(String method, List params) { String endpoint = exrEndpoint + "/xrs/" + method; String currency = params.isEmpty() || !(params.get(0) instanceof String) ? - "unknown" : (String) params.get(0); - + "unknown" : (String) params.get(0); String requestBody = gson.toJson(params); HttpPost httpPost = new HttpPost(); httpPost.setURI(URI.create(endpoint)); httpPost.setHeader("Content-Type", "application/json"); - try { httpPost.setEntity(new StringEntity(requestBody)); String responseBody = executeHttpRequest(httpPost, "execute POST for " + method + " " + currency); return responseBody != null ? processResponse(responseBody) : null; - } catch (java.io.IOException e) { + } catch (IOException e) { LOGGER.log(Level.WARNING, LOG_TAG + " execute POST failed for " + method + " " + currency + " endpoint: " + endpoint, e); return null; } finally { httpPost.reset(); } } - + /** * Execute a GET request to an EXR endpoint. * * @param method The method name (e.g., "fees", "heights") * @return JsonObject response or null on error */ - public com.google.gson.JsonObject executeGet(String method) { + public JsonObject executeGet(String method) { String endpoint = exrEndpoint + "/xrs/" + method; HttpGet httpGet = new HttpGet(endpoint); httpGet.setHeader("Content-Type", "application/json"); - String responseBody = executeHttpRequest(httpGet, "execute GET for method " + method); return responseBody != null ? processResponse(responseBody) : null; } - + /** * Close the HTTP client resources. */ @@ -150,10 +142,10 @@ public void close() { LOGGER.log(Level.WARNING, LOG_TAG + " Failed to close HTTP client", e); } } - + private boolean validateResponse(HttpResponse response) { - return response.getStatusLine().getStatusCode() == 200 && - response.getEntity() != null && - response.getEntity().getContentLength() != 0; + return response.getStatusLine().getStatusCode() == 200 && + response.getEntity() != null && + response.getEntity().getContentLength() != 0; } } \ No newline at end of file diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java b/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java index e17dadc..3f76210 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java +++ b/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java @@ -5,13 +5,13 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; import com.google.gson.reflect.TypeToken; import com.subgraph.orchid.encoders.Hex; import io.cloudchains.app.App; import io.cloudchains.app.net.CoinInstance; import io.cloudchains.app.net.CoinTicker; import io.cloudchains.app.net.CoinTickerUtils; -import io.cloudchains.app.net.api.http.client.EXRWrapper; import io.cloudchains.app.util.AddressBalance; import io.cloudchains.app.util.UTXO; import io.cloudchains.app.util.history.Transaction; @@ -20,7 +20,10 @@ import org.apache.http.HttpHeaders; import org.apache.http.HttpResponse; import org.apache.http.client.config.RequestConfig; -import org.apache.http.client.methods.*; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.config.RegistryBuilder; import org.apache.http.conn.socket.ConnectionSocketFactory; import org.apache.http.conn.socket.PlainConnectionSocketFactory; @@ -61,7 +64,7 @@ public class HTTPClient { private CloseableHttpClient client; private ConcurrentHashMap lastFetchTimes; private int logCount = 0; - + /** * Helper method to wait for EXR capabilities to be probed with a timeout. * @param timeoutMs Maximum time to wait in milliseconds @@ -71,13 +74,13 @@ private boolean waitForCapabilities(int timeoutMs) { if (!useEXR()) { return false; } - + if (App.exrServerPool.isCapabilitiesProbed()) { return true; } - + LOGGER.log(Level.FINE, "[httpclient] Waiting for EXR capabilities to be probed (timeout: " + timeoutMs + "ms)"); - + int waitTime = 0; while (!App.exrServerPool.isCapabilitiesProbed() && waitTime < timeoutMs) { try { @@ -89,12 +92,12 @@ private boolean waitForCapabilities(int timeoutMs) { return false; } } - + boolean probed = App.exrServerPool.isCapabilitiesProbed(); LOGGER.log(Level.FINE, "[httpclient] EXR capabilities " + - (probed ? "probed successfully" : "still not probed") + - " after waiting " + waitTime + "ms"); - + (probed ? "probed successfully" : "still not probed") + + " after waiting " + waitTime + "ms"); + return probed; } @@ -113,8 +116,8 @@ private boolean useEXR() { */ private boolean shouldUseEXR(String endpoint) { return useEXR() && (endpoint.equals("/fees") || - endpoint.equals("/height") || - endpoint.equals("/")); + endpoint.equals("/height") || + endpoint.equals("/")); } /** @@ -124,17 +127,17 @@ private boolean shouldUseEXR(String endpoint) { private EXRServer getEXRServer() { return useEXR() ? App.exrServerPool.selectServer() : null; } - + /** * Convert JsonArray to List for EXR execution * @param exrParams JsonArray of parameters * @return List of parameters */ - private java.util.List convertParams(com.google.gson.JsonArray exrParams) { - java.util.List paramList = new java.util.ArrayList<>(); - for (com.google.gson.JsonElement element : exrParams) { + private List convertParams(JsonArray exrParams) { + List paramList = new ArrayList<>(); + for (JsonElement element : exrParams) { if (element.isJsonPrimitive()) { - com.google.gson.JsonPrimitive primitive = element.getAsJsonPrimitive(); + JsonPrimitive primitive = element.getAsJsonPrimitive(); if (primitive.isString()) { paramList.add(primitive.getAsString()); } else if (primitive.isNumber()) { @@ -166,7 +169,7 @@ private String executeHttpRequest(T request) { return result; } return null; - } catch (java.io.IOException e) { + } catch (IOException e) { LOGGER.log(Level.WARNING, "HTTP request failed: " + e.toString()); return null; } finally { @@ -174,7 +177,7 @@ private String executeHttpRequest(T request) { if (response != null) { try { response.close(); - } catch (java.io.IOException e) { + } catch (IOException e) { LOGGER.log(Level.WARNING, "Failed to close response: " + e.toString()); } } @@ -197,12 +200,12 @@ private String executeGetRequest(String endpoint) { * @param params The parameters to POST * @return Response string or null on error */ - private String executePostRequest(String endpoint, com.google.gson.JsonObject params) { + private String executePostRequest(String endpoint, JsonObject params) { HttpPost httpPost = new HttpPost(); - httpPost.setURI(java.net.URI.create(App.BASE_URL + endpoint)); + httpPost.setURI(URI.create(App.BASE_URL + endpoint)); try { httpPost.setEntity(new StringEntity(params.toString())); - } catch (java.io.UnsupportedEncodingException e) { + } catch (UnsupportedEncodingException e) { LOGGER.log(Level.WARNING, "executePostRequest failed to set entity " + endpoint + " err: " + e.toString()); httpPost.reset(); return null; @@ -265,21 +268,21 @@ public void close() { */ private String aggregateEXRResponse(String method) { // Aggregate from ALL EXR servers - com.google.gson.JsonObject aggregatedResult = new com.google.gson.JsonObject(); - com.google.gson.JsonArray aggregatedErrors = new com.google.gson.JsonArray(); - + JsonObject aggregatedResult = new JsonObject(); + JsonArray aggregatedErrors = new JsonArray(); + for (EXRServer server : App.exrServerPool.getServers()) { if (!server.isHealthy()) { continue; } - + try { - com.google.gson.JsonObject result = server.executeGet(method); + JsonObject result = server.executeGet(method); if (result != null && result.has("result")) { - com.google.gson.JsonElement serverResult = result.get("result"); - + JsonElement serverResult = result.get("result"); + if (serverResult.isJsonObject()) { - com.google.gson.JsonObject serverObj = serverResult.getAsJsonObject(); + JsonObject serverObj = serverResult.getAsJsonObject(); for (String key : serverObj.keySet()) { if (!aggregatedResult.has(key)) { aggregatedResult.add(key, serverObj.get(key)); @@ -292,11 +295,11 @@ private String aggregateEXRResponse(String method) { aggregatedErrors.add("Failed " + method + " from " + server.getEndpoint()); } } - - com.google.gson.JsonObject finalResult = new com.google.gson.JsonObject(); + + JsonObject finalResult = new JsonObject(); finalResult.add("result", aggregatedResult); finalResult.add("errors", aggregatedErrors); - + return finalResult.toString(); } @@ -316,56 +319,55 @@ private String executeEXRGet(String endpoint) { * @param params The parameters to POST * @return Response from appropriate EXR server */ - private String executeEXRPost(String endpoint, com.google.gson.JsonObject params) { + private String executeEXRPost(String endpoint, JsonObject params) { if (params.has("method") && params.has("params")) { String method = params.get("method").getAsString(); - com.google.gson.JsonArray exrParams = params.getAsJsonArray("params"); - + JsonArray exrParams = params.getAsJsonArray("params"); + // Extract coin from first parameter - io.cloudchains.app.net.CoinTicker coin = null; + CoinTicker coin = null; if (exrParams.size() > 0) { String coinString = exrParams.get(0).getAsString(); - coin = io.cloudchains.app.net.CoinTickerUtils.stringToTicker(coinString); + coin = CoinTickerUtils.stringToTicker(coinString); if (coin == null) { // Log the failed coin extraction for debugging LOGGER.log(Level.WARNING, "[httpclient] Failed to extract coin from parameter: " + coinString); // Not a coin-specific request } } - + EXRServer server = null; - + // Route ONLY to EXR servers that support this coin if (coin != null) { // Wait for capabilities to be probed if not already done if (!App.exrServerPool.isCapabilitiesProbed()) { LOGGER.log(Level.FINE, "[httpclient] Waiting for EXR capabilities to be probed for coin: " + - io.cloudchains.app.net.CoinTickerUtils.tickerToString(coin)); + CoinTickerUtils.tickerToString(coin)); if (!waitForCapabilities(10000)) { // Wait up to 10 seconds LOGGER.log(Level.WARNING, "[httpclient] EXR capabilities not probed yet for coin: " + - io.cloudchains.app.net.CoinTickerUtils.tickerToString(coin)); + CoinTickerUtils.tickerToString(coin)); return null; // FAIL - NO FALLBACK TO BASE_URL } } - if (App.exrServerPool.isCapabilitiesProbed()) { server = App.exrServerPool.selectServerForCoin(coin); // LOGGER.log(Level.INFO, "[httpclient] DEBUG: selectServerForCoin returned: " + // (server != null ? server.getEndpoint() : "null")); - + if (server == null) { LOGGER.log(Level.SEVERE, "[httpclient] NO EXR SERVER SUPPORTS COIN: " + - io.cloudchains.app.net.CoinTickerUtils.tickerToString(coin)); + CoinTickerUtils.tickerToString(coin)); return null; // FAIL - NO FALLBACK TO BASE_URL } else { LOGGER.log(Level.INFO, "[httpclient] DEBUG: Selected server " + server.getEndpoint() + - " for coin " + io.cloudchains.app.net.CoinTickerUtils.tickerToString(coin) + - ", method: " + method); + " for coin " + CoinTickerUtils.tickerToString(coin) + + ", method: " + method); } } else { // Capabilities still not probed after waiting LOGGER.log(Level.WARNING, "[httpclient] EXR capabilities not probed yet for coin: " + - io.cloudchains.app.net.CoinTickerUtils.tickerToString(coin)); + CoinTickerUtils.tickerToString(coin)); return null; // FAIL - NO FALLBACK TO BASE_URL } } else { @@ -375,7 +377,7 @@ private String executeEXRPost(String endpoint, com.google.gson.JsonObject params server = App.exrServerPool.selectServerForCoin(coin); if (server == null) { LOGGER.log(Level.SEVERE, "[httpclient] NO EXR SERVER SUPPORTS COIN: " + - io.cloudchains.app.net.CoinTickerUtils.tickerToString(coin)); + CoinTickerUtils.tickerToString(coin)); return null; // FAIL - NO FALLBACK TO BASE_URL } } else { @@ -384,15 +386,15 @@ private String executeEXRPost(String endpoint, com.google.gson.JsonObject params return null; // FAIL instead of using wrong server } } - + if (server != null) { - java.util.List paramList = convertParams(exrParams); - com.google.gson.JsonObject result = server.execute(method, paramList); + List paramList = convertParams(exrParams); + JsonObject result = server.execute(method, paramList); if (result != null) { // Handle wrapped responses from EXR wrapper // If the result has a "result" field, extract it to maintain backward compatibility if (result.has("result")) { - com.google.gson.JsonElement resultElement = result.get("result"); + JsonElement resultElement = result.get("result"); if (!resultElement.isJsonNull()) { return resultElement.toString(); } @@ -410,7 +412,7 @@ private String executeEXRPost(String endpoint, com.google.gson.JsonObject params * @param params Parameters for POST requests, null for GET * @return Response string or null on error */ - private String executeRequest(String endpoint, com.google.gson.JsonObject params) { + private String executeRequest(String endpoint, JsonObject params) { // When EXR is configured, ONLY use EXR - NO fallback to BASE_URL if (shouldUseEXR(endpoint)) { if (params == null) { @@ -421,7 +423,7 @@ private String executeRequest(String endpoint, com.google.gson.JsonObject params return executeEXRPost(endpoint, params); } } - + // ONLY fall back to BASE_URL when EXR is NOT configured if (!useEXR()) { if (params == null) { @@ -432,7 +434,7 @@ private String executeRequest(String endpoint, com.google.gson.JsonObject params return executePostRequest(endpoint, params); } } - + return null; // EXR configured but no valid response } @@ -440,7 +442,7 @@ private String doGet(String endpoint) { return executeRequest(endpoint, null); } - private String doPost(String endpoint, com.google.gson.JsonObject params) { + private String doPost(String endpoint, JsonObject params) { return executeRequest(endpoint, params); } diff --git a/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerHandler.java b/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerHandler.java index def902b..60bc0a9 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerHandler.java +++ b/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerHandler.java @@ -1,20 +1,16 @@ package io.cloudchains.app.net.api.http.master; import com.google.common.base.Preconditions; -import com.google.gson.*; +import com.google.gson.JsonArray; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import com.subgraph.orchid.encoders.Base64; -import com.subgraph.orchid.encoders.Hex; -import io.cloudchains.app.App; import io.cloudchains.app.Version; import io.cloudchains.app.net.CoinInstance; import io.cloudchains.app.net.CoinTicker; import io.cloudchains.app.net.CoinTickerUtils; -import io.cloudchains.app.net.api.http.client.HTTPClient; -import io.cloudchains.app.net.protocols.blocknet.BlocknetPeer; -import io.cloudchains.app.util.AddressBalance; import io.cloudchains.app.util.ConfigHelper; -import io.cloudchains.app.util.UTXO; -import io.cloudchains.app.wallet.WalletHelper; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFutureListener; @@ -22,250 +18,238 @@ import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.*; import io.netty.util.CharsetUtil; -import org.bitcoinj.core.*; -import org.bitcoinj.script.Script; -import org.bitcoinj.script.ScriptBuilder; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.math.BigDecimal; -import java.math.BigInteger; + import java.nio.charset.StandardCharsets; import java.security.SecureRandom; -import java.security.SignatureException; -import java.util.Arrays; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; public class HTTPServerHandler extends SimpleChannelInboundHandler { - private final static LogManager LOGMANAGER = LogManager.getLogManager(); - private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); + private final static LogManager LOGMANAGER = LogManager.getLogManager(); + private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); - private ConfigHelper configHelper; + private ConfigHelper configHelper; - HTTPServerHandler() { - configHelper = new ConfigHelper("master"); + HTTPServerHandler() { + configHelper = new ConfigHelper("master"); - if (configHelper.getRpcUsername().isEmpty() && configHelper.getRpcPassword().isEmpty()) { - configHelper.setRpcUsername(generateRandomString(12)); - configHelper.setRpcPassword(generateRandomString(32)); + if (configHelper.getRpcUsername().isEmpty() && configHelper.getRpcPassword().isEmpty()) { + configHelper.setRpcUsername(generateRandomString(12)); + configHelper.setRpcPassword(generateRandomString(32)); - configHelper.writeConfig(); - } - } + configHelper.writeConfig(); + } + } - private String generateRandomString(int length) { - SecureRandom secureRandom = new SecureRandom(); + private String generateRandomString(int length) { + SecureRandom secureRandom = new SecureRandom(); - byte[] token = new byte[length]; - secureRandom.nextBytes(token); + byte[] token = new byte[length]; + secureRandom.nextBytes(token); - return java.util.Base64.getUrlEncoder().withoutPadding().encodeToString(token); - } + return java.util.Base64.getUrlEncoder().withoutPadding().encodeToString(token); + } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { super.channelInactive(ctx); } - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { - cause.printStackTrace(); - - FullHttpResponse httpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST); - writeResponse(ctx, httpResponse, null); - ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); - ctx.close(); - } - - @Override - public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { - LOGGER.log(Level.FINER, "[http-server-handler] DEBUG: Channel read complete. Flushing context."); - super.channelReadComplete(ctx); - ctx.flush(); - } - - @Override - protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) { - boolean successfulAuth = false; - HttpResponseStatus status = HttpResponseStatus.OK; - JsonObject response = new JsonObject(); - - if (request != null) { - HttpHeaders httpHeaders = request.headers(); - - if (HttpUtil.is100ContinueExpected(request)) { - send100Continue(ctx); - } - - String headerUser; - String headerPass; - if (httpHeaders.contains("Authorization")) { - String authorization = httpHeaders.get("Authorization"); - - if (authorization != null && authorization.toLowerCase().startsWith("basic")) { - String base64Credentials = authorization.substring("Basic".length()).trim(); - byte[] credDecoded = Base64.decode(base64Credentials); - String credentials = new String(credDecoded, StandardCharsets.UTF_8); - final String[] values = credentials.split(":", 2); - - headerUser = values[0]; - headerPass = values[1]; - - if (headerUser.equals(configHelper.getRpcUsername()) && headerPass.equals(configHelper.getRpcPassword())) { - successfulAuth = true; - LOGGER.log(Level.FINER, "[http-server-handler] Successful Auth"); - } - } - } - - if (!request.uri().equals("/")) { - JsonObject onlyServerRootJSON = new JsonObject(); - onlyServerRootJSON.addProperty("code", -1002); - onlyServerRootJSON.addProperty("message", "Only the server root ('/') is being served."); - - response.add("error", onlyServerRootJSON); - response.add("result", JsonNull.INSTANCE); - status = HttpResponseStatus.BAD_REQUEST; - } - - if (request.method() != HttpMethod.POST) { - JsonObject onlyPostAllowedJSON = new JsonObject(); - - onlyPostAllowedJSON.addProperty("code", -1003); - onlyPostAllowedJSON.addProperty("message", "Only HTTP POST is accepted."); - - response.add("error", onlyPostAllowedJSON); - response.add("result", JsonNull.INSTANCE); - status = HttpResponseStatus.BAD_REQUEST; - } - - if (!successfulAuth) { - JsonObject onlyServerRootJSON = new JsonObject(); - onlyServerRootJSON.addProperty("code", -1111); - onlyServerRootJSON.addProperty("message", "Unauthorized!"); - - response.add("error", onlyServerRootJSON); - response.add("result", JsonNull.INSTANCE); - status = HttpResponseStatus.FORBIDDEN; - } - } - - if (status != HttpResponseStatus.OK) { - ByteBuf responseContent = Unpooled.copiedBuffer(response.toString(), CharsetUtil.UTF_8); - FullHttpResponse httpResponse = new DefaultFullHttpResponse(request.protocolVersion(), status, responseContent); - - writeResponse(ctx, httpResponse, request); - ctx.write(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); - return; - } - - if (request != null) { - String content = request.content().toString(CharsetUtil.UTF_8); - JsonObject jsonReq = null; - - try { - jsonReq = new JsonParser().parse(content).getAsJsonObject(); - - Preconditions.checkNotNull(jsonReq); - - if (!jsonReq.has("method") || !jsonReq.has("params")) { - ctx.close(); - throw new IllegalArgumentException("Bad JSON-RPC request by client."); - } - } catch (Exception e) { - LOGGER.log(Level.INFO, "Failed Content: " + content); - e.printStackTrace(); - JsonObject errorParsingJSON = new JsonObject(); - errorParsingJSON.addProperty("code", -1001); - errorParsingJSON.addProperty("message", "Error parsing JSON."); - - response.add("error", errorParsingJSON); - response.add("result", JsonNull.INSTANCE); - if (e instanceof IllegalArgumentException) { - LOGGER.log(Level.FINER, "[http-server-handler] WARNING: Client sent valid JSON, but did not specify method and/or parameters!"); - } else { - LOGGER.log(Level.FINER, "[http-server-handler] WARNING: Client sent invalid JSON!"); - } - status = HttpResponseStatus.BAD_REQUEST; - } - - if (status == HttpResponseStatus.OK) { - Preconditions.checkNotNull(jsonReq); - - String method = jsonReq.get("method").getAsString(); - JsonArray params = jsonReq.get("params").getAsJsonArray(); - - LOGGER.log(Level.INFO, "[http-server-handler] RPC CALL: " + method + " PARAMS: " + params.size()); - for (int i = 0; i < params.size(); i++) { - LOGGER.log(Level.INFO, "[http-server-handler] PARAM " + i + ": " + params.get(i).toString()); - } - - response = getResponse(method, params); - LOGGER.log(Level.FINER, response.toString()); - } else { - ByteBuf responseContent = Unpooled.copiedBuffer(response.toString(), CharsetUtil.UTF_8); - FullHttpResponse httpResponse = new DefaultFullHttpResponse(request.protocolVersion(), status, responseContent); - - writeResponse(ctx, httpResponse, request); - ctx.write(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); - return; - } - - if (request instanceof LastHttpContent) { - ByteBuf responseContent = Unpooled.copiedBuffer(response.toString(), CharsetUtil.UTF_8); - - FullHttpResponse httpResponse = new DefaultFullHttpResponse(request.protocolVersion(), status, responseContent); - - if (!writeResponse(ctx, httpResponse, request)) { - ctx.write(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); - } - } - } - } - - private JsonObject getResponse(String method, JsonArray params) { - JsonObject response = new JsonObject(); - boolean shutdownRequested = false; - - switch (method.toLowerCase()) { - case "reloadconfig": { - if (params.size() != 1) { - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - errorJSON.addProperty("code", -1); - errorJSON.addProperty("message", "Usage: reloadconfig \n\ntoken (string, required)"); - - response.add("error", errorJSON); - break; - } - - CoinTicker ticker = CoinTickerUtils.stringToTicker(params.get(0).getAsString()); - CoinInstance instance = CoinInstance.getInstance(ticker); - - Runnable r = () -> { - try { - Thread.sleep(500); - instance.reloadConfig(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - }; - new Thread(r).start(); - - response.addProperty("result", true); - response.add("error", JsonNull.INSTANCE); - break; - } - case "version": { - response.addProperty("result", Version.CLIENT_VERSION); - response.add("error", JsonNull.INSTANCE); - break; - } + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + cause.printStackTrace(); + + FullHttpResponse httpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST); + writeResponse(ctx, httpResponse, null); + ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); + ctx.close(); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + LOGGER.log(Level.FINER, "[http-server-handler] DEBUG: Channel read complete. Flushing context."); + super.channelReadComplete(ctx); + ctx.flush(); + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) { + boolean successfulAuth = false; + HttpResponseStatus status = HttpResponseStatus.OK; + JsonObject response = new JsonObject(); + + if (request != null) { + HttpHeaders httpHeaders = request.headers(); + + if (HttpUtil.is100ContinueExpected(request)) { + send100Continue(ctx); + } + + String headerUser; + String headerPass; + if (httpHeaders.contains("Authorization")) { + String authorization = httpHeaders.get("Authorization"); + + if (authorization != null && authorization.toLowerCase().startsWith("basic")) { + String base64Credentials = authorization.substring("Basic".length()).trim(); + byte[] credDecoded = Base64.decode(base64Credentials); + String credentials = new String(credDecoded, StandardCharsets.UTF_8); + final String[] values = credentials.split(":", 2); + + headerUser = values[0]; + headerPass = values[1]; + + if (headerUser.equals(configHelper.getRpcUsername()) && headerPass.equals(configHelper.getRpcPassword())) { + successfulAuth = true; + LOGGER.log(Level.FINER, "[http-server-handler] Successful Auth"); + } + } + } + + if (!request.uri().equals("/")) { + JsonObject onlyServerRootJSON = new JsonObject(); + onlyServerRootJSON.addProperty("code", -1002); + onlyServerRootJSON.addProperty("message", "Only the server root ('/') is being served."); + + response.add("error", onlyServerRootJSON); + response.add("result", JsonNull.INSTANCE); + status = HttpResponseStatus.BAD_REQUEST; + } + + if (request.method() != HttpMethod.POST) { + JsonObject onlyPostAllowedJSON = new JsonObject(); + + onlyPostAllowedJSON.addProperty("code", -1003); + onlyPostAllowedJSON.addProperty("message", "Only HTTP POST is accepted."); + + response.add("error", onlyPostAllowedJSON); + response.add("result", JsonNull.INSTANCE); + status = HttpResponseStatus.BAD_REQUEST; + } + + if (!successfulAuth) { + JsonObject onlyServerRootJSON = new JsonObject(); + onlyServerRootJSON.addProperty("code", -1111); + onlyServerRootJSON.addProperty("message", "Unauthorized!"); + + response.add("error", onlyServerRootJSON); + response.add("result", JsonNull.INSTANCE); + status = HttpResponseStatus.FORBIDDEN; + } + } + + if (status != HttpResponseStatus.OK) { + ByteBuf responseContent = Unpooled.copiedBuffer(response.toString(), CharsetUtil.UTF_8); + FullHttpResponse httpResponse = new DefaultFullHttpResponse(request.protocolVersion(), status, responseContent); + + writeResponse(ctx, httpResponse, request); + ctx.write(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); + return; + } + + if (request != null) { + String content = request.content().toString(CharsetUtil.UTF_8); + JsonObject jsonReq = null; + + try { + jsonReq = JsonParser.parseString(content).getAsJsonObject(); + + Preconditions.checkNotNull(jsonReq); + + if (!jsonReq.has("method") || !jsonReq.has("params")) { + ctx.close(); + throw new IllegalArgumentException("Bad JSON-RPC request by client."); + } + } catch (Exception e) { + LOGGER.log(Level.INFO, "Failed Content: " + content); + e.printStackTrace(); + JsonObject errorParsingJSON = new JsonObject(); + errorParsingJSON.addProperty("code", -1001); + errorParsingJSON.addProperty("message", "Error parsing JSON."); + + response.add("error", errorParsingJSON); + response.add("result", JsonNull.INSTANCE); + if (e instanceof IllegalArgumentException) { + LOGGER.log(Level.FINER, "[http-server-handler] WARNING: Client sent valid JSON, but did not specify method and/or parameters!"); + } else { + LOGGER.log(Level.FINER, "[http-server-handler] WARNING: Client sent invalid JSON!"); + } + status = HttpResponseStatus.BAD_REQUEST; + } + + if (status == HttpResponseStatus.OK) { + Preconditions.checkNotNull(jsonReq); + + String method = jsonReq.get("method").getAsString(); + JsonArray params = jsonReq.get("params").getAsJsonArray(); + + LOGGER.log(Level.INFO, "[http-server-handler] RPC CALL: " + method + " PARAMS: " + params.size()); + for (int i = 0; i < params.size(); i++) { + LOGGER.log(Level.INFO, "[http-server-handler] PARAM " + i + ": " + params.get(i).toString()); + } + + response = getResponse(method, params); + LOGGER.log(Level.FINER, response.toString()); + } else { + ByteBuf responseContent = Unpooled.copiedBuffer(response.toString(), CharsetUtil.UTF_8); + FullHttpResponse httpResponse = new DefaultFullHttpResponse(request.protocolVersion(), status, responseContent); + + writeResponse(ctx, httpResponse, request); + ctx.write(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); + return; + } + + if (request instanceof LastHttpContent) { + ByteBuf responseContent = Unpooled.copiedBuffer(response.toString(), CharsetUtil.UTF_8); + + FullHttpResponse httpResponse = new DefaultFullHttpResponse(request.protocolVersion(), status, responseContent); + + if (!writeResponse(ctx, httpResponse, request)) { + ctx.write(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); + } + } + } + } + + private JsonObject getResponse(String method, JsonArray params) { + JsonObject response = new JsonObject(); + boolean shutdownRequested = false; + + switch (method.toLowerCase()) { + case "reloadconfig": { + if (params.size() != 1) { + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", -1); + errorJSON.addProperty("message", "Usage: reloadconfig \n\ntoken (string, required)"); + + response.add("error", errorJSON); + break; + } + + CoinTicker ticker = CoinTickerUtils.stringToTicker(params.get(0).getAsString()); + CoinInstance instance = CoinInstance.getInstance(ticker); + + Runnable r = () -> { + try { + Thread.sleep(500); + instance.reloadConfig(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }; + new Thread(r).start(); + + response.addProperty("result", true); + response.add("error", JsonNull.INSTANCE); + break; + } + case "version": { + response.addProperty("result", Version.CLIENT_VERSION); + response.add("error", JsonNull.INSTANCE); + break; + } // case "reloadconfigs": { // boolean success = true; // for (CoinInstance instance : CoinInstance.getCoinInstances()) { @@ -285,64 +269,64 @@ private JsonObject getResponse(String method, JsonArray params) { // response.add("error", JsonNull.INSTANCE); // break; // } - case "help": { - String helpString = "Master JSON-RPC server\n" - + "This JSON-RPC server is served by " + CoinInstance.getVersionString() + "\n" - + "\n" - + "help - Display the help\n" - + "\n=====RPC Master=====\n" - + "stop - Shutdown the server\n" - + "reloadconfig - Reload configuration for specified token\n" - + "version - Get version\n"; + case "help": { + String helpString = "Master JSON-RPC server\n" + + "This JSON-RPC server is served by " + CoinInstance.getVersionString() + "\n" + + "\n" + + "help - Display the help\n" + + "\n=====RPC Master=====\n" + + "stop - Shutdown the server\n" + + "reloadconfig - Reload configuration for specified token\n" + + "version - Get version\n"; // + "reloadconfigs - Reload all configuration files\n"; - response.addProperty("result", helpString); - response.add("error", JsonNull.INSTANCE); - break; - } - case "stop": { + response.addProperty("result", helpString); + response.add("error", JsonNull.INSTANCE); + break; + } + case "stop": { shutdownRequested = true; - response.addProperty("result", "shutting down..."); - response.add("error", JsonNull.INSTANCE); - break; - } - default: { - JsonObject methodNotFound = new JsonObject(); - methodNotFound.addProperty("code", -32601); - methodNotFound.addProperty("message", "Method not found."); - response.add("error", methodNotFound); - response.add("result", JsonNull.INSTANCE); - break; - } - } - - if (shutdownRequested) { + response.addProperty("result", "shutting down..."); + response.add("error", JsonNull.INSTANCE); + break; + } + default: { + JsonObject methodNotFound = new JsonObject(); + methodNotFound.addProperty("code", -32601); + methodNotFound.addProperty("message", "Method not found."); + response.add("error", methodNotFound); + response.add("result", JsonNull.INSTANCE); + break; + } + } + + if (shutdownRequested) { (new Thread(() -> { System.exit(0); })).start(); // shutdown the server } - return response; - } - - private boolean writeResponse(ChannelHandlerContext ctx, FullHttpResponse httpResponse, FullHttpRequest request) { - boolean keepAlive = false; - if (request != null) - keepAlive = HttpUtil.isKeepAlive(request); - - httpResponse.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json"); - httpResponse.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, httpResponse.content().readableBytes()); - httpResponse.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); - httpResponse.headers().set(HttpHeaderNames.SERVER, CoinInstance.getVersionString()); - - LOGGER.log(Level.FINER, "[http-server-handler] Writing response to channel. Keep alive? " + keepAlive); - LOGGER.log(Level.FINER, "[http-server-handler] Response content: " + httpResponse.content().toString(CharsetUtil.UTF_8)); - ctx.write(httpResponse); - - return keepAlive; - } - - private static void send100Continue(ChannelHandlerContext ctx) { - FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE); - ctx.write(response); - } + return response; + } + + private boolean writeResponse(ChannelHandlerContext ctx, FullHttpResponse httpResponse, FullHttpRequest request) { + boolean keepAlive = false; + if (request != null) + keepAlive = HttpUtil.isKeepAlive(request); + + httpResponse.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json"); + httpResponse.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, httpResponse.content().readableBytes()); + httpResponse.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); + httpResponse.headers().set(HttpHeaderNames.SERVER, CoinInstance.getVersionString()); + + LOGGER.log(Level.FINER, "[http-server-handler] Writing response to channel. Keep alive? " + keepAlive); + LOGGER.log(Level.FINER, "[http-server-handler] Response content: " + httpResponse.content().toString(CharsetUtil.UTF_8)); + ctx.write(httpResponse); + + return keepAlive; + } + + private static void send100Continue(ChannelHandlerContext ctx) { + FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE); + ctx.write(response); + } } diff --git a/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerInitializer.java b/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerInitializer.java index 806ea69..4319d06 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerInitializer.java +++ b/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerInitializer.java @@ -11,19 +11,20 @@ import io.netty.handler.timeout.WriteTimeoutHandler; public class HTTPServerInitializer extends ChannelInitializer { - public HTTPServerInitializer() {} + public HTTPServerInitializer() { + } - @Override - protected void initChannel(SocketChannel ch) { - ChannelPipeline pipeline = ch.pipeline(); + @Override + protected void initChannel(SocketChannel ch) { + ChannelPipeline pipeline = ch.pipeline(); - pipeline.addLast(new WriteTimeoutHandler(30)); - pipeline.addLast(new ReadTimeoutHandler(30)); - pipeline.addLast(new HttpRequestDecoder()); - pipeline.addLast(new HttpResponseEncoder()); - pipeline.addLast(new HttpObjectAggregator(100000000)); - pipeline.addLast(new HTTPServerHandler()); - pipeline.addLast(new ExceptionHandler()); - } + pipeline.addLast(new WriteTimeoutHandler(30)); + pipeline.addLast(new ReadTimeoutHandler(30)); + pipeline.addLast(new HttpRequestDecoder()); + pipeline.addLast(new HttpResponseEncoder()); + pipeline.addLast(new HttpObjectAggregator(100000000)); + pipeline.addLast(new HTTPServerHandler()); + pipeline.addLast(new ExceptionHandler()); + } } diff --git a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java b/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java index 2c01d77..fd678de 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java +++ b/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java @@ -4,8 +4,6 @@ import com.google.gson.*; import com.subgraph.orchid.encoders.Base64; import com.subgraph.orchid.encoders.Hex; - -import io.cloudchains.app.App; import io.cloudchains.app.Version; import io.cloudchains.app.net.CoinInstance; import io.cloudchains.app.net.CoinTickerUtils; @@ -31,6 +29,7 @@ import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; +import java.math.RoundingMode; import java.nio.charset.StandardCharsets; import java.security.SignatureException; import java.util.ArrayList; @@ -45,274 +44,274 @@ // Define a helper class for output entries class OutputEntry { - public String address; - public double amount; + public String address; + public double amount; - public OutputEntry(String address, double amount) { - this.address = address; - this.amount = amount; - } + public OutputEntry(String address, double amount) { + this.address = address; + this.amount = amount; + } } public class HTTPServerHandler extends SimpleChannelInboundHandler { - private final static LogManager LOGMANAGER = LogManager.getLogManager(); - private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); + private final static LogManager LOGMANAGER = LogManager.getLogManager(); + private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); - private HTTPClient httpClient; - private CoinInstance coin; - private ConfigHelper configHelper; + private HTTPClient httpClient; + private CoinInstance coin; + private ConfigHelper configHelper; - HTTPServerHandler(CoinInstance coin) { - this.coin = coin; - this.configHelper = coin.getConfigHelper(); - this.httpClient = new HTTPClient(5); - } + HTTPServerHandler(CoinInstance coin) { + this.coin = coin; + this.configHelper = coin.getConfigHelper(); + this.httpClient = new HTTPClient(5); + } - @Override + @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { - super.channelInactive(ctx); + super.channelInactive(ctx); + this.httpClient.close(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + cause.printStackTrace(); + + FullHttpResponse httpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST); + writeResponse(ctx, httpResponse, null); + ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); + ctx.close(); this.httpClient.close(); } - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { - cause.printStackTrace(); - - FullHttpResponse httpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST); - writeResponse(ctx, httpResponse, null); - ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); - ctx.close(); - this.httpClient.close(); - } - - @Override - public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { - LOGGER.log(Level.FINER, "[http-server-handler] DEBUG: Channel read complete. Flushing context."); - super.channelReadComplete(ctx); - ctx.flush(); - } - - @Override - protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) { - boolean successfulAuth = false; - HttpResponseStatus status = HttpResponseStatus.OK; - JsonObject response = new JsonObject(); - - if (request != null) { - HttpHeaders httpHeaders = request.headers(); - - if (HttpUtil.is100ContinueExpected(request)) { - send100Continue(ctx); - } - - String headerUser; - String headerPass; - if (httpHeaders.contains("Authorization")) { - String authorization = httpHeaders.get("Authorization"); - - if (authorization != null && authorization.toLowerCase().startsWith("basic")) { - String base64Credentials = authorization.substring("Basic".length()).trim(); - byte[] credDecoded = Base64.decode(base64Credentials); - String credentials = new String(credDecoded, StandardCharsets.UTF_8); - final String[] values = credentials.split(":", 2); - - headerUser = values[0]; - headerPass = values[1]; - - if (headerUser.equals(configHelper.getRpcUsername()) && headerPass.equals(configHelper.getRpcPassword())) { - successfulAuth = true; - LOGGER.log(Level.FINER, "[http-server-handler] Successful Auth"); - } - } - } - - if (!request.uri().equals("/")) { - JsonObject onlyServerRootJSON = new JsonObject(); - onlyServerRootJSON.addProperty("code", -1002); - onlyServerRootJSON.addProperty("message", "Only the server root ('/') is being served."); - - response.add("error", onlyServerRootJSON); - response.add("result", JsonNull.INSTANCE); - status = HttpResponseStatus.BAD_REQUEST; - } - - if (request.method() != HttpMethod.POST) { - JsonObject onlyPostAllowedJSON = new JsonObject(); - - onlyPostAllowedJSON.addProperty("code", -1003); - onlyPostAllowedJSON.addProperty("message", "Only HTTP POST is accepted."); - - response.add("error", onlyPostAllowedJSON); - response.add("result", JsonNull.INSTANCE); - status = HttpResponseStatus.BAD_REQUEST; - } - - if (!coin.isInstanceRunning()) { - JsonObject instanceNotRunning = new JsonObject(); - - instanceNotRunning.addProperty("code", -1112); - instanceNotRunning.addProperty("message", "This coin is temporarily unavailable."); - - response.add("error", instanceNotRunning); - response.add("result", JsonNull.INSTANCE); - status = HttpResponseStatus.SERVICE_UNAVAILABLE; - } - - if (!successfulAuth) { - JsonObject onlyServerRootJSON = new JsonObject(); - onlyServerRootJSON.addProperty("code", -1111); - onlyServerRootJSON.addProperty("message", "Unauthorized!"); - - response.add("error", onlyServerRootJSON); - response.add("result", JsonNull.INSTANCE); - status = HttpResponseStatus.FORBIDDEN; - } - } - - if (status != HttpResponseStatus.OK) { - ByteBuf responseContent = Unpooled.copiedBuffer(response.toString(), CharsetUtil.UTF_8); - FullHttpResponse httpResponse = new DefaultFullHttpResponse(request.protocolVersion(), status, responseContent); - - writeResponse(ctx, httpResponse, request); - ctx.write(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); - return; - } - - if (request != null) { - String content = request.content().toString(CharsetUtil.UTF_8); - JsonObject jsonReq = null; - - try { - jsonReq = new JsonParser().parse(content).getAsJsonObject(); - - Preconditions.checkNotNull(jsonReq); - - if (!jsonReq.has("method") || !jsonReq.has("params")) { - ctx.close(); - throw new IllegalArgumentException("Bad JSON-RPC request by client."); - } - } catch (Exception e) { - LOGGER.log(Level.INFO, "Failed Content: " + content); - e.printStackTrace(); - JsonObject errorParsingJSON = new JsonObject(); - errorParsingJSON.addProperty("code", -1001); - errorParsingJSON.addProperty("message", "Error parsing JSON."); - - response.add("error", errorParsingJSON); - response.add("result", JsonNull.INSTANCE); - if (e instanceof IllegalArgumentException) { - LOGGER.log(Level.FINER, "[http-server-handler] WARNING: Client sent valid JSON, but did not specify method and/or parameters!"); - } else { - LOGGER.log(Level.FINER, "[http-server-handler] WARNING: Client sent invalid JSON!"); - } - status = HttpResponseStatus.BAD_REQUEST; - } - - if (status == HttpResponseStatus.OK) { - Preconditions.checkNotNull(jsonReq); - - String method = jsonReq.get("method").getAsString(); - JsonArray params = jsonReq.get("params").getAsJsonArray(); - - LOGGER.log(Level.INFO, "[http-server-handler] RPC CALL: " + coin.getTicker()+ " " + method + " PARAMS: " + params.size()); - for (int i = 0; i < params.size(); i++) { - LOGGER.log(Level.INFO, "[http-server-handler] PARAM " + i + ": " + params.get(i).toString()); - } - - response = getResponse(method, params); - LOGGER.log(Level.FINER, response.toString()); - } else { - ByteBuf responseContent = Unpooled.copiedBuffer(response.toString(), CharsetUtil.UTF_8); - FullHttpResponse httpResponse = new DefaultFullHttpResponse(request.protocolVersion(), status, responseContent); - - writeResponse(ctx, httpResponse, request); - ctx.write(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); - return; - } - - if (request instanceof LastHttpContent) { - ByteBuf responseContent = Unpooled.copiedBuffer(response.toString(), CharsetUtil.UTF_8); - - FullHttpResponse httpResponse = new DefaultFullHttpResponse(request.protocolVersion(), status, responseContent); - - if (!writeResponse(ctx, httpResponse, request)) { - ctx.write(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); - } - } - } - } - - private JsonObject getResponse(String method, JsonArray params) { - JsonObject response = new JsonObject(); - - switch (method.toLowerCase()) { - case "reloadconfig": { - Runnable r = () -> { - try { - Thread.sleep(500); - } catch (InterruptedException e) { - e.printStackTrace(); - } - - coin.reloadConfig(); - }; - new Thread(r).start(); - - response.addProperty("result", true); - response.add("error", JsonNull.INSTANCE); - break; - } - case "getinfo": { - JsonObject infoJSON = new JsonObject(); - - infoJSON.addProperty("protocolversion", coin.getNetworkParameters().getProtocolVersionNum(NetworkParameters.ProtocolVersion.CURRENT)); - infoJSON.addProperty("ticker", CoinTickerUtils.tickerToString(coin.getTicker())); - infoJSON.addProperty("balance", coin.getAllBalances()); - infoJSON.addProperty("testnet", coin.isTestnet()); - infoJSON.addProperty("difficulty", 0.0); - infoJSON.addProperty("connections", 1); // We make this a static value for now, since we are deprecating P2P - infoJSON.addProperty("blocks", CoinInstance.getBlockCountByTicker(coin.getTicker())); - infoJSON.addProperty("keypoolsize", coin.getAddressKeyPairs().size()); - infoJSON.addProperty("keypoololdest", 0.0); - - BigDecimal relayFeeDecimal = new BigDecimal(coin.getConfigHelper().getFee()).setScale(8, BigDecimal.ROUND_DOWN); - infoJSON.addProperty("relayfee", relayFeeDecimal); - - infoJSON.addProperty("networkactive", true); - infoJSON.addProperty("timeoffset", 0); - infoJSON.addProperty("rpcready", coin.isInstanceRunning()); - - response.add("result", infoJSON); - response.add("error", JsonNull.INSTANCE); - break; - } - case "getblockcount": { - response.addProperty("result", CoinInstance.getBlockCountByTicker(coin.getTicker())); - response.add("error", JsonNull.INSTANCE); - break; - } - case "getnetworkinfo": { - JsonObject networkInfoJSON = new JsonObject(); - networkInfoJSON.addProperty("protocolversion", coin.getNetworkParameters().getProtocolVersionNum(NetworkParameters.ProtocolVersion.CURRENT)); - networkInfoJSON.addProperty("ticker", CoinTickerUtils.tickerToString(coin.getTicker())); - networkInfoJSON.addProperty("subversion", CoinInstance.getVersionString()); - networkInfoJSON.addProperty("connections", 1); - networkInfoJSON.addProperty("localservices", "0000000000000000"); - - double relayFee = CoinInstance.getRelayFeeByTicker(coin.getTicker()); - if (relayFee == -1) { - relayFee = coin.getConfigHelper().getFee(); - } - - BigDecimal relayFeeDecimal = BigDecimal.valueOf(relayFee).setScale(8, BigDecimal.ROUND_DOWN); - - networkInfoJSON.addProperty("relayfee", relayFeeDecimal); - - response.add("result", networkInfoJSON); - response.add("error", JsonNull.INSTANCE); - break; - } - case "listunspent": { + @Override + public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + LOGGER.log(Level.FINER, "[http-server-handler] DEBUG: Channel read complete. Flushing context."); + super.channelReadComplete(ctx); + ctx.flush(); + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) { + boolean successfulAuth = false; + HttpResponseStatus status = HttpResponseStatus.OK; + JsonObject response = new JsonObject(); + + if (request != null) { + HttpHeaders httpHeaders = request.headers(); + + if (HttpUtil.is100ContinueExpected(request)) { + send100Continue(ctx); + } + + String headerUser; + String headerPass; + if (httpHeaders.contains("Authorization")) { + String authorization = httpHeaders.get("Authorization"); + + if (authorization != null && authorization.toLowerCase().startsWith("basic")) { + String base64Credentials = authorization.substring("Basic".length()).trim(); + byte[] credDecoded = Base64.decode(base64Credentials); + String credentials = new String(credDecoded, StandardCharsets.UTF_8); + final String[] values = credentials.split(":", 2); + + headerUser = values[0]; + headerPass = values[1]; + + if (headerUser.equals(configHelper.getRpcUsername()) && headerPass.equals(configHelper.getRpcPassword())) { + successfulAuth = true; + LOGGER.log(Level.FINER, "[http-server-handler] Successful Auth"); + } + } + } + + if (!request.uri().equals("/")) { + JsonObject onlyServerRootJSON = new JsonObject(); + onlyServerRootJSON.addProperty("code", -1002); + onlyServerRootJSON.addProperty("message", "Only the server root ('/') is being served."); + + response.add("error", onlyServerRootJSON); + response.add("result", JsonNull.INSTANCE); + status = HttpResponseStatus.BAD_REQUEST; + } + + if (request.method() != HttpMethod.POST) { + JsonObject onlyPostAllowedJSON = new JsonObject(); + + onlyPostAllowedJSON.addProperty("code", -1003); + onlyPostAllowedJSON.addProperty("message", "Only HTTP POST is accepted."); + + response.add("error", onlyPostAllowedJSON); + response.add("result", JsonNull.INSTANCE); + status = HttpResponseStatus.BAD_REQUEST; + } + + if (!coin.isInstanceRunning()) { + JsonObject instanceNotRunning = new JsonObject(); + + instanceNotRunning.addProperty("code", -1112); + instanceNotRunning.addProperty("message", "This coin is temporarily unavailable."); + + response.add("error", instanceNotRunning); + response.add("result", JsonNull.INSTANCE); + status = HttpResponseStatus.SERVICE_UNAVAILABLE; + } + + if (!successfulAuth) { + JsonObject onlyServerRootJSON = new JsonObject(); + onlyServerRootJSON.addProperty("code", -1111); + onlyServerRootJSON.addProperty("message", "Unauthorized!"); + + response.add("error", onlyServerRootJSON); + response.add("result", JsonNull.INSTANCE); + status = HttpResponseStatus.FORBIDDEN; + } + } + + if (status != HttpResponseStatus.OK) { + ByteBuf responseContent = Unpooled.copiedBuffer(response.toString(), CharsetUtil.UTF_8); + FullHttpResponse httpResponse = new DefaultFullHttpResponse(request.protocolVersion(), status, responseContent); + + writeResponse(ctx, httpResponse, request); + ctx.write(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); + return; + } + + if (request != null) { + String content = request.content().toString(CharsetUtil.UTF_8); + JsonObject jsonReq = null; + + try { + jsonReq = JsonParser.parseString(content).getAsJsonObject(); + + Preconditions.checkNotNull(jsonReq); + + if (!jsonReq.has("method") || !jsonReq.has("params")) { + ctx.close(); + throw new IllegalArgumentException("Bad JSON-RPC request by client."); + } + } catch (Exception e) { + LOGGER.log(Level.INFO, "Failed Content: " + content); + e.printStackTrace(); + JsonObject errorParsingJSON = new JsonObject(); + errorParsingJSON.addProperty("code", -1001); + errorParsingJSON.addProperty("message", "Error parsing JSON."); + + response.add("error", errorParsingJSON); + response.add("result", JsonNull.INSTANCE); + if (e instanceof IllegalArgumentException) { + LOGGER.log(Level.FINER, "[http-server-handler] WARNING: Client sent valid JSON, but did not specify method and/or parameters!"); + } else { + LOGGER.log(Level.FINER, "[http-server-handler] WARNING: Client sent invalid JSON!"); + } + status = HttpResponseStatus.BAD_REQUEST; + } + + if (status == HttpResponseStatus.OK) { + Preconditions.checkNotNull(jsonReq); + + String method = jsonReq.get("method").getAsString(); + JsonArray params = jsonReq.get("params").getAsJsonArray(); + + LOGGER.log(Level.INFO, "[http-server-handler] RPC CALL: " + coin.getTicker() + " " + method + " PARAMS: " + params.size()); + for (int i = 0; i < params.size(); i++) { + LOGGER.log(Level.INFO, "[http-server-handler] PARAM " + i + ": " + params.get(i).toString()); + } + + response = getResponse(method, params); + LOGGER.log(Level.FINER, response.toString()); + } else { + ByteBuf responseContent = Unpooled.copiedBuffer(response.toString(), CharsetUtil.UTF_8); + FullHttpResponse httpResponse = new DefaultFullHttpResponse(request.protocolVersion(), status, responseContent); + + writeResponse(ctx, httpResponse, request); + ctx.write(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); + return; + } + + if (request instanceof LastHttpContent) { + ByteBuf responseContent = Unpooled.copiedBuffer(response.toString(), CharsetUtil.UTF_8); + + FullHttpResponse httpResponse = new DefaultFullHttpResponse(request.protocolVersion(), status, responseContent); + + if (!writeResponse(ctx, httpResponse, request)) { + ctx.write(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); + } + } + } + } + + private JsonObject getResponse(String method, JsonArray params) { + JsonObject response = new JsonObject(); + + switch (method.toLowerCase()) { + case "reloadconfig": { + Runnable r = () -> { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + coin.reloadConfig(); + }; + new Thread(r).start(); + + response.addProperty("result", true); + response.add("error", JsonNull.INSTANCE); + break; + } + case "getinfo": { + JsonObject infoJSON = new JsonObject(); + + infoJSON.addProperty("protocolversion", coin.getNetworkParameters().getProtocolVersionNum(NetworkParameters.ProtocolVersion.CURRENT)); + infoJSON.addProperty("ticker", CoinTickerUtils.tickerToString(coin.getTicker())); + infoJSON.addProperty("balance", coin.getAllBalances()); + infoJSON.addProperty("testnet", coin.isTestnet()); + infoJSON.addProperty("difficulty", 0.0); + infoJSON.addProperty("connections", 1); // We make this a static value for now, since we are deprecating P2P + infoJSON.addProperty("blocks", CoinInstance.getBlockCountByTicker(coin.getTicker())); + infoJSON.addProperty("keypoolsize", coin.getAddressKeyPairs().size()); + infoJSON.addProperty("keypoololdest", 0.0); + + BigDecimal relayFeeDecimal = new BigDecimal(coin.getConfigHelper().getFee()).setScale(8, RoundingMode.DOWN); + infoJSON.addProperty("relayfee", relayFeeDecimal); + + infoJSON.addProperty("networkactive", true); + infoJSON.addProperty("timeoffset", 0); + infoJSON.addProperty("rpcready", coin.isInstanceRunning()); + + response.add("result", infoJSON); + response.add("error", JsonNull.INSTANCE); + break; + } + case "getblockcount": { + response.addProperty("result", CoinInstance.getBlockCountByTicker(coin.getTicker())); + response.add("error", JsonNull.INSTANCE); + break; + } + case "getnetworkinfo": { + JsonObject networkInfoJSON = new JsonObject(); + networkInfoJSON.addProperty("protocolversion", coin.getNetworkParameters().getProtocolVersionNum(NetworkParameters.ProtocolVersion.CURRENT)); + networkInfoJSON.addProperty("ticker", CoinTickerUtils.tickerToString(coin.getTicker())); + networkInfoJSON.addProperty("subversion", CoinInstance.getVersionString()); + networkInfoJSON.addProperty("connections", 1); + networkInfoJSON.addProperty("localservices", "0000000000000000"); + + double relayFee = CoinInstance.getRelayFeeByTicker(coin.getTicker()); + if (relayFee == -1) { + relayFee = coin.getConfigHelper().getFee(); + } + + BigDecimal relayFeeDecimal = BigDecimal.valueOf(relayFee).setScale(8, RoundingMode.DOWN); + + networkInfoJSON.addProperty("relayfee", relayFeeDecimal); + + response.add("result", networkInfoJSON); + response.add("error", JsonNull.INSTANCE); + break; + } + case "listunspent": { JsonArray unspent = httpClient.getUtxos(coin.getTicker(), 30000); if (unspent == null) { response.add("result", JsonNull.INSTANCE); @@ -324,9 +323,9 @@ private JsonObject getResponse(String method, JsonArray params) { } response.add("result", unspent); response.add("error", JsonNull.INSTANCE); - break; - } - case "listtransactions": { + break; + } + case "listtransactions": { int startTime = 0; int endTime = 0; if (params.size() == 2) { @@ -344,69 +343,69 @@ private JsonObject getResponse(String method, JsonArray params) { } response.add("result", transactions); response.add("error", JsonNull.INSTANCE); - break; - } - case "getblockchaininfo": { - JsonObject blockchainInfoJSON = new JsonObject(); - - blockchainInfoJSON.addProperty("chain", coin.getNetworkParameters().getPaymentProtocolId()); - blockchainInfoJSON.addProperty("blocks", CoinInstance.getBlockCountByTicker(coin.getTicker())); - blockchainInfoJSON.addProperty("headers", CoinInstance.getBlockCountByTicker(coin.getTicker())); - blockchainInfoJSON.addProperty("verificationprogress", 1.0); - blockchainInfoJSON.addProperty("difficulty", 0.0); - - blockchainInfoJSON.addProperty("initialblockdownload", false); - blockchainInfoJSON.addProperty("pruned", false); - - response.add("result", blockchainInfoJSON); - response.add("error", JsonNull.INSTANCE); - break; - } - case "getblockhash": { - if (params.size() != 1) { - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - errorJSON.addProperty("code", -1); - errorJSON.addProperty("message", "Usage: getblockhash index\n\nindex (num, required)"); - - response.add("error", errorJSON); - break; - } + break; + } + case "getblockchaininfo": { + JsonObject blockchainInfoJSON = new JsonObject(); + + blockchainInfoJSON.addProperty("chain", coin.getNetworkParameters().getPaymentProtocolId()); + blockchainInfoJSON.addProperty("blocks", CoinInstance.getBlockCountByTicker(coin.getTicker())); + blockchainInfoJSON.addProperty("headers", CoinInstance.getBlockCountByTicker(coin.getTicker())); + blockchainInfoJSON.addProperty("verificationprogress", 1.0); + blockchainInfoJSON.addProperty("difficulty", 0.0); + + blockchainInfoJSON.addProperty("initialblockdownload", false); + blockchainInfoJSON.addProperty("pruned", false); + + response.add("result", blockchainInfoJSON); + response.add("error", JsonNull.INSTANCE); + break; + } + case "getblockhash": { + if (params.size() != 1) { + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", -1); + errorJSON.addProperty("message", "Usage: getblockhash index\n\nindex (num, required)"); + + response.add("error", errorJSON); + break; + } int blockIndex = params.get(0).getAsInt(); - JsonObject blockHash = httpClient.getBlockHash(coin.getTicker(), blockIndex); + JsonObject blockHash = httpClient.getBlockHash(coin.getTicker(), blockIndex); - if (blockHash == null || blockHash.has("error") && !blockHash.get("error").isJsonNull()) { - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - errorJSON.addProperty("code", -1); - errorJSON.addProperty("message", "Error obtaining blockhash!"); - response.add("error", errorJSON); + if (blockHash == null || blockHash.has("error") && !blockHash.get("error").isJsonNull()) { + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", -1); + errorJSON.addProperty("message", "Error obtaining blockhash!"); + response.add("error", errorJSON); - break; - } + break; + } - if (blockHash.has("result")) { - response.add("result", blockHash.get("result")); - response.add("error", JsonNull.INSTANCE); + if (blockHash.has("result")) { + response.add("result", blockHash.get("result")); + response.add("error", JsonNull.INSTANCE); - break; - } + break; + } - response.add("result", blockHash); + response.add("result", blockHash); response.add("error", JsonNull.INSTANCE); - break; - } - case "sendrawtransaction": { - if (params.size() != 1) { - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - errorJSON.addProperty("code", -1); - errorJSON.addProperty("message", "Usage: sendrawtransaction hex-tx\n\nhex-tx (string, required) - Raw transaction, hex encoded"); - - response.add("error", errorJSON); - break; - } + break; + } + case "sendrawtransaction": { + if (params.size() != 1) { + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", -1); + errorJSON.addProperty("message", "Usage: sendrawtransaction hex-tx\n\nhex-tx (string, required) - Raw transaction, hex encoded"); + + response.add("error", errorJSON); + break; + } String rawTx; Transaction transaction; @@ -426,11 +425,11 @@ private JsonObject getResponse(String method, JsonArray params) { } JsonObject txid = httpClient.sendRawTransaction(coin.getTicker(), rawTx); - if (txid == null || txid.has("error") && !txid.get("error").isJsonNull()) { - int code = -1; + if (txid == null || txid.has("error") && !txid.get("error").isJsonNull()) { + int code = -1; - if (txid != null) - code = txid.get("error").getAsInt(); + if (txid != null) + code = txid.get("error").getAsInt(); response.add("result", JsonNull.INSTANCE); JsonObject errorJSON = new JsonObject(); @@ -441,116 +440,116 @@ private JsonObject getResponse(String method, JsonArray params) { break; } - if (txid.has("result")) { - response.add("result", txid.get("result")); - response.add("error", JsonNull.INSTANCE); + if (txid.has("result")) { + response.add("result", txid.get("result")); + response.add("error", JsonNull.INSTANCE); + + break; + } + + response.add("result", txid); + response.add("error", JsonNull.INSTANCE); + break; + } + case "getrawtransaction": { + if (params.size() > 2) { + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", -1); + errorJSON.addProperty("message", "Usage: getrawtransaction\n\ntxid (string, required) - TXID is required, verbose(optional)"); + + response.add("error", errorJSON); + break; + } + + String txid = params.get(0).getAsString(); + boolean verbose = false; + + if (params.size() == 2) { + String v = params.get(1).toString(); + if (v.equalsIgnoreCase("true") || v.equals("1")) + verbose = true; + } + + JsonObject rawTransaction = httpClient.getRawTransaction(coin.getTicker(), txid, verbose); + + if (rawTransaction == null || rawTransaction.has("error") && !rawTransaction.get("error").isJsonNull()) { + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", -5); + errorJSON.addProperty("message", "No information available about transaction"); + + response.add("error", errorJSON); + + break; + } + + if (rawTransaction.has("result")) { + response.add("result", rawTransaction.get("result")); + response.add("error", JsonNull.INSTANCE); + + break; + } + + response.add("result", rawTransaction); + response.add("error", JsonNull.INSTANCE); + + break; + } + case "getrawmempool": { + if (params.size() > 1) { + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", -1); + errorJSON.addProperty("message", "Usage: getrawmempool\n\nverbose (int, optional) - VERBOSE is optional"); + + response.add("error", errorJSON); + break; + } + + boolean verbose = false; + + if (params.size() == 2) { + String v = params.get(1).toString(); + if (v.equalsIgnoreCase("true") || v.equals("1")) + verbose = true; + } + + JsonObject rawMempool = httpClient.getRawMempool(coin.getTicker(), verbose); + + if (rawMempool == null || rawMempool.has("error") && !rawMempool.get("error").isJsonNull()) { + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", -1); + errorJSON.addProperty("message", "No information available"); + + response.add("error", errorJSON); + + break; + } + + if (rawMempool.has("result")) { + response.add("result", rawMempool.get("result")); + response.add("error", JsonNull.INSTANCE); - break; - } + break; + } - response.add("result", txid); + response.add("result", rawMempool); response.add("error", JsonNull.INSTANCE); + break; - } - case "getrawtransaction": { - if (params.size() > 2) { - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - errorJSON.addProperty("code", -1); - errorJSON.addProperty("message", "Usage: getrawtransaction\n\ntxid (string, required) - TXID is required, verbose(optional)"); - - response.add("error", errorJSON); - break; - } - - String txid = params.get(0).getAsString(); - boolean verbose = false; - - if (params.size() == 2) { - String v = params.get(1).toString(); - if (v.equalsIgnoreCase("true") || v.equals("1")) - verbose = true; - } - - JsonObject rawTransaction = httpClient.getRawTransaction(coin.getTicker(), txid, verbose); - - if (rawTransaction == null || rawTransaction.has("error") && !rawTransaction.get("error").isJsonNull()) { - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - errorJSON.addProperty("code", -5); - errorJSON.addProperty("message", "No information available about transaction"); - - response.add("error", errorJSON); - - break; - } - - if (rawTransaction.has("result")) { - response.add("result", rawTransaction.get("result")); - response.add("error", JsonNull.INSTANCE); - - break; - } - - response.add("result", rawTransaction); - response.add("error", JsonNull.INSTANCE); - - break; - } - case "getrawmempool": { - if (params.size() > 1) { - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - errorJSON.addProperty("code", -1); - errorJSON.addProperty("message", "Usage: getrawmempool\n\nverbose (int, optional) - VERBOSE is optional"); - - response.add("error", errorJSON); - break; - } - - boolean verbose = false; - - if (params.size() == 2) { - String v = params.get(1).toString(); - if (v.equalsIgnoreCase("true") || v.equals("1")) - verbose = true; - } - - JsonObject rawMempool = httpClient.getRawMempool(coin.getTicker(), verbose); - - if (rawMempool == null || rawMempool.has("error") && !rawMempool.get("error").isJsonNull()) { - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - errorJSON.addProperty("code", -1); - errorJSON.addProperty("message", "No information available"); - - response.add("error", errorJSON); - - break; - } - - if (rawMempool.has("result")) { - response.add("result", rawMempool.get("result")); - response.add("error", JsonNull.INSTANCE); - - break; - } - - response.add("result", rawMempool); - response.add("error", JsonNull.INSTANCE); - - break; - } - case "getblock": { - if (params.size() != 1) { - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - errorJSON.addProperty("code", -1); - errorJSON.addProperty("message", "Usage: getblock hash\n\nhash (string, required) - Hash of block to retrieve"); - - response.add("error", errorJSON); - break; - } + } + case "getblock": { + if (params.size() != 1) { + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", -1); + errorJSON.addProperty("message", "Usage: getblock hash\n\nhash (string, required) - Hash of block to retrieve"); + + response.add("error", errorJSON); + break; + } String hash; try { @@ -569,7 +568,7 @@ private JsonObject getResponse(String method, JsonArray params) { JsonObject block = httpClient.getBlock(coin.getTicker(), hash, true); - if (block == null || block.has("error") && !block.get("error").isJsonNull()) { + if (block == null || block.has("error") && !block.get("error").isJsonNull()) { response.add("result", JsonNull.INSTANCE); JsonObject errorJSON = new JsonObject(); errorJSON.addProperty("code", -1); @@ -579,34 +578,34 @@ private JsonObject getResponse(String method, JsonArray params) { break; } - if (block.has("result")) { - response.add("result", block.get("result")); - response.add("error", JsonNull.INSTANCE); + if (block.has("result")) { + response.add("result", block.get("result")); + response.add("error", JsonNull.INSTANCE); - break; - } + break; + } - response.add("result", block); + response.add("result", block); response.add("error", JsonNull.INSTANCE); break; - } - case "gettransaction": { - if (params.size() != 1) { - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - errorJSON.addProperty("code", -1); - errorJSON.addProperty("message", "Usage: gettransaction txid\n\ntxid (string, required) - The transaction ID"); - - response.add("error", errorJSON); - break; - } + } + case "gettransaction": { + if (params.size() != 1) { + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", -1); + errorJSON.addProperty("message", "Usage: gettransaction txid\n\ntxid (string, required) - The transaction ID"); + + response.add("error", errorJSON); + break; + } String txid = params.get(0).getAsString(); JsonObject transaction = httpClient.getTransaction(coin.getTicker(), txid, true); - if (transaction == null || transaction.has("error") && !transaction.get("error").isJsonNull()) { - response.add("result", JsonNull.INSTANCE); + if (transaction == null || transaction.has("error") && !transaction.get("error").isJsonNull()) { + response.add("result", JsonNull.INSTANCE); JsonObject errorJSON = new JsonObject(); errorJSON.addProperty("code", -1); errorJSON.addProperty("message", "Error while obtaining transaction!"); @@ -615,301 +614,301 @@ private JsonObject getResponse(String method, JsonArray params) { break; } - if (transaction.has("result")) { - response.add("result", transaction.get("result")); - response.add("error", JsonNull.INSTANCE); + if (transaction.has("result")) { + response.add("result", transaction.get("result")); + response.add("error", JsonNull.INSTANCE); - break; - } + break; + } - response.add("result", transaction); + response.add("result", transaction); response.add("error", JsonNull.INSTANCE); break; - } - case "getaddressesbyaccount": { - if (params.size() != 1) { - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - errorJSON.addProperty("code", -1); - errorJSON.addProperty("message", "Usage: getaddressesbyaccount account\n\naccount (string, required) - The account from which to grab addresses. The only account available (for now) is 'main'."); - - response.add("error", errorJSON); - break; - } - - String account = params.get(0).getAsString(); - - if (account.equals("main")) { - JsonArray addresses = new JsonArray(); - for (AddressBalance addressBalance : coin.getAddressKeyPairs()) { - addresses.add(addressBalance.getAddress().toBase58()); - } - - response.add("result", addresses); - response.add("error", JsonNull.INSTANCE); - } else { - response.add("result", new JsonArray()); - response.add("error", JsonNull.INSTANCE); - } - break; - } - case "createrawtransaction": { - if (params.size() < 2 || params.size() > 3) { - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - errorJSON.addProperty("code", -1); - errorJSON.addProperty("message", - "Usage: createrawtransaction inputs outputs\n\ninputs (string, required) - Inputs in JSON format\nexample: [{\"txid\": \"id\", \"vout\": n}, ...]\n\noutputs (string, required) - Outputs in JSON format\nexample (legacy): {\"address1\": amount1, \"address2\": amount2, ...}\nexample (new): [{\"address\": \"address1\", \"amount\": amount1}, {\"address\": \"address1\", \"amount\": amount2}, ...]"); - response.add("error", errorJSON); - break; - } - - JsonArray inputs; - // Instead of using a JsonObject for outputs, we’ll parse outputs into a list of - // OutputEntry. - List outputEntries = new ArrayList<>(); - long locktime = 0; - - if (params.size() >= 3) - locktime = params.get(3).getAsLong(); - - try { - inputs = params.get(0).getAsJsonArray(); - for (int i = 0; i < inputs.size(); i++) { - JsonObject input = inputs.get(i).getAsJsonObject(); - if (!input.has("txid") || !input.has("vout")) - throw new JsonParseException("Bad transaction input."); - } - - JsonElement outputsElem = params.get(1); - if (outputsElem.isJsonArray()) { - // New array format: allows duplicate outputs with same address. - JsonArray outputsArray = outputsElem.getAsJsonArray(); - for (JsonElement elem : outputsArray) { - JsonObject obj = elem.getAsJsonObject(); - if (!obj.has("address") || !obj.has("amount")) - throw new JsonParseException("Output entry must have 'address' and 'amount'"); - String addr = obj.get("address").getAsString(); - double amt = obj.get("amount").getAsDouble(); - outputEntries.add(new OutputEntry(addr, amt)); - } - } else if (outputsElem.isJsonObject()) { - // Legacy format: keys are addresses. - JsonObject outputsObject = outputsElem.getAsJsonObject(); - for (String addr : outputsObject.keySet()) { - double amt = outputsObject.get(addr).getAsDouble(); - outputEntries.add(new OutputEntry(addr, amt)); - } - } else { - throw new JsonParseException("Invalid outputs format"); - } - } catch (JsonParseException e) { - LOGGER.log(Level.FINER, - "[http-server-handler] ERROR: Error while parsing JSON for createrawtransaction!"); - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - errorJSON.addProperty("code", -2); - errorJSON.addProperty("message", "Error parsing JSON"); - response.add("error", errorJSON); - break; - } - - Transaction tx = new Transaction(coin.getNetworkParameters()); - if (locktime > 0 && !tx.isTimeLocked()) { - tx.setLockTime(locktime); - } - - boolean inputSuccess = true; - for (int i = 0; i < inputs.size(); i++) { - JsonObject input = inputs.get(i).getAsJsonObject(); - try { - String txid = input.get("txid").getAsString(); - int vout = input.get("vout").getAsInt(); - tx.addInput(Sha256Hash.wrap(txid), vout, ScriptBuilder.createInputScript(null)); - } catch (Exception e) { - LOGGER.log(Level.FINER, - "[http-server-handler] ERROR: Error while constructing transaction (input phase)!"); - txConstructionError(response, e, "Error while constructing transaction (input phase)"); - inputSuccess = false; - break; - } - } - if (!inputSuccess) - break; - - boolean outputSuccess = true; - - // First, add P2SH outputs. - for (OutputEntry entry : outputEntries) { - try { - Address address = Address.fromBase58(coin.getNetworkParameters(), entry.address); - Coin outputValue = Coin.valueOf((long) Math.floor(entry.amount * Coin.COIN.value)); - if (isP2SHAddress(entry.address)) { - LOGGER.log(Level.FINER, "[http-server-handler] P2SH Address Found: " + entry.address); - Script p2shScript = ScriptBuilder.createP2SHOutputScript(address.getHash160()); - tx.addOutput(outputValue, p2shScript); - } - } catch (Exception e) { - LOGGER.log(Level.FINER, - "[http-server-handler] ERROR: Error while constructing transaction (output phase)!"); - e.printStackTrace(); - txConstructionError(response, e, "Error while constructing transaction (output phase)"); - outputSuccess = false; - } - } - // Then, add non-P2SH outputs. - for (OutputEntry entry : outputEntries) { - try { - Address address = Address.fromBase58(coin.getNetworkParameters(), entry.address); - Coin outputValue = Coin.valueOf((long) Math.floor(entry.amount * Coin.COIN.value)); - if (!isP2SHAddress(entry.address)) { - tx.addOutput(outputValue, address); - } - } catch (Exception e) { - LOGGER.log(Level.FINER, - "[http-server-handler] ERROR: Error while constructing transaction (output phase)!"); - e.printStackTrace(); - txConstructionError(response, e, "Error while constructing transaction (output phase)"); - outputSuccess = false; - } - } - - if (!outputSuccess) - break; - - String hexTx = new String(Hex.encode(tx.bitcoinSerialize())); - LOGGER.log(Level.FINER, "[http-server-handler] DEBUG: Raw transaction = " + hexTx); - response.addProperty("result", hexTx); - response.add("error", JsonNull.INSTANCE); - break; - } - case "decoderawtransaction": { - if (params.size() != 1) { - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - errorJSON.addProperty("code", -1); - errorJSON.addProperty("message", "Usage: decoderawtransaction rawtx\n\nrawtx (string, required) - The raw transaction to decode, hex encoded."); - - response.add("error", errorJSON); - break; - } - - String rawTx = params.get(0).getAsString(); - Transaction tx; - - try { - tx = new Transaction(coin.getNetworkParameters(), Hex.decode(rawTx)); - } catch (Exception e) { - e.printStackTrace(); - getInvalidTxResponse(response, e); - break; - } - - JsonObject txJSON = new JsonObject(); - - txJSON.addProperty("txid", tx.getHashAsString()); - txJSON.addProperty("version", tx.getVersion()); - txJSON.addProperty("locktime", tx.getLockTime()); - JsonArray vin = new JsonArray(); - - boolean inputParseSuccess = true; - - for (TransactionInput input : tx.getInputs()) { - try { - JsonObject thisVin = new JsonObject(); - thisVin.addProperty("txid", input.getOutpoint().getHash().toString()); - thisVin.addProperty("vout", input.getOutpoint().getIndex()); - - JsonObject scriptSig = new JsonObject(); - scriptSig.addProperty("asm", canonicalizeASM(input.getScriptSig().toString())); - scriptSig.addProperty("hex", new String(Hex.encode(input.getScriptSig().getProgram()))); - - thisVin.add("scriptSig", scriptSig); - thisVin.addProperty("sequence", input.getSequenceNumber()); - - vin.add(thisVin); - } catch (Exception e) { - LOGGER.log(Level.FINER, "[http-server-handler] ERROR: Error while parsing transaction inputs!"); - e.printStackTrace(); - - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - errorJSON.addProperty("code", -1008); - errorJSON.addProperty("message", "Error while parsing transaction inputs"); - - response.add("error", errorJSON); - - inputParseSuccess = false; - } - } - - if (!inputParseSuccess) - break; - - txJSON.add("vin", vin); - JsonArray vout = new JsonArray(); - - for (TransactionOutput output : tx.getOutputs()) { - try { - Script.ScriptType type = output.getScriptPubKey().getScriptType(); - - JsonObject thisVout = new JsonObject(); - - thisVout.addProperty("value", (double) output.getValue().value / 100000000.0); - thisVout.addProperty("n", output.getIndex()); - - JsonObject scriptPubKey = new JsonObject(); - scriptPubKey.addProperty("asm", canonicalizeASM(output.getScriptPubKey().toString())); - scriptPubKey.addProperty("hex", new String(Hex.encode(output.getScriptPubKey().getProgram()))); - - if (type == Script.ScriptType.P2SH) { - scriptPubKey.addProperty("reqSigs", 1); - } else { - scriptPubKey.addProperty("reqSigs", output.getScriptPubKey().getNumberOfSignaturesRequiredToSpend()); - } - - getScriptType(scriptPubKey, type); - - JsonArray addresses = new JsonArray(); - addresses.add(output.getScriptPubKey().getToAddress(coin.getNetworkParameters()).toBase58()); - - scriptPubKey.add("addresses", addresses); - thisVout.add("scriptPubKey", scriptPubKey); - - vout.add(thisVout); - } catch (Exception e) { - LOGGER.log(Level.FINER, "[http-server-handler] ERROR: Error while parsing transaction outputs!"); - e.printStackTrace(); - - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - errorJSON.addProperty("code", -1009); - errorJSON.addProperty("message", "Error while parsing transaction outputs"); + } + case "getaddressesbyaccount": { + if (params.size() != 1) { + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", -1); + errorJSON.addProperty("message", "Usage: getaddressesbyaccount account\n\naccount (string, required) - The account from which to grab addresses. The only account available (for now) is 'main'."); - response.add("error", errorJSON); - } - } + response.add("error", errorJSON); + break; + } - txJSON.add("vout", vout); + String account = params.get(0).getAsString(); - response.add("result", txJSON); - response.add("error", JsonNull.INSTANCE); + if (account.equals("main")) { + JsonArray addresses = new JsonArray(); + for (AddressBalance addressBalance : coin.getAddressKeyPairs()) { + addresses.add(addressBalance.getAddress().toBase58()); + } - break; - } - case "signrawtransaction": { - if (params.size() < 1 || params.size() > 3) { - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - errorJSON.addProperty("code", -1); - errorJSON.addProperty("message", "Usage: signrawtransaction rawtx\n\nrawtx (string, required) - The raw transaction to sign, hex encoded."); + response.add("result", addresses); + response.add("error", JsonNull.INSTANCE); + } else { + response.add("result", new JsonArray()); + response.add("error", JsonNull.INSTANCE); + } + break; + } + case "createrawtransaction": { + if (params.size() < 2 || params.size() > 3) { + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", -1); + errorJSON.addProperty("message", + "Usage: createrawtransaction inputs outputs\n\ninputs (string, required) - Inputs in JSON format\nexample: [{\"txid\": \"id\", \"vout\": n}, ...]\n\noutputs (string, required) - Outputs in JSON format\nexample (legacy): {\"address1\": amount1, \"address2\": amount2, ...}\nexample (new): [{\"address\": \"address1\", \"amount\": amount1}, {\"address\": \"address1\", \"amount\": amount2}, ...]"); + response.add("error", errorJSON); + break; + } + + JsonArray inputs; + // Instead of using a JsonObject for outputs, we’ll parse outputs into a list of + // OutputEntry. + List outputEntries = new ArrayList<>(); + long locktime = 0; - response.add("error", errorJSON); - break; - } + if (params.size() >= 3) + locktime = params.get(3).getAsLong(); - String rawTx = params.get(0).getAsString(); - Transaction tx; + try { + inputs = params.get(0).getAsJsonArray(); + for (int i = 0; i < inputs.size(); i++) { + JsonObject input = inputs.get(i).getAsJsonObject(); + if (!input.has("txid") || !input.has("vout")) + throw new JsonParseException("Bad transaction input."); + } + + JsonElement outputsElem = params.get(1); + if (outputsElem.isJsonArray()) { + // New array format: allows duplicate outputs with same address. + JsonArray outputsArray = outputsElem.getAsJsonArray(); + for (JsonElement elem : outputsArray) { + JsonObject obj = elem.getAsJsonObject(); + if (!obj.has("address") || !obj.has("amount")) + throw new JsonParseException("Output entry must have 'address' and 'amount'"); + String addr = obj.get("address").getAsString(); + double amt = obj.get("amount").getAsDouble(); + outputEntries.add(new OutputEntry(addr, amt)); + } + } else if (outputsElem.isJsonObject()) { + // Legacy format: keys are addresses. + JsonObject outputsObject = outputsElem.getAsJsonObject(); + for (String addr : outputsObject.keySet()) { + double amt = outputsObject.get(addr).getAsDouble(); + outputEntries.add(new OutputEntry(addr, amt)); + } + } else { + throw new JsonParseException("Invalid outputs format"); + } + } catch (JsonParseException e) { + LOGGER.log(Level.FINER, + "[http-server-handler] ERROR: Error while parsing JSON for createrawtransaction!"); + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", -2); + errorJSON.addProperty("message", "Error parsing JSON"); + response.add("error", errorJSON); + break; + } + + Transaction tx = new Transaction(coin.getNetworkParameters()); + if (locktime > 0 && !tx.isTimeLocked()) { + tx.setLockTime(locktime); + } + + boolean inputSuccess = true; + for (int i = 0; i < inputs.size(); i++) { + JsonObject input = inputs.get(i).getAsJsonObject(); + try { + String txid = input.get("txid").getAsString(); + int vout = input.get("vout").getAsInt(); + tx.addInput(Sha256Hash.wrap(txid), vout, ScriptBuilder.createInputScript(null)); + } catch (Exception e) { + LOGGER.log(Level.FINER, + "[http-server-handler] ERROR: Error while constructing transaction (input phase)!"); + txConstructionError(response, e, "Error while constructing transaction (input phase)"); + inputSuccess = false; + break; + } + } + if (!inputSuccess) + break; + + boolean outputSuccess = true; + + // First, add P2SH outputs. + for (OutputEntry entry : outputEntries) { + try { + Address address = Address.fromBase58(coin.getNetworkParameters(), entry.address); + Coin outputValue = Coin.valueOf((long) Math.floor(entry.amount * Coin.COIN.value)); + if (isP2SHAddress(entry.address)) { + LOGGER.log(Level.FINER, "[http-server-handler] P2SH Address Found: " + entry.address); + Script p2shScript = ScriptBuilder.createP2SHOutputScript(address.getHash160()); + tx.addOutput(outputValue, p2shScript); + } + } catch (Exception e) { + LOGGER.log(Level.FINER, + "[http-server-handler] ERROR: Error while constructing transaction (output phase)!"); + e.printStackTrace(); + txConstructionError(response, e, "Error while constructing transaction (output phase)"); + outputSuccess = false; + } + } + // Then, add non-P2SH outputs. + for (OutputEntry entry : outputEntries) { + try { + Address address = Address.fromBase58(coin.getNetworkParameters(), entry.address); + Coin outputValue = Coin.valueOf((long) Math.floor(entry.amount * Coin.COIN.value)); + if (!isP2SHAddress(entry.address)) { + tx.addOutput(outputValue, address); + } + } catch (Exception e) { + LOGGER.log(Level.FINER, + "[http-server-handler] ERROR: Error while constructing transaction (output phase)!"); + e.printStackTrace(); + txConstructionError(response, e, "Error while constructing transaction (output phase)"); + outputSuccess = false; + } + } + + if (!outputSuccess) + break; + + String hexTx = new String(Hex.encode(tx.bitcoinSerialize())); + LOGGER.log(Level.FINER, "[http-server-handler] DEBUG: Raw transaction = " + hexTx); + response.addProperty("result", hexTx); + response.add("error", JsonNull.INSTANCE); + break; + } + case "decoderawtransaction": { + if (params.size() != 1) { + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", -1); + errorJSON.addProperty("message", "Usage: decoderawtransaction rawtx\n\nrawtx (string, required) - The raw transaction to decode, hex encoded."); + + response.add("error", errorJSON); + break; + } + + String rawTx = params.get(0).getAsString(); + Transaction tx; + + try { + tx = new Transaction(coin.getNetworkParameters(), Hex.decode(rawTx)); + } catch (Exception e) { + e.printStackTrace(); + getInvalidTxResponse(response, e); + break; + } + + JsonObject txJSON = new JsonObject(); + + txJSON.addProperty("txid", tx.getHashAsString()); + txJSON.addProperty("version", tx.getVersion()); + txJSON.addProperty("locktime", tx.getLockTime()); + JsonArray vin = new JsonArray(); + + boolean inputParseSuccess = true; + + for (TransactionInput input : tx.getInputs()) { + try { + JsonObject thisVin = new JsonObject(); + thisVin.addProperty("txid", input.getOutpoint().getHash().toString()); + thisVin.addProperty("vout", input.getOutpoint().getIndex()); + + JsonObject scriptSig = new JsonObject(); + scriptSig.addProperty("asm", canonicalizeASM(input.getScriptSig().toString())); + scriptSig.addProperty("hex", new String(Hex.encode(input.getScriptSig().getProgram()))); + + thisVin.add("scriptSig", scriptSig); + thisVin.addProperty("sequence", input.getSequenceNumber()); + + vin.add(thisVin); + } catch (Exception e) { + LOGGER.log(Level.FINER, "[http-server-handler] ERROR: Error while parsing transaction inputs!"); + e.printStackTrace(); + + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", -1008); + errorJSON.addProperty("message", "Error while parsing transaction inputs"); + + response.add("error", errorJSON); + + inputParseSuccess = false; + } + } + + if (!inputParseSuccess) + break; + + txJSON.add("vin", vin); + JsonArray vout = new JsonArray(); + + for (TransactionOutput output : tx.getOutputs()) { + try { + Script.ScriptType type = output.getScriptPubKey().getScriptType(); + + JsonObject thisVout = new JsonObject(); + + thisVout.addProperty("value", (double) output.getValue().value / 100000000.0); + thisVout.addProperty("n", output.getIndex()); + + JsonObject scriptPubKey = new JsonObject(); + scriptPubKey.addProperty("asm", canonicalizeASM(output.getScriptPubKey().toString())); + scriptPubKey.addProperty("hex", new String(Hex.encode(output.getScriptPubKey().getProgram()))); + + if (type == Script.ScriptType.P2SH) { + scriptPubKey.addProperty("reqSigs", 1); + } else { + scriptPubKey.addProperty("reqSigs", output.getScriptPubKey().getNumberOfSignaturesRequiredToSpend()); + } + + getScriptType(scriptPubKey, type); + + JsonArray addresses = new JsonArray(); + addresses.add(output.getScriptPubKey().getToAddress(coin.getNetworkParameters()).toBase58()); + + scriptPubKey.add("addresses", addresses); + thisVout.add("scriptPubKey", scriptPubKey); + + vout.add(thisVout); + } catch (Exception e) { + LOGGER.log(Level.FINER, "[http-server-handler] ERROR: Error while parsing transaction outputs!"); + e.printStackTrace(); + + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", -1009); + errorJSON.addProperty("message", "Error while parsing transaction outputs"); + + response.add("error", errorJSON); + } + } + + txJSON.add("vout", vout); + + response.add("result", txJSON); + response.add("error", JsonNull.INSTANCE); + + break; + } + case "signrawtransaction": { + if (params.size() < 1 || params.size() > 3) { + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", -1); + errorJSON.addProperty("message", "Usage: signrawtransaction rawtx\n\nrawtx (string, required) - The raw transaction to sign, hex encoded."); + + response.add("error", errorJSON); + break; + } + + String rawTx = params.get(0).getAsString(); + Transaction tx; // JsonArray prevtxs; // JsonArray privkeys; // @@ -918,399 +917,399 @@ private JsonObject getResponse(String method, JsonArray params) { // privkeys = params.get(2).getAsJsonArray(); // } - try { - tx = new Transaction(coin.getNetworkParameters(), Hex.decode(rawTx)); - } catch (Exception e) { - e.printStackTrace(); - getInvalidTxResponse(response, e); - break; - } + try { + tx = new Transaction(coin.getNetworkParameters(), Hex.decode(rawTx)); + } catch (Exception e) { + e.printStackTrace(); + getInvalidTxResponse(response, e); + break; + } - Transaction signedTx = new Transaction(coin.getNetworkParameters()); + Transaction signedTx = new Transaction(coin.getNetworkParameters()); - boolean complete = true; + boolean complete = true; - for (TransactionOutput output : tx.getOutputs()) { - signedTx.addOutput(output); - } + for (TransactionOutput output : tx.getOutputs()) { + signedTx.addOutput(output); + } - for (TransactionInput input : tx.getInputs()) { - Sha256Hash txid = input.getOutpoint().getHash(); - long vout = input.getOutpoint().getIndex(); + for (TransactionInput input : tx.getInputs()) { + Sha256Hash txid = input.getOutpoint().getHash(); + long vout = input.getOutpoint().getIndex(); - ECKey signingKey = getSigningKey(txid, vout); - UTXO utxo = getUtxo(txid, vout); + ECKey signingKey = getSigningKey(txid, vout); + UTXO utxo = getUtxo(txid, vout); - if (utxo == null || signingKey == null) { - getInvalidTxResponse(response, new Exception("Transaction contains an utxo/input which does not exist in our wallet.")); - break; - } + if (utxo == null || signingKey == null) { + getInvalidTxResponse(response, new Exception("Transaction contains an utxo/input which does not exist in our wallet.")); + break; + } - org.bitcoinj.core.UTXO bUtxo = utxo.createUTXO(); + org.bitcoinj.core.UTXO bUtxo = utxo.createUTXO(); - TransactionOutPoint outPoint = new TransactionOutPoint(coin.getNetworkParameters(), bUtxo.getIndex(), bUtxo.getHash()); + TransactionOutPoint outPoint = new TransactionOutPoint(coin.getNetworkParameters(), bUtxo.getIndex(), bUtxo.getHash()); - signedTx.addSignedInput(outPoint, bUtxo.getScript(), signingKey, Transaction.SigHash.ALL, true); + signedTx.addSignedInput(outPoint, bUtxo.getScript(), signingKey, Transaction.SigHash.ALL, true); // utxo.setSpent(true); - } - - String signedTxHex = new String(Hex.encode(signedTx.bitcoinSerialize())); - JsonObject resultJSON = new JsonObject(); - resultJSON.addProperty("hex", signedTxHex); - resultJSON.addProperty("complete", complete); - - response.add("result", resultJSON); - response.add("error", JsonNull.INSTANCE); - - LOGGER.log(Level.FINER, "[DEBUG http-server-handler] Signed Raw Transaction: " + response.toString()); - break; - } - case "gettxout": { - if (params.size() < 2 || params.size() > 3) { - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - errorJSON.addProperty("code", -1); - errorJSON.addProperty("message", "Usage: gettxout txid n [include_mempool]\\n" + // - "\\n" + // - "txid (string, required) - The transaction ID\\n" + // - "n (numeric, required) - The vout value\\n" + // - "include_mempool (boolean, optional, default=true) - Whether to include the mempool (WARNING: This can block execution for several seconds)"); - - response.add("error", errorJSON); - break; - } - - String txid = params.get(0).getAsString(); - int n = params.get(1).getAsInt(); - boolean includeMempool = true; - if (params.size() == 3) { - includeMempool = params.get(2).getAsBoolean(); - } - - // attempt to find UTXO in cache - UTXO requested = this.getUtxo(Sha256Hash.wrap(txid), n); - - if (requested != null) { - LOGGER.log(Level.FINER, "[http-server-handler] Using cached UTXO for gettxout"); - - org.bitcoinj.core.UTXO utxo = requested.createUTXO(); - JsonObject resultJSON = new JsonObject(); - - resultJSON.addProperty("confirmations", - (CoinInstance.getBlockCountByTicker(coin.getTicker()) - utxo.getHeight()) + 1); - resultJSON.addProperty("value", utxo.getValue().value / 100000000.0); - - JsonObject scriptPubKey = new JsonObject(); - scriptPubKey.addProperty("asm", utxo.getScript().toString()); - scriptPubKey.addProperty("hex", new String(Hex.encode(utxo.getScript().getProgram()))); - scriptPubKey.addProperty("reqSigs", utxo.getScript().getNumberOfSignaturesRequiredToSpend()); - - Script.ScriptType type = utxo.getScript().getScriptType(); - getScriptType(scriptPubKey, type); - - JsonArray addresses = new JsonArray(); - addresses.add(utxo.getAddress()); - scriptPubKey.add("addresses", addresses); - - resultJSON.add("scriptPubKey", scriptPubKey); - resultJSON.addProperty("coinbase", utxo.isCoinbase()); - - response.add("result", resultJSON); - response.add("error", JsonNull.INSTANCE); - break; - } - - if (!includeMempool) { - LOGGER.log(Level.FINER, "[http-server-handler] WARNING: Client requested UTXO that is not ours!"); - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - - errorJSON.addProperty("code", -5); - errorJSON.addProperty("message", "Invalid or non-wallet transaction ID (not ours)"); - response.add("error", errorJSON); - break; - } - - // considering mempool, find/wait for the transaction - JsonObject transaction = null; - int retries = includeMempool ? 5 : 1; - for (int i = 0; i < retries; i++) { - transaction = httpClient.getTransaction(coin.getTicker(), txid, true); - if (transaction == null || transaction.has("result") && transaction.get("result").isJsonNull()) { - if (i < retries - 1) { - try { - Thread.sleep(2000); - } catch (Exception e) { - } - continue; - } - } else { - break; - } - } - - if (transaction == null || transaction.has("error") && !transaction.get("error").isJsonNull()) { - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - - errorJSON.addProperty("code", -5); - errorJSON.addProperty("message", "Invalid transaction ID"); - response.add("error", errorJSON); - break; - } - - // extract the UTXO - try { - // Note: will throw when attempting to parse a coinbase utxo (that's fine) - JsonObject result = transaction.getAsJsonObject("result"); - JsonElement confirmations = result.get("confirmations"); - JsonArray vout = result.getAsJsonArray("vout"); - - if (vout.size() <= n) { - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - - errorJSON.addProperty("code", -5); - errorJSON.addProperty("message", "Invalid transaction output index"); - response.add("error", errorJSON); - break; - } - - JsonObject entry = vout.get(n).getAsJsonObject(); - JsonElement value = entry.get("value"); - JsonObject scriptPubKey = entry.getAsJsonObject("scriptPubKey"); - JsonElement addr = scriptPubKey.get("address"); - JsonArray addresses = scriptPubKey.getAsJsonArray("addresses"); - - String address = null; - if(addr != null) { - address = addr.getAsString(); - } else { - address = addresses.asList().get(0).getAsString(); - } - - // ensure address belongs to our wallet - boolean isOurs = false; - for (AddressBalance addressBalance : coin.getAddressKeyPairs()) { - String addressCheck = addressBalance.getAddress().toString(); - if (address.equals(addressCheck)) { - isOurs = true; - break; - } - } - - if (!isOurs) { - LOGGER.log(Level.FINER, "[http-server-handler] WARNING: Client requested UTXO that cannot be ours!"); - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - - errorJSON.addProperty("code", -5); - errorJSON.addProperty("message", "Invalid or non-wallet transaction ID (cannot be ours)"); - response.add("error", errorJSON); - break; - } - - int count = 0; - if (confirmations != null) { - count = confirmations.getAsInt(); - } - - // ensure UTXO is unspent - // Caution: Beware of race conditions; the backend might return results for 'getTransaction' - // before updating entries for 'getUtxos'. As a result, we only check confirmed transactions. - // Note that unconfirmed UTXOs spent in the memory pool will still be returned, leading to - // a slightly different behavior compared to a core wallet. - if (count > 0) { - // Note: this request is expensive - boolean unspent = false; - JsonArray utxos = httpClient.getUtxosUncached(coin.getTicker(), new String[] { address }); - for (JsonElement utxo : utxos.asList()) { - String newtxid = utxo.getAsJsonObject().get("txid").getAsString(); - int newvout = utxo.getAsJsonObject().get("vout").getAsInt(); - if (newtxid.equals(txid) && newvout == n) { - unspent = true; - break; - } - } - - if (!unspent) { - LOGGER.log(Level.FINER, "[http-server-handler] WARNING: Client requested UTXO that was already spent!"); - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - - errorJSON.addProperty("code", -5); - errorJSON.addProperty("message", "Invalid or non-wallet transaction ID (already spent)"); - response.add("error", errorJSON); - break; - } - } - - // assemble result - JsonObject resultJSON = new JsonObject(); - resultJSON.addProperty("confirmations", count); - resultJSON.add("value", value); - resultJSON.add("scriptPubKey", scriptPubKey); - resultJSON.addProperty("coinbase", false); - - response.add("result", resultJSON); - response.add("error", JsonNull.INSTANCE); - } catch (Exception e) { - LOGGER.log(Level.FINER, "[http-server-handler] ERROR: Error while parsing transaction!"); - e.printStackTrace(); - - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - errorJSON.addProperty("code", -1010); - errorJSON.addProperty("message", "Error while parsing transaction"); - - response.add("error", errorJSON); - } - - break; - } - case "getnewaddress": { - if (params.size() != 0) { - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - errorJSON.addProperty("code", -1); - errorJSON.addProperty("message", "Usage: getnewaddress"); - - response.add("error", errorJSON); - break; - } - - AddressBalance newAddress = coin.generateAddress(true); - response.addProperty("result", newAddress.getAddress().toBase58()); - response.add("error", JsonNull.INSTANCE); - break; - } - case "importprivkey": { - if (params.size() != 1) { - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - errorJSON.addProperty("code", -1); - errorJSON.addProperty("message", "Usage: importprivkey privatekey\n\nNOTE: Key's are not persistent!\n\nprivatekey (string, required)"); - - response.add("error", errorJSON); - break; - } - - coin.importPrivateKey(params.get(0).getAsString()); - response.addProperty("result", ""); - response.add("error", JsonNull.INSTANCE); - break; - } - case "dumpprivkey": { - if (params.size() != 1) { - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - errorJSON.addProperty("code", -1); - errorJSON.addProperty("message", "Usage: dumpprivkey address\n\naddress (string, required)"); - - response.add("error", errorJSON); - break; - } - - AddressBalance addressBalance = coin.getAddress(params.get(0).getAsString()); - - if (addressBalance == null) { - JsonObject errorJSON = new JsonObject(); - errorJSON.addProperty("code", -4); - errorJSON.addProperty("message", "Address does not exist"); - - response.add("result", JsonNull.INSTANCE); - response.add("error", errorJSON); - - break; - } - - response.addProperty("result", addressBalance.getPrivateKey().toBase58()); - response.add("error", JsonNull.INSTANCE); - break; - } - case "signmessage": { - if (params.size() != 2) { - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - errorJSON.addProperty("code", -1); - errorJSON.addProperty("message", "Usage: signmessage address message\n\naddress (string, required) - The address whose private key to use to sign the message\nmessage (string, required) - The message to sign"); - - response.add("error", errorJSON); - break; - } - - String addr = params.get(0).getAsString(); - String message = params.get(1).getAsString(); - - AddressBalance address = coin.getAddressBalance(addr); - if (address == null) { - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - errorJSON.addProperty("code", -5); - errorJSON.addProperty("message", "Invalid or non-wallet address"); - - response.add("error", errorJSON); - break; - } - - ECKey key = address.getPrivateKey().getKey(); - String signatureB64 = signMessage(key, message); - - response.addProperty("result", signatureB64); - response.add("error", JsonNull.INSTANCE); - break; - } - case "verifymessage": { - if (params.size() != 3) { - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - errorJSON.addProperty("code", -1); - errorJSON.addProperty("message", "Usage: verifymessage address signature message\n\naddress (string, required) - The address whose private key to use to sign the message\nsignature (string, required) - The signature to verify, base64 encoded\nmessage (string, required) - The message to sign"); - - response.add("error", errorJSON); - break; - } - - String addr = params.get(0).getAsString(); - String signatureB64 = params.get(1).getAsString(); - String message = params.get(2).getAsString(); - - boolean verified = false; - - try { - ECKey key = signedMessageToKey(signatureB64, message); - verified = verifyMessage(key, signatureB64, message); - if (!verified) - throw new SignatureException("Signature was not verified."); - - String derivedAddr = key.toAddress(coin.getNetworkParameters()).toBase58(); - if (!addr.equals(derivedAddr)) { - LOGGER.log(Level.FINER, "[http-server-handler] ERROR: Addresses do not match! Failing."); - verified = false; - } - } catch (Exception e) { - LOGGER.log(Level.FINER, "[http-server-handler] Error while verifying signature! Invalid signature?"); - e.printStackTrace(); - - response.addProperty("result", verified); - response.add("error", JsonNull.INSTANCE); - break; - } - - response.addProperty("result", verified); - response.add("error", JsonNull.INSTANCE); - break; - } - case "sendtransaction": { - if (params.size() != 2) { - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - errorJSON.addProperty("code", -1); - errorJSON.addProperty("message", "Usage: sendtransaction address amount\n\naddress (string, required)\namount (number, required)"); - - response.add("error", errorJSON); - break; - } + } + + String signedTxHex = new String(Hex.encode(signedTx.bitcoinSerialize())); + JsonObject resultJSON = new JsonObject(); + resultJSON.addProperty("hex", signedTxHex); + resultJSON.addProperty("complete", complete); + + response.add("result", resultJSON); + response.add("error", JsonNull.INSTANCE); + + LOGGER.log(Level.FINER, "[DEBUG http-server-handler] Signed Raw Transaction: " + response.toString()); + break; + } + case "gettxout": { + if (params.size() < 2 || params.size() > 3) { + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", -1); + errorJSON.addProperty("message", "Usage: gettxout txid n [include_mempool]\\n" + // + "\\n" + // + "txid (string, required) - The transaction ID\\n" + // + "n (numeric, required) - The vout value\\n" + // + "include_mempool (boolean, optional, default=true) - Whether to include the mempool (WARNING: This can block execution for several seconds)"); + + response.add("error", errorJSON); + break; + } + + String txid = params.get(0).getAsString(); + int n = params.get(1).getAsInt(); + boolean includeMempool = true; + if (params.size() == 3) { + includeMempool = params.get(2).getAsBoolean(); + } + + // attempt to find UTXO in cache + UTXO requested = this.getUtxo(Sha256Hash.wrap(txid), n); + + if (requested != null) { + LOGGER.log(Level.FINER, "[http-server-handler] Using cached UTXO for gettxout"); + + org.bitcoinj.core.UTXO utxo = requested.createUTXO(); + JsonObject resultJSON = new JsonObject(); + + resultJSON.addProperty("confirmations", + (CoinInstance.getBlockCountByTicker(coin.getTicker()) - utxo.getHeight()) + 1); + resultJSON.addProperty("value", utxo.getValue().value / 100000000.0); + + JsonObject scriptPubKey = new JsonObject(); + scriptPubKey.addProperty("asm", utxo.getScript().toString()); + scriptPubKey.addProperty("hex", new String(Hex.encode(utxo.getScript().getProgram()))); + scriptPubKey.addProperty("reqSigs", utxo.getScript().getNumberOfSignaturesRequiredToSpend()); + + Script.ScriptType type = utxo.getScript().getScriptType(); + getScriptType(scriptPubKey, type); + + JsonArray addresses = new JsonArray(); + addresses.add(utxo.getAddress()); + scriptPubKey.add("addresses", addresses); + + resultJSON.add("scriptPubKey", scriptPubKey); + resultJSON.addProperty("coinbase", utxo.isCoinbase()); + + response.add("result", resultJSON); + response.add("error", JsonNull.INSTANCE); + break; + } + + if (!includeMempool) { + LOGGER.log(Level.FINER, "[http-server-handler] WARNING: Client requested UTXO that is not ours!"); + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + + errorJSON.addProperty("code", -5); + errorJSON.addProperty("message", "Invalid or non-wallet transaction ID (not ours)"); + response.add("error", errorJSON); + break; + } + + // considering mempool, find/wait for the transaction + JsonObject transaction = null; + int retries = includeMempool ? 5 : 1; + for (int i = 0; i < retries; i++) { + transaction = httpClient.getTransaction(coin.getTicker(), txid, true); + if (transaction == null || transaction.has("result") && transaction.get("result").isJsonNull()) { + if (i < retries - 1) { + try { + Thread.sleep(2000); + } catch (Exception e) { + } + continue; + } + } else { + break; + } + } + + if (transaction == null || transaction.has("error") && !transaction.get("error").isJsonNull()) { + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + + errorJSON.addProperty("code", -5); + errorJSON.addProperty("message", "Invalid transaction ID"); + response.add("error", errorJSON); + break; + } + + // extract the UTXO + try { + // Note: will throw when attempting to parse a coinbase utxo (that's fine) + JsonObject result = transaction.getAsJsonObject("result"); + JsonElement confirmations = result.get("confirmations"); + JsonArray vout = result.getAsJsonArray("vout"); + + if (vout.size() <= n) { + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + + errorJSON.addProperty("code", -5); + errorJSON.addProperty("message", "Invalid transaction output index"); + response.add("error", errorJSON); + break; + } + + JsonObject entry = vout.get(n).getAsJsonObject(); + JsonElement value = entry.get("value"); + JsonObject scriptPubKey = entry.getAsJsonObject("scriptPubKey"); + JsonElement addr = scriptPubKey.get("address"); + JsonArray addresses = scriptPubKey.getAsJsonArray("addresses"); + + String address = null; + if (addr != null) { + address = addr.getAsString(); + } else { + address = addresses.asList().get(0).getAsString(); + } + + // ensure address belongs to our wallet + boolean isOurs = false; + for (AddressBalance addressBalance : coin.getAddressKeyPairs()) { + String addressCheck = addressBalance.getAddress().toString(); + if (address.equals(addressCheck)) { + isOurs = true; + break; + } + } + + if (!isOurs) { + LOGGER.log(Level.FINER, "[http-server-handler] WARNING: Client requested UTXO that cannot be ours!"); + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + + errorJSON.addProperty("code", -5); + errorJSON.addProperty("message", "Invalid or non-wallet transaction ID (cannot be ours)"); + response.add("error", errorJSON); + break; + } + + int count = 0; + if (confirmations != null) { + count = confirmations.getAsInt(); + } + + // ensure UTXO is unspent + // Caution: Beware of race conditions; the backend might return results for 'getTransaction' + // before updating entries for 'getUtxos'. As a result, we only check confirmed transactions. + // Note that unconfirmed UTXOs spent in the memory pool will still be returned, leading to + // a slightly different behavior compared to a core wallet. + if (count > 0) { + // Note: this request is expensive + boolean unspent = false; + JsonArray utxos = httpClient.getUtxosUncached(coin.getTicker(), new String[]{address}); + for (JsonElement utxo : utxos.asList()) { + String newtxid = utxo.getAsJsonObject().get("txid").getAsString(); + int newvout = utxo.getAsJsonObject().get("vout").getAsInt(); + if (newtxid.equals(txid) && newvout == n) { + unspent = true; + break; + } + } + + if (!unspent) { + LOGGER.log(Level.FINER, "[http-server-handler] WARNING: Client requested UTXO that was already spent!"); + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + + errorJSON.addProperty("code", -5); + errorJSON.addProperty("message", "Invalid or non-wallet transaction ID (already spent)"); + response.add("error", errorJSON); + break; + } + } + + // assemble result + JsonObject resultJSON = new JsonObject(); + resultJSON.addProperty("confirmations", count); + resultJSON.add("value", value); + resultJSON.add("scriptPubKey", scriptPubKey); + resultJSON.addProperty("coinbase", false); + + response.add("result", resultJSON); + response.add("error", JsonNull.INSTANCE); + } catch (Exception e) { + LOGGER.log(Level.FINER, "[http-server-handler] ERROR: Error while parsing transaction!"); + e.printStackTrace(); + + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", -1010); + errorJSON.addProperty("message", "Error while parsing transaction"); + + response.add("error", errorJSON); + } + + break; + } + case "getnewaddress": { + if (params.size() != 0) { + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", -1); + errorJSON.addProperty("message", "Usage: getnewaddress"); + + response.add("error", errorJSON); + break; + } + + AddressBalance newAddress = coin.generateAddress(true); + response.addProperty("result", newAddress.getAddress().toBase58()); + response.add("error", JsonNull.INSTANCE); + break; + } + case "importprivkey": { + if (params.size() != 1) { + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", -1); + errorJSON.addProperty("message", "Usage: importprivkey privatekey\n\nNOTE: Key's are not persistent!\n\nprivatekey (string, required)"); + + response.add("error", errorJSON); + break; + } + + coin.importPrivateKey(params.get(0).getAsString()); + response.addProperty("result", ""); + response.add("error", JsonNull.INSTANCE); + break; + } + case "dumpprivkey": { + if (params.size() != 1) { + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", -1); + errorJSON.addProperty("message", "Usage: dumpprivkey address\n\naddress (string, required)"); + + response.add("error", errorJSON); + break; + } + + AddressBalance addressBalance = coin.getAddress(params.get(0).getAsString()); + + if (addressBalance == null) { + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", -4); + errorJSON.addProperty("message", "Address does not exist"); + + response.add("result", JsonNull.INSTANCE); + response.add("error", errorJSON); + + break; + } + + response.addProperty("result", addressBalance.getPrivateKey().toBase58()); + response.add("error", JsonNull.INSTANCE); + break; + } + case "signmessage": { + if (params.size() != 2) { + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", -1); + errorJSON.addProperty("message", "Usage: signmessage address message\n\naddress (string, required) - The address whose private key to use to sign the message\nmessage (string, required) - The message to sign"); + + response.add("error", errorJSON); + break; + } + + String addr = params.get(0).getAsString(); + String message = params.get(1).getAsString(); + + AddressBalance address = coin.getAddressBalance(addr); + if (address == null) { + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", -5); + errorJSON.addProperty("message", "Invalid or non-wallet address"); + + response.add("error", errorJSON); + break; + } + + ECKey key = address.getPrivateKey().getKey(); + String signatureB64 = signMessage(key, message); + + response.addProperty("result", signatureB64); + response.add("error", JsonNull.INSTANCE); + break; + } + case "verifymessage": { + if (params.size() != 3) { + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", -1); + errorJSON.addProperty("message", "Usage: verifymessage address signature message\n\naddress (string, required) - The address whose private key to use to sign the message\nsignature (string, required) - The signature to verify, base64 encoded\nmessage (string, required) - The message to sign"); + + response.add("error", errorJSON); + break; + } + + String addr = params.get(0).getAsString(); + String signatureB64 = params.get(1).getAsString(); + String message = params.get(2).getAsString(); + + boolean verified = false; + + try { + ECKey key = signedMessageToKey(signatureB64, message); + verified = verifyMessage(key, signatureB64, message); + if (!verified) + throw new SignatureException("Signature was not verified."); + + String derivedAddr = key.toAddress(coin.getNetworkParameters()).toBase58(); + if (!addr.equals(derivedAddr)) { + LOGGER.log(Level.FINER, "[http-server-handler] ERROR: Addresses do not match! Failing."); + verified = false; + } + } catch (Exception e) { + LOGGER.log(Level.FINER, "[http-server-handler] Error while verifying signature! Invalid signature?"); + e.printStackTrace(); + + response.addProperty("result", verified); + response.add("error", JsonNull.INSTANCE); + break; + } + + response.addProperty("result", verified); + response.add("error", JsonNull.INSTANCE); + break; + } + case "sendtransaction": { + if (params.size() != 2) { + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", -1); + errorJSON.addProperty("message", "Usage: sendtransaction address amount\n\naddress (string, required)\namount (number, required)"); + + response.add("error", errorJSON); + break; + } String address; double amount; @@ -1343,459 +1342,459 @@ private JsonObject getResponse(String method, JsonArray params) { break; } - JsonObject txid = httpClient.sendRawTransaction(coin.getTicker(), new String(Hex.encode(transaction.bitcoinSerialize()))); - if (txid == null || txid.has("error") && !txid.get("error").isJsonNull()) { - int code = -1; + JsonObject txid = httpClient.sendRawTransaction(coin.getTicker(), new String(Hex.encode(transaction.bitcoinSerialize()))); + if (txid == null || txid.has("error") && !txid.get("error").isJsonNull()) { + int code = -1; - if (txid != null) - code = txid.get("error").getAsInt(); + if (txid != null) + code = txid.get("error").getAsInt(); - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - errorJSON.addProperty("code", code); - errorJSON.addProperty("message", "Error sending transaction!"); - response.add("error", errorJSON); + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", code); + errorJSON.addProperty("message", "Error sending transaction!"); + response.add("error", errorJSON); - break; - } + break; + } - if (txid.has("result")) { - response.add("result", txid.get("result")); - response.add("error", JsonNull.INSTANCE); + if (txid.has("result")) { + response.add("result", txid.get("result")); + response.add("error", JsonNull.INSTANCE); - break; - } + break; + } response.add("result", txid); response.add("error", JsonNull.INSTANCE); break; - } - case "version": { - response.addProperty("result", Version.CLIENT_VERSION); - response.add("error", JsonNull.INSTANCE); - break; - } - case "validateaddress": { - if (params.size() != 1) { - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - errorJSON.addProperty("code", -1); - errorJSON.addProperty("message", "Usage: validateaddress address\n\naddress (string, required) - The address to validate."); - - response.add("error", errorJSON); - break; - } - - JsonObject resultJSON = new JsonObject(); - String address = params.get(0).getAsString(); - - boolean isValidAddress = Utility.isValidAddress(coin.getNetworkParameters(), address); - - resultJSON.addProperty("isvalid", isValidAddress); - resultJSON.addProperty("address", address); - - boolean isP2SH = false; - String scriptPubKey = ""; - if (isValidAddress) { - Address toAddress = Address.fromBase58(coin.getNetworkParameters(), address); - if (isP2SHAddress(address)) { - isP2SH = true; - - TransactionOutput output = new TransactionOutput(coin.getNetworkParameters(), null, Coin.valueOf(0), toAddress); - scriptPubKey = new String(Hex.encode(output.getScriptPubKey().getProgram())); - } - } - - resultJSON.addProperty("scriptPubKey", scriptPubKey); - resultJSON.addProperty("isscript", isP2SH); - - response.add("result", resultJSON); - response.add("error", JsonNull.INSTANCE); - break; - } - case "help": { - String helpString = "JSON-RPC server for " + CoinTickerUtils.tickerToString(coin.getTicker()) + "\n" - + "This JSON-RPC server is served by " + CoinInstance.getVersionString() + "\n" - + "\n" - + "help - This command help.\n" - + "version - Get version\n" - + "\n=====Blockchain=====\n" - + "gettxout - Get info about an unspent transaction output\n" - + "\n=====Network=====\n" - + "getinfo - Get information such as balances, protocol version, and more.\n" - + "getblockcount - Get block count\n" - + "getnetworkinfo - Get network information\n" - + "getrawmempool - Get raw mempool\n" - + "getblockchaininfo - Get blockchain info\n" - + "getblockhash - Get the hash of a block at a given height\n" - + "getblock - Get a block's JSON representation given its hash\n" - + "\n=====Wallet=====\n" - + "listunspent - Get all UTXOs in the wallet\n" - + "listtransactions - Get all transactions in the wallet\n" - + "getnewaddress - Generate a new address\n" - + "gettransaction - Get a transaction given its TXID\n" - + "getaddressesbyaccount - Get addresses belonging to a given account. The only account available is 'main' which contains all addresses.\n" - + "importprivkey - Import an address given it's privkey\n" - + "dumpprivkey
- Dump an addresses private key\n" - + "\n=====Utilities=====\n" - + "signmessage
- Sign a message with a given address' private key\n" - + "verifymessage
- Verify a signature for a message signed by a given address\n" - + "validateaddress
- Validate a given address\n" - + "sendtransaction
- Create and broadcast a signed transaction to the network\n" - + "\n=====Raw Transactions=====\n" - + "createrawtransaction - Create a raw transaction given inputs and outputs in JSON format. For more info, run createrawtransaction with no arguments.\n" - + "decoderawtransaction - Get a raw transaction's JSON representation\n" - + "signrawtransaction - Sign a raw transaction\n" - + "sendrawtransaction - Broadcast a signed raw transaction to the network\n"; - - response.addProperty("result", helpString); - response.add("error", JsonNull.INSTANCE); - break; - } - default: { - JsonObject methodNotFound = new JsonObject(); - methodNotFound.addProperty("code", -32601); - methodNotFound.addProperty("message", "Method not found."); - response.add("error", methodNotFound); - response.add("result", JsonNull.INSTANCE); - break; - } - } - - return response; - } - - private String canonicalizeASM(String asm) { - return asm - .replaceAll("DUP", "OP_DUP") - .replaceAll("HASH160", "OP_HASH160") - .replaceAll("EQUALVERIFY", "OP_EQUALVERIFY") - .replaceAll("CHECKSIG", "OP_CHECKSIG") - .replaceAll("RETURN", "OP_RETURN") - .replaceAll("PUSHDATA", "") - .replaceAll("\\[", "") - .replaceAll("]", "") - .replaceAll("\\([0-9]+\\)", ""); - } - - private byte[] formatMessageForSigning(String message) { - String header = null; - - switch (coin.getTicker()) { - case BLOCKNET: - case BLOCKNET_TESTNET5: - header = "Blocknet Signed Message:\n"; - break; - case BITCOIN: - // case BITCOIN_CASH: - // header = "Bitcoin Signed Message:\n"; - // break; - case LITECOIN: - header = "Litecoin Signed Message:\n"; - break; - // case ALQOCOIN: - // case PHORECOIN: - case PIVX: - header = "DarkNet Signed Message:\n"; - break; - case DASHCOIN: - header = "DarkCoin Signed Message:\n"; - break; - case UNOBTANIUM: - header = "Unobtanium Signed Message:\n"; - break; - // case DIGIBYTE: - // header = "DigiByte Signed Message:\n"; - // break; - // case BITBAY: - // header = "BitBay Signed Message:\n"; - // break; - // case POLISCOIN: - // header = "Polis Signed Message:\n"; - // break; - // case RAVENCOIN: - // header = "Raven Signed Message:\n"; - // break; - case DOGECOIN: - header = "Dogecoin Signed Message:\n"; - break; - // case TREZARCOIN: - // header = "Trezarcoin Signed Message:\n"; - // break; - case SYSCOIN: - header = "Syscoin Signed Message:\n"; - break; - default: - LOGGER.log(Level.FINER, "[http-server-handler] ERROR: Unsupported coin. This should never happen."); - break; - } - - try { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - byte[] headerBytes = header.getBytes(StandardCharsets.UTF_8); - bos.write(headerBytes.length); - bos.write(headerBytes); - byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8); - VarInt size = new VarInt(messageBytes.length); - bos.write(size.encode()); - bos.write(messageBytes); - return bos.toByteArray(); - } catch (IOException e) { - LOGGER.log(Level.FINER, "[http-server-handler] Error while formatting message for signing!"); - e.printStackTrace(); - } - - return null; - } - - private String signMessage(ECKey key, String message) { - byte[] formatted = formatMessageForSigning(message); - Preconditions.checkNotNull(formatted, "Formatted message is null"); - Sha256Hash hash = Sha256Hash.twiceOf(formatted); - ECKey.ECDSASignature signature = key.sign(hash); - byte recoveryId = -1; - for (int i = 0; i < 4; i++) { - ECKey k = ECKey.recoverFromSignature(i, signature, hash, key.isCompressed()); - if (k != null && Arrays.equals(k.getPubKey(), key.getPubKey())) { - recoveryId = (byte) i; - break; - } - } - if (recoveryId == -1) - throw new IllegalStateException("Recovery ID is invalid."); - - byte[] sigData = new byte[65]; - byte headerByte = (byte) (recoveryId + 27 + (key.isCompressed() ? 4 : 0)); - sigData[0] = headerByte; - System.arraycopy(Utils.bigIntegerToBytes(signature.r, 32), 0, sigData, 1, 32); - System.arraycopy(Utils.bigIntegerToBytes(signature.s, 32), 0, sigData, 33, 32); - - return new String(Base64.encode(sigData)); - } - - private ECKey signedMessageToKey(String signatureB64, String message) throws SignatureException { - byte[] signatureEncoded; - try { - signatureEncoded = Base64.decode(signatureB64); - } catch (RuntimeException e) { - throw new SignatureException("Could not decode base64", e); - } - - if (signatureEncoded.length < 65) - throw new SignatureException("Signature truncated, expected 65 bytes and got " + signatureEncoded.length); - int header = signatureEncoded[0] & 0xFF; - - if (header < 27 || header > 34) - throw new SignatureException("Header byte out of range: " + header); - - BigInteger r = new BigInteger(1, Arrays.copyOfRange(signatureEncoded, 1, 33)); - BigInteger s = new BigInteger(1, Arrays.copyOfRange(signatureEncoded, 33, 65)); - ECKey.ECDSASignature sig = new ECKey.ECDSASignature(r, s); - byte[] messageBytes = formatMessageForSigning(message); - Preconditions.checkNotNull(messageBytes, "Message bytes are null."); - - Sha256Hash messageHash = Sha256Hash.twiceOf(messageBytes); - boolean compressed = false; - if (header >= 31) { - compressed = true; - header -= 4; - } - int recId = header - 27; - ECKey key = ECKey.recoverFromSignature(recId, sig, messageHash, compressed); - if (key == null) - throw new SignatureException("Could not recover public key from signature"); - return key; - } - - private boolean verifyMessage(ECKey key, String signatureB64, String message) { - boolean verified = false; - try { - ECKey k = signedMessageToKey(signatureB64, message); - if (Arrays.equals(k.getPubKey(), key.getPubKey())) - verified = true; - } catch (SignatureException e) { - LOGGER.log(Level.FINER, "[http-server-handler] ERROR: Error while verifying message. Invalid signature?"); - e.printStackTrace(); - } - - return verified; - } - - private boolean isP2SHAddress(String address) { - byte[] versionAndDataBytes = Base58.decodeChecked(address); - int version = versionAndDataBytes[0] & 0xFF; - - if (coin.getNetworkParameters().getAcceptableAddressCodes().length > 2) { - LOGGER.log(Level.FINER, "[http-server-handler] Coin has more than 2 acceptable address codes"); - - for (int t : coin.getNetworkParameters().getAcceptableAddressCodes()) { - if (coin.getNetworkParameters().getAddressHeader() != t && t == version) { - return true; - } - } - } - - return coin.getNetworkParameters().getP2SHHeader() == version; - } - - private void getScriptType(JsonObject scriptPubKey, Script.ScriptType type) { - String typeStr = "unknown"; - - switch (type) { - case P2PKH: - typeStr = "pubkeyhash"; - break; - case P2SH: - typeStr = "scripthash"; - break; - default: - break; - } - - scriptPubKey.addProperty("type", typeStr); - } - - private UTXO getUtxo(Sha256Hash txid, long vout) { - for (AddressBalance addressBalance : coin.getAddressKeyPairs()) { - for (UTXO utxo : addressBalance.getUtxos()) { - if (utxo.createUTXO().getHash().equals(txid) && utxo.getVout() == vout) { - return utxo; - } else { - LOGGER.log(Level.FINER, "[http-server-handler] DEBUG: UTXO " + utxo.createUTXO().getHash().toString() + " does not equal " + txid.toString()); - } - } - } - - return null; - } - - private ECKey getSigningKey(Sha256Hash txid, long vout) { - for (AddressBalance addressBalance : coin.getAddressKeyPairs()) { - for (UTXO utxo : addressBalance.getUtxos()) { - if (utxo.createUTXO().getHash().equals(txid) && utxo.getVout() == vout) { - return addressBalance.getPrivateKey().getKey(); - } else { - LOGGER.log(Level.FINER, "[http-server-handler] DEBUG: UTXO " + utxo.createUTXO().getHash().toString() + " does not equal " + txid.toString()); - } - } - } - - return null; - } - - private void getInvalidTxResponse(JsonObject response, Exception e) { - LOGGER.log(Level.FINER, "[http-server-handler] ERROR: Error while decoding raw tx!"); - e.printStackTrace(); - - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - errorJSON.addProperty("code", -1007); - errorJSON.addProperty("message", "Error decoding raw tx. Invalid transaction?"); - - response.add("error", errorJSON); - } - - private void txConstructionError(JsonObject response, Exception e, String s) { - e.printStackTrace(); - - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - errorJSON.addProperty("code", -1006); - errorJSON.addProperty("message", s); - response.add("error", errorJSON); - } - - private boolean isXRouterConfigInvalid(JsonObject response, BlocknetPeer blocknetPeer) { - if (blocknetPeer.getxRouterConfiguration() == null) { - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - errorJSON.addProperty("code", -1005); - errorJSON.addProperty("message", "Server has not received XRouter configuration from node."); - response.add("error", errorJSON); - - return true; - } - return false; - } - - private void getXRouterResponse(JsonObject response, CountDownLatch latch, AtomicReference xRouterResult, BlocknetPeer blocknetPeer) { - int timeoutPeriod = blocknetPeer.getxRouterConfiguration().getTimeout(); - - try { - if (timeoutPeriod == 0) { - latch.await(10, TimeUnit.SECONDS); - } else { - latch.await(timeoutPeriod, TimeUnit.SECONDS); - } - } catch (InterruptedException e) { - e.printStackTrace(); - } - - if (xRouterResult.get() == null || xRouterResult.get().isEmpty()) { - LOGGER.log(Level.FINER, "[http-server-handler] ERROR: XRouter request timed out or errored! Timeout period = " + timeoutPeriod); - - response.add("result", JsonNull.INSTANCE); - JsonObject errorJSON = new JsonObject(); - errorJSON.addProperty("code", -1004); - errorJSON.addProperty("message", "XRouter request timed out"); - - response.add("error", errorJSON); - } else { - JsonObject xRouterResObj = null; - if (isValidJSON(xRouterResult.get())) { - xRouterResObj = new Gson().fromJson(xRouterResult.get(), JsonObject.class); - } - - if (xRouterResObj != null && xRouterResObj.has("result")) { - if (xRouterResObj.has("code")) - response.add("code", xRouterResObj.get("code")); - if (xRouterResObj.has("error")) - response.add("error", xRouterResObj.get("error")); - response.add("result", xRouterResObj.get("result")); - } else { - if (xRouterResObj != null) { - response.add("result", xRouterResObj); - } else { - response.addProperty("result", xRouterResult.get()); - } - - response.add("error", JsonNull.INSTANCE); - } - } - } - - private boolean isValidJSON(String content) { - try { - new Gson().fromJson(content, JsonObject.class); - } catch (JsonParseException jsExcp) { - try { - new Gson().fromJson(content, JsonArray.class); - } catch (JsonParseException jsExcp1) { - return false; - } - } - return true; - } - - private boolean writeResponse(ChannelHandlerContext ctx, FullHttpResponse httpResponse, FullHttpRequest request) { - boolean keepAlive = false; - if (request != null) - keepAlive = HttpUtil.isKeepAlive(request); - - httpResponse.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json"); - httpResponse.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, httpResponse.content().readableBytes()); - httpResponse.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); - httpResponse.headers().set(HttpHeaderNames.SERVER, CoinInstance.getVersionString()); - - LOGGER.log(Level.FINER, "[http-server-handler] Writing response to channel. Keep alive? " + keepAlive); - LOGGER.log(Level.FINER, "[http-server-handler] Response content: " + httpResponse.content().toString(CharsetUtil.UTF_8)); - ctx.write(httpResponse); - - return keepAlive; - } - - private static void send100Continue(ChannelHandlerContext ctx) { - FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE); - ctx.write(response); - } + } + case "version": { + response.addProperty("result", Version.CLIENT_VERSION); + response.add("error", JsonNull.INSTANCE); + break; + } + case "validateaddress": { + if (params.size() != 1) { + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", -1); + errorJSON.addProperty("message", "Usage: validateaddress address\n\naddress (string, required) - The address to validate."); + + response.add("error", errorJSON); + break; + } + + JsonObject resultJSON = new JsonObject(); + String address = params.get(0).getAsString(); + + boolean isValidAddress = Utility.isValidAddress(coin.getNetworkParameters(), address); + + resultJSON.addProperty("isvalid", isValidAddress); + resultJSON.addProperty("address", address); + + boolean isP2SH = false; + String scriptPubKey = ""; + if (isValidAddress) { + Address toAddress = Address.fromBase58(coin.getNetworkParameters(), address); + if (isP2SHAddress(address)) { + isP2SH = true; + + TransactionOutput output = new TransactionOutput(coin.getNetworkParameters(), null, Coin.valueOf(0), toAddress); + scriptPubKey = new String(Hex.encode(output.getScriptPubKey().getProgram())); + } + } + + resultJSON.addProperty("scriptPubKey", scriptPubKey); + resultJSON.addProperty("isscript", isP2SH); + + response.add("result", resultJSON); + response.add("error", JsonNull.INSTANCE); + break; + } + case "help": { + String helpString = "JSON-RPC server for " + CoinTickerUtils.tickerToString(coin.getTicker()) + "\n" + + "This JSON-RPC server is served by " + CoinInstance.getVersionString() + "\n" + + "\n" + + "help - This command help.\n" + + "version - Get version\n" + + "\n=====Blockchain=====\n" + + "gettxout - Get info about an unspent transaction output\n" + + "\n=====Network=====\n" + + "getinfo - Get information such as balances, protocol version, and more.\n" + + "getblockcount - Get block count\n" + + "getnetworkinfo - Get network information\n" + + "getrawmempool - Get raw mempool\n" + + "getblockchaininfo - Get blockchain info\n" + + "getblockhash - Get the hash of a block at a given height\n" + + "getblock - Get a block's JSON representation given its hash\n" + + "\n=====Wallet=====\n" + + "listunspent - Get all UTXOs in the wallet\n" + + "listtransactions - Get all transactions in the wallet\n" + + "getnewaddress - Generate a new address\n" + + "gettransaction - Get a transaction given its TXID\n" + + "getaddressesbyaccount - Get addresses belonging to a given account. The only account available is 'main' which contains all addresses.\n" + + "importprivkey - Import an address given it's privkey\n" + + "dumpprivkey
- Dump an addresses private key\n" + + "\n=====Utilities=====\n" + + "signmessage
- Sign a message with a given address' private key\n" + + "verifymessage
- Verify a signature for a message signed by a given address\n" + + "validateaddress
- Validate a given address\n" + + "sendtransaction
- Create and broadcast a signed transaction to the network\n" + + "\n=====Raw Transactions=====\n" + + "createrawtransaction - Create a raw transaction given inputs and outputs in JSON format. For more info, run createrawtransaction with no arguments.\n" + + "decoderawtransaction - Get a raw transaction's JSON representation\n" + + "signrawtransaction - Sign a raw transaction\n" + + "sendrawtransaction - Broadcast a signed raw transaction to the network\n"; + + response.addProperty("result", helpString); + response.add("error", JsonNull.INSTANCE); + break; + } + default: { + JsonObject methodNotFound = new JsonObject(); + methodNotFound.addProperty("code", -32601); + methodNotFound.addProperty("message", "Method not found."); + response.add("error", methodNotFound); + response.add("result", JsonNull.INSTANCE); + break; + } + } + + return response; + } + + private String canonicalizeASM(String asm) { + return asm + .replaceAll("DUP", "OP_DUP") + .replaceAll("HASH160", "OP_HASH160") + .replaceAll("EQUALVERIFY", "OP_EQUALVERIFY") + .replaceAll("CHECKSIG", "OP_CHECKSIG") + .replaceAll("RETURN", "OP_RETURN") + .replaceAll("PUSHDATA", "") + .replaceAll("\\[", "") + .replaceAll("]", "") + .replaceAll("\\([0-9]+\\)", ""); + } + + private byte[] formatMessageForSigning(String message) { + String header = null; + + switch (coin.getTicker()) { + case BLOCKNET: + case BLOCKNET_TESTNET5: + header = "Blocknet Signed Message:\n"; + break; + case BITCOIN: + // case BITCOIN_CASH: + // header = "Bitcoin Signed Message:\n"; + // break; + case LITECOIN: + header = "Litecoin Signed Message:\n"; + break; + // case ALQOCOIN: + // case PHORECOIN: + case PIVX: + header = "DarkNet Signed Message:\n"; + break; + case DASHCOIN: + header = "DarkCoin Signed Message:\n"; + break; + case UNOBTANIUM: + header = "Unobtanium Signed Message:\n"; + break; + // case DIGIBYTE: + // header = "DigiByte Signed Message:\n"; + // break; + // case BITBAY: + // header = "BitBay Signed Message:\n"; + // break; + // case POLISCOIN: + // header = "Polis Signed Message:\n"; + // break; + // case RAVENCOIN: + // header = "Raven Signed Message:\n"; + // break; + case DOGECOIN: + header = "Dogecoin Signed Message:\n"; + break; + // case TREZARCOIN: + // header = "Trezarcoin Signed Message:\n"; + // break; + case SYSCOIN: + header = "Syscoin Signed Message:\n"; + break; + default: + LOGGER.log(Level.FINER, "[http-server-handler] ERROR: Unsupported coin. This should never happen."); + break; + } + + try { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + byte[] headerBytes = header.getBytes(StandardCharsets.UTF_8); + bos.write(headerBytes.length); + bos.write(headerBytes); + byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8); + VarInt size = new VarInt(messageBytes.length); + bos.write(size.encode()); + bos.write(messageBytes); + return bos.toByteArray(); + } catch (IOException e) { + LOGGER.log(Level.FINER, "[http-server-handler] Error while formatting message for signing!"); + e.printStackTrace(); + } + + return null; + } + + private String signMessage(ECKey key, String message) { + byte[] formatted = formatMessageForSigning(message); + Preconditions.checkNotNull(formatted, "Formatted message is null"); + Sha256Hash hash = Sha256Hash.twiceOf(formatted); + ECKey.ECDSASignature signature = key.sign(hash); + byte recoveryId = -1; + for (int i = 0; i < 4; i++) { + ECKey k = ECKey.recoverFromSignature(i, signature, hash, key.isCompressed()); + if (k != null && Arrays.equals(k.getPubKey(), key.getPubKey())) { + recoveryId = (byte) i; + break; + } + } + if (recoveryId == -1) + throw new IllegalStateException("Recovery ID is invalid."); + + byte[] sigData = new byte[65]; + byte headerByte = (byte) (recoveryId + 27 + (key.isCompressed() ? 4 : 0)); + sigData[0] = headerByte; + System.arraycopy(Utils.bigIntegerToBytes(signature.r, 32), 0, sigData, 1, 32); + System.arraycopy(Utils.bigIntegerToBytes(signature.s, 32), 0, sigData, 33, 32); + + return new String(Base64.encode(sigData)); + } + + private ECKey signedMessageToKey(String signatureB64, String message) throws SignatureException { + byte[] signatureEncoded; + try { + signatureEncoded = Base64.decode(signatureB64); + } catch (RuntimeException e) { + throw new SignatureException("Could not decode base64", e); + } + + if (signatureEncoded.length < 65) + throw new SignatureException("Signature truncated, expected 65 bytes and got " + signatureEncoded.length); + int header = signatureEncoded[0] & 0xFF; + + if (header < 27 || header > 34) + throw new SignatureException("Header byte out of range: " + header); + + BigInteger r = new BigInteger(1, Arrays.copyOfRange(signatureEncoded, 1, 33)); + BigInteger s = new BigInteger(1, Arrays.copyOfRange(signatureEncoded, 33, 65)); + ECKey.ECDSASignature sig = new ECKey.ECDSASignature(r, s); + byte[] messageBytes = formatMessageForSigning(message); + Preconditions.checkNotNull(messageBytes, "Message bytes are null."); + + Sha256Hash messageHash = Sha256Hash.twiceOf(messageBytes); + boolean compressed = false; + if (header >= 31) { + compressed = true; + header -= 4; + } + int recId = header - 27; + ECKey key = ECKey.recoverFromSignature(recId, sig, messageHash, compressed); + if (key == null) + throw new SignatureException("Could not recover public key from signature"); + return key; + } + + private boolean verifyMessage(ECKey key, String signatureB64, String message) { + boolean verified = false; + try { + ECKey k = signedMessageToKey(signatureB64, message); + if (Arrays.equals(k.getPubKey(), key.getPubKey())) + verified = true; + } catch (SignatureException e) { + LOGGER.log(Level.FINER, "[http-server-handler] ERROR: Error while verifying message. Invalid signature?"); + e.printStackTrace(); + } + + return verified; + } + + private boolean isP2SHAddress(String address) { + byte[] versionAndDataBytes = Base58.decodeChecked(address); + int version = versionAndDataBytes[0] & 0xFF; + + if (coin.getNetworkParameters().getAcceptableAddressCodes().length > 2) { + LOGGER.log(Level.FINER, "[http-server-handler] Coin has more than 2 acceptable address codes"); + + for (int t : coin.getNetworkParameters().getAcceptableAddressCodes()) { + if (coin.getNetworkParameters().getAddressHeader() != t && t == version) { + return true; + } + } + } + + return coin.getNetworkParameters().getP2SHHeader() == version; + } + + private void getScriptType(JsonObject scriptPubKey, Script.ScriptType type) { + String typeStr = "unknown"; + + switch (type) { + case P2PKH: + typeStr = "pubkeyhash"; + break; + case P2SH: + typeStr = "scripthash"; + break; + default: + break; + } + + scriptPubKey.addProperty("type", typeStr); + } + + private UTXO getUtxo(Sha256Hash txid, long vout) { + for (AddressBalance addressBalance : coin.getAddressKeyPairs()) { + for (UTXO utxo : addressBalance.getUtxos()) { + if (utxo.createUTXO().getHash().equals(txid) && utxo.getVout() == vout) { + return utxo; + } else { + LOGGER.log(Level.FINER, "[http-server-handler] DEBUG: UTXO " + utxo.createUTXO().getHash().toString() + " does not equal " + txid.toString()); + } + } + } + + return null; + } + + private ECKey getSigningKey(Sha256Hash txid, long vout) { + for (AddressBalance addressBalance : coin.getAddressKeyPairs()) { + for (UTXO utxo : addressBalance.getUtxos()) { + if (utxo.createUTXO().getHash().equals(txid) && utxo.getVout() == vout) { + return addressBalance.getPrivateKey().getKey(); + } else { + LOGGER.log(Level.FINER, "[http-server-handler] DEBUG: UTXO " + utxo.createUTXO().getHash().toString() + " does not equal " + txid.toString()); + } + } + } + + return null; + } + + private void getInvalidTxResponse(JsonObject response, Exception e) { + LOGGER.log(Level.FINER, "[http-server-handler] ERROR: Error while decoding raw tx!"); + e.printStackTrace(); + + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", -1007); + errorJSON.addProperty("message", "Error decoding raw tx. Invalid transaction?"); + + response.add("error", errorJSON); + } + + private void txConstructionError(JsonObject response, Exception e, String s) { + e.printStackTrace(); + + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", -1006); + errorJSON.addProperty("message", s); + response.add("error", errorJSON); + } + + private boolean isXRouterConfigInvalid(JsonObject response, BlocknetPeer blocknetPeer) { + if (blocknetPeer.getxRouterConfiguration() == null) { + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", -1005); + errorJSON.addProperty("message", "Server has not received XRouter configuration from node."); + response.add("error", errorJSON); + + return true; + } + return false; + } + + private void getXRouterResponse(JsonObject response, CountDownLatch latch, AtomicReference xRouterResult, BlocknetPeer blocknetPeer) { + int timeoutPeriod = blocknetPeer.getxRouterConfiguration().getTimeout(); + + try { + if (timeoutPeriod == 0) { + latch.await(10, TimeUnit.SECONDS); + } else { + latch.await(timeoutPeriod, TimeUnit.SECONDS); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + + if (xRouterResult.get() == null || xRouterResult.get().isEmpty()) { + LOGGER.log(Level.FINER, "[http-server-handler] ERROR: XRouter request timed out or errored! Timeout period = " + timeoutPeriod); + + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", -1004); + errorJSON.addProperty("message", "XRouter request timed out"); + + response.add("error", errorJSON); + } else { + JsonObject xRouterResObj = null; + if (isValidJSON(xRouterResult.get())) { + xRouterResObj = new Gson().fromJson(xRouterResult.get(), JsonObject.class); + } + + if (xRouterResObj != null && xRouterResObj.has("result")) { + if (xRouterResObj.has("code")) + response.add("code", xRouterResObj.get("code")); + if (xRouterResObj.has("error")) + response.add("error", xRouterResObj.get("error")); + response.add("result", xRouterResObj.get("result")); + } else { + if (xRouterResObj != null) { + response.add("result", xRouterResObj); + } else { + response.addProperty("result", xRouterResult.get()); + } + + response.add("error", JsonNull.INSTANCE); + } + } + } + + private boolean isValidJSON(String content) { + try { + new Gson().fromJson(content, JsonObject.class); + } catch (JsonParseException jsExcp) { + try { + new Gson().fromJson(content, JsonArray.class); + } catch (JsonParseException jsExcp1) { + return false; + } + } + return true; + } + + private boolean writeResponse(ChannelHandlerContext ctx, FullHttpResponse httpResponse, FullHttpRequest request) { + boolean keepAlive = false; + if (request != null) + keepAlive = HttpUtil.isKeepAlive(request); + + httpResponse.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json"); + httpResponse.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, httpResponse.content().readableBytes()); + httpResponse.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); + httpResponse.headers().set(HttpHeaderNames.SERVER, CoinInstance.getVersionString()); + + LOGGER.log(Level.FINER, "[http-server-handler] Writing response to channel. Keep alive? " + keepAlive); + LOGGER.log(Level.FINER, "[http-server-handler] Response content: " + httpResponse.content().toString(CharsetUtil.UTF_8)); + ctx.write(httpResponse); + + return keepAlive; + } + + private static void send100Continue(ChannelHandlerContext ctx) { + FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE); + ctx.write(response); + } } diff --git a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerInitializer.java b/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerInitializer.java index c485f2c..d4f95fd 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerInitializer.java +++ b/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerInitializer.java @@ -12,23 +12,23 @@ public class HTTPServerInitializer extends ChannelInitializer { - private CoinInstance coin; + private CoinInstance coin; - public HTTPServerInitializer(CoinInstance coin) { - this.coin = coin; - } + public HTTPServerInitializer(CoinInstance coin) { + this.coin = coin; + } - @Override - protected void initChannel(SocketChannel ch) { - ChannelPipeline pipeline = ch.pipeline(); + @Override + protected void initChannel(SocketChannel ch) { + ChannelPipeline pipeline = ch.pipeline(); - pipeline.addLast(new WriteTimeoutHandler(30)); - pipeline.addLast(new ReadTimeoutHandler(30)); - pipeline.addLast(new HttpRequestDecoder()); - pipeline.addLast(new HttpResponseEncoder()); - pipeline.addLast(new HttpObjectAggregator(100000000)); - pipeline.addLast(new HTTPServerHandler(coin)); - pipeline.addLast(new ExceptionHandler()); - } + pipeline.addLast(new WriteTimeoutHandler(30)); + pipeline.addLast(new ReadTimeoutHandler(30)); + pipeline.addLast(new HttpRequestDecoder()); + pipeline.addLast(new HttpResponseEncoder()); + pipeline.addLast(new HttpObjectAggregator(100000000)); + pipeline.addLast(new HTTPServerHandler(coin)); + pipeline.addLast(new ExceptionHandler()); + } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/alqocoin/AlqocoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/alqocoin/AlqocoinNetworkParameters.java index a0ca792..3dc95b2 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/alqocoin/AlqocoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/alqocoin/AlqocoinNetworkParameters.java @@ -7,95 +7,96 @@ public class AlqocoinNetworkParameters extends NetworkParameters { - public AlqocoinNetworkParameters() { - super(); - } - - @Override - public String getPaymentProtocolId() { - return "main"; - } - - @Override - public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException {} - - @Override - public Coin getMaxMoney() { - return Coin.valueOf(100000000 * Coin.COIN.value); - } - - @Override - public Coin getMinNonDustOutput() { - return Transaction.MIN_NONDUST_OUTPUT; - } - - @Override - public MonetaryFormat getMonetaryFormat() { - return new MonetaryFormat().code(0, "XLQ"); - } - - @Override - public String getUriScheme() { - return "alqocoin:"; - } - - @Override - public boolean hasMaxMoney() { - return true; - } - - @Override - public BitcoinSerializer getSerializer(boolean parseRetain) { - return new BitcoinSerializer(this, parseRetain); - } - - @Override - public int getProtocolVersionNum(ProtocolVersion version) { - return 70719; - } - - @Override - public int getAddressHeader() { - return 23; - } - - @Override - public int getP2SHHeader() { - return 16; - } - - @Override - public int getDumpedPrivateKeyHeader() { - return 193; - } - - @Override - public int[] getAcceptableAddressCodes() { - return new int[] {getAddressHeader(), getP2SHHeader()}; - } - - @Override - public int getBip32HeaderPriv() { - return 0x0488ADE4; - } - - @Override - public int getBip32HeaderPub() { - return 0x0488B21E; - } - - @Override - public int getSubsidyDecreaseBlockCount() { - return 210240; - } - - @Override - public int getInterval() { - return 60; - } - - @Override - public String getId() { - return "XLQ"; - } + public AlqocoinNetworkParameters() { + super(); + } + + @Override + public String getPaymentProtocolId() { + return "main"; + } + + @Override + public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException { + } + + @Override + public Coin getMaxMoney() { + return Coin.valueOf(100000000 * Coin.COIN.value); + } + + @Override + public Coin getMinNonDustOutput() { + return Transaction.MIN_NONDUST_OUTPUT; + } + + @Override + public MonetaryFormat getMonetaryFormat() { + return new MonetaryFormat().code(0, "XLQ"); + } + + @Override + public String getUriScheme() { + return "alqocoin:"; + } + + @Override + public boolean hasMaxMoney() { + return true; + } + + @Override + public BitcoinSerializer getSerializer(boolean parseRetain) { + return new BitcoinSerializer(this, parseRetain); + } + + @Override + public int getProtocolVersionNum(ProtocolVersion version) { + return 70719; + } + + @Override + public int getAddressHeader() { + return 23; + } + + @Override + public int getP2SHHeader() { + return 16; + } + + @Override + public int getDumpedPrivateKeyHeader() { + return 193; + } + + @Override + public int[] getAcceptableAddressCodes() { + return new int[]{getAddressHeader(), getP2SHHeader()}; + } + + @Override + public int getBip32HeaderPriv() { + return 0x0488ADE4; + } + + @Override + public int getBip32HeaderPub() { + return 0x0488B21E; + } + + @Override + public int getSubsidyDecreaseBlockCount() { + return 210240; + } + + @Override + public int getInterval() { + return 60; + } + + @Override + public String getId() { + return "XLQ"; + } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/bitbay/BitbayNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/bitbay/BitbayNetworkParameters.java index b76e004..df6057b 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/bitbay/BitbayNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/bitbay/BitbayNetworkParameters.java @@ -7,95 +7,96 @@ public class BitbayNetworkParameters extends NetworkParameters { - public BitbayNetworkParameters() { - super(); - } - - @Override - public String getPaymentProtocolId() { - return "main"; - } - - @Override - public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException {} - - @Override - public Coin getMaxMoney() { - return Coin.valueOf(100000000 * Coin.COIN.value); - } - - @Override - public Coin getMinNonDustOutput() { - return Transaction.MIN_NONDUST_OUTPUT; - } - - @Override - public MonetaryFormat getMonetaryFormat() { - return new MonetaryFormat().code(0, "BAY"); - } - - @Override - public String getUriScheme() { - return "bitbay:"; - } - - @Override - public boolean hasMaxMoney() { - return true; - } - - @Override - public BitcoinSerializer getSerializer(boolean parseRetain) { - return new BitcoinSerializer(this, parseRetain); - } - - @Override - public int getProtocolVersionNum(ProtocolVersion version) { - return 70719; - } - - @Override - public int getAddressHeader() { - return 25; - } - - @Override - public int getP2SHHeader() { - return 85; - } - - @Override - public int getDumpedPrivateKeyHeader() { - return 153; - } - - @Override - public int[] getAcceptableAddressCodes() { - return new int[] {getAddressHeader(), getP2SHHeader()}; - } - - @Override - public int getBip32HeaderPriv() { - return 0x0488ADE4; - } - - @Override - public int getBip32HeaderPub() { - return 0x0488B21E; - } - - @Override - public int getSubsidyDecreaseBlockCount() { - return 210240; - } - - @Override - public int getInterval() { - return 60; - } - - @Override - public String getId() { - return "BAY"; - } + public BitbayNetworkParameters() { + super(); + } + + @Override + public String getPaymentProtocolId() { + return "main"; + } + + @Override + public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException { + } + + @Override + public Coin getMaxMoney() { + return Coin.valueOf(100000000 * Coin.COIN.value); + } + + @Override + public Coin getMinNonDustOutput() { + return Transaction.MIN_NONDUST_OUTPUT; + } + + @Override + public MonetaryFormat getMonetaryFormat() { + return new MonetaryFormat().code(0, "BAY"); + } + + @Override + public String getUriScheme() { + return "bitbay:"; + } + + @Override + public boolean hasMaxMoney() { + return true; + } + + @Override + public BitcoinSerializer getSerializer(boolean parseRetain) { + return new BitcoinSerializer(this, parseRetain); + } + + @Override + public int getProtocolVersionNum(ProtocolVersion version) { + return 70719; + } + + @Override + public int getAddressHeader() { + return 25; + } + + @Override + public int getP2SHHeader() { + return 85; + } + + @Override + public int getDumpedPrivateKeyHeader() { + return 153; + } + + @Override + public int[] getAcceptableAddressCodes() { + return new int[]{getAddressHeader(), getP2SHHeader()}; + } + + @Override + public int getBip32HeaderPriv() { + return 0x0488ADE4; + } + + @Override + public int getBip32HeaderPub() { + return 0x0488B21E; + } + + @Override + public int getSubsidyDecreaseBlockCount() { + return 210240; + } + + @Override + public int getInterval() { + return 60; + } + + @Override + public String getId() { + return "BAY"; + } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/bitcoincash/BitcoinCashNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/bitcoincash/BitcoinCashNetworkParameters.java index e49ac66..83c1142 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/bitcoincash/BitcoinCashNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/bitcoincash/BitcoinCashNetworkParameters.java @@ -7,95 +7,96 @@ public class BitcoinCashNetworkParameters extends NetworkParameters { - public BitcoinCashNetworkParameters() { - super(); - } - - @Override - public String getPaymentProtocolId() { - return PAYMENT_PROTOCOL_ID_MAINNET; - } - - @Override - public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException {} - - @Override - public Coin getMaxMoney() { - return MAX_MONEY; - } - - @Override - public Coin getMinNonDustOutput() { - return Transaction.MIN_NONDUST_OUTPUT; - } - - @Override - public MonetaryFormat getMonetaryFormat() { - return new MonetaryFormat().code(0, "BCH"); - } - - @Override - public String getUriScheme() { - return "bch:"; - } - - @Override - public boolean hasMaxMoney() { - return true; - } - - @Override - public BitcoinSerializer getSerializer(boolean parseRetain) { - return new BitcoinSerializer(this, parseRetain); - } - - @Override - public int getProtocolVersionNum(final ProtocolVersion version) { - return version.getBitcoinProtocolVersion(); - } - - @Override - public int getAddressHeader() { - return 0; - } - - @Override - public int getP2SHHeader() { - return 5; - } - - @Override - public int getDumpedPrivateKeyHeader() { - return 128; - } - - @Override - public int[] getAcceptableAddressCodes() { - return new int[] {getAddressHeader(), getP2SHHeader()}; - } - - @Override - public int getBip32HeaderPriv() { - return 0x0488ADE4; - } - - @Override - public int getBip32HeaderPub() { - return 0x0488B21E; - } - - @Override - public int getSubsidyDecreaseBlockCount() { - return 210240; - } - - @Override - public int getInterval() { - return INTERVAL; - } - - @Override - public String getId() { - return "BCH"; - } + public BitcoinCashNetworkParameters() { + super(); + } + + @Override + public String getPaymentProtocolId() { + return PAYMENT_PROTOCOL_ID_MAINNET; + } + + @Override + public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException { + } + + @Override + public Coin getMaxMoney() { + return MAX_MONEY; + } + + @Override + public Coin getMinNonDustOutput() { + return Transaction.MIN_NONDUST_OUTPUT; + } + + @Override + public MonetaryFormat getMonetaryFormat() { + return new MonetaryFormat().code(0, "BCH"); + } + + @Override + public String getUriScheme() { + return "bch:"; + } + + @Override + public boolean hasMaxMoney() { + return true; + } + + @Override + public BitcoinSerializer getSerializer(boolean parseRetain) { + return new BitcoinSerializer(this, parseRetain); + } + + @Override + public int getProtocolVersionNum(final ProtocolVersion version) { + return version.getBitcoinProtocolVersion(); + } + + @Override + public int getAddressHeader() { + return 0; + } + + @Override + public int getP2SHHeader() { + return 5; + } + + @Override + public int getDumpedPrivateKeyHeader() { + return 128; + } + + @Override + public int[] getAcceptableAddressCodes() { + return new int[]{getAddressHeader(), getP2SHHeader()}; + } + + @Override + public int getBip32HeaderPriv() { + return 0x0488ADE4; + } + + @Override + public int getBip32HeaderPub() { + return 0x0488B21E; + } + + @Override + public int getSubsidyDecreaseBlockCount() { + return 210240; + } + + @Override + public int getInterval() { + return INTERVAL; + } + + @Override + public String getId() { + return "BCH"; + } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetBlockingClient.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetBlockingClient.java index 4c799b6..329575a 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetBlockingClient.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetBlockingClient.java @@ -19,106 +19,106 @@ import java.util.logging.Logger; public class BlocknetBlockingClient implements MessageWriteTarget { - private final static LogManager LOGMANAGER = LogManager.getLogManager(); - private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); + private final static LogManager LOGMANAGER = LogManager.getLogManager(); + private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); - private static final int BUFFER_SIZE_LOWER_BOUND = 4096; - private static final int BUFFER_SIZE_UPPER_BOUND = 65536; + private static final int BUFFER_SIZE_LOWER_BOUND = 4096; + private static final int BUFFER_SIZE_UPPER_BOUND = 65536; - private Socket socket; - private volatile boolean closeRequested = false; - private SettableFuture connectFuture; + private Socket socket; + private volatile boolean closeRequested = false; + private SettableFuture connectFuture; - public BlocknetBlockingClient(SocketAddress serverAddress, StreamConnection connection, int connectTimeoutMillis, SocketFactory socketFactory, @Nullable Set clientSet) throws IOException { - connectFuture = SettableFuture.create(); + public BlocknetBlockingClient(SocketAddress serverAddress, StreamConnection connection, int connectTimeoutMillis, SocketFactory socketFactory, @Nullable Set clientSet) throws IOException { + connectFuture = SettableFuture.create(); - connection.setWriteTarget(this); - socket = socketFactory.createSocket(); - final Context context = Context.get(); - Thread t = new Thread(() -> { - Context.propagate(context); - if (clientSet != null) - clientSet.add(BlocknetBlockingClient.this); - try { - socket.connect(serverAddress, connectTimeoutMillis); - connection.connectionOpened(); - connectFuture.set(serverAddress); - InputStream stream = socket.getInputStream(); - runReadLoop(stream, connection); - } catch (Exception e) { - if (!closeRequested) { - LOGGER.log(Level.FINER, "[blocknet-blocking-client] Error trying to open/read from connection with " + serverAddress.toString() + "."); - e.printStackTrace(); - connectFuture.setException(e); - } - } finally { - try { - if (!socket.isClosed()) - socket.close(); - } catch (IOException e1) { - // At this point there isn't much we can do, and we can probably assume the channel is closed - } - if (clientSet != null) - clientSet.remove(BlocknetBlockingClient.this); - connection.connectionClosed(); - } - }); - t.setName("Blocknet network thread - " + serverAddress); - t.setDaemon(true); - t.start(); - } + connection.setWriteTarget(this); + socket = socketFactory.createSocket(); + final Context context = Context.get(); + Thread t = new Thread(() -> { + Context.propagate(context); + if (clientSet != null) + clientSet.add(BlocknetBlockingClient.this); + try { + socket.connect(serverAddress, connectTimeoutMillis); + connection.connectionOpened(); + connectFuture.set(serverAddress); + InputStream stream = socket.getInputStream(); + runReadLoop(stream, connection); + } catch (Exception e) { + if (!closeRequested) { + LOGGER.log(Level.FINER, "[blocknet-blocking-client] Error trying to open/read from connection with " + serverAddress.toString() + "."); + e.printStackTrace(); + connectFuture.setException(e); + } + } finally { + try { + if (!socket.isClosed()) + socket.close(); + } catch (IOException e1) { + // At this point there isn't much we can do, and we can probably assume the channel is closed + } + if (clientSet != null) + clientSet.remove(BlocknetBlockingClient.this); + connection.connectionClosed(); + } + }); + t.setName("Blocknet network thread - " + serverAddress); + t.setDaemon(true); + t.start(); + } - private static void runReadLoop(InputStream inputStream, StreamConnection connection) throws Exception { - ByteBuffer dbuf = ByteBuffer.allocateDirect(Math.min(Math.max(connection.getMaxMessageSize(), BUFFER_SIZE_LOWER_BOUND), BUFFER_SIZE_UPPER_BOUND)); - byte[] readBuff = new byte[dbuf.capacity()]; + private static void runReadLoop(InputStream inputStream, StreamConnection connection) throws Exception { + ByteBuffer dbuf = ByteBuffer.allocateDirect(Math.min(Math.max(connection.getMaxMessageSize(), BUFFER_SIZE_LOWER_BOUND), BUFFER_SIZE_UPPER_BOUND)); + byte[] readBuff = new byte[dbuf.capacity()]; - while (true) { - // TODO Kill the message duplication here - if (!(dbuf.remaining() > 0 && dbuf.remaining() <= readBuff.length)) { - throw new IllegalStateException(); - } - int read = inputStream.read(readBuff, 0, Math.max(1, Math.min(dbuf.remaining(), inputStream.available()))); - if (read == -1) - return; - dbuf.put(readBuff, 0, read); + while (true) { + // TODO Kill the message duplication here + if (!(dbuf.remaining() > 0 && dbuf.remaining() <= readBuff.length)) { + throw new IllegalStateException(); + } + int read = inputStream.read(readBuff, 0, Math.max(1, Math.min(dbuf.remaining(), inputStream.available()))); + if (read == -1) + return; + dbuf.put(readBuff, 0, read); - dbuf.flip(); + dbuf.flip(); - int bytesConsumed = connection.receiveBytes(dbuf); - if (dbuf.position() != bytesConsumed) { - throw new IllegalStateException("Buffer did not stop reading at the correct location."); - } + int bytesConsumed = connection.receiveBytes(dbuf); + if (dbuf.position() != bytesConsumed) { + throw new IllegalStateException("Buffer did not stop reading at the correct location."); + } - dbuf.compact(); - } - } + dbuf.compact(); + } + } - @Override - public void closeConnection() { - try { - closeRequested = true; - socket.close(); - } catch (IOException e) { - LOGGER.log(Level.FINER, "[blocknet-blocking-client] Error while closing socket!"); - e.printStackTrace(); - } - } + @Override + public void closeConnection() { + try { + closeRequested = true; + socket.close(); + } catch (IOException e) { + LOGGER.log(Level.FINER, "[blocknet-blocking-client] Error while closing socket!"); + e.printStackTrace(); + } + } - @Override - public synchronized void writeBytes(byte[] bytes) throws IOException { - try { - OutputStream out = socket.getOutputStream(); - out.write(bytes); - out.flush(); - } catch (IOException e) { - LOGGER.log(Level.FINER, "[blocknet-blocking-client] Error while writing bytes to socket!"); - e.printStackTrace(); - closeConnection(); - throw e; - } - } + @Override + public synchronized void writeBytes(byte[] bytes) throws IOException { + try { + OutputStream out = socket.getOutputStream(); + out.write(bytes); + out.flush(); + } catch (IOException e) { + LOGGER.log(Level.FINER, "[blocknet-blocking-client] Error while writing bytes to socket!"); + e.printStackTrace(); + closeConnection(); + throw e; + } + } - public SettableFuture getConnectFuture() { - return connectFuture; - } + public SettableFuture getConnectFuture() { + return connectFuture; + } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetBlockingClientManager.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetBlockingClientManager.java index 3f25589..0457031 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetBlockingClientManager.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetBlockingClientManager.java @@ -45,7 +45,8 @@ public void setConnectTimeoutMillis(int connectTimeoutMillis) { } @Override - protected void startUp() throws Exception { } + protected void startUp() throws Exception { + } @Override protected void shutDown() throws Exception { diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetNetworkParameters.java index efd8f9e..d57b726 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetNetworkParameters.java @@ -11,165 +11,165 @@ public class BlocknetNetworkParameters extends BlocknetParameters { - public BlocknetNetworkParameters() { - super(); - } - - @Override - public String getPaymentProtocolId() { - return "main"; - } - - @Override - public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException { - - } - - @Override - public int[] getAcceptableAddressCodes() { - return new int[] {getAddressHeader(), getP2SHHeader()}; - } - - @Override - public Sha256Hash getGenesisBlockHash() { - return Sha256Hash.wrap("00000eb7919102da5a07dc90905651664e6ebf0811c28f06573b9a0fd84ab7b8"); - } - - @Override - public Coin getMaxMoney() { - return Coin.valueOf(43199500).times(Coin.COIN.value); - } - - @Override - public Coin getMinNonDustOutput() { - return Coin.valueOf(5500); - } - - @Override - public MonetaryFormat getMonetaryFormat() { - return new MonetaryFormat().code(0, "BLOCK"); - } - - @Override - public String getUriScheme() { - return "blocknetdx:"; - } - - @Override - public boolean hasMaxMoney() { - return true; - } - - @Override - public BlocknetSerializer getSerializer(boolean parseRetain) { - return new BlocknetSerializer(this, parseRetain); - } - - @Override - public int getProtocolVersionNum(ProtocolVersion version) { - return 70712; - } - - /*@Override - public Block getGenesisBlock() { - return genesisBlock; - }*/ - - @Override - public int getSubsidyDecreaseBlockCount() { - return 210000; - } - - @Override - public byte[] getAlertSigningKey() { - return Hex.decode("0415758705177c87c35dadf7ebf66e93ecc2710253bbac955e695664011fa39ff29a84fa21ae9e203a43debb487170c143ab6eaffe4fa3b12e162d8a6d4da87395"); - } - - @Override - public int getMajorityEnforceBlockUpgrade() { - return 750; - } - - @Override - public int getMajorityRejectBlockOutdated() { - return 950; - } - - @Override - public int getMajorityWindow() { - return 1000; - } - - @Override - public int getPort() { - return 41412; - } - - @Override - public long getPacketMagic() { - return 0xA1A0A2A3; - } - - @Override - public String[] getDnsSeeds() { - int nodeCount = 5; - - String[] dnsSeeds = new String[nodeCount]; - - for (int i = 0; i < nodeCount; i++) - dnsSeeds[i] = "node-" + i + ".cloudchainsinc.com"; - - return dnsSeeds; - } - - @Override - public int getInterval() { - return 1; - } - - @Override - public int getAddressHeader() { - return 26; - } - - @Override - public int getP2SHHeader() { - return 28; - } - - @Override - public int getDumpedPrivateKeyHeader() { - return 154; - } - - @Override - public int getBip32HeaderPub() { - return 0x0488B21E; - } - - @Override - public int getBip32HeaderPriv() { - return 0x0488ADE4; - } - - @Override - public BigInteger getMaxTarget() { - return Utils.decodeCompactBits(0x1E0FFFFF); - } - - @Override - public int getTargetTimespan() { - return 60; - } - - @Override - public String getId() { - return "BLOCK"; - } - - @Override - public XRouterMessageSerializer getXRouterMessageSerializer(boolean parseRetain) { - return new XRouterMessageSerializer(parseRetain, this); - } + public BlocknetNetworkParameters() { + super(); + } + + @Override + public String getPaymentProtocolId() { + return "main"; + } + + @Override + public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException { + + } + + @Override + public int[] getAcceptableAddressCodes() { + return new int[]{getAddressHeader(), getP2SHHeader()}; + } + + @Override + public Sha256Hash getGenesisBlockHash() { + return Sha256Hash.wrap("00000eb7919102da5a07dc90905651664e6ebf0811c28f06573b9a0fd84ab7b8"); + } + + @Override + public Coin getMaxMoney() { + return Coin.valueOf(43199500).times(Coin.COIN.value); + } + + @Override + public Coin getMinNonDustOutput() { + return Coin.valueOf(5500); + } + + @Override + public MonetaryFormat getMonetaryFormat() { + return new MonetaryFormat().code(0, "BLOCK"); + } + + @Override + public String getUriScheme() { + return "blocknetdx:"; + } + + @Override + public boolean hasMaxMoney() { + return true; + } + + @Override + public BlocknetSerializer getSerializer(boolean parseRetain) { + return new BlocknetSerializer(this, parseRetain); + } + + @Override + public int getProtocolVersionNum(ProtocolVersion version) { + return 70712; + } + + /*@Override + public Block getGenesisBlock() { + return genesisBlock; + }*/ + + @Override + public int getSubsidyDecreaseBlockCount() { + return 210000; + } + + @Override + public byte[] getAlertSigningKey() { + return Hex.decode("0415758705177c87c35dadf7ebf66e93ecc2710253bbac955e695664011fa39ff29a84fa21ae9e203a43debb487170c143ab6eaffe4fa3b12e162d8a6d4da87395"); + } + + @Override + public int getMajorityEnforceBlockUpgrade() { + return 750; + } + + @Override + public int getMajorityRejectBlockOutdated() { + return 950; + } + + @Override + public int getMajorityWindow() { + return 1000; + } + + @Override + public int getPort() { + return 41412; + } + + @Override + public long getPacketMagic() { + return 0xA1A0A2A3; + } + + @Override + public String[] getDnsSeeds() { + int nodeCount = 5; + + String[] dnsSeeds = new String[nodeCount]; + + for (int i = 0; i < nodeCount; i++) + dnsSeeds[i] = "node-" + i + ".cloudchainsinc.com"; + + return dnsSeeds; + } + + @Override + public int getInterval() { + return 1; + } + + @Override + public int getAddressHeader() { + return 26; + } + + @Override + public int getP2SHHeader() { + return 28; + } + + @Override + public int getDumpedPrivateKeyHeader() { + return 154; + } + + @Override + public int getBip32HeaderPub() { + return 0x0488B21E; + } + + @Override + public int getBip32HeaderPriv() { + return 0x0488ADE4; + } + + @Override + public BigInteger getMaxTarget() { + return Utils.decodeCompactBits(0x1E0FFFFF); + } + + @Override + public int getTargetTimespan() { + return 60; + } + + @Override + public String getId() { + return "BLOCK"; + } + + @Override + public XRouterMessageSerializer getXRouterMessageSerializer(boolean parseRetain) { + return new XRouterMessageSerializer(parseRetain, this); + } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPacketHeader.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPacketHeader.java index 80796e5..b18aaa0 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPacketHeader.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPacketHeader.java @@ -1,6 +1,5 @@ package io.cloudchains.app.net.protocols.blocknet; -import com.subgraph.orchid.encoders.Hex; import org.bitcoinj.core.BitcoinSerializer; import org.bitcoinj.core.Message; import org.bitcoinj.core.ProtocolException; @@ -11,59 +10,59 @@ public class BlocknetPacketHeader extends BitcoinSerializer.BitcoinPacketHeader { - public static final int HEADER_LENGTH = 20; + public static final int HEADER_LENGTH = 20; - private String command; - private byte[] checksum; - private int length; + private String command; + private byte[] checksum; + private int length; - public BlocknetPacketHeader(ByteBuffer in) throws ProtocolException, BufferUnderflowException { - super(in); - in.position(in.position() - BitcoinSerializer.BitcoinPacketHeader.HEADER_LENGTH); - byte[] header = new byte[HEADER_LENGTH]; - in.get(header, 0, header.length); + public BlocknetPacketHeader(ByteBuffer in) throws ProtocolException, BufferUnderflowException { + super(in); + in.position(in.position() - BitcoinSerializer.BitcoinPacketHeader.HEADER_LENGTH); + byte[] header = new byte[HEADER_LENGTH]; + in.get(header, 0, header.length); - int cursor = 0; + int cursor = 0; - for (; header[cursor] != 0x00 && cursor < 12; cursor++); + for (; header[cursor] != 0x00 && cursor < 12; cursor++); - byte[] commandBytes = new byte[cursor]; - System.arraycopy(header, 0, commandBytes, 0, cursor); - cursor = 12; + byte[] commandBytes = new byte[cursor]; + System.arraycopy(header, 0, commandBytes, 0, cursor); + cursor = 12; - command = new String(commandBytes).trim(); + command = new String(commandBytes).trim(); // LOGGER.log(Level.FINER, "[blocknet-header] Retrieved command: " + command); - length = (int) Utils.readUint32(header, cursor); + length = (int) Utils.readUint32(header, cursor); // LOGGER.log(Level.FINER, "[blocknet-header] Retrieved length: " + length); - cursor += 4; + cursor += 4; - if (length > Message.MAX_SIZE || length < 0) { - throw new ProtocolException("Message too large or negative length: " + length); - } + if (length > Message.MAX_SIZE || length < 0) { + throw new ProtocolException("Message too large or negative length: " + length); + } - checksum = new byte[4]; - System.arraycopy(header, cursor, checksum, 0, 4); + checksum = new byte[4]; + System.arraycopy(header, cursor, checksum, 0, 4); // LOGGER.log(Level.FINER, "[blocknet-header] Retrieved checksum: " + new String(Hex.encode(checksum))); - } + } - public String getCommand() { - return command; - } + public String getCommand() { + return command; + } - public void setChecksum(byte[] checksum) { - this.checksum = checksum; - } + public void setChecksum(byte[] checksum) { + this.checksum = checksum; + } - public byte[] getChecksum() { - return checksum; - } + public byte[] getChecksum() { + return checksum; + } - public void setLength(int length) { - this.length = length; - } + public void setLength(int length) { + this.length = length; + } - public int getLength() { - return length; - } + public int getLength() { + return length; + } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetParameters.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetParameters.java index 1fae6aa..d22846f 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetParameters.java @@ -6,10 +6,10 @@ public abstract class BlocknetParameters extends NetworkParameters { - public abstract XRouterMessageSerializer getXRouterMessageSerializer(boolean parseRetain); + public abstract XRouterMessageSerializer getXRouterMessageSerializer(boolean parseRetain); - public abstract Sha256Hash getGenesisBlockHash(); + public abstract Sha256Hash getGenesisBlockHash(); - @Override - public abstract BlocknetSerializer getSerializer(boolean parseRetain); + @Override + public abstract BlocknetSerializer getSerializer(boolean parseRetain); } diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeer.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeer.java index 939a5e8..f461e0e 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeer.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeer.java @@ -21,6 +21,7 @@ import org.bitcoinj.utils.Threading; import org.bitcoinj.wallet.Wallet; import org.json.JSONObject; + import javax.annotation.Nullable; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -40,709 +41,709 @@ import java.util.logging.Logger; public class BlocknetPeer extends PeerSocketHandler { - private final static LogManager LOGMANAGER = LogManager.getLogManager(); - private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); + private final static LogManager LOGMANAGER = LogManager.getLogManager(); + private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); - private final ReentrantLock lock = Threading.lock("BlocknetPeer"); + private final ReentrantLock lock = Threading.lock("BlocknetPeer"); - private boolean activePeer; - private boolean hasRequiredPlugins; + private boolean activePeer; + private boolean hasRequiredPlugins; - private boolean pastConnectionSuccess; + private boolean pastConnectionSuccess; - private BlocknetParameters params; - private BlocknetSerializer serializer; - private XRouterMessageSerializer xRouterMessageSerializer; - private AbstractBlockChain blockChain; + private BlocknetParameters params; + private BlocknetSerializer serializer; + private XRouterMessageSerializer xRouterMessageSerializer; + private AbstractBlockChain blockChain; - private BlocknetSeed blocknetSeed; - private XRouterConfiguration xRouterConfiguration; - private final ArrayList pluginConfigurations = new ArrayList<>(); - private final AtomicBoolean haveConfig = new AtomicBoolean(false); + private BlocknetSeed blocknetSeed; + private XRouterConfiguration xRouterConfiguration; + private final ArrayList pluginConfigurations = new ArrayList<>(); + private final AtomicBoolean haveConfig = new AtomicBoolean(false); - private Context context; - private CopyOnWriteArrayList messagesPendingReply = new CopyOnWriteArrayList<>(); + private Context context; + private CopyOnWriteArrayList messagesPendingReply = new CopyOnWriteArrayList<>(); - private CopyOnWriteArrayList> initialMessagesSentListeners = new CopyOnWriteArrayList<>(); - private CopyOnWriteArrayList> preMessageReceivedEventListeners = new CopyOnWriteArrayList<>(); - private CopyOnWriteArrayList> peerConnectedEventListeners = new CopyOnWriteArrayList<>(); - private CopyOnWriteArrayList> blocksDownloadedEventListeners = new CopyOnWriteArrayList<>(); - private CopyOnWriteArrayList> disconnectedEventListeners = new CopyOnWriteArrayList<>(); - private CopyOnWriteArrayList> xRouterMessageListeners = new CopyOnWriteArrayList<>(); + private CopyOnWriteArrayList> initialMessagesSentListeners = new CopyOnWriteArrayList<>(); + private CopyOnWriteArrayList> preMessageReceivedEventListeners = new CopyOnWriteArrayList<>(); + private CopyOnWriteArrayList> peerConnectedEventListeners = new CopyOnWriteArrayList<>(); + private CopyOnWriteArrayList> blocksDownloadedEventListeners = new CopyOnWriteArrayList<>(); + private CopyOnWriteArrayList> disconnectedEventListeners = new CopyOnWriteArrayList<>(); + private CopyOnWriteArrayList> xRouterMessageListeners = new CopyOnWriteArrayList<>(); - private volatile boolean downloadData; + private volatile boolean downloadData; - @GuardedBy("lock") - private boolean downloadBlockBodies; + @GuardedBy("lock") + private boolean downloadBlockBodies; - private static class GetDataRequest { - final Sha256Hash hash; - final SettableFuture future; + private static class GetDataRequest { + final Sha256Hash hash; + final SettableFuture future; - public GetDataRequest(Sha256Hash hash, SettableFuture future) { - this.hash = hash; - this.future = future; - } - } + public GetDataRequest(Sha256Hash hash, SettableFuture future) { + this.hash = hash; + this.future = future; + } + } - private final CopyOnWriteArrayList getDataFutures = new CopyOnWriteArrayList<>(); + private final CopyOnWriteArrayList getDataFutures = new CopyOnWriteArrayList<>(); - @GuardedBy("lock") - private Sha256Hash lastGetBlocksBegin, lastGetBlocksEnd; + @GuardedBy("lock") + private Sha256Hash lastGetBlocksBegin, lastGetBlocksEnd; - private final CopyOnWriteArrayList pendingPings = new CopyOnWriteArrayList<>(); - private VersionMessage peerVersionMessage = null; - private final VersionMessage ourVersionMessage; - private CopyOnWriteArrayList wallets = new CopyOnWriteArrayList<>(); - private volatile int downloadTxDependencyDepth; + private final CopyOnWriteArrayList pendingPings = new CopyOnWriteArrayList<>(); + private VersionMessage peerVersionMessage = null; + private final VersionMessage ourVersionMessage; + private CopyOnWriteArrayList wallets = new CopyOnWriteArrayList<>(); + private volatile int downloadTxDependencyDepth; - private final SettableFuture connectionOpenFuture = SettableFuture.create(); - private final SettableFuture incomingVersionHandshakeFuture = SettableFuture.create(); - private final SettableFuture outgoingVersionHandshakeFuture = SettableFuture.create(); - private final SettableFuture incomingPingHandshakeFuture = SettableFuture.create(); - private boolean firstPingReceived = false; + private final SettableFuture connectionOpenFuture = SettableFuture.create(); + private final SettableFuture incomingVersionHandshakeFuture = SettableFuture.create(); + private final SettableFuture outgoingVersionHandshakeFuture = SettableFuture.create(); + private final SettableFuture incomingPingHandshakeFuture = SettableFuture.create(); + private boolean firstPingReceived = false; - @SuppressWarnings({"UnstableApiUsage", "unchecked"}) - private final ListenableFuture versionHandshakeFuture = Futures.transform(Futures.allAsList(outgoingVersionHandshakeFuture, - incomingVersionHandshakeFuture, - incomingPingHandshakeFuture), - new Function, BlocknetPeer>() { - @Nullable - @Override - public BlocknetPeer apply(@Nullable List peers) { - if (peers == null) { - throw new NullPointerException("Peer list is null."); - } + @SuppressWarnings({"UnstableApiUsage", "unchecked"}) + private final ListenableFuture versionHandshakeFuture = Futures.transform(Futures.allAsList(outgoingVersionHandshakeFuture, + incomingVersionHandshakeFuture, + incomingPingHandshakeFuture), + new Function, BlocknetPeer>() { + @Nullable + @Override + public BlocknetPeer apply(@Nullable List peers) { + if (peers == null) { + throw new NullPointerException("Peer list is null."); + } - if (peers.size() != 2 || peers.get(0) != peers.get(1)) { - throw new IllegalStateException("Bad peer list state."); - } + if (peers.size() != 2 || peers.get(0) != peers.get(1)) { + throw new IllegalStateException("Bad peer list state."); + } - return peers.get(0); - } - }, Threading.SAME_THREAD); + return peers.get(0); + } + }, Threading.SAME_THREAD); - private FilteredBlock currentFilteredBlock; - private final HashSet pendingBlockDownloads = new HashSet<>(); + private FilteredBlock currentFilteredBlock; + private final HashSet pendingBlockDownloads = new HashSet<>(); - @GuardedBy("lock") - @Nullable - private List awaitingFreshFilter; + @GuardedBy("lock") + @Nullable + private List awaitingFreshFilter; - private static final int minProtocolVersion = 70712; + private static final int minProtocolVersion = 70712; - private AtomicReference largeReadBuffer = new AtomicReference<>(); - private AtomicInteger largeReadBufferPos = new AtomicInteger(); - private AtomicReference header; + private AtomicReference largeReadBuffer = new AtomicReference<>(); + private AtomicInteger largeReadBufferPos = new AtomicInteger(); + private AtomicReference header; - protected BlocknetPeer(BlocknetParameters params, AbstractBlockChain chain, PeerAddress peerAddress, BlocknetSeed blocknetSeed) { - super(params, peerAddress); + protected BlocknetPeer(BlocknetParameters params, AbstractBlockChain chain, PeerAddress peerAddress, BlocknetSeed blocknetSeed) { + super(params, peerAddress); - this.params = params; - this.serializer = this.params.getSerializer(false); - this.xRouterMessageSerializer = this.params.getXRouterMessageSerializer(false); + this.params = params; + this.serializer = this.params.getSerializer(false); + this.xRouterMessageSerializer = this.params.getXRouterMessageSerializer(false); - this.blockChain = chain; - this.downloadData = chain != null; - this.downloadTxDependencyDepth = chain != null ? Integer.MAX_VALUE : 0; - this.peerAddress = peerAddress; + this.blockChain = chain; + this.downloadData = chain != null; + this.downloadTxDependencyDepth = chain != null ? Integer.MAX_VALUE : 0; + this.peerAddress = peerAddress; - this.blocknetSeed = blocknetSeed; - - this.versionHandshakeFuture.addListener(this::versionHandshakeComplete, Threading.SAME_THREAD); - - this.context = Context.getOrCreate(params); - - this.ourVersionMessage = new VersionMessageImpl(this.params, chain != null ? chain.getBestChainHeight() : 0); - this.ourVersionMessage.appendToSubVer(Version.CLIENT_TYPE, Version.CLIENT_VERSION, Version.CLIENT_COMMENTS); - - LOGGER.log(Level.FINER, "[blocknet-peer] DEBUG: Our version message:"); - LOGGER.log(Level.FINER, this.ourVersionMessage.toString()); - - this.activePeer = true; - this.pastConnectionSuccess = false; - } - - @Override - public void connectionClosed() { - if (!activePeer) return; - - activePeer = false; - LOGGER.log(Level.FINER, "[blocknet-peer] Connection with " + (getAddress() != null ? getAddress().toString() : "") + " closed. Notifying receivers."); - - for (final ListenerRegistration registration : disconnectedEventListeners) { - registration.executor.execute(() -> registration.listener.onPeerDisconnected(BlocknetPeer.this, 0)); - } - } - - @Override - public void connectionOpened() { - LOGGER.log(Level.FINER, "[blocknet-peer] Connection open to " + (getAddress() != null ? getAddress().toString() : "") + ", sending version message."); - - sendMessage(ourVersionMessage); - connectionOpenFuture.set(this); - } - - @Override - protected void timeoutOccurred() { - super.timeoutOccurred(); - LOGGER.log(Level.FINER, "[blocknet-peer] Timeout occurred."); - if (!connectionOpenFuture.isDone()) { - connectionClosed(); - } - } - - public void addPreMessageReceivedEventListener(BlocknetPreMessageReceivedEventListener listener) { - addPreMessageReceivedEventListener(Threading.SAME_THREAD, listener); - } - - private void addPreMessageReceivedEventListener(Executor executor, BlocknetPreMessageReceivedEventListener listener) { - preMessageReceivedEventListeners.add(new ListenerRegistration<>(listener, executor)); - } - - public void addConnectedEventListener(BlocknetPeerConnectedEventListener listener) { - addConnectedEventListener(Threading.SAME_THREAD, listener); - } - - public void addConnectedEventListener(Executor executor, BlocknetPeerConnectedEventListener listener) { - peerConnectedEventListeners.add(new ListenerRegistration<>(listener, executor)); - } - - public void addBlocksDownloadedEventListener(BlocknetOnBlocksDownloadedEventListener listener) { - addBlocksDownloadedEventListener(Threading.SAME_THREAD, listener); - } - - private void addBlocksDownloadedEventListener(Executor executor, BlocknetOnBlocksDownloadedEventListener listener) { - blocksDownloadedEventListeners.add(new ListenerRegistration<>(listener, executor)); - } - - public void addPeerDisconnectedEventListener(BlocknetPeerDisconnectedEventListener listener) { - addPeerDisconnectedEventListener(Threading.SAME_THREAD, listener); - } - - public void addPeerDisconnectedEventListener(Executor executor, BlocknetPeerDisconnectedEventListener listener) { - disconnectedEventListeners.add(new ListenerRegistration<>(listener, executor)); - } - - public void addXRouterMessageReceivedEventListener(BlocknetOnXRouterMessageReceivedListener listener) { - addXRouterMessageReceivedEventListener(Threading.SAME_THREAD, listener); - } - - private void addXRouterMessageReceivedEventListener(Executor executor, BlocknetOnXRouterMessageReceivedListener listener) { - xRouterMessageListeners.add(new ListenerRegistration<>(listener, executor)); - } - - public void removeXRouterMessageReceivedEventListener(BlocknetOnXRouterMessageReceivedListener listener) { - ListenerRegistration.removeFromList(listener, xRouterMessageListeners); - } - - public BlocknetOnXRouterMessageReceivedListener getListener(final String uuid, final AtomicReference response, final CountDownLatch latch) { - return new BlocknetOnXRouterMessageReceivedListener() { - @Override - public void onXRouterMessageReceived(XRouterMessage message, XRouterMessage original) { - if (message.getXRouterHeader().getUUID().equals(uuid)) { - response.set((String) message.getParsedData().get("reply")); - - latch.countDown(); - - removeXRouterMessageReceivedEventListener(this); - } - } - }; - } - - @Override - public void sendMessage(Message message) throws NotYetConnectedException { - lock.lock(); - try { - if (writeTarget == null) { - LOGGER.log(Level.FINER, "[blocknet-peer] ERROR: Attempted to send message on non-connected socket."); - throw new NotYetConnectedException(); - } - } finally { - lock.unlock(); - } - - if (message instanceof XRouterMessage) { - try { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - xRouterMessageSerializer.serialize(message, outputStream); - LOGGER.log(Level.FINER, "[blocknet-peer] DEBUG: Sending XRouter message. Actual length (excluding network header) is " + (outputStream.size() - BlocknetPacketHeader.HEADER_LENGTH - 4) + " bytes."); - writeTarget.writeBytes(outputStream.toByteArray()); - - messagesPendingReply.add((XRouterMessage) message); - LOGGER.log(Level.FINER, "[blocknet-peer] DEBUG: Added UUID " + ((XRouterMessage) message).getXRouterHeader().getUUID() + " to pending reply list."); - } catch (IOException e) { - LOGGER.log(Level.FINER, "[blocknet-peer] Error while serializing XRouter message!"); - e.printStackTrace(); - } - } else { - try { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - serializer.serialize(message, outputStream); - writeTarget.writeBytes(outputStream.toByteArray()); - } catch (IOException e) { - LOGGER.log(Level.FINER, "[blocknet-peer] Error while serializing/sending non-XRouter message!"); - e.printStackTrace(); - } - } - } - - @Override - protected void processMessage(Message message) { - for (ListenerRegistration registration : preMessageReceivedEventListeners) { - if (registration.executor == Threading.SAME_THREAD) { - message = registration.listener.onPreMessageReceived(this, message); - } - - if (message == null) - break; - } - - if (message == null) - return; - - if (currentFilteredBlock != null && !(message instanceof Transaction)) { - endFilteredBlock(currentFilteredBlock); - currentFilteredBlock = null; - } - - if (!(message instanceof VersionMessage || message instanceof Ping || message instanceof VersionAck || (versionHandshakeFuture.isDone() && !versionHandshakeFuture.isCancelled()))) { - throw new ProtocolException("Received " + message.getClass().getSimpleName() + " before version handshake was complete."); - } - - if (message instanceof VersionMessage) { - processVersionMessage((VersionMessage) message); - } else if (message instanceof VersionAck) { - processVersionAck((VersionAck) message); - } else if (message instanceof Ping) { - LOGGER.log(Level.FINER, "[blocknet-peer] Received ping message from " + getAddress().toString() + ", sending pong."); - processPing((Ping) message); - } else if (message instanceof RejectMessage) { - LOGGER.log(Level.FINER, "[blocknet-peer] ERROR: Received rejection message from " + getAddress().toString() + ": " + message.toString()); - } else if (message instanceof XRouterMessage) { - processXRouterMessage((XRouterMessage) message); - } else { - LOGGER.log(Level.FINER, "[blocknet-peer] Warning: Received unhandled message from " + getAddress().toString() + ": " + message.toString()); - } - - //TODO process other message types - } - - private void processPing(Ping pingMessage) throws ProtocolException { - sendMessage(new Pong(pingMessage.getNonce())); - - if (!firstPingReceived) { - firstPingReceived = true; - incomingPingHandshakeFuture.set(this); - } - } - - private void processVersionMessage(VersionMessage versionMessage) throws ProtocolException { - if (peerVersionMessage != null) { - throw new ProtocolException("Received more than one version message from this peer!"); - } - - peerVersionMessage = versionMessage; - - LOGGER.log(Level.FINER, "[blocknet-peer] Received version message: " + peerVersionMessage.subVer - + ", version " + peerVersionMessage.clientVersion - + ", blocks=" + peerVersionMessage.bestHeight - + ", us=" + peerVersionMessage.theirAddr); - - if (!peerVersionMessage.hasBlockChain() || (!params.allowEmptyPeerChain() && peerVersionMessage.bestHeight == 0)) { - LOGGER.log(Level.FINER, "[blocknet-peer] ERROR: Peer has an empty blockchain while this network does not allow empty blockchains. Disconnecting."); - close(); - } - - if (peerVersionMessage.bestHeight < 0) { - LOGGER.log(Level.FINER, "[blocknet-peer] ERROR: Peer reported bad blockchain height (" + peerVersionMessage.bestHeight + "). Disconnecting."); - close(); - } - - sendMessage(new VersionAck()); - LOGGER.log(Level.FINER, "[blocknet-peer] Incoming version handshake complete."); - incomingVersionHandshakeFuture.set(this); - } - - private void processVersionAck(VersionAck versionAck) throws ProtocolException { - if (peerVersionMessage == null) { - throw new ProtocolException("Received version acknowledgement before version message."); - } - - if (!incomingVersionHandshakeFuture.isDone()) { - throw new ProtocolException("Received more than one version acknowledgement."); - } - - LOGGER.log(Level.FINER, "[blocknet-peer] Outgoing version handshake complete."); - outgoingVersionHandshakeFuture.set(this); - } - - private void versionHandshakeComplete() { - setTimeoutEnabled(false); - for (final ListenerRegistration registration : peerConnectedEventListeners) { - registration.executor.execute(() -> registration.listener.onPeerConnected(BlocknetPeer.this, 1)); - } - - if (peerVersionMessage.clientVersion < minProtocolVersion) { - LOGGER.log(Level.FINER, "[blocknet-peer] Peer's protocol version (" + peerVersionMessage.clientVersion + ") is lower than the minimum (" + minProtocolVersion + ")! Disconnecting."); - close(); - } - } - - private XRouterMessage getOriginalXRouterMessage(String uuid) { - for (XRouterMessage msg : messagesPendingReply) { - if (msg.getXRouterHeader().getUUID().equalsIgnoreCase(uuid)) - return msg; - } - - return null; - } - - private int removeUUIDFromPendingReplyList(String uuid) { - int removed = 0; - - for (XRouterMessage msg : messagesPendingReply) { - if (msg.getXRouterHeader().getUUID().equalsIgnoreCase(uuid)) { - messagesPendingReply.remove(msg); - removed++; - } - } - - return removed; - } - - private void processReply(XRouterMessage message) { - if (message.getXRouterHeader().getUUID().isEmpty()) { - LOGGER.log(Level.FINER, "[blocknet-peer] ERROR: XRouter server sent back packet with blank UUID!"); - return; - } - - final XRouterMessage original = getOriginalXRouterMessage(message.getXRouterHeader().getUUID()); - if (original == null) { - LOGGER.log(Level.FINER, "[blocknet-peer] ERROR: Unexpected UUID in reply message! Perhaps the server thinks we sent a packet that we didn't send?"); - throw new ProtocolException("Unexpected UUID in reply message"); - } - - int removed = removeUUIDFromPendingReplyList(message.getXRouterHeader().getUUID()); - if (removed != 1) { - LOGGER.log(Level.FINER, "[blocknet-peer] Warning: Exception occurred while removing message from pending list! This may break things later on. Amount of messages removed = " + removed); - if (removed == 0) { - LOGGER.log(Level.FINER, "[blocknet-peer] ERROR: Invalid UUID in reply message!"); - throw new ProtocolException("Invalid UUID in reply message"); - } - } - - LOGGER.log(Level.FINER, "[blocknet-peer] XRouter pre-processing successful. Notifying listeners."); - for (ListenerRegistration registration : xRouterMessageListeners) { - if (registration.executor == Threading.SAME_THREAD) { - registration.executor.execute(() -> registration.listener.onXRouterMessageReceived(message, original)); - } - } - } - - private void processXRouterMessage(final XRouterMessage message) { - LOGGER.log(Level.FINER, "processXRouterMessage() called."); - LOGGER.log(Level.FINER, "This XRouter message's UUID is '" + message.getXRouterHeader().getUUID() + "'"); - - switch (XRouterCommandUtils.commandIdToString(message.getXRouterHeader().getCommand())) { - case "xrReply": //xrReply - case "xrConfigReply": { // xrConfigReply - processReply(message); - break; - } - default: { //xrInvalid or unexpected message - for (ListenerRegistration registration : xRouterMessageListeners) { - if (registration.executor == Threading.SAME_THREAD) { - registration.executor.execute(() -> registration.listener.onXRouterMessageReceived(message, null)); - } - } - break; - } - } - - } - - @GuardedBy("lock") - private void blockChainDownloadLocked(Sha256Hash toHash) { - if (!lock.isHeldByCurrentThread()) { - throw new IllegalStateException("Lock is not held by current thread."); - } - - List blockLocator = new ArrayList<>(51); - - if (blockChain == null) { - throw new NullPointerException("Blockchain object is null."); - } - - BlockStore blockStore = blockChain.getBlockStore(); - StoredBlock chainHead = blockChain.getChainHead(); - Sha256Hash chainHeadHash = chainHead.getHeader().getHash(); - - if (Objects.equals(chainHeadHash, lastGetBlocksBegin) || Objects.equals(toHash, lastGetBlocksEnd)) { - LOGGER.log(Level.FINER, "[blocknet-peer] Ignoring dupliated request: chainHeadHash = " + chainHeadHash.toString() + ", toHash = " + toHash.toString()); - - for (Sha256Hash hash : pendingBlockDownloads) - LOGGER.log(Level.FINER, "[blocknet-peer] Pending block download: " + hash.toString()); - - LOGGER.log(Level.FINER, Throwables.getStackTraceAsString(new Throwable())); - return; - } - - LOGGER.log(Level.FINER, "[blocknet-peer] blockChainDownloadLocked(" + toHash.toString() + "): Current head = " + chainHeadHash.toString()); - - StoredBlock cursor = chainHead; - for (int i = 100; cursor != null && i > 0; i--) { - blockLocator.add(cursor.getHeader().getHash()); - try { - cursor = cursor.getPrev(blockStore); - } catch (BlockStoreException e) { - LOGGER.log(Level.FINER, "[blocknet-peer] Failed to walk the blockchain while constructing a locator."); - e.printStackTrace(); - } - } - - if (cursor != null) - blockLocator.add(params.getGenesisBlockHash()); - - lastGetBlocksBegin = chainHeadHash; - lastGetBlocksEnd = toHash; - - if (downloadBlockBodies) { - GetBlocksMessage getBlocksMessage = new GetBlocksMessage(params, blockLocator, toHash); - sendMessage(getBlocksMessage); - } else { - GetHeadersMessage getHeadersMessage = new GetHeadersMessage(params, blockLocator, toHash); - sendMessage(getHeadersMessage); - } - } - - private void endFilteredBlock(FilteredBlock filteredBlock) { - if (!downloadData) { - LOGGER.log(Level.FINER, "[blocknet-peer] WARNING: [" + getAddress().toString() + "] Received block we did not ask for! Hash: " + filteredBlock.getHash().toString()); - return; - } - - if (blockChain == null) { - LOGGER.log(Level.FINER, "[blocknet-peer] WARNING: Received a block, but a blockchain object was not configured!"); - return; - } - - pendingBlockDownloads.remove(filteredBlock.getBlockHeader().getHash()); - try { - lock.lock(); - - try { - if (awaitingFreshFilter != null) { - LOGGER.log(Level.FINER, "[blocknet-peer] Discarding this block because we are waiting for a fresh filter. Hash: " + filteredBlock.getHash().toString()); - - awaitingFreshFilter.add(filteredBlock.getHash()); - return; - } else if (checkForFilterExhaustion(filteredBlock)) { - awaitingFreshFilter = new LinkedList<>(); - awaitingFreshFilter.add(filteredBlock.getHash()); - awaitingFreshFilter.addAll(blockChain.drainOrphanBlocks()); - return; - } - } finally { - lock.unlock(); - } - - if (blockChain.add(filteredBlock)) { - invokeOnBlocksDownloaded(filteredBlock.getBlockHeader(), filteredBlock); - } else { - lock.lock(); - try { - final Block orphanRoot = blockChain.getOrphanRoot(filteredBlock.getHash()); - if (orphanRoot == null) { - throw new NullPointerException("Orphan root is null."); - } - - blockChainDownloadLocked(orphanRoot.getHash()); - } finally { - lock.unlock(); - } - } - } catch (VerificationException e) { - LOGGER.log(Level.FINER, "[blocknet-peer] Block failed to properly verify!"); - e.printStackTrace(); - } catch (PrunedException e) { - LOGGER.log(Level.FINER, "[blocknet-peer] Some data needed to handle this block was pruned! Hash: " + filteredBlock.getHash().toString()); - throw new RuntimeException(e); - } - } - - private boolean checkForFilterExhaustion(FilteredBlock filteredBlock) { - boolean exhausted = false; - for (Wallet wallet : wallets) { - exhausted |= wallet.checkForFilterExhaustion(filteredBlock); - } - return exhausted; - } - - private void invokeOnBlocksDownloaded(final Block block, @Nullable final FilteredBlock filteredBlock) { - if (blockChain == null) { - return; - } - - final int blocksLeft = Math.max(0, (int) peerVersionMessage.bestHeight - blockChain.getBestChainHeight()); - for (final ListenerRegistration registration : blocksDownloadedEventListeners) { - registration.executor.execute(() -> registration.listener.onBlocksDownloaded(BlocknetPeer.this, block, filteredBlock, blocksLeft)); - } - } - - @Override - public int receiveBytes(ByteBuffer buff) { - if (buff.position() != 0 || buff.capacity() < BlocknetPacketHeader.HEADER_LENGTH + 4) { - throw new IllegalArgumentException("Buffer position is nonzero or bad header."); - } - - try { - boolean firstMessage = true; - - while (true) { - if (largeReadBuffer.get() != null) { - if (!firstMessage) { - throw new IllegalStateException("Bad firstMessage state."); - } - - int bytesToGet = Math.min(buff.remaining(), largeReadBuffer.get().length - largeReadBufferPos.get()); - byte[] buf = largeReadBuffer.get(); - buff.get(buf, largeReadBufferPos.get(), bytesToGet); - - largeReadBuffer.set(buf); - - largeReadBufferPos.set(largeReadBufferPos.get() + bytesToGet); - - if (largeReadBufferPos.get() == largeReadBuffer.get().length) { - processMessage(serializer.deserializePayload(header.get(), ByteBuffer.wrap(largeReadBuffer.get()))); - - largeReadBuffer.set(null); - header = null; - firstMessage = false; - } else { - return buff.position(); - } - } - - Message message; - int preSerializePos = buff.position(); - try { - message = serializer.deserialize(buff); - } catch (BufferUnderflowException e) { - if (firstMessage && buff.limit() == buff.capacity()) { - buff.position(0); - - try { - serializer.seekPastMagicBytes(buff); - header.set(serializer.deserializeHeader(buff)); - - largeReadBufferPos.set(buff.remaining()); - byte[] buf = new byte[header.get().getLength()]; - buff.get(buf, 0, largeReadBufferPos.get()); - largeReadBuffer.set(buf); - } catch (BufferUnderflowException e1) { - throw new ProtocolException("No magic/header after reading " + buff.capacity() + " bytes."); - } - } else { - buff.position(preSerializePos); - } - - return buff.position(); - } - - processMessage(message); - firstMessage = false; - } - } catch (Exception e) { - LOGGER.log(Level.FINER, "Error while receiving bytes!"); - e.printStackTrace(); - return -1; - } - } - - public void parsePlugins(JSONObject pluginsList) { - for (String plugin : pluginsList.keySet()) { - String rawPluginConfig = pluginsList.getString(plugin); - - XRouterConfiguration.XRouterPluginConfiguration pluginConfig = new XRouterConfiguration.XRouterPluginConfiguration(plugin, rawPluginConfig); - pluginConfig.parsePluginConfig(); - getPluginConfigurations().add(pluginConfig); - } - } - - public XRouterConfiguration.XRouterPluginConfiguration getPluginConfig(String pluginName) { - for (XRouterConfiguration.XRouterPluginConfiguration pluginConfig : pluginConfigurations) { - if (pluginConfig.getPluginName().equals(pluginName)) { - return pluginConfig; - } - } - - return null; - } - - public CopyOnWriteArrayList getMessagesPendingReply() { - return messagesPendingReply; - } - - public CopyOnWriteArrayList> getInitialMessagesSentListeners() { - return initialMessagesSentListeners; - } - - public CopyOnWriteArrayList> getXRouterMessageListeners() { - return xRouterMessageListeners; - } - - public void addInitialMessagesSentListener(XRouterInitialMessagesSentListener listener) { - initialMessagesSentListeners.add(new ListenerRegistration<>(listener, Threading.SAME_THREAD)); - } - - public void removeInitialMessagesSentListener(XRouterInitialMessagesSentListener listener) { - ListenerRegistration.removeFromList(listener, initialMessagesSentListeners); - } - - public BlocknetSeed getBlocknetSeed() { - return blocknetSeed; - } - - public AtomicBoolean getHaveConfig() { - return haveConfig; - } - - public XRouterConfiguration getxRouterConfiguration() { - return xRouterConfiguration; - } - - public ArrayList getPluginConfigurations() { - return pluginConfigurations; - } - - public boolean isActivePeer() { - return activePeer; - } - - public boolean pastConnectionSuccess() { - return pastConnectionSuccess; - } - - public boolean hasRequiredPlugins() { - return hasRequiredPlugins; - } - - public void setPastConnectionSuccess(boolean pastConnectionSuccess) { - this.pastConnectionSuccess = pastConnectionSuccess; - } - - public void setHaveConfig(boolean hasConfig) { - this.haveConfig.set(hasConfig); - } - - public void setxRouterConfiguration(XRouterConfiguration xRouterConfiguration) { - this.xRouterConfiguration = xRouterConfiguration; - } - - public void setHasRequiredPlugins(boolean hasRequiredPlugins) { - this.hasRequiredPlugins = hasRequiredPlugins; - } + this.blocknetSeed = blocknetSeed; + + this.versionHandshakeFuture.addListener(this::versionHandshakeComplete, Threading.SAME_THREAD); + + this.context = Context.getOrCreate(params); + + this.ourVersionMessage = new VersionMessageImpl(this.params, chain != null ? chain.getBestChainHeight() : 0); + this.ourVersionMessage.appendToSubVer(Version.CLIENT_TYPE, Version.CLIENT_VERSION, Version.CLIENT_COMMENTS); + + LOGGER.log(Level.FINER, "[blocknet-peer] DEBUG: Our version message:"); + LOGGER.log(Level.FINER, this.ourVersionMessage.toString()); + + this.activePeer = true; + this.pastConnectionSuccess = false; + } + + @Override + public void connectionClosed() { + if (!activePeer) return; + + activePeer = false; + LOGGER.log(Level.FINER, "[blocknet-peer] Connection with " + (getAddress() != null ? getAddress().toString() : "") + " closed. Notifying receivers."); + + for (final ListenerRegistration registration : disconnectedEventListeners) { + registration.executor.execute(() -> registration.listener.onPeerDisconnected(BlocknetPeer.this, 0)); + } + } + + @Override + public void connectionOpened() { + LOGGER.log(Level.FINER, "[blocknet-peer] Connection open to " + (getAddress() != null ? getAddress().toString() : "") + ", sending version message."); + + sendMessage(ourVersionMessage); + connectionOpenFuture.set(this); + } + + @Override + protected void timeoutOccurred() { + super.timeoutOccurred(); + LOGGER.log(Level.FINER, "[blocknet-peer] Timeout occurred."); + if (!connectionOpenFuture.isDone()) { + connectionClosed(); + } + } + + public void addPreMessageReceivedEventListener(BlocknetPreMessageReceivedEventListener listener) { + addPreMessageReceivedEventListener(Threading.SAME_THREAD, listener); + } + + private void addPreMessageReceivedEventListener(Executor executor, BlocknetPreMessageReceivedEventListener listener) { + preMessageReceivedEventListeners.add(new ListenerRegistration<>(listener, executor)); + } + + public void addConnectedEventListener(BlocknetPeerConnectedEventListener listener) { + addConnectedEventListener(Threading.SAME_THREAD, listener); + } + + public void addConnectedEventListener(Executor executor, BlocknetPeerConnectedEventListener listener) { + peerConnectedEventListeners.add(new ListenerRegistration<>(listener, executor)); + } + + public void addBlocksDownloadedEventListener(BlocknetOnBlocksDownloadedEventListener listener) { + addBlocksDownloadedEventListener(Threading.SAME_THREAD, listener); + } + + private void addBlocksDownloadedEventListener(Executor executor, BlocknetOnBlocksDownloadedEventListener listener) { + blocksDownloadedEventListeners.add(new ListenerRegistration<>(listener, executor)); + } + + public void addPeerDisconnectedEventListener(BlocknetPeerDisconnectedEventListener listener) { + addPeerDisconnectedEventListener(Threading.SAME_THREAD, listener); + } + + public void addPeerDisconnectedEventListener(Executor executor, BlocknetPeerDisconnectedEventListener listener) { + disconnectedEventListeners.add(new ListenerRegistration<>(listener, executor)); + } + + public void addXRouterMessageReceivedEventListener(BlocknetOnXRouterMessageReceivedListener listener) { + addXRouterMessageReceivedEventListener(Threading.SAME_THREAD, listener); + } + + private void addXRouterMessageReceivedEventListener(Executor executor, BlocknetOnXRouterMessageReceivedListener listener) { + xRouterMessageListeners.add(new ListenerRegistration<>(listener, executor)); + } + + public void removeXRouterMessageReceivedEventListener(BlocknetOnXRouterMessageReceivedListener listener) { + ListenerRegistration.removeFromList(listener, xRouterMessageListeners); + } + + public BlocknetOnXRouterMessageReceivedListener getListener(final String uuid, final AtomicReference response, final CountDownLatch latch) { + return new BlocknetOnXRouterMessageReceivedListener() { + @Override + public void onXRouterMessageReceived(XRouterMessage message, XRouterMessage original) { + if (message.getXRouterHeader().getUUID().equals(uuid)) { + response.set((String) message.getParsedData().get("reply")); + + latch.countDown(); + + removeXRouterMessageReceivedEventListener(this); + } + } + }; + } + + @Override + public void sendMessage(Message message) throws NotYetConnectedException { + lock.lock(); + try { + if (writeTarget == null) { + LOGGER.log(Level.FINER, "[blocknet-peer] ERROR: Attempted to send message on non-connected socket."); + throw new NotYetConnectedException(); + } + } finally { + lock.unlock(); + } + + if (message instanceof XRouterMessage) { + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + xRouterMessageSerializer.serialize(message, outputStream); + LOGGER.log(Level.FINER, "[blocknet-peer] DEBUG: Sending XRouter message. Actual length (excluding network header) is " + (outputStream.size() - BlocknetPacketHeader.HEADER_LENGTH - 4) + " bytes."); + writeTarget.writeBytes(outputStream.toByteArray()); + + messagesPendingReply.add((XRouterMessage) message); + LOGGER.log(Level.FINER, "[blocknet-peer] DEBUG: Added UUID " + ((XRouterMessage) message).getXRouterHeader().getUUID() + " to pending reply list."); + } catch (IOException e) { + LOGGER.log(Level.FINER, "[blocknet-peer] Error while serializing XRouter message!"); + e.printStackTrace(); + } + } else { + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + serializer.serialize(message, outputStream); + writeTarget.writeBytes(outputStream.toByteArray()); + } catch (IOException e) { + LOGGER.log(Level.FINER, "[blocknet-peer] Error while serializing/sending non-XRouter message!"); + e.printStackTrace(); + } + } + } + + @Override + protected void processMessage(Message message) { + for (ListenerRegistration registration : preMessageReceivedEventListeners) { + if (registration.executor == Threading.SAME_THREAD) { + message = registration.listener.onPreMessageReceived(this, message); + } + + if (message == null) + break; + } + + if (message == null) + return; + + if (currentFilteredBlock != null && !(message instanceof Transaction)) { + endFilteredBlock(currentFilteredBlock); + currentFilteredBlock = null; + } + + if (!(message instanceof VersionMessage || message instanceof Ping || message instanceof VersionAck || (versionHandshakeFuture.isDone() && !versionHandshakeFuture.isCancelled()))) { + throw new ProtocolException("Received " + message.getClass().getSimpleName() + " before version handshake was complete."); + } + + if (message instanceof VersionMessage) { + processVersionMessage((VersionMessage) message); + } else if (message instanceof VersionAck) { + processVersionAck((VersionAck) message); + } else if (message instanceof Ping) { + LOGGER.log(Level.FINER, "[blocknet-peer] Received ping message from " + getAddress().toString() + ", sending pong."); + processPing((Ping) message); + } else if (message instanceof RejectMessage) { + LOGGER.log(Level.FINER, "[blocknet-peer] ERROR: Received rejection message from " + getAddress().toString() + ": " + message.toString()); + } else if (message instanceof XRouterMessage) { + processXRouterMessage((XRouterMessage) message); + } else { + LOGGER.log(Level.FINER, "[blocknet-peer] Warning: Received unhandled message from " + getAddress().toString() + ": " + message.toString()); + } + + //TODO process other message types + } + + private void processPing(Ping pingMessage) throws ProtocolException { + sendMessage(new Pong(pingMessage.getNonce())); + + if (!firstPingReceived) { + firstPingReceived = true; + incomingPingHandshakeFuture.set(this); + } + } + + private void processVersionMessage(VersionMessage versionMessage) throws ProtocolException { + if (peerVersionMessage != null) { + throw new ProtocolException("Received more than one version message from this peer!"); + } + + peerVersionMessage = versionMessage; + + LOGGER.log(Level.FINER, "[blocknet-peer] Received version message: " + peerVersionMessage.subVer + + ", version " + peerVersionMessage.clientVersion + + ", blocks=" + peerVersionMessage.bestHeight + + ", us=" + peerVersionMessage.theirAddr); + + if (!peerVersionMessage.hasBlockChain() || (!params.allowEmptyPeerChain() && peerVersionMessage.bestHeight == 0)) { + LOGGER.log(Level.FINER, "[blocknet-peer] ERROR: Peer has an empty blockchain while this network does not allow empty blockchains. Disconnecting."); + close(); + } + + if (peerVersionMessage.bestHeight < 0) { + LOGGER.log(Level.FINER, "[blocknet-peer] ERROR: Peer reported bad blockchain height (" + peerVersionMessage.bestHeight + "). Disconnecting."); + close(); + } + + sendMessage(new VersionAck()); + LOGGER.log(Level.FINER, "[blocknet-peer] Incoming version handshake complete."); + incomingVersionHandshakeFuture.set(this); + } + + private void processVersionAck(VersionAck versionAck) throws ProtocolException { + if (peerVersionMessage == null) { + throw new ProtocolException("Received version acknowledgement before version message."); + } + + if (!incomingVersionHandshakeFuture.isDone()) { + throw new ProtocolException("Received more than one version acknowledgement."); + } + + LOGGER.log(Level.FINER, "[blocknet-peer] Outgoing version handshake complete."); + outgoingVersionHandshakeFuture.set(this); + } + + private void versionHandshakeComplete() { + setTimeoutEnabled(false); + for (final ListenerRegistration registration : peerConnectedEventListeners) { + registration.executor.execute(() -> registration.listener.onPeerConnected(BlocknetPeer.this, 1)); + } + + if (peerVersionMessage.clientVersion < minProtocolVersion) { + LOGGER.log(Level.FINER, "[blocknet-peer] Peer's protocol version (" + peerVersionMessage.clientVersion + ") is lower than the minimum (" + minProtocolVersion + ")! Disconnecting."); + close(); + } + } + + private XRouterMessage getOriginalXRouterMessage(String uuid) { + for (XRouterMessage msg : messagesPendingReply) { + if (msg.getXRouterHeader().getUUID().equalsIgnoreCase(uuid)) + return msg; + } + + return null; + } + + private int removeUUIDFromPendingReplyList(String uuid) { + int removed = 0; + + for (XRouterMessage msg : messagesPendingReply) { + if (msg.getXRouterHeader().getUUID().equalsIgnoreCase(uuid)) { + messagesPendingReply.remove(msg); + removed++; + } + } + + return removed; + } + + private void processReply(XRouterMessage message) { + if (message.getXRouterHeader().getUUID().isEmpty()) { + LOGGER.log(Level.FINER, "[blocknet-peer] ERROR: XRouter server sent back packet with blank UUID!"); + return; + } + + final XRouterMessage original = getOriginalXRouterMessage(message.getXRouterHeader().getUUID()); + if (original == null) { + LOGGER.log(Level.FINER, "[blocknet-peer] ERROR: Unexpected UUID in reply message! Perhaps the server thinks we sent a packet that we didn't send?"); + throw new ProtocolException("Unexpected UUID in reply message"); + } + + int removed = removeUUIDFromPendingReplyList(message.getXRouterHeader().getUUID()); + if (removed != 1) { + LOGGER.log(Level.FINER, "[blocknet-peer] Warning: Exception occurred while removing message from pending list! This may break things later on. Amount of messages removed = " + removed); + if (removed == 0) { + LOGGER.log(Level.FINER, "[blocknet-peer] ERROR: Invalid UUID in reply message!"); + throw new ProtocolException("Invalid UUID in reply message"); + } + } + + LOGGER.log(Level.FINER, "[blocknet-peer] XRouter pre-processing successful. Notifying listeners."); + for (ListenerRegistration registration : xRouterMessageListeners) { + if (registration.executor == Threading.SAME_THREAD) { + registration.executor.execute(() -> registration.listener.onXRouterMessageReceived(message, original)); + } + } + } + + private void processXRouterMessage(final XRouterMessage message) { + LOGGER.log(Level.FINER, "processXRouterMessage() called."); + LOGGER.log(Level.FINER, "This XRouter message's UUID is '" + message.getXRouterHeader().getUUID() + "'"); + + switch (XRouterCommandUtils.commandIdToString(message.getXRouterHeader().getCommand())) { + case "xrReply": //xrReply + case "xrConfigReply": { // xrConfigReply + processReply(message); + break; + } + default: { //xrInvalid or unexpected message + for (ListenerRegistration registration : xRouterMessageListeners) { + if (registration.executor == Threading.SAME_THREAD) { + registration.executor.execute(() -> registration.listener.onXRouterMessageReceived(message, null)); + } + } + break; + } + } + + } + + @GuardedBy("lock") + private void blockChainDownloadLocked(Sha256Hash toHash) { + if (!lock.isHeldByCurrentThread()) { + throw new IllegalStateException("Lock is not held by current thread."); + } + + List blockLocator = new ArrayList<>(51); + + if (blockChain == null) { + throw new NullPointerException("Blockchain object is null."); + } + + BlockStore blockStore = blockChain.getBlockStore(); + StoredBlock chainHead = blockChain.getChainHead(); + Sha256Hash chainHeadHash = chainHead.getHeader().getHash(); + + if (Objects.equals(chainHeadHash, lastGetBlocksBegin) || Objects.equals(toHash, lastGetBlocksEnd)) { + LOGGER.log(Level.FINER, "[blocknet-peer] Ignoring dupliated request: chainHeadHash = " + chainHeadHash.toString() + ", toHash = " + toHash.toString()); + + for (Sha256Hash hash : pendingBlockDownloads) + LOGGER.log(Level.FINER, "[blocknet-peer] Pending block download: " + hash.toString()); + + LOGGER.log(Level.FINER, Throwables.getStackTraceAsString(new Throwable())); + return; + } + + LOGGER.log(Level.FINER, "[blocknet-peer] blockChainDownloadLocked(" + toHash.toString() + "): Current head = " + chainHeadHash.toString()); + + StoredBlock cursor = chainHead; + for (int i = 100; cursor != null && i > 0; i--) { + blockLocator.add(cursor.getHeader().getHash()); + try { + cursor = cursor.getPrev(blockStore); + } catch (BlockStoreException e) { + LOGGER.log(Level.FINER, "[blocknet-peer] Failed to walk the blockchain while constructing a locator."); + e.printStackTrace(); + } + } + + if (cursor != null) + blockLocator.add(params.getGenesisBlockHash()); + + lastGetBlocksBegin = chainHeadHash; + lastGetBlocksEnd = toHash; + + if (downloadBlockBodies) { + GetBlocksMessage getBlocksMessage = new GetBlocksMessage(params, blockLocator, toHash); + sendMessage(getBlocksMessage); + } else { + GetHeadersMessage getHeadersMessage = new GetHeadersMessage(params, blockLocator, toHash); + sendMessage(getHeadersMessage); + } + } + + private void endFilteredBlock(FilteredBlock filteredBlock) { + if (!downloadData) { + LOGGER.log(Level.FINER, "[blocknet-peer] WARNING: [" + getAddress().toString() + "] Received block we did not ask for! Hash: " + filteredBlock.getHash().toString()); + return; + } + + if (blockChain == null) { + LOGGER.log(Level.FINER, "[blocknet-peer] WARNING: Received a block, but a blockchain object was not configured!"); + return; + } + + pendingBlockDownloads.remove(filteredBlock.getBlockHeader().getHash()); + try { + lock.lock(); + + try { + if (awaitingFreshFilter != null) { + LOGGER.log(Level.FINER, "[blocknet-peer] Discarding this block because we are waiting for a fresh filter. Hash: " + filteredBlock.getHash().toString()); + + awaitingFreshFilter.add(filteredBlock.getHash()); + return; + } else if (checkForFilterExhaustion(filteredBlock)) { + awaitingFreshFilter = new LinkedList<>(); + awaitingFreshFilter.add(filteredBlock.getHash()); + awaitingFreshFilter.addAll(blockChain.drainOrphanBlocks()); + return; + } + } finally { + lock.unlock(); + } + + if (blockChain.add(filteredBlock)) { + invokeOnBlocksDownloaded(filteredBlock.getBlockHeader(), filteredBlock); + } else { + lock.lock(); + try { + final Block orphanRoot = blockChain.getOrphanRoot(filteredBlock.getHash()); + if (orphanRoot == null) { + throw new NullPointerException("Orphan root is null."); + } + + blockChainDownloadLocked(orphanRoot.getHash()); + } finally { + lock.unlock(); + } + } + } catch (VerificationException e) { + LOGGER.log(Level.FINER, "[blocknet-peer] Block failed to properly verify!"); + e.printStackTrace(); + } catch (PrunedException e) { + LOGGER.log(Level.FINER, "[blocknet-peer] Some data needed to handle this block was pruned! Hash: " + filteredBlock.getHash().toString()); + throw new RuntimeException(e); + } + } + + private boolean checkForFilterExhaustion(FilteredBlock filteredBlock) { + boolean exhausted = false; + for (Wallet wallet : wallets) { + exhausted |= wallet.checkForFilterExhaustion(filteredBlock); + } + return exhausted; + } + + private void invokeOnBlocksDownloaded(final Block block, @Nullable final FilteredBlock filteredBlock) { + if (blockChain == null) { + return; + } + + final int blocksLeft = Math.max(0, (int) peerVersionMessage.bestHeight - blockChain.getBestChainHeight()); + for (final ListenerRegistration registration : blocksDownloadedEventListeners) { + registration.executor.execute(() -> registration.listener.onBlocksDownloaded(BlocknetPeer.this, block, filteredBlock, blocksLeft)); + } + } + + @Override + public int receiveBytes(ByteBuffer buff) { + if (buff.position() != 0 || buff.capacity() < BlocknetPacketHeader.HEADER_LENGTH + 4) { + throw new IllegalArgumentException("Buffer position is nonzero or bad header."); + } + + try { + boolean firstMessage = true; + + while (true) { + if (largeReadBuffer.get() != null) { + if (!firstMessage) { + throw new IllegalStateException("Bad firstMessage state."); + } + + int bytesToGet = Math.min(buff.remaining(), largeReadBuffer.get().length - largeReadBufferPos.get()); + byte[] buf = largeReadBuffer.get(); + buff.get(buf, largeReadBufferPos.get(), bytesToGet); + + largeReadBuffer.set(buf); + + largeReadBufferPos.set(largeReadBufferPos.get() + bytesToGet); + + if (largeReadBufferPos.get() == largeReadBuffer.get().length) { + processMessage(serializer.deserializePayload(header.get(), ByteBuffer.wrap(largeReadBuffer.get()))); + + largeReadBuffer.set(null); + header = null; + firstMessage = false; + } else { + return buff.position(); + } + } + + Message message; + int preSerializePos = buff.position(); + try { + message = serializer.deserialize(buff); + } catch (BufferUnderflowException e) { + if (firstMessage && buff.limit() == buff.capacity()) { + buff.position(0); + + try { + serializer.seekPastMagicBytes(buff); + header.set(serializer.deserializeHeader(buff)); + + largeReadBufferPos.set(buff.remaining()); + byte[] buf = new byte[header.get().getLength()]; + buff.get(buf, 0, largeReadBufferPos.get()); + largeReadBuffer.set(buf); + } catch (BufferUnderflowException e1) { + throw new ProtocolException("No magic/header after reading " + buff.capacity() + " bytes."); + } + } else { + buff.position(preSerializePos); + } + + return buff.position(); + } + + processMessage(message); + firstMessage = false; + } + } catch (Exception e) { + LOGGER.log(Level.FINER, "Error while receiving bytes!"); + e.printStackTrace(); + return -1; + } + } + + public void parsePlugins(JSONObject pluginsList) { + for (String plugin : pluginsList.keySet()) { + String rawPluginConfig = pluginsList.getString(plugin); + + XRouterConfiguration.XRouterPluginConfiguration pluginConfig = new XRouterConfiguration.XRouterPluginConfiguration(plugin, rawPluginConfig); + pluginConfig.parsePluginConfig(); + getPluginConfigurations().add(pluginConfig); + } + } + + public XRouterConfiguration.XRouterPluginConfiguration getPluginConfig(String pluginName) { + for (XRouterConfiguration.XRouterPluginConfiguration pluginConfig : pluginConfigurations) { + if (pluginConfig.getPluginName().equals(pluginName)) { + return pluginConfig; + } + } + + return null; + } + + public CopyOnWriteArrayList getMessagesPendingReply() { + return messagesPendingReply; + } + + public CopyOnWriteArrayList> getInitialMessagesSentListeners() { + return initialMessagesSentListeners; + } + + public CopyOnWriteArrayList> getXRouterMessageListeners() { + return xRouterMessageListeners; + } + + public void addInitialMessagesSentListener(XRouterInitialMessagesSentListener listener) { + initialMessagesSentListeners.add(new ListenerRegistration<>(listener, Threading.SAME_THREAD)); + } + + public void removeInitialMessagesSentListener(XRouterInitialMessagesSentListener listener) { + ListenerRegistration.removeFromList(listener, initialMessagesSentListeners); + } + + public BlocknetSeed getBlocknetSeed() { + return blocknetSeed; + } + + public AtomicBoolean getHaveConfig() { + return haveConfig; + } + + public XRouterConfiguration getxRouterConfiguration() { + return xRouterConfiguration; + } + + public ArrayList getPluginConfigurations() { + return pluginConfigurations; + } + + public boolean isActivePeer() { + return activePeer; + } + + public boolean pastConnectionSuccess() { + return pastConnectionSuccess; + } + + public boolean hasRequiredPlugins() { + return hasRequiredPlugins; + } + + public void setPastConnectionSuccess(boolean pastConnectionSuccess) { + this.pastConnectionSuccess = pastConnectionSuccess; + } + + public void setHaveConfig(boolean hasConfig) { + this.haveConfig.set(hasConfig); + } + + public void setxRouterConfiguration(XRouterConfiguration xRouterConfiguration) { + this.xRouterConfiguration = xRouterConfiguration; + } + + public void setHasRequiredPlugins(boolean hasRequiredPlugins) { + this.hasRequiredPlugins = hasRequiredPlugins; + } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeerGroup.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeerGroup.java index 80cbd78..3cd3b1d 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeerGroup.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeerGroup.java @@ -115,7 +115,8 @@ private BlocknetPeer createPeer(BlocknetParameters blocknetNetworkParameters, Bl return new BlocknetPeer(blocknetNetworkParameters, chain, peerAddress, - blocknetSeed) {}; + blocknetSeed) { + }; } @GuardedBy("lock") @@ -166,7 +167,7 @@ private void startBackgroundThreads() { threadPool.submit(new BackgroundTimerThread()); } - private ListenableFuture startAsync() { + private ListenableFuture startAsync() { executorStartupLatch.countDown(); return executor.submit(() -> { @@ -183,6 +184,7 @@ private ListenableFuture startAsync() { } catch (Throwable e) { e.printStackTrace(); } + return null; }); } @@ -591,14 +593,13 @@ private Runnable processQueue() { private boolean waitForConnection(BlocknetPeer blocknetPeer, int maxWaitSeconds) { long startTime = System.currentTimeMillis(); - while((System.currentTimeMillis() - startTime) < (maxWaitSeconds * 1000)) { + while ((System.currentTimeMillis() - startTime) < (maxWaitSeconds * 1000)) { BlocknetPeer filteredPeer = getConnectedPeers().stream().filter( e -> (e.getHaveConfig().get() && e.getAddress().getAddr() == blocknetPeer.getAddress().getAddr()) ).findFirst().orElse(null); if (filteredPeer != null) - return true; - else { + return true;else { try { Thread.sleep(100); } catch (InterruptedException e) { diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetSerializer.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetSerializer.java index 28a773a..285f2d9 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetSerializer.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetSerializer.java @@ -15,205 +15,205 @@ import java.util.logging.Logger; public class BlocknetSerializer extends BitcoinSerializer { - private final static LogManager LOGMANAGER = LogManager.getLogManager(); - private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); - - private BlocknetParameters params; - private boolean parseRetain; - - private static final HashMap, String> messageNames = new HashMap<>(); - - static { - messageNames.put(VersionMessage.class, "version"); - messageNames.put(VersionMessageImpl.class, "version"); - messageNames.put(InventoryMessage.class, "inv"); - messageNames.put(Block.class, "block"); - messageNames.put(GetDataMessage.class, "getdata"); - messageNames.put(Transaction.class, "tx"); - messageNames.put(AddressMessage.class, "addr"); - messageNames.put(Ping.class, "ping"); - messageNames.put(Pong.class, "pong"); - messageNames.put(VersionAck.class, "verack"); - messageNames.put(GetBlocksMessage.class, "getblocks"); - messageNames.put(GetHeadersMessage.class, "getheaders"); - messageNames.put(GetAddrMessage.class, "getaddr"); - messageNames.put(HeadersMessage.class, "headers"); - messageNames.put(BloomFilter.class, "filterload"); - messageNames.put(FilteredBlock.class, "merkleblock"); - messageNames.put(NotFoundMessage.class, "notfound"); - messageNames.put(MemoryPoolMessage.class, "mempool"); - messageNames.put(RejectMessage.class, "reject"); - messageNames.put(GetUTXOsMessage.class, "getutxos"); - messageNames.put(UTXOsMessage.class, "utxos"); - } - - public BlocknetSerializer(NetworkParameters params, boolean parseRetain) { - super(params, parseRetain); - - if (!(params instanceof BlocknetParameters)) { - throw new IllegalArgumentException("Invalid network parameters for Blocknet."); - } - - this.params = (BlocknetParameters) params; - this.parseRetain = parseRetain; - } - - @Override - public BlocknetPacketHeader deserializeHeader(ByteBuffer in) throws ProtocolException { - //in.position(0); - //seekPastMagicBytes(in); - return new BlocknetPacketHeader(in); - } - - @Override - public Message deserializePayload(BitcoinSerializer.BitcoinPacketHeader header, ByteBuffer in) throws ProtocolException, BufferUnderflowException { - //in.position(0); - //seekPastMagicBytes(in); - BlocknetPacketHeader blocknetPacketHeader = (BlocknetPacketHeader) header; - - byte[] payloadBytes = new byte[blocknetPacketHeader.getLength()]; - in.get(payloadBytes, 0, payloadBytes.length); - - if (!BlocknetUtils.verifyChecksum(blocknetPacketHeader, payloadBytes)) { - throw new ProtocolException("Checksum failed to verify."); - } - - switch (blocknetPacketHeader.getCommand().toLowerCase()) { - case "xrouter": - LOGGER.log(Level.FINER, "[blocknet-serializer] Received XRouter packet, at position: " + in.position()); - return new XRouterMessage(params, payloadBytes); - case "version": + private final static LogManager LOGMANAGER = LogManager.getLogManager(); + private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); + + private BlocknetParameters params; + private boolean parseRetain; + + private static final HashMap, String> messageNames = new HashMap<>(); + + static { + messageNames.put(VersionMessage.class, "version"); + messageNames.put(VersionMessageImpl.class, "version"); + messageNames.put(InventoryMessage.class, "inv"); + messageNames.put(Block.class, "block"); + messageNames.put(GetDataMessage.class, "getdata"); + messageNames.put(Transaction.class, "tx"); + messageNames.put(AddressMessage.class, "addr"); + messageNames.put(Ping.class, "ping"); + messageNames.put(Pong.class, "pong"); + messageNames.put(VersionAck.class, "verack"); + messageNames.put(GetBlocksMessage.class, "getblocks"); + messageNames.put(GetHeadersMessage.class, "getheaders"); + messageNames.put(GetAddrMessage.class, "getaddr"); + messageNames.put(HeadersMessage.class, "headers"); + messageNames.put(BloomFilter.class, "filterload"); + messageNames.put(FilteredBlock.class, "merkleblock"); + messageNames.put(NotFoundMessage.class, "notfound"); + messageNames.put(MemoryPoolMessage.class, "mempool"); + messageNames.put(RejectMessage.class, "reject"); + messageNames.put(GetUTXOsMessage.class, "getutxos"); + messageNames.put(UTXOsMessage.class, "utxos"); + } + + public BlocknetSerializer(NetworkParameters params, boolean parseRetain) { + super(params, parseRetain); + + if (!(params instanceof BlocknetParameters)) { + throw new IllegalArgumentException("Invalid network parameters for Blocknet."); + } + + this.params = (BlocknetParameters) params; + this.parseRetain = parseRetain; + } + + @Override + public BlocknetPacketHeader deserializeHeader(ByteBuffer in) throws ProtocolException { + //in.position(0); + //seekPastMagicBytes(in); + return new BlocknetPacketHeader(in); + } + + @Override + public Message deserializePayload(BitcoinSerializer.BitcoinPacketHeader header, ByteBuffer in) throws ProtocolException, BufferUnderflowException { + //in.position(0); + //seekPastMagicBytes(in); + BlocknetPacketHeader blocknetPacketHeader = (BlocknetPacketHeader) header; + + byte[] payloadBytes = new byte[blocknetPacketHeader.getLength()]; + in.get(payloadBytes, 0, payloadBytes.length); + + if (!BlocknetUtils.verifyChecksum(blocknetPacketHeader, payloadBytes)) { + throw new ProtocolException("Checksum failed to verify."); + } + + switch (blocknetPacketHeader.getCommand().toLowerCase()) { + case "xrouter": + LOGGER.log(Level.FINER, "[blocknet-serializer] Received XRouter packet, at position: " + in.position()); + return new XRouterMessage(params, payloadBytes); + case "version": // LOGGER.log(Level.FINER, "[blocknet-serializer] Version message received"); - return new VersionMessage(params, payloadBytes); - case "inv": + return new VersionMessage(params, payloadBytes); + case "inv": // LOGGER.log(Level.FINER, "[blocknet-serializer] Warning: Inventory messages are ignored"); - return null; - case "block": - return new Block(params, payloadBytes, 0, this, blocknetPacketHeader.getLength()); - case "merkleblock": - return new FilteredBlock(params, payloadBytes); - case "getdata": - return new GetDataMessage(params, payloadBytes, this, blocknetPacketHeader.getLength()); - case "getblocks": - return new GetBlocksMessage(params, payloadBytes); - case "getheaders": - return new GetHeadersMessage(params, payloadBytes); - case "tx": - return new Transaction(params, payloadBytes, 0, null, this, blocknetPacketHeader.getLength()); - case "addr": - return makeAddressMessage(payloadBytes, blocknetPacketHeader.getLength()); - case "alert": - return new AlertMessage(params, payloadBytes); - case "ping": - return new Ping(params, payloadBytes); - case "pong": - return new Pong(params, payloadBytes); - case "verack": - return new VersionAck(params, payloadBytes); - case "headers": - return new HeadersMessage(params, payloadBytes); - case "filterload": - return new BloomFilter(params, payloadBytes); - case "notfound": - return new NotFoundMessage(params, payloadBytes); - case "mempool": - return new MemoryPoolMessage(); - case "reject": - return new RejectMessage(params, payloadBytes); - case "utxos": - return new UTXOsMessage(params, payloadBytes); - case "getutxos": - return new GetUTXOsMessage(params, payloadBytes); - case "getsporks": - case "ssc": - case "mnget": - case "xbridge": + return null; + case "block": + return new Block(params, payloadBytes, 0, this, blocknetPacketHeader.getLength()); + case "merkleblock": + return new FilteredBlock(params, payloadBytes); + case "getdata": + return new GetDataMessage(params, payloadBytes, this, blocknetPacketHeader.getLength()); + case "getblocks": + return new GetBlocksMessage(params, payloadBytes); + case "getheaders": + return new GetHeadersMessage(params, payloadBytes); + case "tx": + return new Transaction(params, payloadBytes, 0, null, this, blocknetPacketHeader.getLength()); + case "addr": + return makeAddressMessage(payloadBytes, blocknetPacketHeader.getLength()); + case "alert": + return new AlertMessage(params, payloadBytes); + case "ping": + return new Ping(params, payloadBytes); + case "pong": + return new Pong(params, payloadBytes); + case "verack": + return new VersionAck(params, payloadBytes); + case "headers": + return new HeadersMessage(params, payloadBytes); + case "filterload": + return new BloomFilter(params, payloadBytes); + case "notfound": + return new NotFoundMessage(params, payloadBytes); + case "mempool": + return new MemoryPoolMessage(); + case "reject": + return new RejectMessage(params, payloadBytes); + case "utxos": + return new UTXOsMessage(params, payloadBytes); + case "getutxos": + return new GetUTXOsMessage(params, payloadBytes); + case "getsporks": + case "ssc": + case "mnget": + case "xbridge": // LOGGER.log(Level.FINER, "[blocknet-serializer] Warning: This serializer does not support deserializing xbridge/ssc/mnget/getsporks packets yet."); - return null; - case "dseg": + return null; + case "dseg": // LOGGER.log(Level.FINER, "[blocknet-serializer] Warning: This serializer does not support deserializing dseg packets yet."); - return null; - default: - LOGGER.log(Level.FINER, "[blocknet-serializer] Warning: This serializer does not support deserializing " + blocknetPacketHeader.getCommand() + " packets (yet)."); - return new UnknownMessage(params, blocknetPacketHeader.getCommand(), payloadBytes); - } - } - - @Override - public Message deserialize(ByteBuffer in) throws ProtocolException { - //in.position(0); - seekPastMagicBytes(in); - BlocknetPacketHeader header = new BlocknetPacketHeader(in); - - return deserializePayload(header, in); - } - - @Override - public boolean isParseRetainMode() { - return parseRetain; - } - - @Override - public AddressMessage makeAddressMessage(byte[] payloadBytes, int length) throws ProtocolException, UnsupportedOperationException { - return null; - } - - @Override - public Message makeAlertMessage(byte[] payloadBytes) throws ProtocolException, UnsupportedOperationException { - return new AlertMessage(params, payloadBytes); - } - - @Override - public Block makeBlock(byte[] payloadBytes, int offset, int length) throws ProtocolException, UnsupportedOperationException { - return new Block(params, payloadBytes, offset, this, length); - } - - @Override - public Message makeBloomFilter(byte[] payloadBytes) throws ProtocolException, UnsupportedOperationException { - return new BloomFilter(params, payloadBytes); - } - - @Override - public FilteredBlock makeFilteredBlock(byte[] payloadBytes) throws ProtocolException, UnsupportedOperationException { - return new FilteredBlock(params, payloadBytes); - } - - @Override - public InventoryMessage makeInventoryMessage(byte[] payloadBytes, int length) throws ProtocolException, UnsupportedOperationException { - return new InventoryMessage(params, payloadBytes, this, length); - } - - @Override - public Transaction makeTransaction(byte[] payloadBytes, int offset, int length, byte[] hash) throws ProtocolException, UnsupportedOperationException { - return new Transaction(params, payloadBytes, offset, null, this, length); - } - - @Override - public void seekPastMagicBytes(ByteBuffer in) throws BufferUnderflowException { - BlocknetUtils.seekPastMagicBytes(in, params); - } - - @Override - public void serialize(String name, byte[] message, OutputStream out) throws IOException, UnsupportedOperationException { - byte[] header = BlocknetUtils.getHeader(name, (int) params.getPacketMagic(), message); - - out.write(header); - out.write(message); - - LOGGER.log(Level.FINER, "[blocknet-serializer] Serialized " + name + " message. Bytes: " + new String(Hex.encode(header)) + new String(Hex.encode(message))); - } - - @Override - public void serialize(Message message, OutputStream out) throws IOException { - if (message instanceof XRouterMessage) { - params.getXRouterMessageSerializer(parseRetain).serialize(message, out); - } else { - String name = messageNames.get(message.getClass()); - if (name == null) { - LOGGER.log(Level.FINER, "[blocknet-serializer] ERROR: BlocknetSerializer cannot serialize " + message.getClass().getSimpleName() + " (yet)!"); - return; - } - serialize(name, message.bitcoinSerialize(), out); - } - } + return null; + default: + LOGGER.log(Level.FINER, "[blocknet-serializer] Warning: This serializer does not support deserializing " + blocknetPacketHeader.getCommand() + " packets (yet)."); + return new UnknownMessage(params, blocknetPacketHeader.getCommand(), payloadBytes); + } + } + + @Override + public Message deserialize(ByteBuffer in) throws ProtocolException { + //in.position(0); + seekPastMagicBytes(in); + BlocknetPacketHeader header = new BlocknetPacketHeader(in); + + return deserializePayload(header, in); + } + + @Override + public boolean isParseRetainMode() { + return parseRetain; + } + + @Override + public AddressMessage makeAddressMessage(byte[] payloadBytes, int length) throws ProtocolException, UnsupportedOperationException { + return null; + } + + @Override + public Message makeAlertMessage(byte[] payloadBytes) throws ProtocolException, UnsupportedOperationException { + return new AlertMessage(params, payloadBytes); + } + + @Override + public Block makeBlock(byte[] payloadBytes, int offset, int length) throws ProtocolException, UnsupportedOperationException { + return new Block(params, payloadBytes, offset, this, length); + } + + @Override + public Message makeBloomFilter(byte[] payloadBytes) throws ProtocolException, UnsupportedOperationException { + return new BloomFilter(params, payloadBytes); + } + + @Override + public FilteredBlock makeFilteredBlock(byte[] payloadBytes) throws ProtocolException, UnsupportedOperationException { + return new FilteredBlock(params, payloadBytes); + } + + @Override + public InventoryMessage makeInventoryMessage(byte[] payloadBytes, int length) throws ProtocolException, UnsupportedOperationException { + return new InventoryMessage(params, payloadBytes, this, length); + } + + @Override + public Transaction makeTransaction(byte[] payloadBytes, int offset, int length, byte[] hash) throws ProtocolException, UnsupportedOperationException { + return new Transaction(params, payloadBytes, offset, null, this, length); + } + + @Override + public void seekPastMagicBytes(ByteBuffer in) throws BufferUnderflowException { + BlocknetUtils.seekPastMagicBytes(in, params); + } + + @Override + public void serialize(String name, byte[] message, OutputStream out) throws IOException, UnsupportedOperationException { + byte[] header = BlocknetUtils.getHeader(name, (int) params.getPacketMagic(), message); + + out.write(header); + out.write(message); + + LOGGER.log(Level.FINER, "[blocknet-serializer] Serialized " + name + " message. Bytes: " + new String(Hex.encode(header)) + new String(Hex.encode(message))); + } + + @Override + public void serialize(Message message, OutputStream out) throws IOException { + if (message instanceof XRouterMessage) { + params.getXRouterMessageSerializer(parseRetain).serialize(message, out); + } else { + String name = messageNames.get(message.getClass()); + if (name == null) { + LOGGER.log(Level.FINER, "[blocknet-serializer] ERROR: BlocknetSerializer cannot serialize " + message.getClass().getSimpleName() + " (yet)!"); + return; + } + serialize(name, message.bitcoinSerialize(), out); + } + } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetTestnet5NetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetTestnet5NetworkParameters.java index 340a2fc..8618632 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetTestnet5NetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetTestnet5NetworkParameters.java @@ -10,153 +10,153 @@ public class BlocknetTestnet5NetworkParameters extends BlocknetParameters { - public BlocknetTestnet5NetworkParameters() { - super(); - } - - @Override - public String getPaymentProtocolId() { - return "test"; - } - - @Override - public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException { - - } - - @Override - public int[] getAcceptableAddressCodes() { - return new int[] {getAddressHeader(), getP2SHHeader()}; - } - - @Override - public Sha256Hash getGenesisBlockHash() { - return Sha256Hash.wrap("0fd62ae4f74c7ee0c11ef60fc5a2e69a5c02eaee2e77b21c3db70934b5a5c8b9"); - } - - @Override - public Coin getMaxMoney() { - return Coin.valueOf(43199500).times(Coin.COIN.value); - } - - @Override - public Coin getMinNonDustOutput() { - return Coin.valueOf(5500); - } - - @Override - public MonetaryFormat getMonetaryFormat() { - return new MonetaryFormat().code(0, "tBLOCK"); - } - - @Override - public String getUriScheme() { - return "blockdx:"; - } - - @Override - public boolean hasMaxMoney() { - return true; - } - - @Override - public BlocknetSerializer getSerializer(boolean parseRetain) { - return new BlocknetSerializer(this, parseRetain); - } - - public XRouterMessageSerializer getXRouterMessageSerializer(boolean parseRetain) { - return new XRouterMessageSerializer(parseRetain, this); - } - - @Override - public int getProtocolVersionNum(ProtocolVersion version) { - return 70712; - } - - @Override - public int getSubsidyDecreaseBlockCount() { - return 210000; - } - - @Override - public byte[] getAlertSigningKey() { - return Hex.decode("000010e83b2703ccf322f7dbd62dd5855ac7c10bd055814ce121ba32607d573b8810c02c0582aed05b4deb9c4b77b26d92428c61256cd42774babea0a073b2ed0c9"); - } - - @Override - public int getMajorityEnforceBlockUpgrade() { - return 51; - } - - @Override - public int getMajorityRejectBlockOutdated() { - return 75; - } - - @Override - public int getMajorityWindow() { - return 100; - } - - @Override - public int getPort() { - return 41474; - } - - @Override - public long getPacketMagic() { - return 0x457665BAL; - } - - @Override - public int getInterval() { - return 1; - } - - @Override - public int getTargetTimespan() { - return 60; - } - - @Override - public int getAddressHeader() { - return 139; - } - - @Override - public int getP2SHHeader() { - return 19; - } - - @Override - public int getDumpedPrivateKeyHeader() { - return 239; - } - - @Override - public int getBip32HeaderPub() { - return 0x3A8061A0; - } - - @Override - public int getBip32HeaderPriv() { - return 0x3A805837; - } - - @Override - public String[] getDnsSeeds() { - return new String[] { - "104.238.198.122", + public BlocknetTestnet5NetworkParameters() { + super(); + } + + @Override + public String getPaymentProtocolId() { + return "test"; + } + + @Override + public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException { + + } + + @Override + public int[] getAcceptableAddressCodes() { + return new int[]{getAddressHeader(), getP2SHHeader()}; + } + + @Override + public Sha256Hash getGenesisBlockHash() { + return Sha256Hash.wrap("0fd62ae4f74c7ee0c11ef60fc5a2e69a5c02eaee2e77b21c3db70934b5a5c8b9"); + } + + @Override + public Coin getMaxMoney() { + return Coin.valueOf(43199500).times(Coin.COIN.value); + } + + @Override + public Coin getMinNonDustOutput() { + return Coin.valueOf(5500); + } + + @Override + public MonetaryFormat getMonetaryFormat() { + return new MonetaryFormat().code(0, "tBLOCK"); + } + + @Override + public String getUriScheme() { + return "blockdx:"; + } + + @Override + public boolean hasMaxMoney() { + return true; + } + + @Override + public BlocknetSerializer getSerializer(boolean parseRetain) { + return new BlocknetSerializer(this, parseRetain); + } + + public XRouterMessageSerializer getXRouterMessageSerializer(boolean parseRetain) { + return new XRouterMessageSerializer(parseRetain, this); + } + + @Override + public int getProtocolVersionNum(ProtocolVersion version) { + return 70712; + } + + @Override + public int getSubsidyDecreaseBlockCount() { + return 210000; + } + + @Override + public byte[] getAlertSigningKey() { + return Hex.decode("000010e83b2703ccf322f7dbd62dd5855ac7c10bd055814ce121ba32607d573b8810c02c0582aed05b4deb9c4b77b26d92428c61256cd42774babea0a073b2ed0c9"); + } + + @Override + public int getMajorityEnforceBlockUpgrade() { + return 51; + } + + @Override + public int getMajorityRejectBlockOutdated() { + return 75; + } + + @Override + public int getMajorityWindow() { + return 100; + } + + @Override + public int getPort() { + return 41474; + } + + @Override + public long getPacketMagic() { + return 0x457665BAL; + } + + @Override + public int getInterval() { + return 1; + } + + @Override + public int getTargetTimespan() { + return 60; + } + + @Override + public int getAddressHeader() { + return 139; + } + + @Override + public int getP2SHHeader() { + return 19; + } + + @Override + public int getDumpedPrivateKeyHeader() { + return 239; + } + + @Override + public int getBip32HeaderPub() { + return 0x3A8061A0; + } + + @Override + public int getBip32HeaderPriv() { + return 0x3A805837; + } + + @Override + public String[] getDnsSeeds() { + return new String[]{ + "104.238.198.122", }; - } + } - @Override - public BigInteger getMaxTarget() { - return Utils.decodeCompactBits(0x203FFFFF); - } + @Override + public BigInteger getMaxTarget() { + return Utils.decodeCompactBits(0x203FFFFF); + } - @Override - public String getId() { - return "tBLOCK"; - } + @Override + public String getId() { + return "tBLOCK"; + } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetUtils.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetUtils.java index 7ddd050..47d2fe9 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetUtils.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetUtils.java @@ -8,47 +8,47 @@ public class BlocknetUtils { - public static void seekPastMagicBytes(ByteBuffer in, BlocknetParameters params) { - int magicCursor = 3; - while (true) { - byte b = in.get(); - - byte expectedByte = (byte)(0xFF & params.getPacketMagic() >>> (magicCursor * 8)); - if (b == expectedByte) { - magicCursor--; - if (magicCursor < 0) { - return; - } - } else { - magicCursor = 3; - } - } - } - - public static boolean verifyChecksum(BlocknetPacketHeader header, byte[] payloadBytes) { - byte[] hash = Sha256Hash.hashTwice(payloadBytes); - byte[] headerChecksum = new byte[4]; - byte[] checksum = new byte[4]; - System.arraycopy(header.getChecksum(), 0, headerChecksum, 0, 4); - System.arraycopy(hash, 0, checksum, 0, 4); - - return Arrays.equals(headerChecksum, checksum); - } - - public static byte[] getHeader(String name, int magic, byte[] payloadBytes) { - byte[] header = new byte[BlocknetPacketHeader.HEADER_LENGTH + 4]; - - Utils.uint32ToByteArrayBE(magic, header, 0); - - for (int i = 0; i < name.length() && i < 12; i++) { - header[4 + i] = (byte) (name.codePointAt(i) & 0xFF); - } - - Utils.uint32ToByteArrayLE(payloadBytes.length, header, 4 + 12); - byte[] hash = Sha256Hash.hashTwice(payloadBytes); - System.arraycopy(hash, 0, header, 4 + 12 + 4, 4); - - return header; - } + public static void seekPastMagicBytes(ByteBuffer in, BlocknetParameters params) { + int magicCursor = 3; + while (true) { + byte b = in.get(); + + byte expectedByte = (byte) (0xFF & params.getPacketMagic() >>> (magicCursor * 8)); + if (b == expectedByte) { + magicCursor--; + if (magicCursor < 0) { + return; + } + } else { + magicCursor = 3; + } + } + } + + public static boolean verifyChecksum(BlocknetPacketHeader header, byte[] payloadBytes) { + byte[] hash = Sha256Hash.hashTwice(payloadBytes); + byte[] headerChecksum = new byte[4]; + byte[] checksum = new byte[4]; + System.arraycopy(header.getChecksum(), 0, headerChecksum, 0, 4); + System.arraycopy(hash, 0, checksum, 0, 4); + + return Arrays.equals(headerChecksum, checksum); + } + + public static byte[] getHeader(String name, int magic, byte[] payloadBytes) { + byte[] header = new byte[BlocknetPacketHeader.HEADER_LENGTH + 4]; + + Utils.uint32ToByteArrayBE(magic, header, 0); + + for (int i = 0; i < name.length() && i < 12; i++) { + header[4 + i] = (byte) (name.codePointAt(i) & 0xFF); + } + + Utils.uint32ToByteArrayLE(payloadBytes.length, header, 4 + 12); + byte[] hash = Sha256Hash.hashTwice(payloadBytes); + System.arraycopy(hash, 0, header, 4 + 12 + 4, 4); + + return header; + } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetOnBlocksDownloadedEventListener.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetOnBlocksDownloadedEventListener.java index 1b4301d..91611ef 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetOnBlocksDownloadedEventListener.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetOnBlocksDownloadedEventListener.java @@ -6,6 +6,6 @@ public interface BlocknetOnBlocksDownloadedEventListener { - void onBlocksDownloaded(BlocknetPeer peer, Block block, FilteredBlock filteredBlock, int blocksLeft); + void onBlocksDownloaded(BlocknetPeer peer, Block block, FilteredBlock filteredBlock, int blocksLeft); } diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetOnXRouterMessageReceivedListener.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetOnXRouterMessageReceivedListener.java index 990f011..973549e 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetOnXRouterMessageReceivedListener.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetOnXRouterMessageReceivedListener.java @@ -4,6 +4,6 @@ public interface BlocknetOnXRouterMessageReceivedListener { - void onXRouterMessageReceived(XRouterMessage message, XRouterMessage original); + void onXRouterMessageReceived(XRouterMessage message, XRouterMessage original); } diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetPeerConnectedEventListener.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetPeerConnectedEventListener.java index 8e75453..7ff5ef5 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetPeerConnectedEventListener.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetPeerConnectedEventListener.java @@ -4,5 +4,5 @@ public interface BlocknetPeerConnectedEventListener { - void onPeerConnected(BlocknetPeer peer, int peerCount); + void onPeerConnected(BlocknetPeer peer, int peerCount); } diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetPeerDisconnectedEventListener.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetPeerDisconnectedEventListener.java index cb98bc6..4736a0e 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetPeerDisconnectedEventListener.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetPeerDisconnectedEventListener.java @@ -4,6 +4,6 @@ public interface BlocknetPeerDisconnectedEventListener { - void onPeerDisconnected(BlocknetPeer peer, int peerCount); + void onPeerDisconnected(BlocknetPeer peer, int peerCount); } diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetPreMessageReceivedEventListener.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetPreMessageReceivedEventListener.java index 0647fcd..7ba433b 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetPreMessageReceivedEventListener.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetPreMessageReceivedEventListener.java @@ -5,6 +5,6 @@ public interface BlocknetPreMessageReceivedEventListener { - Message onPreMessageReceived(BlocknetPeer peer, Message message); + Message onPreMessageReceived(BlocknetPeer peer, Message message); } diff --git a/src/main/java/io/cloudchains/app/net/protocols/dashcoin/DashcoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/dashcoin/DashcoinNetworkParameters.java index a54f828..39ca8f1 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/dashcoin/DashcoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/dashcoin/DashcoinNetworkParameters.java @@ -7,95 +7,96 @@ public class DashcoinNetworkParameters extends NetworkParameters { - public DashcoinNetworkParameters() { - super(); - } - - @Override - public String getPaymentProtocolId() { - return "main"; - } - - @Override - public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException {} - - @Override - public Coin getMaxMoney() { - return Coin.valueOf(22000000 * Coin.COIN.value); - } - - @Override - public Coin getMinNonDustOutput() { - return Coin.valueOf(5460); - } - - @Override - public MonetaryFormat getMonetaryFormat() { - return new MonetaryFormat().code(0, "DASH"); - } - - @Override - public String getUriScheme() { - return "dashcoin:"; - } - - @Override - public boolean hasMaxMoney() { - return true; - } - - @Override - public BitcoinSerializer getSerializer(boolean parseRetain) { - return new BitcoinSerializer(this, parseRetain); - } - - @Override - public int getProtocolVersionNum(ProtocolVersion version) { - return 70210; - } - - @Override - public int getAddressHeader() { - return 76; - } - - @Override - public int getP2SHHeader() { - return 16; - } - - @Override - public int getDumpedPrivateKeyHeader() { - return 204; - } - - @Override - public int[] getAcceptableAddressCodes() { - return new int[] {getAddressHeader(), getP2SHHeader()}; - } - - @Override - public int getBip32HeaderPriv() { - return 0x0488ADE4; - } - - @Override - public int getBip32HeaderPub() { - return 0x0488B21E; - } - - @Override - public int getSubsidyDecreaseBlockCount() { - return 210240; - } - - @Override - public int getInterval() { - return 57; - } - - @Override - public String getId() { - return "DASH"; - } + public DashcoinNetworkParameters() { + super(); + } + + @Override + public String getPaymentProtocolId() { + return "main"; + } + + @Override + public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException { + } + + @Override + public Coin getMaxMoney() { + return Coin.valueOf(22000000 * Coin.COIN.value); + } + + @Override + public Coin getMinNonDustOutput() { + return Coin.valueOf(5460); + } + + @Override + public MonetaryFormat getMonetaryFormat() { + return new MonetaryFormat().code(0, "DASH"); + } + + @Override + public String getUriScheme() { + return "dashcoin:"; + } + + @Override + public boolean hasMaxMoney() { + return true; + } + + @Override + public BitcoinSerializer getSerializer(boolean parseRetain) { + return new BitcoinSerializer(this, parseRetain); + } + + @Override + public int getProtocolVersionNum(ProtocolVersion version) { + return 70210; + } + + @Override + public int getAddressHeader() { + return 76; + } + + @Override + public int getP2SHHeader() { + return 16; + } + + @Override + public int getDumpedPrivateKeyHeader() { + return 204; + } + + @Override + public int[] getAcceptableAddressCodes() { + return new int[]{getAddressHeader(), getP2SHHeader()}; + } + + @Override + public int getBip32HeaderPriv() { + return 0x0488ADE4; + } + + @Override + public int getBip32HeaderPub() { + return 0x0488B21E; + } + + @Override + public int getSubsidyDecreaseBlockCount() { + return 210240; + } + + @Override + public int getInterval() { + return 57; + } + + @Override + public String getId() { + return "DASH"; + } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/digibyte/DigibyteNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/digibyte/DigibyteNetworkParameters.java index db571ed..e2677f4 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/digibyte/DigibyteNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/digibyte/DigibyteNetworkParameters.java @@ -7,95 +7,96 @@ public class DigibyteNetworkParameters extends NetworkParameters { - public DigibyteNetworkParameters() { - super(); - } - - @Override - public String getPaymentProtocolId() { - return "main"; - } - - @Override - public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException {} - - @Override - public Coin getMaxMoney() { - return Coin.valueOf(2000000000 * Coin.COIN.value); - } - - @Override - public Coin getMinNonDustOutput() { - return Coin.valueOf(1000); - } - - @Override - public MonetaryFormat getMonetaryFormat() { - return new MonetaryFormat().code(0, "DGB"); - } - - @Override - public String getUriScheme() { - return "digibyte:"; - } - - @Override - public boolean hasMaxMoney() { - return true; - } - - @Override - public BitcoinSerializer getSerializer(boolean parseRetain) { - return new BitcoinSerializer(this, parseRetain); - } - - @Override - public int getProtocolVersionNum(ProtocolVersion version) { - return 70002; - } - - @Override - public int getAddressHeader() { - return 30; - } - - @Override - public int getP2SHHeader() { - return 5; - } - - @Override - public int getDumpedPrivateKeyHeader() { - return 128; - } - - @Override - public int[] getAcceptableAddressCodes() { - return new int[] {getAddressHeader(), getP2SHHeader()}; - } - - @Override - public int getBip32HeaderPriv() { - return 0x0488ADE4; - } - - @Override - public int getBip32HeaderPub() { - return 0x0488B21E; - } - - @Override - public int getSubsidyDecreaseBlockCount() { - return 100000; - } - - @Override - public int getInterval() { - return 108; - } - - @Override - public String getId() { - return "DGB"; - } + public DigibyteNetworkParameters() { + super(); + } + + @Override + public String getPaymentProtocolId() { + return "main"; + } + + @Override + public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException { + } + + @Override + public Coin getMaxMoney() { + return Coin.valueOf(2000000000 * Coin.COIN.value); + } + + @Override + public Coin getMinNonDustOutput() { + return Coin.valueOf(1000); + } + + @Override + public MonetaryFormat getMonetaryFormat() { + return new MonetaryFormat().code(0, "DGB"); + } + + @Override + public String getUriScheme() { + return "digibyte:"; + } + + @Override + public boolean hasMaxMoney() { + return true; + } + + @Override + public BitcoinSerializer getSerializer(boolean parseRetain) { + return new BitcoinSerializer(this, parseRetain); + } + + @Override + public int getProtocolVersionNum(ProtocolVersion version) { + return 70002; + } + + @Override + public int getAddressHeader() { + return 30; + } + + @Override + public int getP2SHHeader() { + return 5; + } + + @Override + public int getDumpedPrivateKeyHeader() { + return 128; + } + + @Override + public int[] getAcceptableAddressCodes() { + return new int[]{getAddressHeader(), getP2SHHeader()}; + } + + @Override + public int getBip32HeaderPriv() { + return 0x0488ADE4; + } + + @Override + public int getBip32HeaderPub() { + return 0x0488B21E; + } + + @Override + public int getSubsidyDecreaseBlockCount() { + return 100000; + } + + @Override + public int getInterval() { + return 108; + } + + @Override + public String getId() { + return "DGB"; + } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/dogecoin/DogecoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/dogecoin/DogecoinNetworkParameters.java index 7390103..4c41d02 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/dogecoin/DogecoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/dogecoin/DogecoinNetworkParameters.java @@ -7,95 +7,96 @@ public class DogecoinNetworkParameters extends NetworkParameters { - public DogecoinNetworkParameters() { - super(); - } - - @Override - public String getPaymentProtocolId() { - return "main"; - } - - @Override - public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException {} - - @Override - public Coin getMaxMoney() { - return Coin.valueOf(2000000000 * Coin.COIN.value); - } - - @Override - public Coin getMinNonDustOutput() { - return Transaction.MIN_NONDUST_OUTPUT; - } - - @Override - public MonetaryFormat getMonetaryFormat() { - return new MonetaryFormat().code(0, "DOGE"); - } - - @Override - public String getUriScheme() { - return "dogecoin:"; - } - - @Override - public boolean hasMaxMoney() { - return true; - } - - @Override - public BitcoinSerializer getSerializer(boolean parseRetain) { - return new BitcoinSerializer(this, parseRetain); - } - - @Override - public int getProtocolVersionNum(ProtocolVersion version) { - return 70004; - } - - @Override - public int getAddressHeader() { - return 30; - } - - @Override - public int getP2SHHeader() { - return 22; - } - - @Override - public int getDumpedPrivateKeyHeader() { - return 158; - } - - @Override - public int[] getAcceptableAddressCodes() { - return new int[] {getAddressHeader(), getP2SHHeader()}; - } - - @Override - public int getBip32HeaderPriv() { - return 0x02fac398; - } - - @Override - public int getBip32HeaderPub() { - return 0x02facafd; - } - - @Override - public int getSubsidyDecreaseBlockCount() { - return 100000; - } - - @Override - public int getInterval() { - return 108; - } - - @Override - public String getId() { - return "DOGE"; - } + public DogecoinNetworkParameters() { + super(); + } + + @Override + public String getPaymentProtocolId() { + return "main"; + } + + @Override + public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException { + } + + @Override + public Coin getMaxMoney() { + return Coin.valueOf(2000000000 * Coin.COIN.value); + } + + @Override + public Coin getMinNonDustOutput() { + return Transaction.MIN_NONDUST_OUTPUT; + } + + @Override + public MonetaryFormat getMonetaryFormat() { + return new MonetaryFormat().code(0, "DOGE"); + } + + @Override + public String getUriScheme() { + return "dogecoin:"; + } + + @Override + public boolean hasMaxMoney() { + return true; + } + + @Override + public BitcoinSerializer getSerializer(boolean parseRetain) { + return new BitcoinSerializer(this, parseRetain); + } + + @Override + public int getProtocolVersionNum(ProtocolVersion version) { + return 70004; + } + + @Override + public int getAddressHeader() { + return 30; + } + + @Override + public int getP2SHHeader() { + return 22; + } + + @Override + public int getDumpedPrivateKeyHeader() { + return 158; + } + + @Override + public int[] getAcceptableAddressCodes() { + return new int[]{getAddressHeader(), getP2SHHeader()}; + } + + @Override + public int getBip32HeaderPriv() { + return 0x02fac398; + } + + @Override + public int getBip32HeaderPub() { + return 0x02facafd; + } + + @Override + public int getSubsidyDecreaseBlockCount() { + return 100000; + } + + @Override + public int getInterval() { + return 108; + } + + @Override + public String getId() { + return "DOGE"; + } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/litecoin/LitecoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/litecoin/LitecoinNetworkParameters.java index d2e0d7b..90e3af6 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/litecoin/LitecoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/litecoin/LitecoinNetworkParameters.java @@ -7,99 +7,100 @@ public class LitecoinNetworkParameters extends NetworkParameters { - public LitecoinNetworkParameters() { - super(); - } - - @Override - public String getPaymentProtocolId() { - return "main"; - } - - @Override - public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException {} - - @Override - public Coin getMaxMoney() { - return Coin.valueOf(84000000 * Coin.COIN.value); - } - - @Override - public Coin getMinNonDustOutput() { - return Coin.valueOf(100000); - } - - @Override - public MonetaryFormat getMonetaryFormat() { - return new MonetaryFormat().code(0, "LTC"); - } - - @Override - public String getUriScheme() { - return "litecoin:"; - } - - @Override - public boolean hasMaxMoney() { - return true; - } - - @Override - public BitcoinSerializer getSerializer(boolean parseRetain) { - return new BitcoinSerializer(this, parseRetain); - } - - @Override - public int getProtocolVersionNum(ProtocolVersion version) { - return 70015; - } - - @Override - public int getAddressHeader() { - return 48; - } - - @Override - public int getP2SHHeader() { - return 50; - } - - public int getP2SHLegacyHeader() { - return 5; - } - - @Override - public int getDumpedPrivateKeyHeader() { - return 176; - } - - @Override - public int[] getAcceptableAddressCodes() { - return new int[] {getAddressHeader(), getP2SHHeader(), getP2SHLegacyHeader()}; - } - - @Override - public int getBip32HeaderPriv() { - return 0x0488B21E; - } - - @Override - public int getBip32HeaderPub() { - return 0x0488ADE4; - } - - @Override - public int getSubsidyDecreaseBlockCount() { - return 840000; - } - - @Override - public int getInterval() { - return 2016; - } - - @Override - public String getId() { - return "LTC"; - } + public LitecoinNetworkParameters() { + super(); + } + + @Override + public String getPaymentProtocolId() { + return "main"; + } + + @Override + public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException { + } + + @Override + public Coin getMaxMoney() { + return Coin.valueOf(84000000 * Coin.COIN.value); + } + + @Override + public Coin getMinNonDustOutput() { + return Coin.valueOf(100000); + } + + @Override + public MonetaryFormat getMonetaryFormat() { + return new MonetaryFormat().code(0, "LTC"); + } + + @Override + public String getUriScheme() { + return "litecoin:"; + } + + @Override + public boolean hasMaxMoney() { + return true; + } + + @Override + public BitcoinSerializer getSerializer(boolean parseRetain) { + return new BitcoinSerializer(this, parseRetain); + } + + @Override + public int getProtocolVersionNum(ProtocolVersion version) { + return 70015; + } + + @Override + public int getAddressHeader() { + return 48; + } + + @Override + public int getP2SHHeader() { + return 50; + } + + public int getP2SHLegacyHeader() { + return 5; + } + + @Override + public int getDumpedPrivateKeyHeader() { + return 176; + } + + @Override + public int[] getAcceptableAddressCodes() { + return new int[]{getAddressHeader(), getP2SHHeader(), getP2SHLegacyHeader()}; + } + + @Override + public int getBip32HeaderPriv() { + return 0x0488B21E; + } + + @Override + public int getBip32HeaderPub() { + return 0x0488ADE4; + } + + @Override + public int getSubsidyDecreaseBlockCount() { + return 840000; + } + + @Override + public int getInterval() { + return 2016; + } + + @Override + public String getId() { + return "LTC"; + } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/phorecoin/PhorecoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/phorecoin/PhorecoinNetworkParameters.java index 607a935..d07c025 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/phorecoin/PhorecoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/phorecoin/PhorecoinNetworkParameters.java @@ -7,95 +7,96 @@ public class PhorecoinNetworkParameters extends NetworkParameters { - public PhorecoinNetworkParameters() { - super(); - } - - @Override - public String getPaymentProtocolId() { - return "main"; - } - - @Override - public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException {} - - @Override - public Coin getMaxMoney() { - return Coin.valueOf(100000000 * Coin.COIN.value); - } - - @Override - public Coin getMinNonDustOutput() { - return Transaction.MIN_NONDUST_OUTPUT; - } - - @Override - public MonetaryFormat getMonetaryFormat() { - return new MonetaryFormat().code(0, "PHR"); - } - - @Override - public String getUriScheme() { - return "phore:"; - } - - @Override - public boolean hasMaxMoney() { - return true; - } - - @Override - public BitcoinSerializer getSerializer(boolean parseRetain) { - return new BitcoinSerializer(this, parseRetain); - } - - @Override - public int getProtocolVersionNum(ProtocolVersion version) { - return 70007; - } - - @Override - public int getAddressHeader() { - return 55; - } - - @Override - public int getP2SHHeader() { - return 13; - } - - @Override - public int getDumpedPrivateKeyHeader() { - return 212; - } - - @Override - public int[] getAcceptableAddressCodes() { - return new int[] {getAddressHeader(), getP2SHHeader()}; - } - - @Override - public int getBip32HeaderPriv() { - return 0x0221312B; - } - - @Override - public int getBip32HeaderPub() { - return 0x022D2533; - } - - @Override - public int getSubsidyDecreaseBlockCount() { - return 210240; - } - - @Override - public int getInterval() { - return 60; - } - - @Override - public String getId() { - return "PHR"; - } + public PhorecoinNetworkParameters() { + super(); + } + + @Override + public String getPaymentProtocolId() { + return "main"; + } + + @Override + public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException { + } + + @Override + public Coin getMaxMoney() { + return Coin.valueOf(100000000 * Coin.COIN.value); + } + + @Override + public Coin getMinNonDustOutput() { + return Transaction.MIN_NONDUST_OUTPUT; + } + + @Override + public MonetaryFormat getMonetaryFormat() { + return new MonetaryFormat().code(0, "PHR"); + } + + @Override + public String getUriScheme() { + return "phore:"; + } + + @Override + public boolean hasMaxMoney() { + return true; + } + + @Override + public BitcoinSerializer getSerializer(boolean parseRetain) { + return new BitcoinSerializer(this, parseRetain); + } + + @Override + public int getProtocolVersionNum(ProtocolVersion version) { + return 70007; + } + + @Override + public int getAddressHeader() { + return 55; + } + + @Override + public int getP2SHHeader() { + return 13; + } + + @Override + public int getDumpedPrivateKeyHeader() { + return 212; + } + + @Override + public int[] getAcceptableAddressCodes() { + return new int[]{getAddressHeader(), getP2SHHeader()}; + } + + @Override + public int getBip32HeaderPriv() { + return 0x0221312B; + } + + @Override + public int getBip32HeaderPub() { + return 0x022D2533; + } + + @Override + public int getSubsidyDecreaseBlockCount() { + return 210240; + } + + @Override + public int getInterval() { + return 60; + } + + @Override + public String getId() { + return "PHR"; + } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/pivx/PivxNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/pivx/PivxNetworkParameters.java index 3fb808b..157fecf 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/pivx/PivxNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/pivx/PivxNetworkParameters.java @@ -7,95 +7,96 @@ public class PivxNetworkParameters extends NetworkParameters { - public PivxNetworkParameters() { - super(); - } - - @Override - public String getPaymentProtocolId() { - return "main"; - } - - @Override - public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException {} - - @Override - public Coin getMaxMoney() { - return Coin.valueOf(100000000 * Coin.COIN.value); - } - - @Override - public Coin getMinNonDustOutput() { - return Transaction.MIN_NONDUST_OUTPUT; - } - - @Override - public MonetaryFormat getMonetaryFormat() { - return new MonetaryFormat().code(0, "PIVX"); - } - - @Override - public String getUriScheme() { - return "pivx:"; - } - - @Override - public boolean hasMaxMoney() { - return true; - } - - @Override - public BitcoinSerializer getSerializer(boolean parseRetain) { - return new BitcoinSerializer(this, parseRetain); - } - - @Override - public int getProtocolVersionNum(ProtocolVersion version) { - return 70007; - } - - @Override - public int getAddressHeader() { - return 30; - } - - @Override - public int getP2SHHeader() { - return 13; - } - - @Override - public int getDumpedPrivateKeyHeader() { - return 212; - } - - @Override - public int[] getAcceptableAddressCodes() { - return new int[] {getAddressHeader(), getP2SHHeader()}; - } - - @Override - public int getBip32HeaderPriv() { - return 0x0221312B; - } - - @Override - public int getBip32HeaderPub() { - return 0x022D2533; - } - - @Override - public int getSubsidyDecreaseBlockCount() { - return 210240; - } - - @Override - public int getInterval() { - return 60; - } - - @Override - public String getId() { - return "PIVX"; - } + public PivxNetworkParameters() { + super(); + } + + @Override + public String getPaymentProtocolId() { + return "main"; + } + + @Override + public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException { + } + + @Override + public Coin getMaxMoney() { + return Coin.valueOf(100000000 * Coin.COIN.value); + } + + @Override + public Coin getMinNonDustOutput() { + return Transaction.MIN_NONDUST_OUTPUT; + } + + @Override + public MonetaryFormat getMonetaryFormat() { + return new MonetaryFormat().code(0, "PIVX"); + } + + @Override + public String getUriScheme() { + return "pivx:"; + } + + @Override + public boolean hasMaxMoney() { + return true; + } + + @Override + public BitcoinSerializer getSerializer(boolean parseRetain) { + return new BitcoinSerializer(this, parseRetain); + } + + @Override + public int getProtocolVersionNum(ProtocolVersion version) { + return 70007; + } + + @Override + public int getAddressHeader() { + return 30; + } + + @Override + public int getP2SHHeader() { + return 13; + } + + @Override + public int getDumpedPrivateKeyHeader() { + return 212; + } + + @Override + public int[] getAcceptableAddressCodes() { + return new int[]{getAddressHeader(), getP2SHHeader()}; + } + + @Override + public int getBip32HeaderPriv() { + return 0x0221312B; + } + + @Override + public int getBip32HeaderPub() { + return 0x022D2533; + } + + @Override + public int getSubsidyDecreaseBlockCount() { + return 210240; + } + + @Override + public int getInterval() { + return 60; + } + + @Override + public String getId() { + return "PIVX"; + } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/poliscoin/PoliscoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/poliscoin/PoliscoinNetworkParameters.java index a4b976d..5d39572 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/poliscoin/PoliscoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/poliscoin/PoliscoinNetworkParameters.java @@ -7,95 +7,96 @@ public class PoliscoinNetworkParameters extends NetworkParameters { - public PoliscoinNetworkParameters() { - super(); - } - - @Override - public String getPaymentProtocolId() { - return "main"; - } - - @Override - public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException {} - - @Override - public Coin getMaxMoney() { - return Coin.valueOf(25000000 * Coin.COIN.value); - } - - @Override - public Coin getMinNonDustOutput() { - return Transaction.MIN_NONDUST_OUTPUT; - } - - @Override - public MonetaryFormat getMonetaryFormat() { - return new MonetaryFormat().code(0, "POLIS"); - } - - @Override - public String getUriScheme() { - return "polis:"; - } - - @Override - public boolean hasMaxMoney() { - return true; - } - - @Override - public BitcoinSerializer getSerializer(boolean parseRetain) { - return new BitcoinSerializer(this, parseRetain); - } - - @Override - public int getProtocolVersionNum(ProtocolVersion version) { - return 70219; - } - - @Override - public int getAddressHeader() { - return 55; - } - - @Override - public int getP2SHHeader() { - return 56; - } - - @Override - public int getDumpedPrivateKeyHeader() { - return 60; - } - - @Override - public int[] getAcceptableAddressCodes() { - return new int[] {getAddressHeader(), getP2SHHeader()}; - } - - @Override - public int getBip32HeaderPriv() { - return 0x03E25945; - } - - @Override - public int getBip32HeaderPub() { - return 0x03E25D7E; - } - - @Override - public int getSubsidyDecreaseBlockCount() { - return 210240; - } - - @Override - public int getInterval() { - return 120; - } - - @Override - public String getId() { - return "DASH"; - } + public PoliscoinNetworkParameters() { + super(); + } + + @Override + public String getPaymentProtocolId() { + return "main"; + } + + @Override + public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException { + } + + @Override + public Coin getMaxMoney() { + return Coin.valueOf(25000000 * Coin.COIN.value); + } + + @Override + public Coin getMinNonDustOutput() { + return Transaction.MIN_NONDUST_OUTPUT; + } + + @Override + public MonetaryFormat getMonetaryFormat() { + return new MonetaryFormat().code(0, "POLIS"); + } + + @Override + public String getUriScheme() { + return "polis:"; + } + + @Override + public boolean hasMaxMoney() { + return true; + } + + @Override + public BitcoinSerializer getSerializer(boolean parseRetain) { + return new BitcoinSerializer(this, parseRetain); + } + + @Override + public int getProtocolVersionNum(ProtocolVersion version) { + return 70219; + } + + @Override + public int getAddressHeader() { + return 55; + } + + @Override + public int getP2SHHeader() { + return 56; + } + + @Override + public int getDumpedPrivateKeyHeader() { + return 60; + } + + @Override + public int[] getAcceptableAddressCodes() { + return new int[]{getAddressHeader(), getP2SHHeader()}; + } + + @Override + public int getBip32HeaderPriv() { + return 0x03E25945; + } + + @Override + public int getBip32HeaderPub() { + return 0x03E25D7E; + } + + @Override + public int getSubsidyDecreaseBlockCount() { + return 210240; + } + + @Override + public int getInterval() { + return 120; + } + + @Override + public String getId() { + return "DASH"; + } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/ravencoin/RavencoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/ravencoin/RavencoinNetworkParameters.java index 5be75bf..44f4760 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/ravencoin/RavencoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/ravencoin/RavencoinNetworkParameters.java @@ -7,95 +7,96 @@ public class RavencoinNetworkParameters extends NetworkParameters { - public RavencoinNetworkParameters() { - super(); - } - - @Override - public String getPaymentProtocolId() { - return "main"; - } - - @Override - public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException {} - - @Override - public Coin getMaxMoney() { - return Coin.valueOf(100000000 * Coin.COIN.value); - } - - @Override - public Coin getMinNonDustOutput() { - return Transaction.MIN_NONDUST_OUTPUT; - } - - @Override - public MonetaryFormat getMonetaryFormat() { - return new MonetaryFormat().code(0, "RVN"); - } - - @Override - public String getUriScheme() { - return "ravencoin:"; - } - - @Override - public boolean hasMaxMoney() { - return true; - } - - @Override - public BitcoinSerializer getSerializer(boolean parseRetain) { - return new BitcoinSerializer(this, parseRetain); - } - - @Override - public int getProtocolVersionNum(ProtocolVersion version) { - return 70026; - } - - @Override - public int getAddressHeader() { - return 60; - } - - @Override - public int getP2SHHeader() { - return 122; - } - - @Override - public int getDumpedPrivateKeyHeader() { - return 128; - } - - @Override - public int[] getAcceptableAddressCodes() { - return new int[] {getAddressHeader(), getP2SHHeader()}; - } - - @Override - public int getBip32HeaderPriv() { - return 0x0488ADE4; - } - - @Override - public int getBip32HeaderPub() { - return 0x0488B21E; - } - - @Override - public int getSubsidyDecreaseBlockCount() { - return 210240; - } - - @Override - public int getInterval() { - return 60; - } - - @Override - public String getId() { - return "RVN"; - } + public RavencoinNetworkParameters() { + super(); + } + + @Override + public String getPaymentProtocolId() { + return "main"; + } + + @Override + public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException { + } + + @Override + public Coin getMaxMoney() { + return Coin.valueOf(100000000 * Coin.COIN.value); + } + + @Override + public Coin getMinNonDustOutput() { + return Transaction.MIN_NONDUST_OUTPUT; + } + + @Override + public MonetaryFormat getMonetaryFormat() { + return new MonetaryFormat().code(0, "RVN"); + } + + @Override + public String getUriScheme() { + return "ravencoin:"; + } + + @Override + public boolean hasMaxMoney() { + return true; + } + + @Override + public BitcoinSerializer getSerializer(boolean parseRetain) { + return new BitcoinSerializer(this, parseRetain); + } + + @Override + public int getProtocolVersionNum(ProtocolVersion version) { + return 70026; + } + + @Override + public int getAddressHeader() { + return 60; + } + + @Override + public int getP2SHHeader() { + return 122; + } + + @Override + public int getDumpedPrivateKeyHeader() { + return 128; + } + + @Override + public int[] getAcceptableAddressCodes() { + return new int[]{getAddressHeader(), getP2SHHeader()}; + } + + @Override + public int getBip32HeaderPriv() { + return 0x0488ADE4; + } + + @Override + public int getBip32HeaderPub() { + return 0x0488B21E; + } + + @Override + public int getSubsidyDecreaseBlockCount() { + return 210240; + } + + @Override + public int getInterval() { + return 60; + } + + @Override + public String getId() { + return "RVN"; + } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/syscoin/SyscoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/syscoin/SyscoinNetworkParameters.java index d0a284b..d036fdc 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/syscoin/SyscoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/syscoin/SyscoinNetworkParameters.java @@ -7,95 +7,96 @@ public class SyscoinNetworkParameters extends NetworkParameters { - public SyscoinNetworkParameters() { - super(); - } - - @Override - public int[] getAcceptableAddressCodes() { - return new int[] {getAddressHeader(), getP2SHHeader()}; - } - - @Override - public String getPaymentProtocolId() { - return "main"; - } - - @Override - public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException {} - - @Override - public Coin getMaxMoney() { - return Coin.valueOf(888000000 * Coin.COIN.value); - } - - @Override - public Coin getMinNonDustOutput() { - return Coin.valueOf(5500); - } - - @Override - public MonetaryFormat getMonetaryFormat() { - return new MonetaryFormat().code(0, "SYS"); - } - - @Override - public String getUriScheme() { - return "syscoin:"; - } - - @Override - public boolean hasMaxMoney() { - return true; - } - - @Override - public BitcoinSerializer getSerializer(boolean parseRetain) { - return new BitcoinSerializer(this, parseRetain); - } - - @Override - public int getProtocolVersionNum(ProtocolVersion version) { - return 70227; - } - - @Override - public int getAddressHeader() { - return 63; - } - - @Override - public int getP2SHHeader() { - return 5; - } - - @Override - public int getDumpedPrivateKeyHeader() { - return 128; - } - - @Override - public int getBip32HeaderPriv() { - return 0x0488ADE4; - } - - @Override - public int getBip32HeaderPub() { - return 0x0488B21E; - } - - @Override - public int getSubsidyDecreaseBlockCount() { - return 525600; - } - - @Override - public int getInterval() { - return 2016; - } - - @Override - public String getId() { - return "SYS"; - } + public SyscoinNetworkParameters() { + super(); + } + + @Override + public int[] getAcceptableAddressCodes() { + return new int[]{getAddressHeader(), getP2SHHeader()}; + } + + @Override + public String getPaymentProtocolId() { + return "main"; + } + + @Override + public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException { + } + + @Override + public Coin getMaxMoney() { + return Coin.valueOf(888000000 * Coin.COIN.value); + } + + @Override + public Coin getMinNonDustOutput() { + return Coin.valueOf(5500); + } + + @Override + public MonetaryFormat getMonetaryFormat() { + return new MonetaryFormat().code(0, "SYS"); + } + + @Override + public String getUriScheme() { + return "syscoin:"; + } + + @Override + public boolean hasMaxMoney() { + return true; + } + + @Override + public BitcoinSerializer getSerializer(boolean parseRetain) { + return new BitcoinSerializer(this, parseRetain); + } + + @Override + public int getProtocolVersionNum(ProtocolVersion version) { + return 70227; + } + + @Override + public int getAddressHeader() { + return 63; + } + + @Override + public int getP2SHHeader() { + return 5; + } + + @Override + public int getDumpedPrivateKeyHeader() { + return 128; + } + + @Override + public int getBip32HeaderPriv() { + return 0x0488ADE4; + } + + @Override + public int getBip32HeaderPub() { + return 0x0488B21E; + } + + @Override + public int getSubsidyDecreaseBlockCount() { + return 525600; + } + + @Override + public int getInterval() { + return 2016; + } + + @Override + public String getId() { + return "SYS"; + } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/trezarcoin/TrezarcoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/trezarcoin/TrezarcoinNetworkParameters.java index df89e54..a1acc4d 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/trezarcoin/TrezarcoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/trezarcoin/TrezarcoinNetworkParameters.java @@ -7,95 +7,96 @@ public class TrezarcoinNetworkParameters extends NetworkParameters { - public TrezarcoinNetworkParameters() { - super(); - } - - @Override - public int[] getAcceptableAddressCodes() { - return new int[] {getAddressHeader(), getP2SHHeader()}; - } - - @Override - public String getPaymentProtocolId() { - return "main"; - } - - @Override - public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException {} - - @Override - public Coin getMaxMoney() { - return Coin.valueOf(888000000 * Coin.COIN.value); - } - - @Override - public Coin getMinNonDustOutput() { - return Coin.valueOf(5500); - } - - @Override - public MonetaryFormat getMonetaryFormat() { - return new MonetaryFormat().code(0, "TZC"); - } - - @Override - public String getUriScheme() { - return "trezarcoin:"; - } - - @Override - public boolean hasMaxMoney() { - return true; - } - - @Override - public BitcoinSerializer getSerializer(boolean parseRetain) { - return new BitcoinSerializer(this, parseRetain); - } - - @Override - public int getProtocolVersionNum(ProtocolVersion version) { - return 70000; - } - - @Override - public int getAddressHeader() { - return 66; - } - - @Override - public int getP2SHHeader() { - return 8; - } - - @Override - public int getDumpedPrivateKeyHeader() { - return 194; - } - - @Override - public int getBip32HeaderPriv() { - return 0x0488ADE4; - } - - @Override - public int getBip32HeaderPub() { - return 0x0488B21E; - } - - @Override - public int getSubsidyDecreaseBlockCount() { - return 1600000; - } - - @Override - public int getInterval() { - return 600; - } - - @Override - public String getId() { - return "TZC"; - } + public TrezarcoinNetworkParameters() { + super(); + } + + @Override + public int[] getAcceptableAddressCodes() { + return new int[]{getAddressHeader(), getP2SHHeader()}; + } + + @Override + public String getPaymentProtocolId() { + return "main"; + } + + @Override + public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException { + } + + @Override + public Coin getMaxMoney() { + return Coin.valueOf(888000000 * Coin.COIN.value); + } + + @Override + public Coin getMinNonDustOutput() { + return Coin.valueOf(5500); + } + + @Override + public MonetaryFormat getMonetaryFormat() { + return new MonetaryFormat().code(0, "TZC"); + } + + @Override + public String getUriScheme() { + return "trezarcoin:"; + } + + @Override + public boolean hasMaxMoney() { + return true; + } + + @Override + public BitcoinSerializer getSerializer(boolean parseRetain) { + return new BitcoinSerializer(this, parseRetain); + } + + @Override + public int getProtocolVersionNum(ProtocolVersion version) { + return 70000; + } + + @Override + public int getAddressHeader() { + return 66; + } + + @Override + public int getP2SHHeader() { + return 8; + } + + @Override + public int getDumpedPrivateKeyHeader() { + return 194; + } + + @Override + public int getBip32HeaderPriv() { + return 0x0488ADE4; + } + + @Override + public int getBip32HeaderPub() { + return 0x0488B21E; + } + + @Override + public int getSubsidyDecreaseBlockCount() { + return 1600000; + } + + @Override + public int getInterval() { + return 600; + } + + @Override + public String getId() { + return "TZC"; + } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/unobtanium/UnobtaniumNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/unobtanium/UnobtaniumNetworkParameters.java index ee98390..03947e9 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/unobtanium/UnobtaniumNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/unobtanium/UnobtaniumNetworkParameters.java @@ -11,19 +11,20 @@ public UnobtaniumNetworkParameters() { super(); } - @Override - public String getPaymentProtocolId() { - return "main"; - } + @Override + public String getPaymentProtocolId() { + return "main"; + } - @Override - public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException {} + @Override + public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException { + } - @Override - public Coin getMaxMoney() { - return Coin.valueOf(250000 * Coin.COIN.value); - } + @Override + public Coin getMaxMoney() { + return Coin.valueOf(250000 * Coin.COIN.value); + } @Override public Coin getMinNonDustOutput() { diff --git a/src/main/java/io/cloudchains/app/net/substitutions/ApacheSubstitutions.java b/src/main/java/io/cloudchains/app/net/substitutions/ApacheSubstitutions.java deleted file mode 100644 index f56a314..0000000 --- a/src/main/java/io/cloudchains/app/net/substitutions/ApacheSubstitutions.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.cloudchains.app.net.substitutions; - -import com.oracle.svm.core.annotate.Substitute; -import com.oracle.svm.core.annotate.TargetClass; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.commons.logging.impl.LogFactoryImpl; -import org.apache.commons.logging.impl.SimpleLog; - - -@SuppressWarnings("unused") -@TargetClass(LogFactory.class) -final class LogFactorySubstituted { - @Substitute - protected static LogFactory newFactory(final String factoryClass, - final ClassLoader classLoader, - final ClassLoader contextClassLoader) { - return new LogFactoryImpl(); - } -} - -@SuppressWarnings("unused") -@TargetClass(LogFactoryImpl.class) -final class LogFactoryImplSubstituted { - @Substitute - private Log discoverLogImplementation(String logCategory) { - return new SimpleLog(logCategory); - } -} - -public class ApacheSubstitutions { -} diff --git a/src/main/java/io/cloudchains/app/net/substitutions/NettySubstitutions.java b/src/main/java/io/cloudchains/app/net/substitutions/NettySubstitutions.java deleted file mode 100644 index ffb1480..0000000 --- a/src/main/java/io/cloudchains/app/net/substitutions/NettySubstitutions.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.cloudchains.app.net.substitutions; - -import com.oracle.svm.core.annotate.Substitute; -import com.oracle.svm.core.annotate.TargetClass; -import io.netty.util.internal.logging.InternalLoggerFactory; -import io.netty.util.internal.logging.JdkLoggerFactory; - - -@TargetClass(io.netty.util.internal.logging.InternalLoggerFactory.class) -final class TargetInternalLoggerFactory { - @Substitute - private static InternalLoggerFactory newDefaultFactory(String name) { - return JdkLoggerFactory.INSTANCE; - } -} - -public class NettySubstitutions { -} \ No newline at end of file diff --git a/src/main/java/io/cloudchains/app/net/xrouter/XRouterCommandUtils.java b/src/main/java/io/cloudchains/app/net/xrouter/XRouterCommandUtils.java index 9c840a1..6d13a8e 100644 --- a/src/main/java/io/cloudchains/app/net/xrouter/XRouterCommandUtils.java +++ b/src/main/java/io/cloudchains/app/net/xrouter/XRouterCommandUtils.java @@ -4,40 +4,40 @@ public class XRouterCommandUtils { - private static final HashBiMap commands; - - static { - commands = HashBiMap.create(19); - - commands.put("xrInvalid", 0); - commands.put("xrReply", 1); - commands.put("xrGetReply", 2); - commands.put("xrGetConfig", 3); - commands.put("xrConfigReply", 4); - commands.put("xrGetBlockCount", 20); - commands.put("xrGetBlockHash", 21); - commands.put("xrGetBlock", 22); - commands.put("xrGetTransaction", 23); - commands.put("xrSendTransaction", 24); - commands.put("xrGetTxBloomFilter", 40); - commands.put("xrGenerateBloomFilter", 41); - commands.put("xrGetBlocks", 50); - commands.put("xrGetTransactions", 51); - commands.put("xrGetBlockAtTime", 52); - commands.put("xrDecodeRawTransaction", 53); - commands.put("xrGetBalance", 60); - commands.put("xrGetBalanceUpdate", 61); - commands.put("xrService", 1000); - } - - public static int commandStringToInt(String command) { - Integer commandId = commands.get(command); - return commandId == null ? 0 : commandId; - } - - public static String commandIdToString(int commandId) { - String command = commands.inverse().get(commandId); - return command == null ? "xrInvalid" : command; - } + private static final HashBiMap commands; + + static { + commands = HashBiMap.create(19); + + commands.put("xrInvalid", 0); + commands.put("xrReply", 1); + commands.put("xrGetReply", 2); + commands.put("xrGetConfig", 3); + commands.put("xrConfigReply", 4); + commands.put("xrGetBlockCount", 20); + commands.put("xrGetBlockHash", 21); + commands.put("xrGetBlock", 22); + commands.put("xrGetTransaction", 23); + commands.put("xrSendTransaction", 24); + commands.put("xrGetTxBloomFilter", 40); + commands.put("xrGenerateBloomFilter", 41); + commands.put("xrGetBlocks", 50); + commands.put("xrGetTransactions", 51); + commands.put("xrGetBlockAtTime", 52); + commands.put("xrDecodeRawTransaction", 53); + commands.put("xrGetBalance", 60); + commands.put("xrGetBalanceUpdate", 61); + commands.put("xrService", 1000); + } + + public static int commandStringToInt(String command) { + Integer commandId = commands.get(command); + return commandId == null ? 0 : commandId; + } + + public static String commandIdToString(int commandId) { + String command = commands.inverse().get(commandId); + return command == null ? "xrInvalid" : command; + } } diff --git a/src/main/java/io/cloudchains/app/net/xrouter/XRouterFeeUtils.java b/src/main/java/io/cloudchains/app/net/xrouter/XRouterFeeUtils.java index 427a131..62e2dcb 100644 --- a/src/main/java/io/cloudchains/app/net/xrouter/XRouterFeeUtils.java +++ b/src/main/java/io/cloudchains/app/net/xrouter/XRouterFeeUtils.java @@ -15,97 +15,97 @@ import java.util.logging.Logger; public class XRouterFeeUtils { - private final static LogManager LOGMANAGER = LogManager.getLogManager(); - private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); + private final static LogManager LOGMANAGER = LogManager.getLogManager(); + private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); - public static String getXRouterFeeTx(BlocknetPeer blocknetPeer, String xRouterCommand) { - CoinInstance blocknetCoin = CoinInstance.getInstance(CoinInstance.getActiveBlocknetNetwork()); - WalletHelper blocknetWalletHelper = blocknetCoin.getWalletHelper(); - NetworkParameters params = blocknetCoin.getNetworkParameters(); - XRouterConfiguration xRouterConfig = blocknetPeer.getxRouterConfiguration(); + public static String getXRouterFeeTx(BlocknetPeer blocknetPeer, String xRouterCommand) { + CoinInstance blocknetCoin = CoinInstance.getInstance(CoinInstance.getActiveBlocknetNetwork()); + WalletHelper blocknetWalletHelper = blocknetCoin.getWalletHelper(); + NetworkParameters params = blocknetCoin.getNetworkParameters(); + XRouterConfiguration xRouterConfig = blocknetPeer.getxRouterConfiguration(); - Preconditions.checkNotNull(xRouterConfig, "XRouter config was not received yet!"); + Preconditions.checkNotNull(xRouterConfig, "XRouter config was not received yet!"); - HashMap feeMap = xRouterConfig.getFeeMap(); + HashMap feeMap = xRouterConfig.getFeeMap(); - if (!feeMap.containsKey(xRouterCommand)) { - LOGGER.log(Level.FINER, "[xrouter-fee-utils] WARNING: Invalid/unknown XRouter command supplied to getXRouterFeeTx()! Assuming this command is free."); - LOGGER.log(Level.FINER, "[xrouter-fee-utils] Command: " + xRouterCommand); + if (!feeMap.containsKey(xRouterCommand)) { + LOGGER.log(Level.FINER, "[xrouter-fee-utils] WARNING: Invalid/unknown XRouter command supplied to getXRouterFeeTx()! Assuming this command is free."); + LOGGER.log(Level.FINER, "[xrouter-fee-utils] Command: " + xRouterCommand); - return "nohash;nofee"; - } + return "nohash;nofee"; + } - double fee = feeMap.get(xRouterCommand); + double fee = feeMap.get(xRouterCommand); - Coin xRouterFeeAmt = Coin.valueOf((long) Math.floor(fee * Coin.COIN.value)); - if (xRouterFeeAmt.value == 0) { - LOGGER.log(Level.FINER, "[xrouter-fee-utils] DEBUG: This command is free."); - return "nohash;nofee"; - } + Coin xRouterFeeAmt = Coin.valueOf((long) Math.floor(fee * Coin.COIN.value)); + if (xRouterFeeAmt.value == 0) { + LOGGER.log(Level.FINER, "[xrouter-fee-utils] DEBUG: This command is free."); + return "nohash;nofee"; + } - double totalSpending = fee + blocknetCoin.getConfigHelper().getFee(); - double totalAvailable = blocknetWalletHelper.getSpendBalance(totalSpending); - double changeAmt = ((totalAvailable - blocknetCoin.getConfigHelper().getFee()) - fee); + double totalSpending = fee + blocknetCoin.getConfigHelper().getFee(); + double totalAvailable = blocknetWalletHelper.getSpendBalance(totalSpending); + double changeAmt = ((totalAvailable - blocknetCoin.getConfigHelper().getFee()) - fee); - Address xRouterPaymentAddress = Address.fromBase58(params, xRouterConfig.getFeeAddress()); - Coin blocknetNetworkFeeAmt = Coin.valueOf((long) Math.floor(blocknetCoin.getConfigHelper().getFee() * Coin.COIN.value)); - Coin xRouterChangeAmt = Coin.valueOf((long) Math.floor(totalAvailable * Coin.COIN.value)).minus(blocknetNetworkFeeAmt).minus(xRouterFeeAmt); + Address xRouterPaymentAddress = Address.fromBase58(params, xRouterConfig.getFeeAddress()); + Coin blocknetNetworkFeeAmt = Coin.valueOf((long) Math.floor(blocknetCoin.getConfigHelper().getFee() * Coin.COIN.value)); + Coin xRouterChangeAmt = Coin.valueOf((long) Math.floor(totalAvailable * Coin.COIN.value)).minus(blocknetNetworkFeeAmt).minus(xRouterFeeAmt); - TransactionOutput feeOutput = new TransactionOutput(params, null, xRouterFeeAmt, xRouterPaymentAddress); + TransactionOutput feeOutput = new TransactionOutput(params, null, xRouterFeeAmt, xRouterPaymentAddress); - ArrayList outputs = new ArrayList<>(); - outputs.add(feeOutput); + ArrayList outputs = new ArrayList<>(); + outputs.add(feeOutput); - if (changeAmt > 0.06) { - double halvedAmt = changeAmt / 3; - Coin halvedChangeAmt = Coin.valueOf((long) Math.floor(halvedAmt * Coin.COIN.value)); - TransactionOutput halvedChangeOutput = new TransactionOutput(params, null, halvedChangeAmt, blocknetWalletHelper.getChangeAddress()); + if (changeAmt > 0.06) { + double halvedAmt = changeAmt / 3; + Coin halvedChangeAmt = Coin.valueOf((long) Math.floor(halvedAmt * Coin.COIN.value)); + TransactionOutput halvedChangeOutput = new TransactionOutput(params, null, halvedChangeAmt, blocknetWalletHelper.getChangeAddress()); - for (int i = 0; i < 3; i++) { - outputs.add(halvedChangeOutput); - } - } else { - TransactionOutput changeOutput = new TransactionOutput(params, null, xRouterChangeAmt, blocknetWalletHelper.getChangeAddress()); - outputs.add(changeOutput); - } + for (int i = 0; i < 3; i++) { + outputs.add(halvedChangeOutput); + } + } else { + TransactionOutput changeOutput = new TransactionOutput(params, null, xRouterChangeAmt, blocknetWalletHelper.getChangeAddress()); + outputs.add(changeOutput); + } - Transaction xRouterFeeTx = blocknetWalletHelper.createRawTransactionWithAllUTXOs(outputs, totalAvailable); + Transaction xRouterFeeTx = blocknetWalletHelper.createRawTransactionWithAllUTXOs(outputs, totalAvailable); - String feetx = new String(Hex.encode(xRouterFeeTx.bitcoinSerialize())); - LOGGER.log(Level.FINER, "[xrouter-fee-utils] XRouter fee transaction string representation:"); - LOGGER.log(Level.FINER, xRouterFeeTx.toString()); - LOGGER.log(Level.FINER, "[xrouter-fee-utils] DEBUG: Feetx: " + feetx); - return feetx; - } + String feetx = new String(Hex.encode(xRouterFeeTx.bitcoinSerialize())); + LOGGER.log(Level.FINER, "[xrouter-fee-utils] XRouter fee transaction string representation:"); + LOGGER.log(Level.FINER, xRouterFeeTx.toString()); + LOGGER.log(Level.FINER, "[xrouter-fee-utils] DEBUG: Feetx: " + feetx); + return feetx; + } - public static TransactionOutput createXrSendTransactionFeeOutput(BlocknetPeer blocknetPeer) { - CoinInstance blocknetCoin = CoinInstance.getInstance(CoinInstance.getActiveBlocknetNetwork()); - XRouterConfiguration xRouterConfig = blocknetPeer.getxRouterConfiguration(); - HashMap feeMap = xRouterConfig.getFeeMap(); - Preconditions.checkState(feeMap.containsKey("xrSendTransaction"), "Fee map has no fee for xrSendTransaction"); + public static TransactionOutput createXrSendTransactionFeeOutput(BlocknetPeer blocknetPeer) { + CoinInstance blocknetCoin = CoinInstance.getInstance(CoinInstance.getActiveBlocknetNetwork()); + XRouterConfiguration xRouterConfig = blocknetPeer.getxRouterConfiguration(); + HashMap feeMap = xRouterConfig.getFeeMap(); + Preconditions.checkState(feeMap.containsKey("xrSendTransaction"), "Fee map has no fee for xrSendTransaction"); - double fee = feeMap.get("xrSendTransaction"); - String feeAddress = xRouterConfig.getFeeAddress(); + double fee = feeMap.get("xrSendTransaction"); + String feeAddress = xRouterConfig.getFeeAddress(); - Coin feeAmount = Coin.valueOf((long) Math.floor(fee * Coin.COIN.value)); + Coin feeAmount = Coin.valueOf((long) Math.floor(fee * Coin.COIN.value)); - return new TransactionOutput(blocknetCoin.getNetworkParameters(), null, feeAmount, Address.fromBase58(blocknetCoin.getNetworkParameters(), feeAddress)); - } + return new TransactionOutput(blocknetCoin.getNetworkParameters(), null, feeAmount, Address.fromBase58(blocknetCoin.getNetworkParameters(), feeAddress)); + } - public static String coveredXrFee(BlocknetPeer blocknetPeer, Transaction transaction) { - CoinInstance blocknetCoin = CoinInstance.getInstance(CoinInstance.getActiveBlocknetNetwork()); - WalletHelper.setAsSpent(blocknetCoin.getTicker(), transaction, true); + public static String coveredXrFee(BlocknetPeer blocknetPeer, Transaction transaction) { + CoinInstance blocknetCoin = CoinInstance.getInstance(CoinInstance.getActiveBlocknetNetwork()); + WalletHelper.setAsSpent(blocknetCoin.getTicker(), transaction, true); - String xrFee = getXRouterFeeTx(blocknetPeer, "xrSendTransaction"); + String xrFee = getXRouterFeeTx(blocknetPeer, "xrSendTransaction"); - if (xrFee.equals("nohash;nofee")) - return xrFee; + if (xrFee.equals("nohash;nofee")) + return xrFee; - Transaction tx = new Transaction(blocknetCoin.getNetworkParameters(), Hex.decode(xrFee)); + Transaction tx = new Transaction(blocknetCoin.getNetworkParameters(), Hex.decode(xrFee)); - if (tx.getInputs().size() == 0) - return null; + if (tx.getInputs().size() == 0) + return null; - return xrFee; - } + return xrFee; + } } diff --git a/src/main/java/io/cloudchains/app/net/xrouter/XRouterInitialMessagesSentListener.java b/src/main/java/io/cloudchains/app/net/xrouter/XRouterInitialMessagesSentListener.java index c07258a..c34a462 100644 --- a/src/main/java/io/cloudchains/app/net/xrouter/XRouterInitialMessagesSentListener.java +++ b/src/main/java/io/cloudchains/app/net/xrouter/XRouterInitialMessagesSentListener.java @@ -4,6 +4,6 @@ public interface XRouterInitialMessagesSentListener { - void initialMessagesSent(CoinInstance instance); + void initialMessagesSent(CoinInstance instance); } diff --git a/src/main/java/io/cloudchains/app/net/xrouter/XRouterMessage.java b/src/main/java/io/cloudchains/app/net/xrouter/XRouterMessage.java index 1532524..93f010a 100644 --- a/src/main/java/io/cloudchains/app/net/xrouter/XRouterMessage.java +++ b/src/main/java/io/cloudchains/app/net/xrouter/XRouterMessage.java @@ -20,425 +20,425 @@ import java.util.logging.Logger; public class XRouterMessage extends Message { - private final static LogManager LOGMANAGER = LogManager.getLogManager(); - private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); - - private BlocknetPeer blocknetPeer; - - private XRouterPacketHeader xRouterHeader; - private byte[] data; - - private HashMap parsedData; - private BlocknetParameters params; - - public XRouterMessage(BlocknetParameters params, byte[] data) { - this.data = data; - - this.params = params; - this.parsedData = new HashMap<>(); - parseHeader(); - parse(); - } - - XRouterMessage(BlocknetPeer blocknetPeer, BlocknetParameters params, XRouterPacketHeader xRouterHeader, HashMap parsedData) { - this.blocknetPeer = blocknetPeer; - - this.xRouterHeader = xRouterHeader; - this.parsedData = parsedData; - - this.params = params; - this.data = bitcoinSerialize(); - } - - private void writeCurrency(HashMap body, OutputStream out) throws IOException { - out.write(((String) body.get("currency")).getBytes()); - out.write(0x00); - } - - private void writeAccountAndNumber(String account, String number, OutputStream out) throws IOException { - out.write(account.getBytes()); - out.write(0x00); - out.write(number.getBytes()); - out.write(0x00); - } - - /** - * Write Payment TX to unserialized XRouter Packet - * @param body HashMap The parsed XRouter packet body - * @param out OutputStream The output stream to which to write the serialized transaction info - * @throws IOException If writing fails or another error occurs - */ - private void writePaymentTx(HashMap body, OutputStream out) throws IOException { - if (!body.containsKey("paymentTx")) - return; - - out.write(((String) body.get("paymentTx")).getBytes()); - out.write(0x00); - } - - public XRouterPacketHeader getXRouterHeader() { - return xRouterHeader; - } - - public HashMap getParsedData() { - return parsedData; - } - - @Override - public byte[] bitcoinSerialize() { - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - - try { - bitcoinSerializeToStream(byteArrayOutputStream); - } catch (Exception e) { - LOGGER.log(Level.FINER, "Error while serializing XRouter packet! Invalid packet structure?"); - e.printStackTrace(); - return null; - } - - return byteArrayOutputStream.toByteArray(); - } - - @Override - protected void bitcoinSerializeToStream(OutputStream stream) throws IOException { - if (xRouterHeader.getExtSize() < 253) { - stream.write(xRouterHeader.getCompactSizeBytes()); - } else if (xRouterHeader.getExtSize() <= 65535) { - stream.write((byte) 253); - stream.write(xRouterHeader.getCompactSizeBytes()); - } else { - stream.write((byte) 254); - stream.write(xRouterHeader.getCompactSizeBytes()); - } - - Utils.uint32ToByteStreamLE(xRouterHeader.getVersion(), stream); - Utils.uint32ToByteStreamLE(xRouterHeader.getCommand(), stream); - Utils.uint32ToByteStreamLE(xRouterHeader.getTimestamp(), stream); - Utils.uint32ToByteStreamLE(xRouterHeader.getSize(), stream); - stream.write(new byte[8]); - stream.write(xRouterHeader.getUUID().getBytes()); - stream.write(xRouterHeader.getPubkey()); - stream.write(xRouterHeader.getSignature()); - - switch (XRouterCommandUtils.commandIdToString(xRouterHeader.getCommand())) { - case "xrReply": - case "xrConfigReply": { - stream.write(((String) parsedData.get("reply")).getBytes()); - stream.write(0x00); - break; - } - case "xrGetReply": { - LOGGER.log(Level.FINER, "[xrouter-message] DEBUG: Fetching reply for packet " + xRouterHeader.getUUID()); - break; - } - case "xrGetConfig": { - stream.write(((String) parsedData.get("addr")).getBytes()); - stream.write(0x00); - break; - } - case "xrGetBlockCount": { - writeCurrency(parsedData, stream); - writePaymentTx(parsedData, stream); - Utils.uint32ToByteStreamLE(0, stream); //param count - break; - } - case "xrGetBlockHash": { - writeCurrency(parsedData, stream); - writePaymentTx(parsedData, stream); - Utils.uint32ToByteStreamLE(1, stream); //param count - stream.write(((String) parsedData.get("blockId")).getBytes()); - stream.write(0x00); - break; - } - case "xrGetBlock": { - writeCurrency(parsedData, stream); - writePaymentTx(parsedData, stream); - Utils.uint32ToByteStreamLE(1, stream); //param count - stream.write(((String) parsedData.get("blockHash")).getBytes()); - stream.write(0x00); - break; - } - case "xrGetTransaction": { - writeCurrency(parsedData, stream); - writePaymentTx(parsedData, stream); - Utils.uint32ToByteStreamLE(1, stream); //param count - stream.write(((String) parsedData.get("txid")).getBytes()); - stream.write(0x00); - break; - } - case "xrSendTransaction": { - writeCurrency(parsedData, stream); - writePaymentTx(parsedData, stream); - Utils.uint32ToByteStreamLE(1, stream); //param count - stream.write(((String) parsedData.get("transaction")).getBytes()); - stream.write(0x00); - break; - } - case "xrGetTxBloomFilter": { - writeCurrency(parsedData, stream); - writePaymentTx(parsedData, stream); - stream.write(((String) parsedData.get("number_s")).getBytes()); - stream.write(0x00); - break; - } - case "xrGenerateBloomFilter": { - LOGGER.log(Level.FINER, "[xrouter-message] ERROR: Attempted to serialize unsupported command 41."); - break; - } - case "xrGetBlocks": { - writeCurrency(parsedData, stream); - writePaymentTx(parsedData, stream); - writeAccountAndNumber((String) parsedData.get("account"), (String) parsedData.get("number"), stream); - break; - } - case "xrGetTransactions": - case "xrGetBalanceUpdate": { - writeCurrency(parsedData, stream); - writePaymentTx(parsedData, stream); - writeAccountAndNumber((String) parsedData.get("account"), (String) parsedData.get("number_s"), stream); - break; - } - case "xrGetBlockAtTime": { - LOGGER.log(Level.FINER, "[xrouter-message] ERROR: Attempted to serialize unsupported command 52."); - break; - } - case "xrGetBalance": { //OBSOLETE, only implemented for backwards compatibility - writeCurrency(parsedData, stream); - writePaymentTx(parsedData, stream); - stream.write(((String) parsedData.get("account")).getBytes()); - stream.write(0x00); - break; - } - case "xrService": { - String command = (String) parsedData.get("command"); - LOGGER.log(Level.FINER, "[xrService] Command: " + command); - - XRouterConfiguration.XRouterPluginConfiguration pluginConfig = blocknetPeer.getPluginConfig(command); - - if (pluginConfig == null) { - LOGGER.log(Level.FINER, "[xrService] ERROR: Unsupported server xrs plugin: " + command); - LOGGER.log(Level.FINER, "[xrService] ERROR: Aborting transmission."); - throw new IllegalArgumentException("Unsupported server xrs plugin: " + command); - } - - ArrayList pluginParamTypes = pluginConfig.getParamTypes(); - - stream.write(command.getBytes()); - stream.write(0x00); - - writePaymentTx(parsedData, stream); - - ArrayList params = (ArrayList) parsedData.get("params"); - - Utils.uint32ToByteStreamLE(params.size(), stream); //param count - - for (int i = 0; i < params.size(); i++) { - Class paramClass = pluginParamTypes.get(i); - Object param = params.get(i); - String classStr = XRouterConfiguration.getStringByClass(paramClass); - - if (!(param instanceof String && ((String) param).equalsIgnoreCase("true") || ((String) param).equalsIgnoreCase("false"))) - Preconditions.checkState(paramClass.isInstance(param), "Supplied parameter at index " + i + " is not of type '" + classStr + "'. Aborting transmission."); - - LOGGER.log(Level.FINER, "[xrService] DEBUG: Parameter " + i + " is of type " + classStr); - - switch (classStr) { - case "string": { - String typedParam = (String) param; - stream.write(typedParam.getBytes()); - stream.write(0x00); - break; - } - case "bool": { - int typedParam; - - if (((String) param).equalsIgnoreCase("true")) - typedParam = 1; - else - typedParam = 0; - - Utils.uint32ToByteStreamLE(typedParam, stream); - break; - } - case "int": { - Integer typedParam = (Integer) param; - Utils.uint32ToByteStreamLE(typedParam, stream); - break; - } - default: { - LOGGER.log(Level.FINER, "[xrService] ERROR: Encountered unhandled parameter of type " + classStr + ". Aborting transmission."); - throw new IllegalStateException("Bad parameter type at index " + i + ": " + classStr); - } - } - } - break; - } - default: { //xrInvalid - break; - } - } - } - - private String readStringNT(ByteBuffer in) { - StringBuilder stringBuilder = new StringBuilder(); - - byte buf; - - while ((buf = in.get()) != 0x00) { - stringBuilder.append((char) buf); - } - - return stringBuilder.toString(); - } - - private void readCurrency(ByteBuffer buf) { - String currency = readStringNT(buf); - parsedData.put("currency", currency); - } - - private void readPaymentTx(ByteBuffer buf) { - String paymentTx = readStringNT(buf); - parsedData.put("paymentTx", paymentTx); - } - - private void readAccountAndNumber(ByteBuffer buf, String secondFieldName) { - String account = readStringNT(buf); - parsedData.put("account", account); - offset += account.length(); - - String number = readStringNT(buf); - parsedData.put(secondFieldName, number); - offset += number.length(); - } - - private void parseHeader() throws ProtocolException { - xRouterHeader = new XRouterPacketHeader(ByteBuffer.wrap(data)); - } - - @Override - protected void parse() throws ProtocolException { - parsedData.put("header", xRouterHeader); - - LOGGER.log(Level.FINER, "Received raw XRouter packet: " + new String(Hex.encode(data))); - ByteBuffer buf = ByteBuffer.wrap(data); - buf.position(xRouterHeader.getHeaderLength()); - - int command = xRouterHeader.getCommand(); - - switch (XRouterCommandUtils.commandIdToString(command)) { - case "xrReply": - case "xrConfigReply": { - String reply = readStringNT(buf); - parsedData.put("reply", reply); - LOGGER.log(Level.FINER, "[xrouter-message] Got reply: '" + reply + "' for packet with UUID '" + xRouterHeader.getUUID() + "'"); - break; - } - case "xrGetReply": { - LOGGER.log(Level.FINER, "[xrouter-message] WARNING: Server asked to fetch reply, but we aren't a server."); - break; - } - case "xrGetConfig": { - LOGGER.log(Level.FINER, "[xrouter-message] WARNING: Server asked us for config, but we aren't a servicenode."); - break; - } - case "xrGetBlockCount": { - readCurrency(buf); - readPaymentTx(buf); - break; - } - case "xrGetBlockHash": { - readCurrency(buf); - - String blockId = readStringNT(buf); - parsedData.put("blockId", blockId); - - readPaymentTx(buf); - break; - } - case "xrGetBlock": { - readCurrency(buf); - - String blockHash = readStringNT(buf); - parsedData.put("blockHash", blockHash); - - readPaymentTx(buf); - break; - } - case "xrGetTransaction": { - readCurrency(buf); - - String txid = readStringNT(buf); - parsedData.put("txid", txid); - - readPaymentTx(buf); - break; - } - case "xrGetBlocks": { - readCurrency(buf); - readPaymentTx(buf); - - readAccountAndNumber(buf, "number"); - break; - } - case "xrGetTransactions": - case "xrGetBalanceUpdate": { - readCurrency(buf); - readPaymentTx(buf); - - readAccountAndNumber(buf, "number_s"); - break; - } - case "xrGetBalance": { //OBSOLETE - readCurrency(buf); - - String account = readStringNT(buf); - parsedData.put("account", account); - - readPaymentTx(buf); - break; - } - case "xrGetTxFilter": { - readCurrency(buf); - readPaymentTx(buf); - - String number_s = readStringNT(buf); - parsedData.put("number_s", number_s); - break; - } - case "xrSendTransaction": { - readCurrency(buf); - readPaymentTx(buf); - - String transaction = readStringNT(buf); - parsedData.put("transaction", transaction); - break; - } - case "xrGetBlockAtTime": { - readCurrency(buf); - - String timestamp = readStringNT(buf); - parsedData.put("timestamp", timestamp); - - readPaymentTx(buf); - break; - } - case "xrService": { - - String paymentTx = readStringNT(buf); - parsedData.put("command", paymentTx); - - ArrayList params = new ArrayList<>(); - while (buf.position() < buf.capacity()) { - String thisParam = readStringNT(buf); - params.add(thisParam); - } - - parsedData.put("params", params); - break; - } - default: { //xbcInvalid - break; - } - } - } + private final static LogManager LOGMANAGER = LogManager.getLogManager(); + private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); + + private BlocknetPeer blocknetPeer; + + private XRouterPacketHeader xRouterHeader; + private byte[] data; + + private HashMap parsedData; + private BlocknetParameters params; + + public XRouterMessage(BlocknetParameters params, byte[] data) { + this.data = data; + + this.params = params; + this.parsedData = new HashMap<>(); + parseHeader(); + parse(); + } + + XRouterMessage(BlocknetPeer blocknetPeer, BlocknetParameters params, XRouterPacketHeader xRouterHeader, HashMap parsedData) { + this.blocknetPeer = blocknetPeer; + + this.xRouterHeader = xRouterHeader; + this.parsedData = parsedData; + + this.params = params; + this.data = bitcoinSerialize(); + } + + private void writeCurrency(HashMap body, OutputStream out) throws IOException { + out.write(((String) body.get("currency")).getBytes()); + out.write(0x00); + } + + private void writeAccountAndNumber(String account, String number, OutputStream out) throws IOException { + out.write(account.getBytes()); + out.write(0x00); + out.write(number.getBytes()); + out.write(0x00); + } + + /** + * Write Payment TX to unserialized XRouter Packet + * @param body HashMap The parsed XRouter packet body + * @param out OutputStream The output stream to which to write the serialized transaction info + * @throws IOException If writing fails or another error occurs + */ + private void writePaymentTx(HashMap body, OutputStream out) throws IOException { + if (!body.containsKey("paymentTx")) + return; + + out.write(((String) body.get("paymentTx")).getBytes()); + out.write(0x00); + } + + public XRouterPacketHeader getXRouterHeader() { + return xRouterHeader; + } + + public HashMap getParsedData() { + return parsedData; + } + + @Override + public byte[] bitcoinSerialize() { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + + try { + bitcoinSerializeToStream(byteArrayOutputStream); + } catch (Exception e) { + LOGGER.log(Level.FINER, "Error while serializing XRouter packet! Invalid packet structure?"); + e.printStackTrace(); + return null; + } + + return byteArrayOutputStream.toByteArray(); + } + + @Override + protected void bitcoinSerializeToStream(OutputStream stream) throws IOException { + if (xRouterHeader.getExtSize() < 253) { + stream.write(xRouterHeader.getCompactSizeBytes()); + } else if (xRouterHeader.getExtSize() <= 65535) { + stream.write((byte) 253); + stream.write(xRouterHeader.getCompactSizeBytes()); + } else { + stream.write((byte) 254); + stream.write(xRouterHeader.getCompactSizeBytes()); + } + + Utils.uint32ToByteStreamLE(xRouterHeader.getVersion(), stream); + Utils.uint32ToByteStreamLE(xRouterHeader.getCommand(), stream); + Utils.uint32ToByteStreamLE(xRouterHeader.getTimestamp(), stream); + Utils.uint32ToByteStreamLE(xRouterHeader.getSize(), stream); + stream.write(new byte[8]); + stream.write(xRouterHeader.getUUID().getBytes()); + stream.write(xRouterHeader.getPubkey()); + stream.write(xRouterHeader.getSignature()); + + switch (XRouterCommandUtils.commandIdToString(xRouterHeader.getCommand())) { + case "xrReply": + case "xrConfigReply": { + stream.write(((String) parsedData.get("reply")).getBytes()); + stream.write(0x00); + break; + } + case "xrGetReply": { + LOGGER.log(Level.FINER, "[xrouter-message] DEBUG: Fetching reply for packet " + xRouterHeader.getUUID()); + break; + } + case "xrGetConfig": { + stream.write(((String) parsedData.get("addr")).getBytes()); + stream.write(0x00); + break; + } + case "xrGetBlockCount": { + writeCurrency(parsedData, stream); + writePaymentTx(parsedData, stream); + Utils.uint32ToByteStreamLE(0, stream); //param count + break; + } + case "xrGetBlockHash": { + writeCurrency(parsedData, stream); + writePaymentTx(parsedData, stream); + Utils.uint32ToByteStreamLE(1, stream); //param count + stream.write(((String) parsedData.get("blockId")).getBytes()); + stream.write(0x00); + break; + } + case "xrGetBlock": { + writeCurrency(parsedData, stream); + writePaymentTx(parsedData, stream); + Utils.uint32ToByteStreamLE(1, stream); //param count + stream.write(((String) parsedData.get("blockHash")).getBytes()); + stream.write(0x00); + break; + } + case "xrGetTransaction": { + writeCurrency(parsedData, stream); + writePaymentTx(parsedData, stream); + Utils.uint32ToByteStreamLE(1, stream); //param count + stream.write(((String) parsedData.get("txid")).getBytes()); + stream.write(0x00); + break; + } + case "xrSendTransaction": { + writeCurrency(parsedData, stream); + writePaymentTx(parsedData, stream); + Utils.uint32ToByteStreamLE(1, stream); //param count + stream.write(((String) parsedData.get("transaction")).getBytes()); + stream.write(0x00); + break; + } + case "xrGetTxBloomFilter": { + writeCurrency(parsedData, stream); + writePaymentTx(parsedData, stream); + stream.write(((String) parsedData.get("number_s")).getBytes()); + stream.write(0x00); + break; + } + case "xrGenerateBloomFilter": { + LOGGER.log(Level.FINER, "[xrouter-message] ERROR: Attempted to serialize unsupported command 41."); + break; + } + case "xrGetBlocks": { + writeCurrency(parsedData, stream); + writePaymentTx(parsedData, stream); + writeAccountAndNumber((String) parsedData.get("account"), (String) parsedData.get("number"), stream); + break; + } + case "xrGetTransactions": + case "xrGetBalanceUpdate": { + writeCurrency(parsedData, stream); + writePaymentTx(parsedData, stream); + writeAccountAndNumber((String) parsedData.get("account"), (String) parsedData.get("number_s"), stream); + break; + } + case "xrGetBlockAtTime": { + LOGGER.log(Level.FINER, "[xrouter-message] ERROR: Attempted to serialize unsupported command 52."); + break; + } + case "xrGetBalance": { //OBSOLETE, only implemented for backwards compatibility + writeCurrency(parsedData, stream); + writePaymentTx(parsedData, stream); + stream.write(((String) parsedData.get("account")).getBytes()); + stream.write(0x00); + break; + } + case "xrService": { + String command = (String) parsedData.get("command"); + LOGGER.log(Level.FINER, "[xrService] Command: " + command); + + XRouterConfiguration.XRouterPluginConfiguration pluginConfig = blocknetPeer.getPluginConfig(command); + + if (pluginConfig == null) { + LOGGER.log(Level.FINER, "[xrService] ERROR: Unsupported server xrs plugin: " + command); + LOGGER.log(Level.FINER, "[xrService] ERROR: Aborting transmission."); + throw new IllegalArgumentException("Unsupported server xrs plugin: " + command); + } + + ArrayList pluginParamTypes = pluginConfig.getParamTypes(); + + stream.write(command.getBytes()); + stream.write(0x00); + + writePaymentTx(parsedData, stream); + + ArrayList params = (ArrayList) parsedData.get("params"); + + Utils.uint32ToByteStreamLE(params.size(), stream); //param count + + for (int i = 0; i < params.size(); i++) { + Class paramClass = pluginParamTypes.get(i); + Object param = params.get(i); + String classStr = XRouterConfiguration.getStringByClass(paramClass); + + if (!(param instanceof String && ((String) param).equalsIgnoreCase("true") || ((String) param).equalsIgnoreCase("false"))) + Preconditions.checkState(paramClass.isInstance(param), "Supplied parameter at index " + i + " is not of type '" + classStr + "'. Aborting transmission."); + + LOGGER.log(Level.FINER, "[xrService] DEBUG: Parameter " + i + " is of type " + classStr); + + switch (classStr) { + case "string": { + String typedParam = (String) param; + stream.write(typedParam.getBytes()); + stream.write(0x00); + break; + } + case "bool": { + int typedParam; + + if (((String) param).equalsIgnoreCase("true")) + typedParam = 1; + else + typedParam = 0; + + Utils.uint32ToByteStreamLE(typedParam, stream); + break; + } + case "int": { + Integer typedParam = (Integer) param; + Utils.uint32ToByteStreamLE(typedParam, stream); + break; + } + default: { + LOGGER.log(Level.FINER, "[xrService] ERROR: Encountered unhandled parameter of type " + classStr + ". Aborting transmission."); + throw new IllegalStateException("Bad parameter type at index " + i + ": " + classStr); + } + } + } + break; + } + default: { //xrInvalid + break; + } + } + } + + private String readStringNT(ByteBuffer in) { + StringBuilder stringBuilder = new StringBuilder(); + + byte buf; + + while ((buf = in.get()) != 0x00) { + stringBuilder.append((char) buf); + } + + return stringBuilder.toString(); + } + + private void readCurrency(ByteBuffer buf) { + String currency = readStringNT(buf); + parsedData.put("currency", currency); + } + + private void readPaymentTx(ByteBuffer buf) { + String paymentTx = readStringNT(buf); + parsedData.put("paymentTx", paymentTx); + } + + private void readAccountAndNumber(ByteBuffer buf, String secondFieldName) { + String account = readStringNT(buf); + parsedData.put("account", account); + offset += account.length(); + + String number = readStringNT(buf); + parsedData.put(secondFieldName, number); + offset += number.length(); + } + + private void parseHeader() throws ProtocolException { + xRouterHeader = new XRouterPacketHeader(ByteBuffer.wrap(data)); + } + + @Override + protected void parse() throws ProtocolException { + parsedData.put("header", xRouterHeader); + + LOGGER.log(Level.FINER, "Received raw XRouter packet: " + new String(Hex.encode(data))); + ByteBuffer buf = ByteBuffer.wrap(data); + buf.position(xRouterHeader.getHeaderLength()); + + int command = xRouterHeader.getCommand(); + + switch (XRouterCommandUtils.commandIdToString(command)) { + case "xrReply": + case "xrConfigReply": { + String reply = readStringNT(buf); + parsedData.put("reply", reply); + LOGGER.log(Level.FINER, "[xrouter-message] Got reply: '" + reply + "' for packet with UUID '" + xRouterHeader.getUUID() + "'"); + break; + } + case "xrGetReply": { + LOGGER.log(Level.FINER, "[xrouter-message] WARNING: Server asked to fetch reply, but we aren't a server."); + break; + } + case "xrGetConfig": { + LOGGER.log(Level.FINER, "[xrouter-message] WARNING: Server asked us for config, but we aren't a servicenode."); + break; + } + case "xrGetBlockCount": { + readCurrency(buf); + readPaymentTx(buf); + break; + } + case "xrGetBlockHash": { + readCurrency(buf); + + String blockId = readStringNT(buf); + parsedData.put("blockId", blockId); + + readPaymentTx(buf); + break; + } + case "xrGetBlock": { + readCurrency(buf); + + String blockHash = readStringNT(buf); + parsedData.put("blockHash", blockHash); + + readPaymentTx(buf); + break; + } + case "xrGetTransaction": { + readCurrency(buf); + + String txid = readStringNT(buf); + parsedData.put("txid", txid); + + readPaymentTx(buf); + break; + } + case "xrGetBlocks": { + readCurrency(buf); + readPaymentTx(buf); + + readAccountAndNumber(buf, "number"); + break; + } + case "xrGetTransactions": + case "xrGetBalanceUpdate": { + readCurrency(buf); + readPaymentTx(buf); + + readAccountAndNumber(buf, "number_s"); + break; + } + case "xrGetBalance": { //OBSOLETE + readCurrency(buf); + + String account = readStringNT(buf); + parsedData.put("account", account); + + readPaymentTx(buf); + break; + } + case "xrGetTxFilter": { + readCurrency(buf); + readPaymentTx(buf); + + String number_s = readStringNT(buf); + parsedData.put("number_s", number_s); + break; + } + case "xrSendTransaction": { + readCurrency(buf); + readPaymentTx(buf); + + String transaction = readStringNT(buf); + parsedData.put("transaction", transaction); + break; + } + case "xrGetBlockAtTime": { + readCurrency(buf); + + String timestamp = readStringNT(buf); + parsedData.put("timestamp", timestamp); + + readPaymentTx(buf); + break; + } + case "xrService": { + + String paymentTx = readStringNT(buf); + parsedData.put("command", paymentTx); + + ArrayList params = new ArrayList<>(); + while (buf.position() < buf.capacity()) { + String thisParam = readStringNT(buf); + params.add(thisParam); + } + + parsedData.put("params", params); + break; + } + default: { //xbcInvalid + break; + } + } + } } diff --git a/src/main/java/io/cloudchains/app/net/xrouter/XRouterMessageSerializer.java b/src/main/java/io/cloudchains/app/net/xrouter/XRouterMessageSerializer.java index 716bd0c..d17d46c 100644 --- a/src/main/java/io/cloudchains/app/net/xrouter/XRouterMessageSerializer.java +++ b/src/main/java/io/cloudchains/app/net/xrouter/XRouterMessageSerializer.java @@ -16,119 +16,119 @@ import java.util.logging.Logger; public class XRouterMessageSerializer extends MessageSerializer { - private final static LogManager LOGMANAGER = LogManager.getLogManager(); - private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); - - private boolean parseRetain; - private BlocknetParameters params; - - public XRouterMessageSerializer(boolean parseRetain, BlocknetParameters params) { - this.parseRetain = parseRetain; - this.params = params; - } - - @Override - public XRouterMessage deserialize(ByteBuffer in) throws ProtocolException, UnsupportedOperationException { - seekPastMagicBytes(in); - BlocknetPacketHeader header = deserializeHeader(in); - - return deserializePayload(header, in); - } - - @Override - public BlocknetPacketHeader deserializeHeader(ByteBuffer in) throws ProtocolException, UnsupportedOperationException { - return new BlocknetPacketHeader(in); - } - - @Override - public XRouterMessage deserializePayload(BitcoinSerializer.BitcoinPacketHeader header, ByteBuffer in) throws ProtocolException, BufferUnderflowException, UnsupportedOperationException { - BlocknetPacketHeader blocknetPacketHeader = (BlocknetPacketHeader) header; - - byte[] payloadBytes = new byte[blocknetPacketHeader.getLength()]; - in.get(payloadBytes, 0, payloadBytes.length); - - if (!BlocknetUtils.verifyChecksum(blocknetPacketHeader, payloadBytes)) { - throw new ProtocolException("XRouter packet's checksum failed to verify."); - } - - int dataLength = in.capacity() - in.position(); - - byte[] data = new byte[dataLength]; - in.get(data, 0, dataLength); - - return new XRouterMessage(params, data); - } - - @Override - public boolean isParseRetainMode() { - return parseRetain; - } - - @Override - public AddressMessage makeAddressMessage(byte[] payloadBytes, int length) throws ProtocolException, UnsupportedOperationException { - throw new UnsupportedOperationException("This serializer does not support address message construction."); - } - - @Override - public Message makeAlertMessage(byte[] payloadBytes) throws ProtocolException, UnsupportedOperationException { - throw new UnsupportedOperationException("This serializer does not support alert message construction."); - } - - @Override - public Block makeBlock(byte[] payloadBytes, int offset, int length) throws ProtocolException, UnsupportedOperationException { - throw new UnsupportedOperationException("This serializer does not support block construction."); - } - - @Override - public Message makeBloomFilter(byte[] payloadBytes) throws ProtocolException, UnsupportedOperationException { - throw new UnsupportedOperationException("This serializer does not support bloom filter construction."); - } - - @Override - public FilteredBlock makeFilteredBlock(byte[] payloadBytes) throws ProtocolException, UnsupportedOperationException { - throw new UnsupportedOperationException("This serializer does not support block construction."); - } - - @Override - public InventoryMessage makeInventoryMessage(byte[] payloadBytes, int length) throws ProtocolException, UnsupportedOperationException { - throw new UnsupportedOperationException("This serializer does not support inventory message construction."); - } - - @Override - public Transaction makeTransaction(byte[] payloadBytes, int offset, int length, byte[] hash) throws ProtocolException, UnsupportedOperationException { - throw new UnsupportedOperationException("This serializer does not support transaction construction"); - } - - @Override - public void serialize(String name, byte[] message, OutputStream out) throws UnsupportedOperationException { - throw new UnsupportedOperationException("This serializer currently does not support name/message serialization."); - } - - private void serialize(byte[] data, OutputStream out) throws IOException { - byte[] header = BlocknetUtils.getHeader("xrouter", (int) params.getPacketMagic(), data); - - out.write(header); - out.write(data); - - LOGGER.log(Level.FINER, "[blocknet-serializer] Serialized xrouter message. Bytes: " + new String(Hex.encode(header)) + new String(Hex.encode(data))); - } - - /** - * Serialize XRouter packet header and body - * @param message XRouterMessage The XRouter message to serialize - * @param out OutputStream The output stream to which to write the serialized XRouter message bytes - * @throws IOException If writing fails or another I/O related error occurs - */ - @Override - public void serialize(Message message, OutputStream out) throws IOException { - Preconditions.checkArgument(message instanceof XRouterMessage, "This message is not an XRouter message."); - - XRouterMessage xRouterMessage = (XRouterMessage) message; - serialize(xRouterMessage.bitcoinSerialize(), out); - } - - @Override - public void seekPastMagicBytes(ByteBuffer in) throws BufferUnderflowException { - BlocknetUtils.seekPastMagicBytes(in, params); - } + private final static LogManager LOGMANAGER = LogManager.getLogManager(); + private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); + + private boolean parseRetain; + private BlocknetParameters params; + + public XRouterMessageSerializer(boolean parseRetain, BlocknetParameters params) { + this.parseRetain = parseRetain; + this.params = params; + } + + @Override + public XRouterMessage deserialize(ByteBuffer in) throws ProtocolException, UnsupportedOperationException { + seekPastMagicBytes(in); + BlocknetPacketHeader header = deserializeHeader(in); + + return deserializePayload(header, in); + } + + @Override + public BlocknetPacketHeader deserializeHeader(ByteBuffer in) throws ProtocolException, UnsupportedOperationException { + return new BlocknetPacketHeader(in); + } + + @Override + public XRouterMessage deserializePayload(BitcoinSerializer.BitcoinPacketHeader header, ByteBuffer in) throws ProtocolException, BufferUnderflowException, UnsupportedOperationException { + BlocknetPacketHeader blocknetPacketHeader = (BlocknetPacketHeader) header; + + byte[] payloadBytes = new byte[blocknetPacketHeader.getLength()]; + in.get(payloadBytes, 0, payloadBytes.length); + + if (!BlocknetUtils.verifyChecksum(blocknetPacketHeader, payloadBytes)) { + throw new ProtocolException("XRouter packet's checksum failed to verify."); + } + + int dataLength = in.capacity() - in.position(); + + byte[] data = new byte[dataLength]; + in.get(data, 0, dataLength); + + return new XRouterMessage(params, data); + } + + @Override + public boolean isParseRetainMode() { + return parseRetain; + } + + @Override + public AddressMessage makeAddressMessage(byte[] payloadBytes, int length) throws ProtocolException, UnsupportedOperationException { + throw new UnsupportedOperationException("This serializer does not support address message construction."); + } + + @Override + public Message makeAlertMessage(byte[] payloadBytes) throws ProtocolException, UnsupportedOperationException { + throw new UnsupportedOperationException("This serializer does not support alert message construction."); + } + + @Override + public Block makeBlock(byte[] payloadBytes, int offset, int length) throws ProtocolException, UnsupportedOperationException { + throw new UnsupportedOperationException("This serializer does not support block construction."); + } + + @Override + public Message makeBloomFilter(byte[] payloadBytes) throws ProtocolException, UnsupportedOperationException { + throw new UnsupportedOperationException("This serializer does not support bloom filter construction."); + } + + @Override + public FilteredBlock makeFilteredBlock(byte[] payloadBytes) throws ProtocolException, UnsupportedOperationException { + throw new UnsupportedOperationException("This serializer does not support block construction."); + } + + @Override + public InventoryMessage makeInventoryMessage(byte[] payloadBytes, int length) throws ProtocolException, UnsupportedOperationException { + throw new UnsupportedOperationException("This serializer does not support inventory message construction."); + } + + @Override + public Transaction makeTransaction(byte[] payloadBytes, int offset, int length, byte[] hash) throws ProtocolException, UnsupportedOperationException { + throw new UnsupportedOperationException("This serializer does not support transaction construction"); + } + + @Override + public void serialize(String name, byte[] message, OutputStream out) throws UnsupportedOperationException { + throw new UnsupportedOperationException("This serializer currently does not support name/message serialization."); + } + + private void serialize(byte[] data, OutputStream out) throws IOException { + byte[] header = BlocknetUtils.getHeader("xrouter", (int) params.getPacketMagic(), data); + + out.write(header); + out.write(data); + + LOGGER.log(Level.FINER, "[blocknet-serializer] Serialized xrouter message. Bytes: " + new String(Hex.encode(header)) + new String(Hex.encode(data))); + } + + /** + * Serialize XRouter packet header and body + * @param message XRouterMessage The XRouter message to serialize + * @param out OutputStream The output stream to which to write the serialized XRouter message bytes + * @throws IOException If writing fails or another I/O related error occurs + */ + @Override + public void serialize(Message message, OutputStream out) throws IOException { + Preconditions.checkArgument(message instanceof XRouterMessage, "This message is not an XRouter message."); + + XRouterMessage xRouterMessage = (XRouterMessage) message; + serialize(xRouterMessage.bitcoinSerialize(), out); + } + + @Override + public void seekPastMagicBytes(ByteBuffer in) throws BufferUnderflowException { + BlocknetUtils.seekPastMagicBytes(in, params); + } } diff --git a/src/main/java/io/cloudchains/app/net/xrouter/XRouterPacketHeader.java b/src/main/java/io/cloudchains/app/net/xrouter/XRouterPacketHeader.java index f99cebd..a56c1aa 100644 --- a/src/main/java/io/cloudchains/app/net/xrouter/XRouterPacketHeader.java +++ b/src/main/java/io/cloudchains/app/net/xrouter/XRouterPacketHeader.java @@ -9,129 +9,129 @@ import java.util.logging.Logger; public class XRouterPacketHeader { - private final static LogManager LOGMANAGER = LogManager.getLogManager(); - private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); - - private byte[] compactSizeBytes; - private int version; - private int command; - private int timestamp; - private int size; - private String uuid; - private byte[] pubkey; - private byte[] signature; - - private int headerLength; - - public XRouterPacketHeader(ByteBuffer in) { - byte compactSize = in.get(); - byte[] rawHeader; - in.rewind(); - int cursor = 1; - - if (compactSize == (byte) 253) { - rawHeader = new byte[160]; - in.get(rawHeader, 0, rawHeader.length); - compactSizeBytes = new byte[2]; - } else if (compactSize == (byte) 254) { - rawHeader = new byte[162]; - in.get(rawHeader, 0, rawHeader.length); - compactSizeBytes = new byte[4]; - } else if (compactSize == (byte) 255) { - rawHeader = new byte[166]; - in.get(rawHeader, 0, rawHeader.length); - compactSizeBytes = new byte[8]; - } else { - rawHeader = new byte[158]; - in.get(rawHeader, 0, rawHeader.length); - compactSizeBytes = new byte[1]; - cursor = 0; - } - - System.arraycopy(rawHeader, cursor, compactSizeBytes, 0, compactSizeBytes.length); - cursor += compactSizeBytes.length; - - LOGGER.log(Level.FINER, "[xrouter] Retrieved compact size: " + new String(Hex.encode(new byte[]{compactSize}))); - LOGGER.log(Level.FINER, "[xrouter] Retrieved compact size bytes: " + new String(Hex.encode(compactSizeBytes))); - - version = (int) Utils.readUint32(rawHeader, cursor); - cursor += 4; - LOGGER.log(Level.FINER, "[xrouter] Retrieved version: " + version); - command = (int) Utils.readUint32(rawHeader, cursor); - cursor += 4; - LOGGER.log(Level.FINER, "[xrouter] Retrieved command: " + command); - timestamp = (int) Utils.readUint32(rawHeader, cursor); - cursor += 4; - LOGGER.log(Level.FINER, "[xrouter] Retrieved timestamp: " + timestamp); - size = (int) Utils.readUint32(rawHeader, cursor); - cursor += 4; - LOGGER.log(Level.FINER, "[xrouter] Retrieved size: " + size); - - //reserved header fields - //we don't use these fields, so we skip them - cursor += 8; - - byte[] uuidArr = new byte[36]; - System.arraycopy(rawHeader, cursor, uuidArr, 0, uuidArr.length); - cursor += 36; - - uuid = new String(uuidArr); - LOGGER.log(Level.FINER, "[xrouter] Retrieved UUID: " + uuid); - - byte[] pubkeyArr = new byte[33]; - System.arraycopy(rawHeader, cursor, pubkeyArr, 0, pubkeyArr.length); - cursor += 33; - LOGGER.log(Level.FINER, "[xrouter] Retrieved pubkey: " + new String(Hex.encode(pubkeyArr))); - pubkey = pubkeyArr; - - byte[] sigArr = new byte[64]; - System.arraycopy(rawHeader, cursor, sigArr, 0, sigArr.length); - cursor += 64; - LOGGER.log(Level.FINER, "[xrouter] Retrieved signature: " + new String(Hex.encode(sigArr))); - signature = sigArr; - - LOGGER.log(Level.FINER, "[xrouter] XRouter header read complete, at position: " + cursor); - headerLength = cursor; - //should have read 157 bytes at this point (excluding compact size) - } - - byte[] getCompactSizeBytes() { - return compactSizeBytes; - } - - int getVersion() { - return version; - } - - public int getCommand() { - return command; - } - - int getTimestamp() { - return timestamp; - } - - int getExtSize() { - return size + 157; - } - - int getSize() { - return size; - } - - public String getUUID() { - return uuid; - } - - byte[] getPubkey() { - return pubkey; - } - - byte[] getSignature() { - return signature; - } - - int getHeaderLength() { - return headerLength; - } + private final static LogManager LOGMANAGER = LogManager.getLogManager(); + private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); + + private byte[] compactSizeBytes; + private int version; + private int command; + private int timestamp; + private int size; + private String uuid; + private byte[] pubkey; + private byte[] signature; + + private int headerLength; + + public XRouterPacketHeader(ByteBuffer in) { + byte compactSize = in.get(); + byte[] rawHeader; + in.rewind(); + int cursor = 1; + + if (compactSize == (byte) 253) { + rawHeader = new byte[160]; + in.get(rawHeader, 0, rawHeader.length); + compactSizeBytes = new byte[2]; + } else if (compactSize == (byte) 254) { + rawHeader = new byte[162]; + in.get(rawHeader, 0, rawHeader.length); + compactSizeBytes = new byte[4]; + } else if (compactSize == (byte) 255) { + rawHeader = new byte[166]; + in.get(rawHeader, 0, rawHeader.length); + compactSizeBytes = new byte[8]; + } else { + rawHeader = new byte[158]; + in.get(rawHeader, 0, rawHeader.length); + compactSizeBytes = new byte[1]; + cursor = 0; + } + + System.arraycopy(rawHeader, cursor, compactSizeBytes, 0, compactSizeBytes.length); + cursor += compactSizeBytes.length; + + LOGGER.log(Level.FINER, "[xrouter] Retrieved compact size: " + new String(Hex.encode(new byte[]{compactSize}))); + LOGGER.log(Level.FINER, "[xrouter] Retrieved compact size bytes: " + new String(Hex.encode(compactSizeBytes))); + + version = (int) Utils.readUint32(rawHeader, cursor); + cursor += 4; + LOGGER.log(Level.FINER, "[xrouter] Retrieved version: " + version); + command = (int) Utils.readUint32(rawHeader, cursor); + cursor += 4; + LOGGER.log(Level.FINER, "[xrouter] Retrieved command: " + command); + timestamp = (int) Utils.readUint32(rawHeader, cursor); + cursor += 4; + LOGGER.log(Level.FINER, "[xrouter] Retrieved timestamp: " + timestamp); + size = (int) Utils.readUint32(rawHeader, cursor); + cursor += 4; + LOGGER.log(Level.FINER, "[xrouter] Retrieved size: " + size); + + //reserved header fields + //we don't use these fields, so we skip them + cursor += 8; + + byte[] uuidArr = new byte[36]; + System.arraycopy(rawHeader, cursor, uuidArr, 0, uuidArr.length); + cursor += 36; + + uuid = new String(uuidArr); + LOGGER.log(Level.FINER, "[xrouter] Retrieved UUID: " + uuid); + + byte[] pubkeyArr = new byte[33]; + System.arraycopy(rawHeader, cursor, pubkeyArr, 0, pubkeyArr.length); + cursor += 33; + LOGGER.log(Level.FINER, "[xrouter] Retrieved pubkey: " + new String(Hex.encode(pubkeyArr))); + pubkey = pubkeyArr; + + byte[] sigArr = new byte[64]; + System.arraycopy(rawHeader, cursor, sigArr, 0, sigArr.length); + cursor += 64; + LOGGER.log(Level.FINER, "[xrouter] Retrieved signature: " + new String(Hex.encode(sigArr))); + signature = sigArr; + + LOGGER.log(Level.FINER, "[xrouter] XRouter header read complete, at position: " + cursor); + headerLength = cursor; + //should have read 157 bytes at this point (excluding compact size) + } + + byte[] getCompactSizeBytes() { + return compactSizeBytes; + } + + int getVersion() { + return version; + } + + public int getCommand() { + return command; + } + + int getTimestamp() { + return timestamp; + } + + int getExtSize() { + return size + 157; + } + + int getSize() { + return size; + } + + public String getUUID() { + return uuid; + } + + byte[] getPubkey() { + return pubkey; + } + + byte[] getSignature() { + return signature; + } + + int getHeaderLength() { + return headerLength; + } } diff --git a/src/main/java/io/cloudchains/app/net/xrouter/XRouterPacketManager.java b/src/main/java/io/cloudchains/app/net/xrouter/XRouterPacketManager.java index 9b57085..7a5280e 100644 --- a/src/main/java/io/cloudchains/app/net/xrouter/XRouterPacketManager.java +++ b/src/main/java/io/cloudchains/app/net/xrouter/XRouterPacketManager.java @@ -17,247 +17,245 @@ import java.util.logging.Logger; public class XRouterPacketManager { - private final static LogManager LOGMANAGER = LogManager.getLogManager(); - private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); - - private static final int XROUTER_PACKET_VERSION = 0xff000023; - - private final XRouterMessageSerializer xRouterMessageSerializer; - private final BlocknetParameters blocknetNetworkParameters; - - public XRouterPacketManager(XRouterMessageSerializer xRouterMessageSerializer, BlocknetParameters blocknetNetworkParameters) { - this.xRouterMessageSerializer = xRouterMessageSerializer; - this.blocknetNetworkParameters = blocknetNetworkParameters; - } - - public static int getXRouterPacketVersion() { - return XROUTER_PACKET_VERSION; - } - - private byte[] signPacket(byte[] packetBytes, ECKey ecPrivateKey) { - LOGGER.log(Level.FINER, "[xrouter] DEBUG: Packet bytes: " + new String(Hex.encode(packetBytes))); - Sha256Hash packetHash = Sha256Hash.wrap(Sha256Hash.hash(packetBytes)); - LOGGER.log(Level.FINER, "[xrouter] DEBUG: Packet byte hash: " + packetHash.toString()); - ECKey.ECDSASignature rawSignature = ecPrivateKey.sign(packetHash).toCanonicalised(); - - byte[] r = rawSignature.r.toByteArray(); - byte[] s = rawSignature.s.toByteArray(); - - if (r.length > 32) { - LOGGER.log(Level.FINER, "[xrouter] WARNING: Signature R is greater than 32 bytes! Trimming from the beginning. Size: " + r.length); - LOGGER.log(Level.FINER, "[xrouter] WARNING: Signature R: " + new String(Hex.encode(r))); - } else if (r.length < 32) { - LOGGER.log(Level.FINER, "[xrouter] WARNING: Signature R is less than 32 bytes! Prepending null bytes to the beginning. Size: " + s.length); - LOGGER.log(Level.FINER, "[xrouter] WARNING: Signature R: " + new String(Hex.encode(r))); - - r = prependNullTo32(r); - } - - if (s.length > 32) { - LOGGER.log(Level.FINER, "[xrouter] WARNING: Signature S is greater than 32 bytes! Trimming from the beginning. Size: " + s.length); - LOGGER.log(Level.FINER, "[xrouter] WARNING: Signature S: " + new String(Hex.encode(s))); - } else if (s.length < 32) { - LOGGER.log(Level.FINER, "[xrouter] WARNING: Signature S is less than 32 bytes! Prepending null bytes. Size: " + s.length); - LOGGER.log(Level.FINER, "[xrouter] WARNING: Signature S: " + new String(Hex.encode(s))); - - s = prependNullTo32(s); - } + private final static LogManager LOGMANAGER = LogManager.getLogManager(); + private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); + + private static final int XROUTER_PACKET_VERSION = 0xff000023; + + private final XRouterMessageSerializer xRouterMessageSerializer; + private final BlocknetParameters blocknetNetworkParameters; + + public XRouterPacketManager(XRouterMessageSerializer xRouterMessageSerializer, BlocknetParameters blocknetNetworkParameters) { + this.xRouterMessageSerializer = xRouterMessageSerializer; + this.blocknetNetworkParameters = blocknetNetworkParameters; + } + + public static int getXRouterPacketVersion() { + return XROUTER_PACKET_VERSION; + } + + private byte[] signPacket(byte[] packetBytes, ECKey ecPrivateKey) { + LOGGER.log(Level.FINER, "[xrouter] DEBUG: Packet bytes: " + new String(Hex.encode(packetBytes))); + Sha256Hash packetHash = Sha256Hash.wrap(Sha256Hash.hash(packetBytes)); + LOGGER.log(Level.FINER, "[xrouter] DEBUG: Packet byte hash: " + packetHash.toString()); + ECKey.ECDSASignature rawSignature = ecPrivateKey.sign(packetHash).toCanonicalised(); + + byte[] r = rawSignature.r.toByteArray(); + byte[] s = rawSignature.s.toByteArray(); + + if (r.length > 32) { + LOGGER.log(Level.FINER, "[xrouter] WARNING: Signature R is greater than 32 bytes! Trimming from the beginning. Size: " + r.length); + LOGGER.log(Level.FINER, "[xrouter] WARNING: Signature R: " + new String(Hex.encode(r))); + } else if (r.length < 32) { + LOGGER.log(Level.FINER, "[xrouter] WARNING: Signature R is less than 32 bytes! Prepending null bytes to the beginning. Size: " + s.length); + LOGGER.log(Level.FINER, "[xrouter] WARNING: Signature R: " + new String(Hex.encode(r))); + + r = prependNullTo32(r); + } + + if (s.length > 32) { + LOGGER.log(Level.FINER, "[xrouter] WARNING: Signature S is greater than 32 bytes! Trimming from the beginning. Size: " + s.length); + LOGGER.log(Level.FINER, "[xrouter] WARNING: Signature S: " + new String(Hex.encode(s))); + } else if (s.length < 32) { + LOGGER.log(Level.FINER, "[xrouter] WARNING: Signature S is less than 32 bytes! Prepending null bytes. Size: " + s.length); + LOGGER.log(Level.FINER, "[xrouter] WARNING: Signature S: " + new String(Hex.encode(s))); + + s = prependNullTo32(s); + } - byte[] signature = new byte[64]; - - System.arraycopy(r, r.length - 32, signature, 0, 32); - System.arraycopy(s, s.length - 32, signature, 32, 32); + byte[] signature = new byte[64]; + + System.arraycopy(r, r.length - 32, signature, 0, 32); + System.arraycopy(s, s.length - 32, signature, 32, 32); - LOGGER.log(Level.FINER, "[xrouter] Signature: " + new String(Hex.encode(signature)) + ", byte length " + signature.length); - - return signature; - } - - private byte[] prependNullTo32(byte[] toPrependTo) { - Preconditions.checkState(toPrependTo.length < 32, "This array is too large (32 bytes or more)"); + LOGGER.log(Level.FINER, "[xrouter] Signature: " + new String(Hex.encode(signature)) + ", byte length " + signature.length); + + return signature; + } + + private byte[] prependNullTo32(byte[] toPrependTo) { + Preconditions.checkState(toPrependTo.length < 32, "This array is too large (32 bytes or more)"); - byte[] processed = new byte[32]; - - System.arraycopy(new byte[32 - toPrependTo.length], 0, processed, 0, 32 - toPrependTo.length); - System.arraycopy(toPrependTo, 0, processed, 32 - toPrependTo.length, toPrependTo.length); - - return processed; - } + byte[] processed = new byte[32]; + + System.arraycopy(new byte[32 - toPrependTo.length], 0, processed, 0, 32 - toPrependTo.length); + System.arraycopy(toPrependTo, 0, processed, 32 - toPrependTo.length, toPrependTo.length); + + return processed; + } - private XRouterMessage getPacket(BlocknetPeer blocknetPeer, int size, int commandId, String uuid, ECKey ecPublicKey, ECKey ecPrivateKey, HashMap body) { - Preconditions.checkNotNull(xRouterMessageSerializer); - Preconditions.checkNotNull(blocknetNetworkParameters); + private XRouterMessage getPacket(BlocknetPeer blocknetPeer, int size, int commandId, String uuid, ECKey ecPublicKey, ECKey ecPrivateKey, HashMap body) { + Preconditions.checkNotNull(xRouterMessageSerializer); + Preconditions.checkNotNull(blocknetNetworkParameters); - int extSize = size + 157; + int extSize = size + 157; - byte[] xRouterHeaderBytes; - byte compactSize; - int compactSizeBytes; + byte[] xRouterHeaderBytes; + byte compactSize; + int compactSizeBytes; - if (extSize < 253) { - xRouterHeaderBytes = new byte[158]; - compactSize = (byte) extSize; - //LOGGER.log(Level.FINER, "Compact size = " + compactSize); - xRouterHeaderBytes[0] = compactSize; - compactSizeBytes = 1; - } else if (extSize <= 65535) { - xRouterHeaderBytes = new byte[160]; - compactSize = (byte) 253; - //LOGGER.log(Level.FINER, "Compact size = " + compactSize + ", extSize = " + extSize); - xRouterHeaderBytes[0] = compactSize; - xRouterHeaderBytes[1] = (byte) (0xFF & (extSize)); - xRouterHeaderBytes[2] = (byte) (0xFF & (extSize >> 8)); - compactSizeBytes = 3; - } else { - xRouterHeaderBytes = new byte[162]; - compactSize = (byte) 254; - //LOGGER.log(Level.FINER, "Compact size = " + compactSize + ", extSize = " + extSize); - xRouterHeaderBytes[0] = compactSize; - Utils.uint32ToByteArrayLE(extSize, xRouterHeaderBytes, 1); - compactSizeBytes = 5; - } + if (extSize < 253) { + xRouterHeaderBytes = new byte[158]; + compactSize = (byte) extSize; + //LOGGER.log(Level.FINER, "Compact size = " + compactSize); + xRouterHeaderBytes[0] = compactSize; + compactSizeBytes = 1; + } else if (extSize <= 65535) { + xRouterHeaderBytes = new byte[160]; + compactSize = (byte) 253; + //LOGGER.log(Level.FINER, "Compact size = " + compactSize + ", extSize = " + extSize); + xRouterHeaderBytes[0] = compactSize; + xRouterHeaderBytes[1] = (byte) (0xFF & (extSize)); + xRouterHeaderBytes[2] = (byte) (0xFF & (extSize >> 8)); + compactSizeBytes = 3; + } else { + xRouterHeaderBytes = new byte[162]; + compactSize = (byte) 254; + //LOGGER.log(Level.FINER, "Compact size = " + compactSize + ", extSize = " + extSize); + xRouterHeaderBytes[0] = compactSize; + Utils.uint32ToByteArrayLE(extSize, xRouterHeaderBytes, 1); + compactSizeBytes = 5; + } - int cursor = compactSizeBytes; - //int cursor = 0; + int cursor = compactSizeBytes; + //int cursor = 0; - Utils.uint32ToByteArrayLE(XROUTER_PACKET_VERSION, xRouterHeaderBytes, cursor); - cursor += 4; - Utils.uint32ToByteArrayLE(commandId, xRouterHeaderBytes, cursor); - cursor += 4; - Utils.uint32ToByteArrayLE(Math.round(System.currentTimeMillis() / 1000), xRouterHeaderBytes, cursor); - cursor += 4; - Utils.uint32ToByteArrayLE(size, xRouterHeaderBytes, cursor); - cursor += 4; + Utils.uint32ToByteArrayLE(XROUTER_PACKET_VERSION, xRouterHeaderBytes, cursor); + cursor += 4; + Utils.uint32ToByteArrayLE(commandId, xRouterHeaderBytes, cursor); + cursor += 4; + Utils.uint32ToByteArrayLE(Math.round(System.currentTimeMillis() / 1000), xRouterHeaderBytes, cursor); + cursor += 4; + Utils.uint32ToByteArrayLE(size, xRouterHeaderBytes, cursor); + cursor += 4; - //fill in reserved header fields with zeros - System.arraycopy(new byte[8], 0, xRouterHeaderBytes, cursor, 8); - cursor += 8; + //fill in reserved header fields with zeros + System.arraycopy(new byte[8], 0, xRouterHeaderBytes, cursor, 8); + cursor += 8; - System.arraycopy(uuid.getBytes(), 0, xRouterHeaderBytes, cursor, 36); - cursor += 36; + System.arraycopy(uuid.getBytes(), 0, xRouterHeaderBytes, cursor, 36); + cursor += 36; - byte[] pubkey = ecPublicKey.getPubKey(); + byte[] pubkey = ecPublicKey.getPubKey(); - System.arraycopy(pubkey, 0, xRouterHeaderBytes, cursor, 33); - cursor += 33; + System.arraycopy(pubkey, 0, xRouterHeaderBytes, cursor, 33); + cursor += 33; - ByteBuffer xRouterHeaderBufSigned = ByteBuffer.allocate(xRouterHeaderBytes.length); - xRouterHeaderBufSigned.put(xRouterHeaderBytes); - xRouterHeaderBufSigned.position(cursor); + ByteBuffer xRouterHeaderBufSigned = ByteBuffer.allocate(xRouterHeaderBytes.length); + xRouterHeaderBufSigned.put(xRouterHeaderBytes); + xRouterHeaderBufSigned.position(cursor); - System.arraycopy(new byte[64], 0, xRouterHeaderBytes, cursor, 64); - cursor += 64; - LOGGER.log(Level.FINER, "[xrouter] Serialized XRouter header. Cursor is at " + cursor); + System.arraycopy(new byte[64], 0, xRouterHeaderBytes, cursor, 64); + cursor += 64; + LOGGER.log(Level.FINER, "[xrouter] Serialized XRouter header. Cursor is at " + cursor); - LOGGER.log(Level.FINER, "[xrouter] Serializing XRouter message (phase 1)."); - XRouterPacketHeader xRouterHeader = new XRouterPacketHeader(ByteBuffer.wrap(xRouterHeaderBytes)); - XRouterMessage message = new XRouterMessage(blocknetPeer, blocknetNetworkParameters, xRouterHeader, body); + LOGGER.log(Level.FINER, "[xrouter] Serializing XRouter message (phase 1)."); + XRouterPacketHeader xRouterHeader = new XRouterPacketHeader(ByteBuffer.wrap(xRouterHeaderBytes)); + XRouterMessage message = new XRouterMessage(blocknetPeer, blocknetNetworkParameters, xRouterHeader, body); - byte[] rawPktUnsigned = message.bitcoinSerialize(); + byte[] rawPktUnsigned = message.bitcoinSerialize(); - byte[] toSign = new byte[rawPktUnsigned.length - compactSizeBytes]; - System.arraycopy(rawPktUnsigned, compactSizeBytes, toSign, 0, toSign.length); + byte[] toSign = new byte[rawPktUnsigned.length - compactSizeBytes]; + System.arraycopy(rawPktUnsigned, compactSizeBytes, toSign, 0, toSign.length); - byte[] signature = signPacket(toSign, ecPrivateKey); - xRouterHeaderBufSigned.put(signature); + byte[] signature = signPacket(toSign, ecPrivateKey); + xRouterHeaderBufSigned.put(signature); - xRouterHeaderBufSigned.flip(); + xRouterHeaderBufSigned.flip(); - LOGGER.log(Level.FINER, "[xrouter] Serializing XRouter message (phase 2)."); - XRouterPacketHeader xRouterHeaderSigned = new XRouterPacketHeader(xRouterHeaderBufSigned); - return new XRouterMessage(blocknetPeer, blocknetNetworkParameters, xRouterHeaderSigned, body); - } + LOGGER.log(Level.FINER, "[xrouter] Serializing XRouter message (phase 2)."); + XRouterPacketHeader xRouterHeaderSigned = new XRouterPacketHeader(xRouterHeaderBufSigned); + return new XRouterMessage(blocknetPeer, blocknetNetworkParameters, xRouterHeaderSigned, body); + } - private HashMap getBody(@Nullable String paymentTx) { - HashMap body = new HashMap<>(); - if (paymentTx != null) - body.put("paymentTx", paymentTx); + private HashMap getBody(@Nullable String paymentTx) { + HashMap body = new HashMap<>(); + if (paymentTx != null) + body.put("paymentTx", paymentTx); - return body; - } + return body; + } - public XRouterMessage getXrGetBlockCount(BlocknetPeer blocknetPeer, String uuid, String currency, ECKey ecPrivateKey, ECKey ecPublicKey) { - String paymentTx = XRouterFeeUtils.getXRouterFeeTx(blocknetPeer,"xrGetBlockCount"); + public XRouterMessage getXrGetBlockCount(BlocknetPeer blocknetPeer, String uuid, String currency, ECKey ecPrivateKey, ECKey ecPublicKey) { + String paymentTx = XRouterFeeUtils.getXRouterFeeTx(blocknetPeer, "xrGetBlockCount"); - HashMap body = getBody(paymentTx); - body.put("currency", currency); + HashMap body = getBody(paymentTx); + body.put("currency", currency); - int size = currency.length() + 1 + paymentTx.length() + 1 + 4; + int size = currency.length() + 1 + paymentTx.length() + 1 + 4; - return getPacket(blocknetPeer, size, XRouterCommandUtils.commandStringToInt("xrGetBlockCount"), uuid, ecPublicKey, ecPrivateKey, body); - } + return getPacket(blocknetPeer, size, XRouterCommandUtils.commandStringToInt("xrGetBlockCount"), uuid, ecPublicKey, ecPrivateKey, body); + } - public XRouterMessage getXrService(BlocknetPeer blocknetPeer, String uuid, String command, ArrayList params, ECKey ecPrivateKey, ECKey ecPublicKey) { - String paymentTx = XRouterFeeUtils.getXRouterFeeTx(blocknetPeer, "xrService"); + public XRouterMessage getXrService(BlocknetPeer blocknetPeer, String uuid, String command, ArrayList params, ECKey ecPrivateKey, ECKey ecPublicKey) { + String paymentTx = XRouterFeeUtils.getXRouterFeeTx(blocknetPeer, "xrService"); - HashMap body = getBody(paymentTx); - body.put("command", command); - body.put("params", params); + HashMap body = getBody(paymentTx); + body.put("command", command); + body.put("params", params); - int size = command.length() + 1 + paymentTx.length() + 1 + 4; + int size = command.length() + 1 + paymentTx.length() + 1 + 4; - for (Object param : params) { - if (param instanceof Boolean) - size += 4; - else if (param instanceof String) - size += ((String) param).length() + 1; - else if (param instanceof Integer) - size += 4; - else - throw new IllegalArgumentException("Argument of unsupported class: " + param.getClass().getSimpleName()); - } + for (Object param : params) { + if (param instanceof Boolean) + size += 4;else if (param instanceof String) + size += ((String) param).length() + 1;else if (param instanceof Integer) + size += 4; + else + throw new IllegalArgumentException("Argument of unsupported class: " + param.getClass().getSimpleName()); + } - return getPacket(blocknetPeer, size, XRouterCommandUtils.commandStringToInt("xrService"), uuid, ecPublicKey, ecPrivateKey, body); - } + return getPacket(blocknetPeer, size, XRouterCommandUtils.commandStringToInt("xrService"), uuid, ecPublicKey, ecPrivateKey, body); + } - public XRouterMessage getXrGetConfig(BlocknetPeer blocknetPeer, String uuid, String address, ECKey ecPrivateKey, ECKey ecPublicKey) { - HashMap body = getBody(null); - body.put("addr", address); + public XRouterMessage getXrGetConfig(BlocknetPeer blocknetPeer, String uuid, String address, ECKey ecPrivateKey, ECKey ecPublicKey) { + HashMap body = getBody(null); + body.put("addr", address); - int size = address.length() + 1; + int size = address.length() + 1; - return getPacket(blocknetPeer, size, XRouterCommandUtils.commandStringToInt("xrGetConfig"), uuid, ecPublicKey, ecPrivateKey, body); - } + return getPacket(blocknetPeer, size, XRouterCommandUtils.commandStringToInt("xrGetConfig"), uuid, ecPublicKey, ecPrivateKey, body); + } - public XRouterMessage getXrSendTransaction(BlocknetPeer blocknetPeer, String uuid, String feePayment, String currency, String transaction, ECKey ecPrivateKey, ECKey ecPublicKey) { - HashMap body = getBody(feePayment); - body.put("currency", currency); - body.put("transaction", transaction); + public XRouterMessage getXrSendTransaction(BlocknetPeer blocknetPeer, String uuid, String feePayment, String currency, String transaction, ECKey ecPrivateKey, ECKey ecPublicKey) { + HashMap body = getBody(feePayment); + body.put("currency", currency); + body.put("transaction", transaction); - int size = feePayment.length() + 1 + currency.length() + 1 + 4 + transaction.length() + 1; + int size = feePayment.length() + 1 + currency.length() + 1 + 4 + transaction.length() + 1; - return getPacket(blocknetPeer, size, XRouterCommandUtils.commandStringToInt("xrSendTransaction"), uuid, ecPublicKey, ecPrivateKey, body); - } + return getPacket(blocknetPeer, size, XRouterCommandUtils.commandStringToInt("xrSendTransaction"), uuid, ecPublicKey, ecPrivateKey, body); + } - public XRouterMessage getXrGetBlockHash(BlocknetPeer blocknetPeer, String uuid, String feePayment, String currency, String blockIndex, ECKey ecPrivateKey, ECKey ecPublicKey) { - HashMap body = getBody(feePayment); - body.put("currency", currency); - body.put("blockId", blockIndex); + public XRouterMessage getXrGetBlockHash(BlocknetPeer blocknetPeer, String uuid, String feePayment, String currency, String blockIndex, ECKey ecPrivateKey, ECKey ecPublicKey) { + HashMap body = getBody(feePayment); + body.put("currency", currency); + body.put("blockId", blockIndex); - int size = feePayment.length() + 1 + currency.length() + 1 + 4 + blockIndex.length() + 1; + int size = feePayment.length() + 1 + currency.length() + 1 + 4 + blockIndex.length() + 1; - return getPacket(blocknetPeer, size, XRouterCommandUtils.commandStringToInt("xrGetBlockHash"), uuid, ecPublicKey, ecPrivateKey, body); - } + return getPacket(blocknetPeer, size, XRouterCommandUtils.commandStringToInt("xrGetBlockHash"), uuid, ecPublicKey, ecPrivateKey, body); + } - public XRouterMessage getXrGetBlock(BlocknetPeer blocknetPeer, String uuid, String feePayment, String currency, String blockHash, ECKey ecPrivateKey, ECKey ecPublicKey) { - HashMap body = getBody(feePayment); - body.put("currency", currency); - body.put("blockHash", blockHash); + public XRouterMessage getXrGetBlock(BlocknetPeer blocknetPeer, String uuid, String feePayment, String currency, String blockHash, ECKey ecPrivateKey, ECKey ecPublicKey) { + HashMap body = getBody(feePayment); + body.put("currency", currency); + body.put("blockHash", blockHash); - int size = feePayment.length() + 1 + currency.length() + 1 + blockHash.length() + 1 + 4; + int size = feePayment.length() + 1 + currency.length() + 1 + blockHash.length() + 1 + 4; - return getPacket(blocknetPeer, size, XRouterCommandUtils.commandStringToInt("xrGetBlock"), uuid, ecPublicKey, ecPrivateKey, body); - } + return getPacket(blocknetPeer, size, XRouterCommandUtils.commandStringToInt("xrGetBlock"), uuid, ecPublicKey, ecPrivateKey, body); + } - public XRouterMessage getXrGetTransaction(BlocknetPeer blocknetPeer, String uuid, String currency, String txid, ECKey ecPrivateKey, ECKey ecPublicKey) { - String paymentTx = XRouterFeeUtils.getXRouterFeeTx(blocknetPeer, "xrGetBlockCount"); + public XRouterMessage getXrGetTransaction(BlocknetPeer blocknetPeer, String uuid, String currency, String txid, ECKey ecPrivateKey, ECKey ecPublicKey) { + String paymentTx = XRouterFeeUtils.getXRouterFeeTx(blocknetPeer, "xrGetBlockCount"); - HashMap body = getBody(paymentTx); - body.put("currency", currency); - body.put("txid", txid); + HashMap body = getBody(paymentTx); + body.put("currency", currency); + body.put("txid", txid); - int size = paymentTx.length() + 1 + currency.length() + 1 + txid.length() + 1 + 4; + int size = paymentTx.length() + 1 + currency.length() + 1 + txid.length() + 1 + 4; - return getPacket(blocknetPeer, size, XRouterCommandUtils.commandStringToInt("xrGetTransaction"), uuid, ecPublicKey, ecPrivateKey, body); - } + return getPacket(blocknetPeer, size, XRouterCommandUtils.commandStringToInt("xrGetTransaction"), uuid, ecPublicKey, ecPrivateKey, body); + } } diff --git a/src/main/java/io/cloudchains/app/util/AddressBalance.java b/src/main/java/io/cloudchains/app/util/AddressBalance.java index 61bf335..2f61425 100644 --- a/src/main/java/io/cloudchains/app/util/AddressBalance.java +++ b/src/main/java/io/cloudchains/app/util/AddressBalance.java @@ -11,129 +11,129 @@ import java.util.stream.Collectors; public class AddressBalance { - private Address address; - private DumpedPrivateKey privateKey; - private AtomicReference addrProp = null; - private AtomicDouble balanceProp = null; - private ArrayList utxos = null; - - public AddressBalance(Address address, DumpedPrivateKey privateKey) { - this.address = address; - this.privateKey = privateKey; - setAddrProp(address.toBase58()); - } - - public Address getAddress() { - return address; - } - - public String getAddrProp() { - return addrProperty().get(); - } - - private void setAddrProp(String value) { - addrProperty().set(value); - } - - private AtomicReference addrProperty() { - if (addrProp == null) - addrProp = new AtomicReference("addrProp"); - return addrProp; - } - - public double getBalanceProp() { - return balanceProperty().get(); - } - - private void setBalanceProp(double value) { - balanceProperty().set(value); - } - - public AtomicDouble balanceProperty() { - if (balanceProp == null) - balanceProp = new AtomicDouble(0); - return balanceProp; - } - - public DumpedPrivateKey getPrivateKey() { - return privateKey; - } - - public void clearUtxos() { - if (utxos == null) - return; - - utxos.removeIf(utxo -> !utxo.isSpent()); - } - - public boolean addUtxo(UTXO utxo) { - Preconditions.checkNotNull(utxo); - if (this.utxos == null) - this.utxos = new ArrayList<>(); - - // Only add UTXO's that do not exist in our wallet - UTXO bUtxo = getUtxo(utxo.getTxid(), utxo.getVout()); - if (bUtxo == null) - this.utxos.add(utxo); - else - return false; - - calculateBalance(); - return true; - } - - public void setUtxos(ArrayList recvUtxos) { - ArrayList newUtxos = new ArrayList<>(); - - if (this.utxos != null && this.utxos.size() > 0) { - for (UTXO utxo : recvUtxos) { - for (UTXO bUtxo : this.utxos) { - if (!utxo.getTxid().equals(bUtxo.getTxid()) || utxo.getVout() != bUtxo.getVout()) { - newUtxos.add(utxo); - } - } - } - - if (newUtxos.size() > 0) { - this.utxos = newUtxos; - } - } else { - this.utxos = recvUtxos; - } - - calculateBalance(); - } - - private UTXO getUtxo(String txid, int vout) { - return utxos.stream().filter(o -> o.getTxid().equals(txid) && o.getVout() == vout).findFirst().orElse(null); - } - - public List getSpentUtxos() { - if (utxos == null) - utxos = new ArrayList<>(); - - return utxos.stream().filter(UTXO::isSpent).collect(Collectors.toList()); - } - - public List getUtxos() { - if (utxos == null) - utxos = new ArrayList<>(); - - return utxos.stream().filter(utxo -> !utxo.isSpent()).collect(Collectors.toList()); - } - - public void calculateBalance() { - Preconditions.checkNotNull(utxos); - double balance = 0; - - for (UTXO utxo : utxos) { - if (!utxo.isSpent()) { - balance += utxo.getValue(); - } - } - - balance /= 100000000.0; - setBalanceProp(balance); - } + private Address address; + private DumpedPrivateKey privateKey; + private AtomicReference addrProp = null; + private AtomicDouble balanceProp = null; + private ArrayList utxos = null; + + public AddressBalance(Address address, DumpedPrivateKey privateKey) { + this.address = address; + this.privateKey = privateKey; + setAddrProp(address.toBase58()); + } + + public Address getAddress() { + return address; + } + + public String getAddrProp() { + return addrProperty().get(); + } + + private void setAddrProp(String value) { + addrProperty().set(value); + } + + private AtomicReference addrProperty() { + if (addrProp == null) + addrProp = new AtomicReference("addrProp"); + return addrProp; + } + + public double getBalanceProp() { + return balanceProperty().get(); + } + + private void setBalanceProp(double value) { + balanceProperty().set(value); + } + + public AtomicDouble balanceProperty() { + if (balanceProp == null) + balanceProp = new AtomicDouble(0); + return balanceProp; + } + + public DumpedPrivateKey getPrivateKey() { + return privateKey; + } + + public void clearUtxos() { + if (utxos == null) + return; + + utxos.removeIf(utxo -> !utxo.isSpent()); + } + + public boolean addUtxo(UTXO utxo) { + Preconditions.checkNotNull(utxo); + if (this.utxos == null) + this.utxos = new ArrayList<>(); + + // Only add UTXO's that do not exist in our wallet + UTXO bUtxo = getUtxo(utxo.getTxid(), utxo.getVout()); + if (bUtxo == null) + this.utxos.add(utxo); + else + return false; + + calculateBalance(); + return true; + } + + public void setUtxos(ArrayList recvUtxos) { + ArrayList newUtxos = new ArrayList<>(); + + if (this.utxos != null && this.utxos.size() > 0) { + for (UTXO utxo : recvUtxos) { + for (UTXO bUtxo : this.utxos) { + if (!utxo.getTxid().equals(bUtxo.getTxid()) || utxo.getVout() != bUtxo.getVout()) { + newUtxos.add(utxo); + } + } + } + + if (newUtxos.size() > 0) { + this.utxos = newUtxos; + } + } else { + this.utxos = recvUtxos; + } + + calculateBalance(); + } + + private UTXO getUtxo(String txid, int vout) { + return utxos.stream().filter(o -> o.getTxid().equals(txid) && o.getVout() == vout).findFirst().orElse(null); + } + + public List getSpentUtxos() { + if (utxos == null) + utxos = new ArrayList<>(); + + return utxos.stream().filter(UTXO::isSpent).collect(Collectors.toList()); + } + + public List getUtxos() { + if (utxos == null) + utxos = new ArrayList<>(); + + return utxos.stream().filter(utxo -> !utxo.isSpent()).collect(Collectors.toList()); + } + + public void calculateBalance() { + Preconditions.checkNotNull(utxos); + double balance = 0; + + for (UTXO utxo : utxos) { + if (!utxo.isSpent()) { + balance += utxo.getValue(); + } + } + + balance /= 100000000.0; + setBalanceProp(balance); + } } diff --git a/src/main/java/io/cloudchains/app/util/AddressDiscoveryService.java b/src/main/java/io/cloudchains/app/util/AddressDiscoveryService.java index 05af66d..c3d7667 100644 --- a/src/main/java/io/cloudchains/app/util/AddressDiscoveryService.java +++ b/src/main/java/io/cloudchains/app/util/AddressDiscoveryService.java @@ -4,10 +4,8 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import io.cloudchains.app.net.CoinInstance; -import io.cloudchains.app.net.CoinTicker; import io.cloudchains.app.net.CoinTickerUtils; import io.cloudchains.app.net.api.http.client.HTTPClient; -import org.bitcoinj.core.Address; import java.util.ArrayList; import java.util.HashMap; @@ -20,24 +18,22 @@ public class AddressDiscoveryService { private final static LogManager LOGMANAGER = LogManager.getLogManager(); private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); - // Simplified configuration values private static final int GAP_LIMIT = 25; private static final int BATCH_SIZE = 100; private static final int MAX_DISCOVERY_DEPTH = 10000; private static final int DISCOVERY_TIMEOUT_MS = 30000; // 30 seconds max private static final int MAX_CONSECUTIVE_FAILURES = 3; - private final CoinInstance coinInstance; private final HTTPClient httpClient; private final ConfigHelper configHelper; private final String currencyString; - + // Enhanced logging helper private String getLogPrefix() { return "[discovery-" + currencyString + "]"; } - + public AddressDiscoveryService(CoinInstance coinInstance) { this.coinInstance = coinInstance; this.httpClient = new HTTPClient(5); @@ -45,90 +41,77 @@ public AddressDiscoveryService(CoinInstance coinInstance) { this.currencyString = CoinTickerUtils.tickerToString(coinInstance.getTicker()); LOGGER.log(Level.INFO, getLogPrefix() + " AddressDiscoveryService initialized for " + currencyString); } - + /** * Main discovery method - determines correct addressCount based on last used address with funds + 1 */ public int discoverAddressCount() { LOGGER.log(Level.INFO, getLogPrefix() + " Starting address discovery for " + currencyString); - long discoveryStartTime = System.currentTimeMillis(); int consecutiveFailures = 0; int lastUsedIndex = -1; int consecutiveEmpty = 0; int currentAddressCount = configHelper.getAddressCount(); int batchStart = currentAddressCount; - LOGGER.log(Level.INFO, getLogPrefix() + " Starting discovery from address index: " + currentAddressCount); - try { while (consecutiveEmpty < GAP_LIMIT && batchStart < MAX_DISCOVERY_DEPTH) { // Check for discovery timeout long elapsedTime = System.currentTimeMillis() - discoveryStartTime; if (elapsedTime > DISCOVERY_TIMEOUT_MS) { LOGGER.log(Level.WARNING, getLogPrefix() + " Discovery timeout reached after " + - (elapsedTime / 1000) + " seconds, aborting discovery"); + (elapsedTime / 1000) + " seconds, aborting discovery"); return configHelper.getAddressCount(); } - // Check for consecutive failures (circuit breaker) if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) { LOGGER.log(Level.SEVERE, getLogPrefix() + " Maximum consecutive failures (" + - MAX_CONSECUTIVE_FAILURES + ") reached, aborting discovery"); + MAX_CONSECUTIVE_FAILURES + ") reached, aborting discovery"); return configHelper.getAddressCount(); } - // Progress logging every 5 batches if (batchStart > currentAddressCount && batchStart % (BATCH_SIZE * 5) == 0) { LOGGER.log(Level.INFO, getLogPrefix() + " Discovery progress: " + batchStart + - " addresses checked, " + consecutiveEmpty + " consecutive empty"); + " addresses checked, " + consecutiveEmpty + " consecutive empty"); } - LOGGER.log(Level.FINE, getLogPrefix() + " Processing batch starting at index " + batchStart); - // Generate batch of addresses List batch = generateAddressBatch(batchStart, BATCH_SIZE); - // Check for UTXOs in batch List batchUtxos = checkBatchForUtxos(batch); - // Handle HTTP failures with circuit breaker if (batchUtxos == null) { consecutiveFailures++; LOGGER.log(Level.WARNING, getLogPrefix() + " HTTP failure " + consecutiveFailures + - "/" + MAX_CONSECUTIVE_FAILURES + " for batch starting at " + batchStart); + "/" + MAX_CONSECUTIVE_FAILURES + " for batch starting at " + batchStart); // Continue to next batch instead of failing immediately batchStart += BATCH_SIZE; continue; } else { consecutiveFailures = 0; // Reset failure count on success } - if (!batchUtxos.isEmpty()) { // Found UTXOs - update last used index int batchLastUsedIndex = findLastUsedIndex(batch, batchUtxos); int globalLastUsedIndex = batchStart + batchLastUsedIndex; lastUsedIndex = Math.max(lastUsedIndex, globalLastUsedIndex); consecutiveEmpty = 0; - - LOGGER.log(Level.INFO, getLogPrefix() + " Found UTXOs in batch, last used index: " + - globalLastUsedIndex + ", batch range: " + batchStart + "-" + - (batchStart + BATCH_SIZE - 1)); + LOGGER.log(Level.INFO, getLogPrefix() + " Found UTXOs in batch, last used index: " + + globalLastUsedIndex + ", batch range: " + batchStart + "-" + + (batchStart + BATCH_SIZE - 1)); } else { consecutiveEmpty += BATCH_SIZE; - LOGGER.log(Level.INFO, getLogPrefix() + " Empty batch (addresses " + batchStart + "-" + - (batchStart + BATCH_SIZE - 1) + "), consecutive empty: " + consecutiveEmpty); + LOGGER.log(Level.INFO, getLogPrefix() + " Empty batch (addresses " + batchStart + "-" + + (batchStart + BATCH_SIZE - 1) + "), consecutive empty: " + consecutiveEmpty); } - batchStart += BATCH_SIZE; - // Safety check for max depth if (batchStart >= MAX_DISCOVERY_DEPTH) { LOGGER.log(Level.WARNING, getLogPrefix() + " Hit max discovery depth at " + MAX_DISCOVERY_DEPTH); break; } } - + // Calculate final address count: last used address with funds detected + 1 int finalCount; if (lastUsedIndex >= 0) { @@ -140,28 +123,26 @@ public int discoverAddressCount() { finalCount = configHelper.getAddressCount(); LOGGER.log(Level.INFO, getLogPrefix() + " No used addresses found, keeping current address count: " + finalCount); } - + LOGGER.log(Level.INFO, getLogPrefix() + " Discovery complete for " + currencyString + - ". Last used index: " + lastUsedIndex + ", final address count: " + finalCount); - + ". Last used index: " + lastUsedIndex + ", final address count: " + finalCount); + return finalCount; - + } catch (Exception e) { LOGGER.log(Level.SEVERE, getLogPrefix() + " Error during discovery for " + currencyString, e); return configHelper.getAddressCount(); } } - + /** * Generate a batch of addresses starting from a specific index */ private List generateAddressBatch(int startIndex, int batchSize) { List batch = new ArrayList<>(); - // Ensure we have enough addresses generated int currentGenerated = coinInstance.getAddressKeyPairs().size(); int needed = startIndex + batchSize; - if (needed > currentGenerated) { // Generate additional addresses starting from currentGenerated for (int i = currentGenerated; i < needed; i++) { @@ -169,17 +150,15 @@ private List generateAddressBatch(int startIndex, int batchSize) // Don't add to batch here - we'll extract the correct slice below } LOGGER.log(Level.INFO, getLogPrefix() + " Generated " + (needed - currentGenerated) + - " new addresses for " + currencyString); + " new addresses for " + currencyString); } - // Always extract the batch from the correct startIndex range for (int i = startIndex; i < needed; i++) { batch.add(coinInstance.getAddressKeyPairs().get(i)); } - return batch; } - + /** * Check a batch of addresses for UTXOs */ @@ -187,36 +166,32 @@ private List checkBatchForUtxos(List batch) { if (batch.isEmpty()) { return new ArrayList<>(); } - // Extract addresses for UTXO query String[] addresses = batch.stream() - .map(addr -> addr.getAddress().toBase58()) - .toArray(String[]::new); - + .map(addr -> addr.getAddress().toBase58()) + .toArray(String[]::new); JsonArray utxoResponse = null; try { utxoResponse = httpClient.getUtxosUncached(coinInstance.getTicker(), addresses); } catch (Exception e) { LOGGER.log(Level.SEVERE, getLogPrefix() + " HTTP request failed for addresses " + - addresses[0] + "..." + addresses[addresses.length - 1], e); + addresses[0] + "..." + addresses[addresses.length - 1], e); return null; // Signal failure to caller } - if (utxoResponse == null || utxoResponse.size() == 0) { return new ArrayList<>(); } - List utxos = new ArrayList<>(); for (JsonElement element : utxoResponse) { try { JsonObject utxoJson = element.getAsJsonObject(); UTXO utxo = new UTXO( - coinInstance.getTicker(), - utxoJson.get("address").getAsString(), - utxoJson.get("txid").getAsString(), - utxoJson.get("vout").getAsInt(), - utxoJson.get("confirmations").getAsInt(), - (long) (utxoJson.get("value").getAsDouble() * 100000000.0) + coinInstance.getTicker(), + utxoJson.get("address").getAsString(), + utxoJson.get("txid").getAsString(), + utxoJson.get("vout").getAsInt(), + utxoJson.get("confirmations").getAsInt(), + (long) (utxoJson.get("value").getAsDouble() * 100000000.0) ); utxos.add(utxo); } catch (Exception e) { @@ -224,10 +199,9 @@ private List checkBatchForUtxos(List batch) { // Continue processing other UTXOs instead of failing completely } } - return utxos; } - + /** * Find the last used address index in the batch * Uses HashMap for O(1) lookups when batch size is large enough to benefit @@ -239,7 +213,7 @@ private int findLastUsedIndex(List batch, List utxos) { for (int i = 0; i < batch.size(); i++) { addressToIndex.put(batch.get(i).getAddress().toBase58(), i); } - + int lastIndex = 0; for (UTXO utxo : utxos) { Integer index = addressToIndex.get(utxo.getAddress()); diff --git a/src/main/java/io/cloudchains/app/util/CCLogger.java b/src/main/java/io/cloudchains/app/util/CCLogger.java index 6df69f5..8aead96 100644 --- a/src/main/java/io/cloudchains/app/util/CCLogger.java +++ b/src/main/java/io/cloudchains/app/util/CCLogger.java @@ -1,7 +1,5 @@ package io.cloudchains.app.util; -import java.io.*; - public class CCLogger { private static boolean isLogging; diff --git a/src/main/java/io/cloudchains/app/util/ConfigHelper.java b/src/main/java/io/cloudchains/app/util/ConfigHelper.java index 1aa8ca2..b2ff02d 100644 --- a/src/main/java/io/cloudchains/app/util/ConfigHelper.java +++ b/src/main/java/io/cloudchains/app/util/ConfigHelper.java @@ -12,232 +12,232 @@ import java.util.logging.Logger; public class ConfigHelper { - private final static LogManager LOGMANAGER = LogManager.getLogManager(); - private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); - - private String tickerStr; - private File file; - private FileWriter fileWriter; - - private double fee; - private boolean feeFlat; - private boolean rpcEnabled; - private String rpcUsername; - private String rpcPassword; - private int rpcPort; - private int addressCount; - - // Override specific configuration directory (useful in unit tests) - public static String CONFIG_DIR = ""; // Must not end with [/], e.g. /home/user/.config, not /home/user/.config/ - - public ConfigHelper(String tickerStr) { - this.tickerStr = tickerStr; - - try { - file = Preconditions.checkNotNull(this.getFile()); - loadConfig(); - } catch (Exception e) { - e.printStackTrace(); - } - } - - public void loadConfig() { - try { - String rawConfig = new String(Files.readAllBytes(file.toPath())); - if (rawConfig.isEmpty()) { - fee = 0.0001; - feeFlat = true; - rpcEnabled = false; - rpcUsername = ""; - rpcPassword = ""; - if (this.tickerStr.equalsIgnoreCase("master")) { - rpcPort = 9955; - } else { - rpcPort = -1000; - } - addressCount = 0; - - writeConfig(); - return; - } - - JSONObject config = new JSONObject(rawConfig); - - final String[] configKeys = new String[] { - "fee", - "feeFlat", - "rpcEnabled", - "rpcUsername", - "rpcPassword", - "rpcPort", - "addressCount" - }; - - for (String configKey : configKeys) { - if (!config.has(configKey)) { - LOGGER.log(Level.FINER, "[config] Warning: Configuration file does not contain required value '" + configKey + "'. This will probably break things later on."); - } - } - - fee = config.getDouble("fee"); - feeFlat = config.getBoolean("feeFlat"); - rpcEnabled = config.getBoolean("rpcEnabled"); - rpcUsername = config.getString("rpcUsername"); - rpcPassword = config.getString("rpcPassword"); - rpcPort = config.getInt("rpcPort"); - - if (!config.has("addressCount")) { - setAddressCount(0); - writeConfig(); - } else { - addressCount = config.getInt("addressCount"); - } - } catch (Exception e) { - LOGGER.log(Level.FINER, "[config] ERROR: Error while reading config file!"); - e.printStackTrace(); - } - } - - private File getFile() { - String userHome = getLocalDataDirectory(); - Preconditions.checkNotNull(userHome); - - File home = new File(userHome); - File settingsDirectory = new File(home, "settings"); - if (!settingsDirectory.exists()) { - if (!settingsDirectory.mkdirs()) { - LOGGER.log(Level.FINER, "[config] ERROR: Could not create base/settings directory!"); - return null; - } - } - - File configFile = new File(settingsDirectory, "config-" + tickerStr + ".json"); - try { - if (!configFile.createNewFile() && !configFile.exists()) - return null; - } catch (IOException e) { - e.printStackTrace(); - } - - return configFile; - } - - public void setFee(double fee) { - this.fee = fee; - } - - public void setFlatFee(boolean flat) { - this.feeFlat = flat; - } - - public void setRpcEnabled(boolean isEnabled) { - this.rpcEnabled = isEnabled; - } - - public void setRpcUsername(String user) { - this.rpcUsername = user; - } - - public void setRpcPassword(String pass) { - this.rpcPassword = pass; - } - - public void setRpcPort(int rpcPort) { - if (PortCheck.available(rpcPort)) - this.rpcPort = rpcPort; - else - setRpcPort(rpcPort + 1); - } - - public void setAddressCount(int addressCount) { - this.addressCount = addressCount; - } - - public double getFee() { - return fee; - } - - public boolean isFlatFee() { - return feeFlat; - } - - public boolean isRpcEnabled() { - return rpcEnabled; - } - - public String getRpcUsername() { - return rpcUsername; - } - - public String getRpcPassword() { - return rpcPassword; - } - - public int getMasterRpcPort() { - if (rpcPort == -1000) { - rpcPort = 9955; - } - - return rpcPort; - } - - public int getRpcPort() { - return rpcPort; - } - - public int getAddressCount() { - return addressCount; - } - - public boolean validAuth() { - return rpcUsername != null && !rpcUsername.equals("") && rpcPassword != null && !rpcPassword.equals(""); - } - - public void writeConfig() { - try { - fileWriter = new FileWriter(file, false); - - JSONObject config = new JSONObject(); - config.put("fee", fee); - config.put("feeFlat", feeFlat); - config.put("rpcEnabled", rpcEnabled); - config.put("rpcUsername", rpcUsername); - config.put("rpcPassword", rpcPassword); - config.put("rpcPort", rpcPort); - config.put("addressCount", addressCount); - - fileWriter.write(config.toString(4)); - fileWriter.flush(); - fileWriter.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - public static String getLocalDataDirectory() { - String userHomeDir; - if (CONFIG_DIR.isEmpty()) { - String OS = (System.getProperty("os.name")).toLowerCase(); - - if (OS.contains("win")) { - userHomeDir = System.getenv("AppData"); - } else if (OS.contains("nix") || OS.contains("nux") || OS.contains("aix")) { - userHomeDir = System.getProperty("user.home") + File.separator + ".config"; - } else if (OS.contains("mac")) { - userHomeDir = System.getProperty("user.home") + File.separator + "Library" + File.separator + "Application Support"; - } else { - userHomeDir = System.getProperty("user.home") + File.separator + ".config"; - } - userHomeDir += File.separator + "CloudChains" + File.separator; - } else { - userHomeDir = CONFIG_DIR + File.separator + "CloudChains" + File.separator; - } - - File directory = new File(userHomeDir); - if (!directory.exists()) { - directory.mkdir(); - } - - return userHomeDir; - } + private final static LogManager LOGMANAGER = LogManager.getLogManager(); + private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); + + private String tickerStr; + private File file; + private FileWriter fileWriter; + + private double fee; + private boolean feeFlat; + private boolean rpcEnabled; + private String rpcUsername; + private String rpcPassword; + private int rpcPort; + private int addressCount; + + // Override specific configuration directory (useful in unit tests) + public static String CONFIG_DIR = ""; // Must not end with [/], e.g. /home/user/.config, not /home/user/.config/ + + public ConfigHelper(String tickerStr) { + this.tickerStr = tickerStr; + + try { + file = Preconditions.checkNotNull(this.getFile()); + loadConfig(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void loadConfig() { + try { + String rawConfig = new String(Files.readAllBytes(file.toPath())); + if (rawConfig.isEmpty()) { + fee = 0.0001; + feeFlat = true; + rpcEnabled = false; + rpcUsername = ""; + rpcPassword = ""; + if (this.tickerStr.equalsIgnoreCase("master")) { + rpcPort = 9955; + } else { + rpcPort = -1000; + } + addressCount = 0; + + writeConfig(); + return; + } + + JSONObject config = new JSONObject(rawConfig); + + final String[] configKeys = new String[]{ + "fee", + "feeFlat", + "rpcEnabled", + "rpcUsername", + "rpcPassword", + "rpcPort", + "addressCount" + }; + + for (String configKey : configKeys) { + if (!config.has(configKey)) { + LOGGER.log(Level.FINER, "[config] Warning: Configuration file does not contain required value '" + configKey + "'. This will probably break things later on."); + } + } + + fee = config.getDouble("fee"); + feeFlat = config.getBoolean("feeFlat"); + rpcEnabled = config.getBoolean("rpcEnabled"); + rpcUsername = config.getString("rpcUsername"); + rpcPassword = config.getString("rpcPassword"); + rpcPort = config.getInt("rpcPort"); + + if (!config.has("addressCount")) { + setAddressCount(0); + writeConfig(); + } else { + addressCount = config.getInt("addressCount"); + } + } catch (Exception e) { + LOGGER.log(Level.FINER, "[config] ERROR: Error while reading config file!"); + e.printStackTrace(); + } + } + + private File getFile() { + String userHome = getLocalDataDirectory(); + Preconditions.checkNotNull(userHome); + + File home = new File(userHome); + File settingsDirectory = new File(home, "settings"); + if (!settingsDirectory.exists()) { + if (!settingsDirectory.mkdirs()) { + LOGGER.log(Level.FINER, "[config] ERROR: Could not create base/settings directory!"); + return null; + } + } + + File configFile = new File(settingsDirectory, "config-" + tickerStr + ".json"); + try { + if (!configFile.createNewFile() && !configFile.exists()) + return null; + } catch (IOException e) { + e.printStackTrace(); + } + + return configFile; + } + + public void setFee(double fee) { + this.fee = fee; + } + + public void setFlatFee(boolean flat) { + this.feeFlat = flat; + } + + public void setRpcEnabled(boolean isEnabled) { + this.rpcEnabled = isEnabled; + } + + public void setRpcUsername(String user) { + this.rpcUsername = user; + } + + public void setRpcPassword(String pass) { + this.rpcPassword = pass; + } + + public void setRpcPort(int rpcPort) { + if (PortCheck.available(rpcPort)) + this.rpcPort = rpcPort; + else + setRpcPort(rpcPort + 1); + } + + public void setAddressCount(int addressCount) { + this.addressCount = addressCount; + } + + public double getFee() { + return fee; + } + + public boolean isFlatFee() { + return feeFlat; + } + + public boolean isRpcEnabled() { + return rpcEnabled; + } + + public String getRpcUsername() { + return rpcUsername; + } + + public String getRpcPassword() { + return rpcPassword; + } + + public int getMasterRpcPort() { + if (rpcPort == -1000) { + rpcPort = 9955; + } + + return rpcPort; + } + + public int getRpcPort() { + return rpcPort; + } + + public int getAddressCount() { + return addressCount; + } + + public boolean validAuth() { + return rpcUsername != null && !rpcUsername.equals("") && rpcPassword != null && !rpcPassword.equals(""); + } + + public void writeConfig() { + try { + fileWriter = new FileWriter(file, false); + + JSONObject config = new JSONObject(); + config.put("fee", fee); + config.put("feeFlat", feeFlat); + config.put("rpcEnabled", rpcEnabled); + config.put("rpcUsername", rpcUsername); + config.put("rpcPassword", rpcPassword); + config.put("rpcPort", rpcPort); + config.put("addressCount", addressCount); + + fileWriter.write(config.toString(4)); + fileWriter.flush(); + fileWriter.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static String getLocalDataDirectory() { + String userHomeDir; + if (CONFIG_DIR.isEmpty()) { + String OS = (System.getProperty("os.name")).toLowerCase(); + + if (OS.contains("win")) { + userHomeDir = System.getenv("AppData"); + } else if (OS.contains("nix") || OS.contains("nux") || OS.contains("aix")) { + userHomeDir = System.getProperty("user.home") + File.separator + ".config"; + } else if (OS.contains("mac")) { + userHomeDir = System.getProperty("user.home") + File.separator + "Library" + File.separator + "Application Support"; + } else { + userHomeDir = System.getProperty("user.home") + File.separator + ".config"; + } + userHomeDir += File.separator + "CloudChains" + File.separator; + } else { + userHomeDir = CONFIG_DIR + File.separator + "CloudChains" + File.separator; + } + + File directory = new File(userHomeDir); + if (!directory.exists()) { + directory.mkdir(); + } + + return userHomeDir; + } } diff --git a/src/main/java/io/cloudchains/app/util/DetectOS.java b/src/main/java/io/cloudchains/app/util/DetectOS.java index ed7aa2d..ea686e9 100644 --- a/src/main/java/io/cloudchains/app/util/DetectOS.java +++ b/src/main/java/io/cloudchains/app/util/DetectOS.java @@ -9,10 +9,8 @@ public class DetectOS { String OS = System.getProperty("os.name").toLowerCase(); if (OS.contains("win")) - isWindows = true; - else if (OS.contains("mac")) - isOSX = true; - else if (OS.contains("nix") || OS.contains("nux") || OS.contains("aix")) + isWindows = true;else if (OS.contains("mac")) + isOSX = true;else if (OS.contains("nix") || OS.contains("nux") || OS.contains("aix")) isUnix = true; } } diff --git a/src/main/java/io/cloudchains/app/util/UTXO.java b/src/main/java/io/cloudchains/app/util/UTXO.java index 680e929..92496c7 100644 --- a/src/main/java/io/cloudchains/app/util/UTXO.java +++ b/src/main/java/io/cloudchains/app/util/UTXO.java @@ -6,77 +6,79 @@ import org.bitcoinj.core.Address; import org.bitcoinj.core.Coin; import org.bitcoinj.core.Sha256Hash; -import org.bitcoinj.core.Transaction; import org.bitcoinj.script.Script; import org.bitcoinj.script.ScriptBuilder; public class UTXO { - @SerializedName("address") private String addressB58; - @SerializedName("txhash") private String txid; - @SerializedName("block_number") private int height; - protected transient long value; - private int vout; - private boolean spent; - protected CoinTicker ticker; - - public UTXO(CoinTicker ticker, String addressB58, String txid, int vout, int blockHeight, long value) { - this.addressB58 = addressB58; - this.txid = txid; - this.vout = vout; - this.height = blockHeight; - this.value = value; - this.spent = false; - - this.ticker = ticker; - } - - public void setSpent(boolean spentBool) { - spent = spentBool; - } - - public String getAddress() { - return addressB58; - } - - public String getTxid() { - return txid; - } - - public long getValue() { - return value; - } - - public int getVout() { - return vout; - } - - public int getHeight() { - return height; - } - - public boolean isSpent() { - return spent; - } - - public double getAmount() { - return getValue() / 100000000.0; - } - - public org.bitcoinj.core.UTXO createUTXO() { - Address address = Address.fromBase58(CoinInstance.getInstance(this.ticker).getNetworkParameters(), getAddress()); - - Script scriptForUTXO = ScriptBuilder.createOutputScript(address); - - Sha256Hash sha256Hash = Sha256Hash.wrap(getTxid()); - return new org.bitcoinj.core.UTXO(sha256Hash, getVout(), Coin.valueOf(getValue()), getHeight(), false, scriptForUTXO, getAddress()); - } - - public String toString() { - return "\n-------------------------\n" + - "Address=" + getAddress() + - "\nTXHash=" + getTxid() + - "\nVout=" + getVout() + - "\nBlockNumber=" + getHeight() + - "\nValue=" + getValue(); - } + @SerializedName("address") + private String addressB58; + @SerializedName("txhash") + private String txid; + @SerializedName("block_number") + private int height; + protected transient long value; + private int vout; + private boolean spent; + protected CoinTicker ticker; + + public UTXO(CoinTicker ticker, String addressB58, String txid, int vout, int blockHeight, long value) { + this.addressB58 = addressB58; + this.txid = txid; + this.vout = vout; + this.height = blockHeight; + this.value = value; + this.spent = false; + + this.ticker = ticker; + } + + public void setSpent(boolean spentBool) { + spent = spentBool; + } + + public String getAddress() { + return addressB58; + } + + public String getTxid() { + return txid; + } + + public long getValue() { + return value; + } + + public int getVout() { + return vout; + } + + public int getHeight() { + return height; + } + + public boolean isSpent() { + return spent; + } + + public double getAmount() { + return getValue() / 100000000.0; + } + + public org.bitcoinj.core.UTXO createUTXO() { + Address address = Address.fromBase58(CoinInstance.getInstance(this.ticker).getNetworkParameters(), getAddress()); + + Script scriptForUTXO = ScriptBuilder.createOutputScript(address); + + Sha256Hash sha256Hash = Sha256Hash.wrap(getTxid()); + return new org.bitcoinj.core.UTXO(sha256Hash, getVout(), Coin.valueOf(getValue()), getHeight(), false, scriptForUTXO, getAddress()); + } + + public String toString() { + return "\n-------------------------\n" + + "Address=" + getAddress() + + "\nTXHash=" + getTxid() + + "\nVout=" + getVout() + + "\nBlockNumber=" + getHeight() + + "\nValue=" + getValue(); + } } diff --git a/src/main/java/io/cloudchains/app/util/Utility.java b/src/main/java/io/cloudchains/app/util/Utility.java index 0048df2..911952d 100644 --- a/src/main/java/io/cloudchains/app/util/Utility.java +++ b/src/main/java/io/cloudchains/app/util/Utility.java @@ -9,7 +9,7 @@ public static boolean isValidAddress(NetworkParameters params, String address) { try { Address.fromBase58(params, address); return true; - } catch(AddressFormatException e) { + } catch (AddressFormatException e) { return false; } } diff --git a/src/main/java/io/cloudchains/app/util/XRouterConfiguration.java b/src/main/java/io/cloudchains/app/util/XRouterConfiguration.java index a9e6e2b..b88b758 100644 --- a/src/main/java/io/cloudchains/app/util/XRouterConfiguration.java +++ b/src/main/java/io/cloudchains/app/util/XRouterConfiguration.java @@ -13,205 +13,204 @@ import java.util.logging.Logger; public class XRouterConfiguration { - private final static LogManager LOGMANAGER = LogManager.getLogManager(); - private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); - - private final String rawXRouterConfig; - private final HashMap feeMap = new HashMap<>(); - private final ArrayList supportedWallets = new ArrayList<>(); - private String feeAddress; - private int timeout; - private int blockLimit; - - private static HashBiMap pluginParamTypes; - - static { - pluginParamTypes = HashBiMap.create(2); - - pluginParamTypes.put("string", String.class); - pluginParamTypes.put("int", Integer.class); - pluginParamTypes.put("bool", Boolean.class); - } - - public static String getStringByClass(Class clazz) { - return pluginParamTypes.inverse().get(clazz); - } - - public static class XRouterPluginConfiguration { - private final String rawPluginConfig; - private final String pluginName; - - private ArrayList paramTypes = new ArrayList<>(); - private double fee; - private int clientRequestLimit; - - public XRouterPluginConfiguration(String pluginName, String rawPluginConfig) { - this.pluginName = pluginName; - this.rawPluginConfig = rawPluginConfig; - } - - public void parsePluginConfig() { - Properties properties; - try { - properties = getPluginProperties(rawPluginConfig); - } catch (IOException e) { - LOGGER.log(Level.FINER, "[xrouter-plugin-config-parser] ERROR: Error while parsing plugin config!"); - e.printStackTrace(); - return; - } - - if (!properties.containsKey("parameters") && !properties.containsKey("paramsType")) { - LOGGER.log(Level.FINER, "[xrouter-plugin-config-parser] ERROR: Plugin has no parameters!"); - } else { - String[] rawParamTypes; - - if (properties.containsKey("parameters")) - rawParamTypes = properties.getProperty("parameters").split(","); - else if (properties.containsKey("paramsType")) - rawParamTypes = properties.getProperty("paramsType").split(","); - else - return; - - for (String rawParamType : rawParamTypes) { - if (rawParamType.isEmpty()) - continue; - - if (!pluginParamTypes.containsKey(rawParamType)) { - LOGGER.log(Level.FINER, "[xrouter-plugin-config-parser] ERROR: Invalid/unsupported plugin parameter type: " + rawParamType + ". Failing."); - throw new IllegalArgumentException("Invalid/unsupported plugin parameter type: " + rawParamType); - } - - paramTypes.add(pluginParamTypes.get(rawParamType)); - } - } - - if (properties.contains("fee")) { - fee = Double.parseDouble(properties.getProperty("fee")); - } else { - fee = 0; - } - - if (properties.contains("clientrequestlimit")) { - clientRequestLimit = Integer.parseInt(properties.getProperty("clientrequestlimit")); - } else { - clientRequestLimit = 100; - } - - LOGGER.log(Level.FINER, "[xrouter-plugin-config-parser] Processing '" + pluginName + "' complete."); - LOGGER.log(Level.FINER, "[xrouter-plugin-config-parser] DEBUG: " + pluginName + ": fee = " + fee); - LOGGER.log(Level.FINER, "[xrouter-plugin-config-parser] DEBUG: " + pluginName + ": params = "); - for (int i = 0; i < paramTypes.size(); i++) { - LOGGER.log(Level.FINER, "Parameter " + i + ":\t" + paramTypes.get(i).getSimpleName()); - } - LOGGER.log(Level.FINER, "[xrouter-plugin-config-parser] DEBUG: " + pluginName + ": clientRequestLimit = " + clientRequestLimit); - } - - private static Properties getPluginProperties(String rawConfig) throws IOException { - String toRead = rawConfig.replace("\\n", "\n"); - - Properties properties = new Properties(); - properties.load(new StringReader(toRead)); - return properties; - } - - public String getPluginName() { - return pluginName; - } - - public ArrayList getParamTypes() { - return paramTypes; - } - - public double getFee() { - return fee; - } - - public int getClientRequestLimit() { - return clientRequestLimit; - } - } - - public XRouterConfiguration(String rawXRouterConfig) { - this.rawXRouterConfig = rawXRouterConfig; - } - - public void parseConfig() { - HashMap properties = getProperties(rawXRouterConfig); - - if (properties == null) - return; - - LOGGER.log(Level.FINER, "[xrouter-config-parser] DEBUG: Properties: " + properties.toString()); - - supportedWallets.addAll(Arrays.asList(((String) properties.get("Main").get("wallets")).split(","))); - timeout = Integer.parseInt((String) properties.get("Main").get("timeout")); - blockLimit = Integer.parseInt((String) properties.get("Main").get("blocklimit")); - feeAddress = (String) properties.get("Main").get("paymentaddress"); - - for (String key : properties.keySet()) { - if (key.startsWith("xr")) { - Properties xRouterPropertySet = properties.get(key); - double fee = Double.parseDouble((String) xRouterPropertySet.get("fee")); - feeMap.put(key, fee); - } - } - - LOGGER.log(Level.FINER, "[xrouter-config-parser] Processing complete."); - } - - public ArrayList getSupportedWallets() { - return supportedWallets; - } - - public HashMap getFeeMap() { - return feeMap; - } - - public String getFeeAddress() { - return feeAddress; - } - - public int getBlockLimit() { - return blockLimit; - } - - public int getTimeout() { - return timeout; - } - - private static HashMap parseINI(String toRead) throws IOException { - HashMap result = new HashMap<>(); - new Properties() { - - private Properties section; - - @Override - public Object put(Object key, Object value) { - String header = (key + " " + value).trim(); - if (header.startsWith("[") && header.endsWith("]")) - return result.put(header.substring(1, header.length() - 1), - section = new Properties()); - else - return section.put(key, value); - } - - }.load(new StringReader(toRead)); - return result; - } - - private static HashMap getProperties(String rawConfig) { - String formatted = rawConfig.replace("\\n", "\n"); - - HashMap properties; - - try { - properties = parseINI(formatted); - } catch (IOException e) { - LOGGER.log(Level.FINER, "[xrouter-config-parser] ERROR: Error while parsing XRouter config!"); - e.printStackTrace(); - return null; - } - - return properties; - } + private final static LogManager LOGMANAGER = LogManager.getLogManager(); + private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); + + private final String rawXRouterConfig; + private final HashMap feeMap = new HashMap<>(); + private final ArrayList supportedWallets = new ArrayList<>(); + private String feeAddress; + private int timeout; + private int blockLimit; + + private static HashBiMap pluginParamTypes; + + static { + pluginParamTypes = HashBiMap.create(2); + + pluginParamTypes.put("string", String.class); + pluginParamTypes.put("int", Integer.class); + pluginParamTypes.put("bool", Boolean.class); + } + + public static String getStringByClass(Class clazz) { + return pluginParamTypes.inverse().get(clazz); + } + + public static class XRouterPluginConfiguration { + private final String rawPluginConfig; + private final String pluginName; + + private ArrayList paramTypes = new ArrayList<>(); + private double fee; + private int clientRequestLimit; + + public XRouterPluginConfiguration(String pluginName, String rawPluginConfig) { + this.pluginName = pluginName; + this.rawPluginConfig = rawPluginConfig; + } + + public void parsePluginConfig() { + Properties properties; + try { + properties = getPluginProperties(rawPluginConfig); + } catch (IOException e) { + LOGGER.log(Level.FINER, "[xrouter-plugin-config-parser] ERROR: Error while parsing plugin config!"); + e.printStackTrace(); + return; + } + + if (!properties.containsKey("parameters") && !properties.containsKey("paramsType")) { + LOGGER.log(Level.FINER, "[xrouter-plugin-config-parser] ERROR: Plugin has no parameters!"); + } else { + String[] rawParamTypes; + + if (properties.containsKey("parameters")) + rawParamTypes = properties.getProperty("parameters").split(",");else if (properties.containsKey("paramsType")) + rawParamTypes = properties.getProperty("paramsType").split(","); + else + return; + + for (String rawParamType : rawParamTypes) { + if (rawParamType.isEmpty()) + continue; + + if (!pluginParamTypes.containsKey(rawParamType)) { + LOGGER.log(Level.FINER, "[xrouter-plugin-config-parser] ERROR: Invalid/unsupported plugin parameter type: " + rawParamType + ". Failing."); + throw new IllegalArgumentException("Invalid/unsupported plugin parameter type: " + rawParamType); + } + + paramTypes.add(pluginParamTypes.get(rawParamType)); + } + } + + if (properties.contains("fee")) { + fee = Double.parseDouble(properties.getProperty("fee")); + } else { + fee = 0; + } + + if (properties.contains("clientrequestlimit")) { + clientRequestLimit = Integer.parseInt(properties.getProperty("clientrequestlimit")); + } else { + clientRequestLimit = 100; + } + + LOGGER.log(Level.FINER, "[xrouter-plugin-config-parser] Processing '" + pluginName + "' complete."); + LOGGER.log(Level.FINER, "[xrouter-plugin-config-parser] DEBUG: " + pluginName + ": fee = " + fee); + LOGGER.log(Level.FINER, "[xrouter-plugin-config-parser] DEBUG: " + pluginName + ": params = "); + for (int i = 0; i < paramTypes.size(); i++) { + LOGGER.log(Level.FINER, "Parameter " + i + ":\t" + paramTypes.get(i).getSimpleName()); + } + LOGGER.log(Level.FINER, "[xrouter-plugin-config-parser] DEBUG: " + pluginName + ": clientRequestLimit = " + clientRequestLimit); + } + + private static Properties getPluginProperties(String rawConfig) throws IOException { + String toRead = rawConfig.replace("\\n", "\n"); + + Properties properties = new Properties(); + properties.load(new StringReader(toRead)); + return properties; + } + + public String getPluginName() { + return pluginName; + } + + public ArrayList getParamTypes() { + return paramTypes; + } + + public double getFee() { + return fee; + } + + public int getClientRequestLimit() { + return clientRequestLimit; + } + } + + public XRouterConfiguration(String rawXRouterConfig) { + this.rawXRouterConfig = rawXRouterConfig; + } + + public void parseConfig() { + HashMap properties = getProperties(rawXRouterConfig); + + if (properties == null) + return; + + LOGGER.log(Level.FINER, "[xrouter-config-parser] DEBUG: Properties: " + properties.toString()); + + supportedWallets.addAll(Arrays.asList(((String) properties.get("Main").get("wallets")).split(","))); + timeout = Integer.parseInt((String) properties.get("Main").get("timeout")); + blockLimit = Integer.parseInt((String) properties.get("Main").get("blocklimit")); + feeAddress = (String) properties.get("Main").get("paymentaddress"); + + for (String key : properties.keySet()) { + if (key.startsWith("xr")) { + Properties xRouterPropertySet = properties.get(key); + double fee = Double.parseDouble((String) xRouterPropertySet.get("fee")); + feeMap.put(key, fee); + } + } + + LOGGER.log(Level.FINER, "[xrouter-config-parser] Processing complete."); + } + + public ArrayList getSupportedWallets() { + return supportedWallets; + } + + public HashMap getFeeMap() { + return feeMap; + } + + public String getFeeAddress() { + return feeAddress; + } + + public int getBlockLimit() { + return blockLimit; + } + + public int getTimeout() { + return timeout; + } + + private static HashMap parseINI(String toRead) throws IOException { + HashMap result = new HashMap<>(); + new Properties() { + + private Properties section; + + @Override + public Object put(Object key, Object value) { + String header = (key + " " + value).trim(); + if (header.startsWith("[") && header.endsWith("]")) + return result.put(header.substring(1, header.length() - 1), + section = new Properties()); + else + return section.put(key, value); + } + + }.load(new StringReader(toRead)); + return result; + } + + private static HashMap getProperties(String rawConfig) { + String formatted = rawConfig.replace("\\n", "\n"); + + HashMap properties; + + try { + properties = parseINI(formatted); + } catch (IOException e) { + LOGGER.log(Level.FINER, "[xrouter-config-parser] ERROR: Error while parsing XRouter config!"); + e.printStackTrace(); + return null; + } + + return properties; + } } diff --git a/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java b/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java index 767a727..814acfe 100644 --- a/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java +++ b/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java @@ -8,7 +8,6 @@ import io.cloudchains.app.net.protocols.blocknet.BlocknetPeerGroup; import io.cloudchains.app.util.XRouterConfiguration; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.logging.Level; @@ -16,154 +15,153 @@ import java.util.logging.Logger; public class BackgroundTimerThread implements Runnable { - private final static LogManager LOGMANAGER = LogManager.getLogManager(); - private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); + private final static LogManager LOGMANAGER = LogManager.getLogManager(); + private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); - public static final boolean HTTP_BLOCK_COUNT_UPDATES = true; - public static final boolean HTTP_BALANCE_UPDATES = true; + public static final boolean HTTP_BLOCK_COUNT_UPDATES = true; + public static final boolean HTTP_BALANCE_UPDATES = true; - private static final int KEEPALIVE_INTERVAL = 10000; - private static final int BALANCE_INTERVAL = 10000; + private static final int KEEPALIVE_INTERVAL = 10000; + private static final int BALANCE_INTERVAL = 10000; - private ExecutorService threadPool = Executors.newSingleThreadExecutor(); + private ExecutorService threadPool = Executors.newSingleThreadExecutor(); - private BlocknetPeerGroup blocknetPeerGroup; - private HTTPClient feeUpdateHttpClient; - private HTTPClient heightUpdateHttpClient; + private BlocknetPeerGroup blocknetPeerGroup; + private HTTPClient feeUpdateHttpClient; + private HTTPClient heightUpdateHttpClient; - private long lastKeepAliveTime; - private long lastBalanceUpdateTime; + private long lastKeepAliveTime; + private long lastBalanceUpdateTime; - private long lastOut; - private boolean shutdownRequested = false; + private long lastOut; + private boolean shutdownRequested = false; - public BackgroundTimerThread() { - blocknetPeerGroup = CoinInstance.getInstance(CoinInstance.getActiveBlocknetNetwork()).getBlocknetPeerGroup(); - feeUpdateHttpClient = App.feeUpdateHttpClient; - heightUpdateHttpClient = App.heightUpdateHttpClient; + public BackgroundTimerThread() { + blocknetPeerGroup = CoinInstance.getInstance(CoinInstance.getActiveBlocknetNetwork()).getBlocknetPeerGroup(); + feeUpdateHttpClient = App.feeUpdateHttpClient; + heightUpdateHttpClient = App.heightUpdateHttpClient; - lastKeepAliveTime = 0; - lastBalanceUpdateTime = 0; + lastKeepAliveTime = 0; + lastBalanceUpdateTime = 0; - lastOut = 0; - } + lastOut = 0; + } - public void stop() { + public void stop() { shutdownRequested = true; } - private void outputAvailableCurrencies() { - long elapsed = (System.currentTimeMillis() - lastOut); + private void outputAvailableCurrencies() { + long elapsed = (System.currentTimeMillis() - lastOut); + + if (elapsed < 60 * 1000 && lastOut != 0) + return; - if (elapsed < 60 * 1000 && lastOut != 0) - return; + for (CoinInstance coinInstance : CoinInstance.getCoinInstances()) { + if (!CoinTickerUtils.isActiveTicker(coinInstance.getTicker())) + continue; - for (CoinInstance coinInstance : CoinInstance.getCoinInstances()) { - if (!CoinTickerUtils.isActiveTicker(coinInstance.getTicker())) - continue; + if (CoinInstance.getBlockCountByTicker(coinInstance.getTicker()) > 0) { + LOGGER.log(Level.INFO, "[coin] Available Currency: " + CoinTickerUtils.tickerToString(coinInstance.getTicker())); + } + } - if (CoinInstance.getBlockCountByTicker(coinInstance.getTicker()) > 0) { - LOGGER.log(Level.INFO, "[coin] Available Currency: " + CoinTickerUtils.tickerToString(coinInstance.getTicker())); - } - } + lastOut = System.currentTimeMillis(); + } - lastOut = System.currentTimeMillis(); - } + private void sendKeepAlive() { + long elapsed = (System.currentTimeMillis() - lastKeepAliveTime); + + if (elapsed < KEEPALIVE_INTERVAL && lastKeepAliveTime != 0) + return; + + if (HTTP_BLOCK_COUNT_UPDATES) { + heightUpdateHttpClient.getAllBlockCounts(); + feeUpdateHttpClient.getAllFees(); + } else if (!blocknetPeerGroup.getConnectedPeers().isEmpty()) { + for (BlocknetPeer blocknetPeer : blocknetPeerGroup.getConnectedPeers()) { + XRouterConfiguration xRouterConfiguration = blocknetPeer.getxRouterConfiguration(); + if (xRouterConfiguration == null) + continue; + + for (CoinInstance coinInstance : CoinInstance.getCoinInstances()) { + if (!CoinTickerUtils.isActiveTicker(coinInstance.getTicker())) + continue;else if (!blocknetPeer.getxRouterConfiguration().getSupportedWallets().contains(coinInstance.getNetworkParameters().getId())) + continue; + + coinInstance.sendXrGetBlockCount(blocknetPeer); + LOGGER.log(Level.FINER, "[BackgroundTimer] Sent keepalive message: " + coinInstance.getNetworkParameters().getId()); + } + } + } else { + return; + } + + lastKeepAliveTime = System.currentTimeMillis(); + } - private void sendKeepAlive() { - long elapsed = (System.currentTimeMillis() - lastKeepAliveTime); + private void sendBalanceUpdate() { + long elapsed = (System.currentTimeMillis() - lastBalanceUpdateTime); - if (elapsed < KEEPALIVE_INTERVAL && lastKeepAliveTime != 0) - return; + if (elapsed < BALANCE_INTERVAL && lastBalanceUpdateTime != 0) + return; - if (HTTP_BLOCK_COUNT_UPDATES) { - heightUpdateHttpClient.getAllBlockCounts(); - feeUpdateHttpClient.getAllFees(); - } else if (!blocknetPeerGroup.getConnectedPeers().isEmpty()) { - for (BlocknetPeer blocknetPeer : blocknetPeerGroup.getConnectedPeers()) { - XRouterConfiguration xRouterConfiguration = blocknetPeer.getxRouterConfiguration(); - if (xRouterConfiguration == null) - continue; + // No longer polling balances and transaction history here. Instead it is requested + // on demand when client requests the data. See HTTPServerHandler.java:302-330 - for (CoinInstance coinInstance : CoinInstance.getCoinInstances()) { - if (!CoinTickerUtils.isActiveTicker(coinInstance.getTicker())) - continue; - else if (!blocknetPeer.getxRouterConfiguration().getSupportedWallets().contains(coinInstance.getNetworkParameters().getId())) - continue; + for (CoinInstance coinInstance : CoinInstance.getCoinInstances()) { + if (!CoinTickerUtils.isActiveTicker(coinInstance.getTicker())) + continue; - coinInstance.sendXrGetBlockCount(blocknetPeer); - LOGGER.log(Level.FINER, "[BackgroundTimer] Sent keepalive message: " + coinInstance.getNetworkParameters().getId()); - } - } - } else { - return; - } + if (CoinInstance.getBlockCountByTicker(coinInstance.getTicker()) <= 0) { + continue; + } - lastKeepAliveTime = System.currentTimeMillis(); - } + if (blocknetPeerGroup.getConnectedPeers().isEmpty()) { + return; + } - private void sendBalanceUpdate() { - long elapsed = (System.currentTimeMillis() - lastBalanceUpdateTime); + BlocknetPeer blocknetPeer = blocknetPeerGroup.getBestBlocknetPeer(coinInstance.getNetworkParameters().getId()); + if (blocknetPeer == null) { + LOGGER.log(Level.FINER, "[BackgroundTimer] Peer was not found for currency " + coinInstance.getNetworkParameters().getId()); + continue; + } - if (elapsed < BALANCE_INTERVAL && lastBalanceUpdateTime != 0) - return; + coinInstance.sendXrGetUtxos(blocknetPeer); + LOGGER.log(Level.FINER, "[BackgroundTimer] Sent GetUtxos message: " + coinInstance.getNetworkParameters().getId()); + } - // No longer polling balances and transaction history here. Instead it is requested - // on demand when client requests the data. See HTTPServerHandler.java:302-330 + lastBalanceUpdateTime = System.currentTimeMillis(); + } - for (CoinInstance coinInstance : CoinInstance.getCoinInstances()) { - if (!CoinTickerUtils.isActiveTicker(coinInstance.getTicker())) - continue; - - if (CoinInstance.getBlockCountByTicker(coinInstance.getTicker()) <= 0) { - continue; - } - - if (blocknetPeerGroup.getConnectedPeers().isEmpty()) { - return; - } - - BlocknetPeer blocknetPeer = blocknetPeerGroup.getBestBlocknetPeer(coinInstance.getNetworkParameters().getId()); - if (blocknetPeer == null) { - LOGGER.log(Level.FINER, "[BackgroundTimer] Peer was not found for currency " + coinInstance.getNetworkParameters().getId()); - continue; - } - - coinInstance.sendXrGetUtxos(blocknetPeer); - LOGGER.log(Level.FINER, "[BackgroundTimer] Sent GetUtxos message: " + coinInstance.getNetworkParameters().getId()); - } - - lastBalanceUpdateTime = System.currentTimeMillis(); - } - - @Override - public void run() { - LOGGER.log(Level.FINER, "[BackgroundTimer] Waiting until initial messages are sent off."); - - for (CoinInstance coinInstance : CoinInstance.getCoinInstances()) { - if (!CoinTickerUtils.isActiveTicker(coinInstance.getTicker())) - continue; - - new Thread(() -> { - App.feeUpdateHttpClient.getHistory(coinInstance.getTicker(), 0, (int) System.currentTimeMillis(), 30000); - }).start(); - } - - while (!Thread.currentThread().isInterrupted()) { - if (shutdownRequested) - break; - try { - sendKeepAlive(); - outputAvailableCurrencies(); - - Thread.sleep(100); - } catch (NullPointerException e) { - e.printStackTrace(); - } catch (Exception e) { - LOGGER.log(Level.FINER, "[BackgroundTimer] Interrupted thread"); - e.printStackTrace(); - Thread.currentThread().interrupt(); - } - } - } + @Override + public void run() { + LOGGER.log(Level.FINER, "[BackgroundTimer] Waiting until initial messages are sent off."); + + for (CoinInstance coinInstance : CoinInstance.getCoinInstances()) { + if (!CoinTickerUtils.isActiveTicker(coinInstance.getTicker())) + continue; + + new Thread(() -> { + App.feeUpdateHttpClient.getHistory(coinInstance.getTicker(), 0, (int) System.currentTimeMillis(), 30000); + }).start(); + } + + while (!Thread.currentThread().isInterrupted()) { + if (shutdownRequested) + break; + try { + sendKeepAlive(); + outputAvailableCurrencies(); + + Thread.sleep(100); + } catch (NullPointerException e) { + e.printStackTrace(); + } catch (Exception e) { + LOGGER.log(Level.FINER, "[BackgroundTimer] Interrupted thread"); + e.printStackTrace(); + Thread.currentThread().interrupt(); + } + } + } } diff --git a/src/main/java/io/cloudchains/app/util/history/Transaction.java b/src/main/java/io/cloudchains/app/util/history/Transaction.java index 7031455..8e23177 100644 --- a/src/main/java/io/cloudchains/app/util/history/Transaction.java +++ b/src/main/java/io/cloudchains/app/util/history/Transaction.java @@ -6,10 +6,14 @@ import java.util.List; public class Transaction { - @SerializedName("address") private String addressB58; - @SerializedName("txhash") private String txid; - @SerializedName("blockhash") private String blockhash; - @SerializedName("category") private String category; + @SerializedName("address") + private String addressB58; + @SerializedName("txhash") + private String txid; + @SerializedName("blockhash") + private String blockhash; + @SerializedName("category") + private String category; private double fee; protected transient double value; private int vout; @@ -39,6 +43,7 @@ public void setCategory(String category) { public void setFee(double fee) { this.fee = fee; } + public double getFee() { return fee; } diff --git a/src/main/java/io/cloudchains/app/wallet/WalletHelper.java b/src/main/java/io/cloudchains/app/wallet/WalletHelper.java index b61f662..8fa4006 100644 --- a/src/main/java/io/cloudchains/app/wallet/WalletHelper.java +++ b/src/main/java/io/cloudchains/app/wallet/WalletHelper.java @@ -8,278 +8,278 @@ import io.cloudchains.app.util.CloudTransaction; import io.cloudchains.app.util.UTXO; import org.bitcoinj.core.*; -import org.bitcoinj.core.Base58; import org.bitcoinj.crypto.DeterministicKey; import org.bitcoinj.script.Script; import org.bitcoinj.script.ScriptBuilder; import org.bitcoinj.wallet.Wallet; + import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Comparator; public class WalletHelper { - private CoinInstance coin; - private NetworkParameters networkParameters; + private CoinInstance coin; + private NetworkParameters networkParameters; - public WalletHelper(CoinInstance coinInstance) { - this.coin = coinInstance; - this.networkParameters = coin.getNetworkParameters(); + public WalletHelper(CoinInstance coinInstance) { + this.coin = coinInstance; + this.networkParameters = coin.getNetworkParameters(); - } + } - public Transaction createRawTransactionWithAllUTXOs(Transaction tx, double amount) { - try { - ArrayList utxos = coinSelector(amount); + public Transaction createRawTransactionWithAllUTXOs(Transaction tx, double amount) { + try { + ArrayList utxos = coinSelector(amount); - Preconditions.checkNotNull(utxos); - for (UTXO utxo : utxos) { - if (utxo.isSpent()) - continue; + Preconditions.checkNotNull(utxos); + for (UTXO utxo : utxos) { + if (utxo.isSpent()) + continue; - org.bitcoinj.core.UTXO bUtxo = utxo.createUTXO(); - AddressBalance addressBalance = coin.getAddressBalance(utxo.getAddress()); - Preconditions.checkNotNull(addressBalance); + org.bitcoinj.core.UTXO bUtxo = utxo.createUTXO(); + AddressBalance addressBalance = coin.getAddressBalance(utxo.getAddress()); + Preconditions.checkNotNull(addressBalance); - TransactionOutPoint outPoint = new TransactionOutPoint(networkParameters, bUtxo.getIndex(), bUtxo.getHash()); + TransactionOutPoint outPoint = new TransactionOutPoint(networkParameters, bUtxo.getIndex(), bUtxo.getHash()); - tx.addSignedInput(outPoint, bUtxo.getScript(), addressBalance.getPrivateKey().getKey(), Transaction.SigHash.ALL, true); + tx.addSignedInput(outPoint, bUtxo.getScript(), addressBalance.getPrivateKey().getKey(), Transaction.SigHash.ALL, true); - utxo.setSpent(true); - addressBalance.calculateBalance(); - } - return tx; - } catch (Exception e) { - e.printStackTrace(); - return null; - } - } + utxo.setSpent(true); + addressBalance.calculateBalance(); + } + return tx; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } - public Transaction createRawTransactionWithAllUTXOs(ArrayList outputs, double amount) { - Transaction tx = new Transaction(networkParameters); + public Transaction createRawTransactionWithAllUTXOs(ArrayList outputs, double amount) { + Transaction tx = new Transaction(networkParameters); - for (TransactionOutput output : outputs) { - tx.addOutput(output); - } + for (TransactionOutput output : outputs) { + tx.addOutput(output); + } - return createRawTransactionWithAllUTXOs(tx, amount); - } + return createRawTransactionWithAllUTXOs(tx, amount); + } - private ArrayList sortLeastToGreatest() { - ArrayList utxos = new ArrayList<>(); + private ArrayList sortLeastToGreatest() { + ArrayList utxos = new ArrayList<>(); - for (AddressBalance addressBalance : coin.getAddressKeyPairs()) { - utxos.addAll(addressBalance.getUtxos()); - } + for (AddressBalance addressBalance : coin.getAddressKeyPairs()) { + utxos.addAll(addressBalance.getUtxos()); + } - utxos.sort(Comparator.comparingLong(UTXO::getValue)); + utxos.sort(Comparator.comparingLong(UTXO::getValue)); - return utxos; - } + return utxos; + } - private ArrayList advancedCoinSorting() { - ArrayList utxos = new ArrayList<>(); + private ArrayList advancedCoinSorting() { + ArrayList utxos = new ArrayList<>(); - for (AddressBalance addressBalance : coin.getAddressKeyPairs()) { - utxos.addAll(addressBalance.getUtxos()); - } + for (AddressBalance addressBalance : coin.getAddressKeyPairs()) { + utxos.addAll(addressBalance.getUtxos()); + } - utxos.sort(Comparator.comparingLong(UTXO::getValue)); + utxos.sort(Comparator.comparingLong(UTXO::getValue)); - int sizeOfUtxos = utxos.size(); + int sizeOfUtxos = utxos.size(); - if (sizeOfUtxos <= 1) - return utxos; + if (sizeOfUtxos <= 1) + return utxos; - ArrayList utxosLowerHalf = new ArrayList<>(utxos.subList(0, ((sizeOfUtxos + 1) / 2))); - ArrayList utxosGreaterHalf = new ArrayList<>(utxos.subList(((sizeOfUtxos + 1) / 2), sizeOfUtxos)); + ArrayList utxosLowerHalf = new ArrayList<>(utxos.subList(0, ((sizeOfUtxos + 1) / 2))); + ArrayList utxosGreaterHalf = new ArrayList<>(utxos.subList(((sizeOfUtxos + 1) / 2), sizeOfUtxos)); - ArrayList res = new ArrayList<>(); + ArrayList res = new ArrayList<>(); - while (utxosLowerHalf.size() > 0 || utxosGreaterHalf.size() > 0) { - if (utxosLowerHalf.size() > 0) { - res.add(utxosLowerHalf.get(0)); - utxosLowerHalf.remove(0); - } + while (utxosLowerHalf.size() > 0 || utxosGreaterHalf.size() > 0) { + if (utxosLowerHalf.size() > 0) { + res.add(utxosLowerHalf.get(0)); + utxosLowerHalf.remove(0); + } - if (utxosGreaterHalf.size() > 0) { - res.add(utxosGreaterHalf.get(0)); - utxosGreaterHalf.remove(0); - } - } + if (utxosGreaterHalf.size() > 0) { + res.add(utxosGreaterHalf.get(0)); + utxosGreaterHalf.remove(0); + } + } - return res; - } + return res; + } - private ArrayList coinSelector(double amount) { - ArrayList utxos = new ArrayList<>(); - double totalBalance = 0.0; + private ArrayList coinSelector(double amount) { + ArrayList utxos = new ArrayList<>(); + double totalBalance = 0.0; - for (UTXO utxo : advancedCoinSorting()) { - if (totalBalance < amount) { - totalBalance += utxo.getAmount(); - utxos.add(utxo); - } else { - break; - } - } + for (UTXO utxo : advancedCoinSorting()) { + if (totalBalance < amount) { + totalBalance += utxo.getAmount(); + utxos.add(utxo); + } else { + break; + } + } - if (utxos.size() > 0) - return utxos; - else - return null; - } + if (utxos.size() > 0) + return utxos; + else + return null; + } - public String formatAmount(double amount) { - DecimalFormat df = new DecimalFormat("#.########"); - return df.format(amount); - } + public String formatAmount(double amount) { + DecimalFormat df = new DecimalFormat("#.########"); + return df.format(amount); + } - public double getTotalBalance() { - double totalBalance = 0.0; + public double getTotalBalance() { + double totalBalance = 0.0; - for (AddressBalance addressBalance : coin.getAddressKeyPairs()) - totalBalance += addressBalance.getBalanceProp(); + for (AddressBalance addressBalance : coin.getAddressKeyPairs()) + totalBalance += addressBalance.getBalanceProp(); - return totalBalance; - } + return totalBalance; + } - public double getSpendBalance(double amount) { - double totalBalance = 0.0; - ArrayList utxos = coinSelector(amount); + public double getSpendBalance(double amount) { + double totalBalance = 0.0; + ArrayList utxos = coinSelector(amount); - Preconditions.checkNotNull(utxos); - for (UTXO utxo : utxos) - totalBalance += utxo.getAmount(); + Preconditions.checkNotNull(utxos); + for (UTXO utxo : utxos) + totalBalance += utxo.getAmount(); - return totalBalance; - } + return totalBalance; + } - public Address getChangeAddress() { - if (coin.getAddressKeyPairs().isEmpty()) { - return null; - } + public Address getChangeAddress() { + if (coin.getAddressKeyPairs().isEmpty()) { + return null; + } - return coin.getAddressKeyPairs().get(0).getAddress(); - } + return coin.getAddressKeyPairs().get(0).getAddress(); + } - public AddressBalance generateAddress() { - Wallet wallet = coin.getWallet(); - NetworkParameters params = coin.getNetworkParameters(); + public AddressBalance generateAddress() { + Wallet wallet = coin.getWallet(); + NetworkParameters params = coin.getNetworkParameters(); - DeterministicKey key = wallet.freshReceiveKey(); - DumpedPrivateKey privateKey = key.getPrivateKeyEncoded(params); + DeterministicKey key = wallet.freshReceiveKey(); + DumpedPrivateKey privateKey = key.getPrivateKeyEncoded(params); - Address address = new Address(params, key.getPubKeyHash()) { - public byte[] getHash() { - return new byte[0]; - } + Address address = new Address(params, key.getPubKeyHash()) { + public byte[] getHash() { + return new byte[0]; + } - public Script.ScriptType getOutputScriptType() { - return null; - } + public Script.ScriptType getOutputScriptType() { + return null; + } - public int compareTo(Address o) { - return 0; - } - }; + public int compareTo(Address o) { + return 0; + } + }; - return new AddressBalance(address, privateKey); - } - - public AddressBalance generateFromPrivateKey(String privKey) { - NetworkParameters params = coin.getNetworkParameters(); - - ECKey key = DumpedPrivateKey.fromBase58(params, privKey).getKey(); - DumpedPrivateKey privateKey = key.getPrivateKeyEncoded(params); - - Address address = new Address(params, key.getPubKeyHash()) { - public byte[] getHash() { - return new byte[0]; - } - - public Script.ScriptType getOutputScriptType() { - return null; - } - - - public int compareTo(Address o) { - return 0; - } - }; - - return new AddressBalance(address, privateKey); - } - - public void addTransactionToWallet(Transaction transaction) { - coin.addCloudTransaction(new CloudTransaction(transaction)); - } - - public double getBlocknetFeeAmount(BlocknetPeer blocknetPeer) { - return blocknetPeer.getxRouterConfiguration().getFeeMap().get("xrSendTransaction"); - } - - public static Transaction createTransactionSimple(CoinTicker coinTicker, String address, double amount) { - CoinInstance coinInstance = CoinInstance.getInstance(coinTicker); - WalletHelper walletHelper = coinInstance.getWalletHelper(); - NetworkParameters params = coinInstance.getNetworkParameters(); + return new AddressBalance(address, privateKey); + } + + public AddressBalance generateFromPrivateKey(String privKey) { + NetworkParameters params = coin.getNetworkParameters(); + + ECKey key = DumpedPrivateKey.fromBase58(params, privKey).getKey(); + DumpedPrivateKey privateKey = key.getPrivateKeyEncoded(params); + + Address address = new Address(params, key.getPubKeyHash()) { + public byte[] getHash() { + return new byte[0]; + } + + public Script.ScriptType getOutputScriptType() { + return null; + } + + + public int compareTo(Address o) { + return 0; + } + }; + + return new AddressBalance(address, privateKey); + } + + public void addTransactionToWallet(Transaction transaction) { + coin.addCloudTransaction(new CloudTransaction(transaction)); + } + + public double getBlocknetFeeAmount(BlocknetPeer blocknetPeer) { + return blocknetPeer.getxRouterConfiguration().getFeeMap().get("xrSendTransaction"); + } + + public static Transaction createTransactionSimple(CoinTicker coinTicker, String address, double amount) { + CoinInstance coinInstance = CoinInstance.getInstance(coinTicker); + WalletHelper walletHelper = coinInstance.getWalletHelper(); + NetworkParameters params = coinInstance.getNetworkParameters(); - double fee = coinInstance.getConfigHelper().getFee(); - double totalSpending = amount + fee; - double totalAvailable = walletHelper.getSpendBalance(totalSpending); - double changeAmt = (totalAvailable - amount) - fee; - Address toAddress = Address.fromBase58(params, address); - Coin sendAmount = Coin.valueOf((long) Math.floor(amount * Coin.COIN.value)); - Coin changeAmount = Coin.valueOf((long) Math.floor(changeAmt * Coin.COIN.value)); + double fee = coinInstance.getConfigHelper().getFee(); + double totalSpending = amount + fee; + double totalAvailable = walletHelper.getSpendBalance(totalSpending); + double changeAmt = (totalAvailable - amount) - fee; + Address toAddress = Address.fromBase58(params, address); + Coin sendAmount = Coin.valueOf((long) Math.floor(amount * Coin.COIN.value)); + Coin changeAmount = Coin.valueOf((long) Math.floor(changeAmt * Coin.COIN.value)); - Transaction tx = new Transaction(params); + Transaction tx = new Transaction(params); - if (isP2SHAddress(coinInstance, address)) { - Script p2shScript = ScriptBuilder.createP2SHOutputScript(toAddress.getHash160()); - tx.addOutput(sendAmount, p2shScript); - } else { - tx.addOutput(sendAmount, toAddress); - } + if (isP2SHAddress(coinInstance, address)) { + Script p2shScript = ScriptBuilder.createP2SHOutputScript(toAddress.getHash160()); + tx.addOutput(sendAmount, p2shScript); + } else { + tx.addOutput(sendAmount, toAddress); + } - if (changeAmount.isPositive()) - tx.addOutput(changeAmount, walletHelper.getChangeAddress()); + if (changeAmount.isPositive()) + tx.addOutput(changeAmount, walletHelper.getChangeAddress()); - return walletHelper.createRawTransactionWithAllUTXOs(tx, totalAvailable); - } + return walletHelper.createRawTransactionWithAllUTXOs(tx, totalAvailable); + } - public static void setAsSpent(CoinTicker coinTicker, Transaction transaction, boolean setSpent) { - CoinInstance coinInstance = CoinInstance.getInstance(coinTicker); + public static void setAsSpent(CoinTicker coinTicker, Transaction transaction, boolean setSpent) { + CoinInstance coinInstance = CoinInstance.getInstance(coinTicker); - for (TransactionInput input : transaction.getInputs()) { - Sha256Hash txid = input.getOutpoint().getHash(); - long vout = input.getOutpoint().getIndex(); + for (TransactionInput input : transaction.getInputs()) { + Sha256Hash txid = input.getOutpoint().getHash(); + long vout = input.getOutpoint().getIndex(); - if (txid == null) - continue; + if (txid == null) + continue; - for (AddressBalance addressBalance : coinInstance.getAddressKeyPairs()) { - for (UTXO utxo : addressBalance.getUtxos()) { + for (AddressBalance addressBalance : coinInstance.getAddressKeyPairs()) { + for (UTXO utxo : addressBalance.getUtxos()) { - if (Sha256Hash.wrap(utxo.getTxid()).equals(txid) && utxo.getVout() == vout) { - utxo.setSpent(setSpent); - } - } - } - } - } + if (Sha256Hash.wrap(utxo.getTxid()).equals(txid) && utxo.getVout() == vout) { + utxo.setSpent(setSpent); + } + } + } + } + } - private static boolean isP2SHAddress(CoinInstance coin, String address) { - byte[] versionAndDataBytes = Base58.decodeChecked(address); - int version = versionAndDataBytes[0] & 0xFF; + private static boolean isP2SHAddress(CoinInstance coin, String address) { + byte[] versionAndDataBytes = Base58.decodeChecked(address); + int version = versionAndDataBytes[0] & 0xFF; - if (coin.getNetworkParameters().getAcceptableAddressCodes().length > 2) { - for (int t : coin.getNetworkParameters().getAcceptableAddressCodes()) { - if (coin.getNetworkParameters().getAddressHeader() != t && t == version) { - return true; - } - } - } - - return coin.getNetworkParameters().getP2SHHeader() == version; - } + if (coin.getNetworkParameters().getAcceptableAddressCodes().length > 2) { + for (int t : coin.getNetworkParameters().getAcceptableAddressCodes()) { + if (coin.getNetworkParameters().getAddressHeader() != t && t == version) { + return true; + } + } + } + + return coin.getNetworkParameters().getP2SHHeader() == version; + } } diff --git a/src/main/resources/config/netty-reflection.json b/src/main/resources/config/netty-reflection.json new file mode 100644 index 0000000..642b744 --- /dev/null +++ b/src/main/resources/config/netty-reflection.json @@ -0,0 +1,8 @@ +[ + { + "name": "io.netty.channel.socket.nio.NioServerSocketChannel", + "methods": [ + { "name": "", "parameterTypes": [] } + ] + } +] \ No newline at end of file diff --git a/src/main/resources/config/resource-config.json b/src/main/resources/config/resource-config.json new file mode 100644 index 0000000..6086ab5 --- /dev/null +++ b/src/main/resources/config/resource-config.json @@ -0,0 +1,7 @@ +{ + "resources":[ + {"pattern":"mozilla/public-suffix-list.txt"}, + {"pattern":"org/apache/http/client/version.properties"}, + {"pattern":"org/bitcoinj/crypto/mnemonic/wordlist/english.txt"} + ] +} diff --git a/src/main/resources/simplelogger.properties b/src/main/resources/simplelogger.properties new file mode 100644 index 0000000..f92b74d --- /dev/null +++ b/src/main/resources/simplelogger.properties @@ -0,0 +1,6 @@ +z# SLF4J SimpleLogger configuration +# Set bitcoinj logging level to WARN only +org.slf4j.simpleLogger.log.org.bitcoinj=warn + +# Set default logging level to WARN to suppress all other INFO messages +org.slf4j.simpleLogger.defaultLogLevel=warn \ No newline at end of file diff --git a/src/test/java/TestWallet.java b/src/test/java/TestWallet.java index 885e52a..88acc80 100644 --- a/src/test/java/TestWallet.java +++ b/src/test/java/TestWallet.java @@ -3,11 +3,15 @@ import io.cloudchains.app.net.CoinTicker; import io.cloudchains.app.util.AddressBalance; import io.cloudchains.app.util.ConfigHelper; -import org.junit.jupiter.api.*; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import java.io.File; -import java.nio.charset.StandardCharsets; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Objects; import static org.junit.jupiter.api.Assertions.*; @@ -114,7 +118,7 @@ void deterministicAddresses_generateForwardAddressesReloadConfig() { coin.getConfigHelper().setAddressCount(ADDRESS_COUNT_INITIAL); assertNull(coin.init(LoginUtils.loginToEntropy(PASSWD), mnemonic, false)); - for (int idx = ADDRESS_COUNT_INITIAL*2; idx < ADDRESS_COUNT; idx += ADDRESS_COUNT_INITIAL) { + for (int idx = ADDRESS_COUNT_INITIAL * 2; idx < ADDRESS_COUNT; idx += ADDRESS_COUNT_INITIAL) { // ReloadConfig with new address count triggers generate forward addresses coin.getConfigHelper().setAddressCount(idx); coin.getConfigHelper().writeConfig(); @@ -159,7 +163,7 @@ static boolean deleteDir(File path) { if (!path.exists()) return true; for (File subFile : Objects.requireNonNull(path.listFiles())) { - if(subFile.isDirectory()) { + if (subFile.isDirectory()) { deleteDir(subFile); } else { if (!subFile.delete()) @@ -170,1021 +174,1021 @@ static boolean deleteDir(File path) { } /** - * Returns true if there's no duplicates in the list. - * @param list + * Returns true if there's no duplicates in the list. + * @param list * @return True if no duplicates - */ + */ static boolean noDups(ArrayList list) { HashSet set = new HashSet<>(list); return list.size() == set.size(); } /** - * 1000 LTC addresses for mnemonic: one two three cake neutral benefit quick hip level mother fine burst - * @return List of base58 addresses in the expected order. - */ + * 1000 LTC addresses for mnemonic: one two three cake neutral benefit quick hip level mother fine burst + * @return List of base58 addresses in the expected order. + */ static ArrayList expectedAddresses() { return new ArrayList<>(Arrays.asList( - "LNTpgLXprtecSEzztNDmGJpfDr7noC65db", - "LRFcxeV3AQ2zvuFtoiTyCYfSjUQLkEddAU", - "Lc14g9TT4yAhAfHqLaKanr8pgCPBVBksgM", - "LM7Jf8MSp8CB7nt2zYoSZuFeqiBZwn6TPi", - "LUdCaBTEFQjVaZeRafinzaUxWXHZiZcvCG", - "LMUjorgJBdhqdy4fdUHAkm6UryJXLtzaWz", - "LRs2qDU825ca8RurD5yysMEgors3YA9hPe", - "LegeUDssVAq5Z7Y8cx2b8dWHnb1DunMZzN", - "LhNHWWFwSG61PY9duCPN98XDsR8jgiQJRF", - "LPw2hdP6ZvkzWTGn8cJHjinPYCrPa6ZpfB", - "LecbTbQ72258tMyoKFvAPhTJfhdzQwVPFR", - "LPwdqFTxhEZAA5ZUpBmydVPL7NRVHRQeiH", - "LR26gQX2Z5ays6tvgbHUeP2CCUTbpEfufG", - "LZXyUvGc2N23iHaDWWkV7drnvbHEE9LBrZ", - "LVSWjf6A7CRnHRFV1YAGKVcqNunfvmgmE3", - "LaZVKJN1DK2XiujjenaJ2jGtu2UKKBur9f", - "Lht2wBTNf3z6pYN8WWRAZw5aMFWGdMfRwo", - "LMdhg8w5QaTpSQzt9kBmzyQBTKp9vgPrCb", - "LNRZqXKFZoUUBTF2RzumfHNAUcxLXahWTo", - "LKqcHm1EkmggfnDMsMY6mrTx7f8PNMkk1z", - "LUnRSgWNfabKFmDTC6EdGWCNRJmRApQ5wH", - "LdCULqXSmA4dR4hKrCYNd13Zpbb83kjbzS", - "LMFF7wwrdwmz4kxsvyF57u5HBs253DDBmT", - "LQryk76nxEUTipXPZMa9Q6h2sgSKe4Xmz8", - "LULo7XAFgpickNw9ZCtz99u9BTdjmiSFh3", - "LUkdgnTnPJ3JYaDKZz6h6DdGQExentjcG7", - "LfGf5o14NUGuQrVSEHTRNFHqMndZUacpnv", - "LdRfkuPH6SppfU3L7o9PQptmw92Yw4JfYb", - "LXrBKZoGpkuk5e2vZ7RMyDqvzNvjuHxcwu", - "LQNHBjKgpMdfJSnDFNUjF34V9xiAFkgWGA", - "LTjbuCtAwc33m61teukpnPyFmkvLi361oH", - "LYAdmeqiW9zYuuQKDrnJff14J8sUEGZYbU", - "LbReUXLecNu4PXoLfhJqqn8KCE4sLoKBWp", - "LNfwCeec9ux2cNLiu69r1r7omiyEGqqq7z", - "LaQZF3arAKViEhhQR2fbAdpxkpaodsYLxR", - "LezikXzZAHDhJMYAFpRbJYipWx2b5yM86r", - "LZSKM3miJU3wVtsav2PJeyywRXjU32Qfdd", - "LfQbiA2wN96n8mUq6S8jtGVFgUA5JZq4P3", - "LghnhiepXMm1ukyYUsAoQurd6fCv2Pdi9U", - "LNAmNuCMv34C42gsGw1okHXeAx7uxeDQN8", - "LPngfnTyCGWbNwRLo3LXZBrqeN2ktLFTPz", - "LSPnz6cppLDwSGPPA2wi4Xo7Fd6GfazYE7", - "LNu4gkrRYhcWuAtiiiMDrSee47qgQ8L7Q3", - "LP92pv1N8tJzRuc1D3Mqd5HqL9gtitu8Gf", - "LPtgpYRpBHKj55zyVqceBL25pLbkt3XV1H", - "LdstQc2vRERBUuferrsHutHrSyXc8Eduu3", - "Lh1mGh9F4Dcabyw4KLFibKzDA8YmdVGFLw", - "LWQ5ibuQdFvqcvfhrR343Sf5cB3HewwPJP", - "LgaXbedKCRRMLRFXtr6gxzqqGC3rswxi9p", - "LLqKb2SDpA9a3R4uryWLgzC2xYxs9sKPM1", - "LLWEBquiHFV8LGivZ3oVwm5b8arwedMNKz", - "LKHnkgsYrdr6QkASAhYp25GmS8RfF5Ynga", - "LUmahz5XqhGzXEfQ5G8re9sGcQojF7E2gH", - "LTZYrdv6HXRzLDBkqcvZT6CvbGFJ4eFchm", - "LVP6iAtFq2JLCE22ELWmRhRPSM4Zzr3RHs", - "LhRLJzGega9paT1QZoSTvNWfpN2jpu6qzc", - "LdAy5N1wVcrYngqV6UuTKWm4xc61SzLzJ3", - "LafonyLZFoj1teorrjrD6MKyoJbKBBN81R", - "LRgK3ttMmJGAXST8c5w4f9H3hAuHiMgCCk", - "LZDwAdTf3aMf3qYNQAuCkA8B3BiaANcixi", - "LM4e4QiHavGFhkguqVcQee5N5exmQZNFFr", - "LNQ97UjwKtEFsc4P9xEDwh8obNg15V4rcB", - "LXQkMaN7ehoK7KQuPYrDms94YYaUkyhAnR", - "LNFgvrFuVP4SXYmfhBHZ4J5GRzDHrrNhYu", - "Li9ARsaUWwjZXnTQRbTaz8WGQxpiwgDPLC", - "Lg96QoS75svudKyjRW3CW4cw32rBvutZbX", - "LYQiqNPKGHHMTV9qnjVv9WWtUhwnWvTQ6i", - "LWGYANGR7RQyiDmktgdrgz7RGFBCShxtd5", - "LZAiCMeZLzy1QGkwwoT6UuKPkvMqXHd8gz", - "LQ82FZ8GLj6bVavFinqFxvLNbjcrvPCxhS", - "LcofKwE78JwmMrBGvBvUcSmT5YmL5UcSRY", - "LTP7BUs5aBX7C4AHGrVAtLiqQpajmvUvqy", - "LgEsVHXPKQja43j1Rd3xE1peVjExpH2qdr", - "LTBXqH8AN41QbmMXPxotXHMSiqhZ2SGvce", - "LUqaaPcFpqZPhYbUs7uvRj5FVz6NEYiEwm", - "LSwu69aWjEzp6ALhyNWkwMqMyRX8d51dAw", - "LcfMfN1SaMGc4qfSR3Z4bYu9nyKE4ivpEk", - "LZjCL8xUKAuy83uvqPcnkMwLxRB2D1TYHU", - "LXzJfLdZ222RAwNeic7pgrNSkwPxkjVdUP", - "Lej7drV2kA6jvRkiVRrxDZvTfX1zyJAMVt", - "LdQoBbvGhHj5ySGRgVF6RKNUNE53LveNdc", - "LMWLPeJXYZ4VPWB8xpQ56gNSVDCgu6Qa11", - "Lhd7oV52q4AHGnNBXfxRpKU3av7aKN37F6", - "LLZutN6a2NYtLeosEqsKMd9N7Ca1pzAeTS", - "LWguMXkcw5rNRhJ8G2N7nyQzRivYEWKACy", - "LeoaenVAPAkD68xTBqvX9TeesyFMGYeWGG", - "LWr1sDHbDLzyKLkdfsLmRLCVctBAcejZdy", - "LcV3JXkPvF6LUaUi4CsjC7zQm4rKF2J3xx", - "LWTf3PEfQVTfKcHrw3NXgpRi6jxtG9P9mC", - "LZTm2LsVpqFJoiDxKTDamWtK75FjuG8sJS", - "LbdNDivoeGXPcTfq2g5fEEK4zK2m4cp5rc", - "LRT8aPfvk5yqsuAs2X5yza9SQ6GtbENUNo", - "Lh2ZRdHMt5cm2aVUD99o7diX99S4GtbkyK", - "LTTjcuMfedGAuJHL4W8uqxCZmbSANnH42x", - "LXX8N9GBnzeDzTQeZWCEm2xRrmqsJgb3bk", - "LMYYVufrxdQpMqiWdinyVYfmTzKha8suA7", - "LPWUpTu23Gjdjaeg23UmDLmyaeBthZQUbd", - "LRHquUoS73mG5Uj3VtQaVgNmtj77a78MD4", - "LVLmwgtkC2pnuptJy46JZmDJbw414DYYE4", - "LKR4yoQFtFGDn6L4Fb79G4bjaPNrnz1Xq7", - "LW5bvYkYF7vS5tZMBwbo9AwMTir6ywGR3S", - "LZXQ6RRPRGXmSeFagG1EQxf19LsESTHKeN", - "LL41uzfhWeeHZmRpbn2XuhDx9z4tdYBy8G", - "LL57dQUequqnXJCYBT8DFnVt9k188pnQb9", - "LV3AVsx39xauq8t77iN2xDhCmpLf5cmKAj", - "LUoyQcyqFtcmEaEm77fNhWFoDQW3NZmRQW", - "LParh4xAPDCNTUfQaWpb9FTiqin1TY29F7", - "LQBYnEUuw4Wyov5Snv3cbgcpxgoMASz6MT", - "LP1SimqRa9nJgDa1pQ5X2BUUgFbwhSr27T", - "LhnooDJzpsiBd9gBDavqLExWk7HCmPdfWk", - "LZUbTRvrcBbJTehG1yL9EhoHYFqBX9GY6u", - "Ldafj4ehAHY8khB1zhCqXeoxtfCqbX3xhN", - "Lbh9xiQGaNvC1mj3J5RQcrcQgMxCsLPui7", - "LbccvJV7BKYLJZTGBeJLNPjnPz7BKJnqzi", - "LbxaR312y3RkAi2dnxneA42mN9Zf7Y4qmL", - "LNJ5kQJL85HPUmAfuqV23k9jVbrYsqFqTV", - "LMgSwsYzXTTPQBh7jNkY8b5J8kS1RGY3bN", - "LNGsETiVjZTiewDwSx6MBjtQJ6cjxsv5kP", - "LdYux77EPviPYsgntTLSoh4EEcYmyGWtnQ", - "LXjaNYexsVg2cFRxb8JqatMGHMhwW5wtXX", - "LRQ7wHfHYuLzXQH5sQZXfc7ZmLUi9NLTxw", - "Ld8osEsH6XiZrJGtL8hcpNRrurqBvpf1Pr", - "Ld4HAEWDqBt8zn6xnppqUfWiW7jqs8qs8y", - "Le326jYzh2HFkKska2SriSGUJB2Wg5MxQh", - "LeHUeSb2gNDA1zzwJT4ZHwmMZRWWSeX7dN", - "LhrDhcG6qKy87wFxyC9pEVkFa2TxUKgVQB", - "LYK79wmjy3t2uP96N6GPbDMJcwM8hCJsX1", - "LY9Xow2RgxC2zfcawmPhk77zE32b4sgZG6", - "LfCG9qkGpAY5Wj7w5cNforfxTs7R8MgWTc", - "LR7FyfFoYoniwGarnsCXmN437i517QJL98", - "Lgwx6Fj5gsEZBpZHdUzoAfDa6UmTjb7sPr", - "LQMvS5eMYSFDa6yxj8hr7m4aPPiCPEVae2", - "LeMGFqhYNMfcx4HDBy1NSCWzLW7vAy3oJ1", - "Lce8zFao77eQUiD2bLJLieRN1gy3iXgR1U", - "LQ5F51XZVHFMr6sDoZL5YUSzaV4WLnEQHo", - "LKXBnp6sG9vqz2fQTQGsbcgfkTwHu7tvAT", - "LToTM5T9yY2RwwPSw6Si6a1g1juK5SwRCh", - "LeqPGow6kzcdxdUbeUL4AUTwnTaWLGfqwP", - "LTo82EctUtn6BoGab66tEttayxpzjusHff", - "LSRUCGARpgcS3Tnc7QWnqkCEkD43z5Fezd", - "Lgh9WpzeBnr4fCzuKY3VFe1Pejh5mYGvQH", - "LWgUo4CsXEr9CDUraginpK8jLteb4Cq5hq", - "LgKS4NmCKBUp54oxEUfi849hB3mTG1vi4P", - "LKnpyE5t1T7EURWxaL1q1rawe51MTq1ajQ", - "LT8fnKfEjfenPfLdKjKNScRFHmdxjXitEa", - "LPjpeXCf3WPhjpYYw2sZCSjNRfv5bHMTBU", - "LVkFzCU3rPLUmPjjjXi2sUtzxPsb1LKB4m", - "LcF4dwsXTdHjngZFq5A3AYnCfJhBvSJ62c", - "LdyVKgdUJg947nD2ig6awLwyAenmMAZycw", - "LKWNJcTNGJqvdFrRLGZgEuvvH4iRcsytKG", - "LX4Rn14HUtFPLtvQvV6hP48gQ8NpCavmJ3", - "LeJPuEgUH5MzUVcDGwwALYa9X2d6h8m1pY", - "LgRTrcgMPzLN1bojHMN7njFnBNPuDS5zjE", - "LWzr1uppdUef4gvJ1QE8jd1Sa2DZUfaEVc", - "LWKuK8anQcGLNV9nrnjduXbYvDRF4Fj3LZ", - "LdsJj1cq4QHg7pVcmbKzbozmiJxiYKuZfv", - "LRnN2DzvhdUCVzQCzCCwMgDspQCoeojzwb", - "LM2fxgnQTCnmyggy7KKhBXbJQStbVo5HeC", - "LNpNCPsNruz6fBUVkRFJTyh2kp2RaicfhM", - "LKnmkxPkDNynjvKzZi2mdwhy3LGs5dzry9", - "LRLz8rLbdog6wYJwka3CVKAcXFzxu9JxEA", - "LbnftHPQ9jP3r1gmNPdDoTsDUp7xfKBtbV", - "LSVsppscBXo4j3wCtikHP1UBeg3tQXxJc9", - "LiW9yYR7CvFxZs9YpTx2JgFtk3yYqegjDE", - "LXcUo1ko1jC2ycX5nvdZfsd2eyJSjws2W8", - "LfnmCHZ3ySVy1aaDwjzA1wcrqK9SoyxsnR", - "LTRwKGCtV3Q72WL9wRoNkAPxuVW8TN7jni", - "LPfhiBxWEmnk4o7qoBE32UXWkuxsJmTsw4", - "LPLnj2KYK36mnHhJAV67UqtPhhyEu87zbm", - "LbSvp2mP4EQpfmQQMZDWe48SyGhyDmKozs", - "LeBGoKBvaVfWneme3bmtCyrYi36QqaEQkB", - "LcNWHL4PyUnrQC1YHUjWPa9bi6mdxpysGw", - "LT8Wd1Yc7JSvkuWXP3Qg14vnzdKBc16WFt", - "LSRgVmauy3UuGr49cQ1Ud6nKG9W9VqrfqW", - "LR9RVacNWgConsYLYnSCD64xzhxapAzys7", - "LfBcxk9WpfagYY9oVnomeNQJGCijNPqWBb", - "LLtU7JvNQ5LhmfbwMazNjDzQ9wB5SrmaXs", - "LPpvKNJXvykhtZCFeCTfCJGkAYiN8jZYBh", - "LbXYCzLXBRDxuwv7Kb8VBf8YjRdJ8Zuc8b", - "LTjQkPWB4NgwXPiZqGT84gjj1UYGjqRbPG", - "LLp6MjHZKdjWGKQe5WGc47dZ4XM5qxn2mA", - "LNh4EK3hND8PKDdFPuyzEVWtr56sb7FAuB", - "Lcgn675CV2xcPKPjYvqLcvKwV86utNkTAu", - "LTw1ETkVwmGpb5HCkmJvERJEsWNdZEPRHm", - "LTK7rNjWEqdAAALKyGB9nZwQTaeknyUXNa", - "Lc8Mw6ErwuEdHVGqLNFMo5NVGcKoHnZPCE", - "LSffxhP4C8SQMexuzgChTmNRfvyLqwncbf", - "LhnPf2VirEeYCrKUbMSiFzkPLJ2fTmNtzm", - "LTTxY187qCEh4dibgc3onwrqyH7FDSzKBX", - "LPkQHme1tmM7tqLsRR1soHUKGgVaLCgRfj", - "LZBxTJ6sZLmTrEDAADrpEMGoanPeWvvRgi", - "LfaPNikFG8AzzpjKTEJMkKQnjPf28rWjMP", - "LNpmRArz6b9ocUsaiGanbnThWxK5vFZxjA", - "LZLBUYZBG9XYGCzSzkW7CEgqnu47AQ83uu", - "LUnNY5vj3xK61o8mJJiKXfLGTq6M25FB9r", - "LcfVXgTxv7Y7F8EFqtyQ37gzyGSDHs2dke", - "LRPcZSWR949S7v1UtsjFXVjjFp1MNx6no9", - "LQRTtbZ7fyNbLifp2VnMjkPAmuXSK65jUP", - "LKKz9XTUkwrwLfA3CkojuHdQbR86addQq3", - "LYUTtt97x26oWJdwFLVFProQVETX9Gv2Af", - "LSFotdspBDfvK5Wf6D2D7nDPPJWbADPxbf", - "LhN96GZLYo1aqM4tdDqTZWFYV5sMxbJ8Ud", - "Ld1VywVYxotJCJnYiJXYcX7vRBCoDKDHKh", - "LQQodxx3oDCTnWTaFkAWUAVu2Z9S7MmCdL", - "LQp6Ajv8Y221izvDSGNjgeykJbckVgxxLN", - "LcGUwRaXCfLiKM22f2A8FzxZSRrCy6vhto", - "LbM44fAzbpuFk4U84S1KxfwAtUERwUQs2U", - "LPiRebPFnteKKFr8RtGDFCzkcFiaxcXMAS", - "LgaBoSmfv8LLKdovvDs2LpMQqDSL4S52v1", - "LcGrF1xqc2cDWfxFw6CqRRQwKzuuXzEnSr", - "LLvufEqjFyDSvG5w2F1fcTnrWAyTXAoBsv", - "LfHjA4ViNZt6NJWtSa5w41H1wuG37gHrr7", - "LKii7xfkG9uPimmwiUojzPPFZDQcbVBfwR", - "LNsWWFDfuG2mi5x5Vh1ypKR2cCfsrKKtdc", - "LWFgLzQBmwiG3KJhXSWECM6oegE5UcRVLd", - "Lfijr38CTnD5FMioHpwSpreCWGcVreLf1s", - "LQDzYkTfNNkkmETTKEcsfBBLgDio7Ch2RS", - "LZWdM1JivYM1gpgBAXugJcqcvfhr2DFPAg", - "LfwPuqWsvqaTAe4VGh9AHFu2f86mcHQ7C9", - "LRwVUNWkhEg4DZNXPboWVMgPam7s67HYk7", - "LR5juRP4QfNcKtswkPU2z63S1VeUHUSTfZ", - "LPaTHZs8311nZggDGFpbLofyyDU9zGfnxh", - "LiXwEJzji9eFwg6Qa2HbAW19DPrQNwNQ8v", - "LNhFfFqYnHEKHdYA1cW7gh2w2qVaRQ3Aga", - "LPWL9SK18JutKwbJBNGLNoiuounbkVgr86", - "LSatLFBU4SMD9nv9R4ageDVRDUTtQQhk8L", - "LRMLqp42TfwHgxpLs8JbJfWJqfs3ZdcGbv", - "LP1enWqWhswMvPQL5HwLKJv3S33oHYgvje", - "LZ25qiaYmv7DtKe2NW6eMsVgogHw76aS3j", - "LaEedjPMoeY2KiQrSqkmotUeEPivEub7xY", - "LTb1UmEfERgmQofhkswsKEBH72qPCUhZae", - "LgTVzwEmuMzDBpHWryXFiHLPzqbMbkbfUb", - "Ld98vrb3Fr566YeqiP8aeJveELmqZaVcuR", - "Le9Db1J8ycebrWFQPafgtxo85NFroj6bkr", - "LPgtvGYe2r431Vor4NtTuCsuNEu5zdbyKr", - "LYd1ypLRtEikKWKjiaP6t9doBfrBJ2LXt1", - "LZ4NFJ9Wo6BXYfX1Fu2hc9B928youeR6Fi", - "LNv6tiwWTJneFnCY9DvLSS5gDi8w45Ebcm", - "LcVNwNatde6YvzToAz2aWFQhDZWe7KYTQ5", - "LMdzgk8G16GdbANbJ2V1scmjirRuduASZj", - "LiAQUYEWK9LrWo3EVVvArJjWroB1p6QW1w", - "LhU8hfcGUAVbQtVWAbzQP6QgnSAb46sdez", - "LdQizQZSqUy9qbjFjbutwC2Ykm6HT1aUVB", - "LN3VXb81XUN8GRi88xiAjxgRK7jmpHSh2T", - "LLojnet7SDEyE6r5ypEbbMoxj7hQHwWtU5", - "LV2ewWVcTXHuoZbQueEnjX1JmARVoFzax5", - "LazBTecQrBBWVoAXGudW74swL8U8qyD8Nr", - "LPtNcbAbWdaxUpVJdwYLZfkxi6wBFAmMit", - "LhRcaWetasKhDAYq2XbqeKcCapGMEwv8zo", - "LNSmDGKbZMZEniXeYuXCYYeYYMkywWabcg", - "LXWS8N8XxExRu1kgj1Pk8DLx9Dvxz8Q2fj", - "LexYfRVCPaKKVGFLstMTQqLDpckeigssqK", - "Ldms5eGY8zJS6FWJxv2EVXpqRi8py4T7NP", - "LaoL88U7Ry6Vu2SAXmCJsEK5vC756nXg97", - "LRgX8PJRoNWq47NejvP9JCcWLTy3GDhGtq", - "LXPSjGUe2xWpy9V18g14JPUxrbYa4zC6gk", - "LVd67tYst7wEaygdbyTrCNnkUCxGwvHMQT", - "LQRjVWXQN7yNLEWYCRCGqD8DZ7ZAnwbGFK", - "LfCwTPmYxcg6ptZt1XsZpXVLD9erm8icfZ", - "LSSeb3WyehJtkN5pC54p9WoZ6pZCqyxHc8", - "LWHGWCrcvRRDWkUz1XRxvgyxYv5XaBY9fx", - "LfnKt6eJ6nEvRVLoMEgy9D7fad2tqiMB55", - "LVoRMs8mfdNUkx3U9rWhHah3fc5jGs32W5", - "LUqSfbzNGBbStRdC3sQZ7MPeHkxbcpdu37", - "LSJhaNKiAYiLG3LfQFQqyRvb7aeJguRzbK", - "LTnJZT9Jz2fiyYCpnoV3wakoxqAeW8rHgN", - "LPDtzuk4TrFXaTn5fD5eyhiVernTJQVcwC", - "LbhFP1AZQ3p2Dg9upDtMaM6L6Y4AP9eViY", - "LfN2hVwHCAcSoanTSmZHPZpTenL6NSxn5b", - "LN41HmbM8GZnjcSfhUWohNL5JHzYc6Q4yS", - "LWCRTvjLsBPpwxHETBYmBfA6xT4YcSgot1", - "LWX9PVUBK7TtSGwNPb5Dg3pLCR9NQZuUtp", - "LbV1MJ8C16snbn1J5cWbcuAPXbojVBxHEu", - "LdwyhS1mXxcwwL5XBpVYQ5nmGhZKXBZxB4", - "LcMGYAjuRVVv4NeQgeyAQxNygtbTa4VKAR", - "Lhk6FBbTkBBAqTN3WMhgNwFUSxxt4wQdFG", - "LZaPP8QwrKMinoqVa8nuVHBARUFJutrmaK", - "LaviUsiNSthtxZqkshztwzgJFSoeRwB7SM", - "LQSCpivkXubZ6WSJ4Sj2FYQREzNYE7eHAa", - "LbjGdRMLBN6ZzZzCEmuSMREm2ppqXxXxG9", - "LXhdBAZRqndks8H86sauQHjdMxfauYh4qP", - "LKULCTZmekgyPJNKKr28rYRhMCuYm2o2r1", - "LhRHZGQND8pLJkYSNd2SxYRQpo6zNb8c7J", - "LS1axSTidtddZ2MqJtBMbTqQ4WUfAXoLfv", - "LPisDsmDks7AifRSbohsZRFwzC42wKvAfb", - "Lf5Hu9DUCFzWgBZxnNVYTA4vRvQv5RsC8Y", - "Lf5mNA8j574KiToy93y6vyJNNujgMB8D5h", - "LfwAVBi7rM9EqWgYVuxgy6s2oruf7rFehH", - "LS4NFvwhLqUZCVUvbsTLxmUciii2c4TXEa", - "LZSXn1cyh6uVGkZfQ3knsYYJUscQ1bH6ct", - "Lh5RVPHaS3XESVeciCQWU7fX88DLXJYohN", - "LeD9p9Z4cs6djuMY8eViFuAEUwZ23SAm9C", - "LMgXv9fa8LaYhT3J5EAvmhAL69SuSQVYj7", - "LKmjzHfHJjy7w6dPQPRbHKUrR4DmJmfVAo", - "LaLBXX5RPuBdtc9PmgYj94xnxSgafXqxLj", - "LXspfsj2Aj3JifsvieKJY855FqbjarcRLS", - "LKMfrpYj2Jpwf6GbcWKVLVXQr9y9Gfgxpj", - "LdW2144tJQy9Wia9WpRQfHWYuH97KPetpc", - "LbrsYcvTG9xPRdhB7EPzGrDBVMBSJVtz4a", - "LcxvDAqY44V21NQVU3nDNaRZbf4j49zg4P", - "LcvQDwfh9ERb1wQxLcbBicyaJ133mBDJJw", - "LZYVtkTRrJzpcx5LY5cY2QgXZsrqcmPHGv", - "LRjkE1xGxLGTYd5PuBCh6JdLbQeBWjzCXn", - "LVce5gDSjAYN8BBDYrYNAeJJj2gYqs4gu7", - "LYQKe5mWg3QWBvngMBD5YCjqaWuAoqCFEF", - "LYySS7JcpcdPcRWMmnUNLTfwdTMgLpZNqf", - "LZoTv6VVpX7vg4whaXujmySxRENuJgkp6B", - "LgUAkvrob7L1Z5J2renUjbVDoH4ipPtyrW", - "LQz9t7uG7mH3CGtaZvUwcBS7w3qMXnVYvV", - "LXggGj4DoRu1PoRCrS4ZriZetn423kNjtM", - "LRDL2tAEDY5Cp21GMxq7mwSmhtCvKHJYmG", - "LZ45DzMZNQoNfeQvVDH97Bbyv83zM8b1un", - "LfLkJ4ETAXFDYdguJYkoumr73rNnvCpRWn", - "LNeH5HhJrcY7TPH3cmJYuPFLs7MRZJhxS4", - "LRwMLedPqzZ9TnqXupuvo8ZpPU775hxfoS", - "LeB4sMZQRFPCNTaov66FnJcMzznzXMEcrx", - "LN58nMYRVDHZMbCYfv866vstjpQYfg7P6L", - "LXbCqAhCGsN1mwTfKmPB8E114CiQfH9csi", - "LfCUv38QDqrJcrf6C2cGTT7cQ8v5BgScUd", - "LTWee5WMtpDek8AvTJJwQDKwUmv5P1kAMp", - "LaKcV5pyABMRWfiwZv5FiEvcZztcmEp8GF", - "LZRoQd74qGjvnoXHLzb9Ksnw4W2v3MD7yz", - "LQ4AU7TBv7NXwn2NJddxgDhtmFr2J8senR", - "LVvfLWXmQaL1YSByH8tVfJkqFCwZcQdJUF", - "LgTpi25reQc7gsHRqbhTpJnUg5ZHWxWc5L", - "LPUjQfZhzEXLZys6EQqt5gm4huPHk7GLQf", - "LSXYAuAkxcrFX4pjdM7mRxFpXUYVHLWMWk", - "LZnVdtGa6YxfBMUDS4zrNLT72mon7dKqkE", - "LewBHdJvLCBfsPSVWch7y8bXFP5F3YCqUF", - "LfGqWfaNP26H4hTQR7h1G4xctuZiyrT8hP", - "LKVXqiteTcAaRYe11oQDTbxsjHNqPzmFxH", - "Li36uQm2C3ntDq35KWEkds727hh1suXGYC", - "LZsrqswTkjyxtpZXfSZ4oGEHqZ6PSnKdcr", - "LcdTZFftQE3mufjgGRBdQ3aCD5tg8tPX5z", - "Ldj9MK45RTiUvqYjD5916vLgkLuZwmDGWP", - "LdCzymEQLHtXQnH2unAiZS7myPanA24vaJ", - "LWfaCBDTZoLfQjb5k1ForKMd6uwxDyFpkW", - "LRgbporuXdvyrKzdq2W95kkdwPEHjpLw2f", - "LQU4W2hUknmqgUSBZjtD2Ch85M69HARHXC", - "LXsnkb7A38NDHSJcD5jrhX8iqwFWqjnYFL", - "LgRQ4TmFMwW82VEPeSXYXKNZAGKWSHuUgP", - "LVcDDKskQD2TqnSSfG2BZG5ufqSmeagxfa", - "LaVv7yH1k7upguY14k2fbt3KfQK3zzkFgZ", - "Li6v55R1F1XeiZZNQ4RywuxZsfssgFHtia", - "LZLp73FKyUYkN74Sy384g4EVUfsD1EhUWf", - "LgWrxX3rQJwiY69FHa93JeW4xsvQgRWxBo", - "LQWm9GY3ciBWCH6bgiNHmPTWmLM5oKtRaH", - "LVqgHt9vUnTB9ZzUYNYxwVariprCbSAv4A", - "LbEsbXSqKZ2Uz5zQp1PTa5UXab7ikXTmSw", - "LaLE1W9XcM2fKRENigadm7DvB1u6FvGgqG", - "LP5cJdt8gm4uB7wyHHB8s6tYmuKyfcPSJs", - "LgfdaifGiyQEzhdCGV35Fwf9536CbaP9AD", - "LMt59pFjsdLp6MGDQQD9VRbHBZmatnFRg2", - "LX7e5JY4yfQGQzcP28NEcZzrzzMLexFCc5", - "LM2cwqThz5yYcUE7b4D2Kx6V9xsfX6135B", - "LVTbHaiMbuK5QJ9iezdwbJXkZxXRNWXc5G", - "LPkSDgscisHp8BJXeUztneoRNbkxJuqhMm", - "LfczRDEX5kxcyN9aRa9S8LD82QrrxMxR2r", - "LLmrgx2pGLQF7iGva9HookfnRgYDXxR92o", - "LRSrWxxwVDk3TtQjKMwaqjJrvp6PEX2hq6", - "LdsJaut1n1oALn71QDHeu5MKCJKYsrbJWL", - "LaTaJDeBb3hwYrMbVd7KgpeJA2ETu7bEwg", - "LgkRjuxxpx6Z8cTJ6A2ecaH6bCg3KvTS5U", - "Lea813yj7mcDEDyD4UxhjYjapWiHTkGkSC", - "LamHNJvYsnuQ5rt49a3mmyttGguF3hLyTq", - "Le6FcDL3NCapwafw6CMcGpnQKByHrzusGq", - "LfnzRrWTnmLKfXDFSKjgKbrsnB1iFrk6yN", - "LZXDUyfNwcfcsNAdr7yaWeDKKsq9YzAK9V", - "LXjcmGbwimUKDb85payAmrQmSXirDf9HJQ", - "LPnJYTcNKBM69Qmgna6yf8FMJqTvSAoNt5", - "Lf37qavaRCar4Wj9Kcqvhebaj2E9G8JB6q", - "LWjJHHZ7jeWXHNoS8SL7UEyd2gcSCFoWnC", - "LXbyzfyrmFonXmBk5GkPxiaZ1qyUZCxKuX", - "LKgfwR76E2YaH1yi6UTV2z3WftBM9pXr9Y", - "LYy8Nh9XPht5Mwnt8624gyc6axhFMAUAxc", - "LNpmSSJuq6tzt5tS9dZNJjuXhezDmUorLK", - "Lfv6agqBwMsFG3ANKvPysuJyQUkwp5XN9J", - "LfcDkWUBDhWVmF3Utf5Ztk4QqG7aehrKW7", - "LR9FK6DZn89Ye1cNu8QAtxZAVrT8wYxC5X", - "LLNChpKyyWgerYTKVfUFk2oeYUFyeZuEoM", - "LRGDjdUCZ4sAgUTK3m5gVDJA3R3rAY1nW4", - "LYeEZr9GdrCgei1RRWiRF5Uyqsi5i63gTj", - "Lh46Rk8bHk3JxomWynjBKzpJg4S6D4s2NE", - "LKtFkG5xJKdNnejKa5HxeUX7y1HAsKZgNP", - "LRKoBygqMvJYmDK8nDpSk5nWZKd2xxzbch", - "LKYb1yherT6oxUoMw4MTLyV51gJ1M75k1t", - "LPWP6xysXnpNXj8rKKPwKUKawmxymmTZ5E", - "LRcoYZSRQ1C5BLeCiNC46xFWhDPQAK2u9u", - "LS44nek3ytjXDxCuKDepHzPZb17LgdqVTE", - "LiAfwhdiwTd69BznWPi2nLjnsGc8aBH3J4", - "LYofrTD6GZZvC6vxF5iKewLm3Xd3iDWz7R", - "LXftf2DrbUVZBC7Ty8S9rnyrv6PdadXCZJ", - "LSazm4eUAaE3BuVVdL4q2cMhdbuwzSMmcA", - "LTVee6L638kvdPdYM7MG2V7JZZKCBwPHB5", - "Lg9LW8Q37hwbcX63RpzXBKprdei3Hocj4f", - "LWx3avsgDwffYTn47x8qtboTUM338c8CKJ", - "LT2PGJPi6xhzeNopVwWntRaxfUAxJzGo4n", - "LKhsCQnPVNPjLWPw9AesPG2rsbYaEbFfVH", - "LVeBNmWUYGoN35MPPb7eogwuApwosVXvNc", - "LiYzqbRHxSdU91kKGHRiw3k8UrPMKV4NXP", - "LPLFKEmRrMkKThEotv6LXSP2ujgLAvvhhG", - "LWuUFRwniKhHYGvpCJhHDsYmiqn6pP1NeG", - "LhrmTf3A8wN9uTG3G9kHzf5ogXHmVM744e", - "LcTYdKZvrwVgkHMsmqsLyRFVgUHJrXLSHQ", - "LVzanUyN8NiSKtspPUCAC1FHXASzEZJMM8", - "LKNCFcQue4RbHkQimmxByQbt6guqi9keYz", - "LZHi5sTPdUmCnmtCodNR4v5NG2BwAqCGHX", - "LgYQybURgdTLSZ7C2B1MhHWyfm7vt8wvW4", - "LdKLBQngeqcs1km3T6s6ZiDVZsQNdKo4QC", - "Ld73DCAtAiV6JccEsMvojGmtSNza4SLoj2", - "LVKWHWqE7tZp6rvmLhVFHcncSpQ5Mwf5pY", - "Lb7WtrcZgCYVz4C2ojjhAea1LWGX47p3mu", - "LU2w7DZWzuSsiSv9WxC6czySDdsTGtyhVG", - "LiMR9FFdEMNmcygQjfaCsN29WpJhQpwwub", - "LcJCTXGhoMc3882LDLWSvVrSvchgcrScee", - "LU1ozeChHY4Dc7tCVSxWz9cqptBgQ9pJb8", - "LMy9TFp2vUZJh1aBA4nSFTnk9dQrRub9aq", - "LexvTJ2Xva2Qx91Br3A6doygTnQrC3FaHU", - "LhJSxkRoXWB1imF1XLTydFGjthNdpMrxRC", - "Le3zA44dL1oxHjyJUySodFnhrQAEZu376w", - "LWdFGJmNPQjvdi6o89MDaFgQPLfzgpmvmv", - "LNqVqTuAi3WAnPr8v5UCb2K9v5xF9xVkUX", - "LamEb2i4o2ZQU227JFYyJ4JaiYroLh1Hoq", - "LMxqjT3dU5RT8LuoXnrZfHQu2wwPAZCuXh", - "Lf2Kwcy9hAcRdG64wZYhga7yvajPkyEQYj", - "LNcA279mX7GqQXa5NfJK9EiUCFvTZUEwHK", - "LZn5SSYFXbMNvWXi6Vzch6sYnEVm1Loyvp", - "LdpvugbTixy8dGDYvojLKC3UxzwQikzE8i", - "LfginKkdNZy1VpXtTspLypqk6KMcfGRW7e", - "LdVmYQAcvtpJdn4Zw21hynWcMxEgNq1qik", - "LdiSKq5rU78uXzibkxXqwfd2eWT1oV4WUn", - "LUaX2jyjQz2NwPicXXqBdYzYowXSeM16yN", - "LZZszxqfznEeCxdTV5t7U2yBvdM2ZJir65", - "LKvSyL6FBZHgN7sL7zVcxKuaTiEPRauqXK", - "LhXry2d4HrVk9E2acACooGs3KgKCDUzL7r", - "LfvCGbTUtRe9eiKqJhs8PAGor9bwqjwEsT", - "Lc98AoHUML45SAGgpzsX5LYc7sPCWpftNi", - "Le6XXyQe8VUbQhYUTn9wFY761xpLVETwEn", - "LPs9CEQHL2Ef3bKuE2BxmfWDnJfTvdffyW", - "LUP4moUpoS4ifVBtEBswFghVWqLdZ5qTYX", - "LQRXpcSkx5d66Y6WWZWXAcQoEUYbXisyR9", - "LUyQxiJqBRit97K9wdiVC7z8GZs858Efay", - "LXcGn1y7Vof9RqCUgD1msBXX4TKbfN78hq", - "LNqQZNJdw8jpwkq7MC5A1Z2U9VPRhxCN7J", - "LNYpdowmDHHkV5kpmX3BzVBqSppSNW8Gr8", - "LKskwLZnzZqCFQunxsKPqsJka7WuW5Yhc9", - "LR15VwhWBw1pnjabSn3M3VReFGmYowrhoH", - "LMzh6FEE7t8QatDihJesgjndmKCEB3GhDk", - "LVNMUN8yDoE7TcbPi9NWDeUEtRj7zBnmD7", - "LbaZwbxNhHiM5vnRf65NTgtM3ibaZCiMSf", - "LcUHUzb2Whg2jww4jqgtq2HiX1cjoW36Mz", - "LScrZ92cYWGz6hszQTryFQcQe41hsd4MVS", - "LXdsv51o41qLX6hRTeScx62DH6NTrBsxP5", - "LTTH7rDBRNYHUurrFBoUFVJCKu3KTL4VEu", - "LhWytosG8nvC7hLDVnuTc1EqPsSKNSXW6d", - "LfdabKhvDa9rzM7JUN2v7g4JXUbvAmn5aZ", - "LRWd21TvMqRd7WE7FowMBU2ZBZjW4b36gs", - "LaEdURp2MHA2FevyhFj22rjfYZ1wU8Wb3w", - "LXNHmVTWLMqnavNwxHUgBRQcUa3NLHxQLZ", - "LZjcbwokoh4Wyv2zqUdjW78rDVKX9H9erY", - "LhFZYaLVVMKvAMGbfEEJBqfNQLsBiDukS7", - "LfMz6875z5cFMPpdgQuYDhdYJjbuo7ydXt", - "LQEe48oXSSiFsKorrFBtTk8EunQv4rU8R7", - "LU76iwcG7HFaw45XB8RxjrBk21DFGtpUxm", - "LUcxMHYwemdh6GFMau9Zr75pvik9n5jVjT", - "Ld5MF45i4U4kFj899L1NY5j5J55YSA6eki", - "LW4CWN7rCAeL1hhQENBSt6XqTvUp1JA4eg", - "LTuJrHZK8mALecAM5ymjVXkPinwcmUwUe2", - "LUA55GSL5RuB7nNqN6w9k4EHenGqRmpoSa", - "LRqPW12HWzSH1CHufFEThj5pmU1oaBFGjN", - "LZ5jF6KjXtMJbJ9s9rYu7LvUhvMWhqCF4X", - "LagEhMhov4wZmciRdamTGmGzwFziAuphY3", - "LfinWg1VzA8ibHWBH6mVYd4VHKr6NHtKq1", - "LRiAzfYMPYrU7wbg6Poi24b2rMR7fefWpy", - "LUsBVE46UVrEHXxWRDB7mttenSX4p2fdzG", - "LeJUwJfjaghuWb4F8rmUrsBcj4Az7GTuRp", - "Li1sLEgHeus5w3sYdnbtJZH7ySwP6mnEhV", - "LhDVcUe4F64Jr7WJYrXpWLWW3Uim5A99Gf", - "LZL5RhLRu5JpSXWMhfVkmdZs34g7zftRFf", - "LWt468oMQ2qzDxAYkMKiVwgSSFi5YiWtHf", - "LT39rRFHHCyn8QNYP6LasYkYCrovdRZWCv", - "LPfvXnzQytWK9kvcSbEJxPaSQuKYZDPsQi", - "LZNYir62U9fRDJuPiPsrKDEzaDePhJtJtq", - "LehtRTWzzkA1zFCtKsrFaK1KUd9dw3dcev", - "LTV7ndtzN3N51GJkvHbePbibwRHQpazpQU", - "LTysFerupYwh6Qb34WFieDArbtyXuUTDQK", - "LNopEK2qHxsgfJmsPhJbTXQuBzmBNVdADd", - "LMLpvLzf28BJgP75ydLw6NHUMW7svpKa6z", - "LedjHgZKzmeGp2ehx3bTTmRjGH4dDztcue", - "LRTUoFmcs47RCcuDPc1wfAJwawCx7DGhH6", - "LM82cAWrNBeEUmBDZiF8TV3yaa3SLdQm4g", - "LZFvxgQrSb7rDX8tcy3pA3vh8cRSAxvaHt", - "LMCaFyi2TXjv3CRddbt2Ha5eX3xEC5TBne", - "Ld2rEdNcSQRGJQwBnof6Ka9jTXKx6e1PUi", - "LYLCh29WQdSSLoHWj4b71th8MZUWG79QfZ", - "LSFy1ACArGPRD9YZEmcNMHxLDmnoPyTpes", - "LPptdyRYSBtGstDYgjwijD8St4MfSCfeSb", - "LWkNyMGdTs34uGbEXYxCQV4uhm28VQoikJ", - "LfzTvK6yahXTDmgVMgM2XHpodqJySnwAq3", - "LaxirQz79hC6BEWGzeNo2EvprUE7zW6TR7", - "LQuWoruvtJ6kdm2rAg6UotNhJJfQcgE3Za", - "LiKpsruQgtbVz1isYzEd2kCDezufNSY21R", - "LiL2MAtuo5DEwsLtgm3oqK7asQVuxmRGkD", - "LKrMYBPBwnQAibxin47PHXKVtp8FdWTWep", - "LQDGoqJT71et48KtZHw1M3TWt5avB9ZXX9", - "Lai2oCdmNfPBLmdEq8Q3G1KEwazqGBb9JL", - "LYTUbJDrrWSauohGCzpxkDfJ6XshbzSvYw", - "LPPNcDbgNVwQKak6pgHMnK6i9EdmBQejRT", - "LLX3dw4EEYL3DNH2eMGoXxumJFJjdxyxux", - "LfNkW8BozxTraZRFQ9kVRZYopv3E2JNnbB", - "LTbDScAZKeL5cnbrECSM7J2CEPGj8Taezp", - "LWX8kRsA5xwSRFVHKKbuXY7V5KHczcvQxH", - "LeoXAWCsYJB3FcYmhw9wQhDohKkBLjiDUA", - "LcNSzD66uvEAP31gcS5mcv1wyZ5ffsEzT2", - "LRkv8FEThwrQkxpf3jFkv8hY4Q3vonwwMW", - "Lgr7QtrNgKrS4htZyhmSp6S6C4jDX4d65W", - "LeJ54WUpjx6tKpBMyFKZbqcWFMAZqUUkKC", - "Li7b8UcA7jp7EMgQ8oNsk6JbV76XV6BoXr", - "LeadTzRiEFVBjjXNJwb3T7AW8ywNkEVLV4", - "LQHteShBs88McZVeWsdyqztHLqBGxdf8Yv", - "LVh7GPmjryAyxxEu5i2rFceAjEeAj9EP79", - "LS4d4NxrqeTUBjVcsaTLYtFHoPWLe2x7ir", - "LTeEzsHxdrMtE2wJdLnDtmJVgJD17U8bem", - "LhZREEZF66XgJFzgAnyThSBbSeoKbdMHBj", - "LapLnKbab5Rt2Twnz9whwsQN1NKnbTETwp", - "LdwcMyu9ygA1zfmDNPgLDVxG9AtQeJCdXV", - "LT6DRkw6XcyXf9pHjqCFYC8zic9DATfE1M", - "LPMVMYFLER425fqpBRghidN65MbzV36V52", - "LNegG5NGKkjTxR4V96qJFuEf7CKkDD5xgz", - "LSg3mgY3FqnvjoEWLTbc46hFPQLMtPCapg", - "LUC3B4C7uVaeScH26xWS4xBe62dv9P7P4k", - "LVPrbqT2JesNv8thx7TXNLd85WNBFyoDxW", - "Lg2SDwobLEhMGsxj1AUdV9AYBskz4a1f5N", - "LLZawqtAeQZpqCwfQVDrJnaPvTxL7RsZhR", - "LSBe3yw2mcrKYkx53yD8daJs3vLBoinCzi", - "LUYp26MjLmWJ5SWaHpccdTx9SYatEjCRhc", - "LejgVcq9oDZZrspvKnrkgeWCfMXGTQ8XGE", - "Lf47jyLKhphuoeuhQaBpy8BSDtP2b8FX46", - "LacqTKQcZZVHXamrtyC98BiMUqGFjre2jJ", - "LQg33sm4kgBza6u1oTmmQC2gDQzi8xHQtp", - "LhQSED2BB5eD2nxSnBuH3cSgtpGRE9Lr6T", - "Lgyym4cr9jyZ6DuuawsH4vU5nGfFJo46hR", - "LRUQb4QTQoyHqGnBtmxmGUmdmPEiBDaHFp", - "LXa7DzWMdrT1ZiPerKkJwxZPs8KGGjotNi", - "LPFvTTZGyPWTzWBniUeabT6PpX6Zzi9D6y", - "LevYJS22KHCWb43JbyBHtQC2ktEt1DnB4M", - "LRLD1D1eHEeEZLWEkq6usamBtkRFMoMQwK", - "LeVxtaGA6CM7m6aC29wZ3hhVqU7QwpiDkL", - "LTMFcs6J2DrhSCRoVJ4VjS4XsbAjtyLFtt", - "LSs69ihmYuq9ZFaZYY64dybDvcRKJ3HT1w", - "LLV9CR2EB97VojFE3QG3sUTswxNLJZ5Zzz", - "LNdPmx17qQ5AfMcThrSKxgg4ats45NSixj", - "LdCBn39uDtnyMUaQrRK8J6M2FvaYoxNvYW", - "LViEH78zotsf5NFSHC6rM1TT7bVXnkQmEK", - "LRwMVqKphLBydypR7xkTFvb5aBibgLcQUq", - "LYsywU9zXUUkmdWajphpEwF2eiL3ERC3Ss", - "Lf22WxRh8MgotGj6QNtsmwBjTi412XedCz", - "LYDum7F3teu4MPfN5TT8sxts7Dq4U2ztmE", - "LbYwshK9qHevNBGAR9Hrdj2ujB5vRq9A5r", - "LP92zovmBBPyfa6EffCWuToXAyDtSjkyNx", - "LLyJ4cDBiJ7mZyi7BFVePC8sKr8151ZLKg", - "LeFScySfBCoWSgvrzkTiUaajX7BHbJ89KQ", - "LTJLMc4yHHg3Gtxwcud5G4bsHgeKQFVTkd", - "LZoMaH46kzYykGStsqETL1gECWAxReWqAy", - "Lag9gsk5Wu9nnyVTLGex9hgKVVv8mBMdia", - "LX8ouPBgk3HemjYTREseKXEZCXuhcRcwZg", - "LRa7BNU9G5wjsE2KxoG5EAg3pWs6wfq4pt", - "LiYjHFqzLsqKvZx7KCCH1VKYV8p2ckbM3J", - "Lfh9oL7amDoxWUZfh1YyN38Dmij1vSYEnm", - "Lb7bTy3kdjs4EiEEAL8iwWgcZ4Be6bPtw6", - "LZULtWkQfjRpSLT1LigDM8126bGtDZQyZk", - "LfMuKSyVh5J38GY59SWswG8prCAucpkcGh", - "LeTnaxgTXm84b17EwJmgovW6t9RADsLiQJ", - "LN4VtcfdHTYmoacP1tpfP1w2uW7gRuhTY1", - "LYQDSnr6hMBKd6x1jky9gPA5NePEdTKMd1", - "LSk8DivGMiXGUy4NqvyKgXDUz7tXi7M6Mg", - "LTuNGVBmKgfdoc13ep6xkQq2qBdAWPcs6L", - "LR3fRTDAGuCq2wJqyQBTuKzEh9TqUo889t", - "LiQHGxYYQwbCZunneqavSmsKfAy75rRpje", - "LSf9xF1KKvWghwc59dHhKg4qdDDQLMe2Ry", - "LPt1FjgHvKzG1apoxvizRp2aTvvpuvbNpz", - "LULUas7LEucL5NCKo85ZoqGwqSWXqfKPAC", - "LddCzGVTs3gC3apfXxHMJDt7DN8Ay2d4z7", - "LRXqDsB9QVAe7JmaeFZxTV5yygQNsTPqhJ", - "LNXY9gjCUANfL565MvBGyB9o1CkkqPxDJA", - "LceXAS6mEK3fyXX9tdbxLREbzZDJGnkrZg", - "Ldo7eHSWqooZXNYfoSKB1gKPWauXGBwAvQ", - "LcqWVgb24TQyD8AQR61qGjahj9Uwys7oqM", - "LULdzyN1WW9BDdTc8hCVS6XHUtUVFfH8XD", - "Le17ydzLqYMoFzyUuVBSszrFMV3qYeiPcH", - "LPbCNLF2QgdLYkrQMNx1E3a46Sm789uGeB", - "LXLap7XRPWGGDV5NW4kAtj9LATqsXmJYbt", - "LYoGvagUXBTkyhxK23tuzqXmkLgE57Coep", - "LfWdzyNfwjEhbJCi2sg9j35RpDXFuqLdyP", - "LTgDa6Xsg63kvZ9pH2wYwDCqesXo5drk2J", - "LYDVpjoYvyEGEtQSzwAssuycoS7VEHGnaS", - "LXv9YPQGQQxxHwtXY6kvfABzo85A6pB6oN", - "LUNRSQeq19vnFfpyTa95RuA9UanSJ8KdUo", - "LbfVboAyyKXQTF6i6hLyoNzKwsRqKFVpyf", - "LN4f5Pg5dShTvTEZ1jXHfZKRuCrSeQHCfJ", - "LgED17g1BsjGx76yPSWaAXYTFf9Mo81L1t", - "LcRCefLQqwHrySie6WYDRwj1iGuHez9wU5", - "Lg2ngZXMJbBYW6RZ5SBRL9w8bZtVQJWQFg", - "LZP613qoCYLDMmVMnefw5RuSt9L4o1YkfR", - "LP3sW4vxFPuyTJC4kw7hhEKqa3pMAjfpYE", - "LhVMmYc8UZHj7KSKPSUsVJyP1GyKff3hgU", - "LPzPycHukP5J95jjiUx6vYi2FhRM95TPi8", - "LbenvZKkJwhCULwRCCuGgqR26rFMZE8Tzd", - "LXqajRBNYWChTpbSoCd2mVtY7gzfzQjPnz", - "La8om9H3kVzt1KCKgp11dURgxCaTqLj3vJ", - "LNVPL3UmQrdbr5uybGVco57Lr6jx6Gqa79", - "LZQ5nJckGbMk33UMob4eoJgi4JJFb44XQz", - "LM5zsEnehjipDxhdNbsX3umTNJtvGgvthB", - "LSTDwxFQki1PSRmhBYk3gZr2qPLbKcPKat", - "LXzW1U8Hk2YKyh2ecTGvxthHocLra2UJDQ", - "LS7sLiEsVasKwbtoidXGudnJB5fHDBNYG5", - "LLz58af37RiZrik2uDPmpc2RQJyNJ1oj37", - "LRLMow7MYFSoQMpfqka5PhJT3Z9k6WqfWb", - "LTbDijjjSUuFkVvVABYdkBrDtRpEsuCMcw", - "LaV9GnFJykDwaUWn9YVRCiSDtUt5GVpvPa", - "LUYJZ9UpqCayVMx7hTyC9UmF9rpEiH3Zmn", - "LgPff5EGamXXnfzHT3t9b3PEqcyRwCRtPK", - "LW4mo41eaLtGg29UzvqNnUqc6yYw6HevXb", - "LgfysTaFcMehG264PmjC6rFbC2PvuSxUs9", - "Ld73tbj9WQokQfyLFeVr7Re8XeWW47FPu2", - "LcAUmE141s87Z5AmDvJjqWvt1Uyqp63onZ", - "LZuXCswGsQQBo3nQaPm9e621GDa7hNPQxj", - "LNc4FeoogJW81pTJoVDjXiBSYsbqdiP4dz", - "LL2prhA3kDjtV7HoTp5hvaotDT8BU6BESi", - "LaQBUxvRXfJSEbpnf49dxqhCeSEVKm2gsz", - "LTmX4Mv4bScp8z1FgW6mEtQavN1c86pT2N", - "LTMWeFwGFtNUHwKvRbReoRSRBRDerNRN7u", - "LLQcxwRu3DR2VTue5oEJL83RbLEh2tML4i", - "LgymiHL6v8pJELfkFRqnWcmQGqpA7T7Zmw", - "LNijHxqraiYvkw1C3CXLvTDJopTWKDb2q9", - "LcPvRMUBNhciUj7ZDinmNMvd2mRDZ5f8gC", - "Lb3XSRYhobeyGGuHyN5uPtzyPcakvhQHWH", - "LbWLi2fZvSbNUdpnotbnfp3q2iKmapuDF6", - "LVeGB8SrXQSF1aD3tB4BKjpbf3mdBRTXtu", - "LMEWoLtwXBhECEhWnpz5frV71EuehX2zHv", - "LebnT4ApLCju1qvxCrJJsP6u7Jrdt3YEZu", - "LQSx5N8jfdykN13tzo3d23HqG64f1bWon6", - "LPk1KKuwH1S9vtyf7HM3qP78UoEHrAnEXX", - "LU9CaMPAgco9wRxc6Wm3FYkuFEzhYX65Ax", - "LSM3bSPAhW9HN5FtRn2Ea9DLKRayQyQx8B", - "LUgSMFvNQzjusUna2Hcz9p75bpEfS9kTZh", - "LesW6eRsSEnGRV9BJjuoDQL8GzWWoNGZek", - "LaCMX8KkRWcYknMMzTrrB8DxcoY6x6XJQt", - "Ld1DqSmpp88VfYX53zo2wZVs2wdTSQj2cW", - "LaT73qYEQ5LyK3YcSFjenQP6MxJMqTLqBc", - "LPMo7n3E4TzPBryFFoav1zfwaTqEU7fkDh", - "LZTefH97XnNiN2cFVUS5PBag9hqHStFBWq", - "LdyN2MJBUfHGS7AHJ2RAnhhK8vru3EirU9", - "LgEsMgND6pxSiwGWtZyETh7aMniTUHUm2M", - "LLPeDekSGQPpfRqQZXfyHa9jgvGqsihuG5", - "LeYgKYNJqdLyD7Jh5EygCSVxsunTVKpiVC", - "LU8fbEyGN4H6gn7kmZDyZPvJdA74LxcVKY", - "LP6TfYe2nRegNzVunREnLxapK7j24HFcws", - "LTJL5QrbrR3zQQuiPVmfPba57edCQtHUp8", - "LWdpr75az3i2dhYGdELRNjcDdG1iiAb1gn", - "LXgkzBRxojgFrvTXm2qeFXN7Dr75rr6jfy", - "LUxero4pkex6r4a4d4JAbJBP3eZVj75F1d", - "LdZKaRExyPhkYPeCZHwDgyX8zoZQJkoUPf", - "LZYY5aHWStB76pPXBtZtkRSUgpFgnBBKQs", - "LdACoqRaJTgxmcPPVLnQpj81mrfvE51u8K", - "LVz1yNKuRyeynTWKtjvhS8bhi8CHtEnzjs", - "LWaVX89EdNksyEs426eyez566uP379jvAW", - "LQPdTLAvpAndXopptBQ2EJDkNkswusweEx", - "LcvjRsJ2KtzbCVCq7dUXZr2cBCmPdVK3hj", - "LV8Mh3KS6EhJhHdDU4b7ZbG4PjkxhiJErV", - "LYJgrdQUV6rUQd31o2oHyeLAJ4yp6R3eXP", - "Lck3dU9y9wyJD2fSfK2dqb8fDwpQRWju4d", - "LS4rnegqmi1i4Pb3N2EPkvP7za8e2vR4gz", - "LTLvVRGGH2yMFmF9u9Mz7Kkqq2AwdJEFLi", - "LKLrHVjWYat4oTLhshmYEsRiCfUspcQR7u", - "LaEyfX2Pd29mXGT6iGzqnrr7uhq1UYZAHm", - "LZwXMbe9qtRWiuNtph8HcALSDzkmfWW6bV", - "LRbqTUWUR2fEJHSgRbJVjFoq52v2C1MUgE", - "LQcGFesvduneESv7HEtkfymaXeH7PJJgC9", - "LQBHTKspLyrqQ3hnCyMr1q2mk4RrT1BFKH", - "LNGjuBnVciDGUTxFQC2bB5gvFE3YqPzXRr", - "Lh2kgp3553BcvCHUsxYjUvrotjYHRgjXt4", - "LhsEHCMaAzGSyvZpCj2V4Wp1HjcwMah7tU", - "LfmRHjSNDXaEmLHEwz59VCUxYfYsh7u5kF", - "LThrqXBHdD1x3uby5J5PCUTWQTVshmdwxa", - "LMHUdMwpeziokMgKSWsuVeLKQryBYtCfZB", - "LgcRVwTKk9jYyvp2DQTsQQxyoyhVphrdVK", - "LdrXpd88kH2v1BWwJtaxohk8LRnr2TEHNm", - "LfC5NbmSeZYmiKvcQKrmz14msUAKH8ZrMk", - "Li3fNhb2ASsoK9jKJZaWs7EBF4XbZbWAv7", - "LhENViKDr1eDcCqEUC7ieQtRkydYABFq2r", - "LUyueRX77ZDtVgXD6G9FWjyRikeUA2Tx2Z", - "LYLudxM8vGRcQaxcQHRn8DzdLj7AAD68qx", - "LfDK8SGgD7aKEEB62iP3Yd82SvtFfip5uF", - "LQpdEHvZKFQF4rCUTy5SnqH384pvvZ3Y5i", - "LdfMWGVYEdDtyR2GHy8Lggz95dQPSaTZBD", - "LdZqGe9HG8Z4jv8oY3fM6DFWVYXyaEoYnT", - "Lg9X3fqua5Qvq5npLAXDfzF24n7jsduFut", - "LZvx36km8ayxZBZkTUDTm4FiDGF5gybJxt", - "LNHE1ejNH26YmU87epxg8SHLg65mrBDeQT", - "LQKSBKX9JwzQgM7EmhgmioYt9LL4N9xo3a", - "LYBz9R4vX3TPSTeoYsKPmEJAAYNyFPQUdJ", - "LUG12kwRamFNEvuWd9X2sFPrLBEKxGDDUC", - "LQZW8XVnYK5e8P9met5drCTj5dpnvkXUDf", - "LXEHZe4KwTfVfS7gwoUVuWsKtSHcsj1fMg", - "LahNS2CDM5skx3AVDU3PqeZKUc3ejsZJEB", - "LL9ztroxHkHEM3AfaaRnWR4GyBmndAQN8v", - "LT8wtacMnb5c6uUjznGgTFAfaaFYmx51AG", - "LZjUrPkpg93F2UUdCXEekTEjBjx8m9sitJ", - "LUC1TrjuMShNaYssEFVLhpBiUaL2sMjpZx", - "LaC2ihRQanZS9LogNNMTAuu8HLdz2uFZHf", - "LfjTwoa3QDxSGMVR4yCkUJ7QV4QXtxswqX", - "LQwd3fFW6k5VoZ6Bq49hTeVBzfQZCzBWEU", - "LPwJsArmye5XaM9xwmoufxPfB23vBxJcm3", - "LS8TFTCeLdB8qxzp5nYF55XHsz2XzRNxup", - "LLy4pPWGkorvB2Qz8wB5tfpP5tzmcu6bxM", - "Lbv5z8qMXWyu9ze3jQiv7viCESorzsZcXG", - "LQcRZZr7EHboLGMSW1VmDvcjCGM99FG4fn", - "LTg9TwJ7XZaawgmpCrVxEvvdBRSe2rtmDU", - "LNkUBp9keeahZzUdzSohNvG3WB5U8NujhJ", - "LKGqws48yQFuToQdGcfK4prxVmTTRzxTrX", - "Ldv7f7KUtb3Gz1zMm3FXS9LSuxx5fVefxg", - "LKJRF3ViVB5uPw1EebUkAVeA11HcMCT1L1", - "LaEEMDkcxr5eKuSf8dPNR7VH3i4aSKESbE", - "LdbUTpAvr62BTB5GnrDHdMfkwe1q61TGUW", - "LZrETjveMnQPzYZpbRW9u7DxoVTT3tLVAq", - "LfDZFiqTJDG6pWC9NwD79sr7ig6XJaSdsx", - "LcVE6S6o4bBMgZ44UM8pUP9yDWJe5y44TP", - "LN12xLfR1riFERGWD2ehTVXV2SgTPEGkda", - "Lb9MtyqceeU8myfnDjopRjtMNPDCdkdYNw", - "LWaf4t66rYFyrwevvSxXUUf1ghXeevwnrF", - "LXqk4DgW94Jj8Ne4w7TVS2cbrDGzFxZNxU", - "LhiFe4iNvqdFFySfCaKcz8LUTDtQsCpsW1", - "LPSSyTfct4eky7bdGaBTpdy3hztmMqMXFZ", - "LgcXMbZDRSE3EigmdkpPJJGrCzuEHVki5T", - "LPApGJ4vcYwJ3PqfrMcCstNjcZ3AZdJWoq", - "LNx8vM4HSHySnZS9FaLP7frerU1b5EXGsB", - "LQK4kcFKcGWJM6dGk2rjPS6SroH2HFfi1C", - "LPco5V5H2FRzqvE3RyA92XdkPKMvwJpNuP", - "LayMbyFpKVCtQ9nFccxcfhBya6qVQhfhbu", - "LiPzSX6U6Yn7EhGPwfEYdjhxyGnUtQQGJZ", - "LNxATzDKeGGt2EhyA8LwTEXoYjPv5iTXE8", - "LUYBZfFzXMNcMaFvgeQT58DgSYmf97zbMF", - "LMbym2ssdxWSdSQ3mbniW8cXhjigeXD5Kc", - "LhUTs87noKSP4TbPLcBddGmFSZpJNAwoqP", - "LSA2jhkmZgwZWBJfdWmdM86QkKAfrYMTP4", - "LYbWxSsJKzi24EGjz1i586XwickyAMBHEo", - "LL5U5BbErTwAxHkNhzgpPpknbgdutAbJfa", - "LSQ1RLD3PtazaMAi1uFte1f2iS5AhGmmTE", - "LMHMqXh1ksc2yU1L7kmSwhmVPLe6n3dWLC", - "LREYqFbYo1ZpmZHagSxGRsYneUJbkStZwG", - "Lf6jQQ2xXF87TsyqpYMPynTF39e4DpnsbS", - "LgGKHMPigMHGTnCFtRbma6nHEs52TDiZkR", - "LSyEah2pmQpKWHW1MsPkBUukv3JwbLFbp7", - "LPMwBp8Mb6o6332oYNF7V2BHxtPcB6xNvV", - "LUAzns7WnqjVRnqjh5Zu6XqjhbzYDbL8jL", - "LLheFGLodSJT7A45C7me94sp8BzcnbtU8X", - "LSXUFKZTicrw9LjdxE8v6RLyDgpaqEQXT8", - "LeUHN9Y1Qe6TdRX3nU7yLpEG1EKSQ7Bzvr", - "LKkw5wELgeaF371xzrUwYriJGu4Enpoe2A", - "LVnRUBuZJ4pWZMUJJevEZHaCqqZvKayZcj", - "LMZMDKE53tZa81MbHW61Lqnx8UcCFQrWdK", - "LS5meyubrb6gpnh7ScfANA5EA6TYE3hgAG", - "LaWf86KLNBi7xtonjoxNusr7dTYJM3Apsb", - "LdWAisbV4u7ufsvZc95AV6nbedkqcDSztz", - "LLewMMzTp9t6buyCxxN1GJKwviThwQuZF9", - "Ldscfk9QZhVgTxcTrpqpPfuh4d13PBk5LX", - "LcUkF9tHB2de1jvv9XWicqCSJH3boGuvrB", - "LVuh6heCKcHgQAdAcQeRa3BHNkYB2SN1sB", - "LbzcWGkCztHWSHrCWZGEK7gBwuCEYBS36F", - "LhTWof4eFXp8b9fiGXBNChVv7F6cCUqzcp", - "LbR3mpr1aYMsRBV35Rrd5tab6CSt1Kok88", - "LYN8VrfVT2KKNECnDXzMrmMsBXTKqT8qUD", - "LPdALGaG13ZDTyjos7ZzZnFKxwnoiq7Vvx", - "LLYNyWwRGLZtbhvVtwE9KaVgqNezmU834F", - "LRQgcZZDGA4ArbSY3t7dDMJQiCC7fm6Ngw", - "LNcWTb8vVNtVcdt8ePdVYtKuceywXGexnd", - "LfZygy36TzCJcLB15gj6iUPF2PqEKkqfjn", - "LY6b1KJ3PAoYEJG62FPsVygPr1Nt4dLxJu", - "LMpzSmo3xMdQu51gxD7AhiFfTdefjUGLcr", - "LePET2HXDooy9HV5XPCfyJb4dSz7btw7tc", - "LSF2ukWRZ6KzXTHYSdDPPVBGkE7QvpyCN3", - "LVNaL3VNted2bwyXkopvm3MSi7BeGNSgc5", - "LeJcMr4q6QqoV3ZTvi4uFxjvopd4Speu52", - "LX7LDTBPhr43pNmpALrVSpZuq3qp1WiDiW", - "LbGqdgDvKayibqwwWBUbc4f6HD8xkns36n", - "LL23kEmGYb1K3CRm1zLSjEfb5pHHBbh1qN", - "LWcz95wSBjuR3AGywLowEmFNFcuVvGU5Qr", - "LKs9Dy2fTeVGunYJXBEqdxxmxCxanG1KSM", - "LWAR63677sTMBh7CwGUgLF6xHrH83ybGgQ", - "LSURJv7fyEnibxuU5c9CCQpjZz4FyEVoTB", - "LeFrisey4bwdBXEPBZUnvsyhvp5R6HxjbN", - "LMhauutxdaTBvNBWt5Z3XVhGE72h56m3Y8", - "LhuS4qaCekfrsac8dW2fsD4pLQm9rf7AkU", - "LcQABUJC14zkbVKvgYKYAyvaDbcaEs6aio", - "LXPvebPoLED5cVyF5WtkVfX3wkXnQvtBFc", - "LQ6VBRdhtXYGsuJz8gn3TEJ94G7tRjvsnj", - "LTY8VWCMTLpkEwMu7GvXJe6Z9G6fytkJBQ", - "LRDR9HF7TmC3W7Z4hCFC8Frwou4qvs5MEG", - "LhGcqqfHd9VPDXVLjnkcisayWqZ7mMWExG", - "LcMMWNyc1MkwZzC896Yf8s3w54XEKjHJRk", - "LQMzwTguqtKaNxGCKQQBf3TG82UHwuhCet", - "LNcbiXmJH518LtazqSsYRNxzKSntJUrwxr", - "LMvJZzJAMShWmGhbqfbcYvFjjkkbjNBtuE", - "LRknefn3RKyVxwKkjdum9us6jerbRUUEYa", - "LSyXCZdZZ6JFQx2eFKzVsx8fCjajshKT2S", - "LMHGHtU5fasRisPz5Xd1qSZR5X1JpSME1S", - "LNodSYeXtiLWvakGXrP7wHsfu1veicrBp7", - "LhJXTJpxjUPV5yqqW9UsMzxM5dMa7WdVuE", - "LQ3GjdVVnbBoZmHGupZmNJYB1rdhACRm73", - "LbqpU58UNaUh6QptH4a325fEFHibXtisEi", - "LeLiUyNTXYV53rJzHBDL9MCeZwHcYg6oer", - "LQfCDij1hUNhUyQjmjEFiS3awGshk81W1j", - "LWES5WWwType47rjoqHTQ3cxhDtn7UKS17", - "LNe2VSdTEW5tWyCsHBtQi7ZEzvqwV6AC6F", - "LP4j37grd1NTRqdsNMVvPhH3YVPxRey6cd", - "LaN2VnxNMswM2gzJP5JBc49UUvNW2hQM2t", - "LZFVajmyg5S4GNmVxZ8w96uUoCuTk49SZb", - "LaCXjjfV7yvWFuoxq4MbaAA26HmDZ8ygnJ", - "LgKanQNSNYEKcx9mMUXGiZEe4sGvDsfEf2", - "LZyL3cG9DFZPShwPNrY5A7bFFC9REDz7nX", - "LUJMcEyxWi4H1t3VJm5p4MvtRNQwJMAkaq", - "LbJdtCU9BwuLadLQrsGw5LXQTw1cofCQUo", - "Lcozgv1ePhxZJmtPhXYUM4mAqBsgrixfJL", - "LT1nhGV3ugGUsuQ5S3P7hamuj8FG1XzXEP", - "LRnuLSyDwEBUwN8Zjv1iM9f2PdSrJaXkiv", - "LXDfRLDT4aSaD59HBFiDLVoDsKzbKHvEG7", - "LYNKSRZU5NzsJ91k8F5d8w9n3wTXRmESco", - "LT7wT3iqV9VBfQUyETdc22YgmuYPpsz92L", - "LR5NkVi57qqNmxBTgNXpaLMyF2R1rSWfdv", - "LR7vwew7NMGS5TwY7nmxjENY3Tw1pAEpBn", - "LRQY6WqBwZPXH6L4HXQSZCLuPZKLoR2A37", - "LPjTYuZumxSp9L8o1PZvpnNJ12iiDrJzpF", - "LchWtmeyR2sSPioQ5uDvj5zHqgRiPuSnx5", - "LR3TKpkSdQLpc9uBfJJhshkc5YQWaeXzzA", - "Lf6kkRPHqxXPtURWyhV3NrSMQhvJxHeFMM", - "Lh3hMVpf5cW5iMs1a5ZSH7wamdj7Cu7A4J", - "Lh1d9VCvuJTH7zankijKBQCHT2TBTS84rQ", - "Ld8GhZGMzsvWrrrLRdt1vK4jQ17swUKVc6", - "LUZnNT1Yxu5weCP1xYmUQoXxJ2yJjNwvyB", - "LYuvKNFjNhXUd4PxRfwBkB4mLwt8SwM1og", - "LaR2Zgy44XV3ZfqwjjL9ZBXJjMdL78LKC5", - "LZssXv5uSuictUyhXtXFmPJT3i7r8WTaFd", - "LbVLwA5kueFzQtCvjLWZcicSACTwfpt8pK", - "Li6QhA1JwQRdooVYWQmqNY7ajapPNiEYpV", - "LeZLuU9H2YoSUcnf8oFZCoGfsnv1PpScgA", - "LZCwXzfqSZWSqUHhZ8ZvQDRA54TdwHkzm4", - "Lfw2PmMhY1s9kSmfGid6wRY4KeswmuT1mT", - "LgZHB8N4sj1ajAqntXEvfZC46pCVfpNWXC", - "LT9CRh2GCidjVSB9yDzhAYR9o29JMzFV52", - "LhApBvac5mtAoeRhyeM1UHBAKD6t5o337g", - "LffTmaekgdvzKBZarsTzQoooictqjyRfZ9", - "LhGbWvFi46d4RmuGZZDedbpeK5cpmDVKjK", - "LaDBbkQG2eg3oTGcJJcUbSqYujEWD8GxGV", - "LSBeVyUmAomavuAaM3NRAcWqBJSCMZkma3", - "LaArmQWVYATX1RqLfcHTuiEKPy1uAUK3So", - "LaqS7mm5khUqsMcoH29pcaxJnMTZy4h3VP", - "LPV77htAh4RvvJnK2poWuBJA1XKU5UuYn6", - "LbtB14P8bSDL7BBS5u9C7QzfSbLXSMuWQ6", - "LeTFdUsPNG9DpFzmxqdNzcRXWiUhVBFSje", - "Lap9Zp3RjjiasRKZ6HVh9PrrxTnbEzmQQ1", - "LRJFh8Gem1G27ZER5RDqjaEqMknkDAnL9H", - "LXPdXPJ2ejvQEUuKii2XmKpcwWGKReQ1AF", - "LV4EBa9dDLAVrnoP1ZJ9uPZ21QSnGsyng5", - "LMTQpVaabmQ4GJRhya5UdDbBYwCnnrw9j2", - "LhomQHA6tTsLcPEmm9XyDBKTUvmZPEGCw2", - "LV6zkyyQ4w6MTstKHGUTvp147qF6tDK4Hv", - "LZ7JjSm2ipzdDSS67TcDz6ypYB4uRRh5qb", - "LKmkBiyWJcrk2qsGpDLSpENU9VmFUhTFLb", - "LcfSw6juAquu7ELptLuWdGVkFeoCGBt7TT", - "LVVeiEtgxcAmZNR5ZS7EkDEuP1rSQ2JR2d", - "LdggQFKWBZWXBEhmXAa4jVqPvV626maE4A", - "Lb2Uf2AJHvMg5gHebHEnhEhLRnjtZ3AKKb", - "Ld9zAq3tJ3KfxcXoXPXmKPS3R5yzrcD1zt", - "LM9B7S8wn1oXUnPpYXPjQeWufnkNYbvXwb", - "LMRdhEeegHTXXwBSEmv3Gzc1fQ9m4bR7wH", - "LdhYdFtLHfnWrnBFCcXirMy6BUr8mapYxT", - "LWLCjJqo4M1XbYQSKrXfhfoUB4pZxMQsTT", - "LYM1ejQWpBhdkbEhmfJrQ2KXFyeB5U9uG4", - "LKkDW2qjDbR2aumXuWysSUxhPsyKsLYLLQ", - "LSiQ7Evxz4KmEpaWoMFsb4in99iiZqchXi", - "LgFdXHkjNNuRsnjvUFByj2y8RFKBwigRvv", - "LgmZeoFz3LFw3M6iH666sXs4efPPPFDFRn", - "LRwVbxqon6hhg4uKFFjsPdsRBcb6CuuY8N", - "LTW9LQ4m3jvpaJQqG9SywG2YGxTxc95XDS", - "LhHYXKgU4ediDQQiVKgFcDss1Faxj3dCwT", - "LhkNzhtVPqyTiM94eDT5jAGpk6rrjBWySD", - "LYQAfktqsZm9UXFT8Gbq7hxQHLXLvvY9Vw", - "LSWtD79xfEKQie9xkYpLq7GtgPRCosKtqr", - "LTfbfSNWmwayq8XcjVVvQ7L6RRv4YqhnXb", - "LcssJQC1XYDMTibztCZQmEk56tefgHWceJ", - "LWr6QzQCSbgCmfmiMXRPEDt3RjQL1b1h6v", - "LexyJFNrgcNnnFL1ty5mWvxbLhJvPQr6Ps", - "LPbSJPvBLwAsFSARdt1zEi8maLMmEG6su4", - "LcJUVq96kyxh4vYjQ2V1mLbRLad4oxckPu", - "LL7sb6UWoGaXZ3Q6Y81sff5ASdW7ziSZgC", - "LXP1QYfCQRdA4CdnUjUJdMxoK3RPCCV3G5", - "LRr5KW4SK2APodwB73UmJWzMJYKk5edKCb", - "LSZjBdRuru46fg8KT6eubsHPfibmbQNgSk", - "LVnGQcnFjRqXWKocqFycqthhejNrDUb6ns", - "Lfmjw7wfBTtKYxeF6yYcg9VFp4yyoCQzVQ", - "LTeFKRndXEA3DDjVENvqmwFFWYarRtScxb", - "LdcV6oRJgYv9Qc52JkYCepnA5pW923Fygh", - "LZ7BP6uy6VcbXa5vYeQwpiCHHADi7R3QE4", - "LhkatG3dDHy85RFmEpKuRv88YHJL7tx11e", - "Li9n2e2GYndUMgjT8YarNmptsqyy57nBCn", - "LLwKLVEKMse8WbFU3t5m6oqeq6yQzv6Vwm", - "LYLEJH4tTtDECUWTRkv2JNgGp2qnC83eS6", - "LPGBJqe867xF4tjgQwknzoVU6v2XpggdaS", - "Li1TcaUhEP4tkbudkWncfVCY1j1wMgPnS6", - "Lbg2zTeZBjm3pN4c2cmJ7Aa83GNgJPP5rk", - "LRYV3RzHidi7vSYqyGTco2cLuTvkKWjDZT", - "Ldv5gad84XcH6AntbxjGxZHzcygCzgsi3G", - "LQ3ghptDNPhd8Cf5N7kBrvTbd6wAnfVTqy", - "LX9UGoNbTQRQmfymnXvqLKgbTqPofo2ZQe", - "LSTizhmpK1xbmAB6ug729uLs5xp8dir99o", - "LaXB6PT7Ur5PfSeKQtizmeaQzfKf9ULHeo", - "Li24gKAxQGHhQMcTNGVFMPikKoGu5HoQNj", - "LZ2mXGuvoZJTvtYxsupvNdjMrUEfFVcs8b", - "LNWTCA42visTpGMMmKMs8EBG5P5QFYwmXw", - "LRMYix9UCn76pmB87daetkypi9rEA9BkcK", - "LZBUZ9WpmCr1ki5tWED6LPRgKtUqi6t3EG", - "LKcGMQghBrnAutuyB9oZryJzyxhnoW5qyA", - "LgNtTi7jnv2T7nVLtiLs7MXc9fUcZCM1PX", - "LRtL2QCJ885ndQRYcCyxHtohojNtQ3kwms", - "Lb3Q8crTsgPJVhF8hGocPadUG8L4Ko7Uk5", - "LVRgQ7MYCVQkGN96mywZDC7X4wVKnVScXL", - "LgFC8Azpm1QQg4zf1stvyhdmk1J3GKRXgg", - "LZyaVaUVx1S4R9RtvVPHmirBCxwYy5BLqy", - "LWqHKipQvgBJXFH1YY9Aqyvidz3JLsbNqu", - "LS4EQ4HUyMj15S7uh2owumLWarpMwzYWf2", - "LeRNyZ4chbEqkuDtfSqC47r6aMDnR1Gi72", - "LeVbPnsv8Kys7dcRaY3yg9infzmz7G9Hpo", - "LUEVtdDJ8X8jVdNiUEX1xvzVQxcsSZPkrz", - "LMas9g21ce1eyXByhr7Ngaygbfo45Q8N4Q", - "LiEwXax16TsBmJpjTe1w332TeyUSamnvvA", - "LeGQnoVFNRWT3tUJFo8SToE9KYy8Px939K", - "Lfq1tZYPSa6LJMWMCzEfPVjyMjnHAUPnwj", - "LhiG7enkC7hUoRVyDset6nG3jBmFAnxP76", - "LhbLG3xjaFjA16LxiWKiA3yz1UPuam2LiA", - "LgafdEoGQynfHUJZGHLVaQcrr69sHjDjVB", - "LWh4HzjkKUMvQYCmyEuiLhwEnzyedFGgRD", - "LhmNnMErWRvw9pBhEaGA1h71iBrxBe91Ef", - "LZdBrWEZYwevQWw5N7cfx4Hp2716tWEo2y", - "LYqypgTC2U1H7e4mWtmDQXEJuokF5SAcLB", - "LQF3JiSeVnhARcgCJHSNvzdqRvmJGsuzvL", - "LURqMivxZfqtWHaydfLtsUwpd76pjxBuke", - "LYvQksgCo4c9pR1id6dbKDwHPEETYMZXuf", - "LREa1oKedRNis7T7XJ3cNq9tBAXCtdcGiU", - "Ldk6o44cz6YxFNvmvYhhPficYgmLe5hT6d", - "Lbx8PM2ss9DcPYMmzZqFcyjyFaLJM6Cd4Q", - "Lc7TBKGbvX9HNsZGyhWWrXGCLzpEZ7cEdX", - "LQfigrdhHAVV26uNS3hCVRxJZtDxe3eVV6", - "LZLynLMfUvPVMAmNXF46LKeA8jfUmUBKxe", - "LWnKVg3QJta6oyMVNLo6htofgyRfupkA7Z", - "LKHnGKbzJX7e24QJL98f6ABtuG4MQjAX4Z", - "LKexRP5HvwdBLSxLG5FnB3QVXZqNqARFiK", - "LgMa3Az7WKhMYcA9HB9abzkZysdkihHy5p", - "LMQybwAsWNJCLhVukHdntHCZ4R6K7nqZJJ", - "LNEbx9Q5mvnkUdKKqBBjm3bVbvTKfBLm7U", - "LL73GoiYM6pz2RVMDLWDAQhyjf2QtEojsj", - "Lfzvumvksk5EyUTKiFJAcKubAUDq7iiDLR", - "LQfuDpy9FtxDuwH8KvV6v8U6CDEmLPCtLq", - "LXAwyHXJ5uXkEtJr8eERwWVDWmiTwZjDXZ", - "Lbh4tQwuELbkx5MSoNgeHPpLgkYgzwBqmN", - "LL8Sr3Pm1DMLSgjgE2tzTgaPsBn4tUBZMf", - "LaTtLUvoWMvjDgUtiu25HFcTXrq1B4ZZiR", - "LhhaJPMJGCK9zfoRqZV4FdvrqHrfBuZRtT", - "LQCko1GY68nozg5TSDYBMvTgah19GUdy9v", - "LiMQxVrAutJDdUoJYMn4HxPJU6MUJo7Wih", - "LTHE6Thf2TD3apc2k8bGe49uT8vk7Duumk", - "LN4HkCAKxkuQTd1ZxxG3MdUZL7h9956sLD", - "LggbiEPxm9q7qujBgH8VowJDz462EeocVe", - "Lai1PeadaURQGssF7wCJVkUCkMKVCct7xt", - "LSpBCFpxVR6rHNmQPF2Emj2biJ8jLHkwRW", - "LdBpRFJECJfRMbvXL1Jp6qVhpkR6orzeUt", - "LKN3hdMPteWLEXBQxi5pwXdeufFa83FwgN", - "LVJvfkYtMNKUdME8zmMAkNCnK8CPgx9hS3", - "LWfJsaBmH9ppZYP4q2tCtUmqe178f1HHw5", - "LMKE317FWXkzDMCTptsvPxHfCWSPsD2rDJ", - "LVsxLvnbCw2Veirdkm57WwJ1g6PezjQc63", - "LXLmZvvK19RiNLxUhAfmWbjQEjDr67bNyV", - "LPTrg77VfLw6sGa5FH31E8C9iepsxNzzDk", - "LLzSdkRuTkSBFVSjJjjMQiKrcMuCTsA28x", - "LfaJR6MJRJ6P8xoZK5bYo8ee2oZUETfeGu", - "LdRrMKNj4qLNpMXLbM53rLxAepF4baTpLe", - "LULuUSgDKn83sESXpTPZ2pMYkR4JVjQaBJ", - "LdoTfSDBuH83aCvWtL9sHwiFvUTXbYwf5Q", - "LfV2DPCW5m2WMGBGMhLUv8bqq4BwN37JP6", - "LXhYvad9n65E9FNeSGwqi7jHMQQnc1gKiQ", - "LMCDQqLdsHiXBwe6Qr5vmMnkg1M8TYU5XN", - "LiSTM5jNER3o6G1uTDosw5kVM1XvRBaY7g", - "LgRrD1BXY3JydbAiBz7DfdcurjnLpHyXJu", - "LPiPe2TxtCzs3gsiGK89urizhyRsd14j5E", - "LUbTszmejtBwnHMWTqofAvTAGSnUcyZhK8", - "LVgVwUBwP45kBjQLq5h221WYqqQbeLmLrY", - "LVqE67xxpsqahokAGYrnCbXEvvUdNgZhwe", - "LXw9igkHBKPN58f9UXRK1CAqoJSFXPr8yu", - "LhkjGCFtV1LbdisVmruFZnC6TdaXT1iD1y", - "LaQEaTTBKk8VudnfpGKKLt8fwu4UNDK7Fe" + "LNTpgLXprtecSEzztNDmGJpfDr7noC65db", + "LRFcxeV3AQ2zvuFtoiTyCYfSjUQLkEddAU", + "Lc14g9TT4yAhAfHqLaKanr8pgCPBVBksgM", + "LM7Jf8MSp8CB7nt2zYoSZuFeqiBZwn6TPi", + "LUdCaBTEFQjVaZeRafinzaUxWXHZiZcvCG", + "LMUjorgJBdhqdy4fdUHAkm6UryJXLtzaWz", + "LRs2qDU825ca8RurD5yysMEgors3YA9hPe", + "LegeUDssVAq5Z7Y8cx2b8dWHnb1DunMZzN", + "LhNHWWFwSG61PY9duCPN98XDsR8jgiQJRF", + "LPw2hdP6ZvkzWTGn8cJHjinPYCrPa6ZpfB", + "LecbTbQ72258tMyoKFvAPhTJfhdzQwVPFR", + "LPwdqFTxhEZAA5ZUpBmydVPL7NRVHRQeiH", + "LR26gQX2Z5ays6tvgbHUeP2CCUTbpEfufG", + "LZXyUvGc2N23iHaDWWkV7drnvbHEE9LBrZ", + "LVSWjf6A7CRnHRFV1YAGKVcqNunfvmgmE3", + "LaZVKJN1DK2XiujjenaJ2jGtu2UKKBur9f", + "Lht2wBTNf3z6pYN8WWRAZw5aMFWGdMfRwo", + "LMdhg8w5QaTpSQzt9kBmzyQBTKp9vgPrCb", + "LNRZqXKFZoUUBTF2RzumfHNAUcxLXahWTo", + "LKqcHm1EkmggfnDMsMY6mrTx7f8PNMkk1z", + "LUnRSgWNfabKFmDTC6EdGWCNRJmRApQ5wH", + "LdCULqXSmA4dR4hKrCYNd13Zpbb83kjbzS", + "LMFF7wwrdwmz4kxsvyF57u5HBs253DDBmT", + "LQryk76nxEUTipXPZMa9Q6h2sgSKe4Xmz8", + "LULo7XAFgpickNw9ZCtz99u9BTdjmiSFh3", + "LUkdgnTnPJ3JYaDKZz6h6DdGQExentjcG7", + "LfGf5o14NUGuQrVSEHTRNFHqMndZUacpnv", + "LdRfkuPH6SppfU3L7o9PQptmw92Yw4JfYb", + "LXrBKZoGpkuk5e2vZ7RMyDqvzNvjuHxcwu", + "LQNHBjKgpMdfJSnDFNUjF34V9xiAFkgWGA", + "LTjbuCtAwc33m61teukpnPyFmkvLi361oH", + "LYAdmeqiW9zYuuQKDrnJff14J8sUEGZYbU", + "LbReUXLecNu4PXoLfhJqqn8KCE4sLoKBWp", + "LNfwCeec9ux2cNLiu69r1r7omiyEGqqq7z", + "LaQZF3arAKViEhhQR2fbAdpxkpaodsYLxR", + "LezikXzZAHDhJMYAFpRbJYipWx2b5yM86r", + "LZSKM3miJU3wVtsav2PJeyywRXjU32Qfdd", + "LfQbiA2wN96n8mUq6S8jtGVFgUA5JZq4P3", + "LghnhiepXMm1ukyYUsAoQurd6fCv2Pdi9U", + "LNAmNuCMv34C42gsGw1okHXeAx7uxeDQN8", + "LPngfnTyCGWbNwRLo3LXZBrqeN2ktLFTPz", + "LSPnz6cppLDwSGPPA2wi4Xo7Fd6GfazYE7", + "LNu4gkrRYhcWuAtiiiMDrSee47qgQ8L7Q3", + "LP92pv1N8tJzRuc1D3Mqd5HqL9gtitu8Gf", + "LPtgpYRpBHKj55zyVqceBL25pLbkt3XV1H", + "LdstQc2vRERBUuferrsHutHrSyXc8Eduu3", + "Lh1mGh9F4Dcabyw4KLFibKzDA8YmdVGFLw", + "LWQ5ibuQdFvqcvfhrR343Sf5cB3HewwPJP", + "LgaXbedKCRRMLRFXtr6gxzqqGC3rswxi9p", + "LLqKb2SDpA9a3R4uryWLgzC2xYxs9sKPM1", + "LLWEBquiHFV8LGivZ3oVwm5b8arwedMNKz", + "LKHnkgsYrdr6QkASAhYp25GmS8RfF5Ynga", + "LUmahz5XqhGzXEfQ5G8re9sGcQojF7E2gH", + "LTZYrdv6HXRzLDBkqcvZT6CvbGFJ4eFchm", + "LVP6iAtFq2JLCE22ELWmRhRPSM4Zzr3RHs", + "LhRLJzGega9paT1QZoSTvNWfpN2jpu6qzc", + "LdAy5N1wVcrYngqV6UuTKWm4xc61SzLzJ3", + "LafonyLZFoj1teorrjrD6MKyoJbKBBN81R", + "LRgK3ttMmJGAXST8c5w4f9H3hAuHiMgCCk", + "LZDwAdTf3aMf3qYNQAuCkA8B3BiaANcixi", + "LM4e4QiHavGFhkguqVcQee5N5exmQZNFFr", + "LNQ97UjwKtEFsc4P9xEDwh8obNg15V4rcB", + "LXQkMaN7ehoK7KQuPYrDms94YYaUkyhAnR", + "LNFgvrFuVP4SXYmfhBHZ4J5GRzDHrrNhYu", + "Li9ARsaUWwjZXnTQRbTaz8WGQxpiwgDPLC", + "Lg96QoS75svudKyjRW3CW4cw32rBvutZbX", + "LYQiqNPKGHHMTV9qnjVv9WWtUhwnWvTQ6i", + "LWGYANGR7RQyiDmktgdrgz7RGFBCShxtd5", + "LZAiCMeZLzy1QGkwwoT6UuKPkvMqXHd8gz", + "LQ82FZ8GLj6bVavFinqFxvLNbjcrvPCxhS", + "LcofKwE78JwmMrBGvBvUcSmT5YmL5UcSRY", + "LTP7BUs5aBX7C4AHGrVAtLiqQpajmvUvqy", + "LgEsVHXPKQja43j1Rd3xE1peVjExpH2qdr", + "LTBXqH8AN41QbmMXPxotXHMSiqhZ2SGvce", + "LUqaaPcFpqZPhYbUs7uvRj5FVz6NEYiEwm", + "LSwu69aWjEzp6ALhyNWkwMqMyRX8d51dAw", + "LcfMfN1SaMGc4qfSR3Z4bYu9nyKE4ivpEk", + "LZjCL8xUKAuy83uvqPcnkMwLxRB2D1TYHU", + "LXzJfLdZ222RAwNeic7pgrNSkwPxkjVdUP", + "Lej7drV2kA6jvRkiVRrxDZvTfX1zyJAMVt", + "LdQoBbvGhHj5ySGRgVF6RKNUNE53LveNdc", + "LMWLPeJXYZ4VPWB8xpQ56gNSVDCgu6Qa11", + "Lhd7oV52q4AHGnNBXfxRpKU3av7aKN37F6", + "LLZutN6a2NYtLeosEqsKMd9N7Ca1pzAeTS", + "LWguMXkcw5rNRhJ8G2N7nyQzRivYEWKACy", + "LeoaenVAPAkD68xTBqvX9TeesyFMGYeWGG", + "LWr1sDHbDLzyKLkdfsLmRLCVctBAcejZdy", + "LcV3JXkPvF6LUaUi4CsjC7zQm4rKF2J3xx", + "LWTf3PEfQVTfKcHrw3NXgpRi6jxtG9P9mC", + "LZTm2LsVpqFJoiDxKTDamWtK75FjuG8sJS", + "LbdNDivoeGXPcTfq2g5fEEK4zK2m4cp5rc", + "LRT8aPfvk5yqsuAs2X5yza9SQ6GtbENUNo", + "Lh2ZRdHMt5cm2aVUD99o7diX99S4GtbkyK", + "LTTjcuMfedGAuJHL4W8uqxCZmbSANnH42x", + "LXX8N9GBnzeDzTQeZWCEm2xRrmqsJgb3bk", + "LMYYVufrxdQpMqiWdinyVYfmTzKha8suA7", + "LPWUpTu23Gjdjaeg23UmDLmyaeBthZQUbd", + "LRHquUoS73mG5Uj3VtQaVgNmtj77a78MD4", + "LVLmwgtkC2pnuptJy46JZmDJbw414DYYE4", + "LKR4yoQFtFGDn6L4Fb79G4bjaPNrnz1Xq7", + "LW5bvYkYF7vS5tZMBwbo9AwMTir6ywGR3S", + "LZXQ6RRPRGXmSeFagG1EQxf19LsESTHKeN", + "LL41uzfhWeeHZmRpbn2XuhDx9z4tdYBy8G", + "LL57dQUequqnXJCYBT8DFnVt9k188pnQb9", + "LV3AVsx39xauq8t77iN2xDhCmpLf5cmKAj", + "LUoyQcyqFtcmEaEm77fNhWFoDQW3NZmRQW", + "LParh4xAPDCNTUfQaWpb9FTiqin1TY29F7", + "LQBYnEUuw4Wyov5Snv3cbgcpxgoMASz6MT", + "LP1SimqRa9nJgDa1pQ5X2BUUgFbwhSr27T", + "LhnooDJzpsiBd9gBDavqLExWk7HCmPdfWk", + "LZUbTRvrcBbJTehG1yL9EhoHYFqBX9GY6u", + "Ldafj4ehAHY8khB1zhCqXeoxtfCqbX3xhN", + "Lbh9xiQGaNvC1mj3J5RQcrcQgMxCsLPui7", + "LbccvJV7BKYLJZTGBeJLNPjnPz7BKJnqzi", + "LbxaR312y3RkAi2dnxneA42mN9Zf7Y4qmL", + "LNJ5kQJL85HPUmAfuqV23k9jVbrYsqFqTV", + "LMgSwsYzXTTPQBh7jNkY8b5J8kS1RGY3bN", + "LNGsETiVjZTiewDwSx6MBjtQJ6cjxsv5kP", + "LdYux77EPviPYsgntTLSoh4EEcYmyGWtnQ", + "LXjaNYexsVg2cFRxb8JqatMGHMhwW5wtXX", + "LRQ7wHfHYuLzXQH5sQZXfc7ZmLUi9NLTxw", + "Ld8osEsH6XiZrJGtL8hcpNRrurqBvpf1Pr", + "Ld4HAEWDqBt8zn6xnppqUfWiW7jqs8qs8y", + "Le326jYzh2HFkKska2SriSGUJB2Wg5MxQh", + "LeHUeSb2gNDA1zzwJT4ZHwmMZRWWSeX7dN", + "LhrDhcG6qKy87wFxyC9pEVkFa2TxUKgVQB", + "LYK79wmjy3t2uP96N6GPbDMJcwM8hCJsX1", + "LY9Xow2RgxC2zfcawmPhk77zE32b4sgZG6", + "LfCG9qkGpAY5Wj7w5cNforfxTs7R8MgWTc", + "LR7FyfFoYoniwGarnsCXmN437i517QJL98", + "Lgwx6Fj5gsEZBpZHdUzoAfDa6UmTjb7sPr", + "LQMvS5eMYSFDa6yxj8hr7m4aPPiCPEVae2", + "LeMGFqhYNMfcx4HDBy1NSCWzLW7vAy3oJ1", + "Lce8zFao77eQUiD2bLJLieRN1gy3iXgR1U", + "LQ5F51XZVHFMr6sDoZL5YUSzaV4WLnEQHo", + "LKXBnp6sG9vqz2fQTQGsbcgfkTwHu7tvAT", + "LToTM5T9yY2RwwPSw6Si6a1g1juK5SwRCh", + "LeqPGow6kzcdxdUbeUL4AUTwnTaWLGfqwP", + "LTo82EctUtn6BoGab66tEttayxpzjusHff", + "LSRUCGARpgcS3Tnc7QWnqkCEkD43z5Fezd", + "Lgh9WpzeBnr4fCzuKY3VFe1Pejh5mYGvQH", + "LWgUo4CsXEr9CDUraginpK8jLteb4Cq5hq", + "LgKS4NmCKBUp54oxEUfi849hB3mTG1vi4P", + "LKnpyE5t1T7EURWxaL1q1rawe51MTq1ajQ", + "LT8fnKfEjfenPfLdKjKNScRFHmdxjXitEa", + "LPjpeXCf3WPhjpYYw2sZCSjNRfv5bHMTBU", + "LVkFzCU3rPLUmPjjjXi2sUtzxPsb1LKB4m", + "LcF4dwsXTdHjngZFq5A3AYnCfJhBvSJ62c", + "LdyVKgdUJg947nD2ig6awLwyAenmMAZycw", + "LKWNJcTNGJqvdFrRLGZgEuvvH4iRcsytKG", + "LX4Rn14HUtFPLtvQvV6hP48gQ8NpCavmJ3", + "LeJPuEgUH5MzUVcDGwwALYa9X2d6h8m1pY", + "LgRTrcgMPzLN1bojHMN7njFnBNPuDS5zjE", + "LWzr1uppdUef4gvJ1QE8jd1Sa2DZUfaEVc", + "LWKuK8anQcGLNV9nrnjduXbYvDRF4Fj3LZ", + "LdsJj1cq4QHg7pVcmbKzbozmiJxiYKuZfv", + "LRnN2DzvhdUCVzQCzCCwMgDspQCoeojzwb", + "LM2fxgnQTCnmyggy7KKhBXbJQStbVo5HeC", + "LNpNCPsNruz6fBUVkRFJTyh2kp2RaicfhM", + "LKnmkxPkDNynjvKzZi2mdwhy3LGs5dzry9", + "LRLz8rLbdog6wYJwka3CVKAcXFzxu9JxEA", + "LbnftHPQ9jP3r1gmNPdDoTsDUp7xfKBtbV", + "LSVsppscBXo4j3wCtikHP1UBeg3tQXxJc9", + "LiW9yYR7CvFxZs9YpTx2JgFtk3yYqegjDE", + "LXcUo1ko1jC2ycX5nvdZfsd2eyJSjws2W8", + "LfnmCHZ3ySVy1aaDwjzA1wcrqK9SoyxsnR", + "LTRwKGCtV3Q72WL9wRoNkAPxuVW8TN7jni", + "LPfhiBxWEmnk4o7qoBE32UXWkuxsJmTsw4", + "LPLnj2KYK36mnHhJAV67UqtPhhyEu87zbm", + "LbSvp2mP4EQpfmQQMZDWe48SyGhyDmKozs", + "LeBGoKBvaVfWneme3bmtCyrYi36QqaEQkB", + "LcNWHL4PyUnrQC1YHUjWPa9bi6mdxpysGw", + "LT8Wd1Yc7JSvkuWXP3Qg14vnzdKBc16WFt", + "LSRgVmauy3UuGr49cQ1Ud6nKG9W9VqrfqW", + "LR9RVacNWgConsYLYnSCD64xzhxapAzys7", + "LfBcxk9WpfagYY9oVnomeNQJGCijNPqWBb", + "LLtU7JvNQ5LhmfbwMazNjDzQ9wB5SrmaXs", + "LPpvKNJXvykhtZCFeCTfCJGkAYiN8jZYBh", + "LbXYCzLXBRDxuwv7Kb8VBf8YjRdJ8Zuc8b", + "LTjQkPWB4NgwXPiZqGT84gjj1UYGjqRbPG", + "LLp6MjHZKdjWGKQe5WGc47dZ4XM5qxn2mA", + "LNh4EK3hND8PKDdFPuyzEVWtr56sb7FAuB", + "Lcgn675CV2xcPKPjYvqLcvKwV86utNkTAu", + "LTw1ETkVwmGpb5HCkmJvERJEsWNdZEPRHm", + "LTK7rNjWEqdAAALKyGB9nZwQTaeknyUXNa", + "Lc8Mw6ErwuEdHVGqLNFMo5NVGcKoHnZPCE", + "LSffxhP4C8SQMexuzgChTmNRfvyLqwncbf", + "LhnPf2VirEeYCrKUbMSiFzkPLJ2fTmNtzm", + "LTTxY187qCEh4dibgc3onwrqyH7FDSzKBX", + "LPkQHme1tmM7tqLsRR1soHUKGgVaLCgRfj", + "LZBxTJ6sZLmTrEDAADrpEMGoanPeWvvRgi", + "LfaPNikFG8AzzpjKTEJMkKQnjPf28rWjMP", + "LNpmRArz6b9ocUsaiGanbnThWxK5vFZxjA", + "LZLBUYZBG9XYGCzSzkW7CEgqnu47AQ83uu", + "LUnNY5vj3xK61o8mJJiKXfLGTq6M25FB9r", + "LcfVXgTxv7Y7F8EFqtyQ37gzyGSDHs2dke", + "LRPcZSWR949S7v1UtsjFXVjjFp1MNx6no9", + "LQRTtbZ7fyNbLifp2VnMjkPAmuXSK65jUP", + "LKKz9XTUkwrwLfA3CkojuHdQbR86addQq3", + "LYUTtt97x26oWJdwFLVFProQVETX9Gv2Af", + "LSFotdspBDfvK5Wf6D2D7nDPPJWbADPxbf", + "LhN96GZLYo1aqM4tdDqTZWFYV5sMxbJ8Ud", + "Ld1VywVYxotJCJnYiJXYcX7vRBCoDKDHKh", + "LQQodxx3oDCTnWTaFkAWUAVu2Z9S7MmCdL", + "LQp6Ajv8Y221izvDSGNjgeykJbckVgxxLN", + "LcGUwRaXCfLiKM22f2A8FzxZSRrCy6vhto", + "LbM44fAzbpuFk4U84S1KxfwAtUERwUQs2U", + "LPiRebPFnteKKFr8RtGDFCzkcFiaxcXMAS", + "LgaBoSmfv8LLKdovvDs2LpMQqDSL4S52v1", + "LcGrF1xqc2cDWfxFw6CqRRQwKzuuXzEnSr", + "LLvufEqjFyDSvG5w2F1fcTnrWAyTXAoBsv", + "LfHjA4ViNZt6NJWtSa5w41H1wuG37gHrr7", + "LKii7xfkG9uPimmwiUojzPPFZDQcbVBfwR", + "LNsWWFDfuG2mi5x5Vh1ypKR2cCfsrKKtdc", + "LWFgLzQBmwiG3KJhXSWECM6oegE5UcRVLd", + "Lfijr38CTnD5FMioHpwSpreCWGcVreLf1s", + "LQDzYkTfNNkkmETTKEcsfBBLgDio7Ch2RS", + "LZWdM1JivYM1gpgBAXugJcqcvfhr2DFPAg", + "LfwPuqWsvqaTAe4VGh9AHFu2f86mcHQ7C9", + "LRwVUNWkhEg4DZNXPboWVMgPam7s67HYk7", + "LR5juRP4QfNcKtswkPU2z63S1VeUHUSTfZ", + "LPaTHZs8311nZggDGFpbLofyyDU9zGfnxh", + "LiXwEJzji9eFwg6Qa2HbAW19DPrQNwNQ8v", + "LNhFfFqYnHEKHdYA1cW7gh2w2qVaRQ3Aga", + "LPWL9SK18JutKwbJBNGLNoiuounbkVgr86", + "LSatLFBU4SMD9nv9R4ageDVRDUTtQQhk8L", + "LRMLqp42TfwHgxpLs8JbJfWJqfs3ZdcGbv", + "LP1enWqWhswMvPQL5HwLKJv3S33oHYgvje", + "LZ25qiaYmv7DtKe2NW6eMsVgogHw76aS3j", + "LaEedjPMoeY2KiQrSqkmotUeEPivEub7xY", + "LTb1UmEfERgmQofhkswsKEBH72qPCUhZae", + "LgTVzwEmuMzDBpHWryXFiHLPzqbMbkbfUb", + "Ld98vrb3Fr566YeqiP8aeJveELmqZaVcuR", + "Le9Db1J8ycebrWFQPafgtxo85NFroj6bkr", + "LPgtvGYe2r431Vor4NtTuCsuNEu5zdbyKr", + "LYd1ypLRtEikKWKjiaP6t9doBfrBJ2LXt1", + "LZ4NFJ9Wo6BXYfX1Fu2hc9B928youeR6Fi", + "LNv6tiwWTJneFnCY9DvLSS5gDi8w45Ebcm", + "LcVNwNatde6YvzToAz2aWFQhDZWe7KYTQ5", + "LMdzgk8G16GdbANbJ2V1scmjirRuduASZj", + "LiAQUYEWK9LrWo3EVVvArJjWroB1p6QW1w", + "LhU8hfcGUAVbQtVWAbzQP6QgnSAb46sdez", + "LdQizQZSqUy9qbjFjbutwC2Ykm6HT1aUVB", + "LN3VXb81XUN8GRi88xiAjxgRK7jmpHSh2T", + "LLojnet7SDEyE6r5ypEbbMoxj7hQHwWtU5", + "LV2ewWVcTXHuoZbQueEnjX1JmARVoFzax5", + "LazBTecQrBBWVoAXGudW74swL8U8qyD8Nr", + "LPtNcbAbWdaxUpVJdwYLZfkxi6wBFAmMit", + "LhRcaWetasKhDAYq2XbqeKcCapGMEwv8zo", + "LNSmDGKbZMZEniXeYuXCYYeYYMkywWabcg", + "LXWS8N8XxExRu1kgj1Pk8DLx9Dvxz8Q2fj", + "LexYfRVCPaKKVGFLstMTQqLDpckeigssqK", + "Ldms5eGY8zJS6FWJxv2EVXpqRi8py4T7NP", + "LaoL88U7Ry6Vu2SAXmCJsEK5vC756nXg97", + "LRgX8PJRoNWq47NejvP9JCcWLTy3GDhGtq", + "LXPSjGUe2xWpy9V18g14JPUxrbYa4zC6gk", + "LVd67tYst7wEaygdbyTrCNnkUCxGwvHMQT", + "LQRjVWXQN7yNLEWYCRCGqD8DZ7ZAnwbGFK", + "LfCwTPmYxcg6ptZt1XsZpXVLD9erm8icfZ", + "LSSeb3WyehJtkN5pC54p9WoZ6pZCqyxHc8", + "LWHGWCrcvRRDWkUz1XRxvgyxYv5XaBY9fx", + "LfnKt6eJ6nEvRVLoMEgy9D7fad2tqiMB55", + "LVoRMs8mfdNUkx3U9rWhHah3fc5jGs32W5", + "LUqSfbzNGBbStRdC3sQZ7MPeHkxbcpdu37", + "LSJhaNKiAYiLG3LfQFQqyRvb7aeJguRzbK", + "LTnJZT9Jz2fiyYCpnoV3wakoxqAeW8rHgN", + "LPDtzuk4TrFXaTn5fD5eyhiVernTJQVcwC", + "LbhFP1AZQ3p2Dg9upDtMaM6L6Y4AP9eViY", + "LfN2hVwHCAcSoanTSmZHPZpTenL6NSxn5b", + "LN41HmbM8GZnjcSfhUWohNL5JHzYc6Q4yS", + "LWCRTvjLsBPpwxHETBYmBfA6xT4YcSgot1", + "LWX9PVUBK7TtSGwNPb5Dg3pLCR9NQZuUtp", + "LbV1MJ8C16snbn1J5cWbcuAPXbojVBxHEu", + "LdwyhS1mXxcwwL5XBpVYQ5nmGhZKXBZxB4", + "LcMGYAjuRVVv4NeQgeyAQxNygtbTa4VKAR", + "Lhk6FBbTkBBAqTN3WMhgNwFUSxxt4wQdFG", + "LZaPP8QwrKMinoqVa8nuVHBARUFJutrmaK", + "LaviUsiNSthtxZqkshztwzgJFSoeRwB7SM", + "LQSCpivkXubZ6WSJ4Sj2FYQREzNYE7eHAa", + "LbjGdRMLBN6ZzZzCEmuSMREm2ppqXxXxG9", + "LXhdBAZRqndks8H86sauQHjdMxfauYh4qP", + "LKULCTZmekgyPJNKKr28rYRhMCuYm2o2r1", + "LhRHZGQND8pLJkYSNd2SxYRQpo6zNb8c7J", + "LS1axSTidtddZ2MqJtBMbTqQ4WUfAXoLfv", + "LPisDsmDks7AifRSbohsZRFwzC42wKvAfb", + "Lf5Hu9DUCFzWgBZxnNVYTA4vRvQv5RsC8Y", + "Lf5mNA8j574KiToy93y6vyJNNujgMB8D5h", + "LfwAVBi7rM9EqWgYVuxgy6s2oruf7rFehH", + "LS4NFvwhLqUZCVUvbsTLxmUciii2c4TXEa", + "LZSXn1cyh6uVGkZfQ3knsYYJUscQ1bH6ct", + "Lh5RVPHaS3XESVeciCQWU7fX88DLXJYohN", + "LeD9p9Z4cs6djuMY8eViFuAEUwZ23SAm9C", + "LMgXv9fa8LaYhT3J5EAvmhAL69SuSQVYj7", + "LKmjzHfHJjy7w6dPQPRbHKUrR4DmJmfVAo", + "LaLBXX5RPuBdtc9PmgYj94xnxSgafXqxLj", + "LXspfsj2Aj3JifsvieKJY855FqbjarcRLS", + "LKMfrpYj2Jpwf6GbcWKVLVXQr9y9Gfgxpj", + "LdW2144tJQy9Wia9WpRQfHWYuH97KPetpc", + "LbrsYcvTG9xPRdhB7EPzGrDBVMBSJVtz4a", + "LcxvDAqY44V21NQVU3nDNaRZbf4j49zg4P", + "LcvQDwfh9ERb1wQxLcbBicyaJ133mBDJJw", + "LZYVtkTRrJzpcx5LY5cY2QgXZsrqcmPHGv", + "LRjkE1xGxLGTYd5PuBCh6JdLbQeBWjzCXn", + "LVce5gDSjAYN8BBDYrYNAeJJj2gYqs4gu7", + "LYQKe5mWg3QWBvngMBD5YCjqaWuAoqCFEF", + "LYySS7JcpcdPcRWMmnUNLTfwdTMgLpZNqf", + "LZoTv6VVpX7vg4whaXujmySxRENuJgkp6B", + "LgUAkvrob7L1Z5J2renUjbVDoH4ipPtyrW", + "LQz9t7uG7mH3CGtaZvUwcBS7w3qMXnVYvV", + "LXggGj4DoRu1PoRCrS4ZriZetn423kNjtM", + "LRDL2tAEDY5Cp21GMxq7mwSmhtCvKHJYmG", + "LZ45DzMZNQoNfeQvVDH97Bbyv83zM8b1un", + "LfLkJ4ETAXFDYdguJYkoumr73rNnvCpRWn", + "LNeH5HhJrcY7TPH3cmJYuPFLs7MRZJhxS4", + "LRwMLedPqzZ9TnqXupuvo8ZpPU775hxfoS", + "LeB4sMZQRFPCNTaov66FnJcMzznzXMEcrx", + "LN58nMYRVDHZMbCYfv866vstjpQYfg7P6L", + "LXbCqAhCGsN1mwTfKmPB8E114CiQfH9csi", + "LfCUv38QDqrJcrf6C2cGTT7cQ8v5BgScUd", + "LTWee5WMtpDek8AvTJJwQDKwUmv5P1kAMp", + "LaKcV5pyABMRWfiwZv5FiEvcZztcmEp8GF", + "LZRoQd74qGjvnoXHLzb9Ksnw4W2v3MD7yz", + "LQ4AU7TBv7NXwn2NJddxgDhtmFr2J8senR", + "LVvfLWXmQaL1YSByH8tVfJkqFCwZcQdJUF", + "LgTpi25reQc7gsHRqbhTpJnUg5ZHWxWc5L", + "LPUjQfZhzEXLZys6EQqt5gm4huPHk7GLQf", + "LSXYAuAkxcrFX4pjdM7mRxFpXUYVHLWMWk", + "LZnVdtGa6YxfBMUDS4zrNLT72mon7dKqkE", + "LewBHdJvLCBfsPSVWch7y8bXFP5F3YCqUF", + "LfGqWfaNP26H4hTQR7h1G4xctuZiyrT8hP", + "LKVXqiteTcAaRYe11oQDTbxsjHNqPzmFxH", + "Li36uQm2C3ntDq35KWEkds727hh1suXGYC", + "LZsrqswTkjyxtpZXfSZ4oGEHqZ6PSnKdcr", + "LcdTZFftQE3mufjgGRBdQ3aCD5tg8tPX5z", + "Ldj9MK45RTiUvqYjD5916vLgkLuZwmDGWP", + "LdCzymEQLHtXQnH2unAiZS7myPanA24vaJ", + "LWfaCBDTZoLfQjb5k1ForKMd6uwxDyFpkW", + "LRgbporuXdvyrKzdq2W95kkdwPEHjpLw2f", + "LQU4W2hUknmqgUSBZjtD2Ch85M69HARHXC", + "LXsnkb7A38NDHSJcD5jrhX8iqwFWqjnYFL", + "LgRQ4TmFMwW82VEPeSXYXKNZAGKWSHuUgP", + "LVcDDKskQD2TqnSSfG2BZG5ufqSmeagxfa", + "LaVv7yH1k7upguY14k2fbt3KfQK3zzkFgZ", + "Li6v55R1F1XeiZZNQ4RywuxZsfssgFHtia", + "LZLp73FKyUYkN74Sy384g4EVUfsD1EhUWf", + "LgWrxX3rQJwiY69FHa93JeW4xsvQgRWxBo", + "LQWm9GY3ciBWCH6bgiNHmPTWmLM5oKtRaH", + "LVqgHt9vUnTB9ZzUYNYxwVariprCbSAv4A", + "LbEsbXSqKZ2Uz5zQp1PTa5UXab7ikXTmSw", + "LaLE1W9XcM2fKRENigadm7DvB1u6FvGgqG", + "LP5cJdt8gm4uB7wyHHB8s6tYmuKyfcPSJs", + "LgfdaifGiyQEzhdCGV35Fwf9536CbaP9AD", + "LMt59pFjsdLp6MGDQQD9VRbHBZmatnFRg2", + "LX7e5JY4yfQGQzcP28NEcZzrzzMLexFCc5", + "LM2cwqThz5yYcUE7b4D2Kx6V9xsfX6135B", + "LVTbHaiMbuK5QJ9iezdwbJXkZxXRNWXc5G", + "LPkSDgscisHp8BJXeUztneoRNbkxJuqhMm", + "LfczRDEX5kxcyN9aRa9S8LD82QrrxMxR2r", + "LLmrgx2pGLQF7iGva9HookfnRgYDXxR92o", + "LRSrWxxwVDk3TtQjKMwaqjJrvp6PEX2hq6", + "LdsJaut1n1oALn71QDHeu5MKCJKYsrbJWL", + "LaTaJDeBb3hwYrMbVd7KgpeJA2ETu7bEwg", + "LgkRjuxxpx6Z8cTJ6A2ecaH6bCg3KvTS5U", + "Lea813yj7mcDEDyD4UxhjYjapWiHTkGkSC", + "LamHNJvYsnuQ5rt49a3mmyttGguF3hLyTq", + "Le6FcDL3NCapwafw6CMcGpnQKByHrzusGq", + "LfnzRrWTnmLKfXDFSKjgKbrsnB1iFrk6yN", + "LZXDUyfNwcfcsNAdr7yaWeDKKsq9YzAK9V", + "LXjcmGbwimUKDb85payAmrQmSXirDf9HJQ", + "LPnJYTcNKBM69Qmgna6yf8FMJqTvSAoNt5", + "Lf37qavaRCar4Wj9Kcqvhebaj2E9G8JB6q", + "LWjJHHZ7jeWXHNoS8SL7UEyd2gcSCFoWnC", + "LXbyzfyrmFonXmBk5GkPxiaZ1qyUZCxKuX", + "LKgfwR76E2YaH1yi6UTV2z3WftBM9pXr9Y", + "LYy8Nh9XPht5Mwnt8624gyc6axhFMAUAxc", + "LNpmSSJuq6tzt5tS9dZNJjuXhezDmUorLK", + "Lfv6agqBwMsFG3ANKvPysuJyQUkwp5XN9J", + "LfcDkWUBDhWVmF3Utf5Ztk4QqG7aehrKW7", + "LR9FK6DZn89Ye1cNu8QAtxZAVrT8wYxC5X", + "LLNChpKyyWgerYTKVfUFk2oeYUFyeZuEoM", + "LRGDjdUCZ4sAgUTK3m5gVDJA3R3rAY1nW4", + "LYeEZr9GdrCgei1RRWiRF5Uyqsi5i63gTj", + "Lh46Rk8bHk3JxomWynjBKzpJg4S6D4s2NE", + "LKtFkG5xJKdNnejKa5HxeUX7y1HAsKZgNP", + "LRKoBygqMvJYmDK8nDpSk5nWZKd2xxzbch", + "LKYb1yherT6oxUoMw4MTLyV51gJ1M75k1t", + "LPWP6xysXnpNXj8rKKPwKUKawmxymmTZ5E", + "LRcoYZSRQ1C5BLeCiNC46xFWhDPQAK2u9u", + "LS44nek3ytjXDxCuKDepHzPZb17LgdqVTE", + "LiAfwhdiwTd69BznWPi2nLjnsGc8aBH3J4", + "LYofrTD6GZZvC6vxF5iKewLm3Xd3iDWz7R", + "LXftf2DrbUVZBC7Ty8S9rnyrv6PdadXCZJ", + "LSazm4eUAaE3BuVVdL4q2cMhdbuwzSMmcA", + "LTVee6L638kvdPdYM7MG2V7JZZKCBwPHB5", + "Lg9LW8Q37hwbcX63RpzXBKprdei3Hocj4f", + "LWx3avsgDwffYTn47x8qtboTUM338c8CKJ", + "LT2PGJPi6xhzeNopVwWntRaxfUAxJzGo4n", + "LKhsCQnPVNPjLWPw9AesPG2rsbYaEbFfVH", + "LVeBNmWUYGoN35MPPb7eogwuApwosVXvNc", + "LiYzqbRHxSdU91kKGHRiw3k8UrPMKV4NXP", + "LPLFKEmRrMkKThEotv6LXSP2ujgLAvvhhG", + "LWuUFRwniKhHYGvpCJhHDsYmiqn6pP1NeG", + "LhrmTf3A8wN9uTG3G9kHzf5ogXHmVM744e", + "LcTYdKZvrwVgkHMsmqsLyRFVgUHJrXLSHQ", + "LVzanUyN8NiSKtspPUCAC1FHXASzEZJMM8", + "LKNCFcQue4RbHkQimmxByQbt6guqi9keYz", + "LZHi5sTPdUmCnmtCodNR4v5NG2BwAqCGHX", + "LgYQybURgdTLSZ7C2B1MhHWyfm7vt8wvW4", + "LdKLBQngeqcs1km3T6s6ZiDVZsQNdKo4QC", + "Ld73DCAtAiV6JccEsMvojGmtSNza4SLoj2", + "LVKWHWqE7tZp6rvmLhVFHcncSpQ5Mwf5pY", + "Lb7WtrcZgCYVz4C2ojjhAea1LWGX47p3mu", + "LU2w7DZWzuSsiSv9WxC6czySDdsTGtyhVG", + "LiMR9FFdEMNmcygQjfaCsN29WpJhQpwwub", + "LcJCTXGhoMc3882LDLWSvVrSvchgcrScee", + "LU1ozeChHY4Dc7tCVSxWz9cqptBgQ9pJb8", + "LMy9TFp2vUZJh1aBA4nSFTnk9dQrRub9aq", + "LexvTJ2Xva2Qx91Br3A6doygTnQrC3FaHU", + "LhJSxkRoXWB1imF1XLTydFGjthNdpMrxRC", + "Le3zA44dL1oxHjyJUySodFnhrQAEZu376w", + "LWdFGJmNPQjvdi6o89MDaFgQPLfzgpmvmv", + "LNqVqTuAi3WAnPr8v5UCb2K9v5xF9xVkUX", + "LamEb2i4o2ZQU227JFYyJ4JaiYroLh1Hoq", + "LMxqjT3dU5RT8LuoXnrZfHQu2wwPAZCuXh", + "Lf2Kwcy9hAcRdG64wZYhga7yvajPkyEQYj", + "LNcA279mX7GqQXa5NfJK9EiUCFvTZUEwHK", + "LZn5SSYFXbMNvWXi6Vzch6sYnEVm1Loyvp", + "LdpvugbTixy8dGDYvojLKC3UxzwQikzE8i", + "LfginKkdNZy1VpXtTspLypqk6KMcfGRW7e", + "LdVmYQAcvtpJdn4Zw21hynWcMxEgNq1qik", + "LdiSKq5rU78uXzibkxXqwfd2eWT1oV4WUn", + "LUaX2jyjQz2NwPicXXqBdYzYowXSeM16yN", + "LZZszxqfznEeCxdTV5t7U2yBvdM2ZJir65", + "LKvSyL6FBZHgN7sL7zVcxKuaTiEPRauqXK", + "LhXry2d4HrVk9E2acACooGs3KgKCDUzL7r", + "LfvCGbTUtRe9eiKqJhs8PAGor9bwqjwEsT", + "Lc98AoHUML45SAGgpzsX5LYc7sPCWpftNi", + "Le6XXyQe8VUbQhYUTn9wFY761xpLVETwEn", + "LPs9CEQHL2Ef3bKuE2BxmfWDnJfTvdffyW", + "LUP4moUpoS4ifVBtEBswFghVWqLdZ5qTYX", + "LQRXpcSkx5d66Y6WWZWXAcQoEUYbXisyR9", + "LUyQxiJqBRit97K9wdiVC7z8GZs858Efay", + "LXcGn1y7Vof9RqCUgD1msBXX4TKbfN78hq", + "LNqQZNJdw8jpwkq7MC5A1Z2U9VPRhxCN7J", + "LNYpdowmDHHkV5kpmX3BzVBqSppSNW8Gr8", + "LKskwLZnzZqCFQunxsKPqsJka7WuW5Yhc9", + "LR15VwhWBw1pnjabSn3M3VReFGmYowrhoH", + "LMzh6FEE7t8QatDihJesgjndmKCEB3GhDk", + "LVNMUN8yDoE7TcbPi9NWDeUEtRj7zBnmD7", + "LbaZwbxNhHiM5vnRf65NTgtM3ibaZCiMSf", + "LcUHUzb2Whg2jww4jqgtq2HiX1cjoW36Mz", + "LScrZ92cYWGz6hszQTryFQcQe41hsd4MVS", + "LXdsv51o41qLX6hRTeScx62DH6NTrBsxP5", + "LTTH7rDBRNYHUurrFBoUFVJCKu3KTL4VEu", + "LhWytosG8nvC7hLDVnuTc1EqPsSKNSXW6d", + "LfdabKhvDa9rzM7JUN2v7g4JXUbvAmn5aZ", + "LRWd21TvMqRd7WE7FowMBU2ZBZjW4b36gs", + "LaEdURp2MHA2FevyhFj22rjfYZ1wU8Wb3w", + "LXNHmVTWLMqnavNwxHUgBRQcUa3NLHxQLZ", + "LZjcbwokoh4Wyv2zqUdjW78rDVKX9H9erY", + "LhFZYaLVVMKvAMGbfEEJBqfNQLsBiDukS7", + "LfMz6875z5cFMPpdgQuYDhdYJjbuo7ydXt", + "LQEe48oXSSiFsKorrFBtTk8EunQv4rU8R7", + "LU76iwcG7HFaw45XB8RxjrBk21DFGtpUxm", + "LUcxMHYwemdh6GFMau9Zr75pvik9n5jVjT", + "Ld5MF45i4U4kFj899L1NY5j5J55YSA6eki", + "LW4CWN7rCAeL1hhQENBSt6XqTvUp1JA4eg", + "LTuJrHZK8mALecAM5ymjVXkPinwcmUwUe2", + "LUA55GSL5RuB7nNqN6w9k4EHenGqRmpoSa", + "LRqPW12HWzSH1CHufFEThj5pmU1oaBFGjN", + "LZ5jF6KjXtMJbJ9s9rYu7LvUhvMWhqCF4X", + "LagEhMhov4wZmciRdamTGmGzwFziAuphY3", + "LfinWg1VzA8ibHWBH6mVYd4VHKr6NHtKq1", + "LRiAzfYMPYrU7wbg6Poi24b2rMR7fefWpy", + "LUsBVE46UVrEHXxWRDB7mttenSX4p2fdzG", + "LeJUwJfjaghuWb4F8rmUrsBcj4Az7GTuRp", + "Li1sLEgHeus5w3sYdnbtJZH7ySwP6mnEhV", + "LhDVcUe4F64Jr7WJYrXpWLWW3Uim5A99Gf", + "LZL5RhLRu5JpSXWMhfVkmdZs34g7zftRFf", + "LWt468oMQ2qzDxAYkMKiVwgSSFi5YiWtHf", + "LT39rRFHHCyn8QNYP6LasYkYCrovdRZWCv", + "LPfvXnzQytWK9kvcSbEJxPaSQuKYZDPsQi", + "LZNYir62U9fRDJuPiPsrKDEzaDePhJtJtq", + "LehtRTWzzkA1zFCtKsrFaK1KUd9dw3dcev", + "LTV7ndtzN3N51GJkvHbePbibwRHQpazpQU", + "LTysFerupYwh6Qb34WFieDArbtyXuUTDQK", + "LNopEK2qHxsgfJmsPhJbTXQuBzmBNVdADd", + "LMLpvLzf28BJgP75ydLw6NHUMW7svpKa6z", + "LedjHgZKzmeGp2ehx3bTTmRjGH4dDztcue", + "LRTUoFmcs47RCcuDPc1wfAJwawCx7DGhH6", + "LM82cAWrNBeEUmBDZiF8TV3yaa3SLdQm4g", + "LZFvxgQrSb7rDX8tcy3pA3vh8cRSAxvaHt", + "LMCaFyi2TXjv3CRddbt2Ha5eX3xEC5TBne", + "Ld2rEdNcSQRGJQwBnof6Ka9jTXKx6e1PUi", + "LYLCh29WQdSSLoHWj4b71th8MZUWG79QfZ", + "LSFy1ACArGPRD9YZEmcNMHxLDmnoPyTpes", + "LPptdyRYSBtGstDYgjwijD8St4MfSCfeSb", + "LWkNyMGdTs34uGbEXYxCQV4uhm28VQoikJ", + "LfzTvK6yahXTDmgVMgM2XHpodqJySnwAq3", + "LaxirQz79hC6BEWGzeNo2EvprUE7zW6TR7", + "LQuWoruvtJ6kdm2rAg6UotNhJJfQcgE3Za", + "LiKpsruQgtbVz1isYzEd2kCDezufNSY21R", + "LiL2MAtuo5DEwsLtgm3oqK7asQVuxmRGkD", + "LKrMYBPBwnQAibxin47PHXKVtp8FdWTWep", + "LQDGoqJT71et48KtZHw1M3TWt5avB9ZXX9", + "Lai2oCdmNfPBLmdEq8Q3G1KEwazqGBb9JL", + "LYTUbJDrrWSauohGCzpxkDfJ6XshbzSvYw", + "LPPNcDbgNVwQKak6pgHMnK6i9EdmBQejRT", + "LLX3dw4EEYL3DNH2eMGoXxumJFJjdxyxux", + "LfNkW8BozxTraZRFQ9kVRZYopv3E2JNnbB", + "LTbDScAZKeL5cnbrECSM7J2CEPGj8Taezp", + "LWX8kRsA5xwSRFVHKKbuXY7V5KHczcvQxH", + "LeoXAWCsYJB3FcYmhw9wQhDohKkBLjiDUA", + "LcNSzD66uvEAP31gcS5mcv1wyZ5ffsEzT2", + "LRkv8FEThwrQkxpf3jFkv8hY4Q3vonwwMW", + "Lgr7QtrNgKrS4htZyhmSp6S6C4jDX4d65W", + "LeJ54WUpjx6tKpBMyFKZbqcWFMAZqUUkKC", + "Li7b8UcA7jp7EMgQ8oNsk6JbV76XV6BoXr", + "LeadTzRiEFVBjjXNJwb3T7AW8ywNkEVLV4", + "LQHteShBs88McZVeWsdyqztHLqBGxdf8Yv", + "LVh7GPmjryAyxxEu5i2rFceAjEeAj9EP79", + "LS4d4NxrqeTUBjVcsaTLYtFHoPWLe2x7ir", + "LTeEzsHxdrMtE2wJdLnDtmJVgJD17U8bem", + "LhZREEZF66XgJFzgAnyThSBbSeoKbdMHBj", + "LapLnKbab5Rt2Twnz9whwsQN1NKnbTETwp", + "LdwcMyu9ygA1zfmDNPgLDVxG9AtQeJCdXV", + "LT6DRkw6XcyXf9pHjqCFYC8zic9DATfE1M", + "LPMVMYFLER425fqpBRghidN65MbzV36V52", + "LNegG5NGKkjTxR4V96qJFuEf7CKkDD5xgz", + "LSg3mgY3FqnvjoEWLTbc46hFPQLMtPCapg", + "LUC3B4C7uVaeScH26xWS4xBe62dv9P7P4k", + "LVPrbqT2JesNv8thx7TXNLd85WNBFyoDxW", + "Lg2SDwobLEhMGsxj1AUdV9AYBskz4a1f5N", + "LLZawqtAeQZpqCwfQVDrJnaPvTxL7RsZhR", + "LSBe3yw2mcrKYkx53yD8daJs3vLBoinCzi", + "LUYp26MjLmWJ5SWaHpccdTx9SYatEjCRhc", + "LejgVcq9oDZZrspvKnrkgeWCfMXGTQ8XGE", + "Lf47jyLKhphuoeuhQaBpy8BSDtP2b8FX46", + "LacqTKQcZZVHXamrtyC98BiMUqGFjre2jJ", + "LQg33sm4kgBza6u1oTmmQC2gDQzi8xHQtp", + "LhQSED2BB5eD2nxSnBuH3cSgtpGRE9Lr6T", + "Lgyym4cr9jyZ6DuuawsH4vU5nGfFJo46hR", + "LRUQb4QTQoyHqGnBtmxmGUmdmPEiBDaHFp", + "LXa7DzWMdrT1ZiPerKkJwxZPs8KGGjotNi", + "LPFvTTZGyPWTzWBniUeabT6PpX6Zzi9D6y", + "LevYJS22KHCWb43JbyBHtQC2ktEt1DnB4M", + "LRLD1D1eHEeEZLWEkq6usamBtkRFMoMQwK", + "LeVxtaGA6CM7m6aC29wZ3hhVqU7QwpiDkL", + "LTMFcs6J2DrhSCRoVJ4VjS4XsbAjtyLFtt", + "LSs69ihmYuq9ZFaZYY64dybDvcRKJ3HT1w", + "LLV9CR2EB97VojFE3QG3sUTswxNLJZ5Zzz", + "LNdPmx17qQ5AfMcThrSKxgg4ats45NSixj", + "LdCBn39uDtnyMUaQrRK8J6M2FvaYoxNvYW", + "LViEH78zotsf5NFSHC6rM1TT7bVXnkQmEK", + "LRwMVqKphLBydypR7xkTFvb5aBibgLcQUq", + "LYsywU9zXUUkmdWajphpEwF2eiL3ERC3Ss", + "Lf22WxRh8MgotGj6QNtsmwBjTi412XedCz", + "LYDum7F3teu4MPfN5TT8sxts7Dq4U2ztmE", + "LbYwshK9qHevNBGAR9Hrdj2ujB5vRq9A5r", + "LP92zovmBBPyfa6EffCWuToXAyDtSjkyNx", + "LLyJ4cDBiJ7mZyi7BFVePC8sKr8151ZLKg", + "LeFScySfBCoWSgvrzkTiUaajX7BHbJ89KQ", + "LTJLMc4yHHg3Gtxwcud5G4bsHgeKQFVTkd", + "LZoMaH46kzYykGStsqETL1gECWAxReWqAy", + "Lag9gsk5Wu9nnyVTLGex9hgKVVv8mBMdia", + "LX8ouPBgk3HemjYTREseKXEZCXuhcRcwZg", + "LRa7BNU9G5wjsE2KxoG5EAg3pWs6wfq4pt", + "LiYjHFqzLsqKvZx7KCCH1VKYV8p2ckbM3J", + "Lfh9oL7amDoxWUZfh1YyN38Dmij1vSYEnm", + "Lb7bTy3kdjs4EiEEAL8iwWgcZ4Be6bPtw6", + "LZULtWkQfjRpSLT1LigDM8126bGtDZQyZk", + "LfMuKSyVh5J38GY59SWswG8prCAucpkcGh", + "LeTnaxgTXm84b17EwJmgovW6t9RADsLiQJ", + "LN4VtcfdHTYmoacP1tpfP1w2uW7gRuhTY1", + "LYQDSnr6hMBKd6x1jky9gPA5NePEdTKMd1", + "LSk8DivGMiXGUy4NqvyKgXDUz7tXi7M6Mg", + "LTuNGVBmKgfdoc13ep6xkQq2qBdAWPcs6L", + "LR3fRTDAGuCq2wJqyQBTuKzEh9TqUo889t", + "LiQHGxYYQwbCZunneqavSmsKfAy75rRpje", + "LSf9xF1KKvWghwc59dHhKg4qdDDQLMe2Ry", + "LPt1FjgHvKzG1apoxvizRp2aTvvpuvbNpz", + "LULUas7LEucL5NCKo85ZoqGwqSWXqfKPAC", + "LddCzGVTs3gC3apfXxHMJDt7DN8Ay2d4z7", + "LRXqDsB9QVAe7JmaeFZxTV5yygQNsTPqhJ", + "LNXY9gjCUANfL565MvBGyB9o1CkkqPxDJA", + "LceXAS6mEK3fyXX9tdbxLREbzZDJGnkrZg", + "Ldo7eHSWqooZXNYfoSKB1gKPWauXGBwAvQ", + "LcqWVgb24TQyD8AQR61qGjahj9Uwys7oqM", + "LULdzyN1WW9BDdTc8hCVS6XHUtUVFfH8XD", + "Le17ydzLqYMoFzyUuVBSszrFMV3qYeiPcH", + "LPbCNLF2QgdLYkrQMNx1E3a46Sm789uGeB", + "LXLap7XRPWGGDV5NW4kAtj9LATqsXmJYbt", + "LYoGvagUXBTkyhxK23tuzqXmkLgE57Coep", + "LfWdzyNfwjEhbJCi2sg9j35RpDXFuqLdyP", + "LTgDa6Xsg63kvZ9pH2wYwDCqesXo5drk2J", + "LYDVpjoYvyEGEtQSzwAssuycoS7VEHGnaS", + "LXv9YPQGQQxxHwtXY6kvfABzo85A6pB6oN", + "LUNRSQeq19vnFfpyTa95RuA9UanSJ8KdUo", + "LbfVboAyyKXQTF6i6hLyoNzKwsRqKFVpyf", + "LN4f5Pg5dShTvTEZ1jXHfZKRuCrSeQHCfJ", + "LgED17g1BsjGx76yPSWaAXYTFf9Mo81L1t", + "LcRCefLQqwHrySie6WYDRwj1iGuHez9wU5", + "Lg2ngZXMJbBYW6RZ5SBRL9w8bZtVQJWQFg", + "LZP613qoCYLDMmVMnefw5RuSt9L4o1YkfR", + "LP3sW4vxFPuyTJC4kw7hhEKqa3pMAjfpYE", + "LhVMmYc8UZHj7KSKPSUsVJyP1GyKff3hgU", + "LPzPycHukP5J95jjiUx6vYi2FhRM95TPi8", + "LbenvZKkJwhCULwRCCuGgqR26rFMZE8Tzd", + "LXqajRBNYWChTpbSoCd2mVtY7gzfzQjPnz", + "La8om9H3kVzt1KCKgp11dURgxCaTqLj3vJ", + "LNVPL3UmQrdbr5uybGVco57Lr6jx6Gqa79", + "LZQ5nJckGbMk33UMob4eoJgi4JJFb44XQz", + "LM5zsEnehjipDxhdNbsX3umTNJtvGgvthB", + "LSTDwxFQki1PSRmhBYk3gZr2qPLbKcPKat", + "LXzW1U8Hk2YKyh2ecTGvxthHocLra2UJDQ", + "LS7sLiEsVasKwbtoidXGudnJB5fHDBNYG5", + "LLz58af37RiZrik2uDPmpc2RQJyNJ1oj37", + "LRLMow7MYFSoQMpfqka5PhJT3Z9k6WqfWb", + "LTbDijjjSUuFkVvVABYdkBrDtRpEsuCMcw", + "LaV9GnFJykDwaUWn9YVRCiSDtUt5GVpvPa", + "LUYJZ9UpqCayVMx7hTyC9UmF9rpEiH3Zmn", + "LgPff5EGamXXnfzHT3t9b3PEqcyRwCRtPK", + "LW4mo41eaLtGg29UzvqNnUqc6yYw6HevXb", + "LgfysTaFcMehG264PmjC6rFbC2PvuSxUs9", + "Ld73tbj9WQokQfyLFeVr7Re8XeWW47FPu2", + "LcAUmE141s87Z5AmDvJjqWvt1Uyqp63onZ", + "LZuXCswGsQQBo3nQaPm9e621GDa7hNPQxj", + "LNc4FeoogJW81pTJoVDjXiBSYsbqdiP4dz", + "LL2prhA3kDjtV7HoTp5hvaotDT8BU6BESi", + "LaQBUxvRXfJSEbpnf49dxqhCeSEVKm2gsz", + "LTmX4Mv4bScp8z1FgW6mEtQavN1c86pT2N", + "LTMWeFwGFtNUHwKvRbReoRSRBRDerNRN7u", + "LLQcxwRu3DR2VTue5oEJL83RbLEh2tML4i", + "LgymiHL6v8pJELfkFRqnWcmQGqpA7T7Zmw", + "LNijHxqraiYvkw1C3CXLvTDJopTWKDb2q9", + "LcPvRMUBNhciUj7ZDinmNMvd2mRDZ5f8gC", + "Lb3XSRYhobeyGGuHyN5uPtzyPcakvhQHWH", + "LbWLi2fZvSbNUdpnotbnfp3q2iKmapuDF6", + "LVeGB8SrXQSF1aD3tB4BKjpbf3mdBRTXtu", + "LMEWoLtwXBhECEhWnpz5frV71EuehX2zHv", + "LebnT4ApLCju1qvxCrJJsP6u7Jrdt3YEZu", + "LQSx5N8jfdykN13tzo3d23HqG64f1bWon6", + "LPk1KKuwH1S9vtyf7HM3qP78UoEHrAnEXX", + "LU9CaMPAgco9wRxc6Wm3FYkuFEzhYX65Ax", + "LSM3bSPAhW9HN5FtRn2Ea9DLKRayQyQx8B", + "LUgSMFvNQzjusUna2Hcz9p75bpEfS9kTZh", + "LesW6eRsSEnGRV9BJjuoDQL8GzWWoNGZek", + "LaCMX8KkRWcYknMMzTrrB8DxcoY6x6XJQt", + "Ld1DqSmpp88VfYX53zo2wZVs2wdTSQj2cW", + "LaT73qYEQ5LyK3YcSFjenQP6MxJMqTLqBc", + "LPMo7n3E4TzPBryFFoav1zfwaTqEU7fkDh", + "LZTefH97XnNiN2cFVUS5PBag9hqHStFBWq", + "LdyN2MJBUfHGS7AHJ2RAnhhK8vru3EirU9", + "LgEsMgND6pxSiwGWtZyETh7aMniTUHUm2M", + "LLPeDekSGQPpfRqQZXfyHa9jgvGqsihuG5", + "LeYgKYNJqdLyD7Jh5EygCSVxsunTVKpiVC", + "LU8fbEyGN4H6gn7kmZDyZPvJdA74LxcVKY", + "LP6TfYe2nRegNzVunREnLxapK7j24HFcws", + "LTJL5QrbrR3zQQuiPVmfPba57edCQtHUp8", + "LWdpr75az3i2dhYGdELRNjcDdG1iiAb1gn", + "LXgkzBRxojgFrvTXm2qeFXN7Dr75rr6jfy", + "LUxero4pkex6r4a4d4JAbJBP3eZVj75F1d", + "LdZKaRExyPhkYPeCZHwDgyX8zoZQJkoUPf", + "LZYY5aHWStB76pPXBtZtkRSUgpFgnBBKQs", + "LdACoqRaJTgxmcPPVLnQpj81mrfvE51u8K", + "LVz1yNKuRyeynTWKtjvhS8bhi8CHtEnzjs", + "LWaVX89EdNksyEs426eyez566uP379jvAW", + "LQPdTLAvpAndXopptBQ2EJDkNkswusweEx", + "LcvjRsJ2KtzbCVCq7dUXZr2cBCmPdVK3hj", + "LV8Mh3KS6EhJhHdDU4b7ZbG4PjkxhiJErV", + "LYJgrdQUV6rUQd31o2oHyeLAJ4yp6R3eXP", + "Lck3dU9y9wyJD2fSfK2dqb8fDwpQRWju4d", + "LS4rnegqmi1i4Pb3N2EPkvP7za8e2vR4gz", + "LTLvVRGGH2yMFmF9u9Mz7Kkqq2AwdJEFLi", + "LKLrHVjWYat4oTLhshmYEsRiCfUspcQR7u", + "LaEyfX2Pd29mXGT6iGzqnrr7uhq1UYZAHm", + "LZwXMbe9qtRWiuNtph8HcALSDzkmfWW6bV", + "LRbqTUWUR2fEJHSgRbJVjFoq52v2C1MUgE", + "LQcGFesvduneESv7HEtkfymaXeH7PJJgC9", + "LQBHTKspLyrqQ3hnCyMr1q2mk4RrT1BFKH", + "LNGjuBnVciDGUTxFQC2bB5gvFE3YqPzXRr", + "Lh2kgp3553BcvCHUsxYjUvrotjYHRgjXt4", + "LhsEHCMaAzGSyvZpCj2V4Wp1HjcwMah7tU", + "LfmRHjSNDXaEmLHEwz59VCUxYfYsh7u5kF", + "LThrqXBHdD1x3uby5J5PCUTWQTVshmdwxa", + "LMHUdMwpeziokMgKSWsuVeLKQryBYtCfZB", + "LgcRVwTKk9jYyvp2DQTsQQxyoyhVphrdVK", + "LdrXpd88kH2v1BWwJtaxohk8LRnr2TEHNm", + "LfC5NbmSeZYmiKvcQKrmz14msUAKH8ZrMk", + "Li3fNhb2ASsoK9jKJZaWs7EBF4XbZbWAv7", + "LhENViKDr1eDcCqEUC7ieQtRkydYABFq2r", + "LUyueRX77ZDtVgXD6G9FWjyRikeUA2Tx2Z", + "LYLudxM8vGRcQaxcQHRn8DzdLj7AAD68qx", + "LfDK8SGgD7aKEEB62iP3Yd82SvtFfip5uF", + "LQpdEHvZKFQF4rCUTy5SnqH384pvvZ3Y5i", + "LdfMWGVYEdDtyR2GHy8Lggz95dQPSaTZBD", + "LdZqGe9HG8Z4jv8oY3fM6DFWVYXyaEoYnT", + "Lg9X3fqua5Qvq5npLAXDfzF24n7jsduFut", + "LZvx36km8ayxZBZkTUDTm4FiDGF5gybJxt", + "LNHE1ejNH26YmU87epxg8SHLg65mrBDeQT", + "LQKSBKX9JwzQgM7EmhgmioYt9LL4N9xo3a", + "LYBz9R4vX3TPSTeoYsKPmEJAAYNyFPQUdJ", + "LUG12kwRamFNEvuWd9X2sFPrLBEKxGDDUC", + "LQZW8XVnYK5e8P9met5drCTj5dpnvkXUDf", + "LXEHZe4KwTfVfS7gwoUVuWsKtSHcsj1fMg", + "LahNS2CDM5skx3AVDU3PqeZKUc3ejsZJEB", + "LL9ztroxHkHEM3AfaaRnWR4GyBmndAQN8v", + "LT8wtacMnb5c6uUjznGgTFAfaaFYmx51AG", + "LZjUrPkpg93F2UUdCXEekTEjBjx8m9sitJ", + "LUC1TrjuMShNaYssEFVLhpBiUaL2sMjpZx", + "LaC2ihRQanZS9LogNNMTAuu8HLdz2uFZHf", + "LfjTwoa3QDxSGMVR4yCkUJ7QV4QXtxswqX", + "LQwd3fFW6k5VoZ6Bq49hTeVBzfQZCzBWEU", + "LPwJsArmye5XaM9xwmoufxPfB23vBxJcm3", + "LS8TFTCeLdB8qxzp5nYF55XHsz2XzRNxup", + "LLy4pPWGkorvB2Qz8wB5tfpP5tzmcu6bxM", + "Lbv5z8qMXWyu9ze3jQiv7viCESorzsZcXG", + "LQcRZZr7EHboLGMSW1VmDvcjCGM99FG4fn", + "LTg9TwJ7XZaawgmpCrVxEvvdBRSe2rtmDU", + "LNkUBp9keeahZzUdzSohNvG3WB5U8NujhJ", + "LKGqws48yQFuToQdGcfK4prxVmTTRzxTrX", + "Ldv7f7KUtb3Gz1zMm3FXS9LSuxx5fVefxg", + "LKJRF3ViVB5uPw1EebUkAVeA11HcMCT1L1", + "LaEEMDkcxr5eKuSf8dPNR7VH3i4aSKESbE", + "LdbUTpAvr62BTB5GnrDHdMfkwe1q61TGUW", + "LZrETjveMnQPzYZpbRW9u7DxoVTT3tLVAq", + "LfDZFiqTJDG6pWC9NwD79sr7ig6XJaSdsx", + "LcVE6S6o4bBMgZ44UM8pUP9yDWJe5y44TP", + "LN12xLfR1riFERGWD2ehTVXV2SgTPEGkda", + "Lb9MtyqceeU8myfnDjopRjtMNPDCdkdYNw", + "LWaf4t66rYFyrwevvSxXUUf1ghXeevwnrF", + "LXqk4DgW94Jj8Ne4w7TVS2cbrDGzFxZNxU", + "LhiFe4iNvqdFFySfCaKcz8LUTDtQsCpsW1", + "LPSSyTfct4eky7bdGaBTpdy3hztmMqMXFZ", + "LgcXMbZDRSE3EigmdkpPJJGrCzuEHVki5T", + "LPApGJ4vcYwJ3PqfrMcCstNjcZ3AZdJWoq", + "LNx8vM4HSHySnZS9FaLP7frerU1b5EXGsB", + "LQK4kcFKcGWJM6dGk2rjPS6SroH2HFfi1C", + "LPco5V5H2FRzqvE3RyA92XdkPKMvwJpNuP", + "LayMbyFpKVCtQ9nFccxcfhBya6qVQhfhbu", + "LiPzSX6U6Yn7EhGPwfEYdjhxyGnUtQQGJZ", + "LNxATzDKeGGt2EhyA8LwTEXoYjPv5iTXE8", + "LUYBZfFzXMNcMaFvgeQT58DgSYmf97zbMF", + "LMbym2ssdxWSdSQ3mbniW8cXhjigeXD5Kc", + "LhUTs87noKSP4TbPLcBddGmFSZpJNAwoqP", + "LSA2jhkmZgwZWBJfdWmdM86QkKAfrYMTP4", + "LYbWxSsJKzi24EGjz1i586XwickyAMBHEo", + "LL5U5BbErTwAxHkNhzgpPpknbgdutAbJfa", + "LSQ1RLD3PtazaMAi1uFte1f2iS5AhGmmTE", + "LMHMqXh1ksc2yU1L7kmSwhmVPLe6n3dWLC", + "LREYqFbYo1ZpmZHagSxGRsYneUJbkStZwG", + "Lf6jQQ2xXF87TsyqpYMPynTF39e4DpnsbS", + "LgGKHMPigMHGTnCFtRbma6nHEs52TDiZkR", + "LSyEah2pmQpKWHW1MsPkBUukv3JwbLFbp7", + "LPMwBp8Mb6o6332oYNF7V2BHxtPcB6xNvV", + "LUAzns7WnqjVRnqjh5Zu6XqjhbzYDbL8jL", + "LLheFGLodSJT7A45C7me94sp8BzcnbtU8X", + "LSXUFKZTicrw9LjdxE8v6RLyDgpaqEQXT8", + "LeUHN9Y1Qe6TdRX3nU7yLpEG1EKSQ7Bzvr", + "LKkw5wELgeaF371xzrUwYriJGu4Enpoe2A", + "LVnRUBuZJ4pWZMUJJevEZHaCqqZvKayZcj", + "LMZMDKE53tZa81MbHW61Lqnx8UcCFQrWdK", + "LS5meyubrb6gpnh7ScfANA5EA6TYE3hgAG", + "LaWf86KLNBi7xtonjoxNusr7dTYJM3Apsb", + "LdWAisbV4u7ufsvZc95AV6nbedkqcDSztz", + "LLewMMzTp9t6buyCxxN1GJKwviThwQuZF9", + "Ldscfk9QZhVgTxcTrpqpPfuh4d13PBk5LX", + "LcUkF9tHB2de1jvv9XWicqCSJH3boGuvrB", + "LVuh6heCKcHgQAdAcQeRa3BHNkYB2SN1sB", + "LbzcWGkCztHWSHrCWZGEK7gBwuCEYBS36F", + "LhTWof4eFXp8b9fiGXBNChVv7F6cCUqzcp", + "LbR3mpr1aYMsRBV35Rrd5tab6CSt1Kok88", + "LYN8VrfVT2KKNECnDXzMrmMsBXTKqT8qUD", + "LPdALGaG13ZDTyjos7ZzZnFKxwnoiq7Vvx", + "LLYNyWwRGLZtbhvVtwE9KaVgqNezmU834F", + "LRQgcZZDGA4ArbSY3t7dDMJQiCC7fm6Ngw", + "LNcWTb8vVNtVcdt8ePdVYtKuceywXGexnd", + "LfZygy36TzCJcLB15gj6iUPF2PqEKkqfjn", + "LY6b1KJ3PAoYEJG62FPsVygPr1Nt4dLxJu", + "LMpzSmo3xMdQu51gxD7AhiFfTdefjUGLcr", + "LePET2HXDooy9HV5XPCfyJb4dSz7btw7tc", + "LSF2ukWRZ6KzXTHYSdDPPVBGkE7QvpyCN3", + "LVNaL3VNted2bwyXkopvm3MSi7BeGNSgc5", + "LeJcMr4q6QqoV3ZTvi4uFxjvopd4Speu52", + "LX7LDTBPhr43pNmpALrVSpZuq3qp1WiDiW", + "LbGqdgDvKayibqwwWBUbc4f6HD8xkns36n", + "LL23kEmGYb1K3CRm1zLSjEfb5pHHBbh1qN", + "LWcz95wSBjuR3AGywLowEmFNFcuVvGU5Qr", + "LKs9Dy2fTeVGunYJXBEqdxxmxCxanG1KSM", + "LWAR63677sTMBh7CwGUgLF6xHrH83ybGgQ", + "LSURJv7fyEnibxuU5c9CCQpjZz4FyEVoTB", + "LeFrisey4bwdBXEPBZUnvsyhvp5R6HxjbN", + "LMhauutxdaTBvNBWt5Z3XVhGE72h56m3Y8", + "LhuS4qaCekfrsac8dW2fsD4pLQm9rf7AkU", + "LcQABUJC14zkbVKvgYKYAyvaDbcaEs6aio", + "LXPvebPoLED5cVyF5WtkVfX3wkXnQvtBFc", + "LQ6VBRdhtXYGsuJz8gn3TEJ94G7tRjvsnj", + "LTY8VWCMTLpkEwMu7GvXJe6Z9G6fytkJBQ", + "LRDR9HF7TmC3W7Z4hCFC8Frwou4qvs5MEG", + "LhGcqqfHd9VPDXVLjnkcisayWqZ7mMWExG", + "LcMMWNyc1MkwZzC896Yf8s3w54XEKjHJRk", + "LQMzwTguqtKaNxGCKQQBf3TG82UHwuhCet", + "LNcbiXmJH518LtazqSsYRNxzKSntJUrwxr", + "LMvJZzJAMShWmGhbqfbcYvFjjkkbjNBtuE", + "LRknefn3RKyVxwKkjdum9us6jerbRUUEYa", + "LSyXCZdZZ6JFQx2eFKzVsx8fCjajshKT2S", + "LMHGHtU5fasRisPz5Xd1qSZR5X1JpSME1S", + "LNodSYeXtiLWvakGXrP7wHsfu1veicrBp7", + "LhJXTJpxjUPV5yqqW9UsMzxM5dMa7WdVuE", + "LQ3GjdVVnbBoZmHGupZmNJYB1rdhACRm73", + "LbqpU58UNaUh6QptH4a325fEFHibXtisEi", + "LeLiUyNTXYV53rJzHBDL9MCeZwHcYg6oer", + "LQfCDij1hUNhUyQjmjEFiS3awGshk81W1j", + "LWES5WWwType47rjoqHTQ3cxhDtn7UKS17", + "LNe2VSdTEW5tWyCsHBtQi7ZEzvqwV6AC6F", + "LP4j37grd1NTRqdsNMVvPhH3YVPxRey6cd", + "LaN2VnxNMswM2gzJP5JBc49UUvNW2hQM2t", + "LZFVajmyg5S4GNmVxZ8w96uUoCuTk49SZb", + "LaCXjjfV7yvWFuoxq4MbaAA26HmDZ8ygnJ", + "LgKanQNSNYEKcx9mMUXGiZEe4sGvDsfEf2", + "LZyL3cG9DFZPShwPNrY5A7bFFC9REDz7nX", + "LUJMcEyxWi4H1t3VJm5p4MvtRNQwJMAkaq", + "LbJdtCU9BwuLadLQrsGw5LXQTw1cofCQUo", + "Lcozgv1ePhxZJmtPhXYUM4mAqBsgrixfJL", + "LT1nhGV3ugGUsuQ5S3P7hamuj8FG1XzXEP", + "LRnuLSyDwEBUwN8Zjv1iM9f2PdSrJaXkiv", + "LXDfRLDT4aSaD59HBFiDLVoDsKzbKHvEG7", + "LYNKSRZU5NzsJ91k8F5d8w9n3wTXRmESco", + "LT7wT3iqV9VBfQUyETdc22YgmuYPpsz92L", + "LR5NkVi57qqNmxBTgNXpaLMyF2R1rSWfdv", + "LR7vwew7NMGS5TwY7nmxjENY3Tw1pAEpBn", + "LRQY6WqBwZPXH6L4HXQSZCLuPZKLoR2A37", + "LPjTYuZumxSp9L8o1PZvpnNJ12iiDrJzpF", + "LchWtmeyR2sSPioQ5uDvj5zHqgRiPuSnx5", + "LR3TKpkSdQLpc9uBfJJhshkc5YQWaeXzzA", + "Lf6kkRPHqxXPtURWyhV3NrSMQhvJxHeFMM", + "Lh3hMVpf5cW5iMs1a5ZSH7wamdj7Cu7A4J", + "Lh1d9VCvuJTH7zankijKBQCHT2TBTS84rQ", + "Ld8GhZGMzsvWrrrLRdt1vK4jQ17swUKVc6", + "LUZnNT1Yxu5weCP1xYmUQoXxJ2yJjNwvyB", + "LYuvKNFjNhXUd4PxRfwBkB4mLwt8SwM1og", + "LaR2Zgy44XV3ZfqwjjL9ZBXJjMdL78LKC5", + "LZssXv5uSuictUyhXtXFmPJT3i7r8WTaFd", + "LbVLwA5kueFzQtCvjLWZcicSACTwfpt8pK", + "Li6QhA1JwQRdooVYWQmqNY7ajapPNiEYpV", + "LeZLuU9H2YoSUcnf8oFZCoGfsnv1PpScgA", + "LZCwXzfqSZWSqUHhZ8ZvQDRA54TdwHkzm4", + "Lfw2PmMhY1s9kSmfGid6wRY4KeswmuT1mT", + "LgZHB8N4sj1ajAqntXEvfZC46pCVfpNWXC", + "LT9CRh2GCidjVSB9yDzhAYR9o29JMzFV52", + "LhApBvac5mtAoeRhyeM1UHBAKD6t5o337g", + "LffTmaekgdvzKBZarsTzQoooictqjyRfZ9", + "LhGbWvFi46d4RmuGZZDedbpeK5cpmDVKjK", + "LaDBbkQG2eg3oTGcJJcUbSqYujEWD8GxGV", + "LSBeVyUmAomavuAaM3NRAcWqBJSCMZkma3", + "LaArmQWVYATX1RqLfcHTuiEKPy1uAUK3So", + "LaqS7mm5khUqsMcoH29pcaxJnMTZy4h3VP", + "LPV77htAh4RvvJnK2poWuBJA1XKU5UuYn6", + "LbtB14P8bSDL7BBS5u9C7QzfSbLXSMuWQ6", + "LeTFdUsPNG9DpFzmxqdNzcRXWiUhVBFSje", + "Lap9Zp3RjjiasRKZ6HVh9PrrxTnbEzmQQ1", + "LRJFh8Gem1G27ZER5RDqjaEqMknkDAnL9H", + "LXPdXPJ2ejvQEUuKii2XmKpcwWGKReQ1AF", + "LV4EBa9dDLAVrnoP1ZJ9uPZ21QSnGsyng5", + "LMTQpVaabmQ4GJRhya5UdDbBYwCnnrw9j2", + "LhomQHA6tTsLcPEmm9XyDBKTUvmZPEGCw2", + "LV6zkyyQ4w6MTstKHGUTvp147qF6tDK4Hv", + "LZ7JjSm2ipzdDSS67TcDz6ypYB4uRRh5qb", + "LKmkBiyWJcrk2qsGpDLSpENU9VmFUhTFLb", + "LcfSw6juAquu7ELptLuWdGVkFeoCGBt7TT", + "LVVeiEtgxcAmZNR5ZS7EkDEuP1rSQ2JR2d", + "LdggQFKWBZWXBEhmXAa4jVqPvV626maE4A", + "Lb2Uf2AJHvMg5gHebHEnhEhLRnjtZ3AKKb", + "Ld9zAq3tJ3KfxcXoXPXmKPS3R5yzrcD1zt", + "LM9B7S8wn1oXUnPpYXPjQeWufnkNYbvXwb", + "LMRdhEeegHTXXwBSEmv3Gzc1fQ9m4bR7wH", + "LdhYdFtLHfnWrnBFCcXirMy6BUr8mapYxT", + "LWLCjJqo4M1XbYQSKrXfhfoUB4pZxMQsTT", + "LYM1ejQWpBhdkbEhmfJrQ2KXFyeB5U9uG4", + "LKkDW2qjDbR2aumXuWysSUxhPsyKsLYLLQ", + "LSiQ7Evxz4KmEpaWoMFsb4in99iiZqchXi", + "LgFdXHkjNNuRsnjvUFByj2y8RFKBwigRvv", + "LgmZeoFz3LFw3M6iH666sXs4efPPPFDFRn", + "LRwVbxqon6hhg4uKFFjsPdsRBcb6CuuY8N", + "LTW9LQ4m3jvpaJQqG9SywG2YGxTxc95XDS", + "LhHYXKgU4ediDQQiVKgFcDss1Faxj3dCwT", + "LhkNzhtVPqyTiM94eDT5jAGpk6rrjBWySD", + "LYQAfktqsZm9UXFT8Gbq7hxQHLXLvvY9Vw", + "LSWtD79xfEKQie9xkYpLq7GtgPRCosKtqr", + "LTfbfSNWmwayq8XcjVVvQ7L6RRv4YqhnXb", + "LcssJQC1XYDMTibztCZQmEk56tefgHWceJ", + "LWr6QzQCSbgCmfmiMXRPEDt3RjQL1b1h6v", + "LexyJFNrgcNnnFL1ty5mWvxbLhJvPQr6Ps", + "LPbSJPvBLwAsFSARdt1zEi8maLMmEG6su4", + "LcJUVq96kyxh4vYjQ2V1mLbRLad4oxckPu", + "LL7sb6UWoGaXZ3Q6Y81sff5ASdW7ziSZgC", + "LXP1QYfCQRdA4CdnUjUJdMxoK3RPCCV3G5", + "LRr5KW4SK2APodwB73UmJWzMJYKk5edKCb", + "LSZjBdRuru46fg8KT6eubsHPfibmbQNgSk", + "LVnGQcnFjRqXWKocqFycqthhejNrDUb6ns", + "Lfmjw7wfBTtKYxeF6yYcg9VFp4yyoCQzVQ", + "LTeFKRndXEA3DDjVENvqmwFFWYarRtScxb", + "LdcV6oRJgYv9Qc52JkYCepnA5pW923Fygh", + "LZ7BP6uy6VcbXa5vYeQwpiCHHADi7R3QE4", + "LhkatG3dDHy85RFmEpKuRv88YHJL7tx11e", + "Li9n2e2GYndUMgjT8YarNmptsqyy57nBCn", + "LLwKLVEKMse8WbFU3t5m6oqeq6yQzv6Vwm", + "LYLEJH4tTtDECUWTRkv2JNgGp2qnC83eS6", + "LPGBJqe867xF4tjgQwknzoVU6v2XpggdaS", + "Li1TcaUhEP4tkbudkWncfVCY1j1wMgPnS6", + "Lbg2zTeZBjm3pN4c2cmJ7Aa83GNgJPP5rk", + "LRYV3RzHidi7vSYqyGTco2cLuTvkKWjDZT", + "Ldv5gad84XcH6AntbxjGxZHzcygCzgsi3G", + "LQ3ghptDNPhd8Cf5N7kBrvTbd6wAnfVTqy", + "LX9UGoNbTQRQmfymnXvqLKgbTqPofo2ZQe", + "LSTizhmpK1xbmAB6ug729uLs5xp8dir99o", + "LaXB6PT7Ur5PfSeKQtizmeaQzfKf9ULHeo", + "Li24gKAxQGHhQMcTNGVFMPikKoGu5HoQNj", + "LZ2mXGuvoZJTvtYxsupvNdjMrUEfFVcs8b", + "LNWTCA42visTpGMMmKMs8EBG5P5QFYwmXw", + "LRMYix9UCn76pmB87daetkypi9rEA9BkcK", + "LZBUZ9WpmCr1ki5tWED6LPRgKtUqi6t3EG", + "LKcGMQghBrnAutuyB9oZryJzyxhnoW5qyA", + "LgNtTi7jnv2T7nVLtiLs7MXc9fUcZCM1PX", + "LRtL2QCJ885ndQRYcCyxHtohojNtQ3kwms", + "Lb3Q8crTsgPJVhF8hGocPadUG8L4Ko7Uk5", + "LVRgQ7MYCVQkGN96mywZDC7X4wVKnVScXL", + "LgFC8Azpm1QQg4zf1stvyhdmk1J3GKRXgg", + "LZyaVaUVx1S4R9RtvVPHmirBCxwYy5BLqy", + "LWqHKipQvgBJXFH1YY9Aqyvidz3JLsbNqu", + "LS4EQ4HUyMj15S7uh2owumLWarpMwzYWf2", + "LeRNyZ4chbEqkuDtfSqC47r6aMDnR1Gi72", + "LeVbPnsv8Kys7dcRaY3yg9infzmz7G9Hpo", + "LUEVtdDJ8X8jVdNiUEX1xvzVQxcsSZPkrz", + "LMas9g21ce1eyXByhr7Ngaygbfo45Q8N4Q", + "LiEwXax16TsBmJpjTe1w332TeyUSamnvvA", + "LeGQnoVFNRWT3tUJFo8SToE9KYy8Px939K", + "Lfq1tZYPSa6LJMWMCzEfPVjyMjnHAUPnwj", + "LhiG7enkC7hUoRVyDset6nG3jBmFAnxP76", + "LhbLG3xjaFjA16LxiWKiA3yz1UPuam2LiA", + "LgafdEoGQynfHUJZGHLVaQcrr69sHjDjVB", + "LWh4HzjkKUMvQYCmyEuiLhwEnzyedFGgRD", + "LhmNnMErWRvw9pBhEaGA1h71iBrxBe91Ef", + "LZdBrWEZYwevQWw5N7cfx4Hp2716tWEo2y", + "LYqypgTC2U1H7e4mWtmDQXEJuokF5SAcLB", + "LQF3JiSeVnhARcgCJHSNvzdqRvmJGsuzvL", + "LURqMivxZfqtWHaydfLtsUwpd76pjxBuke", + "LYvQksgCo4c9pR1id6dbKDwHPEETYMZXuf", + "LREa1oKedRNis7T7XJ3cNq9tBAXCtdcGiU", + "Ldk6o44cz6YxFNvmvYhhPficYgmLe5hT6d", + "Lbx8PM2ss9DcPYMmzZqFcyjyFaLJM6Cd4Q", + "Lc7TBKGbvX9HNsZGyhWWrXGCLzpEZ7cEdX", + "LQfigrdhHAVV26uNS3hCVRxJZtDxe3eVV6", + "LZLynLMfUvPVMAmNXF46LKeA8jfUmUBKxe", + "LWnKVg3QJta6oyMVNLo6htofgyRfupkA7Z", + "LKHnGKbzJX7e24QJL98f6ABtuG4MQjAX4Z", + "LKexRP5HvwdBLSxLG5FnB3QVXZqNqARFiK", + "LgMa3Az7WKhMYcA9HB9abzkZysdkihHy5p", + "LMQybwAsWNJCLhVukHdntHCZ4R6K7nqZJJ", + "LNEbx9Q5mvnkUdKKqBBjm3bVbvTKfBLm7U", + "LL73GoiYM6pz2RVMDLWDAQhyjf2QtEojsj", + "Lfzvumvksk5EyUTKiFJAcKubAUDq7iiDLR", + "LQfuDpy9FtxDuwH8KvV6v8U6CDEmLPCtLq", + "LXAwyHXJ5uXkEtJr8eERwWVDWmiTwZjDXZ", + "Lbh4tQwuELbkx5MSoNgeHPpLgkYgzwBqmN", + "LL8Sr3Pm1DMLSgjgE2tzTgaPsBn4tUBZMf", + "LaTtLUvoWMvjDgUtiu25HFcTXrq1B4ZZiR", + "LhhaJPMJGCK9zfoRqZV4FdvrqHrfBuZRtT", + "LQCko1GY68nozg5TSDYBMvTgah19GUdy9v", + "LiMQxVrAutJDdUoJYMn4HxPJU6MUJo7Wih", + "LTHE6Thf2TD3apc2k8bGe49uT8vk7Duumk", + "LN4HkCAKxkuQTd1ZxxG3MdUZL7h9956sLD", + "LggbiEPxm9q7qujBgH8VowJDz462EeocVe", + "Lai1PeadaURQGssF7wCJVkUCkMKVCct7xt", + "LSpBCFpxVR6rHNmQPF2Emj2biJ8jLHkwRW", + "LdBpRFJECJfRMbvXL1Jp6qVhpkR6orzeUt", + "LKN3hdMPteWLEXBQxi5pwXdeufFa83FwgN", + "LVJvfkYtMNKUdME8zmMAkNCnK8CPgx9hS3", + "LWfJsaBmH9ppZYP4q2tCtUmqe178f1HHw5", + "LMKE317FWXkzDMCTptsvPxHfCWSPsD2rDJ", + "LVsxLvnbCw2Veirdkm57WwJ1g6PezjQc63", + "LXLmZvvK19RiNLxUhAfmWbjQEjDr67bNyV", + "LPTrg77VfLw6sGa5FH31E8C9iepsxNzzDk", + "LLzSdkRuTkSBFVSjJjjMQiKrcMuCTsA28x", + "LfaJR6MJRJ6P8xoZK5bYo8ee2oZUETfeGu", + "LdRrMKNj4qLNpMXLbM53rLxAepF4baTpLe", + "LULuUSgDKn83sESXpTPZ2pMYkR4JVjQaBJ", + "LdoTfSDBuH83aCvWtL9sHwiFvUTXbYwf5Q", + "LfV2DPCW5m2WMGBGMhLUv8bqq4BwN37JP6", + "LXhYvad9n65E9FNeSGwqi7jHMQQnc1gKiQ", + "LMCDQqLdsHiXBwe6Qr5vmMnkg1M8TYU5XN", + "LiSTM5jNER3o6G1uTDosw5kVM1XvRBaY7g", + "LgRrD1BXY3JydbAiBz7DfdcurjnLpHyXJu", + "LPiPe2TxtCzs3gsiGK89urizhyRsd14j5E", + "LUbTszmejtBwnHMWTqofAvTAGSnUcyZhK8", + "LVgVwUBwP45kBjQLq5h221WYqqQbeLmLrY", + "LVqE67xxpsqahokAGYrnCbXEvvUdNgZhwe", + "LXw9igkHBKPN58f9UXRK1CAqoJSFXPr8yu", + "LhkjGCFtV1LbdisVmruFZnC6TdaXT1iD1y", + "LaQEaTTBKk8VudnfpGKKLt8fwu4UNDK7Fe" )); } } From 90524d3880be4455ad8599c2739e73dea0d306ae Mon Sep 17 00:00:00 2001 From: tryiou Date: Thu, 11 Dec 2025 23:13:03 +0100 Subject: [PATCH 02/73] chore: simplify Visual Studio Build Tools installation step --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6754bd9..76b80fb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -141,7 +141,7 @@ jobs: - name: Install Windows SDK run: choco install -y windows-sdk-10.0 - - name: Install Visual Studio Build Tools & sdk + - name: Install Visual Studio Build Tools run: choco install -y visualstudio2022-workload-vctools - name: Build native image From 4afd66085fbe9df8ae9e6ac0e2f0e25f8935186b Mon Sep 17 00:00:00 2001 From: tryiou Date: Thu, 11 Dec 2025 23:23:44 +0100 Subject: [PATCH 03/73] chore: fix Windows build artifact handling and release upload --- .github/workflows/build.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 76b80fb..4dcf902 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -145,17 +145,16 @@ jobs: run: choco install -y visualstudio2022-workload-vctools - name: Build native image - run: mvnw.cmd clean package -Pnative -DskipTests + run: .\mvnw.cmd clean package -Pnative -DskipTests - name: Rename executable - run: Rename-Item target/xlite-daemon.exe xlite-daemon-win64.exe + run: Rename-Item target\xlite-daemon.exe xlite-daemon-win64.exe - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: artifacts-win - path: | - target\xlite-daemon-win64.exe + path: target\xlite-daemon-win64.exe - name: Create release uses: softprops/action-gh-release@v2 @@ -164,5 +163,4 @@ jobs: name: XLite Daemon ${{ github.ref_name}} prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') || contains(github.ref_name, 'rc') }} generate_release_notes: true - files: | - target\xlite-daemon-win64.exe \ No newline at end of file + files: target\xlite-daemon-win64.exe \ No newline at end of file From e0c8991ce60aa18a0c2a81cbb213c7b3addb3b59 Mon Sep 17 00:00:00 2001 From: tryiou Date: Fri, 12 Dec 2025 14:56:50 +0100 Subject: [PATCH 04/73] feat: implement log rotation system with separate console and file formatters --- src/main/java/io/cloudchains/app/App.java | 21 +- .../cloudchains/app/console/ConsoleMenu.java | 1 - .../app/util/ConsoleFormatter.java | 50 ++++ .../cloudchains/app/util/FileFormatter.java | 69 +++++ .../app/util/LogRotationManager.java | 266 ++++++++++++++++++ .../cloudchains/app/util/LogRotationUtil.java | 101 +++++++ .../background/BackgroundTimerThread.java | 74 ++++- 7 files changed, 566 insertions(+), 16 deletions(-) create mode 100644 src/main/java/io/cloudchains/app/util/ConsoleFormatter.java create mode 100644 src/main/java/io/cloudchains/app/util/FileFormatter.java create mode 100644 src/main/java/io/cloudchains/app/util/LogRotationManager.java create mode 100644 src/main/java/io/cloudchains/app/util/LogRotationUtil.java diff --git a/src/main/java/io/cloudchains/app/App.java b/src/main/java/io/cloudchains/app/App.java index 741bc90..6e2b81b 100644 --- a/src/main/java/io/cloudchains/app/App.java +++ b/src/main/java/io/cloudchains/app/App.java @@ -6,13 +6,15 @@ import io.cloudchains.app.net.api.http.client.EXRServerPool; import io.cloudchains.app.net.api.http.client.HTTPClient; import io.cloudchains.app.util.CCLogger; +import io.cloudchains.app.util.ConsoleFormatter; +import io.cloudchains.app.util.FileFormatter; +import io.cloudchains.app.util.LogRotationUtil; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; -import java.util.Date; import java.util.logging.*; public class App { @@ -46,6 +48,9 @@ public static void main(String[] args) { LOGGER.setLevel(Level.INFO); LOGGER.setUseParentHandlers(false); + // Perform log rotation before initializing other components + LogRotationUtil.performLogRotation(); + Runtime.getRuntime().addShutdownHook(new Thread(App::shutdown)); try { @@ -74,18 +79,7 @@ public static void main(String[] args) { true ); - fileHandler.setFormatter(new SimpleFormatter() { - private static final String format = "[%1$tF %1$tT] [%2$-7s] %3$s %n"; - - @Override - public synchronized String format(LogRecord lr) { - return String.format(format, - new Date(lr.getMillis()), - lr.getLevel().getLocalizedName(), - lr.getMessage() - ); - } - }); + fileHandler.setFormatter(new FileFormatter()); fileHandler.setLevel(Level.INFO); LOGGER.addHandler(fileHandler); @@ -100,6 +94,7 @@ protected synchronized void setOutputStream(OutputStream out) throws SecurityExc super.setOutputStream(System.out); } }; + consoleHandler.setFormatter(new ConsoleFormatter()); consoleHandler.setLevel(Level.FINE); LOGGER.addHandler(consoleHandler); diff --git a/src/main/java/io/cloudchains/app/console/ConsoleMenu.java b/src/main/java/io/cloudchains/app/console/ConsoleMenu.java index dfa8abc..447a07e 100644 --- a/src/main/java/io/cloudchains/app/console/ConsoleMenu.java +++ b/src/main/java/io/cloudchains/app/console/ConsoleMenu.java @@ -322,7 +322,6 @@ private void completeLogin(String entropy, String userMnemonic, boolean isMnemon if (coinError != null) { String msg = "[master] Error(" + coinError.getCode().name() + "): " + coinError.getMessage(); LOGGER.log(Level.SEVERE, msg); - System.out.println(msg); System.exit(0); } diff --git a/src/main/java/io/cloudchains/app/util/ConsoleFormatter.java b/src/main/java/io/cloudchains/app/util/ConsoleFormatter.java new file mode 100644 index 0000000..cd47078 --- /dev/null +++ b/src/main/java/io/cloudchains/app/util/ConsoleFormatter.java @@ -0,0 +1,50 @@ +package io.cloudchains.app.util; + +import java.util.logging.Formatter; +import java.util.logging.LogRecord; + +/** + * Custom formatter for console output that produces clean, readable logs. + * Format: LEVEL: message + * Example: INFO: Wallet initialized successfully + * + * This formatter: + * - Uses English locale for consistent output + * - Excludes timestamps and class/method information + * - Provides clean, user-friendly console output + */ +public class ConsoleFormatter extends Formatter { + + private static final String LINE_SEPARATOR = System.getProperty("line.separator"); + + @Override + public String format(LogRecord record) { + StringBuilder sb = new StringBuilder(); + + // Format: LEVEL: message + sb.append(record.getLevel().getName()); + sb.append(": "); + sb.append(formatMessage(record)); + sb.append(LINE_SEPARATOR); + + // Include thrown exception if present + if (record.getThrown() != null) { + try { + sb.append("Exception: "); + sb.append(record.getThrown().toString()); + sb.append(LINE_SEPARATOR); + + // Add stack trace + for (StackTraceElement element : record.getThrown().getStackTrace()) { + sb.append("\tat "); + sb.append(element.toString()); + sb.append(LINE_SEPARATOR); + } + } catch (Exception ex) { + // Ignore exceptions during exception formatting + } + } + + return sb.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/io/cloudchains/app/util/FileFormatter.java b/src/main/java/io/cloudchains/app/util/FileFormatter.java new file mode 100644 index 0000000..458955b --- /dev/null +++ b/src/main/java/io/cloudchains/app/util/FileFormatter.java @@ -0,0 +1,69 @@ +package io.cloudchains.app.util; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.logging.Formatter; +import java.util.logging.LogRecord; + +/** + * Custom formatter for file output that produces detailed logs with timestamps and class information. + * Format: yyyy-MM-dd HH:mm:ss class.method: message + * Example: 2025-12-12 12:01:51 io.cloudchains.app.console.ConsoleMenu.init: Wallet initialized + * + * This formatter: + * - Uses English locale for consistent date formatting + * - Includes full timestamp with seconds precision + * - Includes fully qualified class name and method + * - Provides detailed information for debugging and auditing + */ +public class FileFormatter extends Formatter { + + private static final String LINE_SEPARATOR = System.getProperty("line.separator"); + private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH); + + @Override + public String format(LogRecord record) { + StringBuilder sb = new StringBuilder(); + + // Format timestamp + sb.append(dateFormat.format(new Date(record.getMillis()))); + sb.append(" "); + + // Format class and method information + if (record.getSourceClassName() != null) { + sb.append(record.getSourceClassName()); + if (record.getSourceMethodName() != null) { + sb.append("."); + sb.append(record.getSourceMethodName()); + } + sb.append(": "); + } + + // Format level and message + sb.append(record.getLevel().getName()); + sb.append(": "); + sb.append(formatMessage(record)); + sb.append(LINE_SEPARATOR); + + // Include thrown exception if present + if (record.getThrown() != null) { + try { + sb.append("Exception: "); + sb.append(record.getThrown().toString()); + sb.append(LINE_SEPARATOR); + + // Add stack trace + for (StackTraceElement element : record.getThrown().getStackTrace()) { + sb.append("\tat "); + sb.append(element.toString()); + sb.append(LINE_SEPARATOR); + } + } catch (Exception ex) { + // Ignore exceptions during exception formatting + } + } + + return sb.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/io/cloudchains/app/util/LogRotationManager.java b/src/main/java/io/cloudchains/app/util/LogRotationManager.java new file mode 100644 index 0000000..f2363e5 --- /dev/null +++ b/src/main/java/io/cloudchains/app/util/LogRotationManager.java @@ -0,0 +1,266 @@ +package io.cloudchains.app.util; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; + +/** + * Manages log file rotation and cleanup to prevent disk space issues. + * + * Features: + * - Configurable log retention period (default: 7 days) + * - Automatic cleanup of old log files during startup + * - Support for both .log and .log.1 files (rotated by FileHandler) + * - Graceful handling of missing directories and files + * - Detailed logging of cleanup operations + * + * Log file naming pattern: error-YYYY-MM-DD.log[.1] + * Example: error-2025-12-12.log, error-2025-12-12.log.1 + */ +public class LogRotationManager { + private final static LogManager LOGMANAGER = LogManager.getLogManager(); + private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); + private static final String LOG_PREFIX = "error-"; + private static final String LOG_SUFFIX = ".log"; + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + private final Path logDirectory; + private final int retentionDays; + + /** + * Creates a new LogRotationManager. + * + * @param logDirectoryPath Path to the directory containing log files + * @param retentionDays Number of days to keep log files (default: 7) + */ + public LogRotationManager(String logDirectoryPath, int retentionDays) { + this.logDirectory = Paths.get(logDirectoryPath); + this.retentionDays = Math.max(1, retentionDays); // Ensure at least 1 day retention + } + + /** + * Creates a LogRotationManager with default 7-day retention. + * + * @param logDirectoryPath Path to the directory containing log files + */ + public LogRotationManager(String logDirectoryPath) { + this(logDirectoryPath, 7); + } + + /** + * Performs log rotation cleanup by removing files older than the retention period. + * + * @return true if cleanup completed successfully, false otherwise + */ + public boolean rotateLogs() { + try { + if (!ensureLogDirectoryExists()) { + return false; + } + + List oldLogFiles = findOldLogFiles(); + + if (oldLogFiles.isEmpty()) { + LOGGER.log(Level.INFO, "[log-rotation] No old log files found. Current retention: {0} days", retentionDays); + return true; + } + + LOGGER.log(Level.INFO, "[log-rotation] Found {0} old log files to clean up", oldLogFiles.size()); + + long totalSize = 0; + int deletedCount = 0; + + for (File file : oldLogFiles) { + try { + long fileSize = file.length(); + if (file.delete()) { + totalSize += fileSize; + deletedCount++; + LOGGER.log(Level.FINE, "[log-rotation] Deleted: {0} ({1} bytes)", + new Object[]{file.getName(), fileSize}); + } else { + LOGGER.log(Level.WARNING, "[log-rotation] Failed to delete: {0}", file.getName()); + } + } catch (SecurityException e) { + LOGGER.log(Level.SEVERE, "[log-rotation] Security exception deleting file: " + file.getName(), e); + } + } + + LOGGER.log(Level.INFO, "[log-rotation] Cleanup completed: {0}/{1} files deleted, {2} bytes freed", + new Object[]{deletedCount, oldLogFiles.size(), totalSize}); + + return true; + + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "[log-rotation] Error during log rotation", e); + return false; + } + } + + /** + * Lists all log files in the directory with their details. + * + * @return List of log file details + */ + public List listLogFiles() { + List files = new ArrayList<>(); + + try { + if (!Files.exists(logDirectory)) { + return files; + } + + Files.list(logDirectory) + .filter(path -> isLogFile(path.toFile())) + .sorted(Comparator.comparing(Path::getFileName)) + .forEach(path -> { + File file = path.toFile(); + files.add(new LogFileInfo( + file.getName(), + file.length(), + file.lastModified() + )); + }); + + } catch (IOException e) { + LOGGER.log(Level.WARNING, "[log-rotation] Error listing log files", e); + } + + return files; + } + + /** + * Gets the current retention period in days. + * + * @return Number of days logs are retained + */ + public int getRetentionDays() { + return retentionDays; + } + + /** + * Checks if a file is a log file based on naming pattern. + * + * @param file File to check + * @return true if it's a log file, false otherwise + */ + private boolean isLogFile(File file) { + String name = file.getName(); + return name.startsWith(LOG_PREFIX) && + name.endsWith(LOG_SUFFIX) && + extractDateFromFileName(name) != null; + } + + /** + * Extracts date from log file name. + * + * @param fileName Name of the log file + * @return LocalDate if valid, null otherwise + */ + private LocalDate extractDateFromFileName(String fileName) { + try { + // Remove prefix and suffix to get date part + String datePart = fileName + .replaceFirst("^" + LOG_PREFIX, "") + .replaceFirst("\\.log.*$", ""); + + return LocalDate.parse(datePart, DATE_FORMATTER); + } catch (DateTimeParseException e) { + LOGGER.log(Level.FINE, "[log-rotation] Could not parse date from filename: " + fileName); + return null; + } + } + + /** + * Finds all log files older than the retention period. + * + * @return List of old log files + */ + private List findOldLogFiles() { + List oldFiles = new ArrayList<>(); + LocalDate cutoffDate = LocalDate.now().minusDays(retentionDays); + + try { + if (!Files.exists(logDirectory)) { + return oldFiles; + } + + Files.list(logDirectory) + .filter(path -> isLogFile(path.toFile())) + .forEach(path -> { + File file = path.toFile(); + LocalDate fileDate = extractDateFromFileName(file.getName()); + + if (fileDate != null && fileDate.isBefore(cutoffDate)) { + oldFiles.add(file); + } + }); + + } catch (IOException e) { + LOGGER.log(Level.WARNING, "[log-rotation] Error finding old log files", e); + } + + return oldFiles; + } + + /** + * Ensures the log directory exists, creating it if necessary. + * + * @return true if directory exists or was created successfully + */ + private boolean ensureLogDirectoryExists() { + try { + if (!Files.exists(logDirectory)) { + Files.createDirectories(logDirectory); + LOGGER.log(Level.INFO, "[log-rotation] Created log directory: {0}", logDirectory); + } + return true; + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "[log-rotation] Failed to create log directory: " + logDirectory, e); + return false; + } + } + + /** + * Immutable class representing log file information. + */ + public static class LogFileInfo { + private final String name; + private final long size; + private final long lastModified; + + public LogFileInfo(String name, long size, long lastModified) { + this.name = name; + this.size = size; + this.lastModified = lastModified; + } + + public String getName() { + return name; + } + + public long getSize() { + return size; + } + + public long getLastModified() { + return lastModified; + } + + @Override + public String toString() { + return String.format("LogFileInfo{name='%s', size=%d bytes, lastModified=%d}", name, size, lastModified); + } + } +} \ No newline at end of file diff --git a/src/main/java/io/cloudchains/app/util/LogRotationUtil.java b/src/main/java/io/cloudchains/app/util/LogRotationUtil.java new file mode 100644 index 0000000..07eafed --- /dev/null +++ b/src/main/java/io/cloudchains/app/util/LogRotationUtil.java @@ -0,0 +1,101 @@ +package io.cloudchains.app.util; + +import java.io.File; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; + +/** + * Utility class for log rotation management. + * Provides a simple interface to perform log cleanup during application startup. + */ +public class LogRotationUtil { + private final static LogManager LOGMANAGER = LogManager.getLogManager(); + private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); + private static final int DEFAULT_LOG_RETENTION_DAYS = 2; + private static final String LOG_RETENTION_ENV_VAR = "CLOUDCHAINS_LOG_RETENTION_DAYS"; + + /** + * Performs log rotation cleanup. + * This method should be called during application startup. + */ + public static void performLogRotation() { + try { + // Determine log directory path (same logic as App.java file handler creation) + String userHomeDir = getUserConfigDirectory(); + String logDirectoryPath = userHomeDir + File.separator + "CloudChains"; + + // Get retention days from environment variable or use default + int retentionDays = getRetentionDaysFromEnvironment(); + + // Perform log rotation + LogRotationManager rotationManager = new LogRotationManager(logDirectoryPath, retentionDays); + boolean success = rotationManager.rotateLogs(); + + if (success) { + // Log current log files after rotation + List logFiles = rotationManager.listLogFiles(); + LOGGER.log(Level.INFO, "[log-rotation] Current log files after rotation: {0}", logFiles.size()); + for (LogRotationManager.LogFileInfo fileInfo : logFiles) { + LOGGER.log(Level.FINE, "[log-rotation] {0} ({1} bytes)", + new Object[]{fileInfo.getName(), fileInfo.getSize()}); + } + } else { + LOGGER.log(Level.WARNING, "[log-rotation] Log rotation completed with errors"); + } + + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "[log-rotation] Failed to perform log rotation", e); + } + } + + /** + * Gets the user configuration directory based on the operating system. + * + * @return Path to user configuration directory + */ + private static String getUserConfigDirectory() { + String OS = (System.getProperty("os.name")).toLowerCase(); + + if (OS.contains("win")) { + return System.getenv("AppData"); + } else if (OS.contains("nix") || OS.contains("nux") || OS.contains("aix")) { + return System.getProperty("user.home") + File.separator + ".config"; + } else if (OS.contains("mac")) { + return System.getProperty("user.home") + File.separator + "Library" + File.separator + "Application Support"; + } else { + return System.getProperty("user.home") + File.separator + ".config"; + } + } + + /** + * Gets the log retention period from environment variable or returns default. + * + * @return Number of days to retain logs + */ + private static int getRetentionDaysFromEnvironment() { + int retentionDays = DEFAULT_LOG_RETENTION_DAYS; + String retentionEnv = System.getenv(LOG_RETENTION_ENV_VAR); + + if (retentionEnv != null && !retentionEnv.trim().isEmpty()) { + try { + int envRetention = Integer.parseInt(retentionEnv.trim()); + if (envRetention > 0) { + retentionDays = envRetention; + LOGGER.log(Level.INFO, "[log-rotation] Using retention period from environment: {0} days", retentionDays); + } else { + LOGGER.log(Level.WARNING, "[log-rotation] Invalid retention period from environment: {0}. Using default: {1} days", + new Object[]{retentionEnv, DEFAULT_LOG_RETENTION_DAYS}); + } + } catch (NumberFormatException e) { + LOGGER.log(Level.WARNING, "[log-rotation] Invalid retention period format from environment: {0}. Using default: {1} days", + new Object[]{retentionEnv, DEFAULT_LOG_RETENTION_DAYS}); + } + } else { + LOGGER.log(Level.INFO, "[log-rotation] Using default retention period: {0} days", retentionDays); + } + + return retentionDays; + } +} \ No newline at end of file diff --git a/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java b/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java index 814acfe..8c65e33 100644 --- a/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java +++ b/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java @@ -6,10 +6,15 @@ import io.cloudchains.app.net.api.http.client.HTTPClient; import io.cloudchains.app.net.protocols.blocknet.BlocknetPeer; import io.cloudchains.app.net.protocols.blocknet.BlocknetPeerGroup; -import io.cloudchains.app.util.XRouterConfiguration; - +import io.cloudchains.app.util.LogRotationUtil; +import io.cloudchains.app.util.XRouterConfiguration; + +import java.time.Duration; +import java.time.LocalTime; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -36,6 +41,11 @@ public class BackgroundTimerThread implements Runnable { private long lastOut; private boolean shutdownRequested = false; + // Log rotation scheduler fields + private ScheduledExecutorService logRotationScheduler; + private static final int DAILY_ROTATION_HOUR = 2; // 2:00 AM + private static final int DAILY_ROTATION_MINUTE = 0; + public BackgroundTimerThread() { blocknetPeerGroup = CoinInstance.getInstance(CoinInstance.getActiveBlocknetNetwork()).getBlocknetPeerGroup(); feeUpdateHttpClient = App.feeUpdateHttpClient; @@ -45,10 +55,70 @@ public BackgroundTimerThread() { lastBalanceUpdateTime = 0; lastOut = 0; + + // Initialize log rotation scheduler + initializeLogRotationScheduler(); + } + + /** + * Initializes the log rotation scheduler to run daily at 2:00 AM. + */ + private void initializeLogRotationScheduler() { + logRotationScheduler = Executors.newSingleThreadScheduledExecutor(); + long initialDelay = calculateInitialDelay(); + logRotationScheduler.scheduleAtFixedRate( + this::performDailyLogRotation, + initialDelay, + 24, TimeUnit.HOURS + ); + LOGGER.log(Level.INFO, "[BackgroundTimer] Scheduled daily log rotation at {0:02d}:{1:02d}", + new Object[]{DAILY_ROTATION_HOUR, DAILY_ROTATION_MINUTE}); + } + + /** + * Calculates the initial delay until the next scheduled log rotation at 2:00 AM. + * + * @return Delay in milliseconds until next 2:00 AM + */ + private long calculateInitialDelay() { + LocalTime now = LocalTime.now(); + LocalTime targetTime = LocalTime.of(DAILY_ROTATION_HOUR, DAILY_ROTATION_MINUTE); + long delay; + if (now.isBefore(targetTime)) { + delay = Duration.between(now, targetTime).toMillis(); + } else { + delay = Duration.between(now, targetTime.plusHours(24)).toMillis(); + } + return Math.max(delay, 0); + } + + /** + * Performs the daily log rotation task. + * Called by the scheduler every 24 hours at 2:00 AM. + */ + private void performDailyLogRotation() { + try { + LOGGER.log(Level.INFO, "[BackgroundTimer] Starting scheduled daily log rotation"); + LogRotationUtil.performLogRotation(); + LOGGER.log(Level.INFO, "[BackgroundTimer] Daily log rotation completed successfully"); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "[BackgroundTimer] Failed to perform daily log rotation", e); + } } public void stop() { shutdownRequested = true; + if (logRotationScheduler != null && !logRotationScheduler.isShutdown()) { + logRotationScheduler.shutdown(); + try { + if (!logRotationScheduler.awaitTermination(5, TimeUnit.SECONDS)) { + logRotationScheduler.shutdownNow(); + } + } catch (InterruptedException e) { + logRotationScheduler.shutdownNow(); + Thread.currentThread().interrupt(); + } + } } private void outputAvailableCurrencies() { From 9854872a4a9c032b17cb6f2a0eea00867545824e Mon Sep 17 00:00:00 2001 From: tryiou Date: Fri, 12 Dec 2025 17:50:38 +0100 Subject: [PATCH 05/73] [feat] externalize TestWallet parameters to JSON config --- src/test/java/TestWallet.java | 1115 ++------------------------- src/test/resources/test_config.json | 1011 ++++++++++++++++++++++++ 2 files changed, 1090 insertions(+), 1036 deletions(-) create mode 100644 src/test/resources/test_config.json diff --git a/src/test/java/TestWallet.java b/src/test/java/TestWallet.java index 88acc80..70a22b2 100644 --- a/src/test/java/TestWallet.java +++ b/src/test/java/TestWallet.java @@ -1,3 +1,6 @@ +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; import io.cloudchains.app.crypto.LoginUtils; import io.cloudchains.app.net.CoinInstance; import io.cloudchains.app.net.CoinTicker; @@ -8,27 +11,74 @@ import org.junit.jupiter.api.Test; import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashSet; +import java.util.List; import java.util.Objects; import static org.junit.jupiter.api.Assertions.*; class TestWallet { - private static final int ADDRESS_COUNT_INITIAL = 20; - private static final int ADDRESS_COUNT = 1000; - private static final String mnemonic = "one two three cake neutral benefit quick hip level mother fine burst"; - private static final String PASSWD = "Test^1234"; + private static final Gson GSON = new GsonBuilder().create(); + private static JsonObject testConfig; + + static { + try { + loadTestConfig(); + } catch (IOException e) { + throw new RuntimeException("Failed to load test configuration", e); + } + } + + private static void loadTestConfig() throws IOException { + try (InputStream is = TestWallet.class.getClassLoader().getResourceAsStream("test_config.json")) { + if (is == null) { + throw new IOException("Could not find test_config.json in classpath"); + } + InputStreamReader reader = new InputStreamReader(is); + testConfig = GSON.fromJson(reader, JsonObject.class); + } + } + + // Test parameters - same interface as TestWalletConfig + private static final int ADDRESS_COUNT_INITIAL = getAddressCountInitial(); + private static final int ADDRESS_COUNT = getAddressCount(); + private static final String MNEMONIC = getMnemonic(); + private static final String PASSWORD = getPassword(); + private static final String COIN_TICKER = "LITECOIN"; + + private static int getAddressCount() { + return testConfig.getAsJsonObject("test_parameters").get("address_count").getAsInt(); + } + + private static int getAddressCountInitial() { + return testConfig.getAsJsonObject("test_parameters").get("address_count_initial").getAsInt(); + } + + private static String getPassword() { + return testConfig.getAsJsonObject("test_parameters").get("password").getAsString(); + } + + private static String getMnemonic() { + return testConfig.getAsJsonObject("test_parameters").get("mnemonic").getAsString(); + } + + private static ArrayList getExpectedAddresses() { + List list = GSON.fromJson(testConfig.get("expected_addresses"), List.class); + return new ArrayList<>(list); + } @Test void deterministicAddresses_fromMnemonic() { for (int runCount = 0; runCount < 10; runCount++) { CoinInstance coin = CoinInstance.getInstance(CoinTicker.LITECOIN); assertNotNull(coin); - coin.getConfigHelper().setAddressCount(ADDRESS_COUNT); - assertNull(coin.init(LoginUtils.loginToEntropy(PASSWD), mnemonic, false)); + coin.getConfigHelper().setAddressCount(getAddressCount()); + assertNull(coin.init(LoginUtils.loginToEntropy(getPassword()), getMnemonic(), false)); ArrayList addresses = coin.getAddressKeyPairs(); ArrayList actual = new ArrayList<>(); @@ -36,11 +86,11 @@ void deterministicAddresses_fromMnemonic() { actual.add(address.getAddress().toBase58()); // System.out.println("\""+actual.get(actual.size()-1)+"\","); } - assertEquals(ADDRESS_COUNT, actual.size()); + assertEquals(getAddressCount(), actual.size()); assertTrue(noDups(actual)); - ArrayList expected = expectedAddresses(); - assertTrue(noDups(expected)); + List expected = getExpectedAddresses(); + assertTrue(noDups(new ArrayList<>(expected))); for (int i = 0; i < expected.size(); i++) assertEquals(expected.get(i), actual.get(i)); assertEquals(expected.size(), actual.size()); @@ -55,10 +105,10 @@ void deterministicAddresses_generateAddress() { for (int runCount = 0; runCount < 10; runCount++) { CoinInstance coin = CoinInstance.getInstance(CoinTicker.LITECOIN); assertNotNull(coin); - coin.getConfigHelper().setAddressCount(ADDRESS_COUNT_INITIAL); - assertNull(coin.init(LoginUtils.loginToEntropy(PASSWD), mnemonic, false)); + coin.getConfigHelper().setAddressCount(getAddressCountInitial()); + assertNull(coin.init(LoginUtils.loginToEntropy(getPassword()), getMnemonic(), false)); - final int total = ADDRESS_COUNT - ADDRESS_COUNT_INITIAL - 1; + final int total = getAddressCount() - getAddressCountInitial() - 1; for (int i = 0; i < total; i++) coin.generateAddress(false); coin.generateAddress(true); // last one @@ -67,11 +117,11 @@ void deterministicAddresses_generateAddress() { ArrayList actual = new ArrayList<>(); for (AddressBalance address : addresses) actual.add(address.getAddress().toBase58()); - assertEquals(ADDRESS_COUNT, actual.size()); + assertEquals(getAddressCount(), actual.size()); assertTrue(noDups(actual)); - ArrayList expected = expectedAddresses(); - assertTrue(noDups(expected)); + List expected = getExpectedAddresses(); + assertTrue(noDups(new ArrayList<>(expected))); for (int i = 0; i < expected.size(); i++) assertEquals(expected.get(i), actual.get(i)); assertEquals(expected.size(), actual.size()); @@ -86,22 +136,22 @@ void deterministicAddresses_generateForwardAddresses() { for (int runCount = 0; runCount < 10; runCount++) { CoinInstance coin = CoinInstance.getInstance(CoinTicker.LITECOIN); assertNotNull(coin); - coin.getConfigHelper().setAddressCount(ADDRESS_COUNT_INITIAL); - assertNull(coin.init(LoginUtils.loginToEntropy(PASSWD), mnemonic, false)); + coin.getConfigHelper().setAddressCount(getAddressCountInitial()); + assertNull(coin.init(LoginUtils.loginToEntropy(getPassword()), getMnemonic(), false)); // Reinit which triggers generate forward addresses - coin.getConfigHelper().setAddressCount(ADDRESS_COUNT); - assertNull(coin.init(LoginUtils.loginToEntropy(PASSWD), null, false)); + coin.getConfigHelper().setAddressCount(getAddressCount()); + assertNull(coin.init(LoginUtils.loginToEntropy(getPassword()), null, false)); ArrayList addresses = coin.getAddressKeyPairs(); ArrayList actual = new ArrayList<>(); for (AddressBalance address : addresses) actual.add(address.getAddress().toBase58()); - assertEquals(ADDRESS_COUNT, actual.size()); + assertEquals(getAddressCount(), actual.size()); assertTrue(noDups(actual)); - ArrayList expected = expectedAddresses(); - assertTrue(noDups(expected)); + List expected = getExpectedAddresses(); + assertTrue(noDups(new ArrayList<>(expected))); for (int i = 0; i < expected.size(); i++) assertEquals(expected.get(i), actual.get(i)); assertEquals(expected.size(), actual.size()); @@ -115,10 +165,10 @@ void deterministicAddresses_generateForwardAddresses() { void deterministicAddresses_generateForwardAddressesReloadConfig() { CoinInstance coin = CoinInstance.getInstance(CoinTicker.LITECOIN); assertNotNull(coin); - coin.getConfigHelper().setAddressCount(ADDRESS_COUNT_INITIAL); - assertNull(coin.init(LoginUtils.loginToEntropy(PASSWD), mnemonic, false)); + coin.getConfigHelper().setAddressCount(getAddressCountInitial()); + assertNull(coin.init(LoginUtils.loginToEntropy(getPassword()), getMnemonic(), false)); - for (int idx = ADDRESS_COUNT_INITIAL * 2; idx < ADDRESS_COUNT; idx += ADDRESS_COUNT_INITIAL) { + for (int idx = getAddressCountInitial() * 2; idx < getAddressCount(); idx += getAddressCountInitial()) { // ReloadConfig with new address count triggers generate forward addresses coin.getConfigHelper().setAddressCount(idx); coin.getConfigHelper().writeConfig(); @@ -130,8 +180,8 @@ void deterministicAddresses_generateForwardAddressesReloadConfig() { assertEquals(idx, actual.size()); assertTrue(noDups(actual)); - ArrayList expected = expectedAddresses(); - assertTrue(noDups(expected)); + List expected = getExpectedAddresses(); + assertTrue(noDups(new ArrayList<>(expected))); for (int i = 0; i < idx; i++) assertEquals(expected.get(i), actual.get(i)); } @@ -154,6 +204,7 @@ static void afterAll() { clean(); } + static void clean() { CoinInstance.getCoinInstances().clear(); assertTrue(deleteDir(new File(ConfigHelper.getLocalDataDirectory()))); @@ -183,1012 +234,4 @@ static boolean noDups(ArrayList list) { return list.size() == set.size(); } - /** - * 1000 LTC addresses for mnemonic: one two three cake neutral benefit quick hip level mother fine burst - * @return List of base58 addresses in the expected order. - */ - static ArrayList expectedAddresses() { - return new ArrayList<>(Arrays.asList( - "LNTpgLXprtecSEzztNDmGJpfDr7noC65db", - "LRFcxeV3AQ2zvuFtoiTyCYfSjUQLkEddAU", - "Lc14g9TT4yAhAfHqLaKanr8pgCPBVBksgM", - "LM7Jf8MSp8CB7nt2zYoSZuFeqiBZwn6TPi", - "LUdCaBTEFQjVaZeRafinzaUxWXHZiZcvCG", - "LMUjorgJBdhqdy4fdUHAkm6UryJXLtzaWz", - "LRs2qDU825ca8RurD5yysMEgors3YA9hPe", - "LegeUDssVAq5Z7Y8cx2b8dWHnb1DunMZzN", - "LhNHWWFwSG61PY9duCPN98XDsR8jgiQJRF", - "LPw2hdP6ZvkzWTGn8cJHjinPYCrPa6ZpfB", - "LecbTbQ72258tMyoKFvAPhTJfhdzQwVPFR", - "LPwdqFTxhEZAA5ZUpBmydVPL7NRVHRQeiH", - "LR26gQX2Z5ays6tvgbHUeP2CCUTbpEfufG", - "LZXyUvGc2N23iHaDWWkV7drnvbHEE9LBrZ", - "LVSWjf6A7CRnHRFV1YAGKVcqNunfvmgmE3", - "LaZVKJN1DK2XiujjenaJ2jGtu2UKKBur9f", - "Lht2wBTNf3z6pYN8WWRAZw5aMFWGdMfRwo", - "LMdhg8w5QaTpSQzt9kBmzyQBTKp9vgPrCb", - "LNRZqXKFZoUUBTF2RzumfHNAUcxLXahWTo", - "LKqcHm1EkmggfnDMsMY6mrTx7f8PNMkk1z", - "LUnRSgWNfabKFmDTC6EdGWCNRJmRApQ5wH", - "LdCULqXSmA4dR4hKrCYNd13Zpbb83kjbzS", - "LMFF7wwrdwmz4kxsvyF57u5HBs253DDBmT", - "LQryk76nxEUTipXPZMa9Q6h2sgSKe4Xmz8", - "LULo7XAFgpickNw9ZCtz99u9BTdjmiSFh3", - "LUkdgnTnPJ3JYaDKZz6h6DdGQExentjcG7", - "LfGf5o14NUGuQrVSEHTRNFHqMndZUacpnv", - "LdRfkuPH6SppfU3L7o9PQptmw92Yw4JfYb", - "LXrBKZoGpkuk5e2vZ7RMyDqvzNvjuHxcwu", - "LQNHBjKgpMdfJSnDFNUjF34V9xiAFkgWGA", - "LTjbuCtAwc33m61teukpnPyFmkvLi361oH", - "LYAdmeqiW9zYuuQKDrnJff14J8sUEGZYbU", - "LbReUXLecNu4PXoLfhJqqn8KCE4sLoKBWp", - "LNfwCeec9ux2cNLiu69r1r7omiyEGqqq7z", - "LaQZF3arAKViEhhQR2fbAdpxkpaodsYLxR", - "LezikXzZAHDhJMYAFpRbJYipWx2b5yM86r", - "LZSKM3miJU3wVtsav2PJeyywRXjU32Qfdd", - "LfQbiA2wN96n8mUq6S8jtGVFgUA5JZq4P3", - "LghnhiepXMm1ukyYUsAoQurd6fCv2Pdi9U", - "LNAmNuCMv34C42gsGw1okHXeAx7uxeDQN8", - "LPngfnTyCGWbNwRLo3LXZBrqeN2ktLFTPz", - "LSPnz6cppLDwSGPPA2wi4Xo7Fd6GfazYE7", - "LNu4gkrRYhcWuAtiiiMDrSee47qgQ8L7Q3", - "LP92pv1N8tJzRuc1D3Mqd5HqL9gtitu8Gf", - "LPtgpYRpBHKj55zyVqceBL25pLbkt3XV1H", - "LdstQc2vRERBUuferrsHutHrSyXc8Eduu3", - "Lh1mGh9F4Dcabyw4KLFibKzDA8YmdVGFLw", - "LWQ5ibuQdFvqcvfhrR343Sf5cB3HewwPJP", - "LgaXbedKCRRMLRFXtr6gxzqqGC3rswxi9p", - "LLqKb2SDpA9a3R4uryWLgzC2xYxs9sKPM1", - "LLWEBquiHFV8LGivZ3oVwm5b8arwedMNKz", - "LKHnkgsYrdr6QkASAhYp25GmS8RfF5Ynga", - "LUmahz5XqhGzXEfQ5G8re9sGcQojF7E2gH", - "LTZYrdv6HXRzLDBkqcvZT6CvbGFJ4eFchm", - "LVP6iAtFq2JLCE22ELWmRhRPSM4Zzr3RHs", - "LhRLJzGega9paT1QZoSTvNWfpN2jpu6qzc", - "LdAy5N1wVcrYngqV6UuTKWm4xc61SzLzJ3", - "LafonyLZFoj1teorrjrD6MKyoJbKBBN81R", - "LRgK3ttMmJGAXST8c5w4f9H3hAuHiMgCCk", - "LZDwAdTf3aMf3qYNQAuCkA8B3BiaANcixi", - "LM4e4QiHavGFhkguqVcQee5N5exmQZNFFr", - "LNQ97UjwKtEFsc4P9xEDwh8obNg15V4rcB", - "LXQkMaN7ehoK7KQuPYrDms94YYaUkyhAnR", - "LNFgvrFuVP4SXYmfhBHZ4J5GRzDHrrNhYu", - "Li9ARsaUWwjZXnTQRbTaz8WGQxpiwgDPLC", - "Lg96QoS75svudKyjRW3CW4cw32rBvutZbX", - "LYQiqNPKGHHMTV9qnjVv9WWtUhwnWvTQ6i", - "LWGYANGR7RQyiDmktgdrgz7RGFBCShxtd5", - "LZAiCMeZLzy1QGkwwoT6UuKPkvMqXHd8gz", - "LQ82FZ8GLj6bVavFinqFxvLNbjcrvPCxhS", - "LcofKwE78JwmMrBGvBvUcSmT5YmL5UcSRY", - "LTP7BUs5aBX7C4AHGrVAtLiqQpajmvUvqy", - "LgEsVHXPKQja43j1Rd3xE1peVjExpH2qdr", - "LTBXqH8AN41QbmMXPxotXHMSiqhZ2SGvce", - "LUqaaPcFpqZPhYbUs7uvRj5FVz6NEYiEwm", - "LSwu69aWjEzp6ALhyNWkwMqMyRX8d51dAw", - "LcfMfN1SaMGc4qfSR3Z4bYu9nyKE4ivpEk", - "LZjCL8xUKAuy83uvqPcnkMwLxRB2D1TYHU", - "LXzJfLdZ222RAwNeic7pgrNSkwPxkjVdUP", - "Lej7drV2kA6jvRkiVRrxDZvTfX1zyJAMVt", - "LdQoBbvGhHj5ySGRgVF6RKNUNE53LveNdc", - "LMWLPeJXYZ4VPWB8xpQ56gNSVDCgu6Qa11", - "Lhd7oV52q4AHGnNBXfxRpKU3av7aKN37F6", - "LLZutN6a2NYtLeosEqsKMd9N7Ca1pzAeTS", - "LWguMXkcw5rNRhJ8G2N7nyQzRivYEWKACy", - "LeoaenVAPAkD68xTBqvX9TeesyFMGYeWGG", - "LWr1sDHbDLzyKLkdfsLmRLCVctBAcejZdy", - "LcV3JXkPvF6LUaUi4CsjC7zQm4rKF2J3xx", - "LWTf3PEfQVTfKcHrw3NXgpRi6jxtG9P9mC", - "LZTm2LsVpqFJoiDxKTDamWtK75FjuG8sJS", - "LbdNDivoeGXPcTfq2g5fEEK4zK2m4cp5rc", - "LRT8aPfvk5yqsuAs2X5yza9SQ6GtbENUNo", - "Lh2ZRdHMt5cm2aVUD99o7diX99S4GtbkyK", - "LTTjcuMfedGAuJHL4W8uqxCZmbSANnH42x", - "LXX8N9GBnzeDzTQeZWCEm2xRrmqsJgb3bk", - "LMYYVufrxdQpMqiWdinyVYfmTzKha8suA7", - "LPWUpTu23Gjdjaeg23UmDLmyaeBthZQUbd", - "LRHquUoS73mG5Uj3VtQaVgNmtj77a78MD4", - "LVLmwgtkC2pnuptJy46JZmDJbw414DYYE4", - "LKR4yoQFtFGDn6L4Fb79G4bjaPNrnz1Xq7", - "LW5bvYkYF7vS5tZMBwbo9AwMTir6ywGR3S", - "LZXQ6RRPRGXmSeFagG1EQxf19LsESTHKeN", - "LL41uzfhWeeHZmRpbn2XuhDx9z4tdYBy8G", - "LL57dQUequqnXJCYBT8DFnVt9k188pnQb9", - "LV3AVsx39xauq8t77iN2xDhCmpLf5cmKAj", - "LUoyQcyqFtcmEaEm77fNhWFoDQW3NZmRQW", - "LParh4xAPDCNTUfQaWpb9FTiqin1TY29F7", - "LQBYnEUuw4Wyov5Snv3cbgcpxgoMASz6MT", - "LP1SimqRa9nJgDa1pQ5X2BUUgFbwhSr27T", - "LhnooDJzpsiBd9gBDavqLExWk7HCmPdfWk", - "LZUbTRvrcBbJTehG1yL9EhoHYFqBX9GY6u", - "Ldafj4ehAHY8khB1zhCqXeoxtfCqbX3xhN", - "Lbh9xiQGaNvC1mj3J5RQcrcQgMxCsLPui7", - "LbccvJV7BKYLJZTGBeJLNPjnPz7BKJnqzi", - "LbxaR312y3RkAi2dnxneA42mN9Zf7Y4qmL", - "LNJ5kQJL85HPUmAfuqV23k9jVbrYsqFqTV", - "LMgSwsYzXTTPQBh7jNkY8b5J8kS1RGY3bN", - "LNGsETiVjZTiewDwSx6MBjtQJ6cjxsv5kP", - "LdYux77EPviPYsgntTLSoh4EEcYmyGWtnQ", - "LXjaNYexsVg2cFRxb8JqatMGHMhwW5wtXX", - "LRQ7wHfHYuLzXQH5sQZXfc7ZmLUi9NLTxw", - "Ld8osEsH6XiZrJGtL8hcpNRrurqBvpf1Pr", - "Ld4HAEWDqBt8zn6xnppqUfWiW7jqs8qs8y", - "Le326jYzh2HFkKska2SriSGUJB2Wg5MxQh", - "LeHUeSb2gNDA1zzwJT4ZHwmMZRWWSeX7dN", - "LhrDhcG6qKy87wFxyC9pEVkFa2TxUKgVQB", - "LYK79wmjy3t2uP96N6GPbDMJcwM8hCJsX1", - "LY9Xow2RgxC2zfcawmPhk77zE32b4sgZG6", - "LfCG9qkGpAY5Wj7w5cNforfxTs7R8MgWTc", - "LR7FyfFoYoniwGarnsCXmN437i517QJL98", - "Lgwx6Fj5gsEZBpZHdUzoAfDa6UmTjb7sPr", - "LQMvS5eMYSFDa6yxj8hr7m4aPPiCPEVae2", - "LeMGFqhYNMfcx4HDBy1NSCWzLW7vAy3oJ1", - "Lce8zFao77eQUiD2bLJLieRN1gy3iXgR1U", - "LQ5F51XZVHFMr6sDoZL5YUSzaV4WLnEQHo", - "LKXBnp6sG9vqz2fQTQGsbcgfkTwHu7tvAT", - "LToTM5T9yY2RwwPSw6Si6a1g1juK5SwRCh", - "LeqPGow6kzcdxdUbeUL4AUTwnTaWLGfqwP", - "LTo82EctUtn6BoGab66tEttayxpzjusHff", - "LSRUCGARpgcS3Tnc7QWnqkCEkD43z5Fezd", - "Lgh9WpzeBnr4fCzuKY3VFe1Pejh5mYGvQH", - "LWgUo4CsXEr9CDUraginpK8jLteb4Cq5hq", - "LgKS4NmCKBUp54oxEUfi849hB3mTG1vi4P", - "LKnpyE5t1T7EURWxaL1q1rawe51MTq1ajQ", - "LT8fnKfEjfenPfLdKjKNScRFHmdxjXitEa", - "LPjpeXCf3WPhjpYYw2sZCSjNRfv5bHMTBU", - "LVkFzCU3rPLUmPjjjXi2sUtzxPsb1LKB4m", - "LcF4dwsXTdHjngZFq5A3AYnCfJhBvSJ62c", - "LdyVKgdUJg947nD2ig6awLwyAenmMAZycw", - "LKWNJcTNGJqvdFrRLGZgEuvvH4iRcsytKG", - "LX4Rn14HUtFPLtvQvV6hP48gQ8NpCavmJ3", - "LeJPuEgUH5MzUVcDGwwALYa9X2d6h8m1pY", - "LgRTrcgMPzLN1bojHMN7njFnBNPuDS5zjE", - "LWzr1uppdUef4gvJ1QE8jd1Sa2DZUfaEVc", - "LWKuK8anQcGLNV9nrnjduXbYvDRF4Fj3LZ", - "LdsJj1cq4QHg7pVcmbKzbozmiJxiYKuZfv", - "LRnN2DzvhdUCVzQCzCCwMgDspQCoeojzwb", - "LM2fxgnQTCnmyggy7KKhBXbJQStbVo5HeC", - "LNpNCPsNruz6fBUVkRFJTyh2kp2RaicfhM", - "LKnmkxPkDNynjvKzZi2mdwhy3LGs5dzry9", - "LRLz8rLbdog6wYJwka3CVKAcXFzxu9JxEA", - "LbnftHPQ9jP3r1gmNPdDoTsDUp7xfKBtbV", - "LSVsppscBXo4j3wCtikHP1UBeg3tQXxJc9", - "LiW9yYR7CvFxZs9YpTx2JgFtk3yYqegjDE", - "LXcUo1ko1jC2ycX5nvdZfsd2eyJSjws2W8", - "LfnmCHZ3ySVy1aaDwjzA1wcrqK9SoyxsnR", - "LTRwKGCtV3Q72WL9wRoNkAPxuVW8TN7jni", - "LPfhiBxWEmnk4o7qoBE32UXWkuxsJmTsw4", - "LPLnj2KYK36mnHhJAV67UqtPhhyEu87zbm", - "LbSvp2mP4EQpfmQQMZDWe48SyGhyDmKozs", - "LeBGoKBvaVfWneme3bmtCyrYi36QqaEQkB", - "LcNWHL4PyUnrQC1YHUjWPa9bi6mdxpysGw", - "LT8Wd1Yc7JSvkuWXP3Qg14vnzdKBc16WFt", - "LSRgVmauy3UuGr49cQ1Ud6nKG9W9VqrfqW", - "LR9RVacNWgConsYLYnSCD64xzhxapAzys7", - "LfBcxk9WpfagYY9oVnomeNQJGCijNPqWBb", - "LLtU7JvNQ5LhmfbwMazNjDzQ9wB5SrmaXs", - "LPpvKNJXvykhtZCFeCTfCJGkAYiN8jZYBh", - "LbXYCzLXBRDxuwv7Kb8VBf8YjRdJ8Zuc8b", - "LTjQkPWB4NgwXPiZqGT84gjj1UYGjqRbPG", - "LLp6MjHZKdjWGKQe5WGc47dZ4XM5qxn2mA", - "LNh4EK3hND8PKDdFPuyzEVWtr56sb7FAuB", - "Lcgn675CV2xcPKPjYvqLcvKwV86utNkTAu", - "LTw1ETkVwmGpb5HCkmJvERJEsWNdZEPRHm", - "LTK7rNjWEqdAAALKyGB9nZwQTaeknyUXNa", - "Lc8Mw6ErwuEdHVGqLNFMo5NVGcKoHnZPCE", - "LSffxhP4C8SQMexuzgChTmNRfvyLqwncbf", - "LhnPf2VirEeYCrKUbMSiFzkPLJ2fTmNtzm", - "LTTxY187qCEh4dibgc3onwrqyH7FDSzKBX", - "LPkQHme1tmM7tqLsRR1soHUKGgVaLCgRfj", - "LZBxTJ6sZLmTrEDAADrpEMGoanPeWvvRgi", - "LfaPNikFG8AzzpjKTEJMkKQnjPf28rWjMP", - "LNpmRArz6b9ocUsaiGanbnThWxK5vFZxjA", - "LZLBUYZBG9XYGCzSzkW7CEgqnu47AQ83uu", - "LUnNY5vj3xK61o8mJJiKXfLGTq6M25FB9r", - "LcfVXgTxv7Y7F8EFqtyQ37gzyGSDHs2dke", - "LRPcZSWR949S7v1UtsjFXVjjFp1MNx6no9", - "LQRTtbZ7fyNbLifp2VnMjkPAmuXSK65jUP", - "LKKz9XTUkwrwLfA3CkojuHdQbR86addQq3", - "LYUTtt97x26oWJdwFLVFProQVETX9Gv2Af", - "LSFotdspBDfvK5Wf6D2D7nDPPJWbADPxbf", - "LhN96GZLYo1aqM4tdDqTZWFYV5sMxbJ8Ud", - "Ld1VywVYxotJCJnYiJXYcX7vRBCoDKDHKh", - "LQQodxx3oDCTnWTaFkAWUAVu2Z9S7MmCdL", - "LQp6Ajv8Y221izvDSGNjgeykJbckVgxxLN", - "LcGUwRaXCfLiKM22f2A8FzxZSRrCy6vhto", - "LbM44fAzbpuFk4U84S1KxfwAtUERwUQs2U", - "LPiRebPFnteKKFr8RtGDFCzkcFiaxcXMAS", - "LgaBoSmfv8LLKdovvDs2LpMQqDSL4S52v1", - "LcGrF1xqc2cDWfxFw6CqRRQwKzuuXzEnSr", - "LLvufEqjFyDSvG5w2F1fcTnrWAyTXAoBsv", - "LfHjA4ViNZt6NJWtSa5w41H1wuG37gHrr7", - "LKii7xfkG9uPimmwiUojzPPFZDQcbVBfwR", - "LNsWWFDfuG2mi5x5Vh1ypKR2cCfsrKKtdc", - "LWFgLzQBmwiG3KJhXSWECM6oegE5UcRVLd", - "Lfijr38CTnD5FMioHpwSpreCWGcVreLf1s", - "LQDzYkTfNNkkmETTKEcsfBBLgDio7Ch2RS", - "LZWdM1JivYM1gpgBAXugJcqcvfhr2DFPAg", - "LfwPuqWsvqaTAe4VGh9AHFu2f86mcHQ7C9", - "LRwVUNWkhEg4DZNXPboWVMgPam7s67HYk7", - "LR5juRP4QfNcKtswkPU2z63S1VeUHUSTfZ", - "LPaTHZs8311nZggDGFpbLofyyDU9zGfnxh", - "LiXwEJzji9eFwg6Qa2HbAW19DPrQNwNQ8v", - "LNhFfFqYnHEKHdYA1cW7gh2w2qVaRQ3Aga", - "LPWL9SK18JutKwbJBNGLNoiuounbkVgr86", - "LSatLFBU4SMD9nv9R4ageDVRDUTtQQhk8L", - "LRMLqp42TfwHgxpLs8JbJfWJqfs3ZdcGbv", - "LP1enWqWhswMvPQL5HwLKJv3S33oHYgvje", - "LZ25qiaYmv7DtKe2NW6eMsVgogHw76aS3j", - "LaEedjPMoeY2KiQrSqkmotUeEPivEub7xY", - "LTb1UmEfERgmQofhkswsKEBH72qPCUhZae", - "LgTVzwEmuMzDBpHWryXFiHLPzqbMbkbfUb", - "Ld98vrb3Fr566YeqiP8aeJveELmqZaVcuR", - "Le9Db1J8ycebrWFQPafgtxo85NFroj6bkr", - "LPgtvGYe2r431Vor4NtTuCsuNEu5zdbyKr", - "LYd1ypLRtEikKWKjiaP6t9doBfrBJ2LXt1", - "LZ4NFJ9Wo6BXYfX1Fu2hc9B928youeR6Fi", - "LNv6tiwWTJneFnCY9DvLSS5gDi8w45Ebcm", - "LcVNwNatde6YvzToAz2aWFQhDZWe7KYTQ5", - "LMdzgk8G16GdbANbJ2V1scmjirRuduASZj", - "LiAQUYEWK9LrWo3EVVvArJjWroB1p6QW1w", - "LhU8hfcGUAVbQtVWAbzQP6QgnSAb46sdez", - "LdQizQZSqUy9qbjFjbutwC2Ykm6HT1aUVB", - "LN3VXb81XUN8GRi88xiAjxgRK7jmpHSh2T", - "LLojnet7SDEyE6r5ypEbbMoxj7hQHwWtU5", - "LV2ewWVcTXHuoZbQueEnjX1JmARVoFzax5", - "LazBTecQrBBWVoAXGudW74swL8U8qyD8Nr", - "LPtNcbAbWdaxUpVJdwYLZfkxi6wBFAmMit", - "LhRcaWetasKhDAYq2XbqeKcCapGMEwv8zo", - "LNSmDGKbZMZEniXeYuXCYYeYYMkywWabcg", - "LXWS8N8XxExRu1kgj1Pk8DLx9Dvxz8Q2fj", - "LexYfRVCPaKKVGFLstMTQqLDpckeigssqK", - "Ldms5eGY8zJS6FWJxv2EVXpqRi8py4T7NP", - "LaoL88U7Ry6Vu2SAXmCJsEK5vC756nXg97", - "LRgX8PJRoNWq47NejvP9JCcWLTy3GDhGtq", - "LXPSjGUe2xWpy9V18g14JPUxrbYa4zC6gk", - "LVd67tYst7wEaygdbyTrCNnkUCxGwvHMQT", - "LQRjVWXQN7yNLEWYCRCGqD8DZ7ZAnwbGFK", - "LfCwTPmYxcg6ptZt1XsZpXVLD9erm8icfZ", - "LSSeb3WyehJtkN5pC54p9WoZ6pZCqyxHc8", - "LWHGWCrcvRRDWkUz1XRxvgyxYv5XaBY9fx", - "LfnKt6eJ6nEvRVLoMEgy9D7fad2tqiMB55", - "LVoRMs8mfdNUkx3U9rWhHah3fc5jGs32W5", - "LUqSfbzNGBbStRdC3sQZ7MPeHkxbcpdu37", - "LSJhaNKiAYiLG3LfQFQqyRvb7aeJguRzbK", - "LTnJZT9Jz2fiyYCpnoV3wakoxqAeW8rHgN", - "LPDtzuk4TrFXaTn5fD5eyhiVernTJQVcwC", - "LbhFP1AZQ3p2Dg9upDtMaM6L6Y4AP9eViY", - "LfN2hVwHCAcSoanTSmZHPZpTenL6NSxn5b", - "LN41HmbM8GZnjcSfhUWohNL5JHzYc6Q4yS", - "LWCRTvjLsBPpwxHETBYmBfA6xT4YcSgot1", - "LWX9PVUBK7TtSGwNPb5Dg3pLCR9NQZuUtp", - "LbV1MJ8C16snbn1J5cWbcuAPXbojVBxHEu", - "LdwyhS1mXxcwwL5XBpVYQ5nmGhZKXBZxB4", - "LcMGYAjuRVVv4NeQgeyAQxNygtbTa4VKAR", - "Lhk6FBbTkBBAqTN3WMhgNwFUSxxt4wQdFG", - "LZaPP8QwrKMinoqVa8nuVHBARUFJutrmaK", - "LaviUsiNSthtxZqkshztwzgJFSoeRwB7SM", - "LQSCpivkXubZ6WSJ4Sj2FYQREzNYE7eHAa", - "LbjGdRMLBN6ZzZzCEmuSMREm2ppqXxXxG9", - "LXhdBAZRqndks8H86sauQHjdMxfauYh4qP", - "LKULCTZmekgyPJNKKr28rYRhMCuYm2o2r1", - "LhRHZGQND8pLJkYSNd2SxYRQpo6zNb8c7J", - "LS1axSTidtddZ2MqJtBMbTqQ4WUfAXoLfv", - "LPisDsmDks7AifRSbohsZRFwzC42wKvAfb", - "Lf5Hu9DUCFzWgBZxnNVYTA4vRvQv5RsC8Y", - "Lf5mNA8j574KiToy93y6vyJNNujgMB8D5h", - "LfwAVBi7rM9EqWgYVuxgy6s2oruf7rFehH", - "LS4NFvwhLqUZCVUvbsTLxmUciii2c4TXEa", - "LZSXn1cyh6uVGkZfQ3knsYYJUscQ1bH6ct", - "Lh5RVPHaS3XESVeciCQWU7fX88DLXJYohN", - "LeD9p9Z4cs6djuMY8eViFuAEUwZ23SAm9C", - "LMgXv9fa8LaYhT3J5EAvmhAL69SuSQVYj7", - "LKmjzHfHJjy7w6dPQPRbHKUrR4DmJmfVAo", - "LaLBXX5RPuBdtc9PmgYj94xnxSgafXqxLj", - "LXspfsj2Aj3JifsvieKJY855FqbjarcRLS", - "LKMfrpYj2Jpwf6GbcWKVLVXQr9y9Gfgxpj", - "LdW2144tJQy9Wia9WpRQfHWYuH97KPetpc", - "LbrsYcvTG9xPRdhB7EPzGrDBVMBSJVtz4a", - "LcxvDAqY44V21NQVU3nDNaRZbf4j49zg4P", - "LcvQDwfh9ERb1wQxLcbBicyaJ133mBDJJw", - "LZYVtkTRrJzpcx5LY5cY2QgXZsrqcmPHGv", - "LRjkE1xGxLGTYd5PuBCh6JdLbQeBWjzCXn", - "LVce5gDSjAYN8BBDYrYNAeJJj2gYqs4gu7", - "LYQKe5mWg3QWBvngMBD5YCjqaWuAoqCFEF", - "LYySS7JcpcdPcRWMmnUNLTfwdTMgLpZNqf", - "LZoTv6VVpX7vg4whaXujmySxRENuJgkp6B", - "LgUAkvrob7L1Z5J2renUjbVDoH4ipPtyrW", - "LQz9t7uG7mH3CGtaZvUwcBS7w3qMXnVYvV", - "LXggGj4DoRu1PoRCrS4ZriZetn423kNjtM", - "LRDL2tAEDY5Cp21GMxq7mwSmhtCvKHJYmG", - "LZ45DzMZNQoNfeQvVDH97Bbyv83zM8b1un", - "LfLkJ4ETAXFDYdguJYkoumr73rNnvCpRWn", - "LNeH5HhJrcY7TPH3cmJYuPFLs7MRZJhxS4", - "LRwMLedPqzZ9TnqXupuvo8ZpPU775hxfoS", - "LeB4sMZQRFPCNTaov66FnJcMzznzXMEcrx", - "LN58nMYRVDHZMbCYfv866vstjpQYfg7P6L", - "LXbCqAhCGsN1mwTfKmPB8E114CiQfH9csi", - "LfCUv38QDqrJcrf6C2cGTT7cQ8v5BgScUd", - "LTWee5WMtpDek8AvTJJwQDKwUmv5P1kAMp", - "LaKcV5pyABMRWfiwZv5FiEvcZztcmEp8GF", - "LZRoQd74qGjvnoXHLzb9Ksnw4W2v3MD7yz", - "LQ4AU7TBv7NXwn2NJddxgDhtmFr2J8senR", - "LVvfLWXmQaL1YSByH8tVfJkqFCwZcQdJUF", - "LgTpi25reQc7gsHRqbhTpJnUg5ZHWxWc5L", - "LPUjQfZhzEXLZys6EQqt5gm4huPHk7GLQf", - "LSXYAuAkxcrFX4pjdM7mRxFpXUYVHLWMWk", - "LZnVdtGa6YxfBMUDS4zrNLT72mon7dKqkE", - "LewBHdJvLCBfsPSVWch7y8bXFP5F3YCqUF", - "LfGqWfaNP26H4hTQR7h1G4xctuZiyrT8hP", - "LKVXqiteTcAaRYe11oQDTbxsjHNqPzmFxH", - "Li36uQm2C3ntDq35KWEkds727hh1suXGYC", - "LZsrqswTkjyxtpZXfSZ4oGEHqZ6PSnKdcr", - "LcdTZFftQE3mufjgGRBdQ3aCD5tg8tPX5z", - "Ldj9MK45RTiUvqYjD5916vLgkLuZwmDGWP", - "LdCzymEQLHtXQnH2unAiZS7myPanA24vaJ", - "LWfaCBDTZoLfQjb5k1ForKMd6uwxDyFpkW", - "LRgbporuXdvyrKzdq2W95kkdwPEHjpLw2f", - "LQU4W2hUknmqgUSBZjtD2Ch85M69HARHXC", - "LXsnkb7A38NDHSJcD5jrhX8iqwFWqjnYFL", - "LgRQ4TmFMwW82VEPeSXYXKNZAGKWSHuUgP", - "LVcDDKskQD2TqnSSfG2BZG5ufqSmeagxfa", - "LaVv7yH1k7upguY14k2fbt3KfQK3zzkFgZ", - "Li6v55R1F1XeiZZNQ4RywuxZsfssgFHtia", - "LZLp73FKyUYkN74Sy384g4EVUfsD1EhUWf", - "LgWrxX3rQJwiY69FHa93JeW4xsvQgRWxBo", - "LQWm9GY3ciBWCH6bgiNHmPTWmLM5oKtRaH", - "LVqgHt9vUnTB9ZzUYNYxwVariprCbSAv4A", - "LbEsbXSqKZ2Uz5zQp1PTa5UXab7ikXTmSw", - "LaLE1W9XcM2fKRENigadm7DvB1u6FvGgqG", - "LP5cJdt8gm4uB7wyHHB8s6tYmuKyfcPSJs", - "LgfdaifGiyQEzhdCGV35Fwf9536CbaP9AD", - "LMt59pFjsdLp6MGDQQD9VRbHBZmatnFRg2", - "LX7e5JY4yfQGQzcP28NEcZzrzzMLexFCc5", - "LM2cwqThz5yYcUE7b4D2Kx6V9xsfX6135B", - "LVTbHaiMbuK5QJ9iezdwbJXkZxXRNWXc5G", - "LPkSDgscisHp8BJXeUztneoRNbkxJuqhMm", - "LfczRDEX5kxcyN9aRa9S8LD82QrrxMxR2r", - "LLmrgx2pGLQF7iGva9HookfnRgYDXxR92o", - "LRSrWxxwVDk3TtQjKMwaqjJrvp6PEX2hq6", - "LdsJaut1n1oALn71QDHeu5MKCJKYsrbJWL", - "LaTaJDeBb3hwYrMbVd7KgpeJA2ETu7bEwg", - "LgkRjuxxpx6Z8cTJ6A2ecaH6bCg3KvTS5U", - "Lea813yj7mcDEDyD4UxhjYjapWiHTkGkSC", - "LamHNJvYsnuQ5rt49a3mmyttGguF3hLyTq", - "Le6FcDL3NCapwafw6CMcGpnQKByHrzusGq", - "LfnzRrWTnmLKfXDFSKjgKbrsnB1iFrk6yN", - "LZXDUyfNwcfcsNAdr7yaWeDKKsq9YzAK9V", - "LXjcmGbwimUKDb85payAmrQmSXirDf9HJQ", - "LPnJYTcNKBM69Qmgna6yf8FMJqTvSAoNt5", - "Lf37qavaRCar4Wj9Kcqvhebaj2E9G8JB6q", - "LWjJHHZ7jeWXHNoS8SL7UEyd2gcSCFoWnC", - "LXbyzfyrmFonXmBk5GkPxiaZ1qyUZCxKuX", - "LKgfwR76E2YaH1yi6UTV2z3WftBM9pXr9Y", - "LYy8Nh9XPht5Mwnt8624gyc6axhFMAUAxc", - "LNpmSSJuq6tzt5tS9dZNJjuXhezDmUorLK", - "Lfv6agqBwMsFG3ANKvPysuJyQUkwp5XN9J", - "LfcDkWUBDhWVmF3Utf5Ztk4QqG7aehrKW7", - "LR9FK6DZn89Ye1cNu8QAtxZAVrT8wYxC5X", - "LLNChpKyyWgerYTKVfUFk2oeYUFyeZuEoM", - "LRGDjdUCZ4sAgUTK3m5gVDJA3R3rAY1nW4", - "LYeEZr9GdrCgei1RRWiRF5Uyqsi5i63gTj", - "Lh46Rk8bHk3JxomWynjBKzpJg4S6D4s2NE", - "LKtFkG5xJKdNnejKa5HxeUX7y1HAsKZgNP", - "LRKoBygqMvJYmDK8nDpSk5nWZKd2xxzbch", - "LKYb1yherT6oxUoMw4MTLyV51gJ1M75k1t", - "LPWP6xysXnpNXj8rKKPwKUKawmxymmTZ5E", - "LRcoYZSRQ1C5BLeCiNC46xFWhDPQAK2u9u", - "LS44nek3ytjXDxCuKDepHzPZb17LgdqVTE", - "LiAfwhdiwTd69BznWPi2nLjnsGc8aBH3J4", - "LYofrTD6GZZvC6vxF5iKewLm3Xd3iDWz7R", - "LXftf2DrbUVZBC7Ty8S9rnyrv6PdadXCZJ", - "LSazm4eUAaE3BuVVdL4q2cMhdbuwzSMmcA", - "LTVee6L638kvdPdYM7MG2V7JZZKCBwPHB5", - "Lg9LW8Q37hwbcX63RpzXBKprdei3Hocj4f", - "LWx3avsgDwffYTn47x8qtboTUM338c8CKJ", - "LT2PGJPi6xhzeNopVwWntRaxfUAxJzGo4n", - "LKhsCQnPVNPjLWPw9AesPG2rsbYaEbFfVH", - "LVeBNmWUYGoN35MPPb7eogwuApwosVXvNc", - "LiYzqbRHxSdU91kKGHRiw3k8UrPMKV4NXP", - "LPLFKEmRrMkKThEotv6LXSP2ujgLAvvhhG", - "LWuUFRwniKhHYGvpCJhHDsYmiqn6pP1NeG", - "LhrmTf3A8wN9uTG3G9kHzf5ogXHmVM744e", - "LcTYdKZvrwVgkHMsmqsLyRFVgUHJrXLSHQ", - "LVzanUyN8NiSKtspPUCAC1FHXASzEZJMM8", - "LKNCFcQue4RbHkQimmxByQbt6guqi9keYz", - "LZHi5sTPdUmCnmtCodNR4v5NG2BwAqCGHX", - "LgYQybURgdTLSZ7C2B1MhHWyfm7vt8wvW4", - "LdKLBQngeqcs1km3T6s6ZiDVZsQNdKo4QC", - "Ld73DCAtAiV6JccEsMvojGmtSNza4SLoj2", - "LVKWHWqE7tZp6rvmLhVFHcncSpQ5Mwf5pY", - "Lb7WtrcZgCYVz4C2ojjhAea1LWGX47p3mu", - "LU2w7DZWzuSsiSv9WxC6czySDdsTGtyhVG", - "LiMR9FFdEMNmcygQjfaCsN29WpJhQpwwub", - "LcJCTXGhoMc3882LDLWSvVrSvchgcrScee", - "LU1ozeChHY4Dc7tCVSxWz9cqptBgQ9pJb8", - "LMy9TFp2vUZJh1aBA4nSFTnk9dQrRub9aq", - "LexvTJ2Xva2Qx91Br3A6doygTnQrC3FaHU", - "LhJSxkRoXWB1imF1XLTydFGjthNdpMrxRC", - "Le3zA44dL1oxHjyJUySodFnhrQAEZu376w", - "LWdFGJmNPQjvdi6o89MDaFgQPLfzgpmvmv", - "LNqVqTuAi3WAnPr8v5UCb2K9v5xF9xVkUX", - "LamEb2i4o2ZQU227JFYyJ4JaiYroLh1Hoq", - "LMxqjT3dU5RT8LuoXnrZfHQu2wwPAZCuXh", - "Lf2Kwcy9hAcRdG64wZYhga7yvajPkyEQYj", - "LNcA279mX7GqQXa5NfJK9EiUCFvTZUEwHK", - "LZn5SSYFXbMNvWXi6Vzch6sYnEVm1Loyvp", - "LdpvugbTixy8dGDYvojLKC3UxzwQikzE8i", - "LfginKkdNZy1VpXtTspLypqk6KMcfGRW7e", - "LdVmYQAcvtpJdn4Zw21hynWcMxEgNq1qik", - "LdiSKq5rU78uXzibkxXqwfd2eWT1oV4WUn", - "LUaX2jyjQz2NwPicXXqBdYzYowXSeM16yN", - "LZZszxqfznEeCxdTV5t7U2yBvdM2ZJir65", - "LKvSyL6FBZHgN7sL7zVcxKuaTiEPRauqXK", - "LhXry2d4HrVk9E2acACooGs3KgKCDUzL7r", - "LfvCGbTUtRe9eiKqJhs8PAGor9bwqjwEsT", - "Lc98AoHUML45SAGgpzsX5LYc7sPCWpftNi", - "Le6XXyQe8VUbQhYUTn9wFY761xpLVETwEn", - "LPs9CEQHL2Ef3bKuE2BxmfWDnJfTvdffyW", - "LUP4moUpoS4ifVBtEBswFghVWqLdZ5qTYX", - "LQRXpcSkx5d66Y6WWZWXAcQoEUYbXisyR9", - "LUyQxiJqBRit97K9wdiVC7z8GZs858Efay", - "LXcGn1y7Vof9RqCUgD1msBXX4TKbfN78hq", - "LNqQZNJdw8jpwkq7MC5A1Z2U9VPRhxCN7J", - "LNYpdowmDHHkV5kpmX3BzVBqSppSNW8Gr8", - "LKskwLZnzZqCFQunxsKPqsJka7WuW5Yhc9", - "LR15VwhWBw1pnjabSn3M3VReFGmYowrhoH", - "LMzh6FEE7t8QatDihJesgjndmKCEB3GhDk", - "LVNMUN8yDoE7TcbPi9NWDeUEtRj7zBnmD7", - "LbaZwbxNhHiM5vnRf65NTgtM3ibaZCiMSf", - "LcUHUzb2Whg2jww4jqgtq2HiX1cjoW36Mz", - "LScrZ92cYWGz6hszQTryFQcQe41hsd4MVS", - "LXdsv51o41qLX6hRTeScx62DH6NTrBsxP5", - "LTTH7rDBRNYHUurrFBoUFVJCKu3KTL4VEu", - "LhWytosG8nvC7hLDVnuTc1EqPsSKNSXW6d", - "LfdabKhvDa9rzM7JUN2v7g4JXUbvAmn5aZ", - "LRWd21TvMqRd7WE7FowMBU2ZBZjW4b36gs", - "LaEdURp2MHA2FevyhFj22rjfYZ1wU8Wb3w", - "LXNHmVTWLMqnavNwxHUgBRQcUa3NLHxQLZ", - "LZjcbwokoh4Wyv2zqUdjW78rDVKX9H9erY", - "LhFZYaLVVMKvAMGbfEEJBqfNQLsBiDukS7", - "LfMz6875z5cFMPpdgQuYDhdYJjbuo7ydXt", - "LQEe48oXSSiFsKorrFBtTk8EunQv4rU8R7", - "LU76iwcG7HFaw45XB8RxjrBk21DFGtpUxm", - "LUcxMHYwemdh6GFMau9Zr75pvik9n5jVjT", - "Ld5MF45i4U4kFj899L1NY5j5J55YSA6eki", - "LW4CWN7rCAeL1hhQENBSt6XqTvUp1JA4eg", - "LTuJrHZK8mALecAM5ymjVXkPinwcmUwUe2", - "LUA55GSL5RuB7nNqN6w9k4EHenGqRmpoSa", - "LRqPW12HWzSH1CHufFEThj5pmU1oaBFGjN", - "LZ5jF6KjXtMJbJ9s9rYu7LvUhvMWhqCF4X", - "LagEhMhov4wZmciRdamTGmGzwFziAuphY3", - "LfinWg1VzA8ibHWBH6mVYd4VHKr6NHtKq1", - "LRiAzfYMPYrU7wbg6Poi24b2rMR7fefWpy", - "LUsBVE46UVrEHXxWRDB7mttenSX4p2fdzG", - "LeJUwJfjaghuWb4F8rmUrsBcj4Az7GTuRp", - "Li1sLEgHeus5w3sYdnbtJZH7ySwP6mnEhV", - "LhDVcUe4F64Jr7WJYrXpWLWW3Uim5A99Gf", - "LZL5RhLRu5JpSXWMhfVkmdZs34g7zftRFf", - "LWt468oMQ2qzDxAYkMKiVwgSSFi5YiWtHf", - "LT39rRFHHCyn8QNYP6LasYkYCrovdRZWCv", - "LPfvXnzQytWK9kvcSbEJxPaSQuKYZDPsQi", - "LZNYir62U9fRDJuPiPsrKDEzaDePhJtJtq", - "LehtRTWzzkA1zFCtKsrFaK1KUd9dw3dcev", - "LTV7ndtzN3N51GJkvHbePbibwRHQpazpQU", - "LTysFerupYwh6Qb34WFieDArbtyXuUTDQK", - "LNopEK2qHxsgfJmsPhJbTXQuBzmBNVdADd", - "LMLpvLzf28BJgP75ydLw6NHUMW7svpKa6z", - "LedjHgZKzmeGp2ehx3bTTmRjGH4dDztcue", - "LRTUoFmcs47RCcuDPc1wfAJwawCx7DGhH6", - "LM82cAWrNBeEUmBDZiF8TV3yaa3SLdQm4g", - "LZFvxgQrSb7rDX8tcy3pA3vh8cRSAxvaHt", - "LMCaFyi2TXjv3CRddbt2Ha5eX3xEC5TBne", - "Ld2rEdNcSQRGJQwBnof6Ka9jTXKx6e1PUi", - "LYLCh29WQdSSLoHWj4b71th8MZUWG79QfZ", - "LSFy1ACArGPRD9YZEmcNMHxLDmnoPyTpes", - "LPptdyRYSBtGstDYgjwijD8St4MfSCfeSb", - "LWkNyMGdTs34uGbEXYxCQV4uhm28VQoikJ", - "LfzTvK6yahXTDmgVMgM2XHpodqJySnwAq3", - "LaxirQz79hC6BEWGzeNo2EvprUE7zW6TR7", - "LQuWoruvtJ6kdm2rAg6UotNhJJfQcgE3Za", - "LiKpsruQgtbVz1isYzEd2kCDezufNSY21R", - "LiL2MAtuo5DEwsLtgm3oqK7asQVuxmRGkD", - "LKrMYBPBwnQAibxin47PHXKVtp8FdWTWep", - "LQDGoqJT71et48KtZHw1M3TWt5avB9ZXX9", - "Lai2oCdmNfPBLmdEq8Q3G1KEwazqGBb9JL", - "LYTUbJDrrWSauohGCzpxkDfJ6XshbzSvYw", - "LPPNcDbgNVwQKak6pgHMnK6i9EdmBQejRT", - "LLX3dw4EEYL3DNH2eMGoXxumJFJjdxyxux", - "LfNkW8BozxTraZRFQ9kVRZYopv3E2JNnbB", - "LTbDScAZKeL5cnbrECSM7J2CEPGj8Taezp", - "LWX8kRsA5xwSRFVHKKbuXY7V5KHczcvQxH", - "LeoXAWCsYJB3FcYmhw9wQhDohKkBLjiDUA", - "LcNSzD66uvEAP31gcS5mcv1wyZ5ffsEzT2", - "LRkv8FEThwrQkxpf3jFkv8hY4Q3vonwwMW", - "Lgr7QtrNgKrS4htZyhmSp6S6C4jDX4d65W", - "LeJ54WUpjx6tKpBMyFKZbqcWFMAZqUUkKC", - "Li7b8UcA7jp7EMgQ8oNsk6JbV76XV6BoXr", - "LeadTzRiEFVBjjXNJwb3T7AW8ywNkEVLV4", - "LQHteShBs88McZVeWsdyqztHLqBGxdf8Yv", - "LVh7GPmjryAyxxEu5i2rFceAjEeAj9EP79", - "LS4d4NxrqeTUBjVcsaTLYtFHoPWLe2x7ir", - "LTeEzsHxdrMtE2wJdLnDtmJVgJD17U8bem", - "LhZREEZF66XgJFzgAnyThSBbSeoKbdMHBj", - "LapLnKbab5Rt2Twnz9whwsQN1NKnbTETwp", - "LdwcMyu9ygA1zfmDNPgLDVxG9AtQeJCdXV", - "LT6DRkw6XcyXf9pHjqCFYC8zic9DATfE1M", - "LPMVMYFLER425fqpBRghidN65MbzV36V52", - "LNegG5NGKkjTxR4V96qJFuEf7CKkDD5xgz", - "LSg3mgY3FqnvjoEWLTbc46hFPQLMtPCapg", - "LUC3B4C7uVaeScH26xWS4xBe62dv9P7P4k", - "LVPrbqT2JesNv8thx7TXNLd85WNBFyoDxW", - "Lg2SDwobLEhMGsxj1AUdV9AYBskz4a1f5N", - "LLZawqtAeQZpqCwfQVDrJnaPvTxL7RsZhR", - "LSBe3yw2mcrKYkx53yD8daJs3vLBoinCzi", - "LUYp26MjLmWJ5SWaHpccdTx9SYatEjCRhc", - "LejgVcq9oDZZrspvKnrkgeWCfMXGTQ8XGE", - "Lf47jyLKhphuoeuhQaBpy8BSDtP2b8FX46", - "LacqTKQcZZVHXamrtyC98BiMUqGFjre2jJ", - "LQg33sm4kgBza6u1oTmmQC2gDQzi8xHQtp", - "LhQSED2BB5eD2nxSnBuH3cSgtpGRE9Lr6T", - "Lgyym4cr9jyZ6DuuawsH4vU5nGfFJo46hR", - "LRUQb4QTQoyHqGnBtmxmGUmdmPEiBDaHFp", - "LXa7DzWMdrT1ZiPerKkJwxZPs8KGGjotNi", - "LPFvTTZGyPWTzWBniUeabT6PpX6Zzi9D6y", - "LevYJS22KHCWb43JbyBHtQC2ktEt1DnB4M", - "LRLD1D1eHEeEZLWEkq6usamBtkRFMoMQwK", - "LeVxtaGA6CM7m6aC29wZ3hhVqU7QwpiDkL", - "LTMFcs6J2DrhSCRoVJ4VjS4XsbAjtyLFtt", - "LSs69ihmYuq9ZFaZYY64dybDvcRKJ3HT1w", - "LLV9CR2EB97VojFE3QG3sUTswxNLJZ5Zzz", - "LNdPmx17qQ5AfMcThrSKxgg4ats45NSixj", - "LdCBn39uDtnyMUaQrRK8J6M2FvaYoxNvYW", - "LViEH78zotsf5NFSHC6rM1TT7bVXnkQmEK", - "LRwMVqKphLBydypR7xkTFvb5aBibgLcQUq", - "LYsywU9zXUUkmdWajphpEwF2eiL3ERC3Ss", - "Lf22WxRh8MgotGj6QNtsmwBjTi412XedCz", - "LYDum7F3teu4MPfN5TT8sxts7Dq4U2ztmE", - "LbYwshK9qHevNBGAR9Hrdj2ujB5vRq9A5r", - "LP92zovmBBPyfa6EffCWuToXAyDtSjkyNx", - "LLyJ4cDBiJ7mZyi7BFVePC8sKr8151ZLKg", - "LeFScySfBCoWSgvrzkTiUaajX7BHbJ89KQ", - "LTJLMc4yHHg3Gtxwcud5G4bsHgeKQFVTkd", - "LZoMaH46kzYykGStsqETL1gECWAxReWqAy", - "Lag9gsk5Wu9nnyVTLGex9hgKVVv8mBMdia", - "LX8ouPBgk3HemjYTREseKXEZCXuhcRcwZg", - "LRa7BNU9G5wjsE2KxoG5EAg3pWs6wfq4pt", - "LiYjHFqzLsqKvZx7KCCH1VKYV8p2ckbM3J", - "Lfh9oL7amDoxWUZfh1YyN38Dmij1vSYEnm", - "Lb7bTy3kdjs4EiEEAL8iwWgcZ4Be6bPtw6", - "LZULtWkQfjRpSLT1LigDM8126bGtDZQyZk", - "LfMuKSyVh5J38GY59SWswG8prCAucpkcGh", - "LeTnaxgTXm84b17EwJmgovW6t9RADsLiQJ", - "LN4VtcfdHTYmoacP1tpfP1w2uW7gRuhTY1", - "LYQDSnr6hMBKd6x1jky9gPA5NePEdTKMd1", - "LSk8DivGMiXGUy4NqvyKgXDUz7tXi7M6Mg", - "LTuNGVBmKgfdoc13ep6xkQq2qBdAWPcs6L", - "LR3fRTDAGuCq2wJqyQBTuKzEh9TqUo889t", - "LiQHGxYYQwbCZunneqavSmsKfAy75rRpje", - "LSf9xF1KKvWghwc59dHhKg4qdDDQLMe2Ry", - "LPt1FjgHvKzG1apoxvizRp2aTvvpuvbNpz", - "LULUas7LEucL5NCKo85ZoqGwqSWXqfKPAC", - "LddCzGVTs3gC3apfXxHMJDt7DN8Ay2d4z7", - "LRXqDsB9QVAe7JmaeFZxTV5yygQNsTPqhJ", - "LNXY9gjCUANfL565MvBGyB9o1CkkqPxDJA", - "LceXAS6mEK3fyXX9tdbxLREbzZDJGnkrZg", - "Ldo7eHSWqooZXNYfoSKB1gKPWauXGBwAvQ", - "LcqWVgb24TQyD8AQR61qGjahj9Uwys7oqM", - "LULdzyN1WW9BDdTc8hCVS6XHUtUVFfH8XD", - "Le17ydzLqYMoFzyUuVBSszrFMV3qYeiPcH", - "LPbCNLF2QgdLYkrQMNx1E3a46Sm789uGeB", - "LXLap7XRPWGGDV5NW4kAtj9LATqsXmJYbt", - "LYoGvagUXBTkyhxK23tuzqXmkLgE57Coep", - "LfWdzyNfwjEhbJCi2sg9j35RpDXFuqLdyP", - "LTgDa6Xsg63kvZ9pH2wYwDCqesXo5drk2J", - "LYDVpjoYvyEGEtQSzwAssuycoS7VEHGnaS", - "LXv9YPQGQQxxHwtXY6kvfABzo85A6pB6oN", - "LUNRSQeq19vnFfpyTa95RuA9UanSJ8KdUo", - "LbfVboAyyKXQTF6i6hLyoNzKwsRqKFVpyf", - "LN4f5Pg5dShTvTEZ1jXHfZKRuCrSeQHCfJ", - "LgED17g1BsjGx76yPSWaAXYTFf9Mo81L1t", - "LcRCefLQqwHrySie6WYDRwj1iGuHez9wU5", - "Lg2ngZXMJbBYW6RZ5SBRL9w8bZtVQJWQFg", - "LZP613qoCYLDMmVMnefw5RuSt9L4o1YkfR", - "LP3sW4vxFPuyTJC4kw7hhEKqa3pMAjfpYE", - "LhVMmYc8UZHj7KSKPSUsVJyP1GyKff3hgU", - "LPzPycHukP5J95jjiUx6vYi2FhRM95TPi8", - "LbenvZKkJwhCULwRCCuGgqR26rFMZE8Tzd", - "LXqajRBNYWChTpbSoCd2mVtY7gzfzQjPnz", - "La8om9H3kVzt1KCKgp11dURgxCaTqLj3vJ", - "LNVPL3UmQrdbr5uybGVco57Lr6jx6Gqa79", - "LZQ5nJckGbMk33UMob4eoJgi4JJFb44XQz", - "LM5zsEnehjipDxhdNbsX3umTNJtvGgvthB", - "LSTDwxFQki1PSRmhBYk3gZr2qPLbKcPKat", - "LXzW1U8Hk2YKyh2ecTGvxthHocLra2UJDQ", - "LS7sLiEsVasKwbtoidXGudnJB5fHDBNYG5", - "LLz58af37RiZrik2uDPmpc2RQJyNJ1oj37", - "LRLMow7MYFSoQMpfqka5PhJT3Z9k6WqfWb", - "LTbDijjjSUuFkVvVABYdkBrDtRpEsuCMcw", - "LaV9GnFJykDwaUWn9YVRCiSDtUt5GVpvPa", - "LUYJZ9UpqCayVMx7hTyC9UmF9rpEiH3Zmn", - "LgPff5EGamXXnfzHT3t9b3PEqcyRwCRtPK", - "LW4mo41eaLtGg29UzvqNnUqc6yYw6HevXb", - "LgfysTaFcMehG264PmjC6rFbC2PvuSxUs9", - "Ld73tbj9WQokQfyLFeVr7Re8XeWW47FPu2", - "LcAUmE141s87Z5AmDvJjqWvt1Uyqp63onZ", - "LZuXCswGsQQBo3nQaPm9e621GDa7hNPQxj", - "LNc4FeoogJW81pTJoVDjXiBSYsbqdiP4dz", - "LL2prhA3kDjtV7HoTp5hvaotDT8BU6BESi", - "LaQBUxvRXfJSEbpnf49dxqhCeSEVKm2gsz", - "LTmX4Mv4bScp8z1FgW6mEtQavN1c86pT2N", - "LTMWeFwGFtNUHwKvRbReoRSRBRDerNRN7u", - "LLQcxwRu3DR2VTue5oEJL83RbLEh2tML4i", - "LgymiHL6v8pJELfkFRqnWcmQGqpA7T7Zmw", - "LNijHxqraiYvkw1C3CXLvTDJopTWKDb2q9", - "LcPvRMUBNhciUj7ZDinmNMvd2mRDZ5f8gC", - "Lb3XSRYhobeyGGuHyN5uPtzyPcakvhQHWH", - "LbWLi2fZvSbNUdpnotbnfp3q2iKmapuDF6", - "LVeGB8SrXQSF1aD3tB4BKjpbf3mdBRTXtu", - "LMEWoLtwXBhECEhWnpz5frV71EuehX2zHv", - "LebnT4ApLCju1qvxCrJJsP6u7Jrdt3YEZu", - "LQSx5N8jfdykN13tzo3d23HqG64f1bWon6", - "LPk1KKuwH1S9vtyf7HM3qP78UoEHrAnEXX", - "LU9CaMPAgco9wRxc6Wm3FYkuFEzhYX65Ax", - "LSM3bSPAhW9HN5FtRn2Ea9DLKRayQyQx8B", - "LUgSMFvNQzjusUna2Hcz9p75bpEfS9kTZh", - "LesW6eRsSEnGRV9BJjuoDQL8GzWWoNGZek", - "LaCMX8KkRWcYknMMzTrrB8DxcoY6x6XJQt", - "Ld1DqSmpp88VfYX53zo2wZVs2wdTSQj2cW", - "LaT73qYEQ5LyK3YcSFjenQP6MxJMqTLqBc", - "LPMo7n3E4TzPBryFFoav1zfwaTqEU7fkDh", - "LZTefH97XnNiN2cFVUS5PBag9hqHStFBWq", - "LdyN2MJBUfHGS7AHJ2RAnhhK8vru3EirU9", - "LgEsMgND6pxSiwGWtZyETh7aMniTUHUm2M", - "LLPeDekSGQPpfRqQZXfyHa9jgvGqsihuG5", - "LeYgKYNJqdLyD7Jh5EygCSVxsunTVKpiVC", - "LU8fbEyGN4H6gn7kmZDyZPvJdA74LxcVKY", - "LP6TfYe2nRegNzVunREnLxapK7j24HFcws", - "LTJL5QrbrR3zQQuiPVmfPba57edCQtHUp8", - "LWdpr75az3i2dhYGdELRNjcDdG1iiAb1gn", - "LXgkzBRxojgFrvTXm2qeFXN7Dr75rr6jfy", - "LUxero4pkex6r4a4d4JAbJBP3eZVj75F1d", - "LdZKaRExyPhkYPeCZHwDgyX8zoZQJkoUPf", - "LZYY5aHWStB76pPXBtZtkRSUgpFgnBBKQs", - "LdACoqRaJTgxmcPPVLnQpj81mrfvE51u8K", - "LVz1yNKuRyeynTWKtjvhS8bhi8CHtEnzjs", - "LWaVX89EdNksyEs426eyez566uP379jvAW", - "LQPdTLAvpAndXopptBQ2EJDkNkswusweEx", - "LcvjRsJ2KtzbCVCq7dUXZr2cBCmPdVK3hj", - "LV8Mh3KS6EhJhHdDU4b7ZbG4PjkxhiJErV", - "LYJgrdQUV6rUQd31o2oHyeLAJ4yp6R3eXP", - "Lck3dU9y9wyJD2fSfK2dqb8fDwpQRWju4d", - "LS4rnegqmi1i4Pb3N2EPkvP7za8e2vR4gz", - "LTLvVRGGH2yMFmF9u9Mz7Kkqq2AwdJEFLi", - "LKLrHVjWYat4oTLhshmYEsRiCfUspcQR7u", - "LaEyfX2Pd29mXGT6iGzqnrr7uhq1UYZAHm", - "LZwXMbe9qtRWiuNtph8HcALSDzkmfWW6bV", - "LRbqTUWUR2fEJHSgRbJVjFoq52v2C1MUgE", - "LQcGFesvduneESv7HEtkfymaXeH7PJJgC9", - "LQBHTKspLyrqQ3hnCyMr1q2mk4RrT1BFKH", - "LNGjuBnVciDGUTxFQC2bB5gvFE3YqPzXRr", - "Lh2kgp3553BcvCHUsxYjUvrotjYHRgjXt4", - "LhsEHCMaAzGSyvZpCj2V4Wp1HjcwMah7tU", - "LfmRHjSNDXaEmLHEwz59VCUxYfYsh7u5kF", - "LThrqXBHdD1x3uby5J5PCUTWQTVshmdwxa", - "LMHUdMwpeziokMgKSWsuVeLKQryBYtCfZB", - "LgcRVwTKk9jYyvp2DQTsQQxyoyhVphrdVK", - "LdrXpd88kH2v1BWwJtaxohk8LRnr2TEHNm", - "LfC5NbmSeZYmiKvcQKrmz14msUAKH8ZrMk", - "Li3fNhb2ASsoK9jKJZaWs7EBF4XbZbWAv7", - "LhENViKDr1eDcCqEUC7ieQtRkydYABFq2r", - "LUyueRX77ZDtVgXD6G9FWjyRikeUA2Tx2Z", - "LYLudxM8vGRcQaxcQHRn8DzdLj7AAD68qx", - "LfDK8SGgD7aKEEB62iP3Yd82SvtFfip5uF", - "LQpdEHvZKFQF4rCUTy5SnqH384pvvZ3Y5i", - "LdfMWGVYEdDtyR2GHy8Lggz95dQPSaTZBD", - "LdZqGe9HG8Z4jv8oY3fM6DFWVYXyaEoYnT", - "Lg9X3fqua5Qvq5npLAXDfzF24n7jsduFut", - "LZvx36km8ayxZBZkTUDTm4FiDGF5gybJxt", - "LNHE1ejNH26YmU87epxg8SHLg65mrBDeQT", - "LQKSBKX9JwzQgM7EmhgmioYt9LL4N9xo3a", - "LYBz9R4vX3TPSTeoYsKPmEJAAYNyFPQUdJ", - "LUG12kwRamFNEvuWd9X2sFPrLBEKxGDDUC", - "LQZW8XVnYK5e8P9met5drCTj5dpnvkXUDf", - "LXEHZe4KwTfVfS7gwoUVuWsKtSHcsj1fMg", - "LahNS2CDM5skx3AVDU3PqeZKUc3ejsZJEB", - "LL9ztroxHkHEM3AfaaRnWR4GyBmndAQN8v", - "LT8wtacMnb5c6uUjznGgTFAfaaFYmx51AG", - "LZjUrPkpg93F2UUdCXEekTEjBjx8m9sitJ", - "LUC1TrjuMShNaYssEFVLhpBiUaL2sMjpZx", - "LaC2ihRQanZS9LogNNMTAuu8HLdz2uFZHf", - "LfjTwoa3QDxSGMVR4yCkUJ7QV4QXtxswqX", - "LQwd3fFW6k5VoZ6Bq49hTeVBzfQZCzBWEU", - "LPwJsArmye5XaM9xwmoufxPfB23vBxJcm3", - "LS8TFTCeLdB8qxzp5nYF55XHsz2XzRNxup", - "LLy4pPWGkorvB2Qz8wB5tfpP5tzmcu6bxM", - "Lbv5z8qMXWyu9ze3jQiv7viCESorzsZcXG", - "LQcRZZr7EHboLGMSW1VmDvcjCGM99FG4fn", - "LTg9TwJ7XZaawgmpCrVxEvvdBRSe2rtmDU", - "LNkUBp9keeahZzUdzSohNvG3WB5U8NujhJ", - "LKGqws48yQFuToQdGcfK4prxVmTTRzxTrX", - "Ldv7f7KUtb3Gz1zMm3FXS9LSuxx5fVefxg", - "LKJRF3ViVB5uPw1EebUkAVeA11HcMCT1L1", - "LaEEMDkcxr5eKuSf8dPNR7VH3i4aSKESbE", - "LdbUTpAvr62BTB5GnrDHdMfkwe1q61TGUW", - "LZrETjveMnQPzYZpbRW9u7DxoVTT3tLVAq", - "LfDZFiqTJDG6pWC9NwD79sr7ig6XJaSdsx", - "LcVE6S6o4bBMgZ44UM8pUP9yDWJe5y44TP", - "LN12xLfR1riFERGWD2ehTVXV2SgTPEGkda", - "Lb9MtyqceeU8myfnDjopRjtMNPDCdkdYNw", - "LWaf4t66rYFyrwevvSxXUUf1ghXeevwnrF", - "LXqk4DgW94Jj8Ne4w7TVS2cbrDGzFxZNxU", - "LhiFe4iNvqdFFySfCaKcz8LUTDtQsCpsW1", - "LPSSyTfct4eky7bdGaBTpdy3hztmMqMXFZ", - "LgcXMbZDRSE3EigmdkpPJJGrCzuEHVki5T", - "LPApGJ4vcYwJ3PqfrMcCstNjcZ3AZdJWoq", - "LNx8vM4HSHySnZS9FaLP7frerU1b5EXGsB", - "LQK4kcFKcGWJM6dGk2rjPS6SroH2HFfi1C", - "LPco5V5H2FRzqvE3RyA92XdkPKMvwJpNuP", - "LayMbyFpKVCtQ9nFccxcfhBya6qVQhfhbu", - "LiPzSX6U6Yn7EhGPwfEYdjhxyGnUtQQGJZ", - "LNxATzDKeGGt2EhyA8LwTEXoYjPv5iTXE8", - "LUYBZfFzXMNcMaFvgeQT58DgSYmf97zbMF", - "LMbym2ssdxWSdSQ3mbniW8cXhjigeXD5Kc", - "LhUTs87noKSP4TbPLcBddGmFSZpJNAwoqP", - "LSA2jhkmZgwZWBJfdWmdM86QkKAfrYMTP4", - "LYbWxSsJKzi24EGjz1i586XwickyAMBHEo", - "LL5U5BbErTwAxHkNhzgpPpknbgdutAbJfa", - "LSQ1RLD3PtazaMAi1uFte1f2iS5AhGmmTE", - "LMHMqXh1ksc2yU1L7kmSwhmVPLe6n3dWLC", - "LREYqFbYo1ZpmZHagSxGRsYneUJbkStZwG", - "Lf6jQQ2xXF87TsyqpYMPynTF39e4DpnsbS", - "LgGKHMPigMHGTnCFtRbma6nHEs52TDiZkR", - "LSyEah2pmQpKWHW1MsPkBUukv3JwbLFbp7", - "LPMwBp8Mb6o6332oYNF7V2BHxtPcB6xNvV", - "LUAzns7WnqjVRnqjh5Zu6XqjhbzYDbL8jL", - "LLheFGLodSJT7A45C7me94sp8BzcnbtU8X", - "LSXUFKZTicrw9LjdxE8v6RLyDgpaqEQXT8", - "LeUHN9Y1Qe6TdRX3nU7yLpEG1EKSQ7Bzvr", - "LKkw5wELgeaF371xzrUwYriJGu4Enpoe2A", - "LVnRUBuZJ4pWZMUJJevEZHaCqqZvKayZcj", - "LMZMDKE53tZa81MbHW61Lqnx8UcCFQrWdK", - "LS5meyubrb6gpnh7ScfANA5EA6TYE3hgAG", - "LaWf86KLNBi7xtonjoxNusr7dTYJM3Apsb", - "LdWAisbV4u7ufsvZc95AV6nbedkqcDSztz", - "LLewMMzTp9t6buyCxxN1GJKwviThwQuZF9", - "Ldscfk9QZhVgTxcTrpqpPfuh4d13PBk5LX", - "LcUkF9tHB2de1jvv9XWicqCSJH3boGuvrB", - "LVuh6heCKcHgQAdAcQeRa3BHNkYB2SN1sB", - "LbzcWGkCztHWSHrCWZGEK7gBwuCEYBS36F", - "LhTWof4eFXp8b9fiGXBNChVv7F6cCUqzcp", - "LbR3mpr1aYMsRBV35Rrd5tab6CSt1Kok88", - "LYN8VrfVT2KKNECnDXzMrmMsBXTKqT8qUD", - "LPdALGaG13ZDTyjos7ZzZnFKxwnoiq7Vvx", - "LLYNyWwRGLZtbhvVtwE9KaVgqNezmU834F", - "LRQgcZZDGA4ArbSY3t7dDMJQiCC7fm6Ngw", - "LNcWTb8vVNtVcdt8ePdVYtKuceywXGexnd", - "LfZygy36TzCJcLB15gj6iUPF2PqEKkqfjn", - "LY6b1KJ3PAoYEJG62FPsVygPr1Nt4dLxJu", - "LMpzSmo3xMdQu51gxD7AhiFfTdefjUGLcr", - "LePET2HXDooy9HV5XPCfyJb4dSz7btw7tc", - "LSF2ukWRZ6KzXTHYSdDPPVBGkE7QvpyCN3", - "LVNaL3VNted2bwyXkopvm3MSi7BeGNSgc5", - "LeJcMr4q6QqoV3ZTvi4uFxjvopd4Speu52", - "LX7LDTBPhr43pNmpALrVSpZuq3qp1WiDiW", - "LbGqdgDvKayibqwwWBUbc4f6HD8xkns36n", - "LL23kEmGYb1K3CRm1zLSjEfb5pHHBbh1qN", - "LWcz95wSBjuR3AGywLowEmFNFcuVvGU5Qr", - "LKs9Dy2fTeVGunYJXBEqdxxmxCxanG1KSM", - "LWAR63677sTMBh7CwGUgLF6xHrH83ybGgQ", - "LSURJv7fyEnibxuU5c9CCQpjZz4FyEVoTB", - "LeFrisey4bwdBXEPBZUnvsyhvp5R6HxjbN", - "LMhauutxdaTBvNBWt5Z3XVhGE72h56m3Y8", - "LhuS4qaCekfrsac8dW2fsD4pLQm9rf7AkU", - "LcQABUJC14zkbVKvgYKYAyvaDbcaEs6aio", - "LXPvebPoLED5cVyF5WtkVfX3wkXnQvtBFc", - "LQ6VBRdhtXYGsuJz8gn3TEJ94G7tRjvsnj", - "LTY8VWCMTLpkEwMu7GvXJe6Z9G6fytkJBQ", - "LRDR9HF7TmC3W7Z4hCFC8Frwou4qvs5MEG", - "LhGcqqfHd9VPDXVLjnkcisayWqZ7mMWExG", - "LcMMWNyc1MkwZzC896Yf8s3w54XEKjHJRk", - "LQMzwTguqtKaNxGCKQQBf3TG82UHwuhCet", - "LNcbiXmJH518LtazqSsYRNxzKSntJUrwxr", - "LMvJZzJAMShWmGhbqfbcYvFjjkkbjNBtuE", - "LRknefn3RKyVxwKkjdum9us6jerbRUUEYa", - "LSyXCZdZZ6JFQx2eFKzVsx8fCjajshKT2S", - "LMHGHtU5fasRisPz5Xd1qSZR5X1JpSME1S", - "LNodSYeXtiLWvakGXrP7wHsfu1veicrBp7", - "LhJXTJpxjUPV5yqqW9UsMzxM5dMa7WdVuE", - "LQ3GjdVVnbBoZmHGupZmNJYB1rdhACRm73", - "LbqpU58UNaUh6QptH4a325fEFHibXtisEi", - "LeLiUyNTXYV53rJzHBDL9MCeZwHcYg6oer", - "LQfCDij1hUNhUyQjmjEFiS3awGshk81W1j", - "LWES5WWwType47rjoqHTQ3cxhDtn7UKS17", - "LNe2VSdTEW5tWyCsHBtQi7ZEzvqwV6AC6F", - "LP4j37grd1NTRqdsNMVvPhH3YVPxRey6cd", - "LaN2VnxNMswM2gzJP5JBc49UUvNW2hQM2t", - "LZFVajmyg5S4GNmVxZ8w96uUoCuTk49SZb", - "LaCXjjfV7yvWFuoxq4MbaAA26HmDZ8ygnJ", - "LgKanQNSNYEKcx9mMUXGiZEe4sGvDsfEf2", - "LZyL3cG9DFZPShwPNrY5A7bFFC9REDz7nX", - "LUJMcEyxWi4H1t3VJm5p4MvtRNQwJMAkaq", - "LbJdtCU9BwuLadLQrsGw5LXQTw1cofCQUo", - "Lcozgv1ePhxZJmtPhXYUM4mAqBsgrixfJL", - "LT1nhGV3ugGUsuQ5S3P7hamuj8FG1XzXEP", - "LRnuLSyDwEBUwN8Zjv1iM9f2PdSrJaXkiv", - "LXDfRLDT4aSaD59HBFiDLVoDsKzbKHvEG7", - "LYNKSRZU5NzsJ91k8F5d8w9n3wTXRmESco", - "LT7wT3iqV9VBfQUyETdc22YgmuYPpsz92L", - "LR5NkVi57qqNmxBTgNXpaLMyF2R1rSWfdv", - "LR7vwew7NMGS5TwY7nmxjENY3Tw1pAEpBn", - "LRQY6WqBwZPXH6L4HXQSZCLuPZKLoR2A37", - "LPjTYuZumxSp9L8o1PZvpnNJ12iiDrJzpF", - "LchWtmeyR2sSPioQ5uDvj5zHqgRiPuSnx5", - "LR3TKpkSdQLpc9uBfJJhshkc5YQWaeXzzA", - "Lf6kkRPHqxXPtURWyhV3NrSMQhvJxHeFMM", - "Lh3hMVpf5cW5iMs1a5ZSH7wamdj7Cu7A4J", - "Lh1d9VCvuJTH7zankijKBQCHT2TBTS84rQ", - "Ld8GhZGMzsvWrrrLRdt1vK4jQ17swUKVc6", - "LUZnNT1Yxu5weCP1xYmUQoXxJ2yJjNwvyB", - "LYuvKNFjNhXUd4PxRfwBkB4mLwt8SwM1og", - "LaR2Zgy44XV3ZfqwjjL9ZBXJjMdL78LKC5", - "LZssXv5uSuictUyhXtXFmPJT3i7r8WTaFd", - "LbVLwA5kueFzQtCvjLWZcicSACTwfpt8pK", - "Li6QhA1JwQRdooVYWQmqNY7ajapPNiEYpV", - "LeZLuU9H2YoSUcnf8oFZCoGfsnv1PpScgA", - "LZCwXzfqSZWSqUHhZ8ZvQDRA54TdwHkzm4", - "Lfw2PmMhY1s9kSmfGid6wRY4KeswmuT1mT", - "LgZHB8N4sj1ajAqntXEvfZC46pCVfpNWXC", - "LT9CRh2GCidjVSB9yDzhAYR9o29JMzFV52", - "LhApBvac5mtAoeRhyeM1UHBAKD6t5o337g", - "LffTmaekgdvzKBZarsTzQoooictqjyRfZ9", - "LhGbWvFi46d4RmuGZZDedbpeK5cpmDVKjK", - "LaDBbkQG2eg3oTGcJJcUbSqYujEWD8GxGV", - "LSBeVyUmAomavuAaM3NRAcWqBJSCMZkma3", - "LaArmQWVYATX1RqLfcHTuiEKPy1uAUK3So", - "LaqS7mm5khUqsMcoH29pcaxJnMTZy4h3VP", - "LPV77htAh4RvvJnK2poWuBJA1XKU5UuYn6", - "LbtB14P8bSDL7BBS5u9C7QzfSbLXSMuWQ6", - "LeTFdUsPNG9DpFzmxqdNzcRXWiUhVBFSje", - "Lap9Zp3RjjiasRKZ6HVh9PrrxTnbEzmQQ1", - "LRJFh8Gem1G27ZER5RDqjaEqMknkDAnL9H", - "LXPdXPJ2ejvQEUuKii2XmKpcwWGKReQ1AF", - "LV4EBa9dDLAVrnoP1ZJ9uPZ21QSnGsyng5", - "LMTQpVaabmQ4GJRhya5UdDbBYwCnnrw9j2", - "LhomQHA6tTsLcPEmm9XyDBKTUvmZPEGCw2", - "LV6zkyyQ4w6MTstKHGUTvp147qF6tDK4Hv", - "LZ7JjSm2ipzdDSS67TcDz6ypYB4uRRh5qb", - "LKmkBiyWJcrk2qsGpDLSpENU9VmFUhTFLb", - "LcfSw6juAquu7ELptLuWdGVkFeoCGBt7TT", - "LVVeiEtgxcAmZNR5ZS7EkDEuP1rSQ2JR2d", - "LdggQFKWBZWXBEhmXAa4jVqPvV626maE4A", - "Lb2Uf2AJHvMg5gHebHEnhEhLRnjtZ3AKKb", - "Ld9zAq3tJ3KfxcXoXPXmKPS3R5yzrcD1zt", - "LM9B7S8wn1oXUnPpYXPjQeWufnkNYbvXwb", - "LMRdhEeegHTXXwBSEmv3Gzc1fQ9m4bR7wH", - "LdhYdFtLHfnWrnBFCcXirMy6BUr8mapYxT", - "LWLCjJqo4M1XbYQSKrXfhfoUB4pZxMQsTT", - "LYM1ejQWpBhdkbEhmfJrQ2KXFyeB5U9uG4", - "LKkDW2qjDbR2aumXuWysSUxhPsyKsLYLLQ", - "LSiQ7Evxz4KmEpaWoMFsb4in99iiZqchXi", - "LgFdXHkjNNuRsnjvUFByj2y8RFKBwigRvv", - "LgmZeoFz3LFw3M6iH666sXs4efPPPFDFRn", - "LRwVbxqon6hhg4uKFFjsPdsRBcb6CuuY8N", - "LTW9LQ4m3jvpaJQqG9SywG2YGxTxc95XDS", - "LhHYXKgU4ediDQQiVKgFcDss1Faxj3dCwT", - "LhkNzhtVPqyTiM94eDT5jAGpk6rrjBWySD", - "LYQAfktqsZm9UXFT8Gbq7hxQHLXLvvY9Vw", - "LSWtD79xfEKQie9xkYpLq7GtgPRCosKtqr", - "LTfbfSNWmwayq8XcjVVvQ7L6RRv4YqhnXb", - "LcssJQC1XYDMTibztCZQmEk56tefgHWceJ", - "LWr6QzQCSbgCmfmiMXRPEDt3RjQL1b1h6v", - "LexyJFNrgcNnnFL1ty5mWvxbLhJvPQr6Ps", - "LPbSJPvBLwAsFSARdt1zEi8maLMmEG6su4", - "LcJUVq96kyxh4vYjQ2V1mLbRLad4oxckPu", - "LL7sb6UWoGaXZ3Q6Y81sff5ASdW7ziSZgC", - "LXP1QYfCQRdA4CdnUjUJdMxoK3RPCCV3G5", - "LRr5KW4SK2APodwB73UmJWzMJYKk5edKCb", - "LSZjBdRuru46fg8KT6eubsHPfibmbQNgSk", - "LVnGQcnFjRqXWKocqFycqthhejNrDUb6ns", - "Lfmjw7wfBTtKYxeF6yYcg9VFp4yyoCQzVQ", - "LTeFKRndXEA3DDjVENvqmwFFWYarRtScxb", - "LdcV6oRJgYv9Qc52JkYCepnA5pW923Fygh", - "LZ7BP6uy6VcbXa5vYeQwpiCHHADi7R3QE4", - "LhkatG3dDHy85RFmEpKuRv88YHJL7tx11e", - "Li9n2e2GYndUMgjT8YarNmptsqyy57nBCn", - "LLwKLVEKMse8WbFU3t5m6oqeq6yQzv6Vwm", - "LYLEJH4tTtDECUWTRkv2JNgGp2qnC83eS6", - "LPGBJqe867xF4tjgQwknzoVU6v2XpggdaS", - "Li1TcaUhEP4tkbudkWncfVCY1j1wMgPnS6", - "Lbg2zTeZBjm3pN4c2cmJ7Aa83GNgJPP5rk", - "LRYV3RzHidi7vSYqyGTco2cLuTvkKWjDZT", - "Ldv5gad84XcH6AntbxjGxZHzcygCzgsi3G", - "LQ3ghptDNPhd8Cf5N7kBrvTbd6wAnfVTqy", - "LX9UGoNbTQRQmfymnXvqLKgbTqPofo2ZQe", - "LSTizhmpK1xbmAB6ug729uLs5xp8dir99o", - "LaXB6PT7Ur5PfSeKQtizmeaQzfKf9ULHeo", - "Li24gKAxQGHhQMcTNGVFMPikKoGu5HoQNj", - "LZ2mXGuvoZJTvtYxsupvNdjMrUEfFVcs8b", - "LNWTCA42visTpGMMmKMs8EBG5P5QFYwmXw", - "LRMYix9UCn76pmB87daetkypi9rEA9BkcK", - "LZBUZ9WpmCr1ki5tWED6LPRgKtUqi6t3EG", - "LKcGMQghBrnAutuyB9oZryJzyxhnoW5qyA", - "LgNtTi7jnv2T7nVLtiLs7MXc9fUcZCM1PX", - "LRtL2QCJ885ndQRYcCyxHtohojNtQ3kwms", - "Lb3Q8crTsgPJVhF8hGocPadUG8L4Ko7Uk5", - "LVRgQ7MYCVQkGN96mywZDC7X4wVKnVScXL", - "LgFC8Azpm1QQg4zf1stvyhdmk1J3GKRXgg", - "LZyaVaUVx1S4R9RtvVPHmirBCxwYy5BLqy", - "LWqHKipQvgBJXFH1YY9Aqyvidz3JLsbNqu", - "LS4EQ4HUyMj15S7uh2owumLWarpMwzYWf2", - "LeRNyZ4chbEqkuDtfSqC47r6aMDnR1Gi72", - "LeVbPnsv8Kys7dcRaY3yg9infzmz7G9Hpo", - "LUEVtdDJ8X8jVdNiUEX1xvzVQxcsSZPkrz", - "LMas9g21ce1eyXByhr7Ngaygbfo45Q8N4Q", - "LiEwXax16TsBmJpjTe1w332TeyUSamnvvA", - "LeGQnoVFNRWT3tUJFo8SToE9KYy8Px939K", - "Lfq1tZYPSa6LJMWMCzEfPVjyMjnHAUPnwj", - "LhiG7enkC7hUoRVyDset6nG3jBmFAnxP76", - "LhbLG3xjaFjA16LxiWKiA3yz1UPuam2LiA", - "LgafdEoGQynfHUJZGHLVaQcrr69sHjDjVB", - "LWh4HzjkKUMvQYCmyEuiLhwEnzyedFGgRD", - "LhmNnMErWRvw9pBhEaGA1h71iBrxBe91Ef", - "LZdBrWEZYwevQWw5N7cfx4Hp2716tWEo2y", - "LYqypgTC2U1H7e4mWtmDQXEJuokF5SAcLB", - "LQF3JiSeVnhARcgCJHSNvzdqRvmJGsuzvL", - "LURqMivxZfqtWHaydfLtsUwpd76pjxBuke", - "LYvQksgCo4c9pR1id6dbKDwHPEETYMZXuf", - "LREa1oKedRNis7T7XJ3cNq9tBAXCtdcGiU", - "Ldk6o44cz6YxFNvmvYhhPficYgmLe5hT6d", - "Lbx8PM2ss9DcPYMmzZqFcyjyFaLJM6Cd4Q", - "Lc7TBKGbvX9HNsZGyhWWrXGCLzpEZ7cEdX", - "LQfigrdhHAVV26uNS3hCVRxJZtDxe3eVV6", - "LZLynLMfUvPVMAmNXF46LKeA8jfUmUBKxe", - "LWnKVg3QJta6oyMVNLo6htofgyRfupkA7Z", - "LKHnGKbzJX7e24QJL98f6ABtuG4MQjAX4Z", - "LKexRP5HvwdBLSxLG5FnB3QVXZqNqARFiK", - "LgMa3Az7WKhMYcA9HB9abzkZysdkihHy5p", - "LMQybwAsWNJCLhVukHdntHCZ4R6K7nqZJJ", - "LNEbx9Q5mvnkUdKKqBBjm3bVbvTKfBLm7U", - "LL73GoiYM6pz2RVMDLWDAQhyjf2QtEojsj", - "Lfzvumvksk5EyUTKiFJAcKubAUDq7iiDLR", - "LQfuDpy9FtxDuwH8KvV6v8U6CDEmLPCtLq", - "LXAwyHXJ5uXkEtJr8eERwWVDWmiTwZjDXZ", - "Lbh4tQwuELbkx5MSoNgeHPpLgkYgzwBqmN", - "LL8Sr3Pm1DMLSgjgE2tzTgaPsBn4tUBZMf", - "LaTtLUvoWMvjDgUtiu25HFcTXrq1B4ZZiR", - "LhhaJPMJGCK9zfoRqZV4FdvrqHrfBuZRtT", - "LQCko1GY68nozg5TSDYBMvTgah19GUdy9v", - "LiMQxVrAutJDdUoJYMn4HxPJU6MUJo7Wih", - "LTHE6Thf2TD3apc2k8bGe49uT8vk7Duumk", - "LN4HkCAKxkuQTd1ZxxG3MdUZL7h9956sLD", - "LggbiEPxm9q7qujBgH8VowJDz462EeocVe", - "Lai1PeadaURQGssF7wCJVkUCkMKVCct7xt", - "LSpBCFpxVR6rHNmQPF2Emj2biJ8jLHkwRW", - "LdBpRFJECJfRMbvXL1Jp6qVhpkR6orzeUt", - "LKN3hdMPteWLEXBQxi5pwXdeufFa83FwgN", - "LVJvfkYtMNKUdME8zmMAkNCnK8CPgx9hS3", - "LWfJsaBmH9ppZYP4q2tCtUmqe178f1HHw5", - "LMKE317FWXkzDMCTptsvPxHfCWSPsD2rDJ", - "LVsxLvnbCw2Veirdkm57WwJ1g6PezjQc63", - "LXLmZvvK19RiNLxUhAfmWbjQEjDr67bNyV", - "LPTrg77VfLw6sGa5FH31E8C9iepsxNzzDk", - "LLzSdkRuTkSBFVSjJjjMQiKrcMuCTsA28x", - "LfaJR6MJRJ6P8xoZK5bYo8ee2oZUETfeGu", - "LdRrMKNj4qLNpMXLbM53rLxAepF4baTpLe", - "LULuUSgDKn83sESXpTPZ2pMYkR4JVjQaBJ", - "LdoTfSDBuH83aCvWtL9sHwiFvUTXbYwf5Q", - "LfV2DPCW5m2WMGBGMhLUv8bqq4BwN37JP6", - "LXhYvad9n65E9FNeSGwqi7jHMQQnc1gKiQ", - "LMCDQqLdsHiXBwe6Qr5vmMnkg1M8TYU5XN", - "LiSTM5jNER3o6G1uTDosw5kVM1XvRBaY7g", - "LgRrD1BXY3JydbAiBz7DfdcurjnLpHyXJu", - "LPiPe2TxtCzs3gsiGK89urizhyRsd14j5E", - "LUbTszmejtBwnHMWTqofAvTAGSnUcyZhK8", - "LVgVwUBwP45kBjQLq5h221WYqqQbeLmLrY", - "LVqE67xxpsqahokAGYrnCbXEvvUdNgZhwe", - "LXw9igkHBKPN58f9UXRK1CAqoJSFXPr8yu", - "LhkjGCFtV1LbdisVmruFZnC6TdaXT1iD1y", - "LaQEaTTBKk8VudnfpGKKLt8fwu4UNDK7Fe" - )); - } } diff --git a/src/test/resources/test_config.json b/src/test/resources/test_config.json new file mode 100644 index 0000000..66902e9 --- /dev/null +++ b/src/test/resources/test_config.json @@ -0,0 +1,1011 @@ +{ + "test_parameters": { + "password": "Test^1234", + "mnemonic": "one two three cake neutral benefit quick hip level mother fine burst", + "address_count_initial": 20, + "coin_ticker": "LITECOIN", + "address_count": 1000 + }, + "expected_addresses": [ + "LNTpgLXprtecSEzztNDmGJpfDr7noC65db", + "LRFcxeV3AQ2zvuFtoiTyCYfSjUQLkEddAU", + "Lc14g9TT4yAhAfHqLaKanr8pgCPBVBksgM", + "LM7Jf8MSp8CB7nt2zYoSZuFeqiBZwn6TPi", + "LUdCaBTEFQjVaZeRafinzaUxWXHZiZcvCG", + "LMUjorgJBdhqdy4fdUHAkm6UryJXLtzaWz", + "LRs2qDU825ca8RurD5yysMEgors3YA9hPe", + "LegeUDssVAq5Z7Y8cx2b8dWHnb1DunMZzN", + "LhNHWWFwSG61PY9duCPN98XDsR8jgiQJRF", + "LPw2hdP6ZvkzWTGn8cJHjinPYCrPa6ZpfB", + "LecbTbQ72258tMyoKFvAPhTJfhdzQwVPFR", + "LPwdqFTxhEZAA5ZUpBmydVPL7NRVHRQeiH", + "LR26gQX2Z5ays6tvgbHUeP2CCUTbpEfufG", + "LZXyUvGc2N23iHaDWWkV7drnvbHEE9LBrZ", + "LVSWjf6A7CRnHRFV1YAGKVcqNunfvmgmE3", + "LaZVKJN1DK2XiujjenaJ2jGtu2UKKBur9f", + "Lht2wBTNf3z6pYN8WWRAZw5aMFWGdMfRwo", + "LMdhg8w5QaTpSQzt9kBmzyQBTKp9vgPrCb", + "LNRZqXKFZoUUBTF2RzumfHNAUcxLXahWTo", + "LKqcHm1EkmggfnDMsMY6mrTx7f8PNMkk1z", + "LUnRSgWNfabKFmDTC6EdGWCNRJmRApQ5wH", + "LdCULqXSmA4dR4hKrCYNd13Zpbb83kjbzS", + "LMFF7wwrdwmz4kxsvyF57u5HBs253DDBmT", + "LQryk76nxEUTipXPZMa9Q6h2sgSKe4Xmz8", + "LULo7XAFgpickNw9ZCtz99u9BTdjmiSFh3", + "LUkdgnTnPJ3JYaDKZz6h6DdGQExentjcG7", + "LfGf5o14NUGuQrVSEHTRNFHqMndZUacpnv", + "LdRfkuPH6SppfU3L7o9PQptmw92Yw4JfYb", + "LXrBKZoGpkuk5e2vZ7RMyDqvzNvjuHxcwu", + "LQNHBjKgpMdfJSnDFNUjF34V9xiAFkgWGA", + "LTjbuCtAwc33m61teukpnPyFmkvLi361oH", + "LYAdmeqiW9zYuuQKDrnJff14J8sUEGZYbU", + "LbReUXLecNu4PXoLfhJqqn8KCE4sLoKBWp", + "LNfwCeec9ux2cNLiu69r1r7omiyEGqqq7z", + "LaQZF3arAKViEhhQR2fbAdpxkpaodsYLxR", + "LezikXzZAHDhJMYAFpRbJYipWx2b5yM86r", + "LZSKM3miJU3wVtsav2PJeyywRXjU32Qfdd", + "LfQbiA2wN96n8mUq6S8jtGVFgUA5JZq4P3", + "LghnhiepXMm1ukyYUsAoQurd6fCv2Pdi9U", + "LNAmNuCMv34C42gsGw1okHXeAx7uxeDQN8", + "LPngfnTyCGWbNwRLo3LXZBrqeN2ktLFTPz", + "LSPnz6cppLDwSGPPA2wi4Xo7Fd6GfazYE7", + "LNu4gkrRYhcWuAtiiiMDrSee47qgQ8L7Q3", + "LP92pv1N8tJzRuc1D3Mqd5HqL9gtitu8Gf", + "LPtgpYRpBHKj55zyVqceBL25pLbkt3XV1H", + "LdstQc2vRERBUuferrsHutHrSyXc8Eduu3", + "Lh1mGh9F4Dcabyw4KLFibKzDA8YmdVGFLw", + "LWQ5ibuQdFvqcvfhrR343Sf5cB3HewwPJP", + "LgaXbedKCRRMLRFXtr6gxzqqGC3rswxi9p", + "LLqKb2SDpA9a3R4uryWLgzC2xYxs9sKPM1", + "LLWEBquiHFV8LGivZ3oVwm5b8arwedMNKz", + "LKHnkgsYrdr6QkASAhYp25GmS8RfF5Ynga", + "LUmahz5XqhGzXEfQ5G8re9sGcQojF7E2gH", + "LTZYrdv6HXRzLDBkqcvZT6CvbGFJ4eFchm", + "LVP6iAtFq2JLCE22ELWmRhRPSM4Zzr3RHs", + "LhRLJzGega9paT1QZoSTvNWfpN2jpu6qzc", + "LdAy5N1wVcrYngqV6UuTKWm4xc61SzLzJ3", + "LafonyLZFoj1teorrjrD6MKyoJbKBBN81R", + "LRgK3ttMmJGAXST8c5w4f9H3hAuHiMgCCk", + "LZDwAdTf3aMf3qYNQAuCkA8B3BiaANcixi", + "LM4e4QiHavGFhkguqVcQee5N5exmQZNFFr", + "LNQ97UjwKtEFsc4P9xEDwh8obNg15V4rcB", + "LXQkMaN7ehoK7KQuPYrDms94YYaUkyhAnR", + "LNFgvrFuVP4SXYmfhBHZ4J5GRzDHrrNhYu", + "Li9ARsaUWwjZXnTQRbTaz8WGQxpiwgDPLC", + "Lg96QoS75svudKyjRW3CW4cw32rBvutZbX", + "LYQiqNPKGHHMTV9qnjVv9WWtUhwnWvTQ6i", + "LWGYANGR7RQyiDmktgdrgz7RGFBCShxtd5", + "LZAiCMeZLzy1QGkwwoT6UuKPkvMqXHd8gz", + "LQ82FZ8GLj6bVavFinqFxvLNbjcrvPCxhS", + "LcofKwE78JwmMrBGvBvUcSmT5YmL5UcSRY", + "LTP7BUs5aBX7C4AHGrVAtLiqQpajmvUvqy", + "LgEsVHXPKQja43j1Rd3xE1peVjExpH2qdr", + "LTBXqH8AN41QbmMXPxotXHMSiqhZ2SGvce", + "LUqaaPcFpqZPhYbUs7uvRj5FVz6NEYiEwm", + "LSwu69aWjEzp6ALhyNWkwMqMyRX8d51dAw", + "LcfMfN1SaMGc4qfSR3Z4bYu9nyKE4ivpEk", + "LZjCL8xUKAuy83uvqPcnkMwLxRB2D1TYHU", + "LXzJfLdZ222RAwNeic7pgrNSkwPxkjVdUP", + "Lej7drV2kA6jvRkiVRrxDZvTfX1zyJAMVt", + "LdQoBbvGhHj5ySGRgVF6RKNUNE53LveNdc", + "LMWLPeJXYZ4VPWB8xpQ56gNSVDCgu6Qa11", + "Lhd7oV52q4AHGnNBXfxRpKU3av7aKN37F6", + "LLZutN6a2NYtLeosEqsKMd9N7Ca1pzAeTS", + "LWguMXkcw5rNRhJ8G2N7nyQzRivYEWKACy", + "LeoaenVAPAkD68xTBqvX9TeesyFMGYeWGG", + "LWr1sDHbDLzyKLkdfsLmRLCVctBAcejZdy", + "LcV3JXkPvF6LUaUi4CsjC7zQm4rKF2J3xx", + "LWTf3PEfQVTfKcHrw3NXgpRi6jxtG9P9mC", + "LZTm2LsVpqFJoiDxKTDamWtK75FjuG8sJS", + "LbdNDivoeGXPcTfq2g5fEEK4zK2m4cp5rc", + "LRT8aPfvk5yqsuAs2X5yza9SQ6GtbENUNo", + "Lh2ZRdHMt5cm2aVUD99o7diX99S4GtbkyK", + "LTTjcuMfedGAuJHL4W8uqxCZmbSANnH42x", + "LXX8N9GBnzeDzTQeZWCEm2xRrmqsJgb3bk", + "LMYYVufrxdQpMqiWdinyVYfmTzKha8suA7", + "LPWUpTu23Gjdjaeg23UmDLmyaeBthZQUbd", + "LRHquUoS73mG5Uj3VtQaVgNmtj77a78MD4", + "LVLmwgtkC2pnuptJy46JZmDJbw414DYYE4", + "LKR4yoQFtFGDn6L4Fb79G4bjaPNrnz1Xq7", + "LW5bvYkYF7vS5tZMBwbo9AwMTir6ywGR3S", + "LZXQ6RRPRGXmSeFagG1EQxf19LsESTHKeN", + "LL41uzfhWeeHZmRpbn2XuhDx9z4tdYBy8G", + "LL57dQUequqnXJCYBT8DFnVt9k188pnQb9", + "LV3AVsx39xauq8t77iN2xDhCmpLf5cmKAj", + "LUoyQcyqFtcmEaEm77fNhWFoDQW3NZmRQW", + "LParh4xAPDCNTUfQaWpb9FTiqin1TY29F7", + "LQBYnEUuw4Wyov5Snv3cbgcpxgoMASz6MT", + "LP1SimqRa9nJgDa1pQ5X2BUUgFbwhSr27T", + "LhnooDJzpsiBd9gBDavqLExWk7HCmPdfWk", + "LZUbTRvrcBbJTehG1yL9EhoHYFqBX9GY6u", + "Ldafj4ehAHY8khB1zhCqXeoxtfCqbX3xhN", + "Lbh9xiQGaNvC1mj3J5RQcrcQgMxCsLPui7", + "LbccvJV7BKYLJZTGBeJLNPjnPz7BKJnqzi", + "LbxaR312y3RkAi2dnxneA42mN9Zf7Y4qmL", + "LNJ5kQJL85HPUmAfuqV23k9jVbrYsqFqTV", + "LMgSwsYzXTTPQBh7jNkY8b5J8kS1RGY3bN", + "LNGsETiVjZTiewDwSx6MBjtQJ6cjxsv5kP", + "LdYux77EPviPYsgntTLSoh4EEcYmyGWtnQ", + "LXjaNYexsVg2cFRxb8JqatMGHMhwW5wtXX", + "LRQ7wHfHYuLzXQH5sQZXfc7ZmLUi9NLTxw", + "Ld8osEsH6XiZrJGtL8hcpNRrurqBvpf1Pr", + "Ld4HAEWDqBt8zn6xnppqUfWiW7jqs8qs8y", + "Le326jYzh2HFkKska2SriSGUJB2Wg5MxQh", + "LeHUeSb2gNDA1zzwJT4ZHwmMZRWWSeX7dN", + "LhrDhcG6qKy87wFxyC9pEVkFa2TxUKgVQB", + "LYK79wmjy3t2uP96N6GPbDMJcwM8hCJsX1", + "LY9Xow2RgxC2zfcawmPhk77zE32b4sgZG6", + "LfCG9qkGpAY5Wj7w5cNforfxTs7R8MgWTc", + "LR7FyfFoYoniwGarnsCXmN437i517QJL98", + "Lgwx6Fj5gsEZBpZHdUzoAfDa6UmTjb7sPr", + "LQMvS5eMYSFDa6yxj8hr7m4aPPiCPEVae2", + "LeMGFqhYNMfcx4HDBy1NSCWzLW7vAy3oJ1", + "Lce8zFao77eQUiD2bLJLieRN1gy3iXgR1U", + "LQ5F51XZVHFMr6sDoZL5YUSzaV4WLnEQHo", + "LKXBnp6sG9vqz2fQTQGsbcgfkTwHu7tvAT", + "LToTM5T9yY2RwwPSw6Si6a1g1juK5SwRCh", + "LeqPGow6kzcdxdUbeUL4AUTwnTaWLGfqwP", + "LTo82EctUtn6BoGab66tEttayxpzjusHff", + "LSRUCGARpgcS3Tnc7QWnqkCEkD43z5Fezd", + "Lgh9WpzeBnr4fCzuKY3VFe1Pejh5mYGvQH", + "LWgUo4CsXEr9CDUraginpK8jLteb4Cq5hq", + "LgKS4NmCKBUp54oxEUfi849hB3mTG1vi4P", + "LKnpyE5t1T7EURWxaL1q1rawe51MTq1ajQ", + "LT8fnKfEjfenPfLdKjKNScRFHmdxjXitEa", + "LPjpeXCf3WPhjpYYw2sZCSjNRfv5bHMTBU", + "LVkFzCU3rPLUmPjjjXi2sUtzxPsb1LKB4m", + "LcF4dwsXTdHjngZFq5A3AYnCfJhBvSJ62c", + "LdyVKgdUJg947nD2ig6awLwyAenmMAZycw", + "LKWNJcTNGJqvdFrRLGZgEuvvH4iRcsytKG", + "LX4Rn14HUtFPLtvQvV6hP48gQ8NpCavmJ3", + "LeJPuEgUH5MzUVcDGwwALYa9X2d6h8m1pY", + "LgRTrcgMPzLN1bojHMN7njFnBNPuDS5zjE", + "LWzr1uppdUef4gvJ1QE8jd1Sa2DZUfaEVc", + "LWKuK8anQcGLNV9nrnjduXbYvDRF4Fj3LZ", + "LdsJj1cq4QHg7pVcmbKzbozmiJxiYKuZfv", + "LRnN2DzvhdUCVzQCzCCwMgDspQCoeojzwb", + "LM2fxgnQTCnmyggy7KKhBXbJQStbVo5HeC", + "LNpNCPsNruz6fBUVkRFJTyh2kp2RaicfhM", + "LKnmkxPkDNynjvKzZi2mdwhy3LGs5dzry9", + "LRLz8rLbdog6wYJwka3CVKAcXFzxu9JxEA", + "LbnftHPQ9jP3r1gmNPdDoTsDUp7xfKBtbV", + "LSVsppscBXo4j3wCtikHP1UBeg3tQXxJc9", + "LiW9yYR7CvFxZs9YpTx2JgFtk3yYqegjDE", + "LXcUo1ko1jC2ycX5nvdZfsd2eyJSjws2W8", + "LfnmCHZ3ySVy1aaDwjzA1wcrqK9SoyxsnR", + "LTRwKGCtV3Q72WL9wRoNkAPxuVW8TN7jni", + "LPfhiBxWEmnk4o7qoBE32UXWkuxsJmTsw4", + "LPLnj2KYK36mnHhJAV67UqtPhhyEu87zbm", + "LbSvp2mP4EQpfmQQMZDWe48SyGhyDmKozs", + "LeBGoKBvaVfWneme3bmtCyrYi36QqaEQkB", + "LcNWHL4PyUnrQC1YHUjWPa9bi6mdxpysGw", + "LT8Wd1Yc7JSvkuWXP3Qg14vnzdKBc16WFt", + "LSRgVmauy3UuGr49cQ1Ud6nKG9W9VqrfqW", + "LR9RVacNWgConsYLYnSCD64xzhxapAzys7", + "LfBcxk9WpfagYY9oVnomeNQJGCijNPqWBb", + "LLtU7JvNQ5LhmfbwMazNjDzQ9wB5SrmaXs", + "LPpvKNJXvykhtZCFeCTfCJGkAYiN8jZYBh", + "LbXYCzLXBRDxuwv7Kb8VBf8YjRdJ8Zuc8b", + "LTjQkPWB4NgwXPiZqGT84gjj1UYGjqRbPG", + "LLp6MjHZKdjWGKQe5WGc47dZ4XM5qxn2mA", + "LNh4EK3hND8PKDdFPuyzEVWtr56sb7FAuB", + "Lcgn675CV2xcPKPjYvqLcvKwV86utNkTAu", + "LTw1ETkVwmGpb5HCkmJvERJEsWNdZEPRHm", + "LTK7rNjWEqdAAALKyGB9nZwQTaeknyUXNa", + "Lc8Mw6ErwuEdHVGqLNFMo5NVGcKoHnZPCE", + "LSffxhP4C8SQMexuzgChTmNRfvyLqwncbf", + "LhnPf2VirEeYCrKUbMSiFzkPLJ2fTmNtzm", + "LTTxY187qCEh4dibgc3onwrqyH7FDSzKBX", + "LPkQHme1tmM7tqLsRR1soHUKGgVaLCgRfj", + "LZBxTJ6sZLmTrEDAADrpEMGoanPeWvvRgi", + "LfaPNikFG8AzzpjKTEJMkKQnjPf28rWjMP", + "LNpmRArz6b9ocUsaiGanbnThWxK5vFZxjA", + "LZLBUYZBG9XYGCzSzkW7CEgqnu47AQ83uu", + "LUnNY5vj3xK61o8mJJiKXfLGTq6M25FB9r", + "LcfVXgTxv7Y7F8EFqtyQ37gzyGSDHs2dke", + "LRPcZSWR949S7v1UtsjFXVjjFp1MNx6no9", + "LQRTtbZ7fyNbLifp2VnMjkPAmuXSK65jUP", + "LKKz9XTUkwrwLfA3CkojuHdQbR86addQq3", + "LYUTtt97x26oWJdwFLVFProQVETX9Gv2Af", + "LSFotdspBDfvK5Wf6D2D7nDPPJWbADPxbf", + "LhN96GZLYo1aqM4tdDqTZWFYV5sMxbJ8Ud", + "Ld1VywVYxotJCJnYiJXYcX7vRBCoDKDHKh", + "LQQodxx3oDCTnWTaFkAWUAVu2Z9S7MmCdL", + "LQp6Ajv8Y221izvDSGNjgeykJbckVgxxLN", + "LcGUwRaXCfLiKM22f2A8FzxZSRrCy6vhto", + "LbM44fAzbpuFk4U84S1KxfwAtUERwUQs2U", + "LPiRebPFnteKKFr8RtGDFCzkcFiaxcXMAS", + "LgaBoSmfv8LLKdovvDs2LpMQqDSL4S52v1", + "LcGrF1xqc2cDWfxFw6CqRRQwKzuuXzEnSr", + "LLvufEqjFyDSvG5w2F1fcTnrWAyTXAoBsv", + "LfHjA4ViNZt6NJWtSa5w41H1wuG37gHrr7", + "LKii7xfkG9uPimmwiUojzPPFZDQcbVBfwR", + "LNsWWFDfuG2mi5x5Vh1ypKR2cCfsrKKtdc", + "LWFgLzQBmwiG3KJhXSWECM6oegE5UcRVLd", + "Lfijr38CTnD5FMioHpwSpreCWGcVreLf1s", + "LQDzYkTfNNkkmETTKEcsfBBLgDio7Ch2RS", + "LZWdM1JivYM1gpgBAXugJcqcvfhr2DFPAg", + "LfwPuqWsvqaTAe4VGh9AHFu2f86mcHQ7C9", + "LRwVUNWkhEg4DZNXPboWVMgPam7s67HYk7", + "LR5juRP4QfNcKtswkPU2z63S1VeUHUSTfZ", + "LPaTHZs8311nZggDGFpbLofyyDU9zGfnxh", + "LiXwEJzji9eFwg6Qa2HbAW19DPrQNwNQ8v", + "LNhFfFqYnHEKHdYA1cW7gh2w2qVaRQ3Aga", + "LPWL9SK18JutKwbJBNGLNoiuounbkVgr86", + "LSatLFBU4SMD9nv9R4ageDVRDUTtQQhk8L", + "LRMLqp42TfwHgxpLs8JbJfWJqfs3ZdcGbv", + "LP1enWqWhswMvPQL5HwLKJv3S33oHYgvje", + "LZ25qiaYmv7DtKe2NW6eMsVgogHw76aS3j", + "LaEedjPMoeY2KiQrSqkmotUeEPivEub7xY", + "LTb1UmEfERgmQofhkswsKEBH72qPCUhZae", + "LgTVzwEmuMzDBpHWryXFiHLPzqbMbkbfUb", + "Ld98vrb3Fr566YeqiP8aeJveELmqZaVcuR", + "Le9Db1J8ycebrWFQPafgtxo85NFroj6bkr", + "LPgtvGYe2r431Vor4NtTuCsuNEu5zdbyKr", + "LYd1ypLRtEikKWKjiaP6t9doBfrBJ2LXt1", + "LZ4NFJ9Wo6BXYfX1Fu2hc9B928youeR6Fi", + "LNv6tiwWTJneFnCY9DvLSS5gDi8w45Ebcm", + "LcVNwNatde6YvzToAz2aWFQhDZWe7KYTQ5", + "LMdzgk8G16GdbANbJ2V1scmjirRuduASZj", + "LiAQUYEWK9LrWo3EVVvArJjWroB1p6QW1w", + "LhU8hfcGUAVbQtVWAbzQP6QgnSAb46sdez", + "LdQizQZSqUy9qbjFjbutwC2Ykm6HT1aUVB", + "LN3VXb81XUN8GRi88xiAjxgRK7jmpHSh2T", + "LLojnet7SDEyE6r5ypEbbMoxj7hQHwWtU5", + "LV2ewWVcTXHuoZbQueEnjX1JmARVoFzax5", + "LazBTecQrBBWVoAXGudW74swL8U8qyD8Nr", + "LPtNcbAbWdaxUpVJdwYLZfkxi6wBFAmMit", + "LhRcaWetasKhDAYq2XbqeKcCapGMEwv8zo", + "LNSmDGKbZMZEniXeYuXCYYeYYMkywWabcg", + "LXWS8N8XxExRu1kgj1Pk8DLx9Dvxz8Q2fj", + "LexYfRVCPaKKVGFLstMTQqLDpckeigssqK", + "Ldms5eGY8zJS6FWJxv2EVXpqRi8py4T7NP", + "LaoL88U7Ry6Vu2SAXmCJsEK5vC756nXg97", + "LRgX8PJRoNWq47NejvP9JCcWLTy3GDhGtq", + "LXPSjGUe2xWpy9V18g14JPUxrbYa4zC6gk", + "LVd67tYst7wEaygdbyTrCNnkUCxGwvHMQT", + "LQRjVWXQN7yNLEWYCRCGqD8DZ7ZAnwbGFK", + "LfCwTPmYxcg6ptZt1XsZpXVLD9erm8icfZ", + "LSSeb3WyehJtkN5pC54p9WoZ6pZCqyxHc8", + "LWHGWCrcvRRDWkUz1XRxvgyxYv5XaBY9fx", + "LfnKt6eJ6nEvRVLoMEgy9D7fad2tqiMB55", + "LVoRMs8mfdNUkx3U9rWhHah3fc5jGs32W5", + "LUqSfbzNGBbStRdC3sQZ7MPeHkxbcpdu37", + "LSJhaNKiAYiLG3LfQFQqyRvb7aeJguRzbK", + "LTnJZT9Jz2fiyYCpnoV3wakoxqAeW8rHgN", + "LPDtzuk4TrFXaTn5fD5eyhiVernTJQVcwC", + "LbhFP1AZQ3p2Dg9upDtMaM6L6Y4AP9eViY", + "LfN2hVwHCAcSoanTSmZHPZpTenL6NSxn5b", + "LN41HmbM8GZnjcSfhUWohNL5JHzYc6Q4yS", + "LWCRTvjLsBPpwxHETBYmBfA6xT4YcSgot1", + "LWX9PVUBK7TtSGwNPb5Dg3pLCR9NQZuUtp", + "LbV1MJ8C16snbn1J5cWbcuAPXbojVBxHEu", + "LdwyhS1mXxcwwL5XBpVYQ5nmGhZKXBZxB4", + "LcMGYAjuRVVv4NeQgeyAQxNygtbTa4VKAR", + "Lhk6FBbTkBBAqTN3WMhgNwFUSxxt4wQdFG", + "LZaPP8QwrKMinoqVa8nuVHBARUFJutrmaK", + "LaviUsiNSthtxZqkshztwzgJFSoeRwB7SM", + "LQSCpivkXubZ6WSJ4Sj2FYQREzNYE7eHAa", + "LbjGdRMLBN6ZzZzCEmuSMREm2ppqXxXxG9", + "LXhdBAZRqndks8H86sauQHjdMxfauYh4qP", + "LKULCTZmekgyPJNKKr28rYRhMCuYm2o2r1", + "LhRHZGQND8pLJkYSNd2SxYRQpo6zNb8c7J", + "LS1axSTidtddZ2MqJtBMbTqQ4WUfAXoLfv", + "LPisDsmDks7AifRSbohsZRFwzC42wKvAfb", + "Lf5Hu9DUCFzWgBZxnNVYTA4vRvQv5RsC8Y", + "Lf5mNA8j574KiToy93y6vyJNNujgMB8D5h", + "LfwAVBi7rM9EqWgYVuxgy6s2oruf7rFehH", + "LS4NFvwhLqUZCVUvbsTLxmUciii2c4TXEa", + "LZSXn1cyh6uVGkZfQ3knsYYJUscQ1bH6ct", + "Lh5RVPHaS3XESVeciCQWU7fX88DLXJYohN", + "LeD9p9Z4cs6djuMY8eViFuAEUwZ23SAm9C", + "LMgXv9fa8LaYhT3J5EAvmhAL69SuSQVYj7", + "LKmjzHfHJjy7w6dPQPRbHKUrR4DmJmfVAo", + "LaLBXX5RPuBdtc9PmgYj94xnxSgafXqxLj", + "LXspfsj2Aj3JifsvieKJY855FqbjarcRLS", + "LKMfrpYj2Jpwf6GbcWKVLVXQr9y9Gfgxpj", + "LdW2144tJQy9Wia9WpRQfHWYuH97KPetpc", + "LbrsYcvTG9xPRdhB7EPzGrDBVMBSJVtz4a", + "LcxvDAqY44V21NQVU3nDNaRZbf4j49zg4P", + "LcvQDwfh9ERb1wQxLcbBicyaJ133mBDJJw", + "LZYVtkTRrJzpcx5LY5cY2QgXZsrqcmPHGv", + "LRjkE1xGxLGTYd5PuBCh6JdLbQeBWjzCXn", + "LVce5gDSjAYN8BBDYrYNAeJJj2gYqs4gu7", + "LYQKe5mWg3QWBvngMBD5YCjqaWuAoqCFEF", + "LYySS7JcpcdPcRWMmnUNLTfwdTMgLpZNqf", + "LZoTv6VVpX7vg4whaXujmySxRENuJgkp6B", + "LgUAkvrob7L1Z5J2renUjbVDoH4ipPtyrW", + "LQz9t7uG7mH3CGtaZvUwcBS7w3qMXnVYvV", + "LXggGj4DoRu1PoRCrS4ZriZetn423kNjtM", + "LRDL2tAEDY5Cp21GMxq7mwSmhtCvKHJYmG", + "LZ45DzMZNQoNfeQvVDH97Bbyv83zM8b1un", + "LfLkJ4ETAXFDYdguJYkoumr73rNnvCpRWn", + "LNeH5HhJrcY7TPH3cmJYuPFLs7MRZJhxS4", + "LRwMLedPqzZ9TnqXupuvo8ZpPU775hxfoS", + "LeB4sMZQRFPCNTaov66FnJcMzznzXMEcrx", + "LN58nMYRVDHZMbCYfv866vstjpQYfg7P6L", + "LXbCqAhCGsN1mwTfKmPB8E114CiQfH9csi", + "LfCUv38QDqrJcrf6C2cGTT7cQ8v5BgScUd", + "LTWee5WMtpDek8AvTJJwQDKwUmv5P1kAMp", + "LaKcV5pyABMRWfiwZv5FiEvcZztcmEp8GF", + "LZRoQd74qGjvnoXHLzb9Ksnw4W2v3MD7yz", + "LQ4AU7TBv7NXwn2NJddxgDhtmFr2J8senR", + "LVvfLWXmQaL1YSByH8tVfJkqFCwZcQdJUF", + "LgTpi25reQc7gsHRqbhTpJnUg5ZHWxWc5L", + "LPUjQfZhzEXLZys6EQqt5gm4huPHk7GLQf", + "LSXYAuAkxcrFX4pjdM7mRxFpXUYVHLWMWk", + "LZnVdtGa6YxfBMUDS4zrNLT72mon7dKqkE", + "LewBHdJvLCBfsPSVWch7y8bXFP5F3YCqUF", + "LfGqWfaNP26H4hTQR7h1G4xctuZiyrT8hP", + "LKVXqiteTcAaRYe11oQDTbxsjHNqPzmFxH", + "Li36uQm2C3ntDq35KWEkds727hh1suXGYC", + "LZsrqswTkjyxtpZXfSZ4oGEHqZ6PSnKdcr", + "LcdTZFftQE3mufjgGRBdQ3aCD5tg8tPX5z", + "Ldj9MK45RTiUvqYjD5916vLgkLuZwmDGWP", + "LdCzymEQLHtXQnH2unAiZS7myPanA24vaJ", + "LWfaCBDTZoLfQjb5k1ForKMd6uwxDyFpkW", + "LRgbporuXdvyrKzdq2W95kkdwPEHjpLw2f", + "LQU4W2hUknmqgUSBZjtD2Ch85M69HARHXC", + "LXsnkb7A38NDHSJcD5jrhX8iqwFWqjnYFL", + "LgRQ4TmFMwW82VEPeSXYXKNZAGKWSHuUgP", + "LVcDDKskQD2TqnSSfG2BZG5ufqSmeagxfa", + "LaVv7yH1k7upguY14k2fbt3KfQK3zzkFgZ", + "Li6v55R1F1XeiZZNQ4RywuxZsfssgFHtia", + "LZLp73FKyUYkN74Sy384g4EVUfsD1EhUWf", + "LgWrxX3rQJwiY69FHa93JeW4xsvQgRWxBo", + "LQWm9GY3ciBWCH6bgiNHmPTWmLM5oKtRaH", + "LVqgHt9vUnTB9ZzUYNYxwVariprCbSAv4A", + "LbEsbXSqKZ2Uz5zQp1PTa5UXab7ikXTmSw", + "LaLE1W9XcM2fKRENigadm7DvB1u6FvGgqG", + "LP5cJdt8gm4uB7wyHHB8s6tYmuKyfcPSJs", + "LgfdaifGiyQEzhdCGV35Fwf9536CbaP9AD", + "LMt59pFjsdLp6MGDQQD9VRbHBZmatnFRg2", + "LX7e5JY4yfQGQzcP28NEcZzrzzMLexFCc5", + "LM2cwqThz5yYcUE7b4D2Kx6V9xsfX6135B", + "LVTbHaiMbuK5QJ9iezdwbJXkZxXRNWXc5G", + "LPkSDgscisHp8BJXeUztneoRNbkxJuqhMm", + "LfczRDEX5kxcyN9aRa9S8LD82QrrxMxR2r", + "LLmrgx2pGLQF7iGva9HookfnRgYDXxR92o", + "LRSrWxxwVDk3TtQjKMwaqjJrvp6PEX2hq6", + "LdsJaut1n1oALn71QDHeu5MKCJKYsrbJWL", + "LaTaJDeBb3hwYrMbVd7KgpeJA2ETu7bEwg", + "LgkRjuxxpx6Z8cTJ6A2ecaH6bCg3KvTS5U", + "Lea813yj7mcDEDyD4UxhjYjapWiHTkGkSC", + "LamHNJvYsnuQ5rt49a3mmyttGguF3hLyTq", + "Le6FcDL3NCapwafw6CMcGpnQKByHrzusGq", + "LfnzRrWTnmLKfXDFSKjgKbrsnB1iFrk6yN", + "LZXDUyfNwcfcsNAdr7yaWeDKKsq9YzAK9V", + "LXjcmGbwimUKDb85payAmrQmSXirDf9HJQ", + "LPnJYTcNKBM69Qmgna6yf8FMJqTvSAoNt5", + "Lf37qavaRCar4Wj9Kcqvhebaj2E9G8JB6q", + "LWjJHHZ7jeWXHNoS8SL7UEyd2gcSCFoWnC", + "LXbyzfyrmFonXmBk5GkPxiaZ1qyUZCxKuX", + "LKgfwR76E2YaH1yi6UTV2z3WftBM9pXr9Y", + "LYy8Nh9XPht5Mwnt8624gyc6axhFMAUAxc", + "LNpmSSJuq6tzt5tS9dZNJjuXhezDmUorLK", + "Lfv6agqBwMsFG3ANKvPysuJyQUkwp5XN9J", + "LfcDkWUBDhWVmF3Utf5Ztk4QqG7aehrKW7", + "LR9FK6DZn89Ye1cNu8QAtxZAVrT8wYxC5X", + "LLNChpKyyWgerYTKVfUFk2oeYUFyeZuEoM", + "LRGDjdUCZ4sAgUTK3m5gVDJA3R3rAY1nW4", + "LYeEZr9GdrCgei1RRWiRF5Uyqsi5i63gTj", + "Lh46Rk8bHk3JxomWynjBKzpJg4S6D4s2NE", + "LKtFkG5xJKdNnejKa5HxeUX7y1HAsKZgNP", + "LRKoBygqMvJYmDK8nDpSk5nWZKd2xxzbch", + "LKYb1yherT6oxUoMw4MTLyV51gJ1M75k1t", + "LPWP6xysXnpNXj8rKKPwKUKawmxymmTZ5E", + "LRcoYZSRQ1C5BLeCiNC46xFWhDPQAK2u9u", + "LS44nek3ytjXDxCuKDepHzPZb17LgdqVTE", + "LiAfwhdiwTd69BznWPi2nLjnsGc8aBH3J4", + "LYofrTD6GZZvC6vxF5iKewLm3Xd3iDWz7R", + "LXftf2DrbUVZBC7Ty8S9rnyrv6PdadXCZJ", + "LSazm4eUAaE3BuVVdL4q2cMhdbuwzSMmcA", + "LTVee6L638kvdPdYM7MG2V7JZZKCBwPHB5", + "Lg9LW8Q37hwbcX63RpzXBKprdei3Hocj4f", + "LWx3avsgDwffYTn47x8qtboTUM338c8CKJ", + "LT2PGJPi6xhzeNopVwWntRaxfUAxJzGo4n", + "LKhsCQnPVNPjLWPw9AesPG2rsbYaEbFfVH", + "LVeBNmWUYGoN35MPPb7eogwuApwosVXvNc", + "LiYzqbRHxSdU91kKGHRiw3k8UrPMKV4NXP", + "LPLFKEmRrMkKThEotv6LXSP2ujgLAvvhhG", + "LWuUFRwniKhHYGvpCJhHDsYmiqn6pP1NeG", + "LhrmTf3A8wN9uTG3G9kHzf5ogXHmVM744e", + "LcTYdKZvrwVgkHMsmqsLyRFVgUHJrXLSHQ", + "LVzanUyN8NiSKtspPUCAC1FHXASzEZJMM8", + "LKNCFcQue4RbHkQimmxByQbt6guqi9keYz", + "LZHi5sTPdUmCnmtCodNR4v5NG2BwAqCGHX", + "LgYQybURgdTLSZ7C2B1MhHWyfm7vt8wvW4", + "LdKLBQngeqcs1km3T6s6ZiDVZsQNdKo4QC", + "Ld73DCAtAiV6JccEsMvojGmtSNza4SLoj2", + "LVKWHWqE7tZp6rvmLhVFHcncSpQ5Mwf5pY", + "Lb7WtrcZgCYVz4C2ojjhAea1LWGX47p3mu", + "LU2w7DZWzuSsiSv9WxC6czySDdsTGtyhVG", + "LiMR9FFdEMNmcygQjfaCsN29WpJhQpwwub", + "LcJCTXGhoMc3882LDLWSvVrSvchgcrScee", + "LU1ozeChHY4Dc7tCVSxWz9cqptBgQ9pJb8", + "LMy9TFp2vUZJh1aBA4nSFTnk9dQrRub9aq", + "LexvTJ2Xva2Qx91Br3A6doygTnQrC3FaHU", + "LhJSxkRoXWB1imF1XLTydFGjthNdpMrxRC", + "Le3zA44dL1oxHjyJUySodFnhrQAEZu376w", + "LWdFGJmNPQjvdi6o89MDaFgQPLfzgpmvmv", + "LNqVqTuAi3WAnPr8v5UCb2K9v5xF9xVkUX", + "LamEb2i4o2ZQU227JFYyJ4JaiYroLh1Hoq", + "LMxqjT3dU5RT8LuoXnrZfHQu2wwPAZCuXh", + "Lf2Kwcy9hAcRdG64wZYhga7yvajPkyEQYj", + "LNcA279mX7GqQXa5NfJK9EiUCFvTZUEwHK", + "LZn5SSYFXbMNvWXi6Vzch6sYnEVm1Loyvp", + "LdpvugbTixy8dGDYvojLKC3UxzwQikzE8i", + "LfginKkdNZy1VpXtTspLypqk6KMcfGRW7e", + "LdVmYQAcvtpJdn4Zw21hynWcMxEgNq1qik", + "LdiSKq5rU78uXzibkxXqwfd2eWT1oV4WUn", + "LUaX2jyjQz2NwPicXXqBdYzYowXSeM16yN", + "LZZszxqfznEeCxdTV5t7U2yBvdM2ZJir65", + "LKvSyL6FBZHgN7sL7zVcxKuaTiEPRauqXK", + "LhXry2d4HrVk9E2acACooGs3KgKCDUzL7r", + "LfvCGbTUtRe9eiKqJhs8PAGor9bwqjwEsT", + "Lc98AoHUML45SAGgpzsX5LYc7sPCWpftNi", + "Le6XXyQe8VUbQhYUTn9wFY761xpLVETwEn", + "LPs9CEQHL2Ef3bKuE2BxmfWDnJfTvdffyW", + "LUP4moUpoS4ifVBtEBswFghVWqLdZ5qTYX", + "LQRXpcSkx5d66Y6WWZWXAcQoEUYbXisyR9", + "LUyQxiJqBRit97K9wdiVC7z8GZs858Efay", + "LXcGn1y7Vof9RqCUgD1msBXX4TKbfN78hq", + "LNqQZNJdw8jpwkq7MC5A1Z2U9VPRhxCN7J", + "LNYpdowmDHHkV5kpmX3BzVBqSppSNW8Gr8", + "LKskwLZnzZqCFQunxsKPqsJka7WuW5Yhc9", + "LR15VwhWBw1pnjabSn3M3VReFGmYowrhoH", + "LMzh6FEE7t8QatDihJesgjndmKCEB3GhDk", + "LVNMUN8yDoE7TcbPi9NWDeUEtRj7zBnmD7", + "LbaZwbxNhHiM5vnRf65NTgtM3ibaZCiMSf", + "LcUHUzb2Whg2jww4jqgtq2HiX1cjoW36Mz", + "LScrZ92cYWGz6hszQTryFQcQe41hsd4MVS", + "LXdsv51o41qLX6hRTeScx62DH6NTrBsxP5", + "LTTH7rDBRNYHUurrFBoUFVJCKu3KTL4VEu", + "LhWytosG8nvC7hLDVnuTc1EqPsSKNSXW6d", + "LfdabKhvDa9rzM7JUN2v7g4JXUbvAmn5aZ", + "LRWd21TvMqRd7WE7FowMBU2ZBZjW4b36gs", + "LaEdURp2MHA2FevyhFj22rjfYZ1wU8Wb3w", + "LXNHmVTWLMqnavNwxHUgBRQcUa3NLHxQLZ", + "LZjcbwokoh4Wyv2zqUdjW78rDVKX9H9erY", + "LhFZYaLVVMKvAMGbfEEJBqfNQLsBiDukS7", + "LfMz6875z5cFMPpdgQuYDhdYJjbuo7ydXt", + "LQEe48oXSSiFsKorrFBtTk8EunQv4rU8R7", + "LU76iwcG7HFaw45XB8RxjrBk21DFGtpUxm", + "LUcxMHYwemdh6GFMau9Zr75pvik9n5jVjT", + "Ld5MF45i4U4kFj899L1NY5j5J55YSA6eki", + "LW4CWN7rCAeL1hhQENBSt6XqTvUp1JA4eg", + "LTuJrHZK8mALecAM5ymjVXkPinwcmUwUe2", + "LUA55GSL5RuB7nNqN6w9k4EHenGqRmpoSa", + "LRqPW12HWzSH1CHufFEThj5pmU1oaBFGjN", + "LZ5jF6KjXtMJbJ9s9rYu7LvUhvMWhqCF4X", + "LagEhMhov4wZmciRdamTGmGzwFziAuphY3", + "LfinWg1VzA8ibHWBH6mVYd4VHKr6NHtKq1", + "LRiAzfYMPYrU7wbg6Poi24b2rMR7fefWpy", + "LUsBVE46UVrEHXxWRDB7mttenSX4p2fdzG", + "LeJUwJfjaghuWb4F8rmUrsBcj4Az7GTuRp", + "Li1sLEgHeus5w3sYdnbtJZH7ySwP6mnEhV", + "LhDVcUe4F64Jr7WJYrXpWLWW3Uim5A99Gf", + "LZL5RhLRu5JpSXWMhfVkmdZs34g7zftRFf", + "LWt468oMQ2qzDxAYkMKiVwgSSFi5YiWtHf", + "LT39rRFHHCyn8QNYP6LasYkYCrovdRZWCv", + "LPfvXnzQytWK9kvcSbEJxPaSQuKYZDPsQi", + "LZNYir62U9fRDJuPiPsrKDEzaDePhJtJtq", + "LehtRTWzzkA1zFCtKsrFaK1KUd9dw3dcev", + "LTV7ndtzN3N51GJkvHbePbibwRHQpazpQU", + "LTysFerupYwh6Qb34WFieDArbtyXuUTDQK", + "LNopEK2qHxsgfJmsPhJbTXQuBzmBNVdADd", + "LMLpvLzf28BJgP75ydLw6NHUMW7svpKa6z", + "LedjHgZKzmeGp2ehx3bTTmRjGH4dDztcue", + "LRTUoFmcs47RCcuDPc1wfAJwawCx7DGhH6", + "LM82cAWrNBeEUmBDZiF8TV3yaa3SLdQm4g", + "LZFvxgQrSb7rDX8tcy3pA3vh8cRSAxvaHt", + "LMCaFyi2TXjv3CRddbt2Ha5eX3xEC5TBne", + "Ld2rEdNcSQRGJQwBnof6Ka9jTXKx6e1PUi", + "LYLCh29WQdSSLoHWj4b71th8MZUWG79QfZ", + "LSFy1ACArGPRD9YZEmcNMHxLDmnoPyTpes", + "LPptdyRYSBtGstDYgjwijD8St4MfSCfeSb", + "LWkNyMGdTs34uGbEXYxCQV4uhm28VQoikJ", + "LfzTvK6yahXTDmgVMgM2XHpodqJySnwAq3", + "LaxirQz79hC6BEWGzeNo2EvprUE7zW6TR7", + "LQuWoruvtJ6kdm2rAg6UotNhJJfQcgE3Za", + "LiKpsruQgtbVz1isYzEd2kCDezufNSY21R", + "LiL2MAtuo5DEwsLtgm3oqK7asQVuxmRGkD", + "LKrMYBPBwnQAibxin47PHXKVtp8FdWTWep", + "LQDGoqJT71et48KtZHw1M3TWt5avB9ZXX9", + "Lai2oCdmNfPBLmdEq8Q3G1KEwazqGBb9JL", + "LYTUbJDrrWSauohGCzpxkDfJ6XshbzSvYw", + "LPPNcDbgNVwQKak6pgHMnK6i9EdmBQejRT", + "LLX3dw4EEYL3DNH2eMGoXxumJFJjdxyxux", + "LfNkW8BozxTraZRFQ9kVRZYopv3E2JNnbB", + "LTbDScAZKeL5cnbrECSM7J2CEPGj8Taezp", + "LWX8kRsA5xwSRFVHKKbuXY7V5KHczcvQxH", + "LeoXAWCsYJB3FcYmhw9wQhDohKkBLjiDUA", + "LcNSzD66uvEAP31gcS5mcv1wyZ5ffsEzT2", + "LRkv8FEThwrQkxpf3jFkv8hY4Q3vonwwMW", + "Lgr7QtrNgKrS4htZyhmSp6S6C4jDX4d65W", + "LeJ54WUpjx6tKpBMyFKZbqcWFMAZqUUkKC", + "Li7b8UcA7jp7EMgQ8oNsk6JbV76XV6BoXr", + "LeadTzRiEFVBjjXNJwb3T7AW8ywNkEVLV4", + "LQHteShBs88McZVeWsdyqztHLqBGxdf8Yv", + "LVh7GPmjryAyxxEu5i2rFceAjEeAj9EP79", + "LS4d4NxrqeTUBjVcsaTLYtFHoPWLe2x7ir", + "LTeEzsHxdrMtE2wJdLnDtmJVgJD17U8bem", + "LhZREEZF66XgJFzgAnyThSBbSeoKbdMHBj", + "LapLnKbab5Rt2Twnz9whwsQN1NKnbTETwp", + "LdwcMyu9ygA1zfmDNPgLDVxG9AtQeJCdXV", + "LT6DRkw6XcyXf9pHjqCFYC8zic9DATfE1M", + "LPMVMYFLER425fqpBRghidN65MbzV36V52", + "LNegG5NGKkjTxR4V96qJFuEf7CKkDD5xgz", + "LSg3mgY3FqnvjoEWLTbc46hFPQLMtPCapg", + "LUC3B4C7uVaeScH26xWS4xBe62dv9P7P4k", + "LVPrbqT2JesNv8thx7TXNLd85WNBFyoDxW", + "Lg2SDwobLEhMGsxj1AUdV9AYBskz4a1f5N", + "LLZawqtAeQZpqCwfQVDrJnaPvTxL7RsZhR", + "LSBe3yw2mcrKYkx53yD8daJs3vLBoinCzi", + "LUYp26MjLmWJ5SWaHpccdTx9SYatEjCRhc", + "LejgVcq9oDZZrspvKnrkgeWCfMXGTQ8XGE", + "Lf47jyLKhphuoeuhQaBpy8BSDtP2b8FX46", + "LacqTKQcZZVHXamrtyC98BiMUqGFjre2jJ", + "LQg33sm4kgBza6u1oTmmQC2gDQzi8xHQtp", + "LhQSED2BB5eD2nxSnBuH3cSgtpGRE9Lr6T", + "Lgyym4cr9jyZ6DuuawsH4vU5nGfFJo46hR", + "LRUQb4QTQoyHqGnBtmxmGUmdmPEiBDaHFp", + "LXa7DzWMdrT1ZiPerKkJwxZPs8KGGjotNi", + "LPFvTTZGyPWTzWBniUeabT6PpX6Zzi9D6y", + "LevYJS22KHCWb43JbyBHtQC2ktEt1DnB4M", + "LRLD1D1eHEeEZLWEkq6usamBtkRFMoMQwK", + "LeVxtaGA6CM7m6aC29wZ3hhVqU7QwpiDkL", + "LTMFcs6J2DrhSCRoVJ4VjS4XsbAjtyLFtt", + "LSs69ihmYuq9ZFaZYY64dybDvcRKJ3HT1w", + "LLV9CR2EB97VojFE3QG3sUTswxNLJZ5Zzz", + "LNdPmx17qQ5AfMcThrSKxgg4ats45NSixj", + "LdCBn39uDtnyMUaQrRK8J6M2FvaYoxNvYW", + "LViEH78zotsf5NFSHC6rM1TT7bVXnkQmEK", + "LRwMVqKphLBydypR7xkTFvb5aBibgLcQUq", + "LYsywU9zXUUkmdWajphpEwF2eiL3ERC3Ss", + "Lf22WxRh8MgotGj6QNtsmwBjTi412XedCz", + "LYDum7F3teu4MPfN5TT8sxts7Dq4U2ztmE", + "LbYwshK9qHevNBGAR9Hrdj2ujB5vRq9A5r", + "LP92zovmBBPyfa6EffCWuToXAyDtSjkyNx", + "LLyJ4cDBiJ7mZyi7BFVePC8sKr8151ZLKg", + "LeFScySfBCoWSgvrzkTiUaajX7BHbJ89KQ", + "LTJLMc4yHHg3Gtxwcud5G4bsHgeKQFVTkd", + "LZoMaH46kzYykGStsqETL1gECWAxReWqAy", + "Lag9gsk5Wu9nnyVTLGex9hgKVVv8mBMdia", + "LX8ouPBgk3HemjYTREseKXEZCXuhcRcwZg", + "LRa7BNU9G5wjsE2KxoG5EAg3pWs6wfq4pt", + "LiYjHFqzLsqKvZx7KCCH1VKYV8p2ckbM3J", + "Lfh9oL7amDoxWUZfh1YyN38Dmij1vSYEnm", + "Lb7bTy3kdjs4EiEEAL8iwWgcZ4Be6bPtw6", + "LZULtWkQfjRpSLT1LigDM8126bGtDZQyZk", + "LfMuKSyVh5J38GY59SWswG8prCAucpkcGh", + "LeTnaxgTXm84b17EwJmgovW6t9RADsLiQJ", + "LN4VtcfdHTYmoacP1tpfP1w2uW7gRuhTY1", + "LYQDSnr6hMBKd6x1jky9gPA5NePEdTKMd1", + "LSk8DivGMiXGUy4NqvyKgXDUz7tXi7M6Mg", + "LTuNGVBmKgfdoc13ep6xkQq2qBdAWPcs6L", + "LR3fRTDAGuCq2wJqyQBTuKzEh9TqUo889t", + "LiQHGxYYQwbCZunneqavSmsKfAy75rRpje", + "LSf9xF1KKvWghwc59dHhKg4qdDDQLMe2Ry", + "LPt1FjgHvKzG1apoxvizRp2aTvvpuvbNpz", + "LULUas7LEucL5NCKo85ZoqGwqSWXqfKPAC", + "LddCzGVTs3gC3apfXxHMJDt7DN8Ay2d4z7", + "LRXqDsB9QVAe7JmaeFZxTV5yygQNsTPqhJ", + "LNXY9gjCUANfL565MvBGyB9o1CkkqPxDJA", + "LceXAS6mEK3fyXX9tdbxLREbzZDJGnkrZg", + "Ldo7eHSWqooZXNYfoSKB1gKPWauXGBwAvQ", + "LcqWVgb24TQyD8AQR61qGjahj9Uwys7oqM", + "LULdzyN1WW9BDdTc8hCVS6XHUtUVFfH8XD", + "Le17ydzLqYMoFzyUuVBSszrFMV3qYeiPcH", + "LPbCNLF2QgdLYkrQMNx1E3a46Sm789uGeB", + "LXLap7XRPWGGDV5NW4kAtj9LATqsXmJYbt", + "LYoGvagUXBTkyhxK23tuzqXmkLgE57Coep", + "LfWdzyNfwjEhbJCi2sg9j35RpDXFuqLdyP", + "LTgDa6Xsg63kvZ9pH2wYwDCqesXo5drk2J", + "LYDVpjoYvyEGEtQSzwAssuycoS7VEHGnaS", + "LXv9YPQGQQxxHwtXY6kvfABzo85A6pB6oN", + "LUNRSQeq19vnFfpyTa95RuA9UanSJ8KdUo", + "LbfVboAyyKXQTF6i6hLyoNzKwsRqKFVpyf", + "LN4f5Pg5dShTvTEZ1jXHfZKRuCrSeQHCfJ", + "LgED17g1BsjGx76yPSWaAXYTFf9Mo81L1t", + "LcRCefLQqwHrySie6WYDRwj1iGuHez9wU5", + "Lg2ngZXMJbBYW6RZ5SBRL9w8bZtVQJWQFg", + "LZP613qoCYLDMmVMnefw5RuSt9L4o1YkfR", + "LP3sW4vxFPuyTJC4kw7hhEKqa3pMAjfpYE", + "LhVMmYc8UZHj7KSKPSUsVJyP1GyKff3hgU", + "LPzPycHukP5J95jjiUx6vYi2FhRM95TPi8", + "LbenvZKkJwhCULwRCCuGgqR26rFMZE8Tzd", + "LXqajRBNYWChTpbSoCd2mVtY7gzfzQjPnz", + "La8om9H3kVzt1KCKgp11dURgxCaTqLj3vJ", + "LNVPL3UmQrdbr5uybGVco57Lr6jx6Gqa79", + "LZQ5nJckGbMk33UMob4eoJgi4JJFb44XQz", + "LM5zsEnehjipDxhdNbsX3umTNJtvGgvthB", + "LSTDwxFQki1PSRmhBYk3gZr2qPLbKcPKat", + "LXzW1U8Hk2YKyh2ecTGvxthHocLra2UJDQ", + "LS7sLiEsVasKwbtoidXGudnJB5fHDBNYG5", + "LLz58af37RiZrik2uDPmpc2RQJyNJ1oj37", + "LRLMow7MYFSoQMpfqka5PhJT3Z9k6WqfWb", + "LTbDijjjSUuFkVvVABYdkBrDtRpEsuCMcw", + "LaV9GnFJykDwaUWn9YVRCiSDtUt5GVpvPa", + "LUYJZ9UpqCayVMx7hTyC9UmF9rpEiH3Zmn", + "LgPff5EGamXXnfzHT3t9b3PEqcyRwCRtPK", + "LW4mo41eaLtGg29UzvqNnUqc6yYw6HevXb", + "LgfysTaFcMehG264PmjC6rFbC2PvuSxUs9", + "Ld73tbj9WQokQfyLFeVr7Re8XeWW47FPu2", + "LcAUmE141s87Z5AmDvJjqWvt1Uyqp63onZ", + "LZuXCswGsQQBo3nQaPm9e621GDa7hNPQxj", + "LNc4FeoogJW81pTJoVDjXiBSYsbqdiP4dz", + "LL2prhA3kDjtV7HoTp5hvaotDT8BU6BESi", + "LaQBUxvRXfJSEbpnf49dxqhCeSEVKm2gsz", + "LTmX4Mv4bScp8z1FgW6mEtQavN1c86pT2N", + "LTMWeFwGFtNUHwKvRbReoRSRBRDerNRN7u", + "LLQcxwRu3DR2VTue5oEJL83RbLEh2tML4i", + "LgymiHL6v8pJELfkFRqnWcmQGqpA7T7Zmw", + "LNijHxqraiYvkw1C3CXLvTDJopTWKDb2q9", + "LcPvRMUBNhciUj7ZDinmNMvd2mRDZ5f8gC", + "Lb3XSRYhobeyGGuHyN5uPtzyPcakvhQHWH", + "LbWLi2fZvSbNUdpnotbnfp3q2iKmapuDF6", + "LVeGB8SrXQSF1aD3tB4BKjpbf3mdBRTXtu", + "LMEWoLtwXBhECEhWnpz5frV71EuehX2zHv", + "LebnT4ApLCju1qvxCrJJsP6u7Jrdt3YEZu", + "LQSx5N8jfdykN13tzo3d23HqG64f1bWon6", + "LPk1KKuwH1S9vtyf7HM3qP78UoEHrAnEXX", + "LU9CaMPAgco9wRxc6Wm3FYkuFEzhYX65Ax", + "LSM3bSPAhW9HN5FtRn2Ea9DLKRayQyQx8B", + "LUgSMFvNQzjusUna2Hcz9p75bpEfS9kTZh", + "LesW6eRsSEnGRV9BJjuoDQL8GzWWoNGZek", + "LaCMX8KkRWcYknMMzTrrB8DxcoY6x6XJQt", + "Ld1DqSmpp88VfYX53zo2wZVs2wdTSQj2cW", + "LaT73qYEQ5LyK3YcSFjenQP6MxJMqTLqBc", + "LPMo7n3E4TzPBryFFoav1zfwaTqEU7fkDh", + "LZTefH97XnNiN2cFVUS5PBag9hqHStFBWq", + "LdyN2MJBUfHGS7AHJ2RAnhhK8vru3EirU9", + "LgEsMgND6pxSiwGWtZyETh7aMniTUHUm2M", + "LLPeDekSGQPpfRqQZXfyHa9jgvGqsihuG5", + "LeYgKYNJqdLyD7Jh5EygCSVxsunTVKpiVC", + "LU8fbEyGN4H6gn7kmZDyZPvJdA74LxcVKY", + "LP6TfYe2nRegNzVunREnLxapK7j24HFcws", + "LTJL5QrbrR3zQQuiPVmfPba57edCQtHUp8", + "LWdpr75az3i2dhYGdELRNjcDdG1iiAb1gn", + "LXgkzBRxojgFrvTXm2qeFXN7Dr75rr6jfy", + "LUxero4pkex6r4a4d4JAbJBP3eZVj75F1d", + "LdZKaRExyPhkYPeCZHwDgyX8zoZQJkoUPf", + "LZYY5aHWStB76pPXBtZtkRSUgpFgnBBKQs", + "LdACoqRaJTgxmcPPVLnQpj81mrfvE51u8K", + "LVz1yNKuRyeynTWKtjvhS8bhi8CHtEnzjs", + "LWaVX89EdNksyEs426eyez566uP379jvAW", + "LQPdTLAvpAndXopptBQ2EJDkNkswusweEx", + "LcvjRsJ2KtzbCVCq7dUXZr2cBCmPdVK3hj", + "LV8Mh3KS6EhJhHdDU4b7ZbG4PjkxhiJErV", + "LYJgrdQUV6rUQd31o2oHyeLAJ4yp6R3eXP", + "Lck3dU9y9wyJD2fSfK2dqb8fDwpQRWju4d", + "LS4rnegqmi1i4Pb3N2EPkvP7za8e2vR4gz", + "LTLvVRGGH2yMFmF9u9Mz7Kkqq2AwdJEFLi", + "LKLrHVjWYat4oTLhshmYEsRiCfUspcQR7u", + "LaEyfX2Pd29mXGT6iGzqnrr7uhq1UYZAHm", + "LZwXMbe9qtRWiuNtph8HcALSDzkmfWW6bV", + "LRbqTUWUR2fEJHSgRbJVjFoq52v2C1MUgE", + "LQcGFesvduneESv7HEtkfymaXeH7PJJgC9", + "LQBHTKspLyrqQ3hnCyMr1q2mk4RrT1BFKH", + "LNGjuBnVciDGUTxFQC2bB5gvFE3YqPzXRr", + "Lh2kgp3553BcvCHUsxYjUvrotjYHRgjXt4", + "LhsEHCMaAzGSyvZpCj2V4Wp1HjcwMah7tU", + "LfmRHjSNDXaEmLHEwz59VCUxYfYsh7u5kF", + "LThrqXBHdD1x3uby5J5PCUTWQTVshmdwxa", + "LMHUdMwpeziokMgKSWsuVeLKQryBYtCfZB", + "LgcRVwTKk9jYyvp2DQTsQQxyoyhVphrdVK", + "LdrXpd88kH2v1BWwJtaxohk8LRnr2TEHNm", + "LfC5NbmSeZYmiKvcQKrmz14msUAKH8ZrMk", + "Li3fNhb2ASsoK9jKJZaWs7EBF4XbZbWAv7", + "LhENViKDr1eDcCqEUC7ieQtRkydYABFq2r", + "LUyueRX77ZDtVgXD6G9FWjyRikeUA2Tx2Z", + "LYLudxM8vGRcQaxcQHRn8DzdLj7AAD68qx", + "LfDK8SGgD7aKEEB62iP3Yd82SvtFfip5uF", + "LQpdEHvZKFQF4rCUTy5SnqH384pvvZ3Y5i", + "LdfMWGVYEdDtyR2GHy8Lggz95dQPSaTZBD", + "LdZqGe9HG8Z4jv8oY3fM6DFWVYXyaEoYnT", + "Lg9X3fqua5Qvq5npLAXDfzF24n7jsduFut", + "LZvx36km8ayxZBZkTUDTm4FiDGF5gybJxt", + "LNHE1ejNH26YmU87epxg8SHLg65mrBDeQT", + "LQKSBKX9JwzQgM7EmhgmioYt9LL4N9xo3a", + "LYBz9R4vX3TPSTeoYsKPmEJAAYNyFPQUdJ", + "LUG12kwRamFNEvuWd9X2sFPrLBEKxGDDUC", + "LQZW8XVnYK5e8P9met5drCTj5dpnvkXUDf", + "LXEHZe4KwTfVfS7gwoUVuWsKtSHcsj1fMg", + "LahNS2CDM5skx3AVDU3PqeZKUc3ejsZJEB", + "LL9ztroxHkHEM3AfaaRnWR4GyBmndAQN8v", + "LT8wtacMnb5c6uUjznGgTFAfaaFYmx51AG", + "LZjUrPkpg93F2UUdCXEekTEjBjx8m9sitJ", + "LUC1TrjuMShNaYssEFVLhpBiUaL2sMjpZx", + "LaC2ihRQanZS9LogNNMTAuu8HLdz2uFZHf", + "LfjTwoa3QDxSGMVR4yCkUJ7QV4QXtxswqX", + "LQwd3fFW6k5VoZ6Bq49hTeVBzfQZCzBWEU", + "LPwJsArmye5XaM9xwmoufxPfB23vBxJcm3", + "LS8TFTCeLdB8qxzp5nYF55XHsz2XzRNxup", + "LLy4pPWGkorvB2Qz8wB5tfpP5tzmcu6bxM", + "Lbv5z8qMXWyu9ze3jQiv7viCESorzsZcXG", + "LQcRZZr7EHboLGMSW1VmDvcjCGM99FG4fn", + "LTg9TwJ7XZaawgmpCrVxEvvdBRSe2rtmDU", + "LNkUBp9keeahZzUdzSohNvG3WB5U8NujhJ", + "LKGqws48yQFuToQdGcfK4prxVmTTRzxTrX", + "Ldv7f7KUtb3Gz1zMm3FXS9LSuxx5fVefxg", + "LKJRF3ViVB5uPw1EebUkAVeA11HcMCT1L1", + "LaEEMDkcxr5eKuSf8dPNR7VH3i4aSKESbE", + "LdbUTpAvr62BTB5GnrDHdMfkwe1q61TGUW", + "LZrETjveMnQPzYZpbRW9u7DxoVTT3tLVAq", + "LfDZFiqTJDG6pWC9NwD79sr7ig6XJaSdsx", + "LcVE6S6o4bBMgZ44UM8pUP9yDWJe5y44TP", + "LN12xLfR1riFERGWD2ehTVXV2SgTPEGkda", + "Lb9MtyqceeU8myfnDjopRjtMNPDCdkdYNw", + "LWaf4t66rYFyrwevvSxXUUf1ghXeevwnrF", + "LXqk4DgW94Jj8Ne4w7TVS2cbrDGzFxZNxU", + "LhiFe4iNvqdFFySfCaKcz8LUTDtQsCpsW1", + "LPSSyTfct4eky7bdGaBTpdy3hztmMqMXFZ", + "LgcXMbZDRSE3EigmdkpPJJGrCzuEHVki5T", + "LPApGJ4vcYwJ3PqfrMcCstNjcZ3AZdJWoq", + "LNx8vM4HSHySnZS9FaLP7frerU1b5EXGsB", + "LQK4kcFKcGWJM6dGk2rjPS6SroH2HFfi1C", + "LPco5V5H2FRzqvE3RyA92XdkPKMvwJpNuP", + "LayMbyFpKVCtQ9nFccxcfhBya6qVQhfhbu", + "LiPzSX6U6Yn7EhGPwfEYdjhxyGnUtQQGJZ", + "LNxATzDKeGGt2EhyA8LwTEXoYjPv5iTXE8", + "LUYBZfFzXMNcMaFvgeQT58DgSYmf97zbMF", + "LMbym2ssdxWSdSQ3mbniW8cXhjigeXD5Kc", + "LhUTs87noKSP4TbPLcBddGmFSZpJNAwoqP", + "LSA2jhkmZgwZWBJfdWmdM86QkKAfrYMTP4", + "LYbWxSsJKzi24EGjz1i586XwickyAMBHEo", + "LL5U5BbErTwAxHkNhzgpPpknbgdutAbJfa", + "LSQ1RLD3PtazaMAi1uFte1f2iS5AhGmmTE", + "LMHMqXh1ksc2yU1L7kmSwhmVPLe6n3dWLC", + "LREYqFbYo1ZpmZHagSxGRsYneUJbkStZwG", + "Lf6jQQ2xXF87TsyqpYMPynTF39e4DpnsbS", + "LgGKHMPigMHGTnCFtRbma6nHEs52TDiZkR", + "LSyEah2pmQpKWHW1MsPkBUukv3JwbLFbp7", + "LPMwBp8Mb6o6332oYNF7V2BHxtPcB6xNvV", + "LUAzns7WnqjVRnqjh5Zu6XqjhbzYDbL8jL", + "LLheFGLodSJT7A45C7me94sp8BzcnbtU8X", + "LSXUFKZTicrw9LjdxE8v6RLyDgpaqEQXT8", + "LeUHN9Y1Qe6TdRX3nU7yLpEG1EKSQ7Bzvr", + "LKkw5wELgeaF371xzrUwYriJGu4Enpoe2A", + "LVnRUBuZJ4pWZMUJJevEZHaCqqZvKayZcj", + "LMZMDKE53tZa81MbHW61Lqnx8UcCFQrWdK", + "LS5meyubrb6gpnh7ScfANA5EA6TYE3hgAG", + "LaWf86KLNBi7xtonjoxNusr7dTYJM3Apsb", + "LdWAisbV4u7ufsvZc95AV6nbedkqcDSztz", + "LLewMMzTp9t6buyCxxN1GJKwviThwQuZF9", + "Ldscfk9QZhVgTxcTrpqpPfuh4d13PBk5LX", + "LcUkF9tHB2de1jvv9XWicqCSJH3boGuvrB", + "LVuh6heCKcHgQAdAcQeRa3BHNkYB2SN1sB", + "LbzcWGkCztHWSHrCWZGEK7gBwuCEYBS36F", + "LhTWof4eFXp8b9fiGXBNChVv7F6cCUqzcp", + "LbR3mpr1aYMsRBV35Rrd5tab6CSt1Kok88", + "LYN8VrfVT2KKNECnDXzMrmMsBXTKqT8qUD", + "LPdALGaG13ZDTyjos7ZzZnFKxwnoiq7Vvx", + "LLYNyWwRGLZtbhvVtwE9KaVgqNezmU834F", + "LRQgcZZDGA4ArbSY3t7dDMJQiCC7fm6Ngw", + "LNcWTb8vVNtVcdt8ePdVYtKuceywXGexnd", + "LfZygy36TzCJcLB15gj6iUPF2PqEKkqfjn", + "LY6b1KJ3PAoYEJG62FPsVygPr1Nt4dLxJu", + "LMpzSmo3xMdQu51gxD7AhiFfTdefjUGLcr", + "LePET2HXDooy9HV5XPCfyJb4dSz7btw7tc", + "LSF2ukWRZ6KzXTHYSdDPPVBGkE7QvpyCN3", + "LVNaL3VNted2bwyXkopvm3MSi7BeGNSgc5", + "LeJcMr4q6QqoV3ZTvi4uFxjvopd4Speu52", + "LX7LDTBPhr43pNmpALrVSpZuq3qp1WiDiW", + "LbGqdgDvKayibqwwWBUbc4f6HD8xkns36n", + "LL23kEmGYb1K3CRm1zLSjEfb5pHHBbh1qN", + "LWcz95wSBjuR3AGywLowEmFNFcuVvGU5Qr", + "LKs9Dy2fTeVGunYJXBEqdxxmxCxanG1KSM", + "LWAR63677sTMBh7CwGUgLF6xHrH83ybGgQ", + "LSURJv7fyEnibxuU5c9CCQpjZz4FyEVoTB", + "LeFrisey4bwdBXEPBZUnvsyhvp5R6HxjbN", + "LMhauutxdaTBvNBWt5Z3XVhGE72h56m3Y8", + "LhuS4qaCekfrsac8dW2fsD4pLQm9rf7AkU", + "LcQABUJC14zkbVKvgYKYAyvaDbcaEs6aio", + "LXPvebPoLED5cVyF5WtkVfX3wkXnQvtBFc", + "LQ6VBRdhtXYGsuJz8gn3TEJ94G7tRjvsnj", + "LTY8VWCMTLpkEwMu7GvXJe6Z9G6fytkJBQ", + "LRDR9HF7TmC3W7Z4hCFC8Frwou4qvs5MEG", + "LhGcqqfHd9VPDXVLjnkcisayWqZ7mMWExG", + "LcMMWNyc1MkwZzC896Yf8s3w54XEKjHJRk", + "LQMzwTguqtKaNxGCKQQBf3TG82UHwuhCet", + "LNcbiXmJH518LtazqSsYRNxzKSntJUrwxr", + "LMvJZzJAMShWmGhbqfbcYvFjjkkbjNBtuE", + "LRknefn3RKyVxwKkjdum9us6jerbRUUEYa", + "LSyXCZdZZ6JFQx2eFKzVsx8fCjajshKT2S", + "LMHGHtU5fasRisPz5Xd1qSZR5X1JpSME1S", + "LNodSYeXtiLWvakGXrP7wHsfu1veicrBp7", + "LhJXTJpxjUPV5yqqW9UsMzxM5dMa7WdVuE", + "LQ3GjdVVnbBoZmHGupZmNJYB1rdhACRm73", + "LbqpU58UNaUh6QptH4a325fEFHibXtisEi", + "LeLiUyNTXYV53rJzHBDL9MCeZwHcYg6oer", + "LQfCDij1hUNhUyQjmjEFiS3awGshk81W1j", + "LWES5WWwType47rjoqHTQ3cxhDtn7UKS17", + "LNe2VSdTEW5tWyCsHBtQi7ZEzvqwV6AC6F", + "LP4j37grd1NTRqdsNMVvPhH3YVPxRey6cd", + "LaN2VnxNMswM2gzJP5JBc49UUvNW2hQM2t", + "LZFVajmyg5S4GNmVxZ8w96uUoCuTk49SZb", + "LaCXjjfV7yvWFuoxq4MbaAA26HmDZ8ygnJ", + "LgKanQNSNYEKcx9mMUXGiZEe4sGvDsfEf2", + "LZyL3cG9DFZPShwPNrY5A7bFFC9REDz7nX", + "LUJMcEyxWi4H1t3VJm5p4MvtRNQwJMAkaq", + "LbJdtCU9BwuLadLQrsGw5LXQTw1cofCQUo", + "Lcozgv1ePhxZJmtPhXYUM4mAqBsgrixfJL", + "LT1nhGV3ugGUsuQ5S3P7hamuj8FG1XzXEP", + "LRnuLSyDwEBUwN8Zjv1iM9f2PdSrJaXkiv", + "LXDfRLDT4aSaD59HBFiDLVoDsKzbKHvEG7", + "LYNKSRZU5NzsJ91k8F5d8w9n3wTXRmESco", + "LT7wT3iqV9VBfQUyETdc22YgmuYPpsz92L", + "LR5NkVi57qqNmxBTgNXpaLMyF2R1rSWfdv", + "LR7vwew7NMGS5TwY7nmxjENY3Tw1pAEpBn", + "LRQY6WqBwZPXH6L4HXQSZCLuPZKLoR2A37", + "LPjTYuZumxSp9L8o1PZvpnNJ12iiDrJzpF", + "LchWtmeyR2sSPioQ5uDvj5zHqgRiPuSnx5", + "LR3TKpkSdQLpc9uBfJJhshkc5YQWaeXzzA", + "Lf6kkRPHqxXPtURWyhV3NrSMQhvJxHeFMM", + "Lh3hMVpf5cW5iMs1a5ZSH7wamdj7Cu7A4J", + "Lh1d9VCvuJTH7zankijKBQCHT2TBTS84rQ", + "Ld8GhZGMzsvWrrrLRdt1vK4jQ17swUKVc6", + "LUZnNT1Yxu5weCP1xYmUQoXxJ2yJjNwvyB", + "LYuvKNFjNhXUd4PxRfwBkB4mLwt8SwM1og", + "LaR2Zgy44XV3ZfqwjjL9ZBXJjMdL78LKC5", + "LZssXv5uSuictUyhXtXFmPJT3i7r8WTaFd", + "LbVLwA5kueFzQtCvjLWZcicSACTwfpt8pK", + "Li6QhA1JwQRdooVYWQmqNY7ajapPNiEYpV", + "LeZLuU9H2YoSUcnf8oFZCoGfsnv1PpScgA", + "LZCwXzfqSZWSqUHhZ8ZvQDRA54TdwHkzm4", + "Lfw2PmMhY1s9kSmfGid6wRY4KeswmuT1mT", + "LgZHB8N4sj1ajAqntXEvfZC46pCVfpNWXC", + "LT9CRh2GCidjVSB9yDzhAYR9o29JMzFV52", + "LhApBvac5mtAoeRhyeM1UHBAKD6t5o337g", + "LffTmaekgdvzKBZarsTzQoooictqjyRfZ9", + "LhGbWvFi46d4RmuGZZDedbpeK5cpmDVKjK", + "LaDBbkQG2eg3oTGcJJcUbSqYujEWD8GxGV", + "LSBeVyUmAomavuAaM3NRAcWqBJSCMZkma3", + "LaArmQWVYATX1RqLfcHTuiEKPy1uAUK3So", + "LaqS7mm5khUqsMcoH29pcaxJnMTZy4h3VP", + "LPV77htAh4RvvJnK2poWuBJA1XKU5UuYn6", + "LbtB14P8bSDL7BBS5u9C7QzfSbLXSMuWQ6", + "LeTFdUsPNG9DpFzmxqdNzcRXWiUhVBFSje", + "Lap9Zp3RjjiasRKZ6HVh9PrrxTnbEzmQQ1", + "LRJFh8Gem1G27ZER5RDqjaEqMknkDAnL9H", + "LXPdXPJ2ejvQEUuKii2XmKpcwWGKReQ1AF", + "LV4EBa9dDLAVrnoP1ZJ9uPZ21QSnGsyng5", + "LMTQpVaabmQ4GJRhya5UdDbBYwCnnrw9j2", + "LhomQHA6tTsLcPEmm9XyDBKTUvmZPEGCw2", + "LV6zkyyQ4w6MTstKHGUTvp147qF6tDK4Hv", + "LZ7JjSm2ipzdDSS67TcDz6ypYB4uRRh5qb", + "LKmkBiyWJcrk2qsGpDLSpENU9VmFUhTFLb", + "LcfSw6juAquu7ELptLuWdGVkFeoCGBt7TT", + "LVVeiEtgxcAmZNR5ZS7EkDEuP1rSQ2JR2d", + "LdggQFKWBZWXBEhmXAa4jVqPvV626maE4A", + "Lb2Uf2AJHvMg5gHebHEnhEhLRnjtZ3AKKb", + "Ld9zAq3tJ3KfxcXoXPXmKPS3R5yzrcD1zt", + "LM9B7S8wn1oXUnPpYXPjQeWufnkNYbvXwb", + "LMRdhEeegHTXXwBSEmv3Gzc1fQ9m4bR7wH", + "LdhYdFtLHfnWrnBFCcXirMy6BUr8mapYxT", + "LWLCjJqo4M1XbYQSKrXfhfoUB4pZxMQsTT", + "LYM1ejQWpBhdkbEhmfJrQ2KXFyeB5U9uG4", + "LKkDW2qjDbR2aumXuWysSUxhPsyKsLYLLQ", + "LSiQ7Evxz4KmEpaWoMFsb4in99iiZqchXi", + "LgFdXHkjNNuRsnjvUFByj2y8RFKBwigRvv", + "LgmZeoFz3LFw3M6iH666sXs4efPPPFDFRn", + "LRwVbxqon6hhg4uKFFjsPdsRBcb6CuuY8N", + "LTW9LQ4m3jvpaJQqG9SywG2YGxTxc95XDS", + "LhHYXKgU4ediDQQiVKgFcDss1Faxj3dCwT", + "LhkNzhtVPqyTiM94eDT5jAGpk6rrjBWySD", + "LYQAfktqsZm9UXFT8Gbq7hxQHLXLvvY9Vw", + "LSWtD79xfEKQie9xkYpLq7GtgPRCosKtqr", + "LTfbfSNWmwayq8XcjVVvQ7L6RRv4YqhnXb", + "LcssJQC1XYDMTibztCZQmEk56tefgHWceJ", + "LWr6QzQCSbgCmfmiMXRPEDt3RjQL1b1h6v", + "LexyJFNrgcNnnFL1ty5mWvxbLhJvPQr6Ps", + "LPbSJPvBLwAsFSARdt1zEi8maLMmEG6su4", + "LcJUVq96kyxh4vYjQ2V1mLbRLad4oxckPu", + "LL7sb6UWoGaXZ3Q6Y81sff5ASdW7ziSZgC", + "LXP1QYfCQRdA4CdnUjUJdMxoK3RPCCV3G5", + "LRr5KW4SK2APodwB73UmJWzMJYKk5edKCb", + "LSZjBdRuru46fg8KT6eubsHPfibmbQNgSk", + "LVnGQcnFjRqXWKocqFycqthhejNrDUb6ns", + "Lfmjw7wfBTtKYxeF6yYcg9VFp4yyoCQzVQ", + "LTeFKRndXEA3DDjVENvqmwFFWYarRtScxb", + "LdcV6oRJgYv9Qc52JkYCepnA5pW923Fygh", + "LZ7BP6uy6VcbXa5vYeQwpiCHHADi7R3QE4", + "LhkatG3dDHy85RFmEpKuRv88YHJL7tx11e", + "Li9n2e2GYndUMgjT8YarNmptsqyy57nBCn", + "LLwKLVEKMse8WbFU3t5m6oqeq6yQzv6Vwm", + "LYLEJH4tTtDECUWTRkv2JNgGp2qnC83eS6", + "LPGBJqe867xF4tjgQwknzoVU6v2XpggdaS", + "Li1TcaUhEP4tkbudkWncfVCY1j1wMgPnS6", + "Lbg2zTeZBjm3pN4c2cmJ7Aa83GNgJPP5rk", + "LRYV3RzHidi7vSYqyGTco2cLuTvkKWjDZT", + "Ldv5gad84XcH6AntbxjGxZHzcygCzgsi3G", + "LQ3ghptDNPhd8Cf5N7kBrvTbd6wAnfVTqy", + "LX9UGoNbTQRQmfymnXvqLKgbTqPofo2ZQe", + "LSTizhmpK1xbmAB6ug729uLs5xp8dir99o", + "LaXB6PT7Ur5PfSeKQtizmeaQzfKf9ULHeo", + "Li24gKAxQGHhQMcTNGVFMPikKoGu5HoQNj", + "LZ2mXGuvoZJTvtYxsupvNdjMrUEfFVcs8b", + "LNWTCA42visTpGMMmKMs8EBG5P5QFYwmXw", + "LRMYix9UCn76pmB87daetkypi9rEA9BkcK", + "LZBUZ9WpmCr1ki5tWED6LPRgKtUqi6t3EG", + "LKcGMQghBrnAutuyB9oZryJzyxhnoW5qyA", + "LgNtTi7jnv2T7nVLtiLs7MXc9fUcZCM1PX", + "LRtL2QCJ885ndQRYcCyxHtohojNtQ3kwms", + "Lb3Q8crTsgPJVhF8hGocPadUG8L4Ko7Uk5", + "LVRgQ7MYCVQkGN96mywZDC7X4wVKnVScXL", + "LgFC8Azpm1QQg4zf1stvyhdmk1J3GKRXgg", + "LZyaVaUVx1S4R9RtvVPHmirBCxwYy5BLqy", + "LWqHKipQvgBJXFH1YY9Aqyvidz3JLsbNqu", + "LS4EQ4HUyMj15S7uh2owumLWarpMwzYWf2", + "LeRNyZ4chbEqkuDtfSqC47r6aMDnR1Gi72", + "LeVbPnsv8Kys7dcRaY3yg9infzmz7G9Hpo", + "LUEVtdDJ8X8jVdNiUEX1xvzVQxcsSZPkrz", + "LMas9g21ce1eyXByhr7Ngaygbfo45Q8N4Q", + "LiEwXax16TsBmJpjTe1w332TeyUSamnvvA", + "LeGQnoVFNRWT3tUJFo8SToE9KYy8Px939K", + "Lfq1tZYPSa6LJMWMCzEfPVjyMjnHAUPnwj", + "LhiG7enkC7hUoRVyDset6nG3jBmFAnxP76", + "LhbLG3xjaFjA16LxiWKiA3yz1UPuam2LiA", + "LgafdEoGQynfHUJZGHLVaQcrr69sHjDjVB", + "LWh4HzjkKUMvQYCmyEuiLhwEnzyedFGgRD", + "LhmNnMErWRvw9pBhEaGA1h71iBrxBe91Ef", + "LZdBrWEZYwevQWw5N7cfx4Hp2716tWEo2y", + "LYqypgTC2U1H7e4mWtmDQXEJuokF5SAcLB", + "LQF3JiSeVnhARcgCJHSNvzdqRvmJGsuzvL", + "LURqMivxZfqtWHaydfLtsUwpd76pjxBuke", + "LYvQksgCo4c9pR1id6dbKDwHPEETYMZXuf", + "LREa1oKedRNis7T7XJ3cNq9tBAXCtdcGiU", + "Ldk6o44cz6YxFNvmvYhhPficYgmLe5hT6d", + "Lbx8PM2ss9DcPYMmzZqFcyjyFaLJM6Cd4Q", + "Lc7TBKGbvX9HNsZGyhWWrXGCLzpEZ7cEdX", + "LQfigrdhHAVV26uNS3hCVRxJZtDxe3eVV6", + "LZLynLMfUvPVMAmNXF46LKeA8jfUmUBKxe", + "LWnKVg3QJta6oyMVNLo6htofgyRfupkA7Z", + "LKHnGKbzJX7e24QJL98f6ABtuG4MQjAX4Z", + "LKexRP5HvwdBLSxLG5FnB3QVXZqNqARFiK", + "LgMa3Az7WKhMYcA9HB9abzkZysdkihHy5p", + "LMQybwAsWNJCLhVukHdntHCZ4R6K7nqZJJ", + "LNEbx9Q5mvnkUdKKqBBjm3bVbvTKfBLm7U", + "LL73GoiYM6pz2RVMDLWDAQhyjf2QtEojsj", + "Lfzvumvksk5EyUTKiFJAcKubAUDq7iiDLR", + "LQfuDpy9FtxDuwH8KvV6v8U6CDEmLPCtLq", + "LXAwyHXJ5uXkEtJr8eERwWVDWmiTwZjDXZ", + "Lbh4tQwuELbkx5MSoNgeHPpLgkYgzwBqmN", + "LL8Sr3Pm1DMLSgjgE2tzTgaPsBn4tUBZMf", + "LaTtLUvoWMvjDgUtiu25HFcTXrq1B4ZZiR", + "LhhaJPMJGCK9zfoRqZV4FdvrqHrfBuZRtT", + "LQCko1GY68nozg5TSDYBMvTgah19GUdy9v", + "LiMQxVrAutJDdUoJYMn4HxPJU6MUJo7Wih", + "LTHE6Thf2TD3apc2k8bGe49uT8vk7Duumk", + "LN4HkCAKxkuQTd1ZxxG3MdUZL7h9956sLD", + "LggbiEPxm9q7qujBgH8VowJDz462EeocVe", + "Lai1PeadaURQGssF7wCJVkUCkMKVCct7xt", + "LSpBCFpxVR6rHNmQPF2Emj2biJ8jLHkwRW", + "LdBpRFJECJfRMbvXL1Jp6qVhpkR6orzeUt", + "LKN3hdMPteWLEXBQxi5pwXdeufFa83FwgN", + "LVJvfkYtMNKUdME8zmMAkNCnK8CPgx9hS3", + "LWfJsaBmH9ppZYP4q2tCtUmqe178f1HHw5", + "LMKE317FWXkzDMCTptsvPxHfCWSPsD2rDJ", + "LVsxLvnbCw2Veirdkm57WwJ1g6PezjQc63", + "LXLmZvvK19RiNLxUhAfmWbjQEjDr67bNyV", + "LPTrg77VfLw6sGa5FH31E8C9iepsxNzzDk", + "LLzSdkRuTkSBFVSjJjjMQiKrcMuCTsA28x", + "LfaJR6MJRJ6P8xoZK5bYo8ee2oZUETfeGu", + "LdRrMKNj4qLNpMXLbM53rLxAepF4baTpLe", + "LULuUSgDKn83sESXpTPZ2pMYkR4JVjQaBJ", + "LdoTfSDBuH83aCvWtL9sHwiFvUTXbYwf5Q", + "LfV2DPCW5m2WMGBGMhLUv8bqq4BwN37JP6", + "LXhYvad9n65E9FNeSGwqi7jHMQQnc1gKiQ", + "LMCDQqLdsHiXBwe6Qr5vmMnkg1M8TYU5XN", + "LiSTM5jNER3o6G1uTDosw5kVM1XvRBaY7g", + "LgRrD1BXY3JydbAiBz7DfdcurjnLpHyXJu", + "LPiPe2TxtCzs3gsiGK89urizhyRsd14j5E", + "LUbTszmejtBwnHMWTqofAvTAGSnUcyZhK8", + "LVgVwUBwP45kBjQLq5h221WYqqQbeLmLrY", + "LVqE67xxpsqahokAGYrnCbXEvvUdNgZhwe", + "LXw9igkHBKPN58f9UXRK1CAqoJSFXPr8yu", + "LhkjGCFtV1LbdisVmruFZnC6TdaXT1iD1y", + "LaQEaTTBKk8VudnfpGKKLt8fwu4UNDK7Fe" + ] +} \ No newline at end of file From df61605e88646973d04b7f6b6aa79e3ebe8b0809 Mon Sep 17 00:00:00 2001 From: tryiou Date: Fri, 12 Dec 2025 17:51:50 +0100 Subject: [PATCH 06/73] [exr] Fix HTTP response resource leak --- .../app/net/api/http/client/EXRWrapper.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/EXRWrapper.java b/src/main/java/io/cloudchains/app/net/api/http/client/EXRWrapper.java index 7215777..543c344 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/client/EXRWrapper.java +++ b/src/main/java/io/cloudchains/app/net/api/http/client/EXRWrapper.java @@ -6,6 +6,7 @@ import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpRequestBase; @@ -47,12 +48,13 @@ public EXRWrapper(String exrEndpoint) { /** * Execute an HTTP request and return the response body. + * Uses the same resource cleanup pattern as HTTPClient.executeHttpRequest() * @param request The HTTP request to execute * @param operation Description of the operation for logging * @return Response body string or null on error */ private String executeHttpRequest(HttpRequestBase request, String operation) { - HttpResponse response = null; + CloseableHttpResponse response = null; try { response = client.execute(request); if (validateResponse(response)) { @@ -69,7 +71,13 @@ private String executeHttpRequest(HttpRequestBase request, String operation) { return null; } finally { request.reset(); - + if (response != null) { + try { + response.close(); + } catch (IOException e) { + LOGGER.log(Level.WARNING, LOG_TAG + " Failed to close HTTP response", e); + } + } } } From dcd7fdb387e4be88c55cd69c1404e240ab110925 Mon Sep 17 00:00:00 2001 From: tryiou Date: Sat, 13 Dec 2025 11:49:02 +0100 Subject: [PATCH 07/73] [feat] Add tests and improve AddressDiscoveryService robustness --- pom.xml | 6 + .../io/cloudchains/app/net/CoinInstance.java | 2 +- .../app/util/AddressDiscoveryService.java | 52 ++- .../background/BackgroundTimerThread.java | 6 +- .../java/TestAddressDiscoveryService.java | 442 ++++++++++++++++++ ...{TestWallet.java => TestCoinInstance.java} | 111 +---- src/test/java/TestConfigHelper.java | 209 +++++++++ src/test/java/TestHelper.java | 143 ++++++ src/test/java/TestLoginUtils.java | 122 +++++ 9 files changed, 979 insertions(+), 114 deletions(-) create mode 100644 src/test/java/TestAddressDiscoveryService.java rename src/test/java/{TestWallet.java => TestCoinInstance.java} (61%) create mode 100644 src/test/java/TestConfigHelper.java create mode 100644 src/test/java/TestHelper.java create mode 100644 src/test/java/TestLoginUtils.java diff --git a/pom.xml b/pom.xml index 45beb89..7b7bf39 100644 --- a/pom.xml +++ b/pom.xml @@ -149,6 +149,12 @@ junit-jupiter-api test + + org.mockito + mockito-core + 5.15.2 + test + diff --git a/src/main/java/io/cloudchains/app/net/CoinInstance.java b/src/main/java/io/cloudchains/app/net/CoinInstance.java index 287a5e2..0aea904 100644 --- a/src/main/java/io/cloudchains/app/net/CoinInstance.java +++ b/src/main/java/io/cloudchains/app/net/CoinInstance.java @@ -484,7 +484,7 @@ public CoinError init(String pw, String userMnemonic, boolean isMnemonic, boolea LOGGER.log(Level.INFO, "[coin] Running address discovery"); runAddressDiscovery(); } else { - LOGGER.log(Level.INFO, "[coin] Address discovery disabled"); + LOGGER.log(Level.FINE, "[coin] Address discovery disabled"); } // Make sure wallet addresses are available diff --git a/src/main/java/io/cloudchains/app/util/AddressDiscoveryService.java b/src/main/java/io/cloudchains/app/util/AddressDiscoveryService.java index c3d7667..fc5a814 100644 --- a/src/main/java/io/cloudchains/app/util/AddressDiscoveryService.java +++ b/src/main/java/io/cloudchains/app/util/AddressDiscoveryService.java @@ -22,7 +22,7 @@ public class AddressDiscoveryService { private static final int GAP_LIMIT = 25; private static final int BATCH_SIZE = 100; private static final int MAX_DISCOVERY_DEPTH = 10000; - private static final int DISCOVERY_TIMEOUT_MS = 30000; // 30 seconds max + private static int DISCOVERY_TIMEOUT_MS = 30000; // 30 seconds timeout - made non-final for testing private static final int MAX_CONSECUTIVE_FAILURES = 3; private final CoinInstance coinInstance; private final HTTPClient httpClient; @@ -34,14 +34,31 @@ private String getLogPrefix() { return "[discovery-" + currencyString + "]"; } + /** + * Constructor for production use - creates its own HTTPClient + */ public AddressDiscoveryService(CoinInstance coinInstance) { + this(coinInstance, new HTTPClient(5)); + } + + /** + * Constructor for testing - accepts HTTPClient as parameter for dependency injection + */ + public AddressDiscoveryService(CoinInstance coinInstance, HTTPClient httpClient) { this.coinInstance = coinInstance; - this.httpClient = new HTTPClient(5); + this.httpClient = httpClient; this.configHelper = coinInstance.getConfigHelper(); this.currencyString = CoinTickerUtils.tickerToString(coinInstance.getTicker()); LOGGER.log(Level.INFO, getLogPrefix() + " AddressDiscoveryService initialized for " + currencyString); } + /** + * Setter for timeout - for testing purposes only + */ + public static void setDiscoveryTimeoutMs(int timeoutMs) { + DISCOVERY_TIMEOUT_MS = timeoutMs; + } + /** * Main discovery method - determines correct addressCount based on last used address with funds + 1 */ @@ -174,8 +191,9 @@ private List checkBatchForUtxos(List batch) { try { utxoResponse = httpClient.getUtxosUncached(coinInstance.getTicker(), addresses); } catch (Exception e) { + // Log without stack trace to avoid bloated output in tests LOGGER.log(Level.SEVERE, getLogPrefix() + " HTTP request failed for addresses " + - addresses[0] + "..." + addresses[addresses.length - 1], e); + addresses[0] + "..." + addresses[addresses.length - 1] + " - " + e.getMessage()); return null; // Signal failure to caller } if (utxoResponse == null || utxoResponse.size() == 0) { @@ -185,17 +203,33 @@ private List checkBatchForUtxos(List batch) { for (JsonElement element : utxoResponse) { try { JsonObject utxoJson = element.getAsJsonObject(); + + // Validate required fields exist and are not null + JsonElement addressElement = utxoJson.get("address"); + JsonElement txidElement = utxoJson.get("txid"); + JsonElement voutElement = utxoJson.get("vout"); + JsonElement confirmationsElement = utxoJson.get("confirmations"); + JsonElement valueElement = utxoJson.get("value"); + + if (addressElement == null || txidElement == null || voutElement == null || + confirmationsElement == null || valueElement == null || + addressElement.isJsonNull() || txidElement.isJsonNull() || voutElement.isJsonNull() || + confirmationsElement.isJsonNull() || valueElement.isJsonNull()) { + LOGGER.log(Level.WARNING, getLogPrefix() + " Skipping invalid UTXO - missing required fields"); + continue; + } + UTXO utxo = new UTXO( coinInstance.getTicker(), - utxoJson.get("address").getAsString(), - utxoJson.get("txid").getAsString(), - utxoJson.get("vout").getAsInt(), - utxoJson.get("confirmations").getAsInt(), - (long) (utxoJson.get("value").getAsDouble() * 100000000.0) + addressElement.getAsString(), + txidElement.getAsString(), + voutElement.getAsInt(), + confirmationsElement.getAsInt(), + (long) (valueElement.getAsDouble() * 100000000.0) ); utxos.add(utxo); } catch (Exception e) { - LOGGER.log(Level.WARNING, getLogPrefix() + " Failed to parse UTXO response element", e); + LOGGER.log(Level.WARNING, getLogPrefix() + " Failed to parse UTXO response element: " + e.getMessage()); // Continue processing other UTXOs instead of failing completely } } diff --git a/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java b/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java index 8c65e33..7726f0a 100644 --- a/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java +++ b/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java @@ -7,9 +7,9 @@ import io.cloudchains.app.net.protocols.blocknet.BlocknetPeer; import io.cloudchains.app.net.protocols.blocknet.BlocknetPeerGroup; import io.cloudchains.app.util.LogRotationUtil; -import io.cloudchains.app.util.XRouterConfiguration; - -import java.time.Duration; +import io.cloudchains.app.util.XRouterConfiguration; + +import java.time.Duration; import java.time.LocalTime; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; diff --git a/src/test/java/TestAddressDiscoveryService.java b/src/test/java/TestAddressDiscoveryService.java new file mode 100644 index 0000000..1ab74e0 --- /dev/null +++ b/src/test/java/TestAddressDiscoveryService.java @@ -0,0 +1,442 @@ +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import io.cloudchains.app.crypto.LoginUtils; +import io.cloudchains.app.net.CoinInstance; +import io.cloudchains.app.net.CoinTicker; +import io.cloudchains.app.net.api.http.client.HTTPClient; +import io.cloudchains.app.util.AddressDiscoveryService; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +/** + * Comprehensive test class for AddressDiscoveryService. + * Tests address discovery functionality including timeout handling, circuit breaker patterns, + * batch processing, and various edge cases. + */ +class TestAddressDiscoveryService extends TestHelper { + + private CoinInstance coinInstance; + private AddressDiscoveryService discoveryService; + private HTTPClient mockHttpClient; + + @BeforeEach + void setup() { + commonSetup(); + // Initialize Mockito + MockitoAnnotations.openMocks(this); + + // Initialize coin instance with test parameters + coinInstance = CoinInstance.getInstance(CoinTicker.LITECOIN); + assertNotNull(coinInstance); + coinInstance.getConfigHelper().setAddressCount(getAddressCountInitial()); + assertNull(coinInstance.init(LoginUtils.loginToEntropy(getPassword()), getMnemonic(), false)); + + // Create mock HTTP client + mockHttpClient = mock(HTTPClient.class); + + // Reset timeout to default value before each test + AddressDiscoveryService.setDiscoveryTimeoutMs(30000); + + // Create discovery service with mocked HTTP client for testing + discoveryService = new AddressDiscoveryService(coinInstance, mockHttpClient); + } + + @AfterAll + static void cleanup() { + // Reset timeout to default value for other tests + commonCleanup(); + } + + /** + * Test 1: Address discovery correctly identifies last used address + 1 + * Tests the core functionality of finding used addresses and setting address count correctly. + */ + @Test + void testAddressDiscovery_FindsUsedAddresses() { + // Setup: Generate addresses and simulate UTXOs for some addresses + int initialAddressCount = getAddressCountInitial(); + int usedAddressIndex = initialAddressCount + 10; // Use an address beyond initial count + + // Generate enough addresses for testing + for (int i = 0; i < 50; i++) { + coinInstance.generateAddress(false); + } + + // Create mock UTXO response for the used address + JsonArray mockUtxos = new JsonArray(); + JsonObject utxo1 = new JsonObject(); + utxo1.addProperty("address", coinInstance.getAddressKeyPairs().get(usedAddressIndex).getAddress().toBase58()); + utxo1.addProperty("txid", "test-txid-1"); + utxo1.addProperty("vout", 0); + utxo1.addProperty("confirmations", 10); + utxo1.addProperty("value", 1.5); + mockUtxos.add(utxo1); + + // Mock HTTP client to return UTXOs for batch containing the used address + when(mockHttpClient.getUtxosUncached(any(), any(String[].class))) + .thenAnswer(invocation -> { + String[] addresses = invocation.getArgument(1); + // Check if this batch contains our used address + for (String addr : addresses) { + if (addr.equals(coinInstance.getAddressKeyPairs().get(usedAddressIndex).getAddress().toBase58())) { + return mockUtxos; + } + } + return new JsonArray(); // Empty for other batches + }); + + // Execute: Run discovery + int discoveredAddressCount = discoveryService.discoverAddressCount(); + + // Verify: Discovery should find the used address and set count to last used + 1 + assertEquals(usedAddressIndex + 1, discoveredAddressCount, + "Discovery should set address count to last used address index + 1"); + + // Verify HTTP client was called + verify(mockHttpClient, atLeastOnce()).getUtxosUncached(any(), any(String[].class)); + } + + /** + * Test 2: Address discovery timeout handling after 1 second + * Tests that discovery aborts gracefully when timeout is reached. + */ + @Test + @Timeout(5) // Test should complete quickly, timeout indicates infinite loop + void testAddressDiscovery_TimeoutHandling() { + // Set timeout to 1 second for test unit only + AddressDiscoveryService.setDiscoveryTimeoutMs(1000); + + // Setup: Mock HTTP client to throw exception immediately (simulating timeout) + when(mockHttpClient.getUtxosUncached(any(), any(String[].class))) + .thenThrow(new RuntimeException("Simulated timeout")) + .thenThrow(new RuntimeException("Simulated timeout")) + .thenThrow(new RuntimeException("Simulated timeout")); + + // Execute: Run discovery (should timeout after 1 second due to circuit breaker) + long startTime = System.currentTimeMillis(); + int discoveredAddressCount = discoveryService.discoverAddressCount(); + long elapsedTime = System.currentTimeMillis() - startTime; + + // Verify: Discovery should return original address count due to circuit breaker + assertEquals(getAddressCountInitial(), discoveredAddressCount, + "Discovery should return original address count when timeout occurs"); + + // Verify circuit breaker triggered (3 calls max) + verify(mockHttpClient, times(3)).getUtxosUncached(any(), any(String[].class)); + + // Verify timeout occurred within expected bounds (should be very fast due to circuit breaker) + if (!(elapsedTime < 1000)) { + fail("Discovery should complete quickly due to circuit breaker, actual: " + elapsedTime + "ms"); + } + } + + /** + * Test 3: Address discovery circuit breaker for 3 consecutive failures + * Tests that discovery aborts after 3 consecutive HTTP failures. + */ + @Test + void testAddressDiscovery_CircuitBreaker() { + // Setup: Mock HTTP client to throw exceptions (simulate failures) + when(mockHttpClient.getUtxosUncached(any(), any(String[].class))) + .thenThrow(new RuntimeException("Network error")) + .thenThrow(new RuntimeException("Network error")) + .thenThrow(new RuntimeException("Network error")); + + // Execute: Run discovery + int discoveredAddressCount = discoveryService.discoverAddressCount(); + + // Verify: Discovery should return original address count due to circuit breaker + assertEquals(getAddressCountInitial(), discoveredAddressCount, + "Discovery should return original address count when circuit breaker trips"); + + // Verify HTTP client was called exactly 3 times (circuit breaker threshold) + verify(mockHttpClient, times(3)).getUtxosUncached(any(), any(String[].class)); + } + + /** + * Test 4: Address discovery when no addresses have been used + * Tests behavior when no addresses have UTXOs (empty wallet). + */ + @Test + void testAddressDiscovery_NoUsedAddresses() { + // Setup: Mock HTTP client to always return empty responses + when(mockHttpClient.getUtxosUncached(any(), any(String[].class))) + .thenReturn(new JsonArray()); + + // Execute: Run discovery + int discoveredAddressCount = discoveryService.discoverAddressCount(); + + // Verify: Discovery should return original address count when no used addresses found + assertEquals(getAddressCountInitial(), discoveredAddressCount, + "Discovery should return original address count when no used addresses are found"); + + // Verify HTTP client was called for multiple batches + verify(mockHttpClient, atLeastOnce()).getUtxosUncached(any(), any(String[].class)); + } + + /** + * Test 5: Address discovery batch processing with 100 addresses per batch + * Tests that discovery processes addresses in correct batch sizes. + */ + @Test + void testAddressDiscovery_BatchProcessing() { + // Setup: Generate more addresses to test batch processing + int totalAddresses = 350; // More than 3 batches of 100 + for (int i = 0; i < totalAddresses - getAddressCountInitial(); i++) { + coinInstance.generateAddress(false); + } + + // Mock HTTP client to track batch sizes + when(mockHttpClient.getUtxosUncached(any(), any(String[].class))) + .thenAnswer(invocation -> { + String[] addresses = invocation.getArgument(1); + // Verify batch size is correct (except possibly the last batch) + if (addresses.length < totalAddresses) { + assertEquals(100, addresses.length, + "Batch size should be 100 for all batches except possibly the last"); + } + return new JsonArray(); // Empty response + }); + + // Execute: Run discovery + int discoveredAddressCount = discoveryService.discoverAddressCount(); + + // Verify: Discovery completed successfully + assertEquals(getAddressCountInitial(), discoveredAddressCount, + "Discovery should complete with original address count"); + + // Verify HTTP client was called for multiple batches + verify(mockHttpClient, atLeastOnce()).getUtxosUncached(any(), any(String[].class)); + } + + /** + * Test 6: Address discovery with mixed UTXO responses + * Tests discovery when some batches have UTXOs and others don't. + */ + @Test + void testAddressDiscovery_MixedUtxoResponses() { + // Setup: Generate addresses and simulate UTXOs in non-consecutive batches + for (int i = 0; i < 200; i++) { + coinInstance.generateAddress(false); + } + + int usedAddressIndex1 = getAddressCountInitial() + 50; + int usedAddressIndex2 = getAddressCountInitial() + 150; + + // Create mock UTXO responses + JsonArray mockUtxos1 = new JsonArray(); + JsonObject utxo1 = new JsonObject(); + utxo1.addProperty("address", coinInstance.getAddressKeyPairs().get(usedAddressIndex1).getAddress().toBase58()); + utxo1.addProperty("txid", "test-txid-1"); + utxo1.addProperty("vout", 0); + utxo1.addProperty("confirmations", 10); + utxo1.addProperty("value", 1.5); + mockUtxos1.add(utxo1); + + JsonArray mockUtxos2 = new JsonArray(); + JsonObject utxo2 = new JsonObject(); + utxo2.addProperty("address", coinInstance.getAddressKeyPairs().get(usedAddressIndex2).getAddress().toBase58()); + utxo2.addProperty("txid", "test-txid-2"); + utxo2.addProperty("vout", 1); + utxo2.addProperty("confirmations", 5); + utxo2.addProperty("value", 2.0); + mockUtxos2.add(utxo2); + + // Mock HTTP client to return UTXOs for specific batches + when(mockHttpClient.getUtxosUncached(any(), any(String[].class))) + .thenAnswer(invocation -> { + String[] addresses = invocation.getArgument(1); + for (String addr : addresses) { + if (addr.equals(coinInstance.getAddressKeyPairs().get(usedAddressIndex1).getAddress().toBase58())) { + return mockUtxos1; + } + if (addr.equals(coinInstance.getAddressKeyPairs().get(usedAddressIndex2).getAddress().toBase58())) { + return mockUtxos2; + } + } + return new JsonArray(); + }); + + // Execute: Run discovery + int discoveredAddressCount = discoveryService.discoverAddressCount(); + + // Verify: Discovery should find the last used address (higher index) + assertEquals(usedAddressIndex2 + 1, discoveredAddressCount, + "Discovery should find the last used address across multiple batches"); + } + + /** + * Test 7: Address discovery with partial batch processing + * Tests discovery when discovery stops before processing all batches due to gap limit. + */ + @Test + void testAddressDiscovery_PartialBatchProcessing() { + // Setup: Generate addresses and simulate UTXOs with a gap + for (int i = 0; i < 150; i++) { + coinInstance.generateAddress(false); + } + + int usedAddressIndex = getAddressCountInitial() + 5; // Early in the sequence + + // Create mock UTXO response + JsonArray mockUtxos = new JsonArray(); + JsonObject utxo = new JsonObject(); + utxo.addProperty("address", coinInstance.getAddressKeyPairs().get(usedAddressIndex).getAddress().toBase58()); + utxo.addProperty("txid", "test-txid"); + utxo.addProperty("vout", 0); + utxo.addProperty("confirmations", 10); + utxo.addProperty("value", 1.0); + mockUtxos.add(utxo); + + // Mock HTTP client + when(mockHttpClient.getUtxosUncached(any(), any(String[].class))) + .thenAnswer(invocation -> { + String[] addresses = invocation.getArgument(1); + for (String addr : addresses) { + if (addr.equals(coinInstance.getAddressKeyPairs().get(usedAddressIndex).getAddress().toBase58())) { + return mockUtxos; + } + } + return new JsonArray(); + }); + + // Execute: Run discovery + int discoveredAddressCount = discoveryService.discoverAddressCount(); + + // Verify: Discovery should stop after finding used address + gap limit + assertEquals(usedAddressIndex + 1, discoveredAddressCount, + "Discovery should stop after finding used address + gap limit"); + } + + /** + * Test 8: Address discovery with UTXO parsing errors + * Tests that discovery continues when some UTXOs cannot be parsed. + */ + @Test + void testAddressDiscovery_UtxoParsingErrors() { + // Setup: Generate addresses + for (int i = 0; i < 100; i++) { + coinInstance.generateAddress(false); + } + + int usedAddressIndex = getAddressCountInitial() + 10; + + // Create mixed UTXO response (valid and invalid) + JsonArray mockUtxos = new JsonArray(); + + // Valid UTXO + JsonObject validUtxo = new JsonObject(); + validUtxo.addProperty("address", coinInstance.getAddressKeyPairs().get(usedAddressIndex).getAddress().toBase58()); + validUtxo.addProperty("txid", "test-txid-1"); + validUtxo.addProperty("vout", 0); + validUtxo.addProperty("confirmations", 10); + validUtxo.addProperty("value", 1.5); + mockUtxos.add(validUtxo); + + // Invalid UTXO (missing required fields) + JsonObject invalidUtxo = new JsonObject(); + invalidUtxo.addProperty("address", "invalid-address"); + // Missing other required fields + mockUtxos.add(invalidUtxo); + + // Mock HTTP client + when(mockHttpClient.getUtxosUncached(any(), any(String[].class))) + .thenAnswer(invocation -> { + String[] addresses = invocation.getArgument(1); + for (String addr : addresses) { + if (addr.equals(coinInstance.getAddressKeyPairs().get(usedAddressIndex).getAddress().toBase58())) { + return mockUtxos; + } + } + return new JsonArray(); + }); + + // Execute: Run discovery + int discoveredAddressCount = discoveryService.discoverAddressCount(); + + // Verify: Discovery should handle parsing errors gracefully and still find valid UTXO + assertEquals(usedAddressIndex + 1, discoveredAddressCount, + "Discovery should continue despite UTXO parsing errors"); + } + + /** + * Test 9: Address discovery with concurrent access + * Tests that discovery service handles concurrent access safely. + */ + @Test + void testAddressDiscovery_ConcurrentAccess() throws InterruptedException { + // Setup: Generate addresses + for (int i = 0; i < 100; i++) { + coinInstance.generateAddress(false); + } + + // Mock HTTP client + when(mockHttpClient.getUtxosUncached(any(), any(String[].class))) + .thenReturn(new JsonArray()); + + // Execute: Run multiple discovery operations concurrently + int numThreads = 5; + ExecutorService executor = Executors.newFixedThreadPool(numThreads); + CountDownLatch latch = new CountDownLatch(numThreads); + List results = new ArrayList<>(); + + for (int i = 0; i < numThreads; i++) { + executor.submit(() -> { + try { + int result = discoveryService.discoverAddressCount(); + synchronized (results) { + results.add(result); + } + } finally { + latch.countDown(); + } + }); + } + + // Wait for all threads to complete + latch.await(10, TimeUnit.SECONDS); + executor.shutdown(); + + // Verify: All concurrent operations should return the same result + assertEquals(numThreads, results.size(), "All threads should complete"); + int expectedResult = getAddressCountInitial(); + for (Integer result : results) { + assertEquals(expectedResult, result, "All concurrent operations should return the same result"); + } + } + + /** + * Test 10: Address discovery with maximum depth limit + * Tests that discovery stops when reaching the maximum discovery depth. + */ + @Test + void testAddressDiscovery_MaxDepthLimit() { + // Setup: Mock HTTP client to always return empty (no used addresses) + when(mockHttpClient.getUtxosUncached(any(), any(String[].class))) + .thenReturn(new JsonArray()); + + // Execute: Run discovery (should hit max depth limit) + int discoveredAddressCount = discoveryService.discoverAddressCount(); + + // Verify: Discovery should return original address count when hitting max depth + assertEquals(getAddressCountInitial(), discoveredAddressCount, + "Discovery should return original address count when hitting max depth"); + + // Verify HTTP client was called multiple times (indicating depth traversal) + verify(mockHttpClient, atLeastOnce()).getUtxosUncached(any(), any(String[].class)); + } +} \ No newline at end of file diff --git a/src/test/java/TestWallet.java b/src/test/java/TestCoinInstance.java similarity index 61% rename from src/test/java/TestWallet.java rename to src/test/java/TestCoinInstance.java index 70a22b2..37e6cde 100644 --- a/src/test/java/TestWallet.java +++ b/src/test/java/TestCoinInstance.java @@ -1,76 +1,21 @@ -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonObject; import io.cloudchains.app.crypto.LoginUtils; import io.cloudchains.app.net.CoinInstance; import io.cloudchains.app.net.CoinTicker; import io.cloudchains.app.util.AddressBalance; -import io.cloudchains.app.util.ConfigHelper; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Objects; import static org.junit.jupiter.api.Assertions.*; -class TestWallet { - - private static final Gson GSON = new GsonBuilder().create(); - private static JsonObject testConfig; - - static { - try { - loadTestConfig(); - } catch (IOException e) { - throw new RuntimeException("Failed to load test configuration", e); - } - } - - private static void loadTestConfig() throws IOException { - try (InputStream is = TestWallet.class.getClassLoader().getResourceAsStream("test_config.json")) { - if (is == null) { - throw new IOException("Could not find test_config.json in classpath"); - } - InputStreamReader reader = new InputStreamReader(is); - testConfig = GSON.fromJson(reader, JsonObject.class); - } - } - - // Test parameters - same interface as TestWalletConfig - private static final int ADDRESS_COUNT_INITIAL = getAddressCountInitial(); - private static final int ADDRESS_COUNT = getAddressCount(); - private static final String MNEMONIC = getMnemonic(); - private static final String PASSWORD = getPassword(); - private static final String COIN_TICKER = "LITECOIN"; - - private static int getAddressCount() { - return testConfig.getAsJsonObject("test_parameters").get("address_count").getAsInt(); - } - - private static int getAddressCountInitial() { - return testConfig.getAsJsonObject("test_parameters").get("address_count_initial").getAsInt(); - } - - private static String getPassword() { - return testConfig.getAsJsonObject("test_parameters").get("password").getAsString(); - } - - private static String getMnemonic() { - return testConfig.getAsJsonObject("test_parameters").get("mnemonic").getAsString(); - } - - private static ArrayList getExpectedAddresses() { - List list = GSON.fromJson(testConfig.get("expected_addresses"), List.class); - return new ArrayList<>(list); - } +/** + * Test class for CoinInstance functionality. + * Tests address generation, wallet initialization, and deterministic address creation. + */ +class TestCoinInstance extends TestHelper { @Test void deterministicAddresses_fromMnemonic() { @@ -84,7 +29,6 @@ void deterministicAddresses_fromMnemonic() { ArrayList actual = new ArrayList<>(); for (AddressBalance address : addresses) { actual.add(address.getAddress().toBase58()); -// System.out.println("\""+actual.get(actual.size()-1)+"\","); } assertEquals(getAddressCount(), actual.size()); assertTrue(noDups(actual)); @@ -186,52 +130,17 @@ void deterministicAddresses_generateForwardAddressesReloadConfig() { assertEquals(expected.get(i), actual.get(i)); } - coin.deinit(); clean(); } @BeforeEach - void beforeEach() { - ConfigHelper.CONFIG_DIR = "."; - clean(); - // Disable address discovery during tests to prevent interference with deterministic address generation - CoinInstance.setAddressDiscoveryEnabled(false); + void setup() { + commonSetup(); } @AfterAll - static void afterAll() { - clean(); - } - - - static void clean() { - CoinInstance.getCoinInstances().clear(); - assertTrue(deleteDir(new File(ConfigHelper.getLocalDataDirectory()))); - } - - static boolean deleteDir(File path) { - if (!path.exists()) - return true; - for (File subFile : Objects.requireNonNull(path.listFiles())) { - if (subFile.isDirectory()) { - deleteDir(subFile); - } else { - if (!subFile.delete()) - return false; - } - } - return path.delete(); - } - - /** - * Returns true if there's no duplicates in the list. - * @param list - * @return True if no duplicates - */ - static boolean noDups(ArrayList list) { - HashSet set = new HashSet<>(list); - return list.size() == set.size(); + static void cleanup() { + commonCleanup(); } - -} +} \ No newline at end of file diff --git a/src/test/java/TestConfigHelper.java b/src/test/java/TestConfigHelper.java new file mode 100644 index 0000000..d4cfda0 --- /dev/null +++ b/src/test/java/TestConfigHelper.java @@ -0,0 +1,209 @@ +import io.cloudchains.app.util.ConfigHelper; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test class for ConfigHelper functionality. + * Tests configuration file operations, address count management, and directory handling. + */ +class TestConfigHelper extends TestHelper { + + private ConfigHelper configHelper; + + @BeforeEach + void setup() { + commonSetup(); + configHelper = new ConfigHelper("test"); + } + + @AfterAll + static void cleanup() { + commonCleanup(); + } + + @Test + void testConfigFileCreation() { + // The config file should be created when ConfigHelper is instantiated + // We can verify this by checking if the config directory exists and has files + String localDataDir = ConfigHelper.getLocalDataDirectory(); + File settingsDir = new File(localDataDir, "settings"); + assertTrue(settingsDir.exists()); + + // Check if config file exists by trying to read it + File configFile = new File(settingsDir, "config-test.json"); + assertTrue(configFile.exists()); + } + + @Test + void testDefaultConfigurationValues() { + assertEquals(0.0001, configHelper.getFee()); + assertTrue(configHelper.isFlatFee()); + assertFalse(configHelper.isRpcEnabled()); + assertEquals("", configHelper.getRpcUsername()); + assertEquals("", configHelper.getRpcPassword()); + assertEquals(-1000, configHelper.getRpcPort()); + assertEquals(0, configHelper.getAddressCount()); + } + + @Test + void testSetAndGetFee() { + double newFee = 0.001; + configHelper.setFee(newFee); + configHelper.writeConfig(); + + ConfigHelper reloadedConfig = new ConfigHelper("test"); + assertEquals(newFee, reloadedConfig.getFee()); + } + + @Test + void testSetAndGetFlatFee() { + configHelper.setFlatFee(false); + configHelper.writeConfig(); + + ConfigHelper reloadedConfig = new ConfigHelper("test"); + assertFalse(reloadedConfig.isFlatFee()); + } + + @Test + void testSetAndGetRpcEnabled() { + configHelper.setRpcEnabled(true); + configHelper.writeConfig(); + + ConfigHelper reloadedConfig = new ConfigHelper("test"); + assertTrue(reloadedConfig.isRpcEnabled()); + } + + @Test + void testSetAndGetRpcCredentials() { + String username = "testuser"; + String password = "testpass"; + + configHelper.setRpcUsername(username); + configHelper.setRpcPassword(password); + configHelper.writeConfig(); + + ConfigHelper reloadedConfig = new ConfigHelper("test"); + assertEquals(username, reloadedConfig.getRpcUsername()); + assertEquals(password, reloadedConfig.getRpcPassword()); + } + + @Test + void testSetAndGetRpcPort() { + int port = 8080; + configHelper.setRpcPort(port); + configHelper.writeConfig(); + + ConfigHelper reloadedConfig = new ConfigHelper("test"); + assertEquals(port, reloadedConfig.getRpcPort()); + } + + @Test + void testSetAndGetAddressCount() { + int addressCount = 50; + configHelper.setAddressCount(addressCount); + configHelper.writeConfig(); + + ConfigHelper reloadedConfig = new ConfigHelper("test"); + assertEquals(addressCount, reloadedConfig.getAddressCount()); + } + + @Test + void testValidAuth() { + assertFalse(configHelper.validAuth()); + + configHelper.setRpcUsername("user"); + configHelper.setRpcPassword("pass"); + configHelper.writeConfig(); + + ConfigHelper reloadedConfig = new ConfigHelper("test"); + assertTrue(reloadedConfig.validAuth()); + } + + @Test + void testInvalidAuthWithEmptyUsername() { + configHelper.setRpcUsername(""); + configHelper.setRpcPassword("pass"); + configHelper.writeConfig(); + + ConfigHelper reloadedConfig = new ConfigHelper("test"); + assertFalse(reloadedConfig.validAuth()); + } + + @Test + void testInvalidAuthWithEmptyPassword() { + configHelper.setRpcUsername("user"); + configHelper.setRpcPassword(""); + configHelper.writeConfig(); + + ConfigHelper reloadedConfig = new ConfigHelper("test"); + assertFalse(reloadedConfig.validAuth()); + } + + @Test + void testConfigDirectoryCreation() { + String localDataDir = ConfigHelper.getLocalDataDirectory(); + assertNotNull(localDataDir); + + File settingsDir = new File(localDataDir, "settings"); + assertTrue(settingsDir.exists() || settingsDir.mkdirs()); + } + + @Test + void testConfigFilePersistence() { + // Set some values + configHelper.setFee(0.005); + configHelper.setFlatFee(false); + configHelper.setRpcEnabled(true); + configHelper.setAddressCount(100); + configHelper.writeConfig(); + + // Create new instance and verify values persist + ConfigHelper newConfig = new ConfigHelper("test"); + assertEquals(0.005, newConfig.getFee()); + assertFalse(newConfig.isFlatFee()); + assertTrue(newConfig.isRpcEnabled()); + assertEquals(100, newConfig.getAddressCount()); + } + + @Test + void testLoadConfigFromFile() { + // Write a config file manually + String configContent = "{\n" + + " \"fee\": 0.002,\n" + + " \"feeFlat\": false,\n" + + " \"rpcEnabled\": true,\n" + + " \"rpcUsername\": \"testuser\",\n" + + " \"rpcPassword\": \"testpass\",\n" + + " \"rpcPort\": 9000,\n" + + " \"addressCount\": 25\n" + + "}"; + + String localDataDir = ConfigHelper.getLocalDataDirectory(); + File settingsDir = new File(localDataDir, "settings"); + File configFile = new File(settingsDir, "config-test.json"); + + try { + Files.write(configFile.toPath(), configContent.getBytes()); + } catch (IOException e) { + fail("Failed to write config file", e); + } + + // Create new ConfigHelper instance to load from file + ConfigHelper loadedConfig = new ConfigHelper("test"); + + assertEquals(0.002, loadedConfig.getFee()); + assertFalse(loadedConfig.isFlatFee()); + assertTrue(loadedConfig.isRpcEnabled()); + assertEquals("testuser", loadedConfig.getRpcUsername()); + assertEquals("testpass", loadedConfig.getRpcPassword()); + assertEquals(9000, loadedConfig.getRpcPort()); + assertEquals(25, loadedConfig.getAddressCount()); + } +} \ No newline at end of file diff --git a/src/test/java/TestHelper.java b/src/test/java/TestHelper.java new file mode 100644 index 0000000..3f27c88 --- /dev/null +++ b/src/test/java/TestHelper.java @@ -0,0 +1,143 @@ +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import com.google.gson.reflect.TypeToken; +import io.cloudchains.app.net.CoinInstance; +import io.cloudchains.app.util.ConfigHelper; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; + +/** + * Test helper class providing shared utilities for all test files. + * This avoids code duplication across TestCoinInstance, TestConfigHelper, and TestLoginUtils. + */ +public class TestHelper { + + private static final Gson GSON = new GsonBuilder().create(); + protected static JsonObject testConfig; + + static { + try { + loadTestConfig(); + } catch (IOException e) { + throw new RuntimeException("Failed to load test configuration", e); + } + } + + private static void loadTestConfig() throws IOException { + try (InputStream is = TestHelper.class.getClassLoader().getResourceAsStream("test_config.json")) { + if (is == null) { + throw new IOException("Could not find test_config.json in classpath"); + } + InputStreamReader reader = new InputStreamReader(is); + testConfig = GSON.fromJson(reader, JsonObject.class); + } + } + + // Test parameters - shared across all test files + protected static final int ADDRESS_COUNT_INITIAL = getAddressCountInitial(); + protected static final int ADDRESS_COUNT = getAddressCount(); + protected static final String MNEMONIC = getMnemonic(); + protected static final String PASSWORD = getPassword(); + protected static final String COIN_TICKER = "LITECOIN"; + + protected static int getAddressCount() { + return testConfig.getAsJsonObject("test_parameters").get("address_count").getAsInt(); + } + + protected static int getAddressCountInitial() { + return testConfig.getAsJsonObject("test_parameters").get("address_count_initial").getAsInt(); + } + + protected static String getPassword() { + return testConfig.getAsJsonObject("test_parameters").get("password").getAsString(); + } + + protected static String getMnemonic() { + return testConfig.getAsJsonObject("test_parameters").get("mnemonic").getAsString(); + } + + protected static ArrayList getExpectedAddresses() { + List list = GSON.fromJson(testConfig.get("expected_addresses"), new TypeToken>(){ + }.getType()); + return new ArrayList<>(list); + } + + /** + * Common setup method for all test files. + * Disables address discovery during tests to prevent interference. + */ + @BeforeEach + public void commonSetup() { + ConfigHelper.CONFIG_DIR = "."; + clean(); + // Disable address discovery during tests to prevent interference with deterministic address generation + CoinInstance.setAddressDiscoveryEnabled(false); + } + + /** + * Common cleanup method for all test files. + */ + @AfterAll + public static void commonCleanup() { + clean(); + } + + /** + * Cleans up test data and resets CoinInstance state. + */ + protected static void clean() { + CoinInstance.getCoinInstances().clear(); + assertTrue(deleteDir(new File(ConfigHelper.getLocalDataDirectory()))); + } + + /** + * Deletes a directory and all its contents recursively. + * @param path The directory to delete + * @return true if deletion was successful + */ + protected static boolean deleteDir(File path) { + if (!path.exists()) + return true; + for (File subFile : Objects.requireNonNull(path.listFiles())) { + if (subFile.isDirectory()) { + deleteDir(subFile); + } else { + if (!subFile.delete()) + return false; + } + } + return path.delete(); + } + + /** + * Returns true if there's no duplicates in the list. + * @param list The list to check for duplicates + * @return True if no duplicates + */ + protected static boolean noDups(ArrayList list) { + HashSet set = new HashSet<>(list); + return list.size() == set.size(); + } + + /** + * Asserts that a condition is true. + * This is a simple assertion method to avoid importing JUnit in the helper. + * @param condition The condition to assert + * @throws AssertionError if the condition is false + */ + protected static void assertTrue(boolean condition) { + if (!condition) { + throw new AssertionError("Expected true but was false"); + } + } +} \ No newline at end of file diff --git a/src/test/java/TestLoginUtils.java b/src/test/java/TestLoginUtils.java new file mode 100644 index 0000000..87b2453 --- /dev/null +++ b/src/test/java/TestLoginUtils.java @@ -0,0 +1,122 @@ +import io.cloudchains.app.crypto.LoginUtils; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test class for LoginUtils functionality. + * Tests password hashing and entropy generation. + */ +class TestLoginUtils { + + @Test + void testLoginToEntropy_ValidPassword() { + String password = "Test^1234"; + String result = LoginUtils.loginToEntropy(password); + + assertNotNull(result); + assertFalse(result.isEmpty()); + assertEquals(64, result.length()); // SHA-256 produces 64 hex characters + } + + @Test + void testLoginToEntropy_DifferentPasswords() { + String password1 = "password123"; + String password2 = "password456"; + + String result1 = LoginUtils.loginToEntropy(password1); + String result2 = LoginUtils.loginToEntropy(password2); + + assertNotNull(result1); + assertNotNull(result2); + assertNotEquals(result1, result2); + } + + @Test + void testLoginToEntropy_SamePasswordConsistency() { + String password = "consistentPassword"; + + String result1 = LoginUtils.loginToEntropy(password); + String result2 = LoginUtils.loginToEntropy(password); + + assertNotNull(result1); + assertNotNull(result2); + assertEquals(result1, result2); + } + + @Test + void testLoginToEntropy_EmptyPassword() { + String password = ""; + String result = LoginUtils.loginToEntropy(password); + + assertNotNull(result); + assertFalse(result.isEmpty()); + assertEquals(64, result.length()); + } + + @Test + void testLoginToEntropy_SpecialCharacters() { + String password = "!@#$%^&*()_+-=[]{}|;:,.<>?"; + String result = LoginUtils.loginToEntropy(password); + + assertNotNull(result); + assertFalse(result.isEmpty()); + assertEquals(64, result.length()); + } + + @Test + void testLoginToEntropy_Whitespace() { + String password1 = "password"; + String password2 = " password "; + String password3 = "password "; + + String result1 = LoginUtils.loginToEntropy(password1); + String result2 = LoginUtils.loginToEntropy(password2); + String result3 = LoginUtils.loginToEntropy(password3); + + assertNotNull(result1); + assertNotNull(result2); + assertNotNull(result3); + + assertNotEquals(result1, result2); + assertNotEquals(result1, result3); + assertNotEquals(result2, result3); + } + + @Test + void testLoginToEntropy_LongPassword() { + StringBuilder longPassword = new StringBuilder(); + for (int i = 0; i < 1000; i++) { + longPassword.append("a"); + } + + String result = LoginUtils.loginToEntropy(longPassword.toString()); + + assertNotNull(result); + assertFalse(result.isEmpty()); + assertEquals(64, result.length()); + } + + @Test + void testLoginToEntropy_NullSafety() { + // Test that the method handles edge cases gracefully + String result = LoginUtils.loginToEntropy("Test^1234"); + assertNotNull(result); + + // Verify it's a valid SHA-256 hash (64 hex characters) + assertTrue(result.matches("[a-f0-9]{64}")); + } + + @Test + void testLoginToEntropy_CaseSensitivity() { + String password1 = "Password"; + String password2 = "password"; + + String result1 = LoginUtils.loginToEntropy(password1); + String result2 = LoginUtils.loginToEntropy(password2); + + assertNotNull(result1); + assertNotNull(result2); + assertNotEquals(result1, result2); + } +} \ No newline at end of file From ba44bbdfeed8811dcbe74485ced6513540906a7e Mon Sep 17 00:00:00 2001 From: tryiou Date: Sat, 13 Dec 2025 17:10:04 +0100 Subject: [PATCH 08/73] [refactor] Centralize HTTP configuration and extract server selection interface --- .../app/net/api/http/client/EXRServer.java | 6 +- .../net/api/http/client/EXRServerPool.java | 97 +++------- .../api/http/client/EXRServerSelector.java | 174 ++++++++++++++++++ .../app/net/api/http/client/EXRWrapper.java | 44 +---- .../app/net/api/http/client/HTTPClient.java | 100 ++++------ .../net/api/http/client/HttpClientConfig.java | 60 ++++++ .../app/net/api/http/client/HttpUtils.java | 86 +++++++++ 7 files changed, 388 insertions(+), 179 deletions(-) create mode 100644 src/main/java/io/cloudchains/app/net/api/http/client/EXRServerSelector.java create mode 100644 src/main/java/io/cloudchains/app/net/api/http/client/HttpClientConfig.java create mode 100644 src/main/java/io/cloudchains/app/net/api/http/client/HttpUtils.java diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/EXRServer.java b/src/main/java/io/cloudchains/app/net/api/http/client/EXRServer.java index d8680d6..18aac12 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/client/EXRServer.java +++ b/src/main/java/io/cloudchains/app/net/api/http/client/EXRServer.java @@ -21,9 +21,9 @@ public class EXRServer { private long lastHealthCheck; private final Set supportedCoins; private volatile boolean capabilitiesProbed; - // Add constants for configuration - private static final int HEALTH_CHECK_INTERVAL_MS = 5000; - private static final int CAPABILITY_PROBE_TIMEOUT_MS = 30000; + // Use centralized configuration constants + private static final int HEALTH_CHECK_INTERVAL_MS = HttpClientConfig.HEALTH_CHECK_INTERVAL_MS; + private static final int CAPABILITY_PROBE_TIMEOUT_MS = HttpClientConfig.CAPABILITY_PROBE_TIMEOUT_MS; public EXRServer(String endpoint) { // Store endpoint with trailing slash for consistency diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/EXRServerPool.java b/src/main/java/io/cloudchains/app/net/api/http/client/EXRServerPool.java index af099f6..5e2315c 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/client/EXRServerPool.java +++ b/src/main/java/io/cloudchains/app/net/api/http/client/EXRServerPool.java @@ -21,17 +21,34 @@ public class EXRServerPool { private volatile boolean capabilitiesProbed; // Add synchronization lock for thread-safe map updates private final Object mapUpdateLock = new Object(); + // Server selector interface for dependency injection + private final ServerSelector serverSelector; - public EXRServerPool(String endpoints) { + /** + * Constructor with dependency injection for flexible server selection. + * @param endpoints Comma-separated list of server endpoints + * @param serverSelector Server selection strategy implementation + */ + public EXRServerPool(String endpoints, ServerSelector serverSelector) { this.servers = new CopyOnWriteArrayList<>(); this.currentIndex = new AtomicInteger(0); this.coinToServersMap = new ConcurrentHashMap<>(); this.endpointToServerMap = new ConcurrentHashMap<>(); this.capabilitiesProbed = false; + // Initialize server selector with dependency injection + this.serverSelector = serverSelector; initializeServers(endpoints); } - // Add constants for configuration - private static final int CAPABILITY_PROBE_TIMEOUT_MS = 30000; + + /** + * Backward compatibility constructor using default round-robin selection. + * @param endpoints Comma-separated list of server endpoints + */ + public EXRServerPool(String endpoints) { + this(endpoints, new EXRServerSelectorImpl(new AtomicInteger(0))); + } + // Use centralized configuration constants + private static final int CAPABILITY_PROBE_TIMEOUT_MS = HttpClientConfig.CAPABILITY_PROBE_TIMEOUT_MS; public void startCapabilityProbing() { if (capabilitiesProbed || servers.isEmpty()) { @@ -124,80 +141,24 @@ private void initializeServers(String endpoints) { } public EXRServer selectServer() { - if (servers.isEmpty()) { - LOGGER.log(Level.WARNING, "[exr-pool] No servers available for selection"); - return null; - } - // Try round-robin through healthy servers - int start = currentIndex.getAndIncrement() % servers.size(); - for (int i = 0; i < servers.size(); i++) { - int index = (start + i) % servers.size(); - EXRServer server = servers.get(index); - if (server.isHealthy()) { - LOGGER.log(Level.FINE, "[exr-pool] Selected server: " + server.getEndpoint()); - return server; - } - } - LOGGER.log(Level.WARNING, "[exr-pool] No healthy servers available"); - return null; // All servers unhealthy + return serverSelector.selectHealthyServer(servers); } /** - * Extract health filtering logic - * @param supportingServers List of servers that support the coin - * @return List of healthy servers that support the coin + * Select a server for a specific coin with proper error handling + * @param coin The coin to select a server for + * @return Selected server or null if none available + * @throws IllegalArgumentException if coin is null */ - private List getHealthySupportingServers(List supportingServers) { - List healthyServers = new ArrayList<>(); - for (EXRServer server : supportingServers) { - if (server.isHealthy()) { - healthyServers.add(server); - } - } - return healthyServers; - } - - /** - * Extract server selection logic - * @param healthyServers List of healthy servers - * @param coin The coin to select a server for - * @return Selected server or null if none available - */ - private EXRServer selectFromHealthyServers(List healthyServers, CoinTicker coin) { - if (healthyServers.isEmpty()) { - LOGGER.log(Level.SEVERE, "[exr-pool] NO HEALTHY EXR SERVERS FOR COIN: " + - CoinTickerUtils.tickerToString(coin)); - return null; - } - int index = currentIndex.getAndIncrement() % healthyServers.size(); - EXRServer selectedServer = healthyServers.get(index); - // Double-check that the selected server actually supports the coin - if (!selectedServer.hasCapability(coin)) { - LOGGER.log(Level.SEVERE, "[exr-pool] CRITICAL ERROR: Selected server " + - selectedServer.getEndpoint() + " does NOT support coin " + - CoinTickerUtils.tickerToString(coin)); - return null; - } - return selectedServer; - } - - /** - * Select a server for a specific coin with proper error handling - * @param coin The coin to select a server for - * @return Selected server or null if none available - */ public EXRServer selectServerForCoin(CoinTicker coin) { + if (coin == null) { + throw new IllegalArgumentException("Coin cannot be null"); + } if (!capabilitiesProbed) { return null; // Wait for probing to complete } List supportingServers = coinToServersMap.get(coin); - if (supportingServers == null || supportingServers.isEmpty()) { - LOGGER.log(Level.SEVERE, "[exr-pool] NO EXR SERVERS SUPPORT COIN: " + - CoinTickerUtils.tickerToString(coin)); - return null; // FAIL - NO FALLBACK TO BASE_URL - } - List healthyServers = getHealthySupportingServers(supportingServers); - return selectFromHealthyServers(healthyServers, coin); + return serverSelector.selectServerForCoin(supportingServers, coin); } public Set getSupportedCoins() { diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/EXRServerSelector.java b/src/main/java/io/cloudchains/app/net/api/http/client/EXRServerSelector.java new file mode 100644 index 0000000..be050c3 --- /dev/null +++ b/src/main/java/io/cloudchains/app/net/api/http/client/EXRServerSelector.java @@ -0,0 +1,174 @@ +package io.cloudchains.app.net.api.http.client; + +import io.cloudchains.app.net.CoinTicker; +import io.cloudchains.app.net.CoinTickerUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; + +/** + * Server selection interface defining the contract for server selection strategies. + * This interface enables dependency injection and loose coupling between + * server pool management and server selection logic. + * + * Implementations can provide different selection algorithms such as: + * - Round-robin selection (default implementation) + * - Weighted selection based on server performance + * - Random selection + * - Health-based selection + * - Load-aware selection + */ +interface ServerSelector { + + /** + * Select a healthy server from the provided list using the selection strategy. + * The selection algorithm is implementation-specific and may use various criteria + * such as round-robin, random selection, or performance-based selection. + * + * @param servers List of servers to select from (must not be null) + * @return Selected healthy server or null if none available + * @throws IllegalArgumentException if servers list is null + */ + EXRServer selectHealthyServer(List servers); + + /** + * Select a server for a specific coin with proper error handling. + * This method ensures that the selected server supports the requested coin + * and is in a healthy state. + * + * @param supportingServers List of servers that support the coin (can be null) + * @param coin The coin to select a server for (must not be null) + * @return Selected server or null if none available + * @throws IllegalArgumentException if coin is null + */ + EXRServer selectServerForCoin(List supportingServers, CoinTicker coin); +} + +/** + * Default implementation of ServerSelector using round-robin selection strategy. + * This class provides the original server selection logic extracted from + * EXRServerPool to separate server selection concerns from pool management. + */ +class EXRServerSelectorImpl implements ServerSelector { + + private final static LogManager LOGMANAGER = LogManager.getLogManager(); + private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); + + private final AtomicInteger currentIndex; + + /** + * Constructor for EXRServerSelectorImpl. + * @param currentIndex Shared atomic integer for round-robin selection + */ + public EXRServerSelectorImpl(AtomicInteger currentIndex) { + this.currentIndex = currentIndex; + } + + /** + * Select a healthy server from the provided list using round-robin. + * This method implements the same logic as EXRServerPool.selectServer() + * but extracts it into a separate utility class. + * + * @param servers List of servers to select from (must not be null) + * @return Selected healthy server or null if none available + * @throws IllegalArgumentException if servers list is null + */ + @Override + public EXRServer selectHealthyServer(List servers) { + if (servers.isEmpty()) { + LOGGER.log(Level.WARNING, "[server-selector] No servers available for selection"); + return null; + } + + // Try round-robin through healthy servers + int start = currentIndex.getAndIncrement() % servers.size(); + for (int i = 0; i < servers.size(); i++) { + int index = (start + i) % servers.size(); + EXRServer server = servers.get(index); + if (server.isHealthy()) { + LOGGER.log(Level.FINE, "[server-selector] Selected server: " + server.getEndpoint()); + return server; + } + } + + LOGGER.log(Level.WARNING, "[server-selector] No healthy servers available"); + return null; // All servers unhealthy + } + + /** + * Select a server for a specific coin with proper error handling. + * This method implements coin-aware server selection logic, + * extracting it from EXRServerPool.selectServerForCoin(). + * + * @param supportingServers List of servers that support the coin (can be null) + * @param coin The coin to select a server for (must not be null) + * @return Selected server or null if none available + * @throws IllegalArgumentException if coin is null + */ + @Override + public EXRServer selectServerForCoin(List supportingServers, CoinTicker coin) { + if (supportingServers == null || supportingServers.isEmpty()) { + LOGGER.log(Level.SEVERE, "[server-selector] NO EXR SERVERS SUPPORT COIN: " + + CoinTickerUtils.tickerToString(coin)); + return null; // FAIL - NO FALLBACK TO BASE_URL + } + + // Filter healthy servers + List healthyServers = getHealthySupportingServers(supportingServers); + + // Select from healthy servers + return selectFromHealthyServers(healthyServers, coin); + } + + /** + * Extract health filtering logic. + * This method filters the list to only include healthy servers. + * + * @param supportingServers List of servers that support the coin + * @return List of healthy servers that support the coin + */ + private List getHealthySupportingServers(List supportingServers) { + List healthyServers = new ArrayList<>(); + for (EXRServer server : supportingServers) { + if (server.isHealthy()) { + healthyServers.add(server); + } + } + return healthyServers; + } + + /** + * Extract server selection logic. + * This method selects a server from the healthy servers list + * using round-robin selection. + * + * @param healthyServers List of healthy servers + * @param coin The coin to select a server for + * @return Selected server or null if none available + */ + private EXRServer selectFromHealthyServers(List healthyServers, CoinTicker coin) { + if (healthyServers.isEmpty()) { + LOGGER.log(Level.SEVERE, "[server-selector] NO HEALTHY EXR SERVERS FOR COIN: " + + CoinTickerUtils.tickerToString(coin)); + return null; + } + + int index = currentIndex.getAndIncrement() % healthyServers.size(); + EXRServer selectedServer = healthyServers.get(index); + + // Double-check that the selected server actually supports the coin + if (!selectedServer.hasCapability(coin)) { + LOGGER.log(Level.SEVERE, "[server-selector] CRITICAL ERROR: Selected server " + + selectedServer.getEndpoint() + " does NOT support coin " + + CoinTickerUtils.tickerToString(coin)); + return null; + } + + return selectedServer; + } + +} \ No newline at end of file diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/EXRWrapper.java b/src/main/java/io/cloudchains/app/net/api/http/client/EXRWrapper.java index 543c344..f772ec0 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/client/EXRWrapper.java +++ b/src/main/java/io/cloudchains/app/net/api/http/client/EXRWrapper.java @@ -3,17 +3,13 @@ import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; import org.apache.http.client.config.RequestConfig; -import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; -import org.apache.http.util.EntityUtils; import java.io.IOException; import java.net.URI; @@ -29,17 +25,16 @@ public class EXRWrapper { private final CloseableHttpClient client; private final Gson gson; // Constants for configuration - private static final int HTTP_TIMEOUT_MS = 30000; - private static final String LOG_TAG = "[exr]"; + private static final String LOG_TAG = HttpClientConfig.LOG_TAG; public EXRWrapper(String exrEndpoint) { this.exrEndpoint = exrEndpoint; this.gson = new Gson(); // Configure HTTP client with timeouts RequestConfig config = RequestConfig.custom() - .setConnectTimeout(30000) - .setConnectionRequestTimeout(30000) - .setSocketTimeout(30000) + .setConnectTimeout(HttpClientConfig.HTTP_TIMEOUT_MS) + .setConnectionRequestTimeout(HttpClientConfig.HTTP_TIMEOUT_MS) + .setSocketTimeout(HttpClientConfig.HTTP_TIMEOUT_MS) .build(); this.client = HttpClients.custom() .setDefaultRequestConfig(config) @@ -54,31 +49,7 @@ public EXRWrapper(String exrEndpoint) { * @return Response body string or null on error */ private String executeHttpRequest(HttpRequestBase request, String operation) { - CloseableHttpResponse response = null; - try { - response = client.execute(request); - if (validateResponse(response)) { - HttpEntity entity = response.getEntity(); - String responseBody = EntityUtils.toString(entity); - EntityUtils.consume(entity); - return responseBody; - } else { - LOGGER.log(Level.WARNING, LOG_TAG + " " + operation + " failed for endpoint: " + exrEndpoint); - return null; - } - } catch (IOException e) { - LOGGER.log(Level.WARNING, LOG_TAG + " " + operation + " failed for endpoint: " + exrEndpoint, e); - return null; - } finally { - request.reset(); - if (response != null) { - try { - response.close(); - } catch (IOException e) { - LOGGER.log(Level.WARNING, LOG_TAG + " Failed to close HTTP response", e); - } - } - } + return HttpUtils.executeHttpRequest(client, request, operation); } /** @@ -151,9 +122,4 @@ public void close() { } } - private boolean validateResponse(HttpResponse response) { - return response.getStatusLine().getStatusCode() == 200 && - response.getEntity() != null && - response.getEntity().getContentLength() != 0; - } } \ No newline at end of file diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java b/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java index 3f76210..6aa3c71 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java +++ b/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java @@ -16,11 +16,8 @@ import io.cloudchains.app.util.UTXO; import io.cloudchains.app.util.history.Transaction; import org.apache.http.Header; -import org.apache.http.HttpEntity; import org.apache.http.HttpHeaders; -import org.apache.http.HttpResponse; import org.apache.http.client.config.RequestConfig; -import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpRequestBase; @@ -35,7 +32,6 @@ import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.message.BasicHeader; import org.apache.http.ssl.SSLContextBuilder; -import org.apache.http.util.EntityUtils; import org.bitcoinj.core.Address; import org.bitcoinj.script.Script; import org.bitcoinj.script.ScriptBuilder; @@ -84,8 +80,8 @@ private boolean waitForCapabilities(int timeoutMs) { int waitTime = 0; while (!App.exrServerPool.isCapabilitiesProbed() && waitTime < timeoutMs) { try { - Thread.sleep(100); - waitTime += 100; + Thread.sleep(HttpClientConfig.CAPABILITY_PROBE_WAIT_INTERVAL_MS); + waitTime += HttpClientConfig.CAPABILITY_PROBE_WAIT_INTERVAL_MS; } catch (InterruptedException e) { Thread.currentThread().interrupt(); LOGGER.log(Level.WARNING, "[httpclient] Waiting for capabilities was interrupted"); @@ -159,29 +155,7 @@ private List convertParams(JsonArray exrParams) { * @return Response string or null on error */ private String executeHttpRequest(T request) { - CloseableHttpResponse response = null; - try { - response = client.execute(request); - if (validateResponse(response)) { - HttpEntity entity = response.getEntity(); - String result = EntityUtils.toString(entity); - EntityUtils.consume(entity); - return result; - } - return null; - } catch (IOException e) { - LOGGER.log(Level.WARNING, "HTTP request failed: " + e.toString()); - return null; - } finally { - request.reset(); - if (response != null) { - try { - response.close(); - } catch (IOException e) { - LOGGER.log(Level.WARNING, "Failed to close response: " + e.toString()); - } - } - } + return HttpUtils.executeHttpRequest(client, request, "HTTP request"); } /** @@ -191,7 +165,7 @@ private String executeHttpRequest(T request) { */ private String executeGetRequest(String endpoint) { HttpGet httpGet = new HttpGet(App.BASE_URL + endpoint); - return executeHttpRequest(httpGet); + return HttpUtils.executeHttpRequest(client, httpGet, "GET " + endpoint); } /** @@ -210,7 +184,7 @@ private String executePostRequest(String endpoint, JsonObject params) { httpPost.reset(); return null; } - return executeHttpRequest(httpPost); + return HttpUtils.executeHttpRequest(client, httpPost, "POST " + endpoint); } public HTTPClient(int maximumSockets) { @@ -229,9 +203,9 @@ public HTTPClient(int maximumSockets) { List
headers = Lists.newArrayList(header); RequestConfig.Builder requestBuilder = RequestConfig.custom(); - requestBuilder.setConnectTimeout(30000); - requestBuilder.setConnectionRequestTimeout(30000); - requestBuilder.setSocketTimeout(30000); + requestBuilder.setConnectTimeout(HttpClientConfig.HTTP_TIMEOUT_MS); + requestBuilder.setConnectionRequestTimeout(HttpClientConfig.HTTP_TIMEOUT_MS); + requestBuilder.setSocketTimeout(HttpClientConfig.HTTP_TIMEOUT_MS); assert sslContext != null; PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager( @@ -344,7 +318,7 @@ private String executeEXRPost(String endpoint, JsonObject params) { if (!App.exrServerPool.isCapabilitiesProbed()) { LOGGER.log(Level.FINE, "[httpclient] Waiting for EXR capabilities to be probed for coin: " + CoinTickerUtils.tickerToString(coin)); - if (!waitForCapabilities(10000)) { // Wait up to 10 seconds + if (!waitForCapabilities(HttpClientConfig.CAPABILITY_PROBE_WAIT_TIMEOUT_MS)) { // Wait up to 10 seconds LOGGER.log(Level.WARNING, "[httpclient] EXR capabilities not probed yet for coin: " + CoinTickerUtils.tickerToString(coin)); return null; // FAIL - NO FALLBACK TO BASE_URL @@ -438,14 +412,6 @@ private String executeRequest(String endpoint, JsonObject params) { return null; // EXR configured but no valid response } - private String doGet(String endpoint) { - return executeRequest(endpoint, null); - } - - private String doPost(String endpoint, JsonObject params) { - return executeRequest(endpoint, params); - } - /** * Returns all utxos for a list of addresses. * Note: This method does neither use nor update any caches! @@ -464,10 +430,10 @@ public JsonArray getUtxosUncached(CoinTicker coinTicker, String[] addresses) { JsonObject params = new JsonObject(); params.addProperty("method", "getutxos"); params.add("params", innerParams); - - String res = doPost("/", params); + String res = executePostRequest("/", params); LOGGER.log(Level.FINER, "[httpclient] getUtxosUncached " + coinInstance.getTicker() + " " + res); + if (res == null) { LOGGER.log(Level.WARNING, "[httpclient] getUtxosUncached " + coinInstance.getTicker() + " null post result"); return null; @@ -543,10 +509,10 @@ public JsonArray getUtxos(CoinTicker coinTicker, int expiry) { JsonObject params = new JsonObject(); params.addProperty("method", "getutxos"); params.add("params", innerParams); - - String res = doPost("/", params); + String res = executePostRequest("/", params); LOGGER.log(Level.FINER, "[httpclient] getUtxos " + coinInstance.getTicker() + " " + res); + if (res == null) { LOGGER.log(Level.WARNING, "[httpclient] getUtxos " + coinInstance.getTicker() + " null post result"); return null; @@ -589,7 +555,7 @@ public JsonArray getUtxos(CoinTicker coinTicker, int expiry) { } public void getAllFees() { - String res = doGet("/fees"); + String res = executeGetRequest("/fees"); if (res == null) return; @@ -609,7 +575,7 @@ public void getAllFees() { coinInstance.addRelayFee(coinTicker, fee); - if (logCount % 30 == 0) + if (logCount % HttpClientConfig.LOG_COUNT_MODULO == 0) LOGGER.log(Level.INFO, "[httpclient] Got relayfee for currency " + ticker + " - " + fee); else LOGGER.log(Level.FINER, "[httpclient] Got relayfee for currency " + ticker + " - " + fee); @@ -628,10 +594,10 @@ public JsonObject getRawTransaction(CoinTicker coinTicker, String txid, boolean JsonObject params = new JsonObject(); params.addProperty("method", "getrawtransaction"); params.add("params", innerParams); - - String res = doPost("/", params); + String res = executePostRequest("/", params); LOGGER.log(Level.FINER, "[httpclient] getRawTransaction " + res); + if (res == null) return null; return new Gson().fromJson(res, JsonObject.class); @@ -647,10 +613,10 @@ public JsonObject getRawMempool(CoinTicker coinTicker, boolean verbose) { JsonObject params = new JsonObject(); params.addProperty("method", "getrawmempool"); params.add("params", innerParams); - - String res = doPost("/", params); + String res = executePostRequest("/", params); LOGGER.log(Level.FINER, "[httpclient] getRawMempool " + res); + if (res == null) return null; return new Gson().fromJson(res, JsonObject.class); @@ -667,7 +633,7 @@ public void getBlockCount(CoinTicker coinTicker) { params.addProperty("method", "getblockcount"); params.add("params", innerParams); - String res = doPost("/", params); + String res = executePostRequest("/", params); if (res == null) return; @@ -680,7 +646,7 @@ public void getBlockCount(CoinTicker coinTicker) { } public void getAllBlockCounts() { - String res = doGet("/height"); + String res = executeGetRequest("/height"); if (res == null) return; @@ -716,10 +682,10 @@ public JsonObject getBlock(CoinTicker coinTicker, String hash, boolean verbose) JsonObject params = new JsonObject(); params.addProperty("method", "getblock"); params.add("params", innerParams); - - String res = doPost("/", params); + String res = executePostRequest("/", params); LOGGER.log(Level.FINER, "[httpclient] getBlock " + res); + if (res == null) return null; return new Gson().fromJson(res, JsonObject.class); @@ -733,10 +699,10 @@ public JsonObject getBlockHash(CoinTicker coinTicker, int height) { JsonObject params = new JsonObject(); params.addProperty("method", "getblockhash"); params.add("params", innerParams); - - String res = doPost("/", params); + String res = executePostRequest("/", params); LOGGER.log(Level.FINER, "[httpclient] getBlockHash " + res); + if (res == null) return null; return new Gson().fromJson(res, JsonObject.class); @@ -753,10 +719,10 @@ public JsonObject getTransaction(CoinTicker coinTicker, String txid, boolean ver JsonObject params = new JsonObject(); params.addProperty("method", "gettransaction"); params.add("params", innerParams); - - String res = doPost("/", params); + String res = executePostRequest("/", params); LOGGER.log(Level.FINER, "[httpclient] getTransaction " + res); + if (res == null) return null; return new Gson().fromJson(res, JsonObject.class); @@ -772,10 +738,10 @@ public JsonObject sendRawTransaction(CoinTicker coinTicker, String rawTx) { JsonObject params = new JsonObject(); params.addProperty("method", "sendrawtransaction"); params.add("params", innerParams); - - String res = doPost("/", params); + String res = executePostRequest("/", params); LOGGER.log(Level.FINER, "[httpclient] sendRawTransaction " + res); + if (res == null) return null; return new Gson().fromJson(res, JsonObject.class); @@ -808,7 +774,7 @@ public JsonArray getHistory(CoinTicker coinTicker, int startTime, int endTime, i params.addProperty("method", "gethistory"); params.add("params", innerParams); - String res = doPost("/", params); + String res = executePostRequest("/", params); LOGGER.log(Level.FINER, "[httpclient] getHistory " + coinInstance.getTicker() + " " + res); if (res == null) { LOGGER.log(Level.WARNING, "[httpclient] getHistory " + coinInstance.getTicker() + " null post result"); @@ -888,7 +854,7 @@ public JsonArray getTransactionHistory(CoinTicker coinTicker, int startTime, int params.addProperty("method", "getaddresshistory"); params.add("params", innerParams); - String res = doPost("/", params); + String res = executePostRequest("/", params); LOGGER.log(Level.FINER, "[httpclient] getAddressHistory " + coinInstance.getTicker() + " " + res); if (res == null) { LOGGER.log(Level.WARNING, "[httpclient] getAddressHistory " + coinInstance.getTicker() + " null post result"); @@ -1052,10 +1018,6 @@ public JsonArray getTransactionHistory(CoinTicker coinTicker, int startTime, int return filterHistory(txs, startTime, endTime); } - private boolean validateResponse(HttpResponse response) { - return response.getStatusLine().getStatusCode() == 200 && response.getEntity().getContentLength() != 0; - } - /** * Filters the transaction array in place. This does not make a copy but modifies * the existing list. diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/HttpClientConfig.java b/src/main/java/io/cloudchains/app/net/api/http/client/HttpClientConfig.java new file mode 100644 index 0000000..c4454e0 --- /dev/null +++ b/src/main/java/io/cloudchains/app/net/api/http/client/HttpClientConfig.java @@ -0,0 +1,60 @@ +package io.cloudchains.app.net.api.http.client; + +/** + * Centralized configuration management for HTTP client settings. + * This class consolidates all timeout and configuration constants + * to eliminate duplication across HTTPClient, EXRWrapper, and other classes. + */ +public final class HttpClientConfig { + + // HTTP timeout configurations (in milliseconds) + public static final int HTTP_TIMEOUT_MS = 30000; + + // Server management configurations + public static final int HEALTH_CHECK_INTERVAL_MS = 5000; + public static final int CAPABILITY_PROBE_TIMEOUT_MS = 30000; + + // Retry configurations + public static final int MAX_RETRY_ATTEMPTS = 3; + + // Logging configuration + public static final String LOG_TAG = "[httpclient]"; + + // Capability probing configurations + public static final int CAPABILITY_PROBE_WAIT_TIMEOUT_MS = 10000; + public static final int CAPABILITY_PROBE_WAIT_INTERVAL_MS = 100; + + // Logging configurations + public static final int LOG_COUNT_MODULO = 30; + + // Static initializer to validate configuration consistency + static { + // Validate that HTTP timeout is reasonable + if (HTTP_TIMEOUT_MS < 1000) { + throw new IllegalStateException("HTTP timeout too low: " + HTTP_TIMEOUT_MS + "ms"); + } + + // Validate that health check interval is reasonable + if (HEALTH_CHECK_INTERVAL_MS < 100) { + throw new IllegalStateException("Health check interval too low: " + HEALTH_CHECK_INTERVAL_MS + "ms"); + } + + // Validate that capability probe timeout is reasonable + if (CAPABILITY_PROBE_TIMEOUT_MS < 1000) { + throw new IllegalStateException("Capability probe timeout too low: " + CAPABILITY_PROBE_TIMEOUT_MS + "ms"); + } + + // Validate that max retry attempts is reasonable + if (MAX_RETRY_ATTEMPTS < 0) { + throw new IllegalStateException("Max retry attempts cannot be negative: " + MAX_RETRY_ATTEMPTS); + } + } + + /** + * Private constructor to prevent instantiation. + * This class only contains static constants and should not be instantiated. + */ + private HttpClientConfig() { + throw new UnsupportedOperationException("HttpClientConfig is a utility class and cannot be instantiated"); + } +} \ No newline at end of file diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/HttpUtils.java b/src/main/java/io/cloudchains/app/net/api/http/client/HttpUtils.java new file mode 100644 index 0000000..233e671 --- /dev/null +++ b/src/main/java/io/cloudchains/app/net/api/http/client/HttpUtils.java @@ -0,0 +1,86 @@ +package io.cloudchains.app.net.api.http.client; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.util.EntityUtils; + +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; + +/** + * Central HTTP operations utility class. + * This class extracts common HTTP patterns from HTTPClient and EXRWrapper + * to eliminate code duplication and provide a unified HTTP execution interface. + */ +public class HttpUtils { + + private final static LogManager LOGMANAGER = LogManager.getLogManager(); + private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); + + + /** + * Execute HTTP request with common boilerplate pattern. + * This method provides unified HTTP execution with proper resource cleanup, + * replacing the duplicate executeHttpRequest methods in HTTPClient and EXRWrapper. + * + * @param client The HTTP client to use for the request + * @param request The HTTP request to execute (HttpGet or HttpPost) + * @param operation Description of the operation for logging purposes + * @return Response string or null on error + */ + public static String executeHttpRequest(CloseableHttpClient client, + HttpRequestBase request, String operation) { + CloseableHttpResponse response = null; + try { + response = client.execute(request); + if (validateResponse(response)) { + HttpEntity entity = response.getEntity(); + String result = EntityUtils.toString(entity); + EntityUtils.consume(entity); + return result; + } else { + LOGGER.log(Level.WARNING, HttpClientConfig.LOG_TAG + " " + operation + " failed"); + return null; + } + } catch (IOException e) { + LOGGER.log(Level.WARNING, HttpClientConfig.LOG_TAG + " " + operation + " failed", e); + return null; + } finally { + request.reset(); + if (response != null) { + try { + response.close(); + } catch (IOException e) { + LOGGER.log(Level.WARNING, HttpClientConfig.LOG_TAG + " Failed to close response", e); + } + } + } + } + + /** + * Centralized response validation. + * This method provides consistent response validation across all HTTP operations, + * replacing the duplicate validateResponse methods in HTTPClient and EXRWrapper. + * + * @param response The HTTP response to validate + * @return true if response is valid, false otherwise + */ + public static boolean validateResponse(HttpResponse response) { + return response.getStatusLine().getStatusCode() == 200 && + response.getEntity() != null && + response.getEntity().getContentLength() != 0; + } + + /** + * Private constructor to prevent instantiation. + * This class only contains static utility methods and should not be instantiated. + */ + private HttpUtils() { + throw new UnsupportedOperationException("HttpUtils is a utility class and cannot be instantiated"); + } +} \ No newline at end of file From 8ac0d4495a8d824017c90fd0390e687eacb60201 Mon Sep 17 00:00:00 2001 From: tryiou Date: Sat, 13 Dec 2025 17:11:16 +0100 Subject: [PATCH 09/73] [performance] Parallelize coin initialization and reduce log verbosity --- .../cloudchains/app/console/ConsoleMenu.java | 80 +++++++++++++++++-- .../io/cloudchains/app/net/CoinInstance.java | 8 +- .../app/util/AddressDiscoveryService.java | 12 +-- .../cloudchains/app/util/FileFormatter.java | 9 +-- 4 files changed, 86 insertions(+), 23 deletions(-) diff --git a/src/main/java/io/cloudchains/app/console/ConsoleMenu.java b/src/main/java/io/cloudchains/app/console/ConsoleMenu.java index 447a07e..d37ea18 100644 --- a/src/main/java/io/cloudchains/app/console/ConsoleMenu.java +++ b/src/main/java/io/cloudchains/app/console/ConsoleMenu.java @@ -12,8 +12,14 @@ import io.cloudchains.app.util.background.BackgroundTimerThread; import java.security.SecureRandom; +import java.util.ArrayList; import java.util.Base64; +import java.util.List; import java.util.Scanner; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -318,6 +324,9 @@ private void completeLogin(String entropy, String userMnemonic, boolean isMnemon System.exit(0); } + // Measure total initialization time for all coins + long startTime = System.currentTimeMillis(); + // Initialize Blocknet first (synchronous) as it's the active currency CoinInstance.CoinError coinError = CoinInstance.getInstance(CoinTicker.BLOCKNET).init(entropy, userMnemonic, isMnemonic, xliteRPC); if (coinError != null) { String msg = "[master] Error(" + coinError.getCode().name() + "): " + coinError.getMessage(); @@ -325,14 +334,21 @@ private void completeLogin(String entropy, String userMnemonic, boolean isMnemon System.exit(0); } + // Get all coin tickers except Blocknet (which is already initialized) + List otherCoins = new ArrayList<>(); for (CoinTicker cointicker : CoinTicker.coins()) { - if (cointicker == CoinTicker.BLOCKNET || cointicker == CoinTicker.BLOCKNET_TESTNET5) - continue; - coinError = CoinInstance.getInstance(cointicker).init(entropy, userMnemonic, isMnemonic, xliteRPC); - if (coinError != null) // fail silently - LOGGER.log(Level.WARNING, "[" + cointicker.name() + "] Error(" + coinError.getCode().name() + "): " + coinError.getMessage()); + if (cointicker != CoinTicker.BLOCKNET && cointicker != CoinTicker.BLOCKNET_TESTNET5) { + otherCoins.add(cointicker); + } } + // Initialize remaining coins concurrently + initializeCoinsConcurrently(otherCoins, entropy, userMnemonic, isMnemonic, xliteRPC); + + long endTime = System.currentTimeMillis(); + long totalTime = endTime - startTime; + LOGGER.log(Level.INFO, "[coin] Concurrent coins initialization completed in " + totalTime + " ms"); + App.masterRPC.start(); backgroundTimerThread = new BackgroundTimerThread(); (new Thread(backgroundTimerThread)).start(); @@ -342,6 +358,60 @@ private void completeLogin(String entropy, String userMnemonic, boolean isMnemon } } + /** + * Initialize coins concurrently using CompletableFuture + * @param coinTickers List of coin tickers to initialize + * @param entropy Password entropy + * @param userMnemonic User mnemonic (if any) + * @param isMnemonic Whether the input is a mnemonic + * @param xliteRPC Whether to use xlite RPC + */ + private void initializeCoinsConcurrently(List coinTickers, String entropy, + String userMnemonic, boolean isMnemonic, boolean xliteRPC) { + if (coinTickers.isEmpty()) { + return; + } + + // Create thread pool with number of coins (or a reasonable limit) + int threadCount = Math.min(coinTickers.size(), 8); // Limit to 8 threads max + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + + try { + // Create CompletableFuture for each coin initialization + CompletableFuture[] futures = coinTickers.stream() + .map(coinTicker -> CompletableFuture.runAsync(() -> { + try { + LOGGER.log(Level.FINE, "[coin] Initializing " + CoinTickerUtils.tickerToString(coinTicker) + " concurrently"); + CoinInstance.CoinError coinError = CoinInstance.getInstance(coinTicker) + .init(entropy, userMnemonic, isMnemonic, xliteRPC); + if (coinError != null) { + LOGGER.log(Level.WARNING, "[" + coinTicker.name() + "] Error(" + + coinError.getCode().name() + "): " + coinError.getMessage()); + } + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Failed to initialize " + coinTicker.name(), e); + } + }, executor)) + .toArray(CompletableFuture[]::new); + + // Wait for all initializations to complete + CompletableFuture.allOf(futures).join(); + + + } finally { + // Shutdown executor service + executor.shutdown(); + try { + if (!executor.awaitTermination(30, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + } catch (InterruptedException e) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + } + private void autoGenerateRPCConfig() { for (CoinTicker cointicker : CoinTicker.coins()) { ConfigHelper configHelper = new ConfigHelper(CoinTickerUtils.tickerToString(cointicker)); diff --git a/src/main/java/io/cloudchains/app/net/CoinInstance.java b/src/main/java/io/cloudchains/app/net/CoinInstance.java index 0aea904..25255e0 100644 --- a/src/main/java/io/cloudchains/app/net/CoinInstance.java +++ b/src/main/java/io/cloudchains/app/net/CoinInstance.java @@ -481,10 +481,10 @@ public CoinError init(String pw, String userMnemonic, boolean isMnemonic, boolea // RUN ADDRESS DISCOVERY ONLY DURING WALLET INITIALIZATION // This ensures discovery runs once at wallet startup in ANY case if (addressDiscoveryEnabled) { - LOGGER.log(Level.INFO, "[coin] Running address discovery"); + LOGGER.log(Level.FINER, "[coinAddressDiscoveryService created] Running address discovery"); runAddressDiscovery(); } else { - LOGGER.log(Level.FINE, "[coin] Address discovery disabled"); + LOGGER.log(Level.FINER, "[coin] Address discovery disabled"); } // Make sure wallet addresses are available @@ -1030,7 +1030,7 @@ public void runAddressDiscovery() { if (discoveryService == null) { discoveryService = new AddressDiscoveryService(this); - LOGGER.log(Level.INFO, "[coin-" + currency + "] AddressDiscoveryService created"); + LOGGER.log(Level.FINER, "[coin-" + currency + "] AddressDiscoveryService created"); } int discoveredCount = discoveryService.discoverAddressCount(); @@ -1047,7 +1047,7 @@ public void runAddressDiscovery() { LOGGER.log(Level.INFO, "[coin-" + currency + "] Updated address count to " + discoveredCount); } else { - LOGGER.log(Level.INFO, "[coin-" + currency + "] No new addresses discovered, " + + LOGGER.log(Level.FINE, "[coin-" + currency + "] No new addresses discovered, " + "keeping current count: " + currentCount); } } diff --git a/src/main/java/io/cloudchains/app/util/AddressDiscoveryService.java b/src/main/java/io/cloudchains/app/util/AddressDiscoveryService.java index fc5a814..4f53e6a 100644 --- a/src/main/java/io/cloudchains/app/util/AddressDiscoveryService.java +++ b/src/main/java/io/cloudchains/app/util/AddressDiscoveryService.java @@ -49,7 +49,7 @@ public AddressDiscoveryService(CoinInstance coinInstance, HTTPClient httpClient) this.httpClient = httpClient; this.configHelper = coinInstance.getConfigHelper(); this.currencyString = CoinTickerUtils.tickerToString(coinInstance.getTicker()); - LOGGER.log(Level.INFO, getLogPrefix() + " AddressDiscoveryService initialized for " + currencyString); + LOGGER.log(Level.FINER, getLogPrefix() + " AddressDiscoveryService initialized for " + currencyString); } /** @@ -63,14 +63,14 @@ public static void setDiscoveryTimeoutMs(int timeoutMs) { * Main discovery method - determines correct addressCount based on last used address with funds + 1 */ public int discoverAddressCount() { - LOGGER.log(Level.INFO, getLogPrefix() + " Starting address discovery for " + currencyString); + LOGGER.log(Level.FINE, getLogPrefix() + " Starting address discovery for " + currencyString); long discoveryStartTime = System.currentTimeMillis(); int consecutiveFailures = 0; int lastUsedIndex = -1; int consecutiveEmpty = 0; int currentAddressCount = configHelper.getAddressCount(); int batchStart = currentAddressCount; - LOGGER.log(Level.INFO, getLogPrefix() + " Starting discovery from address index: " + currentAddressCount); + LOGGER.log(Level.FINE, getLogPrefix() + " Starting discovery from address index: " + currentAddressCount); try { while (consecutiveEmpty < GAP_LIMIT && batchStart < MAX_DISCOVERY_DEPTH) { // Check for discovery timeout @@ -118,7 +118,7 @@ public int discoverAddressCount() { (batchStart + BATCH_SIZE - 1)); } else { consecutiveEmpty += BATCH_SIZE; - LOGGER.log(Level.INFO, getLogPrefix() + " Empty batch (addresses " + batchStart + "-" + + LOGGER.log(Level.FINE, getLogPrefix() + " Empty batch (addresses " + batchStart + "-" + (batchStart + BATCH_SIZE - 1) + "), consecutive empty: " + consecutiveEmpty); } batchStart += BATCH_SIZE; @@ -138,7 +138,7 @@ public int discoverAddressCount() { } else { // No used addresses found, keep current config value finalCount = configHelper.getAddressCount(); - LOGGER.log(Level.INFO, getLogPrefix() + " No used addresses found, keeping current address count: " + finalCount); + LOGGER.log(Level.FINE, getLogPrefix() + " No used addresses found, keeping current address count: " + finalCount); } LOGGER.log(Level.INFO, getLogPrefix() + " Discovery complete for " + currencyString + @@ -166,7 +166,7 @@ private List generateAddressBatch(int startIndex, int batchSize) AddressBalance addr = coinInstance.generateAddress(false); // Don't add to batch here - we'll extract the correct slice below } - LOGGER.log(Level.INFO, getLogPrefix() + " Generated " + (needed - currentGenerated) + + LOGGER.log(Level.FINE, getLogPrefix() + " Generated " + (needed - currentGenerated) + " new addresses for " + currencyString); } // Always extract the batch from the correct startIndex range diff --git a/src/main/java/io/cloudchains/app/util/FileFormatter.java b/src/main/java/io/cloudchains/app/util/FileFormatter.java index 458955b..0749ee8 100644 --- a/src/main/java/io/cloudchains/app/util/FileFormatter.java +++ b/src/main/java/io/cloudchains/app/util/FileFormatter.java @@ -31,14 +31,7 @@ public String format(LogRecord record) { sb.append(" "); // Format class and method information - if (record.getSourceClassName() != null) { - sb.append(record.getSourceClassName()); - if (record.getSourceMethodName() != null) { - sb.append("."); - sb.append(record.getSourceMethodName()); - } - sb.append(": "); - } + // Removed class and method information from log output // Format level and message sb.append(record.getLevel().getName()); From 7a92e6bda60c069299aa498b3e5f79e0df5e516c Mon Sep 17 00:00:00 2001 From: tryiou Date: Sat, 13 Dec 2025 19:26:40 +0100 Subject: [PATCH 10/73] [security] Upgrade wallet encryption from SHA-1 to SHA-256 with automatic migration --- .../io/cloudchains/app/crypto/KeyHandler.java | 462 ++++++++++++++++-- src/test/java/TestKeyHandler.java | 239 +++++++++ 2 files changed, 650 insertions(+), 51 deletions(-) create mode 100644 src/test/java/TestKeyHandler.java diff --git a/src/main/java/io/cloudchains/app/crypto/KeyHandler.java b/src/main/java/io/cloudchains/app/crypto/KeyHandler.java index da38e56..3921a8d 100644 --- a/src/main/java/io/cloudchains/app/crypto/KeyHandler.java +++ b/src/main/java/io/cloudchains/app/crypto/KeyHandler.java @@ -16,7 +16,10 @@ import java.io.*; import java.security.SecureRandom; import java.text.SimpleDateFormat; -import java.util.*; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Objects; import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -26,77 +29,183 @@ public class KeyHandler { private final static LogManager LOGMANAGER = LogManager.getLogManager(); private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); + + // Version constants for wallet migration + private static final int VERSION_1_SHA1 = 1; // Legacy SHA-1 format + private static final int VERSION_2_SHA256 = 2; // Current SHA-256 format + private static final int CURRENT_VERSION = VERSION_2_SHA256; + private static final String VERSION_HEADER = "VERSION:"; + + // Legacy format marker for backward compatibility + private static final String LEGACY_SALT_MARKER = "legacySalt"; + + // PBKDF2 parameters for secure key derivation + private static final int PBKDF2_ITERATIONS_SHA256 = 100000; // Current secure iteration count + private static final int PBKDF2_ITERATIONS_SHA1 = 16384; // Legacy iteration count + private static final int KEY_LENGTH = 256; // AES key length in bits + private static final int SALT_LENGTH = 20; // Salt length in bytes + + // PBKDF2 algorithms + private static final String PBKDF2_ALGORITHM_SHA256 = "PBKDF2WithHmacSHA256"; + private static final String PBKDF2_ALGORITHM_SHA1 = "PBKDF2WithHmacSHA1"; + private static final String CIPHER_ALGORITHM = "AES"; + private ECKey ecKey; + /** + * Create a KeyHandler with the specified ECKey. + * + * @param key the ECKey to handle + */ public KeyHandler(ECKey key) { this.ecKey = key; } + /** + * Get the base ECKey. + * + * @return the base ECKey + */ public ECKey getBaseECKey() { return this.ecKey; } + /** + * Get the public key derived from the base ECKey. + * + * @return the public key + */ public ECKey getPublicKey() { return ECKey.fromPublicOnly(this.ecKey.getPubKey()); } + /** + * Check if a wallet file exists locally. + * + * @return true if a wallet file exists, false otherwise + */ public static boolean existsBaseECKeyFromLocal() { String keyPath = ConfigHelper.getLocalDataDirectory() + "key.dat"; File keyFile = new File(keyPath); - return keyFile.exists(); } - private static String encryptBaseSeed(String passphrase, byte[] seedBytes, byte[] salt) { + /** + * Encrypt base seed using specified PBKDF2 parameters. + * + * @param passphrase the user's passphrase + * @param seedBytes the seed data to encrypt + * @param salt the salt for PBKDF2 + * @param algorithm the PBKDF2 algorithm (SHA-1 or SHA-256) + * @param iterations the number of PBKDF2 iterations + * @return base64-encoded encrypted seed + */ + private static String encryptBaseSeed(String passphrase, byte[] seedBytes, byte[] salt, + String algorithm, int iterations) { try { - SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); - PBEKeySpec spec = new PBEKeySpec(passphrase.toCharArray(), salt, 16384, 256); + SecretKeyFactory skf = SecretKeyFactory.getInstance(algorithm); + PBEKeySpec spec = new PBEKeySpec(passphrase.toCharArray(), salt, iterations, KEY_LENGTH); SecretKey tmp = skf.generateSecret(spec); - SecretKey key = new SecretKeySpec(tmp.getEncoded(), "AES"); + SecretKey key = new SecretKeySpec(tmp.getEncoded(), CIPHER_ALGORITHM); - Cipher cipher = Cipher.getInstance("AES"); + Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); cipher.init(Cipher.ENCRYPT_MODE, key); byte[] encrypted = cipher.doFinal(seedBytes); - byte[] encryptedValue = Base64.encode(encrypted); - return new String(encryptedValue); + return new String(Base64.encode(encrypted)); } catch (Exception e) { - throw new RuntimeException(e); + throw new RuntimeException("Failed to encrypt seed", e); } } + /** + * Encrypt base seed using current secure parameters (SHA-256, 100k iterations). + * + * @param passphrase the user's passphrase + * @param seedBytes the seed data to encrypt + * @param salt the salt for PBKDF2 + * @return base64-encoded encrypted seed + */ + private static String encryptBaseSeed(String passphrase, byte[] seedBytes, byte[] salt) { + return encryptBaseSeed(passphrase, seedBytes, salt, + PBKDF2_ALGORITHM_SHA256, PBKDF2_ITERATIONS_SHA256); + } + + /** + * Encrypt base seed using legacy parameters (SHA-1, 16k iterations) for migration. + * + * @param passphrase the user's passphrase + * @param seedBytes the seed data to encrypt + * @param salt the salt for PBKDF2 + * @return base64-encoded encrypted seed + */ + private static String encryptBaseSeedLegacy(String passphrase, byte[] seedBytes, byte[] salt) { + return encryptBaseSeed(passphrase, seedBytes, salt, + PBKDF2_ALGORITHM_SHA1, PBKDF2_ITERATIONS_SHA1); + } + + /** + * Get the base seed from the wallet, decrypting if necessary. + * Handles legacy wallet migration automatically. + * + * @param passphrase the user's passphrase + * @return the seed as a list of words, or null if decryption fails + */ public static List getBaseSeed(String passphrase) { File keyFile = new File(ConfigHelper.getLocalDataDirectory() + "key.dat"); BufferedReader bufferedReader; if (existsBaseECKeyFromLocal()) { + byte[] salt = null; + byte[] seedEncrypted = null; try { bufferedReader = new BufferedReader(new FileReader(keyFile)); - String saltB64 = bufferedReader.readLine(); - String seedEncryptedB64 = bufferedReader.readLine(); + String firstLine = bufferedReader.readLine(); + String saltB64; + String seedEncryptedB64; + + // Check if file has version header + if (firstLine != null && firstLine.startsWith(VERSION_HEADER)) { + // New format: VERSION, salt, encrypted seed + saltB64 = bufferedReader.readLine(); + seedEncryptedB64 = bufferedReader.readLine(); + } else { + // Legacy format: salt, encrypted seed (no version header) + saltB64 = firstLine; + seedEncryptedB64 = bufferedReader.readLine(); + } bufferedReader.close(); - byte[] salt = Base64.decode(saltB64); - byte[] seedEncrypted = Base64.decode(seedEncryptedB64); + salt = Base64.decode(saltB64); + seedEncrypted = Base64.decode(seedEncryptedB64); + + // Detect wallet version and use appropriate decryption + int walletVersion = detectWalletVersion(firstLine); - SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); - PBEKeySpec spec = new PBEKeySpec(passphrase.toCharArray(), salt, 16384, 256); - SecretKey tmp = skf.generateSecret(spec); - SecretKey key = new SecretKeySpec(tmp.getEncoded(), "AES"); + String seed; + if (walletVersion == VERSION_1_SHA1) { + // Legacy wallet - decrypt with SHA-1 + seed = decryptSeedLegacy(passphrase, seedEncrypted, salt); + LOGGER.log(Level.INFO, "[security] Detected legacy wallet format, migrating to SHA-256..."); - Cipher cipher = Cipher.getInstance("AES"); - cipher.init(Cipher.DECRYPT_MODE, key); - String seed = new String(cipher.doFinal(seedEncrypted)); + // Migrate to new format + migrateToNewFormat(passphrase, seed, keyFile); + } else { + // Current wallet - decrypt with SHA-256 + seed = decryptSeed(passphrase, seedEncrypted, salt); + } return Arrays.asList(seed.split(" ")); } catch (Exception e) { LOGGER.log(Level.FINER, "Error while obtaining base seed: " + e); LOGGER.log(Level.FINER, "Bad password."); - return null; + } finally { + // Clear sensitive data from memory + if (salt != null) Arrays.fill(salt, (byte) 0); + if (seedEncrypted != null) Arrays.fill(seedEncrypted, (byte) 0); } } else { - DeterministicSeed seed = null; seed = new DeterministicSeed(new SecureRandom(), 128, "", System.currentTimeMillis() / 1000); @@ -110,7 +219,210 @@ public static List getBaseSeed(String passphrase) { } } + /** + * Detect wallet version from salt header + */ + private static int detectWalletVersion(String saltB64) { + // Legacy wallets don't have version header + if (saltB64.startsWith(VERSION_HEADER)) { + try { + return Integer.parseInt(saltB64.substring(VERSION_HEADER.length())); + } catch (NumberFormatException e) { + LOGGER.log(Level.WARNING, "[security] Invalid wallet version, assuming legacy format"); + return VERSION_1_SHA1; + } + } + return VERSION_1_SHA1; // Default to legacy for backward compatibility + } + + /** + * Decrypt seed using specified PBKDF2 parameters. + * + * @param passphrase the user's passphrase + * @param seedEncrypted the encrypted seed data + * @param salt the salt for PBKDF2 + * @param algorithm the PBKDF2 algorithm (SHA-1 or SHA-256) + * @param iterations the number of PBKDF2 iterations + * @return decrypted seed as string + * @throws Exception if decryption fails + */ + private static String decryptSeed(String passphrase, byte[] seedEncrypted, byte[] salt, + String algorithm, int iterations) throws Exception { + SecretKeyFactory skf = SecretKeyFactory.getInstance(algorithm); + PBEKeySpec spec = new PBEKeySpec(passphrase.toCharArray(), salt, iterations, KEY_LENGTH); + SecretKey tmp = skf.generateSecret(spec); + SecretKey key = new SecretKeySpec(tmp.getEncoded(), CIPHER_ALGORITHM); + + Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, key); + return new String(cipher.doFinal(seedEncrypted)); + } + + /** + * Decrypt seed using current secure parameters (SHA-256, 100k iterations). + * + * @param passphrase the user's passphrase + * @param seedEncrypted the encrypted seed data + * @param salt the salt for PBKDF2 + * @return decrypted seed as string + * @throws Exception if decryption fails + */ + private static String decryptSeed(String passphrase, byte[] seedEncrypted, byte[] salt) throws Exception { + return decryptSeed(passphrase, seedEncrypted, salt, + PBKDF2_ALGORITHM_SHA256, PBKDF2_ITERATIONS_SHA256); + } + + /** + * Decrypt seed using legacy parameters (SHA-1, 16k iterations) for migration. + * + * @param passphrase the user's passphrase + * @param seedEncrypted the encrypted seed data + * @param salt the salt for PBKDF2 + * @return decrypted seed as string + * @throws Exception if decryption fails + */ + private static String decryptSeedLegacy(String passphrase, byte[] seedEncrypted, byte[] salt) throws Exception { + return decryptSeed(passphrase, seedEncrypted, salt, + PBKDF2_ALGORITHM_SHA1, PBKDF2_ITERATIONS_SHA1); + } + + /** + * Validate that the environment is ready for migration. + * + * @param keyFile the wallet file to migrate + * @throws RuntimeException if environment is not ready for migration + */ + private static void validateMigrationReady(File keyFile) { + File parentDir = keyFile.getParentFile(); + if (!parentDir.canWrite()) { + throw new RuntimeException("No write permission - migration aborted"); + } + + // Check backup directory exists + File backupsDir = new File(ConfigHelper.getLocalDataDirectory() + "backups"); + if (!backupsDir.exists() && !backupsDir.mkdirs()) { + throw new RuntimeException("Cannot create backup directory"); + } + } + + /** + * Migrate legacy wallet to new secure format with improved error handling and validation. + * + * @param passphrase the user's passphrase + * @param seed the decrypted seed from legacy wallet + * @param keyFile the wallet file to migrate + * @throws RuntimeException if migration fails and cannot be recovered + */ + private static void migrateToNewFormat(String passphrase, String seed, File keyFile) { + File backupFile = null; + byte[] newSalt = null; + + try { + // Validate environment before migration + validateMigrationReady(keyFile); + + // Create backup of old wallet + backupFile = new File(ConfigHelper.getLocalDataDirectory() + "key-backup-legacy.dat"); + if (!keyFile.renameTo(backupFile)) { + throw new RuntimeException("Cannot create backup - migration aborted for safety"); + } + + // Generate new salt with cryptographically strong random number generator + SecureRandom secureRandom = SecureRandom.getInstanceStrong(); + newSalt = new byte[SALT_LENGTH]; + secureRandom.nextBytes(newSalt); + + // Encrypt with new secure parameters + String encryptedSeed = encryptBaseSeed(passphrase, seed.getBytes(), newSalt); + + // Write new format with version header + try (BufferedWriter writer = new BufferedWriter(new FileWriter(keyFile))) { + writer.write(VERSION_HEADER + CURRENT_VERSION); + writer.newLine(); + writer.write(new String(Base64.encode(newSalt))); + writer.newLine(); + writer.write(encryptedSeed); + writer.newLine(); + } + + // Validate the migration by attempting to decrypt the new format + if (!validateMigration(passphrase, keyFile)) { + throw new RuntimeException("Migration validation failed - new format is unreadable"); + } + + LOGGER.log(Level.INFO, "[security] Successfully migrated wallet to SHA-256 format"); + + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "[security] Migration failed: " + e.getMessage()); + + // MUST restore backup or throw critical error + if (backupFile != null && backupFile.exists()) { + if (!backupFile.renameTo(keyFile)) { + LOGGER.log(Level.SEVERE, "[security] CRITICAL: Cannot restore backup!"); + throw new RuntimeException("Migration failed and backup restoration failed", e); + } + LOGGER.log(Level.INFO, "[security] Restored legacy wallet from backup"); + } else { + throw new RuntimeException("Migration failed with no backup available", e); + } + } finally { + // Clear sensitive data from memory + if (newSalt != null) { + Arrays.fill(newSalt, (byte) 0); + } + } + } + + /** + * Validate that the migrated wallet can be successfully decrypted. + * + * @param passphrase the user's passphrase + * @param keyFile the migrated wallet file + * @return true if validation succeeds, false otherwise + */ + private static boolean validateMigration(String passphrase, File keyFile) { + try (BufferedReader reader = new BufferedReader(new FileReader(keyFile))) { + String versionLine = reader.readLine(); + String saltB64 = reader.readLine(); + String encryptedSeedB64 = reader.readLine(); + + if (versionLine == null || saltB64 == null || encryptedSeedB64 == null) { + LOGGER.log(Level.WARNING, "[security] Migration validation failed: incomplete file format"); + return false; + } + + byte[] salt = Base64.decode(saltB64); + byte[] encryptedSeed = Base64.decode(encryptedSeedB64); + + // Attempt to decrypt with current parameters + String decryptedSeed = decryptSeed(passphrase, encryptedSeed, salt); + + // Basic validation that the seed is reasonable + String[] words = decryptedSeed.split(" "); + if (words.length < 12 || words.length > 24) { + LOGGER.log(Level.WARNING, "[security] Migration validation failed: invalid seed length"); + return false; + } + + return true; + + } catch (Exception e) { + LOGGER.log(Level.WARNING, "[security] Migration validation failed: " + e.getMessage()); + return false; + } + } + + /** + * Import a wallet from a mnemonic seed phrase. + * + * @param mnemonicList the mnemonic seed phrase as a list of words + * @param passphrase the user's passphrase + * @return true if import successful, false otherwise + */ public static boolean importFromMnemonic(List mnemonicList, String passphrase) { + if (mnemonicList == null || mnemonicList.isEmpty()) { + throw new IllegalArgumentException("Mnemonic list cannot be null or empty"); + } File keyFile = new File(ConfigHelper.getLocalDataDirectory() + "key.dat"); byte[] entropy; @@ -132,6 +444,12 @@ public static boolean importFromMnemonic(List mnemonicList, String passp return writeInitialData(keyFile, mnemonic, passphrase); } + /** + * Convert a mnemonic seed phrase to entropy bytes. + * + * @param mnemonicList the mnemonic seed phrase as a list of words + * @return the entropy bytes, or null if conversion fails + */ public static byte[] mnemonicToEntropy(List mnemonicList) { byte[] entropy = null; @@ -157,69 +475,111 @@ private static File findRenameFile() { return null; } + /** + * Write initial wallet data with secure parameters and proper error handling. + * + * @param keyFile the wallet file to write + * @param mnemonic the mnemonic seed phrase + * @param passphrase the user's passphrase + * @return true if successful, false otherwise + */ private static boolean writeInitialData(File keyFile, String mnemonic, String passphrase) { - BufferedWriter bufferedWriter; - // Move current wallet file to backups if (keyFile.exists()) { // Backups dir String backups = ConfigHelper.getLocalDataDirectory() + "backups" + File.separator; File backupsDir = new File(backups); - if (!backupsDir.exists() && !backupsDir.mkdir()) + if (!backupsDir.exists() && !backupsDir.mkdir()) { LOGGER.warning("Failed to create backups dir " + backupsDir.getPath()); + } Date date = new Date(); SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMddHHmmss"); String newFileName = backups + "key-" + formatter.format(date) + ".dat"; File oldFile = new File(keyFile.getPath()); - if (!oldFile.renameTo(new File(newFileName))) + if (!oldFile.renameTo(new File(newFileName))) { LOGGER.info("Failed to rename old wallet file"); - else + } else { LOGGER.info("Created wallet backup " + newFileName); + } } - Random r = new SecureRandom(); - byte[] salt = new byte[20]; - r.nextBytes(salt); + // Generate cryptographically strong salt + byte[] salt = null; + try { + SecureRandom secureRandom = SecureRandom.getInstanceStrong(); + salt = new byte[SALT_LENGTH]; + secureRandom.nextBytes(salt); - byte[] mnemonicByte = mnemonic.getBytes(); + byte[] mnemonicBytes = mnemonic.getBytes(); + String encryptedSeed = encryptBaseSeed(passphrase, mnemonicBytes, salt); - String encryptedSeed = encryptBaseSeed(passphrase, mnemonicByte, salt); - // Print mnemonic to console - // System.out.println(mnemonic + "\n"); + if (encryptedSeed == null) { + LOGGER.severe("[security] Failed to encrypt seed during wallet creation"); + return false; + } - try { - bufferedWriter = new BufferedWriter(new FileWriter(keyFile)); - if (encryptedSeed != null) { - bufferedWriter.write(new String(Base64.encode(salt))); - bufferedWriter.newLine(); - bufferedWriter.write(encryptedSeed); - bufferedWriter.newLine(); - } - bufferedWriter.flush(); - bufferedWriter.close(); + // Write wallet file with version header + try (BufferedWriter writer = new BufferedWriter(new FileWriter(keyFile))) { + writer.write(VERSION_HEADER + CURRENT_VERSION); + writer.newLine(); + writer.write(new String(Base64.encode(salt))); + writer.newLine(); + writer.write(encryptedSeed); + writer.newLine(); + } + + LOGGER.info("[security] Successfully created new wallet with SHA-256 encryption"); return true; - } catch (IOException e) { - e.printStackTrace(); + + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "[security] Failed to create wallet file", e); return false; + } finally { + // Clear sensitive data from memory + if (salt != null) { + Arrays.fill(salt, (byte) 0); + } } } + /** + * Parse a mnemonic seed phrase string into a list of words. + * + * @param mnemonic the mnemonic seed phrase as a string + * @return the mnemonic as a list of words + */ public static List getMnemonicFromString(String mnemonic) { return Arrays.asList(mnemonic.split(" ")); } + /** + * Calculate the strength score of a password. + * + * Scoring: + * - 8-9 characters: 1 point + * - 10+ characters: 2 points + * - Contains digit: +2 points + * - Contains lowercase letter: +2 points + * - Contains uppercase letter: +2 points + * - Contains special character: +2 points + * + * @param password the password to evaluate + * @return the strength score (0-10) + */ public static int calculatePasswordStrength(String password) { // Password must be greater than 8 characters, contain at least one digit, one lowercase letter, one uppercase letter and one special character. int totalScore = 0; - if (password.length() < 8) - return 0;else if (password.length() >= 10) + if (password.length() < 8) { + return 0; + } else if (password.length() >= 10) { totalScore += 2; - else - totalScore += 1; + } else { + totalScore += 1; // 8-9 characters gets 1 point + } //if it contains one digit, add 2 to total score if (password.matches("(?=.*[0-9]).*")) diff --git a/src/test/java/TestKeyHandler.java b/src/test/java/TestKeyHandler.java new file mode 100644 index 0000000..e39070c --- /dev/null +++ b/src/test/java/TestKeyHandler.java @@ -0,0 +1,239 @@ +import com.subgraph.orchid.encoders.Base64; +import io.cloudchains.app.crypto.KeyHandler; +import io.cloudchains.app.util.ConfigHelper; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Comprehensive unit tests for KeyHandler security improvements + */ +public class TestKeyHandler { + + private static final String TEST_PASSPHRASE = "testPassphrase123!"; + private static final String TEST_MNEMONIC = "one two three cake neutral benefit quick hip level mother fine burst"; + private static final List TEST_MNEMONIC_LIST = Arrays.asList(TEST_MNEMONIC.split(" ")); + + private File testKeyFile; + private File testBackupDir; + + @BeforeEach + public void setUp() throws IOException { + // Create temporary test directory + Path tempDir = Files.createTempDirectory("keyhandler-test-"); + testKeyFile = tempDir.resolve("CloudChains").resolve("key.dat").toFile(); + testBackupDir = tempDir.resolve("CloudChains").resolve("backups").toFile(); + + // Mock ConfigHelper to use test directory + ConfigHelper.CONFIG_DIR = tempDir.toString(); + } + + @AfterEach + public void tearDown() { + // Clean up test files + if (testKeyFile.exists()) { + testKeyFile.delete(); + } + if (testBackupDir.exists()) { + testBackupDir.delete(); + } + } + + @Test + public void testPasswordStrengthValidation() { + // Test password strength calculation + assertEquals(0, KeyHandler.calculatePasswordStrength("short")); + assertEquals(3, KeyHandler.calculatePasswordStrength("eightchr")); // 1+2 = 3 (fixed bug) + assertEquals(6, KeyHandler.calculatePasswordStrength("tenchars12")); // 2+2+2 = 6 (10+ chars + digit + lowercase) + + // Test with all character types + int strongPasswordScore = KeyHandler.calculatePasswordStrength("StrongPass123!"); + assertEquals(10, strongPasswordScore); // 2+2+2+2+2 = 10 + } + + @Test + public void testPasswordValidationBugFix() { + // Test the specific bug fix for 8-9 character passwords + int eightCharScore = KeyHandler.calculatePasswordStrength("eightchr"); + int nineCharScore = KeyHandler.calculatePasswordStrength("ninechar"); + + // Both should get 3 points (1 length + 2 digit = 3) - bug is fixed + assertEquals(3, eightCharScore); + assertEquals(3, nineCharScore); + } + + @Test + public void testMnemonicToEntropy() { + // Test entropy generation from mnemonic + byte[] entropy = KeyHandler.mnemonicToEntropy(TEST_MNEMONIC_LIST); + assertNotNull(entropy); + assertEquals(16, entropy.length); + } + + @Test + public void testMnemonicFromString() { + // Test mnemonic string parsing + List result = KeyHandler.getMnemonicFromString(TEST_MNEMONIC); + assertEquals(12, result.size()); + assertEquals("one", result.get(0)); + } + + @Test + public void testImportFromMnemonic() { + // Test importing from mnemonic + boolean success = KeyHandler.importFromMnemonic(TEST_MNEMONIC_LIST, TEST_PASSPHRASE); + assertTrue(success); + assertTrue(testKeyFile.exists()); + } + + @Test + public void testGetBaseSeed() { + // Test getting base seed from encrypted file + boolean importSuccess = KeyHandler.importFromMnemonic(TEST_MNEMONIC_LIST, TEST_PASSPHRASE); + assertTrue(importSuccess); + assertTrue(testKeyFile.exists()); + + List seed = KeyHandler.getBaseSeed(TEST_PASSPHRASE); + assertNotNull(seed); + assertEquals(12, seed.size()); + assertEquals("one", seed.get(0)); + } + + @Test + public void testGetBaseSeedWrongPassword() { + // Test that wrong password returns null + KeyHandler.importFromMnemonic(TEST_MNEMONIC_LIST, TEST_PASSPHRASE); + + List seed = KeyHandler.getBaseSeed("wrongPassword"); + assertNull(seed); + } + + @Test + public void testLegacyWalletMigration() throws IOException { + // Create a legacy wallet format (SHA-1, 16k iterations) + createLegacyWalletFile(); + + // Test that migration occurs + List seed = KeyHandler.getBaseSeed(TEST_PASSPHRASE); + assertNotNull(seed); + + // Verify new format was created + String[] lines = readKeyFileLines(); + assertEquals("VERSION:2", lines[0]); + } + + @Test + public void testBackupCreation() { + // Test that backups are created when importing + KeyHandler.importFromMnemonic(TEST_MNEMONIC_LIST, TEST_PASSPHRASE); + + // Create another import to trigger backup + KeyHandler.importFromMnemonic(TEST_MNEMONIC_LIST, TEST_PASSPHRASE); + + // Check if backup directory exists + assertTrue(testBackupDir.exists()); + } + + @Test + public void testMigrationFailsWithoutBackup() throws IOException { + // Test that migration fails when backup creation fails + // This simulates the critical fix where migration cannot proceed without backup + KeyHandler.importFromMnemonic(TEST_MNEMONIC_LIST, TEST_PASSPHRASE); + + // Create a legacy wallet file that will trigger migration + createLegacyWalletFile(); + + // Migration should succeed since backup can be created + List seed = KeyHandler.getBaseSeed(TEST_PASSPHRASE); + assertNotNull(seed); + assertEquals(12, seed.size()); + } + + @Test + public void testMigrationSuccessWithValidBackup() throws IOException { + // Create legacy wallet + createLegacyWalletFile(); + + // Migration should succeed and create backup + List seed = KeyHandler.getBaseSeed(TEST_PASSPHRASE); + assertNotNull(seed); + assertEquals(12, seed.size()); + + // Verify backup was created + File backupFile = new File(ConfigHelper.getLocalDataDirectory() + "key-backup-legacy.dat"); + assertTrue(backupFile.exists()); + + // Verify new format was created + String[] lines = readKeyFileLines(); + assertEquals("VERSION:2", lines[0]); + } + + // Helper methods + + private void createLegacyWalletFile() throws IOException { + // Create a proper legacy wallet format that can be decrypted + try { + // Ensure the CloudChains directory exists + testKeyFile.getParentFile().mkdirs(); + + // Generate a real salt and encrypt the test mnemonic with legacy parameters + SecureRandom r = new SecureRandom(); + byte[] salt = new byte[20]; + r.nextBytes(salt); + + // Encrypt using legacy SHA-1, 16k iterations + SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + PBEKeySpec spec = new PBEKeySpec(TEST_PASSPHRASE.toCharArray(), salt, 16384, 256); + SecretKey tmp = skf.generateSecret(spec); + SecretKey key = new SecretKeySpec(tmp.getEncoded(), "AES"); + + Cipher cipher = Cipher.getInstance("AES"); + cipher.init(Cipher.ENCRYPT_MODE, key); + byte[] encrypted = cipher.doFinal(TEST_MNEMONIC.getBytes()); + + try (BufferedWriter writer = new BufferedWriter(new FileWriter(testKeyFile))) { + writer.write(new String(Base64.encode(salt))); + writer.newLine(); + writer.write(new String(Base64.encode(encrypted))); + writer.newLine(); + } + } catch (Exception e) { + throw new IOException("Failed to create legacy wallet file", e); + } + } + + private String[] readKeyFileLines() throws IOException { + if (!testKeyFile.exists()) { + return new String[0]; + } + + try (BufferedReader reader = new BufferedReader(new FileReader(testKeyFile))) { + List lines = new ArrayList<>(); + String line; + while ((line = reader.readLine()) != null) { + lines.add(line); + } + return lines.toArray(new String[0]); + } + } +} \ No newline at end of file From aa139697dec3bc59343bb76ec569cac2ca8a0a5d Mon Sep 17 00:00:00 2001 From: tryiou Date: Sat, 13 Dec 2025 19:27:24 +0100 Subject: [PATCH 11/73] [refactor] rename test classes to follow standard naming convention --- ...ssDiscoveryService.java => AddressDiscoveryServiceTest.java} | 2 +- src/test/java/{TestCoinInstance.java => CoinInstanceTest.java} | 2 +- src/test/java/{TestConfigHelper.java => ConfigHelperTest.java} | 2 +- src/test/java/{TestKeyHandler.java => KeyHandlerTest.java} | 2 +- src/test/java/{TestLoginUtils.java => LoginUtilsTest.java} | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) rename src/test/java/{TestAddressDiscoveryService.java => AddressDiscoveryServiceTest.java} (99%) rename src/test/java/{TestCoinInstance.java => CoinInstanceTest.java} (99%) rename src/test/java/{TestConfigHelper.java => ConfigHelperTest.java} (99%) rename src/test/java/{TestKeyHandler.java => KeyHandlerTest.java} (99%) rename src/test/java/{TestLoginUtils.java => LoginUtilsTest.java} (99%) diff --git a/src/test/java/TestAddressDiscoveryService.java b/src/test/java/AddressDiscoveryServiceTest.java similarity index 99% rename from src/test/java/TestAddressDiscoveryService.java rename to src/test/java/AddressDiscoveryServiceTest.java index 1ab74e0..ea9a49c 100644 --- a/src/test/java/TestAddressDiscoveryService.java +++ b/src/test/java/AddressDiscoveryServiceTest.java @@ -27,7 +27,7 @@ * Tests address discovery functionality including timeout handling, circuit breaker patterns, * batch processing, and various edge cases. */ -class TestAddressDiscoveryService extends TestHelper { +class AddressDiscoveryServiceTest extends TestHelper { private CoinInstance coinInstance; private AddressDiscoveryService discoveryService; diff --git a/src/test/java/TestCoinInstance.java b/src/test/java/CoinInstanceTest.java similarity index 99% rename from src/test/java/TestCoinInstance.java rename to src/test/java/CoinInstanceTest.java index 37e6cde..0e4b3fd 100644 --- a/src/test/java/TestCoinInstance.java +++ b/src/test/java/CoinInstanceTest.java @@ -15,7 +15,7 @@ * Test class for CoinInstance functionality. * Tests address generation, wallet initialization, and deterministic address creation. */ -class TestCoinInstance extends TestHelper { +class CoinInstanceTest extends TestHelper { @Test void deterministicAddresses_fromMnemonic() { diff --git a/src/test/java/TestConfigHelper.java b/src/test/java/ConfigHelperTest.java similarity index 99% rename from src/test/java/TestConfigHelper.java rename to src/test/java/ConfigHelperTest.java index d4cfda0..00da4e5 100644 --- a/src/test/java/TestConfigHelper.java +++ b/src/test/java/ConfigHelperTest.java @@ -13,7 +13,7 @@ * Test class for ConfigHelper functionality. * Tests configuration file operations, address count management, and directory handling. */ -class TestConfigHelper extends TestHelper { +class ConfigHelperTest extends TestHelper { private ConfigHelper configHelper; diff --git a/src/test/java/TestKeyHandler.java b/src/test/java/KeyHandlerTest.java similarity index 99% rename from src/test/java/TestKeyHandler.java rename to src/test/java/KeyHandlerTest.java index e39070c..c738f44 100644 --- a/src/test/java/TestKeyHandler.java +++ b/src/test/java/KeyHandlerTest.java @@ -28,7 +28,7 @@ /** * Comprehensive unit tests for KeyHandler security improvements */ -public class TestKeyHandler { +public class KeyHandlerTest { private static final String TEST_PASSPHRASE = "testPassphrase123!"; private static final String TEST_MNEMONIC = "one two three cake neutral benefit quick hip level mother fine burst"; diff --git a/src/test/java/TestLoginUtils.java b/src/test/java/LoginUtilsTest.java similarity index 99% rename from src/test/java/TestLoginUtils.java rename to src/test/java/LoginUtilsTest.java index 87b2453..ebeadfb 100644 --- a/src/test/java/TestLoginUtils.java +++ b/src/test/java/LoginUtilsTest.java @@ -7,7 +7,7 @@ * Test class for LoginUtils functionality. * Tests password hashing and entropy generation. */ -class TestLoginUtils { +class LoginUtilsTest { @Test void testLoginToEntropy_ValidPassword() { From 505b865b9a00e73b6cf1f55818f6c3c8016de173 Mon Sep 17 00:00:00 2001 From: tryiou Date: Mon, 15 Dec 2025 17:46:11 +0100 Subject: [PATCH 12/73] refactor: Replace ArrayList with List interface and add error handling for JSON parsing --- src/main/java/io/cloudchains/app/net/CoinInstance.java | 4 ++-- .../cloudchains/app/net/api/http/client/HTTPClient.java | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/cloudchains/app/net/CoinInstance.java b/src/main/java/io/cloudchains/app/net/CoinInstance.java index 25255e0..5655cf9 100644 --- a/src/main/java/io/cloudchains/app/net/CoinInstance.java +++ b/src/main/java/io/cloudchains/app/net/CoinInstance.java @@ -82,7 +82,7 @@ public String getMessage() { private static final int FORWARD_ADDRESS_COUNT = 0; - private static ArrayList coinInstances = new ArrayList<>(); + private static final List coinInstances = Collections.synchronizedList(new ArrayList<>()); private static CoinInstance activeCurrency; private static CoinTicker activeBlocknetNetwork = null; private static CopyOnWriteArrayList> activeCoinChangedListeners = new CopyOnWriteArrayList<>(); @@ -156,7 +156,7 @@ public static double getRelayFeeByTicker(CoinTicker ticker) { return relayFees.get(ticker).get(); } - public static ArrayList getCoinInstances() { + public static List getCoinInstances() { return coinInstances; } diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java b/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java index 6aa3c71..1962087 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java +++ b/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java @@ -781,7 +781,14 @@ public JsonArray getHistory(CoinTicker coinTicker, int startTime, int endTime, i return null; } - JsonArray json = new Gson().fromJson(res, JsonArray.class); + JsonArray json; + try { + json = new Gson().fromJson(res, JsonArray.class); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "[httpclient] getHistory parsing error - Response: " + res, e); + return null; + } + if (json == null) { LOGGER.log(Level.WARNING, "[httpclient] getHistory " + coinInstance.getTicker() + " null json"); return null; From 960a1304df031c8783d176da5a37dd25a07b47bb Mon Sep 17 00:00:00 2001 From: tryiou Date: Mon, 15 Dec 2025 17:51:52 +0100 Subject: [PATCH 13/73] chore: update version to 0.5.15 --- pom.xml | 2 +- src/main/java/io/cloudchains/app/Version.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 7b7bf39..8fc4f23 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.cloudchains xlite-daemon - 0.5.14 + 0.5.15 jar XLite Daemon diff --git a/src/main/java/io/cloudchains/app/Version.java b/src/main/java/io/cloudchains/app/Version.java index e9821cd..abb910a 100644 --- a/src/main/java/io/cloudchains/app/Version.java +++ b/src/main/java/io/cloudchains/app/Version.java @@ -2,7 +2,7 @@ public class Version { private static final String CLIENT_NAME = "CloudChains"; - private static final String CLIENT_PROTOCOL_VERSION = "0.5.14"; + private static final String CLIENT_PROTOCOL_VERSION = "0.5.15"; public static final String CLIENT_TYPE = "CloudPeer"; public static final String CLIENT_COMMENTS = "SPV"; From 0b5843e0c7d29dbfe8105d3584a4ed44440aeeee Mon Sep 17 00:00:00 2001 From: tryiou Date: Mon, 15 Dec 2025 18:36:46 +0100 Subject: [PATCH 14/73] fix: correct file path separator in GitHub Actions workflow --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4dcf902..cbd5320 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -163,4 +163,4 @@ jobs: name: XLite Daemon ${{ github.ref_name}} prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') || contains(github.ref_name, 'rc') }} generate_release_notes: true - files: target\xlite-daemon-win64.exe \ No newline at end of file + files: target/xlite-daemon-win64.exe \ No newline at end of file From dcfeb0c10c2a37a98f7ef2904bd0d4a2a958a3ac Mon Sep 17 00:00:00 2001 From: tryiou Date: Mon, 15 Dec 2025 18:38:50 +0100 Subject: [PATCH 15/73] chore: remove generate_release_notes from GitHub Actions workflow --- .github/workflows/build.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cbd5320..2c8ef90 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,7 +69,6 @@ jobs: with: name: XLite Daemon ${{ github.ref_name}} prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') || contains(github.ref_name, 'rc') }} - generate_release_notes: true files: | target/xlite-daemon-linux64 @@ -116,7 +115,6 @@ jobs: with: name: XLite Daemon ${{ github.ref_name}} prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') || contains(github.ref_name, 'rc') }} - generate_release_notes: true files: | target/xlite-daemon-osx64 @@ -162,5 +160,4 @@ jobs: with: name: XLite Daemon ${{ github.ref_name}} prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') || contains(github.ref_name, 'rc') }} - generate_release_notes: true files: target/xlite-daemon-win64.exe \ No newline at end of file From 25ee3cf6af870213fe1b7944b19c45982446520b Mon Sep 17 00:00:00 2001 From: tryiou Date: Tue, 16 Dec 2025 18:27:56 +0100 Subject: [PATCH 16/73] build: Add -march=x86-64 flag to native image builds for consistent architecture --- .github/workflows/build.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2c8ef90..e0ea806 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,7 +48,7 @@ jobs: run: chmod +x mvnw - name: Build native image - run: ./mvnw clean package -Pnative -DskipTests + run: ./mvnw clean package -Pnative -DskipTests "-Dnative-image.args=-march=x86-64" - name: Make daemon executable run: chmod +x target/xlite-daemon @@ -94,7 +94,7 @@ jobs: run: chmod +x mvnw - name: Build native image - run: ./mvnw clean package -Pnative -DskipTests + run: ./mvnw clean package -Pnative -DskipTests "-Dnative-image.args=-march=x86-64" - name: Make daemon executable run: chmod +x target/xlite-daemon @@ -143,7 +143,8 @@ jobs: run: choco install -y visualstudio2022-workload-vctools - name: Build native image - run: .\mvnw.cmd clean package -Pnative -DskipTests + run: .\mvnw.cmd clean package -Pnative -DskipTests "-Dnative-image.args=-march=x86-64" + - name: Rename executable run: Rename-Item target\xlite-daemon.exe xlite-daemon-win64.exe From f74a7bbcf21a9902623e89e9aed014bca0286882 Mon Sep 17 00:00:00 2001 From: tryiou Date: Tue, 16 Dec 2025 18:41:04 +0100 Subject: [PATCH 17/73] feat: add native.march property for configurable build architecture --- .github/workflows/build.yml | 6 +++--- pom.xml | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e0ea806..3483b53 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,7 +48,7 @@ jobs: run: chmod +x mvnw - name: Build native image - run: ./mvnw clean package -Pnative -DskipTests "-Dnative-image.args=-march=x86-64" + run: ./mvnw clean package -Pnative -DskipTests -Dnative.march=x86-64 - name: Make daemon executable run: chmod +x target/xlite-daemon @@ -94,7 +94,7 @@ jobs: run: chmod +x mvnw - name: Build native image - run: ./mvnw clean package -Pnative -DskipTests "-Dnative-image.args=-march=x86-64" + run: ./mvnw clean package -Pnative -DskipTests -Dnative.march=x86-64 - name: Make daemon executable run: chmod +x target/xlite-daemon @@ -143,7 +143,7 @@ jobs: run: choco install -y visualstudio2022-workload-vctools - name: Build native image - run: .\mvnw.cmd clean package -Pnative -DskipTests "-Dnative-image.args=-march=x86-64" + run: .\mvnw.cmd clean package -Pnative -DskipTests -Dnative.march=x86-64 - name: Rename executable diff --git a/pom.xml b/pom.xml index 8fc4f23..45f5835 100644 --- a/pom.xml +++ b/pom.xml @@ -20,6 +20,9 @@ 21 21 + + native + 0.14.7 4.2.7.Final @@ -315,7 +318,7 @@ --enable-url-protocols=http,https --enable-native-access=ALL-UNNAMED --strict-image-heap - -march=native + -march=${native.march} -H:+ReportExceptionStackTraces From 6c8861d8e700fe2872c928e8da2bc25c583e2a2b Mon Sep 17 00:00:00 2001 From: tryiou Date: Wed, 17 Dec 2025 22:01:56 +0100 Subject: [PATCH 18/73] chore: fix native build command quoting for Windows CI --- .github/workflows/build.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3483b53..aefba61 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -143,8 +143,7 @@ jobs: run: choco install -y visualstudio2022-workload-vctools - name: Build native image - run: .\mvnw.cmd clean package -Pnative -DskipTests -Dnative.march=x86-64 - + run: .\mvnw.cmd clean package -Pnative -DskipTests "-Dnative.march=x86-64" - name: Rename executable run: Rename-Item target\xlite-daemon.exe xlite-daemon-win64.exe From 9b86a7a34a400985112164bbc62e1709c846ee60 Mon Sep 17 00:00:00 2001 From: tryiou Date: Wed, 17 Dec 2025 22:07:46 +0100 Subject: [PATCH 19/73] chore: optimize Windows SDK and Visual Studio setup for native image build --- .github/workflows/build.yml | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index aefba61..127295b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -136,11 +136,20 @@ jobs: java -version native-image --version - - name: Install Windows SDK - run: choco install -y windows-sdk-10.0 + - name: Setup Windows SDK (pre-installed) + run: | + echo "Windows SDK already available on windows-2022" + echo "SDKROOT=C:\Program Files (x86)\Windows Kits\10" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + - name: Install Visual Studio Build Tools (minimal) + run: | + choco install -y visualstudio2022buildtools --installargs "--add Microsoft.VisualStudio.Workload.VCTools --quiet --wait" + + # - name: Install Windows SDK + # run: choco install -y windows-sdk-10.0 - - name: Install Visual Studio Build Tools - run: choco install -y visualstudio2022-workload-vctools + # - name: Install Visual Studio Build Tools + # run: choco install -y visualstudio2022-workload-vctools - name: Build native image run: .\mvnw.cmd clean package -Pnative -DskipTests "-Dnative.march=x86-64" From abe8bbed93e87bc3f82caa4527f8a28d86e9bf6a Mon Sep 17 00:00:00 2001 From: tryiou Date: Wed, 17 Dec 2025 22:15:36 +0100 Subject: [PATCH 20/73] chore: update Windows build configuration for native image --- .github/workflows/build.yml | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 127295b..5a9eeb4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -136,21 +136,10 @@ jobs: java -version native-image --version - - name: Setup Windows SDK (pre-installed) - run: | - echo "Windows SDK already available on windows-2022" - echo "SDKROOT=C:\Program Files (x86)\Windows Kits\10" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - - - name: Install Visual Studio Build Tools (minimal) + - name: Install Visual Studio Build Tools run: | choco install -y visualstudio2022buildtools --installargs "--add Microsoft.VisualStudio.Workload.VCTools --quiet --wait" - # - name: Install Windows SDK - # run: choco install -y windows-sdk-10.0 - - # - name: Install Visual Studio Build Tools - # run: choco install -y visualstudio2022-workload-vctools - - name: Build native image run: .\mvnw.cmd clean package -Pnative -DskipTests "-Dnative.march=x86-64" From 17e0dc7cc4795a309d7ac0ec6b001b2295a39dd2 Mon Sep 17 00:00:00 2001 From: tryiou Date: Mon, 30 Mar 2026 22:22:51 +0200 Subject: [PATCH 21/73] refactor(coin): process only enabled coins and skip RPC-disabled instances - Filter coin initialization to only enabled coins in ConsoleMenu - Check RPC enabled status before returning CoinInstance - Skip address discovery for coins with RPC disabled - Update HTTPClient to iterate over active coin instances only - Update tests to use BLOCKNET coin ticker --- .../cloudchains/app/console/ConsoleMenu.java | 8 +- .../io/cloudchains/app/net/CoinInstance.java | 27 +- .../app/net/api/http/client/HTTPClient.java | 14 +- src/test/java/CoinInstanceTest.java | 8 +- src/test/resources/test_config.json | 2002 ++++++++--------- 5 files changed, 1037 insertions(+), 1022 deletions(-) diff --git a/src/main/java/io/cloudchains/app/console/ConsoleMenu.java b/src/main/java/io/cloudchains/app/console/ConsoleMenu.java index d37ea18..bc1297f 100644 --- a/src/main/java/io/cloudchains/app/console/ConsoleMenu.java +++ b/src/main/java/io/cloudchains/app/console/ConsoleMenu.java @@ -23,6 +23,7 @@ import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; +import java.util.stream.Collectors; public class ConsoleMenu { private final static LogManager LOGMANAGER = LogManager.getLogManager(); @@ -377,8 +378,13 @@ private void initializeCoinsConcurrently(List coinTickers, String en ExecutorService executor = Executors.newFixedThreadPool(threadCount); try { + // Filter to only enabled coins before initialization + List enabledCoins = coinTickers.stream() + .filter(ticker -> ticker == CoinTicker.BLOCKNET || CoinInstance.getInstance(ticker) != null) + .collect(Collectors.toList()); + // Create CompletableFuture for each coin initialization - CompletableFuture[] futures = coinTickers.stream() + CompletableFuture[] futures = enabledCoins.stream() .map(coinTicker -> CompletableFuture.runAsync(() -> { try { LOGGER.log(Level.FINE, "[coin] Initializing " + CoinTickerUtils.tickerToString(coinTicker) + " concurrently"); diff --git a/src/main/java/io/cloudchains/app/net/CoinInstance.java b/src/main/java/io/cloudchains/app/net/CoinInstance.java index 5655cf9..058deea 100644 --- a/src/main/java/io/cloudchains/app/net/CoinInstance.java +++ b/src/main/java/io/cloudchains/app/net/CoinInstance.java @@ -239,16 +239,14 @@ public static CoinTicker getActiveBlocknetNetwork() { } public static CoinInstance getInstance(CoinTicker ticker) { - CoinInstance instance = getInstanceByTicker(ticker); - if (getActiveBlocknetNetwork() != null && (ticker == CoinTicker.BLOCKNET || ticker == CoinTicker.BLOCKNET_TESTNET5)) { - return getInstanceByTicker(activeBlocknetNetwork); + if (ticker != CoinTicker.BLOCKNET) { + ConfigHelper cfg = new ConfigHelper(CoinTickerUtils.tickerToString(ticker)); + if (!cfg.isRpcEnabled()) { + return null; + } } - if (ticker == CoinTicker.BLOCKNET || ticker == CoinTicker.BLOCKNET_TESTNET5) { - activeBlocknetNetwork = ticker; - LOGGER.log(Level.FINER, "[coin] Initialized active Blocknet network: " + ticker.toString()); - LOGGER.log(Level.FINER, "[coin] All subsequent calls to this function requesting a Blocknet network will return the above regardless of testnet or mainnet status."); - } + CoinInstance instance = getInstanceByTicker(ticker); if (instance == null) { instance = new CoinInstance(ticker); @@ -258,6 +256,14 @@ public static CoinInstance getInstance(CoinTicker ticker) { coinInstances.add(instance); } + if (ticker == CoinTicker.BLOCKNET || ticker == CoinTicker.BLOCKNET_TESTNET5) { + activeBlocknetNetwork = ticker; + } + + if (getActiveBlocknetNetwork() != null && (ticker == CoinTicker.BLOCKNET || ticker == CoinTicker.BLOCKNET_TESTNET5)) { + return getInstanceByTicker(activeBlocknetNetwork); + } + return instance; } @@ -1028,6 +1034,11 @@ public void resetUpdateFailures() { public void runAddressDiscovery() { String currency = CoinTickerUtils.tickerToString(this.getTicker()); + if (!configHelper.isRpcEnabled()) { + LOGGER.log(Level.FINE, "[coin-" + currency + "] RPC disabled, skipping address discovery"); + return; + } + if (discoveryService == null) { discoveryService = new AddressDiscoveryService(this); LOGGER.log(Level.FINER, "[coin-" + currency + "] AddressDiscoveryService created"); diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java b/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java index 1962087..98d6ba7 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java +++ b/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java @@ -562,9 +562,8 @@ public void getAllFees() { JsonObject result = new Gson().fromJson(res, JsonObject.class); JsonObject fees = result.get("result").getAsJsonObject(); - for (CoinTicker coinTicker : CoinTicker.coins()) { - CoinInstance coinInstance = CoinInstance.getInstance(coinTicker); - String ticker = CoinTickerUtils.tickerToString(coinTicker); + for (CoinInstance coinInstance : CoinInstance.getCoinInstances()) { + String ticker = CoinTickerUtils.tickerToString(coinInstance.getTicker()); if (!fees.keySet().contains(ticker) || fees.get(ticker).isJsonNull()) { coinInstance.incrementUpdateFailures(); @@ -573,7 +572,7 @@ public void getAllFees() { double fee = fees.get(ticker).getAsDouble(); - coinInstance.addRelayFee(coinTicker, fee); + coinInstance.addRelayFee(coinInstance.getTicker(), fee); if (logCount % HttpClientConfig.LOG_COUNT_MODULO == 0) LOGGER.log(Level.INFO, "[httpclient] Got relayfee for currency " + ticker + " - " + fee); @@ -653,9 +652,8 @@ public void getAllBlockCounts() { JsonObject result = new Gson().fromJson(res, JsonObject.class); JsonObject blockCounts = result.get("result").getAsJsonObject(); - for (CoinTicker coinTicker : CoinTicker.coins()) { - CoinInstance coinInstance = CoinInstance.getInstance(coinTicker); - String ticker = CoinTickerUtils.tickerToString(coinTicker); + for (CoinInstance coinInstance : CoinInstance.getCoinInstances()) { + String ticker = CoinTickerUtils.tickerToString(coinInstance.getTicker()); if (!blockCounts.keySet().contains(ticker) || blockCounts.get(ticker).isJsonNull()) { coinInstance.incrementUpdateFailures(); @@ -664,7 +662,7 @@ public void getAllBlockCounts() { int blockCount = blockCounts.get(ticker).getAsInt(); - coinInstance.addBlockCount(coinTicker, blockCount); + coinInstance.addBlockCount(coinInstance.getTicker(), blockCount); coinInstance.resetUpdateFailures(); LOGGER.log(Level.FINER, "[httpclient] Got blockcount for currency " + ticker + " - " + blockCount); diff --git a/src/test/java/CoinInstanceTest.java b/src/test/java/CoinInstanceTest.java index 0e4b3fd..1fd4433 100644 --- a/src/test/java/CoinInstanceTest.java +++ b/src/test/java/CoinInstanceTest.java @@ -20,7 +20,7 @@ class CoinInstanceTest extends TestHelper { @Test void deterministicAddresses_fromMnemonic() { for (int runCount = 0; runCount < 10; runCount++) { - CoinInstance coin = CoinInstance.getInstance(CoinTicker.LITECOIN); + CoinInstance coin = CoinInstance.getInstance(CoinTicker.BLOCKNET); assertNotNull(coin); coin.getConfigHelper().setAddressCount(getAddressCount()); assertNull(coin.init(LoginUtils.loginToEntropy(getPassword()), getMnemonic(), false)); @@ -47,7 +47,7 @@ void deterministicAddresses_fromMnemonic() { @Test void deterministicAddresses_generateAddress() { for (int runCount = 0; runCount < 10; runCount++) { - CoinInstance coin = CoinInstance.getInstance(CoinTicker.LITECOIN); + CoinInstance coin = CoinInstance.getInstance(CoinTicker.BLOCKNET); assertNotNull(coin); coin.getConfigHelper().setAddressCount(getAddressCountInitial()); assertNull(coin.init(LoginUtils.loginToEntropy(getPassword()), getMnemonic(), false)); @@ -78,7 +78,7 @@ void deterministicAddresses_generateAddress() { @Test void deterministicAddresses_generateForwardAddresses() { for (int runCount = 0; runCount < 10; runCount++) { - CoinInstance coin = CoinInstance.getInstance(CoinTicker.LITECOIN); + CoinInstance coin = CoinInstance.getInstance(CoinTicker.BLOCKNET); assertNotNull(coin); coin.getConfigHelper().setAddressCount(getAddressCountInitial()); assertNull(coin.init(LoginUtils.loginToEntropy(getPassword()), getMnemonic(), false)); @@ -107,7 +107,7 @@ void deterministicAddresses_generateForwardAddresses() { @Test void deterministicAddresses_generateForwardAddressesReloadConfig() { - CoinInstance coin = CoinInstance.getInstance(CoinTicker.LITECOIN); + CoinInstance coin = CoinInstance.getInstance(CoinTicker.BLOCKNET); assertNotNull(coin); coin.getConfigHelper().setAddressCount(getAddressCountInitial()); assertNull(coin.init(LoginUtils.loginToEntropy(getPassword()), getMnemonic(), false)); diff --git a/src/test/resources/test_config.json b/src/test/resources/test_config.json index 66902e9..2520e9a 100644 --- a/src/test/resources/test_config.json +++ b/src/test/resources/test_config.json @@ -3,1009 +3,1009 @@ "password": "Test^1234", "mnemonic": "one two three cake neutral benefit quick hip level mother fine burst", "address_count_initial": 20, - "coin_ticker": "LITECOIN", + "coin_ticker": "BLOCKNET", "address_count": 1000 }, "expected_addresses": [ - "LNTpgLXprtecSEzztNDmGJpfDr7noC65db", - "LRFcxeV3AQ2zvuFtoiTyCYfSjUQLkEddAU", - "Lc14g9TT4yAhAfHqLaKanr8pgCPBVBksgM", - "LM7Jf8MSp8CB7nt2zYoSZuFeqiBZwn6TPi", - "LUdCaBTEFQjVaZeRafinzaUxWXHZiZcvCG", - "LMUjorgJBdhqdy4fdUHAkm6UryJXLtzaWz", - "LRs2qDU825ca8RurD5yysMEgors3YA9hPe", - "LegeUDssVAq5Z7Y8cx2b8dWHnb1DunMZzN", - "LhNHWWFwSG61PY9duCPN98XDsR8jgiQJRF", - "LPw2hdP6ZvkzWTGn8cJHjinPYCrPa6ZpfB", - "LecbTbQ72258tMyoKFvAPhTJfhdzQwVPFR", - "LPwdqFTxhEZAA5ZUpBmydVPL7NRVHRQeiH", - "LR26gQX2Z5ays6tvgbHUeP2CCUTbpEfufG", - "LZXyUvGc2N23iHaDWWkV7drnvbHEE9LBrZ", - "LVSWjf6A7CRnHRFV1YAGKVcqNunfvmgmE3", - "LaZVKJN1DK2XiujjenaJ2jGtu2UKKBur9f", - "Lht2wBTNf3z6pYN8WWRAZw5aMFWGdMfRwo", - "LMdhg8w5QaTpSQzt9kBmzyQBTKp9vgPrCb", - "LNRZqXKFZoUUBTF2RzumfHNAUcxLXahWTo", - "LKqcHm1EkmggfnDMsMY6mrTx7f8PNMkk1z", - "LUnRSgWNfabKFmDTC6EdGWCNRJmRApQ5wH", - "LdCULqXSmA4dR4hKrCYNd13Zpbb83kjbzS", - "LMFF7wwrdwmz4kxsvyF57u5HBs253DDBmT", - "LQryk76nxEUTipXPZMa9Q6h2sgSKe4Xmz8", - "LULo7XAFgpickNw9ZCtz99u9BTdjmiSFh3", - "LUkdgnTnPJ3JYaDKZz6h6DdGQExentjcG7", - "LfGf5o14NUGuQrVSEHTRNFHqMndZUacpnv", - "LdRfkuPH6SppfU3L7o9PQptmw92Yw4JfYb", - "LXrBKZoGpkuk5e2vZ7RMyDqvzNvjuHxcwu", - "LQNHBjKgpMdfJSnDFNUjF34V9xiAFkgWGA", - "LTjbuCtAwc33m61teukpnPyFmkvLi361oH", - "LYAdmeqiW9zYuuQKDrnJff14J8sUEGZYbU", - "LbReUXLecNu4PXoLfhJqqn8KCE4sLoKBWp", - "LNfwCeec9ux2cNLiu69r1r7omiyEGqqq7z", - "LaQZF3arAKViEhhQR2fbAdpxkpaodsYLxR", - "LezikXzZAHDhJMYAFpRbJYipWx2b5yM86r", - "LZSKM3miJU3wVtsav2PJeyywRXjU32Qfdd", - "LfQbiA2wN96n8mUq6S8jtGVFgUA5JZq4P3", - "LghnhiepXMm1ukyYUsAoQurd6fCv2Pdi9U", - "LNAmNuCMv34C42gsGw1okHXeAx7uxeDQN8", - "LPngfnTyCGWbNwRLo3LXZBrqeN2ktLFTPz", - "LSPnz6cppLDwSGPPA2wi4Xo7Fd6GfazYE7", - "LNu4gkrRYhcWuAtiiiMDrSee47qgQ8L7Q3", - "LP92pv1N8tJzRuc1D3Mqd5HqL9gtitu8Gf", - "LPtgpYRpBHKj55zyVqceBL25pLbkt3XV1H", - "LdstQc2vRERBUuferrsHutHrSyXc8Eduu3", - "Lh1mGh9F4Dcabyw4KLFibKzDA8YmdVGFLw", - "LWQ5ibuQdFvqcvfhrR343Sf5cB3HewwPJP", - "LgaXbedKCRRMLRFXtr6gxzqqGC3rswxi9p", - "LLqKb2SDpA9a3R4uryWLgzC2xYxs9sKPM1", - "LLWEBquiHFV8LGivZ3oVwm5b8arwedMNKz", - "LKHnkgsYrdr6QkASAhYp25GmS8RfF5Ynga", - "LUmahz5XqhGzXEfQ5G8re9sGcQojF7E2gH", - "LTZYrdv6HXRzLDBkqcvZT6CvbGFJ4eFchm", - "LVP6iAtFq2JLCE22ELWmRhRPSM4Zzr3RHs", - "LhRLJzGega9paT1QZoSTvNWfpN2jpu6qzc", - "LdAy5N1wVcrYngqV6UuTKWm4xc61SzLzJ3", - "LafonyLZFoj1teorrjrD6MKyoJbKBBN81R", - "LRgK3ttMmJGAXST8c5w4f9H3hAuHiMgCCk", - "LZDwAdTf3aMf3qYNQAuCkA8B3BiaANcixi", - "LM4e4QiHavGFhkguqVcQee5N5exmQZNFFr", - "LNQ97UjwKtEFsc4P9xEDwh8obNg15V4rcB", - "LXQkMaN7ehoK7KQuPYrDms94YYaUkyhAnR", - "LNFgvrFuVP4SXYmfhBHZ4J5GRzDHrrNhYu", - "Li9ARsaUWwjZXnTQRbTaz8WGQxpiwgDPLC", - "Lg96QoS75svudKyjRW3CW4cw32rBvutZbX", - "LYQiqNPKGHHMTV9qnjVv9WWtUhwnWvTQ6i", - "LWGYANGR7RQyiDmktgdrgz7RGFBCShxtd5", - "LZAiCMeZLzy1QGkwwoT6UuKPkvMqXHd8gz", - "LQ82FZ8GLj6bVavFinqFxvLNbjcrvPCxhS", - "LcofKwE78JwmMrBGvBvUcSmT5YmL5UcSRY", - "LTP7BUs5aBX7C4AHGrVAtLiqQpajmvUvqy", - "LgEsVHXPKQja43j1Rd3xE1peVjExpH2qdr", - "LTBXqH8AN41QbmMXPxotXHMSiqhZ2SGvce", - "LUqaaPcFpqZPhYbUs7uvRj5FVz6NEYiEwm", - "LSwu69aWjEzp6ALhyNWkwMqMyRX8d51dAw", - "LcfMfN1SaMGc4qfSR3Z4bYu9nyKE4ivpEk", - "LZjCL8xUKAuy83uvqPcnkMwLxRB2D1TYHU", - "LXzJfLdZ222RAwNeic7pgrNSkwPxkjVdUP", - "Lej7drV2kA6jvRkiVRrxDZvTfX1zyJAMVt", - "LdQoBbvGhHj5ySGRgVF6RKNUNE53LveNdc", - "LMWLPeJXYZ4VPWB8xpQ56gNSVDCgu6Qa11", - "Lhd7oV52q4AHGnNBXfxRpKU3av7aKN37F6", - "LLZutN6a2NYtLeosEqsKMd9N7Ca1pzAeTS", - "LWguMXkcw5rNRhJ8G2N7nyQzRivYEWKACy", - "LeoaenVAPAkD68xTBqvX9TeesyFMGYeWGG", - "LWr1sDHbDLzyKLkdfsLmRLCVctBAcejZdy", - "LcV3JXkPvF6LUaUi4CsjC7zQm4rKF2J3xx", - "LWTf3PEfQVTfKcHrw3NXgpRi6jxtG9P9mC", - "LZTm2LsVpqFJoiDxKTDamWtK75FjuG8sJS", - "LbdNDivoeGXPcTfq2g5fEEK4zK2m4cp5rc", - "LRT8aPfvk5yqsuAs2X5yza9SQ6GtbENUNo", - "Lh2ZRdHMt5cm2aVUD99o7diX99S4GtbkyK", - "LTTjcuMfedGAuJHL4W8uqxCZmbSANnH42x", - "LXX8N9GBnzeDzTQeZWCEm2xRrmqsJgb3bk", - "LMYYVufrxdQpMqiWdinyVYfmTzKha8suA7", - "LPWUpTu23Gjdjaeg23UmDLmyaeBthZQUbd", - "LRHquUoS73mG5Uj3VtQaVgNmtj77a78MD4", - "LVLmwgtkC2pnuptJy46JZmDJbw414DYYE4", - "LKR4yoQFtFGDn6L4Fb79G4bjaPNrnz1Xq7", - "LW5bvYkYF7vS5tZMBwbo9AwMTir6ywGR3S", - "LZXQ6RRPRGXmSeFagG1EQxf19LsESTHKeN", - "LL41uzfhWeeHZmRpbn2XuhDx9z4tdYBy8G", - "LL57dQUequqnXJCYBT8DFnVt9k188pnQb9", - "LV3AVsx39xauq8t77iN2xDhCmpLf5cmKAj", - "LUoyQcyqFtcmEaEm77fNhWFoDQW3NZmRQW", - "LParh4xAPDCNTUfQaWpb9FTiqin1TY29F7", - "LQBYnEUuw4Wyov5Snv3cbgcpxgoMASz6MT", - "LP1SimqRa9nJgDa1pQ5X2BUUgFbwhSr27T", - "LhnooDJzpsiBd9gBDavqLExWk7HCmPdfWk", - "LZUbTRvrcBbJTehG1yL9EhoHYFqBX9GY6u", - "Ldafj4ehAHY8khB1zhCqXeoxtfCqbX3xhN", - "Lbh9xiQGaNvC1mj3J5RQcrcQgMxCsLPui7", - "LbccvJV7BKYLJZTGBeJLNPjnPz7BKJnqzi", - "LbxaR312y3RkAi2dnxneA42mN9Zf7Y4qmL", - "LNJ5kQJL85HPUmAfuqV23k9jVbrYsqFqTV", - "LMgSwsYzXTTPQBh7jNkY8b5J8kS1RGY3bN", - "LNGsETiVjZTiewDwSx6MBjtQJ6cjxsv5kP", - "LdYux77EPviPYsgntTLSoh4EEcYmyGWtnQ", - "LXjaNYexsVg2cFRxb8JqatMGHMhwW5wtXX", - "LRQ7wHfHYuLzXQH5sQZXfc7ZmLUi9NLTxw", - "Ld8osEsH6XiZrJGtL8hcpNRrurqBvpf1Pr", - "Ld4HAEWDqBt8zn6xnppqUfWiW7jqs8qs8y", - "Le326jYzh2HFkKska2SriSGUJB2Wg5MxQh", - "LeHUeSb2gNDA1zzwJT4ZHwmMZRWWSeX7dN", - "LhrDhcG6qKy87wFxyC9pEVkFa2TxUKgVQB", - "LYK79wmjy3t2uP96N6GPbDMJcwM8hCJsX1", - "LY9Xow2RgxC2zfcawmPhk77zE32b4sgZG6", - "LfCG9qkGpAY5Wj7w5cNforfxTs7R8MgWTc", - "LR7FyfFoYoniwGarnsCXmN437i517QJL98", - "Lgwx6Fj5gsEZBpZHdUzoAfDa6UmTjb7sPr", - "LQMvS5eMYSFDa6yxj8hr7m4aPPiCPEVae2", - "LeMGFqhYNMfcx4HDBy1NSCWzLW7vAy3oJ1", - "Lce8zFao77eQUiD2bLJLieRN1gy3iXgR1U", - "LQ5F51XZVHFMr6sDoZL5YUSzaV4WLnEQHo", - "LKXBnp6sG9vqz2fQTQGsbcgfkTwHu7tvAT", - "LToTM5T9yY2RwwPSw6Si6a1g1juK5SwRCh", - "LeqPGow6kzcdxdUbeUL4AUTwnTaWLGfqwP", - "LTo82EctUtn6BoGab66tEttayxpzjusHff", - "LSRUCGARpgcS3Tnc7QWnqkCEkD43z5Fezd", - "Lgh9WpzeBnr4fCzuKY3VFe1Pejh5mYGvQH", - "LWgUo4CsXEr9CDUraginpK8jLteb4Cq5hq", - "LgKS4NmCKBUp54oxEUfi849hB3mTG1vi4P", - "LKnpyE5t1T7EURWxaL1q1rawe51MTq1ajQ", - "LT8fnKfEjfenPfLdKjKNScRFHmdxjXitEa", - "LPjpeXCf3WPhjpYYw2sZCSjNRfv5bHMTBU", - "LVkFzCU3rPLUmPjjjXi2sUtzxPsb1LKB4m", - "LcF4dwsXTdHjngZFq5A3AYnCfJhBvSJ62c", - "LdyVKgdUJg947nD2ig6awLwyAenmMAZycw", - "LKWNJcTNGJqvdFrRLGZgEuvvH4iRcsytKG", - "LX4Rn14HUtFPLtvQvV6hP48gQ8NpCavmJ3", - "LeJPuEgUH5MzUVcDGwwALYa9X2d6h8m1pY", - "LgRTrcgMPzLN1bojHMN7njFnBNPuDS5zjE", - "LWzr1uppdUef4gvJ1QE8jd1Sa2DZUfaEVc", - "LWKuK8anQcGLNV9nrnjduXbYvDRF4Fj3LZ", - "LdsJj1cq4QHg7pVcmbKzbozmiJxiYKuZfv", - "LRnN2DzvhdUCVzQCzCCwMgDspQCoeojzwb", - "LM2fxgnQTCnmyggy7KKhBXbJQStbVo5HeC", - "LNpNCPsNruz6fBUVkRFJTyh2kp2RaicfhM", - "LKnmkxPkDNynjvKzZi2mdwhy3LGs5dzry9", - "LRLz8rLbdog6wYJwka3CVKAcXFzxu9JxEA", - "LbnftHPQ9jP3r1gmNPdDoTsDUp7xfKBtbV", - "LSVsppscBXo4j3wCtikHP1UBeg3tQXxJc9", - "LiW9yYR7CvFxZs9YpTx2JgFtk3yYqegjDE", - "LXcUo1ko1jC2ycX5nvdZfsd2eyJSjws2W8", - "LfnmCHZ3ySVy1aaDwjzA1wcrqK9SoyxsnR", - "LTRwKGCtV3Q72WL9wRoNkAPxuVW8TN7jni", - "LPfhiBxWEmnk4o7qoBE32UXWkuxsJmTsw4", - "LPLnj2KYK36mnHhJAV67UqtPhhyEu87zbm", - "LbSvp2mP4EQpfmQQMZDWe48SyGhyDmKozs", - "LeBGoKBvaVfWneme3bmtCyrYi36QqaEQkB", - "LcNWHL4PyUnrQC1YHUjWPa9bi6mdxpysGw", - "LT8Wd1Yc7JSvkuWXP3Qg14vnzdKBc16WFt", - "LSRgVmauy3UuGr49cQ1Ud6nKG9W9VqrfqW", - "LR9RVacNWgConsYLYnSCD64xzhxapAzys7", - "LfBcxk9WpfagYY9oVnomeNQJGCijNPqWBb", - "LLtU7JvNQ5LhmfbwMazNjDzQ9wB5SrmaXs", - "LPpvKNJXvykhtZCFeCTfCJGkAYiN8jZYBh", - "LbXYCzLXBRDxuwv7Kb8VBf8YjRdJ8Zuc8b", - "LTjQkPWB4NgwXPiZqGT84gjj1UYGjqRbPG", - "LLp6MjHZKdjWGKQe5WGc47dZ4XM5qxn2mA", - "LNh4EK3hND8PKDdFPuyzEVWtr56sb7FAuB", - "Lcgn675CV2xcPKPjYvqLcvKwV86utNkTAu", - "LTw1ETkVwmGpb5HCkmJvERJEsWNdZEPRHm", - "LTK7rNjWEqdAAALKyGB9nZwQTaeknyUXNa", - "Lc8Mw6ErwuEdHVGqLNFMo5NVGcKoHnZPCE", - "LSffxhP4C8SQMexuzgChTmNRfvyLqwncbf", - "LhnPf2VirEeYCrKUbMSiFzkPLJ2fTmNtzm", - "LTTxY187qCEh4dibgc3onwrqyH7FDSzKBX", - "LPkQHme1tmM7tqLsRR1soHUKGgVaLCgRfj", - "LZBxTJ6sZLmTrEDAADrpEMGoanPeWvvRgi", - "LfaPNikFG8AzzpjKTEJMkKQnjPf28rWjMP", - "LNpmRArz6b9ocUsaiGanbnThWxK5vFZxjA", - "LZLBUYZBG9XYGCzSzkW7CEgqnu47AQ83uu", - "LUnNY5vj3xK61o8mJJiKXfLGTq6M25FB9r", - "LcfVXgTxv7Y7F8EFqtyQ37gzyGSDHs2dke", - "LRPcZSWR949S7v1UtsjFXVjjFp1MNx6no9", - "LQRTtbZ7fyNbLifp2VnMjkPAmuXSK65jUP", - "LKKz9XTUkwrwLfA3CkojuHdQbR86addQq3", - "LYUTtt97x26oWJdwFLVFProQVETX9Gv2Af", - "LSFotdspBDfvK5Wf6D2D7nDPPJWbADPxbf", - "LhN96GZLYo1aqM4tdDqTZWFYV5sMxbJ8Ud", - "Ld1VywVYxotJCJnYiJXYcX7vRBCoDKDHKh", - "LQQodxx3oDCTnWTaFkAWUAVu2Z9S7MmCdL", - "LQp6Ajv8Y221izvDSGNjgeykJbckVgxxLN", - "LcGUwRaXCfLiKM22f2A8FzxZSRrCy6vhto", - "LbM44fAzbpuFk4U84S1KxfwAtUERwUQs2U", - "LPiRebPFnteKKFr8RtGDFCzkcFiaxcXMAS", - "LgaBoSmfv8LLKdovvDs2LpMQqDSL4S52v1", - "LcGrF1xqc2cDWfxFw6CqRRQwKzuuXzEnSr", - "LLvufEqjFyDSvG5w2F1fcTnrWAyTXAoBsv", - "LfHjA4ViNZt6NJWtSa5w41H1wuG37gHrr7", - "LKii7xfkG9uPimmwiUojzPPFZDQcbVBfwR", - "LNsWWFDfuG2mi5x5Vh1ypKR2cCfsrKKtdc", - "LWFgLzQBmwiG3KJhXSWECM6oegE5UcRVLd", - "Lfijr38CTnD5FMioHpwSpreCWGcVreLf1s", - "LQDzYkTfNNkkmETTKEcsfBBLgDio7Ch2RS", - "LZWdM1JivYM1gpgBAXugJcqcvfhr2DFPAg", - "LfwPuqWsvqaTAe4VGh9AHFu2f86mcHQ7C9", - "LRwVUNWkhEg4DZNXPboWVMgPam7s67HYk7", - "LR5juRP4QfNcKtswkPU2z63S1VeUHUSTfZ", - "LPaTHZs8311nZggDGFpbLofyyDU9zGfnxh", - "LiXwEJzji9eFwg6Qa2HbAW19DPrQNwNQ8v", - "LNhFfFqYnHEKHdYA1cW7gh2w2qVaRQ3Aga", - "LPWL9SK18JutKwbJBNGLNoiuounbkVgr86", - "LSatLFBU4SMD9nv9R4ageDVRDUTtQQhk8L", - "LRMLqp42TfwHgxpLs8JbJfWJqfs3ZdcGbv", - "LP1enWqWhswMvPQL5HwLKJv3S33oHYgvje", - "LZ25qiaYmv7DtKe2NW6eMsVgogHw76aS3j", - "LaEedjPMoeY2KiQrSqkmotUeEPivEub7xY", - "LTb1UmEfERgmQofhkswsKEBH72qPCUhZae", - "LgTVzwEmuMzDBpHWryXFiHLPzqbMbkbfUb", - "Ld98vrb3Fr566YeqiP8aeJveELmqZaVcuR", - "Le9Db1J8ycebrWFQPafgtxo85NFroj6bkr", - "LPgtvGYe2r431Vor4NtTuCsuNEu5zdbyKr", - "LYd1ypLRtEikKWKjiaP6t9doBfrBJ2LXt1", - "LZ4NFJ9Wo6BXYfX1Fu2hc9B928youeR6Fi", - "LNv6tiwWTJneFnCY9DvLSS5gDi8w45Ebcm", - "LcVNwNatde6YvzToAz2aWFQhDZWe7KYTQ5", - "LMdzgk8G16GdbANbJ2V1scmjirRuduASZj", - "LiAQUYEWK9LrWo3EVVvArJjWroB1p6QW1w", - "LhU8hfcGUAVbQtVWAbzQP6QgnSAb46sdez", - "LdQizQZSqUy9qbjFjbutwC2Ykm6HT1aUVB", - "LN3VXb81XUN8GRi88xiAjxgRK7jmpHSh2T", - "LLojnet7SDEyE6r5ypEbbMoxj7hQHwWtU5", - "LV2ewWVcTXHuoZbQueEnjX1JmARVoFzax5", - "LazBTecQrBBWVoAXGudW74swL8U8qyD8Nr", - "LPtNcbAbWdaxUpVJdwYLZfkxi6wBFAmMit", - "LhRcaWetasKhDAYq2XbqeKcCapGMEwv8zo", - "LNSmDGKbZMZEniXeYuXCYYeYYMkywWabcg", - "LXWS8N8XxExRu1kgj1Pk8DLx9Dvxz8Q2fj", - "LexYfRVCPaKKVGFLstMTQqLDpckeigssqK", - "Ldms5eGY8zJS6FWJxv2EVXpqRi8py4T7NP", - "LaoL88U7Ry6Vu2SAXmCJsEK5vC756nXg97", - "LRgX8PJRoNWq47NejvP9JCcWLTy3GDhGtq", - "LXPSjGUe2xWpy9V18g14JPUxrbYa4zC6gk", - "LVd67tYst7wEaygdbyTrCNnkUCxGwvHMQT", - "LQRjVWXQN7yNLEWYCRCGqD8DZ7ZAnwbGFK", - "LfCwTPmYxcg6ptZt1XsZpXVLD9erm8icfZ", - "LSSeb3WyehJtkN5pC54p9WoZ6pZCqyxHc8", - "LWHGWCrcvRRDWkUz1XRxvgyxYv5XaBY9fx", - "LfnKt6eJ6nEvRVLoMEgy9D7fad2tqiMB55", - "LVoRMs8mfdNUkx3U9rWhHah3fc5jGs32W5", - "LUqSfbzNGBbStRdC3sQZ7MPeHkxbcpdu37", - "LSJhaNKiAYiLG3LfQFQqyRvb7aeJguRzbK", - "LTnJZT9Jz2fiyYCpnoV3wakoxqAeW8rHgN", - "LPDtzuk4TrFXaTn5fD5eyhiVernTJQVcwC", - "LbhFP1AZQ3p2Dg9upDtMaM6L6Y4AP9eViY", - "LfN2hVwHCAcSoanTSmZHPZpTenL6NSxn5b", - "LN41HmbM8GZnjcSfhUWohNL5JHzYc6Q4yS", - "LWCRTvjLsBPpwxHETBYmBfA6xT4YcSgot1", - "LWX9PVUBK7TtSGwNPb5Dg3pLCR9NQZuUtp", - "LbV1MJ8C16snbn1J5cWbcuAPXbojVBxHEu", - "LdwyhS1mXxcwwL5XBpVYQ5nmGhZKXBZxB4", - "LcMGYAjuRVVv4NeQgeyAQxNygtbTa4VKAR", - "Lhk6FBbTkBBAqTN3WMhgNwFUSxxt4wQdFG", - "LZaPP8QwrKMinoqVa8nuVHBARUFJutrmaK", - "LaviUsiNSthtxZqkshztwzgJFSoeRwB7SM", - "LQSCpivkXubZ6WSJ4Sj2FYQREzNYE7eHAa", - "LbjGdRMLBN6ZzZzCEmuSMREm2ppqXxXxG9", - "LXhdBAZRqndks8H86sauQHjdMxfauYh4qP", - "LKULCTZmekgyPJNKKr28rYRhMCuYm2o2r1", - "LhRHZGQND8pLJkYSNd2SxYRQpo6zNb8c7J", - "LS1axSTidtddZ2MqJtBMbTqQ4WUfAXoLfv", - "LPisDsmDks7AifRSbohsZRFwzC42wKvAfb", - "Lf5Hu9DUCFzWgBZxnNVYTA4vRvQv5RsC8Y", - "Lf5mNA8j574KiToy93y6vyJNNujgMB8D5h", - "LfwAVBi7rM9EqWgYVuxgy6s2oruf7rFehH", - "LS4NFvwhLqUZCVUvbsTLxmUciii2c4TXEa", - "LZSXn1cyh6uVGkZfQ3knsYYJUscQ1bH6ct", - "Lh5RVPHaS3XESVeciCQWU7fX88DLXJYohN", - "LeD9p9Z4cs6djuMY8eViFuAEUwZ23SAm9C", - "LMgXv9fa8LaYhT3J5EAvmhAL69SuSQVYj7", - "LKmjzHfHJjy7w6dPQPRbHKUrR4DmJmfVAo", - "LaLBXX5RPuBdtc9PmgYj94xnxSgafXqxLj", - "LXspfsj2Aj3JifsvieKJY855FqbjarcRLS", - "LKMfrpYj2Jpwf6GbcWKVLVXQr9y9Gfgxpj", - "LdW2144tJQy9Wia9WpRQfHWYuH97KPetpc", - "LbrsYcvTG9xPRdhB7EPzGrDBVMBSJVtz4a", - "LcxvDAqY44V21NQVU3nDNaRZbf4j49zg4P", - "LcvQDwfh9ERb1wQxLcbBicyaJ133mBDJJw", - "LZYVtkTRrJzpcx5LY5cY2QgXZsrqcmPHGv", - "LRjkE1xGxLGTYd5PuBCh6JdLbQeBWjzCXn", - "LVce5gDSjAYN8BBDYrYNAeJJj2gYqs4gu7", - "LYQKe5mWg3QWBvngMBD5YCjqaWuAoqCFEF", - "LYySS7JcpcdPcRWMmnUNLTfwdTMgLpZNqf", - "LZoTv6VVpX7vg4whaXujmySxRENuJgkp6B", - "LgUAkvrob7L1Z5J2renUjbVDoH4ipPtyrW", - "LQz9t7uG7mH3CGtaZvUwcBS7w3qMXnVYvV", - "LXggGj4DoRu1PoRCrS4ZriZetn423kNjtM", - "LRDL2tAEDY5Cp21GMxq7mwSmhtCvKHJYmG", - "LZ45DzMZNQoNfeQvVDH97Bbyv83zM8b1un", - "LfLkJ4ETAXFDYdguJYkoumr73rNnvCpRWn", - "LNeH5HhJrcY7TPH3cmJYuPFLs7MRZJhxS4", - "LRwMLedPqzZ9TnqXupuvo8ZpPU775hxfoS", - "LeB4sMZQRFPCNTaov66FnJcMzznzXMEcrx", - "LN58nMYRVDHZMbCYfv866vstjpQYfg7P6L", - "LXbCqAhCGsN1mwTfKmPB8E114CiQfH9csi", - "LfCUv38QDqrJcrf6C2cGTT7cQ8v5BgScUd", - "LTWee5WMtpDek8AvTJJwQDKwUmv5P1kAMp", - "LaKcV5pyABMRWfiwZv5FiEvcZztcmEp8GF", - "LZRoQd74qGjvnoXHLzb9Ksnw4W2v3MD7yz", - "LQ4AU7TBv7NXwn2NJddxgDhtmFr2J8senR", - "LVvfLWXmQaL1YSByH8tVfJkqFCwZcQdJUF", - "LgTpi25reQc7gsHRqbhTpJnUg5ZHWxWc5L", - "LPUjQfZhzEXLZys6EQqt5gm4huPHk7GLQf", - "LSXYAuAkxcrFX4pjdM7mRxFpXUYVHLWMWk", - "LZnVdtGa6YxfBMUDS4zrNLT72mon7dKqkE", - "LewBHdJvLCBfsPSVWch7y8bXFP5F3YCqUF", - "LfGqWfaNP26H4hTQR7h1G4xctuZiyrT8hP", - "LKVXqiteTcAaRYe11oQDTbxsjHNqPzmFxH", - "Li36uQm2C3ntDq35KWEkds727hh1suXGYC", - "LZsrqswTkjyxtpZXfSZ4oGEHqZ6PSnKdcr", - "LcdTZFftQE3mufjgGRBdQ3aCD5tg8tPX5z", - "Ldj9MK45RTiUvqYjD5916vLgkLuZwmDGWP", - "LdCzymEQLHtXQnH2unAiZS7myPanA24vaJ", - "LWfaCBDTZoLfQjb5k1ForKMd6uwxDyFpkW", - "LRgbporuXdvyrKzdq2W95kkdwPEHjpLw2f", - "LQU4W2hUknmqgUSBZjtD2Ch85M69HARHXC", - "LXsnkb7A38NDHSJcD5jrhX8iqwFWqjnYFL", - "LgRQ4TmFMwW82VEPeSXYXKNZAGKWSHuUgP", - "LVcDDKskQD2TqnSSfG2BZG5ufqSmeagxfa", - "LaVv7yH1k7upguY14k2fbt3KfQK3zzkFgZ", - "Li6v55R1F1XeiZZNQ4RywuxZsfssgFHtia", - "LZLp73FKyUYkN74Sy384g4EVUfsD1EhUWf", - "LgWrxX3rQJwiY69FHa93JeW4xsvQgRWxBo", - "LQWm9GY3ciBWCH6bgiNHmPTWmLM5oKtRaH", - "LVqgHt9vUnTB9ZzUYNYxwVariprCbSAv4A", - "LbEsbXSqKZ2Uz5zQp1PTa5UXab7ikXTmSw", - "LaLE1W9XcM2fKRENigadm7DvB1u6FvGgqG", - "LP5cJdt8gm4uB7wyHHB8s6tYmuKyfcPSJs", - "LgfdaifGiyQEzhdCGV35Fwf9536CbaP9AD", - "LMt59pFjsdLp6MGDQQD9VRbHBZmatnFRg2", - "LX7e5JY4yfQGQzcP28NEcZzrzzMLexFCc5", - "LM2cwqThz5yYcUE7b4D2Kx6V9xsfX6135B", - "LVTbHaiMbuK5QJ9iezdwbJXkZxXRNWXc5G", - "LPkSDgscisHp8BJXeUztneoRNbkxJuqhMm", - "LfczRDEX5kxcyN9aRa9S8LD82QrrxMxR2r", - "LLmrgx2pGLQF7iGva9HookfnRgYDXxR92o", - "LRSrWxxwVDk3TtQjKMwaqjJrvp6PEX2hq6", - "LdsJaut1n1oALn71QDHeu5MKCJKYsrbJWL", - "LaTaJDeBb3hwYrMbVd7KgpeJA2ETu7bEwg", - "LgkRjuxxpx6Z8cTJ6A2ecaH6bCg3KvTS5U", - "Lea813yj7mcDEDyD4UxhjYjapWiHTkGkSC", - "LamHNJvYsnuQ5rt49a3mmyttGguF3hLyTq", - "Le6FcDL3NCapwafw6CMcGpnQKByHrzusGq", - "LfnzRrWTnmLKfXDFSKjgKbrsnB1iFrk6yN", - "LZXDUyfNwcfcsNAdr7yaWeDKKsq9YzAK9V", - "LXjcmGbwimUKDb85payAmrQmSXirDf9HJQ", - "LPnJYTcNKBM69Qmgna6yf8FMJqTvSAoNt5", - "Lf37qavaRCar4Wj9Kcqvhebaj2E9G8JB6q", - "LWjJHHZ7jeWXHNoS8SL7UEyd2gcSCFoWnC", - "LXbyzfyrmFonXmBk5GkPxiaZ1qyUZCxKuX", - "LKgfwR76E2YaH1yi6UTV2z3WftBM9pXr9Y", - "LYy8Nh9XPht5Mwnt8624gyc6axhFMAUAxc", - "LNpmSSJuq6tzt5tS9dZNJjuXhezDmUorLK", - "Lfv6agqBwMsFG3ANKvPysuJyQUkwp5XN9J", - "LfcDkWUBDhWVmF3Utf5Ztk4QqG7aehrKW7", - "LR9FK6DZn89Ye1cNu8QAtxZAVrT8wYxC5X", - "LLNChpKyyWgerYTKVfUFk2oeYUFyeZuEoM", - "LRGDjdUCZ4sAgUTK3m5gVDJA3R3rAY1nW4", - "LYeEZr9GdrCgei1RRWiRF5Uyqsi5i63gTj", - "Lh46Rk8bHk3JxomWynjBKzpJg4S6D4s2NE", - "LKtFkG5xJKdNnejKa5HxeUX7y1HAsKZgNP", - "LRKoBygqMvJYmDK8nDpSk5nWZKd2xxzbch", - "LKYb1yherT6oxUoMw4MTLyV51gJ1M75k1t", - "LPWP6xysXnpNXj8rKKPwKUKawmxymmTZ5E", - "LRcoYZSRQ1C5BLeCiNC46xFWhDPQAK2u9u", - "LS44nek3ytjXDxCuKDepHzPZb17LgdqVTE", - "LiAfwhdiwTd69BznWPi2nLjnsGc8aBH3J4", - "LYofrTD6GZZvC6vxF5iKewLm3Xd3iDWz7R", - "LXftf2DrbUVZBC7Ty8S9rnyrv6PdadXCZJ", - "LSazm4eUAaE3BuVVdL4q2cMhdbuwzSMmcA", - "LTVee6L638kvdPdYM7MG2V7JZZKCBwPHB5", - "Lg9LW8Q37hwbcX63RpzXBKprdei3Hocj4f", - "LWx3avsgDwffYTn47x8qtboTUM338c8CKJ", - "LT2PGJPi6xhzeNopVwWntRaxfUAxJzGo4n", - "LKhsCQnPVNPjLWPw9AesPG2rsbYaEbFfVH", - "LVeBNmWUYGoN35MPPb7eogwuApwosVXvNc", - "LiYzqbRHxSdU91kKGHRiw3k8UrPMKV4NXP", - "LPLFKEmRrMkKThEotv6LXSP2ujgLAvvhhG", - "LWuUFRwniKhHYGvpCJhHDsYmiqn6pP1NeG", - "LhrmTf3A8wN9uTG3G9kHzf5ogXHmVM744e", - "LcTYdKZvrwVgkHMsmqsLyRFVgUHJrXLSHQ", - "LVzanUyN8NiSKtspPUCAC1FHXASzEZJMM8", - "LKNCFcQue4RbHkQimmxByQbt6guqi9keYz", - "LZHi5sTPdUmCnmtCodNR4v5NG2BwAqCGHX", - "LgYQybURgdTLSZ7C2B1MhHWyfm7vt8wvW4", - "LdKLBQngeqcs1km3T6s6ZiDVZsQNdKo4QC", - "Ld73DCAtAiV6JccEsMvojGmtSNza4SLoj2", - "LVKWHWqE7tZp6rvmLhVFHcncSpQ5Mwf5pY", - "Lb7WtrcZgCYVz4C2ojjhAea1LWGX47p3mu", - "LU2w7DZWzuSsiSv9WxC6czySDdsTGtyhVG", - "LiMR9FFdEMNmcygQjfaCsN29WpJhQpwwub", - "LcJCTXGhoMc3882LDLWSvVrSvchgcrScee", - "LU1ozeChHY4Dc7tCVSxWz9cqptBgQ9pJb8", - "LMy9TFp2vUZJh1aBA4nSFTnk9dQrRub9aq", - "LexvTJ2Xva2Qx91Br3A6doygTnQrC3FaHU", - "LhJSxkRoXWB1imF1XLTydFGjthNdpMrxRC", - "Le3zA44dL1oxHjyJUySodFnhrQAEZu376w", - "LWdFGJmNPQjvdi6o89MDaFgQPLfzgpmvmv", - "LNqVqTuAi3WAnPr8v5UCb2K9v5xF9xVkUX", - "LamEb2i4o2ZQU227JFYyJ4JaiYroLh1Hoq", - "LMxqjT3dU5RT8LuoXnrZfHQu2wwPAZCuXh", - "Lf2Kwcy9hAcRdG64wZYhga7yvajPkyEQYj", - "LNcA279mX7GqQXa5NfJK9EiUCFvTZUEwHK", - "LZn5SSYFXbMNvWXi6Vzch6sYnEVm1Loyvp", - "LdpvugbTixy8dGDYvojLKC3UxzwQikzE8i", - "LfginKkdNZy1VpXtTspLypqk6KMcfGRW7e", - "LdVmYQAcvtpJdn4Zw21hynWcMxEgNq1qik", - "LdiSKq5rU78uXzibkxXqwfd2eWT1oV4WUn", - "LUaX2jyjQz2NwPicXXqBdYzYowXSeM16yN", - "LZZszxqfznEeCxdTV5t7U2yBvdM2ZJir65", - "LKvSyL6FBZHgN7sL7zVcxKuaTiEPRauqXK", - "LhXry2d4HrVk9E2acACooGs3KgKCDUzL7r", - "LfvCGbTUtRe9eiKqJhs8PAGor9bwqjwEsT", - "Lc98AoHUML45SAGgpzsX5LYc7sPCWpftNi", - "Le6XXyQe8VUbQhYUTn9wFY761xpLVETwEn", - "LPs9CEQHL2Ef3bKuE2BxmfWDnJfTvdffyW", - "LUP4moUpoS4ifVBtEBswFghVWqLdZ5qTYX", - "LQRXpcSkx5d66Y6WWZWXAcQoEUYbXisyR9", - "LUyQxiJqBRit97K9wdiVC7z8GZs858Efay", - "LXcGn1y7Vof9RqCUgD1msBXX4TKbfN78hq", - "LNqQZNJdw8jpwkq7MC5A1Z2U9VPRhxCN7J", - "LNYpdowmDHHkV5kpmX3BzVBqSppSNW8Gr8", - "LKskwLZnzZqCFQunxsKPqsJka7WuW5Yhc9", - "LR15VwhWBw1pnjabSn3M3VReFGmYowrhoH", - "LMzh6FEE7t8QatDihJesgjndmKCEB3GhDk", - "LVNMUN8yDoE7TcbPi9NWDeUEtRj7zBnmD7", - "LbaZwbxNhHiM5vnRf65NTgtM3ibaZCiMSf", - "LcUHUzb2Whg2jww4jqgtq2HiX1cjoW36Mz", - "LScrZ92cYWGz6hszQTryFQcQe41hsd4MVS", - "LXdsv51o41qLX6hRTeScx62DH6NTrBsxP5", - "LTTH7rDBRNYHUurrFBoUFVJCKu3KTL4VEu", - "LhWytosG8nvC7hLDVnuTc1EqPsSKNSXW6d", - "LfdabKhvDa9rzM7JUN2v7g4JXUbvAmn5aZ", - "LRWd21TvMqRd7WE7FowMBU2ZBZjW4b36gs", - "LaEdURp2MHA2FevyhFj22rjfYZ1wU8Wb3w", - "LXNHmVTWLMqnavNwxHUgBRQcUa3NLHxQLZ", - "LZjcbwokoh4Wyv2zqUdjW78rDVKX9H9erY", - "LhFZYaLVVMKvAMGbfEEJBqfNQLsBiDukS7", - "LfMz6875z5cFMPpdgQuYDhdYJjbuo7ydXt", - "LQEe48oXSSiFsKorrFBtTk8EunQv4rU8R7", - "LU76iwcG7HFaw45XB8RxjrBk21DFGtpUxm", - "LUcxMHYwemdh6GFMau9Zr75pvik9n5jVjT", - "Ld5MF45i4U4kFj899L1NY5j5J55YSA6eki", - "LW4CWN7rCAeL1hhQENBSt6XqTvUp1JA4eg", - "LTuJrHZK8mALecAM5ymjVXkPinwcmUwUe2", - "LUA55GSL5RuB7nNqN6w9k4EHenGqRmpoSa", - "LRqPW12HWzSH1CHufFEThj5pmU1oaBFGjN", - "LZ5jF6KjXtMJbJ9s9rYu7LvUhvMWhqCF4X", - "LagEhMhov4wZmciRdamTGmGzwFziAuphY3", - "LfinWg1VzA8ibHWBH6mVYd4VHKr6NHtKq1", - "LRiAzfYMPYrU7wbg6Poi24b2rMR7fefWpy", - "LUsBVE46UVrEHXxWRDB7mttenSX4p2fdzG", - "LeJUwJfjaghuWb4F8rmUrsBcj4Az7GTuRp", - "Li1sLEgHeus5w3sYdnbtJZH7ySwP6mnEhV", - "LhDVcUe4F64Jr7WJYrXpWLWW3Uim5A99Gf", - "LZL5RhLRu5JpSXWMhfVkmdZs34g7zftRFf", - "LWt468oMQ2qzDxAYkMKiVwgSSFi5YiWtHf", - "LT39rRFHHCyn8QNYP6LasYkYCrovdRZWCv", - "LPfvXnzQytWK9kvcSbEJxPaSQuKYZDPsQi", - "LZNYir62U9fRDJuPiPsrKDEzaDePhJtJtq", - "LehtRTWzzkA1zFCtKsrFaK1KUd9dw3dcev", - "LTV7ndtzN3N51GJkvHbePbibwRHQpazpQU", - "LTysFerupYwh6Qb34WFieDArbtyXuUTDQK", - "LNopEK2qHxsgfJmsPhJbTXQuBzmBNVdADd", - "LMLpvLzf28BJgP75ydLw6NHUMW7svpKa6z", - "LedjHgZKzmeGp2ehx3bTTmRjGH4dDztcue", - "LRTUoFmcs47RCcuDPc1wfAJwawCx7DGhH6", - "LM82cAWrNBeEUmBDZiF8TV3yaa3SLdQm4g", - "LZFvxgQrSb7rDX8tcy3pA3vh8cRSAxvaHt", - "LMCaFyi2TXjv3CRddbt2Ha5eX3xEC5TBne", - "Ld2rEdNcSQRGJQwBnof6Ka9jTXKx6e1PUi", - "LYLCh29WQdSSLoHWj4b71th8MZUWG79QfZ", - "LSFy1ACArGPRD9YZEmcNMHxLDmnoPyTpes", - "LPptdyRYSBtGstDYgjwijD8St4MfSCfeSb", - "LWkNyMGdTs34uGbEXYxCQV4uhm28VQoikJ", - "LfzTvK6yahXTDmgVMgM2XHpodqJySnwAq3", - "LaxirQz79hC6BEWGzeNo2EvprUE7zW6TR7", - "LQuWoruvtJ6kdm2rAg6UotNhJJfQcgE3Za", - "LiKpsruQgtbVz1isYzEd2kCDezufNSY21R", - "LiL2MAtuo5DEwsLtgm3oqK7asQVuxmRGkD", - "LKrMYBPBwnQAibxin47PHXKVtp8FdWTWep", - "LQDGoqJT71et48KtZHw1M3TWt5avB9ZXX9", - "Lai2oCdmNfPBLmdEq8Q3G1KEwazqGBb9JL", - "LYTUbJDrrWSauohGCzpxkDfJ6XshbzSvYw", - "LPPNcDbgNVwQKak6pgHMnK6i9EdmBQejRT", - "LLX3dw4EEYL3DNH2eMGoXxumJFJjdxyxux", - "LfNkW8BozxTraZRFQ9kVRZYopv3E2JNnbB", - "LTbDScAZKeL5cnbrECSM7J2CEPGj8Taezp", - "LWX8kRsA5xwSRFVHKKbuXY7V5KHczcvQxH", - "LeoXAWCsYJB3FcYmhw9wQhDohKkBLjiDUA", - "LcNSzD66uvEAP31gcS5mcv1wyZ5ffsEzT2", - "LRkv8FEThwrQkxpf3jFkv8hY4Q3vonwwMW", - "Lgr7QtrNgKrS4htZyhmSp6S6C4jDX4d65W", - "LeJ54WUpjx6tKpBMyFKZbqcWFMAZqUUkKC", - "Li7b8UcA7jp7EMgQ8oNsk6JbV76XV6BoXr", - "LeadTzRiEFVBjjXNJwb3T7AW8ywNkEVLV4", - "LQHteShBs88McZVeWsdyqztHLqBGxdf8Yv", - "LVh7GPmjryAyxxEu5i2rFceAjEeAj9EP79", - "LS4d4NxrqeTUBjVcsaTLYtFHoPWLe2x7ir", - "LTeEzsHxdrMtE2wJdLnDtmJVgJD17U8bem", - "LhZREEZF66XgJFzgAnyThSBbSeoKbdMHBj", - "LapLnKbab5Rt2Twnz9whwsQN1NKnbTETwp", - "LdwcMyu9ygA1zfmDNPgLDVxG9AtQeJCdXV", - "LT6DRkw6XcyXf9pHjqCFYC8zic9DATfE1M", - "LPMVMYFLER425fqpBRghidN65MbzV36V52", - "LNegG5NGKkjTxR4V96qJFuEf7CKkDD5xgz", - "LSg3mgY3FqnvjoEWLTbc46hFPQLMtPCapg", - "LUC3B4C7uVaeScH26xWS4xBe62dv9P7P4k", - "LVPrbqT2JesNv8thx7TXNLd85WNBFyoDxW", - "Lg2SDwobLEhMGsxj1AUdV9AYBskz4a1f5N", - "LLZawqtAeQZpqCwfQVDrJnaPvTxL7RsZhR", - "LSBe3yw2mcrKYkx53yD8daJs3vLBoinCzi", - "LUYp26MjLmWJ5SWaHpccdTx9SYatEjCRhc", - "LejgVcq9oDZZrspvKnrkgeWCfMXGTQ8XGE", - "Lf47jyLKhphuoeuhQaBpy8BSDtP2b8FX46", - "LacqTKQcZZVHXamrtyC98BiMUqGFjre2jJ", - "LQg33sm4kgBza6u1oTmmQC2gDQzi8xHQtp", - "LhQSED2BB5eD2nxSnBuH3cSgtpGRE9Lr6T", - "Lgyym4cr9jyZ6DuuawsH4vU5nGfFJo46hR", - "LRUQb4QTQoyHqGnBtmxmGUmdmPEiBDaHFp", - "LXa7DzWMdrT1ZiPerKkJwxZPs8KGGjotNi", - "LPFvTTZGyPWTzWBniUeabT6PpX6Zzi9D6y", - "LevYJS22KHCWb43JbyBHtQC2ktEt1DnB4M", - "LRLD1D1eHEeEZLWEkq6usamBtkRFMoMQwK", - "LeVxtaGA6CM7m6aC29wZ3hhVqU7QwpiDkL", - "LTMFcs6J2DrhSCRoVJ4VjS4XsbAjtyLFtt", - "LSs69ihmYuq9ZFaZYY64dybDvcRKJ3HT1w", - "LLV9CR2EB97VojFE3QG3sUTswxNLJZ5Zzz", - "LNdPmx17qQ5AfMcThrSKxgg4ats45NSixj", - "LdCBn39uDtnyMUaQrRK8J6M2FvaYoxNvYW", - "LViEH78zotsf5NFSHC6rM1TT7bVXnkQmEK", - "LRwMVqKphLBydypR7xkTFvb5aBibgLcQUq", - "LYsywU9zXUUkmdWajphpEwF2eiL3ERC3Ss", - "Lf22WxRh8MgotGj6QNtsmwBjTi412XedCz", - "LYDum7F3teu4MPfN5TT8sxts7Dq4U2ztmE", - "LbYwshK9qHevNBGAR9Hrdj2ujB5vRq9A5r", - "LP92zovmBBPyfa6EffCWuToXAyDtSjkyNx", - "LLyJ4cDBiJ7mZyi7BFVePC8sKr8151ZLKg", - "LeFScySfBCoWSgvrzkTiUaajX7BHbJ89KQ", - "LTJLMc4yHHg3Gtxwcud5G4bsHgeKQFVTkd", - "LZoMaH46kzYykGStsqETL1gECWAxReWqAy", - "Lag9gsk5Wu9nnyVTLGex9hgKVVv8mBMdia", - "LX8ouPBgk3HemjYTREseKXEZCXuhcRcwZg", - "LRa7BNU9G5wjsE2KxoG5EAg3pWs6wfq4pt", - "LiYjHFqzLsqKvZx7KCCH1VKYV8p2ckbM3J", - "Lfh9oL7amDoxWUZfh1YyN38Dmij1vSYEnm", - "Lb7bTy3kdjs4EiEEAL8iwWgcZ4Be6bPtw6", - "LZULtWkQfjRpSLT1LigDM8126bGtDZQyZk", - "LfMuKSyVh5J38GY59SWswG8prCAucpkcGh", - "LeTnaxgTXm84b17EwJmgovW6t9RADsLiQJ", - "LN4VtcfdHTYmoacP1tpfP1w2uW7gRuhTY1", - "LYQDSnr6hMBKd6x1jky9gPA5NePEdTKMd1", - "LSk8DivGMiXGUy4NqvyKgXDUz7tXi7M6Mg", - "LTuNGVBmKgfdoc13ep6xkQq2qBdAWPcs6L", - "LR3fRTDAGuCq2wJqyQBTuKzEh9TqUo889t", - "LiQHGxYYQwbCZunneqavSmsKfAy75rRpje", - "LSf9xF1KKvWghwc59dHhKg4qdDDQLMe2Ry", - "LPt1FjgHvKzG1apoxvizRp2aTvvpuvbNpz", - "LULUas7LEucL5NCKo85ZoqGwqSWXqfKPAC", - "LddCzGVTs3gC3apfXxHMJDt7DN8Ay2d4z7", - "LRXqDsB9QVAe7JmaeFZxTV5yygQNsTPqhJ", - "LNXY9gjCUANfL565MvBGyB9o1CkkqPxDJA", - "LceXAS6mEK3fyXX9tdbxLREbzZDJGnkrZg", - "Ldo7eHSWqooZXNYfoSKB1gKPWauXGBwAvQ", - "LcqWVgb24TQyD8AQR61qGjahj9Uwys7oqM", - "LULdzyN1WW9BDdTc8hCVS6XHUtUVFfH8XD", - "Le17ydzLqYMoFzyUuVBSszrFMV3qYeiPcH", - "LPbCNLF2QgdLYkrQMNx1E3a46Sm789uGeB", - "LXLap7XRPWGGDV5NW4kAtj9LATqsXmJYbt", - "LYoGvagUXBTkyhxK23tuzqXmkLgE57Coep", - "LfWdzyNfwjEhbJCi2sg9j35RpDXFuqLdyP", - "LTgDa6Xsg63kvZ9pH2wYwDCqesXo5drk2J", - "LYDVpjoYvyEGEtQSzwAssuycoS7VEHGnaS", - "LXv9YPQGQQxxHwtXY6kvfABzo85A6pB6oN", - "LUNRSQeq19vnFfpyTa95RuA9UanSJ8KdUo", - "LbfVboAyyKXQTF6i6hLyoNzKwsRqKFVpyf", - "LN4f5Pg5dShTvTEZ1jXHfZKRuCrSeQHCfJ", - "LgED17g1BsjGx76yPSWaAXYTFf9Mo81L1t", - "LcRCefLQqwHrySie6WYDRwj1iGuHez9wU5", - "Lg2ngZXMJbBYW6RZ5SBRL9w8bZtVQJWQFg", - "LZP613qoCYLDMmVMnefw5RuSt9L4o1YkfR", - "LP3sW4vxFPuyTJC4kw7hhEKqa3pMAjfpYE", - "LhVMmYc8UZHj7KSKPSUsVJyP1GyKff3hgU", - "LPzPycHukP5J95jjiUx6vYi2FhRM95TPi8", - "LbenvZKkJwhCULwRCCuGgqR26rFMZE8Tzd", - "LXqajRBNYWChTpbSoCd2mVtY7gzfzQjPnz", - "La8om9H3kVzt1KCKgp11dURgxCaTqLj3vJ", - "LNVPL3UmQrdbr5uybGVco57Lr6jx6Gqa79", - "LZQ5nJckGbMk33UMob4eoJgi4JJFb44XQz", - "LM5zsEnehjipDxhdNbsX3umTNJtvGgvthB", - "LSTDwxFQki1PSRmhBYk3gZr2qPLbKcPKat", - "LXzW1U8Hk2YKyh2ecTGvxthHocLra2UJDQ", - "LS7sLiEsVasKwbtoidXGudnJB5fHDBNYG5", - "LLz58af37RiZrik2uDPmpc2RQJyNJ1oj37", - "LRLMow7MYFSoQMpfqka5PhJT3Z9k6WqfWb", - "LTbDijjjSUuFkVvVABYdkBrDtRpEsuCMcw", - "LaV9GnFJykDwaUWn9YVRCiSDtUt5GVpvPa", - "LUYJZ9UpqCayVMx7hTyC9UmF9rpEiH3Zmn", - "LgPff5EGamXXnfzHT3t9b3PEqcyRwCRtPK", - "LW4mo41eaLtGg29UzvqNnUqc6yYw6HevXb", - "LgfysTaFcMehG264PmjC6rFbC2PvuSxUs9", - "Ld73tbj9WQokQfyLFeVr7Re8XeWW47FPu2", - "LcAUmE141s87Z5AmDvJjqWvt1Uyqp63onZ", - "LZuXCswGsQQBo3nQaPm9e621GDa7hNPQxj", - "LNc4FeoogJW81pTJoVDjXiBSYsbqdiP4dz", - "LL2prhA3kDjtV7HoTp5hvaotDT8BU6BESi", - "LaQBUxvRXfJSEbpnf49dxqhCeSEVKm2gsz", - "LTmX4Mv4bScp8z1FgW6mEtQavN1c86pT2N", - "LTMWeFwGFtNUHwKvRbReoRSRBRDerNRN7u", - "LLQcxwRu3DR2VTue5oEJL83RbLEh2tML4i", - "LgymiHL6v8pJELfkFRqnWcmQGqpA7T7Zmw", - "LNijHxqraiYvkw1C3CXLvTDJopTWKDb2q9", - "LcPvRMUBNhciUj7ZDinmNMvd2mRDZ5f8gC", - "Lb3XSRYhobeyGGuHyN5uPtzyPcakvhQHWH", - "LbWLi2fZvSbNUdpnotbnfp3q2iKmapuDF6", - "LVeGB8SrXQSF1aD3tB4BKjpbf3mdBRTXtu", - "LMEWoLtwXBhECEhWnpz5frV71EuehX2zHv", - "LebnT4ApLCju1qvxCrJJsP6u7Jrdt3YEZu", - "LQSx5N8jfdykN13tzo3d23HqG64f1bWon6", - "LPk1KKuwH1S9vtyf7HM3qP78UoEHrAnEXX", - "LU9CaMPAgco9wRxc6Wm3FYkuFEzhYX65Ax", - "LSM3bSPAhW9HN5FtRn2Ea9DLKRayQyQx8B", - "LUgSMFvNQzjusUna2Hcz9p75bpEfS9kTZh", - "LesW6eRsSEnGRV9BJjuoDQL8GzWWoNGZek", - "LaCMX8KkRWcYknMMzTrrB8DxcoY6x6XJQt", - "Ld1DqSmpp88VfYX53zo2wZVs2wdTSQj2cW", - "LaT73qYEQ5LyK3YcSFjenQP6MxJMqTLqBc", - "LPMo7n3E4TzPBryFFoav1zfwaTqEU7fkDh", - "LZTefH97XnNiN2cFVUS5PBag9hqHStFBWq", - "LdyN2MJBUfHGS7AHJ2RAnhhK8vru3EirU9", - "LgEsMgND6pxSiwGWtZyETh7aMniTUHUm2M", - "LLPeDekSGQPpfRqQZXfyHa9jgvGqsihuG5", - "LeYgKYNJqdLyD7Jh5EygCSVxsunTVKpiVC", - "LU8fbEyGN4H6gn7kmZDyZPvJdA74LxcVKY", - "LP6TfYe2nRegNzVunREnLxapK7j24HFcws", - "LTJL5QrbrR3zQQuiPVmfPba57edCQtHUp8", - "LWdpr75az3i2dhYGdELRNjcDdG1iiAb1gn", - "LXgkzBRxojgFrvTXm2qeFXN7Dr75rr6jfy", - "LUxero4pkex6r4a4d4JAbJBP3eZVj75F1d", - "LdZKaRExyPhkYPeCZHwDgyX8zoZQJkoUPf", - "LZYY5aHWStB76pPXBtZtkRSUgpFgnBBKQs", - "LdACoqRaJTgxmcPPVLnQpj81mrfvE51u8K", - "LVz1yNKuRyeynTWKtjvhS8bhi8CHtEnzjs", - "LWaVX89EdNksyEs426eyez566uP379jvAW", - "LQPdTLAvpAndXopptBQ2EJDkNkswusweEx", - "LcvjRsJ2KtzbCVCq7dUXZr2cBCmPdVK3hj", - "LV8Mh3KS6EhJhHdDU4b7ZbG4PjkxhiJErV", - "LYJgrdQUV6rUQd31o2oHyeLAJ4yp6R3eXP", - "Lck3dU9y9wyJD2fSfK2dqb8fDwpQRWju4d", - "LS4rnegqmi1i4Pb3N2EPkvP7za8e2vR4gz", - "LTLvVRGGH2yMFmF9u9Mz7Kkqq2AwdJEFLi", - "LKLrHVjWYat4oTLhshmYEsRiCfUspcQR7u", - "LaEyfX2Pd29mXGT6iGzqnrr7uhq1UYZAHm", - "LZwXMbe9qtRWiuNtph8HcALSDzkmfWW6bV", - "LRbqTUWUR2fEJHSgRbJVjFoq52v2C1MUgE", - "LQcGFesvduneESv7HEtkfymaXeH7PJJgC9", - "LQBHTKspLyrqQ3hnCyMr1q2mk4RrT1BFKH", - "LNGjuBnVciDGUTxFQC2bB5gvFE3YqPzXRr", - "Lh2kgp3553BcvCHUsxYjUvrotjYHRgjXt4", - "LhsEHCMaAzGSyvZpCj2V4Wp1HjcwMah7tU", - "LfmRHjSNDXaEmLHEwz59VCUxYfYsh7u5kF", - "LThrqXBHdD1x3uby5J5PCUTWQTVshmdwxa", - "LMHUdMwpeziokMgKSWsuVeLKQryBYtCfZB", - "LgcRVwTKk9jYyvp2DQTsQQxyoyhVphrdVK", - "LdrXpd88kH2v1BWwJtaxohk8LRnr2TEHNm", - "LfC5NbmSeZYmiKvcQKrmz14msUAKH8ZrMk", - "Li3fNhb2ASsoK9jKJZaWs7EBF4XbZbWAv7", - "LhENViKDr1eDcCqEUC7ieQtRkydYABFq2r", - "LUyueRX77ZDtVgXD6G9FWjyRikeUA2Tx2Z", - "LYLudxM8vGRcQaxcQHRn8DzdLj7AAD68qx", - "LfDK8SGgD7aKEEB62iP3Yd82SvtFfip5uF", - "LQpdEHvZKFQF4rCUTy5SnqH384pvvZ3Y5i", - "LdfMWGVYEdDtyR2GHy8Lggz95dQPSaTZBD", - "LdZqGe9HG8Z4jv8oY3fM6DFWVYXyaEoYnT", - "Lg9X3fqua5Qvq5npLAXDfzF24n7jsduFut", - "LZvx36km8ayxZBZkTUDTm4FiDGF5gybJxt", - "LNHE1ejNH26YmU87epxg8SHLg65mrBDeQT", - "LQKSBKX9JwzQgM7EmhgmioYt9LL4N9xo3a", - "LYBz9R4vX3TPSTeoYsKPmEJAAYNyFPQUdJ", - "LUG12kwRamFNEvuWd9X2sFPrLBEKxGDDUC", - "LQZW8XVnYK5e8P9met5drCTj5dpnvkXUDf", - "LXEHZe4KwTfVfS7gwoUVuWsKtSHcsj1fMg", - "LahNS2CDM5skx3AVDU3PqeZKUc3ejsZJEB", - "LL9ztroxHkHEM3AfaaRnWR4GyBmndAQN8v", - "LT8wtacMnb5c6uUjznGgTFAfaaFYmx51AG", - "LZjUrPkpg93F2UUdCXEekTEjBjx8m9sitJ", - "LUC1TrjuMShNaYssEFVLhpBiUaL2sMjpZx", - "LaC2ihRQanZS9LogNNMTAuu8HLdz2uFZHf", - "LfjTwoa3QDxSGMVR4yCkUJ7QV4QXtxswqX", - "LQwd3fFW6k5VoZ6Bq49hTeVBzfQZCzBWEU", - "LPwJsArmye5XaM9xwmoufxPfB23vBxJcm3", - "LS8TFTCeLdB8qxzp5nYF55XHsz2XzRNxup", - "LLy4pPWGkorvB2Qz8wB5tfpP5tzmcu6bxM", - "Lbv5z8qMXWyu9ze3jQiv7viCESorzsZcXG", - "LQcRZZr7EHboLGMSW1VmDvcjCGM99FG4fn", - "LTg9TwJ7XZaawgmpCrVxEvvdBRSe2rtmDU", - "LNkUBp9keeahZzUdzSohNvG3WB5U8NujhJ", - "LKGqws48yQFuToQdGcfK4prxVmTTRzxTrX", - "Ldv7f7KUtb3Gz1zMm3FXS9LSuxx5fVefxg", - "LKJRF3ViVB5uPw1EebUkAVeA11HcMCT1L1", - "LaEEMDkcxr5eKuSf8dPNR7VH3i4aSKESbE", - "LdbUTpAvr62BTB5GnrDHdMfkwe1q61TGUW", - "LZrETjveMnQPzYZpbRW9u7DxoVTT3tLVAq", - "LfDZFiqTJDG6pWC9NwD79sr7ig6XJaSdsx", - "LcVE6S6o4bBMgZ44UM8pUP9yDWJe5y44TP", - "LN12xLfR1riFERGWD2ehTVXV2SgTPEGkda", - "Lb9MtyqceeU8myfnDjopRjtMNPDCdkdYNw", - "LWaf4t66rYFyrwevvSxXUUf1ghXeevwnrF", - "LXqk4DgW94Jj8Ne4w7TVS2cbrDGzFxZNxU", - "LhiFe4iNvqdFFySfCaKcz8LUTDtQsCpsW1", - "LPSSyTfct4eky7bdGaBTpdy3hztmMqMXFZ", - "LgcXMbZDRSE3EigmdkpPJJGrCzuEHVki5T", - "LPApGJ4vcYwJ3PqfrMcCstNjcZ3AZdJWoq", - "LNx8vM4HSHySnZS9FaLP7frerU1b5EXGsB", - "LQK4kcFKcGWJM6dGk2rjPS6SroH2HFfi1C", - "LPco5V5H2FRzqvE3RyA92XdkPKMvwJpNuP", - "LayMbyFpKVCtQ9nFccxcfhBya6qVQhfhbu", - "LiPzSX6U6Yn7EhGPwfEYdjhxyGnUtQQGJZ", - "LNxATzDKeGGt2EhyA8LwTEXoYjPv5iTXE8", - "LUYBZfFzXMNcMaFvgeQT58DgSYmf97zbMF", - "LMbym2ssdxWSdSQ3mbniW8cXhjigeXD5Kc", - "LhUTs87noKSP4TbPLcBddGmFSZpJNAwoqP", - "LSA2jhkmZgwZWBJfdWmdM86QkKAfrYMTP4", - "LYbWxSsJKzi24EGjz1i586XwickyAMBHEo", - "LL5U5BbErTwAxHkNhzgpPpknbgdutAbJfa", - "LSQ1RLD3PtazaMAi1uFte1f2iS5AhGmmTE", - "LMHMqXh1ksc2yU1L7kmSwhmVPLe6n3dWLC", - "LREYqFbYo1ZpmZHagSxGRsYneUJbkStZwG", - "Lf6jQQ2xXF87TsyqpYMPynTF39e4DpnsbS", - "LgGKHMPigMHGTnCFtRbma6nHEs52TDiZkR", - "LSyEah2pmQpKWHW1MsPkBUukv3JwbLFbp7", - "LPMwBp8Mb6o6332oYNF7V2BHxtPcB6xNvV", - "LUAzns7WnqjVRnqjh5Zu6XqjhbzYDbL8jL", - "LLheFGLodSJT7A45C7me94sp8BzcnbtU8X", - "LSXUFKZTicrw9LjdxE8v6RLyDgpaqEQXT8", - "LeUHN9Y1Qe6TdRX3nU7yLpEG1EKSQ7Bzvr", - "LKkw5wELgeaF371xzrUwYriJGu4Enpoe2A", - "LVnRUBuZJ4pWZMUJJevEZHaCqqZvKayZcj", - "LMZMDKE53tZa81MbHW61Lqnx8UcCFQrWdK", - "LS5meyubrb6gpnh7ScfANA5EA6TYE3hgAG", - "LaWf86KLNBi7xtonjoxNusr7dTYJM3Apsb", - "LdWAisbV4u7ufsvZc95AV6nbedkqcDSztz", - "LLewMMzTp9t6buyCxxN1GJKwviThwQuZF9", - "Ldscfk9QZhVgTxcTrpqpPfuh4d13PBk5LX", - "LcUkF9tHB2de1jvv9XWicqCSJH3boGuvrB", - "LVuh6heCKcHgQAdAcQeRa3BHNkYB2SN1sB", - "LbzcWGkCztHWSHrCWZGEK7gBwuCEYBS36F", - "LhTWof4eFXp8b9fiGXBNChVv7F6cCUqzcp", - "LbR3mpr1aYMsRBV35Rrd5tab6CSt1Kok88", - "LYN8VrfVT2KKNECnDXzMrmMsBXTKqT8qUD", - "LPdALGaG13ZDTyjos7ZzZnFKxwnoiq7Vvx", - "LLYNyWwRGLZtbhvVtwE9KaVgqNezmU834F", - "LRQgcZZDGA4ArbSY3t7dDMJQiCC7fm6Ngw", - "LNcWTb8vVNtVcdt8ePdVYtKuceywXGexnd", - "LfZygy36TzCJcLB15gj6iUPF2PqEKkqfjn", - "LY6b1KJ3PAoYEJG62FPsVygPr1Nt4dLxJu", - "LMpzSmo3xMdQu51gxD7AhiFfTdefjUGLcr", - "LePET2HXDooy9HV5XPCfyJb4dSz7btw7tc", - "LSF2ukWRZ6KzXTHYSdDPPVBGkE7QvpyCN3", - "LVNaL3VNted2bwyXkopvm3MSi7BeGNSgc5", - "LeJcMr4q6QqoV3ZTvi4uFxjvopd4Speu52", - "LX7LDTBPhr43pNmpALrVSpZuq3qp1WiDiW", - "LbGqdgDvKayibqwwWBUbc4f6HD8xkns36n", - "LL23kEmGYb1K3CRm1zLSjEfb5pHHBbh1qN", - "LWcz95wSBjuR3AGywLowEmFNFcuVvGU5Qr", - "LKs9Dy2fTeVGunYJXBEqdxxmxCxanG1KSM", - "LWAR63677sTMBh7CwGUgLF6xHrH83ybGgQ", - "LSURJv7fyEnibxuU5c9CCQpjZz4FyEVoTB", - "LeFrisey4bwdBXEPBZUnvsyhvp5R6HxjbN", - "LMhauutxdaTBvNBWt5Z3XVhGE72h56m3Y8", - "LhuS4qaCekfrsac8dW2fsD4pLQm9rf7AkU", - "LcQABUJC14zkbVKvgYKYAyvaDbcaEs6aio", - "LXPvebPoLED5cVyF5WtkVfX3wkXnQvtBFc", - "LQ6VBRdhtXYGsuJz8gn3TEJ94G7tRjvsnj", - "LTY8VWCMTLpkEwMu7GvXJe6Z9G6fytkJBQ", - "LRDR9HF7TmC3W7Z4hCFC8Frwou4qvs5MEG", - "LhGcqqfHd9VPDXVLjnkcisayWqZ7mMWExG", - "LcMMWNyc1MkwZzC896Yf8s3w54XEKjHJRk", - "LQMzwTguqtKaNxGCKQQBf3TG82UHwuhCet", - "LNcbiXmJH518LtazqSsYRNxzKSntJUrwxr", - "LMvJZzJAMShWmGhbqfbcYvFjjkkbjNBtuE", - "LRknefn3RKyVxwKkjdum9us6jerbRUUEYa", - "LSyXCZdZZ6JFQx2eFKzVsx8fCjajshKT2S", - "LMHGHtU5fasRisPz5Xd1qSZR5X1JpSME1S", - "LNodSYeXtiLWvakGXrP7wHsfu1veicrBp7", - "LhJXTJpxjUPV5yqqW9UsMzxM5dMa7WdVuE", - "LQ3GjdVVnbBoZmHGupZmNJYB1rdhACRm73", - "LbqpU58UNaUh6QptH4a325fEFHibXtisEi", - "LeLiUyNTXYV53rJzHBDL9MCeZwHcYg6oer", - "LQfCDij1hUNhUyQjmjEFiS3awGshk81W1j", - "LWES5WWwType47rjoqHTQ3cxhDtn7UKS17", - "LNe2VSdTEW5tWyCsHBtQi7ZEzvqwV6AC6F", - "LP4j37grd1NTRqdsNMVvPhH3YVPxRey6cd", - "LaN2VnxNMswM2gzJP5JBc49UUvNW2hQM2t", - "LZFVajmyg5S4GNmVxZ8w96uUoCuTk49SZb", - "LaCXjjfV7yvWFuoxq4MbaAA26HmDZ8ygnJ", - "LgKanQNSNYEKcx9mMUXGiZEe4sGvDsfEf2", - "LZyL3cG9DFZPShwPNrY5A7bFFC9REDz7nX", - "LUJMcEyxWi4H1t3VJm5p4MvtRNQwJMAkaq", - "LbJdtCU9BwuLadLQrsGw5LXQTw1cofCQUo", - "Lcozgv1ePhxZJmtPhXYUM4mAqBsgrixfJL", - "LT1nhGV3ugGUsuQ5S3P7hamuj8FG1XzXEP", - "LRnuLSyDwEBUwN8Zjv1iM9f2PdSrJaXkiv", - "LXDfRLDT4aSaD59HBFiDLVoDsKzbKHvEG7", - "LYNKSRZU5NzsJ91k8F5d8w9n3wTXRmESco", - "LT7wT3iqV9VBfQUyETdc22YgmuYPpsz92L", - "LR5NkVi57qqNmxBTgNXpaLMyF2R1rSWfdv", - "LR7vwew7NMGS5TwY7nmxjENY3Tw1pAEpBn", - "LRQY6WqBwZPXH6L4HXQSZCLuPZKLoR2A37", - "LPjTYuZumxSp9L8o1PZvpnNJ12iiDrJzpF", - "LchWtmeyR2sSPioQ5uDvj5zHqgRiPuSnx5", - "LR3TKpkSdQLpc9uBfJJhshkc5YQWaeXzzA", - "Lf6kkRPHqxXPtURWyhV3NrSMQhvJxHeFMM", - "Lh3hMVpf5cW5iMs1a5ZSH7wamdj7Cu7A4J", - "Lh1d9VCvuJTH7zankijKBQCHT2TBTS84rQ", - "Ld8GhZGMzsvWrrrLRdt1vK4jQ17swUKVc6", - "LUZnNT1Yxu5weCP1xYmUQoXxJ2yJjNwvyB", - "LYuvKNFjNhXUd4PxRfwBkB4mLwt8SwM1og", - "LaR2Zgy44XV3ZfqwjjL9ZBXJjMdL78LKC5", - "LZssXv5uSuictUyhXtXFmPJT3i7r8WTaFd", - "LbVLwA5kueFzQtCvjLWZcicSACTwfpt8pK", - "Li6QhA1JwQRdooVYWQmqNY7ajapPNiEYpV", - "LeZLuU9H2YoSUcnf8oFZCoGfsnv1PpScgA", - "LZCwXzfqSZWSqUHhZ8ZvQDRA54TdwHkzm4", - "Lfw2PmMhY1s9kSmfGid6wRY4KeswmuT1mT", - "LgZHB8N4sj1ajAqntXEvfZC46pCVfpNWXC", - "LT9CRh2GCidjVSB9yDzhAYR9o29JMzFV52", - "LhApBvac5mtAoeRhyeM1UHBAKD6t5o337g", - "LffTmaekgdvzKBZarsTzQoooictqjyRfZ9", - "LhGbWvFi46d4RmuGZZDedbpeK5cpmDVKjK", - "LaDBbkQG2eg3oTGcJJcUbSqYujEWD8GxGV", - "LSBeVyUmAomavuAaM3NRAcWqBJSCMZkma3", - "LaArmQWVYATX1RqLfcHTuiEKPy1uAUK3So", - "LaqS7mm5khUqsMcoH29pcaxJnMTZy4h3VP", - "LPV77htAh4RvvJnK2poWuBJA1XKU5UuYn6", - "LbtB14P8bSDL7BBS5u9C7QzfSbLXSMuWQ6", - "LeTFdUsPNG9DpFzmxqdNzcRXWiUhVBFSje", - "Lap9Zp3RjjiasRKZ6HVh9PrrxTnbEzmQQ1", - "LRJFh8Gem1G27ZER5RDqjaEqMknkDAnL9H", - "LXPdXPJ2ejvQEUuKii2XmKpcwWGKReQ1AF", - "LV4EBa9dDLAVrnoP1ZJ9uPZ21QSnGsyng5", - "LMTQpVaabmQ4GJRhya5UdDbBYwCnnrw9j2", - "LhomQHA6tTsLcPEmm9XyDBKTUvmZPEGCw2", - "LV6zkyyQ4w6MTstKHGUTvp147qF6tDK4Hv", - "LZ7JjSm2ipzdDSS67TcDz6ypYB4uRRh5qb", - "LKmkBiyWJcrk2qsGpDLSpENU9VmFUhTFLb", - "LcfSw6juAquu7ELptLuWdGVkFeoCGBt7TT", - "LVVeiEtgxcAmZNR5ZS7EkDEuP1rSQ2JR2d", - "LdggQFKWBZWXBEhmXAa4jVqPvV626maE4A", - "Lb2Uf2AJHvMg5gHebHEnhEhLRnjtZ3AKKb", - "Ld9zAq3tJ3KfxcXoXPXmKPS3R5yzrcD1zt", - "LM9B7S8wn1oXUnPpYXPjQeWufnkNYbvXwb", - "LMRdhEeegHTXXwBSEmv3Gzc1fQ9m4bR7wH", - "LdhYdFtLHfnWrnBFCcXirMy6BUr8mapYxT", - "LWLCjJqo4M1XbYQSKrXfhfoUB4pZxMQsTT", - "LYM1ejQWpBhdkbEhmfJrQ2KXFyeB5U9uG4", - "LKkDW2qjDbR2aumXuWysSUxhPsyKsLYLLQ", - "LSiQ7Evxz4KmEpaWoMFsb4in99iiZqchXi", - "LgFdXHkjNNuRsnjvUFByj2y8RFKBwigRvv", - "LgmZeoFz3LFw3M6iH666sXs4efPPPFDFRn", - "LRwVbxqon6hhg4uKFFjsPdsRBcb6CuuY8N", - "LTW9LQ4m3jvpaJQqG9SywG2YGxTxc95XDS", - "LhHYXKgU4ediDQQiVKgFcDss1Faxj3dCwT", - "LhkNzhtVPqyTiM94eDT5jAGpk6rrjBWySD", - "LYQAfktqsZm9UXFT8Gbq7hxQHLXLvvY9Vw", - "LSWtD79xfEKQie9xkYpLq7GtgPRCosKtqr", - "LTfbfSNWmwayq8XcjVVvQ7L6RRv4YqhnXb", - "LcssJQC1XYDMTibztCZQmEk56tefgHWceJ", - "LWr6QzQCSbgCmfmiMXRPEDt3RjQL1b1h6v", - "LexyJFNrgcNnnFL1ty5mWvxbLhJvPQr6Ps", - "LPbSJPvBLwAsFSARdt1zEi8maLMmEG6su4", - "LcJUVq96kyxh4vYjQ2V1mLbRLad4oxckPu", - "LL7sb6UWoGaXZ3Q6Y81sff5ASdW7ziSZgC", - "LXP1QYfCQRdA4CdnUjUJdMxoK3RPCCV3G5", - "LRr5KW4SK2APodwB73UmJWzMJYKk5edKCb", - "LSZjBdRuru46fg8KT6eubsHPfibmbQNgSk", - "LVnGQcnFjRqXWKocqFycqthhejNrDUb6ns", - "Lfmjw7wfBTtKYxeF6yYcg9VFp4yyoCQzVQ", - "LTeFKRndXEA3DDjVENvqmwFFWYarRtScxb", - "LdcV6oRJgYv9Qc52JkYCepnA5pW923Fygh", - "LZ7BP6uy6VcbXa5vYeQwpiCHHADi7R3QE4", - "LhkatG3dDHy85RFmEpKuRv88YHJL7tx11e", - "Li9n2e2GYndUMgjT8YarNmptsqyy57nBCn", - "LLwKLVEKMse8WbFU3t5m6oqeq6yQzv6Vwm", - "LYLEJH4tTtDECUWTRkv2JNgGp2qnC83eS6", - "LPGBJqe867xF4tjgQwknzoVU6v2XpggdaS", - "Li1TcaUhEP4tkbudkWncfVCY1j1wMgPnS6", - "Lbg2zTeZBjm3pN4c2cmJ7Aa83GNgJPP5rk", - "LRYV3RzHidi7vSYqyGTco2cLuTvkKWjDZT", - "Ldv5gad84XcH6AntbxjGxZHzcygCzgsi3G", - "LQ3ghptDNPhd8Cf5N7kBrvTbd6wAnfVTqy", - "LX9UGoNbTQRQmfymnXvqLKgbTqPofo2ZQe", - "LSTizhmpK1xbmAB6ug729uLs5xp8dir99o", - "LaXB6PT7Ur5PfSeKQtizmeaQzfKf9ULHeo", - "Li24gKAxQGHhQMcTNGVFMPikKoGu5HoQNj", - "LZ2mXGuvoZJTvtYxsupvNdjMrUEfFVcs8b", - "LNWTCA42visTpGMMmKMs8EBG5P5QFYwmXw", - "LRMYix9UCn76pmB87daetkypi9rEA9BkcK", - "LZBUZ9WpmCr1ki5tWED6LPRgKtUqi6t3EG", - "LKcGMQghBrnAutuyB9oZryJzyxhnoW5qyA", - "LgNtTi7jnv2T7nVLtiLs7MXc9fUcZCM1PX", - "LRtL2QCJ885ndQRYcCyxHtohojNtQ3kwms", - "Lb3Q8crTsgPJVhF8hGocPadUG8L4Ko7Uk5", - "LVRgQ7MYCVQkGN96mywZDC7X4wVKnVScXL", - "LgFC8Azpm1QQg4zf1stvyhdmk1J3GKRXgg", - "LZyaVaUVx1S4R9RtvVPHmirBCxwYy5BLqy", - "LWqHKipQvgBJXFH1YY9Aqyvidz3JLsbNqu", - "LS4EQ4HUyMj15S7uh2owumLWarpMwzYWf2", - "LeRNyZ4chbEqkuDtfSqC47r6aMDnR1Gi72", - "LeVbPnsv8Kys7dcRaY3yg9infzmz7G9Hpo", - "LUEVtdDJ8X8jVdNiUEX1xvzVQxcsSZPkrz", - "LMas9g21ce1eyXByhr7Ngaygbfo45Q8N4Q", - "LiEwXax16TsBmJpjTe1w332TeyUSamnvvA", - "LeGQnoVFNRWT3tUJFo8SToE9KYy8Px939K", - "Lfq1tZYPSa6LJMWMCzEfPVjyMjnHAUPnwj", - "LhiG7enkC7hUoRVyDset6nG3jBmFAnxP76", - "LhbLG3xjaFjA16LxiWKiA3yz1UPuam2LiA", - "LgafdEoGQynfHUJZGHLVaQcrr69sHjDjVB", - "LWh4HzjkKUMvQYCmyEuiLhwEnzyedFGgRD", - "LhmNnMErWRvw9pBhEaGA1h71iBrxBe91Ef", - "LZdBrWEZYwevQWw5N7cfx4Hp2716tWEo2y", - "LYqypgTC2U1H7e4mWtmDQXEJuokF5SAcLB", - "LQF3JiSeVnhARcgCJHSNvzdqRvmJGsuzvL", - "LURqMivxZfqtWHaydfLtsUwpd76pjxBuke", - "LYvQksgCo4c9pR1id6dbKDwHPEETYMZXuf", - "LREa1oKedRNis7T7XJ3cNq9tBAXCtdcGiU", - "Ldk6o44cz6YxFNvmvYhhPficYgmLe5hT6d", - "Lbx8PM2ss9DcPYMmzZqFcyjyFaLJM6Cd4Q", - "Lc7TBKGbvX9HNsZGyhWWrXGCLzpEZ7cEdX", - "LQfigrdhHAVV26uNS3hCVRxJZtDxe3eVV6", - "LZLynLMfUvPVMAmNXF46LKeA8jfUmUBKxe", - "LWnKVg3QJta6oyMVNLo6htofgyRfupkA7Z", - "LKHnGKbzJX7e24QJL98f6ABtuG4MQjAX4Z", - "LKexRP5HvwdBLSxLG5FnB3QVXZqNqARFiK", - "LgMa3Az7WKhMYcA9HB9abzkZysdkihHy5p", - "LMQybwAsWNJCLhVukHdntHCZ4R6K7nqZJJ", - "LNEbx9Q5mvnkUdKKqBBjm3bVbvTKfBLm7U", - "LL73GoiYM6pz2RVMDLWDAQhyjf2QtEojsj", - "Lfzvumvksk5EyUTKiFJAcKubAUDq7iiDLR", - "LQfuDpy9FtxDuwH8KvV6v8U6CDEmLPCtLq", - "LXAwyHXJ5uXkEtJr8eERwWVDWmiTwZjDXZ", - "Lbh4tQwuELbkx5MSoNgeHPpLgkYgzwBqmN", - "LL8Sr3Pm1DMLSgjgE2tzTgaPsBn4tUBZMf", - "LaTtLUvoWMvjDgUtiu25HFcTXrq1B4ZZiR", - "LhhaJPMJGCK9zfoRqZV4FdvrqHrfBuZRtT", - "LQCko1GY68nozg5TSDYBMvTgah19GUdy9v", - "LiMQxVrAutJDdUoJYMn4HxPJU6MUJo7Wih", - "LTHE6Thf2TD3apc2k8bGe49uT8vk7Duumk", - "LN4HkCAKxkuQTd1ZxxG3MdUZL7h9956sLD", - "LggbiEPxm9q7qujBgH8VowJDz462EeocVe", - "Lai1PeadaURQGssF7wCJVkUCkMKVCct7xt", - "LSpBCFpxVR6rHNmQPF2Emj2biJ8jLHkwRW", - "LdBpRFJECJfRMbvXL1Jp6qVhpkR6orzeUt", - "LKN3hdMPteWLEXBQxi5pwXdeufFa83FwgN", - "LVJvfkYtMNKUdME8zmMAkNCnK8CPgx9hS3", - "LWfJsaBmH9ppZYP4q2tCtUmqe178f1HHw5", - "LMKE317FWXkzDMCTptsvPxHfCWSPsD2rDJ", - "LVsxLvnbCw2Veirdkm57WwJ1g6PezjQc63", - "LXLmZvvK19RiNLxUhAfmWbjQEjDr67bNyV", - "LPTrg77VfLw6sGa5FH31E8C9iepsxNzzDk", - "LLzSdkRuTkSBFVSjJjjMQiKrcMuCTsA28x", - "LfaJR6MJRJ6P8xoZK5bYo8ee2oZUETfeGu", - "LdRrMKNj4qLNpMXLbM53rLxAepF4baTpLe", - "LULuUSgDKn83sESXpTPZ2pMYkR4JVjQaBJ", - "LdoTfSDBuH83aCvWtL9sHwiFvUTXbYwf5Q", - "LfV2DPCW5m2WMGBGMhLUv8bqq4BwN37JP6", - "LXhYvad9n65E9FNeSGwqi7jHMQQnc1gKiQ", - "LMCDQqLdsHiXBwe6Qr5vmMnkg1M8TYU5XN", - "LiSTM5jNER3o6G1uTDosw5kVM1XvRBaY7g", - "LgRrD1BXY3JydbAiBz7DfdcurjnLpHyXJu", - "LPiPe2TxtCzs3gsiGK89urizhyRsd14j5E", - "LUbTszmejtBwnHMWTqofAvTAGSnUcyZhK8", - "LVgVwUBwP45kBjQLq5h221WYqqQbeLmLrY", - "LVqE67xxpsqahokAGYrnCbXEvvUdNgZhwe", - "LXw9igkHBKPN58f9UXRK1CAqoJSFXPr8yu", - "LhkjGCFtV1LbdisVmruFZnC6TdaXT1iD1y", - "LaQEaTTBKk8VudnfpGKKLt8fwu4UNDK7Fe" + "BX2Z1wyVEvTLShw6M8tkbYqMNkT39X37kA", + "BZpMJFvhYRqiwNBzGV8xXng8tNjb7Dhybr", + "BkZo1ku7SzyRB8DvoLza869Wq6iRsF42tz", + "BVg2zjo7C9zu8Fp8TKURu9GLzcWpNEBRcJ", + "BdBvunttdSYDb2aX3SPnKpVefRcp5iemM7", + "BW3U9U7xZfWZeRzm6ExA617B1sdmkteR2r", + "BaRmApunQ7RJ8tqwfreyCbFNxmCHtUoFG3", + "BoFNoqKXsCdoZaUE5ihaTsWywVLUGQoYQ4", + "Bqw1r7hbpHtjQ15jMy4MUNXv2KTz6KjoBi", + "BYVm3EpkwxZiWvCsbNyH4xo5h7Be1cezgE", + "BoBKoCqmQ3srtputn2b9iwTzpbyEnkQUhf", + "BYWNArud5GMtAYVaGxSxxjQ2GGkjfvHBGs", + "BZaq21xgw7PhsZq29MxTyd2tMNnrFst1pe", + "Bi6hpXiGQPpmikWJyHRUSssV5VcUbTNmgs", + "Be1F5GXpVEEWHtBaUJqFejdXXp7vKrqnJX", + "Bj8DeuofbLqFjNfq7ZFHMyHb3voZikuPN3", + "BrSmGnu335npq1JDyH69uB6GW9qWzHUZSH", + "BWCS1kNjncGYSsvycWrmLDQscE9QKuNzPr", + "BWzJB8kuwqHCBvB7tmakzXNrdXHav2VvNW", + "BUQLdNSu8oVQgF9TL8D676UeGZTde7exRR", + "BdM9nHx33cQ3GE9YerucbkD4aD6fYJHKW3", + "BmmCgSy79BsMRXdRJyDMxF4FyVvNP81rN4", + "BVoyTZPX1yai5DtyPjv4T95yLmMKPzan6d", + "BZRi5iYTLGHBjHTV28F8jLhj2ama4HsWE5", + "BcuXT8bv4rXLkqsF1yZyUPuqLMxzBBa2Zi", + "BdKN2PuSmKr2Z39R2kmgRTdxZ9HuBeAkzZ", + "BoqPRQSikW5dRKRXh48QhVJXWgxoksoGZw", + "BmzQ6WpwUUdYfvyRaZpNk4uU63MoFnbftq", + "BgQufBEwCniU66y21t6MJTrd9HFzKTZvPS", + "BYw1XLmMCPSPJuiJi99iaH5BJs3QaLPWzX", + "BcJLEpKqKdqmmYwz7gRp7dywvfFb6HodMq", + "BgjN7GHNtBoGvNLQgdTHzu1kT3CidJ5PbS", + "BjzNp8nJzQhnPzjS8TyqB291M8Q7ggMjNm", + "BXEfYG6GXwkkcqGpMrpqM68VvdJUayW88T", + "BiyHaf2WYMJSFAdVsoLaVsqeuiv3tgjdyE", + "BoZT69SDYK2RJpUFib6adnjWfrMqW1am7f", + "Bi13gfDNgVrfWMogNo4HzDzdaS4iRPZrXj", + "BoyL3mUbkAuW9EQvZCojDWVwqNVKfsyGbC", + "BqGX3L6UuPZjvDudwdqnk9sKFZYANXWeWa", + "BWjViWe2J4rv4Vcxjhgo5XYLKrTAKPbpAS", + "BYMR1PudaJKKPQMSFp1WtRsXoGN1HbKchq", + "BaxXKi4VCN2fSjKUcochPmooQXRX727Yxt", + "BXTo2NJ5vjREudppBV2DBgfLD2AvkN7btG", + "BXhmAXT2Wv7iSNY6fp2pxKJXV4295sCASN", + "BYTRA9sUZK8T5Yw4xcHdWa2myEw1FjGGC1", + "BnSckDUaoGDuVNbkKdYHF8JYbsrrWxS2UN", + "BqaVcJauSFRJcSs9n6vhvZzuK2t22SD57C", + "Bexp4DM51HjZdPboKBi3Ngfmm5NY4NsdXV", + "Bq9FwG4yaTE5LtBdMcmgJErXR6P7KmVkST", + "BVQ3vdstCBxJ3t11KkBL2ECj7TJ7Wx9bEp", + "BV4xXTMNfHHrLjf21pUVH16HHVCByzmGho", + "BTrX6JKDEfepRD6XdUDoMKHTb2kufc5z9S", + "BdLK3bXCDj5iXhbVY2oqyPsxmK8ydQQ26C", + "Bc8HCFMkfZEiLg7rJPbYnLDckAaYPqdXBx", + "Bdwq3nKvD474Cgx7h7BkkwS5bFPpNnZD8B", + "Bqz4ebiK4bxYauwW2a7TFcXMyGMz8gQ3fj", + "BmjhQyTbsefGo9maZFaSekmm7WRFonXE2n", + "BjEY8anDdqXju7jxKWXCRbLfxCvZYExcdf", + "BaF3PWL29L4tXuPE4rc3zPHjr5EY6B6wQG", + "BhnfWEuKRcAP4JUTrwaC5Q8sC63pUU9xCd", + "BVdNQ29wxx4yiDd1JGHPyt64EZJ1ndCDAs", + "BWxsT6Bbhv2yt4zUciuDGw9VkH1FVzLFZ3", + "BfyUhBon2jc37nLzrKXD779khSuj8ev9Ht", + "BWpRGThZsQsAY1hm9wxYPY5xatYYGyuVK7", + "BrhtmV28tyYHYFPVtN8aKNWxZs9yHkARk2", + "BphpkQsmTujddnuptGiBqJddBwBSFQpLzC", + "BgyTAypyeK65Tx5wFWAuUkXadcH2sGTXG6", + "BeqGVyi5VTDhighrMTJr2E87R9WSmnFb2p", + "BhjSXy6Dj2mjQjh3Qa85p9L5uph5qMmxaX", + "BYgkbAZvikuKW3rMBZWFJAM4kdx7LhKzzE", + "BmNPfYfmWLkVNK7NNxbTwgn9ET6aQeuUJ2", + "BbwqX6JjxDKqCX6NjdAADajXZiuzAQh4m1", + "Bpobpty3hSYJ4Wf6tPiwZFqLedaDCzhRre", + "BbkGAtZpk5p8cEHcrjUsrXN8sk2oPU6ktt", + "BdQJv13vCsN7i1XaKtauky5wetRcfmvawK", + "BbWdRm2B7GoY6dGoS9BkGbr48KrP14e2df", + "BmE5zyT6xP5L5JbXspE3vnuqwseUVSW4tu", + "BiHvfkQ8hCih8Wr2JAHn5bx37KWGb8iYu9", + "BgZ2zx5DQ3q9BQJkBNnp26P8uqjD4JKrKy", + "BoHqyTvh8BuTvtgoxCXwYow9pRMFJdMLir", + "BmyXXDMw5KXoyuCX9Fv5kZPAX8QHhFhnUb", + "BW54jFkBvasDPy7ERb54RvP8e7XwCni51r", + "BrBr96WhD5y1HFJGzSdR9ZUjjpSpiYdwaa", + "BV8eDyYEQQMcM7jxhcYJgsA4G6uGDC8XFV", + "BfFdh9CHK7f6SAEDio378DRgadFnYwRhRn", + "BoNJzPvpmCYw6btYecbWUhfM2sabbSRVuP", + "BfQkCpjFbNohKogj8e1kkaDBmnWQuTDG8M", + "Bm3me9C4JGu4V3QoWyYiXN16uyBZbMHySa", + "Bf2PNzgKnXGPL5DxPp3X24SQFeJ8czK38T", + "Bi2VMxKACs42pBA3nDta6ku1FyazFy9Y5w", + "BkC6ZLNU2JL7cvbvVSkeZUKm9DN1QdowhY", + "Ba1rv17b87nZtN6xVHkyKpA8Yzc8whrjC9", + "BqbHmEj2G7RV33RZfupnSsjDJ3mJeqjHXk", + "Bc2TxWoL2f4tumDRXGouBCDFvVmQkit1nS", + "Bg5rhkhrB2SwzvLk2GsE6Gy81gB7dRGMnQ", + "BW7GqX7XLfDYNJec6VTxpngTctewyzs9yu", + "BY5DA5LgRJYMk3amUp9kYanfjYX8zq6Py4", + "BZraF6F6V5Zz5wf8xf5ZpvPU3dSMw5USUn", + "BduWHJLQa4dWvHpQRpmHu1DzkqPFT3aG92", + "BTyoKQqvGH4wnZG9iMn8bJcRjHi7DPuiSN", + "BeeLGACCd9jA6MVSeiGnUQx3cdBMKCXqTt", + "Bi68S2s3oJLVT7Bg92gDkCfhJFCUmPBSyw", + "BUckFc7MtgT1aEMv4YhXEwEeJtQ94GWBsy", + "BUdqy1vKDweWXm8deDoCb2WaJeLNQmrvLy", + "BdbtqVPhXzPdqbpCaV32HThtvifuT5XzEX", + "BdNhkERVdvRVF3ArZtLN2kGVNJqHjGVq2n", + "BY9b2gPpmF16TwbW3HVaUVUQzd7FrErfwK", + "BYkH7qvaK6KhpP1YFgibvvdX7b8bUQ9z4r", + "BXaB4PH5xBb2ggW7HAkWMRVAq9wC54twov", + "BrMY8pkfCuWudccGgMbpfUyCu1cT4PomoX", + "Bi3Ko3NWzDQ2U7dMUk18ZwoyhAARuysyzd", + "Bn9Q4g6MYKLrmA77TTsprtpf3ZY5sBwoqa", + "BkFtJKqvxQiv2Ef8kr6Px6d6qGHTHcuMER", + "BkBMFuvmZMM4K2PMeQyKhdkUYtSRieopDY", + "BkXJkeShM5EUBAxjFjTdVJ3TX3tuRFdreM", + "BWrp61jzW767VE6mNcA1NzAReWBoHbo1FU", + "BWFBHUzeuVG7QedDC9RXTq5zHemFn4SUwF", + "BWqba5AA7bGSfQA2uimLWyu6SzwzGNzjBb", + "Bn7eHiYtmxX7ZLctME1S8w4vPWt2H4ALNe", + "BgJJiA6dFXUkciN43typv8MxSG3Br4gD9B", + "BZxrGu6wvw9iXsDBLBEWzr8FvEoxVZWhbc", + "BmhYCrJwUZXHrmCynuNc9cSZ4mASLP23cW", + "Bmd1VqwtDDgs1F34FbVpouXQf256BBh9it", + "BnbkSLzf545yknor2o7r3gHAT5Mm4uiYXL", + "BnrCz42h4Q1t2Tw2mDjYdBn3iKqkrwpoGv", + "BrQx3DhmDMmr8QC4RxpoZjkwivoCs79Qyo", + "BgsqVZDQM5gkur5BprwNvTMzmqgP5sYuAW", + "BgiG9YU64yzm18YgQY4h5M8gNwMqLmSLTV", + "BokzVTBwCCLoXC42YP3f96gecmSfWbrCDM", + "BZfzKGhTvqbSwjWxFdsX6c4jGcQFaZkePx", + "BqWgRsAk4u3HCHVP6FfnVuEGFP6i53W6eB", + "BYvemh61vU3waZv4BuNqT15GYJ3SgDpbXP", + "BnuzbT9CkPULxXDJejgMmSXgVQTAYYeGqV", + "BmCsKs2TV9T8VB9846yL3tS4AbJJ6rwKGt", + "BYdyQcyDsK45rZoKGL14siTgjPPkhhUx7B", + "BU5v8RYXeBjZzVbVvAwrvrhMuNGYERrcjV", + "BcNBggtpMZq9xQKYPs7hRp2NAeEZPEUc2x", + "BoQ7cRNm92RMy6Qh7F13ViUdwMukgFr3Tp", + "BcMrMr4YrvapCGCg3rmsa8uH8sAF1zqnM4", + "BazCXsc6CiRA3vihaBBnAzCvu7PJJN8pfD", + "BqFsrSSJZpenffvznJiUat25oe2L8xBTpU", + "BfFD8feXuGesCgQx3TPn9Z9RVnyqQUMrfT", + "BptAPzCrhDHY5Xk3hFLhTJAPKx6hXdFjn9", + "BUMZJqXYPUuxUtT436gpM6bdnyLbrQmRNY", + "BbhQ7w6u7hTWQ8GinVzMmrRwSfyD6656gE", + "BYJYz8eKRYCRkHUePoYYXgk4aaFKxbR1a3", + "BeJzKouiER9CmrfqCJP2Ciuh7JCqL8VFJT", + "BkonyZKBqf6To9VMHqq2VnntpD2SGopLPt", + "BnYDfJ58ghwn8F98BSmaGaxfKZ81i1LWPx", + "BU56eDu2eLeedinWo3Efa9wcRy3fxP3Nds", + "BfdA7cVwrv47MMrWPFmgiJ9NZ2i4cinsNc", + "Bns8Er88f7AiUxYJjic9fnaqfvxM7nWiRg", + "BpzCCE81n29624jpk8377yGULGj9c4d1o5", + "BfZaMXGV1WTP59rPUAu84s28ivYoqTFeGy", + "Betdek2Sne54Nx5tKZQdEmcF57kVMA2f2Q", + "BnS34d4VSS6Q8HRiEMzyw41TsDHxtMGM5o", + "BaM6MqSb5fGvWTLJSxsvgvEZyJY42t1ioi", + "BVbQJJE4qEbVz9d4a5zgWmbzZMDqsVdPrj", + "BXP6Y1K3EwnpfeQbDBvHoDhiuiMfynsNXy", + "BUMW6ZqQbQnWkPG62UhkyBifCEc7QhcKFB", + "BZuiUTnG1qUpx1F3DLiBpZBJgALDFMiLea", + "BkMQDtq4XmBmrUcrqAJD8hsudiTD2uC5vf", + "Bb4cASKGZZbnjWsJMVRGiFUsoaP8kFBfGZ", + "Bs4tK9rmax4gaL5eHEd1dvGatxJoDkk8FL", + "BgBD8dCTPkzkz5TBFhJZ17diosdh3g1rE8", + "BpMVXtziMUJh23WKQWf9MBdYzDUhA74XJW", + "BbzfeseYs5Cq2yGFQCUN5QQf4PqNkDFaac", + "BYES3oQAcobU5G3wFwu2MiYCupJ7e4QyWh", + "BXuX4dmCh4uVnkdPdFm6p5u5rcJVE7mbbe", + "Bk1f9eD3SGDYgELVpKtVyJ998B3DZbXGAr", + "Bnk18vdaxXUEo7hjWNSsYDsErwRf7gJR6x", + "BkwEcwW4MWbaQewdkFQVipAHs16tMrhGAo", + "BbhExczGVLFemNScqp5fLJwV9XeRwfRKXR", + "BazQqP2aM5HdHJzF5AgTxLo1R3qPqRBqwu", + "BZi9qC42ti1XoLUS1Z7BYL5f9cHqAYPPto", + "BokMJMbBChPQZ15txZUkycQzR73yjqa7ev", + "BVTCSvN2n79Rn8Y2pMfN4U16JqWKi6XUjj", + "BYPeeykCK1ZRu28M6y8eXYHSKT3cVWcZzx", + "Bk6GYbnBZT2gvQrCnMoUWu9EtKxYYT2y6w", + "BcJ95zwqSQVfXrefJ387PvkRANsX8aEiGQ", + "BVNphLjDhfYEGnLjYGwbPMeFDRgLHYERpP", + "BXFnZvVMkEw7KgZLrgeyZjXazyS7seiPGo", + "BmFWRiWrs4mLPnKq1hWKxALde2SAEYtwbT", + "BcVja5CAKo5YbYDJDXyuZfJw2QhsxrXrF9", + "BbsrBzBAcsRtAdGRS2r97ox6cUz16oyYhe", + "Bkh6GhgXKw3MHxCvo8vM8KPBRWf3emaDc6", + "BbEQJJpiaAF8N7u1TSsgo1P7pqJbFaBay6", + "BrM7zdwPEGTGDKFa487hbEm5VCMuqdQNmZ", + "Bc2gscZnDE3R56eh9Nio8BsY8BSVWHqjhH", + "BYK8dP5gGo9quJGxtBgs8XV1RappgKQHVG", + "BhkgnuYXwNaBrh9FczXoZbHVjgitu3rTYm", + "Bp97iLBue9yj1HfQuzyM5ZRUtHzGVK6xQF", + "BXPVknJeUcxXcwogB3Fmw2UPfreLL3rcQ5", + "Bhtup9zqeBLGGfvYTXB6XUhXwoPMXRLzQp", + "BdM6shNPRz7p2G4rm5PJruLxcjRbP4qS69", + "BmEDsHudJ9LqFbAMJfePNMhh8AmTdEVWus", + "BZxLu3x5X5xA8NwaMeQErjkRQiLbnMMnxE", + "BYzCECzn41BKMBbuVGTM4zPrvorgfiSmLM", + "BTtiV8u98yffM868fXUjEXe6kKTLymBmXo", + "Bh3CEVanL3uXWma2i7AEj6p6e8nmRiyZAK", + "BapYEFKUZFUeKYSkYyhCT2E5YCqqX72uoC", + "BqvsRszzvppJqozz5zWStkGEdzCcM7EQe6", + "BmaEKYwDLqh2CmieB5CXwm8ca5Y3cMWS76", + "BYyXyaPiBF1BnyPfiWqVoQWbBTUgPKoX6q", + "BZNpWMMnv3pjjTrJu33j1tzSTVwzrLB33N", + "BkqDH32Bah9SKox87nq7bEyFbLBTLi13cj", + "BjunQGceyrhykXQDXCgKHuws3NZgHKbq8w", + "BYH9zCpvAvT3KinDtewCaT1SmA3qEzyWSp", + "Bq8v94DLJA94L6k2NzY1g4N6z7maTyTRxS", + "BkqaadQVz4QwX8tMPrspkfRdUuF9rokQ99", + "BVVdzrHPe12Avj22V1gewhoYf5JhvGVE6p", + "BorTVfwNkbgpNmSyuLkvPFHi6obHUyq7KG", + "BUHSTa7QeBi7jEi3BFUjKdPwi7js21EA81", + "BXSEqrfLHHqViYtAxTgy9ZRim7187KiNPP", + "BepQgbqr9yWz3nEnzDBDXb7VoaZKqq6jLF", + "BpHUBeZrqp1oFpetkbcSA6etfAwkBeJxQC", + "BYnitMuKkQZUmhPYn1HrzRC2q843TJnDqQ", + "Bi5MgckPJa9jhHcGdJafdrrK5a36L27gex", + "BpW8FSxYJsPBB6zajTp9cVuip2S1xYR73f", + "BaWDoyxR5GUnE2JcrNUVpbh5jfT7SryLQp", + "BZeUF2pinhBLLMp3DA92KL48APyieUNLWx", + "BY9BdBJnR2pWa9cJj2Vag3gg87oQJ9yMHz", + "Bs6fZvSQ6BSyx92W2nxaVk1qNJBehKsrHD", + "BXFyzsHDAK33J6UFUPB71w3dBjppmJJJTN", + "BY54V3kfWLicLQXPe8wKi3jbxp7r7mfQvq", + "Bb9cfrd8SU9wAFrEsqFfyTW7NNo8nASXZK", + "BZv5BRVgqhk1hRkSKtyaduWzzaCJ1VRwMB", + "BXaP88HB5uk5vrLRY4cKeYvjawP3jaW6Av", + "BhapBL2D9wuwtna7qGmdh7WNxadBTBzAkD", + "BioNyLq2BgLkLBLwucRm98VLPJ4AbFp29V", + "Bc9jpNgKcTVVRGboDecreUByFwAdWzfLxk", + "Bq2ELYgSHPnwCHDcKkCF3XM69jvc1V7gyz", + "BmhsGU2hdssp71awB9oZyYwLPF75wNTDkx", + "BnhwvcjoMeTKryBVrMLgECopEGb79NQe5t", + "BYFdFszJQsrm1xjwX9ZTEStbX9ELQE87UA", + "BhBkKRn6GGXUKyFqBM46DPeVLaBRe9xi9o", + "Bhd6aubBB7zFZ8T6ifhgwPBqB3K4KHFgTS", + "BXUqELPAqLbNGF8dbzbKmg6NNcUBMC6zHx", + "Bm47Gz2Z1fuGwTPtdkhZqVRPNTqtVfFhuW", + "BWCj2MZvP85MbdJgkoA1CrnRskm9z4veX9", + "Brj8p9gAhB9aXFyKxGbABYkD1hWG9UbKN7", + "Br2s3H3vrCJKRMRbdNfPiLRNwLVqQKpy8R", + "BmyTL217DWmsr4fMCNatGS3EufRXnqPcUi", + "BWcDsCZfuWArGteDbjPA5Ch7U252Dmys6f", + "BVNU8GKmpF3hEZnBSauavbpet22eeLW1mV", + "BdbPH7wGqZ6dp2XWNQun4m1zv4kkG5dc7y", + "BjYuoG45ECzEWG6cjgJVSJtdV2oPCiRh2G", + "BYT6xCcFtfPgVHRQ6iDKtumes1GRf5Pte2", + "BqzLv86Yxu8RDdUvVJGpyZctjibbZtZ7Kk", + "BX1VYsmFwPMxoBTk1gCBsnfEhG6EGGozNs", + "Bg5ATyaCLGm9uUgnBn4jTTMeJ8GDNBpRH3", + "BoXH12vrmc83VjBSLf2Sk5LuyX5uAbxTk8", + "BnLbRFiCX27A6iSQRghDpmqXacU5JFaQ7s", + "BjN4TjumozuDuVNFzXsJCUKn56SKTj2Zix", + "BaFFTzk6BQKZ4aJkCh48dSdCVNJHc3Lw3v", + "BfxB4svJQzKYycR6bSg3ddVf1VspTfMCkZ", + "BeBpTVzYG9jxbScj4k8qXcoSd7HXPu7QC5", + "BYzTq7y4k9n6LhSdfBsGAT8ui1tREyeTHy", + "Bomfo1DDLeUpqMVyUJYZ9mW2N3z78PXFm6", + "Bb1Nvexe2j7ckq1ueqjoUkpFFitT9zuxzE", + "BeqzqpJHJTDwXDR5UJ6xFvzehpQn3EhtCU", + "BpM4Di5xUp3eRxGtp1MxUT8MjXN9CfKoAf", + "BeN9hUaS3fBCmQyZcdBgcphjpWQyZLETED", + "BdQB1DS2eDQAttZHWe5YSbQLSfHqz4EJX1", + "BasRuymNYaX4GWGks25qJfwHGUyYzMzV2F", + "BcM2u4ayN4USz18vFaA3GpmW7jVtr64Gsd", + "BXndLXBiqt4FaviB7ykeJwjBom7hiWmaTc", + "BkFyiccDn5ckE961GzZLub72FSPQjAoBKV", + "Bovm37NwaCRAp3iYuYEGioq9ogfLiDQc1z", + "BWcjdP31WJNWk5NmAFBo2cLmTCKnyAgKjf", + "Bem9oYB1FDCYxRDKuxDkWuAo7MPnwpHZte", + "Bf5sj6uqh9GcSjsTrMkD1Hq2MKUcjj4RhL", + "Bk3jguZrP8gWcEwPYPBax9B5gW8yncFVYL", + "BnWi33TRuzRfwo1cebAXjKoTRbtZvKgama", + "BkuzsnBZoXJe4qaW9Re9kCPfqnvhufP8y4", + "BrJpao388CytqvJ8y8NfiBGAbsJ8RShQDS", + "Bi97ijrcEMASoGmb2uTtpXBraNaZFYfPRb", + "BjVSpVA2pvWcy2mrLUftHEgzQM8tsxMb1q", + "BYzwALNQuwQH6yNPXDQ1anR7PthncSfbLm", + "BkHzy2nzZPuJ12vHhYaRgfFTBjA5w9bRdi", + "BgGMWn16DpSUsbDDZeFtjXkKWrzqF14Uco", + "BU34Y51S2nVhPmJQnch8BnSPW7Eo399vgc", + "Bqz1tsr2bAd4KDUXqPhSHnS6yhSEftyTng", + "BaaKJ3uP1vSMZVHvmerLvhr6DQouZMmBqX", + "BYHbZVCt8tutj8MY4aNrtfGe96PHPDGRkc", + "Boe2Ekf8aHoEgeW4F9AXnQ5capkARQEBdd", + "BoeVhmaPT8s3ivk4bpe6GDK4Xp4viXABkL", + "BpVtpo9nENwxqycdxgdgJLsixmEuT1BhXf", + "Bad6bYPMisHHCxR24e8LJ1VJsd3GuX78DG", + "Bi1G7d4e58iDHDVkrpRnCnYzdmwePi8fR9", + "Bqe9pzjEp5KxSxaiAy5VoMgDH2YarZcsGu", + "Bnmt9kziztuMkNHdbRAhb9AvdqtGLgyowE", + "BWFGFm7EWNPGhuyPXzqv6wB2F3n9ofjWDo", + "BULUKu6wgmmqwZZUsA6acZVYZxZ1fpHrmA", + "Bitus8X5mvzMu55VETDiUJyV7M1pxydfpb", + "BgSZ1VAgYkr2j8p2BQzHsN5mQjvyzgmxDS", + "BTvQCRzPQLdffZCh5GzUfjY714JPeHBtko", + "Bn4kLfWYgSmsXBWEyb6PzXXF4BUMiDQaLg", + "BkRbtEN7eBm7S6dGa14yc6DseFWgieRPXt", + "BmXeYnHCS6Hk1qLavpTChpSFkZPyPBNPVn", + "BmV8ZZ7MXGEK2QM3oPGB3rzGSuNJ9z2CUf", + "Bi7EEMu6ELoYdR1RzrHXMehDinC5yWjaiP", + "BaJUZdPwLN5BZ61VMwsgRYe2kJyRw6AiJ1", + "BeBNRHf77CM68e7K1dDMVtJzsw1oFiGx1j", + "Bgy3yhDB45DECPimowt4sSkXjRER9wwJCe", + "BhYAmikHCeS7ctSTEZ9MfhgdnMgveXPWsx", + "BiNCFhwACYvegXso3Jaj7DTea8i9jxVRQC", + "Bq2u6YJTy98jZYE8KRTU4qVuxBPy8GFFia", + "BZYtDjLvVo5mCjpg2h9vwRSp5xAbq7ag4h", + "BgFQcLVtBThjQGMJKCjZBxaM3gPGLDiuF2", + "BZn4NVbtbZsvpUwMpjW77BTTrnYAgjUW7e", + "BhcoZboDkSc6g7M1wyx8SRcg52PEkcTRMN", + "BouUdfg7YZ3wZ6czmKRoF1roCki3JZZZjT", + "BXD1Qu8yEeLqTrD95XyYEdG321gftEyV28", + "BaW5gG54E2MsUFmdNbav8NaWYNSMWBLitX", + "BnjoCy14oHBvNvWuNrmF7Yd49u8ErAEvF4", + "BWds7xz5sF6HN48e8go5SAtatijnyh1FdR", + "Bg9wAn8reuAjnQPknY4ATU1hD73f3ZiV5r", + "BomDFea4bsf2dKbBeoHFnh8JZ3FKWo3yCH", + "Bc5Nygx2Gr2Nkb71v4yvjTLddgFKiCTJSH", + "BitLphGdYDA9X8f32gkF3UwJiuDs7mkKnf", + "BhzXkEYjDJYeoGTNomG8f7odDQNATuoiK1", + "BYctoitrJ9BFxExTmQJx1TiavABGboJ5Fm", + "BeVPg7yRnc8jYu84juZUzYmXQ7GouG3xsb", + "Bq2Z3dXX2SQqhLDXJNNT9YoApytXuVtpgG", + "BY3TkH1NNGL4aSoBhBWsQvmkroiY4i5RzB", + "Bb6GWWcRLeeyXXkq67nkmCGWgNsjeEtrpz", + "BiMDyViEUamPBpQJtqfqhaToBg92VSALbd", + "BoVudEkaiDzPsrNayPN7JNcDQHQVQMFBeh", + "BoqZrH22m3u15APVstMzbJyK3otyMHk15G", + "BU4GBLLJqdyJS1a6Ua5CnqyZtBi5m1F63H", + "BrbqF2Cga5bcEHyAnGujy77iGc2GEQan1S", + "BiSbBVP88mnguHVd8DE48WEyzTRdhUbzMp", + "BmCBts7YnFrVv8fmjBrcjHatMzDvSnd9GX", + "BnHsgvVjoVXCwJUpfqozSAMNuFEpNSucjQ", + "BmmjKNg4iKhFRFD8NYqhtg8U8Hv2VzV2DA", + "BfEJXnf7wq9PRCXBCmvoBZNKFpHCd5uASS", + "BaFLARJZufjhrnvjHoB8QzmL6HZY7ThAgX", + "BZ2nqe998paZgwNH2WZCMShpEFRPh1eg4G", + "BgSX6CYpRAAwHuEhfrQr2m9QzqamHhfbv8", + "Bpz8Q5CujyJr2xAV7DCXrZPFKAekkX7hVe", + "BeAwYwKQnEqBrFNY82hAtW6bpjn24i9YmL", + "Bj4eTaig89iYhNU6XWhew841pJeJLQKktG", + "BrfeQgrfd3LNj2VTrq6yH9yG2aD84mgJyB", + "BhuYSegzMWMUNZzYRoo41JFBdaCTNvUcg9", + "Bq5bJ8VWnLkSYZ5LkLp2dtWm7nFf3v9EhA", + "BZ5VUsyhzjzECk2h9V3H6dUCvEgL71VaN4", + "BeQQdVbarpFuA2va19DxGjbYsjBSyNG5dZ", + "Bjobw8tVhaqCzYvWGn4SuKVDjVSy9YpDtj", + "BitxM7bBzNqPKtAUBTFd6MEcKvELbgrSGH", + "BXeLeFKo4nsdBat4k3r8CLuEvofE6HCT7i", + "BqEMvL6w71Cy1AZHjFi4bBfqDwRSzbyA5p", + "BWSoVRhQFf9Y6pCJsAt8pfbyLU6qH47DSK", + "BfgNQuyjMhCzRTYUUu3Dwp1Z9tgay4vTfp", + "BVbMHSuNN7nGcwAD3pt1fC7BJsCuossFtx", + "Be2KdCA1yw7oQm5p7mJvvYYSirrffFDLFA", + "BYKAZJKH6u6Y8eEd7Fft7tp7XW6CdcNUt3", + "BpBikpgBTnmLyq5ftLpRTaDpBKC7Jdj6ks", + "BVLb2ZUUeNCy8BD22uxo8zgUaasTtv9d12", + "Ba1araQbsFYmUMLpn8caAyKZ5iRdZ88Cob", + "BnS2vXKgA3btMF36ryxeEKN1MCeoJPGnaL", + "Bj2Jdq5qy5WfZKHgxPnK24ezJvZiCL4QsT", + "BqKA5XQdCyuH95PPYvhdwpHnk71HfihtRG", + "Bo8rLfRPVoQwEguJXFdh4nkGyR3XpNSHwV", + "BjL1hvNDFpi86Kp9cLim7DuaRbEVKBtSKB", + "BneywpmhkEPYx3c2Yy2bc4o6U6JYFoxq7E", + "BpMimTx8Ao93fz9Lu6QfeqsZw5Lxf2ripe", + "Bi5wpb73KeULsq6jJteZqtE1UnAPudkwBQ", + "BgJM6t3c6oH3E44BHMeA76RTbS46ceh6ns", + "BYM2t542hD9p9shnFLmxzNG3TjoAmiSXQh", + "BobrBCNEoEPa4yfEnPWv2tcGsvZPZkQP6w", + "BfJ2ctzn7gKFHqjXbD16oUzKBawgcZpcfS", + "BgAiLHRX9HcWYE7qY3RPHxbFAkJiwBZP1H", + "BUFQH2Ykc4MJHUuoZF8UNE4CpnWbY4Khzt", + "BhXriJbBmjgoNQiyarh42Dcnjs2VnazXQm", + "BXPVn3kaD8hitYpXcQEMdyvDrZKU8CtSYS", + "BpUpvJGrKPfyGW6Tnh4yD9KfZP6C6dXics", + "BpAx67uqbjKDmhyaMRkZDz56zASq1Vn1cZ", + "BZhyehfEA9xGeUYUMu5AECZreknPKG5JCL", + "BUvw3RmeMYVNs1PQxS9F5GpLhNbE1zQb76", + "BZpx5Eurw6ftgwPQWXkfpTJrCKP6YxwL5z", + "BhCxuTaw1t1QfAwWtHPQaKVfzn3L1ZVwo8", + "BqcpmMaFfmr2yGhcSZQAfEpzpxmLbHRYsD", + "BUSz5sXcgMS6o7fR2qxwyiXp7ucRAd31G1", + "BZtXXb8Vjx7GmgFEEzVS5KoCiDxHHvPgVo", + "BU7KMb9KEUuXxwjTPq2SgDVmAadFhZZ8iv", + "BY57SaRXupd6YC4wn64veiLH6gJE9c5jUH", + "BaBXtAt5n2zoBoaJB8s3SCGCr7ieZd3spy", + "Baco8GBiMvYFER8zmzKodEQFjuSb5sS8SG", + "BrjQHK5PKVRp9evsyAP27akV2AwNwjGfZC", + "BhNQC4ekebNeCZs3hrPJzBMTCRxJ4i5goK", + "BgEczdfWyWJHBf3ZRu79C2zZ4zistCCEUX", + "Bb9j6g68Yc2mCNRb66jpMrNPnWFCJUpKs4", + "Bc4NyhmkRAZedrZdot2FMj7ziTeSVWwiQ7", + "Bpi4qjqhVjkKcz28tbfWWZqYnZ3HhuqT8W", + "BfWmvYKLbyUPYvi9aioqDqp9dFNHTkbcig", + "Bbb7buqNUzWieqjuxiBnDfbepNWCeQ7TCS", + "BUGbY2E3sQCTLyL2bwKriW3Z2VspbVvwm7", + "BeCuiNx8vJc63YHUrMne8vxbKjH4DE5eHR", + "Bs7jBCrxLUSC9UgQj46iGHkpdkibmBeRiB", + "BXtyerD6EPZ3UAAuMgmKrgPj4e1aXxvVAk", + "BfUCb3PT6MW1Yjruf5NGZ7ZTsk7MBaCcFY", + "BrRVoGUpWyAsuvC8ivRHKu6VqRd1skDkm2", + "Bm2Gxw1bEyJQkkHyEcYLJfGBqNcZDdVXsT", + "BeZK86R2WQXALMourEs9XFFyg4nEYfXa9q", + "BTvvbDra26EKJDLpEYdBJecaFbF657dwrv", + "BhrSRUu41WZvoEpJGQ3QQA64QvXBTVGnge", + "Bq79KCv64fG4T23HUwgM2XXfpfTBEjM9MZ", + "Bmt4X2EM2sRb2Dh8usY5txEBimjd3gtouS", + "BmfmYocYYkHpK5YLL8bo4WnabHKpTYWkVK", + "BdtEd8GtVvNY7KrroUAEcroJbijKiKgSew", + "BjgFEU4E4EMDzX88GWQgVtahVQbmSCwGmp", + "BcbfSq1BNwFbiurEyis5xEz8NYCheMmMGg", + "Brv9UrhHcPBVdScWCSFCCc2qfidwind6Cd", + "Bkrvo8iNBPQm8axRg7BSFjs95X2vvWYuA7", + "BcaYLFeMfZrwcapHxDdWKPdXynWvn6HtoL", + "BWXsnsFhJWN2hUWGcqTRahoSJXk6kQr3Jm", + "BoXenuUCJbq8xbwHJoq5y3zNcgk6VjVZMr", + "BqsBJMsTuXyjjEB6z78xxVHS3bhtEfLeGq", + "BnciVfWHi3cgJCuPwk7nxVoQ1JVV24K6Mf", + "BfBybvD2mSYeeB2tav2CuVh6YF1F39AnVr", + "BXQEB5Lq65JtnrnENr9BvGKr4zHVYDefbS", + "BjKxve9jB4N8UUxCm2DxdJKGsTC3dW3zhH", + "BWXa54VHr7EB8oqtzZXYzXRbBrGdXYXtWj", + "Bob4HEQp5CR9dj2AQLDh1p8g5V4eDCNY6R", + "BXAtMibRu95ZQzWAqRyJUUjAMAFhyA3BzP", + "BiLon3yuudA6vyToZGfc2LtEw8q1R7XJc6", + "BnPfFJ386zmrdj9ePaQKeS4B7uGf8pui3P", + "BpFT7wCHkbmjWHTyveVLK4rSFDgs33iCi7", + "Bn4Vt1cHJvd2eEzfPnghK2XJWrZvmf6iQt", + "BnHAfSXWr8wdYTehDjCqGudioQnGBC3Uxp", + "Bd9FNMRPo1q6wrehzJWAxo1Exqrgy3MBei", + "Bi8cLaHLNp3NDRZYwrZ6oGyt5XgGuy6Mmo", + "BUVBJwXuZb6QNaoRamAcHZvGccZdkamK3D", + "Br6bJe4iftJU9gxg4vso8WsjUaeScGGXtv", + "BpUvcCu9GTSsfBFvmUY7iQHW13wCCzBNSt", + "BkhrWQj8jMroSdCnHmYWQaZJGmiSpREcT6", + "BnfFsarJWXHKRAUZvYpvan7nAs9akyBcqk", + "BYRsXqqwi43P44Fzgnrx6uWuwCziEPZ89d", + "Bcwo7QvVBTsSfx7ygxYvaviBfjfsvd7DCw", + "BYzGADtRL7Rp712byLBWVrRVPNsqw2scxT", + "BdY9JKkVZTXc9aFFQQPUXMzpRUCNPtFhou", + "BgB17dQmsqTsSJ8a8ygmCRYDDMer2CzuAq", + "BXQ8tykJKAYYxDmCoxk9Lo3AJPig4CoHmW", + "BX7YyRPRbK6UVYgvEHiBKjCXbj9ggVVxKz", + "BUSVGx1TNbdvFsqtRdzPB7KSj1r9mx2C4x", + "BZZoqZ9AZxpYoCWguYiLNjSLQB6oAS3XJm", + "BWZRRrftVuw8bM9pA5Ks1yoKvDXUdy48Up", + "Bdw5oyadbq2qU5XVAv3VYtUw3L4NRw9qCQ", + "Bk9JHDQ35KX56PiX7rkMnvu3CcvpuTf2D4", + "Bm31pc2gtjUkkQsACcMtAGJQfuwzDspoZX", + "BbBatkUGvY5i7Ap5sEXxaed6nxLxAQ4GzE", + "BgCcFgTTS3e4XZdWvR7cHL2uRzhiJGdqoT", + "Bc21TTeqoQM1VNnwhxUTajJtUoNZs4yZHv", + "Br5iERJvWpiv8AGJxZaSwFFXYmmZhWj8wH", + "BpCJvw9abbxazp3Pw8huSv4zgNwAVqBNfY", + "Ba5MMcuajsEM7yACiacLWi3FLU4kVEy2CJ", + "BioMp3FgjJxkG7s5A2Q1N6kMhTMBoS6DiJ", + "Bfw276uAiPeWbPK3R49fWfRJdUNchTgYVD", + "BiJLwZFRBisEzNy6JFJiqM9YNPemTGHurk", + "BqpHtBn9sP8eApCh7zuHX5g4ZFCS7hBEkq", + "BoviRjYkN7QyMrkj9BaXYweETdwA9xr8fx", + "BYoNPkFBpUWysnjxK1rsnz8w4gkAUkjABY", + "Bcfq4Z3vVK4JwX1cdu6x56CSAuYVctKwYo", + "BdBggtzc2oSR6jBT3fpZBM6X5d5Q5hNRsg", + "Bme5afXNSVsUGC4Ec6gMsKjmSyQnjx5Jn4", + "BecvqyZWaCT42AdVh8rSDLYXcpp4MS527P", + "BcU3BtzyWny4f56SYkSipmm5shGs8CSk3j", + "BcioQsszTThu8FJvpsc95JEyogc5nkAaJv", + "BaQ7qcTwu2F11fE181uT2y6WvNM3rDXrj7", + "BheTahmPuvA2bm5xcdDtSawArpgm6jyz76", + "BjEy2y9UJ6kHn5eX6MSSc1Hh6AKxVB9Uv3", + "BpHWrHTANBwSbkSGjsSUss5BSEBLo5YWMz", + "BaGuLGz1mafC8QXmZAUhMJbj1FkN6pszVi", + "BdRupqVkrXexHztbsyr778uLwLrK9H3APH", + "BnsDGv7PxiWdX3zLbdSUC7CJsxWET35Mmq", + "Brabfr7x2wfowWoe6ZGsdoHp8MGdTjudCC", + "BqnDx65id7s2raSQ1dCoqaXCCP41SFGPh5", + "BhtomJn6H77YSzSTASAk6saZBy1NNQYWxD", + "BfSnRkF1n4eiER6eD7zhqBh8bA3KvdCark", + "BbbtC2gwfEnW8sJdqs1aCnmEMm9B1uZtMz", + "BYEesQS5MvK3ADrhuMuJHdb8ZoenxpeNdm", + "BhwH4TXgrBU9DmqVBAYqeTFgj7ye6Qb1fH", + "BoGcm4xfNmxjzi8yneXEuZ21dXUtMjvEuS", + "Bc3r8FLek5Ao1jErP4GdiqjJ6KcfGuy1UT", + "BcYbbGJaCakR6sX8XGvhyTBYkoJnDEMuzS", + "BXNYZvUVfzgQfmhxrTyanmRbLu6RkAqpQn", + "BVuZFxSKQ9z2gr3BSQ1vRcJAWQT8DfEEpa", + "BoCTdHzzNoSzpVaoQpGSo1SRRBPsWgxHd4", + "Ba2D8sDHF5v9D5qJrNgvzQKdjqYCXdYSBT", + "BVgkwmxWkDSxVE7K2Uv7nj4fjUNgi2GWCk", + "BhpfJHrWpcvaDz4z5jioVHwPHWkgcN1P47", + "BVmJbb9gqZYe3fMj6NZ1cp6LfxHUZARc4z", + "BmbaaEpGpSDzJssHFaL5epARcRfCQtAtXJ", + "Bgtw2dbAnfFAMGDcBqG6M8hpWTokeAtztz", + "BaphLmdqEJC9DcUehYHMgXy2Ng83mZW8fz", + "BYPcyasCpDgztM9e9Wci4T992xgus2LnXG", + "BfK7JxiHqtqnujXKzKdBjj5brfMNqox4jQ", + "BpZCFvYdxjLBEEcapT21rXqVnjeDksEcXU", + "BjXTC2RmXizpBhSNTR3nMUwX1NZNLZkAJA", + "BZUF9UMbGKuUeDxwdSmU98PPTCzevQPCAS", + "BrtZDUM54vQDzUey1kucMzCuouEughegN5", + "BrtkgnLaB71xxLGz9XioAZ8H2JqALhRU2n", + "BUR5snprKpCtj4tpEpnNcmLC3iTW5jF5yb", + "BYn19Sk7V3Tc4bFz24bzgHUD2yvAVyi6QS", + "BjGm8p5RkhBuMEZLHu52bFKw6VL5YF3qnT", + "Bh2CvufXEYFJvGdMfmVx5TfzFSCwxnwHFD", + "BXx6wq3LkXk8L3gCHSxM7Z7QJ8y1UzxVHU", + "BV5myYVtca8mDqD877wnsCvTT9dyysTb6G", + "BowUqjdUNzGab2MLrvRUkoZVypNUTzVHkr", + "Bc9wnDcDhg8odFXwgy7LSY2tPHbyRUu5Fe", + "Bf5s63JpTzkARiRNn6Gtrn8BEDcsJCCrkw", + "BoNFW7eXvKymG5UsAhpvjwEVrE5RgwC1L2", + "BkwBKpXmHx2tPVwn5CkkxA2e8TQuyDWd2w", + "BaKeTrg85yf8mRkkWVvkFNiEDJPB9raKVm", + "BqQqkWJ34MfA5ApfSUSS9LSnLy4TpCpVrH", + "BnroQ7vV7yucLH7TS1zYw5dCQFVpCfsARL", + "BrgKU63pVmcqEpcVba3s5LKHe1RmsaENhq", + "Bo9MobsNcHHukCTTmiG2nMBCHtGd69qhjV", + "BYrcz48rF9w5d2RjyeJyBEtyVjWXNHASNt", + "BeFqc1DQEzyhyRAzYUhqarert8yR3CKzbk", + "BadMPzQXDgGCCCRiLM8Kt8FyxHqb2dgqco", + "BcCyLUjd1tAcEVsQ67TDE1KBqCYFRp6YAE", + "Br89ZqzuU8LQJivmdZeT2gCHbZ8a1WbNSo", + "BjP57w3Ey7Ec2vstSvchH7R4AGf32ccTHx", + "BnWLhbLpMhxk18hJqAMKYjxxJ5Df2pSBee", + "BbewmNNkuenFfckPCbsEsS9gsWUTSmnPUe", + "BXvDh9gzcSrk68mueCMh3sNnEFwEsJDnEs", + "BXDQbgovhnYBxszabsWHb9FMG6ezeYDNHy", + "BbEn7HyhdsbekGAboEGbPLhwYJfcCKsXfH", + "BckmWfdnHXPNT5D7ZjBRQCCLEvyAW239jm", + "BdxawStgggg6vbpoQt8WhadpEQhRaF7TR1", + "BpbAZZFFiGW5HLtpTw9cpPBELn6EKuEA9q", + "BV8KHTKq2SNYqfsksFtqe2b65NHaWcHVgy", + "BakNPbNh9ef3ZDtAWjt7xpKZCpfSBaFyzZ", + "Bd7YMhoPioK25uSfkbHbxhxqbSv8ZS21Nm", + "BoJQqEGpBFNHsLm1nZXk1tWtpFrWudDthH", + "Bocr5amz5rWdp7qnsLrpJNC8NniGxgZ71y", + "BjBZnvrGwbJ1Y3hxMjs8TRj3djbW4ccHfS", + "BZEmPVCj8hziaZq7GESkjS3NNKKxXDgj3B", + "BqyAZpTqZ7Sw3FtYExaGNrTP3ibfcKMKmk", + "BqYi6g4WXmnH6gr13iYGQAUmwAzVcQSPET", + "Ba38vfr7nqn1qjiHMYdkbinKvHZxbAXLds", + "Bg8qZbx21tFjaBKkK6RJHCa622eWbmeYvk", + "BXpeo4zwMRKBzy7tBFKZvh75yRRpMcYqFT", + "BoVGe3TghK1EbWyQ4jrHDeCiuna8Puuxci", + "BZtwLpTJfGSxZoSLDbmuCpmt3ekVdYgiX8", + "Bo4hEBhpUE9qmZWHUvcYNwiBzNSfLy2LHY", + "BbuyxUXxQFfRSfMtx4jV4g5E2VVzGu53xm", + "BbRpVL9RvwdsZiWf1Jm3yDbv5WkZh5iG4f", + "BV3sY2TtZAvDpCBKWAw3CiUa6rhaguodr2", + "BXC87ZSnDRstfpYZAd7KHvgkjoCJUDuBNJ", + "Bmkv7ebZbvbhMwWWKBz7dLMiQpuo74MbiX", + "BeGxciafBvgP5qBXjxmqgFU9GVpnBAg3AK", + "BaW5qSmV5MzheSkWajRSbAbmj63r6cYKx8", + "BhSiH5beuWHUn6SgCbNoaBFiocfHYVS1mB", + "BoakrZsMWPVXtjfBs9Zs7BCRccPFS5NWoD", + "Bgne6igiGghnMrbTYE88DCuZG8AJmhSASM", + "Bk7gDJkpDKTeNeCFsuxqxy3bt5RAnxc76v", + "BXhmLRNRZDChg32L8RsWEhpDKsZ8jy6eYV", + "BVY2QDer6KvVaSeCe2AdiS9ZUkTFUfHt3c", + "BnpAxatKZEcET9rxTX8hopbRg1WY4MLXZh", + "Bbs4hDWdfKUmHMu35gJ4bJcZSayZnc91hp", + "BiN5utVm92MhkjNzLbuSfFgvMQWCr8BYvR", + "BjEt2VBjtvxWoSRYo3KwUwh1eQFP7e2NZo", + "BfhYEzdM856NnCUYt1YdemFFMSEwzT7GvK", + "Ba8qWyuoe7kTsgxRRZw4ZQgjyRCMNBVqR6", + "Bs7TcsHeiue3w2tCmxsGLjLEe39H1n1tZZ", + "BpFt8wZF9FcgWwVm9nDxhH8uvd4GJF82Di", + "BjgKoaVR1mfnFBAKd6oiGkhJhxWtTN8zXe", + "Bi35E8C53mEYSoP6oVMCgN1iFVc8Y5Fet4", + "Bovdf4RA576m8jUAcDBsGW9X16W9wrrfLu", + "Bo2Wva87unvnbU3LQ5Sg9AWo33kQbJkS3d", + "BWdEEE7HfVMVp3YUUfVeiFwj4QSvnaY3VB", + "BgxwnQHm5Nz3dZt7CXe91dAmXYiUzXW4dq", + "BbJrZLMvjkKzVRzUJheK1mEB92Dn58ygMQ", + "BcU6c6dRhiUMp4w97amx5eqiz5xQuQFtcW", + "BZcPm4epew1Z3QEwSArTEZzvr3o5uXz4Hf", + "Bry1cZzCnyPvaNit7cFun1t1p5JMQyTfSy", + "BbDtHrSyhxKQiQYAcPxgev5Xn7YehzNLr7", + "BYSjbM7xJMnz23kuRhPym43GcqG5DqzPpY", + "BcuCvUYzcwR45q8RFtkZ95HdzLqnB3zq3C", + "BnBwKsw8F5Uv43kkzixLdTtoNGTRFUW9ca", + "Ba6ZZUconWyN7mhg72Ewnj6g8ajdGyq99Y", + "BX6GVJArrCBPLY2ApgrGJRAVA761BuUPGy", + "BmDFW3YRcLrPyzTFMQGwffFJ9TYYdYnCMf", + "BnMqyttBDqcHXqUmGCzALvL5fVEmhDohXi", + "BmQEqJ2gSVDhDb6VsrgpbybPt3pCQS3SUv", + "BcuNLaoftXwuE6PhbTsUmLXydnojbpGYun", + "BnZrKFS1DaAXGTuaNFrSDErwWPP5vhynvS", + "BY9vhwggniS4ZDnVp9czZHakFM6MTXn56m", + "BfuK9iy5mY4zDx1TxqRADyA2KNB7vNedEW", + "BhN1GC88uDGUzAtQUpZuL5YTuF1US2kPng", + "Bp5NLapLKm3Rbm8oVeM94H67y7rWGoaJt8", + "BcEwuhyY47rUw25ujocYGTDXoms3VfbpUG", + "BgnEAMFDK12zFMLYThqsD9zJxLSjZnRMVJ", + "BgUsszqvnSmgJQpczsRuzQCgx2QQTAJi26", + "Bcw9n26VPBjWG8m4vLp4m9AqdV7gaaxvFo", + "BkEDwQceMML8Ti2oZU1y8d126mm5bunBdG", + "BWdPR17k1UWBvvAeUWCGzoL847BgwWizyU", + "BpnwLj7fZuXzxa34rDBZVmZ9QZUc3zkQd7", + "BkyvzGn5Dy6ayuejZHDCmBjhsBEXwoS2Ej", + "BpbX2Ay1gczGWZMeYCrQfPwpkUDjn6tbcs", + "BhwpLfHTaa8wNERTFRLvQfv933fKAyTYy4", + "BXcbqgNcdRihTm8ADhnh2ULXix9bZnj1C5", + "Br467A3nrb6T7nNQrD9rpYz5ABJa5S42by", + "BYZ8KDja8Qt29YfqBFd6FniiQbkbSvTp6U", + "BkDXGAmQgyVvUosWeyaG25RiFkabrpWmyh", + "BgQK52d2vY1RUHXYFyJ26juEGbKvNhT27X", + "BihY6kii8Xoc1n8R9afzxiSP76uiBW2nHk", + "BX47fevRntSKrYr543Ac8K83115CQymimg", + "Bhxp7v4QedAU3WQTGMje8YhQDCdVvM2M24", + "BVejCrEK5mXYERdiqNYWP9n9XDEAYHF4Bc", + "Bb1xHZh58jp7SthneKR31orizHfqjj8qrE", + "BgZEM5Zx84M3z9xk5DwvJ8hyxWg6t13syH", + "BagbgKgXscg3x4puBQCGEsnzKyzXYVUqQS", + "BVYoUC6hVTXHsBg8Mz4m9r37ZDJcmN5JtZ", + "BZu69YZ1vHFXQpkmJXF4iwK9CTUzUDJdy6", + "Bc9x4MBPpWhykxracxDd5Rrv3L9VDyiwYc", + "Bj3scPgyMn2fawSscKAQXxSv3PDKZeNR4E", + "Bd72tkvVDEPhVptDAEeBUimwJm9V3CYtqn", + "BpxPzgfvxoLFo8vNupZ8vHPvzXJgHWv4gN", + "BedW8fTJxNgzgV5aThWN7irJFstBRo5bf5", + "BqEiD51uzPTRGV29rYQBS6GHLvjBGdJZAH", + "BmfnEDAotScUR8uRiRAqSfepgYqkNA2ep9", + "BkjD6qSiPtvqZY6rggyjAkwaAPK69Msu4e", + "BiUFYVNwFSCuoWiW3AS8yL2hR7uMy6zqPE", + "BXAnbGFU4LJr2HPQGFtirxC8hmw5xrRZFi", + "BUbZCJbi8FYcVaDtvakhFppaNMTRsV4a5Z", + "BixupaN5uh7AF4kt7ppdJ5htoLZjgRLCpr", + "BcLFPyMiyURY9SwM9Gmka8RH5GLrY8TT2Q", + "BbvEysNvdvBCJQG1tN6e8fT7LKYuFFWrfe", + "BUyMJYsZRFDkVvqjYZuHfN47kEZwU6ooNw", + "BqYW3tmmJAd2EobqiCWmqrn6Rk9QWmbXmR", + "BXHTdaHWxkMemPwHVyCLFhDzxinkdrnvNH", + "BkxekxuqkjRSVC3egVTkhbwKBfkTzEmhFR", + "BjcFn2zNBdThGjqPS8ktj91fYWv1JSSrgj", + "Bk553e7EJUQ6V6ktGfGn144XBcf1x877Gt", + "BeCzWjtWuSEy2399LwjAeyqHox6sXyxWrc", + "BVoF8xLbuDVxChdcFbf516VoA9Eu3kQuKH", + "BoAWnfcUiEYd2Js3fcyJCd7bGDBtAjaXBt", + "BZ1gQyaQ3fnUNTyzTZicMHJXQzPuMhHqnp", + "BYJjewMbf3EswMuka423Ad7pdhZYCg3Ndt", + "Bchvuxpq4ebswtthZHS2anmbQ9KwpL5NLg", + "Baumw3pq5Xx1NYBytYhDuPE2UKvDqSJRP9", + "BdFAgsN2o2YdswifV4HyV47mkiZuok9A1E", + "BoSESFsXpGazRx5GmWanYeLpRtqmDjXuAH", + "Bim5rjmQoYRGmFHTTEXqWNEemhsME5JoJg", + "BmZxB4DVC9wDg1TAWmU2GoWZBqxhn8cPrk", + "Bj1qPSytn79hKWUhu2Qe7ePnWrdcC8f7aP", + "BXvXTPUtSVo7CKuLiaFuMEgdjNAUnFcfKs", + "Bi2NztamupBSNVYLxF74iRbNJcAXtppzRK", + "BnY6Mxjqrh5zSa6Nko6A7wi1HqC9UwiejU", + "BpobhHosUrmAjQCcMLeDnw8GWh3hr3TB1a", + "BUxNZGC6eSCYftmW2JLxcpARqpc6GxTs5g", + "Bo7Qf9oyDf9hDaEnY1efXgWf2p7hq1pDDN", + "BchPvrQvk65phF3rEKtxtdvzn4SJeMYpKs", + "BXfC1A5hATTQPTS1FBumgCbWU24GK6tohe", + "Bbs4R2JGESriQsqorGSeiqamGYxSnQt3c6", + "BfCZBiXFN5WkeAUN611QhycunALxysn75D", + "BgFVKnsdBmUysPPdDoWdamNoNkSLBM4w4o", + "BdXPCQWV8gkprXWA5py9vYC5CYtk2FCGnk", + "Bn83v2gdMRWUYraJ24cD2DXq9htejXTD7p", + "Bi7GRBjApuyq7HKcefEt5fTAqiaw72cbzq", + "Bmiw9SsEgVVgn5KUx7TQ9y8hvm1AbkCdV4", + "BeYkJymZp1ThnvSRMWbgmNcPs2XYFWmbh6", + "Bf9Drjau1QZbyho9UsKxzE5nFoiHUwXu7H", + "BYxMnwcbCCbMYGkvLx51ZYESXfDCJa76cJ", + "BmVTmUjghvoKCx8vaQ9Wu63JL76dxDxYQF", + "Bdh62em6UGW2hkZJvqG6tqGkYe6D9uiU5R", + "BgsRCEr8s8fCR5y7FoUHJtLrSyK4PZzjRF", + "BmJmy5bdXyn2DVbY85hdAq9MNr9emjTQDa", + "Badb8G8W9jpS4rX8pnuP6APp9UTtMLYxf2", + "Bbueq2hvf4n5GEBFMv2ySZmXyvWC4zJX8b", + "BTuad7BAvcgnovGoLUSXa7SQMZp8GfX6BG", + "Bioi18U413xVXjPCB3fq86rp4cAFsCU8Gc", + "BiWFhD5pDvEEjNJzHToGwQM8Nu622TEdN3", + "BaAZo5x8o4TxJkNmtMyV4VpXDwFGY52G67", + "BZAzbGKb1wbNEurCk1Zk1DnGgYcMo49Mcc", + "BYk1nwKUj1fZQWdsfk2qM53Ttxm6nr7eE3", + "BWqUEoE9zk1zUvtLrxhaWKhcQ8NoEtdxkM", + "BqbV2RUjT4zLvfDaLjDipAsW3dsXoibhJe", + "BrRxcooEZ25AzPVufVhUPkphSdxBei4JxT", + "BpL9dLt2bZNxmoDLQkk8pSVehZt85T8Zcv", + "BcGbB8cx1Epg4NY4Y4kNXiUCZMq8A6P7k3", + "BVrCxyPV32XXkpcQuHYtptM1ZmJRzJXFAg", + "BqB9qYtz8BYGzPk7gB8rjeyfxt2k98qgMN", + "BnRGAEZo8Jqe1eT2mfFx8wkpVL86Ni8RN3", + "BokoiDD72bMVinrhs6XmKF5U2NVZfjkKxv", + "BrcPiK2gYUgXKcfQmLFWCMEsPxrqtDyehr", + "Bqo6qKktE3SwcfmKvxnhyeu7usxnY1wK4y", + "BdYdz2xmVb2cW9TJZ2pEqyz7seyiX2kBnY", + "BgudyZnoJJELR3ths46mTU1KVdSQX27aod", + "Bon3U3iLb9P3Eh7BVV42ss8ibqDW6Hrc1B", + "BZPMZuNDhHCy5K8ZvjkS85HjGyABMrkwNG", + "BnE5qswCcf2cysxMkjoL1vzqEXjdnvBjms", + "Bn8ZcFaweAMnkP4tzpLLRTGCeSsDzTKUtq", + "BpiFPHHZx7DeqYiunwCD1EFiDgSzAdaWLF", + "BiVgNiCRWcngZeVqvEtT6JGQNAaL3bNijy", + "BWqxMGB2f3uGmw4D7bdfTgJ2pzR2DkNGvE", + "BYtAWvxogyo8gp3LEUMm43ZaJEfJmfgQfT", + "BgkiV2Wau5G7Svau1dzP6UJrKSiDYQFMca", + "BcpjNNP5xo46FPqc5vC2CVQYV5ZaKHnfvX", + "BZ8EU8wSvLtN8r5s7ekdBSUREYA3MDqx9n", + "Bfo1uFVzKVUDfu3nQa9VEkt23LcsD3EJzn", + "BjG6mddsj7gUxW6agEiPAta1dWNu2KdWnF", + "BUijEUFcfn5xMW6m3M6mqf4y8672ycCXeR", + "BbhgEC42ActL7NQqTYwfnVBMjUaoC9ave1", + "BiJDC1CV4Aqy2wQifHue5hFRLeHP7XESfg", + "BckjoUBZjUW6b1oxh2AL34CQdUfHL1T31W", + "Bikm4Js4xpNA9ojmq92SW9upSEyELrSrSr", + "BpJCHR1hnFmAGpRWXjsjoY86dxjnFUzGyj", + "BZWMPGhAUmtDp22HHppgntVt9Zjoa4dTwi", + "BYW3CnJSMftFap64QYUu1CQMKvPAbfwDMT", + "BahBb4eJieyrrRvuYZDEQKXz2tMnMfBT7R", + "BVXo9zww8qfeBVM5bhr5Duq5EoL1wxV74H", + "BkUpKkH1uYndATa9CBPuTAitPM97Qrv9iQ", + "BZB9uBHmcKQXLjHXxnAkZAdRMAgPZcxGWG", + "BcEsoYjmubPJx9hufdAwaAwKLKmtTK8FAt", + "BXKCXRbR2gPRaTQjTDUgiAGjf5QiUxE7ra", + "BTqaHUVoMS4dUGLijPLJQ4seefnhrzKG4H", + "BnUqzim9GcqzzUvTDovWmPM94sHKy4zKCd", + "BTs9aewNsCtdQPwL7N9jVjer9ucrf4nbre", + "BinxgqCHLstNLNNkbQ4MkMVyCcPpmwBnyf", + "BnACoRcbE7puTe1NFctGxbgT6YM5UBKpWE", + "BiQxoMNJjpD811Vv4CB9EMEexPnhSbhfTY", + "BonHbLH7gF4ppy8Eqht6V7rosaRmgud7RK", + "Bm3xS3YTScz5h1z9w7ooodAfNQdtTfzcVJ", + "BWZmHx75PtWyEtCbfoKgnjYBBM1hoAy6By", + "Bji6EbHH2gGrnSbsgWUokyu3XHYT3hqX5G", + "Bf9PQVXmEa4hsQb2PDdWoifhqbrtz2oUdn", + "BgQUPq8AX67T8qaAPt8UmGdJ17cEfD7tvB", + "BrGyygA3JsRyGSNkfLzcKNMAc8DfCXxraa", + "BY1BK57HG6TUyaXijLrT9syjruE1i9s2Sc", + "BqBFhCzsoU2mFBcs6XVNdYHYMuEUjCY4Y3", + "BXjYbuWazak23rmmK8HCD8PRmTNQsDhQUu", + "BXWsFxVwpKnAo2NEiM1NSusM1NLqQ2HoUN", + "BYso6DgyzJK2MZZNCoXiig791hcGf9Zd2V", + "BYBXR6WwQHEirPA8tjq8MmeSYDhBJJiTEV", + "BjY5wahUhX1cQciM5PdbzwCfj1AjpQ9jhA", + "Brxin8Y8UaaqFACVQRuXxyif8B7jF5j98v", + "BXWtobez2J5c2he4cu1vnUYVhdjANitwpy", + "Bd6uuGheuPBLN3C29R5SQNENbT6uVJ3ZQ1", + "BWAi6eKY1zKAduL9ENThqNdDre3w1K9KFc", + "Br3CCjZTBMF74vXUoNrcxWmwbU9YjTJqqP", + "Baim5KCRwikHWeEm6HScgN76uDVvAmarFY", + "BhAFJ4Jxi2Wk4hCqSnP4TLYdsX6DZwPuun", + "BUeCQo2uEVjtxkgUAmMoj4mUkayADYqP2b", + "BaxjkwehmvPiap6oUfvsyFfisLQR3d66X3", + "BVr6B98g8uQkyvwRaXSSGwnBYEyM8oqghT", + "BZoHAs3DB3NYn2Dg9DdFm7ZUoNdr6GH74B", + "BofTk1UcuGvqULuwHK2PK2TwC3yJWnfmoX", + "Bpq3cxqP4P5zUF8MMCGkuLnyPmQGo5Pfjx", + "BbXxvJUV9Sd3WkS6pe4jWivT4weC365N2K", + "BXvfXRa1y8bp3Vxu18v6pGBz7nirYGhQtA", + "Bcjj8UZBAsYDSFmq9rEtRmrRrWKncqwTYB", + "BVGNasnU1U7B7czAetSdUJtWH6KsB47g3L", + "Bb6Caw186eff9ofjQzouRfMfNb9qEraUGM", + "Bo31hkyfnfuBdtT9FEnxg4ExA8egkkEYa6", + "BUKfRYg14gNy3Zx4Td9vt6izRoPVFFj6BX", + "BeM9ooMDg6dEZpQPmRbDtXatzjuAiwyp6g", + "BW85YvfjRvNJ8UHgkGkzg5oeHNwSaLMaBj", + "BaeVzbMGEcuQqFdCuPL9hQ5vJznncpX8pv", + "Bj5PThkzkDWqyMjtCadNF7ronMsYhvt8Lx", + "Bn4u4V39SvvdgLrf4uk9pLoHoY65vM1cHD", + "BVDfgyS8CBgpcNuJRj2zbYLe5cnxCPiQfM", + "BnSM1Mb4wjJQURYZKbWoiuvPDXLHkAqjGJ", + "Bm3UamKwZ4SN2Cs1cJBhx5D8TBNr9EEL8M", + "BeURSK5rhe6QQdZG5BKQuHByXesRPGe3QX", + "BkZLqtBsNv6ESknHyKwDeMgt6oXUu3qLmm", + "Br2F9GWJdZcrbcbojHrMXwWcG9Rrbc77gJ", + "Bjyn7SHfxaAbReR8YCXcR8bHF6n8LfeQNs", + "BgvrqU79q483Nh8sgJfMC1NZLRna8FkP1z", + "BYBtft1vP5MwUSfuKtEyu2G27r845mAeFh", + "BV77K8P5eNNccArbMhu8epWNzGzF9bGCgD", + "BZyQxAzseBrts4NdWencYbK6s6XMyRfrYy", + "BXBEoCaasQhDd6pE7AJUt8LbmZKBtA9ShD", + "Bp8i2aUkr212co76YTQ63iPwBJAUiXxi7B", + "BgfKLvjhmCcGEmCBV24rqDh5zui8RQ1rc8", + "BWPinPEiLPS8uXwnQynA2xGMcXyv6ESJcG", + "BnwxndjBbqch9kRAz9sfJYbknMKMyYP8JM", + "BaomFMx5w88iXvDduPtNijBxu8SfJJFL7v", + "BdwJfew3GgRkcQudDaVv6HN8s1Wtc4aKPU", + "BnsLhTWVUSeXVWVZPUjtbCkcxixJqhrRnM", + "Bfg4Z4d45srmpqhud7XUn4abyxB4KirA5s", + "BjqZyHfahcnScJt2xx9awJfnS7UD8J2Yqz", + "BUan5rCvvcp33fMrUm1S4UgHEicXVQboHr", + "BfBiUhP6Zmi93dD5Q7Uva1G4QXEkDwYpic", + "BURsZaUKqgHzvFUPywupyCyU77Hq6mgEZW", + "Bej9ReXmVuG5CA3JQ39ffV7eSkcNMwDUtG", + "Bb39eXZLMGbScRqZYNpBXeqRitPWEXDHks", + "Bnpb4V6dSdkMBzAUeL9nG7zQ5iQfX57ifA", + "BWGKFXLd1cFuvq7cLrE2rjhxP1MwPd1Hh2", + "BrUAQT1s2nUat3YE6GhfCT5WVK6QEgKq27", + "BkxtX5jrP6oUbxG29JzXWDwGNVwpcZBwPi", + "BfxezCqTiG1ocxuLYHZjpuXk6es2mg3TqS", + "BYfDX35NGZLztNF5bTT2nUJqDAT8tNAvah", + "Bc6rq7e1qNdUFQHza3bWdt7FJARvNy6zu6", + "BZn9UtgmqnzmWaVA9xvBTVsdxoQ6FS1qEi", + "BqqMBT6x1BJ7DzRSCZRc47bffjtNACqzLT", + "Bkv5qzRGPPZfaT8DbsDeU74dDxrUikoXQ7", + "BYvjH58aDv8JPRCHnB5AzHTxGvoYJJPYKF", + "BXBL49Cxf6orMMX6JDYXkcygUM88cYY9Hd", + "BWV2ubjpjUWEmjdhJSGbtAGRtf5r7N75Kj", + "BaKWzHDhoMnDyQFrCQakV9sntZBqpVWiSV", + "BbYFYB5Dw86yRQxji6fVDC9MMduzJmTTT9", + "BVqzdVuk3cg9jLL5YJJ1Aga7ERLZAtJCEz", + "BXNMnA6CGk9Ew3gMzd47GXtN3vFu1FB3xh", + "BqsFnvGd7WCD6Smvxv9rhEy3EXgpVDjXac", + "BYc15EwAAczXaEDNNbEkhYYsAkxwVki6EA", + "BkQYoga8kcHR6skyjqF2MKfvQC3qr6MQDE", + "BnuSpap7uaHo4KF5jwtKUbDLiqcrpzVJCG", + "BZDvZLAg5WBRVSLqEVuF3g4H6BCx4KzF8n", + "BeoAR7xbr1dN4anqGbxSjHder8E2Y34XVR", + "BXCkq457cXtcXS8xjxZQ3MZw9qBBpqo4L9", + "BXdTNj8X13BBSJZxq8AuiwHjhPjCkL4TeM", + "BivkqQQ2juk539vPqqyAwJAAdphkS2Qh7U", + "BhpDvMDe47EnGqhbRKovULvAx7Ei2Kof4p", + "BimG5M79W1jEGNk4Hq2auQAiFC6TrHKqCf", + "BptK81p6ka33dR5rpFCG3oFLDmcAaJwzP9", + "BiY4PDhobHN7TAsUqdD4VMbwQ6UfgAcvd6", + "Bcs5wrRctjs12LyamXkoPbwaaGkBjDe1sf", + "BjsNDouoZyi4b6GWKdwvQaY6cqLs9VmzFN", + "BmNj2XTJmjmHKEpVAJDTgJmrz6CwDqcRoH", + "BbaX2sviHi5CtNLAtp472pnbt2aWNSXqUe", + "BaMdg4QtKFzCwq4fCgghgPfiYXn6bx1FNV", + "BfnPkwf7ScFJDY5Ne2PCfjov2EKqgRfCQQ", + "Bgw3n318TQobJbwqb1kcUBAUCqnmictiQy", + "BbgfnfAVsBHufsR4hEJbMGZNvoseG2hyeB", + "BZe7679jVse6nR7Z99CouaNfPvkGCbpiTJ", + "BZgfHGNmkP5A5vsdaZSx4UPECNGG9SjbEM", + "BZyGS8GrKbCFHZG9kJ5RtSMbYTeb83xCuQ", + "BYJBtX1a9zFY9o4tUAEvA2Nz9w3xbV9Gsm", + "BmGFEP6do4gAQBjVYftv4KzyzakxiChCX5", + "BZcBfSC71S9YccqH84yhCwmJESjkyHD4RS", + "BofV62pxDzL7twMcSUA2i6T3ZcFZG4myea", + "BqcRh7GKTeJoipo72rERcMxGvY4MbjvzZv", + "BqaMV6ebHLG18TWtDVQJWeCybvnRrj4o6n", + "Bmh13Ai2NujEsKnRtQZ1FZ5RYuT8KNmeCz", + "Bd8Wi4TDLvtfefK7RKSTk3YeSwJZ5gpAFC", + "BhUeeyhPkjLCdXL3tScB5R5TVrDNknPBzj", + "BiykuJQiSZHma8n3CW18tRXztFxaUYmQLW", + "BiSbsXXZpwXLtwunzfCF6dK9CcT6Xm5cSC", + "Bk45GmXRHg4iRM92C7BYwxd8K6oC1yDBZh", + "Brf92mSyKSEMpGRdyBSphn8GtV9dio4Xam", + "Bo85F5awQacAV5ikbZvYY3HN2hFFkgyyty", + "Bhmfsc7VpbKAqwDo1uEujTRrDxntLeJ2L9", + "BpVkjNoMv3fskuhkjVJ6GfYkUZDC8jUgRj", + "Bq81WjojFkpJjdmtMHuuzoCkFiXk4SiKDR", + "BbhvmJTvakSTVu7FRzfgVnRqwvUYikRjhX", + "BqjYXY2GTogtp7MoSR1zoXBrU7S8Y5Xeor", + "BpEC7C6R4fjiKeVgKe8yk3pVsXE6A9UUaz", + "BqqKrXhNS8RnSEqN2KtdxqqLTyx56C9Boc", + "BimuwMqvQgUmovChm5HTvgrF4dZkXmNr3x", + "BakNqavRYqaJwN6fop3QVrXXLCmSd69CK3", + "Bijb71x9vCGF1tmS8NxTExF1YsM9Wr42qK", + "BjQATPCk8jHZspYtjnpowpxzwFnpLdL3b7", + "BY3qTKKq56EevmiQVbUWERJrAReiRrciwY", + "BkSuLfpnyU247e7XYfpBSf1MbVfmmMVfmj", + "Bo1yy6K3kHwwpivsRcJNKrSDfcowtGx2i5", + "BjNsuRV67mXJstFeZ4AgUdsZ7N7qe8nADP", + "BZrz2jiK934k82AWYBtq4pFXWf7zXzpa8L", + "BfxMrzjh2mj8EwqRBUhX6ZqK6QbZoRMC3f", + "BdcxXBbHbMyDsFjUUKy9EdZiAJn2evS2Jp", + "BW29A72EyoCnGmMoSLkTxTbshqY37yJDoF", + "BrNVjtbmGVg4crAsDvCxYRL9dq6ogtyJB5", + "Bdfj6bR4Sxu5ULpQk39TG41kGjaMH2VZqZ", + "Bhg354Ch6roMDuNBaEHDKLzWh5Q9nE4Zyc", + "BULUXLRAgefU3JoNGz1S9UPAJQ6VnuJE8r", + "BmEBGiBZYsid7hGvM7aVxWWSQZ8ShyqFrj", + "Be4P3rLMLdyVZqMB2CnE5TFbXvBgi4w7yx", + "BnFQjrmAZbKFBhdrywF44jr65PRGTFTV5M", + "BjbCzdbxfxAQ69Dk43un2Ui2ah58uKsZVz", + "BmiiWSVYg58Py5TtzACkedSjZzKFJqA3f6", + "BVhuT3acA3cFVFKv1J4ijtXbph5cxVrkhQ", + "BVzN2r6K4KGFYQ7XhYb2cEchpJV1Tn1dVG", + "BnGGxsKzfhbEsF7LfPCiBbynLPBPAySbjD", + "Betw4vHTSNpFc1LXndCf2upAKy9pMVK57D", + "BgujzLrBCDWMm4AoERyqjGLDQsyRUW5eTW", + "BUJwqeHPbdDkbNhdNHermiyPYnJaH7kZv7", + "BbH8SrNdN68VFHWcG7vrvJjUJ43xrUmTaq", + "BppMruCPkQi9tFg1w1ry4Gypa9eSJxfoNh", + "BqLHzQheRN4f3p2ojrm6CmskoZidhAtjAE", + "BaWDwaHUA8WRgXqQi2Qrist7LWvLb6sw9L", + "Bc4sg1WRRmjYamLviv7yGW3ERroD2zuwwj", + "BqrGrw88SgSSDsLox6MEwTtZA9vD4VTmdL", + "BrK7LKL9msnBip5A6z854QHWu1C72WMQNm", + "Bgxu1NLWFbZsUzBYb3GpSwy6SErbJbyiMk", + "Bb5cYibd3G88j764DKVLAMHaqHkT6H4MFk", + "BcEL13pB9yPhqbTiCGAujMLnaLFJzEwnKL", + "BmSbe1dfua25UBY6LyEQ6UkmFnyv57afJb", + "BfQpkbqrpdUvn8hopJ6NZTtjadjaMhbfEp", + "BoXhdrpX4eBWniG7MjkkrAyHVbeAksGxc2", + "BYAAe1MqixybFu6X6egyZx9TjEh1dnEsz9", + "BksCqSam91mR5PUproA16ac7VUxK6nQToB", + "BUgbvhvBBJPFZWLBztgrzu5rbXqNPDEHLc", + "BfwjkA6rnTRt4fZswW9HxbyVTwkdYqDCCu", + "BaQof7W6h3y7p6sGZp9kdm13TSezRRWJYu", + "Bb8TXEsaEvrpg94QusKtw7J5pcw1yC5HNx", + "BeLzkEDv7TeFWnjiJ2ecB8iPodi6VvQH24", + "BpLUGjPKZVh3ZRaLZkDc1PVwxyKE5yizkH", + "BcCyf3EHuFxmDgfah9bq7BFwfSv6nFpbtP", + "BnBDSQry4aisR517mXDBz4nrEiqPN4rU3K", + "BhfuiiMdUXRKY3221R5w9xCyS4YxTwaaRC", + "BrKKDsVHbKmr5tBrhaztmA8phBdaZZ3wSw", + "BriWNFTvvpSCN9fYbKFqi1qb2kKDW8rgtT", + "BVW3g6fyjuSrX4BZWekkS3rLz1JfHbWc9K", + "BgtxdtWYqv1xCwSYtXb1dcgxxwB2c7HSSL", + "BXpueT5nU9ky5MfmsiRnL3WAFpMnAxsyfr", + "BraBxBvMcQscm4qjDHTbzjDEAdMBibTpDt", + "BkEmL56DZmZmppzhVPSHSQapCAhvgTHXd1", + "Ba7DP3Rx6fWqvuUwS38c8Gd34NFzfTejiT", + "BnUp2C4nSZR16diz4jQGHoJgmt1TM1JfrP", + "BYcR3SKskRWM8fbAptRBCAUHn1GRDGqERb", + "BfiCcQpFqSE8n8usFJbpfZhHcjj44v3KnY", + "Bb2TLKDUh3mKmd7CNSn1V9MZEs9P2f97ck", + "Bj5uRztmrst7fuaQsfPz6tb79ZeuXLkRie", + "Brao1vccnJ6RQpYYq3AEgdjSUhc9SW9x4e", + "BhbVrtMbBb7BwMV4LgVuhsk41NZue9kjNk", + "BX5BXmVhJkgBpjHTE62rTUBxEHQeX7dTmq", + "BZvH4Zb8aoupqE7DaQFeDzzWs4BUXQx2tW", + "BhkCtkxV9EejmB1yxzt5fdSNUnp66DMcUn", + "BUAzh28MZtatvMr4dvUZCDKh8s33FUafG4", + "BpwcoKZQAwqB8FRSMV1rSbYJJZorvzkRsn", + "BaT4N1dxW9tWdsMe4yewd8pPxdi8i3csuQ", + "Bjc8UEJ8FiC2WABEA3UbipeAR2fJiHrbt4", + "BdzQjioCaXDUGq5CEkcYYS8DDqpa7yxjTL", + "BpovTnSV93D8gXvkUeZvJweTtudHbJw3iH", + "BiYJqBvAL3EnRcMzPG4H6xrsMsGoHkvKyo", + "BfQ1fLG5Jhz2XiD71JpABDwQntNYkxkspc", + "Bacxjfj9MPXj5u419oUwF1MCjm9cK7SDaj", + "Bnz7KAWH5d3ZmN9z8DWBPMrnjFZ2nzk4yQ", + "Bo4KjQKaWMnb86YX3Jiy1PjUpu7ERAVoMY", + "BcoEEEexWYwTW6Jow1C1JB1BZrx7qRfPjp", + "BW9bVHTfzfpNyz85AcnN1pzNka8JRLcjcY", + "BrofsCPfUVfummkpvQgvNH39osogyy7JrE", + "Bnq98QvukTKB4MQPiZoRo3EqUTJNjGPRi9", + "BpPkEAz3pbu4JpSSfkueijkfWe7XX8PXFb", + "BrGzTGEQa9WCotS4geKsS2Gjt66VYzSMvN", + "BrA4bfQPxHXt1ZH4BGzhVHzgANj9zR1nJC", + "Bq9PxrEvo1bPHwEej41UuedYzzV7d7qsVX", + "BfFndcBQhWAeR18sS1ahfwwvwuJtxLfJuu", + "BrL77xgWtTjfAH7nhLw9Lw7hs6CCXwUHSA", + "BiBvC7gDvyTeQysAptHfHJJWB1LMEnHuJg", + "BhQiAHtrQVp186zryfSCjmF14i5VMyp6n3", + "BYomeKtJspVtS5cHm47NGEeXaq6YcrJqPA", + "BczZhLNcwhecWkX56S1tCixWn1S55UKdiX", + "BhV96V7sB6Qspswp5sJaeTwyY8Zhrc4w5d", + "BZoJMQmK1TBSsaPCz4ibi5AaL4rTF7ekJC", + "BnJq8fWHN8MgFqrsPKNgiujJhb6ayVW7vC", + "BkWrixUYFB2LQ1HsTLWExDkfQUfYhvPA8v", + "BkgBWviGJYx1PLVNSUBWBmGtVu9Uvgo1y7", + "BZET2U5MfCJD2ZqTtpNBpfxzinZD42bUFc", + "Bhui7woKrxCDMdhTz1j5fZerHdzj6fSF86", + "BfM3qHV4gvNppSHaq7U638pMqskvG7awDj", + "BTrWbw3egYvN2XLPnuoeRQCb4APbjKC712", + "BUDgkzWxJyRuLutRiqvmWHRBgUAdFtjqiu", + "BpvJNnRmtMW5Z56EjwpZwEmG8my19Pjw3d", + "BVyhwYcXtQ6vMAS1D4JnDXDFDKRZX8RbLg", + "BWoLHkqk9xbUV6FRHwrj6HcBkpna1aVnTL", + "BUfmcRACj8di2tRSg7BCVeiftZMfKi88yd", + "BpZfFPNRFmsxywPRB1y9wZvHKNZ5SUwYzh", + "BZEdZSQodvkwvQDDnhA6FNUnM7a1fc8fG6", + "BfjgJtxxTwLUFMEwbQuRGkVufg3iJ2H5JG", + "BkFoE2PZcNQUxYHYG9Mdcdq2qeswSNHeUG", + "BUhBBeqRPFA4T9fmgoZynvb6267KJfcp7d", + "Bj2cg6NTtPjTE9QzBfh4cVd9gmAFYeTeFD", + "BrGJdznxeE7t18jXJLA3aswYzCBuZiuUEf", + "BYmV8ciCUAbY191YtzDAhAUNjbLPd5Vgha", + "Brv9J7HqHv6wdwjQ18T3dCPzczgieBBfU4", + "BbqxS59KQV1mbHY8CuGFyJAbc3FzUp5BXb", + "BWd25obzLni8U5wfRiw2gsVFV22PVBnFDP", + "BqFL3qqd9BdqrNfH93oV9BJv8xRGXJpK1f", + "BjGjjG2HxWE8HLoLahsHpzUtuFejZfNdHy", + "BbNuXsGcsSuaHqhVr1hE6y3HsCTykLx5RW", + "BmkYkrjtaLU9N4rcnmyoS5WPyekM4ogZML", + "BTvn3Eo4GgK4Ez7WRUkpGmeM4ZapV3K7UH", + "Bdsf1MzYjQ8CdpAETY2A5cDUU2Xe18GHcb", + "BfE3DBdRfBdYa1KAHoZCDinXnuSNz37avx", + "BVsxNcYutZZiDp8ZHfYujCJMMQmeFmCCpg", + "BeSggYEFaxqDfBnjDXk6rBJhpziuMmG7tE", + "BfuVuYMyPBESNota9wLkqqk6PdZ6SYcnrN", + "BY2b1iZA3NjpsjWAi3hzZNCqsZA8M8ac1n", + "BVZAyMsZqnEuFxNpmWQLjxLYmGEStwMqN8", + "Bp92khnxoKu79RjemrGY8NfLBhtierwY96", + "BmzagvpPSs96ppTS47k3BaxroiaJzT1XgQ", + "Bcudp47shovmshNdHE4YN4NEuKPYtUzKpR", + "BnNC13erHJvmafrcM6prdBix5NnmxfbLqj", + "Bp3kYzeATnqEMj7MpU1UFNcXyxXBihBVwQ", + "BgGHGC4pA7sx9iJju3cq3MjyWJk2wLGLsD", + "BVkwkSnJFKXFCQaBsckv6boSpugNkKH8vd", + "Bs1BghB2cSrX6iwzuzUsGKmBVusApmcukJ", + "BpzaYcdBv57he46oeknCzsdc1e7bA8wSe3", + "BYH7ydudGEob49ooj5o9F6jgrsm7zoUR7m", + "BdACDcDK7uzfnkHbvcUeWATrRM7izAfKZm", + "BeFEH5dbm5tUCCLSHrN1MFXEzjjr3v7VzT", + "BePxRjQdCueJiGgFjKXmXqXw5posmWrn4E", + "BgVt4JBwZMC65bbEwJ6JLSBXxCmVxknbon", + "BrKTbohYs39KeBobEdaEu2CncXumr4ddSu", + "Bixxv4tqhmwDv6imH2zJg89N6oPinoDyZT" ] } \ No newline at end of file From 8d2e3c4e7df0964bd7fb14aae48ec2cbd2cc9480 Mon Sep 17 00:00:00 2001 From: tryiou Date: Mon, 30 Mar 2026 22:30:50 +0200 Subject: [PATCH 22/73] feat(config): add java-dotenv dependency and centralized environment variable management - Add io.github.cdimascio:java-dotenv 5.2.2 dependency to manage .env files - Implement App.getEnv() method with dotenv fallback to System.getenv() - Update environment variable access in App.java, ConsoleMenu.java, ConfigHelper.java, and LogRotationUtil.java - Increase default log retention from 2 to 30 days - Add current time display to background timer log messages for better observability --- pom.xml | 8 ++++++++ src/main/java/io/cloudchains/app/App.java | 20 +++++++++++++++++-- .../cloudchains/app/console/ConsoleMenu.java | 8 ++++---- .../io/cloudchains/app/util/ConfigHelper.java | 3 ++- .../cloudchains/app/util/LogRotationUtil.java | 8 +++++--- .../background/BackgroundTimerThread.java | 6 ++++-- 6 files changed, 41 insertions(+), 12 deletions(-) diff --git a/pom.xml b/pom.xml index 45f5835..7d103d0 100644 --- a/pom.xml +++ b/pom.xml @@ -112,6 +112,13 @@ 20250517 + + + io.github.cdimascio + java-dotenv + 5.2.2 + + io.netty @@ -285,6 +292,7 @@ io.netty:netty-codec-http io.netty:netty-codec-compression com.google.code.findbugs:jsr305 + io.github.cdimascio:java-dotenv diff --git a/src/main/java/io/cloudchains/app/App.java b/src/main/java/io/cloudchains/app/App.java index 6e2b81b..f67b294 100644 --- a/src/main/java/io/cloudchains/app/App.java +++ b/src/main/java/io/cloudchains/app/App.java @@ -9,6 +9,7 @@ import io.cloudchains.app.util.ConsoleFormatter; import io.cloudchains.app.util.FileFormatter; import io.cloudchains.app.util.LogRotationUtil; +import io.github.cdimascio.dotenv.Dotenv; import java.io.File; import java.io.IOException; @@ -32,10 +33,25 @@ public class App { public static HTTPClient heightUpdateHttpClient = new HTTPClient(2); public static JSONRPCMasterServer masterRPC = JSONRPCController.getMasterServer(); public static ConsoleMenu console = null; + public static Dotenv dotenv = null; + + public static String getEnv(String key) { + if (dotenv != null) { + String value = dotenv.get(key); + if (value != null) return value; + } + return System.getenv(key); + } static { + // Load .env file if present + try { + dotenv = Dotenv.configure().ignoreIfMissing().load(); + } catch (Exception ignored) { + } + // Check for EXR_ENDPOINT environment variable - String exrEndpoint = System.getenv("EXR_ENDPOINT"); + String exrEndpoint = getEnv("EXR_ENDPOINT"); if (exrEndpoint != null && !exrEndpoint.isEmpty()) { EXR_ENDPOINT = exrEndpoint; exrServerPool = new EXRServerPool(EXR_ENDPOINT); @@ -58,7 +74,7 @@ public static void main(String[] args) { String OS = (System.getProperty("os.name")).toLowerCase(); if (OS.contains("win")) { - userHomeDir = System.getenv("AppData"); + userHomeDir = getEnv("AppData"); } else if (OS.contains("nix") || OS.contains("nux") || OS.contains("aix")) { userHomeDir = System.getProperty("user.home") + File.separator + ".config"; } else if (OS.contains("mac")) { diff --git a/src/main/java/io/cloudchains/app/console/ConsoleMenu.java b/src/main/java/io/cloudchains/app/console/ConsoleMenu.java index bc1297f..2a5d3ae 100644 --- a/src/main/java/io/cloudchains/app/console/ConsoleMenu.java +++ b/src/main/java/io/cloudchains/app/console/ConsoleMenu.java @@ -52,8 +52,8 @@ public void logBadChangePass(String msg) { } public void init() { - if (System.getenv("WALLET_MNEMONIC") != null) { - String mnemonicImport = System.getenv("WALLET_MNEMONIC"); + if (App.getEnv("WALLET_MNEMONIC") != null) { + String mnemonicImport = App.getEnv("WALLET_MNEMONIC"); if (mnemonicImport == null) { LOGGER.log(Level.INFO, "Bad mnemonic."); return; @@ -61,8 +61,8 @@ public void init() { completeLogin(mnemonicImport, null, true); return; - } else if (System.getenv("WALLET_PASSWORD") != null) { - String password = System.getenv("WALLET_PASSWORD"); + } else if (App.getEnv("WALLET_PASSWORD") != null) { + String password = App.getEnv("WALLET_PASSWORD"); if (password == null) { LOGGER.log(Level.INFO, "Bad password."); return; diff --git a/src/main/java/io/cloudchains/app/util/ConfigHelper.java b/src/main/java/io/cloudchains/app/util/ConfigHelper.java index b2ff02d..7b784a2 100644 --- a/src/main/java/io/cloudchains/app/util/ConfigHelper.java +++ b/src/main/java/io/cloudchains/app/util/ConfigHelper.java @@ -1,6 +1,7 @@ package io.cloudchains.app.util; import com.google.common.base.Preconditions; +import io.cloudchains.app.App; import org.json.JSONObject; import java.io.File; @@ -220,7 +221,7 @@ public static String getLocalDataDirectory() { String OS = (System.getProperty("os.name")).toLowerCase(); if (OS.contains("win")) { - userHomeDir = System.getenv("AppData"); + userHomeDir = App.getEnv("AppData"); } else if (OS.contains("nix") || OS.contains("nux") || OS.contains("aix")) { userHomeDir = System.getProperty("user.home") + File.separator + ".config"; } else if (OS.contains("mac")) { diff --git a/src/main/java/io/cloudchains/app/util/LogRotationUtil.java b/src/main/java/io/cloudchains/app/util/LogRotationUtil.java index 07eafed..71f5caf 100644 --- a/src/main/java/io/cloudchains/app/util/LogRotationUtil.java +++ b/src/main/java/io/cloudchains/app/util/LogRotationUtil.java @@ -1,5 +1,7 @@ package io.cloudchains.app.util; +import io.cloudchains.app.App; + import java.io.File; import java.util.List; import java.util.logging.Level; @@ -13,7 +15,7 @@ public class LogRotationUtil { private final static LogManager LOGMANAGER = LogManager.getLogManager(); private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); - private static final int DEFAULT_LOG_RETENTION_DAYS = 2; + private static final int DEFAULT_LOG_RETENTION_DAYS = 30; private static final String LOG_RETENTION_ENV_VAR = "CLOUDCHAINS_LOG_RETENTION_DAYS"; /** @@ -59,7 +61,7 @@ private static String getUserConfigDirectory() { String OS = (System.getProperty("os.name")).toLowerCase(); if (OS.contains("win")) { - return System.getenv("AppData"); + return App.getEnv("AppData"); } else if (OS.contains("nix") || OS.contains("nux") || OS.contains("aix")) { return System.getProperty("user.home") + File.separator + ".config"; } else if (OS.contains("mac")) { @@ -76,7 +78,7 @@ private static String getUserConfigDirectory() { */ private static int getRetentionDaysFromEnvironment() { int retentionDays = DEFAULT_LOG_RETENTION_DAYS; - String retentionEnv = System.getenv(LOG_RETENTION_ENV_VAR); + String retentionEnv = App.getEnv(LOG_RETENTION_ENV_VAR); if (retentionEnv != null && !retentionEnv.trim().isEmpty()) { try { diff --git a/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java b/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java index 7726f0a..53e11c6 100644 --- a/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java +++ b/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java @@ -71,8 +71,10 @@ private void initializeLogRotationScheduler() { initialDelay, 24, TimeUnit.HOURS ); - LOGGER.log(Level.INFO, "[BackgroundTimer] Scheduled daily log rotation at {0:02d}:{1:02d}", - new Object[]{DAILY_ROTATION_HOUR, DAILY_ROTATION_MINUTE}); + LocalTime now = LocalTime.now(); + LOGGER.log(Level.INFO, "[BackgroundTimer] Scheduled daily log rotation at {0} (current time: {1})", + new Object[]{String.format("%02d:%02d", DAILY_ROTATION_HOUR, DAILY_ROTATION_MINUTE), + String.format("%02d:%02d", now.getHour(), now.getMinute())}); } /** From 0d2630b4fad6399fd381d83c5fdb1a264b99717c Mon Sep 17 00:00:00 2001 From: tryiou Date: Mon, 30 Mar 2026 23:12:39 +0200 Subject: [PATCH 23/73] build: Add ByteBuddy agent for testing --- .gitignore | 2 ++ pom.xml | 1 + 2 files changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index fd0578a..46fc178 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,5 @@ Thumbs.db *.tmp *.temp *~ + +CloudChains/* \ No newline at end of file diff --git a/pom.xml b/pom.xml index 7d103d0..02d9a6d 100644 --- a/pom.xml +++ b/pom.xml @@ -179,6 +179,7 @@ **/*Test*.java + -javaagent:${settings.localRepository}/net/bytebuddy/byte-buddy-agent/1.15.11/byte-buddy-agent-1.15.11.jar From 0c1c8a483a4a3764f3f670628b055b7b783de442 Mon Sep 17 00:00:00 2001 From: tryiou Date: Mon, 30 Mar 2026 23:34:01 +0200 Subject: [PATCH 24/73] refactor(address-discovery): redesign sequential batch scanning algorithm --- .../app/util/AddressDiscoveryService.java | 266 ++++++------ .../java/AddressDiscoveryServiceTest.java | 408 ++++-------------- 2 files changed, 215 insertions(+), 459 deletions(-) diff --git a/src/main/java/io/cloudchains/app/util/AddressDiscoveryService.java b/src/main/java/io/cloudchains/app/util/AddressDiscoveryService.java index 4f53e6a..26a9d91 100644 --- a/src/main/java/io/cloudchains/app/util/AddressDiscoveryService.java +++ b/src/main/java/io/cloudchains/app/util/AddressDiscoveryService.java @@ -18,32 +18,33 @@ public class AddressDiscoveryService { private final static LogManager LOGMANAGER = LogManager.getLogManager(); private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); - // Simplified configuration values - private static final int GAP_LIMIT = 25; - private static final int BATCH_SIZE = 100; - private static final int MAX_DISCOVERY_DEPTH = 10000; - private static int DISCOVERY_TIMEOUT_MS = 30000; // 30 seconds timeout - made non-final for testing - private static final int MAX_CONSECUTIVE_FAILURES = 3; + + private static final int BATCH_SIZE = 250; + private static final int NUM_BATCHES = 100; // 250 * 100 = 25,000 address range + private static final int MAX_CONSECUTIVE_ERRORS = 3; + private static int DISCOVERY_TIMEOUT_MS = 30000; // non-final for testing + + public static int getBatchSize() { + return BATCH_SIZE; + } + + public static int getNumBatches() { + return NUM_BATCHES; + } + private final CoinInstance coinInstance; private final HTTPClient httpClient; private final ConfigHelper configHelper; private final String currencyString; - // Enhanced logging helper private String getLogPrefix() { return "[discovery-" + currencyString + "]"; } - /** - * Constructor for production use - creates its own HTTPClient - */ public AddressDiscoveryService(CoinInstance coinInstance) { this(coinInstance, new HTTPClient(5)); } - /** - * Constructor for testing - accepts HTTPClient as parameter for dependency injection - */ public AddressDiscoveryService(CoinInstance coinInstance, HTTPClient httpClient) { this.coinInstance = coinInstance; this.httpClient = httpClient; @@ -52,138 +53,131 @@ public AddressDiscoveryService(CoinInstance coinInstance, HTTPClient httpClient) LOGGER.log(Level.FINER, getLogPrefix() + " AddressDiscoveryService initialized for " + currencyString); } - /** - * Setter for timeout - for testing purposes only - */ public static void setDiscoveryTimeoutMs(int timeoutMs) { DISCOVERY_TIMEOUT_MS = timeoutMs; } /** - * Main discovery method - determines correct addressCount based on last used address with funds + 1 + * Discovers the correct address count by scanning batches sequentially from batch 0. + * + * Each batch (100 addresses) is checked for any UTXOs. As long as a batch has funds, + * the scan continues to the next batch. The first empty batch marks the boundary. + * Within the last non-empty batch, the exact highest funded address is found. + * + * Returns the index of the last funded address + 1 as the discovered address count. + * Returns the current config value if batch 0 is empty (wallet unused) or on failure. */ public int discoverAddressCount() { - LOGGER.log(Level.FINE, getLogPrefix() + " Starting address discovery for " + currencyString); - long discoveryStartTime = System.currentTimeMillis(); - int consecutiveFailures = 0; - int lastUsedIndex = -1; - int consecutiveEmpty = 0; + long startTime = System.currentTimeMillis(); int currentAddressCount = configHelper.getAddressCount(); - int batchStart = currentAddressCount; - LOGGER.log(Level.FINE, getLogPrefix() + " Starting discovery from address index: " + currentAddressCount); + + LOGGER.log(Level.FINE, getLogPrefix() + " Starting sequential batch scan"); + try { - while (consecutiveEmpty < GAP_LIMIT && batchStart < MAX_DISCOVERY_DEPTH) { - // Check for discovery timeout - long elapsedTime = System.currentTimeMillis() - discoveryStartTime; - if (elapsedTime > DISCOVERY_TIMEOUT_MS) { - LOGGER.log(Level.WARNING, getLogPrefix() + " Discovery timeout reached after " + - (elapsedTime / 1000) + " seconds, aborting discovery"); - return configHelper.getAddressCount(); - } - // Check for consecutive failures (circuit breaker) - if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) { - LOGGER.log(Level.SEVERE, getLogPrefix() + " Maximum consecutive failures (" + - MAX_CONSECUTIVE_FAILURES + ") reached, aborting discovery"); - return configHelper.getAddressCount(); - } - // Progress logging every 5 batches - if (batchStart > currentAddressCount && batchStart % (BATCH_SIZE * 5) == 0) { - LOGGER.log(Level.INFO, getLogPrefix() + " Discovery progress: " + batchStart + - " addresses checked, " + consecutiveEmpty + " consecutive empty"); + if (isTimedOut(startTime)) return currentAddressCount; + + int lastNonEmptyBatch = -1; + List lastBatchUtxos = null; + int consecutiveErrors = 0; + + for (int i = 0; i < NUM_BATCHES; i++) { + if (isTimedOut(startTime)) { + LOGGER.log(Level.WARNING, getLogPrefix() + " Timeout at batch " + i); + break; } - LOGGER.log(Level.FINE, getLogPrefix() + " Processing batch starting at index " + batchStart); - // Generate batch of addresses - List batch = generateAddressBatch(batchStart, BATCH_SIZE); - // Check for UTXOs in batch - List batchUtxos = checkBatchForUtxos(batch); - // Handle HTTP failures with circuit breaker - if (batchUtxos == null) { - consecutiveFailures++; - LOGGER.log(Level.WARNING, getLogPrefix() + " HTTP failure " + consecutiveFailures + - "/" + MAX_CONSECUTIVE_FAILURES + " for batch starting at " + batchStart); - // Continue to next batch instead of failing immediately - batchStart += BATCH_SIZE; + + ensureAddressesGenerated((i + 1) * BATCH_SIZE); + + List utxos = probeBatch(i); + + if (utxos == null) { + consecutiveErrors++; + if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) { + LOGGER.log(Level.WARNING, getLogPrefix() + + " Aborting: " + MAX_CONSECUTIVE_ERRORS + " consecutive HTTP failures"); + break; + } continue; - } else { - consecutiveFailures = 0; // Reset failure count on success - } - if (!batchUtxos.isEmpty()) { - // Found UTXOs - update last used index - int batchLastUsedIndex = findLastUsedIndex(batch, batchUtxos); - int globalLastUsedIndex = batchStart + batchLastUsedIndex; - lastUsedIndex = Math.max(lastUsedIndex, globalLastUsedIndex); - consecutiveEmpty = 0; - LOGGER.log(Level.INFO, getLogPrefix() + " Found UTXOs in batch, last used index: " + - globalLastUsedIndex + ", batch range: " + batchStart + "-" + - (batchStart + BATCH_SIZE - 1)); - } else { - consecutiveEmpty += BATCH_SIZE; - LOGGER.log(Level.FINE, getLogPrefix() + " Empty batch (addresses " + batchStart + "-" + - (batchStart + BATCH_SIZE - 1) + "), consecutive empty: " + consecutiveEmpty); - } - batchStart += BATCH_SIZE; - // Safety check for max depth - if (batchStart >= MAX_DISCOVERY_DEPTH) { - LOGGER.log(Level.WARNING, getLogPrefix() + " Hit max discovery depth at " + MAX_DISCOVERY_DEPTH); - break; } + + consecutiveErrors = 0; + if (utxos.isEmpty()) break; + + lastNonEmptyBatch = i; + lastBatchUtxos = utxos; } - // Calculate final address count: last used address with funds detected + 1 - int finalCount; - if (lastUsedIndex >= 0) { - // Found used addresses, set to last used + 1 - finalCount = lastUsedIndex + 1; - LOGGER.log(Level.INFO, getLogPrefix() + " Found used addresses, setting address count to: " + finalCount); - } else { - // No used addresses found, keep current config value - finalCount = configHelper.getAddressCount(); - LOGGER.log(Level.FINE, getLogPrefix() + " No used addresses found, keeping current address count: " + finalCount); + if (lastNonEmptyBatch < 0) { + LOGGER.log(Level.INFO, getLogPrefix() + " No UTXOs found"); + return currentAddressCount; } - LOGGER.log(Level.INFO, getLogPrefix() + " Discovery complete for " + currencyString + - ". Last used index: " + lastUsedIndex + ", final address count: " + finalCount); + // Find exact highest funded address in the last non-empty batch + int batchStart = lastNonEmptyBatch * BATCH_SIZE; + List batch = getBatch(batchStart, BATCH_SIZE); + + int lastUsedInBatch = findLastUsedIndexInBatch(batch, lastBatchUtxos); + int discoveredCount = batchStart + lastUsedInBatch + 1; + + LOGGER.log(Level.INFO, getLogPrefix() + " Discovery complete: lastBatch=" + + lastNonEmptyBatch + ", count=" + discoveredCount + + ", time=" + (System.currentTimeMillis() - startTime) + "ms"); - return finalCount; + return discoveredCount; } catch (Exception e) { - LOGGER.log(Level.SEVERE, getLogPrefix() + " Error during discovery for " + currencyString, e); - return configHelper.getAddressCount(); + LOGGER.log(Level.SEVERE, getLogPrefix() + " Error during discovery", e); + return currentAddressCount; } } /** - * Generate a batch of addresses starting from a specific index + * Probe a batch for UTXOs. + * @return UTXO list (may be empty), or null on HTTP failure */ - private List generateAddressBatch(int startIndex, int batchSize) { - List batch = new ArrayList<>(); - // Ensure we have enough addresses generated - int currentGenerated = coinInstance.getAddressKeyPairs().size(); - int needed = startIndex + batchSize; - if (needed > currentGenerated) { - // Generate additional addresses starting from currentGenerated - for (int i = currentGenerated; i < needed; i++) { - AddressBalance addr = coinInstance.generateAddress(false); - // Don't add to batch here - we'll extract the correct slice below - } - LOGGER.log(Level.FINE, getLogPrefix() + " Generated " + (needed - currentGenerated) + - " new addresses for " + currencyString); - } - // Always extract the batch from the correct startIndex range - for (int i = startIndex; i < needed; i++) { + private List probeBatch(int batchIndex) { + List batch = getBatch(batchIndex * BATCH_SIZE, BATCH_SIZE); + return checkBatchForUtxos(batch); + } + + /** + * Get a slice of addresses [startIndex, startIndex + size) from CoinInstance. + */ + private List getBatch(int startIndex, int size) { + List batch = new ArrayList<>(size); + int end = Math.min(startIndex + size, coinInstance.getAddressKeyPairs().size()); + for (int i = startIndex; i < end; i++) { batch.add(coinInstance.getAddressKeyPairs().get(i)); } return batch; } /** - * Check a batch of addresses for UTXOs + * Ensure addresses up to count are generated in CoinInstance. + */ + private void ensureAddressesGenerated(int count) { + int currentGenerated = coinInstance.getAddressKeyPairs().size(); + if (count > currentGenerated) { + int toGenerate = count - currentGenerated; + for (int i = 0; i < toGenerate; i++) { + coinInstance.generateAddress(false); + } + LOGGER.log(Level.FINE, getLogPrefix() + " Generated " + toGenerate + " addresses"); + } + } + + private boolean isTimedOut(long startTime) { + return (System.currentTimeMillis() - startTime) > DISCOVERY_TIMEOUT_MS; + } + + /** + * Check a batch of addresses for UTXOs via HTTP. + * @return list of UTXOs found (may be empty), or null on HTTP failure */ private List checkBatchForUtxos(List batch) { if (batch.isEmpty()) { return new ArrayList<>(); } - // Extract addresses for UTXO query String[] addresses = batch.stream() .map(addr -> addr.getAddress().toBase58()) .toArray(String[]::new); @@ -191,10 +185,9 @@ private List checkBatchForUtxos(List batch) { try { utxoResponse = httpClient.getUtxosUncached(coinInstance.getTicker(), addresses); } catch (Exception e) { - // Log without stack trace to avoid bloated output in tests - LOGGER.log(Level.SEVERE, getLogPrefix() + " HTTP request failed for addresses " + - addresses[0] + "..." + addresses[addresses.length - 1] + " - " + e.getMessage()); - return null; // Signal failure to caller + LOGGER.log(Level.SEVERE, getLogPrefix() + " HTTP request failed for addresses " + + addresses[0] + "..." + addresses[addresses.length - 1] + " - " + e.getMessage()); + return null; } if (utxoResponse == null || utxoResponse.size() == 0) { return new ArrayList<>(); @@ -204,7 +197,6 @@ private List checkBatchForUtxos(List batch) { try { JsonObject utxoJson = element.getAsJsonObject(); - // Validate required fields exist and are not null JsonElement addressElement = utxoJson.get("address"); JsonElement txidElement = utxoJson.get("txid"); JsonElement voutElement = utxoJson.get("vout"); @@ -230,44 +222,28 @@ private List checkBatchForUtxos(List batch) { utxos.add(utxo); } catch (Exception e) { LOGGER.log(Level.WARNING, getLogPrefix() + " Failed to parse UTXO response element: " + e.getMessage()); - // Continue processing other UTXOs instead of failing completely } } return utxos; } /** - * Find the last used address index in the batch - * Uses HashMap for O(1) lookups when batch size is large enough to benefit + * Find the highest address index within a batch that has UTXOs. + * Uses HashMap for O(1) lookups when batch size > 20. */ - private int findLastUsedIndex(List batch, List utxos) { - // Use HashMap for O(1) lookups when batch is large enough to benefit - if (batch.size() > 20) { - Map addressToIndex = new HashMap<>(batch.size()); - for (int i = 0; i < batch.size(); i++) { - addressToIndex.put(batch.get(i).getAddress().toBase58(), i); - } + private int findLastUsedIndexInBatch(List batch, List utxos) { + Map addressToIndex = new HashMap<>(batch.size()); + for (int i = 0; i < batch.size(); i++) { + addressToIndex.put(batch.get(i).getAddress().toBase58(), i); + } - int lastIndex = 0; - for (UTXO utxo : utxos) { - Integer index = addressToIndex.get(utxo.getAddress()); - if (index != null) { - lastIndex = Math.max(lastIndex, index); - } - } - return lastIndex; - } else { - // For small batches, linear search is faster due to cache locality - int lastIndex = 0; - for (UTXO utxo : utxos) { - for (int i = 0; i < batch.size(); i++) { - if (batch.get(i).getAddress().toBase58().equals(utxo.getAddress())) { - lastIndex = Math.max(lastIndex, i); - break; - } - } + int lastIndex = 0; + for (UTXO utxo : utxos) { + Integer index = addressToIndex.get(utxo.getAddress()); + if (index != null) { + lastIndex = Math.max(lastIndex, index); } - return lastIndex; } + return lastIndex; } -} \ No newline at end of file +} diff --git a/src/test/java/AddressDiscoveryServiceTest.java b/src/test/java/AddressDiscoveryServiceTest.java index ea9a49c..befe7a6 100644 --- a/src/test/java/AddressDiscoveryServiceTest.java +++ b/src/test/java/AddressDiscoveryServiceTest.java @@ -17,15 +17,17 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; /** - * Comprehensive test class for AddressDiscoveryService. - * Tests address discovery functionality including timeout handling, circuit breaker patterns, - * batch processing, and various edge cases. + * Tests for AddressDiscoveryService sequential batch scan. + * + * Algorithm: scan batches 0, 1, 2, ... Stop at first empty batch. + * 3 consecutive HTTP failures → abort. No gap limit. */ class AddressDiscoveryServiceTest extends TestHelper { @@ -36,359 +38,149 @@ class AddressDiscoveryServiceTest extends TestHelper { @BeforeEach void setup() { commonSetup(); - // Initialize Mockito MockitoAnnotations.openMocks(this); - // Initialize coin instance with test parameters - coinInstance = CoinInstance.getInstance(CoinTicker.LITECOIN); + coinInstance = CoinInstance.getInstance(CoinTicker.BLOCKNET); assertNotNull(coinInstance); coinInstance.getConfigHelper().setAddressCount(getAddressCountInitial()); assertNull(coinInstance.init(LoginUtils.loginToEntropy(getPassword()), getMnemonic(), false)); - // Create mock HTTP client mockHttpClient = mock(HTTPClient.class); - - // Reset timeout to default value before each test AddressDiscoveryService.setDiscoveryTimeoutMs(30000); - - // Create discovery service with mocked HTTP client for testing discoveryService = new AddressDiscoveryService(coinInstance, mockHttpClient); } @AfterAll static void cleanup() { - // Reset timeout to default value for other tests commonCleanup(); } - /** - * Test 1: Address discovery correctly identifies last used address + 1 - * Tests the core functionality of finding used addresses and setting address count correctly. - */ - @Test - void testAddressDiscovery_FindsUsedAddresses() { - // Setup: Generate addresses and simulate UTXOs for some addresses - int initialAddressCount = getAddressCountInitial(); - int usedAddressIndex = initialAddressCount + 10; // Use an address beyond initial count - - // Generate enough addresses for testing - for (int i = 0; i < 50; i++) { - coinInstance.generateAddress(false); - } - - // Create mock UTXO response for the used address - JsonArray mockUtxos = new JsonArray(); - JsonObject utxo1 = new JsonObject(); - utxo1.addProperty("address", coinInstance.getAddressKeyPairs().get(usedAddressIndex).getAddress().toBase58()); - utxo1.addProperty("txid", "test-txid-1"); - utxo1.addProperty("vout", 0); - utxo1.addProperty("confirmations", 10); - utxo1.addProperty("value", 1.5); - mockUtxos.add(utxo1); - - // Mock HTTP client to return UTXOs for batch containing the used address - when(mockHttpClient.getUtxosUncached(any(), any(String[].class))) - .thenAnswer(invocation -> { - String[] addresses = invocation.getArgument(1); - // Check if this batch contains our used address - for (String addr : addresses) { - if (addr.equals(coinInstance.getAddressKeyPairs().get(usedAddressIndex).getAddress().toBase58())) { - return mockUtxos; - } - } - return new JsonArray(); // Empty for other batches - }); - - // Execute: Run discovery - int discoveredAddressCount = discoveryService.discoverAddressCount(); - - // Verify: Discovery should find the used address and set count to last used + 1 - assertEquals(usedAddressIndex + 1, discoveredAddressCount, - "Discovery should set address count to last used address index + 1"); - - // Verify HTTP client was called - verify(mockHttpClient, atLeastOnce()).getUtxosUncached(any(), any(String[].class)); + private JsonArray buildUtxoResponse(String address) { + JsonArray result = new JsonArray(); + JsonObject utxo = new JsonObject(); + utxo.addProperty("address", address); + utxo.addProperty("txid", "test-txid"); + utxo.addProperty("vout", 0); + utxo.addProperty("confirmations", 10); + utxo.addProperty("value", 1.5); + result.add(utxo); + return result; } - /** - * Test 2: Address discovery timeout handling after 1 second - * Tests that discovery aborts gracefully when timeout is reached. - */ + /** Batch 0 has UTXOs at index 30, batch 1 empty. Returns 31. */ @Test - @Timeout(5) // Test should complete quickly, timeout indicates infinite loop - void testAddressDiscovery_TimeoutHandling() { - // Set timeout to 1 second for test unit only - AddressDiscoveryService.setDiscoveryTimeoutMs(1000); - - // Setup: Mock HTTP client to throw exception immediately (simulating timeout) - when(mockHttpClient.getUtxosUncached(any(), any(String[].class))) - .thenThrow(new RuntimeException("Simulated timeout")) - .thenThrow(new RuntimeException("Simulated timeout")) - .thenThrow(new RuntimeException("Simulated timeout")); - - // Execute: Run discovery (should timeout after 1 second due to circuit breaker) - long startTime = System.currentTimeMillis(); - int discoveredAddressCount = discoveryService.discoverAddressCount(); - long elapsedTime = System.currentTimeMillis() - startTime; + void testDiscovery_FindsUsedAddresses() { + int usedAddressIndex = getAddressCountInitial() + 10; + for (int i = 0; i < 50; i++) coinInstance.generateAddress(false); - // Verify: Discovery should return original address count due to circuit breaker - assertEquals(getAddressCountInitial(), discoveredAddressCount, - "Discovery should return original address count when timeout occurs"); + String usedAddress = coinInstance.getAddressKeyPairs().get(usedAddressIndex).getAddress().toBase58(); - // Verify circuit breaker triggered (3 calls max) - verify(mockHttpClient, times(3)).getUtxosUncached(any(), any(String[].class)); - - // Verify timeout occurred within expected bounds (should be very fast due to circuit breaker) - if (!(elapsedTime < 1000)) { - fail("Discovery should complete quickly due to circuit breaker, actual: " + elapsedTime + "ms"); - } - } - - /** - * Test 3: Address discovery circuit breaker for 3 consecutive failures - * Tests that discovery aborts after 3 consecutive HTTP failures. - */ - @Test - void testAddressDiscovery_CircuitBreaker() { - // Setup: Mock HTTP client to throw exceptions (simulate failures) when(mockHttpClient.getUtxosUncached(any(), any(String[].class))) - .thenThrow(new RuntimeException("Network error")) - .thenThrow(new RuntimeException("Network error")) - .thenThrow(new RuntimeException("Network error")); - - // Execute: Run discovery - int discoveredAddressCount = discoveryService.discoverAddressCount(); - - // Verify: Discovery should return original address count due to circuit breaker - assertEquals(getAddressCountInitial(), discoveredAddressCount, - "Discovery should return original address count when circuit breaker trips"); + .thenAnswer(inv -> { + String[] addrs = inv.getArgument(1); + for (String a : addrs) + if (a.equals(usedAddress)) return buildUtxoResponse(usedAddress); + return new JsonArray(); + }); - // Verify HTTP client was called exactly 3 times (circuit breaker threshold) - verify(mockHttpClient, times(3)).getUtxosUncached(any(), any(String[].class)); + assertEquals(usedAddressIndex + 1, discoveryService.discoverAddressCount()); + verify(mockHttpClient, times(2)).getUtxosUncached(any(), any(String[].class)); } - /** - * Test 4: Address discovery when no addresses have been used - * Tests behavior when no addresses have UTXOs (empty wallet). - */ + /** Batch 0 empty → wallet unused. 1 HTTP call. */ @Test - void testAddressDiscovery_NoUsedAddresses() { - // Setup: Mock HTTP client to always return empty responses + void testDiscovery_EmptyWallet() { when(mockHttpClient.getUtxosUncached(any(), any(String[].class))) .thenReturn(new JsonArray()); - // Execute: Run discovery - int discoveredAddressCount = discoveryService.discoverAddressCount(); - - // Verify: Discovery should return original address count when no used addresses found - assertEquals(getAddressCountInitial(), discoveredAddressCount, - "Discovery should return original address count when no used addresses are found"); - - // Verify HTTP client was called for multiple batches - verify(mockHttpClient, atLeastOnce()).getUtxosUncached(any(), any(String[].class)); + assertEquals(getAddressCountInitial(), discoveryService.discoverAddressCount()); + verify(mockHttpClient, times(1)).getUtxosUncached(any(), any(String[].class)); } - /** - * Test 5: Address discovery batch processing with 100 addresses per batch - * Tests that discovery processes addresses in correct batch sizes. - */ + /** 3 consecutive HTTP failures → abort. 3 HTTP calls. */ @Test - void testAddressDiscovery_BatchProcessing() { - // Setup: Generate more addresses to test batch processing - int totalAddresses = 350; // More than 3 batches of 100 - for (int i = 0; i < totalAddresses - getAddressCountInitial(); i++) { - coinInstance.generateAddress(false); - } - - // Mock HTTP client to track batch sizes + @Timeout(30) + void testDiscovery_ThreeConsecutiveErrorsAbort() { when(mockHttpClient.getUtxosUncached(any(), any(String[].class))) - .thenAnswer(invocation -> { - String[] addresses = invocation.getArgument(1); - // Verify batch size is correct (except possibly the last batch) - if (addresses.length < totalAddresses) { - assertEquals(100, addresses.length, - "Batch size should be 100 for all batches except possibly the last"); - } - return new JsonArray(); // Empty response - }); - - // Execute: Run discovery - int discoveredAddressCount = discoveryService.discoverAddressCount(); - - // Verify: Discovery completed successfully - assertEquals(getAddressCountInitial(), discoveredAddressCount, - "Discovery should complete with original address count"); + .thenThrow(new RuntimeException("Network error")); - // Verify HTTP client was called for multiple batches - verify(mockHttpClient, atLeastOnce()).getUtxosUncached(any(), any(String[].class)); + assertEquals(getAddressCountInitial(), discoveryService.discoverAddressCount()); + verify(mockHttpClient, times(3)).getUtxosUncached(any(), any(String[].class)); } - /** - * Test 6: Address discovery with mixed UTXO responses - * Tests discovery when some batches have UTXOs and others don't. - */ + /** Batch 0 fails (1 error), batch 1 has UTXOs, batch 2 empty. Error counter resets on success. */ @Test - void testAddressDiscovery_MixedUtxoResponses() { - // Setup: Generate addresses and simulate UTXOs in non-consecutive batches - for (int i = 0; i < 200; i++) { - coinInstance.generateAddress(false); - } + void testDiscovery_SkipsFailedBatch() { + int batchSize = AddressDiscoveryService.getBatchSize(); + for (int i = 0; i < 2 * batchSize; i++) coinInstance.generateAddress(false); - int usedAddressIndex1 = getAddressCountInitial() + 50; - int usedAddressIndex2 = getAddressCountInitial() + 150; - - // Create mock UTXO responses - JsonArray mockUtxos1 = new JsonArray(); - JsonObject utxo1 = new JsonObject(); - utxo1.addProperty("address", coinInstance.getAddressKeyPairs().get(usedAddressIndex1).getAddress().toBase58()); - utxo1.addProperty("txid", "test-txid-1"); - utxo1.addProperty("vout", 0); - utxo1.addProperty("confirmations", 10); - utxo1.addProperty("value", 1.5); - mockUtxos1.add(utxo1); - - JsonArray mockUtxos2 = new JsonArray(); - JsonObject utxo2 = new JsonObject(); - utxo2.addProperty("address", coinInstance.getAddressKeyPairs().get(usedAddressIndex2).getAddress().toBase58()); - utxo2.addProperty("txid", "test-txid-2"); - utxo2.addProperty("vout", 1); - utxo2.addProperty("confirmations", 5); - utxo2.addProperty("value", 2.0); - mockUtxos2.add(utxo2); - - // Mock HTTP client to return UTXOs for specific batches + int usedAddressIndex = getAddressCountInitial() + batchSize + batchSize / 2; // batch 1 + String usedAddress = coinInstance.getAddressKeyPairs().get(usedAddressIndex).getAddress().toBase58(); + + AtomicInteger callCount = new AtomicInteger(0); when(mockHttpClient.getUtxosUncached(any(), any(String[].class))) - .thenAnswer(invocation -> { - String[] addresses = invocation.getArgument(1); - for (String addr : addresses) { - if (addr.equals(coinInstance.getAddressKeyPairs().get(usedAddressIndex1).getAddress().toBase58())) { - return mockUtxos1; - } - if (addr.equals(coinInstance.getAddressKeyPairs().get(usedAddressIndex2).getAddress().toBase58())) { - return mockUtxos2; - } - } + .thenAnswer(inv -> { + int call = callCount.incrementAndGet(); + if (call == 1) throw new RuntimeException("Transient error"); + String[] addrs = inv.getArgument(1); + for (String a : addrs) + if (a.equals(usedAddress)) return buildUtxoResponse(usedAddress); return new JsonArray(); }); - // Execute: Run discovery - int discoveredAddressCount = discoveryService.discoverAddressCount(); - - // Verify: Discovery should find the last used address (higher index) - assertEquals(usedAddressIndex2 + 1, discoveredAddressCount, - "Discovery should find the last used address across multiple batches"); + assertEquals(usedAddressIndex + 1, discoveryService.discoverAddressCount()); + verify(mockHttpClient, times(3)).getUtxosUncached(any(), any(String[].class)); } - /** - * Test 7: Address discovery with partial batch processing - * Tests discovery when discovery stops before processing all batches due to gap limit. - */ + /** Valid UTXO plus invalid entry. Should still find the address. */ @Test - void testAddressDiscovery_PartialBatchProcessing() { - // Setup: Generate addresses and simulate UTXOs with a gap - for (int i = 0; i < 150; i++) { - coinInstance.generateAddress(false); - } + void testDiscovery_UtxoParsingErrors() { + for (int i = 0; i < 100; i++) coinInstance.generateAddress(false); - int usedAddressIndex = getAddressCountInitial() + 5; // Early in the sequence - - // Create mock UTXO response - JsonArray mockUtxos = new JsonArray(); - JsonObject utxo = new JsonObject(); - utxo.addProperty("address", coinInstance.getAddressKeyPairs().get(usedAddressIndex).getAddress().toBase58()); - utxo.addProperty("txid", "test-txid"); - utxo.addProperty("vout", 0); - utxo.addProperty("confirmations", 10); - utxo.addProperty("value", 1.0); - mockUtxos.add(utxo); + int usedAddressIndex = getAddressCountInitial() + 10; + String usedAddress = coinInstance.getAddressKeyPairs().get(usedAddressIndex).getAddress().toBase58(); - // Mock HTTP client when(mockHttpClient.getUtxosUncached(any(), any(String[].class))) - .thenAnswer(invocation -> { - String[] addresses = invocation.getArgument(1); - for (String addr : addresses) { - if (addr.equals(coinInstance.getAddressKeyPairs().get(usedAddressIndex).getAddress().toBase58())) { - return mockUtxos; + .thenAnswer(inv -> { + String[] addrs = inv.getArgument(1); + for (String a : addrs) { + if (a.equals(usedAddress)) { + JsonArray result = new JsonArray(); + result.add(buildUtxoResponse(usedAddress).get(0)); + JsonObject invalid = new JsonObject(); + invalid.addProperty("address", "invalid"); + result.add(invalid); + return result; } } return new JsonArray(); }); - // Execute: Run discovery - int discoveredAddressCount = discoveryService.discoverAddressCount(); - - // Verify: Discovery should stop after finding used address + gap limit - assertEquals(usedAddressIndex + 1, discoveredAddressCount, - "Discovery should stop after finding used address + gap limit"); + assertEquals(usedAddressIndex + 1, discoveryService.discoverAddressCount()); } - /** - * Test 8: Address discovery with UTXO parsing errors - * Tests that discovery continues when some UTXOs cannot be parsed. - */ + /** All NUM_BATCHES have UTXOs. Scans all batches. */ @Test - void testAddressDiscovery_UtxoParsingErrors() { - // Setup: Generate addresses - for (int i = 0; i < 100; i++) { - coinInstance.generateAddress(false); - } - - int usedAddressIndex = getAddressCountInitial() + 10; + void testDiscovery_AllBatchesFunded() { + int batchSize = AddressDiscoveryService.getBatchSize(); + int numBatches = AddressDiscoveryService.getNumBatches(); + for (int i = 0; i < numBatches * batchSize; i++) coinInstance.generateAddress(false); - // Create mixed UTXO response (valid and invalid) - JsonArray mockUtxos = new JsonArray(); - - // Valid UTXO - JsonObject validUtxo = new JsonObject(); - validUtxo.addProperty("address", coinInstance.getAddressKeyPairs().get(usedAddressIndex).getAddress().toBase58()); - validUtxo.addProperty("txid", "test-txid-1"); - validUtxo.addProperty("vout", 0); - validUtxo.addProperty("confirmations", 10); - validUtxo.addProperty("value", 1.5); - mockUtxos.add(validUtxo); - - // Invalid UTXO (missing required fields) - JsonObject invalidUtxo = new JsonObject(); - invalidUtxo.addProperty("address", "invalid-address"); - // Missing other required fields - mockUtxos.add(invalidUtxo); - - // Mock HTTP client when(mockHttpClient.getUtxosUncached(any(), any(String[].class))) - .thenAnswer(invocation -> { - String[] addresses = invocation.getArgument(1); - for (String addr : addresses) { - if (addr.equals(coinInstance.getAddressKeyPairs().get(usedAddressIndex).getAddress().toBase58())) { - return mockUtxos; - } - } - return new JsonArray(); - }); + .thenAnswer(inv -> buildUtxoResponse(((String[]) inv.getArgument(1))[0])); - // Execute: Run discovery - int discoveredAddressCount = discoveryService.discoverAddressCount(); - - // Verify: Discovery should handle parsing errors gracefully and still find valid UTXO - assertEquals(usedAddressIndex + 1, discoveredAddressCount, - "Discovery should continue despite UTXO parsing errors"); + assertEquals((numBatches - 1) * batchSize + 1, discoveryService.discoverAddressCount()); + verify(mockHttpClient, times(numBatches)).getUtxosUncached(any(), any(String[].class)); } - /** - * Test 9: Address discovery with concurrent access - * Tests that discovery service handles concurrent access safely. - */ + /** Concurrent access — all threads return same result. */ @Test - void testAddressDiscovery_ConcurrentAccess() throws InterruptedException { - // Setup: Generate addresses - for (int i = 0; i < 100; i++) { - coinInstance.generateAddress(false); - } + void testDiscovery_ConcurrentAccess() throws InterruptedException { + for (int i = 0; i < 100; i++) coinInstance.generateAddress(false); - // Mock HTTP client when(mockHttpClient.getUtxosUncached(any(), any(String[].class))) .thenReturn(new JsonArray()); - // Execute: Run multiple discovery operations concurrently int numThreads = 5; ExecutorService executor = Executors.newFixedThreadPool(numThreads); CountDownLatch latch = new CountDownLatch(numThreads); @@ -407,36 +199,24 @@ void testAddressDiscovery_ConcurrentAccess() throws InterruptedException { }); } - // Wait for all threads to complete latch.await(10, TimeUnit.SECONDS); executor.shutdown(); - // Verify: All concurrent operations should return the same result - assertEquals(numThreads, results.size(), "All threads should complete"); - int expectedResult = getAddressCountInitial(); - for (Integer result : results) { - assertEquals(expectedResult, result, "All concurrent operations should return the same result"); - } + assertEquals(numThreads, results.size()); + for (Integer result : results) + assertEquals(getAddressCountInitial(), result); } - /** - * Test 10: Address discovery with maximum depth limit - * Tests that discovery stops when reaching the maximum discovery depth. - */ + /** Batch probes use BATCH_SIZE addresses. */ @Test - void testAddressDiscovery_MaxDepthLimit() { - // Setup: Mock HTTP client to always return empty (no used addresses) + void testDiscovery_BatchSizeRespected() { + int batchSize = AddressDiscoveryService.getBatchSize(); when(mockHttpClient.getUtxosUncached(any(), any(String[].class))) - .thenReturn(new JsonArray()); - - // Execute: Run discovery (should hit max depth limit) - int discoveredAddressCount = discoveryService.discoverAddressCount(); - - // Verify: Discovery should return original address count when hitting max depth - assertEquals(getAddressCountInitial(), discoveredAddressCount, - "Discovery should return original address count when hitting max depth"); + .thenAnswer(inv -> { + assertEquals(batchSize, ((String[]) inv.getArgument(1)).length); + return new JsonArray(); + }); - // Verify HTTP client was called multiple times (indicating depth traversal) - verify(mockHttpClient, atLeastOnce()).getUtxosUncached(any(), any(String[].class)); + discoveryService.discoverAddressCount(); } -} \ No newline at end of file +} From eee2cb45974c91f57cbfa5e4d97b2f8a74b6ada9 Mon Sep 17 00:00:00 2001 From: tryiou Date: Thu, 2 Apr 2026 09:27:54 +0200 Subject: [PATCH 25/73] docs: add AGENTS.md and user guide --- AGENTS.md | 123 +++++++ docs/USER_GUIDE.md | 873 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 996 insertions(+) create mode 100644 AGENTS.md create mode 100644 docs/USER_GUIDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..6664088 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,123 @@ +# AGENTS.md + +## Project Overview + +XLite Daemon — a multi-cryptocurrency wallet daemon built with Java 21 and Maven. +Core packages: `crypto` (wallet encryption/key management), `net` (coin networking, JSON-RPC), +`util` (config, address discovery, logging), `wallet` (wallet helpers). + +## Build & Test Commands + +```bash +# Compile +mvn compile -q + +# Run all tests +mvn test + +# Run a single test class +mvn test -pl . -Dtest=KeyHandlerTest + +# Run a single test method +mvn test -pl . -Dtest=KeyHandlerTest#testGetBaseSeed + +# Build shaded JAR +mvn package -q + +# Requirements: Java 21, Maven 3.8.6+ +``` + +## Code Style + +### Imports + +- Group order: third-party libraries, then `java.*`, then `javax.*` +- Wildcard imports are acceptable for large groups (e.g., `java.io.*`, `org.bitcoinj.core.*`) +- No unused imports; OpenRewrite cleanup runs on `mvn compile` + +### Formatting + +- 4-space indentation, no tabs +- Opening braces on same line +- Blank line between methods, between logical sections within methods +- No trailing whitespace + +### Logging + +Every class uses this logger pattern: + +```java +private final static LogManager LOGMANAGER = LogManager.getLogManager(); +private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); +``` + +Log levels: `SEVERE` for critical failures, `WARNING` for recoverable errors, +`INFO` for operational events, `FINER` for debugging (e.g., wrong password). + +Log messages use bracketed prefixes: `[security]`, `[discovery-BLOCK]`, `[wallet]`. + +### Naming + +- Classes: `PascalCase` +- Methods/variables: `camelCase` +- Constants: `UPPER_SNAKE_CASE` +- Test classes: `Test.java` +- Test methods: `test` (e.g., `testGetBaseSeed`, `testLegacyWalletMigration`) + +### Error Handling + +- Crypto operations: use try/finally to clear sensitive byte arrays with `Arrays.fill(bytes, (byte) 0)` +- Use `PBEKeySpec.clearPassword()` after key derivation +- Do not use `e.printStackTrace()` — use `LOGGER.log(Level.WARNING, "message", e)` instead +- Catch specific exceptions (`BadPaddingException`) before generic `Exception` +- `RuntimeException` for unrecoverable state; return `null` or `false` for expected failures + +### Security Conventions + +- AES-CBC with random IV for all new encryption; ECB only for legacy decryption +- PBKDF2 with `PBKDF2WithHmacSHA256`, 100k iterations for current format +- `SecureRandom.getInstanceStrong()` for all cryptographic RNG +- Explicit `StandardCharsets.UTF_8` in all `getBytes()` calls +- Clear sensitive data in `finally` blocks — never rely on GC alone + +## Project Structure + +``` +src/main/java/io/cloudchains/app/ + crypto/ KeyHandler (wallet encryption), LoginUtils (auth) + net/ CoinInstance (coin lifecycle), JSON-RPC servers, protocols/ + util/ ConfigHelper, AddressDiscoveryService, UTXO, logging + wallet/ WalletHelper + App.java Main entry point + +src/test/java/ + KeyHandlerTest.java, CoinInstanceTest.java, ConfigHelperTest.java, + AddressDiscoveryServiceTest.java, LoginUtilsTest.java +``` + +## Key Dependencies + +| Library | Purpose | +|---------|---------| +| bitcoinj-core 0.14.7 | Bitcoin/crypto primitives, MnemonicCode, ECKey | +| Gson 2.13.2 | JSON serialization | +| Netty 4.2.7 | HTTP servers, networking | +| Guava (via bitcoinj) | Joiner, Preconditions, utilities | +| JUnit Jupiter 5.11.3 | Test framework | +| Mockito 5.15.2 | Test mocking | + +## Commit Message Style + +Recent commits use conventional commits: `type(scope): description` + +Types: `feat`, `fix`, `chore`, `refactor`, `build`, `security`, `test`. + +Some older commits use `[category] description` style (e.g., `[security] Upgrade wallet encryption`). + +## Things to Watch For + +- The `rewrite-maven-plugin` runs on `mvn compile` and may auto-modify imports and formatting. + Always review `git diff` after compiling. +- `ConfigHelper.CONFIG_DIR` is a mutable static used to override config path in tests. +- Tests create temp directories and write wallet files; `@AfterEach` handles cleanup. +- The enforcer plugin requires Java 21 and Maven 3.8.6+. diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md new file mode 100644 index 0000000..167c43e --- /dev/null +++ b/docs/USER_GUIDE.md @@ -0,0 +1,873 @@ +# XLite Daemon User Guide + +## Table of Contents +1. [Overview](#overview) +2. [System Requirements](#system-requirements) +3. [Installation](#installation) +4. [Configuration](#configuration) +5. [Usage](#usage) +6. [Supported Cryptocurrencies](#supported-cryptocurrencies) +7. [Security Features](#security-features) +8. [Troubleshooting](#troubleshooting) +9. [API Reference](#api-reference) +10. [Advanced Configuration](#advanced-configuration) + +## Overview + +The XLite Daemon is a multi-cryptocurrency wallet backend that provides secure wallet management, blockchain connectivity, and JSON-RPC API services for various cryptocurrencies. It serves as the backend infrastructure for the XLite wallet application. + +### Key Features +- **Multi-Currency Support**: Supports 10+ cryptocurrencies including Bitcoin, Litecoin, Dash, and more +- **Secure Wallet Management**: Industry-standard encryption with PBKDF2 key derivation +- **Address Discovery**: Automatic detection of used addresses for wallet recovery +- **JSON-RPC API**: Comprehensive API for wallet operations and blockchain queries +- **XRouter Integration**: Support for cross-chain communication (Blocknet network) +- **Native Compilation**: Optimized native binary for better performance + +## System Requirements + +### Minimum Requirements +- **Operating System**: Windows 10+, macOS 10.14+, Linux (kernel 3.10+) +- **Java**: JDK 21 or higher +- **Maven**: 3.8.6 or higher +- **Memory**: 2GB RAM minimum, 4GB recommended +- **Storage**: 500MB free space for wallet data and blockchain metadata + +### Recommended Requirements +- **Operating System**: Latest stable version of your preferred OS +- **Java**: Latest JDK 21 LTS +- **Memory**: 8GB RAM or more +- **Storage**: SSD with 1GB+ free space +- **Network**: Stable internet connection + +## Installation + +### Prerequisites +1. Install JDK 21: + ```bash + # Ubuntu/Debian + sudo apt update && sudo apt install openjdk-21-jdk + + # macOS (using Homebrew) + brew install openjdk@21 + + # Windows: Download from Oracle or Adoptium + ``` + +2. Install Maven 3.8.6+: + ```bash + # Ubuntu/Debian + sudo apt install maven + + # macOS (using Homebrew) + brew install maven + + # Verify installation + java -version + mvn -version + ``` + +### Building from Source + +1. **Clone the repository**: + ```bash + git clone https://github.com/blocknetdx/xlite-daemon + cd xlite-daemon + ``` + +2. **Make Maven wrapper executable** (Linux/macOS only): + ```bash + chmod +x mvnw + ``` + +3. **Build the project**: + ```bash + # Full build with native compilation (recommended) + ./mvnw clean package -Pnative + + # Faster build without tests (development) + ./mvnw clean package -Pnative-fast + ``` + +4. **Run the application**: + ```bash + # Using the native binary + ./target/xlite-daemon + + # Or using Maven + ./mvnw exec:java + ``` + +### Environment Variables + +Set these environment variables before running: + +```bash +# Required for wallet initialization +export WALLET_MNEMONIC="your twelve word mnemonic phrase here" +export WALLET_PASSWORD="your secure password" + +# Optional: Custom EXR endpoints (comma-separated list of backend servers) +export EXR_ENDPOINT="https://server1.example.com,https://server2.example.com" +``` + +## Configuration + +### Configuration Files Location + +The daemon stores configuration files in your system's application data directory: + +- **Windows**: `%appdata%\CloudChains\settings\config-*.json` +- **macOS**: `~/Library/Application Support/CloudChains/settings/config-*.json` +- **Linux**: `~/.config/CloudChains/settings/config-*.json` + +### Configuration Structure + +Each cryptocurrency has its own configuration file named `config-{ticker}.json`: + +```json +{ + "fee": 0.0001, + "feeFlat": true, + "rpcEnabled": true, + "rpcUsername": "xlite", + "rpcPassword": "securepassword123", + "rpcPort": 9955, + "addressCount": 25 +} +``` + +### Configuration Options + +| Setting | Type | Description | Default | +|---------|------|-------------|---------| +| `fee` | number | Transaction fee rate | 0.0001 | +| `feeFlat` | boolean | Use flat fee vs dynamic | true | +| `rpcEnabled` | boolean | Enable JSON-RPC server | false | +| `rpcUsername` | string | RPC authentication username | "" | +| `rpcPassword` | string | RPC authentication password | "" | +| `rpcPort` | number | RPC server port | -1000 (auto) | +| `addressCount` | number | Number of addresses to generate | 0 | + +## Usage + +### Starting the Daemon + +1. **Basic startup**: + ```bash + ./target/xlite-daemon + ``` + +2. **With custom arguments**: + ```bash + ./target/xlite-daemon --network=testnet --debug + ``` + +3. **As a background service**: + ```bash + # Linux/macOS + nohup ./target/xlite-daemon > daemon.log 2>&1 & + + # Windows + start /B xlite-daemon.exe > daemon.log + ``` + +### Wallet Management + +#### Creating a New Wallet + +1. **Generate a new mnemonic**: + ```bash + # The daemon will automatically generate a 12-word mnemonic on first run + # or you can provide one via WALLET_MNEMONIC environment variable + ``` + +2. **Set a secure password**: + ```bash + export WALLET_PASSWORD="your-secure-password-here" + ``` + +3. **Verify password strength**: + The daemon includes a password strength calculator that evaluates: + - Length (8+ characters required) + - Character variety (uppercase, lowercase, numbers, symbols) + - Overall security score (0-10) + +#### Importing an Existing Wallet + +```bash +# Set your existing mnemonic +export WALLET_MNEMONIC="your existing twelve word phrase here" +export WALLET_PASSWORD="your password" + +# Start the daemon +./target/xlite-daemon +``` + + +### Supported Operations + +#### Basic Wallet Operations + +```bash +# Get wallet balance +curl -X POST http://localhost:9955 \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"getinfo","params":[],"id":1}' + +# Generate new address +curl -X POST http://localhost:9955 \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"getnewaddress","params":[],"id":1}' + +# List unspent transactions (UTXOs) +curl -X POST http://localhost:9955 \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"listunspent","params":[],"id":1}' +``` + +#### Transaction Operations + +```bash +# Create raw transaction +curl -X POST http://localhost:9955 \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"createrawtransaction","params":[[{"txid":"...","vout":0}],{"address":0.001}],"id":1}' + +# Sign transaction +curl -X POST http://localhost:9955 \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"signrawtransaction","params":["rawtx"],"id":1}' + +# Broadcast transaction +curl -X POST http://localhost:9955 \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"sendrawtransaction","params":["signedtx"],"id":1}' +``` + +## Supported Cryptocurrencies + +The daemon supports the following cryptocurrencies: + +| Coin | Ticker | Network | RPC Port | Status | +|------|--------|---------|----------|---------| +| Blocknet | BLOCK | Mainnet | 41419 | ✅ Active | +| Blocknet Testnet | TBLOCK | Testnet | 41419 | ✅ Active | +| Bitcoin | BTC | Mainnet | 8332 | ✅ Active | +| Litecoin | LTC | Mainnet | 9332 | ✅ Active | +| Dash | DASH | Mainnet | 9998 | ✅ Active | +| Dogecoin | DOGE | Mainnet | 22555 | ✅ Active | +| Syscoin | SYS | Mainnet | 8370 | ✅ Active | +| PIVX | PIVX | Mainnet | 9951 | ✅ Active | +| Unobtanium | UNO | Mainnet | 65111 | ✅ Active | + +### Network Configuration + +Each coin has specific network parameters: +- **Mainnet**: Production blockchain networks +- **Testnet**: Testing and development networks +- **RPC Ports**: Default ports for JSON-RPC communication + +## Security Features + +### Wallet Encryption + +The daemon uses industry-standard security practices: + +1. **PBKDF2 Key Derivation**: SHA-256 with 100,000 iterations +2. **AES Encryption**: 256-bit encryption for wallet data +3. **Secure Random Generation**: Cryptographically strong random number generation +4. **Memory Protection**: Sensitive data cleared from memory after use + +### Legacy Wallet Migration + +The daemon automatically migrates legacy wallets: + +1. **Detection**: Identifies SHA-1 based legacy wallets +2. **Backup**: Creates backup before migration +3. **Upgrade**: Migrates to SHA-256 with improved security +4. **Validation**: Verifies migration success before cleanup + +### Password Security + +The daemon includes password strength validation: + +```bash +# Password scoring system: +# 8-9 characters: 1 point +# 10+ characters: 2 points +# Contains digit: +2 points +# Contains lowercase: +2 points +# Contains uppercase: +2 points +# Contains special character: +2 points +# Maximum score: 10 points +``` + +### Address Security + +1. **HD Wallet Support**: Hierarchical Deterministic wallet generation +2. **Address Gap Limit**: Prevents address exhaustion attacks +3. **Forward Address Generation**: Pre-generates addresses for performance +4. **Address Discovery**: Automatically finds used addresses for recovery + +## Troubleshooting + +### Common Issues + +#### 1. Port Already in Use + +**Problem**: "Address already in use" error on startup + +**Solution**: +```bash +# Check which process is using the port +lsof -i :9955 # Linux/macOS +netstat -ano | findstr :9955 # Windows + +# Kill the process or change the port in config +``` + +#### 2. Wallet Not Found + +**Problem**: "Wallet not found on disk" error + +**Solution**: +```bash +# Check if wallet file exists +ls ~/.config/CloudChains/key.dat + +# Verify permissions +chmod 600 ~/.config/CloudChains/key.dat +``` + +#### 3. Network Connection Issues + +**Problem**: Cannot connect to blockchain networks + +**Solution**: +```bash +# Check network connectivity +ping blockexplorer.com + +# Verify firewall settings +# Ensure outbound connections are allowed +``` + +#### 4. Memory Issues + +**Problem**: OutOfMemoryError or slow performance + +**Solution**: +```bash +# Increase Java heap size +export JAVA_OPTS="-Xmx2g -Xms1g" + +# Or modify the startup script +java -Xmx2g -jar xlite-daemon.jar +``` + +### Log Analysis + +#### Log File Locations + +- **Error logs**: `~/.config/CloudChains/error-YYYY-MM-DD.log` +- **Application logs**: Console output (configurable) + +#### Common Log Patterns + +```bash +# Wallet initialization +[wallet] Initializing wallet for BTC + +# Network connection +[peer] Connecting to network... + +# RPC requests +[rpc] Received JSON-RPC request: getinfo + +# Address discovery +[discovery] Processing batch starting at index 100 +``` + +### Debug Mode + +Enable debug logging: + +```bash +# Set logging level +export LOG_LEVEL=DEBUG + +# Or modify logging.properties +handlers=java.util.logging.ConsoleHandler +.level=FINE +``` + +### Performance Optimization + +#### Address Discovery Optimization + +1. **Batch Size**: Adjust batch size for your network +2. **Gap Limit**: Modify gap limit for faster discovery +3. **Timeout Settings**: Configure discovery timeouts + +#### Memory Management + +1. **Heap Size**: Allocate sufficient memory +2. **GC Tuning**: Optimize garbage collection +3. **Connection Pooling**: Reuse network connections + +### Recovery Procedures + +#### Wallet Recovery + +1. **From Mnemonic**: + ```bash + export WALLET_MNEMONIC="your twelve word phrase" + ./target/xlite-daemon + ``` + +2. **From Backup**: + ```bash + # Restore from backup directory + cp ~/.config/CloudChains/backups/key-backup-*.dat ~/.config/CloudChains/key.dat + ``` + +#### Configuration Recovery + +1. **Reset Configuration**: + ```bash + # Remove config files to reset + rm ~/.config/CloudChains/settings/config-*.json + ``` + +2. **Rebuild from Source**: + ```bash + ./mvnw clean package -Pnative + ``` + +## API Reference + +### JSON-RPC API + +The daemon provides a comprehensive JSON-RPC API for wallet and blockchain operations. + +#### Authentication + +All RPC requests require authentication: + +```bash +# Set credentials in config file or environment +rpcUsername=xlite +rpcPassword=yourpassword +``` + +#### Blockchain Operations + +##### getblockchaininfo +Get blockchain information + +```bash +curl -X POST http://localhost:9955 \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"getblockchaininfo","params":[],"id":1}' +``` + +**Response**: +```json +{ + "result": { + "chain": "main", + "blocks": 700000, + "headers": 700000, + "bestblockhash": "00000000000000000007bd1b11320e37172c4467554f7a87bf769874e65e361d", + "difficulty": 21768156514421.46, + "mediantime": 1640995200 + } +} +``` + +##### getblockhash +Get block hash by height + +```bash +curl -X POST http://localhost:9955 \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"getblockhash","params":[700000],"id":1}' +``` + +##### getblock +Get block information + +```bash +curl -X POST http://localhost:9955 \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"getblock","params":["blockhash"],"id":1}' +``` + +#### Wallet Operations + +##### getinfo +Get wallet information + +```bash +curl -X POST http://localhost:9955 \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"getinfo","params":[],"id":1}' +``` + +**Response**: +```json +{ + "result": { + "version": "0.5.15", + "protocolversion": 70015, + "walletversion": 169900, + "balance": 1.50000000, + "blocks": 700000, + "timeoffset": 0, + "connections": 8, + "proxy": "", + "difficulty": 21768156514421.46, + "testnet": false, + "keypoololdest": 1640995200, + "keypoolsize": 1000, + "paytxfee": 0.00001000, + "relayfee": 0.00001000, + "errors": "" + } +} +``` + +##### getnewaddress +Generate new wallet address + +```bash +curl -X POST http://localhost:9955 \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"getnewaddress","params":[],"id":1}' +``` + +**Response**: +```json +{ + "result": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh" +} +``` + +##### listunspent +List unspent transaction outputs + +```bash +curl -X POST http://localhost:9955 \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"listunspent","params":[],"id":1}' +``` + +**Response**: +```json +{ + "result": [ + { + "txid": "a1b2c3d4...", + "vout": 0, + "address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh", + "scriptPubKey": "001472749d6b6e...", + "amount": 0.50000000, + "confirmations": 100, + "spendable": true + } + ] +} +``` + +##### gettransaction +Get transaction details + +```bash +curl -X POST http://localhost:9955 \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"gettransaction","params":["txid"],"id":1}' +``` + +#### Raw Transaction Operations + +##### createrawtransaction +Create raw transaction + +```bash +curl -X POST http://localhost:9955 \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"createrawtransaction","params":[[{"txid":"a1b2c3d4...","vout":0}],[{"bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh":0.1}]],"id":1}' +``` + +##### decoderawtransaction +Decode raw transaction + +```bash +curl -X POST http://localhost:9955 \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"decoderawtransaction","params":["rawtx"],"id":1}' +``` + +##### signrawtransaction +Sign raw transaction + +```bash +curl -X POST http://localhost:9955 \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"signrawtransaction","params":["rawtx"],"id":1}' +``` + +##### sendrawtransaction +Broadcast transaction + +```bash +curl -X POST http://localhost:9955 \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"sendrawtransaction","params":["signedtx"],"id":1}' +``` + +#### Utility Operations + +##### dumpprivkey +Export private key + +```bash +curl -X POST http://localhost:9955 \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"dumpprivkey","params":["address"],"id":1}' +``` + +##### importprivkey +Import private key + +```bash +curl -X POST http://localhost:9955 \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"importprivkey","params":["privatekey"],"id":1}' +``` + +##### signmessage +Sign message with address + +```bash +curl -X POST http://localhost:9955 \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"signmessage","params":["address","message"],"id":1}' +``` + +##### verifymessage +Verify signed message + +```bash +curl -X POST http://localhost:9955 \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"verifymessage","params":["address","signature","message"],"id":1}' +``` + +### Error Handling + +#### Common Error Codes + +| Code | Message | Description | +|------|---------|-------------| +| -1 | "Bad password" | Incorrect wallet password | +| -2 | "Unsupported coin" | Coin not supported | +| -3 | "Bad mnemonic" | Invalid mnemonic phrase | +| -4 | "Change password failed" | Password change failed | +| -5 | "Wallet not found" | No wallet file exists | + +#### Error Response Format + +```json +{ + "error": { + "code": -1, + "message": "Bad password" + }, + "id": 1 +} +``` + +## Advanced Configuration + +### Custom RPC Ports + +Configure custom RPC ports for each coin: + +```json +{ + "rpcPort": 9955, + "rpcEnabled": true, + "rpcUsername": "xlite", + "rpcPassword": "securepassword" +} +``` + +### Network Configuration + +#### Proxy Settings + +Configure proxy for network connections: + +```bash +# Set proxy environment variables +export http_proxy="http://proxy.company.com:8080" +export https_proxy="http://proxy.company.com:8080" +``` + +#### Custom Nodes + +Configure custom blockchain nodes: + +```json +{ + "customNodes": [ + "node1.example.com:8333", + "node2.example.com:8333" + ] +} +``` + +### Performance Tuning + +#### JVM Parameters + +Optimize Java Virtual Machine settings: + +```bash +# High-performance settings +export JAVA_OPTS="-Xmx4g -Xms2g -XX:+UseG1GC -XX:MaxGCPauseMillis=200" +``` + +#### Network Optimization + +```bash +# Increase connection limits +export MAX_CONNECTIONS=50 + +# Optimize network buffers +export NETWORK_BUFFER_SIZE=65536 +``` + +### Security Hardening + +#### Firewall Configuration + +Allow necessary ports: + +```bash +# Linux (iptables) +sudo iptables -A INPUT -p tcp --dport 9955 -j ACCEPT +sudo iptables -A INPUT -p tcp --dport 41419 -j ACCEPT + +# Windows (PowerShell) +New-NetFirewallRule -DisplayName "XLite Daemon" -Direction Inbound -Protocol TCP -LocalPort 9955 -Action Allow +``` + +#### File Permissions + +Secure wallet and configuration files: + +```bash +# Set restrictive permissions +chmod 600 ~/.config/CloudChains/key.dat +chmod 600 ~/.config/CloudChains/settings/config-*.json +chmod 700 ~/.config/CloudChains/ +``` + +### Monitoring and Logging + +#### Log Rotation + +Configure automatic log rotation: + +```bash +# Create logrotate configuration +sudo nano /etc/logrotate.d/xlite-daemon + +# Add configuration +/home/user/.config/CloudChains/error-*.log { + daily + rotate 30 + compress + delaycompress + missingok + notifempty +} +``` + +#### Health Monitoring + +Monitor daemon health: + +```bash +# Check process status +ps aux | grep xlite-daemon + +# Monitor logs in real-time +tail -f ~/.config/CloudChains/error-*.log + +# Check network connections +netstat -tulpn | grep xlite-daemon +``` + +### Backup and Recovery + +#### Automated Backups + +Create automated backup script: + +```bash +#!/bin/bash +# backup-wallet.sh + +BACKUP_DIR="/backup/xlite-daemon" +DATE=$(date +%Y%m%d_%H%M%S) + +mkdir -p "$BACKUP_DIR" + +# Backup wallet file +cp ~/.config/CloudChains/key.dat "$BACKUP_DIR/key-$DATE.dat" + +# Backup configuration +cp ~/.config/CloudChains/settings/config-*.json "$BACKUP_DIR/" + +# Compress backup +tar -czf "$BACKUP_DIR/backup-$DATE.tar.gz" -C "$BACKUP_DIR" . + +# Clean old backups (keep 30 days) +find "$BACKUP_DIR" -name "*.tar.gz" -mtime +30 -delete + +echo "Backup completed: $BACKUP_DIR/backup-$DATE.tar.gz" +``` + +#### Recovery Script + +Create recovery script: + +```bash +#!/bin/bash +# restore-wallet.sh + +BACKUP_FILE="$1" + +if [ -z "$BACKUP_FILE" ]; then + echo "Usage: $0 " + exit 1 +fi + +# Stop daemon +pkill xlite-daemon + +# Backup current data +mv ~/.config/CloudChains ~/.config/CloudChains.backup + +# Extract backup +tar -xzf "$BACKUP_FILE" -C ~/.config/ + +echo "Wallet restored from $BACKUP_FILE" +echo "Start the daemon to verify recovery" +``` + +This comprehensive user guide provides everything needed to install, configure, and use the XLite Daemon effectively. For additional support, refer to the troubleshooting section or consult the API reference for detailed technical information. \ No newline at end of file From 7701b2a95466d50758e0a4f35d8520ddb73feb90 Mon Sep 17 00:00:00 2001 From: tryiou Date: Thu, 2 Apr 2026 09:30:47 +0200 Subject: [PATCH 26/73] security: AES-CBC encryption with PBKDF2-SHA-256, char[] passphrases - KeyHandler: rewrite with WalletData inner class, 4-line wallet format, deriveKey/clearPassword - CoinInstance: toCharArray() passphrases with Arrays.fill in finally blocks - HTTPClient: remove SSL trust-all, use system socket factory - LoginUtils: replace e.printStackTrace with LOGGER.log - KeyHandlerTest: rewrite to char[] API, JUnit 5 @TempDir - CoinInstanceTest: update List signature --- .../io/cloudchains/app/crypto/KeyHandler.java | 882 +++++++++--------- .../io/cloudchains/app/crypto/LoginUtils.java | 3 +- .../io/cloudchains/app/net/CoinInstance.java | 151 +-- .../app/net/api/http/client/HTTPClient.java | 37 +- src/test/java/CoinInstanceTest.java | 8 +- src/test/java/KeyHandlerTest.java | 411 +++++--- 6 files changed, 818 insertions(+), 674 deletions(-) diff --git a/src/main/java/io/cloudchains/app/crypto/KeyHandler.java b/src/main/java/io/cloudchains/app/crypto/KeyHandler.java index 3921a8d..d58f689 100644 --- a/src/main/java/io/cloudchains/app/crypto/KeyHandler.java +++ b/src/main/java/io/cloudchains/app/crypto/KeyHandler.java @@ -8,595 +8,597 @@ import org.bitcoinj.crypto.MnemonicException; import org.bitcoinj.wallet.DeterministicSeed; +import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; import java.io.*; +import java.nio.charset.StandardCharsets; +import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; -import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.Arrays; -import java.util.Date; import java.util.List; import java.util.Objects; import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; - +/** + * Handles BIP39 mnemonic seed storage, AES-CBC encryption/decryption, + * and automatic migration from the legacy AES-ECB (SHA-1) format. + * + *

Passphrase handling: all public methods accept {@code char[]} so callers + * can zero the array immediately after use. Passing a {@code String} literal is + * intentionally unsupported — {@code String} is immutable and cannot be wiped. + */ public class KeyHandler { + private final static LogManager LOGMANAGER = LogManager.getLogManager(); private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); - - // Version constants for wallet migration - private static final int VERSION_1_SHA1 = 1; // Legacy SHA-1 format - private static final int VERSION_2_SHA256 = 2; // Current SHA-256 format + // ------------------------------------------------------------------------- + // Version constants + // ------------------------------------------------------------------------- + private static final int VERSION_1_SHA1 = 1; // Legacy: AES-ECB, PBKDF2-SHA-1, 16k iters + private static final int VERSION_2_SHA256 = 2; // Current: AES-CBC, PBKDF2-SHA-256, 100k iters private static final int CURRENT_VERSION = VERSION_2_SHA256; private static final String VERSION_HEADER = "VERSION:"; - // Legacy format marker for backward compatibility - private static final String LEGACY_SALT_MARKER = "legacySalt"; + // ------------------------------------------------------------------------- + // Crypto parameters + // ------------------------------------------------------------------------- + private static final int PBKDF2_ITERATIONS_SHA256 = 100_000; + private static final int PBKDF2_ITERATIONS_SHA1 = 16_384; // legacy only + private static final int KEY_LENGTH = 256; // AES key bits + private static final int SALT_LENGTH = 20; // bytes + private static final int IV_LENGTH = 16; // bytes, AES block size + + private static final String PBKDF2_SHA256 = "PBKDF2WithHmacSHA256"; + private static final String PBKDF2_SHA1 = "PBKDF2WithHmacSHA1"; // legacy only + private static final String AES = "AES"; + private static final String AES_CBC = "AES/CBC/PKCS5Padding"; + private static final String AES_ECB = "AES/ECB/PKCS5Padding"; // legacy migration only + + // DateTimeFormatter is immutable and thread-safe — no SimpleDateFormat. + private static final DateTimeFormatter TIMESTAMP_FORMAT = + DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); + + // Cache MnemonicCode: its constructor reads the BIP39 word list from disk. + private static final MnemonicCode MNEMONIC_CODE; + + static { + try { + MNEMONIC_CODE = new MnemonicCode(); + } catch (IOException e) { + throw new ExceptionInInitializerError( + "Failed to load BIP39 word list: " + e.getMessage()); + } + } + + // Characters accepted as "special" by calculatePasswordStrength. + private static final String SPECIAL_CHARS = "~!@#$%^&*()_-+=[]{|};:',.<>?/\\"; - // PBKDF2 parameters for secure key derivation - private static final int PBKDF2_ITERATIONS_SHA256 = 100000; // Current secure iteration count - private static final int PBKDF2_ITERATIONS_SHA1 = 16384; // Legacy iteration count - private static final int KEY_LENGTH = 256; // AES key length in bits - private static final int SALT_LENGTH = 20; // Salt length in bytes - - // PBKDF2 algorithms - private static final String PBKDF2_ALGORITHM_SHA256 = "PBKDF2WithHmacSHA256"; - private static final String PBKDF2_ALGORITHM_SHA1 = "PBKDF2WithHmacSHA1"; - private static final String CIPHER_ALGORITHM = "AES"; + // ------------------------------------------------------------------------- + // Instance + // ------------------------------------------------------------------------- - private ECKey ecKey; + private final ECKey ecKey; /** - * Create a KeyHandler with the specified ECKey. + * Wrap an existing {@link ECKey}. * - * @param key the ECKey to handle + * @param key the key to handle */ public KeyHandler(ECKey key) { this.ecKey = key; } - /** - * Get the base ECKey. - * - * @return the base ECKey - */ + /** Return the underlying {@link ECKey}. */ public ECKey getBaseECKey() { - return this.ecKey; + return ecKey; } - /** - * Get the public key derived from the base ECKey. - * - * @return the public key - */ + /** Return a public-only view of the underlying key. */ public ECKey getPublicKey() { - return ECKey.fromPublicOnly(this.ecKey.getPubKey()); + return ECKey.fromPublicOnly(ecKey.getPubKey()); } + // ========================================================================= + // Public API + // ========================================================================= /** - * Check if a wallet file exists locally. - * - * @return true if a wallet file exists, false otherwise + * Return {@code true} if a wallet file already exists on disk. */ public static boolean existsBaseECKeyFromLocal() { - String keyPath = ConfigHelper.getLocalDataDirectory() + "key.dat"; - File keyFile = new File(keyPath); - return keyFile.exists(); + return keyFile().exists(); } /** - * Encrypt base seed using specified PBKDF2 parameters. + * Decrypt and return the mnemonic seed phrase. + * + *

If no wallet file exists, a new one is generated and persisted. + * Legacy wallets (V1 / AES-ECB) are migrated to V2 (AES-CBC) transparently. * - * @param passphrase the user's passphrase - * @param seedBytes the seed data to encrypt - * @param salt the salt for PBKDF2 - * @param algorithm the PBKDF2 algorithm (SHA-1 or SHA-256) - * @param iterations the number of PBKDF2 iterations - * @return base64-encoded encrypted seed + * @param passphrase caller-owned char array; must be zeroed by the + * caller immediately after this method returns + * @return the mnemonic word list, or {@code null} if decryption fails */ - private static String encryptBaseSeed(String passphrase, byte[] seedBytes, byte[] salt, - String algorithm, int iterations) { + public static List getBaseSeed(char[] passphrase) { + File file = keyFile(); + if (!file.exists()) { + return generateAndPersistNewSeed(passphrase, file); + } try { - SecretKeyFactory skf = SecretKeyFactory.getInstance(algorithm); - PBEKeySpec spec = new PBEKeySpec(passphrase.toCharArray(), salt, iterations, KEY_LENGTH); - SecretKey tmp = skf.generateSecret(spec); - SecretKey key = new SecretKeySpec(tmp.getEncoded(), CIPHER_ALGORITHM); - - Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); - cipher.init(Cipher.ENCRYPT_MODE, key); - byte[] encrypted = cipher.doFinal(seedBytes); - return new String(Base64.encode(encrypted)); + WalletData data = readWalletFile(file); + if (data.version == VERSION_1_SHA1) { + LOGGER.log(Level.INFO, + "[security] Legacy V1 wallet detected — migrating to V2 (SHA-256/CBC)"); + String seed = decryptSeedEcb(passphrase, data.encrypted, data.salt); + migrateToNewFormat(passphrase, seed, file); + return Arrays.asList(seed.split("\\s+")); + } + String seed = decryptSeedCbc(passphrase, data.encrypted, data.salt, data.iv); + return Arrays.asList(seed.split("\\s+")); + } catch (BadPaddingException e) { + // Wrong password — expected failure, low log level. + LOGGER.log(Level.FINER, + "[security] Decryption failed — wrong passphrase or corrupted wallet"); + return null; + } catch (IOException e) { + LOGGER.log(Level.WARNING, "[security] Cannot read wallet file", e); + return null; } catch (Exception e) { - throw new RuntimeException("Failed to encrypt seed", e); + LOGGER.log(Level.WARNING, "[security] Unexpected error reading wallet", e); + return null; } } /** - * Encrypt base seed using current secure parameters (SHA-256, 100k iterations). + * Import a wallet from a BIP39 mnemonic word list. * - * @param passphrase the user's passphrase - * @param seedBytes the seed data to encrypt - * @param salt the salt for PBKDF2 - * @return base64-encoded encrypted seed + * @param mnemonicList the BIP39 word list (12 / 15 / 18 / 21 / 24 words) + * @param passphrase caller-owned char array; must be zeroed by the + * caller immediately after this method returns + * @return {@code true} on success + * @throws IllegalArgumentException if {@code mnemonicList} is null or empty */ - private static String encryptBaseSeed(String passphrase, byte[] seedBytes, byte[] salt) { - return encryptBaseSeed(passphrase, seedBytes, salt, - PBKDF2_ALGORITHM_SHA256, PBKDF2_ITERATIONS_SHA256); + public static boolean importFromMnemonic(List mnemonicList, char[] passphrase) { + if (mnemonicList == null || mnemonicList.isEmpty()) { + throw new IllegalArgumentException("Mnemonic list cannot be null or empty"); + } + byte[] entropy = null; + try { + entropy = MNEMONIC_CODE.toEntropy(mnemonicList); + DeterministicSeed seed = new DeterministicSeed( + entropy, "", System.currentTimeMillis() / 1000); + List derived = Objects.requireNonNull(seed.getMnemonicCode()); + if (!derived.equals(mnemonicList)) { + return false; + } + String mnemonic = Joiner.on(" ").join(derived); + return writeInitialData(keyFile(), mnemonic, passphrase); + } catch (MnemonicException e) { + LOGGER.log(Level.WARNING, "Failed to convert mnemonic to entropy", e); + return false; + } finally { + if (entropy != null) Arrays.fill(entropy, (byte) 0); + } } /** - * Encrypt base seed using legacy parameters (SHA-1, 16k iterations) for migration. + * Convert a BIP39 mnemonic word list to its raw entropy bytes. * - * @param passphrase the user's passphrase - * @param seedBytes the seed data to encrypt - * @param salt the salt for PBKDF2 - * @return base64-encoded encrypted seed + * @param mnemonicList the mnemonic word list + * @return entropy bytes, or {@code null} on failure */ - private static String encryptBaseSeedLegacy(String passphrase, byte[] seedBytes, byte[] salt) { - return encryptBaseSeed(passphrase, seedBytes, salt, - PBKDF2_ALGORITHM_SHA1, PBKDF2_ITERATIONS_SHA1); + public static byte[] mnemonicToEntropy(List mnemonicList) { + try { + return MNEMONIC_CODE.toEntropy(mnemonicList); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Failed to convert mnemonic to entropy", e); + return null; + } } /** - * Get the base seed from the wallet, decrypting if necessary. - * Handles legacy wallet migration automatically. + * Parse a space-separated mnemonic string into a word list. + * Tolerates leading/trailing whitespace and runs of multiple spaces. * - * @param passphrase the user's passphrase - * @return the seed as a list of words, or null if decryption fails + * @param mnemonic the raw mnemonic string + * @return list of mnemonic words */ - public static List getBaseSeed(String passphrase) { - File keyFile = new File(ConfigHelper.getLocalDataDirectory() + "key.dat"); - BufferedReader bufferedReader; - - if (existsBaseECKeyFromLocal()) { - byte[] salt = null; - byte[] seedEncrypted = null; - try { - bufferedReader = new BufferedReader(new FileReader(keyFile)); - String firstLine = bufferedReader.readLine(); - String saltB64; - String seedEncryptedB64; - - // Check if file has version header - if (firstLine != null && firstLine.startsWith(VERSION_HEADER)) { - // New format: VERSION, salt, encrypted seed - saltB64 = bufferedReader.readLine(); - seedEncryptedB64 = bufferedReader.readLine(); - } else { - // Legacy format: salt, encrypted seed (no version header) - saltB64 = firstLine; - seedEncryptedB64 = bufferedReader.readLine(); - } - bufferedReader.close(); - - salt = Base64.decode(saltB64); - seedEncrypted = Base64.decode(seedEncryptedB64); - - // Detect wallet version and use appropriate decryption - int walletVersion = detectWalletVersion(firstLine); - - String seed; - if (walletVersion == VERSION_1_SHA1) { - // Legacy wallet - decrypt with SHA-1 - seed = decryptSeedLegacy(passphrase, seedEncrypted, salt); - LOGGER.log(Level.INFO, "[security] Detected legacy wallet format, migrating to SHA-256..."); - - // Migrate to new format - migrateToNewFormat(passphrase, seed, keyFile); - } else { - // Current wallet - decrypt with SHA-256 - seed = decryptSeed(passphrase, seedEncrypted, salt); - } - - return Arrays.asList(seed.split(" ")); - } catch (Exception e) { - LOGGER.log(Level.FINER, "Error while obtaining base seed: " + e); - LOGGER.log(Level.FINER, "Bad password."); - return null; - } finally { - // Clear sensitive data from memory - if (salt != null) Arrays.fill(salt, (byte) 0); - if (seedEncrypted != null) Arrays.fill(seedEncrypted, (byte) 0); - } - } else { - DeterministicSeed seed = null; - seed = new DeterministicSeed(new SecureRandom(), 128, "", System.currentTimeMillis() / 1000); - - String mnemonic = Joiner.on(" ").join(Objects.requireNonNull(seed.getMnemonicCode())); + public static List getMnemonicFromString(String mnemonic) { + return Arrays.asList(mnemonic.trim().split("\\s+")); + } - if (writeInitialData(keyFile, mnemonic, passphrase)) { - return seed.getMnemonicCode(); - } else { - return null; + /** + * Score password strength from 0 (too short) to 10 (all criteria met). + * + * + * + * + * + * + * + * + * + *
CriterionPoints
8–9 characters+1
10+ characters+2
Contains digit+2
Contains lowercase+2
Contains uppercase+2
Contains special char+2
+ * + * @param password the password to evaluate + * @return score in [0, 10] + */ + public static int calculatePasswordStrength(String password) { + if (password.length() < 8) return 0; + + int score = password.length() >= 10 ? 2 : 1; + boolean hasDigit = false, hasLower = false, hasUpper = false, hasSpecial = false; + + for (char c : password.toCharArray()) { + if (Character.isDigit(c)) { + hasDigit = true; + } else if (Character.isLowerCase(c)) { + hasLower = true; + } else if (Character.isUpperCase(c)) { + hasUpper = true; + } else if (SPECIAL_CHARS.indexOf(c) >= 0) { + hasSpecial = true; } } + + if (hasDigit) score += 2; + if (hasLower) score += 2; + if (hasUpper) score += 2; + if (hasSpecial) score += 2; + return score; } + // ========================================================================= + // Private — key derivation and cipher + // ========================================================================= + /** - * Detect wallet version from salt header + * Derive a 256-bit AES key from a passphrase using PBKDF2. + * The intermediate raw key bytes are zeroed before returning. */ - private static int detectWalletVersion(String saltB64) { - // Legacy wallets don't have version header - if (saltB64.startsWith(VERSION_HEADER)) { + private static SecretKey deriveKey(char[] passphrase, byte[] salt, + String algorithm, int iterations) { + PBEKeySpec spec = new PBEKeySpec(passphrase, salt, iterations, KEY_LENGTH); + try { + byte[] raw = SecretKeyFactory.getInstance(algorithm) + .generateSecret(spec) + .getEncoded(); try { - return Integer.parseInt(saltB64.substring(VERSION_HEADER.length())); - } catch (NumberFormatException e) { - LOGGER.log(Level.WARNING, "[security] Invalid wallet version, assuming legacy format"); - return VERSION_1_SHA1; + return new SecretKeySpec(raw, AES); + } finally { + Arrays.fill(raw, (byte) 0); } + } catch (Exception e) { + throw new RuntimeException("Failed to derive encryption key", e); + } finally { + spec.clearPassword(); } - return VERSION_1_SHA1; // Default to legacy for backward compatibility } /** - * Decrypt seed using specified PBKDF2 parameters. + * Encrypt {@code seedBytes} with AES-CBC and a freshly generated random IV. * - * @param passphrase the user's passphrase - * @param seedEncrypted the encrypted seed data - * @param salt the salt for PBKDF2 - * @param algorithm the PBKDF2 algorithm (SHA-1 or SHA-256) - * @param iterations the number of PBKDF2 iterations - * @return decrypted seed as string - * @throws Exception if decryption fails + * @return {@code String[2]} — {@code [0]} = Base64 IV, {@code [1]} = Base64 ciphertext */ - private static String decryptSeed(String passphrase, byte[] seedEncrypted, byte[] salt, - String algorithm, int iterations) throws Exception { - SecretKeyFactory skf = SecretKeyFactory.getInstance(algorithm); - PBEKeySpec spec = new PBEKeySpec(passphrase.toCharArray(), salt, iterations, KEY_LENGTH); - SecretKey tmp = skf.generateSecret(spec); - SecretKey key = new SecretKeySpec(tmp.getEncoded(), CIPHER_ALGORITHM); - - Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); - cipher.init(Cipher.DECRYPT_MODE, key); - return new String(cipher.doFinal(seedEncrypted)); + private static String[] encryptBaseSeedWithIv(char[] passphrase, + byte[] seedBytes, byte[] salt) { + SecretKey key = deriveKey(passphrase, salt, PBKDF2_SHA256, PBKDF2_ITERATIONS_SHA256); + try { + byte[] iv = new byte[IV_LENGTH]; + SecureRandom.getInstanceStrong().nextBytes(iv); + Cipher cipher = Cipher.getInstance(AES_CBC); + cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv)); + byte[] encrypted = cipher.doFinal(seedBytes); + return new String[]{ + new String(Base64.encode(iv), StandardCharsets.UTF_8), + new String(Base64.encode(encrypted), StandardCharsets.UTF_8) + }; + } catch (Exception e) { + throw new RuntimeException("Failed to encrypt seed", e); + } } /** - * Decrypt seed using current secure parameters (SHA-256, 100k iterations). - * - * @param passphrase the user's passphrase - * @param seedEncrypted the encrypted seed data - * @param salt the salt for PBKDF2 - * @return decrypted seed as string - * @throws Exception if decryption fails + * Decrypt with the current V2 scheme: AES-CBC, PBKDF2-SHA-256 @ 100 000 iterations. */ - private static String decryptSeed(String passphrase, byte[] seedEncrypted, byte[] salt) throws Exception { - return decryptSeed(passphrase, seedEncrypted, salt, - PBKDF2_ALGORITHM_SHA256, PBKDF2_ITERATIONS_SHA256); + private static String decryptSeedCbc(char[] passphrase, byte[] encrypted, + byte[] salt, byte[] iv) throws Exception { + if (iv == null) throw new IllegalArgumentException("IV must not be null for CBC decryption"); + SecretKey key = deriveKey(passphrase, salt, PBKDF2_SHA256, PBKDF2_ITERATIONS_SHA256); + Cipher cipher = Cipher.getInstance(AES_CBC); + cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv)); + return new String(cipher.doFinal(encrypted), StandardCharsets.UTF_8); } /** - * Decrypt seed using legacy parameters (SHA-1, 16k iterations) for migration. - * - * @param passphrase the user's passphrase - * @param seedEncrypted the encrypted seed data - * @param salt the salt for PBKDF2 - * @return decrypted seed as string - * @throws Exception if decryption fails + * Decrypt with the legacy V1 scheme: AES-ECB, PBKDF2-SHA-1 @ 16 384 iterations. + * Do not use for anything other than migrating legacy wallets. */ - private static String decryptSeedLegacy(String passphrase, byte[] seedEncrypted, byte[] salt) throws Exception { - return decryptSeed(passphrase, seedEncrypted, salt, - PBKDF2_ALGORITHM_SHA1, PBKDF2_ITERATIONS_SHA1); + private static String decryptSeedEcb(char[] passphrase, byte[] encrypted, + byte[] salt) throws Exception { + SecretKey key = deriveKey(passphrase, salt, PBKDF2_SHA1, PBKDF2_ITERATIONS_SHA1); + Cipher cipher = Cipher.getInstance(AES_ECB); + cipher.init(Cipher.DECRYPT_MODE, key); + return new String(cipher.doFinal(encrypted), StandardCharsets.UTF_8); } - /** - * Validate that the environment is ready for migration. - * - * @param keyFile the wallet file to migrate - * @throws RuntimeException if environment is not ready for migration - */ - private static void validateMigrationReady(File keyFile) { - File parentDir = keyFile.getParentFile(); - if (!parentDir.canWrite()) { - throw new RuntimeException("No write permission - migration aborted"); - } + // ========================================================================= + // Private — file I/O + // ========================================================================= - // Check backup directory exists - File backupsDir = new File(ConfigHelper.getLocalDataDirectory() + "backups"); - if (!backupsDir.exists() && !backupsDir.mkdirs()) { - throw new RuntimeException("Cannot create backup directory"); - } + /** Canonical location of the wallet file. Single source of truth. */ + private static File keyFile() { + return new File(ConfigHelper.getLocalDataDirectory(), "key.dat"); } /** - * Migrate legacy wallet to new secure format with improved error handling and validation. + * Read and parse a wallet file. * - * @param passphrase the user's passphrase - * @param seed the decrypted seed from legacy wallet - * @param keyFile the wallet file to migrate - * @throws RuntimeException if migration fails and cannot be recovered + * @param file the wallet file to read + * @return parsed {@link WalletData} + * @throws IOException if the file is missing, incomplete, or corrupted */ - private static void migrateToNewFormat(String passphrase, String seed, File keyFile) { - File backupFile = null; - byte[] newSalt = null; - - try { - // Validate environment before migration - validateMigrationReady(keyFile); - - // Create backup of old wallet - backupFile = new File(ConfigHelper.getLocalDataDirectory() + "key-backup-legacy.dat"); - if (!keyFile.renameTo(backupFile)) { - throw new RuntimeException("Cannot create backup - migration aborted for safety"); - } - - // Generate new salt with cryptographically strong random number generator - SecureRandom secureRandom = SecureRandom.getInstanceStrong(); - newSalt = new byte[SALT_LENGTH]; - secureRandom.nextBytes(newSalt); - - // Encrypt with new secure parameters - String encryptedSeed = encryptBaseSeed(passphrase, seed.getBytes(), newSalt); - - // Write new format with version header - try (BufferedWriter writer = new BufferedWriter(new FileWriter(keyFile))) { - writer.write(VERSION_HEADER + CURRENT_VERSION); - writer.newLine(); - writer.write(new String(Base64.encode(newSalt))); - writer.newLine(); - writer.write(encryptedSeed); - writer.newLine(); + private static WalletData readWalletFile(File file) throws IOException { + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String firstLine = reader.readLine(); + int version = detectWalletVersion(firstLine); + + String saltB64, ivB64 = null, encB64; + if (version == VERSION_2_SHA256) { + saltB64 = reader.readLine(); + ivB64 = reader.readLine(); + encB64 = reader.readLine(); + } else { + // V1: first line IS the salt; no IV. + saltB64 = firstLine; + encB64 = reader.readLine(); } - // Validate the migration by attempting to decrypt the new format - if (!validateMigration(passphrase, keyFile)) { - throw new RuntimeException("Migration validation failed - new format is unreadable"); + if (saltB64 == null || encB64 == null) { + throw new IOException("Wallet file is incomplete or corrupted"); } - LOGGER.log(Level.INFO, "[security] Successfully migrated wallet to SHA-256 format"); - - } catch (Exception e) { - LOGGER.log(Level.SEVERE, "[security] Migration failed: " + e.getMessage()); - - // MUST restore backup or throw critical error - if (backupFile != null && backupFile.exists()) { - if (!backupFile.renameTo(keyFile)) { - LOGGER.log(Level.SEVERE, "[security] CRITICAL: Cannot restore backup!"); - throw new RuntimeException("Migration failed and backup restoration failed", e); - } - LOGGER.log(Level.INFO, "[security] Restored legacy wallet from backup"); - } else { - throw new RuntimeException("Migration failed with no backup available", e); - } - } finally { - // Clear sensitive data from memory - if (newSalt != null) { - Arrays.fill(newSalt, (byte) 0); - } + return new WalletData( + version, + Base64.decode(saltB64), + ivB64 != null ? Base64.decode(ivB64) : null, + Base64.decode(encB64) + ); } } /** - * Validate that the migrated wallet can be successfully decrypted. - * - * @param passphrase the user's passphrase - * @param keyFile the migrated wallet file - * @return true if validation succeeds, false otherwise + * Write the versioned 4-line wallet format to {@code file}. + * Creates parent directories if they do not exist. */ - private static boolean validateMigration(String passphrase, File keyFile) { - try (BufferedReader reader = new BufferedReader(new FileReader(keyFile))) { - String versionLine = reader.readLine(); - String saltB64 = reader.readLine(); - String encryptedSeedB64 = reader.readLine(); - - if (versionLine == null || saltB64 == null || encryptedSeedB64 == null) { - LOGGER.log(Level.WARNING, "[security] Migration validation failed: incomplete file format"); - return false; - } - - byte[] salt = Base64.decode(saltB64); - byte[] encryptedSeed = Base64.decode(encryptedSeedB64); - - // Attempt to decrypt with current parameters - String decryptedSeed = decryptSeed(passphrase, encryptedSeed, salt); - - // Basic validation that the seed is reasonable - String[] words = decryptedSeed.split(" "); - if (words.length < 12 || words.length > 24) { - LOGGER.log(Level.WARNING, "[security] Migration validation failed: invalid seed length"); - return false; - } - - return true; - - } catch (Exception e) { - LOGGER.log(Level.WARNING, "[security] Migration validation failed: " + e.getMessage()); - return false; + private static void writeWalletFile(File file, byte[] salt, + String ivB64, String encryptedSeed) throws IOException { + file.getParentFile().mkdirs(); + try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { + writer.write(VERSION_HEADER + CURRENT_VERSION); + writer.newLine(); + writer.write(new String(Base64.encode(salt), StandardCharsets.UTF_8)); + writer.newLine(); + writer.write(ivB64); + writer.newLine(); + writer.write(encryptedSeed); + writer.newLine(); } } /** - * Import a wallet from a mnemonic seed phrase. + * Move the current wallet file into the {@code backups/} subdirectory with + * a timestamp suffix. * - * @param mnemonicList the mnemonic seed phrase as a list of words - * @param passphrase the user's passphrase - * @return true if import successful, false otherwise + * @param keyFile the wallet file to back up + * @return the backup {@link File}, or {@code null} if there was nothing to back up */ - public static boolean importFromMnemonic(List mnemonicList, String passphrase) { - if (mnemonicList == null || mnemonicList.isEmpty()) { - throw new IllegalArgumentException("Mnemonic list cannot be null or empty"); - } - File keyFile = new File(ConfigHelper.getLocalDataDirectory() + "key.dat"); - byte[] entropy; + private static File backupKeyFile(File keyFile) { + if (!keyFile.exists()) return null; - try { - MnemonicCode mnemonicCode = new MnemonicCode(); - entropy = mnemonicCode.toEntropy(mnemonicList); - } catch (IOException | MnemonicException.MnemonicWordException | MnemonicException.MnemonicChecksumException | MnemonicException.MnemonicLengthException e) { - e.printStackTrace(); - return false; + File backupsDir = new File(ConfigHelper.getLocalDataDirectory(), "backups"); + if (!backupsDir.exists() && !backupsDir.mkdirs()) { + LOGGER.warning("[security] Failed to create backups directory: " + backupsDir.getPath()); } - DeterministicSeed seed = new DeterministicSeed(entropy, "", System.currentTimeMillis() / 1000); - - String mnemonic = Joiner.on(" ").join(Objects.requireNonNull(seed.getMnemonicCode())); - - if (!seed.getMnemonicCode().toString().equals(mnemonicList.toString())) - return false; - - return writeInitialData(keyFile, mnemonic, passphrase); + String timestamp = TIMESTAMP_FORMAT.format(LocalDateTime.now()); + File backup = new File(backupsDir, "key-" + timestamp + ".dat"); + if (!keyFile.renameTo(backup)) { + LOGGER.warning("[security] Failed to move wallet to backup location"); + return null; + } + LOGGER.info("[security] Wallet backup created: " + backup.getPath()); + return backup; } - /** - * Convert a mnemonic seed phrase to entropy bytes. - * - * @param mnemonicList the mnemonic seed phrase as a list of words - * @return the entropy bytes, or null if conversion fails - */ - public static byte[] mnemonicToEntropy(List mnemonicList) { - byte[] entropy = null; - - try { - MnemonicCode mnemonicCode = new MnemonicCode(); - entropy = mnemonicCode.toEntropy(mnemonicList); - } catch (Exception e) { - e.printStackTrace(); + /** Inspect the first line of a wallet file to determine its format version. */ + private static int detectWalletVersion(String firstLine) { + if (firstLine != null && firstLine.startsWith(VERSION_HEADER)) { + try { + return Integer.parseInt(firstLine.substring(VERSION_HEADER.length())); + } catch (NumberFormatException e) { + LOGGER.log(Level.WARNING, + "[security] Unrecognised version header — treating wallet as legacy V1"); + } } - - return entropy; + return VERSION_1_SHA1; } - private static File findRenameFile() { - for (int i = 0; i < 100; i++) { - File file = new File(ConfigHelper.getLocalDataDirectory() + "key-" + i + ".dat"); + // ========================================================================= + // Private — wallet lifecycle + // ========================================================================= - if (!file.exists()) { - return file; + /** Generate a fresh BIP39 seed, persist it, and return the mnemonic word list. */ + private static List generateAndPersistNewSeed(char[] passphrase, File file) { + try { + DeterministicSeed seed = new DeterministicSeed( + SecureRandom.getInstanceStrong(), 128, "", + System.currentTimeMillis() / 1000); + String mnemonic = Joiner.on(" ").join( + Objects.requireNonNull(seed.getMnemonicCode())); + if (writeInitialData(file, mnemonic, passphrase)) { + return seed.getMnemonicCode(); } + return null; + } catch (NoSuchAlgorithmException e) { + LOGGER.log(Level.SEVERE, "Failed to obtain strong SecureRandom", e); + return null; } - - return null; } /** - * Write initial wallet data with secure parameters and proper error handling. + * Back up any existing wallet, then encrypt and persist the mnemonic. * - * @param keyFile the wallet file to write - * @param mnemonic the mnemonic seed phrase - * @param passphrase the user's passphrase - * @return true if successful, false otherwise + * @param keyFile destination wallet file + * @param mnemonic space-separated mnemonic string + * @param passphrase caller-owned; never copied to a String + * @return {@code true} on success */ - private static boolean writeInitialData(File keyFile, String mnemonic, String passphrase) { - // Move current wallet file to backups - if (keyFile.exists()) { - // Backups dir - String backups = ConfigHelper.getLocalDataDirectory() + "backups" + File.separator; - File backupsDir = new File(backups); - if (!backupsDir.exists() && !backupsDir.mkdir()) { - LOGGER.warning("Failed to create backups dir " + backupsDir.getPath()); - } - - Date date = new Date(); - SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMddHHmmss"); - String newFileName = backups + "key-" + formatter.format(date) + ".dat"; + private static boolean writeInitialData(File keyFile, String mnemonic, char[] passphrase) { + backupKeyFile(keyFile); - File oldFile = new File(keyFile.getPath()); - if (!oldFile.renameTo(new File(newFileName))) { - LOGGER.info("Failed to rename old wallet file"); - } else { - LOGGER.info("Created wallet backup " + newFileName); - } - } - - // Generate cryptographically strong salt byte[] salt = null; + byte[] mnemonicBytes = null; try { - SecureRandom secureRandom = SecureRandom.getInstanceStrong(); salt = new byte[SALT_LENGTH]; - secureRandom.nextBytes(salt); + SecureRandom.getInstanceStrong().nextBytes(salt); - byte[] mnemonicBytes = mnemonic.getBytes(); - String encryptedSeed = encryptBaseSeed(passphrase, mnemonicBytes, salt); + mnemonicBytes = mnemonic.getBytes(StandardCharsets.UTF_8); + String[] parts = encryptBaseSeedWithIv(passphrase, mnemonicBytes, salt); + writeWalletFile(keyFile, salt, parts[0], parts[1]); - if (encryptedSeed == null) { - LOGGER.severe("[security] Failed to encrypt seed during wallet creation"); - return false; - } - - // Write wallet file with version header - try (BufferedWriter writer = new BufferedWriter(new FileWriter(keyFile))) { - writer.write(VERSION_HEADER + CURRENT_VERSION); - writer.newLine(); - writer.write(new String(Base64.encode(salt))); - writer.newLine(); - writer.write(encryptedSeed); - writer.newLine(); - } - - LOGGER.info("[security] Successfully created new wallet with SHA-256 encryption"); + LOGGER.info("[security] Wallet created with AES-256-CBC / PBKDF2-SHA-256"); return true; - } catch (Exception e) { LOGGER.log(Level.SEVERE, "[security] Failed to create wallet file", e); return false; } finally { - // Clear sensitive data from memory - if (salt != null) { - Arrays.fill(salt, (byte) 0); - } + if (salt != null) Arrays.fill(salt, (byte) 0); + if (mnemonicBytes != null) Arrays.fill(mnemonicBytes, (byte) 0); } } /** - * Parse a mnemonic seed phrase string into a list of words. + * Re-encrypt a legacy V1 wallet in the V2 format. * - * @param mnemonic the mnemonic seed phrase as a string - * @return the mnemonic as a list of words + *

Safety contract: + *

    + *
  1. The legacy file is renamed to a timestamped legacy-backup before any write. + *
  2. After writing, the new file is verified by attempting decryption. + *
  3. If verification fails, the new file is deleted and the legacy backup is + * restored. If restoration itself fails, a {@link RuntimeException} is thrown + * so the caller knows the wallet is in an inconsistent state. + *
*/ - public static List getMnemonicFromString(String mnemonic) { - return Arrays.asList(mnemonic.split(" ")); - } + private static void migrateToNewFormat(char[] passphrase, String seed, File keyFile) { + File legacyBackup = null; + byte[] newSalt = null; + byte[] seedBytes = null; - /** - * Calculate the strength score of a password. - * - * Scoring: - * - 8-9 characters: 1 point - * - 10+ characters: 2 points - * - Contains digit: +2 points - * - Contains lowercase letter: +2 points - * - Contains uppercase letter: +2 points - * - Contains special character: +2 points - * - * @param password the password to evaluate - * @return the strength score (0-10) - */ - public static int calculatePasswordStrength(String password) { - // Password must be greater than 8 characters, contain at least one digit, one lowercase letter, one uppercase letter and one special character. + try { + validateMigrationReady(keyFile); - int totalScore = 0; + // Create a distinctly named legacy backup (not in the backups/ subdir, so + // it is easy to find and recover manually). + String timestamp = TIMESTAMP_FORMAT.format(LocalDateTime.now()); + legacyBackup = new File(ConfigHelper.getLocalDataDirectory(), + "key-backup-legacy-" + timestamp + ".dat"); + if (!keyFile.renameTo(legacyBackup)) { + throw new RuntimeException("Cannot back up legacy wallet — migration aborted"); + } - if (password.length() < 8) { - return 0; - } else if (password.length() >= 10) { - totalScore += 2; - } else { - totalScore += 1; // 8-9 characters gets 1 point - } + newSalt = new byte[SALT_LENGTH]; + seedBytes = seed.getBytes(StandardCharsets.UTF_8); + SecureRandom.getInstanceStrong().nextBytes(newSalt); + + String[] parts = encryptBaseSeedWithIv(passphrase, seedBytes, newSalt); + writeWalletFile(keyFile, newSalt, parts[0], parts[1]); + + if (!validateMigration(passphrase, keyFile)) { + throw new RuntimeException("Post-migration validation failed — new file is unreadable"); + } + + LOGGER.log(Level.INFO, "[security] Wallet successfully migrated to V2 (SHA-256/CBC)"); - //if it contains one digit, add 2 to total score - if (password.matches("(?=.*[0-9]).*")) - totalScore += 2; + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "[security] Migration failed: " + e.getMessage()); - //if it contains one lower case letter, add 2 to total score - if (password.matches("(?=.*[a-z]).*")) - totalScore += 2; + // Rollback: remove the (potentially partial) new file, then restore the backup. + keyFile.delete(); + if (legacyBackup != null && legacyBackup.exists()) { + if (!legacyBackup.renameTo(keyFile)) { + throw new RuntimeException( + "[security] CRITICAL: migration failed AND backup restoration failed", e); + } + LOGGER.log(Level.INFO, "[security] Legacy wallet restored from backup"); + } else { + throw new RuntimeException("Migration failed with no backup available", e); + } + } finally { + if (newSalt != null) Arrays.fill(newSalt, (byte) 0); + if (seedBytes != null) Arrays.fill(seedBytes, (byte) 0); + } + } - //if it contains one upper case letter, add 2 to total score - if (password.matches("(?=.*[A-Z]).*")) - totalScore += 2; + /** Verify that the directory is writable before attempting migration. */ + private static void validateMigrationReady(File keyFile) { + if (!keyFile.getParentFile().canWrite()) { + throw new RuntimeException( + "No write permission on wallet directory — migration aborted"); + } + File backupsDir = new File(ConfigHelper.getLocalDataDirectory(), "backups"); + if (!backupsDir.exists() && !backupsDir.mkdirs()) { + throw new RuntimeException("Cannot create backup directory — migration aborted"); + } + } - //if it contains one special character, add 2 to total score - if (password.matches("(?=.*[~!@#$%^&*()_-]).*")) - totalScore += 2; + /** + * Verify that the migrated wallet can be decrypted and yields a valid BIP39 + * word count (12, 15, 18, 21, or 24). + */ + private static boolean validateMigration(char[] passphrase, File keyFile) { + try { + WalletData data = readWalletFile(keyFile); + String decrypted = decryptSeedCbc(passphrase, data.encrypted, data.salt, data.iv); + int wordCount = decrypted.trim().split("\\s+").length; + boolean valid = wordCount == 12 || wordCount == 15 || wordCount == 18 + || wordCount == 21 || wordCount == 24; + if (!valid) { + LOGGER.log(Level.WARNING, + "[security] Migration validation: unexpected word count " + wordCount); + } + return valid; + } catch (Exception e) { + LOGGER.log(Level.WARNING, "[security] Migration validation failed: " + e.getMessage()); + return false; + } + } - return totalScore; + // ========================================================================= + // WalletData — immutable value holder + // ========================================================================= + + private static final class WalletData { + final int version; + final byte[] salt; + final byte[] iv; // null for V1 (ECB) wallets + final byte[] encrypted; + + WalletData(int version, byte[] salt, byte[] iv, byte[] encrypted) { + this.version = version; + this.salt = salt; + this.iv = iv; + this.encrypted = encrypted; + } } -} +} \ No newline at end of file diff --git a/src/main/java/io/cloudchains/app/crypto/LoginUtils.java b/src/main/java/io/cloudchains/app/crypto/LoginUtils.java index 89ff3ee..ec5d024 100644 --- a/src/main/java/io/cloudchains/app/crypto/LoginUtils.java +++ b/src/main/java/io/cloudchains/app/crypto/LoginUtils.java @@ -23,8 +23,7 @@ private static String toSha256(String message) { } return hex.toString(); } catch (Exception e) { - LOGGER.log(Level.FINER, "Error while hashing message with SHA256!"); - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[security] Error hashing message with SHA-256", e); } return null; } diff --git a/src/main/java/io/cloudchains/app/net/CoinInstance.java b/src/main/java/io/cloudchains/app/net/CoinInstance.java index 058deea..8cb04f1 100644 --- a/src/main/java/io/cloudchains/app/net/CoinInstance.java +++ b/src/main/java/io/cloudchains/app/net/CoinInstance.java @@ -82,7 +82,7 @@ public String getMessage() { private static final int FORWARD_ADDRESS_COUNT = 0; - private static final List coinInstances = Collections.synchronizedList(new ArrayList<>()); + private static final List coinInstances = new CopyOnWriteArrayList<>(); private static CoinInstance activeCurrency; private static CoinTicker activeBlocknetNetwork = null; private static CopyOnWriteArrayList> activeCoinChangedListeners = new CopyOnWriteArrayList<>(); @@ -93,8 +93,8 @@ public String getMessage() { private WalletHelper walletHelper = null; private CoinTicker ticker; private ConcurrentHashMap transactionList = new ConcurrentHashMap<>(); - private ArrayList addressKeyPairs = new ArrayList<>(); - private ArrayList transactionObservableList = new ArrayList<>(); + private final CopyOnWriteArrayList addressKeyPairs = new CopyOnWriteArrayList<>(); + private final CopyOnWriteArrayList transactionObservableList = new CopyOnWriteArrayList<>(); private BlocknetPeerGroup blocknetPeerGroup; private BlocknetParameters blocknetNetworkParameters; private NetworkParameters networkParameters; @@ -133,11 +133,15 @@ public static String getMnemonicForPw(String pw) { if (!KeyHandler.existsBaseECKeyFromLocal()) return ""; - List seed = KeyHandler.getBaseSeed(pw); - if (seed == null) - return ""; - - return Joiner.on(" ").join(seed); + char[] passphrase = pw.toCharArray(); + try { + List seed = KeyHandler.getBaseSeed(passphrase); + if (seed == null) + return ""; + return Joiner.on(" ").join(seed); + } finally { + Arrays.fill(passphrase, '\0'); + } } public static int getBlockCountByTicker(CoinTicker ticker) { @@ -200,7 +204,7 @@ public AddressBalance generateAddress(boolean updateConfig) { Address address = addressKeyPair.getAddress(); DumpedPrivateKey privateKey = addressKeyPair.getPrivateKey(); addressKeyPairs.add(addressKeyPair); - LOGGER.log(Level.FINER, "[wallet] DEBUG: Generated new address, have " + addressKeyPairs.size() + ": " + address.toBase58() + ", private key: " + privateKey.toBase58() + " (hex: " + privateKey.getKey().getPrivateKeyAsHex() + ")"); + LOGGER.log(Level.FINER, "[wallet] Generated new address, have " + addressKeyPairs.size() + ": " + address.toBase58()); if (updateConfig) { configHelper.setAddressCount(configHelper.getAddressCount() + 1); @@ -246,25 +250,27 @@ public static CoinInstance getInstance(CoinTicker ticker) { } } - CoinInstance instance = getInstanceByTicker(ticker); + synchronized (CoinInstance.class) { + CoinInstance instance = getInstanceByTicker(ticker); - if (instance == null) { - instance = new CoinInstance(ticker); - if (ticker == CoinTicker.BLOCKNET) - coinInstances.add(0, instance); - else - coinInstances.add(instance); - } + if (instance == null) { + instance = new CoinInstance(ticker); + if (ticker == CoinTicker.BLOCKNET) + coinInstances.add(0, instance); + else + coinInstances.add(instance); + } - if (ticker == CoinTicker.BLOCKNET || ticker == CoinTicker.BLOCKNET_TESTNET5) { - activeBlocknetNetwork = ticker; - } + if (ticker == CoinTicker.BLOCKNET || ticker == CoinTicker.BLOCKNET_TESTNET5) { + activeBlocknetNetwork = ticker; + } - if (getActiveBlocknetNetwork() != null && (ticker == CoinTicker.BLOCKNET || ticker == CoinTicker.BLOCKNET_TESTNET5)) { - return getInstanceByTicker(activeBlocknetNetwork); - } + if (getActiveBlocknetNetwork() != null && (ticker == CoinTicker.BLOCKNET || ticker == CoinTicker.BLOCKNET_TESTNET5)) { + return getInstanceByTicker(activeBlocknetNetwork); + } - return instance; + return instance; + } } /** @@ -280,24 +286,31 @@ public static CoinError changePassword(String oldPassword, String newPassword) { CoinError.CoinErrorCode.CHANGEPASSWORDFAILED); } - List baseSeed = KeyHandler.getBaseSeed(oldPassword); - if (baseSeed == null) { - LOGGER.log(Level.FINER, "[wallet] Unable to change the password: Incorrect password"); - return new CoinError("Unable to change the password: Incorrect password", - CoinError.CoinErrorCode.CHANGEPASSWORDFAILED); - } + char[] oldPassphrase = oldPassword.toCharArray(); + char[] newPassphrase = newPassword.toCharArray(); + try { + List baseSeed = KeyHandler.getBaseSeed(oldPassphrase); + if (baseSeed == null) { + LOGGER.log(Level.FINER, "[wallet] Unable to change the password: Incorrect password"); + return new CoinError("Unable to change the password: Incorrect password", + CoinError.CoinErrorCode.CHANGEPASSWORDFAILED); + } - // Get current wallet seed - DeterministicSeed seed = new DeterministicSeed(baseSeed, null, "", System.currentTimeMillis() / 1000); - List mnemonic = seed.getMnemonicCode(); + // Get current wallet seed + DeterministicSeed seed = new DeterministicSeed(baseSeed, null, "", System.currentTimeMillis() / 1000); + List mnemonic = seed.getMnemonicCode(); - if (!KeyHandler.importFromMnemonic(mnemonic, newPassword)) { - LOGGER.log(Level.FINER, "[wallet] Unable to change the password: Failed to create new wallet file"); - return new CoinError("Unable to change the password: Failed to create new wallet file", - CoinError.CoinErrorCode.CHANGEPASSWORDFAILED); - } + if (!KeyHandler.importFromMnemonic(mnemonic, newPassphrase)) { + LOGGER.log(Level.FINER, "[wallet] Unable to change the password: Failed to create new wallet file"); + return new CoinError("Unable to change the password: Failed to create new wallet file", + CoinError.CoinErrorCode.CHANGEPASSWORDFAILED); + } - return null; + return null; + } finally { + Arrays.fill(oldPassphrase, '\0'); + Arrays.fill(newPassphrase, '\0'); + } } public NetworkParameters getNetworkParameters() { @@ -322,8 +335,7 @@ public void deinit() { coinRPCServer.deinit(); coinRPCServer.join(); } catch (Exception e) { - LOGGER.log(Level.FINER, "[coin] ERROR: Error while deinitializing coin RPC server!"); - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[coin] Error deinitializing RPC server for " + CoinTickerUtils.tickerToString(ticker), e); } } } @@ -460,15 +472,29 @@ public CoinError init(String pw, String userMnemonic, boolean isMnemonic, boolea if (isMnemonic) { baseSeed = Arrays.asList(pw.split(" ")); } else { - if (KeyHandler.existsBaseECKeyFromLocal()) - existsOnDisk = true;else if (userMnemonic != null) { - if (!KeyHandler.importFromMnemonic(Arrays.asList(new String(userMnemonic).split(" ")), pw)) { - LOGGER.log(Level.FINER, "[wallet] Unable to create wallet from mnemonic"); - return new CoinError("Unable to create wallet from mnemonic", CoinError.CoinErrorCode.BADMNEMONIC); + if (KeyHandler.existsBaseECKeyFromLocal()) { + existsOnDisk = true; + if (userMnemonic != null) { + LOGGER.log(Level.WARNING, "[wallet] Wallet already exists on disk, ignoring provided mnemonic"); + } + } else if (userMnemonic != null) { + char[] importPassphrase = pw.toCharArray(); + try { + if (!KeyHandler.importFromMnemonic(Arrays.asList(userMnemonic.split(" ")), importPassphrase)) { + LOGGER.log(Level.FINER, "[wallet] Unable to create wallet from mnemonic"); + return new CoinError("Unable to create wallet from mnemonic", CoinError.CoinErrorCode.BADMNEMONIC); + } + } finally { + Arrays.fill(importPassphrase, '\0'); } } - baseSeed = KeyHandler.getBaseSeed(pw); + char[] readPassphrase = pw.toCharArray(); + try { + baseSeed = KeyHandler.getBaseSeed(readPassphrase); + } finally { + Arrays.fill(readPassphrase, '\0'); + } } if (baseSeed == null) { @@ -591,8 +617,7 @@ private void connectToBlocknetNetwork() { try { blocknetPeerGroup.start(); } catch (Exception e) { - LOGGER.log(Level.FINER, "Error while initializing blocking client object!"); - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[coin] Error initializing blocking client for " + CoinTickerUtils.tickerToString(ticker), e); return; } @@ -914,16 +939,18 @@ public void addRelayFee(CoinTicker ticker, Double relayFee) { } public void addCloudTransaction(CloudTransaction cloudTransaction) { - if (transactionObservableList.isEmpty()) { - transactionObservableList.add(cloudTransaction); - return; - } + synchronized (transactionObservableList) { + if (transactionObservableList.isEmpty()) { + transactionObservableList.add(cloudTransaction); + return; + } - CloudTransaction tx = transactionObservableList.stream() - .filter(e -> e.getTxHash().equals(cloudTransaction.getTxHash())).findAny().orElse(null); + CloudTransaction tx = transactionObservableList.stream() + .filter(e -> e.getTxHash().equals(cloudTransaction.getTxHash())).findAny().orElse(null); - if (tx == null) { - transactionObservableList.add(cloudTransaction); + if (tx == null) { + transactionObservableList.add(cloudTransaction); + } } } @@ -995,12 +1022,12 @@ public BlocknetPeer getBestBlocknetPeer(String currency) { return blocknetPeerGroup.getBestBlocknetPeer(currency); } - public ArrayList getAddressKeyPairs() { - return addressKeyPairs; + public List getAddressKeyPairs() { + return Collections.unmodifiableList(addressKeyPairs); } - public ArrayList getTransactionList() { - return transactionObservableList; + public List getTransactionList() { + return Collections.unmodifiableList(transactionObservableList); } public static AtomicInteger getBlockCount(CoinTicker ticker) { @@ -1074,4 +1101,4 @@ public static void setAddressDiscoveryEnabled(boolean enabled) { public static boolean isAddressDiscoveryEnabled() { return addressDiscoveryEnabled; } -} +} \ No newline at end of file diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java b/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java index 98d6ba7..a4c97d2 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java +++ b/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java @@ -24,27 +24,21 @@ import org.apache.http.config.RegistryBuilder; import org.apache.http.conn.socket.ConnectionSocketFactory; import org.apache.http.conn.socket.PlainConnectionSocketFactory; -import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.message.BasicHeader; -import org.apache.http.ssl.SSLContextBuilder; import org.bitcoinj.core.Address; import org.bitcoinj.script.Script; import org.bitcoinj.script.ScriptBuilder; import org.json.JSONArray; import org.json.JSONObject; -import javax.net.ssl.SSLContext; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URI; -import java.security.KeyManagementException; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -188,17 +182,8 @@ private String executePostRequest(String endpoint, JsonObject params) { } public HTTPClient(int maximumSockets) { - SSLContext sslContext = null; lastFetchTimes = new ConcurrentHashMap<>(); - try { - sslContext = new SSLContextBuilder() - .loadTrustMaterial(null, (x509CertChain, authType) -> true) - .build(); - } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) { - e.printStackTrace(); - } - Header header = new BasicHeader(HttpHeaders.CONTENT_TYPE, "application/json"); List
headers = Lists.newArrayList(header); @@ -207,12 +192,10 @@ public HTTPClient(int maximumSockets) { requestBuilder.setConnectionRequestTimeout(HttpClientConfig.HTTP_TIMEOUT_MS); requestBuilder.setSocketTimeout(HttpClientConfig.HTTP_TIMEOUT_MS); - assert sslContext != null; PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager( RegistryBuilder.create() .register("http", PlainConnectionSocketFactory.INSTANCE) - .register("https", new SSLConnectionSocketFactory(sslContext, - NoopHostnameVerifier.INSTANCE)) + .register("https", SSLConnectionSocketFactory.getSystemSocketFactory()) .build() ); connectionManager.setDefaultMaxPerRoute(maximumSockets); @@ -220,8 +203,6 @@ public HTTPClient(int maximumSockets) { client = HttpClients.custom() .setDefaultHeaders(headers) - .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE) - .setSSLContext(sslContext) .setConnectionManager(connectionManager) .setDefaultRequestConfig(requestBuilder.build()) .build(); @@ -231,7 +212,7 @@ public void close() { try { client.close(); } catch (IOException e) { - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[httpclient] Failed to close HTTP client", e); } } @@ -330,7 +311,7 @@ private String executeEXRPost(String endpoint, JsonObject params) { // (server != null ? server.getEndpoint() : "null")); if (server == null) { - LOGGER.log(Level.SEVERE, "[httpclient] NO EXR SERVER SUPPORTS COIN: " + + LOGGER.log(Level.WARNING, "[httpclient] NO EXR SERVER SUPPORTS COIN: " + CoinTickerUtils.tickerToString(coin)); return null; // FAIL - NO FALLBACK TO BASE_URL } else { @@ -350,7 +331,7 @@ private String executeEXRPost(String endpoint, JsonObject params) { if (coin != null) { server = App.exrServerPool.selectServerForCoin(coin); if (server == null) { - LOGGER.log(Level.SEVERE, "[httpclient] NO EXR SERVER SUPPORTS COIN: " + + LOGGER.log(Level.WARNING, "[httpclient] NO EXR SERVER SUPPORTS COIN: " + CoinTickerUtils.tickerToString(coin)); return null; // FAIL - NO FALLBACK TO BASE_URL } @@ -445,7 +426,7 @@ public JsonArray getUtxosUncached(CoinTicker coinTicker, String[] addresses) { jsonObject = new JSONObject(res); utxoArr = jsonObject.getJSONArray("utxos"); } catch (Exception e) { - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[httpclient] getUtxosUncached " + coinInstance.getTicker() + " parse error - " + e.getMessage()); } if (jsonObject == null || utxoArr == null) { @@ -524,7 +505,7 @@ public JsonArray getUtxos(CoinTicker coinTicker, int expiry) { jsonObject = new JSONObject(res); utxoArr = jsonObject.getJSONArray("utxos"); } catch (Exception e) { - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[httpclient] getUtxos " + coinInstance.getTicker() + " parse error - " + e.getMessage()); } if (jsonObject == null || utxoArr == null) { @@ -783,7 +764,7 @@ public JsonArray getHistory(CoinTicker coinTicker, int startTime, int endTime, i try { json = new Gson().fromJson(res, JsonArray.class); } catch (Exception e) { - LOGGER.log(Level.SEVERE, "[httpclient] getHistory parsing error - Response: " + res, e); + LOGGER.log(Level.WARNING, "[httpclient] getHistory parsing error - Response: " + res + " - " + e.getMessage()); return null; } @@ -897,7 +878,7 @@ public JsonArray getTransactionHistory(CoinTicker coinTicker, int startTime, int } else ++fails; } catch (Exception e) { - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[httpclient] getRawTransaction failed - " + e.getMessage()); ++fails; } } @@ -927,7 +908,7 @@ public JsonArray getTransactionHistory(CoinTicker coinTicker, int startTime, int } else ++fails; } catch (Exception e) { - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[httpclient] getRawTransaction(vout) failed - " + e.getMessage()); ++fails; } } diff --git a/src/test/java/CoinInstanceTest.java b/src/test/java/CoinInstanceTest.java index 1fd4433..436606d 100644 --- a/src/test/java/CoinInstanceTest.java +++ b/src/test/java/CoinInstanceTest.java @@ -25,7 +25,7 @@ void deterministicAddresses_fromMnemonic() { coin.getConfigHelper().setAddressCount(getAddressCount()); assertNull(coin.init(LoginUtils.loginToEntropy(getPassword()), getMnemonic(), false)); - ArrayList addresses = coin.getAddressKeyPairs(); + List addresses = coin.getAddressKeyPairs(); ArrayList actual = new ArrayList<>(); for (AddressBalance address : addresses) { actual.add(address.getAddress().toBase58()); @@ -57,7 +57,7 @@ void deterministicAddresses_generateAddress() { coin.generateAddress(false); coin.generateAddress(true); // last one - ArrayList addresses = coin.getAddressKeyPairs(); + List addresses = coin.getAddressKeyPairs(); ArrayList actual = new ArrayList<>(); for (AddressBalance address : addresses) actual.add(address.getAddress().toBase58()); @@ -87,7 +87,7 @@ void deterministicAddresses_generateForwardAddresses() { coin.getConfigHelper().setAddressCount(getAddressCount()); assertNull(coin.init(LoginUtils.loginToEntropy(getPassword()), null, false)); - ArrayList addresses = coin.getAddressKeyPairs(); + List addresses = coin.getAddressKeyPairs(); ArrayList actual = new ArrayList<>(); for (AddressBalance address : addresses) actual.add(address.getAddress().toBase58()); @@ -117,7 +117,7 @@ void deterministicAddresses_generateForwardAddressesReloadConfig() { coin.getConfigHelper().setAddressCount(idx); coin.getConfigHelper().writeConfig(); coin.reloadConfig(); - ArrayList addresses = coin.getAddressKeyPairs(); + List addresses = coin.getAddressKeyPairs(); ArrayList actual = new ArrayList<>(); for (AddressBalance address : addresses) actual.add(address.getAddress().toBase58()); diff --git a/src/test/java/KeyHandlerTest.java b/src/test/java/KeyHandlerTest.java index c738f44..208023e 100644 --- a/src/test/java/KeyHandlerTest.java +++ b/src/test/java/KeyHandlerTest.java @@ -1,239 +1,374 @@ -import com.subgraph.orchid.encoders.Base64; import io.cloudchains.app.crypto.KeyHandler; import io.cloudchains.app.util.ConfigHelper; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.io.TempDir; import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.nio.file.Files; +import java.io.*; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.security.SecureRandom; -import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.List; import static org.junit.jupiter.api.Assertions.*; /** - * Comprehensive unit tests for KeyHandler security improvements + * Unit tests for KeyHandler. + * + *

Key design choices: + *

    + *
  • {@code @TempDir} is declared as a static field so the same directory is + * reused across all tests in this class and cleaned up automatically by JUnit 5. + *
  • Passphrases are {@code char[]} throughout, matching the production API. + * Each test allocates a fresh array and zero-fills it in a {@code finally} block. + *
  • The legacy wallet helper uses {@code java.util.Base64} (standard library) + * instead of a third-party encoder, removing an accidental cross-library coupling. + *
+ * + *

Thread safety: {@code ConfigHelper.CONFIG_DIR} is a mutable static field. + * Do not run these tests with JUnit 5 parallel execution enabled unless you isolate + * the static state per worker. */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class KeyHandlerTest { + // ------------------------------------------------------------------------- + // Test fixtures + // ------------------------------------------------------------------------- + private static final String TEST_PASSPHRASE = "testPassphrase123!"; - private static final String TEST_MNEMONIC = "one two three cake neutral benefit quick hip level mother fine burst"; - private static final List TEST_MNEMONIC_LIST = Arrays.asList(TEST_MNEMONIC.split(" ")); + private static final String TEST_MNEMONIC = + "one two three cake neutral benefit quick hip level mother fine burst"; + private static final List TEST_MNEMONIC_LIST = + Arrays.asList(TEST_MNEMONIC.split(" ")); + + // Expected scores for calculatePasswordStrength — named so assertion failures + // are self-documenting. + private static final int SCORE_TOO_SHORT = 0; // < 8 chars + private static final int SCORE_EIGHT_LOWERCASE_ONLY = 3; // 1 (len 8-9) + 2 (lower) + private static final int SCORE_TEN_LOWER_DIGIT = 6; // 2 (len 10+) + 2 (lower) + 2 (digit) + private static final int SCORE_ALL_CRITERIA = 10; // 2+2+2+2+2 + + /** Shared temp directory — JUnit 5 removes it and all contents after the test class. */ + @TempDir + static Path tempDir; private File testKeyFile; private File testBackupDir; @BeforeEach - public void setUp() throws IOException { - // Create temporary test directory - Path tempDir = Files.createTempDirectory("keyhandler-test-"); + void setUp() { + // Point ConfigHelper at the isolated temp directory for every test. + ConfigHelper.CONFIG_DIR = tempDir.toString(); testKeyFile = tempDir.resolve("CloudChains").resolve("key.dat").toFile(); testBackupDir = tempDir.resolve("CloudChains").resolve("backups").toFile(); - - // Mock ConfigHelper to use test directory - ConfigHelper.CONFIG_DIR = tempDir.toString(); } - @AfterEach - public void tearDown() { - // Clean up test files - if (testKeyFile.exists()) { - testKeyFile.delete(); - } - if (testBackupDir.exists()) { - testBackupDir.delete(); - } - } + // ========================================================================= + // calculatePasswordStrength + // ========================================================================= @Test - public void testPasswordStrengthValidation() { - // Test password strength calculation - assertEquals(0, KeyHandler.calculatePasswordStrength("short")); - assertEquals(3, KeyHandler.calculatePasswordStrength("eightchr")); // 1+2 = 3 (fixed bug) - assertEquals(6, KeyHandler.calculatePasswordStrength("tenchars12")); // 2+2+2 = 6 (10+ chars + digit + lowercase) + @Order(1) + void testPasswordStrengthTooShort() { + assertEquals(SCORE_TOO_SHORT, KeyHandler.calculatePasswordStrength("short")); + } - // Test with all character types - int strongPasswordScore = KeyHandler.calculatePasswordStrength("StrongPass123!"); - assertEquals(10, strongPasswordScore); // 2+2+2+2+2 = 10 + @Test + @Order(2) + void testPasswordStrengthEightCharLowercaseOnly() { + // 1 (8-9 chars) + 2 (lowercase) = 3 + assertEquals(SCORE_EIGHT_LOWERCASE_ONLY, + KeyHandler.calculatePasswordStrength("eightchr")); } @Test - public void testPasswordValidationBugFix() { - // Test the specific bug fix for 8-9 character passwords - int eightCharScore = KeyHandler.calculatePasswordStrength("eightchr"); - int nineCharScore = KeyHandler.calculatePasswordStrength("ninechar"); + @Order(3) + void testPasswordStrengthNineCharSameAsEight() { + // Both 8 and 9 characters should yield the same length bonus (+1). + assertEquals( + KeyHandler.calculatePasswordStrength("eightchr"), + KeyHandler.calculatePasswordStrength("ninechars"), + "8-char and 9-char passwords must receive the same length bonus" + ); + } - // Both should get 3 points (1 length + 2 digit = 3) - bug is fixed - assertEquals(3, eightCharScore); - assertEquals(3, nineCharScore); + @Test + @Order(4) + void testPasswordStrengthTenPlusWithDigitAndLower() { + // 2 (10+ chars) + 2 (digit) + 2 (lowercase) = 6 + assertEquals(SCORE_TEN_LOWER_DIGIT, + KeyHandler.calculatePasswordStrength("tenchars12")); } @Test - public void testMnemonicToEntropy() { - // Test entropy generation from mnemonic - byte[] entropy = KeyHandler.mnemonicToEntropy(TEST_MNEMONIC_LIST); - assertNotNull(entropy); - assertEquals(16, entropy.length); + @Order(5) + void testPasswordStrengthAllCriteria() { + assertEquals(SCORE_ALL_CRITERIA, + KeyHandler.calculatePasswordStrength("StrongPass123!")); } + // ========================================================================= + // getMnemonicFromString + // ========================================================================= + @Test - public void testMnemonicFromString() { - // Test mnemonic string parsing + @Order(6) + void testMnemonicFromStringNormal() { List result = KeyHandler.getMnemonicFromString(TEST_MNEMONIC); assertEquals(12, result.size()); assertEquals("one", result.get(0)); + assertEquals("burst", result.get(11)); } @Test - public void testImportFromMnemonic() { - // Test importing from mnemonic - boolean success = KeyHandler.importFromMnemonic(TEST_MNEMONIC_LIST, TEST_PASSPHRASE); - assertTrue(success); - assertTrue(testKeyFile.exists()); + @Order(7) + void testMnemonicFromStringExtraWhitespace() { + // Leading, trailing, and double spaces must all be collapsed. + List result = KeyHandler.getMnemonicFromString(" one two three "); + assertEquals(3, result.size()); + assertEquals("one", result.get(0)); } - @Test - public void testGetBaseSeed() { - // Test getting base seed from encrypted file - boolean importSuccess = KeyHandler.importFromMnemonic(TEST_MNEMONIC_LIST, TEST_PASSPHRASE); - assertTrue(importSuccess); - assertTrue(testKeyFile.exists()); + // ========================================================================= + // mnemonicToEntropy + // ========================================================================= - List seed = KeyHandler.getBaseSeed(TEST_PASSPHRASE); - assertNotNull(seed); - assertEquals(12, seed.size()); - assertEquals("one", seed.get(0)); + @Test + @Order(8) + void testMnemonicToEntropy() { + byte[] entropy = KeyHandler.mnemonicToEntropy(TEST_MNEMONIC_LIST); + assertNotNull(entropy, "Entropy must not be null for a valid mnemonic"); + assertEquals(16, entropy.length, + "A 12-word BIP39 mnemonic yields 16 bytes of entropy"); } - @Test - public void testGetBaseSeedWrongPassword() { - // Test that wrong password returns null - KeyHandler.importFromMnemonic(TEST_MNEMONIC_LIST, TEST_PASSPHRASE); + // ========================================================================= + // importFromMnemonic + key file structure + // ========================================================================= - List seed = KeyHandler.getBaseSeed("wrongPassword"); - assertNull(seed); + @Test + @Order(9) + void testImportFromMnemonicCreatesFile() { + char[] passphrase = TEST_PASSPHRASE.toCharArray(); + try { + boolean success = KeyHandler.importFromMnemonic(TEST_MNEMONIC_LIST, passphrase); + assertTrue(success, "Import should succeed for a valid mnemonic"); + assertTrue(testKeyFile.exists(), "Wallet file must be created on disk"); + } finally { + Arrays.fill(passphrase, '\0'); + } } @Test - public void testLegacyWalletMigration() throws IOException { - // Create a legacy wallet format (SHA-1, 16k iterations) - createLegacyWalletFile(); - - // Test that migration occurs - List seed = KeyHandler.getBaseSeed(TEST_PASSPHRASE); - assertNotNull(seed); + @Order(10) + void testImportedWalletFileHasCorrectStructure() throws IOException { + char[] passphrase = TEST_PASSPHRASE.toCharArray(); + try { + KeyHandler.importFromMnemonic(TEST_MNEMONIC_LIST, passphrase); + } finally { + Arrays.fill(passphrase, '\0'); + } - // Verify new format was created String[] lines = readKeyFileLines(); - assertEquals("VERSION:2", lines[0]); + assertEquals(4, lines.length, + "V2 wallet file must have exactly 4 lines: VERSION, salt, IV, ciphertext"); + assertEquals("VERSION:2", lines[0], + "First line must be the version header"); + assertFalse(lines[1].isEmpty(), "Salt line must not be empty"); + assertFalse(lines[2].isEmpty(), "IV line must not be empty"); + assertFalse(lines[3].isEmpty(), "Ciphertext line must not be empty"); } - @Test - public void testBackupCreation() { - // Test that backups are created when importing - KeyHandler.importFromMnemonic(TEST_MNEMONIC_LIST, TEST_PASSPHRASE); + // ========================================================================= + // getBaseSeed — round-trip and error paths + // ========================================================================= - // Create another import to trigger backup - KeyHandler.importFromMnemonic(TEST_MNEMONIC_LIST, TEST_PASSPHRASE); + @Test + @Order(11) + void testGetBaseSeedRoundTrip() { + char[] importPass = TEST_PASSPHRASE.toCharArray(); + char[] readPass = TEST_PASSPHRASE.toCharArray(); + try { + assertTrue(KeyHandler.importFromMnemonic(TEST_MNEMONIC_LIST, importPass)); + + List seed = KeyHandler.getBaseSeed(readPass); + assertNotNull(seed, "Seed must be recoverable with the correct passphrase"); + assertEquals(12, seed.size()); + assertEquals("one", seed.get(0)); + assertEquals("burst", seed.get(11)); + } finally { + Arrays.fill(importPass, '\0'); + Arrays.fill(readPass, '\0'); + } + } - // Check if backup directory exists - assertTrue(testBackupDir.exists()); + @Test + @Order(12) + void testGetBaseSeedWrongPassphraseReturnsNull() { + char[] importPass = TEST_PASSPHRASE.toCharArray(); + char[] wrongPass = "wrongPassword".toCharArray(); + try { + KeyHandler.importFromMnemonic(TEST_MNEMONIC_LIST, importPass); + assertTrue(testKeyFile.exists(), + "Wallet file must exist before testing wrong passphrase"); + + List seed = KeyHandler.getBaseSeed(wrongPass); + assertNull(seed, "Wrong passphrase must return null"); + + // Verify the wallet file was not damaged by the failed attempt. + assertTrue(testKeyFile.exists(), + "Wallet file must still exist after a failed decryption"); + assertEquals(4, readKeyFileLines().length, + "Wallet file must retain its valid 4-line structure"); + } catch (IOException e) { + fail("Unexpected IOException reading wallet file: " + e.getMessage()); + } finally { + Arrays.fill(importPass, '\0'); + Arrays.fill(wrongPass, '\0'); + } } + // ========================================================================= + // Backup on re-import + // ========================================================================= + @Test - public void testMigrationFailsWithoutBackup() throws IOException { - // Test that migration fails when backup creation fails - // This simulates the critical fix where migration cannot proceed without backup - KeyHandler.importFromMnemonic(TEST_MNEMONIC_LIST, TEST_PASSPHRASE); + @Order(13) + void testReimportCreatesBackup() { + char[] passphrase = TEST_PASSPHRASE.toCharArray(); + try { + // First import — no backup should exist yet. + KeyHandler.importFromMnemonic(TEST_MNEMONIC_LIST, passphrase); + passphrase = TEST_PASSPHRASE.toCharArray(); // re-allocate after first use + + // Second import — the previous wallet must be backed up. + KeyHandler.importFromMnemonic(TEST_MNEMONIC_LIST, passphrase); + + assertTrue(testBackupDir.exists(), "Backups directory must be created"); + File[] backups = testBackupDir.listFiles(); + assertNotNull(backups); + assertTrue(backups.length > 0, + "At least one backup file must exist after a second import"); + } finally { + Arrays.fill(passphrase, '\0'); + } + } + + // ========================================================================= + // Legacy wallet migration + // ========================================================================= - // Create a legacy wallet file that will trigger migration + @Test + @Order(14) + void testLegacyWalletIsReadable() throws IOException { createLegacyWalletFile(); - // Migration should succeed since backup can be created - List seed = KeyHandler.getBaseSeed(TEST_PASSPHRASE); - assertNotNull(seed); - assertEquals(12, seed.size()); + char[] passphrase = TEST_PASSPHRASE.toCharArray(); + try { + List seed = KeyHandler.getBaseSeed(passphrase); + assertNotNull(seed, "Legacy wallet must be decryptable with the correct passphrase"); + assertEquals(12, seed.size()); + } finally { + Arrays.fill(passphrase, '\0'); + } } @Test - public void testMigrationSuccessWithValidBackup() throws IOException { - // Create legacy wallet + @Order(15) + void testLegacyWalletIsMigratedToV2() throws IOException { createLegacyWalletFile(); - // Migration should succeed and create backup - List seed = KeyHandler.getBaseSeed(TEST_PASSPHRASE); - assertNotNull(seed); - assertEquals(12, seed.size()); - - // Verify backup was created - File backupFile = new File(ConfigHelper.getLocalDataDirectory() + "key-backup-legacy.dat"); - assertTrue(backupFile.exists()); + char[] passphrase = TEST_PASSPHRASE.toCharArray(); + try { + KeyHandler.getBaseSeed(passphrase); + } finally { + Arrays.fill(passphrase, '\0'); + } - // Verify new format was created + // After migration the key file must be V2 format. String[] lines = readKeyFileLines(); - assertEquals("VERSION:2", lines[0]); + assertEquals("VERSION:2", lines[0], + "Migrated wallet must carry the V2 version header"); + assertEquals(4, lines.length, + "Migrated wallet must have the 4-line V2 structure"); } - // Helper methods - - private void createLegacyWalletFile() throws IOException { - // Create a proper legacy wallet format that can be decrypted + @Test + @Order(16) + void testLegacyMigrationCreatesLegacyBackup() throws IOException { + createLegacyWalletFile(); + + char[] passphrase = TEST_PASSPHRASE.toCharArray(); try { - // Ensure the CloudChains directory exists - testKeyFile.getParentFile().mkdirs(); + List seed = KeyHandler.getBaseSeed(passphrase); + assertNotNull(seed); + assertEquals(12, seed.size()); + } finally { + Arrays.fill(passphrase, '\0'); + } + + // migrateToNewFormat places the legacy backup directly in getLocalDataDirectory(), + // not in the backups/ subdirectory, so it is easy to locate manually. + File dataDir = new File(ConfigHelper.getLocalDataDirectory()); + File[] legacyBackups = dataDir.listFiles( + (dir, name) -> name.startsWith("key-backup-legacy-")); + assertNotNull(legacyBackups); + assertTrue(legacyBackups.length > 0, + "A timestamped legacy backup must be created during migration"); + } + + // ========================================================================= + // Helpers + // ========================================================================= - // Generate a real salt and encrypt the test mnemonic with legacy parameters - SecureRandom r = new SecureRandom(); + /** + * Write a valid V1 (AES-ECB / PBKDF2-SHA-1 / 16 384 iterations) wallet file + * to the path that KeyHandler will read. + * + *

Uses only {@link java.util.Base64} (standard library) — no third-party encoder. + */ + private void createLegacyWalletFile() throws IOException { + testKeyFile.getParentFile().mkdirs(); + try { + SecureRandom rng = new SecureRandom(); byte[] salt = new byte[20]; - r.nextBytes(salt); + rng.nextBytes(salt); - // Encrypt using legacy SHA-1, 16k iterations SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); - PBEKeySpec spec = new PBEKeySpec(TEST_PASSPHRASE.toCharArray(), salt, 16384, 256); + PBEKeySpec spec = new PBEKeySpec(TEST_PASSPHRASE.toCharArray(), salt, 16_384, 256); SecretKey tmp = skf.generateSecret(spec); SecretKey key = new SecretKeySpec(tmp.getEncoded(), "AES"); + spec.clearPassword(); - Cipher cipher = Cipher.getInstance("AES"); + Cipher cipher = Cipher.getInstance("AES"); // defaults to ECB — intentional for V1 cipher.init(Cipher.ENCRYPT_MODE, key); - byte[] encrypted = cipher.doFinal(TEST_MNEMONIC.getBytes()); + byte[] encrypted = cipher.doFinal(TEST_MNEMONIC.getBytes(StandardCharsets.UTF_8)); + // V1 format: two lines — Base64(salt) then Base64(ciphertext). + Base64.Encoder encoder = Base64.getEncoder(); try (BufferedWriter writer = new BufferedWriter(new FileWriter(testKeyFile))) { - writer.write(new String(Base64.encode(salt))); + writer.write(encoder.encodeToString(salt)); writer.newLine(); - writer.write(new String(Base64.encode(encrypted))); + writer.write(encoder.encodeToString(encrypted)); writer.newLine(); } } catch (Exception e) { - throw new IOException("Failed to create legacy wallet file", e); + throw new IOException("Failed to create legacy wallet file for test", e); } } + /** Read the wallet file and return its lines, or an empty array if absent. */ private String[] readKeyFileLines() throws IOException { - if (!testKeyFile.exists()) { - return new String[0]; - } - + if (!testKeyFile.exists()) return new String[0]; try (BufferedReader reader = new BufferedReader(new FileReader(testKeyFile))) { - List lines = new ArrayList<>(); - String line; - while ((line = reader.readLine()) != null) { - lines.add(line); - } - return lines.toArray(new String[0]); + return reader.lines().toArray(String[]::new); } } } \ No newline at end of file From 2407cb49fcc9e4739a525381de4d58595ba20328 Mon Sep 17 00:00:00 2001 From: tryiou Date: Thu, 2 Apr 2026 09:49:09 +0200 Subject: [PATCH 27/73] feat(app): add CLI flags and environment support Lazy load dotenv configuration on first access. Add --version and --help flags to main entry. Support environment variables for passwords. Use secure console input for password reading. Improve shutdown sequence with null safety. Validate endpoint arguments to prevent errors. --- src/main/java/io/cloudchains/app/App.java | 39 ++- .../cloudchains/app/console/ConsoleMenu.java | 238 +++++++++++------- 2 files changed, 173 insertions(+), 104 deletions(-) diff --git a/src/main/java/io/cloudchains/app/App.java b/src/main/java/io/cloudchains/app/App.java index f67b294..5f04c6e 100644 --- a/src/main/java/io/cloudchains/app/App.java +++ b/src/main/java/io/cloudchains/app/App.java @@ -36,6 +36,12 @@ public class App { public static Dotenv dotenv = null; public static String getEnv(String key) { + if (dotenv == null) { + try { + dotenv = Dotenv.configure().ignoreIfMissing().load(); + } catch (Exception ignored) { + } + } if (dotenv != null) { String value = dotenv.get(key); if (value != null) return value; @@ -43,14 +49,8 @@ public static String getEnv(String key) { return System.getenv(key); } - static { - // Load .env file if present - try { - dotenv = Dotenv.configure().ignoreIfMissing().load(); - } catch (Exception ignored) { - } - - // Check for EXR_ENDPOINT environment variable + public static void initExrEndpoint() { + if (EXR_ENDPOINT != null) return; String exrEndpoint = getEnv("EXR_ENDPOINT"); if (exrEndpoint != null && !exrEndpoint.isEmpty()) { EXR_ENDPOINT = exrEndpoint; @@ -60,6 +60,19 @@ public static String getEnv(String key) { } public static void main(String[] args) { + for (String arg : args) { + if (arg.equals("--version")) { + System.out.println(Version.CLIENT_VERSION); + System.exit(0); + } + if (arg.equals("--help")) { + System.out.println(ConsoleMenu.getHelpText()); + System.exit(0); + } + } + + initExrEndpoint(); + CCLogger.setLogging(isLoggingEnabled); LOGGER.setLevel(Level.INFO); LOGGER.setUseParentHandlers(false); @@ -120,10 +133,14 @@ protected synchronized void setOutputStream(OutputStream out) throws SecurityExc } public static void shutdown() { - if (masterRPC.isAlive()) { + if (masterRPC != null && masterRPC.isAlive()) { System.out.println("Shutting down..."); } + if (console != null) { + console.deinit(); + } + if (feeUpdateHttpClient != null) { feeUpdateHttpClient.close(); } @@ -140,10 +157,6 @@ public static void shutdown() { masterRPC.deinit(); } - if (console != null) { - console.deinit(); - } - for (Handler handler : LOGGER.getHandlers()) { LOGGER.removeHandler(handler); handler.close(); diff --git a/src/main/java/io/cloudchains/app/console/ConsoleMenu.java b/src/main/java/io/cloudchains/app/console/ConsoleMenu.java index 2a5d3ae..6c70119 100644 --- a/src/main/java/io/cloudchains/app/console/ConsoleMenu.java +++ b/src/main/java/io/cloudchains/app/console/ConsoleMenu.java @@ -11,6 +11,7 @@ import io.cloudchains.app.util.ConfigHelper; import io.cloudchains.app.util.background.BackgroundTimerThread; +import java.io.Console; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Base64; @@ -52,32 +53,6 @@ public void logBadChangePass(String msg) { } public void init() { - if (App.getEnv("WALLET_MNEMONIC") != null) { - String mnemonicImport = App.getEnv("WALLET_MNEMONIC"); - if (mnemonicImport == null) { - LOGGER.log(Level.INFO, "Bad mnemonic."); - return; - } - - completeLogin(mnemonicImport, null, true); - return; - } else if (App.getEnv("WALLET_PASSWORD") != null) { - String password = App.getEnv("WALLET_PASSWORD"); - if (password == null) { - LOGGER.log(Level.INFO, "Bad password."); - return; - } - - int strength = KeyHandler.calculatePasswordStrength(password); - - if (!KeyHandler.existsBaseECKeyFromLocal() && strength < 9) { - LOGGER.log(Level.INFO, "Bad password."); - return; - } - - completeLogin(LoginUtils.loginToEntropy(password), null, false); - } - int selection; String newWalletStr = ""; Scanner input = new Scanner(System.in); @@ -93,83 +68,109 @@ public void init() { switch (argument) { case "--enablerpcandconfigure": autoGenerateRPCConfig(); - break; + System.exit(0); case "--development-endpoint": { // sample endpoint url: "https://utils.blocknet.org/" if (i + 1 < arguments.length) { - // Check if there is another argument after "--development-endpoint" String customEndpoint = arguments[i + 1]; + if (customEndpoint.startsWith("--")) { + LOGGER.log(Level.WARNING, "Invalid endpoint: " + customEndpoint); + break; + } App.BASE_URL = customEndpoint; - i++; // Increment i to skip the next argument (custom endpoint) + i++; } else { - LOGGER.log(Level.WARNING, "Missing custom endpoint after '--development-endpoint'"); + String envEndpoint = App.getEnv("BASE_URL"); + if (envEndpoint != null && !envEndpoint.isEmpty()) { + App.BASE_URL = envEndpoint; + } else { + LOGGER.log(Level.WARNING, "Missing custom endpoint after '--development-endpoint'"); + } } break; } case "--exr-endpoint": { if (i + 1 < arguments.length) { - // Check if there is another argument after "--exr-endpoint" String exrEndpoint = arguments[i + 1]; + if (exrEndpoint.startsWith("--")) { + LOGGER.log(Level.WARNING, "Invalid endpoint: " + exrEndpoint); + break; + } App.EXR_ENDPOINT = exrEndpoint; App.exrServerPool = new EXRServerPool(App.EXR_ENDPOINT); LOGGER.log(Level.INFO, "[console] EXR mode enabled with " + App.exrServerPool.getServerCount() + " servers: " + App.EXR_ENDPOINT); - // Start capability probing in background new Thread(() -> { try { - Thread.sleep(1000); // Wait 1 second before starting probe + Thread.sleep(1000); App.exrServerPool.probeAllCapabilities(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }, "EXR-Capability-Prober").start(); - i++; // Increment i to skip the next argument (EXR endpoint) + i++; } else { - LOGGER.log(Level.WARNING, "Missing EXR endpoint after '--exr-endpoint'"); + String envExrEndpoint = App.getEnv("EXR_ENDPOINT"); + if (envExrEndpoint != null && !envExrEndpoint.isEmpty()) { + App.EXR_ENDPOINT = envExrEndpoint; + App.exrServerPool = new EXRServerPool(App.EXR_ENDPOINT); + LOGGER.log(Level.INFO, "[console] EXR mode enabled with " + App.exrServerPool.getServerCount() + " servers: " + App.EXR_ENDPOINT); + new Thread(() -> { + try { + Thread.sleep(1000); + App.exrServerPool.probeAllCapabilities(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }, "EXR-Capability-Prober").start(); + } else { + LOGGER.log(Level.WARNING, "Missing EXR endpoint after '--exr-endpoint'"); + } } break; } case "--version": LOGGER.log(Level.INFO, Version.CLIENT_VERSION); - return; + System.exit(0); + break; case "--createdefaultwallet": { if (KeyHandler.existsBaseECKeyFromLocal()) { LOGGER.log(Level.INFO, "Wallet already exists"); - return; + System.exit(0); } - String password = readPassword(input, arguments, i + 1, ""); + String password = readPassword(input, arguments, i + 1, "", "WALLET_PASSWORD"); int strength = KeyHandler.calculatePasswordStrength(password); if (strength < 9) { logBadPassword(null); - return; + System.exit(1); } String entropy = LoginUtils.loginToEntropy(password); completeLogin(entropy, null, false); - return; + System.exit(0); } case "--createwalletmnemonic": { if (KeyHandler.existsBaseECKeyFromLocal()) { LOGGER.log(Level.INFO, "Wallet already exists"); - return; + System.exit(0); } - String password = readPassword(input, arguments, i + 1, ""); - String mnemonic = readPassword(input, arguments, i + 2, "Mnemonic:\n").trim(); + String password = readPassword(input, arguments, i + 1, "", "WALLET_PASSWORD"); + String mnemonic = readPassword(input, arguments, i + 2, "Mnemonic:\n", "WALLET_MNEMONIC").trim(); int strength = KeyHandler.calculatePasswordStrength(password); if (strength < 9) { logBadPassword(null); - return; + System.exit(1); } if (mnemonic.isEmpty()) { logBadMnemonic(); - return; + System.exit(1); } String entropy = LoginUtils.loginToEntropy(password); completeLogin(entropy, mnemonic, false); - return; + System.exit(0); } case "--xliterpc": { // Increment RPC port by 1 @@ -178,15 +179,12 @@ public void init() { break; } case "--password": { - // If password is provided as an arg then use it, otherwise ask for - // password via stdin. Don't mistake another cmd option as the - // password. - String password = readPassword(input, arguments, i + 1, ""); + String password = readPassword(input, arguments, i + 1, "", "WALLET_PASSWORD"); int strength = KeyHandler.calculatePasswordStrength(password); if (!KeyHandler.existsBaseECKeyFromLocal() && strength < 9) { LOGGER.log(Level.INFO, "Bad password."); - return; + System.exit(1); } String entropy = LoginUtils.loginToEntropy(password); @@ -195,40 +193,40 @@ public void init() { return; } case "--getmnemonic": { - String password = readPassword(input, arguments, i + 1, ""); + String password = readPassword(input, arguments, i + 1, "", "WALLET_PASSWORD"); if (!KeyHandler.existsBaseECKeyFromLocal()) { LOGGER.log(Level.INFO, "No wallet found."); - return; + System.exit(1); } String entropy = LoginUtils.loginToEntropy(password); String mnemonic = CoinInstance.getMnemonicForPw(entropy); System.out.println(mnemonic); - return; + System.exit(0); } case "--changepassword": { if (!KeyHandler.existsBaseECKeyFromLocal()) { logBadChangePass("Wallet not found"); - return; + System.exit(1); } - String currentPassword = readPassword(input, arguments, i + 1, ""); - String newPassword = readPassword(input, arguments, i + 2, ""); + String currentPassword = readPassword(input, arguments, i + 1, "", "WALLET_PASSWORD"); + String newPassword = readPassword(input, arguments, i + 2, "", null); if (currentPassword.isEmpty() || newPassword.isEmpty()) { LOGGER.log(Level.INFO, "Password cannot be empty"); - return; + System.exit(1); } if (currentPassword.equals(newPassword)) { LOGGER.log(Level.INFO, "New password must be different from old password"); - return; + System.exit(1); } // Check new password strength int strength = KeyHandler.calculatePasswordStrength(newPassword); if (strength < 9) { LOGGER.log(Level.INFO, "Unable to change the password: New password is not strong enough"); - return; + System.exit(1); } CoinInstance.CoinError err = CoinInstance.changePassword(LoginUtils.loginToEntropy(currentPassword), @@ -238,15 +236,42 @@ public void init() { else LOGGER.log(Level.INFO, "Wallet password changed successfully"); - return; + System.exit(0); } case "--help": displayHelp(); - return; // Exit after displaying help + System.exit(0); } } } + if (App.getEnv("WALLET_MNEMONIC") != null) { + String mnemonicImport = App.getEnv("WALLET_MNEMONIC"); + if (mnemonicImport == null) { + LOGGER.log(Level.INFO, "Bad mnemonic."); + return; + } + + completeLogin(mnemonicImport, null, true); + return; + } else if (App.getEnv("WALLET_PASSWORD") != null) { + String password = App.getEnv("WALLET_PASSWORD"); + if (password == null) { + LOGGER.log(Level.INFO, "Bad password."); + return; + } + + int strength = KeyHandler.calculatePasswordStrength(password); + + if (!KeyHandler.existsBaseECKeyFromLocal() && strength < 9) { + LOGGER.log(Level.INFO, "Bad password."); + return; + } + + completeLogin(LoginUtils.loginToEntropy(password), null, false); + return; + } + String entropy = null; while (entropy == null) { @@ -267,8 +292,14 @@ public void init() { return; } - LOGGER.log(Level.INFO, "Enter new password: "); - String password = input.next(); + Console console = System.console(); + String password; + if (console != null) { + password = new String(console.readPassword("Enter new password: ")); + } else { + LOGGER.log(Level.INFO, "Enter new password: "); + password = input.next(); + } int strength = KeyHandler.calculatePasswordStrength(password); if (!KeyHandler.existsBaseECKeyFromLocal() && strength < 9) { @@ -280,7 +311,14 @@ public void init() { } case 2: { LOGGER.log(Level.INFO, "Enter password: "); - String password = new String(System.console().readPassword()); + Console console = System.console(); + String password; + if (console != null) { + password = new String(console.readPassword()); + } else { + LOGGER.log(Level.WARNING, "Console not available, using Scanner fallback"); + password = readPassword(input, null, 0, "", null); + } int strength = KeyHandler.calculatePasswordStrength(password); if (!KeyHandler.existsBaseECKeyFromLocal() && strength < 9) { @@ -315,8 +353,11 @@ public void init() { public void deinit() { if (backgroundTimerThread != null) backgroundTimerThread.stop(); - for (CoinTicker cointicker : CoinTicker.coins()) - CoinInstance.getInstance(cointicker).deinit(); + for (CoinTicker cointicker : CoinTicker.coins()) { + CoinInstance instance = CoinInstance.getInstance(cointicker); + if (instance != null) + instance.deinit(); + } } private void completeLogin(String entropy, String userMnemonic, boolean isMnemonic) { @@ -427,6 +468,11 @@ private void autoGenerateRPCConfig() { configHelper.setRpcEnabled(true); configHelper.writeConfig(); } + ConfigHelper masterConf = new ConfigHelper("master"); + masterConf.setRpcUsername(generateRandomString(24)); + masterConf.setRpcPassword(generateRandomString(32)); + masterConf.setRpcEnabled(true); + masterConf.writeConfig(); } private String generateRandomString(int length) { @@ -439,42 +485,52 @@ private String generateRandomString(int length) { } /** - * Reads the password from args or from stdin if password is not specified. + * Reads the password from args, environment variable, or stdin (in that priority order). + * When no positional arg is available, checks the env var before falling back to stdin. * @param input Stdin * @param args Program arguments * @param argPos Current arg position - * @param msg Message to display on stdin - * @return Password + * @param msg Message to display on stdin (defaults to "Password:\n" if empty) + * @param envVar Environment variable name to check as fallback (nullable) + * @return Password string */ - private String readPassword(Scanner input, String[] args, int argPos, String msg) { + private String readPassword(Scanner input, String[] args, int argPos, String msg, String envVar) { if (msg.isEmpty()) msg = "Password:\n"; - if (args.length <= argPos || args[argPos].contains("--")) { // ask pw on stdin + if (args.length <= argPos || args[argPos].contains("--")) { + if (envVar != null) { + String envVal = App.getEnv(envVar); + if (envVal != null && !envVal.isEmpty()) + return envVal; + } System.out.println(msg); - return input.nextLine(); // clear buffer + return input.nextLine(); } - // get pw from args return args[argPos]; } // Function to display help information private static void displayHelp() { - System.out.println("Usage: xlite-daemon [options]"); - System.out.println("Options:"); - System.out.println(" --enablerpcandconfigure Enable and configure RPC"); - System.out.println(" --development-endpoint Set a custom development endpoint"); - System.out.println(" Example: --development-endpoint "); - System.out.println(" --exr-endpoint Set EXR endpoint for EXR server"); - System.out.println(" Example: --exr-endpoint "); - System.out.println(" --version Display the version"); - System.out.println(" --createdefaultwallet Create a default wallet"); - System.out.println(" --createwalletmnemonic Create a wallet with a mnemonic"); - System.out.println(" --xliterpc Increment RPC port by 1"); - System.out.println(" --password Set password without prompt"); - System.out.println(" Example: --password "); - System.out.println(" --getmnemonic Retrieve mnemonic for a password"); - System.out.println(" Example: --getmnemonic "); - System.out.println(" --changepassword Change wallet password"); - System.out.println(" Example: --changepassword "); + System.out.print(getHelpText()); + } + + public static String getHelpText() { + return "Usage: xlite-daemon [options]\n" + + "Options:\n" + + " --enablerpcandconfigure Enable and configure RPC\n" + + " --development-endpoint Set a custom development endpoint\n" + + " Example: --development-endpoint \n" + + " --exr-endpoint Set EXR endpoint for EXR server\n" + + " Example: --exr-endpoint \n" + + " --version Display the version\n" + + " --createdefaultwallet Create a default wallet\n" + + " --createwalletmnemonic Create a wallet with a mnemonic\n" + + " --xliterpc Increment RPC port by 1\n" + + " --password Set password without prompt\n" + + " Example: --password \n" + + " --getmnemonic Retrieve mnemonic for a password\n" + + " Example: --getmnemonic \n" + + " --changepassword Change wallet password\n" + + " Example: --changepassword \n"; } } From f3cebbe693f9ffcbf45b5d640e0be58a8a2b4ff9 Mon Sep 17 00:00:00 2001 From: tryiou Date: Thu, 2 Apr 2026 10:19:41 +0200 Subject: [PATCH 28/73] refactor(logging): standardize exception handling and logging across all modules - Replace e.printStackTrace() with proper LOGGER.log(Level.WARNING, ...) calls - Update log levels from FINER/SEVERE to WARNING for operational errors - Add exception objects to logger calls for better error tracking - Add context-specific messages (coin ticker, operation type, etc.) - Change AddressBalance.utxos from ArrayList to CopyOnWriteArrayList with synchronization - Add proper thread interrupt handling in BackgroundTimerThread This improves error visibility and maintainability of the codebase. --- .../app/net/api/JSONRPCMasterServer.java | 3 +- .../app/net/api/JSONRPCServer.java | 3 +- .../api/http/client/EXRServerSelector.java | 4 +- .../api/http/master/HTTPServerHandler.java | 6 +- .../api/http/server/HTTPServerHandler.java | 43 ++++++----- .../blocknet/BlocknetBlockingClient.java | 9 +-- .../net/protocols/blocknet/BlocknetPeer.java | 15 ++-- .../protocols/blocknet/BlocknetPeerGroup.java | 25 ++++--- .../app/net/xrouter/XRouterMessage.java | 3 +- .../cloudchains/app/util/AddressBalance.java | 71 +++++++++---------- .../app/util/AddressDiscoveryService.java | 6 +- .../io/cloudchains/app/util/ConfigHelper.java | 9 ++- .../app/util/XRouterConfiguration.java | 6 +- .../background/BackgroundTimerThread.java | 13 ++-- .../cloudchains/app/wallet/WalletHelper.java | 8 ++- 15 files changed, 109 insertions(+), 115 deletions(-) diff --git a/src/main/java/io/cloudchains/app/net/api/JSONRPCMasterServer.java b/src/main/java/io/cloudchains/app/net/api/JSONRPCMasterServer.java index 6326c8c..7d94c0d 100644 --- a/src/main/java/io/cloudchains/app/net/api/JSONRPCMasterServer.java +++ b/src/main/java/io/cloudchains/app/net/api/JSONRPCMasterServer.java @@ -44,8 +44,7 @@ public void run() { channel.closeFuture().sync(); } catch (Exception e) { if (!stopping) { - LOGGER.log(Level.FINER, "[json-rpc-server] ERROR: Error during server operation! (master RPC)"); - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[rpc-master] Error during master RPC server operation", e); } } } diff --git a/src/main/java/io/cloudchains/app/net/api/JSONRPCServer.java b/src/main/java/io/cloudchains/app/net/api/JSONRPCServer.java index d5f3a9e..59c9bdd 100644 --- a/src/main/java/io/cloudchains/app/net/api/JSONRPCServer.java +++ b/src/main/java/io/cloudchains/app/net/api/JSONRPCServer.java @@ -48,8 +48,7 @@ public void run() { channel.closeFuture().sync(); } catch (Exception e) { if (!stopping) { - LOGGER.log(Level.FINER, "[json-rpc-server] ERROR: Error during server operation! (" + CoinTickerUtils.tickerToString(coin.getTicker()) + ")"); - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[rpc-server] Error during RPC server operation for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); } } } diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/EXRServerSelector.java b/src/main/java/io/cloudchains/app/net/api/http/client/EXRServerSelector.java index be050c3..987219b 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/client/EXRServerSelector.java +++ b/src/main/java/io/cloudchains/app/net/api/http/client/EXRServerSelector.java @@ -112,7 +112,7 @@ public EXRServer selectHealthyServer(List servers) { @Override public EXRServer selectServerForCoin(List supportingServers, CoinTicker coin) { if (supportingServers == null || supportingServers.isEmpty()) { - LOGGER.log(Level.SEVERE, "[server-selector] NO EXR SERVERS SUPPORT COIN: " + + LOGGER.log(Level.WARNING, "[server-selector] NO EXR SERVERS SUPPORT COIN: " + CoinTickerUtils.tickerToString(coin)); return null; // FAIL - NO FALLBACK TO BASE_URL } @@ -152,7 +152,7 @@ private List getHealthySupportingServers(List supportingSe */ private EXRServer selectFromHealthyServers(List healthyServers, CoinTicker coin) { if (healthyServers.isEmpty()) { - LOGGER.log(Level.SEVERE, "[server-selector] NO HEALTHY EXR SERVERS FOR COIN: " + + LOGGER.log(Level.WARNING, "[server-selector] NO HEALTHY EXR SERVERS FOR COIN: " + CoinTickerUtils.tickerToString(coin)); return null; } diff --git a/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerHandler.java b/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerHandler.java index 60bc0a9..886df2e 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerHandler.java +++ b/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerHandler.java @@ -58,7 +58,7 @@ public void channelInactive(ChannelHandlerContext ctx) throws Exception { @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { - cause.printStackTrace(); + LOGGER.log(Level.WARNING, "[http-master] Exception caught on channel", cause); FullHttpResponse httpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST); writeResponse(ctx, httpResponse, null); @@ -163,7 +163,7 @@ protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) } } catch (Exception e) { LOGGER.log(Level.INFO, "Failed Content: " + content); - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[http-master] Failed to parse JSON-RPC request", e); JsonObject errorParsingJSON = new JsonObject(); errorParsingJSON.addProperty("code", -1001); errorParsingJSON.addProperty("message", "Error parsing JSON."); @@ -236,7 +236,7 @@ private JsonObject getResponse(String method, JsonArray params) { Thread.sleep(500); instance.reloadConfig(); } catch (InterruptedException e) { - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[http-master] Interrupted during reloadconfig for " + ticker, e); } }; new Thread(r).start(); diff --git a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java b/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java index fd678de..0b421f7 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java +++ b/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java @@ -75,7 +75,7 @@ public void channelInactive(ChannelHandlerContext ctx) throws Exception { @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { - cause.printStackTrace(); + LOGGER.log(Level.WARNING, "[http-server-handler] Exception caught on channel for " + CoinTickerUtils.tickerToString(coin.getTicker()), cause); FullHttpResponse httpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST); writeResponse(ctx, httpResponse, null); @@ -192,7 +192,7 @@ protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) } } catch (Exception e) { LOGGER.log(Level.INFO, "Failed Content: " + content); - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[http-server-handler] Failed to parse JSON-RPC request for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); JsonObject errorParsingJSON = new JsonObject(); errorParsingJSON.addProperty("code", -1001); errorParsingJSON.addProperty("message", "Error parsing JSON."); @@ -250,7 +250,7 @@ private JsonObject getResponse(String method, JsonArray params) { try { Thread.sleep(500); } catch (InterruptedException e) { - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[http-server-handler] Interrupted during reloadconfig for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); } coin.reloadConfig(); @@ -420,7 +420,7 @@ private JsonObject getResponse(String method, JsonArray params) { errorJSON.addProperty("message", "Error parsing JSON!"); response.add("error", errorJSON); - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[http-server-handler] Error parsing JSON in sendrawtransaction for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); break; } @@ -562,7 +562,7 @@ private JsonObject getResponse(String method, JsonArray params) { errorJSON.addProperty("message", "Error parsing JSON!"); response.add("error", errorJSON); - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[http-server-handler] Error parsing JSON in getblock for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); break; } @@ -751,7 +751,7 @@ private JsonObject getResponse(String method, JsonArray params) { } catch (Exception e) { LOGGER.log(Level.FINER, "[http-server-handler] ERROR: Error while constructing transaction (output phase)!"); - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[http-server-handler] Error in createrawtransaction P2SH output phase for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); txConstructionError(response, e, "Error while constructing transaction (output phase)"); outputSuccess = false; } @@ -767,7 +767,7 @@ private JsonObject getResponse(String method, JsonArray params) { } catch (Exception e) { LOGGER.log(Level.FINER, "[http-server-handler] ERROR: Error while constructing transaction (output phase)!"); - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[http-server-handler] Error in createrawtransaction output phase for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); txConstructionError(response, e, "Error while constructing transaction (output phase)"); outputSuccess = false; } @@ -799,7 +799,7 @@ private JsonObject getResponse(String method, JsonArray params) { try { tx = new Transaction(coin.getNetworkParameters(), Hex.decode(rawTx)); } catch (Exception e) { - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[http-server-handler] Error decoding raw transaction in decoderawtransaction for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); getInvalidTxResponse(response, e); break; } @@ -829,7 +829,7 @@ private JsonObject getResponse(String method, JsonArray params) { vin.add(thisVin); } catch (Exception e) { LOGGER.log(Level.FINER, "[http-server-handler] ERROR: Error while parsing transaction inputs!"); - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[http-server-handler] Error parsing transaction inputs for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); response.add("result", JsonNull.INSTANCE); JsonObject errorJSON = new JsonObject(); @@ -878,7 +878,7 @@ private JsonObject getResponse(String method, JsonArray params) { vout.add(thisVout); } catch (Exception e) { LOGGER.log(Level.FINER, "[http-server-handler] ERROR: Error while parsing transaction outputs!"); - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[http-server-handler] Error parsing transaction outputs for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); response.add("result", JsonNull.INSTANCE); JsonObject errorJSON = new JsonObject(); @@ -920,7 +920,7 @@ private JsonObject getResponse(String method, JsonArray params) { try { tx = new Transaction(coin.getNetworkParameters(), Hex.decode(rawTx)); } catch (Exception e) { - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[http-server-handler] Error decoding raw tx in signrawtransaction for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); getInvalidTxResponse(response, e); break; } @@ -1155,7 +1155,7 @@ private JsonObject getResponse(String method, JsonArray params) { response.add("error", JsonNull.INSTANCE); } catch (Exception e) { LOGGER.log(Level.FINER, "[http-server-handler] ERROR: Error while parsing transaction!"); - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[http-server-handler] Error parsing transaction in gettxout for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); response.add("result", JsonNull.INSTANCE); JsonObject errorJSON = new JsonObject(); @@ -1289,7 +1289,7 @@ private JsonObject getResponse(String method, JsonArray params) { } } catch (Exception e) { LOGGER.log(Level.FINER, "[http-server-handler] Error while verifying signature! Invalid signature?"); - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[http-server-handler] Error verifying message for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); response.addProperty("result", verified); response.add("error", JsonNull.INSTANCE); @@ -1323,7 +1323,7 @@ private JsonObject getResponse(String method, JsonArray params) { errorJSON.addProperty("message", "Error parsing JSON!"); response.add("error", errorJSON); - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[http-server-handler] Error parsing JSON in sendtransaction for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); break; } @@ -1338,7 +1338,7 @@ private JsonObject getResponse(String method, JsonArray params) { errorJSON.addProperty("message", "Error while creating transaction!"); response.add("error", errorJSON); - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[http-server-handler] Error creating transaction in sendtransaction for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); break; } @@ -1540,8 +1540,7 @@ private byte[] formatMessageForSigning(String message) { bos.write(messageBytes); return bos.toByteArray(); } catch (IOException e) { - LOGGER.log(Level.FINER, "[http-server-handler] Error while formatting message for signing!"); - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[http-server-handler] Error formatting message for signing for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); } return null; @@ -1613,8 +1612,7 @@ private boolean verifyMessage(ECKey key, String signatureB64, String message) { if (Arrays.equals(k.getPubKey(), key.getPubKey())) verified = true; } catch (SignatureException e) { - LOGGER.log(Level.FINER, "[http-server-handler] ERROR: Error while verifying message. Invalid signature?"); - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[http-server-handler] Error verifying message for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); } return verified; @@ -1683,8 +1681,7 @@ private ECKey getSigningKey(Sha256Hash txid, long vout) { } private void getInvalidTxResponse(JsonObject response, Exception e) { - LOGGER.log(Level.FINER, "[http-server-handler] ERROR: Error while decoding raw tx!"); - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[http-server-handler] Error decoding raw tx for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); response.add("result", JsonNull.INSTANCE); JsonObject errorJSON = new JsonObject(); @@ -1695,7 +1692,7 @@ private void getInvalidTxResponse(JsonObject response, Exception e) { } private void txConstructionError(JsonObject response, Exception e, String s) { - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[http-server-handler] Error constructing transaction for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); response.add("result", JsonNull.INSTANCE); JsonObject errorJSON = new JsonObject(); @@ -1727,7 +1724,7 @@ private void getXRouterResponse(JsonObject response, CountDownLatch latch, Atomi latch.await(timeoutPeriod, TimeUnit.SECONDS); } } catch (InterruptedException e) { - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[http-server-handler] Interrupted waiting for XRouter response for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); } if (xRouterResult.get() == null || xRouterResult.get().isEmpty()) { diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetBlockingClient.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetBlockingClient.java index 329575a..5b85805 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetBlockingClient.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetBlockingClient.java @@ -47,8 +47,7 @@ public BlocknetBlockingClient(SocketAddress serverAddress, StreamConnection conn runReadLoop(stream, connection); } catch (Exception e) { if (!closeRequested) { - LOGGER.log(Level.FINER, "[blocknet-blocking-client] Error trying to open/read from connection with " + serverAddress.toString() + "."); - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[blocknet] Error opening/reading connection with " + serverAddress.toString(), e); connectFuture.setException(e); } } finally { @@ -99,8 +98,7 @@ public void closeConnection() { closeRequested = true; socket.close(); } catch (IOException e) { - LOGGER.log(Level.FINER, "[blocknet-blocking-client] Error while closing socket!"); - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[blocknet] Error closing socket", e); } } @@ -111,8 +109,7 @@ public synchronized void writeBytes(byte[] bytes) throws IOException { out.write(bytes); out.flush(); } catch (IOException e) { - LOGGER.log(Level.FINER, "[blocknet-blocking-client] Error while writing bytes to socket!"); - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[blocknet] Error writing bytes to socket", e); closeConnection(); throw e; } diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeer.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeer.java index f461e0e..08a26c9 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeer.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeer.java @@ -275,8 +275,7 @@ public void sendMessage(Message message) throws NotYetConnectedException { messagesPendingReply.add((XRouterMessage) message); LOGGER.log(Level.FINER, "[blocknet-peer] DEBUG: Added UUID " + ((XRouterMessage) message).getXRouterHeader().getUUID() + " to pending reply list."); } catch (IOException e) { - LOGGER.log(Level.FINER, "[blocknet-peer] Error while serializing XRouter message!"); - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[blocknet] Error serializing XRouter message", e); } } else { try { @@ -284,8 +283,7 @@ public void sendMessage(Message message) throws NotYetConnectedException { serializer.serialize(message, outputStream); writeTarget.writeBytes(outputStream.toByteArray()); } catch (IOException e) { - LOGGER.log(Level.FINER, "[blocknet-peer] Error while serializing/sending non-XRouter message!"); - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[blocknet] Error serializing/sending non-XRouter message", e); } } } @@ -499,8 +497,7 @@ private void blockChainDownloadLocked(Sha256Hash toHash) { try { cursor = cursor.getPrev(blockStore); } catch (BlockStoreException e) { - LOGGER.log(Level.FINER, "[blocknet-peer] Failed to walk the blockchain while constructing a locator."); - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[blocknet] Failed to walk blockchain while constructing locator", e); } } @@ -566,8 +563,7 @@ private void endFilteredBlock(FilteredBlock filteredBlock) { } } } catch (VerificationException e) { - LOGGER.log(Level.FINER, "[blocknet-peer] Block failed to properly verify!"); - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[blocknet] Block failed to properly verify", e); } catch (PrunedException e) { LOGGER.log(Level.FINER, "[blocknet-peer] Some data needed to handle this block was pruned! Hash: " + filteredBlock.getHash().toString()); throw new RuntimeException(e); @@ -657,8 +653,7 @@ public int receiveBytes(ByteBuffer buff) { firstMessage = false; } } catch (Exception e) { - LOGGER.log(Level.FINER, "Error while receiving bytes!"); - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[blocknet] Error closing peer connection", e); return -1; } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeerGroup.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeerGroup.java index 3cd3b1d..57761a3 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeerGroup.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeerGroup.java @@ -139,7 +139,7 @@ private void connectTo(InetSocketAddress inetSocketAddress, BlocknetPeer blockne if (future.isDone()) Uninterruptibles.getUninterruptibly(future); } catch (ExecutionException e) { - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[blocknet] Error connecting to peer", e); Throwable cause = Throwables.getRootCause(e); handlePeerDeath(blocknetPeer, cause); } @@ -182,7 +182,7 @@ private ListenableFuture startAsync() { // scheduleMessageQueueRuns(); } catch (Throwable e) { - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[blocknet] Error starting connections", e); } return null; }); @@ -198,8 +198,7 @@ public void stop() { clientManager.awaitTerminated(); threadPool.shutdownNow(); } catch (Exception e) { - LOGGER.log(Level.FINER, "[coin] ERROR: Error while deinitializing keep alive or balance update thread!"); - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[blocknet] Error stopping peer group", e); } } @@ -305,8 +304,7 @@ private void sendInitialXRouterMessages(BlocknetPeer peer) { } } catch (Exception e) { - LOGGER.log(Level.FINER, "[xrouter] ERROR: Error while parsing XRouter config/plugin list!"); - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[blocknet] Error processing XRouter config/plugin list", e); } if (!peer.getHaveConfig().get()) { @@ -516,7 +514,7 @@ private Runnable attemptReconnects(boolean forceReconnect) { } } } catch (Exception e) { - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[blocknet] Error handling peer group event", e); } }; } @@ -526,10 +524,12 @@ private void attemptReconnect(BlocknetPeer blocknetPeer) { BlocknetSeed blocknetSeed = blocknetPeer.getBlocknetSeed(); connectTo(new InetSocketAddress(blocknetSeed.getAddress(), blocknetSeed.getPort()), blocknetPeer); } catch (Exception e) { - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[blocknet] Error during peer group shutdown", e); } } + // --- XRouter connector message queue (latent feature) --- + public void addToMessageQueue(QueueItem queueItem) { messageQueue.add(queueItem); } @@ -598,12 +598,13 @@ private boolean waitForConnection(BlocknetPeer blocknetPeer, int maxWaitSeconds) e -> (e.getHaveConfig().get() && e.getAddress().getAddr() == blocknetPeer.getAddress().getAddr()) ).findFirst().orElse(null); - if (filteredPeer != null) - return true;else { + if (filteredPeer != null) { + return true; + } else { try { Thread.sleep(100); } catch (InterruptedException e) { - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[blocknet] Error waiting for connection", e); } } } @@ -615,6 +616,8 @@ private void scheduleMessageQueueRuns() { executor.scheduleWithFixedDelay(processQueue(), 0, 1, TimeUnit.SECONDS); } + // --- end XRouter connector --- + private void scheduleReconnects() { executor.scheduleWithFixedDelay(attemptReconnects(false), 0, 60, TimeUnit.SECONDS); } diff --git a/src/main/java/io/cloudchains/app/net/xrouter/XRouterMessage.java b/src/main/java/io/cloudchains/app/net/xrouter/XRouterMessage.java index 93f010a..3c431b2 100644 --- a/src/main/java/io/cloudchains/app/net/xrouter/XRouterMessage.java +++ b/src/main/java/io/cloudchains/app/net/xrouter/XRouterMessage.java @@ -91,8 +91,7 @@ public byte[] bitcoinSerialize() { try { bitcoinSerializeToStream(byteArrayOutputStream); } catch (Exception e) { - LOGGER.log(Level.FINER, "Error while serializing XRouter packet! Invalid packet structure?"); - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[xrouter] Error serializing XRouter packet", e); return null; } diff --git a/src/main/java/io/cloudchains/app/util/AddressBalance.java b/src/main/java/io/cloudchains/app/util/AddressBalance.java index 2f61425..a331197 100644 --- a/src/main/java/io/cloudchains/app/util/AddressBalance.java +++ b/src/main/java/io/cloudchains/app/util/AddressBalance.java @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; @@ -15,7 +16,7 @@ public class AddressBalance { private DumpedPrivateKey privateKey; private AtomicReference addrProp = null; private AtomicDouble balanceProp = null; - private ArrayList utxos = null; + private final CopyOnWriteArrayList utxos = new CopyOnWriteArrayList<>(); public AddressBalance(Address address, DumpedPrivateKey privateKey) { this.address = address; @@ -60,48 +61,50 @@ public DumpedPrivateKey getPrivateKey() { } public void clearUtxos() { - if (utxos == null) - return; - - utxos.removeIf(utxo -> !utxo.isSpent()); + synchronized (this) { + utxos.removeIf(utxo -> !utxo.isSpent()); + } } public boolean addUtxo(UTXO utxo) { Preconditions.checkNotNull(utxo); - if (this.utxos == null) - this.utxos = new ArrayList<>(); - - // Only add UTXO's that do not exist in our wallet - UTXO bUtxo = getUtxo(utxo.getTxid(), utxo.getVout()); - if (bUtxo == null) - this.utxos.add(utxo); - else - return false; - - calculateBalance(); - return true; + synchronized (this) { + // Only add UTXO's that do not exist in our wallet + UTXO bUtxo = getUtxo(utxo.getTxid(), utxo.getVout()); + if (bUtxo == null) + this.utxos.add(utxo); + else + return false; + + calculateBalance(); + return true; + } } - public void setUtxos(ArrayList recvUtxos) { - ArrayList newUtxos = new ArrayList<>(); + public void setUtxos(List recvUtxos) { + synchronized (this) { + List newUtxos = new ArrayList<>(); - if (this.utxos != null && this.utxos.size() > 0) { - for (UTXO utxo : recvUtxos) { - for (UTXO bUtxo : this.utxos) { - if (!utxo.getTxid().equals(bUtxo.getTxid()) || utxo.getVout() != bUtxo.getVout()) { - newUtxos.add(utxo); + if (this.utxos.size() > 0) { + for (UTXO utxo : recvUtxos) { + for (UTXO bUtxo : this.utxos) { + if (!utxo.getTxid().equals(bUtxo.getTxid()) || utxo.getVout() != bUtxo.getVout()) { + newUtxos.add(utxo); + } } } - } - if (newUtxos.size() > 0) { - this.utxos = newUtxos; + if (newUtxos.size() > 0) { + this.utxos.clear(); + this.utxos.addAll(newUtxos); + } + } else { + this.utxos.clear(); + this.utxos.addAll(recvUtxos); } - } else { - this.utxos = recvUtxos; - } - calculateBalance(); + calculateBalance(); + } } private UTXO getUtxo(String txid, int vout) { @@ -109,16 +112,10 @@ private UTXO getUtxo(String txid, int vout) { } public List getSpentUtxos() { - if (utxos == null) - utxos = new ArrayList<>(); - return utxos.stream().filter(UTXO::isSpent).collect(Collectors.toList()); } public List getUtxos() { - if (utxos == null) - utxos = new ArrayList<>(); - return utxos.stream().filter(utxo -> !utxo.isSpent()).collect(Collectors.toList()); } diff --git a/src/main/java/io/cloudchains/app/util/AddressDiscoveryService.java b/src/main/java/io/cloudchains/app/util/AddressDiscoveryService.java index 26a9d91..374a3fe 100644 --- a/src/main/java/io/cloudchains/app/util/AddressDiscoveryService.java +++ b/src/main/java/io/cloudchains/app/util/AddressDiscoveryService.java @@ -126,7 +126,7 @@ public int discoverAddressCount() { return discoveredCount; } catch (Exception e) { - LOGGER.log(Level.SEVERE, getLogPrefix() + " Error during discovery", e); + LOGGER.log(Level.WARNING, getLogPrefix() + " Error during discovery", e); return currentAddressCount; } } @@ -185,8 +185,8 @@ private List checkBatchForUtxos(List batch) { try { utxoResponse = httpClient.getUtxosUncached(coinInstance.getTicker(), addresses); } catch (Exception e) { - LOGGER.log(Level.SEVERE, getLogPrefix() + " HTTP request failed for addresses " - + addresses[0] + "..." + addresses[addresses.length - 1] + " - " + e.getMessage()); + LOGGER.log(Level.WARNING, getLogPrefix() + " HTTP request failed for addresses " + + addresses[0] + "..." + addresses[addresses.length - 1] + " - " + e.getMessage(), e); return null; } if (utxoResponse == null || utxoResponse.size() == 0) { diff --git a/src/main/java/io/cloudchains/app/util/ConfigHelper.java b/src/main/java/io/cloudchains/app/util/ConfigHelper.java index 7b784a2..e615dde 100644 --- a/src/main/java/io/cloudchains/app/util/ConfigHelper.java +++ b/src/main/java/io/cloudchains/app/util/ConfigHelper.java @@ -38,7 +38,7 @@ public ConfigHelper(String tickerStr) { file = Preconditions.checkNotNull(this.getFile()); loadConfig(); } catch (Exception e) { - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[config] Failed to initialize config for " + tickerStr, e); } } @@ -94,8 +94,7 @@ public void loadConfig() { addressCount = config.getInt("addressCount"); } } catch (Exception e) { - LOGGER.log(Level.FINER, "[config] ERROR: Error while reading config file!"); - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[config] Error reading config file for " + tickerStr, e); } } @@ -117,7 +116,7 @@ private File getFile() { if (!configFile.createNewFile() && !configFile.exists()) return null; } catch (IOException e) { - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[config] IOException creating config file for " + tickerStr, e); } return configFile; @@ -211,7 +210,7 @@ public void writeConfig() { fileWriter.flush(); fileWriter.close(); } catch (IOException e) { - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[config] IOException writing config for " + tickerStr, e); } } diff --git a/src/main/java/io/cloudchains/app/util/XRouterConfiguration.java b/src/main/java/io/cloudchains/app/util/XRouterConfiguration.java index b88b758..7e7a7ba 100644 --- a/src/main/java/io/cloudchains/app/util/XRouterConfiguration.java +++ b/src/main/java/io/cloudchains/app/util/XRouterConfiguration.java @@ -55,8 +55,7 @@ public void parsePluginConfig() { try { properties = getPluginProperties(rawPluginConfig); } catch (IOException e) { - LOGGER.log(Level.FINER, "[xrouter-plugin-config-parser] ERROR: Error while parsing plugin config!"); - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[xrouter-config] Failed to parse plugin config for " + pluginName, e); return; } @@ -206,8 +205,7 @@ private static HashMap getProperties(String rawConfig) { try { properties = parseINI(formatted); } catch (IOException e) { - LOGGER.log(Level.FINER, "[xrouter-config-parser] ERROR: Error while parsing XRouter config!"); - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[xrouter-config] Failed to parse XRouter config", e); return null; } diff --git a/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java b/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java index 53e11c6..860549b 100644 --- a/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java +++ b/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java @@ -40,6 +40,7 @@ public class BackgroundTimerThread implements Runnable { private long lastOut; private boolean shutdownRequested = false; + private volatile Thread workerThread; // Log rotation scheduler fields private ScheduledExecutorService logRotationScheduler; @@ -110,6 +111,9 @@ private void performDailyLogRotation() { public void stop() { shutdownRequested = true; + if (workerThread != null) { + workerThread.interrupt(); + } if (logRotationScheduler != null && !logRotationScheduler.isShutdown()) { logRotationScheduler.shutdown(); try { @@ -208,6 +212,7 @@ private void sendBalanceUpdate() { @Override public void run() { + workerThread = Thread.currentThread(); LOGGER.log(Level.FINER, "[BackgroundTimer] Waiting until initial messages are sent off."); for (CoinInstance coinInstance : CoinInstance.getCoinInstances()) { @@ -227,12 +232,12 @@ public void run() { outputAvailableCurrencies(); Thread.sleep(100); + } catch (InterruptedException e) { + break; } catch (NullPointerException e) { - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[BackgroundTimer] Null pointer", e); } catch (Exception e) { - LOGGER.log(Level.FINER, "[BackgroundTimer] Interrupted thread"); - e.printStackTrace(); - Thread.currentThread().interrupt(); + LOGGER.log(Level.WARNING, "[BackgroundTimer] Unexpected error", e); } } } diff --git a/src/main/java/io/cloudchains/app/wallet/WalletHelper.java b/src/main/java/io/cloudchains/app/wallet/WalletHelper.java index 8fa4006..41fe664 100644 --- a/src/main/java/io/cloudchains/app/wallet/WalletHelper.java +++ b/src/main/java/io/cloudchains/app/wallet/WalletHelper.java @@ -16,8 +16,14 @@ import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Comparator; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; public class WalletHelper { + private final static LogManager LOGMANAGER = LogManager.getLogManager(); + private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); + private CoinInstance coin; private NetworkParameters networkParameters; @@ -49,7 +55,7 @@ public Transaction createRawTransactionWithAllUTXOs(Transaction tx, double amoun } return tx; } catch (Exception e) { - e.printStackTrace(); + LOGGER.log(Level.WARNING, "[wallet-helper] Error creating transaction", e); return null; } } From 6e016a89e348c28c815828132390a77c4964a87e Mon Sep 17 00:00:00 2001 From: tryiou Date: Thu, 2 Apr 2026 11:42:03 +0200 Subject: [PATCH 29/73] feat(net): enable Digibyte and Ravencoin --- .../io/cloudchains/app/net/CoinInstance.java | 28 +++++++++---------- .../io/cloudchains/app/net/CoinTicker.java | 6 ++-- .../cloudchains/app/net/CoinTickerUtils.java | 10 +++---- .../api/http/server/HTTPServerHandler.java | 12 ++++---- 4 files changed, 27 insertions(+), 29 deletions(-) diff --git a/src/main/java/io/cloudchains/app/net/CoinInstance.java b/src/main/java/io/cloudchains/app/net/CoinInstance.java index 8cb04f1..42ce201 100644 --- a/src/main/java/io/cloudchains/app/net/CoinInstance.java +++ b/src/main/java/io/cloudchains/app/net/CoinInstance.java @@ -15,13 +15,13 @@ //import io.cloudchains.app.net.protocols.bitcoincash.BitcoinCashNetworkParameters; import io.cloudchains.app.net.protocols.blocknet.*; import io.cloudchains.app.net.protocols.dashcoin.DashcoinNetworkParameters; -//import io.cloudchains.app.net.protocols.digibyte.DigibyteNetworkParameters; +import io.cloudchains.app.net.protocols.digibyte.DigibyteNetworkParameters; import io.cloudchains.app.net.protocols.dogecoin.DogecoinNetworkParameters; import io.cloudchains.app.net.protocols.litecoin.LitecoinNetworkParameters; //import io.cloudchains.app.net.protocols.phorecoin.PhorecoinNetworkParameters; import io.cloudchains.app.net.protocols.pivx.PivxNetworkParameters; //import io.cloudchains.app.net.protocols.poliscoin.PoliscoinNetworkParameters; -//import io.cloudchains.app.net.protocols.ravencoin.RavencoinNetworkParameters; +import io.cloudchains.app.net.protocols.ravencoin.RavencoinNetworkParameters; import io.cloudchains.app.net.protocols.syscoin.SyscoinNetworkParameters; import io.cloudchains.app.net.protocols.unobtanium.UnobtaniumNetworkParameters; //import io.cloudchains.app.net.protocols.trezarcoin.TrezarcoinNetworkParameters; @@ -387,12 +387,12 @@ public CoinError init(String pw, String userMnemonic, boolean isMnemonic, boolea rpcPort = 9998; break; } - // case DIGIBYTE: { - // LOGGER.log(Level.FINER, "[coin] Initializing for Digibyte main network."); - // networkParameters = new DigibyteNetworkParameters(); - // rpcPort = 14022; - // break; - // } + case DIGIBYTE: { + LOGGER.log(Level.FINER, "[coin] Initializing for Digibyte main network."); + networkParameters = new DigibyteNetworkParameters(); + rpcPort = 14022; + break; + } case DOGECOIN: { LOGGER.log(Level.FINER, "[coin] Initializing for Dogecoin main network."); networkParameters = new DogecoinNetworkParameters(); @@ -445,12 +445,12 @@ public CoinError init(String pw, String userMnemonic, boolean isMnemonic, boolea // rpcPort = 11772; // break; // } - // case RAVENCOIN: { - // LOGGER.log(Level.FINER, "[coin] Initializing for Ravencoin main network."); - // networkParameters = new RavencoinNetworkParameters(); - // rpcPort = 8766; - // break; - // } + case RAVENCOIN: { + LOGGER.log(Level.FINER, "[coin] Initializing for Ravencoin main network."); + networkParameters = new RavencoinNetworkParameters(); + rpcPort = 8766; + break; + } default: { LOGGER.log(Level.FINER, "[coin] ERROR: Invalid/unsupported network: " + ticker.toString()); return new CoinError("Unsupported coin", CoinError.CoinErrorCode.UNSUPPORTEDCOIN); diff --git a/src/main/java/io/cloudchains/app/net/CoinTicker.java b/src/main/java/io/cloudchains/app/net/CoinTicker.java index 80b7cb4..ca3c43c 100644 --- a/src/main/java/io/cloudchains/app/net/CoinTicker.java +++ b/src/main/java/io/cloudchains/app/net/CoinTicker.java @@ -36,16 +36,16 @@ public static List coins() { // BITCOIN_CASH, - not support on backend LITECOIN, DASHCOIN, -// DIGIBYTE, - not support on backend + DIGIBYTE, DOGECOIN, // TREZARCOIN, - not support on backend SYSCOIN, PIVX, - UNOBTANIUM + UNOBTANIUM, // ALQOCOIN, - not support on backend // POLISCOIN, - not support on backend // PHORECOIN, - not support on backend -// RAVENCOIN + RAVENCOIN // BITBAY - not support on backend ); } diff --git a/src/main/java/io/cloudchains/app/net/CoinTickerUtils.java b/src/main/java/io/cloudchains/app/net/CoinTickerUtils.java index 2e4a218..110a11d 100644 --- a/src/main/java/io/cloudchains/app/net/CoinTickerUtils.java +++ b/src/main/java/io/cloudchains/app/net/CoinTickerUtils.java @@ -21,10 +21,9 @@ public class CoinTickerUtils { tickers.put(CoinTicker.SYSCOIN, "SYS"); tickers.put(CoinTicker.PIVX, "PIVX"); - // TODO Temporarily disable until supported -// tickers.put(CoinTicker.DIGIBYTE, "DGB"); + tickers.put(CoinTicker.DIGIBYTE, "DGB"); // tickers.put(CoinTicker.BITCOIN_CASH, "BCH"); -// tickers.put(CoinTicker.RAVENCOIN, "RVN"); + tickers.put(CoinTicker.RAVENCOIN, "RVN"); tickers.put(CoinTicker.ALQOCOIN, "XLQ"); // TODO Temporarily disable PHORE and POLIS until supported @@ -58,10 +57,9 @@ public static CoinTicker[] getActiveTickers() { CoinTicker.SYSCOIN, CoinTicker.PIVX, - // TODO Temporarily disable until supported -// CoinTicker.DIGIBYTE, + CoinTicker.DIGIBYTE, // CoinTicker.BITCOIN_CASH, -// CoinTicker.RAVENCOIN, + CoinTicker.RAVENCOIN, CoinTicker.ALQOCOIN, // TODO Temporarily disable PHORE and POLIS until supported diff --git a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java b/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java index 0b421f7..f762c07 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java +++ b/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java @@ -1503,18 +1503,18 @@ private byte[] formatMessageForSigning(String message) { case UNOBTANIUM: header = "Unobtanium Signed Message:\n"; break; - // case DIGIBYTE: - // header = "DigiByte Signed Message:\n"; - // break; + case DIGIBYTE: + header = "DigiByte Signed Message:\n"; + break; // case BITBAY: // header = "BitBay Signed Message:\n"; // break; // case POLISCOIN: // header = "Polis Signed Message:\n"; // break; - // case RAVENCOIN: - // header = "Raven Signed Message:\n"; - // break; + case RAVENCOIN: + header = "Raven Signed Message:\n"; + break; case DOGECOIN: header = "Dogecoin Signed Message:\n"; break; From 61422a288332df7afaeb30ffc63954ae65b791b3 Mon Sep 17 00:00:00 2001 From: tryiou Date: Thu, 2 Apr 2026 14:21:28 +0200 Subject: [PATCH 30/73] feat: add Pocketcoin support --- .../io/cloudchains/app/net/CoinInstance.java | 7 ++ .../io/cloudchains/app/net/CoinTicker.java | 4 +- .../cloudchains/app/net/CoinTickerUtils.java | 2 + .../api/http/server/HTTPServerHandler.java | 3 + .../PocketcoinNetworkParameters.java | 102 ++++++++++++++++++ 5 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 src/main/java/io/cloudchains/app/net/protocols/pocketcoin/PocketcoinNetworkParameters.java diff --git a/src/main/java/io/cloudchains/app/net/CoinInstance.java b/src/main/java/io/cloudchains/app/net/CoinInstance.java index 42ce201..f506578 100644 --- a/src/main/java/io/cloudchains/app/net/CoinInstance.java +++ b/src/main/java/io/cloudchains/app/net/CoinInstance.java @@ -20,6 +20,7 @@ import io.cloudchains.app.net.protocols.litecoin.LitecoinNetworkParameters; //import io.cloudchains.app.net.protocols.phorecoin.PhorecoinNetworkParameters; import io.cloudchains.app.net.protocols.pivx.PivxNetworkParameters; +import io.cloudchains.app.net.protocols.pocketcoin.PocketcoinNetworkParameters; //import io.cloudchains.app.net.protocols.poliscoin.PoliscoinNetworkParameters; import io.cloudchains.app.net.protocols.ravencoin.RavencoinNetworkParameters; import io.cloudchains.app.net.protocols.syscoin.SyscoinNetworkParameters; @@ -427,6 +428,12 @@ public CoinError init(String pw, String userMnemonic, boolean isMnemonic, boolea rpcPort = 65111; break; } + case PKOIN: { + LOGGER.log(Level.FINER, "[coin] Initializing for Pocketcoin main network."); + networkParameters = new PocketcoinNetworkParameters(); + rpcPort = 37071; + break; + } // case ALQOCOIN: { // LOGGER.log(Level.FINER, "[coin] Initializing for Alqo main network."); // networkParameters = new AlqocoinNetworkParameters(); diff --git a/src/main/java/io/cloudchains/app/net/CoinTicker.java b/src/main/java/io/cloudchains/app/net/CoinTicker.java index ca3c43c..8b83865 100644 --- a/src/main/java/io/cloudchains/app/net/CoinTicker.java +++ b/src/main/java/io/cloudchains/app/net/CoinTicker.java @@ -21,7 +21,8 @@ public enum CoinTicker { PHORECOIN, RAVENCOIN, BITBAY, - UNOBTANIUM + UNOBTANIUM, + PKOIN ; /** @@ -42,6 +43,7 @@ public static List coins() { SYSCOIN, PIVX, UNOBTANIUM, + PKOIN, // ALQOCOIN, - not support on backend // POLISCOIN, - not support on backend // PHORECOIN, - not support on backend diff --git a/src/main/java/io/cloudchains/app/net/CoinTickerUtils.java b/src/main/java/io/cloudchains/app/net/CoinTickerUtils.java index 110a11d..3db43d4 100644 --- a/src/main/java/io/cloudchains/app/net/CoinTickerUtils.java +++ b/src/main/java/io/cloudchains/app/net/CoinTickerUtils.java @@ -32,6 +32,7 @@ public class CoinTickerUtils { tickers.put(CoinTicker.TREZARCOIN, "TZC"); tickers.put(CoinTicker.BITBAY, "BAY"); tickers.put(CoinTicker.UNOBTANIUM, "UNO"); + tickers.put(CoinTicker.PKOIN, "PKOIN"); } @@ -68,6 +69,7 @@ public static CoinTicker[] getActiveTickers() { CoinTicker.TREZARCOIN, CoinTicker.BITBAY, CoinTicker.UNOBTANIUM, + CoinTicker.PKOIN, }; } diff --git a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java b/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java index f762c07..63b0916 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java +++ b/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java @@ -1503,6 +1503,9 @@ private byte[] formatMessageForSigning(String message) { case UNOBTANIUM: header = "Unobtanium Signed Message:\n"; break; + case PKOIN: + header = "Pocketcoin Signed Message:\n"; + break; case DIGIBYTE: header = "DigiByte Signed Message:\n"; break; diff --git a/src/main/java/io/cloudchains/app/net/protocols/pocketcoin/PocketcoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/pocketcoin/PocketcoinNetworkParameters.java new file mode 100644 index 0000000..a63f41d --- /dev/null +++ b/src/main/java/io/cloudchains/app/net/protocols/pocketcoin/PocketcoinNetworkParameters.java @@ -0,0 +1,102 @@ +package io.cloudchains.app.net.protocols.pocketcoin; + +import org.bitcoinj.core.*; +import org.bitcoinj.store.BlockStore; +import org.bitcoinj.store.BlockStoreException; +import org.bitcoinj.utils.MonetaryFormat; + +public class PocketcoinNetworkParameters extends NetworkParameters { + + public PocketcoinNetworkParameters() { + super(); + } + + @Override + public String getPaymentProtocolId() { + return "main"; + } + + @Override + public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException { + } + + @Override + public Coin getMaxMoney() { + return Coin.valueOf(21000000 * Coin.COIN.value); + } + + @Override + public Coin getMinNonDustOutput() { + return Transaction.MIN_NONDUST_OUTPUT; + } + + @Override + public MonetaryFormat getMonetaryFormat() { + return new MonetaryFormat().code(0, "PKOIN"); + } + + @Override + public String getUriScheme() { + return "pocketcoin:"; + } + + @Override + public boolean hasMaxMoney() { + return true; + } + + @Override + public BitcoinSerializer getSerializer(boolean parseRetain) { + return new BitcoinSerializer(this, parseRetain); + } + + @Override + public int getProtocolVersionNum(ProtocolVersion version) { + return 70031; + } + + @Override + public int getAddressHeader() { + return 55; + } + + @Override + public int getP2SHHeader() { + return 80; + } + + @Override + public int getDumpedPrivateKeyHeader() { + return 33; + } + + @Override + public int[] getAcceptableAddressCodes() { + return new int[]{getAddressHeader(), getP2SHHeader()}; + } + + @Override + public int getBip32HeaderPriv() { + return 0x1E88ADE4; + } + + @Override + public int getBip32HeaderPub() { + return 0x1E88B21E; + } + + @Override + public int getSubsidyDecreaseBlockCount() { + return 2100000; + } + + @Override + public int getInterval() { + return 60; + } + + @Override + public String getId() { + return "PKOIN"; + } +} From d202b88da609981275f8ae21c67e7745b2313cd2 Mon Sep 17 00:00:00 2001 From: tryiou Date: Thu, 2 Apr 2026 17:23:44 +0200 Subject: [PATCH 31/73] refactor(bitcoinj): upgrade bitcoinj to 0.15.10 Update pom.xml dependencies to bitcoinj 0.15.10. Add explicit Guava and Orchid dependencies. Migrate Address class usage to LegacyAddress. Rename network parameter methods to P2PKH variants. Update BlocknetPeer to return ListenableFuture. Adjust native image build-time initialization args. --- pom.xml | 20 ++++++++- .../io/cloudchains/app/crypto/KeyHandler.java | 3 +- .../io/cloudchains/app/net/CoinInstance.java | 2 +- .../app/net/api/http/client/HTTPClient.java | 4 +- .../api/http/server/HTTPServerHandler.java | 23 +++------- .../alqocoin/AlqocoinNetworkParameters.java | 8 +--- .../bitbay/BitbayNetworkParameters.java | 8 +--- .../BitcoinCashNetworkParameters.java | 8 +--- .../blocknet/BlocknetBlockingClient.java | 5 ++- .../blocknet/BlocknetNetworkParameters.java | 8 +--- .../net/protocols/blocknet/BlocknetPeer.java | 22 ++++++---- .../protocols/blocknet/BlocknetPeerGroup.java | 2 +- .../blocknet/BlocknetSerializer.java | 4 +- .../BlocknetTestnet5NetworkParameters.java | 8 +--- .../dashcoin/DashcoinNetworkParameters.java | 8 +--- .../digibyte/DigibyteNetworkParameters.java | 8 +--- .../dogecoin/DogecoinNetworkParameters.java | 8 +--- .../litecoin/LitecoinNetworkParameters.java | 8 +--- .../phorecoin/PhorecoinNetworkParameters.java | 8 +--- .../protocols/pivx/PivxNetworkParameters.java | 8 +--- .../PocketcoinNetworkParameters.java | 8 +--- .../poliscoin/PoliscoinNetworkParameters.java | 8 +--- .../ravencoin/RavencoinNetworkParameters.java | 8 +--- .../syscoin/SyscoinNetworkParameters.java | 8 +--- .../TrezarcoinNetworkParameters.java | 8 +--- .../UnobtaniumNetworkParameters.java | 8 +--- .../app/net/xrouter/XRouterFeeUtils.java | 4 +- .../cloudchains/app/util/AddressBalance.java | 8 ++-- .../java/io/cloudchains/app/util/UTXO.java | 4 +- .../java/io/cloudchains/app/util/Utility.java | 4 +- .../cloudchains/app/wallet/WalletHelper.java | 43 ++----------------- 31 files changed, 97 insertions(+), 187 deletions(-) diff --git a/pom.xml b/pom.xml index 02d9a6d..a447cff 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ native - 0.14.7 + 0.15.10 4.2.7.Final 5.11.3 2.13.2 @@ -77,6 +77,20 @@ + + + com.google.guava + guava + 28.2-android + + + + + org.bitcoinj + orchid + 1.2.1 + + com.google.code.gson @@ -269,7 +283,7 @@ org.bitcoinj:bitcoinj-core - com.madgag.spongycastle:core + org.bouncycastle:bcprov-jdk15to18 com.google.protobuf:protobuf-java com.google.guava:guava net.jcip:jcip-annotations @@ -353,9 +367,11 @@ --initialize-at-build-time=com.google.common.io.BaseEncoding --initialize-at-build-time=com.google.common.io.BaseEncoding$StandardBaseEncoding --initialize-at-build-time=com.google.common.io.BaseEncoding$Alphabet + --initialize-at-build-time=com.google.common.io.BaseEncoding$Base16Encoding --initialize-at-build-time=org.apache.commons.logging --initialize-at-build-time=org.slf4j.helpers.NOPLogger --initialize-at-build-time=org.bitcoinj.core.Utils + --initialize-at-build-time=org.bitcoinj.core.Utils$Runtime --initialize-at-build-time=org.bitcoinj.core.Sha256Hash --initialize-at-build-time=org.bitcoinj.crypto.MnemonicCode --initialize-at-build-time=io.cloudchains.app.util diff --git a/src/main/java/io/cloudchains/app/crypto/KeyHandler.java b/src/main/java/io/cloudchains/app/crypto/KeyHandler.java index d58f689..7705446 100644 --- a/src/main/java/io/cloudchains/app/crypto/KeyHandler.java +++ b/src/main/java/io/cloudchains/app/crypto/KeyHandler.java @@ -442,8 +442,7 @@ private static int detectWalletVersion(String firstLine) { private static List generateAndPersistNewSeed(char[] passphrase, File file) { try { DeterministicSeed seed = new DeterministicSeed( - SecureRandom.getInstanceStrong(), 128, "", - System.currentTimeMillis() / 1000); + SecureRandom.getInstanceStrong(), 128, ""); String mnemonic = Joiner.on(" ").join( Objects.requireNonNull(seed.getMnemonicCode())); if (writeInitialData(file, mnemonic, passphrase)) { diff --git a/src/main/java/io/cloudchains/app/net/CoinInstance.java b/src/main/java/io/cloudchains/app/net/CoinInstance.java index f506578..8a1e21f 100644 --- a/src/main/java/io/cloudchains/app/net/CoinInstance.java +++ b/src/main/java/io/cloudchains/app/net/CoinInstance.java @@ -202,7 +202,7 @@ public AddressBalance getAddress(String addressB58) { public AddressBalance generateAddress(boolean updateConfig) { AddressBalance addressKeyPair = getWalletHelper().generateAddress(); - Address address = addressKeyPair.getAddress(); + LegacyAddress address = (LegacyAddress) addressKeyPair.getAddress(); DumpedPrivateKey privateKey = addressKeyPair.getPrivateKey(); addressKeyPairs.add(addressKeyPair); LOGGER.log(Level.FINER, "[wallet] Generated new address, have " + addressKeyPairs.size() + ": " + address.toBase58()); diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java b/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java index a4c97d2..f446c31 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java +++ b/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java @@ -30,7 +30,7 @@ import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.message.BasicHeader; -import org.bitcoinj.core.Address; +import org.bitcoinj.core.LegacyAddress; import org.bitcoinj.script.Script; import org.bitcoinj.script.ScriptBuilder; import org.json.JSONArray; @@ -448,7 +448,7 @@ public JsonArray getUtxosUncached(CoinTicker coinTicker, String[] addresses) { String address = utxoArr.getJSONObject(i).getString("address"); utxoJSON.addProperty("address", address); - Address addr = Address.fromBase58(coinInstance.getNetworkParameters(), address); + LegacyAddress addr = LegacyAddress.fromBase58(coinInstance.getNetworkParameters(), address); Script script = ScriptBuilder.createOutputScript(addr); utxoJSON.addProperty("scriptPubKey", new String(Hex.encode(script.getProgram()))); diff --git a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java b/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java index 63b0916..e139b24 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java +++ b/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java @@ -741,11 +741,11 @@ private JsonObject getResponse(String method, JsonArray params) { // First, add P2SH outputs. for (OutputEntry entry : outputEntries) { try { - Address address = Address.fromBase58(coin.getNetworkParameters(), entry.address); + LegacyAddress address = LegacyAddress.fromBase58(coin.getNetworkParameters(), entry.address); Coin outputValue = Coin.valueOf((long) Math.floor(entry.amount * Coin.COIN.value)); if (isP2SHAddress(entry.address)) { LOGGER.log(Level.FINER, "[http-server-handler] P2SH Address Found: " + entry.address); - Script p2shScript = ScriptBuilder.createP2SHOutputScript(address.getHash160()); + Script p2shScript = ScriptBuilder.createP2SHOutputScript(address.getHash()); tx.addOutput(outputValue, p2shScript); } } catch (Exception e) { @@ -759,7 +759,7 @@ private JsonObject getResponse(String method, JsonArray params) { // Then, add non-P2SH outputs. for (OutputEntry entry : outputEntries) { try { - Address address = Address.fromBase58(coin.getNetworkParameters(), entry.address); + LegacyAddress address = LegacyAddress.fromBase58(coin.getNetworkParameters(), entry.address); Coin outputValue = Coin.valueOf((long) Math.floor(entry.amount * Coin.COIN.value)); if (!isP2SHAddress(entry.address)) { tx.addOutput(outputValue, address); @@ -870,7 +870,7 @@ private JsonObject getResponse(String method, JsonArray params) { getScriptType(scriptPubKey, type); JsonArray addresses = new JsonArray(); - addresses.add(output.getScriptPubKey().getToAddress(coin.getNetworkParameters()).toBase58()); + addresses.add(((LegacyAddress) output.getScriptPubKey().getToAddress(coin.getNetworkParameters())).toBase58()); scriptPubKey.add("addresses", addresses); thisVout.add("scriptPubKey", scriptPubKey); @@ -1282,7 +1282,7 @@ private JsonObject getResponse(String method, JsonArray params) { if (!verified) throw new SignatureException("Signature was not verified."); - String derivedAddr = key.toAddress(coin.getNetworkParameters()).toBase58(); + String derivedAddr = LegacyAddress.fromKey(coin.getNetworkParameters(), key).toBase58(); if (!addr.equals(derivedAddr)) { LOGGER.log(Level.FINER, "[http-server-handler] ERROR: Addresses do not match! Failing."); verified = false; @@ -1396,7 +1396,7 @@ private JsonObject getResponse(String method, JsonArray params) { boolean isP2SH = false; String scriptPubKey = ""; if (isValidAddress) { - Address toAddress = Address.fromBase58(coin.getNetworkParameters(), address); + LegacyAddress toAddress = LegacyAddress.fromBase58(coin.getNetworkParameters(), address); if (isP2SHAddress(address)) { isP2SH = true; @@ -1624,17 +1624,6 @@ private boolean verifyMessage(ECKey key, String signatureB64, String message) { private boolean isP2SHAddress(String address) { byte[] versionAndDataBytes = Base58.decodeChecked(address); int version = versionAndDataBytes[0] & 0xFF; - - if (coin.getNetworkParameters().getAcceptableAddressCodes().length > 2) { - LOGGER.log(Level.FINER, "[http-server-handler] Coin has more than 2 acceptable address codes"); - - for (int t : coin.getNetworkParameters().getAcceptableAddressCodes()) { - if (coin.getNetworkParameters().getAddressHeader() != t && t == version) { - return true; - } - } - } - return coin.getNetworkParameters().getP2SHHeader() == version; } diff --git a/src/main/java/io/cloudchains/app/net/protocols/alqocoin/AlqocoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/alqocoin/AlqocoinNetworkParameters.java index 3dc95b2..631aa63 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/alqocoin/AlqocoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/alqocoin/AlqocoinNetworkParameters.java @@ -70,18 +70,14 @@ public int getDumpedPrivateKeyHeader() { return 193; } - @Override - public int[] getAcceptableAddressCodes() { - return new int[]{getAddressHeader(), getP2SHHeader()}; - } @Override - public int getBip32HeaderPriv() { + public int getBip32HeaderP2PKHpriv() { return 0x0488ADE4; } @Override - public int getBip32HeaderPub() { + public int getBip32HeaderP2PKHpub() { return 0x0488B21E; } diff --git a/src/main/java/io/cloudchains/app/net/protocols/bitbay/BitbayNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/bitbay/BitbayNetworkParameters.java index df6057b..703fbe2 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/bitbay/BitbayNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/bitbay/BitbayNetworkParameters.java @@ -70,18 +70,14 @@ public int getDumpedPrivateKeyHeader() { return 153; } - @Override - public int[] getAcceptableAddressCodes() { - return new int[]{getAddressHeader(), getP2SHHeader()}; - } @Override - public int getBip32HeaderPriv() { + public int getBip32HeaderP2PKHpriv() { return 0x0488ADE4; } @Override - public int getBip32HeaderPub() { + public int getBip32HeaderP2PKHpub() { return 0x0488B21E; } diff --git a/src/main/java/io/cloudchains/app/net/protocols/bitcoincash/BitcoinCashNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/bitcoincash/BitcoinCashNetworkParameters.java index 83c1142..bbbbddf 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/bitcoincash/BitcoinCashNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/bitcoincash/BitcoinCashNetworkParameters.java @@ -70,18 +70,14 @@ public int getDumpedPrivateKeyHeader() { return 128; } - @Override - public int[] getAcceptableAddressCodes() { - return new int[]{getAddressHeader(), getP2SHHeader()}; - } @Override - public int getBip32HeaderPriv() { + public int getBip32HeaderP2PKHpriv() { return 0x0488ADE4; } @Override - public int getBip32HeaderPub() { + public int getBip32HeaderP2PKHpub() { return 0x0488B21E; } diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetBlockingClient.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetBlockingClient.java index 5b85805..1fb24d9 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetBlockingClient.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetBlockingClient.java @@ -1,5 +1,7 @@ package io.cloudchains.app.net.protocols.blocknet; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import org.bitcoinj.core.Context; import org.bitcoinj.net.MessageWriteTarget; @@ -103,11 +105,12 @@ public void closeConnection() { } @Override - public synchronized void writeBytes(byte[] bytes) throws IOException { + public synchronized ListenableFuture writeBytes(byte[] bytes) throws IOException { try { OutputStream out = socket.getOutputStream(); out.write(bytes); out.flush(); + return Futures.immediateFuture(null); } catch (IOException e) { LOGGER.log(Level.WARNING, "[blocknet] Error writing bytes to socket", e); closeConnection(); diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetNetworkParameters.java index d57b726..5560aa7 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetNetworkParameters.java @@ -25,10 +25,6 @@ public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, Block } - @Override - public int[] getAcceptableAddressCodes() { - return new int[]{getAddressHeader(), getP2SHHeader()}; - } @Override public Sha256Hash getGenesisBlockHash() { @@ -143,12 +139,12 @@ public int getDumpedPrivateKeyHeader() { } @Override - public int getBip32HeaderPub() { + public int getBip32HeaderP2PKHpub() { return 0x0488B21E; } @Override - public int getBip32HeaderPriv() { + public int getBip32HeaderP2PKHpriv() { return 0x0488ADE4; } diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeer.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeer.java index 08a26c9..8200b4d 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeer.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeer.java @@ -2,6 +2,7 @@ import com.google.common.base.Function; import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; @@ -13,7 +14,6 @@ import io.cloudchains.app.net.xrouter.XRouterMessage; import io.cloudchains.app.net.xrouter.XRouterMessageSerializer; import io.cloudchains.app.util.XRouterConfiguration; -import net.jcip.annotations.GuardedBy; import org.bitcoinj.core.*; import org.bitcoinj.store.BlockStore; import org.bitcoinj.store.BlockStoreException; @@ -23,6 +23,7 @@ import org.json.JSONObject; import javax.annotation.Nullable; +import javax.annotation.concurrent.GuardedBy; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.BufferUnderflowException; @@ -254,7 +255,7 @@ public void onXRouterMessageReceived(XRouterMessage message, XRouterMessage orig } @Override - public void sendMessage(Message message) throws NotYetConnectedException { + public ListenableFuture sendMessage(Message message) throws NotYetConnectedException { lock.lock(); try { if (writeTarget == null) { @@ -270,10 +271,11 @@ public void sendMessage(Message message) throws NotYetConnectedException { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); xRouterMessageSerializer.serialize(message, outputStream); LOGGER.log(Level.FINER, "[blocknet-peer] DEBUG: Sending XRouter message. Actual length (excluding network header) is " + (outputStream.size() - BlocknetPacketHeader.HEADER_LENGTH - 4) + " bytes."); - writeTarget.writeBytes(outputStream.toByteArray()); + ListenableFuture future = writeTarget.writeBytes(outputStream.toByteArray()); messagesPendingReply.add((XRouterMessage) message); LOGGER.log(Level.FINER, "[blocknet-peer] DEBUG: Added UUID " + ((XRouterMessage) message).getXRouterHeader().getUUID() + " to pending reply list."); + return future; } catch (IOException e) { LOGGER.log(Level.WARNING, "[blocknet] Error serializing XRouter message", e); } @@ -281,11 +283,13 @@ public void sendMessage(Message message) throws NotYetConnectedException { try { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); serializer.serialize(message, outputStream); - writeTarget.writeBytes(outputStream.toByteArray()); + return writeTarget.writeBytes(outputStream.toByteArray()); } catch (IOException e) { LOGGER.log(Level.WARNING, "[blocknet] Error serializing/sending non-XRouter message", e); } } + + return Futures.immediateFuture(null); } @Override @@ -348,7 +352,7 @@ private void processVersionMessage(VersionMessage versionMessage) throws Protoco LOGGER.log(Level.FINER, "[blocknet-peer] Received version message: " + peerVersionMessage.subVer + ", version " + peerVersionMessage.clientVersion + ", blocks=" + peerVersionMessage.bestHeight - + ", us=" + peerVersionMessage.theirAddr); + + ", us=" + peerVersionMessage.receivingAddr); if (!peerVersionMessage.hasBlockChain() || (!params.allowEmptyPeerChain() && peerVersionMessage.bestHeight == 0)) { LOGGER.log(Level.FINER, "[blocknet-peer] ERROR: Peer has an empty blockchain while this network does not allow empty blockchains. Disconnecting."); @@ -469,7 +473,7 @@ private void blockChainDownloadLocked(Sha256Hash toHash) { throw new IllegalStateException("Lock is not held by current thread."); } - List blockLocator = new ArrayList<>(51); + List blockLocatorHashes = new ArrayList<>(51); if (blockChain == null) { throw new NullPointerException("Blockchain object is null."); @@ -493,7 +497,7 @@ private void blockChainDownloadLocked(Sha256Hash toHash) { StoredBlock cursor = chainHead; for (int i = 100; cursor != null && i > 0; i--) { - blockLocator.add(cursor.getHeader().getHash()); + blockLocatorHashes.add(cursor.getHeader().getHash()); try { cursor = cursor.getPrev(blockStore); } catch (BlockStoreException e) { @@ -502,7 +506,9 @@ private void blockChainDownloadLocked(Sha256Hash toHash) { } if (cursor != null) - blockLocator.add(params.getGenesisBlockHash()); + blockLocatorHashes.add(params.getGenesisBlockHash()); + + BlockLocator blockLocator = new BlockLocator(ImmutableList.copyOf(blockLocatorHashes)); lastGetBlocksBegin = chainHeadHash; lastGetBlocksEnd = toHash; diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeerGroup.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeerGroup.java index 57761a3..e5243cc 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeerGroup.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeerGroup.java @@ -107,7 +107,7 @@ protected ListeningScheduledExecutorService createPrivateExecutor() { private BlocknetPeer createPeer(BlocknetParameters blocknetNetworkParameters, BlockChain chain, BlocknetSeed blocknetSeed) { PeerAddress peerAddress; try { - peerAddress = new PeerAddress(InetAddress.getByName(blocknetSeed.getAddress()), blocknetSeed.getPort(), 0); + peerAddress = new PeerAddress(blocknetNetworkParameters, InetAddress.getByName(blocknetSeed.getAddress()), blocknetSeed.getPort()); } catch (UnknownHostException e) { return null; } diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetSerializer.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetSerializer.java index 285f2d9..df2384c 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetSerializer.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetSerializer.java @@ -99,7 +99,7 @@ public Message deserializePayload(BitcoinSerializer.BitcoinPacketHeader header, case "getheaders": return new GetHeadersMessage(params, payloadBytes); case "tx": - return new Transaction(params, payloadBytes, 0, null, this, blocknetPacketHeader.getLength()); + return new Transaction(params, payloadBytes, 0, null, this, blocknetPacketHeader.getLength(), null); case "addr": return makeAddressMessage(payloadBytes, blocknetPacketHeader.getLength()); case "alert": @@ -185,7 +185,7 @@ public InventoryMessage makeInventoryMessage(byte[] payloadBytes, int length) th @Override public Transaction makeTransaction(byte[] payloadBytes, int offset, int length, byte[] hash) throws ProtocolException, UnsupportedOperationException { - return new Transaction(params, payloadBytes, offset, null, this, length); + return new Transaction(params, payloadBytes, offset, null, this, length, hash); } @Override diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetTestnet5NetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetTestnet5NetworkParameters.java index 8618632..642afd4 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetTestnet5NetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetTestnet5NetworkParameters.java @@ -24,10 +24,6 @@ public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, Block } - @Override - public int[] getAcceptableAddressCodes() { - return new int[]{getAddressHeader(), getP2SHHeader()}; - } @Override public Sha256Hash getGenesisBlockHash() { @@ -134,12 +130,12 @@ public int getDumpedPrivateKeyHeader() { } @Override - public int getBip32HeaderPub() { + public int getBip32HeaderP2PKHpub() { return 0x3A8061A0; } @Override - public int getBip32HeaderPriv() { + public int getBip32HeaderP2PKHpriv() { return 0x3A805837; } diff --git a/src/main/java/io/cloudchains/app/net/protocols/dashcoin/DashcoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/dashcoin/DashcoinNetworkParameters.java index 39ca8f1..f0910e5 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/dashcoin/DashcoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/dashcoin/DashcoinNetworkParameters.java @@ -70,18 +70,14 @@ public int getDumpedPrivateKeyHeader() { return 204; } - @Override - public int[] getAcceptableAddressCodes() { - return new int[]{getAddressHeader(), getP2SHHeader()}; - } @Override - public int getBip32HeaderPriv() { + public int getBip32HeaderP2PKHpriv() { return 0x0488ADE4; } @Override - public int getBip32HeaderPub() { + public int getBip32HeaderP2PKHpub() { return 0x0488B21E; } diff --git a/src/main/java/io/cloudchains/app/net/protocols/digibyte/DigibyteNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/digibyte/DigibyteNetworkParameters.java index e2677f4..1ead56e 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/digibyte/DigibyteNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/digibyte/DigibyteNetworkParameters.java @@ -70,18 +70,14 @@ public int getDumpedPrivateKeyHeader() { return 128; } - @Override - public int[] getAcceptableAddressCodes() { - return new int[]{getAddressHeader(), getP2SHHeader()}; - } @Override - public int getBip32HeaderPriv() { + public int getBip32HeaderP2PKHpriv() { return 0x0488ADE4; } @Override - public int getBip32HeaderPub() { + public int getBip32HeaderP2PKHpub() { return 0x0488B21E; } diff --git a/src/main/java/io/cloudchains/app/net/protocols/dogecoin/DogecoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/dogecoin/DogecoinNetworkParameters.java index 4c41d02..b9190bd 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/dogecoin/DogecoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/dogecoin/DogecoinNetworkParameters.java @@ -70,18 +70,14 @@ public int getDumpedPrivateKeyHeader() { return 158; } - @Override - public int[] getAcceptableAddressCodes() { - return new int[]{getAddressHeader(), getP2SHHeader()}; - } @Override - public int getBip32HeaderPriv() { + public int getBip32HeaderP2PKHpriv() { return 0x02fac398; } @Override - public int getBip32HeaderPub() { + public int getBip32HeaderP2PKHpub() { return 0x02facafd; } diff --git a/src/main/java/io/cloudchains/app/net/protocols/litecoin/LitecoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/litecoin/LitecoinNetworkParameters.java index 90e3af6..af3036e 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/litecoin/LitecoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/litecoin/LitecoinNetworkParameters.java @@ -74,18 +74,14 @@ public int getDumpedPrivateKeyHeader() { return 176; } - @Override - public int[] getAcceptableAddressCodes() { - return new int[]{getAddressHeader(), getP2SHHeader(), getP2SHLegacyHeader()}; - } @Override - public int getBip32HeaderPriv() { + public int getBip32HeaderP2PKHpriv() { return 0x0488B21E; } @Override - public int getBip32HeaderPub() { + public int getBip32HeaderP2PKHpub() { return 0x0488ADE4; } diff --git a/src/main/java/io/cloudchains/app/net/protocols/phorecoin/PhorecoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/phorecoin/PhorecoinNetworkParameters.java index d07c025..ac66eca 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/phorecoin/PhorecoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/phorecoin/PhorecoinNetworkParameters.java @@ -70,18 +70,14 @@ public int getDumpedPrivateKeyHeader() { return 212; } - @Override - public int[] getAcceptableAddressCodes() { - return new int[]{getAddressHeader(), getP2SHHeader()}; - } @Override - public int getBip32HeaderPriv() { + public int getBip32HeaderP2PKHpriv() { return 0x0221312B; } @Override - public int getBip32HeaderPub() { + public int getBip32HeaderP2PKHpub() { return 0x022D2533; } diff --git a/src/main/java/io/cloudchains/app/net/protocols/pivx/PivxNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/pivx/PivxNetworkParameters.java index 157fecf..0b56aef 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/pivx/PivxNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/pivx/PivxNetworkParameters.java @@ -70,18 +70,14 @@ public int getDumpedPrivateKeyHeader() { return 212; } - @Override - public int[] getAcceptableAddressCodes() { - return new int[]{getAddressHeader(), getP2SHHeader()}; - } @Override - public int getBip32HeaderPriv() { + public int getBip32HeaderP2PKHpriv() { return 0x0221312B; } @Override - public int getBip32HeaderPub() { + public int getBip32HeaderP2PKHpub() { return 0x022D2533; } diff --git a/src/main/java/io/cloudchains/app/net/protocols/pocketcoin/PocketcoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/pocketcoin/PocketcoinNetworkParameters.java index a63f41d..e054383 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/pocketcoin/PocketcoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/pocketcoin/PocketcoinNetworkParameters.java @@ -70,18 +70,14 @@ public int getDumpedPrivateKeyHeader() { return 33; } - @Override - public int[] getAcceptableAddressCodes() { - return new int[]{getAddressHeader(), getP2SHHeader()}; - } @Override - public int getBip32HeaderPriv() { + public int getBip32HeaderP2PKHpriv() { return 0x1E88ADE4; } @Override - public int getBip32HeaderPub() { + public int getBip32HeaderP2PKHpub() { return 0x1E88B21E; } diff --git a/src/main/java/io/cloudchains/app/net/protocols/poliscoin/PoliscoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/poliscoin/PoliscoinNetworkParameters.java index 5d39572..334cedc 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/poliscoin/PoliscoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/poliscoin/PoliscoinNetworkParameters.java @@ -70,18 +70,14 @@ public int getDumpedPrivateKeyHeader() { return 60; } - @Override - public int[] getAcceptableAddressCodes() { - return new int[]{getAddressHeader(), getP2SHHeader()}; - } @Override - public int getBip32HeaderPriv() { + public int getBip32HeaderP2PKHpriv() { return 0x03E25945; } @Override - public int getBip32HeaderPub() { + public int getBip32HeaderP2PKHpub() { return 0x03E25D7E; } diff --git a/src/main/java/io/cloudchains/app/net/protocols/ravencoin/RavencoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/ravencoin/RavencoinNetworkParameters.java index 44f4760..a0606c0 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/ravencoin/RavencoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/ravencoin/RavencoinNetworkParameters.java @@ -70,18 +70,14 @@ public int getDumpedPrivateKeyHeader() { return 128; } - @Override - public int[] getAcceptableAddressCodes() { - return new int[]{getAddressHeader(), getP2SHHeader()}; - } @Override - public int getBip32HeaderPriv() { + public int getBip32HeaderP2PKHpriv() { return 0x0488ADE4; } @Override - public int getBip32HeaderPub() { + public int getBip32HeaderP2PKHpub() { return 0x0488B21E; } diff --git a/src/main/java/io/cloudchains/app/net/protocols/syscoin/SyscoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/syscoin/SyscoinNetworkParameters.java index d036fdc..c7f906f 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/syscoin/SyscoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/syscoin/SyscoinNetworkParameters.java @@ -11,10 +11,6 @@ public SyscoinNetworkParameters() { super(); } - @Override - public int[] getAcceptableAddressCodes() { - return new int[]{getAddressHeader(), getP2SHHeader()}; - } @Override public String getPaymentProtocolId() { @@ -76,12 +72,12 @@ public int getDumpedPrivateKeyHeader() { } @Override - public int getBip32HeaderPriv() { + public int getBip32HeaderP2PKHpriv() { return 0x0488ADE4; } @Override - public int getBip32HeaderPub() { + public int getBip32HeaderP2PKHpub() { return 0x0488B21E; } diff --git a/src/main/java/io/cloudchains/app/net/protocols/trezarcoin/TrezarcoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/trezarcoin/TrezarcoinNetworkParameters.java index a1acc4d..bb48adf 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/trezarcoin/TrezarcoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/trezarcoin/TrezarcoinNetworkParameters.java @@ -11,10 +11,6 @@ public TrezarcoinNetworkParameters() { super(); } - @Override - public int[] getAcceptableAddressCodes() { - return new int[]{getAddressHeader(), getP2SHHeader()}; - } @Override public String getPaymentProtocolId() { @@ -76,12 +72,12 @@ public int getDumpedPrivateKeyHeader() { } @Override - public int getBip32HeaderPriv() { + public int getBip32HeaderP2PKHpriv() { return 0x0488ADE4; } @Override - public int getBip32HeaderPub() { + public int getBip32HeaderP2PKHpub() { return 0x0488B21E; } diff --git a/src/main/java/io/cloudchains/app/net/protocols/unobtanium/UnobtaniumNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/unobtanium/UnobtaniumNetworkParameters.java index 03947e9..314cc70 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/unobtanium/UnobtaniumNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/unobtanium/UnobtaniumNetworkParameters.java @@ -71,18 +71,14 @@ public int getDumpedPrivateKeyHeader() { return 224; } - @Override - public int[] getAcceptableAddressCodes() { - return new int[]{getAddressHeader(), getP2SHHeader()}; - } @Override - public int getBip32HeaderPriv() { + public int getBip32HeaderP2PKHpriv() { return 0x0488ADE4; } @Override - public int getBip32HeaderPub() { + public int getBip32HeaderP2PKHpub() { return 0x0488B21E; } diff --git a/src/main/java/io/cloudchains/app/net/xrouter/XRouterFeeUtils.java b/src/main/java/io/cloudchains/app/net/xrouter/XRouterFeeUtils.java index 62e2dcb..6108c5f 100644 --- a/src/main/java/io/cloudchains/app/net/xrouter/XRouterFeeUtils.java +++ b/src/main/java/io/cloudchains/app/net/xrouter/XRouterFeeUtils.java @@ -47,7 +47,7 @@ public static String getXRouterFeeTx(BlocknetPeer blocknetPeer, String xRouterCo double totalAvailable = blocknetWalletHelper.getSpendBalance(totalSpending); double changeAmt = ((totalAvailable - blocknetCoin.getConfigHelper().getFee()) - fee); - Address xRouterPaymentAddress = Address.fromBase58(params, xRouterConfig.getFeeAddress()); + LegacyAddress xRouterPaymentAddress = LegacyAddress.fromBase58(params, xRouterConfig.getFeeAddress()); Coin blocknetNetworkFeeAmt = Coin.valueOf((long) Math.floor(blocknetCoin.getConfigHelper().getFee() * Coin.COIN.value)); Coin xRouterChangeAmt = Coin.valueOf((long) Math.floor(totalAvailable * Coin.COIN.value)).minus(blocknetNetworkFeeAmt).minus(xRouterFeeAmt); @@ -89,7 +89,7 @@ public static TransactionOutput createXrSendTransactionFeeOutput(BlocknetPeer bl Coin feeAmount = Coin.valueOf((long) Math.floor(fee * Coin.COIN.value)); - return new TransactionOutput(blocknetCoin.getNetworkParameters(), null, feeAmount, Address.fromBase58(blocknetCoin.getNetworkParameters(), feeAddress)); + return new TransactionOutput(blocknetCoin.getNetworkParameters(), null, feeAmount, LegacyAddress.fromBase58(blocknetCoin.getNetworkParameters(), feeAddress)); } public static String coveredXrFee(BlocknetPeer blocknetPeer, Transaction transaction) { diff --git a/src/main/java/io/cloudchains/app/util/AddressBalance.java b/src/main/java/io/cloudchains/app/util/AddressBalance.java index a331197..fb48357 100644 --- a/src/main/java/io/cloudchains/app/util/AddressBalance.java +++ b/src/main/java/io/cloudchains/app/util/AddressBalance.java @@ -2,8 +2,8 @@ import com.google.common.base.Preconditions; import com.google.common.util.concurrent.AtomicDouble; -import org.bitcoinj.core.Address; import org.bitcoinj.core.DumpedPrivateKey; +import org.bitcoinj.core.LegacyAddress; import java.util.ArrayList; import java.util.List; @@ -12,19 +12,19 @@ import java.util.stream.Collectors; public class AddressBalance { - private Address address; + private LegacyAddress address; private DumpedPrivateKey privateKey; private AtomicReference addrProp = null; private AtomicDouble balanceProp = null; private final CopyOnWriteArrayList utxos = new CopyOnWriteArrayList<>(); - public AddressBalance(Address address, DumpedPrivateKey privateKey) { + public AddressBalance(LegacyAddress address, DumpedPrivateKey privateKey) { this.address = address; this.privateKey = privateKey; setAddrProp(address.toBase58()); } - public Address getAddress() { + public LegacyAddress getAddress() { return address; } diff --git a/src/main/java/io/cloudchains/app/util/UTXO.java b/src/main/java/io/cloudchains/app/util/UTXO.java index 92496c7..bda7edd 100644 --- a/src/main/java/io/cloudchains/app/util/UTXO.java +++ b/src/main/java/io/cloudchains/app/util/UTXO.java @@ -3,8 +3,8 @@ import com.google.gson.annotations.SerializedName; import io.cloudchains.app.net.CoinInstance; import io.cloudchains.app.net.CoinTicker; -import org.bitcoinj.core.Address; import org.bitcoinj.core.Coin; +import org.bitcoinj.core.LegacyAddress; import org.bitcoinj.core.Sha256Hash; import org.bitcoinj.script.Script; import org.bitcoinj.script.ScriptBuilder; @@ -65,7 +65,7 @@ public double getAmount() { } public org.bitcoinj.core.UTXO createUTXO() { - Address address = Address.fromBase58(CoinInstance.getInstance(this.ticker).getNetworkParameters(), getAddress()); + LegacyAddress address = LegacyAddress.fromBase58(CoinInstance.getInstance(this.ticker).getNetworkParameters(), getAddress()); Script scriptForUTXO = ScriptBuilder.createOutputScript(address); diff --git a/src/main/java/io/cloudchains/app/util/Utility.java b/src/main/java/io/cloudchains/app/util/Utility.java index 911952d..317fcea 100644 --- a/src/main/java/io/cloudchains/app/util/Utility.java +++ b/src/main/java/io/cloudchains/app/util/Utility.java @@ -1,13 +1,13 @@ package io.cloudchains.app.util; -import org.bitcoinj.core.Address; import org.bitcoinj.core.AddressFormatException; +import org.bitcoinj.core.LegacyAddress; import org.bitcoinj.core.NetworkParameters; public class Utility { public static boolean isValidAddress(NetworkParameters params, String address) { try { - Address.fromBase58(params, address); + LegacyAddress.fromBase58(params, address); return true; } catch (AddressFormatException e) { return false; diff --git a/src/main/java/io/cloudchains/app/wallet/WalletHelper.java b/src/main/java/io/cloudchains/app/wallet/WalletHelper.java index 41fe664..2c81c88 100644 --- a/src/main/java/io/cloudchains/app/wallet/WalletHelper.java +++ b/src/main/java/io/cloudchains/app/wallet/WalletHelper.java @@ -175,20 +175,7 @@ public AddressBalance generateAddress() { DeterministicKey key = wallet.freshReceiveKey(); DumpedPrivateKey privateKey = key.getPrivateKeyEncoded(params); - Address address = new Address(params, key.getPubKeyHash()) { - public byte[] getHash() { - return new byte[0]; - } - - public Script.ScriptType getOutputScriptType() { - return null; - } - - - public int compareTo(Address o) { - return 0; - } - }; + LegacyAddress address = LegacyAddress.fromPubKeyHash(params, key.getPubKeyHash()); return new AddressBalance(address, privateKey); } @@ -199,20 +186,7 @@ public AddressBalance generateFromPrivateKey(String privKey) { ECKey key = DumpedPrivateKey.fromBase58(params, privKey).getKey(); DumpedPrivateKey privateKey = key.getPrivateKeyEncoded(params); - Address address = new Address(params, key.getPubKeyHash()) { - public byte[] getHash() { - return new byte[0]; - } - - public Script.ScriptType getOutputScriptType() { - return null; - } - - - public int compareTo(Address o) { - return 0; - } - }; + LegacyAddress address = LegacyAddress.fromPubKeyHash(params, key.getPubKeyHash()); return new AddressBalance(address, privateKey); } @@ -234,14 +208,14 @@ public static Transaction createTransactionSimple(CoinTicker coinTicker, String double totalSpending = amount + fee; double totalAvailable = walletHelper.getSpendBalance(totalSpending); double changeAmt = (totalAvailable - amount) - fee; - Address toAddress = Address.fromBase58(params, address); + LegacyAddress toAddress = LegacyAddress.fromBase58(params, address); Coin sendAmount = Coin.valueOf((long) Math.floor(amount * Coin.COIN.value)); Coin changeAmount = Coin.valueOf((long) Math.floor(changeAmt * Coin.COIN.value)); Transaction tx = new Transaction(params); if (isP2SHAddress(coinInstance, address)) { - Script p2shScript = ScriptBuilder.createP2SHOutputScript(toAddress.getHash160()); + Script p2shScript = ScriptBuilder.createP2SHOutputScript(toAddress.getHash()); tx.addOutput(sendAmount, p2shScript); } else { tx.addOutput(sendAmount, toAddress); @@ -277,15 +251,6 @@ public static void setAsSpent(CoinTicker coinTicker, Transaction transaction, bo private static boolean isP2SHAddress(CoinInstance coin, String address) { byte[] versionAndDataBytes = Base58.decodeChecked(address); int version = versionAndDataBytes[0] & 0xFF; - - if (coin.getNetworkParameters().getAcceptableAddressCodes().length > 2) { - for (int t : coin.getNetworkParameters().getAcceptableAddressCodes()) { - if (coin.getNetworkParameters().getAddressHeader() != t && t == version) { - return true; - } - } - } - return coin.getNetworkParameters().getP2SHHeader() == version; } } From 083c9128f05dd424234d942d7ad32dc43947134b Mon Sep 17 00:00:00 2001 From: tryiou Date: Sat, 4 Apr 2026 16:34:19 +0200 Subject: [PATCH 32/73] fix(core): fix coin lifecycle, config resilience, and transaction broadcast ordering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CoinInstance: Fix getInstance() returning duplicates; pass null userMnemonic on re-login; processUtxos null guard + dedup; setRpcPort returns boolean with range validation; derive Ravencoin relay fee from network params - ConfigHelper: Thread-safe synchronized getters/setters; resilient loading with per-key defaults; skip write if unchanged; mkdirs(); UTF-8 charset; try-with-resources - HTTPServerHandler: Move setAsSpent after successful broadcast; null transaction guard; parse structured RPC error objects; RuntimeException for tx parsing - WalletHelper: Return null on insufficient funds; extract signTransactionWithUtxos; reuse pre-selected UTXOs instead of double coinSelector - BackgroundTimerThread: Disable getAllFees() — remote relayfee data incorrect --- .../io/cloudchains/app/net/CoinInstance.java | 130 ++++++----- .../api/http/server/HTTPServerHandler.java | 86 +++++--- .../io/cloudchains/app/util/ConfigHelper.java | 201 ++++++++++++------ .../background/BackgroundTimerThread.java | 7 +- .../cloudchains/app/wallet/WalletHelper.java | 65 ++++-- 5 files changed, 325 insertions(+), 164 deletions(-) diff --git a/src/main/java/io/cloudchains/app/net/CoinInstance.java b/src/main/java/io/cloudchains/app/net/CoinInstance.java index 8a1e21f..655188d 100644 --- a/src/main/java/io/cloudchains/app/net/CoinInstance.java +++ b/src/main/java/io/cloudchains/app/net/CoinInstance.java @@ -113,9 +113,9 @@ public String getMessage() { private AddressDiscoveryService discoveryService = null; private static boolean addressDiscoveryEnabled = true; - private CoinInstance(CoinTicker ticker) { + private CoinInstance(CoinTicker ticker, ConfigHelper configHelper) { this.ticker = ticker; - this.configHelper = new ConfigHelper(CoinTickerUtils.tickerToString(getTicker())); + this.configHelper = configHelper; addBlockCount(ticker, 0); @@ -244,8 +244,14 @@ public static CoinTicker getActiveBlocknetNetwork() { } public static CoinInstance getInstance(CoinTicker ticker) { + CoinInstance existing = getInstanceByTicker(ticker); + if (existing != null) { + return existing; + } + + ConfigHelper cfg = null; if (ticker != CoinTicker.BLOCKNET) { - ConfigHelper cfg = new ConfigHelper(CoinTickerUtils.tickerToString(ticker)); + cfg = new ConfigHelper(CoinTickerUtils.tickerToString(ticker)); if (!cfg.isRpcEnabled()) { return null; } @@ -255,7 +261,10 @@ public static CoinInstance getInstance(CoinTicker ticker) { CoinInstance instance = getInstanceByTicker(ticker); if (instance == null) { - instance = new CoinInstance(ticker); + if (cfg == null) { + cfg = new ConfigHelper(CoinTickerUtils.tickerToString(ticker)); + } + instance = new CoinInstance(ticker, cfg); if (ticker == CoinTicker.BLOCKNET) coinInstances.add(0, instance); else @@ -282,7 +291,7 @@ public static CoinInstance getInstance(CoinTicker ticker) { */ public static CoinError changePassword(String oldPassword, String newPassword) { if (!KeyHandler.existsBaseECKeyFromLocal()) { - LOGGER.log(Level.FINER, "[wallet] Unable to change the password: Wallet not found on disk"); + LOGGER.log(Level.WARNING, "[wallet] Unable to change the password: Wallet not found on disk"); return new CoinError("Unable to change the password: Wallet not found on disk", CoinError.CoinErrorCode.CHANGEPASSWORDFAILED); } @@ -292,7 +301,7 @@ public static CoinError changePassword(String oldPassword, String newPassword) { try { List baseSeed = KeyHandler.getBaseSeed(oldPassphrase); if (baseSeed == null) { - LOGGER.log(Level.FINER, "[wallet] Unable to change the password: Incorrect password"); + LOGGER.log(Level.WARNING, "[wallet] Unable to change the password: Incorrect password"); return new CoinError("Unable to change the password: Incorrect password", CoinError.CoinErrorCode.CHANGEPASSWORDFAILED); } @@ -302,7 +311,7 @@ public static CoinError changePassword(String oldPassword, String newPassword) { List mnemonic = seed.getMnemonicCode(); if (!KeyHandler.importFromMnemonic(mnemonic, newPassphrase)) { - LOGGER.log(Level.FINER, "[wallet] Unable to change the password: Failed to create new wallet file"); + LOGGER.log(Level.WARNING, "[wallet] Unable to change the password: Failed to create new wallet file"); return new CoinError("Unable to change the password: Failed to create new wallet file", CoinError.CoinErrorCode.CHANGEPASSWORDFAILED); } @@ -348,7 +357,7 @@ public CoinError init(String pw, String userMnemonic, boolean isMnemonic) { public CoinError init(String pw, String userMnemonic, boolean isMnemonic, boolean xliteRPC) { switch (ticker) { case BLOCKNET: { - LOGGER.log(Level.FINER, "[coin] Initializing for Blocknet main network."); + LOGGER.log(Level.FINE, "[coin] Initializing for Blocknet main network."); blocknetNetworkParameters = new BlocknetNetworkParameters(); networkParameters = blocknetNetworkParameters; hasXRouter = true; @@ -356,7 +365,7 @@ public CoinError init(String pw, String userMnemonic, boolean isMnemonic, boolea break; } case BLOCKNET_TESTNET5: { - LOGGER.log(Level.FINER, "[coin] Initializing for Blocknet test network v5."); + LOGGER.log(Level.FINE, "[coin] Initializing for Blocknet test network v5."); blocknetNetworkParameters = new BlocknetTestnet5NetworkParameters(); networkParameters = blocknetNetworkParameters; hasXRouter = true; @@ -365,43 +374,43 @@ public CoinError init(String pw, String userMnemonic, boolean isMnemonic, boolea break; } case BITCOIN: { - LOGGER.log(Level.FINER, "[coin] Initializing for Bitcoin main network."); + LOGGER.log(Level.FINE, "[coin] Initializing for Bitcoin main network."); networkParameters = MainNetParams.get(); rpcPort = 8332; break; } // case BITCOIN_CASH: { - // LOGGER.log(Level.FINER, "[coin] Initializing for BitcoinCash main network."); + // LOGGER.log(Level.FINE, "[coin] Initializing for BitcoinCash main network."); // networkParameters = new BitcoinCashNetworkParameters(); // rpcPort = 48332; // break; // } case LITECOIN: { - LOGGER.log(Level.FINER, "[coin] Initializing for Litecoin main network."); + LOGGER.log(Level.FINE, "[coin] Initializing for Litecoin main network."); networkParameters = new LitecoinNetworkParameters(); rpcPort = 9332; break; } case DASHCOIN: { - LOGGER.log(Level.FINER, "[coin] Initializing for Dashcoin main network."); + LOGGER.log(Level.FINE, "[coin] Initializing for Dashcoin main network."); networkParameters = new DashcoinNetworkParameters(); rpcPort = 9998; break; } case DIGIBYTE: { - LOGGER.log(Level.FINER, "[coin] Initializing for Digibyte main network."); + LOGGER.log(Level.FINE, "[coin] Initializing for Digibyte main network."); networkParameters = new DigibyteNetworkParameters(); rpcPort = 14022; break; } case DOGECOIN: { - LOGGER.log(Level.FINER, "[coin] Initializing for Dogecoin main network."); + LOGGER.log(Level.FINE, "[coin] Initializing for Dogecoin main network."); networkParameters = new DogecoinNetworkParameters(); rpcPort = 22555; break; } case SYSCOIN: { - LOGGER.log(Level.FINER, "[coin] Initializing for Syscoin main network."); + LOGGER.log(Level.FINE, "[coin] Initializing for Syscoin main network."); networkParameters = new SyscoinNetworkParameters(); rpcPort = 8370; break; @@ -417,57 +426,65 @@ public CoinError init(String pw, String userMnemonic, boolean isMnemonic, boolea // break; // } case PIVX: { - LOGGER.log(Level.FINER, "[coin] Initializing for Pivx main network."); + LOGGER.log(Level.FINE, "[coin] Initializing for Pivx main network."); networkParameters = new PivxNetworkParameters(); rpcPort = 9951; break; } case UNOBTANIUM: { - LOGGER.log(Level.FINER, "[coin] Initializing for Unobtanium main network."); + LOGGER.log(Level.FINE, "[coin] Initializing for Unobtanium main network."); networkParameters = new UnobtaniumNetworkParameters(); rpcPort = 65111; break; } case PKOIN: { - LOGGER.log(Level.FINER, "[coin] Initializing for Pocketcoin main network."); + LOGGER.log(Level.FINE, "[coin] Initializing for Pocketcoin main network."); networkParameters = new PocketcoinNetworkParameters(); rpcPort = 37071; break; } // case ALQOCOIN: { - // LOGGER.log(Level.FINER, "[coin] Initializing for Alqo main network."); + // LOGGER.log(Level.FINE, "[coin] Initializing for Alqo main network."); // networkParameters = new AlqocoinNetworkParameters(); // rpcPort = 55000; // break; // } // case POLISCOIN: { - // LOGGER.log(Level.FINER, "[coin] Initializing for Polis main network."); + // LOGGER.log(Level.FINE, "[coin] Initializing for Polis main network."); // networkParameters = new PoliscoinNetworkParameters(); // rpcPort = 24127; // break; // } // case PHORECOIN: { - // LOGGER.log(Level.FINER, "[coin] Initializing for Phore main network."); + // LOGGER.log(Level.FINE, "[coin] Initializing for Phore main network."); // networkParameters = new PhorecoinNetworkParameters(); // rpcPort = 11772; // break; // } case RAVENCOIN: { - LOGGER.log(Level.FINER, "[coin] Initializing for Ravencoin main network."); - networkParameters = new RavencoinNetworkParameters(); + LOGGER.log(Level.FINE, "[coin] Initializing for Ravencoin main network."); + RavencoinNetworkParameters rvnParams = new RavencoinNetworkParameters(); + networkParameters = rvnParams; rpcPort = 8766; + Coin minFee = rvnParams.getMinRelayTxFee(); + LOGGER.log(Level.FINE, "[coin] " + ticker + " getMinRelayTxFee: " + minFee.value + " satoshis"); + configHelper.setFee(minFee.value / (double) Coin.COIN.value); break; } default: { - LOGGER.log(Level.FINER, "[coin] ERROR: Invalid/unsupported network: " + ticker.toString()); + LOGGER.log(Level.FINE, "[coin] ERROR: Invalid/unsupported network: " + ticker.toString()); return new CoinError("Unsupported coin", CoinError.CoinErrorCode.UNSUPPORTEDCOIN); } } + configHelper.writeConfig(); + if (xliteRPC) { rpcPort = rpcPort + 1; - configHelper.setRpcPort(rpcPort); + if (!configHelper.setRpcPort(rpcPort)) { + LOGGER.log(Level.WARNING, "[coin] Failed to allocate RPC port, skipping RPC config"); + } configHelper.writeConfig(); } @@ -488,7 +505,7 @@ public CoinError init(String pw, String userMnemonic, boolean isMnemonic, boolea char[] importPassphrase = pw.toCharArray(); try { if (!KeyHandler.importFromMnemonic(Arrays.asList(userMnemonic.split(" ")), importPassphrase)) { - LOGGER.log(Level.FINER, "[wallet] Unable to create wallet from mnemonic"); + LOGGER.log(Level.WARNING, "[wallet] Unable to create wallet from mnemonic"); return new CoinError("Unable to create wallet from mnemonic", CoinError.CoinErrorCode.BADMNEMONIC); } } finally { @@ -505,7 +522,7 @@ public CoinError init(String pw, String userMnemonic, boolean isMnemonic, boolea } if (baseSeed == null) { - LOGGER.log(Level.FINER, "[wallet] Possible Bad password: Unable to import or create base seed!"); + LOGGER.log(Level.WARNING, "[wallet] Possible Bad password: Unable to import or create base seed!"); return new CoinError("Bad password", CoinError.CoinErrorCode.BADPASSWORD); } @@ -520,17 +537,19 @@ public CoinError init(String pw, String userMnemonic, boolean isMnemonic, boolea // RUN ADDRESS DISCOVERY ONLY DURING WALLET INITIALIZATION // This ensures discovery runs once at wallet startup in ANY case if (addressDiscoveryEnabled) { - LOGGER.log(Level.FINER, "[coinAddressDiscoveryService created] Running address discovery"); + LOGGER.log(Level.FINE, "[coinAddressDiscoveryService created] Running address discovery"); runAddressDiscovery(); } else { - LOGGER.log(Level.FINER, "[coin] Address discovery disabled"); + LOGGER.log(Level.FINE, "[coin] Address discovery disabled"); } // Make sure wallet addresses are available generateForwardAddresses(true); if (configHelper.getRpcPort() == -1000) { - configHelper.setRpcPort(rpcPort); + if (!configHelper.setRpcPort(rpcPort)) { + LOGGER.log(Level.WARNING, "[coin] Failed to allocate RPC port, RPC server will not start"); + } configHelper.writeConfig(); } else { rpcPort = configHelper.getRpcPort(); @@ -550,9 +569,9 @@ public CoinError init(String pw, String userMnemonic, boolean isMnemonic, boolea // if (isBlocknetNetwork() && hasXRouter()) { // XRouterMessageSerializer xRouterMessageSerializer = (getBlocknetNetworkParameters()).getXRouterMessageSerializer(false); // xRouterPacketManager = new XRouterPacketManager(xRouterMessageSerializer, blocknetNetworkParameters); -// LOGGER.log(Level.FINER, "[coin] This network is a Blocknet network and supports XRouter. Our packet version is " + Integer.toString(XRouterPacketManager.getXRouterPacketVersion(), 16)); +// LOGGER.log(Level.FINE, "[coin] This network is a Blocknet network and supports XRouter. Our packet version is " + Integer.toString(XRouterPacketManager.getXRouterPacketVersion(), 16)); // } else { -// LOGGER.log(Level.FINER, "[coin] WARNING: This network (" + CoinTickerUtils.tickerToString(getTicker()) + ") does not support XRouter."); +// LOGGER.log(Level.FINE, "[coin] WARNING: This network (" + CoinTickerUtils.tickerToString(getTicker()) + ") does not support XRouter."); // } // // if (isBlocknetNetwork()) { @@ -566,7 +585,7 @@ public CoinError init(String pw, String userMnemonic, boolean isMnemonic, boolea // try { // chain = new BlockChain(networkParameters, getWallet(), new SPVBlockStore(networkParameters, spvDat)); // } catch (BlockStoreException ex) { -// LOGGER.log(Level.FINER, "Error while initializing blockchain object!"); +// LOGGER.log(Level.WARNING, "Error while initializing blockchain object!"); // ex.printStackTrace(); // return false; // } @@ -575,7 +594,7 @@ public CoinError init(String pw, String userMnemonic, boolean isMnemonic, boolea // LOGGER.log(Level.INFO, "[coin] Connecting to the (" + getTicker().toString() + ") network."); // // if (getAddressKeyPairs().size() == 0) { -// LOGGER.log(Level.FINER, "[peer] Have no addresses. Generating forward addresses."); +// LOGGER.log(Level.FINE, "[peer] Have no addresses. Generating forward addresses."); // // generateForwardAddresses(true); // } @@ -599,7 +618,7 @@ private void generateForwardAddresses(boolean fromStartup) { updateConfig = true; } - LOGGER.log(Level.FINER, "[wallet] Generating " + configAddressCount + " forward addresses for network " + getTicker().toString() + "."); + LOGGER.log(Level.FINE, "[wallet] Generating " + configAddressCount + " forward addresses for network " + getTicker().toString() + "."); // Ensure that internal HD wallet pointer matches the count we're expecting. // Required because wallet doesn't remember last HD wallet address prior to @@ -628,7 +647,7 @@ private void connectToBlocknetNetwork() { return; } - LOGGER.log(Level.FINER, "[coin] This network is connecting/connected."); + LOGGER.log(Level.FINE, "[coin] This network is connecting/connected."); } public Wallet getWallet() { @@ -675,7 +694,7 @@ public void sendXrGetBlockCount(BlocknetPeer blocknetPeer) { public void sendXrGetUtxos(BlocknetPeer blocknetPeer) { if (System.currentTimeMillis() - lastUtxoUpdate < MINIMUM_UTXO_UPDATE_INTERVAL) { - LOGGER.log(Level.FINER, "[coin] Aborting UTXO checking as the list was updated less than 1 second ago."); + LOGGER.log(Level.FINE, "[coin] Aborting UTXO checking as the list was updated less than 1 second ago."); return; } @@ -697,7 +716,7 @@ public String sendXrMessage(BlocknetPeer blocknetPeer, String uuid, String comma XRouterMessage message = null; if (blocknetPeer == null || !blocknetPeer.getHaveConfig().get()) { - LOGGER.log(Level.FINER, "[sendXrMessage] Config not received yet"); + LOGGER.log(Level.FINE, "[sendXrMessage] Config not received yet"); return null; } @@ -795,7 +814,7 @@ public String sendXrMessage(BlocknetPeer blocknetPeer, String uuid, String comma break; } default: { - LOGGER.log(Level.FINER, "[coin] ERROR: Unknown XRouter Message! Command: " + command); + LOGGER.log(Level.FINE, "[coin] ERROR: Unknown XRouter Message! Command: " + command); uuid = null; break; } @@ -962,29 +981,39 @@ public void addCloudTransaction(CloudTransaction cloudTransaction) { } public void processUtxos(List utxoList) { - // first lets clear UTXOs out of each address + if (utxoList == null) { + LOGGER.log(Level.WARNING, "[coin-" + CoinTickerUtils.tickerToString(getTicker()) + "] processUtxos: null UTXO list received"); + return; + } + LOGGER.log(Level.FINE, "[coin-" + CoinTickerUtils.tickerToString(getTicker()) + "] processUtxos: remote returned " + utxoList.size() + " UTXOs, tracking " + addressKeyPairs.size() + " addresses locally"); + int added = 0, skipped = 0; + + Set clearedAddresses = new HashSet<>(); for (UTXO utxo : utxoList) { - AddressBalance addressBalance = getAddress(utxo.getAddress()); - addressBalance.clearUtxos(); + String addr = utxo.getAddress(); + if (clearedAddresses.add(addr)) { + AddressBalance addressBalance = getAddress(addr); + if (addressBalance != null) { + addressBalance.clearUtxos(); + } + } } - // now let's add them back for (UTXO utxo : utxoList) { AddressBalance addressBalance = getAddress(utxo.getAddress()); - if (addressBalance == null) { - LOGGER.log(Level.FINER, "[utxo-parser] Warning: Encountered non-tracked address in reply: " + utxo.getAddress()); + LOGGER.log(Level.WARNING, "[utxo-parser] Warning: Encountered non-tracked address in reply: " + utxo.getAddress()); + skipped++; continue; } - boolean isNewUtxo = addressBalance.addUtxo(utxo); - if (isNewUtxo) { + added++; addCloudTransaction(new CloudTransaction(utxo)); LOGGER.log(Level.FINER, "[utxo-parser] Added new UTXO, address: " + utxo.getAddress() + " value: " + utxo.getAmount()); } } - + LOGGER.log(Level.FINE, "[coin-" + CoinTickerUtils.tickerToString(getTicker()) + "] processUtxos: added=" + added + ", skipped=" + skipped); setLastUtxoUpdate(System.currentTimeMillis()); } @@ -1075,10 +1104,11 @@ public void runAddressDiscovery() { if (discoveryService == null) { discoveryService = new AddressDiscoveryService(this); - LOGGER.log(Level.FINER, "[coin-" + currency + "] AddressDiscoveryService created"); + LOGGER.log(Level.FINE, "[coin-" + currency + "] AddressDiscoveryService created"); } int discoveredCount = discoveryService.discoverAddressCount(); + discoveryService.clearExternalChainKey(); int currentCount = configHelper.getAddressCount(); if (discoveredCount > currentCount) { diff --git a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java b/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java index e139b24..9940f92 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java +++ b/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java @@ -200,9 +200,9 @@ protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) response.add("error", errorParsingJSON); response.add("result", JsonNull.INSTANCE); if (e instanceof IllegalArgumentException) { - LOGGER.log(Level.FINER, "[http-server-handler] WARNING: Client sent valid JSON, but did not specify method and/or parameters!"); + LOGGER.log(Level.WARNING, "[http-server-handler] WARNING: Client sent valid JSON, but did not specify method and/or parameters!"); } else { - LOGGER.log(Level.FINER, "[http-server-handler] WARNING: Client sent invalid JSON!"); + LOGGER.log(Level.WARNING, "[http-server-handler] WARNING: Client sent invalid JSON!"); } status = HttpResponseStatus.BAD_REQUEST; } @@ -412,15 +412,14 @@ private JsonObject getResponse(String method, JsonArray params) { try { rawTx = params.get(0).getAsString(); transaction = new Transaction(coin.getNetworkParameters(), Hex.decode(rawTx)); - WalletHelper.setAsSpent(coin.getTicker(), transaction, true); - } catch (JsonParseException e) { + } catch (RuntimeException e) { response.add("result", JsonNull.INSTANCE); JsonObject errorJSON = new JsonObject(); errorJSON.addProperty("code", -1); - errorJSON.addProperty("message", "Error parsing JSON!"); + errorJSON.addProperty("message", "Error parsing transaction!"); response.add("error", errorJSON); - LOGGER.log(Level.WARNING, "[http-server-handler] Error parsing JSON in sendrawtransaction for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); + LOGGER.log(Level.WARNING, "[http-server-handler] Error parsing transaction in sendrawtransaction for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); break; } @@ -440,6 +439,8 @@ private JsonObject getResponse(String method, JsonArray params) { break; } + WalletHelper.setAsSpent(coin.getTicker(), transaction, true); + if (txid.has("result")) { response.add("result", txid.get("result")); response.add("error", JsonNull.INSTANCE); @@ -703,7 +704,7 @@ private JsonObject getResponse(String method, JsonArray params) { throw new JsonParseException("Invalid outputs format"); } } catch (JsonParseException e) { - LOGGER.log(Level.FINER, + LOGGER.log(Level.WARNING, "[http-server-handler] ERROR: Error while parsing JSON for createrawtransaction!"); response.add("result", JsonNull.INSTANCE); JsonObject errorJSON = new JsonObject(); @@ -726,7 +727,7 @@ private JsonObject getResponse(String method, JsonArray params) { int vout = input.get("vout").getAsInt(); tx.addInput(Sha256Hash.wrap(txid), vout, ScriptBuilder.createInputScript(null)); } catch (Exception e) { - LOGGER.log(Level.FINER, + LOGGER.log(Level.WARNING, "[http-server-handler] ERROR: Error while constructing transaction (input phase)!"); txConstructionError(response, e, "Error while constructing transaction (input phase)"); inputSuccess = false; @@ -744,12 +745,12 @@ private JsonObject getResponse(String method, JsonArray params) { LegacyAddress address = LegacyAddress.fromBase58(coin.getNetworkParameters(), entry.address); Coin outputValue = Coin.valueOf((long) Math.floor(entry.amount * Coin.COIN.value)); if (isP2SHAddress(entry.address)) { - LOGGER.log(Level.FINER, "[http-server-handler] P2SH Address Found: " + entry.address); + LOGGER.log(Level.FINE, "[http-server-handler] P2SH Address Found: " + entry.address); Script p2shScript = ScriptBuilder.createP2SHOutputScript(address.getHash()); tx.addOutput(outputValue, p2shScript); } } catch (Exception e) { - LOGGER.log(Level.FINER, + LOGGER.log(Level.WARNING, "[http-server-handler] ERROR: Error while constructing transaction (output phase)!"); LOGGER.log(Level.WARNING, "[http-server-handler] Error in createrawtransaction P2SH output phase for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); txConstructionError(response, e, "Error while constructing transaction (output phase)"); @@ -765,7 +766,7 @@ private JsonObject getResponse(String method, JsonArray params) { tx.addOutput(outputValue, address); } } catch (Exception e) { - LOGGER.log(Level.FINER, + LOGGER.log(Level.WARNING, "[http-server-handler] ERROR: Error while constructing transaction (output phase)!"); LOGGER.log(Level.WARNING, "[http-server-handler] Error in createrawtransaction output phase for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); txConstructionError(response, e, "Error while constructing transaction (output phase)"); @@ -828,7 +829,7 @@ private JsonObject getResponse(String method, JsonArray params) { vin.add(thisVin); } catch (Exception e) { - LOGGER.log(Level.FINER, "[http-server-handler] ERROR: Error while parsing transaction inputs!"); + LOGGER.log(Level.WARNING, "[http-server-handler] ERROR: Error while parsing transaction inputs!"); LOGGER.log(Level.WARNING, "[http-server-handler] Error parsing transaction inputs for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); response.add("result", JsonNull.INSTANCE); @@ -877,7 +878,7 @@ private JsonObject getResponse(String method, JsonArray params) { vout.add(thisVout); } catch (Exception e) { - LOGGER.log(Level.FINER, "[http-server-handler] ERROR: Error while parsing transaction outputs!"); + LOGGER.log(Level.WARNING, "[http-server-handler] ERROR: Error while parsing transaction outputs!"); LOGGER.log(Level.WARNING, "[http-server-handler] Error parsing transaction outputs for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); response.add("result", JsonNull.INSTANCE); @@ -990,7 +991,7 @@ private JsonObject getResponse(String method, JsonArray params) { UTXO requested = this.getUtxo(Sha256Hash.wrap(txid), n); if (requested != null) { - LOGGER.log(Level.FINER, "[http-server-handler] Using cached UTXO for gettxout"); + LOGGER.log(Level.FINE, "[http-server-handler] Using cached UTXO for gettxout"); org.bitcoinj.core.UTXO utxo = requested.createUTXO(); JsonObject resultJSON = new JsonObject(); @@ -1020,7 +1021,7 @@ private JsonObject getResponse(String method, JsonArray params) { } if (!includeMempool) { - LOGGER.log(Level.FINER, "[http-server-handler] WARNING: Client requested UTXO that is not ours!"); + LOGGER.log(Level.WARNING, "[http-server-handler] WARNING: Client requested UTXO that is not ours!"); response.add("result", JsonNull.INSTANCE); JsonObject errorJSON = new JsonObject(); @@ -1099,7 +1100,7 @@ private JsonObject getResponse(String method, JsonArray params) { } if (!isOurs) { - LOGGER.log(Level.FINER, "[http-server-handler] WARNING: Client requested UTXO that cannot be ours!"); + LOGGER.log(Level.WARNING, "[http-server-handler] WARNING: Client requested UTXO that cannot be ours!"); response.add("result", JsonNull.INSTANCE); JsonObject errorJSON = new JsonObject(); @@ -1133,7 +1134,7 @@ private JsonObject getResponse(String method, JsonArray params) { } if (!unspent) { - LOGGER.log(Level.FINER, "[http-server-handler] WARNING: Client requested UTXO that was already spent!"); + LOGGER.log(Level.WARNING, "[http-server-handler] WARNING: Client requested UTXO that was already spent!"); response.add("result", JsonNull.INSTANCE); JsonObject errorJSON = new JsonObject(); @@ -1154,7 +1155,7 @@ private JsonObject getResponse(String method, JsonArray params) { response.add("result", resultJSON); response.add("error", JsonNull.INSTANCE); } catch (Exception e) { - LOGGER.log(Level.FINER, "[http-server-handler] ERROR: Error while parsing transaction!"); + LOGGER.log(Level.WARNING, "[http-server-handler] ERROR: Error while parsing transaction!"); LOGGER.log(Level.WARNING, "[http-server-handler] Error parsing transaction in gettxout for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); response.add("result", JsonNull.INSTANCE); @@ -1284,11 +1285,11 @@ private JsonObject getResponse(String method, JsonArray params) { String derivedAddr = LegacyAddress.fromKey(coin.getNetworkParameters(), key).toBase58(); if (!addr.equals(derivedAddr)) { - LOGGER.log(Level.FINER, "[http-server-handler] ERROR: Addresses do not match! Failing."); + LOGGER.log(Level.WARNING, "[http-server-handler] ERROR: Addresses do not match! Failing."); verified = false; } } catch (Exception e) { - LOGGER.log(Level.FINER, "[http-server-handler] Error while verifying signature! Invalid signature?"); + LOGGER.log(Level.WARNING, "[http-server-handler] Error while verifying signature! Invalid signature?"); LOGGER.log(Level.WARNING, "[http-server-handler] Error verifying message for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); response.addProperty("result", verified); @@ -1316,7 +1317,7 @@ private JsonObject getResponse(String method, JsonArray params) { try { address = params.get(0).getAsString(); amount = params.get(1).getAsDouble(); - } catch (JsonParseException e) { + } catch (RuntimeException e) { response.add("result", JsonNull.INSTANCE); JsonObject errorJSON = new JsonObject(); errorJSON.addProperty("code", -1); @@ -1330,34 +1331,53 @@ private JsonObject getResponse(String method, JsonArray params) { Transaction transaction; try { transaction = WalletHelper.createTransactionSimple(coin.getTicker(), address, amount); - WalletHelper.setAsSpent(coin.getTicker(), transaction, true); - } catch (JsonParseException e) { + if (transaction == null) { + response.add("result", JsonNull.INSTANCE); + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", -6); + errorJSON.addProperty("message", "Failed to create transaction"); + response.add("error", errorJSON); + break; + } + } catch (RuntimeException e) { response.add("result", JsonNull.INSTANCE); JsonObject errorJSON = new JsonObject(); - errorJSON.addProperty("code", -1); - errorJSON.addProperty("message", "Error while creating transaction!"); + errorJSON.addProperty("code", -6); + errorJSON.addProperty("message", e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName()); response.add("error", errorJSON); LOGGER.log(Level.WARNING, "[http-server-handler] Error creating transaction in sendtransaction for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); break; } - JsonObject txid = httpClient.sendRawTransaction(coin.getTicker(), new String(Hex.encode(transaction.bitcoinSerialize()))); + String rawTxHex = new String(Hex.encode(transaction.bitcoinSerialize())); + LOGGER.log(Level.FINE, "[http-server-handler] sendtransaction: rawTx size=" + rawTxHex.length() + " bytes"); + LOGGER.log(Level.FINER, "[http-server-handler] sendtransaction: rawTx hex=" + rawTxHex); + JsonObject txid = httpClient.sendRawTransaction(coin.getTicker(), rawTxHex); if (txid == null || txid.has("error") && !txid.get("error").isJsonNull()) { int code = -1; - - if (txid != null) - code = txid.get("error").getAsInt(); - + String errorMsg = "Error sending transaction!"; + if (txid != null) { + JsonElement errorNode = txid.get("error"); + if (errorNode.isJsonPrimitive()) { + code = errorNode.getAsInt(); + } else if (errorNode.isJsonObject()) { + JsonObject errObj = errorNode.getAsJsonObject(); + if (errObj.has("code")) code = errObj.get("code").getAsInt(); + if (errObj.has("message")) errorMsg = errObj.get("message").getAsString(); + } + } + LOGGER.log(Level.WARNING, "[http-server-handler] sendrawtransaction failed for " + CoinTickerUtils.tickerToString(coin.getTicker()) + " code=" + code + " message=" + errorMsg); response.add("result", JsonNull.INSTANCE); JsonObject errorJSON = new JsonObject(); errorJSON.addProperty("code", code); - errorJSON.addProperty("message", "Error sending transaction!"); + errorJSON.addProperty("message", errorMsg); response.add("error", errorJSON); - break; } + WalletHelper.setAsSpent(coin.getTicker(), transaction, true); + if (txid.has("result")) { response.add("result", txid.get("result")); response.add("error", JsonNull.INSTANCE); @@ -1528,7 +1548,7 @@ private byte[] formatMessageForSigning(String message) { header = "Syscoin Signed Message:\n"; break; default: - LOGGER.log(Level.FINER, "[http-server-handler] ERROR: Unsupported coin. This should never happen."); + LOGGER.log(Level.WARNING, "[http-server-handler] ERROR: Unsupported coin. This should never happen."); break; } diff --git a/src/main/java/io/cloudchains/app/util/ConfigHelper.java b/src/main/java/io/cloudchains/app/util/ConfigHelper.java index e615dde..905511b 100644 --- a/src/main/java/io/cloudchains/app/util/ConfigHelper.java +++ b/src/main/java/io/cloudchains/app/util/ConfigHelper.java @@ -7,6 +7,7 @@ import java.io.File; import java.io.FileWriter; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.logging.Level; import java.util.logging.LogManager; @@ -18,7 +19,6 @@ public class ConfigHelper { private String tickerStr; private File file; - private FileWriter fileWriter; private double fee; private boolean feeFlat; @@ -32,7 +32,7 @@ public class ConfigHelper { public static String CONFIG_DIR = ""; // Must not end with [/], e.g. /home/user/.config, not /home/user/.config/ public ConfigHelper(String tickerStr) { - this.tickerStr = tickerStr; + this.tickerStr = Preconditions.checkNotNull(tickerStr, "tickerStr must not be null"); try { file = Preconditions.checkNotNull(this.getFile()); @@ -42,20 +42,16 @@ public ConfigHelper(String tickerStr) { } } - public void loadConfig() { + public synchronized void loadConfig() { try { - String rawConfig = new String(Files.readAllBytes(file.toPath())); + String rawConfig = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); if (rawConfig.isEmpty()) { fee = 0.0001; feeFlat = true; rpcEnabled = false; rpcUsername = ""; rpcPassword = ""; - if (this.tickerStr.equalsIgnoreCase("master")) { - rpcPort = 9955; - } else { - rpcPort = -1000; - } + rpcPort = defaultRpcPort(); addressCount = 0; writeConfig(); @@ -76,23 +72,73 @@ public void loadConfig() { for (String configKey : configKeys) { if (!config.has(configKey)) { - LOGGER.log(Level.FINER, "[config] Warning: Configuration file does not contain required value '" + configKey + "'. This will probably break things later on."); + LOGGER.log(Level.FINER, "[config] Missing config key '" + configKey + "' for " + tickerStr + ", will use default"); + } + } + + boolean needsWrite = false; + + if (!config.has("fee")) { + fee = 0.0001; + needsWrite = true; + } else { + fee = config.getDouble("fee"); + LOGGER.log(Level.FINE, "[config] " + tickerStr + " fee from config: " + fee); + if (fee <= 0) { + fee = 0.0001; + needsWrite = true; } } - fee = config.getDouble("fee"); - feeFlat = config.getBoolean("feeFlat"); - rpcEnabled = config.getBoolean("rpcEnabled"); - rpcUsername = config.getString("rpcUsername"); - rpcPassword = config.getString("rpcPassword"); - rpcPort = config.getInt("rpcPort"); + if (!config.has("feeFlat")) { + feeFlat = true; + needsWrite = true; + } else { + feeFlat = config.getBoolean("feeFlat"); + } + + if (!config.has("rpcEnabled")) { + rpcEnabled = false; + needsWrite = true; + } else { + rpcEnabled = config.getBoolean("rpcEnabled"); + } + + if (!config.has("rpcUsername")) { + rpcUsername = ""; + needsWrite = true; + } else { + rpcUsername = config.getString("rpcUsername"); + } + + if (!config.has("rpcPassword")) { + rpcPassword = ""; + needsWrite = true; + } else { + rpcPassword = config.getString("rpcPassword"); + } + + if (!config.has("rpcPort")) { + rpcPort = defaultRpcPort(); + needsWrite = true; + } else { + rpcPort = config.getInt("rpcPort"); + if (rpcPort == 0) { + rpcPort = -1000; + needsWrite = true; + } + } if (!config.has("addressCount")) { setAddressCount(0); - writeConfig(); + needsWrite = true; } else { addressCount = config.getInt("addressCount"); } + + if (needsWrite) { + writeConfig(); + } } catch (Exception e) { LOGGER.log(Level.WARNING, "[config] Error reading config file for " + tickerStr, e); } @@ -122,120 +168,139 @@ private File getFile() { return configFile; } - public void setFee(double fee) { + public synchronized void setFee(double fee) { this.fee = fee; } - public void setFlatFee(boolean flat) { + public synchronized void setFlatFee(boolean flat) { this.feeFlat = flat; } - public void setRpcEnabled(boolean isEnabled) { + public synchronized void setRpcEnabled(boolean isEnabled) { this.rpcEnabled = isEnabled; } - public void setRpcUsername(String user) { + public synchronized void setRpcUsername(String user) { this.rpcUsername = user; } - public void setRpcPassword(String pass) { + public synchronized void setRpcPassword(String pass) { this.rpcPassword = pass; } - public void setRpcPort(int rpcPort) { - if (PortCheck.available(rpcPort)) - this.rpcPort = rpcPort; - else - setRpcPort(rpcPort + 1); + public synchronized boolean setRpcPort(int rpcPort) { + if (rpcPort < 1 || rpcPort > 65535) { + LOGGER.log(Level.WARNING, "[config] Invalid port " + rpcPort + ", must be 1-65535"); + return false; + } + int maxAttempts = 100; + for (int i = 0; i < maxAttempts && rpcPort + i <= 65535; i++) { + if (PortCheck.available(rpcPort + i)) { + this.rpcPort = rpcPort + i; + return true; + } + } + LOGGER.log(Level.WARNING, "[config] No available port in range " + rpcPort + "-" + Math.min(rpcPort + maxAttempts - 1, 65535)); + return false; } - public void setAddressCount(int addressCount) { + public synchronized void setAddressCount(int addressCount) { this.addressCount = addressCount; } - public double getFee() { + public synchronized double getFee() { return fee; } - public boolean isFlatFee() { + public synchronized boolean isFlatFee() { return feeFlat; } - public boolean isRpcEnabled() { + public synchronized boolean isRpcEnabled() { return rpcEnabled; } - public String getRpcUsername() { + public synchronized String getRpcUsername() { return rpcUsername; } - public String getRpcPassword() { + public synchronized String getRpcPassword() { return rpcPassword; } - public int getMasterRpcPort() { + private int defaultRpcPort() { + return this.tickerStr.equalsIgnoreCase("master") ? 9955 : -1000; + } + + public synchronized int getMasterRpcPort() { if (rpcPort == -1000) { - rpcPort = 9955; + return 9955; } - return rpcPort; } - public int getRpcPort() { + public synchronized int getRpcPort() { return rpcPort; } - public int getAddressCount() { + public synchronized int getAddressCount() { return addressCount; } - public boolean validAuth() { - return rpcUsername != null && !rpcUsername.equals("") && rpcPassword != null && !rpcPassword.equals(""); + private JSONObject toConfigJson() { + JSONObject config = new JSONObject(); + config.put("fee", fee); + config.put("feeFlat", feeFlat); + config.put("rpcEnabled", rpcEnabled); + config.put("rpcUsername", rpcUsername); + config.put("rpcPassword", rpcPassword); + config.put("rpcPort", rpcPort); + config.put("addressCount", addressCount); + return config; + } + + public synchronized boolean validAuth() { + return rpcUsername != null && !rpcUsername.isEmpty() && rpcPassword != null && !rpcPassword.isEmpty(); } - public void writeConfig() { + public synchronized void writeConfig() { try { - fileWriter = new FileWriter(file, false); - - JSONObject config = new JSONObject(); - config.put("fee", fee); - config.put("feeFlat", feeFlat); - config.put("rpcEnabled", rpcEnabled); - config.put("rpcUsername", rpcUsername); - config.put("rpcPassword", rpcPassword); - config.put("rpcPort", rpcPort); - config.put("addressCount", addressCount); - - fileWriter.write(config.toString(4)); - fileWriter.flush(); - fileWriter.close(); + String newContent = toConfigJson().toString(4); + + if (file.exists()) { + String existingContent = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); + if (existingContent.equals(newContent)) { + return; + } + } + + try (FileWriter fw = new FileWriter(file, false)) { + fw.write(newContent); + } } catch (IOException e) { LOGGER.log(Level.WARNING, "[config] IOException writing config for " + tickerStr, e); } } public static String getLocalDataDirectory() { - String userHomeDir; + String baseDir; if (CONFIG_DIR.isEmpty()) { - String OS = (System.getProperty("os.name")).toLowerCase(); - - if (OS.contains("win")) { - userHomeDir = App.getEnv("AppData"); - } else if (OS.contains("nix") || OS.contains("nux") || OS.contains("aix")) { - userHomeDir = System.getProperty("user.home") + File.separator + ".config"; - } else if (OS.contains("mac")) { - userHomeDir = System.getProperty("user.home") + File.separator + "Library" + File.separator + "Application Support"; + String os = System.getProperty("os.name").toLowerCase(); + if (os.contains("win")) { + baseDir = App.getEnv("AppData"); + } else if (os.contains("mac")) { + baseDir = System.getProperty("user.home") + File.separator + "Library" + File.separator + "Application Support"; } else { - userHomeDir = System.getProperty("user.home") + File.separator + ".config"; + baseDir = System.getProperty("user.home") + File.separator + ".config"; } - userHomeDir += File.separator + "CloudChains" + File.separator; } else { - userHomeDir = CONFIG_DIR + File.separator + "CloudChains" + File.separator; + baseDir = CONFIG_DIR; } + String userHomeDir = baseDir + File.separator + "CloudChains" + File.separator; File directory = new File(userHomeDir); if (!directory.exists()) { - directory.mkdir(); + directory.mkdirs(); } return userHomeDir; diff --git a/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java b/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java index 860549b..01eb653 100644 --- a/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java +++ b/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java @@ -153,7 +153,12 @@ private void sendKeepAlive() { if (HTTP_BLOCK_COUNT_UPDATES) { heightUpdateHttpClient.getAllBlockCounts(); - feeUpdateHttpClient.getAllFees(); + + // feeUpdateHttpClient.getAllFees(); + // TODO: Re-enable when remote servers support relayfee queries. + // Currently disabled — remote endpoints return incorrect relayfee data. + // Using locally-configured fee values until server-side fixes are deployed. + } else if (!blocknetPeerGroup.getConnectedPeers().isEmpty()) { for (BlocknetPeer blocknetPeer : blocknetPeerGroup.getConnectedPeers()) { XRouterConfiguration xRouterConfiguration = blocknetPeer.getxRouterConfiguration(); diff --git a/src/main/java/io/cloudchains/app/wallet/WalletHelper.java b/src/main/java/io/cloudchains/app/wallet/WalletHelper.java index 2c81c88..f3eb170 100644 --- a/src/main/java/io/cloudchains/app/wallet/WalletHelper.java +++ b/src/main/java/io/cloudchains/app/wallet/WalletHelper.java @@ -3,6 +3,7 @@ import com.google.common.base.Preconditions; import io.cloudchains.app.net.CoinInstance; import io.cloudchains.app.net.CoinTicker; +import io.cloudchains.app.net.CoinTickerUtils; import io.cloudchains.app.net.protocols.blocknet.BlocknetPeer; import io.cloudchains.app.util.AddressBalance; import io.cloudchains.app.util.CloudTransaction; @@ -34,11 +35,24 @@ public WalletHelper(CoinInstance coinInstance) { } public Transaction createRawTransactionWithAllUTXOs(Transaction tx, double amount) { - try { - ArrayList utxos = coinSelector(amount); + ArrayList utxos = coinSelector(amount); + + if (utxos == null) { + LOGGER.log(Level.WARNING, "[wallet-" + coin.getTicker() + "] createRawTransactionWithAllUTXOs: no UTXOs for amount=" + amount); + return null; + } - Preconditions.checkNotNull(utxos); - for (UTXO utxo : utxos) { + return signTransactionWithUtxos(tx, utxos); + } + + private Transaction signTransactionWithUtxos(Transaction tx, ArrayList selectedUtxos) { + try { + if (selectedUtxos == null) { + LOGGER.log(Level.WARNING, "[wallet-" + coin.getTicker() + "] signTransactionWithUtxos: no UTXOs provided"); + return null; + } + LOGGER.log(Level.FINE, "[wallet-" + coin.getTicker() + "] signTransactionWithUtxos: signing " + selectedUtxos.size() + " UTXOs"); + for (UTXO utxo : selectedUtxos) { if (utxo.isSpent()) continue; @@ -49,13 +63,14 @@ public Transaction createRawTransactionWithAllUTXOs(Transaction tx, double amoun TransactionOutPoint outPoint = new TransactionOutPoint(networkParameters, bUtxo.getIndex(), bUtxo.getHash()); tx.addSignedInput(outPoint, bUtxo.getScript(), addressBalance.getPrivateKey().getKey(), Transaction.SigHash.ALL, true); + LOGGER.log(Level.FINE, "[wallet-" + coin.getTicker() + "] signed input: txid=" + utxo.getTxid() + " vout=" + utxo.getVout()); utxo.setSpent(true); addressBalance.calculateBalance(); } return tx; } catch (Exception e) { - LOGGER.log(Level.WARNING, "[wallet-helper] Error creating transaction", e); + LOGGER.log(Level.WARNING, "[wallet-" + coin.getTicker() + "] Error creating transaction", e); return null; } } @@ -84,8 +99,9 @@ private ArrayList sortLeastToGreatest() { private ArrayList advancedCoinSorting() { ArrayList utxos = new ArrayList<>(); - + LOGGER.log(Level.FINE, "[wallet-" + coin.getTicker() + "] advancedCoinSorting: " + coin.getAddressKeyPairs().size() + " addresses tracked locally"); for (AddressBalance addressBalance : coin.getAddressKeyPairs()) { + LOGGER.log(Level.FINE, "[wallet-" + coin.getTicker() + "] addr=" + addressBalance.getAddress().toBase58() + " utxos=" + addressBalance.getUtxos().size()); utxos.addAll(addressBalance.getUtxos()); } @@ -120,7 +136,13 @@ private ArrayList coinSelector(double amount) { ArrayList utxos = new ArrayList<>(); double totalBalance = 0.0; - for (UTXO utxo : advancedCoinSorting()) { + ArrayList sorted = advancedCoinSorting(); + LOGGER.log(Level.FINE, "[wallet-" + coin.getTicker() + "] coinSelector: requested=" + amount + ", available UTXOs=" + sorted.size()); + for (UTXO utxo : sorted) { + LOGGER.log(Level.FINE, "[wallet-" + coin.getTicker() + "] UTXO: txid=" + utxo.getTxid() + " vout=" + utxo.getVout() + " amount=" + utxo.getAmount() + " spent=" + utxo.isSpent()); + } + + for (UTXO utxo : sorted) { if (totalBalance < amount) { totalBalance += utxo.getAmount(); utxos.add(utxo); @@ -129,10 +151,13 @@ private ArrayList coinSelector(double amount) { } } - if (utxos.size() > 0) + if (utxos.size() > 0) { + LOGGER.log(Level.FINE, "[wallet-" + coin.getTicker() + "] coinSelector: selected " + utxos.size() + " UTXOs, total=" + totalBalance); return utxos; - else + } else { + LOGGER.log(Level.WARNING, "[wallet-" + coin.getTicker() + "] coinSelector: no UTXOs found (requested=" + amount + ", available in wallet=" + sorted.size() + ")"); return null; + } } public String formatAmount(double amount) { @@ -153,10 +178,14 @@ public double getSpendBalance(double amount) { double totalBalance = 0.0; ArrayList utxos = coinSelector(amount); - Preconditions.checkNotNull(utxos); + if (utxos == null) { + LOGGER.log(Level.WARNING, "[wallet-" + coin.getTicker() + "] getSpendBalance: insufficient funds (requested=" + amount + ")"); + return 0.0; + } for (UTXO utxo : utxos) totalBalance += utxo.getAmount(); + LOGGER.log(Level.FINE, "[wallet-" + coin.getTicker() + "] getSpendBalance: available=" + totalBalance + " for request=" + amount); return totalBalance; } @@ -206,8 +235,20 @@ public static Transaction createTransactionSimple(CoinTicker coinTicker, String double fee = coinInstance.getConfigHelper().getFee(); double totalSpending = amount + fee; - double totalAvailable = walletHelper.getSpendBalance(totalSpending); + + LOGGER.log(Level.FINE, "[wallet-" + CoinTickerUtils.tickerToString(coinTicker) + "] createTransactionSimple: to=" + address + " amount=" + amount + " fee=" + fee + " totalSpending=" + totalSpending); + + ArrayList selectedUtxos = walletHelper.coinSelector(totalSpending); + if (selectedUtxos == null) { + LOGGER.log(Level.WARNING, "[wallet-" + CoinTickerUtils.tickerToString(coinTicker) + "] createTransactionSimple: insufficient funds (need " + totalSpending + ")"); + return null; + } + double totalAvailable = 0.0; + for (UTXO utxo : selectedUtxos) totalAvailable += utxo.getAmount(); double changeAmt = (totalAvailable - amount) - fee; + + LOGGER.log(Level.FINE, "[wallet-" + CoinTickerUtils.tickerToString(coinTicker) + "] createTransactionSimple: totalAvailable=" + totalAvailable + " changeAmt=" + changeAmt); + LegacyAddress toAddress = LegacyAddress.fromBase58(params, address); Coin sendAmount = Coin.valueOf((long) Math.floor(amount * Coin.COIN.value)); Coin changeAmount = Coin.valueOf((long) Math.floor(changeAmt * Coin.COIN.value)); @@ -224,7 +265,7 @@ public static Transaction createTransactionSimple(CoinTicker coinTicker, String if (changeAmount.isPositive()) tx.addOutput(changeAmount, walletHelper.getChangeAddress()); - return walletHelper.createRawTransactionWithAllUTXOs(tx, totalAvailable); + return walletHelper.signTransactionWithUtxos(tx, selectedUtxos); } public static void setAsSpent(CoinTicker coinTicker, Transaction transaction, boolean setSpent) { From 7c29d7ce38b987393ed45c618092d24cc3478b2f Mon Sep 17 00:00:00 2001 From: tryiou Date: Sat, 4 Apr 2026 16:35:49 +0200 Subject: [PATCH 33/73] security: harden passphrase and key memory handling across login, discovery, and address balance - ConsoleMenu: Replace String entropy with char[] passphrase for both --xli login and --xli-mnemonic import; wrap in try/finally with Arrays.fill(passphrase, '\0') - AddressDiscoveryService: Derive addresses directly from HD seed via deriveAddressRange() bypassing wallet lookahead window; clear seed byte copy immediately after master key derivation; clear private keys on each batch after scanning and after discovery completes; add clearExternalChainKey() called after discovery finishes - AddressBalance: Add clearPrivateKey() method to release private key references after use --- .../cloudchains/app/console/ConsoleMenu.java | 37 +++++-- .../cloudchains/app/util/AddressBalance.java | 4 + .../app/util/AddressDiscoveryService.java | 100 +++++++++++++----- 3 files changed, 103 insertions(+), 38 deletions(-) diff --git a/src/main/java/io/cloudchains/app/console/ConsoleMenu.java b/src/main/java/io/cloudchains/app/console/ConsoleMenu.java index 6c70119..98b2d03 100644 --- a/src/main/java/io/cloudchains/app/console/ConsoleMenu.java +++ b/src/main/java/io/cloudchains/app/console/ConsoleMenu.java @@ -14,6 +14,7 @@ import java.io.Console; import java.security.SecureRandom; import java.util.ArrayList; +import java.util.Arrays; import java.util.Base64; import java.util.List; import java.util.Scanner; @@ -145,8 +146,16 @@ public void init() { System.exit(1); } - String entropy = LoginUtils.loginToEntropy(password); - completeLogin(entropy, null, false); + char[] passphrase = LoginUtils.loginToEntropy(password).toCharArray(); + try { + List mnemonic = KeyHandler.getBaseSeed(passphrase); + if (mnemonic == null) { + logBadPassword(null); + System.exit(1); + } + } finally { + Arrays.fill(passphrase, '\0'); + } System.exit(0); } @@ -168,8 +177,16 @@ public void init() { System.exit(1); } - String entropy = LoginUtils.loginToEntropy(password); - completeLogin(entropy, mnemonic, false); + char[] passphrase = LoginUtils.loginToEntropy(password).toCharArray(); + try { + if (!KeyHandler.importFromMnemonic(Arrays.asList(mnemonic.split(" ")), passphrase)) { + logBadMnemonic(); + System.exit(1); + } + } finally { + Arrays.fill(passphrase, '\0'); + } + System.exit(0); } case "--xliterpc": { @@ -366,17 +383,17 @@ private void completeLogin(String entropy, String userMnemonic, boolean isMnemon System.exit(0); } - // Measure total initialization time for all coins long startTime = System.currentTimeMillis(); - // Initialize Blocknet first (synchronous) as it's the active currency - CoinInstance.CoinError coinError = CoinInstance.getInstance(CoinTicker.BLOCKNET).init(entropy, userMnemonic, isMnemonic, xliteRPC); + + // Wallet file already exists on disk at this point. Pass null for userMnemonic + // so CoinInstance reads the seed from disk rather than attempting to create it. + CoinInstance.CoinError coinError = CoinInstance.getInstance(CoinTicker.BLOCKNET).init(entropy, null, isMnemonic, xliteRPC); if (coinError != null) { String msg = "[master] Error(" + coinError.getCode().name() + "): " + coinError.getMessage(); LOGGER.log(Level.SEVERE, msg); System.exit(0); } - // Get all coin tickers except Blocknet (which is already initialized) List otherCoins = new ArrayList<>(); for (CoinTicker cointicker : CoinTicker.coins()) { if (cointicker != CoinTicker.BLOCKNET && cointicker != CoinTicker.BLOCKNET_TESTNET5) { @@ -384,8 +401,7 @@ private void completeLogin(String entropy, String userMnemonic, boolean isMnemon } } - // Initialize remaining coins concurrently - initializeCoinsConcurrently(otherCoins, entropy, userMnemonic, isMnemonic, xliteRPC); + initializeCoinsConcurrently(otherCoins, entropy, null, isMnemonic, xliteRPC); long endTime = System.currentTimeMillis(); long totalTime = endTime - startTime; @@ -394,7 +410,6 @@ private void completeLogin(String entropy, String userMnemonic, boolean isMnemon App.masterRPC.start(); backgroundTimerThread = new BackgroundTimerThread(); (new Thread(backgroundTimerThread)).start(); - // Start EXR capability probing after wallet is decrypted if (App.exrServerPool != null) { App.exrServerPool.probeAllCapabilities(); } diff --git a/src/main/java/io/cloudchains/app/util/AddressBalance.java b/src/main/java/io/cloudchains/app/util/AddressBalance.java index fb48357..8b16807 100644 --- a/src/main/java/io/cloudchains/app/util/AddressBalance.java +++ b/src/main/java/io/cloudchains/app/util/AddressBalance.java @@ -60,6 +60,10 @@ public DumpedPrivateKey getPrivateKey() { return privateKey; } + public void clearPrivateKey() { + privateKey = null; + } + public void clearUtxos() { synchronized (this) { utxos.removeIf(utxo -> !utxo.isSpent()); diff --git a/src/main/java/io/cloudchains/app/util/AddressDiscoveryService.java b/src/main/java/io/cloudchains/app/util/AddressDiscoveryService.java index 374a3fe..a679e1d 100644 --- a/src/main/java/io/cloudchains/app/util/AddressDiscoveryService.java +++ b/src/main/java/io/cloudchains/app/util/AddressDiscoveryService.java @@ -1,13 +1,22 @@ package io.cloudchains.app.util; +import com.google.common.collect.ImmutableList; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import io.cloudchains.app.net.CoinInstance; import io.cloudchains.app.net.CoinTickerUtils; import io.cloudchains.app.net.api.http.client.HTTPClient; +import org.bitcoinj.core.DumpedPrivateKey; +import org.bitcoinj.core.LegacyAddress; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.crypto.ChildNumber; +import org.bitcoinj.crypto.DeterministicKey; +import org.bitcoinj.crypto.HDKeyDerivation; +import org.bitcoinj.wallet.Wallet; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -19,8 +28,8 @@ public class AddressDiscoveryService { private final static LogManager LOGMANAGER = LogManager.getLogManager(); private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); - private static final int BATCH_SIZE = 250; - private static final int NUM_BATCHES = 100; // 250 * 100 = 25,000 address range + private static final int BATCH_SIZE = 500; + private static final int NUM_BATCHES = 100; // 500 * 100 = 50,000 address range private static final int MAX_CONSECUTIVE_ERRORS = 3; private static int DISCOVERY_TIMEOUT_MS = 30000; // non-final for testing @@ -36,6 +45,7 @@ public static int getNumBatches() { private final HTTPClient httpClient; private final ConfigHelper configHelper; private final String currencyString; + private DeterministicKey externalChainKey; private String getLogPrefix() { return "[discovery-" + currencyString + "]"; @@ -50,9 +60,41 @@ public AddressDiscoveryService(CoinInstance coinInstance, HTTPClient httpClient) this.httpClient = httpClient; this.configHelper = coinInstance.getConfigHelper(); this.currencyString = CoinTickerUtils.tickerToString(coinInstance.getTicker()); + this.externalChainKey = initExternalChainKey(coinInstance.getWallet()); LOGGER.log(Level.FINER, getLogPrefix() + " AddressDiscoveryService initialized for " + currencyString); } + /** + * Derive and cache the external chain key from the wallet seed. + * Clears the seed copy immediately after derivation per security conventions. + * Note: the wallet's original seed is never modified. + */ + private static DeterministicKey initExternalChainKey(Wallet wallet) { + byte[] seedBytes = wallet.getKeyChainSeed().getSeedBytes(); + if (seedBytes == null) { + return null; + } + byte[] seedCopy = Arrays.copyOf(seedBytes, seedBytes.length); + try { + DeterministicKey masterKey = HDKeyDerivation.createMasterPrivateKey(seedCopy); + ImmutableList accountPath = wallet.getActiveKeyChain().getAccountPath(); + DeterministicKey accountKey = masterKey; + for (ChildNumber child : accountPath) { + accountKey = HDKeyDerivation.deriveChildKey(accountKey, child); + } + return HDKeyDerivation.deriveChildKey(accountKey, ChildNumber.ZERO); + } finally { + Arrays.fill(seedCopy, (byte) 0); + } + } + + /** + * Clear the cached external chain key from memory after address discovery is complete. + */ + public void clearExternalChainKey() { + this.externalChainKey = null; + } + public static void setDiscoveryTimeoutMs(int timeoutMs) { DISCOVERY_TIMEOUT_MS = timeoutMs; } @@ -86,8 +128,6 @@ public int discoverAddressCount() { break; } - ensureAddressesGenerated((i + 1) * BATCH_SIZE); - List utxos = probeBatch(i); if (utxos == null) { @@ -114,11 +154,15 @@ public int discoverAddressCount() { // Find exact highest funded address in the last non-empty batch int batchStart = lastNonEmptyBatch * BATCH_SIZE; - List batch = getBatch(batchStart, BATCH_SIZE); + List batch = deriveAddressRange(batchStart, BATCH_SIZE); int lastUsedInBatch = findLastUsedIndexInBatch(batch, lastBatchUtxos); int discoveredCount = batchStart + lastUsedInBatch + 1; + for (AddressBalance addr : batch) { + addr.clearPrivateKey(); + } + LOGGER.log(Level.INFO, getLogPrefix() + " Discovery complete: lastBatch=" + lastNonEmptyBatch + ", count=" + discoveredCount + ", time=" + (System.currentTimeMillis() - startTime) + "ms"); @@ -136,34 +180,36 @@ public int discoverAddressCount() { * @return UTXO list (may be empty), or null on HTTP failure */ private List probeBatch(int batchIndex) { - List batch = getBatch(batchIndex * BATCH_SIZE, BATCH_SIZE); - return checkBatchForUtxos(batch); - } - - /** - * Get a slice of addresses [startIndex, startIndex + size) from CoinInstance. - */ - private List getBatch(int startIndex, int size) { - List batch = new ArrayList<>(size); - int end = Math.min(startIndex + size, coinInstance.getAddressKeyPairs().size()); - for (int i = startIndex; i < end; i++) { - batch.add(coinInstance.getAddressKeyPairs().get(i)); + int startIndex = batchIndex * BATCH_SIZE; + List batch = deriveAddressRange(startIndex, BATCH_SIZE); + try { + return checkBatchForUtxos(batch); + } finally { + for (AddressBalance addr : batch) { + addr.clearPrivateKey(); + } } - return batch; } /** - * Ensure addresses up to count are generated in CoinInstance. + * Derive addresses at specific HD indices without modifying wallet state. + * Uses the pre-derived external chain key, bypassing the wallet lookahead window limitation. */ - private void ensureAddressesGenerated(int count) { - int currentGenerated = coinInstance.getAddressKeyPairs().size(); - if (count > currentGenerated) { - int toGenerate = count - currentGenerated; - for (int i = 0; i < toGenerate; i++) { - coinInstance.generateAddress(false); - } - LOGGER.log(Level.FINE, getLogPrefix() + " Generated " + toGenerate + " addresses"); + private List deriveAddressRange(int startIndex, int count) { + List result = new ArrayList<>(count); + if (externalChainKey == null) { + return result; + } + NetworkParameters params = coinInstance.getNetworkParameters(); + + for (int i = 0; i < count; i++) { + int index = startIndex + i; + DeterministicKey addressKey = HDKeyDerivation.deriveChildKey(externalChainKey, new ChildNumber(index, false)); + LegacyAddress address = LegacyAddress.fromPubKeyHash(params, addressKey.getPubKeyHash()); + DumpedPrivateKey privKey = addressKey.getPrivateKeyEncoded(params); + result.add(new AddressBalance(address, privKey)); } + return result; } private boolean isTimedOut(long startTime) { From 10f15862309211ff030e992422c01d916ba73e7d Mon Sep 17 00:00:00 2001 From: tryiou Date: Sat, 4 Apr 2026 16:36:09 +0200 Subject: [PATCH 34/73] docs: update AGENTS.md with jabba, native build, and refined conventions --- AGENTS.md | 60 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6664088..08d4170 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,6 +6,9 @@ XLite Daemon — a multi-cryptocurrency wallet daemon built with Java 21 and Mav Core packages: `crypto` (wallet encryption/key management), `net` (coin networking, JSON-RPC), `util` (config, address discovery, logging), `wallet` (wallet helpers). +# Java — use jabba +source ~/.jabba/jabba.sh && jabba use graalvm_community@21.0.2 + ## Build & Test Commands ```bash @@ -19,12 +22,15 @@ mvn test mvn test -pl . -Dtest=KeyHandlerTest # Run a single test method -mvn test -pl . -Dtest=KeyHandlerTest#testGetBaseSeed +mvn test -pl . -Dtest=KeyHandlerTest#testGetBaseSeedRoundTrip # Build shaded JAR mvn package -q -# Requirements: Java 21, Maven 3.8.6+ +# Build GraalVM native image +mvn package -Pnative -Pnative-fast -q + +# Requirements: Java 21 (jabba: graalvm_community@21.0.2), Maven 3.8.6+ ``` ## Code Style @@ -62,23 +68,21 @@ Log messages use bracketed prefixes: `[security]`, `[discovery-BLOCK]`, `[wallet - Methods/variables: `camelCase` - Constants: `UPPER_SNAKE_CASE` - Test classes: `Test.java` -- Test methods: `test` (e.g., `testGetBaseSeed`, `testLegacyWalletMigration`) - -### Error Handling - -- Crypto operations: use try/finally to clear sensitive byte arrays with `Arrays.fill(bytes, (byte) 0)` -- Use `PBEKeySpec.clearPassword()` after key derivation -- Do not use `e.printStackTrace()` — use `LOGGER.log(Level.WARNING, "message", e)` instead -- Catch specific exceptions (`BadPaddingException`) before generic `Exception` -- `RuntimeException` for unrecoverable state; return `null` or `false` for expected failures - -### Security Conventions - -- AES-CBC with random IV for all new encryption; ECB only for legacy decryption -- PBKDF2 with `PBKDF2WithHmacSHA256`, 100k iterations for current format -- `SecureRandom.getInstanceStrong()` for all cryptographic RNG -- Explicit `StandardCharsets.UTF_8` in all `getBytes()` calls -- Clear sensitive data in `finally` blocks — never rely on GC alone +- Test methods: `test` (e.g., `testGetBaseSeedRoundTrip`, `testDiscovery_FindsUsedAddresses`) +- Tests use `@TestMethodOrder(OrderAnnotation.class)` + `@Order(n)` for sequencing +- Use `static import org.junit.jupiter.api.Assertions.*` for assertions + +### Error Handling & Security + +- Crypto ops: try/finally with `Arrays.fill(bytes, (byte) 0)` to clear sensitive data +- Call `PBEKeySpec.clearPassword()` after key derivation +- Never use `e.printStackTrace()` — use `LOGGER.log(Level.WARNING, "msg", e)` +- Catch specific exceptions before generic `Exception` +- `RuntimeException` for unrecoverable state; return `null`/`false` for expected failures +- AES-CBC + random IV for new encryption; ECB only for legacy migration +- PBKDF2WithHmacSHA256, 100k iterations; `SecureRandom.getInstanceStrong()` for RNG +- Always use `StandardCharsets.UTF_8` for `getBytes()` calls +- Passphrases use `char[]` — `String` is unsupported (immutable, cannot be wiped) ## Project Structure @@ -91,18 +95,22 @@ src/main/java/io/cloudchains/app/ App.java Main entry point src/test/java/ - KeyHandlerTest.java, CoinInstanceTest.java, ConfigHelperTest.java, - AddressDiscoveryServiceTest.java, LoginUtilsTest.java + KeyHandlerTest, CoinInstanceTest, ConfigHelperTest, + AddressDiscoveryServiceTest, LoginUtilsTest, TestHelper ``` ## Key Dependencies | Library | Purpose | |---------|---------| -| bitcoinj-core 0.14.7 | Bitcoin/crypto primitives, MnemonicCode, ECKey | +| bitcoinj-core 0.15.10 | Bitcoin/crypto, MnemonicCode, ECKey | | Gson 2.13.2 | JSON serialization | | Netty 4.2.7 | HTTP servers, networking | -| Guava (via bitcoinj) | Joiner, Preconditions, utilities | +| Guava 28.2-android | Joiner, Preconditions, AtomicDouble | +| Orchid 1.2.1 | Hex/Base64 encoders | +| httpclient 4.5.14 | HTTP client | +| json 20250517 | JSONObject/JSONArray | +| java-dotenv 5.2.2 | .env file support | | JUnit Jupiter 5.11.3 | Test framework | | Mockito 5.15.2 | Test mocking | @@ -119,5 +127,9 @@ Some older commits use `[category] description` style (e.g., `[security] Upgrade - The `rewrite-maven-plugin` runs on `mvn compile` and may auto-modify imports and formatting. Always review `git diff` after compiling. - `ConfigHelper.CONFIG_DIR` is a mutable static used to override config path in tests. -- Tests create temp directories and write wallet files; `@AfterEach` handles cleanup. +- Tests use `@TempDir` (JUnit 5 auto-cleanup); never run with parallel execution + due to mutable `ConfigHelper.CONFIG_DIR` static state. +- Mockito requires ByteBuddy agent (configured in pom.xml surefire plugin). +- App reads `.env` via java-dotenv (`App.getEnv()` wraps `Dotenv`). +- Javadoc uses `

` tags and `{@code}` inline; section dividers use `// ===` / `// ---`. - The enforcer plugin requires Java 21 and Maven 3.8.6+. From 89b11931a2518c3c5213e706aa851c0d20856492 Mon Sep 17 00:00:00 2001 From: tryiou Date: Sat, 4 Apr 2026 16:37:25 +0200 Subject: [PATCH 35/73] feat: add Ravencoin minimum relay fee calculation --- .../net/protocols/ravencoin/RavencoinNetworkParameters.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/io/cloudchains/app/net/protocols/ravencoin/RavencoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/ravencoin/RavencoinNetworkParameters.java index a0606c0..08f80e2 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/ravencoin/RavencoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/ravencoin/RavencoinNetworkParameters.java @@ -95,4 +95,8 @@ public int getInterval() { public String getId() { return "RVN"; } + + public Coin getMinRelayTxFee() { + return Coin.valueOf(500000); + } } From 7cdca42bd3a8f9d8e40285be051c9af94b3ffc8d Mon Sep 17 00:00:00 2001 From: tryiou Date: Mon, 6 Apr 2026 14:24:28 +0200 Subject: [PATCH 36/73] refactor(server): condense RPC logging, use daemon threads, and fix RPC param bugs --- .../net/api/http/master/HTTPServerHandler.java | 18 ++++++++---------- .../net/api/http/server/HTTPServerHandler.java | 16 +++++++--------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerHandler.java b/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerHandler.java index 886df2e..9dd2bf4 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerHandler.java +++ b/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerHandler.java @@ -184,10 +184,7 @@ protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) String method = jsonReq.get("method").getAsString(); JsonArray params = jsonReq.get("params").getAsJsonArray(); - LOGGER.log(Level.INFO, "[http-server-handler] RPC CALL: " + method + " PARAMS: " + params.size()); - for (int i = 0; i < params.size(); i++) { - LOGGER.log(Level.INFO, "[http-server-handler] PARAM " + i + ": " + params.get(i).toString()); - } + LOGGER.log(Level.INFO, "[http-server-handler] RPC CALL: " + method + " PARAMS: " + params.toString().replace(",", ", ")); response = getResponse(method, params); LOGGER.log(Level.FINER, response.toString()); @@ -231,15 +228,16 @@ private JsonObject getResponse(String method, JsonArray params) { CoinTicker ticker = CoinTickerUtils.stringToTicker(params.get(0).getAsString()); CoinInstance instance = CoinInstance.getInstance(ticker); - Runnable r = () -> { + Thread t = new Thread(() -> { try { Thread.sleep(500); instance.reloadConfig(); } catch (InterruptedException e) { LOGGER.log(Level.WARNING, "[http-master] Interrupted during reloadconfig for " + ticker, e); } - }; - new Thread(r).start(); + }); + t.setDaemon(true); + t.start(); response.addProperty("result", true); response.add("error", JsonNull.INSTANCE); @@ -301,9 +299,9 @@ private JsonObject getResponse(String method, JsonArray params) { } if (shutdownRequested) { - (new Thread(() -> { - System.exit(0); - })).start(); // shutdown the server + Thread t = new Thread(() -> System.exit(0)); + t.setDaemon(true); + t.start(); } return response; } diff --git a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java b/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java index 9940f92..1d8cff4 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java +++ b/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java @@ -213,10 +213,7 @@ protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) String method = jsonReq.get("method").getAsString(); JsonArray params = jsonReq.get("params").getAsJsonArray(); - LOGGER.log(Level.INFO, "[http-server-handler] RPC CALL: " + coin.getTicker() + " " + method + " PARAMS: " + params.size()); - for (int i = 0; i < params.size(); i++) { - LOGGER.log(Level.INFO, "[http-server-handler] PARAM " + i + ": " + params.get(i).toString()); - } + LOGGER.log(Level.INFO, "[http-server-handler] RPC CALL: " + coin.getTicker() + " " + method + " PARAMS: " + params.toString().replace(",", ", ")); response = getResponse(method, params); LOGGER.log(Level.FINER, response.toString()); @@ -246,7 +243,7 @@ private JsonObject getResponse(String method, JsonArray params) { switch (method.toLowerCase()) { case "reloadconfig": { - Runnable r = () -> { + Thread t = new Thread(() -> { try { Thread.sleep(500); } catch (InterruptedException e) { @@ -254,8 +251,9 @@ private JsonObject getResponse(String method, JsonArray params) { } coin.reloadConfig(); - }; - new Thread(r).start(); + }); + t.setDaemon(true); + t.start(); response.addProperty("result", true); response.add("error", JsonNull.INSTANCE); @@ -498,7 +496,7 @@ private JsonObject getResponse(String method, JsonArray params) { break; } case "getrawmempool": { - if (params.size() > 1) { + if (params.size() > 2) { response.add("result", JsonNull.INSTANCE); JsonObject errorJSON = new JsonObject(); errorJSON.addProperty("code", -1); @@ -671,7 +669,7 @@ private JsonObject getResponse(String method, JsonArray params) { long locktime = 0; if (params.size() >= 3) - locktime = params.get(3).getAsLong(); + locktime = params.get(2).getAsLong(); try { inputs = params.get(0).getAsJsonArray(); From ae336a17bbe3fa70a34994e5ae430982c622a032 Mon Sep 17 00:00:00 2001 From: tryiou Date: Mon, 6 Apr 2026 14:24:33 +0200 Subject: [PATCH 37/73] fix: use StandardCharsets.UTF_8 in LoginUtils --- src/main/java/io/cloudchains/app/crypto/LoginUtils.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/cloudchains/app/crypto/LoginUtils.java b/src/main/java/io/cloudchains/app/crypto/LoginUtils.java index ec5d024..02714fa 100644 --- a/src/main/java/io/cloudchains/app/crypto/LoginUtils.java +++ b/src/main/java/io/cloudchains/app/crypto/LoginUtils.java @@ -1,5 +1,6 @@ package io.cloudchains.app.crypto; +import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.util.logging.Level; import java.util.logging.LogManager; @@ -12,7 +13,7 @@ public class LoginUtils { private static String toSha256(String message) { try { MessageDigest digest = MessageDigest.getInstance("SHA-256"); - digest.update((message).getBytes()); + digest.update(message.getBytes(StandardCharsets.UTF_8)); byte[] hash = digest.digest(); StringBuilder hex = new StringBuilder(); for (byte b : hash) { From 8e2b32d9a82f51968f30083ca7d28de2c3b6075f Mon Sep 17 00:00:00 2001 From: tryiou Date: Mon, 6 Apr 2026 14:24:44 +0200 Subject: [PATCH 38/73] chore: add Eclipse IDE files to .gitignore --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 46fc178..b0a4112 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,6 @@ Thumbs.db *.temp *~ -CloudChains/* \ No newline at end of file +CloudChains/* +.settings +.project \ No newline at end of file From a4317af38bc69bfbddcd7cc6823884b943abd30d Mon Sep 17 00:00:00 2001 From: tryiou Date: Mon, 6 Apr 2026 17:17:37 +0200 Subject: [PATCH 39/73] fix(api): ensure proper shutdown of Netty worker groups and thread pools The changes improve resource management and logging efficiency: - In `JSONRPCMasterServer` and `JSONRPCServer`, the `EventLoopGroup` is now stored as a class member and explicitly shut down gracefully during the server stop sequence to prevent resource leaks. - In `BackgroundTimerThread`, the `threadPool` is now properly shut down with a waiting period before forcing a shutdown. - Updated `BackgroundTimerThread` logging to prevent spamming the same availability status; it now only logs when the set of available or unavailable currencies changes. --- .../app/net/api/JSONRPCMasterServer.java | 10 +++--- .../app/net/api/JSONRPCServer.java | 10 ++++-- .../background/BackgroundTimerThread.java | 36 ++++++++++++++++++- 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/src/main/java/io/cloudchains/app/net/api/JSONRPCMasterServer.java b/src/main/java/io/cloudchains/app/net/api/JSONRPCMasterServer.java index 7d94c0d..6df5f41 100644 --- a/src/main/java/io/cloudchains/app/net/api/JSONRPCMasterServer.java +++ b/src/main/java/io/cloudchains/app/net/api/JSONRPCMasterServer.java @@ -21,13 +21,14 @@ public class JSONRPCMasterServer extends Thread { private boolean stopping = false; private Channel channel; + private EventLoopGroup workerGroup; JSONRPCMasterServer(int port) { this.port = port; } public void run() { - EventLoopGroup workerGroup = new NioEventLoopGroup(2); + workerGroup = new NioEventLoopGroup(2); try { LOGGER.log(Level.INFO, "[rpc] Starting master RPC server on port " + port + "."); @@ -53,10 +54,11 @@ public void deinit() { stopping = true; LOGGER.log(Level.FINER, "[json-rpc-server] Interrupting server."); - if (channel != null && channel.isOpen()) { + if (channel != null) { channel.close(); - } else { - LOGGER.log(Level.FINER, "[json-rpc-server] Channel is null or not open during deinitialization."); + } + if (workerGroup != null) { + workerGroup.shutdownGracefully(); } } } diff --git a/src/main/java/io/cloudchains/app/net/api/JSONRPCServer.java b/src/main/java/io/cloudchains/app/net/api/JSONRPCServer.java index 59c9bdd..43621f6 100644 --- a/src/main/java/io/cloudchains/app/net/api/JSONRPCServer.java +++ b/src/main/java/io/cloudchains/app/net/api/JSONRPCServer.java @@ -24,6 +24,7 @@ public class JSONRPCServer extends Thread { private boolean stopping = false; private Channel channel; + private EventLoopGroup workerGroup; JSONRPCServer(CoinInstance coin, int port) { this.coin = coin; @@ -31,7 +32,7 @@ public class JSONRPCServer extends Thread { } public void run() { - EventLoopGroup workerGroup = new NioEventLoopGroup(5); + workerGroup = new NioEventLoopGroup(5); try { ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(workerGroup) @@ -57,6 +58,11 @@ public void deinit() { stopping = true; LOGGER.log(Level.FINER, "[json-rpc-server] Interrupting server."); - channel.close(); + if (channel != null) { + channel.close(); + } + if (workerGroup != null) { + workerGroup.shutdownGracefully(); + } } } diff --git a/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java b/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java index 01eb653..3c321e6 100644 --- a/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java +++ b/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java @@ -11,6 +11,10 @@ import java.time.Duration; import java.time.LocalTime; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -42,6 +46,9 @@ public class BackgroundTimerThread implements Runnable { private boolean shutdownRequested = false; private volatile Thread workerThread; + private Set lastAvailable = new HashSet<>(); + private Set lastUnavailable = new HashSet<>(); + // Log rotation scheduler fields private ScheduledExecutorService logRotationScheduler; private static final int DAILY_ROTATION_HOUR = 2; // 2:00 AM @@ -114,6 +121,17 @@ public void stop() { if (workerThread != null) { workerThread.interrupt(); } + if (threadPool != null && !threadPool.isShutdown()) { + threadPool.shutdown(); + try { + if (!threadPool.awaitTermination(5, TimeUnit.SECONDS)) { + threadPool.shutdownNow(); + } + } catch (InterruptedException e) { + threadPool.shutdownNow(); + Thread.currentThread().interrupt(); + } + } if (logRotationScheduler != null && !logRotationScheduler.isShutdown()) { logRotationScheduler.shutdown(); try { @@ -133,15 +151,31 @@ private void outputAvailableCurrencies() { if (elapsed < 60 * 1000 && lastOut != 0) return; + List available = new ArrayList<>(); + Set unavailable = new HashSet<>(); + for (CoinInstance coinInstance : CoinInstance.getCoinInstances()) { if (!CoinTickerUtils.isActiveTicker(coinInstance.getTicker())) continue; + String name = CoinTickerUtils.tickerToString(coinInstance.getTicker()); if (CoinInstance.getBlockCountByTicker(coinInstance.getTicker()) > 0) { - LOGGER.log(Level.INFO, "[coin] Available Currency: " + CoinTickerUtils.tickerToString(coinInstance.getTicker())); + available.add(name); + } else { + unavailable.add(name); } } + Set currentAvailable = new HashSet<>(available); + if (!currentAvailable.equals(lastAvailable)) { + LOGGER.log(Level.INFO, "[coin] Available: " + String.join(", ", available)); + } + lastAvailable = currentAvailable; + + if (!unavailable.isEmpty() && !unavailable.equals(lastUnavailable)) { + LOGGER.log(Level.INFO, "[coin] Unavailable: " + String.join(", ", unavailable)); + } + lastUnavailable = unavailable; lastOut = System.currentTimeMillis(); } From e19e02b77562ba9b2c9d7d0e289c3efd3698ed8a Mon Sep 17 00:00:00 2001 From: tryiou Date: Mon, 6 Apr 2026 17:18:45 +0200 Subject: [PATCH 40/73] refactor(net): improve thread safety and API error handling The changes enhance concurrency safety and robustness of the HTTP server handlers: - Replace `HashMap` with `ConcurrentHashMap` for `blockCounts` and `relayFees` in `CoinInstance` to prevent potential `ConcurrentModificationException` and ensure thread-safe access to shared data. - Add validation for Basic Auth headers in `HTTPServerHandler` to gracefully handle malformed credentials. - Implement explicit error responses for invalid tickers or missing coin instances in the HTTP API. - Add proper interruption handling when catching `InterruptedException` during retry sleep cycles. --- .../io/cloudchains/app/net/CoinInstance.java | 6 ++-- .../api/http/master/HTTPServerHandler.java | 32 +++++++++++++++---- .../api/http/server/HTTPServerHandler.java | 19 +++++++---- 3 files changed, 40 insertions(+), 17 deletions(-) diff --git a/src/main/java/io/cloudchains/app/net/CoinInstance.java b/src/main/java/io/cloudchains/app/net/CoinInstance.java index 655188d..0e1f249 100644 --- a/src/main/java/io/cloudchains/app/net/CoinInstance.java +++ b/src/main/java/io/cloudchains/app/net/CoinInstance.java @@ -87,8 +87,8 @@ public String getMessage() { private static CoinInstance activeCurrency; private static CoinTicker activeBlocknetNetwork = null; private static CopyOnWriteArrayList> activeCoinChangedListeners = new CopyOnWriteArrayList<>(); - private static HashMap blockCounts = new HashMap<>(); - private static HashMap relayFees = new HashMap<>(); + private static ConcurrentHashMap blockCounts = new ConcurrentHashMap<>(); + private static ConcurrentHashMap relayFees = new ConcurrentHashMap<>(); private ConfigHelper configHelper; private WalletHelper walletHelper = null; @@ -1070,7 +1070,7 @@ public static AtomicInteger getBlockCount(CoinTicker ticker) { return blockCounts.putIfAbsent(ticker, new AtomicInteger(0)); } - public static HashMap getBlockCounts() { + public static ConcurrentHashMap getBlockCounts() { return blockCounts; } diff --git a/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerHandler.java b/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerHandler.java index 9dd2bf4..7d02968 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerHandler.java +++ b/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerHandler.java @@ -96,13 +96,16 @@ protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) byte[] credDecoded = Base64.decode(base64Credentials); String credentials = new String(credDecoded, StandardCharsets.UTF_8); final String[] values = credentials.split(":", 2); - - headerUser = values[0]; - headerPass = values[1]; - - if (headerUser.equals(configHelper.getRpcUsername()) && headerPass.equals(configHelper.getRpcPassword())) { - successfulAuth = true; - LOGGER.log(Level.FINER, "[http-server-handler] Successful Auth"); + if (values.length < 2) { + LOGGER.log(Level.WARNING, "[http-server-handler] Malformed Basic Auth header"); + } else { + headerUser = values[0]; + headerPass = values[1]; + + if (headerUser.equals(configHelper.getRpcUsername()) && headerPass.equals(configHelper.getRpcPassword())) { + successfulAuth = true; + LOGGER.log(Level.FINER, "[http-server-handler] Successful Auth"); + } } } } @@ -226,7 +229,22 @@ private JsonObject getResponse(String method, JsonArray params) { } CoinTicker ticker = CoinTickerUtils.stringToTicker(params.get(0).getAsString()); + if (ticker == null) { + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", -1); + errorJSON.addProperty("message", "Unknown ticker: " + params.get(0).getAsString()); + response.add("error", errorJSON); + break; + } + CoinInstance instance = CoinInstance.getInstance(ticker); + if (instance == null) { + JsonObject errorJSON = new JsonObject(); + errorJSON.addProperty("code", -1); + errorJSON.addProperty("message", "Coin instance not found for ticker: " + CoinTickerUtils.tickerToString(ticker)); + response.add("error", errorJSON); + break; + } Thread t = new Thread(() -> { try { diff --git a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java b/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java index 1d8cff4..e0e8759 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java +++ b/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java @@ -114,13 +114,16 @@ protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) byte[] credDecoded = Base64.decode(base64Credentials); String credentials = new String(credDecoded, StandardCharsets.UTF_8); final String[] values = credentials.split(":", 2); + if (values.length < 2) { + LOGGER.log(Level.WARNING, "[http-server-handler] Malformed Basic Auth header"); + } else { + headerUser = values[0]; + headerPass = values[1]; - headerUser = values[0]; - headerPass = values[1]; - - if (headerUser.equals(configHelper.getRpcUsername()) && headerPass.equals(configHelper.getRpcPassword())) { - successfulAuth = true; - LOGGER.log(Level.FINER, "[http-server-handler] Successful Auth"); + if (headerUser.equals(configHelper.getRpcUsername()) && headerPass.equals(configHelper.getRpcPassword())) { + successfulAuth = true; + LOGGER.log(Level.FINER, "[http-server-handler] Successful Auth"); + } } } } @@ -1038,7 +1041,9 @@ private JsonObject getResponse(String method, JsonArray params) { if (i < retries - 1) { try { Thread.sleep(2000); - } catch (Exception e) { + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; } continue; } From 0eb9dc76e6c40a0d7fb5f194a6ddbb1b0f08f4ae Mon Sep 17 00:00:00 2001 From: tryiou Date: Mon, 6 Apr 2026 17:19:11 +0200 Subject: [PATCH 41/73] refactor(app): clean up logging and remove unused UTXO setter - Replace TODO with proper warning logging for file handler initialization in `App`. - Remove unused `setUtxos` method and `ArrayList` import in `AddressBalance`. --- src/main/java/io/cloudchains/app/App.java | 2 +- .../cloudchains/app/util/AddressBalance.java | 27 ------------------- 2 files changed, 1 insertion(+), 28 deletions(-) diff --git a/src/main/java/io/cloudchains/app/App.java b/src/main/java/io/cloudchains/app/App.java index 5f04c6e..b988200 100644 --- a/src/main/java/io/cloudchains/app/App.java +++ b/src/main/java/io/cloudchains/app/App.java @@ -114,7 +114,7 @@ public static void main(String[] args) { LOGGER.addHandler(fileHandler); } catch (IOException e) { - // TODO Auto-generated catch block + LOGGER.log(Level.WARNING, "[app] Failed to initialize file handler", e); } ConsoleHandler consoleHandler = new ConsoleHandler(){ diff --git a/src/main/java/io/cloudchains/app/util/AddressBalance.java b/src/main/java/io/cloudchains/app/util/AddressBalance.java index 8b16807..472a850 100644 --- a/src/main/java/io/cloudchains/app/util/AddressBalance.java +++ b/src/main/java/io/cloudchains/app/util/AddressBalance.java @@ -5,7 +5,6 @@ import org.bitcoinj.core.DumpedPrivateKey; import org.bitcoinj.core.LegacyAddress; -import java.util.ArrayList; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicReference; @@ -85,32 +84,6 @@ public boolean addUtxo(UTXO utxo) { } } - public void setUtxos(List recvUtxos) { - synchronized (this) { - List newUtxos = new ArrayList<>(); - - if (this.utxos.size() > 0) { - for (UTXO utxo : recvUtxos) { - for (UTXO bUtxo : this.utxos) { - if (!utxo.getTxid().equals(bUtxo.getTxid()) || utxo.getVout() != bUtxo.getVout()) { - newUtxos.add(utxo); - } - } - } - - if (newUtxos.size() > 0) { - this.utxos.clear(); - this.utxos.addAll(newUtxos); - } - } else { - this.utxos.clear(); - this.utxos.addAll(recvUtxos); - } - - calculateBalance(); - } - } - private UTXO getUtxo(String txid, int vout) { return utxos.stream().filter(o -> o.getTxid().equals(txid) && o.getVout() == vout).findFirst().orElse(null); } From 752c271630d2b5417a484790bec0dfee6377c9be Mon Sep 17 00:00:00 2001 From: tryiou Date: Mon, 6 Apr 2026 19:16:46 +0200 Subject: [PATCH 42/73] perf(net): enhance concurrency and thread safety Optimize thread safety and concurrency across the networking and utility layers by implementing atomic operations and ensuring visibility of shared mutable state. - Apply `volatile` to shared variables in `App`, `CoinInstance`, `BlocknetPeer`, `BlocknetSeed`, and `UTXO` to ensure cross-thread visibility. - Replace manual state checks with atomic methods like `computeIfAbsent` and `updateAndGet` in `CoinInstance` and `JSONRPCController`. - Prevent race conditions in `EXRServer` capability probing using `synchronized` blocks. - Refactor `AddressBalance` to use final atomic references, removing lazy initialization logic. - Add null safety checks for `threadPool` shutdown in `BlocknetPeerGroup`. --- src/main/java/io/cloudchains/app/App.java | 4 +- .../io/cloudchains/app/net/CoinInstance.java | 33 +++------- .../app/net/api/JSONRPCController.java | 11 ++-- .../app/net/api/http/client/EXRServer.java | 64 ++++++++++--------- .../net/protocols/blocknet/BlocknetPeer.java | 6 +- .../protocols/blocknet/BlocknetPeerGroup.java | 4 +- .../net/protocols/blocknet/BlocknetSeed.java | 6 +- .../cloudchains/app/util/AddressBalance.java | 11 ++-- .../java/io/cloudchains/app/util/UTXO.java | 2 +- 9 files changed, 63 insertions(+), 78 deletions(-) diff --git a/src/main/java/io/cloudchains/app/App.java b/src/main/java/io/cloudchains/app/App.java index b988200..4683fef 100644 --- a/src/main/java/io/cloudchains/app/App.java +++ b/src/main/java/io/cloudchains/app/App.java @@ -27,8 +27,8 @@ public class App { public static String BASE_URL = "https://xliterevp.mywire.org/"; // "http://xl-dae-prox.airdns.org:42111/"; // DEBUG ENDPOINT - public static String EXR_ENDPOINT = null; - public static EXRServerPool exrServerPool = null; + public static volatile String EXR_ENDPOINT = null; + public static volatile EXRServerPool exrServerPool = null; public static HTTPClient feeUpdateHttpClient = new HTTPClient(2); public static HTTPClient heightUpdateHttpClient = new HTTPClient(2); public static JSONRPCMasterServer masterRPC = JSONRPCController.getMasterServer(); diff --git a/src/main/java/io/cloudchains/app/net/CoinInstance.java b/src/main/java/io/cloudchains/app/net/CoinInstance.java index 0e1f249..6f08b14 100644 --- a/src/main/java/io/cloudchains/app/net/CoinInstance.java +++ b/src/main/java/io/cloudchains/app/net/CoinInstance.java @@ -84,7 +84,7 @@ public String getMessage() { private static final int FORWARD_ADDRESS_COUNT = 0; private static final List coinInstances = new CopyOnWriteArrayList<>(); - private static CoinInstance activeCurrency; + private static volatile CoinInstance activeCurrency; private static CoinTicker activeBlocknetNetwork = null; private static CopyOnWriteArrayList> activeCoinChangedListeners = new CopyOnWriteArrayList<>(); private static ConcurrentHashMap blockCounts = new ConcurrentHashMap<>(); @@ -107,11 +107,11 @@ public String getMessage() { private int rpcPort = -1; private boolean testnet = false; private JSONRPCServer coinRPCServer = null; - private long lastUtxoUpdate = 0; - private int updateFailures = 0; + private volatile long lastUtxoUpdate = 0; + private final AtomicInteger updateFailures = new AtomicInteger(0); private int generatedAddressCount; private AddressDiscoveryService discoveryService = null; - private static boolean addressDiscoveryEnabled = true; + private static volatile boolean addressDiscoveryEnabled = true; private CoinInstance(CoinTicker ticker, ConfigHelper configHelper) { this.ticker = ticker; @@ -942,26 +942,13 @@ public WalletHelper getWalletHelper() { } public void addBlockCount(CoinTicker ticker, Integer blockCount) { - if (blockCounts.containsKey(ticker)) { - if (blockCounts.get(ticker).get() > blockCount) - return; - - blockCounts.get(ticker).set(blockCount); - return; - } - - blockCounts.put(ticker, new AtomicInteger(blockCount)); + blockCounts.computeIfAbsent(ticker, k -> new AtomicInteger(0)) + .updateAndGet(current -> Math.max(current, blockCount)); } public void addRelayFee(CoinTicker ticker, Double relayFee) { - if (relayFees.containsKey(ticker)) { - relayFees.get(ticker).set(relayFee); - return; - } - + relayFees.computeIfAbsent(ticker, k -> new AtomicDouble(relayFee)).set(relayFee); configHelper.setFee(relayFee); - - relayFees.put(ticker, new AtomicDouble(relayFee)); } public void addCloudTransaction(CloudTransaction cloudTransaction) { @@ -1087,11 +1074,11 @@ public static String getVersionString() { } public void incrementUpdateFailures() { - updateFailures += 1; + updateFailures.incrementAndGet(); } public void resetUpdateFailures() { - updateFailures = 0; + updateFailures.set(0); } public void runAddressDiscovery() { @@ -1128,7 +1115,7 @@ public void runAddressDiscovery() { } public boolean isInstanceRunning() { - return updateFailures < 5; + return updateFailures.get() < 5; } public static void setAddressDiscoveryEnabled(boolean enabled) { diff --git a/src/main/java/io/cloudchains/app/net/api/JSONRPCController.java b/src/main/java/io/cloudchains/app/net/api/JSONRPCController.java index d84fd61..72ec796 100644 --- a/src/main/java/io/cloudchains/app/net/api/JSONRPCController.java +++ b/src/main/java/io/cloudchains/app/net/api/JSONRPCController.java @@ -3,11 +3,11 @@ import io.cloudchains.app.net.CoinInstance; import io.cloudchains.app.util.ConfigHelper; -import java.util.HashMap; +import java.util.concurrent.ConcurrentHashMap; public class JSONRPCController { - private static final HashMap servers = new HashMap<>(); + private static final ConcurrentHashMap servers = new ConcurrentHashMap<>(); private static JSONRPCMasterServer masterServer = new JSONRPCMasterServer(new ConfigHelper("master").getMasterRpcPort()); public static JSONRPCMasterServer getMasterServer() { @@ -19,11 +19,8 @@ public static JSONRPCServer getRPCServer(CoinInstance coinInstance) { throw new IllegalArgumentException("Bad coin instance"); } - if (!servers.containsKey(coinInstance)) { - servers.put(coinInstance, new JSONRPCServer(coinInstance, coinInstance.getRPCPort())); - } - - return servers.get(coinInstance); + return servers.computeIfAbsent(coinInstance, + coin -> new JSONRPCServer(coin, coin.getRPCPort())); } public static void removeRPCServer(CoinInstance coinInstance) { diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/EXRServer.java b/src/main/java/io/cloudchains/app/net/api/http/client/EXRServer.java index 18aac12..c99d8d2 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/client/EXRServer.java +++ b/src/main/java/io/cloudchains/app/net/api/http/client/EXRServer.java @@ -18,7 +18,7 @@ public class EXRServer { private final String endpoint; private final EXRWrapper wrapper; private volatile boolean healthy; - private long lastHealthCheck; + private volatile long lastHealthCheck; private final Set supportedCoins; private volatile boolean capabilitiesProbed; // Use centralized configuration constants @@ -46,40 +46,42 @@ public boolean probeCapabilities() { if (capabilitiesProbed) { return true; } - if (!isHealthy()) { - return false; - } - try { - // CALL HEIGHTS ONCE - not per coin - JsonObject result = wrapper.executeGet("heights"); - if (result != null && result.has("result")) { - JsonObject heights = result.getAsJsonObject("result"); - // Extract ALL supported coins from single response - // Only include coins that have non-null values (null means not supported) - for (String coinName : heights.keySet()) { - JsonElement heightValue = heights.get(coinName); - if (heightValue.isJsonNull()) { - continue; // Skip unsupported coins (null values) - } - try { - CoinTicker coin = CoinTickerUtils.stringToTicker(coinName); - if (coin != null) { - supportedCoins.add(coin); + synchronized (this) { + if (capabilitiesProbed) { + return true; + } + if (!isHealthy()) { + return false; + } + try { + JsonObject result = wrapper.executeGet("heights"); + if (result != null && result.has("result")) { + JsonObject heights = result.getAsJsonObject("result"); + for (String coinName : heights.keySet()) { + JsonElement heightValue = heights.get(coinName); + if (heightValue.isJsonNull()) { + continue; + } + try { + CoinTicker coin = CoinTickerUtils.stringToTicker(coinName); + if (coin != null) { + supportedCoins.add(coin); + } + } catch (Exception e) { + LOGGER.log(Level.FINER, "[exr-server] Failed to map coin " + coinName, e); } - } catch (Exception e) { - LOGGER.log(Level.FINER, "[exr-server] Failed to map coin " + coinName, e); } } - } - capabilitiesProbed = true; - LOGGER.log(Level.INFO, "[exr-server] Probed capabilities for " + endpoint + ", supports: " + supportedCoins.size() + " coins: " + - supportedCoins.stream().map(CoinTickerUtils::tickerToString) - .reduce((a, b) -> a + ", " + b).orElse("none")); - return !supportedCoins.isEmpty(); - } catch (Exception e) { - LOGGER.log(Level.WARNING, "[exr-server] Failed to probe capabilities for " + endpoint, e); - return false; + capabilitiesProbed = true; + LOGGER.log(Level.INFO, "[exr-server] Probed capabilities for " + endpoint + ", supports: " + supportedCoins.size() + " coins: " + + supportedCoins.stream().map(CoinTickerUtils::tickerToString) + .reduce((a, b) -> a + ", " + b).orElse("none")); + return !supportedCoins.isEmpty(); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "[exr-server] Failed to probe capabilities for " + endpoint, e); + return false; + } } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeer.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeer.java index 8200b4d..5924b0f 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeer.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeer.java @@ -47,10 +47,10 @@ public class BlocknetPeer extends PeerSocketHandler { private final ReentrantLock lock = Threading.lock("BlocknetPeer"); - private boolean activePeer; - private boolean hasRequiredPlugins; + private volatile boolean activePeer; + private volatile boolean hasRequiredPlugins; - private boolean pastConnectionSuccess; + private volatile boolean pastConnectionSuccess; private BlocknetParameters params; private BlocknetSerializer serializer; diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeerGroup.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeerGroup.java index e5243cc..a9c2754 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeerGroup.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeerGroup.java @@ -196,7 +196,9 @@ public void stop() { try { clientManager.stopAsync(); clientManager.awaitTerminated(); - threadPool.shutdownNow(); + if (threadPool != null) { + threadPool.shutdownNow(); + } } catch (Exception e) { LOGGER.log(Level.WARNING, "[blocknet] Error stopping peer group", e); } diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetSeed.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetSeed.java index 4a58859..f9c72a5 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetSeed.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetSeed.java @@ -6,10 +6,10 @@ public class BlocknetSeed { private String address; private Integer port; - private int failCount; - private long lastFailTime; + private volatile int failCount; + private volatile long lastFailTime; - private boolean isActivePeer; + private volatile boolean isActivePeer; BlocknetSeed(String address, int port) { this.address = address; diff --git a/src/main/java/io/cloudchains/app/util/AddressBalance.java b/src/main/java/io/cloudchains/app/util/AddressBalance.java index 472a850..3d8361f 100644 --- a/src/main/java/io/cloudchains/app/util/AddressBalance.java +++ b/src/main/java/io/cloudchains/app/util/AddressBalance.java @@ -13,14 +13,15 @@ public class AddressBalance { private LegacyAddress address; private DumpedPrivateKey privateKey; - private AtomicReference addrProp = null; - private AtomicDouble balanceProp = null; + private final AtomicReference addrProp; + private final AtomicDouble balanceProp; private final CopyOnWriteArrayList utxos = new CopyOnWriteArrayList<>(); public AddressBalance(LegacyAddress address, DumpedPrivateKey privateKey) { this.address = address; this.privateKey = privateKey; - setAddrProp(address.toBase58()); + this.addrProp = new AtomicReference<>(address.toBase58()); + this.balanceProp = new AtomicDouble(0); } public LegacyAddress getAddress() { @@ -36,8 +37,6 @@ private void setAddrProp(String value) { } private AtomicReference addrProperty() { - if (addrProp == null) - addrProp = new AtomicReference("addrProp"); return addrProp; } @@ -50,8 +49,6 @@ private void setBalanceProp(double value) { } public AtomicDouble balanceProperty() { - if (balanceProp == null) - balanceProp = new AtomicDouble(0); return balanceProp; } diff --git a/src/main/java/io/cloudchains/app/util/UTXO.java b/src/main/java/io/cloudchains/app/util/UTXO.java index bda7edd..f56a770 100644 --- a/src/main/java/io/cloudchains/app/util/UTXO.java +++ b/src/main/java/io/cloudchains/app/util/UTXO.java @@ -18,7 +18,7 @@ public class UTXO { private int height; protected transient long value; private int vout; - private boolean spent; + private volatile boolean spent; protected CoinTicker ticker; public UTXO(CoinTicker ticker, String addressB58, String txid, int vout, int blockHeight, long value) { From 2a3514b4c25acc1b43bdbf46b7590c2727f3c45d Mon Sep 17 00:00:00 2001 From: tryiou Date: Tue, 7 Apr 2026 12:46:45 +0200 Subject: [PATCH 43/73] security: refactor crypto ops to use char[] instead of String for better memory safety --- .../io/cloudchains/app/crypto/KeyHandler.java | 60 ++++++++- .../io/cloudchains/app/crypto/LoginUtils.java | 43 ------ src/test/java/LoginUtilsTest.java | 122 ------------------ 3 files changed, 53 insertions(+), 172 deletions(-) delete mode 100644 src/main/java/io/cloudchains/app/crypto/LoginUtils.java delete mode 100644 src/test/java/LoginUtilsTest.java diff --git a/src/main/java/io/cloudchains/app/crypto/KeyHandler.java b/src/main/java/io/cloudchains/app/crypto/KeyHandler.java index 7705446..3fb6e64 100644 --- a/src/main/java/io/cloudchains/app/crypto/KeyHandler.java +++ b/src/main/java/io/cloudchains/app/crypto/KeyHandler.java @@ -16,7 +16,10 @@ import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; import java.io.*; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.time.LocalDateTime; @@ -139,9 +142,15 @@ public static List getBaseSeed(char[] passphrase) { if (data.version == VERSION_1_SHA1) { LOGGER.log(Level.INFO, "[security] Legacy V1 wallet detected — migrating to V2 (SHA-256/CBC)"); - String seed = decryptSeedEcb(passphrase, data.encrypted, data.salt); - migrateToNewFormat(passphrase, seed, file); - return Arrays.asList(seed.split("\\s+")); + char[] legacyPassphrase = null; + try { + legacyPassphrase = sha256ToChars(passphrase); + String seed = decryptSeedEcb(legacyPassphrase, data.encrypted, data.salt); + migrateToNewFormat(passphrase, seed, file); + return Arrays.asList(seed.split("\\s+")); + } finally { + if (legacyPassphrase != null) Arrays.fill(legacyPassphrase, '\0'); + } } String seed = decryptSeedCbc(passphrase, data.encrypted, data.salt, data.iv); return Arrays.asList(seed.split("\\s+")); @@ -233,13 +242,13 @@ public static List getMnemonicFromString(String mnemonic) { * @param password the password to evaluate * @return score in [0, 10] */ - public static int calculatePasswordStrength(String password) { - if (password.length() < 8) return 0; + public static int calculatePasswordStrength(char[] password) { + if (password.length < 8) return 0; - int score = password.length() >= 10 ? 2 : 1; + int score = password.length >= 10 ? 2 : 1; boolean hasDigit = false, hasLower = false, hasUpper = false, hasSpecial = false; - for (char c : password.toCharArray()) { + for (char c : password) { if (Character.isDigit(c)) { hasDigit = true; } else if (Character.isLowerCase(c)) { @@ -262,6 +271,43 @@ public static int calculatePasswordStrength(String password) { // Private — key derivation and cipher // ========================================================================= + /** + * Hash a raw password with SHA-256 and return the hex digest as a char[]. + * Used only for decrypting legacy V1 wallets where the passphrase was + * pre-hashed via {@code LoginUtils.loginToEntropy()} before PBKDF2. + */ + private static char[] sha256ToChars(char[] input) { + byte[] hash = null; + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + hash = digest.digest(encodeUtf8Chars(input)); + StringBuilder hex = new StringBuilder(); + for (byte b : hash) { + if ((0xff & b) < 0x10) + hex.append("0").append(Integer.toHexString(0xFF & b)); + else + hex.append(Integer.toHexString(0xFF & b)); + } + return hex.toString().toCharArray(); + } catch (Exception e) { + throw new RuntimeException("Failed to compute SHA-256 for legacy migration", e); + } finally { + if (hash != null) Arrays.fill(hash, (byte) 0); + } + } + + /** + * Encode a char[] as UTF-8 bytes without creating an intermediate String. + * Caller must zero the returned byte array after use. + */ + private static byte[] encodeUtf8Chars(char[] chars) { + ByteBuffer buf = StandardCharsets.UTF_8.encode( + CharBuffer.wrap(chars)); + byte[] bytes = new byte[buf.remaining()]; + buf.get(bytes); + return bytes; + } + /** * Derive a 256-bit AES key from a passphrase using PBKDF2. * The intermediate raw key bytes are zeroed before returning. diff --git a/src/main/java/io/cloudchains/app/crypto/LoginUtils.java b/src/main/java/io/cloudchains/app/crypto/LoginUtils.java deleted file mode 100644 index 02714fa..0000000 --- a/src/main/java/io/cloudchains/app/crypto/LoginUtils.java +++ /dev/null @@ -1,43 +0,0 @@ -package io.cloudchains.app.crypto; - -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.util.logging.Level; -import java.util.logging.LogManager; -import java.util.logging.Logger; - -public class LoginUtils { - private final static LogManager LOGMANAGER = LogManager.getLogManager(); - private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); - - private static String toSha256(String message) { - try { - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - digest.update(message.getBytes(StandardCharsets.UTF_8)); - byte[] hash = digest.digest(); - StringBuilder hex = new StringBuilder(); - for (byte b : hash) { - if ((0xff & b) < 0x10) - hex.append("0").append(Integer.toHexString((0xFF & b))); - else - hex.append(Integer.toHexString(0xFF & b)); - } - return hex.toString(); - } catch (Exception e) { - LOGGER.log(Level.WARNING, "[security] Error hashing message with SHA-256", e); - } - return null; - } - - public static String loginToEntropy(String password) { - String shaPassword = toSha256(password); - - if (shaPassword == null) { - LOGGER.log(Level.FINER, "Password hashing failed"); - return null; - } - - return shaPassword; - } - -} diff --git a/src/test/java/LoginUtilsTest.java b/src/test/java/LoginUtilsTest.java deleted file mode 100644 index ebeadfb..0000000 --- a/src/test/java/LoginUtilsTest.java +++ /dev/null @@ -1,122 +0,0 @@ -import io.cloudchains.app.crypto.LoginUtils; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * Test class for LoginUtils functionality. - * Tests password hashing and entropy generation. - */ -class LoginUtilsTest { - - @Test - void testLoginToEntropy_ValidPassword() { - String password = "Test^1234"; - String result = LoginUtils.loginToEntropy(password); - - assertNotNull(result); - assertFalse(result.isEmpty()); - assertEquals(64, result.length()); // SHA-256 produces 64 hex characters - } - - @Test - void testLoginToEntropy_DifferentPasswords() { - String password1 = "password123"; - String password2 = "password456"; - - String result1 = LoginUtils.loginToEntropy(password1); - String result2 = LoginUtils.loginToEntropy(password2); - - assertNotNull(result1); - assertNotNull(result2); - assertNotEquals(result1, result2); - } - - @Test - void testLoginToEntropy_SamePasswordConsistency() { - String password = "consistentPassword"; - - String result1 = LoginUtils.loginToEntropy(password); - String result2 = LoginUtils.loginToEntropy(password); - - assertNotNull(result1); - assertNotNull(result2); - assertEquals(result1, result2); - } - - @Test - void testLoginToEntropy_EmptyPassword() { - String password = ""; - String result = LoginUtils.loginToEntropy(password); - - assertNotNull(result); - assertFalse(result.isEmpty()); - assertEquals(64, result.length()); - } - - @Test - void testLoginToEntropy_SpecialCharacters() { - String password = "!@#$%^&*()_+-=[]{}|;:,.<>?"; - String result = LoginUtils.loginToEntropy(password); - - assertNotNull(result); - assertFalse(result.isEmpty()); - assertEquals(64, result.length()); - } - - @Test - void testLoginToEntropy_Whitespace() { - String password1 = "password"; - String password2 = " password "; - String password3 = "password "; - - String result1 = LoginUtils.loginToEntropy(password1); - String result2 = LoginUtils.loginToEntropy(password2); - String result3 = LoginUtils.loginToEntropy(password3); - - assertNotNull(result1); - assertNotNull(result2); - assertNotNull(result3); - - assertNotEquals(result1, result2); - assertNotEquals(result1, result3); - assertNotEquals(result2, result3); - } - - @Test - void testLoginToEntropy_LongPassword() { - StringBuilder longPassword = new StringBuilder(); - for (int i = 0; i < 1000; i++) { - longPassword.append("a"); - } - - String result = LoginUtils.loginToEntropy(longPassword.toString()); - - assertNotNull(result); - assertFalse(result.isEmpty()); - assertEquals(64, result.length()); - } - - @Test - void testLoginToEntropy_NullSafety() { - // Test that the method handles edge cases gracefully - String result = LoginUtils.loginToEntropy("Test^1234"); - assertNotNull(result); - - // Verify it's a valid SHA-256 hash (64 hex characters) - assertTrue(result.matches("[a-f0-9]{64}")); - } - - @Test - void testLoginToEntropy_CaseSensitivity() { - String password1 = "Password"; - String password2 = "password"; - - String result1 = LoginUtils.loginToEntropy(password1); - String result2 = LoginUtils.loginToEntropy(password2); - - assertNotNull(result1); - assertNotNull(result2); - assertNotEquals(result1, result2); - } -} \ No newline at end of file From bfcc3170f6db7385d853404c9b10d41ad1edbda6 Mon Sep 17 00:00:00 2001 From: tryiou Date: Tue, 7 Apr 2026 12:56:06 +0200 Subject: [PATCH 44/73] refactor: update CoinInstance and HTTPServerHandler to handle char[] passwords --- .../io/cloudchains/app/net/CoinInstance.java | 101 +++++++++--------- .../api/http/server/HTTPServerHandler.java | 6 +- 2 files changed, 53 insertions(+), 54 deletions(-) diff --git a/src/main/java/io/cloudchains/app/net/CoinInstance.java b/src/main/java/io/cloudchains/app/net/CoinInstance.java index 6f08b14..feb1d8a 100644 --- a/src/main/java/io/cloudchains/app/net/CoinInstance.java +++ b/src/main/java/io/cloudchains/app/net/CoinInstance.java @@ -127,22 +127,17 @@ private CoinInstance(CoinTicker ticker, ConfigHelper configHelper) { * Return mnemonic seed from wallet stored on disk. Correct passphrase required. * Returns empty string on error or failure to retrieve mnemonic (or if mnemonic * doesn't exist). - * @param pw String + * @param pw caller-owned char array; must be zeroed by the caller after use * @return String */ - public static String getMnemonicForPw(String pw) { + public static String getMnemonicForPw(char[] pw) { if (!KeyHandler.existsBaseECKeyFromLocal()) return ""; - char[] passphrase = pw.toCharArray(); - try { - List seed = KeyHandler.getBaseSeed(passphrase); - if (seed == null) - return ""; - return Joiner.on(" ").join(seed); - } finally { - Arrays.fill(passphrase, '\0'); - } + List seed = KeyHandler.getBaseSeed(pw); + if (seed == null) + return ""; + return Joiner.on(" ").join(seed); } public static int getBlockCountByTicker(CoinTicker ticker) { @@ -285,42 +280,34 @@ public static CoinInstance getInstance(CoinTicker ticker) { /** * Change the password. Recreates the wallet file and encrypts with new password. - * @param oldPassword - * @param newPassword + * @param oldPassword caller-owned char array; must be zeroed by the caller after use + * @param newPassword caller-owned char array; must be zeroed by the caller after use * @return Error or null */ - public static CoinError changePassword(String oldPassword, String newPassword) { + public static CoinError changePassword(char[] oldPassword, char[] newPassword) { if (!KeyHandler.existsBaseECKeyFromLocal()) { LOGGER.log(Level.WARNING, "[wallet] Unable to change the password: Wallet not found on disk"); return new CoinError("Unable to change the password: Wallet not found on disk", CoinError.CoinErrorCode.CHANGEPASSWORDFAILED); } - char[] oldPassphrase = oldPassword.toCharArray(); - char[] newPassphrase = newPassword.toCharArray(); - try { - List baseSeed = KeyHandler.getBaseSeed(oldPassphrase); - if (baseSeed == null) { - LOGGER.log(Level.WARNING, "[wallet] Unable to change the password: Incorrect password"); - return new CoinError("Unable to change the password: Incorrect password", - CoinError.CoinErrorCode.CHANGEPASSWORDFAILED); - } - - // Get current wallet seed - DeterministicSeed seed = new DeterministicSeed(baseSeed, null, "", System.currentTimeMillis() / 1000); - List mnemonic = seed.getMnemonicCode(); + List baseSeed = KeyHandler.getBaseSeed(oldPassword); + if (baseSeed == null) { + LOGGER.log(Level.WARNING, "[wallet] Unable to change the password: Incorrect password"); + return new CoinError("Unable to change the password: Incorrect password", + CoinError.CoinErrorCode.CHANGEPASSWORDFAILED); + } - if (!KeyHandler.importFromMnemonic(mnemonic, newPassphrase)) { - LOGGER.log(Level.WARNING, "[wallet] Unable to change the password: Failed to create new wallet file"); - return new CoinError("Unable to change the password: Failed to create new wallet file", - CoinError.CoinErrorCode.CHANGEPASSWORDFAILED); - } + DeterministicSeed seed = new DeterministicSeed(baseSeed, null, "", System.currentTimeMillis() / 1000); + List mnemonic = seed.getMnemonicCode(); - return null; - } finally { - Arrays.fill(oldPassphrase, '\0'); - Arrays.fill(newPassphrase, '\0'); + if (!KeyHandler.importFromMnemonic(mnemonic, newPassword)) { + LOGGER.log(Level.WARNING, "[wallet] Unable to change the password: Failed to create new wallet file"); + return new CoinError("Unable to change the password: Failed to create new wallet file", + CoinError.CoinErrorCode.CHANGEPASSWORDFAILED); } + + return null; } public NetworkParameters getNetworkParameters() { @@ -350,11 +337,11 @@ public void deinit() { } } - public CoinError init(String pw, String userMnemonic, boolean isMnemonic) { + public CoinError init(char[] pw, String userMnemonic, boolean isMnemonic) { return init(pw, userMnemonic, isMnemonic, false); } - public CoinError init(String pw, String userMnemonic, boolean isMnemonic, boolean xliteRPC) { + public CoinError init(char[] pw, String userMnemonic, boolean isMnemonic, boolean xliteRPC) { switch (ticker) { case BLOCKNET: { LOGGER.log(Level.FINE, "[coin] Initializing for Blocknet main network."); @@ -494,7 +481,7 @@ public CoinError init(String pw, String userMnemonic, boolean isMnemonic, boolea boolean existsOnDisk = false; if (isMnemonic) { - baseSeed = Arrays.asList(pw.split(" ")); + baseSeed = splitMnemonicChars(pw); } else { if (KeyHandler.existsBaseECKeyFromLocal()) { existsOnDisk = true; @@ -502,23 +489,13 @@ public CoinError init(String pw, String userMnemonic, boolean isMnemonic, boolea LOGGER.log(Level.WARNING, "[wallet] Wallet already exists on disk, ignoring provided mnemonic"); } } else if (userMnemonic != null) { - char[] importPassphrase = pw.toCharArray(); - try { - if (!KeyHandler.importFromMnemonic(Arrays.asList(userMnemonic.split(" ")), importPassphrase)) { - LOGGER.log(Level.WARNING, "[wallet] Unable to create wallet from mnemonic"); - return new CoinError("Unable to create wallet from mnemonic", CoinError.CoinErrorCode.BADMNEMONIC); - } - } finally { - Arrays.fill(importPassphrase, '\0'); + if (!KeyHandler.importFromMnemonic(Arrays.asList(userMnemonic.split(" ")), pw)) { + LOGGER.log(Level.WARNING, "[wallet] Unable to create wallet from mnemonic"); + return new CoinError("Unable to create wallet from mnemonic", CoinError.CoinErrorCode.BADMNEMONIC); } } - char[] readPassphrase = pw.toCharArray(); - try { - baseSeed = KeyHandler.getBaseSeed(readPassphrase); - } finally { - Arrays.fill(readPassphrase, '\0'); - } + baseSeed = KeyHandler.getBaseSeed(pw); } if (baseSeed == null) { @@ -1125,4 +1102,22 @@ public static void setAddressDiscoveryEnabled(boolean enabled) { public static boolean isAddressDiscoveryEnabled() { return addressDiscoveryEnabled; } + + /** + * Split a mnemonic char[] into individual word strings without + * materializing the full mnemonic as a String. + */ + private static List splitMnemonicChars(char[] chars) { + List words = new ArrayList<>(); + int start = 0; + for (int i = 0; i <= chars.length; i++) { + if (i == chars.length || chars[i] == ' ') { + if (i > start) { + words.add(new String(chars, start, i - start)); + } + start = i + 1; + } + } + return words; + } } \ No newline at end of file diff --git a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java b/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java index e0e8759..abac9a6 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java +++ b/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java @@ -31,6 +31,7 @@ import java.math.BigInteger; import java.math.RoundingMode; import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; import java.security.SignatureException; import java.util.ArrayList; import java.util.Arrays; @@ -120,7 +121,10 @@ protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) headerUser = values[0]; headerPass = values[1]; - if (headerUser.equals(configHelper.getRpcUsername()) && headerPass.equals(configHelper.getRpcPassword())) { + if (MessageDigest.isEqual(headerUser.getBytes(StandardCharsets.UTF_8), + configHelper.getRpcUsername().getBytes(StandardCharsets.UTF_8)) + && MessageDigest.isEqual(headerPass.getBytes(StandardCharsets.UTF_8), + configHelper.getRpcPassword().getBytes(StandardCharsets.UTF_8))) { successfulAuth = true; LOGGER.log(Level.FINER, "[http-server-handler] Successful Auth"); } From 0836296805603a30dc1f2a5e8c79be1bf83e37c1 Mon Sep 17 00:00:00 2001 From: tryiou Date: Tue, 7 Apr 2026 12:58:04 +0200 Subject: [PATCH 45/73] refactor: update CLI menus and tests to match char[] password API --- .gitignore | 2 +- .../io/cloudchains/app/console/ArgMenu.java | 198 ++++++------ .../cloudchains/app/console/ConsoleMenu.java | 298 +++++++++--------- .../java/AddressDiscoveryServiceTest.java | 9 +- src/test/java/CoinInstanceTest.java | 39 ++- src/test/java/KeyHandlerTest.java | 36 ++- src/test/java/TestHelper.java | 2 +- 7 files changed, 321 insertions(+), 263 deletions(-) diff --git a/.gitignore b/.gitignore index b0a4112..5a52d09 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,7 @@ .env .aider* .idea* - +.kilo* # Maven-specific ignores target/ dependency-reduced-pom.xml diff --git a/src/main/java/io/cloudchains/app/console/ArgMenu.java b/src/main/java/io/cloudchains/app/console/ArgMenu.java index fd7b74d..1f3e23d 100644 --- a/src/main/java/io/cloudchains/app/console/ArgMenu.java +++ b/src/main/java/io/cloudchains/app/console/ArgMenu.java @@ -1,98 +1,100 @@ -package io.cloudchains.app.console; - -import io.cloudchains.app.crypto.KeyHandler; -import io.cloudchains.app.crypto.LoginUtils; -import io.cloudchains.app.net.CoinInstance; -import io.cloudchains.app.net.CoinTicker; - -public class ArgMenu { - private String[] arguments; - - public ArgMenu(String[] args) { - this.arguments = args; - } - - public void init() { - int selection = 2; - String password = ""; - - System.out.println("-------------------------"); - System.out.println("Help: "); - System.out.println("Create new wallet: --new-wallet (password)"); - System.out.println("Decrypt wallet: --decrypt-wallet (password)"); - - if (arguments.length == 0) { - System.out.println("No arguments given."); - System.exit(0); - } else { - if (arguments.length == 1) { - password = arguments[0]; - } else if (arguments.length == 2 && arguments[0].equals("--new-wallet")) { - selection = 1; - password = arguments[1]; - } else if (arguments.length == 2 && arguments[0].equals("--decrypt-wallet")) { - password = arguments[1]; - } - } - - String entropy = null; - - switch (selection) { - case 1: { - if (KeyHandler.existsBaseECKeyFromLocal()) { - System.out.println("Key already exists"); - return; - } - - int strength = KeyHandler.calculatePasswordStrength(password); - - if (!KeyHandler.existsBaseECKeyFromLocal() && strength < 9) { - System.out.println("Bad password."); - return; - } - - entropy = LoginUtils.loginToEntropy(password); - break; - } - case 2: { - int strength = KeyHandler.calculatePasswordStrength(password); - - if (!KeyHandler.existsBaseECKeyFromLocal() && strength < 9) { - System.out.println("Bad password."); - return; - } - - entropy = LoginUtils.loginToEntropy(password); - break; - } - case 3: { - System.out.println("Exiting..."); - System.exit(0); - } - break; - default: - throw new IllegalStateException("Unexpected value: " + selection); - } - - if (entropy == null) - return; - - completeLogin(entropy, null); - } - - private void completeLogin(String entropy, String userMnemonic) { - CoinInstance.CoinError coinError = CoinInstance.getInstance(CoinTicker.BLOCKNET).init(entropy, userMnemonic, false); - if (coinError != null) { - System.out.println("[master] Error(" + coinError.getCode().name() + "): " + coinError.getMessage()); - System.exit(0); - } - - for (CoinTicker cointicker : CoinTicker.coins()) { - if (cointicker == CoinTicker.BLOCKNET || cointicker == CoinTicker.BLOCKNET_TESTNET5 || cointicker == CoinTicker.BITCOIN) - continue; - coinError = CoinInstance.getInstance(cointicker).init(entropy, userMnemonic, false); - if (coinError != null) - System.out.println("[" + cointicker.name() + "] Error(" + coinError.getCode().name() + "): " + coinError.getMessage()); - } - } -} +package io.cloudchains.app.console; + +import io.cloudchains.app.crypto.KeyHandler; +import io.cloudchains.app.net.CoinInstance; +import io.cloudchains.app.net.CoinTicker; + +import java.util.Arrays; + +public class ArgMenu { + private String[] arguments; + + public ArgMenu(String[] args) { + this.arguments = args; + } + + public void init() { + int selection = 2; + char[] password = null; + + System.out.println("-------------------------"); + System.out.println("Help: "); + System.out.println("Create new wallet: --new-wallet (password)"); + System.out.println("Decrypt wallet: --decrypt-wallet (password)"); + + if (arguments.length == 0) { + System.out.println("No arguments given."); + System.exit(0); + } else { + if (arguments.length == 1) { + password = arguments[0].toCharArray(); + } else if (arguments.length == 2 && arguments[0].equals("--new-wallet")) { + selection = 1; + password = arguments[1].toCharArray(); + } else if (arguments.length == 2 && arguments[0].equals("--decrypt-wallet")) { + password = arguments[1].toCharArray(); + } + } + + try { + if (password == null) { + System.out.println("Unrecognized arguments. Use --new-wallet or --decrypt-wallet ."); + return; + } + switch (selection) { + case 1: { + if (KeyHandler.existsBaseECKeyFromLocal()) { + System.out.println("Key already exists"); + return; + } + + int strength = KeyHandler.calculatePasswordStrength(password); + + if (!KeyHandler.existsBaseECKeyFromLocal() && strength < 9) { + System.out.println("Bad password."); + return; + } + + completeLogin(password, null); + break; + } + case 2: { + int strength = KeyHandler.calculatePasswordStrength(password); + + if (!KeyHandler.existsBaseECKeyFromLocal() && strength < 9) { + System.out.println("Bad password."); + return; + } + + completeLogin(password, null); + break; + } + case 3: { + System.out.println("Exiting..."); + System.exit(0); + } + break; + default: + throw new IllegalStateException("Unexpected value: " + selection); + } + } finally { + if (password != null) Arrays.fill(password, '\0'); + } + } + + private void completeLogin(char[] password, String userMnemonic) { + CoinInstance.CoinError coinError = CoinInstance.getInstance(CoinTicker.BLOCKNET).init(password, userMnemonic, false); + if (coinError != null) { + System.out.println("[master] Error(" + coinError.getCode().name() + "): " + coinError.getMessage()); + System.exit(0); + } + + for (CoinTicker cointicker : CoinTicker.coins()) { + if (cointicker == CoinTicker.BLOCKNET || cointicker == CoinTicker.BLOCKNET_TESTNET5 || cointicker == CoinTicker.BITCOIN) + continue; + coinError = CoinInstance.getInstance(cointicker).init(password, userMnemonic, false); + if (coinError != null) + System.out.println("[" + cointicker.name() + "] Error(" + coinError.getCode().name() + "): " + coinError.getMessage()); + } + } +} diff --git a/src/main/java/io/cloudchains/app/console/ConsoleMenu.java b/src/main/java/io/cloudchains/app/console/ConsoleMenu.java index 98b2d03..acdb085 100644 --- a/src/main/java/io/cloudchains/app/console/ConsoleMenu.java +++ b/src/main/java/io/cloudchains/app/console/ConsoleMenu.java @@ -3,7 +3,6 @@ import io.cloudchains.app.App; import io.cloudchains.app.Version; import io.cloudchains.app.crypto.KeyHandler; -import io.cloudchains.app.crypto.LoginUtils; import io.cloudchains.app.net.CoinInstance; import io.cloudchains.app.net.CoinTicker; import io.cloudchains.app.net.CoinTickerUtils; @@ -71,7 +70,6 @@ public void init() { autoGenerateRPCConfig(); System.exit(0); case "--development-endpoint": { - // sample endpoint url: "https://utils.blocknet.org/" if (i + 1 < arguments.length) { String customEndpoint = arguments[i + 1]; if (customEndpoint.startsWith("--")) { @@ -139,22 +137,21 @@ public void init() { System.exit(0); } - String password = readPassword(input, arguments, i + 1, "", "WALLET_PASSWORD"); - int strength = KeyHandler.calculatePasswordStrength(password); - if (strength < 9) { - logBadPassword(null); - System.exit(1); - } - - char[] passphrase = LoginUtils.loginToEntropy(password).toCharArray(); + char[] password = readPasswordChars(input, arguments, i + 1, "", "WALLET_PASSWORD"); try { - List mnemonic = KeyHandler.getBaseSeed(passphrase); + int strength = KeyHandler.calculatePasswordStrength(password); + if (strength < 9) { + logBadPassword(null); + System.exit(1); + } + + List mnemonic = KeyHandler.getBaseSeed(password); if (mnemonic == null) { logBadPassword(null); System.exit(1); } } finally { - Arrays.fill(passphrase, '\0'); + Arrays.fill(password, '\0'); } System.exit(0); @@ -165,61 +162,63 @@ public void init() { System.exit(0); } - String password = readPassword(input, arguments, i + 1, "", "WALLET_PASSWORD"); + char[] password = readPasswordChars(input, arguments, i + 1, "", "WALLET_PASSWORD"); String mnemonic = readPassword(input, arguments, i + 2, "Mnemonic:\n", "WALLET_MNEMONIC").trim(); - int strength = KeyHandler.calculatePasswordStrength(password); - if (strength < 9) { - logBadPassword(null); - System.exit(1); - } - if (mnemonic.isEmpty()) { - logBadMnemonic(); - System.exit(1); - } - - char[] passphrase = LoginUtils.loginToEntropy(password).toCharArray(); try { - if (!KeyHandler.importFromMnemonic(Arrays.asList(mnemonic.split(" ")), passphrase)) { + int strength = KeyHandler.calculatePasswordStrength(password); + if (strength < 9) { + logBadPassword(null); + System.exit(1); + } + if (mnemonic.isEmpty()) { + logBadMnemonic(); + System.exit(1); + } + + if (!KeyHandler.importFromMnemonic(Arrays.asList(mnemonic.split(" ")), password)) { logBadMnemonic(); System.exit(1); } } finally { - Arrays.fill(passphrase, '\0'); + Arrays.fill(password, '\0'); } System.exit(0); } case "--xliterpc": { - // Increment RPC port by 1 xliteRPC = true; - break; } case "--password": { - String password = readPassword(input, arguments, i + 1, "", "WALLET_PASSWORD"); - int strength = KeyHandler.calculatePasswordStrength(password); + char[] password = readPasswordChars(input, arguments, i + 1, "", "WALLET_PASSWORD"); + try { + int strength = KeyHandler.calculatePasswordStrength(password); - if (!KeyHandler.existsBaseECKeyFromLocal() && strength < 9) { - LOGGER.log(Level.INFO, "Bad password."); - System.exit(1); - } + if (!KeyHandler.existsBaseECKeyFromLocal() && strength < 9) { + LOGGER.log(Level.INFO, "Bad password."); + System.exit(1); + } - String entropy = LoginUtils.loginToEntropy(password); - completeLogin(entropy, null, false); + completeLogin(password, null, false); + } finally { + Arrays.fill(password, '\0'); + } return; } case "--getmnemonic": { - String password = readPassword(input, arguments, i + 1, "", "WALLET_PASSWORD"); + char[] password = readPasswordChars(input, arguments, i + 1, "", "WALLET_PASSWORD"); + try { + if (!KeyHandler.existsBaseECKeyFromLocal()) { + LOGGER.log(Level.INFO, "No wallet found."); + System.exit(1); + } - if (!KeyHandler.existsBaseECKeyFromLocal()) { - LOGGER.log(Level.INFO, "No wallet found."); - System.exit(1); + String mnemonic = CoinInstance.getMnemonicForPw(password); + System.out.println(mnemonic); + } finally { + Arrays.fill(password, '\0'); } - - String entropy = LoginUtils.loginToEntropy(password); - String mnemonic = CoinInstance.getMnemonicForPw(entropy); - System.out.println(mnemonic); System.exit(0); } case "--changepassword": { @@ -228,30 +227,33 @@ public void init() { System.exit(1); } - String currentPassword = readPassword(input, arguments, i + 1, "", "WALLET_PASSWORD"); - String newPassword = readPassword(input, arguments, i + 2, "", null); - if (currentPassword.isEmpty() || newPassword.isEmpty()) { - LOGGER.log(Level.INFO, "Password cannot be empty"); - System.exit(1); - } - if (currentPassword.equals(newPassword)) { - LOGGER.log(Level.INFO, "New password must be different from old password"); - System.exit(1); - } + char[] currentPassword = readPasswordChars(input, arguments, i + 1, "", "WALLET_PASSWORD"); + char[] newPassword = readPasswordChars(input, arguments, i + 2, "", null); + try { + if (currentPassword.length == 0 || newPassword.length == 0) { + LOGGER.log(Level.INFO, "Password cannot be empty"); + System.exit(1); + } + if (Arrays.equals(currentPassword, newPassword)) { + LOGGER.log(Level.INFO, "New password must be different from old password"); + System.exit(1); + } - // Check new password strength - int strength = KeyHandler.calculatePasswordStrength(newPassword); - if (strength < 9) { - LOGGER.log(Level.INFO, "Unable to change the password: New password is not strong enough"); - System.exit(1); - } + int strength = KeyHandler.calculatePasswordStrength(newPassword); + if (strength < 9) { + LOGGER.log(Level.INFO, "Unable to change the password: New password is not strong enough"); + System.exit(1); + } - CoinInstance.CoinError err = CoinInstance.changePassword(LoginUtils.loginToEntropy(currentPassword), - LoginUtils.loginToEntropy(newPassword)); - if (err != null) - logBadChangePass(err.getMessage()); - else - LOGGER.log(Level.INFO, "Wallet password changed successfully"); + CoinInstance.CoinError err = CoinInstance.changePassword(currentPassword, newPassword); + if (err != null) + logBadChangePass(err.getMessage()); + else + LOGGER.log(Level.INFO, "Wallet password changed successfully"); + } finally { + Arrays.fill(currentPassword, '\0'); + Arrays.fill(newPassword, '\0'); + } System.exit(0); } @@ -262,36 +264,36 @@ public void init() { } } - if (App.getEnv("WALLET_MNEMONIC") != null) { - String mnemonicImport = App.getEnv("WALLET_MNEMONIC"); - if (mnemonicImport == null) { - LOGGER.log(Level.INFO, "Bad mnemonic."); - return; + String mnemonicImport = App.getEnv("WALLET_MNEMONIC"); + if (mnemonicImport != null && !mnemonicImport.isEmpty()) { + char[] mnemonicChars = mnemonicImport.toCharArray(); + try { + completeLogin(mnemonicChars, null, true); + } finally { + Arrays.fill(mnemonicChars, '\0'); } - - completeLogin(mnemonicImport, null, true); return; - } else if (App.getEnv("WALLET_PASSWORD") != null) { - String password = App.getEnv("WALLET_PASSWORD"); - if (password == null) { - LOGGER.log(Level.INFO, "Bad password."); - return; - } + } else { + String passwordEnv = App.getEnv("WALLET_PASSWORD"); + if (passwordEnv != null && !passwordEnv.isEmpty()) { + char[] password = passwordEnv.toCharArray(); + try { + int strength = KeyHandler.calculatePasswordStrength(password); - int strength = KeyHandler.calculatePasswordStrength(password); + if (!KeyHandler.existsBaseECKeyFromLocal() && strength < 9) { + LOGGER.log(Level.INFO, "Bad password."); + return; + } - if (!KeyHandler.existsBaseECKeyFromLocal() && strength < 9) { - LOGGER.log(Level.INFO, "Bad password."); + completeLogin(password, null, false); + } finally { + Arrays.fill(password, '\0'); + } return; } - - completeLogin(LoginUtils.loginToEntropy(password), null, false); - return; } - String entropy = null; - - while (entropy == null) { + while (true) { LOGGER.log(Level.INFO, "-------------------------"); LOGGER.log(Level.INFO, "1 - Create new wallet " + newWalletStr); LOGGER.log(Level.INFO, "2 - Decrypt wallet"); @@ -300,7 +302,7 @@ public void init() { LOGGER.log(Level.INFO, "Selection: "); selection = input.nextInt(); - input.nextLine(); // clear buffer + input.nextLine(); switch (selection) { case 1: { @@ -310,47 +312,59 @@ public void init() { } Console console = System.console(); - String password; + char[] password; if (console != null) { - password = new String(console.readPassword("Enter new password: ")); + password = console.readPassword("Enter new password: "); } else { LOGGER.log(Level.INFO, "Enter new password: "); - password = input.next(); + password = input.next().toCharArray(); } - int strength = KeyHandler.calculatePasswordStrength(password); + try { + int strength = KeyHandler.calculatePasswordStrength(password); - if (!KeyHandler.existsBaseECKeyFromLocal() && strength < 9) { - LOGGER.log(Level.INFO, "Bad password."); - return; + if (!KeyHandler.existsBaseECKeyFromLocal() && strength < 9) { + LOGGER.log(Level.INFO, "Bad password."); + return; + } + completeLogin(password, null, false); + } finally { + Arrays.fill(password, '\0'); } - entropy = LoginUtils.loginToEntropy(password); - break; + return; } case 2: { LOGGER.log(Level.INFO, "Enter password: "); Console console = System.console(); - String password; + char[] password; if (console != null) { - password = new String(console.readPassword()); + password = console.readPassword(); } else { LOGGER.log(Level.WARNING, "Console not available, using Scanner fallback"); - password = readPassword(input, null, 0, "", null); + password = readPasswordChars(input, null, 0, "", null); } - int strength = KeyHandler.calculatePasswordStrength(password); + try { + int strength = KeyHandler.calculatePasswordStrength(password); - if (!KeyHandler.existsBaseECKeyFromLocal() && strength < 9) { - LOGGER.log(Level.INFO, "Bad password."); - return; + if (!KeyHandler.existsBaseECKeyFromLocal() && strength < 9) { + LOGGER.log(Level.INFO, "Bad password."); + return; + } + completeLogin(password, null, false); + } finally { + Arrays.fill(password, '\0'); } - - entropy = LoginUtils.loginToEntropy(password); - break; + return; } case 3: { LOGGER.log(Level.INFO, "Enter mnemonic: "); - String mnemonicImport = input.nextLine().trim(); + String mnemonicInput = input.nextLine().trim(); - completeLogin(mnemonicImport, null, true); + char[] mnemonicChars = mnemonicInput.toCharArray(); + try { + completeLogin(mnemonicChars, null, true); + } finally { + Arrays.fill(mnemonicChars, '\0'); + } return; } case 4: { @@ -362,9 +376,6 @@ public void init() { } } } - - input.close(); - completeLogin(entropy, null, false); } public void deinit() { @@ -377,17 +388,15 @@ public void deinit() { } } - private void completeLogin(String entropy, String userMnemonic, boolean isMnemonic) { - if (entropy == null && userMnemonic == null) { + private void completeLogin(char[] password, String userMnemonic, boolean isMnemonic) { + if (password == null && userMnemonic == null) { logBadPassword(null); System.exit(0); } long startTime = System.currentTimeMillis(); - // Wallet file already exists on disk at this point. Pass null for userMnemonic - // so CoinInstance reads the seed from disk rather than attempting to create it. - CoinInstance.CoinError coinError = CoinInstance.getInstance(CoinTicker.BLOCKNET).init(entropy, null, isMnemonic, xliteRPC); + CoinInstance.CoinError coinError = CoinInstance.getInstance(CoinTicker.BLOCKNET).init(password, null, isMnemonic, xliteRPC); if (coinError != null) { String msg = "[master] Error(" + coinError.getCode().name() + "): " + coinError.getMessage(); LOGGER.log(Level.SEVERE, msg); @@ -401,7 +410,7 @@ private void completeLogin(String entropy, String userMnemonic, boolean isMnemon } } - initializeCoinsConcurrently(otherCoins, entropy, null, isMnemonic, xliteRPC); + initializeCoinsConcurrently(otherCoins, password, null, isMnemonic, xliteRPC); long endTime = System.currentTimeMillis(); long totalTime = endTime - startTime; @@ -415,37 +424,26 @@ private void completeLogin(String entropy, String userMnemonic, boolean isMnemon } } - /** - * Initialize coins concurrently using CompletableFuture - * @param coinTickers List of coin tickers to initialize - * @param entropy Password entropy - * @param userMnemonic User mnemonic (if any) - * @param isMnemonic Whether the input is a mnemonic - * @param xliteRPC Whether to use xlite RPC - */ - private void initializeCoinsConcurrently(List coinTickers, String entropy, + private void initializeCoinsConcurrently(List coinTickers, char[] password, String userMnemonic, boolean isMnemonic, boolean xliteRPC) { if (coinTickers.isEmpty()) { return; } - // Create thread pool with number of coins (or a reasonable limit) - int threadCount = Math.min(coinTickers.size(), 8); // Limit to 8 threads max + int threadCount = Math.min(coinTickers.size(), 8); ExecutorService executor = Executors.newFixedThreadPool(threadCount); try { - // Filter to only enabled coins before initialization List enabledCoins = coinTickers.stream() .filter(ticker -> ticker == CoinTicker.BLOCKNET || CoinInstance.getInstance(ticker) != null) .collect(Collectors.toList()); - // Create CompletableFuture for each coin initialization CompletableFuture[] futures = enabledCoins.stream() .map(coinTicker -> CompletableFuture.runAsync(() -> { try { LOGGER.log(Level.FINE, "[coin] Initializing " + CoinTickerUtils.tickerToString(coinTicker) + " concurrently"); CoinInstance.CoinError coinError = CoinInstance.getInstance(coinTicker) - .init(entropy, userMnemonic, isMnemonic, xliteRPC); + .init(password, userMnemonic, isMnemonic, xliteRPC); if (coinError != null) { LOGGER.log(Level.WARNING, "[" + coinTicker.name() + "] Error(" + coinError.getCode().name() + "): " + coinError.getMessage()); @@ -456,12 +454,10 @@ private void initializeCoinsConcurrently(List coinTickers, String en }, executor)) .toArray(CompletableFuture[]::new); - // Wait for all initializations to complete CompletableFuture.allOf(futures).join(); } finally { - // Shutdown executor service executor.shutdown(); try { if (!executor.awaitTermination(30, TimeUnit.SECONDS)) { @@ -499,20 +495,10 @@ private String generateRandomString(int length) { return Base64.getUrlEncoder().withoutPadding().encodeToString(token); } - /** - * Reads the password from args, environment variable, or stdin (in that priority order). - * When no positional arg is available, checks the env var before falling back to stdin. - * @param input Stdin - * @param args Program arguments - * @param argPos Current arg position - * @param msg Message to display on stdin (defaults to "Password:\n" if empty) - * @param envVar Environment variable name to check as fallback (nullable) - * @return Password string - */ private String readPassword(Scanner input, String[] args, int argPos, String msg, String envVar) { if (msg.isEmpty()) msg = "Password:\n"; - if (args.length <= argPos || args[argPos].contains("--")) { + if (args == null || args.length <= argPos || args[argPos].contains("--")) { if (envVar != null) { String envVal = App.getEnv(envVar); if (envVal != null && !envVal.isEmpty()) @@ -524,7 +510,25 @@ private String readPassword(Scanner input, String[] args, int argPos, String msg return args[argPos]; } - // Function to display help information + /** + * Reads the password as a char[] from args, environment variable, or stdin. + * Caller MUST zero-fill the returned array after use. + */ + private char[] readPasswordChars(Scanner input, String[] args, int argPos, String msg, String envVar) { + if (msg.isEmpty()) + msg = "Password:\n"; + if (args == null || args.length <= argPos || args[argPos].contains("--")) { + if (envVar != null) { + String envVal = App.getEnv(envVar); + if (envVal != null && !envVal.isEmpty()) + return envVal.toCharArray(); + } + System.out.println(msg); + return input.nextLine().toCharArray(); + } + return args[argPos].toCharArray(); + } + private static void displayHelp() { System.out.print(getHelpText()); } diff --git a/src/test/java/AddressDiscoveryServiceTest.java b/src/test/java/AddressDiscoveryServiceTest.java index befe7a6..fb1aa00 100644 --- a/src/test/java/AddressDiscoveryServiceTest.java +++ b/src/test/java/AddressDiscoveryServiceTest.java @@ -1,6 +1,5 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; -import io.cloudchains.app.crypto.LoginUtils; import io.cloudchains.app.net.CoinInstance; import io.cloudchains.app.net.CoinTicker; import io.cloudchains.app.net.api.http.client.HTTPClient; @@ -12,6 +11,7 @@ import org.mockito.MockitoAnnotations; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; @@ -43,7 +43,12 @@ void setup() { coinInstance = CoinInstance.getInstance(CoinTicker.BLOCKNET); assertNotNull(coinInstance); coinInstance.getConfigHelper().setAddressCount(getAddressCountInitial()); - assertNull(coinInstance.init(LoginUtils.loginToEntropy(getPassword()), getMnemonic(), false)); + char[] pw = getPassword().toCharArray(); + try { + assertNull(coinInstance.init(pw, getMnemonic(), false)); + } finally { + Arrays.fill(pw, '\0'); + } mockHttpClient = mock(HTTPClient.class); AddressDiscoveryService.setDiscoveryTimeoutMs(30000); diff --git a/src/test/java/CoinInstanceTest.java b/src/test/java/CoinInstanceTest.java index 436606d..8f6a865 100644 --- a/src/test/java/CoinInstanceTest.java +++ b/src/test/java/CoinInstanceTest.java @@ -1,4 +1,3 @@ -import io.cloudchains.app.crypto.LoginUtils; import io.cloudchains.app.net.CoinInstance; import io.cloudchains.app.net.CoinTicker; import io.cloudchains.app.util.AddressBalance; @@ -7,6 +6,7 @@ import org.junit.jupiter.api.Test; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import static org.junit.jupiter.api.Assertions.*; @@ -23,7 +23,12 @@ void deterministicAddresses_fromMnemonic() { CoinInstance coin = CoinInstance.getInstance(CoinTicker.BLOCKNET); assertNotNull(coin); coin.getConfigHelper().setAddressCount(getAddressCount()); - assertNull(coin.init(LoginUtils.loginToEntropy(getPassword()), getMnemonic(), false)); + char[] pw = getPassword().toCharArray(); + try { + assertNull(coin.init(pw, getMnemonic(), false)); + } finally { + Arrays.fill(pw, '\0'); + } List addresses = coin.getAddressKeyPairs(); ArrayList actual = new ArrayList<>(); @@ -50,7 +55,12 @@ void deterministicAddresses_generateAddress() { CoinInstance coin = CoinInstance.getInstance(CoinTicker.BLOCKNET); assertNotNull(coin); coin.getConfigHelper().setAddressCount(getAddressCountInitial()); - assertNull(coin.init(LoginUtils.loginToEntropy(getPassword()), getMnemonic(), false)); + char[] pw = getPassword().toCharArray(); + try { + assertNull(coin.init(pw, getMnemonic(), false)); + } finally { + Arrays.fill(pw, '\0'); + } final int total = getAddressCount() - getAddressCountInitial() - 1; for (int i = 0; i < total; i++) @@ -81,11 +91,21 @@ void deterministicAddresses_generateForwardAddresses() { CoinInstance coin = CoinInstance.getInstance(CoinTicker.BLOCKNET); assertNotNull(coin); coin.getConfigHelper().setAddressCount(getAddressCountInitial()); - assertNull(coin.init(LoginUtils.loginToEntropy(getPassword()), getMnemonic(), false)); + char[] pw = getPassword().toCharArray(); + try { + assertNull(coin.init(pw, getMnemonic(), false)); + } finally { + Arrays.fill(pw, '\0'); + } // Reinit which triggers generate forward addresses coin.getConfigHelper().setAddressCount(getAddressCount()); - assertNull(coin.init(LoginUtils.loginToEntropy(getPassword()), null, false)); + char[] pw2 = getPassword().toCharArray(); + try { + assertNull(coin.init(pw2, null, false)); + } finally { + Arrays.fill(pw2, '\0'); + } List addresses = coin.getAddressKeyPairs(); ArrayList actual = new ArrayList<>(); @@ -110,7 +130,12 @@ void deterministicAddresses_generateForwardAddressesReloadConfig() { CoinInstance coin = CoinInstance.getInstance(CoinTicker.BLOCKNET); assertNotNull(coin); coin.getConfigHelper().setAddressCount(getAddressCountInitial()); - assertNull(coin.init(LoginUtils.loginToEntropy(getPassword()), getMnemonic(), false)); + char[] pw = getPassword().toCharArray(); + try { + assertNull(coin.init(pw, getMnemonic(), false)); + } finally { + Arrays.fill(pw, '\0'); + } for (int idx = getAddressCountInitial() * 2; idx < getAddressCount(); idx += getAddressCountInitial()) { // ReloadConfig with new address count triggers generate forward addresses @@ -143,4 +168,4 @@ void setup() { static void cleanup() { commonCleanup(); } -} \ No newline at end of file +} diff --git a/src/test/java/KeyHandlerTest.java b/src/test/java/KeyHandlerTest.java index 208023e..6c46a7a 100644 --- a/src/test/java/KeyHandlerTest.java +++ b/src/test/java/KeyHandlerTest.java @@ -11,6 +11,7 @@ import java.io.*; import java.nio.charset.StandardCharsets; import java.nio.file.Path; +import java.security.MessageDigest; import java.security.SecureRandom; import java.util.Arrays; import java.util.Base64; @@ -77,7 +78,7 @@ void setUp() { @Test @Order(1) void testPasswordStrengthTooShort() { - assertEquals(SCORE_TOO_SHORT, KeyHandler.calculatePasswordStrength("short")); + assertEquals(SCORE_TOO_SHORT, KeyHandler.calculatePasswordStrength("short".toCharArray())); } @Test @@ -85,7 +86,7 @@ void testPasswordStrengthTooShort() { void testPasswordStrengthEightCharLowercaseOnly() { // 1 (8-9 chars) + 2 (lowercase) = 3 assertEquals(SCORE_EIGHT_LOWERCASE_ONLY, - KeyHandler.calculatePasswordStrength("eightchr")); + KeyHandler.calculatePasswordStrength("eightchr".toCharArray())); } @Test @@ -93,8 +94,8 @@ void testPasswordStrengthEightCharLowercaseOnly() { void testPasswordStrengthNineCharSameAsEight() { // Both 8 and 9 characters should yield the same length bonus (+1). assertEquals( - KeyHandler.calculatePasswordStrength("eightchr"), - KeyHandler.calculatePasswordStrength("ninechars"), + KeyHandler.calculatePasswordStrength("eightchr".toCharArray()), + KeyHandler.calculatePasswordStrength("ninechars".toCharArray()), "8-char and 9-char passwords must receive the same length bonus" ); } @@ -104,14 +105,14 @@ void testPasswordStrengthNineCharSameAsEight() { void testPasswordStrengthTenPlusWithDigitAndLower() { // 2 (10+ chars) + 2 (digit) + 2 (lowercase) = 6 assertEquals(SCORE_TEN_LOWER_DIGIT, - KeyHandler.calculatePasswordStrength("tenchars12")); + KeyHandler.calculatePasswordStrength("tenchars12".toCharArray())); } @Test @Order(5) void testPasswordStrengthAllCriteria() { assertEquals(SCORE_ALL_CRITERIA, - KeyHandler.calculatePasswordStrength("StrongPass123!")); + KeyHandler.calculatePasswordStrength("StrongPass123!".toCharArray())); } // ========================================================================= @@ -336,13 +337,14 @@ void testLegacyMigrationCreatesLegacyBackup() throws IOException { */ private void createLegacyWalletFile() throws IOException { testKeyFile.getParentFile().mkdirs(); + char[] hashedPass = sha256Hex(TEST_PASSPHRASE); try { SecureRandom rng = new SecureRandom(); byte[] salt = new byte[20]; rng.nextBytes(salt); SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); - PBEKeySpec spec = new PBEKeySpec(TEST_PASSPHRASE.toCharArray(), salt, 16_384, 256); + PBEKeySpec spec = new PBEKeySpec(hashedPass, salt, 16_384, 256); SecretKey tmp = skf.generateSecret(spec); SecretKey key = new SecretKeySpec(tmp.getEncoded(), "AES"); spec.clearPassword(); @@ -361,6 +363,8 @@ private void createLegacyWalletFile() throws IOException { } } catch (Exception e) { throw new IOException("Failed to create legacy wallet file for test", e); + } finally { + Arrays.fill(hashedPass, '\0'); } } @@ -371,4 +375,22 @@ private String[] readKeyFileLines() throws IOException { return reader.lines().toArray(String[]::new); } } + + /** + * Replicate the legacy LoginUtils.loginToEntropy() behavior: + * SHA-256 hash the input and return the hex digest as a char[]. + */ + private static char[] sha256Hex(String input) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8)); + StringBuilder hex = new StringBuilder(); + for (byte b : hash) { + hex.append(String.format("%02x", b)); + } + return hex.toString().toCharArray(); + } catch (Exception e) { + throw new RuntimeException("Failed to compute SHA-256 for test", e); + } + } } \ No newline at end of file diff --git a/src/test/java/TestHelper.java b/src/test/java/TestHelper.java index 3f27c88..aa7b06b 100644 --- a/src/test/java/TestHelper.java +++ b/src/test/java/TestHelper.java @@ -18,7 +18,7 @@ /** * Test helper class providing shared utilities for all test files. - * This avoids code duplication across TestCoinInstance, TestConfigHelper, and TestLoginUtils. + * This avoids code duplication across test files. */ public class TestHelper { From 62d64f08ffb86b831c7084d0c8a8970ee62ba1ab Mon Sep 17 00:00:00 2001 From: tryiou Date: Fri, 17 Apr 2026 17:54:02 +0200 Subject: [PATCH 46/73] refactor(net): migrate fee handling to network parameter-based model Replace dynamic remote fee fetching with static fee values defined in network parameters. Each coin's network parameters now implement HasFeeParams interface providing getFeePerByte() and getMinTxFee(). Changes: - Remove relayFees map and related methods from CoinInstance - Remove HTTPClient.getAllFees() remote fee fetching - Add HasFeeParams interface with default implementations - Implement HasFeeParams in all NetworkParameters classes with coin-specific fee values - Add new getfees RPC endpoint returning fee data - Update WalletHelper to use network parameter fee methods - Update XRouterFeeUtils to use new fee calculation - Update ConfigHelper to store feePerByte and minTxFee as longs - Remove unused CCLogger class - Update tests to reflect new fee configuration format - Update README with new getfees command --- README.md | 1 + pom.xml | 2 +- src/main/java/io/cloudchains/app/App.java | 62 +++-- .../io/cloudchains/app/net/CoinInstance.java | 25 +- .../io/cloudchains/app/net/HasFeeParams.java | 11 + .../app/net/api/http/client/HTTPClient.java | 28 -- .../net/api/http/server/ExceptionHandler.java | 2 +- .../api/http/server/HTTPServerHandler.java | 20 +- .../alqocoin/AlqocoinNetworkParameters.java | 11 +- .../bitbay/BitbayNetworkParameters.java | 11 +- .../bitcoin/BitcoinNetworkParameters.java | 19 ++ .../BitcoinCashNetworkParameters.java | 11 +- .../blocknet/BlocknetNetworkParameters.java | 10 +- .../BlocknetTestnet5NetworkParameters.java | 11 +- .../dashcoin/DashcoinNetworkParameters.java | 11 +- .../digibyte/DigibyteNetworkParameters.java | 11 +- .../dogecoin/DogecoinNetworkParameters.java | 11 +- .../litecoin/LitecoinNetworkParameters.java | 11 +- .../phorecoin/PhorecoinNetworkParameters.java | 11 +- .../protocols/pivx/PivxNetworkParameters.java | 11 +- .../PocketcoinNetworkParameters.java | 11 +- .../poliscoin/PoliscoinNetworkParameters.java | 98 ------- .../ravencoin/RavencoinNetworkParameters.java | 11 +- .../syscoin/SyscoinNetworkParameters.java | 11 +- .../TrezarcoinNetworkParameters.java | 98 ------- .../UnobtaniumNetworkParameters.java | 11 +- .../app/net/xrouter/XRouterFeeUtils.java | 11 +- .../app/util/AddressDiscoveryService.java | 2 +- .../io/cloudchains/app/util/CCLogger.java | 22 -- .../io/cloudchains/app/util/ConfigHelper.java | 45 ++-- .../cloudchains/app/util/LogRotationUtil.java | 22 +- .../background/BackgroundTimerThread.java | 6 - .../cloudchains/app/wallet/WalletHelper.java | 182 ++++++++++--- src/test/java/ConfigHelperTest.java | 39 ++- src/test/java/WalletHelperFeeTest.java | 241 ++++++++++++++++++ 35 files changed, 664 insertions(+), 436 deletions(-) create mode 100644 src/main/java/io/cloudchains/app/net/HasFeeParams.java create mode 100644 src/main/java/io/cloudchains/app/net/protocols/bitcoin/BitcoinNetworkParameters.java delete mode 100644 src/main/java/io/cloudchains/app/net/protocols/poliscoin/PoliscoinNetworkParameters.java delete mode 100644 src/main/java/io/cloudchains/app/net/protocols/trezarcoin/TrezarcoinNetworkParameters.java delete mode 100644 src/main/java/io/cloudchains/app/util/CCLogger.java create mode 100644 src/test/java/WalletHelperFeeTest.java diff --git a/README.md b/README.md index bf1c56b..8bc09d4 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,7 @@ gettxout - Get info about an unspent transaction output =====Network===== getinfo - Get information such as balances, protocol version, and more getnetworkinfo - Get network information +getfees - Get fee per byte and minimum transaction fee for the current coin getrawmempool - Get raw mempool getblockchaininfo - Get blockchain info getblockhash - Get the hash of a block at a given height diff --git a/pom.xml b/pom.xml index a447cff..2b137ea 100644 --- a/pom.xml +++ b/pom.xml @@ -193,7 +193,7 @@ **/*Test*.java - -javaagent:${settings.localRepository}/net/bytebuddy/byte-buddy-agent/1.15.11/byte-buddy-agent-1.15.11.jar + -javaagent:${settings.localRepository}/net/bytebuddy/byte-buddy-agent/1.15.11/byte-buddy-agent-1.15.11.jar -Djava.util.logging.level=WARNING diff --git a/src/main/java/io/cloudchains/app/App.java b/src/main/java/io/cloudchains/app/App.java index 4683fef..a66a826 100644 --- a/src/main/java/io/cloudchains/app/App.java +++ b/src/main/java/io/cloudchains/app/App.java @@ -5,7 +5,6 @@ import io.cloudchains.app.net.api.JSONRPCMasterServer; import io.cloudchains.app.net.api.http.client.EXRServerPool; import io.cloudchains.app.net.api.http.client.HTTPClient; -import io.cloudchains.app.util.CCLogger; import io.cloudchains.app.util.ConsoleFormatter; import io.cloudchains.app.util.FileFormatter; import io.cloudchains.app.util.LogRotationUtil; @@ -22,7 +21,6 @@ public class App { private final static LogManager LOGMANAGER = LogManager.getLogManager(); private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); - private static final boolean isLoggingEnabled = false; // DEBUG ENDPOINT public static String BASE_URL = "https://xliterevp.mywire.org/"; // "http://xl-dae-prox.airdns.org:42111/"; @@ -39,7 +37,8 @@ public static String getEnv(String key) { if (dotenv == null) { try { dotenv = Dotenv.configure().ignoreIfMissing().load(); - } catch (Exception ignored) { + } catch (Exception e) { + LOGGER.log(Level.FINE, "[app] No .env file found or failed to load", e); } } if (dotenv != null) { @@ -49,6 +48,31 @@ public static String getEnv(String key) { return System.getenv(key); } + public static String getUserConfigDir() { + String OS = (System.getProperty("os.name")).toLowerCase(); + if (OS.contains("win")) { + return getEnv("AppData"); + } else if (OS.contains("nix") || OS.contains("nux") || OS.contains("aix")) { + return System.getProperty("user.home") + File.separator + ".config"; + } else if (OS.contains("mac")) { + return System.getProperty("user.home") + File.separator + "Library" + File.separator + "Application Support"; + } + return System.getProperty("user.home") + File.separator + ".config"; + } + + private static Level parseLogLevel(String envValue, Level defaultLevel) { + if (envValue == null || envValue.trim().isEmpty()) { + return defaultLevel; + } + try { + return Level.parse(envValue.trim().toUpperCase()); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "[app] Invalid log level '{0}', using default {1}", + new Object[]{envValue, defaultLevel}); + return defaultLevel; + } + } + public static void initExrEndpoint() { if (EXR_ENDPOINT != null) return; String exrEndpoint = getEnv("EXR_ENDPOINT"); @@ -73,39 +97,23 @@ public static void main(String[] args) { initExrEndpoint(); - CCLogger.setLogging(isLoggingEnabled); - LOGGER.setLevel(Level.INFO); + Level logLevel = parseLogLevel(getEnv("CLOUDCHAINS_LOG_LEVEL"), Level.INFO); + LOGGER.setLevel(logLevel); LOGGER.setUseParentHandlers(false); - // Perform log rotation before initializing other components LogRotationUtil.performLogRotation(); Runtime.getRuntime().addShutdownHook(new Thread(App::shutdown)); try { - String userHomeDir; - String OS = (System.getProperty("os.name")).toLowerCase(); - - if (OS.contains("win")) { - userHomeDir = getEnv("AppData"); - } else if (OS.contains("nix") || OS.contains("nux") || OS.contains("aix")) { - userHomeDir = System.getProperty("user.home") + File.separator + ".config"; - } else if (OS.contains("mac")) { - userHomeDir = System.getProperty("user.home") + File.separator + "Library" + File.separator + "Application Support"; - } else { - userHomeDir = System.getProperty("user.home") + File.separator + ".config"; - } - + String userHomeDir = getUserConfigDir(); + String logDir = userHomeDir + File.separator + "CloudChains"; DateTimeFormatter timeStampPattern = DateTimeFormatter.ofPattern("yyyy-MM-dd"); Handler fileHandler = new FileHandler( - userHomeDir + - File.separator + - "CloudChains" + - File.separator + - "error-" + - timeStampPattern.format(LocalDateTime.now()) + - ".log", - true + logDir + File.separator + "error-" + timeStampPattern.format(LocalDateTime.now()) + ".log", + 10_000_000, // max file size 10MB + 5, // 5 rotated files + true // append to existing ); fileHandler.setFormatter(new FileFormatter()); diff --git a/src/main/java/io/cloudchains/app/net/CoinInstance.java b/src/main/java/io/cloudchains/app/net/CoinInstance.java index feb1d8a..5b19659 100644 --- a/src/main/java/io/cloudchains/app/net/CoinInstance.java +++ b/src/main/java/io/cloudchains/app/net/CoinInstance.java @@ -1,7 +1,6 @@ package io.cloudchains.app.net; import com.google.common.base.Joiner; -import com.google.common.util.concurrent.AtomicDouble; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.subgraph.orchid.encoders.Hex; @@ -13,6 +12,7 @@ //import io.cloudchains.app.net.protocols.alqocoin.AlqocoinNetworkParameters; //import io.cloudchains.app.net.protocols.bitbay.BitbayNetworkParameters; //import io.cloudchains.app.net.protocols.bitcoincash.BitcoinCashNetworkParameters; +import io.cloudchains.app.net.protocols.bitcoin.BitcoinNetworkParameters; import io.cloudchains.app.net.protocols.blocknet.*; import io.cloudchains.app.net.protocols.dashcoin.DashcoinNetworkParameters; import io.cloudchains.app.net.protocols.digibyte.DigibyteNetworkParameters; @@ -36,7 +36,6 @@ import io.cloudchains.app.util.history.Transaction; import io.cloudchains.app.wallet.WalletHelper; import org.bitcoinj.core.*; -import org.bitcoinj.params.MainNetParams; import org.bitcoinj.utils.BtcFormat; import org.bitcoinj.utils.ListenerRegistration; import org.bitcoinj.utils.MonetaryFormat; @@ -88,7 +87,6 @@ public String getMessage() { private static CoinTicker activeBlocknetNetwork = null; private static CopyOnWriteArrayList> activeCoinChangedListeners = new CopyOnWriteArrayList<>(); private static ConcurrentHashMap blockCounts = new ConcurrentHashMap<>(); - private static ConcurrentHashMap relayFees = new ConcurrentHashMap<>(); private ConfigHelper configHelper; private WalletHelper walletHelper = null; @@ -148,14 +146,6 @@ public static int getBlockCountByTicker(CoinTicker ticker) { return blockCounts.get(ticker).get(); } - public static double getRelayFeeByTicker(CoinTicker ticker) { - if (!relayFees.containsKey(ticker)) { - return -1; - } - - return relayFees.get(ticker).get(); - } - public static List getCoinInstances() { return coinInstances; } @@ -362,7 +352,7 @@ public CoinError init(char[] pw, String userMnemonic, boolean isMnemonic, boolea } case BITCOIN: { LOGGER.log(Level.FINE, "[coin] Initializing for Bitcoin main network."); - networkParameters = MainNetParams.get(); + networkParameters = new BitcoinNetworkParameters(); rpcPort = 8332; break; } @@ -450,12 +440,8 @@ public CoinError init(char[] pw, String userMnemonic, boolean isMnemonic, boolea // } case RAVENCOIN: { LOGGER.log(Level.FINE, "[coin] Initializing for Ravencoin main network."); - RavencoinNetworkParameters rvnParams = new RavencoinNetworkParameters(); - networkParameters = rvnParams; + networkParameters = new RavencoinNetworkParameters(); rpcPort = 8766; - Coin minFee = rvnParams.getMinRelayTxFee(); - LOGGER.log(Level.FINE, "[coin] " + ticker + " getMinRelayTxFee: " + minFee.value + " satoshis"); - configHelper.setFee(minFee.value / (double) Coin.COIN.value); break; } default: { @@ -923,11 +909,6 @@ public void addBlockCount(CoinTicker ticker, Integer blockCount) { .updateAndGet(current -> Math.max(current, blockCount)); } - public void addRelayFee(CoinTicker ticker, Double relayFee) { - relayFees.computeIfAbsent(ticker, k -> new AtomicDouble(relayFee)).set(relayFee); - configHelper.setFee(relayFee); - } - public void addCloudTransaction(CloudTransaction cloudTransaction) { synchronized (transactionObservableList) { if (transactionObservableList.isEmpty()) { diff --git a/src/main/java/io/cloudchains/app/net/HasFeeParams.java b/src/main/java/io/cloudchains/app/net/HasFeeParams.java new file mode 100644 index 0000000..f253dee --- /dev/null +++ b/src/main/java/io/cloudchains/app/net/HasFeeParams.java @@ -0,0 +1,11 @@ +package io.cloudchains.app.net; + +public interface HasFeeParams { + default long getFeePerByte() { + throw new UnsupportedOperationException("getFeePerByte not implemented"); + } + + default long getMinTxFee() { + throw new UnsupportedOperationException("getMinTxFee not implemented"); + } +} diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java b/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java index f446c31..a190104 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java +++ b/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java @@ -535,34 +535,6 @@ public JsonArray getUtxos(CoinTicker coinTicker, int expiry) { return coinInstance.getAllUTXOS(); } - public void getAllFees() { - String res = executeGetRequest("/fees"); - - if (res == null) return; - - JsonObject result = new Gson().fromJson(res, JsonObject.class); - JsonObject fees = result.get("result").getAsJsonObject(); - - for (CoinInstance coinInstance : CoinInstance.getCoinInstances()) { - String ticker = CoinTickerUtils.tickerToString(coinInstance.getTicker()); - - if (!fees.keySet().contains(ticker) || fees.get(ticker).isJsonNull()) { - coinInstance.incrementUpdateFailures(); - continue; - } - - double fee = fees.get(ticker).getAsDouble(); - - coinInstance.addRelayFee(coinInstance.getTicker(), fee); - - if (logCount % HttpClientConfig.LOG_COUNT_MODULO == 0) - LOGGER.log(Level.INFO, "[httpclient] Got relayfee for currency " + ticker + " - " + fee); - else - LOGGER.log(Level.FINER, "[httpclient] Got relayfee for currency " + ticker + " - " + fee); - } - logCount += 1; - } - public JsonObject getRawTransaction(CoinTicker coinTicker, String txid, boolean verbose) { ArrayList rawTxParams = new ArrayList<>(); rawTxParams.add(0, CoinTickerUtils.tickerToString(coinTicker)); diff --git a/src/main/java/io/cloudchains/app/net/api/http/server/ExceptionHandler.java b/src/main/java/io/cloudchains/app/net/api/http/server/ExceptionHandler.java index 0c4fcbb..437791a 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/server/ExceptionHandler.java +++ b/src/main/java/io/cloudchains/app/net/api/http/server/ExceptionHandler.java @@ -21,7 +21,7 @@ public class ExceptionHandler extends ChannelDuplexHandler { @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { - LOGGER.log(Level.FINER, cause.getMessage()); + LOGGER.log(Level.WARNING, "[http-server] Channel exception", cause); writeErrorResponse(ctx); } diff --git a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java b/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java index abac9a6..37dc49a 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java +++ b/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java @@ -279,7 +279,8 @@ private JsonObject getResponse(String method, JsonArray params) { infoJSON.addProperty("keypoolsize", coin.getAddressKeyPairs().size()); infoJSON.addProperty("keypoololdest", 0.0); - BigDecimal relayFeeDecimal = new BigDecimal(coin.getConfigHelper().getFee()).setScale(8, RoundingMode.DOWN); + long relayFeeSats = WalletHelper.getFeePerByte(coin.getNetworkParameters()) * 1000; + BigDecimal relayFeeDecimal = new BigDecimal(relayFeeSats).divide(new BigDecimal(Coin.COIN.value), 8, RoundingMode.DOWN); infoJSON.addProperty("relayfee", relayFeeDecimal); infoJSON.addProperty("networkactive", true); @@ -303,12 +304,8 @@ private JsonObject getResponse(String method, JsonArray params) { networkInfoJSON.addProperty("connections", 1); networkInfoJSON.addProperty("localservices", "0000000000000000"); - double relayFee = CoinInstance.getRelayFeeByTicker(coin.getTicker()); - if (relayFee == -1) { - relayFee = coin.getConfigHelper().getFee(); - } - - BigDecimal relayFeeDecimal = BigDecimal.valueOf(relayFee).setScale(8, RoundingMode.DOWN); + long relayFeeSats = WalletHelper.getFeePerByte(coin.getNetworkParameters()) * 1000; + BigDecimal relayFeeDecimal = new BigDecimal(relayFeeSats).divide(new BigDecimal(Coin.COIN.value), 8, RoundingMode.DOWN); networkInfoJSON.addProperty("relayfee", relayFeeDecimal); @@ -316,6 +313,14 @@ private JsonObject getResponse(String method, JsonArray params) { response.add("error", JsonNull.INSTANCE); break; } + case "getfees": { + JsonObject feesJSON = new JsonObject(); + feesJSON.addProperty("feeperbyte", WalletHelper.getFeePerByte(coin.getNetworkParameters())); + feesJSON.addProperty("mintxfee", WalletHelper.getMinTxFee(coin.getNetworkParameters())); + response.add("result", feesJSON); + response.add("error", JsonNull.INSTANCE); + break; + } case "listunspent": { JsonArray unspent = httpClient.getUtxos(coin.getTicker(), 30000); if (unspent == null) { @@ -1451,6 +1456,7 @@ private JsonObject getResponse(String method, JsonArray params) { + "getinfo - Get information such as balances, protocol version, and more.\n" + "getblockcount - Get block count\n" + "getnetworkinfo - Get network information\n" + + "getfees - Get fee per byte and minimum transaction fee\n" + "getrawmempool - Get raw mempool\n" + "getblockchaininfo - Get blockchain info\n" + "getblockhash - Get the hash of a block at a given height\n" diff --git a/src/main/java/io/cloudchains/app/net/protocols/alqocoin/AlqocoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/alqocoin/AlqocoinNetworkParameters.java index 631aa63..3850528 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/alqocoin/AlqocoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/alqocoin/AlqocoinNetworkParameters.java @@ -1,11 +1,12 @@ package io.cloudchains.app.net.protocols.alqocoin; +import io.cloudchains.app.net.HasFeeParams; import org.bitcoinj.core.*; import org.bitcoinj.store.BlockStore; import org.bitcoinj.store.BlockStoreException; import org.bitcoinj.utils.MonetaryFormat; -public class AlqocoinNetworkParameters extends NetworkParameters { +public class AlqocoinNetworkParameters extends NetworkParameters implements HasFeeParams { public AlqocoinNetworkParameters() { super(); @@ -95,4 +96,12 @@ public int getInterval() { public String getId() { return "XLQ"; } + + public long getFeePerByte() { + return 20; + } + + public long getMinTxFee() { + return 10000; + } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/bitbay/BitbayNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/bitbay/BitbayNetworkParameters.java index 703fbe2..16287fc 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/bitbay/BitbayNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/bitbay/BitbayNetworkParameters.java @@ -1,11 +1,12 @@ package io.cloudchains.app.net.protocols.bitbay; +import io.cloudchains.app.net.HasFeeParams; import org.bitcoinj.core.*; import org.bitcoinj.store.BlockStore; import org.bitcoinj.store.BlockStoreException; import org.bitcoinj.utils.MonetaryFormat; -public class BitbayNetworkParameters extends NetworkParameters { +public class BitbayNetworkParameters extends NetworkParameters implements HasFeeParams { public BitbayNetworkParameters() { super(); @@ -95,4 +96,12 @@ public int getInterval() { public String getId() { return "BAY"; } + + public long getFeePerByte() { + return 100; + } + + public long getMinTxFee() { + return 20000; + } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/bitcoin/BitcoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/bitcoin/BitcoinNetworkParameters.java new file mode 100644 index 0000000..2bce613 --- /dev/null +++ b/src/main/java/io/cloudchains/app/net/protocols/bitcoin/BitcoinNetworkParameters.java @@ -0,0 +1,19 @@ +package io.cloudchains.app.net.protocols.bitcoin; + +import io.cloudchains.app.net.HasFeeParams; +import org.bitcoinj.params.MainNetParams; + +public class BitcoinNetworkParameters extends MainNetParams implements HasFeeParams { + + public BitcoinNetworkParameters() { + super(); + } + + public long getFeePerByte() { + return 60; + } + + public long getMinTxFee() { + return 12000; + } +} \ No newline at end of file diff --git a/src/main/java/io/cloudchains/app/net/protocols/bitcoincash/BitcoinCashNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/bitcoincash/BitcoinCashNetworkParameters.java index bbbbddf..1bc4310 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/bitcoincash/BitcoinCashNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/bitcoincash/BitcoinCashNetworkParameters.java @@ -1,11 +1,12 @@ package io.cloudchains.app.net.protocols.bitcoincash; +import io.cloudchains.app.net.HasFeeParams; import org.bitcoinj.core.*; import org.bitcoinj.store.BlockStore; import org.bitcoinj.store.BlockStoreException; import org.bitcoinj.utils.MonetaryFormat; -public class BitcoinCashNetworkParameters extends NetworkParameters { +public class BitcoinCashNetworkParameters extends NetworkParameters implements HasFeeParams { public BitcoinCashNetworkParameters() { super(); @@ -95,4 +96,12 @@ public int getInterval() { public String getId() { return "BCH"; } + + public long getFeePerByte() { + return 2; + } + + public long getMinTxFee() { + return 500; + } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetNetworkParameters.java index 5560aa7..1b6b961 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetNetworkParameters.java @@ -1,6 +1,7 @@ package io.cloudchains.app.net.protocols.blocknet; import com.subgraph.orchid.encoders.Hex; +import io.cloudchains.app.net.HasFeeParams; import io.cloudchains.app.net.xrouter.XRouterMessageSerializer; import org.bitcoinj.core.*; import org.bitcoinj.store.BlockStore; @@ -9,7 +10,7 @@ import java.math.BigInteger; -public class BlocknetNetworkParameters extends BlocknetParameters { +public class BlocknetNetworkParameters extends BlocknetParameters implements HasFeeParams { public BlocknetNetworkParameters() { super(); @@ -168,4 +169,11 @@ public XRouterMessageSerializer getXRouterMessageSerializer(boolean parseRetain) return new XRouterMessageSerializer(parseRetain, this); } + public long getFeePerByte() { + return 20; + } + + public long getMinTxFee() { + return 10000; + } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetTestnet5NetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetTestnet5NetworkParameters.java index 642afd4..ef51380 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetTestnet5NetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetTestnet5NetworkParameters.java @@ -1,6 +1,7 @@ package io.cloudchains.app.net.protocols.blocknet; import com.subgraph.orchid.encoders.Hex; +import io.cloudchains.app.net.HasFeeParams; import io.cloudchains.app.net.xrouter.XRouterMessageSerializer; import org.bitcoinj.core.*; import org.bitcoinj.store.BlockStore; @@ -8,7 +9,7 @@ import java.math.BigInteger; -public class BlocknetTestnet5NetworkParameters extends BlocknetParameters { +public class BlocknetTestnet5NetworkParameters extends BlocknetParameters implements HasFeeParams { public BlocknetTestnet5NetworkParameters() { super(); @@ -155,4 +156,12 @@ public BigInteger getMaxTarget() { public String getId() { return "tBLOCK"; } + + public long getFeePerByte() { + return 20; + } + + public long getMinTxFee() { + return 10000; + } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/dashcoin/DashcoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/dashcoin/DashcoinNetworkParameters.java index f0910e5..4b113b4 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/dashcoin/DashcoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/dashcoin/DashcoinNetworkParameters.java @@ -1,11 +1,12 @@ package io.cloudchains.app.net.protocols.dashcoin; +import io.cloudchains.app.net.HasFeeParams; import org.bitcoinj.core.*; import org.bitcoinj.store.BlockStore; import org.bitcoinj.store.BlockStoreException; import org.bitcoinj.utils.MonetaryFormat; -public class DashcoinNetworkParameters extends NetworkParameters { +public class DashcoinNetworkParameters extends NetworkParameters implements HasFeeParams { public DashcoinNetworkParameters() { super(); @@ -95,4 +96,12 @@ public int getInterval() { public String getId() { return "DASH"; } + + public long getFeePerByte() { + return 5; + } + + public long getMinTxFee() { + return 2500; + } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/digibyte/DigibyteNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/digibyte/DigibyteNetworkParameters.java index 1ead56e..5bd5947 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/digibyte/DigibyteNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/digibyte/DigibyteNetworkParameters.java @@ -1,11 +1,12 @@ package io.cloudchains.app.net.protocols.digibyte; +import io.cloudchains.app.net.HasFeeParams; import org.bitcoinj.core.*; import org.bitcoinj.store.BlockStore; import org.bitcoinj.store.BlockStoreException; import org.bitcoinj.utils.MonetaryFormat; -public class DigibyteNetworkParameters extends NetworkParameters { +public class DigibyteNetworkParameters extends NetworkParameters implements HasFeeParams { public DigibyteNetworkParameters() { super(); @@ -95,4 +96,12 @@ public int getInterval() { public String getId() { return "DGB"; } + + public long getFeePerByte() { + return 200; + } + + public long getMinTxFee() { + return 100000; + } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/dogecoin/DogecoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/dogecoin/DogecoinNetworkParameters.java index b9190bd..68ffc12 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/dogecoin/DogecoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/dogecoin/DogecoinNetworkParameters.java @@ -1,11 +1,12 @@ package io.cloudchains.app.net.protocols.dogecoin; +import io.cloudchains.app.net.HasFeeParams; import org.bitcoinj.core.*; import org.bitcoinj.store.BlockStore; import org.bitcoinj.store.BlockStoreException; import org.bitcoinj.utils.MonetaryFormat; -public class DogecoinNetworkParameters extends NetworkParameters { +public class DogecoinNetworkParameters extends NetworkParameters implements HasFeeParams { public DogecoinNetworkParameters() { super(); @@ -95,4 +96,12 @@ public int getInterval() { public String getId() { return "DOGE"; } + + public long getFeePerByte() { + return 2500; + } + + public long getMinTxFee() { + return 225000; + } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/litecoin/LitecoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/litecoin/LitecoinNetworkParameters.java index af3036e..27c2d87 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/litecoin/LitecoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/litecoin/LitecoinNetworkParameters.java @@ -1,11 +1,12 @@ package io.cloudchains.app.net.protocols.litecoin; +import io.cloudchains.app.net.HasFeeParams; import org.bitcoinj.core.*; import org.bitcoinj.store.BlockStore; import org.bitcoinj.store.BlockStoreException; import org.bitcoinj.utils.MonetaryFormat; -public class LitecoinNetworkParameters extends NetworkParameters { +public class LitecoinNetworkParameters extends NetworkParameters implements HasFeeParams { public LitecoinNetworkParameters() { super(); @@ -99,4 +100,12 @@ public int getInterval() { public String getId() { return "LTC"; } + + public long getFeePerByte() { + return 10; + } + + public long getMinTxFee() { + return 5000; + } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/phorecoin/PhorecoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/phorecoin/PhorecoinNetworkParameters.java index ac66eca..123816d 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/phorecoin/PhorecoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/phorecoin/PhorecoinNetworkParameters.java @@ -1,11 +1,12 @@ package io.cloudchains.app.net.protocols.phorecoin; +import io.cloudchains.app.net.HasFeeParams; import org.bitcoinj.core.*; import org.bitcoinj.store.BlockStore; import org.bitcoinj.store.BlockStoreException; import org.bitcoinj.utils.MonetaryFormat; -public class PhorecoinNetworkParameters extends NetworkParameters { +public class PhorecoinNetworkParameters extends NetworkParameters implements HasFeeParams { public PhorecoinNetworkParameters() { super(); @@ -95,4 +96,12 @@ public int getInterval() { public String getId() { return "PHR"; } + + public long getFeePerByte() { + return 20; + } + + public long getMinTxFee() { + return 10000; + } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/pivx/PivxNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/pivx/PivxNetworkParameters.java index 0b56aef..72a7ced 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/pivx/PivxNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/pivx/PivxNetworkParameters.java @@ -1,11 +1,12 @@ package io.cloudchains.app.net.protocols.pivx; +import io.cloudchains.app.net.HasFeeParams; import org.bitcoinj.core.*; import org.bitcoinj.store.BlockStore; import org.bitcoinj.store.BlockStoreException; import org.bitcoinj.utils.MonetaryFormat; -public class PivxNetworkParameters extends NetworkParameters { +public class PivxNetworkParameters extends NetworkParameters implements HasFeeParams { public PivxNetworkParameters() { super(); @@ -95,4 +96,12 @@ public int getInterval() { public String getId() { return "PIVX"; } + + public long getFeePerByte() { + return 20; + } + + public long getMinTxFee() { + return 10000; + } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/pocketcoin/PocketcoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/pocketcoin/PocketcoinNetworkParameters.java index e054383..3411582 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/pocketcoin/PocketcoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/pocketcoin/PocketcoinNetworkParameters.java @@ -1,11 +1,12 @@ package io.cloudchains.app.net.protocols.pocketcoin; +import io.cloudchains.app.net.HasFeeParams; import org.bitcoinj.core.*; import org.bitcoinj.store.BlockStore; import org.bitcoinj.store.BlockStoreException; import org.bitcoinj.utils.MonetaryFormat; -public class PocketcoinNetworkParameters extends NetworkParameters { +public class PocketcoinNetworkParameters extends NetworkParameters implements HasFeeParams { public PocketcoinNetworkParameters() { super(); @@ -95,4 +96,12 @@ public int getInterval() { public String getId() { return "PKOIN"; } + + public long getFeePerByte() { + return 20; + } + + public long getMinTxFee() { + return 10000; + } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/poliscoin/PoliscoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/poliscoin/PoliscoinNetworkParameters.java deleted file mode 100644 index 334cedc..0000000 --- a/src/main/java/io/cloudchains/app/net/protocols/poliscoin/PoliscoinNetworkParameters.java +++ /dev/null @@ -1,98 +0,0 @@ -package io.cloudchains.app.net.protocols.poliscoin; - -import org.bitcoinj.core.*; -import org.bitcoinj.store.BlockStore; -import org.bitcoinj.store.BlockStoreException; -import org.bitcoinj.utils.MonetaryFormat; - -public class PoliscoinNetworkParameters extends NetworkParameters { - - public PoliscoinNetworkParameters() { - super(); - } - - @Override - public String getPaymentProtocolId() { - return "main"; - } - - @Override - public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException { - } - - @Override - public Coin getMaxMoney() { - return Coin.valueOf(25000000 * Coin.COIN.value); - } - - @Override - public Coin getMinNonDustOutput() { - return Transaction.MIN_NONDUST_OUTPUT; - } - - @Override - public MonetaryFormat getMonetaryFormat() { - return new MonetaryFormat().code(0, "POLIS"); - } - - @Override - public String getUriScheme() { - return "polis:"; - } - - @Override - public boolean hasMaxMoney() { - return true; - } - - @Override - public BitcoinSerializer getSerializer(boolean parseRetain) { - return new BitcoinSerializer(this, parseRetain); - } - - @Override - public int getProtocolVersionNum(ProtocolVersion version) { - return 70219; - } - - @Override - public int getAddressHeader() { - return 55; - } - - @Override - public int getP2SHHeader() { - return 56; - } - - @Override - public int getDumpedPrivateKeyHeader() { - return 60; - } - - - @Override - public int getBip32HeaderP2PKHpriv() { - return 0x03E25945; - } - - @Override - public int getBip32HeaderP2PKHpub() { - return 0x03E25D7E; - } - - @Override - public int getSubsidyDecreaseBlockCount() { - return 210240; - } - - @Override - public int getInterval() { - return 120; - } - - @Override - public String getId() { - return "DASH"; - } -} diff --git a/src/main/java/io/cloudchains/app/net/protocols/ravencoin/RavencoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/ravencoin/RavencoinNetworkParameters.java index 08f80e2..1033c30 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/ravencoin/RavencoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/ravencoin/RavencoinNetworkParameters.java @@ -1,11 +1,12 @@ package io.cloudchains.app.net.protocols.ravencoin; +import io.cloudchains.app.net.HasFeeParams; import org.bitcoinj.core.*; import org.bitcoinj.store.BlockStore; import org.bitcoinj.store.BlockStoreException; import org.bitcoinj.utils.MonetaryFormat; -public class RavencoinNetworkParameters extends NetworkParameters { +public class RavencoinNetworkParameters extends NetworkParameters implements HasFeeParams { public RavencoinNetworkParameters() { super(); @@ -96,7 +97,11 @@ public String getId() { return "RVN"; } - public Coin getMinRelayTxFee() { - return Coin.valueOf(500000); + public long getFeePerByte() { + return 1000; + } + + public long getMinTxFee() { + return 100000; } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/syscoin/SyscoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/syscoin/SyscoinNetworkParameters.java index c7f906f..c03c126 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/syscoin/SyscoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/syscoin/SyscoinNetworkParameters.java @@ -1,11 +1,12 @@ package io.cloudchains.app.net.protocols.syscoin; +import io.cloudchains.app.net.HasFeeParams; import org.bitcoinj.core.*; import org.bitcoinj.store.BlockStore; import org.bitcoinj.store.BlockStoreException; import org.bitcoinj.utils.MonetaryFormat; -public class SyscoinNetworkParameters extends NetworkParameters { +public class SyscoinNetworkParameters extends NetworkParameters implements HasFeeParams { public SyscoinNetworkParameters() { super(); @@ -95,4 +96,12 @@ public int getInterval() { public String getId() { return "SYS"; } + + public long getFeePerByte() { + return 40; + } + + public long getMinTxFee() { + return 20000; + } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/trezarcoin/TrezarcoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/trezarcoin/TrezarcoinNetworkParameters.java deleted file mode 100644 index bb48adf..0000000 --- a/src/main/java/io/cloudchains/app/net/protocols/trezarcoin/TrezarcoinNetworkParameters.java +++ /dev/null @@ -1,98 +0,0 @@ -package io.cloudchains.app.net.protocols.trezarcoin; - -import org.bitcoinj.core.*; -import org.bitcoinj.store.BlockStore; -import org.bitcoinj.store.BlockStoreException; -import org.bitcoinj.utils.MonetaryFormat; - -public class TrezarcoinNetworkParameters extends NetworkParameters { - - public TrezarcoinNetworkParameters() { - super(); - } - - - @Override - public String getPaymentProtocolId() { - return "main"; - } - - @Override - public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException { - } - - @Override - public Coin getMaxMoney() { - return Coin.valueOf(888000000 * Coin.COIN.value); - } - - @Override - public Coin getMinNonDustOutput() { - return Coin.valueOf(5500); - } - - @Override - public MonetaryFormat getMonetaryFormat() { - return new MonetaryFormat().code(0, "TZC"); - } - - @Override - public String getUriScheme() { - return "trezarcoin:"; - } - - @Override - public boolean hasMaxMoney() { - return true; - } - - @Override - public BitcoinSerializer getSerializer(boolean parseRetain) { - return new BitcoinSerializer(this, parseRetain); - } - - @Override - public int getProtocolVersionNum(ProtocolVersion version) { - return 70000; - } - - @Override - public int getAddressHeader() { - return 66; - } - - @Override - public int getP2SHHeader() { - return 8; - } - - @Override - public int getDumpedPrivateKeyHeader() { - return 194; - } - - @Override - public int getBip32HeaderP2PKHpriv() { - return 0x0488ADE4; - } - - @Override - public int getBip32HeaderP2PKHpub() { - return 0x0488B21E; - } - - @Override - public int getSubsidyDecreaseBlockCount() { - return 1600000; - } - - @Override - public int getInterval() { - return 600; - } - - @Override - public String getId() { - return "TZC"; - } -} diff --git a/src/main/java/io/cloudchains/app/net/protocols/unobtanium/UnobtaniumNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/unobtanium/UnobtaniumNetworkParameters.java index 314cc70..09fdb4c 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/unobtanium/UnobtaniumNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/unobtanium/UnobtaniumNetworkParameters.java @@ -1,11 +1,12 @@ package io.cloudchains.app.net.protocols.unobtanium; +import io.cloudchains.app.net.HasFeeParams; import org.bitcoinj.core.*; import org.bitcoinj.store.BlockStore; import org.bitcoinj.store.BlockStoreException; import org.bitcoinj.utils.MonetaryFormat; -public class UnobtaniumNetworkParameters extends NetworkParameters { +public class UnobtaniumNetworkParameters extends NetworkParameters implements HasFeeParams { public UnobtaniumNetworkParameters() { super(); @@ -96,4 +97,12 @@ public int getInterval() { public String getId() { return "UNO"; } + + public long getFeePerByte() { + return 3; + } + + public long getMinTxFee() { + return 1000; + } } \ No newline at end of file diff --git a/src/main/java/io/cloudchains/app/net/xrouter/XRouterFeeUtils.java b/src/main/java/io/cloudchains/app/net/xrouter/XRouterFeeUtils.java index 6108c5f..00a27fe 100644 --- a/src/main/java/io/cloudchains/app/net/xrouter/XRouterFeeUtils.java +++ b/src/main/java/io/cloudchains/app/net/xrouter/XRouterFeeUtils.java @@ -43,12 +43,17 @@ public static String getXRouterFeeTx(BlocknetPeer blocknetPeer, String xRouterCo return "nohash;nofee"; } - double totalSpending = fee + blocknetCoin.getConfigHelper().getFee(); + long feePerByte = WalletHelper.getFeePerByte(blocknetCoin.getNetworkParameters()); + long minTxFee = WalletHelper.getMinTxFee(blocknetCoin.getNetworkParameters()); + long networkFeeSats = Math.max(feePerByte * (192 + 34), minTxFee); + double networkFee = (double) networkFeeSats / Coin.COIN.value; + + double totalSpending = fee + networkFee; double totalAvailable = blocknetWalletHelper.getSpendBalance(totalSpending); - double changeAmt = ((totalAvailable - blocknetCoin.getConfigHelper().getFee()) - fee); + double changeAmt = totalAvailable - networkFee - fee; LegacyAddress xRouterPaymentAddress = LegacyAddress.fromBase58(params, xRouterConfig.getFeeAddress()); - Coin blocknetNetworkFeeAmt = Coin.valueOf((long) Math.floor(blocknetCoin.getConfigHelper().getFee() * Coin.COIN.value)); + Coin blocknetNetworkFeeAmt = Coin.valueOf(networkFeeSats); Coin xRouterChangeAmt = Coin.valueOf((long) Math.floor(totalAvailable * Coin.COIN.value)).minus(blocknetNetworkFeeAmt).minus(xRouterFeeAmt); TransactionOutput feeOutput = new TransactionOutput(params, null, xRouterFeeAmt, xRouterPaymentAddress); diff --git a/src/main/java/io/cloudchains/app/util/AddressDiscoveryService.java b/src/main/java/io/cloudchains/app/util/AddressDiscoveryService.java index a679e1d..43e88db 100644 --- a/src/main/java/io/cloudchains/app/util/AddressDiscoveryService.java +++ b/src/main/java/io/cloudchains/app/util/AddressDiscoveryService.java @@ -232,7 +232,7 @@ private List checkBatchForUtxos(List batch) { utxoResponse = httpClient.getUtxosUncached(coinInstance.getTicker(), addresses); } catch (Exception e) { LOGGER.log(Level.WARNING, getLogPrefix() + " HTTP request failed for addresses " - + addresses[0] + "..." + addresses[addresses.length - 1] + " - " + e.getMessage(), e); + + addresses[0] + "..." + addresses[addresses.length - 1] + " - " + e.getMessage()); return null; } if (utxoResponse == null || utxoResponse.size() == 0) { diff --git a/src/main/java/io/cloudchains/app/util/CCLogger.java b/src/main/java/io/cloudchains/app/util/CCLogger.java deleted file mode 100644 index 8aead96..0000000 --- a/src/main/java/io/cloudchains/app/util/CCLogger.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.cloudchains.app.util; - -public class CCLogger { - private static boolean isLogging; - - public static boolean isLoggingEnabled() { - return isLogging; - } - - static { - System.setProperty("java.util.logging.SimpleFormatter.format", "[%4$s] %5$s %n"); - } - - public static void setLogging(boolean isEnabled) { -// System.setErr(new PrintStream(new OutputStream() { -// public void write(int b) { -// } -// })); - - isLogging = isEnabled; - } -} diff --git a/src/main/java/io/cloudchains/app/util/ConfigHelper.java b/src/main/java/io/cloudchains/app/util/ConfigHelper.java index 905511b..ae0bae0 100644 --- a/src/main/java/io/cloudchains/app/util/ConfigHelper.java +++ b/src/main/java/io/cloudchains/app/util/ConfigHelper.java @@ -20,8 +20,8 @@ public class ConfigHelper { private String tickerStr; private File file; - private double fee; - private boolean feeFlat; + private long feePerByte; + private long minTxFee; private boolean rpcEnabled; private String rpcUsername; private String rpcPassword; @@ -46,8 +46,6 @@ public synchronized void loadConfig() { try { String rawConfig = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); if (rawConfig.isEmpty()) { - fee = 0.0001; - feeFlat = true; rpcEnabled = false; rpcUsername = ""; rpcPassword = ""; @@ -61,8 +59,8 @@ public synchronized void loadConfig() { JSONObject config = new JSONObject(rawConfig); final String[] configKeys = new String[]{ - "fee", - "feeFlat", + "feeperbyte", + "mintxfee", "rpcEnabled", "rpcUsername", "rpcPassword", @@ -78,23 +76,16 @@ public synchronized void loadConfig() { boolean needsWrite = false; - if (!config.has("fee")) { - fee = 0.0001; + if (!config.has("feeperbyte")) { needsWrite = true; } else { - fee = config.getDouble("fee"); - LOGGER.log(Level.FINE, "[config] " + tickerStr + " fee from config: " + fee); - if (fee <= 0) { - fee = 0.0001; - needsWrite = true; - } + feePerByte = config.getLong("feeperbyte"); } - if (!config.has("feeFlat")) { - feeFlat = true; + if (!config.has("mintxfee")) { needsWrite = true; } else { - feeFlat = config.getBoolean("feeFlat"); + minTxFee = config.getLong("mintxfee"); } if (!config.has("rpcEnabled")) { @@ -168,12 +159,12 @@ private File getFile() { return configFile; } - public synchronized void setFee(double fee) { - this.fee = fee; + public synchronized void setFeePerByte(long feePerByte) { + this.feePerByte = feePerByte; } - public synchronized void setFlatFee(boolean flat) { - this.feeFlat = flat; + public synchronized void setMinTxFee(long minTxFee) { + this.minTxFee = minTxFee; } public synchronized void setRpcEnabled(boolean isEnabled) { @@ -208,12 +199,12 @@ public synchronized void setAddressCount(int addressCount) { this.addressCount = addressCount; } - public synchronized double getFee() { - return fee; + public synchronized long getFeePerByte() { + return feePerByte; } - public synchronized boolean isFlatFee() { - return feeFlat; + public synchronized long getMinTxFee() { + return minTxFee; } public synchronized boolean isRpcEnabled() { @@ -249,8 +240,8 @@ public synchronized int getAddressCount() { private JSONObject toConfigJson() { JSONObject config = new JSONObject(); - config.put("fee", fee); - config.put("feeFlat", feeFlat); + config.put("feeperbyte", feePerByte); + config.put("mintxfee", minTxFee); config.put("rpcEnabled", rpcEnabled); config.put("rpcUsername", rpcUsername); config.put("rpcPassword", rpcPassword); diff --git a/src/main/java/io/cloudchains/app/util/LogRotationUtil.java b/src/main/java/io/cloudchains/app/util/LogRotationUtil.java index 71f5caf..3b75a46 100644 --- a/src/main/java/io/cloudchains/app/util/LogRotationUtil.java +++ b/src/main/java/io/cloudchains/app/util/LogRotationUtil.java @@ -24,8 +24,7 @@ public class LogRotationUtil { */ public static void performLogRotation() { try { - // Determine log directory path (same logic as App.java file handler creation) - String userHomeDir = getUserConfigDirectory(); + String userHomeDir = App.getUserConfigDir(); String logDirectoryPath = userHomeDir + File.separator + "CloudChains"; // Get retention days from environment variable or use default @@ -52,25 +51,6 @@ public static void performLogRotation() { } } - /** - * Gets the user configuration directory based on the operating system. - * - * @return Path to user configuration directory - */ - private static String getUserConfigDirectory() { - String OS = (System.getProperty("os.name")).toLowerCase(); - - if (OS.contains("win")) { - return App.getEnv("AppData"); - } else if (OS.contains("nix") || OS.contains("nux") || OS.contains("aix")) { - return System.getProperty("user.home") + File.separator + ".config"; - } else if (OS.contains("mac")) { - return System.getProperty("user.home") + File.separator + "Library" + File.separator + "Application Support"; - } else { - return System.getProperty("user.home") + File.separator + ".config"; - } - } - /** * Gets the log retention period from environment variable or returns default. * diff --git a/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java b/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java index 3c321e6..1c6def4 100644 --- a/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java +++ b/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java @@ -187,12 +187,6 @@ private void sendKeepAlive() { if (HTTP_BLOCK_COUNT_UPDATES) { heightUpdateHttpClient.getAllBlockCounts(); - - // feeUpdateHttpClient.getAllFees(); - // TODO: Re-enable when remote servers support relayfee queries. - // Currently disabled — remote endpoints return incorrect relayfee data. - // Using locally-configured fee values until server-side fixes are deployed. - } else if (!blocknetPeerGroup.getConnectedPeers().isEmpty()) { for (BlocknetPeer blocknetPeer : blocknetPeerGroup.getConnectedPeers()) { XRouterConfiguration xRouterConfiguration = blocknetPeer.getxRouterConfiguration(); diff --git a/src/main/java/io/cloudchains/app/wallet/WalletHelper.java b/src/main/java/io/cloudchains/app/wallet/WalletHelper.java index f3eb170..253e349 100644 --- a/src/main/java/io/cloudchains/app/wallet/WalletHelper.java +++ b/src/main/java/io/cloudchains/app/wallet/WalletHelper.java @@ -4,20 +4,18 @@ import io.cloudchains.app.net.CoinInstance; import io.cloudchains.app.net.CoinTicker; import io.cloudchains.app.net.CoinTickerUtils; +import io.cloudchains.app.net.HasFeeParams; import io.cloudchains.app.net.protocols.blocknet.BlocknetPeer; import io.cloudchains.app.util.AddressBalance; import io.cloudchains.app.util.CloudTransaction; import io.cloudchains.app.util.UTXO; import org.bitcoinj.core.*; import org.bitcoinj.crypto.DeterministicKey; -import org.bitcoinj.script.Script; -import org.bitcoinj.script.ScriptBuilder; import org.bitcoinj.wallet.Wallet; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Comparator; -import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -38,7 +36,7 @@ public Transaction createRawTransactionWithAllUTXOs(Transaction tx, double amoun ArrayList utxos = coinSelector(amount); if (utxos == null) { - LOGGER.log(Level.WARNING, "[wallet-" + coin.getTicker() + "] createRawTransactionWithAllUTXOs: no UTXOs for amount=" + amount); + LOGGER.warning("[wallet-" + coin.getTicker() + "] createRawTransactionWithAllUTXOs: no UTXOs for amount=" + amount); return null; } @@ -48,10 +46,10 @@ public Transaction createRawTransactionWithAllUTXOs(Transaction tx, double amoun private Transaction signTransactionWithUtxos(Transaction tx, ArrayList selectedUtxos) { try { if (selectedUtxos == null) { - LOGGER.log(Level.WARNING, "[wallet-" + coin.getTicker() + "] signTransactionWithUtxos: no UTXOs provided"); + LOGGER.warning("[wallet-" + coin.getTicker() + "] signTransactionWithUtxos: no UTXOs provided"); return null; } - LOGGER.log(Level.FINE, "[wallet-" + coin.getTicker() + "] signTransactionWithUtxos: signing " + selectedUtxos.size() + " UTXOs"); + LOGGER.fine("[wallet-" + coin.getTicker() + "] signTransactionWithUtxos: signing " + selectedUtxos.size() + " UTXOs"); for (UTXO utxo : selectedUtxos) { if (utxo.isSpent()) continue; @@ -63,14 +61,14 @@ private Transaction signTransactionWithUtxos(Transaction tx, ArrayList sel TransactionOutPoint outPoint = new TransactionOutPoint(networkParameters, bUtxo.getIndex(), bUtxo.getHash()); tx.addSignedInput(outPoint, bUtxo.getScript(), addressBalance.getPrivateKey().getKey(), Transaction.SigHash.ALL, true); - LOGGER.log(Level.FINE, "[wallet-" + coin.getTicker() + "] signed input: txid=" + utxo.getTxid() + " vout=" + utxo.getVout()); + LOGGER.fine("[wallet-" + coin.getTicker() + "] signed input: txid=" + utxo.getTxid() + " vout=" + utxo.getVout()); utxo.setSpent(true); addressBalance.calculateBalance(); } return tx; } catch (Exception e) { - LOGGER.log(Level.WARNING, "[wallet-" + coin.getTicker() + "] Error creating transaction", e); + LOGGER.warning("[wallet-" + coin.getTicker() + "] Error creating transaction" + e.getMessage()); return null; } } @@ -99,9 +97,9 @@ private ArrayList sortLeastToGreatest() { private ArrayList advancedCoinSorting() { ArrayList utxos = new ArrayList<>(); - LOGGER.log(Level.FINE, "[wallet-" + coin.getTicker() + "] advancedCoinSorting: " + coin.getAddressKeyPairs().size() + " addresses tracked locally"); + LOGGER.fine("[wallet-" + coin.getTicker() + "] advancedCoinSorting: " + coin.getAddressKeyPairs().size() + " addresses tracked locally"); for (AddressBalance addressBalance : coin.getAddressKeyPairs()) { - LOGGER.log(Level.FINE, "[wallet-" + coin.getTicker() + "] addr=" + addressBalance.getAddress().toBase58() + " utxos=" + addressBalance.getUtxos().size()); + LOGGER.fine("[wallet-" + coin.getTicker() + "] addr=" + addressBalance.getAddress().toBase58() + " utxos=" + addressBalance.getUtxos().size()); utxos.addAll(addressBalance.getUtxos()); } @@ -137,9 +135,9 @@ private ArrayList coinSelector(double amount) { double totalBalance = 0.0; ArrayList sorted = advancedCoinSorting(); - LOGGER.log(Level.FINE, "[wallet-" + coin.getTicker() + "] coinSelector: requested=" + amount + ", available UTXOs=" + sorted.size()); + LOGGER.fine("[wallet-" + coin.getTicker() + "] coinSelector: requested=" + amount + ", available UTXOs=" + sorted.size()); for (UTXO utxo : sorted) { - LOGGER.log(Level.FINE, "[wallet-" + coin.getTicker() + "] UTXO: txid=" + utxo.getTxid() + " vout=" + utxo.getVout() + " amount=" + utxo.getAmount() + " spent=" + utxo.isSpent()); + LOGGER.fine("[wallet-" + coin.getTicker() + "] UTXO: txid=" + utxo.getTxid() + " vout=" + utxo.getVout() + " amount=" + utxo.getAmount() + " spent=" + utxo.isSpent()); } for (UTXO utxo : sorted) { @@ -152,10 +150,10 @@ private ArrayList coinSelector(double amount) { } if (utxos.size() > 0) { - LOGGER.log(Level.FINE, "[wallet-" + coin.getTicker() + "] coinSelector: selected " + utxos.size() + " UTXOs, total=" + totalBalance); + LOGGER.fine("[wallet-" + coin.getTicker() + "] coinSelector: selected " + utxos.size() + " UTXOs, total=" + totalBalance); return utxos; } else { - LOGGER.log(Level.WARNING, "[wallet-" + coin.getTicker() + "] coinSelector: no UTXOs found (requested=" + amount + ", available in wallet=" + sorted.size() + ")"); + LOGGER.warning("[wallet-" + coin.getTicker() + "] coinSelector: no UTXOs found (requested=" + amount + ", available in wallet=" + sorted.size() + ")"); return null; } } @@ -179,13 +177,13 @@ public double getSpendBalance(double amount) { ArrayList utxos = coinSelector(amount); if (utxos == null) { - LOGGER.log(Level.WARNING, "[wallet-" + coin.getTicker() + "] getSpendBalance: insufficient funds (requested=" + amount + ")"); + LOGGER.warning("[wallet-" + coin.getTicker() + "] getSpendBalance: insufficient funds (requested=" + amount + ")"); return 0.0; } for (UTXO utxo : utxos) totalBalance += utxo.getAmount(); - LOGGER.log(Level.FINE, "[wallet-" + coin.getTicker() + "] getSpendBalance: available=" + totalBalance + " for request=" + amount); + LOGGER.fine("[wallet-" + coin.getTicker() + "] getSpendBalance: available=" + totalBalance + " for request=" + amount); return totalBalance; } @@ -229,45 +227,145 @@ public double getBlocknetFeeAmount(BlocknetPeer blocknetPeer) { } public static Transaction createTransactionSimple(CoinTicker coinTicker, String address, double amount) { + return createTransactionSimple(coinTicker, address, amount, false); + } + + public static Transaction createTransactionSimple(CoinTicker coinTicker, String address, double amount, boolean subtractFees) { CoinInstance coinInstance = CoinInstance.getInstance(coinTicker); WalletHelper walletHelper = coinInstance.getWalletHelper(); NetworkParameters params = coinInstance.getNetworkParameters(); - double fee = coinInstance.getConfigHelper().getFee(); - double totalSpending = amount + fee; + long feePerByte = getFeePerByte(params); + long minTxFee = getMinTxFee(params); + + long coinUnit = Coin.COIN.value; - LOGGER.log(Level.FINE, "[wallet-" + CoinTickerUtils.tickerToString(coinTicker) + "] createTransactionSimple: to=" + address + " amount=" + amount + " fee=" + fee + " totalSpending=" + totalSpending); + ArrayList allUtxos = walletHelper.sortLeastToGreatest(); + allUtxos.removeIf(UTXO::isSpent); - ArrayList selectedUtxos = walletHelper.coinSelector(totalSpending); - if (selectedUtxos == null) { - LOGGER.log(Level.WARNING, "[wallet-" + CoinTickerUtils.tickerToString(coinTicker) + "] createTransactionSimple: insufficient funds (need " + totalSpending + ")"); + if (allUtxos.isEmpty()) { + LOGGER.warning("[wallet-" + CoinTickerUtils.tickerToString(coinTicker) + "] createTransactionSimple: no UTXOs available"); return null; } + + double amountNotIncludingFees = amount; double totalAvailable = 0.0; - for (UTXO utxo : selectedUtxos) totalAvailable += utxo.getAmount(); - double changeAmt = (totalAvailable - amount) - fee; + for (UTXO utxo : allUtxos) { + totalAvailable += utxo.getAmount(); + } + + if (totalAvailable < amountNotIncludingFees) { + LOGGER.warning("[wallet-" + CoinTickerUtils.tickerToString(coinTicker) + "] createTransactionSimple: insufficient funds (have=" + totalAvailable + ", need=" + amountNotIncludingFees + ")"); + return null; + } - LOGGER.log(Level.FINE, "[wallet-" + CoinTickerUtils.tickerToString(coinTicker) + "] createTransactionSimple: totalAvailable=" + totalAvailable + " changeAmt=" + changeAmt); + ArrayList selectedUtxos = new ArrayList<>(); + ArrayList outputs = new ArrayList<>(); + outputs.add(createTransactionOutput(coinTicker, address, amount)); - LegacyAddress toAddress = LegacyAddress.fromBase58(params, address); - Coin sendAmount = Coin.valueOf((long) Math.floor(amount * Coin.COIN.value)); - Coin changeAmount = Coin.valueOf((long) Math.floor(changeAmt * Coin.COIN.value)); + long estimatedFeeSats = Math.max(feePerByte * (192 + 34), minTxFee); + double estimatedFee = (double) estimatedFeeSats / coinUnit; - Transaction tx = new Transaction(params); + double changeAmt = fundTransaction(allUtxos, selectedUtxos, outputs, amountNotIncludingFees, estimatedFee, subtractFees, feePerByte, minTxFee, coinUnit, totalAvailable); - if (isP2SHAddress(coinInstance, address)) { - Script p2shScript = ScriptBuilder.createP2SHOutputScript(toAddress.getHash()); - tx.addOutput(sendAmount, p2shScript); - } else { - tx.addOutput(sendAmount, toAddress); + if (selectedUtxos.isEmpty()) { + LOGGER.warning("[wallet-" + CoinTickerUtils.tickerToString(coinTicker) + "] createTransactionSimple: failed to select UTXOs"); + return null; + } + + Transaction tx = new Transaction(params); + for (UTXO output : outputs) { + Address addr = LegacyAddress.fromBase58(params, output.getAddress()); + tx.addOutput(Coin.valueOf((long) (output.getAmount() * coinUnit)), addr); } - if (changeAmount.isPositive()) - tx.addOutput(changeAmount, walletHelper.getChangeAddress()); + if (changeAmt > 0 && !isDust(changeAmt, params)) { + tx.addOutput(Coin.valueOf((long) (changeAmt * coinUnit)), walletHelper.getChangeAddress()); + } return walletHelper.signTransactionWithUtxos(tx, selectedUtxos); } + + private static double fundTransaction(ArrayList allUtxos, ArrayList selectedUtxos, + ArrayList recipientOutputs, double sendAmount, + double initialFee, boolean subtractFees, + long feePerByte, long minTxFee, long coinUnit, double totalAvailable) { + double totalSelected = 0.0; + double fees = initialFee; + double changeAmount = 0.0; + + allUtxos.sort(Comparator.comparingLong(UTXO::getValue)); + + if (allUtxos.size() == 1) { + UTXO utxo = allUtxos.get(0); + selectedUtxos.add(utxo); + totalSelected = utxo.getAmount(); + double required = sendAmount + fees; + if (totalSelected >= required) { + return totalSelected - required; + } + return 0.0; + } + + UTXO largestUtxo = allUtxos.get(allUtxos.size() - 1); + changeAmount = largestUtxo.getAmount(); + + for (int i = allUtxos.size() - 1; i >= 0; i--) { + fees = calculateFee(selectedUtxos.size() + 1, recipientOutputs.size() + 1, feePerByte, minTxFee, coinUnit); + double requiredAmount = sendAmount + fees; + UTXO utxo = allUtxos.get(i); + + if (largestUtxo.getAmount() < requiredAmount) { + if (totalSelected < requiredAmount) { + if (totalSelected == 0.0) { + changeAmount = utxo.getAmount(); + } + totalSelected += utxo.getAmount(); + selectedUtxos.add(utxo); + continue; + } else { + break; + } + } else { + if (i == 0 || utxo.getAmount() < requiredAmount) { + UTXO prevUtxo = allUtxos.get(i + 1); + if (totalSelected == 0.0) { + changeAmount = prevUtxo.getAmount(); + } + totalSelected += prevUtxo.getAmount(); + selectedUtxos.add(prevUtxo); + break; + } + } + } + + double totalSendAmount = sendAmount + fees; + if (totalSelected < totalSendAmount) { + if (subtractFees) { + return 0.0; + } + throw new RuntimeException("Not enough funds"); + } + + return totalSelected - totalSendAmount; + } + + private static double calculateFee(int inputCount, int outputCount, long feePerByte, long minTxFee, long coinUnit) { + long feeSats = Math.max(feePerByte * (192 * inputCount + 34 * outputCount), minTxFee); + return (double) feeSats / coinUnit; + } + + private static boolean isDust(double amount, NetworkParameters params) { + return amount * Coin.COIN.value < params.getMinNonDustOutput().value; + } + + private static UTXO createTransactionOutput(CoinTicker ticker, String address, double amount) { + LegacyAddress addr = LegacyAddress.fromBase58(null, address); + Coin coin = Coin.valueOf((long) (amount * Coin.COIN.value)); + return new UTXO(ticker, address, "", 0, 0, coin.value); + } + public static void setAsSpent(CoinTicker coinTicker, Transaction transaction, boolean setSpent) { CoinInstance coinInstance = CoinInstance.getInstance(coinTicker); @@ -294,4 +392,16 @@ private static boolean isP2SHAddress(CoinInstance coin, String address) { int version = versionAndDataBytes[0] & 0xFF; return coin.getNetworkParameters().getP2SHHeader() == version; } + + public static long getFeePerByte(NetworkParameters params) { + if (params instanceof HasFeeParams) + return ((HasFeeParams) params).getFeePerByte(); + throw new RuntimeException("Failed to get feePerByte for " + params.getClass().getSimpleName()); + } + + public static long getMinTxFee(NetworkParameters params) { + if (params instanceof HasFeeParams) + return ((HasFeeParams) params).getMinTxFee(); + throw new RuntimeException("Failed to get minTxFee for " + params.getClass().getSimpleName()); + } } diff --git a/src/test/java/ConfigHelperTest.java b/src/test/java/ConfigHelperTest.java index 00da4e5..2f9e51f 100644 --- a/src/test/java/ConfigHelperTest.java +++ b/src/test/java/ConfigHelperTest.java @@ -43,8 +43,8 @@ void testConfigFileCreation() { @Test void testDefaultConfigurationValues() { - assertEquals(0.0001, configHelper.getFee()); - assertTrue(configHelper.isFlatFee()); + assertEquals(0L, configHelper.getFeePerByte()); + assertEquals(0L, configHelper.getMinTxFee()); assertFalse(configHelper.isRpcEnabled()); assertEquals("", configHelper.getRpcUsername()); assertEquals("", configHelper.getRpcPassword()); @@ -53,22 +53,23 @@ void testDefaultConfigurationValues() { } @Test - void testSetAndGetFee() { - double newFee = 0.001; - configHelper.setFee(newFee); + void testSetAndGetFeePerByte() { + long newFeePerByte = 60L; + configHelper.setFeePerByte(newFeePerByte); configHelper.writeConfig(); ConfigHelper reloadedConfig = new ConfigHelper("test"); - assertEquals(newFee, reloadedConfig.getFee()); + assertEquals(newFeePerByte, reloadedConfig.getFeePerByte()); } @Test - void testSetAndGetFlatFee() { - configHelper.setFlatFee(false); + void testSetAndGetMinTxFee() { + long newMinTxFee = 12000L; + configHelper.setMinTxFee(newMinTxFee); configHelper.writeConfig(); ConfigHelper reloadedConfig = new ConfigHelper("test"); - assertFalse(reloadedConfig.isFlatFee()); + assertEquals(newMinTxFee, reloadedConfig.getMinTxFee()); } @Test @@ -157,27 +158,24 @@ void testConfigDirectoryCreation() { @Test void testConfigFilePersistence() { - // Set some values - configHelper.setFee(0.005); - configHelper.setFlatFee(false); + configHelper.setFeePerByte(60L); + configHelper.setMinTxFee(12000L); configHelper.setRpcEnabled(true); configHelper.setAddressCount(100); configHelper.writeConfig(); - // Create new instance and verify values persist ConfigHelper newConfig = new ConfigHelper("test"); - assertEquals(0.005, newConfig.getFee()); - assertFalse(newConfig.isFlatFee()); + assertEquals(60L, newConfig.getFeePerByte()); + assertEquals(12000L, newConfig.getMinTxFee()); assertTrue(newConfig.isRpcEnabled()); assertEquals(100, newConfig.getAddressCount()); } @Test void testLoadConfigFromFile() { - // Write a config file manually String configContent = "{\n" + - " \"fee\": 0.002,\n" + - " \"feeFlat\": false,\n" + + " \"feeperbyte\": 60,\n" + + " \"mintxfee\": 12000,\n" + " \"rpcEnabled\": true,\n" + " \"rpcUsername\": \"testuser\",\n" + " \"rpcPassword\": \"testpass\",\n" + @@ -195,11 +193,10 @@ void testLoadConfigFromFile() { fail("Failed to write config file", e); } - // Create new ConfigHelper instance to load from file ConfigHelper loadedConfig = new ConfigHelper("test"); - assertEquals(0.002, loadedConfig.getFee()); - assertFalse(loadedConfig.isFlatFee()); + assertEquals(60L, loadedConfig.getFeePerByte()); + assertEquals(12000L, loadedConfig.getMinTxFee()); assertTrue(loadedConfig.isRpcEnabled()); assertEquals("testuser", loadedConfig.getRpcUsername()); assertEquals("testpass", loadedConfig.getRpcPassword()); diff --git a/src/test/java/WalletHelperFeeTest.java b/src/test/java/WalletHelperFeeTest.java new file mode 100644 index 0000000..b96a8a5 --- /dev/null +++ b/src/test/java/WalletHelperFeeTest.java @@ -0,0 +1,241 @@ +import io.cloudchains.app.net.protocols.alqocoin.AlqocoinNetworkParameters; +import io.cloudchains.app.net.protocols.bitbay.BitbayNetworkParameters; +import io.cloudchains.app.net.protocols.bitcoin.BitcoinNetworkParameters; +import io.cloudchains.app.net.protocols.blocknet.BlocknetNetworkParameters; +import io.cloudchains.app.net.protocols.blocknet.BlocknetTestnet5NetworkParameters; +import io.cloudchains.app.net.protocols.dashcoin.DashcoinNetworkParameters; +import io.cloudchains.app.net.protocols.digibyte.DigibyteNetworkParameters; +import io.cloudchains.app.net.protocols.dogecoin.DogecoinNetworkParameters; +import io.cloudchains.app.net.protocols.litecoin.LitecoinNetworkParameters; +import io.cloudchains.app.net.protocols.phorecoin.PhorecoinNetworkParameters; +import io.cloudchains.app.net.protocols.pivx.PivxNetworkParameters; +import io.cloudchains.app.net.protocols.pocketcoin.PocketcoinNetworkParameters; +import io.cloudchains.app.net.protocols.ravencoin.RavencoinNetworkParameters; +import io.cloudchains.app.net.protocols.syscoin.SyscoinNetworkParameters; +import io.cloudchains.app.net.protocols.unobtanium.UnobtaniumNetworkParameters; +import io.cloudchains.app.wallet.WalletHelper; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.params.TestNet3Params; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class WalletHelperFeeTest extends TestHelper { + + @BeforeEach + void setup() { + commonSetup(); + } + + @AfterAll + static void cleanup() { + commonCleanup(); + } + + // ======================================================================== + // Happy Path - getFeePerByte() tests + // ======================================================================== + + @Test + void testGetFeePerByte_Bitcoin() { + BitcoinNetworkParameters params = new BitcoinNetworkParameters(); + assertEquals(60L, WalletHelper.getFeePerByte(params)); + } + + @Test + void testGetFeePerByte_Litecoin() { + LitecoinNetworkParameters params = new LitecoinNetworkParameters(); + assertEquals(10L, WalletHelper.getFeePerByte(params)); + } + + @Test + void testGetFeePerByte_Blocknet() { + BlocknetNetworkParameters params = new BlocknetNetworkParameters(); + assertEquals(20L, WalletHelper.getFeePerByte(params)); + } + + @Test + void testGetFeePerByte_Dashcoin() { + DashcoinNetworkParameters params = new DashcoinNetworkParameters(); + assertEquals(5L, WalletHelper.getFeePerByte(params)); + } + + @Test + void testGetFeePerByte_Digibyte() { + DigibyteNetworkParameters params = new DigibyteNetworkParameters(); + assertEquals(200L, WalletHelper.getFeePerByte(params)); + } + + @Test + void testGetFeePerByte_Dogecoin() { + DogecoinNetworkParameters params = new DogecoinNetworkParameters(); + assertEquals(2500L, WalletHelper.getFeePerByte(params)); + } + + @Test + void testGetFeePerByte_Syscoin() { + SyscoinNetworkParameters params = new SyscoinNetworkParameters(); + assertEquals(40L, WalletHelper.getFeePerByte(params)); + } + + @Test + void testGetFeePerByte_Pivx() { + PivxNetworkParameters params = new PivxNetworkParameters(); + assertEquals(20L, WalletHelper.getFeePerByte(params)); + } + + @Test + void testGetFeePerByte_Unobtanium() { + UnobtaniumNetworkParameters params = new UnobtaniumNetworkParameters(); + assertEquals(3L, WalletHelper.getFeePerByte(params)); + } + + @Test + void testGetFeePerByte_Pocketcoin() { + PocketcoinNetworkParameters params = new PocketcoinNetworkParameters(); + assertEquals(20L, WalletHelper.getFeePerByte(params)); + } + + @Test + void testGetFeePerByte_Ravencoin() { + RavencoinNetworkParameters params = new RavencoinNetworkParameters(); + assertEquals(1000L, WalletHelper.getFeePerByte(params)); + } + + @Test + void testGetFeePerByte_Alqocoin() { + AlqocoinNetworkParameters params = new AlqocoinNetworkParameters(); + assertEquals(20L, WalletHelper.getFeePerByte(params)); + } + + @Test + void testGetFeePerByte_Bitbay() { + BitbayNetworkParameters params = new BitbayNetworkParameters(); + assertEquals(100L, WalletHelper.getFeePerByte(params)); + } + + @Test + void testGetFeePerByte_BlocknetTestnet5() { + BlocknetTestnet5NetworkParameters params = new BlocknetTestnet5NetworkParameters(); + assertEquals(20L, WalletHelper.getFeePerByte(params)); + } + + @Test + void testGetFeePerByte_Phorecoin() { + PhorecoinNetworkParameters params = new PhorecoinNetworkParameters(); + assertEquals(20L, WalletHelper.getFeePerByte(params)); + } + + // ======================================================================== + // Happy Path - getMinTxFee() tests + // ======================================================================== + + @Test + void testGetMinTxFee_Bitcoin() { + BitcoinNetworkParameters params = new BitcoinNetworkParameters(); + assertEquals(12000L, WalletHelper.getMinTxFee(params)); + } + + @Test + void testGetMinTxFee_Litecoin() { + LitecoinNetworkParameters params = new LitecoinNetworkParameters(); + assertEquals(5000L, WalletHelper.getMinTxFee(params)); + } + + @Test + void testGetMinTxFee_Blocknet() { + BlocknetNetworkParameters params = new BlocknetNetworkParameters(); + assertEquals(10000L, WalletHelper.getMinTxFee(params)); + } + + @Test + void testGetMinTxFee_Dashcoin() { + DashcoinNetworkParameters params = new DashcoinNetworkParameters(); + assertEquals(2500L, WalletHelper.getMinTxFee(params)); + } + + @Test + void testGetMinTxFee_Digibyte() { + DigibyteNetworkParameters params = new DigibyteNetworkParameters(); + assertEquals(100000L, WalletHelper.getMinTxFee(params)); + } + + @Test + void testGetMinTxFee_Dogecoin() { + DogecoinNetworkParameters params = new DogecoinNetworkParameters(); + assertEquals(225000L, WalletHelper.getMinTxFee(params)); + } + + @Test + void testGetMinTxFee_Syscoin() { + SyscoinNetworkParameters params = new SyscoinNetworkParameters(); + assertEquals(20000L, WalletHelper.getMinTxFee(params)); + } + + @Test + void testGetMinTxFee_Pivx() { + PivxNetworkParameters params = new PivxNetworkParameters(); + assertEquals(10000L, WalletHelper.getMinTxFee(params)); + } + + @Test + void testGetMinTxFee_Unobtanium() { + UnobtaniumNetworkParameters params = new UnobtaniumNetworkParameters(); + assertEquals(1000L, WalletHelper.getMinTxFee(params)); + } + + @Test + void testGetMinTxFee_Pocketcoin() { + PocketcoinNetworkParameters params = new PocketcoinNetworkParameters(); + assertEquals(10000L, WalletHelper.getMinTxFee(params)); + } + + @Test + void testGetMinTxFee_Ravencoin() { + RavencoinNetworkParameters params = new RavencoinNetworkParameters(); + assertEquals(100000L, WalletHelper.getMinTxFee(params)); + } + + @Test + void testGetMinTxFee_Alqocoin() { + AlqocoinNetworkParameters params = new AlqocoinNetworkParameters(); + assertEquals(10000L, WalletHelper.getMinTxFee(params)); + } + + @Test + void testGetMinTxFee_Bitbay() { + BitbayNetworkParameters params = new BitbayNetworkParameters(); + assertEquals(20000L, WalletHelper.getMinTxFee(params)); + } + + @Test + void testGetMinTxFee_BlocknetTestnet5() { + BlocknetTestnet5NetworkParameters params = new BlocknetTestnet5NetworkParameters(); + assertEquals(10000L, WalletHelper.getMinTxFee(params)); + } + + @Test + void testGetMinTxFee_Phorecoin() { + PhorecoinNetworkParameters params = new PhorecoinNetworkParameters(); + assertEquals(10000L, WalletHelper.getMinTxFee(params)); + } + + // ======================================================================== + // Edge Case - unknown coin returns default + // ======================================================================== + + @Test + void testGetFeePerByte_UnknownCoin_Throws() { + NetworkParameters unknownParams = TestNet3Params.get(); + assertThrows(RuntimeException.class, () -> WalletHelper.getFeePerByte(unknownParams)); + } + + @Test + void testGetMinTxFee_UnknownCoin_Throws() { + NetworkParameters unknownParams = TestNet3Params.get(); + assertThrows(RuntimeException.class, () -> WalletHelper.getMinTxFee(unknownParams)); + } +} \ No newline at end of file From 6cc526015666d942438a6578a9a6d96b36aa1525 Mon Sep 17 00:00:00 2001 From: tryiou Date: Fri, 17 Apr 2026 17:54:45 +0200 Subject: [PATCH 47/73] refactor(logging): standardize logging calls and exception handling Replace java.util.logging.Logger.log(Level, String, Throwable) calls with direct logger method invocations (warning, info, finer, etc.). Update exception handling to concatenate messages instead of using varargs format. Remove unused Level imports throughout. This improves logging consistency, reduces boilerplate, and simplifies the logging API usage across all modules. --- AGENTS.md | 2 +- src/main/java/io/cloudchains/app/App.java | 9 +- .../cloudchains/app/console/ConsoleMenu.java | 78 +-- .../io/cloudchains/app/crypto/KeyHandler.java | 45 +- .../io/cloudchains/app/net/CoinInstance.java | 118 ++-- .../io/cloudchains/app/net/CoinTicker.java | 6 +- .../cloudchains/app/net/CoinTickerUtils.java | 11 +- .../app/net/api/JSONRPCMasterServer.java | 7 +- .../app/net/api/JSONRPCServer.java | 7 +- .../app/net/api/http/client/EXRServer.java | 9 +- .../net/api/http/client/EXRServerPool.java | 25 +- .../api/http/client/EXRServerSelector.java | 13 +- .../app/net/api/http/client/EXRWrapper.java | 5 +- .../app/net/api/http/client/HTTPClient.java | 97 ++- .../app/net/api/http/client/HttpUtils.java | 7 +- .../api/http/master/HTTPServerHandler.java | 29 +- .../net/api/http/server/ExceptionHandler.java | 3 +- .../api/http/server/HTTPServerHandler.java | 115 ++-- .../blocknet/BlocknetBlockingClient.java | 7 +- .../blocknet/BlocknetPacketHeader.java | 6 +- .../net/protocols/blocknet/BlocknetPeer.java | 75 ++- .../protocols/blocknet/BlocknetPeerGroup.java | 73 ++- .../blocknet/BlocknetSerializer.java | 17 +- .../app/net/xrouter/XRouterFeeUtils.java | 13 +- .../app/net/xrouter/XRouterMessage.java | 27 +- .../net/xrouter/XRouterMessageSerializer.java | 3 +- .../app/net/xrouter/XRouterPacketHeader.java | 21 +- .../app/net/xrouter/XRouterPacketManager.java | 35 +- .../app/util/AddressDiscoveryService.java | 21 +- .../io/cloudchains/app/util/ConfigHelper.java | 17 +- .../app/util/LogRotationManager.java | 29 +- .../cloudchains/app/util/LogRotationUtil.java | 22 +- .../app/util/XRouterConfiguration.java | 23 +- .../background/BackgroundTimerThread.java | 551 +++++++++--------- src/main/resources/simplelogger.properties | 2 +- 35 files changed, 732 insertions(+), 796 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 08d4170..1e38dde 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -76,7 +76,7 @@ Log messages use bracketed prefixes: `[security]`, `[discovery-BLOCK]`, `[wallet - Crypto ops: try/finally with `Arrays.fill(bytes, (byte) 0)` to clear sensitive data - Call `PBEKeySpec.clearPassword()` after key derivation -- Never use `e.printStackTrace()` — use `LOGGER.log(Level.WARNING, "msg", e)` +- Never use `e.printStackTrace()` — use `LOGGER.level(msg + e.getMessage());` - Catch specific exceptions before generic `Exception` - `RuntimeException` for unrecoverable state; return `null`/`false` for expected failures - AES-CBC + random IV for new encryption; ECB only for legacy migration diff --git a/src/main/java/io/cloudchains/app/App.java b/src/main/java/io/cloudchains/app/App.java index a66a826..9494e1d 100644 --- a/src/main/java/io/cloudchains/app/App.java +++ b/src/main/java/io/cloudchains/app/App.java @@ -38,7 +38,7 @@ public static String getEnv(String key) { try { dotenv = Dotenv.configure().ignoreIfMissing().load(); } catch (Exception e) { - LOGGER.log(Level.FINE, "[app] No .env file found or failed to load", e); + LOGGER.finer("[app] No .env file found or failed to load" + e.getMessage()); } } if (dotenv != null) { @@ -67,8 +67,7 @@ private static Level parseLogLevel(String envValue, Level defaultLevel) { try { return Level.parse(envValue.trim().toUpperCase()); } catch (Exception e) { - LOGGER.log(Level.WARNING, "[app] Invalid log level '{0}', using default {1}", - new Object[]{envValue, defaultLevel}); + LOGGER.warning("[app] Invalid log level '" + envValue + "', using default " + defaultLevel); return defaultLevel; } } @@ -79,7 +78,7 @@ public static void initExrEndpoint() { if (exrEndpoint != null && !exrEndpoint.isEmpty()) { EXR_ENDPOINT = exrEndpoint; exrServerPool = new EXRServerPool(EXR_ENDPOINT); - LOGGER.log(Level.INFO, "[app] EXR mode enabled with " + exrServerPool.getServerCount() + " servers: " + EXR_ENDPOINT); + LOGGER.info("[app] EXR mode enabled with " + exrServerPool.getServerCount() + " servers: " + EXR_ENDPOINT); } } @@ -122,7 +121,7 @@ public static void main(String[] args) { LOGGER.addHandler(fileHandler); } catch (IOException e) { - LOGGER.log(Level.WARNING, "[app] Failed to initialize file handler", e); + LOGGER.warning("[app] Failed to initialize file handler: " + e.getMessage()); } ConsoleHandler consoleHandler = new ConsoleHandler(){ diff --git a/src/main/java/io/cloudchains/app/console/ConsoleMenu.java b/src/main/java/io/cloudchains/app/console/ConsoleMenu.java index acdb085..1af62e9 100644 --- a/src/main/java/io/cloudchains/app/console/ConsoleMenu.java +++ b/src/main/java/io/cloudchains/app/console/ConsoleMenu.java @@ -41,15 +41,15 @@ public ConsoleMenu(String[] args) { public void logBadPassword(String msg) { if (msg == null || msg.isEmpty()) msg = "Bad password"; - LOGGER.log(Level.INFO, "[master] Error(" + CoinInstance.CoinError.CoinErrorCode.BADPASSWORD.name() + "): " + msg); + LOGGER.info("[master] Error(" + CoinInstance.CoinError.CoinErrorCode.BADPASSWORD.name() + "): " + msg); } public void logBadMnemonic() { - LOGGER.log(Level.INFO, "[master] Error(" + CoinInstance.CoinError.CoinErrorCode.BADMNEMONIC.name() + "): Bad mnemonic"); + LOGGER.info("[master] Error(" + CoinInstance.CoinError.CoinErrorCode.BADMNEMONIC.name() + "): Bad mnemonic"); } public void logBadChangePass(String msg) { - LOGGER.log(Level.INFO, "[master] Error(" + CoinInstance.CoinError.CoinErrorCode.CHANGEPASSWORDFAILED.name() + "): " + msg); + LOGGER.info("[master] Error(" + CoinInstance.CoinError.CoinErrorCode.CHANGEPASSWORDFAILED.name() + "): " + msg); } public void init() { @@ -73,7 +73,7 @@ public void init() { if (i + 1 < arguments.length) { String customEndpoint = arguments[i + 1]; if (customEndpoint.startsWith("--")) { - LOGGER.log(Level.WARNING, "Invalid endpoint: " + customEndpoint); + LOGGER.warning("Invalid endpoint: " + customEndpoint); break; } App.BASE_URL = customEndpoint; @@ -83,7 +83,7 @@ public void init() { if (envEndpoint != null && !envEndpoint.isEmpty()) { App.BASE_URL = envEndpoint; } else { - LOGGER.log(Level.WARNING, "Missing custom endpoint after '--development-endpoint'"); + LOGGER.warning("Missing custom endpoint after '--development-endpoint'"); } } break; @@ -92,12 +92,12 @@ public void init() { if (i + 1 < arguments.length) { String exrEndpoint = arguments[i + 1]; if (exrEndpoint.startsWith("--")) { - LOGGER.log(Level.WARNING, "Invalid endpoint: " + exrEndpoint); + LOGGER.warning("Invalid endpoint: " + exrEndpoint); break; } App.EXR_ENDPOINT = exrEndpoint; App.exrServerPool = new EXRServerPool(App.EXR_ENDPOINT); - LOGGER.log(Level.INFO, "[console] EXR mode enabled with " + App.exrServerPool.getServerCount() + " servers: " + App.EXR_ENDPOINT); + LOGGER.info("[console] EXR mode enabled with " + App.exrServerPool.getServerCount() + " servers: " + App.EXR_ENDPOINT); new Thread(() -> { try { Thread.sleep(1000); @@ -112,7 +112,7 @@ public void init() { if (envExrEndpoint != null && !envExrEndpoint.isEmpty()) { App.EXR_ENDPOINT = envExrEndpoint; App.exrServerPool = new EXRServerPool(App.EXR_ENDPOINT); - LOGGER.log(Level.INFO, "[console] EXR mode enabled with " + App.exrServerPool.getServerCount() + " servers: " + App.EXR_ENDPOINT); + LOGGER.info("[console] EXR mode enabled with " + App.exrServerPool.getServerCount() + " servers: " + App.EXR_ENDPOINT); new Thread(() -> { try { Thread.sleep(1000); @@ -122,18 +122,18 @@ public void init() { } }, "EXR-Capability-Prober").start(); } else { - LOGGER.log(Level.WARNING, "Missing EXR endpoint after '--exr-endpoint'"); + LOGGER.warning("Missing EXR endpoint after '--exr-endpoint'"); } } break; } case "--version": - LOGGER.log(Level.INFO, Version.CLIENT_VERSION); + LOGGER.info(Version.CLIENT_VERSION); System.exit(0); break; case "--createdefaultwallet": { if (KeyHandler.existsBaseECKeyFromLocal()) { - LOGGER.log(Level.INFO, "Wallet already exists"); + LOGGER.info("Wallet already exists"); System.exit(0); } @@ -158,7 +158,7 @@ public void init() { } case "--createwalletmnemonic": { if (KeyHandler.existsBaseECKeyFromLocal()) { - LOGGER.log(Level.INFO, "Wallet already exists"); + LOGGER.info("Wallet already exists"); System.exit(0); } @@ -195,7 +195,7 @@ public void init() { int strength = KeyHandler.calculatePasswordStrength(password); if (!KeyHandler.existsBaseECKeyFromLocal() && strength < 9) { - LOGGER.log(Level.INFO, "Bad password."); + LOGGER.info("Bad password."); System.exit(1); } @@ -210,7 +210,7 @@ public void init() { char[] password = readPasswordChars(input, arguments, i + 1, "", "WALLET_PASSWORD"); try { if (!KeyHandler.existsBaseECKeyFromLocal()) { - LOGGER.log(Level.INFO, "No wallet found."); + LOGGER.info("No wallet found."); System.exit(1); } @@ -231,17 +231,17 @@ public void init() { char[] newPassword = readPasswordChars(input, arguments, i + 2, "", null); try { if (currentPassword.length == 0 || newPassword.length == 0) { - LOGGER.log(Level.INFO, "Password cannot be empty"); + LOGGER.info("Password cannot be empty"); System.exit(1); } if (Arrays.equals(currentPassword, newPassword)) { - LOGGER.log(Level.INFO, "New password must be different from old password"); + LOGGER.info("New password must be different from old password"); System.exit(1); } int strength = KeyHandler.calculatePasswordStrength(newPassword); if (strength < 9) { - LOGGER.log(Level.INFO, "Unable to change the password: New password is not strong enough"); + LOGGER.info("Unable to change the password: New password is not strong enough"); System.exit(1); } @@ -249,7 +249,7 @@ public void init() { if (err != null) logBadChangePass(err.getMessage()); else - LOGGER.log(Level.INFO, "Wallet password changed successfully"); + LOGGER.info("Wallet password changed successfully"); } finally { Arrays.fill(currentPassword, '\0'); Arrays.fill(newPassword, '\0'); @@ -281,7 +281,7 @@ public void init() { int strength = KeyHandler.calculatePasswordStrength(password); if (!KeyHandler.existsBaseECKeyFromLocal() && strength < 9) { - LOGGER.log(Level.INFO, "Bad password."); + LOGGER.info("Bad password."); return; } @@ -294,20 +294,20 @@ public void init() { } while (true) { - LOGGER.log(Level.INFO, "-------------------------"); - LOGGER.log(Level.INFO, "1 - Create new wallet " + newWalletStr); - LOGGER.log(Level.INFO, "2 - Decrypt wallet"); - LOGGER.log(Level.INFO, "3 - Import from mnemonic"); - LOGGER.log(Level.INFO, "4 - Quit"); + LOGGER.info("-------------------------"); + LOGGER.info("1 - Create new wallet " + newWalletStr); + LOGGER.info("2 - Decrypt wallet"); + LOGGER.info("3 - Import from mnemonic"); + LOGGER.info("4 - Quit"); - LOGGER.log(Level.INFO, "Selection: "); + LOGGER.info("Selection: "); selection = input.nextInt(); input.nextLine(); switch (selection) { case 1: { if (KeyHandler.existsBaseECKeyFromLocal()) { - LOGGER.log(Level.INFO, "Key already exists"); + LOGGER.info("Key already exists"); return; } @@ -316,14 +316,14 @@ public void init() { if (console != null) { password = console.readPassword("Enter new password: "); } else { - LOGGER.log(Level.INFO, "Enter new password: "); + LOGGER.info("Enter new password: "); password = input.next().toCharArray(); } try { int strength = KeyHandler.calculatePasswordStrength(password); if (!KeyHandler.existsBaseECKeyFromLocal() && strength < 9) { - LOGGER.log(Level.INFO, "Bad password."); + LOGGER.info("Bad password."); return; } completeLogin(password, null, false); @@ -333,20 +333,20 @@ public void init() { return; } case 2: { - LOGGER.log(Level.INFO, "Enter password: "); + LOGGER.info("Enter password: "); Console console = System.console(); char[] password; if (console != null) { password = console.readPassword(); } else { - LOGGER.log(Level.WARNING, "Console not available, using Scanner fallback"); + LOGGER.warning("Console not available, using Scanner fallback"); password = readPasswordChars(input, null, 0, "", null); } try { int strength = KeyHandler.calculatePasswordStrength(password); if (!KeyHandler.existsBaseECKeyFromLocal() && strength < 9) { - LOGGER.log(Level.INFO, "Bad password."); + LOGGER.info("Bad password."); return; } completeLogin(password, null, false); @@ -356,7 +356,7 @@ public void init() { return; } case 3: { - LOGGER.log(Level.INFO, "Enter mnemonic: "); + LOGGER.info("Enter mnemonic: "); String mnemonicInput = input.nextLine().trim(); char[] mnemonicChars = mnemonicInput.toCharArray(); @@ -368,11 +368,11 @@ public void init() { return; } case 4: { - LOGGER.log(Level.INFO, "Exiting..."); + LOGGER.info("Exiting..."); System.exit(0); } default: { - LOGGER.log(Level.INFO, "Unknown Option."); + LOGGER.info("Unknown Option."); } } } @@ -399,7 +399,7 @@ private void completeLogin(char[] password, String userMnemonic, boolean isMnemo CoinInstance.CoinError coinError = CoinInstance.getInstance(CoinTicker.BLOCKNET).init(password, null, isMnemonic, xliteRPC); if (coinError != null) { String msg = "[master] Error(" + coinError.getCode().name() + "): " + coinError.getMessage(); - LOGGER.log(Level.SEVERE, msg); + LOGGER.severe(msg); System.exit(0); } @@ -414,7 +414,7 @@ private void completeLogin(char[] password, String userMnemonic, boolean isMnemo long endTime = System.currentTimeMillis(); long totalTime = endTime - startTime; - LOGGER.log(Level.INFO, "[coin] Concurrent coins initialization completed in " + totalTime + " ms"); + LOGGER.info("[coin] Concurrent coins initialization completed in " + totalTime + " ms"); App.masterRPC.start(); backgroundTimerThread = new BackgroundTimerThread(); @@ -441,15 +441,15 @@ private void initializeCoinsConcurrently(List coinTickers, char[] pa CompletableFuture[] futures = enabledCoins.stream() .map(coinTicker -> CompletableFuture.runAsync(() -> { try { - LOGGER.log(Level.FINE, "[coin] Initializing " + CoinTickerUtils.tickerToString(coinTicker) + " concurrently"); + LOGGER.fine("[coin] Initializing " + CoinTickerUtils.tickerToString(coinTicker) + " concurrently"); CoinInstance.CoinError coinError = CoinInstance.getInstance(coinTicker) .init(password, userMnemonic, isMnemonic, xliteRPC); if (coinError != null) { - LOGGER.log(Level.WARNING, "[" + coinTicker.name() + "] Error(" + + LOGGER.warning("[" + coinTicker.name() + "] Error(" + coinError.getCode().name() + "): " + coinError.getMessage()); } } catch (Exception e) { - LOGGER.log(Level.SEVERE, "Failed to initialize " + coinTicker.name(), e); + LOGGER.severe("Failed to initialize " + coinTicker.name() + ", " + e.getMessage()); } }, executor)) .toArray(CompletableFuture[]::new); diff --git a/src/main/java/io/cloudchains/app/crypto/KeyHandler.java b/src/main/java/io/cloudchains/app/crypto/KeyHandler.java index 3fb6e64..fb70a1a 100644 --- a/src/main/java/io/cloudchains/app/crypto/KeyHandler.java +++ b/src/main/java/io/cloudchains/app/crypto/KeyHandler.java @@ -27,7 +27,6 @@ import java.util.Arrays; import java.util.List; import java.util.Objects; -import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -140,8 +139,7 @@ public static List getBaseSeed(char[] passphrase) { try { WalletData data = readWalletFile(file); if (data.version == VERSION_1_SHA1) { - LOGGER.log(Level.INFO, - "[security] Legacy V1 wallet detected — migrating to V2 (SHA-256/CBC)"); + LOGGER.info("[security] Legacy V1 wallet detected — migrating to V2 (SHA-256/CBC)"); char[] legacyPassphrase = null; try { legacyPassphrase = sha256ToChars(passphrase); @@ -156,14 +154,13 @@ public static List getBaseSeed(char[] passphrase) { return Arrays.asList(seed.split("\\s+")); } catch (BadPaddingException e) { // Wrong password — expected failure, low log level. - LOGGER.log(Level.FINER, - "[security] Decryption failed — wrong passphrase or corrupted wallet"); + LOGGER.finer("[security] Decryption failed — wrong passphrase or corrupted wallet"); return null; } catch (IOException e) { - LOGGER.log(Level.WARNING, "[security] Cannot read wallet file", e); + LOGGER.warning("[security] Cannot read wallet file" + e.getMessage()); return null; } catch (Exception e) { - LOGGER.log(Level.WARNING, "[security] Unexpected error reading wallet", e); + LOGGER.warning("[security] Unexpected error reading wallet" + e.getMessage()); return null; } } @@ -193,7 +190,7 @@ public static boolean importFromMnemonic(List mnemonicList, char[] passp String mnemonic = Joiner.on(" ").join(derived); return writeInitialData(keyFile(), mnemonic, passphrase); } catch (MnemonicException e) { - LOGGER.log(Level.WARNING, "Failed to convert mnemonic to entropy", e); + LOGGER.warning("Failed to convert mnemonic to entropy" + e.getMessage()); return false; } finally { if (entropy != null) Arrays.fill(entropy, (byte) 0); @@ -210,7 +207,7 @@ public static byte[] mnemonicToEntropy(List mnemonicList) { try { return MNEMONIC_CODE.toEntropy(mnemonicList); } catch (Exception e) { - LOGGER.log(Level.WARNING, "Failed to convert mnemonic to entropy", e); + LOGGER.warning("Failed to convert mnemonic to entropy" + e.getMessage()); return null; } } @@ -290,7 +287,7 @@ private static char[] sha256ToChars(char[] input) { } return hex.toString().toCharArray(); } catch (Exception e) { - throw new RuntimeException("Failed to compute SHA-256 for legacy migration", e); + throw new RuntimeException("Failed to compute SHA-256 for legacy migration" + e.getMessage()); } finally { if (hash != null) Arrays.fill(hash, (byte) 0); } @@ -325,7 +322,7 @@ private static SecretKey deriveKey(char[] passphrase, byte[] salt, Arrays.fill(raw, (byte) 0); } } catch (Exception e) { - throw new RuntimeException("Failed to derive encryption key", e); + throw new RuntimeException("Failed to derive encryption key" + e.getMessage()); } finally { spec.clearPassword(); } @@ -350,7 +347,7 @@ private static String[] encryptBaseSeedWithIv(char[] passphrase, new String(Base64.encode(encrypted), StandardCharsets.UTF_8) }; } catch (Exception e) { - throw new RuntimeException("Failed to encrypt seed", e); + throw new RuntimeException("Failed to encrypt seed" + e.getMessage()); } } @@ -473,8 +470,7 @@ private static int detectWalletVersion(String firstLine) { try { return Integer.parseInt(firstLine.substring(VERSION_HEADER.length())); } catch (NumberFormatException e) { - LOGGER.log(Level.WARNING, - "[security] Unrecognised version header — treating wallet as legacy V1"); + LOGGER.warning("[security] Unrecognised version header — treating wallet as legacy V1"); } } return VERSION_1_SHA1; @@ -496,7 +492,7 @@ private static List generateAndPersistNewSeed(char[] passphrase, File fi } return null; } catch (NoSuchAlgorithmException e) { - LOGGER.log(Level.SEVERE, "Failed to obtain strong SecureRandom", e); + LOGGER.severe("Failed to obtain strong SecureRandom"); return null; } } @@ -525,7 +521,7 @@ private static boolean writeInitialData(File keyFile, String mnemonic, char[] pa LOGGER.info("[security] Wallet created with AES-256-CBC / PBKDF2-SHA-256"); return true; } catch (Exception e) { - LOGGER.log(Level.SEVERE, "[security] Failed to create wallet file", e); + LOGGER.severe("[security] Failed to create wallet file"); return false; } finally { if (salt != null) Arrays.fill(salt, (byte) 0); @@ -573,21 +569,21 @@ private static void migrateToNewFormat(char[] passphrase, String seed, File keyF throw new RuntimeException("Post-migration validation failed — new file is unreadable"); } - LOGGER.log(Level.INFO, "[security] Wallet successfully migrated to V2 (SHA-256/CBC)"); + LOGGER.info("[security] Wallet successfully migrated to V2 (SHA-256/CBC)"); } catch (Exception e) { - LOGGER.log(Level.SEVERE, "[security] Migration failed: " + e.getMessage()); + LOGGER.severe("[security] Migration failed: " + e.getMessage()); // Rollback: remove the (potentially partial) new file, then restore the backup. keyFile.delete(); if (legacyBackup != null && legacyBackup.exists()) { if (!legacyBackup.renameTo(keyFile)) { throw new RuntimeException( - "[security] CRITICAL: migration failed AND backup restoration failed", e); + "[security] CRITICAL: migration failed AND backup restoration failed" + e.getMessage()); } - LOGGER.log(Level.INFO, "[security] Legacy wallet restored from backup"); + LOGGER.info("[security] Legacy wallet restored from backup"); } else { - throw new RuntimeException("Migration failed with no backup available", e); + throw new RuntimeException("Migration failed with no backup available" + e.getMessage()); } } finally { if (newSalt != null) Arrays.fill(newSalt, (byte) 0); @@ -619,12 +615,11 @@ private static boolean validateMigration(char[] passphrase, File keyFile) { boolean valid = wordCount == 12 || wordCount == 15 || wordCount == 18 || wordCount == 21 || wordCount == 24; if (!valid) { - LOGGER.log(Level.WARNING, - "[security] Migration validation: unexpected word count " + wordCount); + LOGGER.warning("[security] Migration validation: unexpected word count " + wordCount); } return valid; } catch (Exception e) { - LOGGER.log(Level.WARNING, "[security] Migration validation failed: " + e.getMessage()); + LOGGER.warning("[security] Migration validation failed: " + e.getMessage()); return false; } } @@ -646,4 +641,4 @@ private static final class WalletData { this.encrypted = encrypted; } } -} \ No newline at end of file +} diff --git a/src/main/java/io/cloudchains/app/net/CoinInstance.java b/src/main/java/io/cloudchains/app/net/CoinInstance.java index 5b19659..ff6b3bf 100644 --- a/src/main/java/io/cloudchains/app/net/CoinInstance.java +++ b/src/main/java/io/cloudchains/app/net/CoinInstance.java @@ -21,11 +21,9 @@ //import io.cloudchains.app.net.protocols.phorecoin.PhorecoinNetworkParameters; import io.cloudchains.app.net.protocols.pivx.PivxNetworkParameters; import io.cloudchains.app.net.protocols.pocketcoin.PocketcoinNetworkParameters; -//import io.cloudchains.app.net.protocols.poliscoin.PoliscoinNetworkParameters; import io.cloudchains.app.net.protocols.ravencoin.RavencoinNetworkParameters; import io.cloudchains.app.net.protocols.syscoin.SyscoinNetworkParameters; import io.cloudchains.app.net.protocols.unobtanium.UnobtaniumNetworkParameters; -//import io.cloudchains.app.net.protocols.trezarcoin.TrezarcoinNetworkParameters; import io.cloudchains.app.net.xrouter.XRouterMessage; import io.cloudchains.app.net.xrouter.XRouterPacketManager; import io.cloudchains.app.util.AddressBalance; @@ -49,7 +47,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicInteger; -import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -190,7 +187,7 @@ public AddressBalance generateAddress(boolean updateConfig) { LegacyAddress address = (LegacyAddress) addressKeyPair.getAddress(); DumpedPrivateKey privateKey = addressKeyPair.getPrivateKey(); addressKeyPairs.add(addressKeyPair); - LOGGER.log(Level.FINER, "[wallet] Generated new address, have " + addressKeyPairs.size() + ": " + address.toBase58()); + LOGGER.finer("[wallet] Generated new address, have " + addressKeyPairs.size() + ": " + address.toBase58()); if (updateConfig) { configHelper.setAddressCount(configHelper.getAddressCount() + 1); @@ -276,14 +273,14 @@ public static CoinInstance getInstance(CoinTicker ticker) { */ public static CoinError changePassword(char[] oldPassword, char[] newPassword) { if (!KeyHandler.existsBaseECKeyFromLocal()) { - LOGGER.log(Level.WARNING, "[wallet] Unable to change the password: Wallet not found on disk"); + LOGGER.warning("[wallet] Unable to change the password: Wallet not found on disk"); return new CoinError("Unable to change the password: Wallet not found on disk", CoinError.CoinErrorCode.CHANGEPASSWORDFAILED); } List baseSeed = KeyHandler.getBaseSeed(oldPassword); if (baseSeed == null) { - LOGGER.log(Level.WARNING, "[wallet] Unable to change the password: Incorrect password"); + LOGGER.warning("[wallet] Unable to change the password: Incorrect password"); return new CoinError("Unable to change the password: Incorrect password", CoinError.CoinErrorCode.CHANGEPASSWORDFAILED); } @@ -292,7 +289,7 @@ public static CoinError changePassword(char[] oldPassword, char[] newPassword) { List mnemonic = seed.getMnemonicCode(); if (!KeyHandler.importFromMnemonic(mnemonic, newPassword)) { - LOGGER.log(Level.WARNING, "[wallet] Unable to change the password: Failed to create new wallet file"); + LOGGER.warning("[wallet] Unable to change the password: Failed to create new wallet file"); return new CoinError("Unable to change the password: Failed to create new wallet file", CoinError.CoinErrorCode.CHANGEPASSWORDFAILED); } @@ -322,7 +319,7 @@ public void deinit() { coinRPCServer.deinit(); coinRPCServer.join(); } catch (Exception e) { - LOGGER.log(Level.WARNING, "[coin] Error deinitializing RPC server for " + CoinTickerUtils.tickerToString(ticker), e); + LOGGER.warning("[coin] Error deinitializing RPC server for " + CoinTickerUtils.tickerToString(ticker) + e.getMessage()); } } } @@ -334,7 +331,7 @@ public CoinError init(char[] pw, String userMnemonic, boolean isMnemonic) { public CoinError init(char[] pw, String userMnemonic, boolean isMnemonic, boolean xliteRPC) { switch (ticker) { case BLOCKNET: { - LOGGER.log(Level.FINE, "[coin] Initializing for Blocknet main network."); + LOGGER.fine("[coin] Initializing for Blocknet main network."); blocknetNetworkParameters = new BlocknetNetworkParameters(); networkParameters = blocknetNetworkParameters; hasXRouter = true; @@ -342,7 +339,7 @@ public CoinError init(char[] pw, String userMnemonic, boolean isMnemonic, boolea break; } case BLOCKNET_TESTNET5: { - LOGGER.log(Level.FINE, "[coin] Initializing for Blocknet test network v5."); + LOGGER.fine("[coin] Initializing for Blocknet test network v5."); blocknetNetworkParameters = new BlocknetTestnet5NetworkParameters(); networkParameters = blocknetNetworkParameters; hasXRouter = true; @@ -351,101 +348,90 @@ public CoinError init(char[] pw, String userMnemonic, boolean isMnemonic, boolea break; } case BITCOIN: { - LOGGER.log(Level.FINE, "[coin] Initializing for Bitcoin main network."); + LOGGER.fine("[coin] Initializing for Bitcoin main network."); networkParameters = new BitcoinNetworkParameters(); rpcPort = 8332; break; } // case BITCOIN_CASH: { - // LOGGER.log(Level.FINE, "[coin] Initializing for BitcoinCash main network."); + // LOGGER.fine("[coin] Initializing for BitcoinCash main network."); // networkParameters = new BitcoinCashNetworkParameters(); // rpcPort = 48332; // break; // } case LITECOIN: { - LOGGER.log(Level.FINE, "[coin] Initializing for Litecoin main network."); + LOGGER.fine("[coin] Initializing for Litecoin main network."); networkParameters = new LitecoinNetworkParameters(); rpcPort = 9332; break; } case DASHCOIN: { - LOGGER.log(Level.FINE, "[coin] Initializing for Dashcoin main network."); + LOGGER.fine("[coin] Initializing for Dashcoin main network."); networkParameters = new DashcoinNetworkParameters(); rpcPort = 9998; break; } case DIGIBYTE: { - LOGGER.log(Level.FINE, "[coin] Initializing for Digibyte main network."); + LOGGER.fine("[coin] Initializing for Digibyte main network."); networkParameters = new DigibyteNetworkParameters(); rpcPort = 14022; break; } case DOGECOIN: { - LOGGER.log(Level.FINE, "[coin] Initializing for Dogecoin main network."); + LOGGER.fine("[coin] Initializing for Dogecoin main network."); networkParameters = new DogecoinNetworkParameters(); rpcPort = 22555; break; } case SYSCOIN: { - LOGGER.log(Level.FINE, "[coin] Initializing for Syscoin main network."); + LOGGER.fine("[coin] Initializing for Syscoin main network."); networkParameters = new SyscoinNetworkParameters(); rpcPort = 8370; break; } - // case TREZARCOIN: { - // networkParameters = new TrezarcoinNetworkParameters(); - // rpcPort = 17299; - // break; - // } // case BITBAY: { // networkParameters = new BitbayNetworkParameters(); // rpcPort = 19915; // break; // } case PIVX: { - LOGGER.log(Level.FINE, "[coin] Initializing for Pivx main network."); + LOGGER.fine("[coin] Initializing for Pivx main network."); networkParameters = new PivxNetworkParameters(); rpcPort = 9951; break; } case UNOBTANIUM: { - LOGGER.log(Level.FINE, "[coin] Initializing for Unobtanium main network."); + LOGGER.fine("[coin] Initializing for Unobtanium main network."); networkParameters = new UnobtaniumNetworkParameters(); rpcPort = 65111; break; } case PKOIN: { - LOGGER.log(Level.FINE, "[coin] Initializing for Pocketcoin main network."); + LOGGER.fine("[coin] Initializing for Pocketcoin main network."); networkParameters = new PocketcoinNetworkParameters(); rpcPort = 37071; break; } // case ALQOCOIN: { - // LOGGER.log(Level.FINE, "[coin] Initializing for Alqo main network."); + // LOGGER.fine("[coin] Initializing for Alqo main network."); // networkParameters = new AlqocoinNetworkParameters(); // rpcPort = 55000; // break; // } - // case POLISCOIN: { - // LOGGER.log(Level.FINE, "[coin] Initializing for Polis main network."); - // networkParameters = new PoliscoinNetworkParameters(); - // rpcPort = 24127; - // break; - // } // case PHORECOIN: { - // LOGGER.log(Level.FINE, "[coin] Initializing for Phore main network."); + // LOGGER.fine("[coin] Initializing for Phore main network."); // networkParameters = new PhorecoinNetworkParameters(); // rpcPort = 11772; // break; // } case RAVENCOIN: { - LOGGER.log(Level.FINE, "[coin] Initializing for Ravencoin main network."); + LOGGER.fine("[coin] Initializing for Ravencoin main network."); networkParameters = new RavencoinNetworkParameters(); rpcPort = 8766; break; } default: { - LOGGER.log(Level.FINE, "[coin] ERROR: Invalid/unsupported network: " + ticker.toString()); + LOGGER.fine("[coin] ERROR: Invalid/unsupported network: " + ticker.toString()); return new CoinError("Unsupported coin", CoinError.CoinErrorCode.UNSUPPORTEDCOIN); } } @@ -456,7 +442,7 @@ public CoinError init(char[] pw, String userMnemonic, boolean isMnemonic, boolea rpcPort = rpcPort + 1; if (!configHelper.setRpcPort(rpcPort)) { - LOGGER.log(Level.WARNING, "[coin] Failed to allocate RPC port, skipping RPC config"); + LOGGER.warning("[coin] Failed to allocate RPC port, skipping RPC config"); } configHelper.writeConfig(); } @@ -472,11 +458,11 @@ public CoinError init(char[] pw, String userMnemonic, boolean isMnemonic, boolea if (KeyHandler.existsBaseECKeyFromLocal()) { existsOnDisk = true; if (userMnemonic != null) { - LOGGER.log(Level.WARNING, "[wallet] Wallet already exists on disk, ignoring provided mnemonic"); + LOGGER.warning("[wallet] Wallet already exists on disk, ignoring provided mnemonic"); } } else if (userMnemonic != null) { if (!KeyHandler.importFromMnemonic(Arrays.asList(userMnemonic.split(" ")), pw)) { - LOGGER.log(Level.WARNING, "[wallet] Unable to create wallet from mnemonic"); + LOGGER.warning("[wallet] Unable to create wallet from mnemonic"); return new CoinError("Unable to create wallet from mnemonic", CoinError.CoinErrorCode.BADMNEMONIC); } } @@ -485,7 +471,7 @@ public CoinError init(char[] pw, String userMnemonic, boolean isMnemonic, boolea } if (baseSeed == null) { - LOGGER.log(Level.WARNING, "[wallet] Possible Bad password: Unable to import or create base seed!"); + LOGGER.warning("[wallet] Possible Bad password: Unable to import or create base seed!"); return new CoinError("Bad password", CoinError.CoinErrorCode.BADPASSWORD); } @@ -494,16 +480,16 @@ public CoinError init(char[] pw, String userMnemonic, boolean isMnemonic, boolea wallet = Wallet.fromSeed(networkParameters, seed); if (isBlocknetNetwork()) { String mnemonic = getMnemonic(); - // LOGGER.log(Level.FINE, "[wallet] Mnemonic = " + mnemonic); + // LOGGER.fine("[wallet] Mnemonic = " + mnemonic); } // RUN ADDRESS DISCOVERY ONLY DURING WALLET INITIALIZATION // This ensures discovery runs once at wallet startup in ANY case if (addressDiscoveryEnabled) { - LOGGER.log(Level.FINE, "[coinAddressDiscoveryService created] Running address discovery"); + LOGGER.fine("[coinAddressDiscoveryService created] Running address discovery"); runAddressDiscovery(); } else { - LOGGER.log(Level.FINE, "[coin] Address discovery disabled"); + LOGGER.fine("[coin] Address discovery disabled"); } // Make sure wallet addresses are available @@ -511,7 +497,7 @@ public CoinError init(char[] pw, String userMnemonic, boolean isMnemonic, boolea if (configHelper.getRpcPort() == -1000) { if (!configHelper.setRpcPort(rpcPort)) { - LOGGER.log(Level.WARNING, "[coin] Failed to allocate RPC port, RPC server will not start"); + LOGGER.warning("[coin] Failed to allocate RPC port, RPC server will not start"); } configHelper.writeConfig(); } else { @@ -520,7 +506,7 @@ public CoinError init(char[] pw, String userMnemonic, boolean isMnemonic, boolea if (configHelper.isRpcEnabled() && configHelper.validAuth() && rpcPort != -1) { coinRPCServer = JSONRPCController.getRPCServer(this); - LOGGER.log(Level.INFO, "[rpc] Starting JSON-RPC server for coin " + CoinTickerUtils.tickerToString(getTicker()) + " on port " + getRPCPort()); + LOGGER.info("[rpc] Starting JSON-RPC server for coin " + CoinTickerUtils.tickerToString(getTicker()) + " on port " + getRPCPort()); if (coinRPCServer.isAlive()) coinRPCServer.deinit(); @@ -532,9 +518,9 @@ public CoinError init(char[] pw, String userMnemonic, boolean isMnemonic, boolea // if (isBlocknetNetwork() && hasXRouter()) { // XRouterMessageSerializer xRouterMessageSerializer = (getBlocknetNetworkParameters()).getXRouterMessageSerializer(false); // xRouterPacketManager = new XRouterPacketManager(xRouterMessageSerializer, blocknetNetworkParameters); -// LOGGER.log(Level.FINE, "[coin] This network is a Blocknet network and supports XRouter. Our packet version is " + Integer.toString(XRouterPacketManager.getXRouterPacketVersion(), 16)); +// LOGGER.fine("[coin] This network is a Blocknet network and supports XRouter. Our packet version is " + Integer.toString(XRouterPacketManager.getXRouterPacketVersion(), 16)); // } else { -// LOGGER.log(Level.FINE, "[coin] WARNING: This network (" + CoinTickerUtils.tickerToString(getTicker()) + ") does not support XRouter."); +// LOGGER.fine("[coin] WARNING: This network (" + CoinTickerUtils.tickerToString(getTicker()) + ") does not support XRouter."); // } // // if (isBlocknetNetwork()) { @@ -548,16 +534,16 @@ public CoinError init(char[] pw, String userMnemonic, boolean isMnemonic, boolea // try { // chain = new BlockChain(networkParameters, getWallet(), new SPVBlockStore(networkParameters, spvDat)); // } catch (BlockStoreException ex) { -// LOGGER.log(Level.WARNING, "Error while initializing blockchain object!"); +// LOGGER.warning("Error while initializing blockchain object!"); // ex.printStackTrace(); // return false; // } // } // -// LOGGER.log(Level.INFO, "[coin] Connecting to the (" + getTicker().toString() + ") network."); +// LOGGER.info("[coin] Connecting to the (" + getTicker().toString() + ") network."); // // if (getAddressKeyPairs().size() == 0) { -// LOGGER.log(Level.FINE, "[peer] Have no addresses. Generating forward addresses."); +// LOGGER.fine("[peer] Have no addresses. Generating forward addresses."); // // generateForwardAddresses(true); // } @@ -581,7 +567,7 @@ private void generateForwardAddresses(boolean fromStartup) { updateConfig = true; } - LOGGER.log(Level.FINE, "[wallet] Generating " + configAddressCount + " forward addresses for network " + getTicker().toString() + "."); + LOGGER.fine("[wallet] Generating " + configAddressCount + " forward addresses for network " + getTicker().toString() + "."); // Ensure that internal HD wallet pointer matches the count we're expecting. // Required because wallet doesn't remember last HD wallet address prior to @@ -606,11 +592,11 @@ private void connectToBlocknetNetwork() { try { blocknetPeerGroup.start(); } catch (Exception e) { - LOGGER.log(Level.WARNING, "[coin] Error initializing blocking client for " + CoinTickerUtils.tickerToString(ticker), e); + LOGGER.warning("[coin] Error initializing blocking client for " + CoinTickerUtils.tickerToString(ticker) + e.getMessage()); return; } - LOGGER.log(Level.FINE, "[coin] This network is connecting/connected."); + LOGGER.fine("[coin] This network is connecting/connected."); } public Wallet getWallet() { @@ -657,7 +643,7 @@ public void sendXrGetBlockCount(BlocknetPeer blocknetPeer) { public void sendXrGetUtxos(BlocknetPeer blocknetPeer) { if (System.currentTimeMillis() - lastUtxoUpdate < MINIMUM_UTXO_UPDATE_INTERVAL) { - LOGGER.log(Level.FINE, "[coin] Aborting UTXO checking as the list was updated less than 1 second ago."); + LOGGER.fine("[coin] Aborting UTXO checking as the list was updated less than 1 second ago."); return; } @@ -679,7 +665,7 @@ public String sendXrMessage(BlocknetPeer blocknetPeer, String uuid, String comma XRouterMessage message = null; if (blocknetPeer == null || !blocknetPeer.getHaveConfig().get()) { - LOGGER.log(Level.FINE, "[sendXrMessage] Config not received yet"); + LOGGER.fine("[sendXrMessage] Config not received yet"); return null; } @@ -777,7 +763,7 @@ public String sendXrMessage(BlocknetPeer blocknetPeer, String uuid, String comma break; } default: { - LOGGER.log(Level.FINE, "[coin] ERROR: Unknown XRouter Message! Command: " + command); + LOGGER.fine("[coin] ERROR: Unknown XRouter Message! Command: " + command); uuid = null; break; } @@ -927,10 +913,10 @@ public void addCloudTransaction(CloudTransaction cloudTransaction) { public void processUtxos(List utxoList) { if (utxoList == null) { - LOGGER.log(Level.WARNING, "[coin-" + CoinTickerUtils.tickerToString(getTicker()) + "] processUtxos: null UTXO list received"); + LOGGER.warning("[coin-" + CoinTickerUtils.tickerToString(getTicker()) + "] processUtxos: null UTXO list received"); return; } - LOGGER.log(Level.FINE, "[coin-" + CoinTickerUtils.tickerToString(getTicker()) + "] processUtxos: remote returned " + utxoList.size() + " UTXOs, tracking " + addressKeyPairs.size() + " addresses locally"); + LOGGER.fine("[coin-" + CoinTickerUtils.tickerToString(getTicker()) + "] processUtxos: remote returned " + utxoList.size() + " UTXOs, tracking " + addressKeyPairs.size() + " addresses locally"); int added = 0, skipped = 0; Set clearedAddresses = new HashSet<>(); @@ -947,7 +933,7 @@ public void processUtxos(List utxoList) { for (UTXO utxo : utxoList) { AddressBalance addressBalance = getAddress(utxo.getAddress()); if (addressBalance == null) { - LOGGER.log(Level.WARNING, "[utxo-parser] Warning: Encountered non-tracked address in reply: " + utxo.getAddress()); + LOGGER.warning("[utxo-parser] Warning: Encountered non-tracked address in reply: " + utxo.getAddress()); skipped++; continue; } @@ -955,10 +941,10 @@ public void processUtxos(List utxoList) { if (isNewUtxo) { added++; addCloudTransaction(new CloudTransaction(utxo)); - LOGGER.log(Level.FINER, "[utxo-parser] Added new UTXO, address: " + utxo.getAddress() + " value: " + utxo.getAmount()); + LOGGER.finer("[utxo-parser] Added new UTXO, address: " + utxo.getAddress() + " value: " + utxo.getAmount()); } } - LOGGER.log(Level.FINE, "[coin-" + CoinTickerUtils.tickerToString(getTicker()) + "] processUtxos: added=" + added + ", skipped=" + skipped); + LOGGER.fine("[coin-" + CoinTickerUtils.tickerToString(getTicker()) + "] processUtxos: added=" + added + ", skipped=" + skipped); setLastUtxoUpdate(System.currentTimeMillis()); } @@ -984,7 +970,7 @@ public void reloadConfig() { coinRPCServer = JSONRPCController.getRPCServer(this); - LOGGER.log(Level.INFO, "[rpc] Starting JSON-RPC server for coin " + CoinTickerUtils.tickerToString(getTicker()) + " on port " + getRPCPort()); + LOGGER.info("[rpc] Starting JSON-RPC server for coin " + CoinTickerUtils.tickerToString(getTicker()) + " on port " + getRPCPort()); coinRPCServer.start(); } @@ -1043,13 +1029,13 @@ public void runAddressDiscovery() { String currency = CoinTickerUtils.tickerToString(this.getTicker()); if (!configHelper.isRpcEnabled()) { - LOGGER.log(Level.FINE, "[coin-" + currency + "] RPC disabled, skipping address discovery"); + LOGGER.fine("[coin-" + currency + "] RPC disabled, skipping address discovery"); return; } if (discoveryService == null) { discoveryService = new AddressDiscoveryService(this); - LOGGER.log(Level.FINE, "[coin-" + currency + "] AddressDiscoveryService created"); + LOGGER.fine("[coin-" + currency + "] AddressDiscoveryService created"); } int discoveredCount = discoveryService.discoverAddressCount(); @@ -1057,17 +1043,17 @@ public void runAddressDiscovery() { int currentCount = configHelper.getAddressCount(); if (discoveredCount > currentCount) { - LOGGER.log(Level.INFO, "[coin-" + currency + "] Address discovery found " + + LOGGER.info("[coin-" + currency + "] Address discovery found " + discoveredCount + " addresses (was " + currentCount + ")"); // Update config and generate missing addresses configHelper.setAddressCount(discoveredCount); configHelper.writeConfig(); - LOGGER.log(Level.INFO, "[coin-" + currency + "] Updated address count to " + + LOGGER.info("[coin-" + currency + "] Updated address count to " + discoveredCount); } else { - LOGGER.log(Level.FINE, "[coin-" + currency + "] No new addresses discovered, " + + LOGGER.fine("[coin-" + currency + "] No new addresses discovered, " + "keeping current count: " + currentCount); } } diff --git a/src/main/java/io/cloudchains/app/net/CoinTicker.java b/src/main/java/io/cloudchains/app/net/CoinTicker.java index 8b83865..4d6cf71 100644 --- a/src/main/java/io/cloudchains/app/net/CoinTicker.java +++ b/src/main/java/io/cloudchains/app/net/CoinTicker.java @@ -13,11 +13,9 @@ public enum CoinTicker { DASHCOIN, DIGIBYTE, DOGECOIN, - TREZARCOIN, SYSCOIN, PIVX, ALQOCOIN, - POLISCOIN, PHORECOIN, RAVENCOIN, BITBAY, @@ -39,13 +37,13 @@ public static List coins() { DASHCOIN, DIGIBYTE, DOGECOIN, -// TREZARCOIN, - not support on backend + SYSCOIN, PIVX, UNOBTANIUM, PKOIN, // ALQOCOIN, - not support on backend -// POLISCOIN, - not support on backend + // PHORECOIN, - not support on backend RAVENCOIN // BITBAY - not support on backend diff --git a/src/main/java/io/cloudchains/app/net/CoinTickerUtils.java b/src/main/java/io/cloudchains/app/net/CoinTickerUtils.java index 3db43d4..5a8db9c 100644 --- a/src/main/java/io/cloudchains/app/net/CoinTickerUtils.java +++ b/src/main/java/io/cloudchains/app/net/CoinTickerUtils.java @@ -26,11 +26,9 @@ public class CoinTickerUtils { tickers.put(CoinTicker.RAVENCOIN, "RVN"); tickers.put(CoinTicker.ALQOCOIN, "XLQ"); - // TODO Temporarily disable PHORE and POLIS until supported -// tickers.put(CoinTicker.POLISCOIN, "POLIS"); + // TODO Temporarily disable PHORE until supported // tickers.put(CoinTicker.PHORECOIN, "PHR"); - tickers.put(CoinTicker.TREZARCOIN, "TZC"); - tickers.put(CoinTicker.BITBAY, "BAY"); + tickers.put(CoinTicker.BITBAY, "BAY"); tickers.put(CoinTicker.UNOBTANIUM, "UNO"); tickers.put(CoinTicker.PKOIN, "PKOIN"); @@ -63,10 +61,9 @@ public static CoinTicker[] getActiveTickers() { CoinTicker.RAVENCOIN, CoinTicker.ALQOCOIN, - // TODO Temporarily disable PHORE and POLIS until supported -// CoinTicker.POLISCOIN, + // TODO Temporarily disable PHORE until supported // CoinTicker.PHORECOIN, - CoinTicker.TREZARCOIN, + CoinTicker.BITBAY, CoinTicker.UNOBTANIUM, CoinTicker.PKOIN, diff --git a/src/main/java/io/cloudchains/app/net/api/JSONRPCMasterServer.java b/src/main/java/io/cloudchains/app/net/api/JSONRPCMasterServer.java index 6df5f41..2997cdb 100644 --- a/src/main/java/io/cloudchains/app/net/api/JSONRPCMasterServer.java +++ b/src/main/java/io/cloudchains/app/net/api/JSONRPCMasterServer.java @@ -9,7 +9,6 @@ import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; -import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -30,7 +29,7 @@ public class JSONRPCMasterServer extends Thread { public void run() { workerGroup = new NioEventLoopGroup(2); try { - LOGGER.log(Level.INFO, "[rpc] Starting master RPC server on port " + port + "."); + LOGGER.info("[rpc] Starting master RPC server on port " + port + "."); ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(workerGroup) @@ -45,14 +44,14 @@ public void run() { channel.closeFuture().sync(); } catch (Exception e) { if (!stopping) { - LOGGER.log(Level.WARNING, "[rpc-master] Error during master RPC server operation", e); + LOGGER.warning("[rpc-master] Error during master RPC server operation" + e.getMessage()); } } } public void deinit() { stopping = true; - LOGGER.log(Level.FINER, "[json-rpc-server] Interrupting server."); + LOGGER.finer("[json-rpc-server] Interrupting server."); if (channel != null) { channel.close(); diff --git a/src/main/java/io/cloudchains/app/net/api/JSONRPCServer.java b/src/main/java/io/cloudchains/app/net/api/JSONRPCServer.java index 43621f6..ed10e34 100644 --- a/src/main/java/io/cloudchains/app/net/api/JSONRPCServer.java +++ b/src/main/java/io/cloudchains/app/net/api/JSONRPCServer.java @@ -11,7 +11,6 @@ import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; -import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -44,19 +43,19 @@ public void run() { channel = bootstrap.bind(port).sync().channel(); - LOGGER.log(Level.FINER, "[rpc] Starting RPC server for " + CoinTickerUtils.tickerToString(coin.getTicker()) + " on port " + port + "."); + LOGGER.finer("[rpc] Starting RPC server for " + CoinTickerUtils.tickerToString(coin.getTicker()) + " on port " + port + "."); channel.closeFuture().sync(); } catch (Exception e) { if (!stopping) { - LOGGER.log(Level.WARNING, "[rpc-server] Error during RPC server operation for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); + LOGGER.warning("[rpc-server] Error during RPC server operation for " + CoinTickerUtils.tickerToString(coin.getTicker()) + e.getMessage()); } } } public void deinit() { stopping = true; - LOGGER.log(Level.FINER, "[json-rpc-server] Interrupting server."); + LOGGER.finer("[json-rpc-server] Interrupting server."); if (channel != null) { channel.close(); diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/EXRServer.java b/src/main/java/io/cloudchains/app/net/api/http/client/EXRServer.java index c99d8d2..9d70565 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/client/EXRServer.java +++ b/src/main/java/io/cloudchains/app/net/api/http/client/EXRServer.java @@ -8,7 +8,6 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -68,18 +67,18 @@ public boolean probeCapabilities() { supportedCoins.add(coin); } } catch (Exception e) { - LOGGER.log(Level.FINER, "[exr-server] Failed to map coin " + coinName, e); + LOGGER.finer("[exr-server] Failed to map coin " + coinName + ", " + e.getMessage()); } } } capabilitiesProbed = true; - LOGGER.log(Level.INFO, "[exr-server] Probed capabilities for " + endpoint + ", supports: " + supportedCoins.size() + " coins: " + + LOGGER.info("[exr-server] Probed capabilities for " + endpoint + ", supports: " + supportedCoins.size() + " coins: " + supportedCoins.stream().map(CoinTickerUtils::tickerToString) .reduce((a, b) -> a + ", " + b).orElse("none")); return !supportedCoins.isEmpty(); } catch (Exception e) { - LOGGER.log(Level.WARNING, "[exr-server] Failed to probe capabilities for " + endpoint, e); + LOGGER.warning("[exr-server] Failed to probe capabilities for " + endpoint + ", " + e.getMessage()); return false; } } @@ -95,7 +94,7 @@ public boolean isHealthy() { healthy = result != null && !result.isJsonNull(); } catch (Exception e) { healthy = false; - LOGGER.log(Level.WARNING, "[exr-server] Health check failed for " + endpoint, e); + LOGGER.warning("[exr-server] Health check failed for " + endpoint + ", " + e.getMessage()); } lastHealthCheck = now; return healthy; diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/EXRServerPool.java b/src/main/java/io/cloudchains/app/net/api/http/client/EXRServerPool.java index 5e2315c..0c956d6 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/client/EXRServerPool.java +++ b/src/main/java/io/cloudchains/app/net/api/http/client/EXRServerPool.java @@ -7,7 +7,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicInteger; -import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -54,7 +53,7 @@ public void startCapabilityProbing() { if (capabilitiesProbed || servers.isEmpty()) { return; } - LOGGER.log(Level.INFO, "[exr-pool] Starting capability probing for " + servers.size() + " servers"); + LOGGER.info("[exr-pool] Starting capability probing for " + servers.size() + " servers"); // Start capability probing in background new Thread(this::probeAllCapabilities, "EXR-Capability-Prober").start(); } @@ -63,7 +62,7 @@ public void probeAllCapabilities() { if (capabilitiesProbed) { return; } - LOGGER.log(Level.INFO, "[exr-pool] Starting capability probing for " + servers.size() + " servers"); + LOGGER.info("[exr-pool] Starting capability probing for " + servers.size() + " servers"); // Probe each server concurrently List probeThreads = new ArrayList<>(); for (EXRServer server : servers) { @@ -71,7 +70,7 @@ public void probeAllCapabilities() { try { server.probeCapabilities(); } catch (Exception e) { - LOGGER.log(Level.WARNING, "[exr-pool] Failed to probe server " + server.getEndpoint(), e); + LOGGER.warning("[exr-pool] Failed to probe server " + server.getEndpoint() + ", " + e.getMessage()); } }); probeThreads.add(t); @@ -83,7 +82,7 @@ public void probeAllCapabilities() { t.join(CAPABILITY_PROBE_TIMEOUT_MS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); - LOGGER.log(Level.WARNING, "[exr-pool] Capability probing interrupted", e); + LOGGER.warning("[exr-pool] Capability probing interrupted" + e.getMessage()); } } // Build coin-to-servers mapping with synchronization @@ -97,26 +96,26 @@ public void probeAllCapabilities() { } capabilitiesProbed = true; logCapabilityResults(); - LOGGER.log(Level.INFO, "[exr-pool] Capability probing completed"); + LOGGER.info("[exr-pool] Capability probing completed"); } private void logCapabilityResults() { - LOGGER.log(Level.INFO, "[exr-pool] === EXR Server Capabilities ==="); + LOGGER.info("[exr-pool] === EXR Server Capabilities ==="); for (EXRServer server : servers) { if (server.isCapabilitiesProbed()) { String supportedCoins = server.getSupportedCoins().stream() .map(coin -> CoinTickerUtils.tickerToString(coin)) .reduce((a, b) -> a + ", " + b).orElse("none"); - LOGGER.log(Level.INFO, "[exr-pool] " + server.getEndpoint() + " supports: " + supportedCoins); + LOGGER.info("[exr-pool] " + server.getEndpoint() + " supports: " + supportedCoins); } else { - LOGGER.log(Level.WARNING, "[exr-pool] " + server.getEndpoint() + " capability probe failed"); + LOGGER.warning("[exr-pool] " + server.getEndpoint() + " capability probe failed"); } } - LOGGER.log(Level.INFO, "[exr-pool] === Coin Distribution ==="); + LOGGER.info("[exr-pool] === Coin Distribution ==="); for (CoinTicker coin : CoinTicker.coins()) { List supportingServers = coinToServersMap.get(coin); if (supportingServers != null && !supportingServers.isEmpty()) { - LOGGER.log(Level.INFO, "[exr-pool] " + CoinTickerUtils.tickerToString(coin) + " supported by " + supportingServers.size() + " servers"); + LOGGER.info("[exr-pool] " + CoinTickerUtils.tickerToString(coin) + " supported by " + supportingServers.size() + " servers"); } } } @@ -132,11 +131,11 @@ private void initializeServers(String endpoints) { EXRServer server = new EXRServer(trimmed); servers.add(server); endpointToServerMap.put(trimmed, server); - LOGGER.log(Level.INFO, "[exr-pool] Added EXR server: " + trimmed); + LOGGER.info("[exr-pool] Added EXR server: " + trimmed); } } if (!servers.isEmpty()) { - LOGGER.log(Level.INFO, "[exr-pool] Created pool with " + servers.size() + " servers"); + LOGGER.info("[exr-pool] Created pool with " + servers.size() + " servers"); } } diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/EXRServerSelector.java b/src/main/java/io/cloudchains/app/net/api/http/client/EXRServerSelector.java index 987219b..a260711 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/client/EXRServerSelector.java +++ b/src/main/java/io/cloudchains/app/net/api/http/client/EXRServerSelector.java @@ -6,7 +6,6 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; -import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -80,7 +79,7 @@ public EXRServerSelectorImpl(AtomicInteger currentIndex) { @Override public EXRServer selectHealthyServer(List servers) { if (servers.isEmpty()) { - LOGGER.log(Level.WARNING, "[server-selector] No servers available for selection"); + LOGGER.warning("[server-selector] No servers available for selection"); return null; } @@ -90,12 +89,12 @@ public EXRServer selectHealthyServer(List servers) { int index = (start + i) % servers.size(); EXRServer server = servers.get(index); if (server.isHealthy()) { - LOGGER.log(Level.FINE, "[server-selector] Selected server: " + server.getEndpoint()); + LOGGER.fine("[server-selector] Selected server: " + server.getEndpoint()); return server; } } - LOGGER.log(Level.WARNING, "[server-selector] No healthy servers available"); + LOGGER.warning("[server-selector] No healthy servers available"); return null; // All servers unhealthy } @@ -112,7 +111,7 @@ public EXRServer selectHealthyServer(List servers) { @Override public EXRServer selectServerForCoin(List supportingServers, CoinTicker coin) { if (supportingServers == null || supportingServers.isEmpty()) { - LOGGER.log(Level.WARNING, "[server-selector] NO EXR SERVERS SUPPORT COIN: " + + LOGGER.warning("[server-selector] NO EXR SERVERS SUPPORT COIN: " + CoinTickerUtils.tickerToString(coin)); return null; // FAIL - NO FALLBACK TO BASE_URL } @@ -152,7 +151,7 @@ private List getHealthySupportingServers(List supportingSe */ private EXRServer selectFromHealthyServers(List healthyServers, CoinTicker coin) { if (healthyServers.isEmpty()) { - LOGGER.log(Level.WARNING, "[server-selector] NO HEALTHY EXR SERVERS FOR COIN: " + + LOGGER.warning("[server-selector] NO HEALTHY EXR SERVERS FOR COIN: " + CoinTickerUtils.tickerToString(coin)); return null; } @@ -162,7 +161,7 @@ private EXRServer selectFromHealthyServers(List healthyServers, CoinT // Double-check that the selected server actually supports the coin if (!selectedServer.hasCapability(coin)) { - LOGGER.log(Level.SEVERE, "[server-selector] CRITICAL ERROR: Selected server " + + LOGGER.severe("[server-selector] CRITICAL ERROR: Selected server " + selectedServer.getEndpoint() + " does NOT support coin " + CoinTickerUtils.tickerToString(coin)); return null; diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/EXRWrapper.java b/src/main/java/io/cloudchains/app/net/api/http/client/EXRWrapper.java index f772ec0..1fe2f5f 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/client/EXRWrapper.java +++ b/src/main/java/io/cloudchains/app/net/api/http/client/EXRWrapper.java @@ -14,7 +14,6 @@ import java.io.IOException; import java.net.URI; import java.util.List; -import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -90,7 +89,7 @@ public JsonObject execute(String method, List params) { String responseBody = executeHttpRequest(httpPost, "execute POST for " + method + " " + currency); return responseBody != null ? processResponse(responseBody) : null; } catch (IOException e) { - LOGGER.log(Level.WARNING, LOG_TAG + " execute POST failed for " + method + " " + currency + " endpoint: " + endpoint, e); + LOGGER.warning(LOG_TAG + " execute POST failed for " + method + " " + currency + " endpoint: " + endpoint + ", " + e.getMessage()); return null; } finally { httpPost.reset(); @@ -118,7 +117,7 @@ public void close() { try { client.close(); } catch (IOException e) { - LOGGER.log(Level.WARNING, LOG_TAG + " Failed to close HTTP client", e); + LOGGER.warning(LOG_TAG + " Failed to close HTTP client" + e.getMessage()); } } diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java b/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java index a190104..7f3a09a 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java +++ b/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java @@ -43,7 +43,6 @@ import java.util.Iterator; import java.util.List; import java.util.concurrent.ConcurrentHashMap; -import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -69,7 +68,7 @@ private boolean waitForCapabilities(int timeoutMs) { return true; } - LOGGER.log(Level.FINE, "[httpclient] Waiting for EXR capabilities to be probed (timeout: " + timeoutMs + "ms)"); + LOGGER.fine("[httpclient] Waiting for EXR capabilities to be probed (timeout: " + timeoutMs + "ms)"); int waitTime = 0; while (!App.exrServerPool.isCapabilitiesProbed() && waitTime < timeoutMs) { @@ -78,13 +77,13 @@ private boolean waitForCapabilities(int timeoutMs) { waitTime += HttpClientConfig.CAPABILITY_PROBE_WAIT_INTERVAL_MS; } catch (InterruptedException e) { Thread.currentThread().interrupt(); - LOGGER.log(Level.WARNING, "[httpclient] Waiting for capabilities was interrupted"); + LOGGER.warning("[httpclient] Waiting for capabilities was interrupted"); return false; } } boolean probed = App.exrServerPool.isCapabilitiesProbed(); - LOGGER.log(Level.FINE, "[httpclient] EXR capabilities " + + LOGGER.fine("[httpclient] EXR capabilities " + (probed ? "probed successfully" : "still not probed") + " after waiting " + waitTime + "ms"); @@ -174,7 +173,7 @@ private String executePostRequest(String endpoint, JsonObject params) { try { httpPost.setEntity(new StringEntity(params.toString())); } catch (UnsupportedEncodingException e) { - LOGGER.log(Level.WARNING, "executePostRequest failed to set entity " + endpoint + " err: " + e.toString()); + LOGGER.warning("executePostRequest failed to set entity " + endpoint + " err: " + e.getMessage()); httpPost.reset(); return null; } @@ -212,7 +211,7 @@ public void close() { try { client.close(); } catch (IOException e) { - LOGGER.log(Level.WARNING, "[httpclient] Failed to close HTTP client", e); + LOGGER.warning("[httpclient] Failed to close HTTP client" + e.getMessage()); } } @@ -286,7 +285,7 @@ private String executeEXRPost(String endpoint, JsonObject params) { coin = CoinTickerUtils.stringToTicker(coinString); if (coin == null) { // Log the failed coin extraction for debugging - LOGGER.log(Level.WARNING, "[httpclient] Failed to extract coin from parameter: " + coinString); + LOGGER.warning("[httpclient] Failed to extract coin from parameter: " + coinString); // Not a coin-specific request } } @@ -297,31 +296,31 @@ private String executeEXRPost(String endpoint, JsonObject params) { if (coin != null) { // Wait for capabilities to be probed if not already done if (!App.exrServerPool.isCapabilitiesProbed()) { - LOGGER.log(Level.FINE, "[httpclient] Waiting for EXR capabilities to be probed for coin: " + + LOGGER.fine("[httpclient] Waiting for EXR capabilities to be probed for coin: " + CoinTickerUtils.tickerToString(coin)); if (!waitForCapabilities(HttpClientConfig.CAPABILITY_PROBE_WAIT_TIMEOUT_MS)) { // Wait up to 10 seconds - LOGGER.log(Level.WARNING, "[httpclient] EXR capabilities not probed yet for coin: " + + LOGGER.warning("[httpclient] EXR capabilities not probed yet for coin: " + CoinTickerUtils.tickerToString(coin)); return null; // FAIL - NO FALLBACK TO BASE_URL } } if (App.exrServerPool.isCapabilitiesProbed()) { server = App.exrServerPool.selectServerForCoin(coin); - // LOGGER.log(Level.INFO, "[httpclient] DEBUG: selectServerForCoin returned: " + + // LOGGER.info("[httpclient] DEBUG: selectServerForCoin returned: " + // (server != null ? server.getEndpoint() : "null")); if (server == null) { - LOGGER.log(Level.WARNING, "[httpclient] NO EXR SERVER SUPPORTS COIN: " + + LOGGER.warning("[httpclient] NO EXR SERVER SUPPORTS COIN: " + CoinTickerUtils.tickerToString(coin)); return null; // FAIL - NO FALLBACK TO BASE_URL } else { - LOGGER.log(Level.INFO, "[httpclient] DEBUG: Selected server " + server.getEndpoint() + + LOGGER.info("[httpclient] DEBUG: Selected server " + server.getEndpoint() + " for coin " + CoinTickerUtils.tickerToString(coin) + ", method: " + method); } } else { // Capabilities still not probed after waiting - LOGGER.log(Level.WARNING, "[httpclient] EXR capabilities not probed yet for coin: " + + LOGGER.warning("[httpclient] EXR capabilities not probed yet for coin: " + CoinTickerUtils.tickerToString(coin)); return null; // FAIL - NO FALLBACK TO BASE_URL } @@ -331,13 +330,13 @@ private String executeEXRPost(String endpoint, JsonObject params) { if (coin != null) { server = App.exrServerPool.selectServerForCoin(coin); if (server == null) { - LOGGER.log(Level.WARNING, "[httpclient] NO EXR SERVER SUPPORTS COIN: " + + LOGGER.warning("[httpclient] NO EXR SERVER SUPPORTS COIN: " + CoinTickerUtils.tickerToString(coin)); return null; // FAIL - NO FALLBACK TO BASE_URL } } else { // No coin extracted - this should not happen for coin-specific requests - LOGGER.log(Level.SEVERE, "[httpclient] Cannot route request: coin extraction failed"); + LOGGER.severe("[httpclient] Cannot route request: coin extraction failed"); return null; // FAIL instead of using wrong server } } @@ -412,11 +411,11 @@ public JsonArray getUtxosUncached(CoinTicker coinTicker, String[] addresses) { params.addProperty("method", "getutxos"); params.add("params", innerParams); String res = executePostRequest("/", params); - LOGGER.log(Level.FINER, "[httpclient] getUtxosUncached " + coinInstance.getTicker() + " " + res); + LOGGER.finer("[httpclient] getUtxosUncached " + coinInstance.getTicker() + " " + res); if (res == null) { - LOGGER.log(Level.WARNING, "[httpclient] getUtxosUncached " + coinInstance.getTicker() + " null post result"); + LOGGER.warning("[httpclient] getUtxosUncached " + coinInstance.getTicker() + " null post result"); return null; } @@ -426,14 +425,14 @@ public JsonArray getUtxosUncached(CoinTicker coinTicker, String[] addresses) { jsonObject = new JSONObject(res); utxoArr = jsonObject.getJSONArray("utxos"); } catch (Exception e) { - LOGGER.log(Level.WARNING, "[httpclient] getUtxosUncached " + coinInstance.getTicker() + " parse error - " + e.getMessage()); + LOGGER.warning("[httpclient] getUtxosUncached " + coinInstance.getTicker() + " parse error - " + e.getMessage()); } if (jsonObject == null || utxoArr == null) { if (jsonObject == null) - LOGGER.log(Level.WARNING, "[httpclient] getUtxosUncached " + coinInstance.getTicker() + " null jsonObject"); + LOGGER.warning("[httpclient] getUtxosUncached " + coinInstance.getTicker() + " null jsonObject"); if (utxoArr == null) - LOGGER.log(Level.WARNING, "[httpclient] getUtxosUncached " + coinInstance.getTicker() + " null utxoArr"); + LOGGER.warning("[httpclient] getUtxosUncached " + coinInstance.getTicker() + " null utxoArr"); return null; } @@ -481,7 +480,7 @@ public JsonArray getUtxos(CoinTicker coinTicker, int expiry) { ArrayList utxoParams = coinInstance.getUTXOParams(); if (utxoParams.size() == 0) { - LOGGER.log(Level.WARNING, "[httpclient] getUtxos " + coinInstance.getTicker() + " null param size"); + LOGGER.warning("[httpclient] getUtxos " + coinInstance.getTicker() + " null param size"); return null; } @@ -491,11 +490,11 @@ public JsonArray getUtxos(CoinTicker coinTicker, int expiry) { params.addProperty("method", "getutxos"); params.add("params", innerParams); String res = executePostRequest("/", params); - LOGGER.log(Level.FINER, "[httpclient] getUtxos " + coinInstance.getTicker() + " " + res); + LOGGER.finer("[httpclient] getUtxos " + coinInstance.getTicker() + " " + res); if (res == null) { - LOGGER.log(Level.WARNING, "[httpclient] getUtxos " + coinInstance.getTicker() + " null post result"); + LOGGER.warning("[httpclient] getUtxos " + coinInstance.getTicker() + " null post result"); return null; } @@ -505,14 +504,14 @@ public JsonArray getUtxos(CoinTicker coinTicker, int expiry) { jsonObject = new JSONObject(res); utxoArr = jsonObject.getJSONArray("utxos"); } catch (Exception e) { - LOGGER.log(Level.WARNING, "[httpclient] getUtxos " + coinInstance.getTicker() + " parse error - " + e.getMessage()); + LOGGER.warning("[httpclient] getUtxos " + coinInstance.getTicker() + " parse error - " + e.getMessage()); } if (jsonObject == null || utxoArr == null) { if (jsonObject == null) - LOGGER.log(Level.WARNING, "[httpclient] getUtxos " + coinInstance.getTicker() + " null jsonObject"); + LOGGER.warning("[httpclient] getUtxos " + coinInstance.getTicker() + " null jsonObject"); if (utxoArr == null) - LOGGER.log(Level.WARNING, "[httpclient] getUtxos " + coinInstance.getTicker() + " null utxoArr"); + LOGGER.warning("[httpclient] getUtxos " + coinInstance.getTicker() + " null utxoArr"); return null; } @@ -547,7 +546,7 @@ public JsonObject getRawTransaction(CoinTicker coinTicker, String txid, boolean params.addProperty("method", "getrawtransaction"); params.add("params", innerParams); String res = executePostRequest("/", params); - LOGGER.log(Level.FINER, "[httpclient] getRawTransaction " + res); + LOGGER.finer("[httpclient] getRawTransaction " + res); if (res == null) return null; @@ -566,7 +565,7 @@ public JsonObject getRawMempool(CoinTicker coinTicker, boolean verbose) { params.addProperty("method", "getrawmempool"); params.add("params", innerParams); String res = executePostRequest("/", params); - LOGGER.log(Level.FINER, "[httpclient] getRawMempool " + res); + LOGGER.finer("[httpclient] getRawMempool " + res); if (res == null) return null; @@ -594,7 +593,7 @@ public void getBlockCount(CoinTicker coinTicker) { coinInstance.addBlockCount(coinTicker, blockCount); - LOGGER.log(Level.FINER, "[httpclient] Got blockcount for currency " + coinTicker + " - " + blockCount); + LOGGER.finer("[httpclient] Got blockcount for currency " + coinTicker + " - " + blockCount); } public void getAllBlockCounts() { @@ -618,7 +617,7 @@ public void getAllBlockCounts() { coinInstance.addBlockCount(coinInstance.getTicker(), blockCount); coinInstance.resetUpdateFailures(); - LOGGER.log(Level.FINER, "[httpclient] Got blockcount for currency " + ticker + " - " + blockCount); + LOGGER.finer("[httpclient] Got blockcount for currency " + ticker + " - " + blockCount); } } @@ -634,7 +633,7 @@ public JsonObject getBlock(CoinTicker coinTicker, String hash, boolean verbose) params.addProperty("method", "getblock"); params.add("params", innerParams); String res = executePostRequest("/", params); - LOGGER.log(Level.FINER, "[httpclient] getBlock " + res); + LOGGER.finer("[httpclient] getBlock " + res); if (res == null) return null; @@ -651,7 +650,7 @@ public JsonObject getBlockHash(CoinTicker coinTicker, int height) { params.addProperty("method", "getblockhash"); params.add("params", innerParams); String res = executePostRequest("/", params); - LOGGER.log(Level.FINER, "[httpclient] getBlockHash " + res); + LOGGER.finer("[httpclient] getBlockHash " + res); if (res == null) return null; @@ -671,7 +670,7 @@ public JsonObject getTransaction(CoinTicker coinTicker, String txid, boolean ver params.addProperty("method", "gettransaction"); params.add("params", innerParams); String res = executePostRequest("/", params); - LOGGER.log(Level.FINER, "[httpclient] getTransaction " + res); + LOGGER.finer("[httpclient] getTransaction " + res); if (res == null) return null; @@ -690,7 +689,7 @@ public JsonObject sendRawTransaction(CoinTicker coinTicker, String rawTx) { params.addProperty("method", "sendrawtransaction"); params.add("params", innerParams); String res = executePostRequest("/", params); - LOGGER.log(Level.FINER, "[httpclient] sendRawTransaction " + res); + LOGGER.finer("[httpclient] sendRawTransaction " + res); if (res == null) return null; @@ -715,7 +714,7 @@ public JsonArray getHistory(CoinTicker coinTicker, int startTime, int endTime, i ArrayList utxoParams = coinInstance.getUTXOParams(); if (utxoParams.size() == 0) { - LOGGER.log(Level.WARNING, "[httpclient] getHistory " + coinInstance.getTicker() + " null param size"); + LOGGER.warning("[httpclient] getHistory " + coinInstance.getTicker() + " null param size"); return null; } @@ -726,9 +725,9 @@ public JsonArray getHistory(CoinTicker coinTicker, int startTime, int endTime, i params.add("params", innerParams); String res = executePostRequest("/", params); - LOGGER.log(Level.FINER, "[httpclient] getHistory " + coinInstance.getTicker() + " " + res); + LOGGER.finer("[httpclient] getHistory " + coinInstance.getTicker() + " " + res); if (res == null) { - LOGGER.log(Level.WARNING, "[httpclient] getHistory " + coinInstance.getTicker() + " null post result"); + LOGGER.warning("[httpclient] getHistory " + coinInstance.getTicker() + " null post result"); return null; } @@ -736,19 +735,19 @@ public JsonArray getHistory(CoinTicker coinTicker, int startTime, int endTime, i try { json = new Gson().fromJson(res, JsonArray.class); } catch (Exception e) { - LOGGER.log(Level.WARNING, "[httpclient] getHistory parsing error - Response: " + res + " - " + e.getMessage()); + LOGGER.warning("[httpclient] getHistory parsing error - Response: " + res + " - " + e.getMessage()); return null; } if (json == null) { - LOGGER.log(Level.WARNING, "[httpclient] getHistory " + coinInstance.getTicker() + " null json"); + LOGGER.warning("[httpclient] getHistory " + coinInstance.getTicker() + " null json"); return null; } List historyList = new ArrayList<>(); for (JsonElement elements : json) { for (JsonElement element : elements.getAsJsonArray()) { - //LOGGER.log(Level.WARNING, "*** DEBUG *** [httpclient] getHistory " + element); + //LOGGER.warning("*** DEBUG *** [httpclient] getHistory " + element); JsonObject jsonObject = element.getAsJsonObject(); List fromAddresses = new Gson().fromJson(jsonObject.get("from_addresses"), new TypeToken>() { @@ -774,7 +773,7 @@ public JsonArray getHistory(CoinTicker coinTicker, int startTime, int endTime, i // Return the latest transaction history JsonArray txs = coinInstance.getAllTransactions(); if (txs == null) { - LOGGER.log(Level.WARNING, "[httpclient] getHistory " + coinInstance.getTicker() + " null txs"); + LOGGER.warning("[httpclient] getHistory " + coinInstance.getTicker() + " null txs"); return null; } @@ -802,7 +801,7 @@ public JsonArray getTransactionHistory(CoinTicker coinTicker, int startTime, int ArrayList utxoParams = coinInstance.getUTXOParams(); if (utxoParams.size() == 0) { - LOGGER.log(Level.WARNING, "[httpclient] getAddressHistory " + coinInstance.getTicker() + " null param size"); + LOGGER.warning("[httpclient] getAddressHistory " + coinInstance.getTicker() + " null param size"); return null; } @@ -813,22 +812,22 @@ public JsonArray getTransactionHistory(CoinTicker coinTicker, int startTime, int params.add("params", innerParams); String res = executePostRequest("/", params); - LOGGER.log(Level.FINER, "[httpclient] getAddressHistory " + coinInstance.getTicker() + " " + res); + LOGGER.finer("[httpclient] getAddressHistory " + coinInstance.getTicker() + " " + res); if (res == null) { - LOGGER.log(Level.WARNING, "[httpclient] getAddressHistory " + coinInstance.getTicker() + " null post result"); + LOGGER.warning("[httpclient] getAddressHistory " + coinInstance.getTicker() + " null post result"); return null; } JsonArray json = new Gson().fromJson(res, JsonArray.class); if (json == null) { - LOGGER.log(Level.WARNING, "[httpclient] getAddressHistory " + coinInstance.getTicker() + " null json"); + LOGGER.warning("[httpclient] getAddressHistory " + coinInstance.getTicker() + " null json"); return null; } List historyList = new ArrayList<>(); for (JsonElement elements : json) { for (JsonElement element : elements.getAsJsonArray()) { - //LOGGER.log(Level.WARNING, "*** DEBUG *** [httpclient] getAddressHistory " + element ); + //LOGGER.warning("*** DEBUG *** [httpclient] getAddressHistory " + element ); JsonObject jsonObject = element.getAsJsonObject(); String txid = jsonObject.get("tx_hash").getAsString(); @@ -850,7 +849,7 @@ public JsonArray getTransactionHistory(CoinTicker coinTicker, int startTime, int } else ++fails; } catch (Exception e) { - LOGGER.log(Level.WARNING, "[httpclient] getRawTransaction failed - " + e.getMessage()); + LOGGER.warning("[httpclient] getRawTransaction failed - " + e.getMessage()); ++fails; } } @@ -880,7 +879,7 @@ public JsonArray getTransactionHistory(CoinTicker coinTicker, int startTime, int } else ++fails; } catch (Exception e) { - LOGGER.log(Level.WARNING, "[httpclient] getRawTransaction(vout) failed - " + e.getMessage()); + LOGGER.warning("[httpclient] getRawTransaction(vout) failed - " + e.getMessage()); ++fails; } } @@ -965,7 +964,7 @@ public JsonArray getTransactionHistory(CoinTicker coinTicker, int startTime, int // Return the latest transaction history JsonArray txs = coinInstance.getAllTransactions(); if (txs == null) { - LOGGER.log(Level.WARNING, "[httpclient] getAddressHistory " + coinInstance.getTicker() + " null txs"); + LOGGER.warning("[httpclient] getAddressHistory " + coinInstance.getTicker() + " null txs"); return null; } diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/HttpUtils.java b/src/main/java/io/cloudchains/app/net/api/http/client/HttpUtils.java index 233e671..0659628 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/client/HttpUtils.java +++ b/src/main/java/io/cloudchains/app/net/api/http/client/HttpUtils.java @@ -8,7 +8,6 @@ import org.apache.http.util.EntityUtils; import java.io.IOException; -import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -44,11 +43,11 @@ public static String executeHttpRequest(CloseableHttpClient client, EntityUtils.consume(entity); return result; } else { - LOGGER.log(Level.WARNING, HttpClientConfig.LOG_TAG + " " + operation + " failed"); + LOGGER.warning(HttpClientConfig.LOG_TAG + " " + operation + " failed"); return null; } } catch (IOException e) { - LOGGER.log(Level.WARNING, HttpClientConfig.LOG_TAG + " " + operation + " failed", e); + LOGGER.warning(HttpClientConfig.LOG_TAG + " " + operation + " failed" + e.getMessage()); return null; } finally { request.reset(); @@ -56,7 +55,7 @@ public static String executeHttpRequest(CloseableHttpClient client, try { response.close(); } catch (IOException e) { - LOGGER.log(Level.WARNING, HttpClientConfig.LOG_TAG + " Failed to close response", e); + LOGGER.warning(HttpClientConfig.LOG_TAG + " Failed to close response" + e.getMessage()); } } } diff --git a/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerHandler.java b/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerHandler.java index 7d02968..802ae1a 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerHandler.java +++ b/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerHandler.java @@ -21,7 +21,6 @@ import java.nio.charset.StandardCharsets; import java.security.SecureRandom; -import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -58,7 +57,7 @@ public void channelInactive(ChannelHandlerContext ctx) throws Exception { @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { - LOGGER.log(Level.WARNING, "[http-master] Exception caught on channel", cause); + LOGGER.warning("[http-master] Exception caught on channel: " + cause.getMessage()); FullHttpResponse httpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST); writeResponse(ctx, httpResponse, null); @@ -68,7 +67,7 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { - LOGGER.log(Level.FINER, "[http-server-handler] DEBUG: Channel read complete. Flushing context."); + LOGGER.finer("[http-server-handler] DEBUG: Channel read complete. Flushing context."); super.channelReadComplete(ctx); ctx.flush(); } @@ -97,14 +96,14 @@ protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) String credentials = new String(credDecoded, StandardCharsets.UTF_8); final String[] values = credentials.split(":", 2); if (values.length < 2) { - LOGGER.log(Level.WARNING, "[http-server-handler] Malformed Basic Auth header"); + LOGGER.warning("[http-server-handler] Malformed Basic Auth header"); } else { headerUser = values[0]; headerPass = values[1]; if (headerUser.equals(configHelper.getRpcUsername()) && headerPass.equals(configHelper.getRpcPassword())) { successfulAuth = true; - LOGGER.log(Level.FINER, "[http-server-handler] Successful Auth"); + LOGGER.finer("[http-server-handler] Successful Auth"); } } } @@ -165,8 +164,8 @@ protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throw new IllegalArgumentException("Bad JSON-RPC request by client."); } } catch (Exception e) { - LOGGER.log(Level.INFO, "Failed Content: " + content); - LOGGER.log(Level.WARNING, "[http-master] Failed to parse JSON-RPC request", e); + LOGGER.info("Failed Content: " + content); + LOGGER.warning("[http-master] Failed to parse JSON-RPC request" + e.getMessage()); JsonObject errorParsingJSON = new JsonObject(); errorParsingJSON.addProperty("code", -1001); errorParsingJSON.addProperty("message", "Error parsing JSON."); @@ -174,9 +173,9 @@ protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) response.add("error", errorParsingJSON); response.add("result", JsonNull.INSTANCE); if (e instanceof IllegalArgumentException) { - LOGGER.log(Level.FINER, "[http-server-handler] WARNING: Client sent valid JSON, but did not specify method and/or parameters!"); + LOGGER.finer("[http-server-handler] WARNING: Client sent valid JSON, but did not specify method and/or parameters!"); } else { - LOGGER.log(Level.FINER, "[http-server-handler] WARNING: Client sent invalid JSON!"); + LOGGER.finer("[http-server-handler] WARNING: Client sent invalid JSON!"); } status = HttpResponseStatus.BAD_REQUEST; } @@ -187,10 +186,10 @@ protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) String method = jsonReq.get("method").getAsString(); JsonArray params = jsonReq.get("params").getAsJsonArray(); - LOGGER.log(Level.INFO, "[http-server-handler] RPC CALL: " + method + " PARAMS: " + params.toString().replace(",", ", ")); + LOGGER.info("[http-server-handler] RPC CALL: " + method + " PARAMS: " + params.toString().replace(",", ", ")); response = getResponse(method, params); - LOGGER.log(Level.FINER, response.toString()); + LOGGER.finer(response.toString()); } else { ByteBuf responseContent = Unpooled.copiedBuffer(response.toString(), CharsetUtil.UTF_8); FullHttpResponse httpResponse = new DefaultFullHttpResponse(request.protocolVersion(), status, responseContent); @@ -251,7 +250,7 @@ private JsonObject getResponse(String method, JsonArray params) { Thread.sleep(500); instance.reloadConfig(); } catch (InterruptedException e) { - LOGGER.log(Level.WARNING, "[http-master] Interrupted during reloadconfig for " + ticker, e); + LOGGER.warning("[http-master] Interrupted during reloadconfig for " + ticker + ", " + e.getMessage()); } }); t.setDaemon(true); @@ -273,7 +272,7 @@ private JsonObject getResponse(String method, JsonArray params) { // continue; // // try { -// LOGGER.log(Level.INFO, instance.getTicker().toString()); +// LOGGER.info(instance.getTicker().toString()); // instance.reloadConfig(); // } catch (Exception e) { // success = false; @@ -334,8 +333,8 @@ private boolean writeResponse(ChannelHandlerContext ctx, FullHttpResponse httpRe httpResponse.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); httpResponse.headers().set(HttpHeaderNames.SERVER, CoinInstance.getVersionString()); - LOGGER.log(Level.FINER, "[http-server-handler] Writing response to channel. Keep alive? " + keepAlive); - LOGGER.log(Level.FINER, "[http-server-handler] Response content: " + httpResponse.content().toString(CharsetUtil.UTF_8)); + LOGGER.finer("[http-server-handler] Writing response to channel. Keep alive? " + keepAlive); + LOGGER.finer("[http-server-handler] Response content: " + httpResponse.content().toString(CharsetUtil.UTF_8)); ctx.write(httpResponse); return keepAlive; diff --git a/src/main/java/io/cloudchains/app/net/api/http/server/ExceptionHandler.java b/src/main/java/io/cloudchains/app/net/api/http/server/ExceptionHandler.java index 437791a..cd0b82e 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/server/ExceptionHandler.java +++ b/src/main/java/io/cloudchains/app/net/api/http/server/ExceptionHandler.java @@ -11,7 +11,6 @@ import io.netty.handler.codec.http.*; import io.netty.util.CharsetUtil; -import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -21,7 +20,7 @@ public class ExceptionHandler extends ChannelDuplexHandler { @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { - LOGGER.log(Level.WARNING, "[http-server] Channel exception", cause); + LOGGER.warning("[http-server] Channel exception" + cause.getMessage()); writeErrorResponse(ctx); } diff --git a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java b/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java index 37dc49a..0d922bc 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java +++ b/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java @@ -39,7 +39,6 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; -import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -76,7 +75,7 @@ public void channelInactive(ChannelHandlerContext ctx) throws Exception { @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { - LOGGER.log(Level.WARNING, "[http-server-handler] Exception caught on channel for " + CoinTickerUtils.tickerToString(coin.getTicker()), cause); + LOGGER.warning("[http-server-handler] Exception caught on channel for " + CoinTickerUtils.tickerToString(coin.getTicker()) + ", " + cause.getMessage()); FullHttpResponse httpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST); writeResponse(ctx, httpResponse, null); @@ -87,7 +86,7 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { - LOGGER.log(Level.FINER, "[http-server-handler] DEBUG: Channel read complete. Flushing context."); + LOGGER.finer("[http-server-handler] DEBUG: Channel read complete. Flushing context."); super.channelReadComplete(ctx); ctx.flush(); } @@ -116,7 +115,7 @@ protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) String credentials = new String(credDecoded, StandardCharsets.UTF_8); final String[] values = credentials.split(":", 2); if (values.length < 2) { - LOGGER.log(Level.WARNING, "[http-server-handler] Malformed Basic Auth header"); + LOGGER.warning("[http-server-handler] Malformed Basic Auth header"); } else { headerUser = values[0]; headerPass = values[1]; @@ -126,7 +125,7 @@ protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) && MessageDigest.isEqual(headerPass.getBytes(StandardCharsets.UTF_8), configHelper.getRpcPassword().getBytes(StandardCharsets.UTF_8))) { successfulAuth = true; - LOGGER.log(Level.FINER, "[http-server-handler] Successful Auth"); + LOGGER.finer("[http-server-handler] Successful Auth"); } } } @@ -198,8 +197,8 @@ protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throw new IllegalArgumentException("Bad JSON-RPC request by client."); } } catch (Exception e) { - LOGGER.log(Level.INFO, "Failed Content: " + content); - LOGGER.log(Level.WARNING, "[http-server-handler] Failed to parse JSON-RPC request for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); + LOGGER.info("Failed Content: " + content); + LOGGER.warning("[http-server-handler] Failed to parse JSON-RPC request for " + CoinTickerUtils.tickerToString(coin.getTicker()) + ", " + e.getMessage()); JsonObject errorParsingJSON = new JsonObject(); errorParsingJSON.addProperty("code", -1001); errorParsingJSON.addProperty("message", "Error parsing JSON."); @@ -207,9 +206,9 @@ protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) response.add("error", errorParsingJSON); response.add("result", JsonNull.INSTANCE); if (e instanceof IllegalArgumentException) { - LOGGER.log(Level.WARNING, "[http-server-handler] WARNING: Client sent valid JSON, but did not specify method and/or parameters!"); + LOGGER.warning("[http-server-handler] WARNING: Client sent valid JSON, but did not specify method and/or parameters!"); } else { - LOGGER.log(Level.WARNING, "[http-server-handler] WARNING: Client sent invalid JSON!"); + LOGGER.warning("[http-server-handler] WARNING: Client sent invalid JSON!"); } status = HttpResponseStatus.BAD_REQUEST; } @@ -220,10 +219,10 @@ protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) String method = jsonReq.get("method").getAsString(); JsonArray params = jsonReq.get("params").getAsJsonArray(); - LOGGER.log(Level.INFO, "[http-server-handler] RPC CALL: " + coin.getTicker() + " " + method + " PARAMS: " + params.toString().replace(",", ", ")); + LOGGER.info("[http-server-handler] RPC CALL: " + coin.getTicker() + " " + method + " PARAMS: " + params.toString().replace(",", ", ")); response = getResponse(method, params); - LOGGER.log(Level.FINER, response.toString()); + LOGGER.finer(response.toString()); } else { ByteBuf responseContent = Unpooled.copiedBuffer(response.toString(), CharsetUtil.UTF_8); FullHttpResponse httpResponse = new DefaultFullHttpResponse(request.protocolVersion(), status, responseContent); @@ -254,7 +253,7 @@ private JsonObject getResponse(String method, JsonArray params) { try { Thread.sleep(500); } catch (InterruptedException e) { - LOGGER.log(Level.WARNING, "[http-server-handler] Interrupted during reloadconfig for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); + LOGGER.warning("[http-server-handler] Interrupted during reloadconfig for " + CoinTickerUtils.tickerToString(coin.getTicker()) + ", " + e.getMessage()); } coin.reloadConfig(); @@ -429,7 +428,7 @@ private JsonObject getResponse(String method, JsonArray params) { errorJSON.addProperty("message", "Error parsing transaction!"); response.add("error", errorJSON); - LOGGER.log(Level.WARNING, "[http-server-handler] Error parsing transaction in sendrawtransaction for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); + LOGGER.warning("[http-server-handler] Error parsing transaction in sendrawtransaction for " + CoinTickerUtils.tickerToString(coin.getTicker()) + ", " + e.getMessage()); break; } @@ -573,7 +572,7 @@ private JsonObject getResponse(String method, JsonArray params) { errorJSON.addProperty("message", "Error parsing JSON!"); response.add("error", errorJSON); - LOGGER.log(Level.WARNING, "[http-server-handler] Error parsing JSON in getblock for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); + LOGGER.warning("[http-server-handler] Error parsing JSON in getblock for " + CoinTickerUtils.tickerToString(coin.getTicker()) + ", " + e.getMessage()); break; } @@ -714,7 +713,7 @@ private JsonObject getResponse(String method, JsonArray params) { throw new JsonParseException("Invalid outputs format"); } } catch (JsonParseException e) { - LOGGER.log(Level.WARNING, + LOGGER.warning( "[http-server-handler] ERROR: Error while parsing JSON for createrawtransaction!"); response.add("result", JsonNull.INSTANCE); JsonObject errorJSON = new JsonObject(); @@ -737,7 +736,7 @@ private JsonObject getResponse(String method, JsonArray params) { int vout = input.get("vout").getAsInt(); tx.addInput(Sha256Hash.wrap(txid), vout, ScriptBuilder.createInputScript(null)); } catch (Exception e) { - LOGGER.log(Level.WARNING, + LOGGER.warning( "[http-server-handler] ERROR: Error while constructing transaction (input phase)!"); txConstructionError(response, e, "Error while constructing transaction (input phase)"); inputSuccess = false; @@ -755,14 +754,14 @@ private JsonObject getResponse(String method, JsonArray params) { LegacyAddress address = LegacyAddress.fromBase58(coin.getNetworkParameters(), entry.address); Coin outputValue = Coin.valueOf((long) Math.floor(entry.amount * Coin.COIN.value)); if (isP2SHAddress(entry.address)) { - LOGGER.log(Level.FINE, "[http-server-handler] P2SH Address Found: " + entry.address); + LOGGER.fine("[http-server-handler] P2SH Address Found: " + entry.address); Script p2shScript = ScriptBuilder.createP2SHOutputScript(address.getHash()); tx.addOutput(outputValue, p2shScript); } } catch (Exception e) { - LOGGER.log(Level.WARNING, + LOGGER.warning( "[http-server-handler] ERROR: Error while constructing transaction (output phase)!"); - LOGGER.log(Level.WARNING, "[http-server-handler] Error in createrawtransaction P2SH output phase for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); + LOGGER.warning("[http-server-handler] Error in createrawtransaction P2SH output phase for " + CoinTickerUtils.tickerToString(coin.getTicker()) + ", " + e.getMessage()); txConstructionError(response, e, "Error while constructing transaction (output phase)"); outputSuccess = false; } @@ -776,9 +775,9 @@ private JsonObject getResponse(String method, JsonArray params) { tx.addOutput(outputValue, address); } } catch (Exception e) { - LOGGER.log(Level.WARNING, + LOGGER.warning( "[http-server-handler] ERROR: Error while constructing transaction (output phase)!"); - LOGGER.log(Level.WARNING, "[http-server-handler] Error in createrawtransaction output phase for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); + LOGGER.warning("[http-server-handler] Error in createrawtransaction output phase for " + CoinTickerUtils.tickerToString(coin.getTicker()) + ", " + e.getMessage()); txConstructionError(response, e, "Error while constructing transaction (output phase)"); outputSuccess = false; } @@ -788,7 +787,7 @@ private JsonObject getResponse(String method, JsonArray params) { break; String hexTx = new String(Hex.encode(tx.bitcoinSerialize())); - LOGGER.log(Level.FINER, "[http-server-handler] DEBUG: Raw transaction = " + hexTx); + LOGGER.finer("[http-server-handler] DEBUG: Raw transaction = " + hexTx); response.addProperty("result", hexTx); response.add("error", JsonNull.INSTANCE); break; @@ -810,7 +809,7 @@ private JsonObject getResponse(String method, JsonArray params) { try { tx = new Transaction(coin.getNetworkParameters(), Hex.decode(rawTx)); } catch (Exception e) { - LOGGER.log(Level.WARNING, "[http-server-handler] Error decoding raw transaction in decoderawtransaction for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); + LOGGER.warning("[http-server-handler] Error decoding raw transaction in decoderawtransaction for " + CoinTickerUtils.tickerToString(coin.getTicker()) + ", " + e.getMessage()); getInvalidTxResponse(response, e); break; } @@ -839,8 +838,8 @@ private JsonObject getResponse(String method, JsonArray params) { vin.add(thisVin); } catch (Exception e) { - LOGGER.log(Level.WARNING, "[http-server-handler] ERROR: Error while parsing transaction inputs!"); - LOGGER.log(Level.WARNING, "[http-server-handler] Error parsing transaction inputs for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); + LOGGER.warning("[http-server-handler] ERROR: Error while parsing transaction inputs!"); + LOGGER.warning("[http-server-handler] Error parsing transaction inputs for " + CoinTickerUtils.tickerToString(coin.getTicker()) + ", " + e.getMessage()); response.add("result", JsonNull.INSTANCE); JsonObject errorJSON = new JsonObject(); @@ -888,8 +887,8 @@ private JsonObject getResponse(String method, JsonArray params) { vout.add(thisVout); } catch (Exception e) { - LOGGER.log(Level.WARNING, "[http-server-handler] ERROR: Error while parsing transaction outputs!"); - LOGGER.log(Level.WARNING, "[http-server-handler] Error parsing transaction outputs for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); + LOGGER.warning("[http-server-handler] ERROR: Error while parsing transaction outputs!"); + LOGGER.warning("[http-server-handler] Error parsing transaction outputs for " + CoinTickerUtils.tickerToString(coin.getTicker()) + ", " + e.getMessage()); response.add("result", JsonNull.INSTANCE); JsonObject errorJSON = new JsonObject(); @@ -931,7 +930,7 @@ private JsonObject getResponse(String method, JsonArray params) { try { tx = new Transaction(coin.getNetworkParameters(), Hex.decode(rawTx)); } catch (Exception e) { - LOGGER.log(Level.WARNING, "[http-server-handler] Error decoding raw tx in signrawtransaction for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); + LOGGER.warning("[http-server-handler] Error decoding raw tx in signrawtransaction for " + CoinTickerUtils.tickerToString(coin.getTicker()) + ", " + e.getMessage()); getInvalidTxResponse(response, e); break; } @@ -972,7 +971,7 @@ private JsonObject getResponse(String method, JsonArray params) { response.add("result", resultJSON); response.add("error", JsonNull.INSTANCE); - LOGGER.log(Level.FINER, "[DEBUG http-server-handler] Signed Raw Transaction: " + response.toString()); + LOGGER.finer("[DEBUG http-server-handler] Signed Raw Transaction: " + response.toString()); break; } case "gettxout": { @@ -1001,7 +1000,7 @@ private JsonObject getResponse(String method, JsonArray params) { UTXO requested = this.getUtxo(Sha256Hash.wrap(txid), n); if (requested != null) { - LOGGER.log(Level.FINE, "[http-server-handler] Using cached UTXO for gettxout"); + LOGGER.fine("[http-server-handler] Using cached UTXO for gettxout"); org.bitcoinj.core.UTXO utxo = requested.createUTXO(); JsonObject resultJSON = new JsonObject(); @@ -1031,7 +1030,7 @@ private JsonObject getResponse(String method, JsonArray params) { } if (!includeMempool) { - LOGGER.log(Level.WARNING, "[http-server-handler] WARNING: Client requested UTXO that is not ours!"); + LOGGER.warning("[http-server-handler] WARNING: Client requested UTXO that is not ours!"); response.add("result", JsonNull.INSTANCE); JsonObject errorJSON = new JsonObject(); @@ -1112,7 +1111,7 @@ private JsonObject getResponse(String method, JsonArray params) { } if (!isOurs) { - LOGGER.log(Level.WARNING, "[http-server-handler] WARNING: Client requested UTXO that cannot be ours!"); + LOGGER.warning("[http-server-handler] WARNING: Client requested UTXO that cannot be ours!"); response.add("result", JsonNull.INSTANCE); JsonObject errorJSON = new JsonObject(); @@ -1146,7 +1145,7 @@ private JsonObject getResponse(String method, JsonArray params) { } if (!unspent) { - LOGGER.log(Level.WARNING, "[http-server-handler] WARNING: Client requested UTXO that was already spent!"); + LOGGER.warning("[http-server-handler] WARNING: Client requested UTXO that was already spent!"); response.add("result", JsonNull.INSTANCE); JsonObject errorJSON = new JsonObject(); @@ -1167,8 +1166,8 @@ private JsonObject getResponse(String method, JsonArray params) { response.add("result", resultJSON); response.add("error", JsonNull.INSTANCE); } catch (Exception e) { - LOGGER.log(Level.WARNING, "[http-server-handler] ERROR: Error while parsing transaction!"); - LOGGER.log(Level.WARNING, "[http-server-handler] Error parsing transaction in gettxout for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); + LOGGER.warning("[http-server-handler] ERROR: Error while parsing transaction!"); + LOGGER.warning("[http-server-handler] Error parsing transaction in gettxout for " + CoinTickerUtils.tickerToString(coin.getTicker()) + ", " + e.getMessage()); response.add("result", JsonNull.INSTANCE); JsonObject errorJSON = new JsonObject(); @@ -1297,12 +1296,12 @@ private JsonObject getResponse(String method, JsonArray params) { String derivedAddr = LegacyAddress.fromKey(coin.getNetworkParameters(), key).toBase58(); if (!addr.equals(derivedAddr)) { - LOGGER.log(Level.WARNING, "[http-server-handler] ERROR: Addresses do not match! Failing."); + LOGGER.warning("[http-server-handler] ERROR: Addresses do not match! Failing."); verified = false; } } catch (Exception e) { - LOGGER.log(Level.WARNING, "[http-server-handler] Error while verifying signature! Invalid signature?"); - LOGGER.log(Level.WARNING, "[http-server-handler] Error verifying message for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); + LOGGER.warning("[http-server-handler] Error while verifying signature! Invalid signature?"); + LOGGER.warning("[http-server-handler] Error verifying message for " + CoinTickerUtils.tickerToString(coin.getTicker()) + ", " + e.getMessage()); response.addProperty("result", verified); response.add("error", JsonNull.INSTANCE); @@ -1336,7 +1335,7 @@ private JsonObject getResponse(String method, JsonArray params) { errorJSON.addProperty("message", "Error parsing JSON!"); response.add("error", errorJSON); - LOGGER.log(Level.WARNING, "[http-server-handler] Error parsing JSON in sendtransaction for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); + LOGGER.warning("[http-server-handler] Error parsing JSON in sendtransaction for " + CoinTickerUtils.tickerToString(coin.getTicker()) + ", " + e.getMessage()); break; } @@ -1358,13 +1357,13 @@ private JsonObject getResponse(String method, JsonArray params) { errorJSON.addProperty("message", e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName()); response.add("error", errorJSON); - LOGGER.log(Level.WARNING, "[http-server-handler] Error creating transaction in sendtransaction for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); + LOGGER.warning("[http-server-handler] Error creating transaction in sendtransaction for " + CoinTickerUtils.tickerToString(coin.getTicker()) + ", " + e.getMessage()); break; } String rawTxHex = new String(Hex.encode(transaction.bitcoinSerialize())); - LOGGER.log(Level.FINE, "[http-server-handler] sendtransaction: rawTx size=" + rawTxHex.length() + " bytes"); - LOGGER.log(Level.FINER, "[http-server-handler] sendtransaction: rawTx hex=" + rawTxHex); + LOGGER.fine("[http-server-handler] sendtransaction: rawTx size=" + rawTxHex.length() + " bytes"); + LOGGER.finer("[http-server-handler] sendtransaction: rawTx hex=" + rawTxHex); JsonObject txid = httpClient.sendRawTransaction(coin.getTicker(), rawTxHex); if (txid == null || txid.has("error") && !txid.get("error").isJsonNull()) { int code = -1; @@ -1379,7 +1378,7 @@ private JsonObject getResponse(String method, JsonArray params) { if (errObj.has("message")) errorMsg = errObj.get("message").getAsString(); } } - LOGGER.log(Level.WARNING, "[http-server-handler] sendrawtransaction failed for " + CoinTickerUtils.tickerToString(coin.getTicker()) + " code=" + code + " message=" + errorMsg); + LOGGER.warning("[http-server-handler] sendrawtransaction failed for " + CoinTickerUtils.tickerToString(coin.getTicker()) + " code=" + code + " message=" + errorMsg); response.add("result", JsonNull.INSTANCE); JsonObject errorJSON = new JsonObject(); errorJSON.addProperty("code", code); @@ -1545,23 +1544,17 @@ private byte[] formatMessageForSigning(String message) { // case BITBAY: // header = "BitBay Signed Message:\n"; // break; - // case POLISCOIN: - // header = "Polis Signed Message:\n"; - // break; - case RAVENCOIN: + case RAVENCOIN: header = "Raven Signed Message:\n"; break; case DOGECOIN: header = "Dogecoin Signed Message:\n"; break; - // case TREZARCOIN: - // header = "Trezarcoin Signed Message:\n"; - // break; case SYSCOIN: header = "Syscoin Signed Message:\n"; break; default: - LOGGER.log(Level.WARNING, "[http-server-handler] ERROR: Unsupported coin. This should never happen."); + LOGGER.warning("[http-server-handler] ERROR: Unsupported coin. This should never happen."); break; } @@ -1576,7 +1569,7 @@ private byte[] formatMessageForSigning(String message) { bos.write(messageBytes); return bos.toByteArray(); } catch (IOException e) { - LOGGER.log(Level.WARNING, "[http-server-handler] Error formatting message for signing for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); + LOGGER.warning("[http-server-handler] Error formatting message for signing for " + CoinTickerUtils.tickerToString(coin.getTicker()) + ", " + e.getMessage()); } return null; @@ -1648,7 +1641,7 @@ private boolean verifyMessage(ECKey key, String signatureB64, String message) { if (Arrays.equals(k.getPubKey(), key.getPubKey())) verified = true; } catch (SignatureException e) { - LOGGER.log(Level.WARNING, "[http-server-handler] Error verifying message for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); + LOGGER.warning("[http-server-handler] Error verifying message for " + CoinTickerUtils.tickerToString(coin.getTicker()) + ", " + e.getMessage()); } return verified; @@ -1683,7 +1676,7 @@ private UTXO getUtxo(Sha256Hash txid, long vout) { if (utxo.createUTXO().getHash().equals(txid) && utxo.getVout() == vout) { return utxo; } else { - LOGGER.log(Level.FINER, "[http-server-handler] DEBUG: UTXO " + utxo.createUTXO().getHash().toString() + " does not equal " + txid.toString()); + LOGGER.finer("[http-server-handler] DEBUG: UTXO " + utxo.createUTXO().getHash().toString() + " does not equal " + txid.toString()); } } } @@ -1697,7 +1690,7 @@ private ECKey getSigningKey(Sha256Hash txid, long vout) { if (utxo.createUTXO().getHash().equals(txid) && utxo.getVout() == vout) { return addressBalance.getPrivateKey().getKey(); } else { - LOGGER.log(Level.FINER, "[http-server-handler] DEBUG: UTXO " + utxo.createUTXO().getHash().toString() + " does not equal " + txid.toString()); + LOGGER.finer("[http-server-handler] DEBUG: UTXO " + utxo.createUTXO().getHash().toString() + " does not equal " + txid.toString()); } } } @@ -1706,7 +1699,7 @@ private ECKey getSigningKey(Sha256Hash txid, long vout) { } private void getInvalidTxResponse(JsonObject response, Exception e) { - LOGGER.log(Level.WARNING, "[http-server-handler] Error decoding raw tx for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); + LOGGER.warning("[http-server-handler] Error decoding raw tx for " + CoinTickerUtils.tickerToString(coin.getTicker()) + ", " + e.getMessage()); response.add("result", JsonNull.INSTANCE); JsonObject errorJSON = new JsonObject(); @@ -1717,7 +1710,7 @@ private void getInvalidTxResponse(JsonObject response, Exception e) { } private void txConstructionError(JsonObject response, Exception e, String s) { - LOGGER.log(Level.WARNING, "[http-server-handler] Error constructing transaction for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); + LOGGER.warning("[http-server-handler] Error constructing transaction for " + CoinTickerUtils.tickerToString(coin.getTicker()) + ", " + e.getMessage()); response.add("result", JsonNull.INSTANCE); JsonObject errorJSON = new JsonObject(); @@ -1749,11 +1742,11 @@ private void getXRouterResponse(JsonObject response, CountDownLatch latch, Atomi latch.await(timeoutPeriod, TimeUnit.SECONDS); } } catch (InterruptedException e) { - LOGGER.log(Level.WARNING, "[http-server-handler] Interrupted waiting for XRouter response for " + CoinTickerUtils.tickerToString(coin.getTicker()), e); + LOGGER.warning("[http-server-handler] Interrupted waiting for XRouter response for " + CoinTickerUtils.tickerToString(coin.getTicker()) + ", " + e.getMessage()); } if (xRouterResult.get() == null || xRouterResult.get().isEmpty()) { - LOGGER.log(Level.FINER, "[http-server-handler] ERROR: XRouter request timed out or errored! Timeout period = " + timeoutPeriod); + LOGGER.finer("[http-server-handler] ERROR: XRouter request timed out or errored! Timeout period = " + timeoutPeriod); response.add("result", JsonNull.INSTANCE); JsonObject errorJSON = new JsonObject(); @@ -1808,8 +1801,8 @@ private boolean writeResponse(ChannelHandlerContext ctx, FullHttpResponse httpRe httpResponse.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); httpResponse.headers().set(HttpHeaderNames.SERVER, CoinInstance.getVersionString()); - LOGGER.log(Level.FINER, "[http-server-handler] Writing response to channel. Keep alive? " + keepAlive); - LOGGER.log(Level.FINER, "[http-server-handler] Response content: " + httpResponse.content().toString(CharsetUtil.UTF_8)); + LOGGER.finer("[http-server-handler] Writing response to channel. Keep alive? " + keepAlive); + LOGGER.finer("[http-server-handler] Response content: " + httpResponse.content().toString(CharsetUtil.UTF_8)); ctx.write(httpResponse); return keepAlive; diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetBlockingClient.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetBlockingClient.java index 1fb24d9..effd35c 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetBlockingClient.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetBlockingClient.java @@ -16,7 +16,6 @@ import java.net.SocketAddress; import java.nio.ByteBuffer; import java.util.Set; -import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -49,7 +48,7 @@ public BlocknetBlockingClient(SocketAddress serverAddress, StreamConnection conn runReadLoop(stream, connection); } catch (Exception e) { if (!closeRequested) { - LOGGER.log(Level.WARNING, "[blocknet] Error opening/reading connection with " + serverAddress.toString(), e); + LOGGER.warning("[blocknet] Error opening/reading connection with " + serverAddress.toString() + ", " + e.getMessage()); connectFuture.setException(e); } } finally { @@ -100,7 +99,7 @@ public void closeConnection() { closeRequested = true; socket.close(); } catch (IOException e) { - LOGGER.log(Level.WARNING, "[blocknet] Error closing socket", e); + LOGGER.warning("[blocknet] Error closing socket" + e.getMessage()); } } @@ -112,7 +111,7 @@ public synchronized ListenableFuture writeBytes(byte[] bytes) throws IOException out.flush(); return Futures.immediateFuture(null); } catch (IOException e) { - LOGGER.log(Level.WARNING, "[blocknet] Error writing bytes to socket", e); + LOGGER.warning("[blocknet] Error writing bytes to socket" + e.getMessage()); closeConnection(); throw e; } diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPacketHeader.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPacketHeader.java index b18aaa0..86c9923 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPacketHeader.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPacketHeader.java @@ -31,10 +31,10 @@ public BlocknetPacketHeader(ByteBuffer in) throws ProtocolException, BufferUnder cursor = 12; command = new String(commandBytes).trim(); -// LOGGER.log(Level.FINER, "[blocknet-header] Retrieved command: " + command); +// LOGGER.finer("[blocknet-header] Retrieved command: " + command); length = (int) Utils.readUint32(header, cursor); -// LOGGER.log(Level.FINER, "[blocknet-header] Retrieved length: " + length); +// LOGGER.finer("[blocknet-header] Retrieved length: " + length); cursor += 4; if (length > Message.MAX_SIZE || length < 0) { @@ -43,7 +43,7 @@ public BlocknetPacketHeader(ByteBuffer in) throws ProtocolException, BufferUnder checksum = new byte[4]; System.arraycopy(header, cursor, checksum, 0, 4); -// LOGGER.log(Level.FINER, "[blocknet-header] Retrieved checksum: " + new String(Hex.encode(checksum))); +// LOGGER.finer("[blocknet-header] Retrieved checksum: " + new String(Hex.encode(checksum))); } public String getCommand() { diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeer.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeer.java index 5924b0f..3c0f807 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeer.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeer.java @@ -37,7 +37,6 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.ReentrantLock; -import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -159,8 +158,8 @@ protected BlocknetPeer(BlocknetParameters params, AbstractBlockChain chain, Peer this.ourVersionMessage = new VersionMessageImpl(this.params, chain != null ? chain.getBestChainHeight() : 0); this.ourVersionMessage.appendToSubVer(Version.CLIENT_TYPE, Version.CLIENT_VERSION, Version.CLIENT_COMMENTS); - LOGGER.log(Level.FINER, "[blocknet-peer] DEBUG: Our version message:"); - LOGGER.log(Level.FINER, this.ourVersionMessage.toString()); + LOGGER.finer("[blocknet-peer] DEBUG: Our version message:"); + LOGGER.finer(this.ourVersionMessage.toString()); this.activePeer = true; this.pastConnectionSuccess = false; @@ -171,7 +170,7 @@ public void connectionClosed() { if (!activePeer) return; activePeer = false; - LOGGER.log(Level.FINER, "[blocknet-peer] Connection with " + (getAddress() != null ? getAddress().toString() : "") + " closed. Notifying receivers."); + LOGGER.finer("[blocknet-peer] Connection with " + (getAddress() != null ? getAddress().toString() : "") + " closed. Notifying receivers."); for (final ListenerRegistration registration : disconnectedEventListeners) { registration.executor.execute(() -> registration.listener.onPeerDisconnected(BlocknetPeer.this, 0)); @@ -180,7 +179,7 @@ public void connectionClosed() { @Override public void connectionOpened() { - LOGGER.log(Level.FINER, "[blocknet-peer] Connection open to " + (getAddress() != null ? getAddress().toString() : "") + ", sending version message."); + LOGGER.finer("[blocknet-peer] Connection open to " + (getAddress() != null ? getAddress().toString() : "") + ", sending version message."); sendMessage(ourVersionMessage); connectionOpenFuture.set(this); @@ -189,7 +188,7 @@ public void connectionOpened() { @Override protected void timeoutOccurred() { super.timeoutOccurred(); - LOGGER.log(Level.FINER, "[blocknet-peer] Timeout occurred."); + LOGGER.finer("[blocknet-peer] Timeout occurred."); if (!connectionOpenFuture.isDone()) { connectionClosed(); } @@ -259,7 +258,7 @@ public ListenableFuture sendMessage(Message message) throws NotYetConnectedExcep lock.lock(); try { if (writeTarget == null) { - LOGGER.log(Level.FINER, "[blocknet-peer] ERROR: Attempted to send message on non-connected socket."); + LOGGER.finer("[blocknet-peer] ERROR: Attempted to send message on non-connected socket."); throw new NotYetConnectedException(); } } finally { @@ -270,14 +269,14 @@ public ListenableFuture sendMessage(Message message) throws NotYetConnectedExcep try { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); xRouterMessageSerializer.serialize(message, outputStream); - LOGGER.log(Level.FINER, "[blocknet-peer] DEBUG: Sending XRouter message. Actual length (excluding network header) is " + (outputStream.size() - BlocknetPacketHeader.HEADER_LENGTH - 4) + " bytes."); + LOGGER.finer("[blocknet-peer] DEBUG: Sending XRouter message. Actual length (excluding network header) is " + (outputStream.size() - BlocknetPacketHeader.HEADER_LENGTH - 4) + " bytes."); ListenableFuture future = writeTarget.writeBytes(outputStream.toByteArray()); messagesPendingReply.add((XRouterMessage) message); - LOGGER.log(Level.FINER, "[blocknet-peer] DEBUG: Added UUID " + ((XRouterMessage) message).getXRouterHeader().getUUID() + " to pending reply list."); + LOGGER.finer("[blocknet-peer] DEBUG: Added UUID " + ((XRouterMessage) message).getXRouterHeader().getUUID() + " to pending reply list."); return future; } catch (IOException e) { - LOGGER.log(Level.WARNING, "[blocknet] Error serializing XRouter message", e); + LOGGER.warning("[blocknet] Error serializing XRouter message" + e.getMessage()); } } else { try { @@ -285,7 +284,7 @@ public ListenableFuture sendMessage(Message message) throws NotYetConnectedExcep serializer.serialize(message, outputStream); return writeTarget.writeBytes(outputStream.toByteArray()); } catch (IOException e) { - LOGGER.log(Level.WARNING, "[blocknet] Error serializing/sending non-XRouter message", e); + LOGGER.warning("[blocknet] Error serializing/sending non-XRouter message" + e.getMessage()); } } @@ -320,14 +319,14 @@ protected void processMessage(Message message) { } else if (message instanceof VersionAck) { processVersionAck((VersionAck) message); } else if (message instanceof Ping) { - LOGGER.log(Level.FINER, "[blocknet-peer] Received ping message from " + getAddress().toString() + ", sending pong."); + LOGGER.finer("[blocknet-peer] Received ping message from " + getAddress().toString() + ", sending pong."); processPing((Ping) message); } else if (message instanceof RejectMessage) { - LOGGER.log(Level.FINER, "[blocknet-peer] ERROR: Received rejection message from " + getAddress().toString() + ": " + message.toString()); + LOGGER.finer("[blocknet-peer] ERROR: Received rejection message from " + getAddress().toString() + ": " + message.toString()); } else if (message instanceof XRouterMessage) { processXRouterMessage((XRouterMessage) message); } else { - LOGGER.log(Level.FINER, "[blocknet-peer] Warning: Received unhandled message from " + getAddress().toString() + ": " + message.toString()); + LOGGER.finer("[blocknet-peer] Warning: Received unhandled message from " + getAddress().toString() + ": " + message.toString()); } //TODO process other message types @@ -349,23 +348,23 @@ private void processVersionMessage(VersionMessage versionMessage) throws Protoco peerVersionMessage = versionMessage; - LOGGER.log(Level.FINER, "[blocknet-peer] Received version message: " + peerVersionMessage.subVer + LOGGER.finer("[blocknet-peer] Received version message: " + peerVersionMessage.subVer + ", version " + peerVersionMessage.clientVersion + ", blocks=" + peerVersionMessage.bestHeight + ", us=" + peerVersionMessage.receivingAddr); if (!peerVersionMessage.hasBlockChain() || (!params.allowEmptyPeerChain() && peerVersionMessage.bestHeight == 0)) { - LOGGER.log(Level.FINER, "[blocknet-peer] ERROR: Peer has an empty blockchain while this network does not allow empty blockchains. Disconnecting."); + LOGGER.finer("[blocknet-peer] ERROR: Peer has an empty blockchain while this network does not allow empty blockchains. Disconnecting."); close(); } if (peerVersionMessage.bestHeight < 0) { - LOGGER.log(Level.FINER, "[blocknet-peer] ERROR: Peer reported bad blockchain height (" + peerVersionMessage.bestHeight + "). Disconnecting."); + LOGGER.finer("[blocknet-peer] ERROR: Peer reported bad blockchain height (" + peerVersionMessage.bestHeight + "). Disconnecting."); close(); } sendMessage(new VersionAck()); - LOGGER.log(Level.FINER, "[blocknet-peer] Incoming version handshake complete."); + LOGGER.finer("[blocknet-peer] Incoming version handshake complete."); incomingVersionHandshakeFuture.set(this); } @@ -378,7 +377,7 @@ private void processVersionAck(VersionAck versionAck) throws ProtocolException { throw new ProtocolException("Received more than one version acknowledgement."); } - LOGGER.log(Level.FINER, "[blocknet-peer] Outgoing version handshake complete."); + LOGGER.finer("[blocknet-peer] Outgoing version handshake complete."); outgoingVersionHandshakeFuture.set(this); } @@ -389,7 +388,7 @@ private void versionHandshakeComplete() { } if (peerVersionMessage.clientVersion < minProtocolVersion) { - LOGGER.log(Level.FINER, "[blocknet-peer] Peer's protocol version (" + peerVersionMessage.clientVersion + ") is lower than the minimum (" + minProtocolVersion + ")! Disconnecting."); + LOGGER.finer("[blocknet-peer] Peer's protocol version (" + peerVersionMessage.clientVersion + ") is lower than the minimum (" + minProtocolVersion + ")! Disconnecting."); close(); } } @@ -418,26 +417,26 @@ private int removeUUIDFromPendingReplyList(String uuid) { private void processReply(XRouterMessage message) { if (message.getXRouterHeader().getUUID().isEmpty()) { - LOGGER.log(Level.FINER, "[blocknet-peer] ERROR: XRouter server sent back packet with blank UUID!"); + LOGGER.finer("[blocknet-peer] ERROR: XRouter server sent back packet with blank UUID!"); return; } final XRouterMessage original = getOriginalXRouterMessage(message.getXRouterHeader().getUUID()); if (original == null) { - LOGGER.log(Level.FINER, "[blocknet-peer] ERROR: Unexpected UUID in reply message! Perhaps the server thinks we sent a packet that we didn't send?"); + LOGGER.finer("[blocknet-peer] ERROR: Unexpected UUID in reply message! Perhaps the server thinks we sent a packet that we didn't send?"); throw new ProtocolException("Unexpected UUID in reply message"); } int removed = removeUUIDFromPendingReplyList(message.getXRouterHeader().getUUID()); if (removed != 1) { - LOGGER.log(Level.FINER, "[blocknet-peer] Warning: Exception occurred while removing message from pending list! This may break things later on. Amount of messages removed = " + removed); + LOGGER.finer("[blocknet-peer] Warning: Exception occurred while removing message from pending list! This may break things later on. Amount of messages removed = " + removed); if (removed == 0) { - LOGGER.log(Level.FINER, "[blocknet-peer] ERROR: Invalid UUID in reply message!"); + LOGGER.finer("[blocknet-peer] ERROR: Invalid UUID in reply message!"); throw new ProtocolException("Invalid UUID in reply message"); } } - LOGGER.log(Level.FINER, "[blocknet-peer] XRouter pre-processing successful. Notifying listeners."); + LOGGER.finer("[blocknet-peer] XRouter pre-processing successful. Notifying listeners."); for (ListenerRegistration registration : xRouterMessageListeners) { if (registration.executor == Threading.SAME_THREAD) { registration.executor.execute(() -> registration.listener.onXRouterMessageReceived(message, original)); @@ -446,8 +445,8 @@ private void processReply(XRouterMessage message) { } private void processXRouterMessage(final XRouterMessage message) { - LOGGER.log(Level.FINER, "processXRouterMessage() called."); - LOGGER.log(Level.FINER, "This XRouter message's UUID is '" + message.getXRouterHeader().getUUID() + "'"); + LOGGER.finer("processXRouterMessage() called."); + LOGGER.finer("This XRouter message's UUID is '" + message.getXRouterHeader().getUUID() + "'"); switch (XRouterCommandUtils.commandIdToString(message.getXRouterHeader().getCommand())) { case "xrReply": //xrReply @@ -484,16 +483,16 @@ private void blockChainDownloadLocked(Sha256Hash toHash) { Sha256Hash chainHeadHash = chainHead.getHeader().getHash(); if (Objects.equals(chainHeadHash, lastGetBlocksBegin) || Objects.equals(toHash, lastGetBlocksEnd)) { - LOGGER.log(Level.FINER, "[blocknet-peer] Ignoring dupliated request: chainHeadHash = " + chainHeadHash.toString() + ", toHash = " + toHash.toString()); + LOGGER.finer("[blocknet-peer] Ignoring dupliated request: chainHeadHash = " + chainHeadHash.toString() + ", toHash = " + toHash.toString()); for (Sha256Hash hash : pendingBlockDownloads) - LOGGER.log(Level.FINER, "[blocknet-peer] Pending block download: " + hash.toString()); + LOGGER.finer("[blocknet-peer] Pending block download: " + hash.toString()); - LOGGER.log(Level.FINER, Throwables.getStackTraceAsString(new Throwable())); + LOGGER.finer(Throwables.getStackTraceAsString(new Throwable())); return; } - LOGGER.log(Level.FINER, "[blocknet-peer] blockChainDownloadLocked(" + toHash.toString() + "): Current head = " + chainHeadHash.toString()); + LOGGER.finer("[blocknet-peer] blockChainDownloadLocked(" + toHash.toString() + "): Current head = " + chainHeadHash.toString()); StoredBlock cursor = chainHead; for (int i = 100; cursor != null && i > 0; i--) { @@ -501,7 +500,7 @@ private void blockChainDownloadLocked(Sha256Hash toHash) { try { cursor = cursor.getPrev(blockStore); } catch (BlockStoreException e) { - LOGGER.log(Level.WARNING, "[blocknet] Failed to walk blockchain while constructing locator", e); + LOGGER.warning("[blocknet] Failed to walk blockchain while constructing locator" + e.getMessage()); } } @@ -524,12 +523,12 @@ private void blockChainDownloadLocked(Sha256Hash toHash) { private void endFilteredBlock(FilteredBlock filteredBlock) { if (!downloadData) { - LOGGER.log(Level.FINER, "[blocknet-peer] WARNING: [" + getAddress().toString() + "] Received block we did not ask for! Hash: " + filteredBlock.getHash().toString()); + LOGGER.finer("[blocknet-peer] WARNING: [" + getAddress().toString() + "] Received block we did not ask for! Hash: " + filteredBlock.getHash().toString()); return; } if (blockChain == null) { - LOGGER.log(Level.FINER, "[blocknet-peer] WARNING: Received a block, but a blockchain object was not configured!"); + LOGGER.finer("[blocknet-peer] WARNING: Received a block, but a blockchain object was not configured!"); return; } @@ -539,7 +538,7 @@ private void endFilteredBlock(FilteredBlock filteredBlock) { try { if (awaitingFreshFilter != null) { - LOGGER.log(Level.FINER, "[blocknet-peer] Discarding this block because we are waiting for a fresh filter. Hash: " + filteredBlock.getHash().toString()); + LOGGER.finer("[blocknet-peer] Discarding this block because we are waiting for a fresh filter. Hash: " + filteredBlock.getHash().toString()); awaitingFreshFilter.add(filteredBlock.getHash()); return; @@ -569,9 +568,9 @@ private void endFilteredBlock(FilteredBlock filteredBlock) { } } } catch (VerificationException e) { - LOGGER.log(Level.WARNING, "[blocknet] Block failed to properly verify", e); + LOGGER.warning("[blocknet] Block failed to properly verify" + e.getMessage()); } catch (PrunedException e) { - LOGGER.log(Level.FINER, "[blocknet-peer] Some data needed to handle this block was pruned! Hash: " + filteredBlock.getHash().toString()); + LOGGER.finer("[blocknet-peer] Some data needed to handle this block was pruned! Hash: " + filteredBlock.getHash().toString()); throw new RuntimeException(e); } } @@ -659,7 +658,7 @@ public int receiveBytes(ByteBuffer buff) { firstMessage = false; } } catch (Exception e) { - LOGGER.log(Level.WARNING, "[blocknet] Error closing peer connection", e); + LOGGER.warning("[blocknet] Error closing peer connection" + e.getMessage()); return -1; } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeerGroup.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeerGroup.java index a9c2754..8db7bc4 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeerGroup.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeerGroup.java @@ -36,7 +36,6 @@ import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantLock; -import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -132,14 +131,14 @@ private void connectTo(InetSocketAddress inetSocketAddress, BlocknetPeer blockne blocknetPeer.addPeerDisconnectedEventListener(startupListener); pendingPeers.add(blocknetPeer); - LOGGER.log(Level.FINER, "[blocknet-peer-group] Attempting to connect to: " + inetSocketAddress.getHostName()); + LOGGER.finer("[blocknet-peer-group] Attempting to connect to: " + inetSocketAddress.getHostName()); try { ListenableFuture future = clientManager.openConnection(inetSocketAddress, blocknetPeer); if (future.isDone()) Uninterruptibles.getUninterruptibly(future); } catch (ExecutionException e) { - LOGGER.log(Level.WARNING, "[blocknet] Error connecting to peer", e); + LOGGER.warning("[blocknet] Error connecting to peer" + e.getMessage()); Throwable cause = Throwables.getRootCause(e); handlePeerDeath(blocknetPeer, cause); } @@ -154,7 +153,7 @@ private void startConnections() { if (blocknetPeer == null) continue; - LOGGER.log(Level.FINER, "[blocknet-peer-group] Connecting to " + blocknetPeer.getBlocknetSeed().getAddress() + ":" + blocknetPeer.getBlocknetSeed().getPort()); + LOGGER.finer("[blocknet-peer-group] Connecting to " + blocknetPeer.getBlocknetSeed().getAddress() + ":" + blocknetPeer.getBlocknetSeed().getPort()); connectTo(new InetSocketAddress(blocknetPeer.getBlocknetSeed().getAddress(), blocknetPeer.getBlocknetSeed().getPort()), blocknetPeer); } } finally { @@ -182,7 +181,7 @@ private ListenableFuture startAsync() { // scheduleMessageQueueRuns(); } catch (Throwable e) { - LOGGER.log(Level.WARNING, "[blocknet] Error starting connections", e); + LOGGER.warning("[blocknet] Error starting connections" + e.getMessage()); } return null; }); @@ -200,13 +199,13 @@ public void stop() { threadPool.shutdownNow(); } } catch (Exception e) { - LOGGER.log(Level.WARNING, "[blocknet] Error stopping peer group", e); + LOGGER.warning("[blocknet] Error stopping peer group" + e.getMessage()); } } public void sendMessage(BlocknetPeer blocknetPeer, Message message) { if (blocknetPeer == null) { - LOGGER.log(Level.FINER, "[blocknet-peer-group] BlocknetPeer is null!"); + LOGGER.finer("[blocknet-peer-group] BlocknetPeer is null!"); return; } @@ -264,7 +263,7 @@ private void sendInitialXRouterMessages(BlocknetPeer peer) { if (peer.getHaveConfig().get()) return; - LOGGER.log(Level.FINER, "[blocknet-peer-group] Sending initial XRouter messages!"); + LOGGER.finer("[blocknet-peer-group] Sending initial XRouter messages!"); CoinInstance activeBlocknetNetwork = blocknetInstance; @@ -300,13 +299,13 @@ private void sendInitialXRouterMessages(BlocknetPeer peer) { } if (peer.getPluginConfig("xrmgetutxos") == null) { - LOGGER.log(Level.FINER, "[xrouter] ERROR: Node missing required configuration... Falling back to HTTP if no available nodes. "); + LOGGER.finer("[xrouter] ERROR: Node missing required configuration... Falling back to HTTP if no available nodes. "); peer.setHasRequiredPlugins(false); // peer.close(); } } catch (Exception e) { - LOGGER.log(Level.WARNING, "[blocknet] Error processing XRouter config/plugin list", e); + LOGGER.warning("[blocknet] Error processing XRouter config/plugin list" + e.getMessage()); } if (!peer.getHaveConfig().get()) { @@ -327,10 +326,10 @@ private void sendInitialXRouterMessages(BlocknetPeer peer) { int blockCount = Integer.parseInt(reply); activeBlocknetNetwork.addBlockCount(CoinTickerUtils.stringToTicker(originalTicker), blockCount); - LOGGER.log(Level.FINER, "Blocks for currency " + originalTicker + ": " + reply); + LOGGER.finer("Blocks for currency " + originalTicker + ": " + reply); } catch (Exception e) { - LOGGER.log(Level.FINER, "[xrouter] ERROR: Error while parsing XRouter reply to xrGetBlockCount! Dumping reply and stack trace."); - LOGGER.log(Level.FINER, reply); + LOGGER.finer("[xrouter] ERROR: Error while parsing XRouter reply to xrGetBlockCount! Dumping reply and stack trace."); + LOGGER.finer(reply); } break; } @@ -341,8 +340,8 @@ private void sendInitialXRouterMessages(BlocknetPeer peer) { switch (originalCustomCmd) { case "xrmgetutxos": { if (replyJson.has("error")) { - LOGGER.log(Level.FINER, "[utxo-parser] ERROR: Error while retrieving UTXOs!"); - LOGGER.log(Level.FINER, replyJson.getString("error")); + LOGGER.finer("[utxo-parser] ERROR: Error while retrieving UTXOs!"); + LOGGER.finer(replyJson.getString("error")); break; } @@ -351,7 +350,7 @@ private void sendInitialXRouterMessages(BlocknetPeer peer) { ArrayList originalList = (ArrayList) original.getParsedData().get("params"); String originalTicker = (String) originalList.get(0); - LOGGER.log(Level.FINER, originalTicker); + LOGGER.finer(originalTicker); CoinTicker coinTicker = CoinTickerUtils.stringToTicker(originalTicker.toUpperCase()); CoinInstance inst = CoinInstance.getInstance(coinTicker); @@ -360,7 +359,7 @@ private void sendInitialXRouterMessages(BlocknetPeer peer) { for (int i = 0; i < utxosJson.length(); i++) { JSONObject utxoJson = utxosJson.getJSONObject(i); - LOGGER.log(Level.FINER, "[utxo-parser] UTXO " + i + ": " + utxoJson.toString()); + LOGGER.finer("[utxo-parser] UTXO " + i + ": " + utxoJson.toString()); String addressB58 = utxoJson.getString("address"); String txid = utxoJson.getString("txhash"); @@ -376,7 +375,7 @@ private void sendInitialXRouterMessages(BlocknetPeer peer) { break; } case "xrmgetbalance": { - LOGGER.log(Level.FINER, "[xrouter] ERROR: xrmgetbalance is not implemented yet!"); + LOGGER.finer("[xrouter] ERROR: xrmgetbalance is not implemented yet!"); break; } case "xrmgetrawtransaction": @@ -384,19 +383,19 @@ private void sendInitialXRouterMessages(BlocknetPeer peer) { break; } default: { - LOGGER.log(Level.FINER, "[xrouter] ERROR: Received reply for command we don't recognize! Original custom command: " + originalCustomCmd + ". Dumping reply."); - LOGGER.log(Level.FINER, reply); + LOGGER.finer("[xrouter] ERROR: Received reply for command we don't recognize! Original custom command: " + originalCustomCmd + ". Dumping reply."); + LOGGER.finer(reply); break; } } } catch (Exception e) { - LOGGER.log(Level.FINER, "[xrouter] ERROR: Error while parsing XRouter reply to xrService! Original custom command: " + originalCustomCmd + ". Dumping reply and stack trace."); - LOGGER.log(Level.FINER, reply); + LOGGER.finer("[xrouter] ERROR: Error while parsing XRouter reply to xrService! Original custom command: " + originalCustomCmd + ". Dumping reply and stack trace."); + LOGGER.finer(reply); } break; } default: { - LOGGER.log(Level.FINER, "[xrouter] WARNING: Core received reply to unexpected packet type. This is probably not a bug. Original command: " + XRouterCommandUtils.commandIdToString(originalCmd)); + LOGGER.finer("[xrouter] WARNING: Core received reply to unexpected packet type. This is probably not a bug. Original command: " + XRouterCommandUtils.commandIdToString(originalCmd)); break; } } @@ -426,17 +425,17 @@ private void handleNewPeer(final BlocknetPeer peer, int peerCount) { peers.add(peer); peer.addPreMessageReceivedEventListener((thisPeer, message) -> { - LOGGER.log(Level.FINER, "[blocknet-peer] Message received from peer: " + peer.getAddress()); + LOGGER.finer("[blocknet-peer] Message received from peer: " + peer.getAddress()); if (message instanceof XRouterMessage) { - LOGGER.log(Level.FINER, "[blocknet-peer] XRouter message received."); + LOGGER.finer("[blocknet-peer] XRouter message received."); } return message; }); - LOGGER.log(Level.FINER, "[peer] Peer " + peer.getAddress().toString() + " connected, version handshake done. peerCount = " + peerCount); + LOGGER.finer("[peer] Peer " + peer.getAddress().toString() + " connected, version handshake done. peerCount = " + peerCount); if (!blocknetInstance.hasXRouter()) { - LOGGER.log(Level.FINER, "[xrouter] WARNING: This network (" + blocknetInstance.getTicker().toString() + ") does not support XRouter. Will attempt to send XRouter messages over active Blocknet network."); + LOGGER.finer("[xrouter] WARNING: This network (" + blocknetInstance.getTicker().toString() + ") does not support XRouter. Will attempt to send XRouter messages over active Blocknet network."); return; } @@ -453,7 +452,7 @@ public void initialMessagesSent(CoinInstance instance) { if (!peer.getHaveConfig().get()) { sendInitialXRouterMessages(peer); - LOGGER.log(Level.FINER, "[blocknet-peer-group] Sent initial messages to peer: " + peer.getAddress()); + LOGGER.finer("[blocknet-peer-group] Sent initial messages to peer: " + peer.getAddress()); } peer.getBlocknetSeed().resetCounters(); @@ -470,7 +469,7 @@ private void handlePeerDeath(final BlocknetPeer peer, @Nullable Throwable except if (peer.getHaveConfig().get() || peer.pastConnectionSuccess()) { pendingPeers.add(peer); - LOGGER.log(Level.FINER, "[testing ] Peer saved."); + LOGGER.finer("[testing ] Peer saved."); } peer.setHaveConfig(false); @@ -479,7 +478,7 @@ private void handlePeerDeath(final BlocknetPeer peer, @Nullable Throwable except blocknetSeed.incrementFailCounter(); blocknetSeed.setActivePeer(false); - LOGGER.log(Level.FINER, "[blocknet-peer-group] Peer died: " + blocknetSeed.getAddress()); + LOGGER.finer("[blocknet-peer-group] Peer died: " + blocknetSeed.getAddress()); setActiveConnectionCount(peers.size()); } finally { @@ -491,7 +490,7 @@ private void handlePeerDeath(final BlocknetPeer peer, @Nullable Throwable except private Runnable attemptReconnects(boolean forceReconnect) { return () -> { - LOGGER.log(Level.FINER, "[blocknet-peer-group] Checking if we can reconnect to any disconnected peers..."); + LOGGER.finer("[blocknet-peer-group] Checking if we can reconnect to any disconnected peers..."); try { for (BlocknetSeed blocknetSeed : blocknetSeeds) { @@ -506,7 +505,7 @@ private Runnable attemptReconnects(boolean forceReconnect) { && blocknetSeed.getLastFailTimeDiff() >= reconnectTime; if (attemptReconnect || forceReconnect) { - LOGGER.log(Level.FINER, "[blocknet-peer-group] Reconnecting to peer: " + blocknetSeed.getAddress()); + LOGGER.finer("[blocknet-peer-group] Reconnecting to peer: " + blocknetSeed.getAddress()); BlocknetPeer blocknetPeer = createPeer(blocknetNetworkParameters, blockChain, blocknetSeed); if (blocknetPeer == null) @@ -516,7 +515,7 @@ private Runnable attemptReconnects(boolean forceReconnect) { } } } catch (Exception e) { - LOGGER.log(Level.WARNING, "[blocknet] Error handling peer group event", e); + LOGGER.warning("[blocknet] Error handling peer group event" + e.getMessage()); } }; } @@ -526,7 +525,7 @@ private void attemptReconnect(BlocknetPeer blocknetPeer) { BlocknetSeed blocknetSeed = blocknetPeer.getBlocknetSeed(); connectTo(new InetSocketAddress(blocknetSeed.getAddress(), blocknetSeed.getPort()), blocknetPeer); } catch (Exception e) { - LOGGER.log(Level.WARNING, "[blocknet] Error during peer group shutdown", e); + LOGGER.warning("[blocknet] Error during peer group shutdown" + e.getMessage()); } } @@ -540,7 +539,7 @@ private Runnable processQueue() { return () -> { CoinInstance coinInstance = CoinInstance.getInstance(CoinInstance.getActiveBlocknetNetwork()); - LOGGER.log(Level.FINER, "[blocknet-peer-group] processing message queue"); + LOGGER.finer("[blocknet-peer-group] processing message queue"); if (messageQueue.size() == 0) return; @@ -580,7 +579,7 @@ private Runnable processQueue() { if (queueItem.getCommmand().equals("xrSendTransaction") && queueItem.getMessageSource() == MessageSource.SOURCE_GUI) { BlocknetPeer finalBlocknetPeer = blocknetPeer; - LOGGER.log(Level.FINER, "Transaction successful!"); + LOGGER.finer("Transaction successful!"); } else if (queueItem.getNewPeer() != null) { for (ListenerRegistration listener : queueItem.getOriginalPeer().getXRouterMessageListeners()) { queueItem.getNewPeer().addXRouterMessageReceivedEventListener(listener.listener); @@ -606,7 +605,7 @@ private boolean waitForConnection(BlocknetPeer blocknetPeer, int maxWaitSeconds) try { Thread.sleep(100); } catch (InterruptedException e) { - LOGGER.log(Level.WARNING, "[blocknet] Error waiting for connection", e); + LOGGER.warning("[blocknet] Error waiting for connection" + e.getMessage()); } } } diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetSerializer.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetSerializer.java index df2384c..ce732c5 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetSerializer.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetSerializer.java @@ -10,7 +10,6 @@ import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.util.HashMap; -import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -80,13 +79,13 @@ public Message deserializePayload(BitcoinSerializer.BitcoinPacketHeader header, switch (blocknetPacketHeader.getCommand().toLowerCase()) { case "xrouter": - LOGGER.log(Level.FINER, "[blocknet-serializer] Received XRouter packet, at position: " + in.position()); + LOGGER.finer("[blocknet-serializer] Received XRouter packet, at position: " + in.position()); return new XRouterMessage(params, payloadBytes); case "version": -// LOGGER.log(Level.FINER, "[blocknet-serializer] Version message received"); +// LOGGER.finer("[blocknet-serializer] Version message received"); return new VersionMessage(params, payloadBytes); case "inv": -// LOGGER.log(Level.FINER, "[blocknet-serializer] Warning: Inventory messages are ignored"); +// LOGGER.finer("[blocknet-serializer] Warning: Inventory messages are ignored"); return null; case "block": return new Block(params, payloadBytes, 0, this, blocknetPacketHeader.getLength()); @@ -128,13 +127,13 @@ public Message deserializePayload(BitcoinSerializer.BitcoinPacketHeader header, case "ssc": case "mnget": case "xbridge": -// LOGGER.log(Level.FINER, "[blocknet-serializer] Warning: This serializer does not support deserializing xbridge/ssc/mnget/getsporks packets yet."); +// LOGGER.finer("[blocknet-serializer] Warning: This serializer does not support deserializing xbridge/ssc/mnget/getsporks packets yet."); return null; case "dseg": -// LOGGER.log(Level.FINER, "[blocknet-serializer] Warning: This serializer does not support deserializing dseg packets yet."); +// LOGGER.finer("[blocknet-serializer] Warning: This serializer does not support deserializing dseg packets yet."); return null; default: - LOGGER.log(Level.FINER, "[blocknet-serializer] Warning: This serializer does not support deserializing " + blocknetPacketHeader.getCommand() + " packets (yet)."); + LOGGER.finer("[blocknet-serializer] Warning: This serializer does not support deserializing " + blocknetPacketHeader.getCommand() + " packets (yet)."); return new UnknownMessage(params, blocknetPacketHeader.getCommand(), payloadBytes); } } @@ -200,7 +199,7 @@ public void serialize(String name, byte[] message, OutputStream out) throws IOEx out.write(header); out.write(message); - LOGGER.log(Level.FINER, "[blocknet-serializer] Serialized " + name + " message. Bytes: " + new String(Hex.encode(header)) + new String(Hex.encode(message))); + LOGGER.finer("[blocknet-serializer] Serialized " + name + " message. Bytes: " + new String(Hex.encode(header)) + new String(Hex.encode(message))); } @Override @@ -210,7 +209,7 @@ public void serialize(Message message, OutputStream out) throws IOException { } else { String name = messageNames.get(message.getClass()); if (name == null) { - LOGGER.log(Level.FINER, "[blocknet-serializer] ERROR: BlocknetSerializer cannot serialize " + message.getClass().getSimpleName() + " (yet)!"); + LOGGER.finer("[blocknet-serializer] ERROR: BlocknetSerializer cannot serialize " + message.getClass().getSimpleName() + " (yet)!"); return; } serialize(name, message.bitcoinSerialize(), out); diff --git a/src/main/java/io/cloudchains/app/net/xrouter/XRouterFeeUtils.java b/src/main/java/io/cloudchains/app/net/xrouter/XRouterFeeUtils.java index 00a27fe..c23ac05 100644 --- a/src/main/java/io/cloudchains/app/net/xrouter/XRouterFeeUtils.java +++ b/src/main/java/io/cloudchains/app/net/xrouter/XRouterFeeUtils.java @@ -10,7 +10,6 @@ import java.util.ArrayList; import java.util.HashMap; -import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -29,8 +28,8 @@ public static String getXRouterFeeTx(BlocknetPeer blocknetPeer, String xRouterCo HashMap feeMap = xRouterConfig.getFeeMap(); if (!feeMap.containsKey(xRouterCommand)) { - LOGGER.log(Level.FINER, "[xrouter-fee-utils] WARNING: Invalid/unknown XRouter command supplied to getXRouterFeeTx()! Assuming this command is free."); - LOGGER.log(Level.FINER, "[xrouter-fee-utils] Command: " + xRouterCommand); + LOGGER.finer("[xrouter-fee-utils] WARNING: Invalid/unknown XRouter command supplied to getXRouterFeeTx()! Assuming this command is free."); + LOGGER.finer("[xrouter-fee-utils] Command: " + xRouterCommand); return "nohash;nofee"; } @@ -39,7 +38,7 @@ public static String getXRouterFeeTx(BlocknetPeer blocknetPeer, String xRouterCo Coin xRouterFeeAmt = Coin.valueOf((long) Math.floor(fee * Coin.COIN.value)); if (xRouterFeeAmt.value == 0) { - LOGGER.log(Level.FINER, "[xrouter-fee-utils] DEBUG: This command is free."); + LOGGER.finer("[xrouter-fee-utils] DEBUG: This command is free."); return "nohash;nofee"; } @@ -77,9 +76,9 @@ public static String getXRouterFeeTx(BlocknetPeer blocknetPeer, String xRouterCo Transaction xRouterFeeTx = blocknetWalletHelper.createRawTransactionWithAllUTXOs(outputs, totalAvailable); String feetx = new String(Hex.encode(xRouterFeeTx.bitcoinSerialize())); - LOGGER.log(Level.FINER, "[xrouter-fee-utils] XRouter fee transaction string representation:"); - LOGGER.log(Level.FINER, xRouterFeeTx.toString()); - LOGGER.log(Level.FINER, "[xrouter-fee-utils] DEBUG: Feetx: " + feetx); + LOGGER.finer("[xrouter-fee-utils] XRouter fee transaction string representation:"); + LOGGER.finer(xRouterFeeTx.toString()); + LOGGER.finer("[xrouter-fee-utils] DEBUG: Feetx: " + feetx); return feetx; } diff --git a/src/main/java/io/cloudchains/app/net/xrouter/XRouterMessage.java b/src/main/java/io/cloudchains/app/net/xrouter/XRouterMessage.java index 3c431b2..6e0b19e 100644 --- a/src/main/java/io/cloudchains/app/net/xrouter/XRouterMessage.java +++ b/src/main/java/io/cloudchains/app/net/xrouter/XRouterMessage.java @@ -15,7 +15,6 @@ import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.HashMap; -import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -91,7 +90,7 @@ public byte[] bitcoinSerialize() { try { bitcoinSerializeToStream(byteArrayOutputStream); } catch (Exception e) { - LOGGER.log(Level.WARNING, "[xrouter] Error serializing XRouter packet", e); + LOGGER.warning("[xrouter] Error serializing XRouter packet" + e.getMessage()); return null; } @@ -127,7 +126,7 @@ protected void bitcoinSerializeToStream(OutputStream stream) throws IOException break; } case "xrGetReply": { - LOGGER.log(Level.FINER, "[xrouter-message] DEBUG: Fetching reply for packet " + xRouterHeader.getUUID()); + LOGGER.finer("[xrouter-message] DEBUG: Fetching reply for packet " + xRouterHeader.getUUID()); break; } case "xrGetConfig": { @@ -181,7 +180,7 @@ protected void bitcoinSerializeToStream(OutputStream stream) throws IOException break; } case "xrGenerateBloomFilter": { - LOGGER.log(Level.FINER, "[xrouter-message] ERROR: Attempted to serialize unsupported command 41."); + LOGGER.finer("[xrouter-message] ERROR: Attempted to serialize unsupported command 41."); break; } case "xrGetBlocks": { @@ -198,7 +197,7 @@ protected void bitcoinSerializeToStream(OutputStream stream) throws IOException break; } case "xrGetBlockAtTime": { - LOGGER.log(Level.FINER, "[xrouter-message] ERROR: Attempted to serialize unsupported command 52."); + LOGGER.finer("[xrouter-message] ERROR: Attempted to serialize unsupported command 52."); break; } case "xrGetBalance": { //OBSOLETE, only implemented for backwards compatibility @@ -210,13 +209,13 @@ protected void bitcoinSerializeToStream(OutputStream stream) throws IOException } case "xrService": { String command = (String) parsedData.get("command"); - LOGGER.log(Level.FINER, "[xrService] Command: " + command); + LOGGER.finer("[xrService] Command: " + command); XRouterConfiguration.XRouterPluginConfiguration pluginConfig = blocknetPeer.getPluginConfig(command); if (pluginConfig == null) { - LOGGER.log(Level.FINER, "[xrService] ERROR: Unsupported server xrs plugin: " + command); - LOGGER.log(Level.FINER, "[xrService] ERROR: Aborting transmission."); + LOGGER.finer("[xrService] ERROR: Unsupported server xrs plugin: " + command); + LOGGER.finer("[xrService] ERROR: Aborting transmission."); throw new IllegalArgumentException("Unsupported server xrs plugin: " + command); } @@ -239,7 +238,7 @@ protected void bitcoinSerializeToStream(OutputStream stream) throws IOException if (!(param instanceof String && ((String) param).equalsIgnoreCase("true") || ((String) param).equalsIgnoreCase("false"))) Preconditions.checkState(paramClass.isInstance(param), "Supplied parameter at index " + i + " is not of type '" + classStr + "'. Aborting transmission."); - LOGGER.log(Level.FINER, "[xrService] DEBUG: Parameter " + i + " is of type " + classStr); + LOGGER.finer("[xrService] DEBUG: Parameter " + i + " is of type " + classStr); switch (classStr) { case "string": { @@ -265,7 +264,7 @@ protected void bitcoinSerializeToStream(OutputStream stream) throws IOException break; } default: { - LOGGER.log(Level.FINER, "[xrService] ERROR: Encountered unhandled parameter of type " + classStr + ". Aborting transmission."); + LOGGER.finer("[xrService] ERROR: Encountered unhandled parameter of type " + classStr + ". Aborting transmission."); throw new IllegalStateException("Bad parameter type at index " + i + ": " + classStr); } } @@ -318,7 +317,7 @@ private void parseHeader() throws ProtocolException { protected void parse() throws ProtocolException { parsedData.put("header", xRouterHeader); - LOGGER.log(Level.FINER, "Received raw XRouter packet: " + new String(Hex.encode(data))); + LOGGER.finer("Received raw XRouter packet: " + new String(Hex.encode(data))); ByteBuffer buf = ByteBuffer.wrap(data); buf.position(xRouterHeader.getHeaderLength()); @@ -329,15 +328,15 @@ protected void parse() throws ProtocolException { case "xrConfigReply": { String reply = readStringNT(buf); parsedData.put("reply", reply); - LOGGER.log(Level.FINER, "[xrouter-message] Got reply: '" + reply + "' for packet with UUID '" + xRouterHeader.getUUID() + "'"); + LOGGER.finer("[xrouter-message] Got reply: '" + reply + "' for packet with UUID '" + xRouterHeader.getUUID() + "'"); break; } case "xrGetReply": { - LOGGER.log(Level.FINER, "[xrouter-message] WARNING: Server asked to fetch reply, but we aren't a server."); + LOGGER.finer("[xrouter-message] WARNING: Server asked to fetch reply, but we aren't a server."); break; } case "xrGetConfig": { - LOGGER.log(Level.FINER, "[xrouter-message] WARNING: Server asked us for config, but we aren't a servicenode."); + LOGGER.finer("[xrouter-message] WARNING: Server asked us for config, but we aren't a servicenode."); break; } case "xrGetBlockCount": { diff --git a/src/main/java/io/cloudchains/app/net/xrouter/XRouterMessageSerializer.java b/src/main/java/io/cloudchains/app/net/xrouter/XRouterMessageSerializer.java index d17d46c..9fcdebf 100644 --- a/src/main/java/io/cloudchains/app/net/xrouter/XRouterMessageSerializer.java +++ b/src/main/java/io/cloudchains/app/net/xrouter/XRouterMessageSerializer.java @@ -11,7 +11,6 @@ import java.io.OutputStream; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; -import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -110,7 +109,7 @@ private void serialize(byte[] data, OutputStream out) throws IOException { out.write(header); out.write(data); - LOGGER.log(Level.FINER, "[blocknet-serializer] Serialized xrouter message. Bytes: " + new String(Hex.encode(header)) + new String(Hex.encode(data))); + LOGGER.finer("[blocknet-serializer] Serialized xrouter message. Bytes: " + new String(Hex.encode(header)) + new String(Hex.encode(data))); } /** diff --git a/src/main/java/io/cloudchains/app/net/xrouter/XRouterPacketHeader.java b/src/main/java/io/cloudchains/app/net/xrouter/XRouterPacketHeader.java index a56c1aa..6c6b89b 100644 --- a/src/main/java/io/cloudchains/app/net/xrouter/XRouterPacketHeader.java +++ b/src/main/java/io/cloudchains/app/net/xrouter/XRouterPacketHeader.java @@ -4,7 +4,6 @@ import org.bitcoinj.core.Utils; import java.nio.ByteBuffer; -import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -51,21 +50,21 @@ public XRouterPacketHeader(ByteBuffer in) { System.arraycopy(rawHeader, cursor, compactSizeBytes, 0, compactSizeBytes.length); cursor += compactSizeBytes.length; - LOGGER.log(Level.FINER, "[xrouter] Retrieved compact size: " + new String(Hex.encode(new byte[]{compactSize}))); - LOGGER.log(Level.FINER, "[xrouter] Retrieved compact size bytes: " + new String(Hex.encode(compactSizeBytes))); + LOGGER.finer("[xrouter] Retrieved compact size: " + new String(Hex.encode(new byte[]{compactSize}))); + LOGGER.finer("[xrouter] Retrieved compact size bytes: " + new String(Hex.encode(compactSizeBytes))); version = (int) Utils.readUint32(rawHeader, cursor); cursor += 4; - LOGGER.log(Level.FINER, "[xrouter] Retrieved version: " + version); + LOGGER.finer("[xrouter] Retrieved version: " + version); command = (int) Utils.readUint32(rawHeader, cursor); cursor += 4; - LOGGER.log(Level.FINER, "[xrouter] Retrieved command: " + command); + LOGGER.finer("[xrouter] Retrieved command: " + command); timestamp = (int) Utils.readUint32(rawHeader, cursor); cursor += 4; - LOGGER.log(Level.FINER, "[xrouter] Retrieved timestamp: " + timestamp); + LOGGER.finer("[xrouter] Retrieved timestamp: " + timestamp); size = (int) Utils.readUint32(rawHeader, cursor); cursor += 4; - LOGGER.log(Level.FINER, "[xrouter] Retrieved size: " + size); + LOGGER.finer("[xrouter] Retrieved size: " + size); //reserved header fields //we don't use these fields, so we skip them @@ -76,21 +75,21 @@ public XRouterPacketHeader(ByteBuffer in) { cursor += 36; uuid = new String(uuidArr); - LOGGER.log(Level.FINER, "[xrouter] Retrieved UUID: " + uuid); + LOGGER.finer("[xrouter] Retrieved UUID: " + uuid); byte[] pubkeyArr = new byte[33]; System.arraycopy(rawHeader, cursor, pubkeyArr, 0, pubkeyArr.length); cursor += 33; - LOGGER.log(Level.FINER, "[xrouter] Retrieved pubkey: " + new String(Hex.encode(pubkeyArr))); + LOGGER.finer("[xrouter] Retrieved pubkey: " + new String(Hex.encode(pubkeyArr))); pubkey = pubkeyArr; byte[] sigArr = new byte[64]; System.arraycopy(rawHeader, cursor, sigArr, 0, sigArr.length); cursor += 64; - LOGGER.log(Level.FINER, "[xrouter] Retrieved signature: " + new String(Hex.encode(sigArr))); + LOGGER.finer("[xrouter] Retrieved signature: " + new String(Hex.encode(sigArr))); signature = sigArr; - LOGGER.log(Level.FINER, "[xrouter] XRouter header read complete, at position: " + cursor); + LOGGER.finer("[xrouter] XRouter header read complete, at position: " + cursor); headerLength = cursor; //should have read 157 bytes at this point (excluding compact size) } diff --git a/src/main/java/io/cloudchains/app/net/xrouter/XRouterPacketManager.java b/src/main/java/io/cloudchains/app/net/xrouter/XRouterPacketManager.java index 7a5280e..f1039bf 100644 --- a/src/main/java/io/cloudchains/app/net/xrouter/XRouterPacketManager.java +++ b/src/main/java/io/cloudchains/app/net/xrouter/XRouterPacketManager.java @@ -12,7 +12,6 @@ import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.HashMap; -import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -35,30 +34,30 @@ public static int getXRouterPacketVersion() { } private byte[] signPacket(byte[] packetBytes, ECKey ecPrivateKey) { - LOGGER.log(Level.FINER, "[xrouter] DEBUG: Packet bytes: " + new String(Hex.encode(packetBytes))); + LOGGER.finer("[xrouter] DEBUG: Packet bytes: " + new String(Hex.encode(packetBytes))); Sha256Hash packetHash = Sha256Hash.wrap(Sha256Hash.hash(packetBytes)); - LOGGER.log(Level.FINER, "[xrouter] DEBUG: Packet byte hash: " + packetHash.toString()); + LOGGER.finer("[xrouter] DEBUG: Packet byte hash: " + packetHash.toString()); ECKey.ECDSASignature rawSignature = ecPrivateKey.sign(packetHash).toCanonicalised(); byte[] r = rawSignature.r.toByteArray(); byte[] s = rawSignature.s.toByteArray(); if (r.length > 32) { - LOGGER.log(Level.FINER, "[xrouter] WARNING: Signature R is greater than 32 bytes! Trimming from the beginning. Size: " + r.length); - LOGGER.log(Level.FINER, "[xrouter] WARNING: Signature R: " + new String(Hex.encode(r))); + LOGGER.finer("[xrouter] WARNING: Signature R is greater than 32 bytes! Trimming from the beginning. Size: " + r.length); + LOGGER.finer("[xrouter] WARNING: Signature R: " + new String(Hex.encode(r))); } else if (r.length < 32) { - LOGGER.log(Level.FINER, "[xrouter] WARNING: Signature R is less than 32 bytes! Prepending null bytes to the beginning. Size: " + s.length); - LOGGER.log(Level.FINER, "[xrouter] WARNING: Signature R: " + new String(Hex.encode(r))); + LOGGER.finer("[xrouter] WARNING: Signature R is less than 32 bytes! Prepending null bytes to the beginning. Size: " + s.length); + LOGGER.finer("[xrouter] WARNING: Signature R: " + new String(Hex.encode(r))); r = prependNullTo32(r); } if (s.length > 32) { - LOGGER.log(Level.FINER, "[xrouter] WARNING: Signature S is greater than 32 bytes! Trimming from the beginning. Size: " + s.length); - LOGGER.log(Level.FINER, "[xrouter] WARNING: Signature S: " + new String(Hex.encode(s))); + LOGGER.finer("[xrouter] WARNING: Signature S is greater than 32 bytes! Trimming from the beginning. Size: " + s.length); + LOGGER.finer("[xrouter] WARNING: Signature S: " + new String(Hex.encode(s))); } else if (s.length < 32) { - LOGGER.log(Level.FINER, "[xrouter] WARNING: Signature S is less than 32 bytes! Prepending null bytes. Size: " + s.length); - LOGGER.log(Level.FINER, "[xrouter] WARNING: Signature S: " + new String(Hex.encode(s))); + LOGGER.finer("[xrouter] WARNING: Signature S is less than 32 bytes! Prepending null bytes. Size: " + s.length); + LOGGER.finer("[xrouter] WARNING: Signature S: " + new String(Hex.encode(s))); s = prependNullTo32(s); } @@ -68,7 +67,7 @@ private byte[] signPacket(byte[] packetBytes, ECKey ecPrivateKey) { System.arraycopy(r, r.length - 32, signature, 0, 32); System.arraycopy(s, s.length - 32, signature, 32, 32); - LOGGER.log(Level.FINER, "[xrouter] Signature: " + new String(Hex.encode(signature)) + ", byte length " + signature.length); + LOGGER.finer("[xrouter] Signature: " + new String(Hex.encode(signature)) + ", byte length " + signature.length); return signature; } @@ -97,13 +96,13 @@ private XRouterMessage getPacket(BlocknetPeer blocknetPeer, int size, int comman if (extSize < 253) { xRouterHeaderBytes = new byte[158]; compactSize = (byte) extSize; - //LOGGER.log(Level.FINER, "Compact size = " + compactSize); + //LOGGER.finer("Compact size = " + compactSize); xRouterHeaderBytes[0] = compactSize; compactSizeBytes = 1; } else if (extSize <= 65535) { xRouterHeaderBytes = new byte[160]; compactSize = (byte) 253; - //LOGGER.log(Level.FINER, "Compact size = " + compactSize + ", extSize = " + extSize); + //LOGGER.finer("Compact size = " + compactSize + ", extSize = " + extSize); xRouterHeaderBytes[0] = compactSize; xRouterHeaderBytes[1] = (byte) (0xFF & (extSize)); xRouterHeaderBytes[2] = (byte) (0xFF & (extSize >> 8)); @@ -111,7 +110,7 @@ private XRouterMessage getPacket(BlocknetPeer blocknetPeer, int size, int comman } else { xRouterHeaderBytes = new byte[162]; compactSize = (byte) 254; - //LOGGER.log(Level.FINER, "Compact size = " + compactSize + ", extSize = " + extSize); + //LOGGER.finer("Compact size = " + compactSize + ", extSize = " + extSize); xRouterHeaderBytes[0] = compactSize; Utils.uint32ToByteArrayLE(extSize, xRouterHeaderBytes, 1); compactSizeBytes = 5; @@ -147,9 +146,9 @@ private XRouterMessage getPacket(BlocknetPeer blocknetPeer, int size, int comman System.arraycopy(new byte[64], 0, xRouterHeaderBytes, cursor, 64); cursor += 64; - LOGGER.log(Level.FINER, "[xrouter] Serialized XRouter header. Cursor is at " + cursor); + LOGGER.finer("[xrouter] Serialized XRouter header. Cursor is at " + cursor); - LOGGER.log(Level.FINER, "[xrouter] Serializing XRouter message (phase 1)."); + LOGGER.finer("[xrouter] Serializing XRouter message (phase 1)."); XRouterPacketHeader xRouterHeader = new XRouterPacketHeader(ByteBuffer.wrap(xRouterHeaderBytes)); XRouterMessage message = new XRouterMessage(blocknetPeer, blocknetNetworkParameters, xRouterHeader, body); @@ -163,7 +162,7 @@ private XRouterMessage getPacket(BlocknetPeer blocknetPeer, int size, int comman xRouterHeaderBufSigned.flip(); - LOGGER.log(Level.FINER, "[xrouter] Serializing XRouter message (phase 2)."); + LOGGER.finer("[xrouter] Serializing XRouter message (phase 2)."); XRouterPacketHeader xRouterHeaderSigned = new XRouterPacketHeader(xRouterHeaderBufSigned); return new XRouterMessage(blocknetPeer, blocknetNetworkParameters, xRouterHeaderSigned, body); } diff --git a/src/main/java/io/cloudchains/app/util/AddressDiscoveryService.java b/src/main/java/io/cloudchains/app/util/AddressDiscoveryService.java index 43e88db..17b6cb9 100644 --- a/src/main/java/io/cloudchains/app/util/AddressDiscoveryService.java +++ b/src/main/java/io/cloudchains/app/util/AddressDiscoveryService.java @@ -20,7 +20,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -61,7 +60,7 @@ public AddressDiscoveryService(CoinInstance coinInstance, HTTPClient httpClient) this.configHelper = coinInstance.getConfigHelper(); this.currencyString = CoinTickerUtils.tickerToString(coinInstance.getTicker()); this.externalChainKey = initExternalChainKey(coinInstance.getWallet()); - LOGGER.log(Level.FINER, getLogPrefix() + " AddressDiscoveryService initialized for " + currencyString); + LOGGER.finer(getLogPrefix() + " AddressDiscoveryService initialized for " + currencyString); } /** @@ -113,7 +112,7 @@ public int discoverAddressCount() { long startTime = System.currentTimeMillis(); int currentAddressCount = configHelper.getAddressCount(); - LOGGER.log(Level.FINE, getLogPrefix() + " Starting sequential batch scan"); + LOGGER.fine(getLogPrefix() + " Starting sequential batch scan"); try { if (isTimedOut(startTime)) return currentAddressCount; @@ -124,7 +123,7 @@ public int discoverAddressCount() { for (int i = 0; i < NUM_BATCHES; i++) { if (isTimedOut(startTime)) { - LOGGER.log(Level.WARNING, getLogPrefix() + " Timeout at batch " + i); + LOGGER.warning(getLogPrefix() + " Timeout at batch " + i); break; } @@ -133,7 +132,7 @@ public int discoverAddressCount() { if (utxos == null) { consecutiveErrors++; if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) { - LOGGER.log(Level.WARNING, getLogPrefix() + LOGGER.warning(getLogPrefix() + " Aborting: " + MAX_CONSECUTIVE_ERRORS + " consecutive HTTP failures"); break; } @@ -148,7 +147,7 @@ public int discoverAddressCount() { } if (lastNonEmptyBatch < 0) { - LOGGER.log(Level.INFO, getLogPrefix() + " No UTXOs found"); + LOGGER.info(getLogPrefix() + " No UTXOs found"); return currentAddressCount; } @@ -163,14 +162,14 @@ public int discoverAddressCount() { addr.clearPrivateKey(); } - LOGGER.log(Level.INFO, getLogPrefix() + " Discovery complete: lastBatch=" + LOGGER.info(getLogPrefix() + " Discovery complete: lastBatch=" + lastNonEmptyBatch + ", count=" + discoveredCount + ", time=" + (System.currentTimeMillis() - startTime) + "ms"); return discoveredCount; } catch (Exception e) { - LOGGER.log(Level.WARNING, getLogPrefix() + " Error during discovery", e); + LOGGER.warning(getLogPrefix() + " Error during discovery" + e.getMessage()); return currentAddressCount; } } @@ -231,7 +230,7 @@ private List checkBatchForUtxos(List batch) { try { utxoResponse = httpClient.getUtxosUncached(coinInstance.getTicker(), addresses); } catch (Exception e) { - LOGGER.log(Level.WARNING, getLogPrefix() + " HTTP request failed for addresses " + LOGGER.warning(getLogPrefix() + " HTTP request failed for addresses " + addresses[0] + "..." + addresses[addresses.length - 1] + " - " + e.getMessage()); return null; } @@ -253,7 +252,7 @@ private List checkBatchForUtxos(List batch) { confirmationsElement == null || valueElement == null || addressElement.isJsonNull() || txidElement.isJsonNull() || voutElement.isJsonNull() || confirmationsElement.isJsonNull() || valueElement.isJsonNull()) { - LOGGER.log(Level.WARNING, getLogPrefix() + " Skipping invalid UTXO - missing required fields"); + LOGGER.warning(getLogPrefix() + " Skipping invalid UTXO - missing required fields"); continue; } @@ -267,7 +266,7 @@ private List checkBatchForUtxos(List batch) { ); utxos.add(utxo); } catch (Exception e) { - LOGGER.log(Level.WARNING, getLogPrefix() + " Failed to parse UTXO response element: " + e.getMessage()); + LOGGER.warning(getLogPrefix() + " Failed to parse UTXO response element: " + e.getMessage()); } } return utxos; diff --git a/src/main/java/io/cloudchains/app/util/ConfigHelper.java b/src/main/java/io/cloudchains/app/util/ConfigHelper.java index ae0bae0..2100940 100644 --- a/src/main/java/io/cloudchains/app/util/ConfigHelper.java +++ b/src/main/java/io/cloudchains/app/util/ConfigHelper.java @@ -9,7 +9,6 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -38,7 +37,7 @@ public ConfigHelper(String tickerStr) { file = Preconditions.checkNotNull(this.getFile()); loadConfig(); } catch (Exception e) { - LOGGER.log(Level.WARNING, "[config] Failed to initialize config for " + tickerStr, e); + LOGGER.warning("[config] Failed to initialize config for " + tickerStr + ", " + e.getMessage()); } } @@ -70,7 +69,7 @@ public synchronized void loadConfig() { for (String configKey : configKeys) { if (!config.has(configKey)) { - LOGGER.log(Level.FINER, "[config] Missing config key '" + configKey + "' for " + tickerStr + ", will use default"); + LOGGER.finer("[config] Missing config key '" + configKey + "' for " + tickerStr + ", will use default"); } } @@ -131,7 +130,7 @@ public synchronized void loadConfig() { writeConfig(); } } catch (Exception e) { - LOGGER.log(Level.WARNING, "[config] Error reading config file for " + tickerStr, e); + LOGGER.warning("[config] Error reading config file for " + tickerStr + ", " + e.getMessage()); } } @@ -143,7 +142,7 @@ private File getFile() { File settingsDirectory = new File(home, "settings"); if (!settingsDirectory.exists()) { if (!settingsDirectory.mkdirs()) { - LOGGER.log(Level.FINER, "[config] ERROR: Could not create base/settings directory!"); + LOGGER.finer("[config] ERROR: Could not create base/settings directory!"); return null; } } @@ -153,7 +152,7 @@ private File getFile() { if (!configFile.createNewFile() && !configFile.exists()) return null; } catch (IOException e) { - LOGGER.log(Level.WARNING, "[config] IOException creating config file for " + tickerStr, e); + LOGGER.warning("[config] IOException creating config file for " + tickerStr + ", " + e.getMessage()); } return configFile; @@ -181,7 +180,7 @@ public synchronized void setRpcPassword(String pass) { public synchronized boolean setRpcPort(int rpcPort) { if (rpcPort < 1 || rpcPort > 65535) { - LOGGER.log(Level.WARNING, "[config] Invalid port " + rpcPort + ", must be 1-65535"); + LOGGER.warning("[config] Invalid port " + rpcPort + ", must be 1-65535"); return false; } int maxAttempts = 100; @@ -191,7 +190,7 @@ public synchronized boolean setRpcPort(int rpcPort) { return true; } } - LOGGER.log(Level.WARNING, "[config] No available port in range " + rpcPort + "-" + Math.min(rpcPort + maxAttempts - 1, 65535)); + LOGGER.warning("[config] No available port in range " + rpcPort + "-" + Math.min(rpcPort + maxAttempts - 1, 65535)); return false; } @@ -269,7 +268,7 @@ public synchronized void writeConfig() { fw.write(newContent); } } catch (IOException e) { - LOGGER.log(Level.WARNING, "[config] IOException writing config for " + tickerStr, e); + LOGGER.warning("[config] IOException writing config for " + tickerStr + ", " + e.getMessage()); } } diff --git a/src/main/java/io/cloudchains/app/util/LogRotationManager.java b/src/main/java/io/cloudchains/app/util/LogRotationManager.java index f2363e5..876f79f 100644 --- a/src/main/java/io/cloudchains/app/util/LogRotationManager.java +++ b/src/main/java/io/cloudchains/app/util/LogRotationManager.java @@ -11,7 +11,6 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; -import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -72,11 +71,11 @@ public boolean rotateLogs() { List oldLogFiles = findOldLogFiles(); if (oldLogFiles.isEmpty()) { - LOGGER.log(Level.INFO, "[log-rotation] No old log files found. Current retention: {0} days", retentionDays); + LOGGER.info("[log-rotation] No old log files found. Current retention: " + retentionDays + " days"); return true; } - LOGGER.log(Level.INFO, "[log-rotation] Found {0} old log files to clean up", oldLogFiles.size()); + LOGGER.info("[log-rotation] Found " + oldLogFiles.size() + " old log files to clean up"); long totalSize = 0; int deletedCount = 0; @@ -87,23 +86,21 @@ public boolean rotateLogs() { if (file.delete()) { totalSize += fileSize; deletedCount++; - LOGGER.log(Level.FINE, "[log-rotation] Deleted: {0} ({1} bytes)", - new Object[]{file.getName(), fileSize}); + LOGGER.fine("[log-rotation] Deleted: " + file.getName() + " (" + fileSize + " bytes)"); } else { - LOGGER.log(Level.WARNING, "[log-rotation] Failed to delete: {0}", file.getName()); + LOGGER.warning("[log-rotation] Failed to delete: " + file.getName()); } } catch (SecurityException e) { - LOGGER.log(Level.SEVERE, "[log-rotation] Security exception deleting file: " + file.getName(), e); + LOGGER.severe("[log-rotation] Security exception deleting file: " + file.getName()); } } - LOGGER.log(Level.INFO, "[log-rotation] Cleanup completed: {0}/{1} files deleted, {2} bytes freed", - new Object[]{deletedCount, oldLogFiles.size(), totalSize}); + LOGGER.info("[log-rotation] Cleanup completed: " + deletedCount + "/" + oldLogFiles.size() + " files deleted, " + totalSize + " bytes freed"); return true; } catch (Exception e) { - LOGGER.log(Level.SEVERE, "[log-rotation] Error during log rotation", e); + LOGGER.severe("[log-rotation] Error during log rotation: " + e.getMessage()); return false; } } @@ -134,7 +131,7 @@ public List listLogFiles() { }); } catch (IOException e) { - LOGGER.log(Level.WARNING, "[log-rotation] Error listing log files", e); + LOGGER.warning("[log-rotation] Error listing log files"); } return files; @@ -177,7 +174,7 @@ private LocalDate extractDateFromFileName(String fileName) { return LocalDate.parse(datePart, DATE_FORMATTER); } catch (DateTimeParseException e) { - LOGGER.log(Level.FINE, "[log-rotation] Could not parse date from filename: " + fileName); + LOGGER.fine("[log-rotation] Could not parse date from filename: " + fileName); return null; } } @@ -208,7 +205,7 @@ private List findOldLogFiles() { }); } catch (IOException e) { - LOGGER.log(Level.WARNING, "[log-rotation] Error finding old log files", e); + LOGGER.warning("[log-rotation] Error finding old log files"); } return oldFiles; @@ -223,11 +220,11 @@ private boolean ensureLogDirectoryExists() { try { if (!Files.exists(logDirectory)) { Files.createDirectories(logDirectory); - LOGGER.log(Level.INFO, "[log-rotation] Created log directory: {0}", logDirectory); + LOGGER.info("[log-rotation] Created log directory: " + logDirectory); } return true; } catch (IOException e) { - LOGGER.log(Level.SEVERE, "[log-rotation] Failed to create log directory: " + logDirectory, e); + LOGGER.severe("[log-rotation] Failed to create log directory: " + logDirectory); return false; } } @@ -263,4 +260,4 @@ public String toString() { return String.format("LogFileInfo{name='%s', size=%d bytes, lastModified=%d}", name, size, lastModified); } } -} \ No newline at end of file +} diff --git a/src/main/java/io/cloudchains/app/util/LogRotationUtil.java b/src/main/java/io/cloudchains/app/util/LogRotationUtil.java index 3b75a46..1beb25a 100644 --- a/src/main/java/io/cloudchains/app/util/LogRotationUtil.java +++ b/src/main/java/io/cloudchains/app/util/LogRotationUtil.java @@ -4,7 +4,6 @@ import java.io.File; import java.util.List; -import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -37,17 +36,16 @@ public static void performLogRotation() { if (success) { // Log current log files after rotation List logFiles = rotationManager.listLogFiles(); - LOGGER.log(Level.INFO, "[log-rotation] Current log files after rotation: {0}", logFiles.size()); + LOGGER.info("[log-rotation] Current log files after rotation: " + logFiles.size()); for (LogRotationManager.LogFileInfo fileInfo : logFiles) { - LOGGER.log(Level.FINE, "[log-rotation] {0} ({1} bytes)", - new Object[]{fileInfo.getName(), fileInfo.getSize()}); + LOGGER.fine("[log-rotation] " + fileInfo.getName() + " (" + fileInfo.getSize() + " bytes)"); } } else { - LOGGER.log(Level.WARNING, "[log-rotation] Log rotation completed with errors"); + LOGGER.warning("[log-rotation] Log rotation completed with errors"); } } catch (Exception e) { - LOGGER.log(Level.SEVERE, "[log-rotation] Failed to perform log rotation", e); + LOGGER.severe("[log-rotation] Failed to perform log rotation: " + e.getMessage()); } } @@ -65,19 +63,17 @@ private static int getRetentionDaysFromEnvironment() { int envRetention = Integer.parseInt(retentionEnv.trim()); if (envRetention > 0) { retentionDays = envRetention; - LOGGER.log(Level.INFO, "[log-rotation] Using retention period from environment: {0} days", retentionDays); + LOGGER.info("[log-rotation] Using retention period from environment: " + retentionDays + " days"); } else { - LOGGER.log(Level.WARNING, "[log-rotation] Invalid retention period from environment: {0}. Using default: {1} days", - new Object[]{retentionEnv, DEFAULT_LOG_RETENTION_DAYS}); + LOGGER.warning("[log-rotation] Invalid retention period from environment: " + retentionEnv + ". Using default: " + DEFAULT_LOG_RETENTION_DAYS + " days"); } } catch (NumberFormatException e) { - LOGGER.log(Level.WARNING, "[log-rotation] Invalid retention period format from environment: {0}. Using default: {1} days", - new Object[]{retentionEnv, DEFAULT_LOG_RETENTION_DAYS}); + LOGGER.warning("[log-rotation] Invalid retention period format from environment: " + retentionEnv + ". Using default: " + DEFAULT_LOG_RETENTION_DAYS + " days"); } } else { - LOGGER.log(Level.INFO, "[log-rotation] Using default retention period: {0} days", retentionDays); + LOGGER.info("[log-rotation] Using default retention period: " + retentionDays + " days"); } return retentionDays; } -} \ No newline at end of file +} diff --git a/src/main/java/io/cloudchains/app/util/XRouterConfiguration.java b/src/main/java/io/cloudchains/app/util/XRouterConfiguration.java index 7e7a7ba..4b10252 100644 --- a/src/main/java/io/cloudchains/app/util/XRouterConfiguration.java +++ b/src/main/java/io/cloudchains/app/util/XRouterConfiguration.java @@ -8,7 +8,6 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Properties; -import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -55,12 +54,12 @@ public void parsePluginConfig() { try { properties = getPluginProperties(rawPluginConfig); } catch (IOException e) { - LOGGER.log(Level.WARNING, "[xrouter-config] Failed to parse plugin config for " + pluginName, e); + LOGGER.warning("[xrouter-config] Failed to parse plugin config for " + pluginName + ": " + e.getMessage()); return; } if (!properties.containsKey("parameters") && !properties.containsKey("paramsType")) { - LOGGER.log(Level.FINER, "[xrouter-plugin-config-parser] ERROR: Plugin has no parameters!"); + LOGGER.finer("[xrouter-plugin-config-parser] ERROR: Plugin has no parameters!"); } else { String[] rawParamTypes; @@ -75,7 +74,7 @@ public void parsePluginConfig() { continue; if (!pluginParamTypes.containsKey(rawParamType)) { - LOGGER.log(Level.FINER, "[xrouter-plugin-config-parser] ERROR: Invalid/unsupported plugin parameter type: " + rawParamType + ". Failing."); + LOGGER.finer("[xrouter-plugin-config-parser] ERROR: Invalid/unsupported plugin parameter type: " + rawParamType + ". Failing."); throw new IllegalArgumentException("Invalid/unsupported plugin parameter type: " + rawParamType); } @@ -95,13 +94,13 @@ public void parsePluginConfig() { clientRequestLimit = 100; } - LOGGER.log(Level.FINER, "[xrouter-plugin-config-parser] Processing '" + pluginName + "' complete."); - LOGGER.log(Level.FINER, "[xrouter-plugin-config-parser] DEBUG: " + pluginName + ": fee = " + fee); - LOGGER.log(Level.FINER, "[xrouter-plugin-config-parser] DEBUG: " + pluginName + ": params = "); + LOGGER.finer("[xrouter-plugin-config-parser] Processing '" + pluginName + "' complete."); + LOGGER.finer("[xrouter-plugin-config-parser] DEBUG: " + pluginName + ": fee = " + fee); + LOGGER.finer("[xrouter-plugin-config-parser] DEBUG: " + pluginName + ": params = "); for (int i = 0; i < paramTypes.size(); i++) { - LOGGER.log(Level.FINER, "Parameter " + i + ":\t" + paramTypes.get(i).getSimpleName()); + LOGGER.finer("Parameter " + i + ":\t" + paramTypes.get(i).getSimpleName()); } - LOGGER.log(Level.FINER, "[xrouter-plugin-config-parser] DEBUG: " + pluginName + ": clientRequestLimit = " + clientRequestLimit); + LOGGER.finer("[xrouter-plugin-config-parser] DEBUG: " + pluginName + ": clientRequestLimit = " + clientRequestLimit); } private static Properties getPluginProperties(String rawConfig) throws IOException { @@ -139,7 +138,7 @@ public void parseConfig() { if (properties == null) return; - LOGGER.log(Level.FINER, "[xrouter-config-parser] DEBUG: Properties: " + properties.toString()); + LOGGER.finer("[xrouter-config-parser] DEBUG: Properties: " + properties.toString()); supportedWallets.addAll(Arrays.asList(((String) properties.get("Main").get("wallets")).split(","))); timeout = Integer.parseInt((String) properties.get("Main").get("timeout")); @@ -154,7 +153,7 @@ public void parseConfig() { } } - LOGGER.log(Level.FINER, "[xrouter-config-parser] Processing complete."); + LOGGER.finer("[xrouter-config-parser] Processing complete."); } public ArrayList getSupportedWallets() { @@ -205,7 +204,7 @@ private static HashMap getProperties(String rawConfig) { try { properties = parseINI(formatted); } catch (IOException e) { - LOGGER.log(Level.WARNING, "[xrouter-config] Failed to parse XRouter config", e); + LOGGER.warning("[xrouter-config] Failed to parse XRouter config: " + e.getMessage()); return null; } diff --git a/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java b/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java index 1c6def4..8ebe4b2 100644 --- a/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java +++ b/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java @@ -1,277 +1,274 @@ -package io.cloudchains.app.util.background; - -import io.cloudchains.app.App; -import io.cloudchains.app.net.CoinInstance; -import io.cloudchains.app.net.CoinTickerUtils; -import io.cloudchains.app.net.api.http.client.HTTPClient; -import io.cloudchains.app.net.protocols.blocknet.BlocknetPeer; -import io.cloudchains.app.net.protocols.blocknet.BlocknetPeerGroup; -import io.cloudchains.app.util.LogRotationUtil; -import io.cloudchains.app.util.XRouterConfiguration; - -import java.time.Duration; -import java.time.LocalTime; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.LogManager; -import java.util.logging.Logger; - -public class BackgroundTimerThread implements Runnable { - private final static LogManager LOGMANAGER = LogManager.getLogManager(); - private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); - - public static final boolean HTTP_BLOCK_COUNT_UPDATES = true; - public static final boolean HTTP_BALANCE_UPDATES = true; - - private static final int KEEPALIVE_INTERVAL = 10000; - private static final int BALANCE_INTERVAL = 10000; - - private ExecutorService threadPool = Executors.newSingleThreadExecutor(); - - private BlocknetPeerGroup blocknetPeerGroup; - private HTTPClient feeUpdateHttpClient; - private HTTPClient heightUpdateHttpClient; - - private long lastKeepAliveTime; - private long lastBalanceUpdateTime; - - private long lastOut; - private boolean shutdownRequested = false; - private volatile Thread workerThread; - - private Set lastAvailable = new HashSet<>(); - private Set lastUnavailable = new HashSet<>(); - - // Log rotation scheduler fields - private ScheduledExecutorService logRotationScheduler; - private static final int DAILY_ROTATION_HOUR = 2; // 2:00 AM - private static final int DAILY_ROTATION_MINUTE = 0; - - public BackgroundTimerThread() { - blocknetPeerGroup = CoinInstance.getInstance(CoinInstance.getActiveBlocknetNetwork()).getBlocknetPeerGroup(); - feeUpdateHttpClient = App.feeUpdateHttpClient; - heightUpdateHttpClient = App.heightUpdateHttpClient; - - lastKeepAliveTime = 0; - lastBalanceUpdateTime = 0; - - lastOut = 0; - - // Initialize log rotation scheduler - initializeLogRotationScheduler(); - } - - /** - * Initializes the log rotation scheduler to run daily at 2:00 AM. - */ - private void initializeLogRotationScheduler() { - logRotationScheduler = Executors.newSingleThreadScheduledExecutor(); - long initialDelay = calculateInitialDelay(); - logRotationScheduler.scheduleAtFixedRate( - this::performDailyLogRotation, - initialDelay, - 24, TimeUnit.HOURS - ); - LocalTime now = LocalTime.now(); - LOGGER.log(Level.INFO, "[BackgroundTimer] Scheduled daily log rotation at {0} (current time: {1})", - new Object[]{String.format("%02d:%02d", DAILY_ROTATION_HOUR, DAILY_ROTATION_MINUTE), - String.format("%02d:%02d", now.getHour(), now.getMinute())}); - } - - /** - * Calculates the initial delay until the next scheduled log rotation at 2:00 AM. - * - * @return Delay in milliseconds until next 2:00 AM - */ - private long calculateInitialDelay() { - LocalTime now = LocalTime.now(); - LocalTime targetTime = LocalTime.of(DAILY_ROTATION_HOUR, DAILY_ROTATION_MINUTE); - long delay; - if (now.isBefore(targetTime)) { - delay = Duration.between(now, targetTime).toMillis(); - } else { - delay = Duration.between(now, targetTime.plusHours(24)).toMillis(); - } - return Math.max(delay, 0); - } - - /** - * Performs the daily log rotation task. - * Called by the scheduler every 24 hours at 2:00 AM. - */ - private void performDailyLogRotation() { - try { - LOGGER.log(Level.INFO, "[BackgroundTimer] Starting scheduled daily log rotation"); - LogRotationUtil.performLogRotation(); - LOGGER.log(Level.INFO, "[BackgroundTimer] Daily log rotation completed successfully"); - } catch (Exception e) { - LOGGER.log(Level.SEVERE, "[BackgroundTimer] Failed to perform daily log rotation", e); - } - } - - public void stop() { - shutdownRequested = true; - if (workerThread != null) { - workerThread.interrupt(); - } - if (threadPool != null && !threadPool.isShutdown()) { - threadPool.shutdown(); - try { - if (!threadPool.awaitTermination(5, TimeUnit.SECONDS)) { - threadPool.shutdownNow(); - } - } catch (InterruptedException e) { - threadPool.shutdownNow(); - Thread.currentThread().interrupt(); - } - } - if (logRotationScheduler != null && !logRotationScheduler.isShutdown()) { - logRotationScheduler.shutdown(); - try { - if (!logRotationScheduler.awaitTermination(5, TimeUnit.SECONDS)) { - logRotationScheduler.shutdownNow(); - } - } catch (InterruptedException e) { - logRotationScheduler.shutdownNow(); - Thread.currentThread().interrupt(); - } - } - } - - private void outputAvailableCurrencies() { - long elapsed = (System.currentTimeMillis() - lastOut); - - if (elapsed < 60 * 1000 && lastOut != 0) - return; - - List available = new ArrayList<>(); - Set unavailable = new HashSet<>(); - - for (CoinInstance coinInstance : CoinInstance.getCoinInstances()) { - if (!CoinTickerUtils.isActiveTicker(coinInstance.getTicker())) - continue; - - String name = CoinTickerUtils.tickerToString(coinInstance.getTicker()); - if (CoinInstance.getBlockCountByTicker(coinInstance.getTicker()) > 0) { - available.add(name); - } else { - unavailable.add(name); - } - } - - Set currentAvailable = new HashSet<>(available); - if (!currentAvailable.equals(lastAvailable)) { - LOGGER.log(Level.INFO, "[coin] Available: " + String.join(", ", available)); - } - lastAvailable = currentAvailable; - - if (!unavailable.isEmpty() && !unavailable.equals(lastUnavailable)) { - LOGGER.log(Level.INFO, "[coin] Unavailable: " + String.join(", ", unavailable)); - } - lastUnavailable = unavailable; - lastOut = System.currentTimeMillis(); - } - - private void sendKeepAlive() { - long elapsed = (System.currentTimeMillis() - lastKeepAliveTime); - - if (elapsed < KEEPALIVE_INTERVAL && lastKeepAliveTime != 0) - return; - - if (HTTP_BLOCK_COUNT_UPDATES) { - heightUpdateHttpClient.getAllBlockCounts(); - } else if (!blocknetPeerGroup.getConnectedPeers().isEmpty()) { - for (BlocknetPeer blocknetPeer : blocknetPeerGroup.getConnectedPeers()) { - XRouterConfiguration xRouterConfiguration = blocknetPeer.getxRouterConfiguration(); - if (xRouterConfiguration == null) - continue; - - for (CoinInstance coinInstance : CoinInstance.getCoinInstances()) { - if (!CoinTickerUtils.isActiveTicker(coinInstance.getTicker())) - continue;else if (!blocknetPeer.getxRouterConfiguration().getSupportedWallets().contains(coinInstance.getNetworkParameters().getId())) - continue; - - coinInstance.sendXrGetBlockCount(blocknetPeer); - LOGGER.log(Level.FINER, "[BackgroundTimer] Sent keepalive message: " + coinInstance.getNetworkParameters().getId()); - } - } - } else { - return; - } - - lastKeepAliveTime = System.currentTimeMillis(); - } - - private void sendBalanceUpdate() { - long elapsed = (System.currentTimeMillis() - lastBalanceUpdateTime); - - if (elapsed < BALANCE_INTERVAL && lastBalanceUpdateTime != 0) - return; - - // No longer polling balances and transaction history here. Instead it is requested - // on demand when client requests the data. See HTTPServerHandler.java:302-330 - - for (CoinInstance coinInstance : CoinInstance.getCoinInstances()) { - if (!CoinTickerUtils.isActiveTicker(coinInstance.getTicker())) - continue; - - if (CoinInstance.getBlockCountByTicker(coinInstance.getTicker()) <= 0) { - continue; - } - - if (blocknetPeerGroup.getConnectedPeers().isEmpty()) { - return; - } - - BlocknetPeer blocknetPeer = blocknetPeerGroup.getBestBlocknetPeer(coinInstance.getNetworkParameters().getId()); - if (blocknetPeer == null) { - LOGGER.log(Level.FINER, "[BackgroundTimer] Peer was not found for currency " + coinInstance.getNetworkParameters().getId()); - continue; - } - - coinInstance.sendXrGetUtxos(blocknetPeer); - LOGGER.log(Level.FINER, "[BackgroundTimer] Sent GetUtxos message: " + coinInstance.getNetworkParameters().getId()); - } - - lastBalanceUpdateTime = System.currentTimeMillis(); - } - - @Override - public void run() { - workerThread = Thread.currentThread(); - LOGGER.log(Level.FINER, "[BackgroundTimer] Waiting until initial messages are sent off."); - - for (CoinInstance coinInstance : CoinInstance.getCoinInstances()) { - if (!CoinTickerUtils.isActiveTicker(coinInstance.getTicker())) - continue; - - new Thread(() -> { - App.feeUpdateHttpClient.getHistory(coinInstance.getTicker(), 0, (int) System.currentTimeMillis(), 30000); - }).start(); - } - - while (!Thread.currentThread().isInterrupted()) { - if (shutdownRequested) - break; - try { - sendKeepAlive(); - outputAvailableCurrencies(); - - Thread.sleep(100); - } catch (InterruptedException e) { - break; - } catch (NullPointerException e) { - LOGGER.log(Level.WARNING, "[BackgroundTimer] Null pointer", e); - } catch (Exception e) { - LOGGER.log(Level.WARNING, "[BackgroundTimer] Unexpected error", e); - } - } - } -} +package io.cloudchains.app.util.background; + +import io.cloudchains.app.App; +import io.cloudchains.app.net.CoinInstance; +import io.cloudchains.app.net.CoinTickerUtils; +import io.cloudchains.app.net.api.http.client.HTTPClient; +import io.cloudchains.app.net.protocols.blocknet.BlocknetPeer; +import io.cloudchains.app.net.protocols.blocknet.BlocknetPeerGroup; +import io.cloudchains.app.util.LogRotationUtil; +import io.cloudchains.app.util.XRouterConfiguration; + +import java.time.Duration; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.logging.LogManager; +import java.util.logging.Logger; + +public class BackgroundTimerThread implements Runnable { + private final static LogManager LOGMANAGER = LogManager.getLogManager(); + private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); + + public static final boolean HTTP_BLOCK_COUNT_UPDATES = true; + public static final boolean HTTP_BALANCE_UPDATES = true; + + private static final int KEEPALIVE_INTERVAL = 10000; + private static final int BALANCE_INTERVAL = 10000; + + private ExecutorService threadPool = Executors.newSingleThreadExecutor(); + + private BlocknetPeerGroup blocknetPeerGroup; + private HTTPClient feeUpdateHttpClient; + private HTTPClient heightUpdateHttpClient; + + private long lastKeepAliveTime; + private long lastBalanceUpdateTime; + + private long lastOut; + private boolean shutdownRequested = false; + private volatile Thread workerThread; + + private Set lastAvailable = new HashSet<>(); + private Set lastUnavailable = new HashSet<>(); + + // Log rotation scheduler fields + private ScheduledExecutorService logRotationScheduler; + private static final int DAILY_ROTATION_HOUR = 2; // 2:00 AM + private static final int DAILY_ROTATION_MINUTE = 0; + + public BackgroundTimerThread() { + blocknetPeerGroup = CoinInstance.getInstance(CoinInstance.getActiveBlocknetNetwork()).getBlocknetPeerGroup(); + feeUpdateHttpClient = App.feeUpdateHttpClient; + heightUpdateHttpClient = App.heightUpdateHttpClient; + + lastKeepAliveTime = 0; + lastBalanceUpdateTime = 0; + + lastOut = 0; + + // Initialize log rotation scheduler + initializeLogRotationScheduler(); + } + + /** + * Initializes the log rotation scheduler to run daily at 2:00 AM. + */ + private void initializeLogRotationScheduler() { + logRotationScheduler = Executors.newSingleThreadScheduledExecutor(); + long initialDelay = calculateInitialDelay(); + logRotationScheduler.scheduleAtFixedRate( + this::performDailyLogRotation, + initialDelay, + 24, TimeUnit.HOURS + ); + LocalTime now = LocalTime.now(); + LOGGER.info("[BackgroundTimer] Scheduled daily log rotation at " + String.format("%02d:%02d", DAILY_ROTATION_HOUR, DAILY_ROTATION_MINUTE) + " (current time: " + String.format("%02d:%02d", now.getHour(), now.getMinute()) + ")"); + } + + /** + * Calculates the initial delay until the next scheduled log rotation at 2:00 AM. + * + * @return Delay in milliseconds until next 2:00 AM + */ + private long calculateInitialDelay() { + LocalTime now = LocalTime.now(); + LocalTime targetTime = LocalTime.of(DAILY_ROTATION_HOUR, DAILY_ROTATION_MINUTE); + long delay; + if (now.isBefore(targetTime)) { + delay = Duration.between(now, targetTime).toMillis(); + } else { + delay = Duration.between(now, targetTime.plusHours(24)).toMillis(); + } + return Math.max(delay, 0); + } + + /** + * Performs the daily log rotation task. + * Called by the scheduler every 24 hours at 2:00 AM. + */ + private void performDailyLogRotation() { + try { + LOGGER.info("[BackgroundTimer] Starting scheduled daily log rotation"); + LogRotationUtil.performLogRotation(); + LOGGER.info("[BackgroundTimer] Daily log rotation completed successfully"); + } catch (Exception e) { + LOGGER.severe("[BackgroundTimer] Failed to perform daily log rotation: " + e.getMessage()); + } + } + + public void stop() { + shutdownRequested = true; + if (workerThread != null) { + workerThread.interrupt(); + } + if (threadPool != null && !threadPool.isShutdown()) { + threadPool.shutdown(); + try { + if (!threadPool.awaitTermination(5, TimeUnit.SECONDS)) { + threadPool.shutdownNow(); + } + } catch (InterruptedException e) { + threadPool.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + if (logRotationScheduler != null && !logRotationScheduler.isShutdown()) { + logRotationScheduler.shutdown(); + try { + if (!logRotationScheduler.awaitTermination(5, TimeUnit.SECONDS)) { + logRotationScheduler.shutdownNow(); + } + } catch (InterruptedException e) { + logRotationScheduler.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + } + + private void outputAvailableCurrencies() { + long elapsed = (System.currentTimeMillis() - lastOut); + + if (elapsed < 60 * 1000 && lastOut != 0) + return; + + List available = new ArrayList<>(); + Set unavailable = new HashSet<>(); + + for (CoinInstance coinInstance : CoinInstance.getCoinInstances()) { + if (!CoinTickerUtils.isActiveTicker(coinInstance.getTicker())) + continue; + + String name = CoinTickerUtils.tickerToString(coinInstance.getTicker()); + if (CoinInstance.getBlockCountByTicker(coinInstance.getTicker()) > 0) { + available.add(name); + } else { + unavailable.add(name); + } + } + + Set currentAvailable = new HashSet<>(available); + if (!currentAvailable.equals(lastAvailable)) { + LOGGER.info("[coin] Available: " + String.join(", ", available)); + } + lastAvailable = currentAvailable; + + if (!unavailable.isEmpty() && !unavailable.equals(lastUnavailable)) { + LOGGER.info("[coin] Unavailable: " + String.join(", ", unavailable)); + } + lastUnavailable = unavailable; + lastOut = System.currentTimeMillis(); + } + + private void sendKeepAlive() { + long elapsed = (System.currentTimeMillis() - lastKeepAliveTime); + + if (elapsed < KEEPALIVE_INTERVAL && lastKeepAliveTime != 0) + return; + + if (HTTP_BLOCK_COUNT_UPDATES) { + heightUpdateHttpClient.getAllBlockCounts(); + } else if (!blocknetPeerGroup.getConnectedPeers().isEmpty()) { + for (BlocknetPeer blocknetPeer : blocknetPeerGroup.getConnectedPeers()) { + XRouterConfiguration xRouterConfiguration = blocknetPeer.getxRouterConfiguration(); + if (xRouterConfiguration == null) + continue; + + for (CoinInstance coinInstance : CoinInstance.getCoinInstances()) { + if (!CoinTickerUtils.isActiveTicker(coinInstance.getTicker())) + continue;else if (!blocknetPeer.getxRouterConfiguration().getSupportedWallets().contains(coinInstance.getNetworkParameters().getId())) + continue; + + coinInstance.sendXrGetBlockCount(blocknetPeer); + LOGGER.finer("[BackgroundTimer] Sent keepalive message: " + coinInstance.getNetworkParameters().getId()); + } + } + } else { + return; + } + + lastKeepAliveTime = System.currentTimeMillis(); + } + + private void sendBalanceUpdate() { + long elapsed = (System.currentTimeMillis() - lastBalanceUpdateTime); + + if (elapsed < BALANCE_INTERVAL && lastBalanceUpdateTime != 0) + return; + + // No longer polling balances and transaction history here. Instead it is requested + // on demand when client requests the data. See HTTPServerHandler.java:302-330 + + for (CoinInstance coinInstance : CoinInstance.getCoinInstances()) { + if (!CoinTickerUtils.isActiveTicker(coinInstance.getTicker())) + continue; + + if (CoinInstance.getBlockCountByTicker(coinInstance.getTicker()) <= 0) { + continue; + } + + if (blocknetPeerGroup.getConnectedPeers().isEmpty()) { + return; + } + + BlocknetPeer blocknetPeer = blocknetPeerGroup.getBestBlocknetPeer(coinInstance.getNetworkParameters().getId()); + if (blocknetPeer == null) { + LOGGER.finer("[BackgroundTimer] Peer was not found for currency " + coinInstance.getNetworkParameters().getId()); + continue; + } + + coinInstance.sendXrGetUtxos(blocknetPeer); + LOGGER.finer("[BackgroundTimer] Sent GetUtxos message: " + coinInstance.getNetworkParameters().getId()); + } + + lastBalanceUpdateTime = System.currentTimeMillis(); + } + + @Override + public void run() { + workerThread = Thread.currentThread(); + LOGGER.finer("[BackgroundTimer] Waiting until initial messages are sent off."); + + for (CoinInstance coinInstance : CoinInstance.getCoinInstances()) { + if (!CoinTickerUtils.isActiveTicker(coinInstance.getTicker())) + continue; + + new Thread(() -> { + App.feeUpdateHttpClient.getHistory(coinInstance.getTicker(), 0, (int) System.currentTimeMillis(), 30000); + }).start(); + } + + while (!Thread.currentThread().isInterrupted()) { + if (shutdownRequested) + break; + try { + sendKeepAlive(); + outputAvailableCurrencies(); + + Thread.sleep(100); + } catch (InterruptedException e) { + break; + } catch (NullPointerException e) { + LOGGER.warning("[BackgroundTimer] Null pointer: " + e.getMessage()); + } catch (Exception e) { + LOGGER.warning("[BackgroundTimer] Unexpected error: " + e.getMessage()); + } + } + } +} diff --git a/src/main/resources/simplelogger.properties b/src/main/resources/simplelogger.properties index f92b74d..ecb2732 100644 --- a/src/main/resources/simplelogger.properties +++ b/src/main/resources/simplelogger.properties @@ -1,4 +1,4 @@ -z# SLF4J SimpleLogger configuration +# SLF4J SimpleLogger configuration # Set bitcoinj logging level to WARN only org.slf4j.simpleLogger.log.org.bitcoinj=warn From 4a8e1dd7de80fc28b46d804ced1b7098e0f47430 Mon Sep 17 00:00:00 2001 From: tryiou Date: Mon, 24 Aug 2026 18:01:53 +0200 Subject: [PATCH 48/73] fix(rpc): document reloadconfig and getrawtransaction in coin help Both methods are implemented and dispatched for every coin but were absent from the per-coin helpText returned by the 'help' RPC, leaving client-side surfaces blind to them: - Utilities section: reloadconfig - Raw Transactions section: getrawtransaction [verbose] Master surface already documented both; this aligns every coin server. Verified mvn compile; methods exercised via full-surface RPC battery. --- .../cloudchains/app/net/api/http/server/HTTPServerHandler.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java b/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java index 0d922bc..e7ed63f 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java +++ b/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java @@ -1472,8 +1472,10 @@ private JsonObject getResponse(String method, JsonArray params) { + "signmessage
- Sign a message with a given address' private key\n" + "verifymessage
- Verify a signature for a message signed by a given address\n" + "validateaddress
- Validate a given address\n" + + "reloadconfig - Reload configuration for the specified coin ticker\n" + "sendtransaction
- Create and broadcast a signed transaction to the network\n" + "\n=====Raw Transactions=====\n" + + "getrawtransaction [verbose] - Get a transaction's serialized hex (JSON when verbose)\n" + "createrawtransaction - Create a raw transaction given inputs and outputs in JSON format. For more info, run createrawtransaction with no arguments.\n" + "decoderawtransaction - Get a raw transaction's JSON representation\n" + "signrawtransaction - Sign a raw transaction\n" From d74d58854322adaaee1b5fbbd400b992a84a734b Mon Sep 17 00:00:00 2001 From: tryiou Date: Mon, 24 Aug 2026 18:02:03 +0200 Subject: [PATCH 49/73] docs: add data-providers guide to AGENTS.md Document the client-side role: one local JSON-RPC port per coin, all blockchain data fetched upstream over HTTP. Describe BASE_URL legacy mode and the EXR endpoint pool mode (--exr-endpoint / EXR_ENDPOINT, capability probing via /xrs/heights, coin-aware routing, no fallback), the upstream chain (exrproxy -> plugin-adapter -> utxo-plugin), and warn that net/xrouter plus BlocknetPeerGroup xrm handling are dead legacy paths. --- AGENTS.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 1e38dde..0981677 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,6 +6,28 @@ XLite Daemon — a multi-cryptocurrency wallet daemon built with Java 21 and Mav Core packages: `crypto` (wallet encryption/key management), `net` (coin networking, JSON-RPC), `util` (config, address discovery, logging), `wallet` (wallet helpers). +## Data Providers + +This is the **client-side** daemon behind the XLite wallet GUI: it serves one +local JSON-RPC port per coin (`net/api/http/master/`) and fetches all blockchain +data upstream over HTTP (`net/api/http/client/`). + +- **EXR mode:** CLI `--exr-endpoint ` or env `EXR_ENDPOINT` + (comma-separated pool → `EXRServerPool`). Capabilities are probed from GET + `/xrs/heights`; health checks every 5 s; requests route to a server that + supports the coin. Calls hit `/xrs/` — GET `heights`/`fees`, + POST coin-first-param methods (`getutxos`, `sendrawtransaction`, …). Once EXR + is configured there is **NO fallback** to BASE_URL — requests for coins no EXR + server supports fail hard. +- **Legacy mode (default):** `BASE_URL` = `https://xliterevp.mywire.org/` in + `App.java`; override with `--development-endpoint`. POST `/` with + `{method, params}` JSON. +- Upstream chain: exrproxy `/xrs/` → plugin-adapter xrm methods → + utxo-plugin containers. +- `net/xrouter/` and the `xrm*` handling in `BlocknetPeerGroup` are dead legacy + XRouter-over-p2p paths (commented out at `CoinInstance.java:517`) — don't build + on them. + # Java — use jabba source ~/.jabba/jabba.sh && jabba use graalvm_community@21.0.2 From 7eda01b67c971097e37ca4fda71689b8a8e69bb2 Mon Sep 17 00:00:00 2001 From: tryiou Date: Mon, 24 Aug 2026 21:12:41 +0200 Subject: [PATCH 50/73] redact sensitive RPC logging; fix correctness and readiness gaps - importprivkey/dumpprivkey/signmessage no longer logged - validateaddress handles bech32 (witness scriptPubKey, no crash); signrawtransaction aborts on missing input instead of returning complete:true partial hex; getrawtransaction arity guard; structured upstream errors forwarded; decoderawtransaction flags output parse failures; listunspent skips bad rows, confirmations clamped >= 0 - fundTransaction single-input shortfall raises insufficient-funds - servers log "listening" only after bind; reloadconfig awaits port release and verifies rebind; height cache accepts decreases - auth failures exit nonzero after wiping credentials; ConsoleMenu no longer clobbers logger level --- .../cloudchains/app/console/ConsoleMenu.java | 23 +++++- .../io/cloudchains/app/net/CoinInstance.java | 34 ++++++++- .../app/net/api/JSONRPCController.java | 4 + .../app/net/api/JSONRPCMasterServer.java | 5 ++ .../app/net/api/JSONRPCServer.java | 35 ++++++++- .../api/http/master/HTTPServerHandler.java | 4 +- .../api/http/server/HTTPServerHandler.java | 75 ++++++++++++++++--- .../java/io/cloudchains/app/util/Utility.java | 11 +++ .../cloudchains/app/wallet/WalletHelper.java | 5 +- 9 files changed, 175 insertions(+), 21 deletions(-) diff --git a/src/main/java/io/cloudchains/app/console/ConsoleMenu.java b/src/main/java/io/cloudchains/app/console/ConsoleMenu.java index 1af62e9..0c973e2 100644 --- a/src/main/java/io/cloudchains/app/console/ConsoleMenu.java +++ b/src/main/java/io/cloudchains/app/console/ConsoleMenu.java @@ -21,7 +21,6 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -35,7 +34,9 @@ public class ConsoleMenu { public ConsoleMenu(String[] args) { this.arguments = args; - LOGGER.setLevel(Level.INFO); + // Do NOT set the logger level here — App owns logging configuration + // (CLOUDCHAINS_LOG_LEVEL); clobbering it in this constructor made all + // FINE/FINER diagnostics unreachable. } public void logBadPassword(String msg) { @@ -215,6 +216,15 @@ public void init() { } String mnemonic = CoinInstance.getMnemonicForPw(password); + if (mnemonic == null || mnemonic.isEmpty()) { + // Wrong password (or unusable wallet) must not + // masquerade as success with an empty phrase. + // Wipe BEFORE exiting — System.exit does not + // unwind, so a finally block would be skipped. + LOGGER.severe("Error(BADPASSWORD): could not retrieve mnemonic for the supplied password"); + Arrays.fill(password, '\0'); + System.exit(4); + } System.out.println(mnemonic); } finally { Arrays.fill(password, '\0'); @@ -391,7 +401,7 @@ public void deinit() { private void completeLogin(char[] password, String userMnemonic, boolean isMnemonic) { if (password == null && userMnemonic == null) { logBadPassword(null); - System.exit(0); + System.exit(5); } long startTime = System.currentTimeMillis(); @@ -400,7 +410,12 @@ private void completeLogin(char[] password, String userMnemonic, boolean isMnemo if (coinError != null) { String msg = "[master] Error(" + coinError.getCode().name() + "): " + coinError.getMessage(); LOGGER.severe(msg); - System.exit(0); + // Auth/init failure must not exit 0 — scripts and the GUI treat + // exit code 0 as success. Wipe the credential before exiting + // (System.exit does not unwind caller finally blocks). + if (password != null) + Arrays.fill(password, '\0'); + System.exit(5); } List otherCoins = new ArrayList<>(); diff --git a/src/main/java/io/cloudchains/app/net/CoinInstance.java b/src/main/java/io/cloudchains/app/net/CoinInstance.java index ff6b3bf..fbd2424 100644 --- a/src/main/java/io/cloudchains/app/net/CoinInstance.java +++ b/src/main/java/io/cloudchains/app/net/CoinInstance.java @@ -506,7 +506,10 @@ public CoinError init(char[] pw, String userMnemonic, boolean isMnemonic, boolea if (configHelper.isRpcEnabled() && configHelper.validAuth() && rpcPort != -1) { coinRPCServer = JSONRPCController.getRPCServer(this); - LOGGER.info("[rpc] Starting JSON-RPC server for coin " + CoinTickerUtils.tickerToString(getTicker()) + " on port " + getRPCPort()); + // Readiness is announced by JSONRPCServer itself AFTER a successful + // bind ("[rpc] RPC server listening for …") — do not log a + // success-shaped line before the socket exists. + LOGGER.finer("[rpc] Requesting start of JSON-RPC server for coin " + CoinTickerUtils.tickerToString(getTicker()) + " on port " + getRPCPort()); if (coinRPCServer.isAlive()) coinRPCServer.deinit(); @@ -784,7 +787,16 @@ public JsonArray getAllUTXOS() { if (utxo.isSpent()) continue; - org.bitcoinj.core.UTXO bUtxo = utxo.createUTXO(); + org.bitcoinj.core.UTXO bUtxo; + try { + bUtxo = utxo.createUTXO(); + } catch (Exception e) { + // One malformed/bech32 address must not kill the whole + // listunspent response — skip the row, keep the rest. + LOGGER.warning("[coin] Skipping unparseable UTXO for " + CoinTickerUtils.tickerToString(getTicker()) + + " (" + e.getClass().getSimpleName() + ": " + e.getMessage() + ")"); + continue; + } JsonObject utxoJSON = new JsonObject(); utxoJSON.addProperty("txid", bUtxo.getHash().toString()); @@ -820,6 +832,9 @@ public int signum() { int confirmations = (totalBlocks - bUtxo.getHeight()) + 1; if (bUtxo.getHeight() == 0) confirmations = 0; + // Pre-first-poll the height cache holds a negative sentinel + // (-1); clamp so no caller ever sees negative confirmations. + confirmations = Math.max(0, confirmations); utxoJSON.addProperty("confirmations", confirmations); @@ -891,8 +906,12 @@ public WalletHelper getWalletHelper() { } public void addBlockCount(CoinTicker ticker, Integer blockCount) { + // Plain set: the cache must track the true tip. The old Math.max + // ratchet pinned a stale-high height forever (e.g. after network + // switch or a rollback), and pre-first-poll zeros poisoned + // confirmation math. blockCounts.computeIfAbsent(ticker, k -> new AtomicInteger(0)) - .updateAndGet(current -> Math.max(current, blockCount)); + .set(blockCount); } public void addCloudTransaction(CloudTransaction cloudTransaction) { @@ -970,8 +989,15 @@ public void reloadConfig() { coinRPCServer = JSONRPCController.getRPCServer(this); - LOGGER.info("[rpc] Starting JSON-RPC server for coin " + CoinTickerUtils.tickerToString(getTicker()) + " on port " + getRPCPort()); + LOGGER.finer("[rpc] Requesting start of JSON-RPC server for coin " + CoinTickerUtils.tickerToString(getTicker()) + " on port " + getRPCPort()); coinRPCServer.start(); + + // Verify the rebind actually took — a silent bind failure used to + // leave the coin RPC dead while callers already got success. + if (!coinRPCServer.awaitBound(5000)) { + LOGGER.severe("[rpc] Failed to rebind JSON-RPC server for coin " + + CoinTickerUtils.tickerToString(getTicker()) + " on port " + getRPCPort() + " after reloadconfig"); + } } public KeyHandler getKeyHandler() { diff --git a/src/main/java/io/cloudchains/app/net/api/JSONRPCController.java b/src/main/java/io/cloudchains/app/net/api/JSONRPCController.java index 72ec796..150c22f 100644 --- a/src/main/java/io/cloudchains/app/net/api/JSONRPCController.java +++ b/src/main/java/io/cloudchains/app/net/api/JSONRPCController.java @@ -35,6 +35,10 @@ public static void removeRPCServer(CoinInstance coinInstance) { if (server.isAlive()) server.deinit(); + // Wait (bounded) for the old listener to release its socket so an + // immediate rebind on the same port cannot lose the race. + server.awaitPortRelease(3000); + servers.remove(coinInstance); } } diff --git a/src/main/java/io/cloudchains/app/net/api/JSONRPCMasterServer.java b/src/main/java/io/cloudchains/app/net/api/JSONRPCMasterServer.java index 2997cdb..e46e4f9 100644 --- a/src/main/java/io/cloudchains/app/net/api/JSONRPCMasterServer.java +++ b/src/main/java/io/cloudchains/app/net/api/JSONRPCMasterServer.java @@ -41,6 +41,11 @@ public void run() { channel = bootstrap.bind(port).sync().channel(); + // Emitted only after the bind actually succeeded — readiness + // consumers (xlite-gui) anchor on this line, NOT on the + // pre-bind "Starting" line below. + LOGGER.info("[rpc-master] Master RPC server listening on port " + port + "."); + channel.closeFuture().sync(); } catch (Exception e) { if (!stopping) { diff --git a/src/main/java/io/cloudchains/app/net/api/JSONRPCServer.java b/src/main/java/io/cloudchains/app/net/api/JSONRPCServer.java index ed10e34..883f1bd 100644 --- a/src/main/java/io/cloudchains/app/net/api/JSONRPCServer.java +++ b/src/main/java/io/cloudchains/app/net/api/JSONRPCServer.java @@ -11,6 +11,7 @@ import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; +import java.util.concurrent.TimeUnit; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -21,6 +22,8 @@ public class JSONRPCServer extends Thread { private final CoinInstance coin; private final int port; private boolean stopping = false; + private volatile boolean bound = false; + private volatile boolean bindFailed = false; private Channel channel; private EventLoopGroup workerGroup; @@ -43,16 +46,46 @@ public void run() { channel = bootstrap.bind(port).sync().channel(); - LOGGER.finer("[rpc] Starting RPC server for " + CoinTickerUtils.tickerToString(coin.getTicker()) + " on port " + port + "."); + // Emitted only after the bind actually succeeded. + LOGGER.info("[rpc] RPC server listening for " + CoinTickerUtils.tickerToString(coin.getTicker()) + " on port " + port + "."); + bound = true; channel.closeFuture().sync(); } catch (Exception e) { + bindFailed = true; if (!stopping) { LOGGER.warning("[rpc-server] Error during RPC server operation for " + CoinTickerUtils.tickerToString(coin.getTicker()) + e.getMessage()); } } } + /** + * Blocks until the listener has either bound successfully or failed. + * @return true iff the port is bound and accepting. + */ + public boolean awaitBound(long timeoutMillis) { + long deadline = System.currentTimeMillis() + timeoutMillis; + while (System.currentTimeMillis() < deadline) { + if (bound || bindFailed) return bound; + try { + Thread.sleep(25); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return bound; + } + } + return bound; + } + + /** + * Best-effort wait until the previous listener has released its socket, + * so an immediate rebind on the same port cannot race the async close. + */ + public void awaitPortRelease(long timeoutMillis) { + if (channel != null) + channel.closeFuture().awaitUninterruptibly(timeoutMillis, TimeUnit.MILLISECONDS); + } + public void deinit() { stopping = true; LOGGER.finer("[json-rpc-server] Interrupting server."); diff --git a/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerHandler.java b/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerHandler.java index 802ae1a..a66940e 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerHandler.java +++ b/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerHandler.java @@ -186,7 +186,9 @@ protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) String method = jsonReq.get("method").getAsString(); JsonArray params = jsonReq.get("params").getAsJsonArray(); - LOGGER.info("[http-server-handler] RPC CALL: " + method + " PARAMS: " + params.toString().replace(",", ", ")); + // Master surface handles wallet management — params may contain + // passwords; never log them verbatim. + LOGGER.info("[http-server-handler] RPC CALL: " + method + " PARAMS: "); response = getResponse(method, params); LOGGER.finer(response.toString()); diff --git a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java b/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java index e7ed63f..e8ec9de 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java +++ b/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java @@ -36,6 +36,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -57,6 +58,10 @@ public class HTTPServerHandler extends SimpleChannelInboundHandler SENSITIVE_METHODS = Set.of( + "importprivkey", "dumpprivkey", "signmessage"); + private HTTPClient httpClient; private CoinInstance coin; private ConfigHelper configHelper; @@ -219,10 +224,15 @@ protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) String method = jsonReq.get("method").getAsString(); JsonArray params = jsonReq.get("params").getAsJsonArray(); - LOGGER.info("[http-server-handler] RPC CALL: " + coin.getTicker() + " " + method + " PARAMS: " + params.toString().replace(",", ", ")); + boolean sensitiveMethod = SENSITIVE_METHODS.contains(method); + LOGGER.info("[http-server-handler] RPC CALL: " + coin.getTicker() + " " + method + + " PARAMS: " + (sensitiveMethod ? "" : params.toString().replace(",", ", "))); response = getResponse(method, params); - LOGGER.finer(response.toString()); + if (sensitiveMethod) + LOGGER.finer("[http-server-handler] response withheld (sensitive method)"); + else + LOGGER.finer(response.toString()); } else { ByteBuf responseContent = Unpooled.copiedBuffer(response.toString(), CharsetUtil.UTF_8); FullHttpResponse httpResponse = new DefaultFullHttpResponse(request.protocolVersion(), status, responseContent); @@ -435,14 +445,27 @@ private JsonObject getResponse(String method, JsonArray params) { JsonObject txid = httpClient.sendRawTransaction(coin.getTicker(), rawTx); if (txid == null || txid.has("error") && !txid.get("error").isJsonNull()) { int code = -1; - - if (txid != null) - code = txid.get("error").getAsInt(); + String message = "Error sending transaction!"; + + // Upstream error may be a legacy int (hosted backend) or a + // structured {code,message} object (plugin-adapter) — accept both. + if (txid != null && txid.has("error") && !txid.get("error").isJsonNull()) { + JsonElement errElement = txid.get("error"); + if (errElement.isJsonPrimitive()) { + code = errElement.getAsInt(); + } else if (errElement.isJsonObject()) { + JsonObject errObj = errElement.getAsJsonObject(); + if (errObj.has("code")) + code = errObj.get("code").getAsInt(); + if (errObj.has("message")) + message = errObj.get("message").getAsString(); + } + } response.add("result", JsonNull.INSTANCE); JsonObject errorJSON = new JsonObject(); errorJSON.addProperty("code", code); - errorJSON.addProperty("message", "Error sending transaction!"); + errorJSON.addProperty("message", message); response.add("error", errorJSON); break; @@ -462,7 +485,7 @@ private JsonObject getResponse(String method, JsonArray params) { break; } case "getrawtransaction": { - if (params.size() > 2) { + if (params.size() < 1 || params.size() > 2) { response.add("result", JsonNull.INSTANCE); JsonObject errorJSON = new JsonObject(); errorJSON.addProperty("code", -1); @@ -857,6 +880,7 @@ private JsonObject getResponse(String method, JsonArray params) { txJSON.add("vin", vin); JsonArray vout = new JsonArray(); + boolean outputParseFailed = false; for (TransactionOutput output : tx.getOutputs()) { try { @@ -887,6 +911,7 @@ private JsonObject getResponse(String method, JsonArray params) { vout.add(thisVout); } catch (Exception e) { + outputParseFailed = true; LOGGER.warning("[http-server-handler] ERROR: Error while parsing transaction outputs!"); LOGGER.warning("[http-server-handler] Error parsing transaction outputs for " + CoinTickerUtils.tickerToString(coin.getTicker()) + ", " + e.getMessage()); @@ -899,6 +924,11 @@ private JsonObject getResponse(String method, JsonArray params) { } } + // A throwing vout must surface as an error, not be silently + // dropped from a success-shaped response. + if (outputParseFailed) + break; + txJSON.add("vout", vout); response.add("result", txJSON); @@ -938,6 +968,7 @@ private JsonObject getResponse(String method, JsonArray params) { Transaction signedTx = new Transaction(coin.getNetworkParameters()); boolean complete = true; + boolean inputFailed = false; for (TransactionOutput output : tx.getOutputs()) { signedTx.addOutput(output); @@ -952,6 +983,7 @@ private JsonObject getResponse(String method, JsonArray params) { if (utxo == null || signingKey == null) { getInvalidTxResponse(response, new Exception("Transaction contains an utxo/input which does not exist in our wallet.")); + inputFailed = true; break; } @@ -963,6 +995,13 @@ private JsonObject getResponse(String method, JsonArray params) { // utxo.setSpent(true); } + if (inputFailed) { + // An error response is already set — never hand back a + // partially signed hex claiming complete:true. + LOGGER.warning("[http-server-handler] signrawtransaction aborted with unsigned inputs for " + CoinTickerUtils.tickerToString(coin.getTicker())); + break; + } + String signedTxHex = new String(Hex.encode(signedTx.bitcoinSerialize())); JsonObject resultJSON = new JsonObject(); resultJSON.addProperty("hex", signedTxHex); @@ -1427,11 +1466,27 @@ private JsonObject getResponse(String method, JsonArray params) { boolean isP2SH = false; String scriptPubKey = ""; if (isValidAddress) { - LegacyAddress toAddress = LegacyAddress.fromBase58(coin.getNetworkParameters(), address); - if (isP2SHAddress(address)) { + Address parsed; + try { + parsed = LegacyAddress.fromBase58(coin.getNetworkParameters(), address); + } catch (AddressFormatException e) { + // Bech32/segwit: derive the witness scriptPubKey + // (OP_ ) instead of crashing — + // isValidAddress legitimately accepted this address. + SegwitAddress segwit = SegwitAddress.fromBech32(coin.getNetworkParameters(), address); + byte[] program = segwit.getWitnessProgram(); + int version = segwit.getWitnessVersion(); + String opVersion = (version == 0) ? "00" + : Integer.toHexString(0x50 + version); + String pushOpCode = String.format("%02x", program.length); + scriptPubKey = opVersion + pushOpCode + + new String(Hex.encode(program)); + parsed = null; + } + if (parsed != null && isP2SHAddress(address)) { isP2SH = true; - TransactionOutput output = new TransactionOutput(coin.getNetworkParameters(), null, Coin.valueOf(0), toAddress); + TransactionOutput output = new TransactionOutput(coin.getNetworkParameters(), null, Coin.valueOf(0), parsed); scriptPubKey = new String(Hex.encode(output.getScriptPubKey().getProgram())); } } diff --git a/src/main/java/io/cloudchains/app/util/Utility.java b/src/main/java/io/cloudchains/app/util/Utility.java index 317fcea..20ec1cf 100644 --- a/src/main/java/io/cloudchains/app/util/Utility.java +++ b/src/main/java/io/cloudchains/app/util/Utility.java @@ -3,12 +3,23 @@ import org.bitcoinj.core.AddressFormatException; import org.bitcoinj.core.LegacyAddress; import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.SegwitAddress; public class Utility { public static boolean isValidAddress(NetworkParameters params, String address) { + if (address == null || address.isEmpty()) + return false; + try { LegacyAddress.fromBase58(params, address); return true; + } catch (AddressFormatException ignored) { + // fall through to segwit check + } + + try { + SegwitAddress.fromBech32(params, address); + return true; } catch (AddressFormatException e) { return false; } diff --git a/src/main/java/io/cloudchains/app/wallet/WalletHelper.java b/src/main/java/io/cloudchains/app/wallet/WalletHelper.java index 253e349..64496fb 100644 --- a/src/main/java/io/cloudchains/app/wallet/WalletHelper.java +++ b/src/main/java/io/cloudchains/app/wallet/WalletHelper.java @@ -305,7 +305,10 @@ private static double fundTransaction(ArrayList allUtxos, ArrayList if (totalSelected >= required) { return totalSelected - required; } - return 0.0; + // Shortfall must fail like the multi-input path — returning + // change 0.0 here used to build a deterministically invalid + // transaction (recipient output exceeding the lone input). + throw new RuntimeException("Not enough funds"); } UTXO largestUtxo = allUtxos.get(allUtxos.size() - 1); From cc748995917ebf9d9e76a6f542ae336949d07320 Mon Sep 17 00:00:00 2001 From: tryiou Date: Tue, 25 Aug 2026 00:10:33 +0200 Subject: [PATCH 51/73] feat: restore EXR data plane with verbatim response passthrough MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The EXR server pool was fully built and probed but never carried any traffic: every fetch called the BASE_URL helpers directly, so --exr-endpoint configured a capability-probing pool that nothing consumed. All 12 fetch sites (11 POST / + GET /height) now route through executeRequest(), which uses the coin-aware EXR path when a pool is configured and falls through to BASE_URL otherwise — default mode is byte-identical. Response handling is rewritten around verbatim passthrough: upstream payload shapes (JSON-RPC style envelopes, bare utxo objects, bare history arrays) are preserved exactly; a JSON text delivered as a string-typed result member is unwrapped exactly once; scalar string results stay wrapped. The old code stripped the result member blindly, which broke envelope-consuming callers. Envelope methods parse through a shared guard that turns upstream errors and empty results into null plus a logged reason instead of NPEs on JsonNull. EXRWrapper returns parsed elements without synthetic wrapping; EXRServer/probeCapabilities adapted. Routing failures keep the hard no-fallback contract: no supporting server or unprobed capabilities yield null, never a silent BASE_URL call. Unit tests: normalizer matrix (envelope/bare/array/string-unwrap/ error passthrough), envelope parser matrix, coin extraction, shouldUseEXR endpoint gating. --- .../app/net/api/http/client/EXRServer.java | 45 +-- .../app/net/api/http/client/EXRWrapper.java | 35 ++- .../app/net/api/http/client/HTTPClient.java | 281 ++++++++++-------- .../http/client/EXRResponseNormalizeTest.java | 212 +++++++++++++ .../http/client/HTTPClientRoutingTest.java | 54 ++++ 5 files changed, 467 insertions(+), 160 deletions(-) create mode 100644 src/test/java/io/cloudchains/app/net/api/http/client/EXRResponseNormalizeTest.java create mode 100644 src/test/java/io/cloudchains/app/net/api/http/client/HTTPClientRoutingTest.java diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/EXRServer.java b/src/main/java/io/cloudchains/app/net/api/http/client/EXRServer.java index 9d70565..9c83319 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/client/EXRServer.java +++ b/src/main/java/io/cloudchains/app/net/api/http/client/EXRServer.java @@ -53,22 +53,29 @@ public boolean probeCapabilities() { return false; } try { - JsonObject result = wrapper.executeGet("heights"); - if (result != null && result.has("result")) { - JsonObject heights = result.getAsJsonObject("result"); - for (String coinName : heights.keySet()) { - JsonElement heightValue = heights.get(coinName); - if (heightValue.isJsonNull()) { - continue; - } - try { - CoinTicker coin = CoinTickerUtils.stringToTicker(coinName); - if (coin != null) { - supportedCoins.add(coin); - } - } catch (Exception e) { - LOGGER.finer("[exr-server] Failed to map coin " + coinName + ", " + e.getMessage()); + JsonElement root = wrapper.executeGet("heights"); + if (root == null || !root.isJsonObject()) { + // Unusable body — leave capabilitiesProbed false so the + // next probe cycle retries instead of pinning an empty set. + return false; + } + JsonObject result = root.getAsJsonObject(); + if (!result.has("result") || !result.get("result").isJsonObject()) { + return false; + } + JsonObject heights = result.getAsJsonObject("result"); + for (String coinName : heights.keySet()) { + JsonElement heightValue = heights.get(coinName); + if (heightValue.isJsonNull()) { + continue; + } + try { + CoinTicker coin = CoinTickerUtils.stringToTicker(coinName); + if (coin != null) { + supportedCoins.add(coin); } + } catch (Exception e) { + LOGGER.finer("[exr-server] Failed to map coin " + coinName + ", " + e.getMessage()); } } @@ -76,7 +83,7 @@ public boolean probeCapabilities() { LOGGER.info("[exr-server] Probed capabilities for " + endpoint + ", supports: " + supportedCoins.size() + " coins: " + supportedCoins.stream().map(CoinTickerUtils::tickerToString) .reduce((a, b) -> a + ", " + b).orElse("none")); - return !supportedCoins.isEmpty(); + return true; } catch (Exception e) { LOGGER.warning("[exr-server] Failed to probe capabilities for " + endpoint + ", " + e.getMessage()); return false; @@ -90,7 +97,7 @@ public boolean isHealthy() { return healthy; } try { - JsonObject result = wrapper.executeGet("heights"); + JsonElement result = wrapper.executeGet("heights"); healthy = result != null && !result.isJsonNull(); } catch (Exception e) { healthy = false; @@ -100,7 +107,7 @@ public boolean isHealthy() { return healthy; } - public JsonObject execute(String method, List params) { + public JsonElement execute(String method, List params) { if (!isHealthy()) { return null; } @@ -109,7 +116,7 @@ public JsonObject execute(String method, List params) { return wrapper.execute(method, params); } - public JsonObject executeGet(String method) { + public JsonElement executeGet(String method) { return isHealthy() ? wrapper.executeGet(method) : null; } diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/EXRWrapper.java b/src/main/java/io/cloudchains/app/net/api/http/client/EXRWrapper.java index 1fe2f5f..7250873 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/client/EXRWrapper.java +++ b/src/main/java/io/cloudchains/app/net/api/http/client/EXRWrapper.java @@ -2,7 +2,7 @@ import com.google.gson.Gson; import com.google.gson.JsonElement; -import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; @@ -52,19 +52,18 @@ private String executeHttpRequest(HttpRequestBase request, String operation) { } /** - * Process response JSON and handle wrapping for different response types. + * Parse a response body verbatim. No synthetic wrapping is applied: + * arrays and primitives are returned as-is so upstream payload shapes + * survive unchanged (callers normalize where needed). * @param responseBody The raw response body - * @return Processed JsonObject with proper wrapping + * @return Parsed JsonElement or null if the body was not valid JSON */ - private JsonObject processResponse(String responseBody) { - JsonElement responseElement = gson.fromJson(responseBody, JsonElement.class); - if (responseElement.isJsonObject()) { - return responseElement.getAsJsonObject(); - } else { - // Wrap non-objects (arrays, primitives) in a result field - JsonObject wrapperObj = new JsonObject(); - wrapperObj.add("result", responseElement); - return wrapperObj; + private JsonElement parseBody(String responseBody) { + try { + return gson.fromJson(responseBody, JsonElement.class); + } catch (JsonSyntaxException e) { + LOGGER.warning(LOG_TAG + " invalid JSON response - " + e.getMessage()); + return null; } } @@ -74,9 +73,9 @@ private JsonObject processResponse(String responseBody) { * * @param method The method name (e.g., "getblockhash") * @param params The parameters as a List of Objects - * @return JsonObject response or null on error + * @return Parsed response element or null on error */ - public JsonObject execute(String method, List params) { + public JsonElement execute(String method, List params) { String endpoint = exrEndpoint + "/xrs/" + method; String currency = params.isEmpty() || !(params.get(0) instanceof String) ? "unknown" : (String) params.get(0); @@ -87,7 +86,7 @@ public JsonObject execute(String method, List params) { try { httpPost.setEntity(new StringEntity(requestBody)); String responseBody = executeHttpRequest(httpPost, "execute POST for " + method + " " + currency); - return responseBody != null ? processResponse(responseBody) : null; + return responseBody != null ? parseBody(responseBody) : null; } catch (IOException e) { LOGGER.warning(LOG_TAG + " execute POST failed for " + method + " " + currency + " endpoint: " + endpoint + ", " + e.getMessage()); return null; @@ -100,14 +99,14 @@ public JsonObject execute(String method, List params) { * Execute a GET request to an EXR endpoint. * * @param method The method name (e.g., "fees", "heights") - * @return JsonObject response or null on error + * @return Parsed response element or null on error */ - public JsonObject executeGet(String method) { + public JsonElement executeGet(String method) { String endpoint = exrEndpoint + "/xrs/" + method; HttpGet httpGet = new HttpGet(endpoint); httpGet.setHeader("Content-Type", "application/json"); String responseBody = executeHttpRequest(httpGet, "execute GET for method " + method); - return responseBody != null ? processResponse(responseBody) : null; + return responseBody != null ? parseBody(responseBody) : null; } /** diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java b/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java index 7f3a09a..fb9c487 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java +++ b/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java @@ -5,7 +5,9 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSyntaxException; import com.google.gson.reflect.TypeToken; import com.subgraph.orchid.encoders.Hex; import io.cloudchains.app.App; @@ -94,7 +96,7 @@ private boolean waitForCapabilities(int timeoutMs) { * Helper method to check if EXR pool is available and configured. * @return true if EXR pool is configured, false otherwise */ - private boolean useEXR() { + boolean useEXR() { return App.exrServerPool != null && App.exrServerPool.getServerCount() > 0; } @@ -103,7 +105,7 @@ private boolean useEXR() { * @param endpoint The API endpoint * @return true if EXR should be used, false otherwise */ - private boolean shouldUseEXR(String endpoint) { + boolean shouldUseEXR(String endpoint) { return useEXR() && (endpoint.equals("/fees") || endpoint.equals("/height") || endpoint.equals("/")); @@ -231,20 +233,19 @@ private String aggregateEXRResponse(String method) { } try { - JsonObject result = server.executeGet(method); - if (result != null && result.has("result")) { - JsonElement serverResult = result.get("result"); - - if (serverResult.isJsonObject()) { - JsonObject serverObj = serverResult.getAsJsonObject(); + JsonElement response = server.executeGet(method); + if (response != null && response.isJsonObject()) { + JsonObject result = response.getAsJsonObject(); + if (result.has("result") && result.get("result").isJsonObject()) { + JsonObject serverObj = result.getAsJsonObject("result"); for (String key : serverObj.keySet()) { if (!aggregatedResult.has(key)) { aggregatedResult.add(key, serverObj.get(key)); } } } - server.probeCapabilities(); } + server.probeCapabilities(); } catch (Exception e) { aggregatedErrors.add("Failed " + method + " from " + server.getEndpoint()); } @@ -267,97 +268,128 @@ private String executeEXRGet(String endpoint) { return aggregateEXRResponse(method); } + /** + * Extract the coin ticker from the first parameter of an EXR request. + * @param exrParams The parameter array + * @return Ticker or null if the first parameter is not a known coin + */ + static CoinTicker extractCoin(JsonArray exrParams) { + if (exrParams.size() == 0 || !exrParams.get(0).isJsonPrimitive() + || !exrParams.get(0).getAsJsonPrimitive().isString()) { + return null; + } + return CoinTickerUtils.stringToTicker(exrParams.get(0).getAsString()); + } + /** * Execute POST request with EXR coin-aware routing * @param endpoint The endpoint to POST to * @param params The parameters to POST - * @return Response from appropriate EXR server + * @return Response from appropriate EXR server, or null (FAIL - NO FALLBACK TO BASE_URL) */ private String executeEXRPost(String endpoint, JsonObject params) { - if (params.has("method") && params.has("params")) { - String method = params.get("method").getAsString(); - JsonArray exrParams = params.getAsJsonArray("params"); - - // Extract coin from first parameter - CoinTicker coin = null; - if (exrParams.size() > 0) { - String coinString = exrParams.get(0).getAsString(); - coin = CoinTickerUtils.stringToTicker(coinString); - if (coin == null) { - // Log the failed coin extraction for debugging - LOGGER.warning("[httpclient] Failed to extract coin from parameter: " + coinString); - // Not a coin-specific request - } - } + if (!params.has("method") || !params.has("params")) { + return null; + } + String method = params.get("method").getAsString(); + JsonArray exrParams = params.getAsJsonArray("params"); - EXRServer server = null; - - // Route ONLY to EXR servers that support this coin - if (coin != null) { - // Wait for capabilities to be probed if not already done - if (!App.exrServerPool.isCapabilitiesProbed()) { - LOGGER.fine("[httpclient] Waiting for EXR capabilities to be probed for coin: " + - CoinTickerUtils.tickerToString(coin)); - if (!waitForCapabilities(HttpClientConfig.CAPABILITY_PROBE_WAIT_TIMEOUT_MS)) { // Wait up to 10 seconds - LOGGER.warning("[httpclient] EXR capabilities not probed yet for coin: " + - CoinTickerUtils.tickerToString(coin)); - return null; // FAIL - NO FALLBACK TO BASE_URL - } - } - if (App.exrServerPool.isCapabilitiesProbed()) { - server = App.exrServerPool.selectServerForCoin(coin); - // LOGGER.info("[httpclient] DEBUG: selectServerForCoin returned: " + - // (server != null ? server.getEndpoint() : "null")); - - if (server == null) { - LOGGER.warning("[httpclient] NO EXR SERVER SUPPORTS COIN: " + - CoinTickerUtils.tickerToString(coin)); - return null; // FAIL - NO FALLBACK TO BASE_URL - } else { - LOGGER.info("[httpclient] DEBUG: Selected server " + server.getEndpoint() + - " for coin " + CoinTickerUtils.tickerToString(coin) + - ", method: " + method); - } - } else { - // Capabilities still not probed after waiting - LOGGER.warning("[httpclient] EXR capabilities not probed yet for coin: " + - CoinTickerUtils.tickerToString(coin)); - return null; // FAIL - NO FALLBACK TO BASE_URL - } - } else { - // Use round-robin for non-coin-specific requests - // But if we have a coin, we MUST use coin-aware selection - if (coin != null) { - server = App.exrServerPool.selectServerForCoin(coin); - if (server == null) { - LOGGER.warning("[httpclient] NO EXR SERVER SUPPORTS COIN: " + - CoinTickerUtils.tickerToString(coin)); - return null; // FAIL - NO FALLBACK TO BASE_URL - } - } else { - // No coin extracted - this should not happen for coin-specific requests - LOGGER.severe("[httpclient] Cannot route request: coin extraction failed"); - return null; // FAIL instead of using wrong server - } - } + CoinTicker coin = extractCoin(exrParams); + if (coin == null) { + LOGGER.severe("[httpclient] Cannot route request: no coin extracted from first parameter for method " + method); + return null; // FAIL instead of using wrong server + } - if (server != null) { - List paramList = convertParams(exrParams); - JsonObject result = server.execute(method, paramList); - if (result != null) { - // Handle wrapped responses from EXR wrapper - // If the result has a "result" field, extract it to maintain backward compatibility - if (result.has("result")) { - JsonElement resultElement = result.get("result"); - if (!resultElement.isJsonNull()) { - return resultElement.toString(); - } + if (!App.exrServerPool.isCapabilitiesProbed() + && !waitForCapabilities(HttpClientConfig.CAPABILITY_PROBE_WAIT_TIMEOUT_MS)) { + LOGGER.warning("[httpclient] EXR capabilities not probed yet for coin: " + + CoinTickerUtils.tickerToString(coin)); + return null; // FAIL - NO FALLBACK TO BASE_URL + } + + EXRServer server = App.exrServerPool.selectServerForCoin(coin); + if (server == null) { + LOGGER.warning("[httpclient] NO EXR SERVER SUPPORTS COIN: " + + CoinTickerUtils.tickerToString(coin)); + return null; // FAIL - NO FALLBACK TO BASE_URL + } + LOGGER.info("[httpclient] Routed " + method + " " + + CoinTickerUtils.tickerToString(coin) + " to " + server.getEndpoint()); + + List paramList = convertParams(exrParams); + JsonElement response = server.execute(method, paramList); + if (response == null) { + LOGGER.warning("[httpclient] EXR request failed for " + method + " via " + server.getEndpoint()); + return null; + } + return normalizeEXRResponse(response); + } + + /** + * Normalize a parsed EXR response body into the text form consumers expect. + * Upstream payload shapes are preserved verbatim: JSON-RPC style envelopes, + * bare objects and bare arrays all pass through unchanged. A JSON text + * delivered as a string-typed {@code result} member is unwrapped exactly + * once; scalar string results (e.g. broadcast txids) are kept wrapped. + * @param response Parsed response element + * @return Response text or null if the element carried no content + */ + static String normalizeEXRResponse(JsonElement response) { + if (response == null || response.isJsonNull()) { + return null; + } + if (response.isJsonObject()) { + JsonObject obj = response.getAsJsonObject(); + if (obj.has("result") && obj.get("result").isJsonPrimitive() + && obj.get("result").getAsJsonPrimitive().isString()) { + try { + JsonElement inner = JsonParser.parseString(obj.get("result").getAsString()); + if (inner.isJsonObject() || inner.isJsonArray()) { + return inner.toString(); } - return result.toString(); + } catch (JsonSyntaxException e) { + // Not JSON text — pass the envelope through unchanged } } + return obj.toString(); + } + return response.toString(); + } + + /** + * Parse an upstream JSON-RPC style envelope ({@code {"result":…,"error":…}}), + * returning null (with a logged reason) when the payload is absent, empty, + * an error, malformed, or not an object. Consumers receive the full envelope + * so they can read the {@code result} member exactly as with the legacy backend. + * @param res Raw response text + * @param opName Operation description for log messages + * @return Envelope object or null on failure + */ + static JsonObject parseEnvelope(String res, String opName) { + if (res == null) { + return null; + } + JsonElement parsed; + try { + parsed = JsonParser.parseString(res); + } catch (JsonSyntaxException e) { + LOGGER.warning("[httpclient] " + opName + " invalid upstream JSON - " + e.getMessage()); + return null; + } + if (!parsed.isJsonObject()) { + LOGGER.warning("[httpclient] " + opName + " unexpected upstream payload shape"); + return null; + } + JsonObject obj = parsed.getAsJsonObject(); + if (obj.has("error") && !obj.get("error").isJsonNull()) { + LOGGER.warning("[httpclient] " + opName + " upstream error - " + obj.get("error").toString()); + return null; + } + if (!obj.has("result") || obj.get("result").isJsonNull()) { + LOGGER.warning("[httpclient] " + opName + " empty upstream result"); + return null; } - return null; + return obj; } /** @@ -366,7 +398,7 @@ private String executeEXRPost(String endpoint, JsonObject params) { * @param params Parameters for POST requests, null for GET * @return Response string or null on error */ - private String executeRequest(String endpoint, JsonObject params) { + String executeRequest(String endpoint, JsonObject params) { // When EXR is configured, ONLY use EXR - NO fallback to BASE_URL if (shouldUseEXR(endpoint)) { if (params == null) { @@ -410,7 +442,7 @@ public JsonArray getUtxosUncached(CoinTicker coinTicker, String[] addresses) { JsonObject params = new JsonObject(); params.addProperty("method", "getutxos"); params.add("params", innerParams); - String res = executePostRequest("/", params); + String res = executeRequest("/", params); LOGGER.finer("[httpclient] getUtxosUncached " + coinInstance.getTicker() + " " + res); @@ -489,7 +521,7 @@ public JsonArray getUtxos(CoinTicker coinTicker, int expiry) { JsonObject params = new JsonObject(); params.addProperty("method", "getutxos"); params.add("params", innerParams); - String res = executePostRequest("/", params); + String res = executeRequest("/", params); LOGGER.finer("[httpclient] getUtxos " + coinInstance.getTicker() + " " + res); @@ -545,13 +577,11 @@ public JsonObject getRawTransaction(CoinTicker coinTicker, String txid, boolean JsonObject params = new JsonObject(); params.addProperty("method", "getrawtransaction"); params.add("params", innerParams); - String res = executePostRequest("/", params); + String res = executeRequest("/", params); LOGGER.finer("[httpclient] getRawTransaction " + res); - if (res == null) return null; - - return new Gson().fromJson(res, JsonObject.class); + return parseEnvelope(res, "getRawTransaction"); } public JsonObject getRawMempool(CoinTicker coinTicker, boolean verbose) { @@ -564,13 +594,11 @@ public JsonObject getRawMempool(CoinTicker coinTicker, boolean verbose) { JsonObject params = new JsonObject(); params.addProperty("method", "getrawmempool"); params.add("params", innerParams); - String res = executePostRequest("/", params); + String res = executeRequest("/", params); LOGGER.finer("[httpclient] getRawMempool " + res); - if (res == null) return null; - - return new Gson().fromJson(res, JsonObject.class); + return parseEnvelope(res, "getRawMempool"); } public void getBlockCount(CoinTicker coinTicker) { @@ -584,11 +612,11 @@ public void getBlockCount(CoinTicker coinTicker) { params.addProperty("method", "getblockcount"); params.add("params", innerParams); - String res = executePostRequest("/", params); + String res = executeRequest("/", params); - if (res == null) return; + JsonObject result = parseEnvelope(res, "getBlockCount " + coinTicker); + if (result == null) return; - JsonObject result = new Gson().fromJson(res, JsonObject.class); int blockCount = result.get("result").getAsInt(); coinInstance.addBlockCount(coinTicker, blockCount); @@ -597,7 +625,7 @@ public void getBlockCount(CoinTicker coinTicker) { } public void getAllBlockCounts() { - String res = executeGetRequest("/height"); + String res = executeRequest("/height", null); if (res == null) return; @@ -632,13 +660,11 @@ public JsonObject getBlock(CoinTicker coinTicker, String hash, boolean verbose) JsonObject params = new JsonObject(); params.addProperty("method", "getblock"); params.add("params", innerParams); - String res = executePostRequest("/", params); + String res = executeRequest("/", params); LOGGER.finer("[httpclient] getBlock " + res); - if (res == null) return null; - - return new Gson().fromJson(res, JsonObject.class); + return parseEnvelope(res, "getBlock"); } public JsonObject getBlockHash(CoinTicker coinTicker, int height) { @@ -649,13 +675,11 @@ public JsonObject getBlockHash(CoinTicker coinTicker, int height) { JsonObject params = new JsonObject(); params.addProperty("method", "getblockhash"); params.add("params", innerParams); - String res = executePostRequest("/", params); + String res = executeRequest("/", params); LOGGER.finer("[httpclient] getBlockHash " + res); - if (res == null) return null; - - return new Gson().fromJson(res, JsonObject.class); + return parseEnvelope(res, "getBlockHash"); } public JsonObject getTransaction(CoinTicker coinTicker, String txid, boolean verbose) { @@ -669,13 +693,11 @@ public JsonObject getTransaction(CoinTicker coinTicker, String txid, boolean ver JsonObject params = new JsonObject(); params.addProperty("method", "gettransaction"); params.add("params", innerParams); - String res = executePostRequest("/", params); + String res = executeRequest("/", params); LOGGER.finer("[httpclient] getTransaction " + res); - if (res == null) return null; - - return new Gson().fromJson(res, JsonObject.class); + return parseEnvelope(res, "getTransaction"); } public JsonObject sendRawTransaction(CoinTicker coinTicker, String rawTx) { @@ -688,13 +710,20 @@ public JsonObject sendRawTransaction(CoinTicker coinTicker, String rawTx) { JsonObject params = new JsonObject(); params.addProperty("method", "sendrawtransaction"); params.add("params", innerParams); - String res = executePostRequest("/", params); + String res = executeRequest("/", params); LOGGER.finer("[httpclient] sendRawTransaction " + res); - - if (res == null) return null; - - return new Gson().fromJson(res, JsonObject.class); + // Full envelope passthrough (including upstream error member) — the + // handler extracts structured {code,message} details for the GUI. + if (res == null) { + return null; + } + try { + return JsonParser.parseString(res).getAsJsonObject(); + } catch (JsonSyntaxException | IllegalStateException e) { + LOGGER.warning("[httpclient] sendRawTransaction invalid upstream JSON - " + e.getMessage()); + return null; + } } /** @@ -724,7 +753,7 @@ public JsonArray getHistory(CoinTicker coinTicker, int startTime, int endTime, i params.addProperty("method", "gethistory"); params.add("params", innerParams); - String res = executePostRequest("/", params); + String res = executeRequest("/", params); LOGGER.finer("[httpclient] getHistory " + coinInstance.getTicker() + " " + res); if (res == null) { LOGGER.warning("[httpclient] getHistory " + coinInstance.getTicker() + " null post result"); @@ -811,14 +840,20 @@ public JsonArray getTransactionHistory(CoinTicker coinTicker, int startTime, int params.addProperty("method", "getaddresshistory"); params.add("params", innerParams); - String res = executePostRequest("/", params); + String res = executeRequest("/", params); LOGGER.finer("[httpclient] getAddressHistory " + coinInstance.getTicker() + " " + res); if (res == null) { LOGGER.warning("[httpclient] getAddressHistory " + coinInstance.getTicker() + " null post result"); return null; } - JsonArray json = new Gson().fromJson(res, JsonArray.class); + JsonArray json; + try { + json = new Gson().fromJson(res, JsonArray.class); + } catch (Exception e) { + LOGGER.warning("[httpclient] getAddressHistory parsing error - Response: " + res + " - " + e.getMessage()); + return null; + } if (json == null) { LOGGER.warning("[httpclient] getAddressHistory " + coinInstance.getTicker() + " null json"); return null; diff --git a/src/test/java/io/cloudchains/app/net/api/http/client/EXRResponseNormalizeTest.java b/src/test/java/io/cloudchains/app/net/api/http/client/EXRResponseNormalizeTest.java new file mode 100644 index 0000000..37cfdc3 --- /dev/null +++ b/src/test/java/io/cloudchains/app/net/api/http/client/EXRResponseNormalizeTest.java @@ -0,0 +1,212 @@ +package io.cloudchains.app.net.api.http.client; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import io.cloudchains.app.net.CoinTicker; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class EXRResponseNormalizeTest { + + // --- normalizeEXRResponse --- + + @Test + public void testNormalize_EnvelopePassthrough() { + String body = "{\"result\":{\"txid\":\"aa\",\"confirmations\":3},\"error\":null}"; + JsonElement parsed = JsonParser.parseString(body); + assertEquals(body, HTTPClient.normalizeEXRResponse(parsed)); + } + + @Test + public void testNormalize_BareUtxosObjectPassthrough() { + String body = "{\"utxos\":[{\"txhash\":\"ab\",\"vout\":1,\"value\":1.5,\"address\":\"X\",\"block_number\":100}]}"; + JsonElement parsed = JsonParser.parseString(body); + assertEquals(body, HTTPClient.normalizeEXRResponse(parsed)); + } + + @Test + public void testNormalize_BareHistoryArrayPassthrough() { + String body = "[[{\"address\":\"X\",\"txid\":\"t1\",\"amount\":0.5}]]"; + JsonElement parsed = JsonParser.parseString(body); + assertEquals(body, HTTPClient.normalizeEXRResponse(parsed)); + } + + @Test + public void testNormalize_StringWrappedObjectResultUnwrappedOnce() { + JsonObject obj = new JsonObject(); + obj.addProperty("result", "{\"utxos\":[]}"); + assertEquals("{\"utxos\":[]}", HTTPClient.normalizeEXRResponse(obj)); + } + + @Test + public void testNormalize_StringWrappedArrayResultUnwrappedOnce() { + JsonObject obj = new JsonObject(); + obj.addProperty("result", "[[{\"address\":\"X\"}]]"); + assertEquals("[[{\"address\":\"X\"}]]", HTTPClient.normalizeEXRResponse(obj)); + } + + @Test + public void testNormalize_ScalarStringResultKeptWrapped() { + String body = "{\"result\":\"abcdef0123\",\"error\":null}"; + JsonElement parsed = JsonParser.parseString(body); + assertEquals(body, HTTPClient.normalizeEXRResponse(parsed)); + } + + @Test + public void testNormalize_ErrorEnvelopePassthrough() { + String body = "{\"result\":null,\"error\":{\"code\":-32000,\"message\":\"boom\"}}"; + JsonElement parsed = JsonParser.parseString(body); + assertEquals(body, HTTPClient.normalizeEXRResponse(parsed)); + } + + @Test + public void testNormalize_NullAndJsonNullYieldNull() { + assertNull(HTTPClient.normalizeEXRResponse(null)); + assertNull(HTTPClient.normalizeEXRResponse(JsonParser.parseString("null"))); + } + + @Test + public void testNormalize_NonJsonTextInResultFallsBackToPassthrough() { + JsonObject obj = new JsonObject(); + obj.addProperty("result", "not-json {"); + assertEquals(obj.toString(), HTTPClient.normalizeEXRResponse(obj)); + } + + // --- parseEnvelope --- + + @Test + public void testParseEnvelope_ValidEnvelopeReturnsObjectWithResult() { + JsonObject env = HTTPClient.parseEnvelope("{\"result\":5,\"error\":null}", "op"); + assertNotNull(env); + assertEquals(5, env.get("result").getAsInt()); + } + + @Test + public void testParseEnvelope_ScalarStringResultPreserved() { + JsonObject env = HTTPClient.parseEnvelope("{\"result\":\"\",\"error\":null}", "op"); + assertNotNull(env); + assertEquals("", env.get("result").getAsString()); + } + + @Test + public void testParseEnvelope_ObjectResultPreserved() { + JsonObject env = HTTPClient.parseEnvelope("{\"result\":{\"hex\":\"aa\"},\"error\":null}", "op"); + assertNotNull(env); + assertEquals("aa", env.getAsJsonObject("result").get("hex").getAsString()); + } + + @Test + public void testParseEnvelope_ErrorYieldsNull() { + assertNull(HTTPClient.parseEnvelope( + "{\"result\":null,\"error\":{\"code\":-25,\"message\":\"missing inputs\"}}", "op")); + } + + @Test + public void testParseEnvelope_NullResultYieldsNull() { + assertNull(HTTPClient.parseEnvelope("{\"result\":null,\"error\":null}", "op")); + } + + @Test + public void testParseEnvelope_MissingResultKeyYieldsNull() { + assertNull(HTTPClient.parseEnvelope("{\"foo\":1}", "op")); + } + + @Test + public void testParseEnvelope_NonObjectPayloadYieldsNull() { + assertNull(HTTPClient.parseEnvelope("[1,2]", "op")); + assertNull(HTTPClient.parseEnvelope("\"str\"", "op")); + } + + @Test + public void testParseEnvelope_MalformedJsonYieldsNull() { + assertNull(HTTPClient.parseEnvelope("{nope", "op")); + } + + @Test + public void testParseEnvelope_NullInputYieldsNull() { + assertNull(HTTPClient.parseEnvelope(null, "op")); + } + + // --- extractCoin --- + + @Test + public void testExtractCoin_ValidTicker() { + JsonArray params = JsonParser.parseString("[\"BLOCK\",[\"addr\"]]").getAsJsonArray(); + assertEquals(CoinTicker.BLOCKNET, HTTPClient.extractCoin(params)); + } + + @Test + public void testExtractCoin_UnknownTickerYieldsNull() { + JsonArray params = JsonParser.parseString("[\"NOPE\",[\"addr\"]]").getAsJsonArray(); + assertNull(HTTPClient.extractCoin(params)); + } + + @Test + public void testExtractCoin_NonStringFirstParamYieldsNull() { + JsonArray params = JsonParser.parseString("[42,\"x\"]").getAsJsonArray(); + assertNull(HTTPClient.extractCoin(params)); + } + + @Test + public void testExtractCoin_EmptyParamsYieldNull() { + assertNull(HTTPClient.extractCoin(new JsonArray())); + } + + // --- sendRawTransaction envelope passthrough --- + + static class CannedClient extends HTTPClient { + final String canned; + + CannedClient(String canned) { + super(2); + this.canned = canned; + } + + @Override + String executeRequest(String endpoint, JsonObject params) { + return canned; + } + } + + @Test + public void testSendRawTransaction_ErrorEnvelopePassedThroughIntact() { + HTTPClient client = new CannedClient( + "{\"result\":null,\"error\":{\"code\":-25,\"message\":\"missing inputs\"}}"); + JsonObject res = client.sendRawTransaction(CoinTicker.BLOCKNET, "deadbeef"); + assertNotNull(res); + assertTrue(res.has("error") && !res.get("error").isJsonNull()); + assertEquals(-25, res.getAsJsonObject("error").get("code").getAsInt()); + assertEquals("missing inputs", res.getAsJsonObject("error").get("message").getAsString()); + } + + @Test + public void testSendRawTransaction_LegacyIntErrorEnvelopePassedThrough() { + HTTPClient client = new CannedClient("{\"result\":null,\"error\":-4}"); + JsonObject res = client.sendRawTransaction(CoinTicker.BLOCKNET, "deadbeef"); + assertNotNull(res); + assertEquals(-4, res.get("error").getAsInt()); + } + + @Test + public void testSendRawTransaction_SuccessEnvelopePassedThrough() { + HTTPClient client = new CannedClient("{\"result\":\"abcdef\",\"error\":null}"); + JsonObject res = client.sendRawTransaction(CoinTicker.BLOCKNET, "deadbeef"); + assertNotNull(res); + assertEquals("abcdef", res.get("result").getAsString()); + } + + @Test + public void testSendRawTransaction_MalformedJsonYieldsNull() { + HTTPClient client = new CannedClient("{nope"); + assertNull(client.sendRawTransaction(CoinTicker.BLOCKNET, "deadbeef")); + } + + @Test + public void testSendRawTransaction_NullResponseYieldsNull() { + HTTPClient client = new CannedClient(null); + assertNull(client.sendRawTransaction(CoinTicker.BLOCKNET, "deadbeef")); + } +} diff --git a/src/test/java/io/cloudchains/app/net/api/http/client/HTTPClientRoutingTest.java b/src/test/java/io/cloudchains/app/net/api/http/client/HTTPClientRoutingTest.java new file mode 100644 index 0000000..d685711 --- /dev/null +++ b/src/test/java/io/cloudchains/app/net/api/http/client/HTTPClientRoutingTest.java @@ -0,0 +1,54 @@ +package io.cloudchains.app.net.api.http.client; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class HTTPClientRoutingTest { + + static class LegacyModeClient extends HTTPClient { + LegacyModeClient() { + super(2); + } + + @Override + boolean useEXR() { + return false; + } + } + + static class ExrModeClient extends HTTPClient { + ExrModeClient() { + super(2); + } + + @Override + boolean useEXR() { + return true; + } + } + + @Test + public void testShouldUseEXR_LegacyModeNeverRoutesToEXR() { + HTTPClient client = new LegacyModeClient(); + assertFalse(client.shouldUseEXR("/")); + assertFalse(client.shouldUseEXR("/height")); + assertFalse(client.shouldUseEXR("/fees")); + } + + @Test + public void testShouldUseEXR_ExrModeRoutesSupportedEndpoints() { + HTTPClient client = new ExrModeClient(); + assertTrue(client.shouldUseEXR("/")); + assertTrue(client.shouldUseEXR("/height")); + assertTrue(client.shouldUseEXR("/fees")); + } + + @Test + public void testShouldUseEXR_ExrModeIgnoresUnknownEndpoints() { + HTTPClient client = new ExrModeClient(); + assertFalse(client.shouldUseEXR("/other")); + assertFalse(client.shouldUseEXR("")); + } +} From 764ef500b373255c330bd2273bd4f2d85b03a78d Mon Sep 17 00:00:00 2001 From: tryiou Date: Tue, 25 Aug 2026 00:10:47 +0200 Subject: [PATCH 52/73] fix: gate reloadconfig rebind on kernel bindability; make RPC threads daemon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two independently root-caused shutdown/rebind defects: reloadconfig rebind always lost a race: Netty's closeFuture completes before the kernel releases the old LISTEN socket (~1s observed lag), so the immediate rebind failed with EADDRINUSE in every session while the still-open old listener kept serving, masking the failure. The controller now gates on actual bindability (probe binds on both wildcard families) and the server retries its own bind (<=20x250ms) for late releases. Lifecycle diagnostics log server state before and after the release wait; a failed rebind reports the new server's full state. Verified by socket-state timeline capture: fresh post-bind listening line per coin, zero rebind failures. JVM shutdown could hang past any grace period for two reasons: SIGINT was inherited as ignored through the spawn chain so shutdown hooks never ran at all (callers must use SIGTERM or another signal — hooks are proven to run and exit cleanly under TERM), and non-daemon Netty event-loop/servant threads stalled DestroyJavaVM after hooks. All RPC pools now use daemon thread factories, server/master threads are named daemons, initial-history fetches are daemonized, and the per-coin join after deinit is bounded at 10s with an escalation log. The shutdown hook logs entry and any failure before running cleanup. awaitBound treated only bound/bindFailed as terminal, so a shutdown racing a pending rebind burned its full 10s budget then stamped a spurious bind-failure SEVERE. Stop-requested is now a third terminal state: the wait exits promptly and truthfully on all three outcomes, while still riding its full budget when a rebind is genuinely pending (slow-but-successful late rebinds must not be misread as failures). Unit tests: bindability gate (free/held/timeout/release-under-wait); awaitBound prompt-return-when-stopping + rides-to-deadline-while- pending regression pins. Reviewed-notes (blind-audit dispositions, settled): - REJECTED/frozen: platform portability of the kernel-bindability probe; test wall-clock/TOCTOU sensitivity (CI runs -DskipTests); cumulative worst-case latency budgets (~18s/coin) vs fire-and-forget RPC replies; daemonize-everything scope expansion beyond RPC pools (timer host, EXR probes) — queued as follow-up audit item; JVM- liveness anchor concern (timer wrapper as sole non-daemon anchor) — invariant documented for future cleanup work. - ACCEPTED-as-is: gate is heuristic, not a guarantee (bounded probe + warning anticipates kernel-drift); retry/budget constants kept local to their classes deliberately; shutdown-hook exception swallowing is deliberate (hook must not mask primary exit path). --- src/main/java/io/cloudchains/app/App.java | 10 ++ .../io/cloudchains/app/net/CoinInstance.java | 28 ++- .../app/net/api/JSONRPCController.java | 23 ++- .../app/net/api/JSONRPCMasterServer.java | 6 +- .../app/net/api/JSONRPCServer.java | 160 +++++++++++++++++- .../background/BackgroundTimerThread.java | 8 +- .../net/api/JSONRPCServerPortGateTest.java | 92 ++++++++++ 7 files changed, 312 insertions(+), 15 deletions(-) create mode 100644 src/test/java/io/cloudchains/app/net/api/JSONRPCServerPortGateTest.java diff --git a/src/main/java/io/cloudchains/app/App.java b/src/main/java/io/cloudchains/app/App.java index 9494e1d..e2b9b5e 100644 --- a/src/main/java/io/cloudchains/app/App.java +++ b/src/main/java/io/cloudchains/app/App.java @@ -140,6 +140,16 @@ protected synchronized void setOutputStream(OutputStream out) throws SecurityExc } public static void shutdown() { + // First-line trace: proves hook entry even if anything below dies. + LOGGER.info("[shutdown] hook entered"); + try { + shutdownInner(); + } catch (Throwable t) { + LOGGER.log(Level.SEVERE, "[shutdown] hook failed", t); + } + } + + private static void shutdownInner() { if (masterRPC != null && masterRPC.isAlive()) { System.out.println("Shutting down..."); } diff --git a/src/main/java/io/cloudchains/app/net/CoinInstance.java b/src/main/java/io/cloudchains/app/net/CoinInstance.java index fbd2424..7053fc7 100644 --- a/src/main/java/io/cloudchains/app/net/CoinInstance.java +++ b/src/main/java/io/cloudchains/app/net/CoinInstance.java @@ -315,12 +315,27 @@ public void deinit() { } if (coinRPCServer != null) { + boolean interruptedDuringJoin = false; try { coinRPCServer.deinit(); - coinRPCServer.join(); + // Bounded: an untimeouted join here would stall the JVM + // shutdown hook indefinitely on a wedged server thread. + coinRPCServer.join(10_000); + } catch (InterruptedException e) { + // Preserve the flag and say so — a silent swallow here would + // hide shutdown-latency facts even when the server dies. + Thread.currentThread().interrupt(); + interruptedDuringJoin = true; + LOGGER.warning("[coin] deinit join interrupted for " + + CoinTickerUtils.tickerToString(ticker)); } catch (Exception e) { LOGGER.warning("[coin] Error deinitializing RPC server for " + CoinTickerUtils.tickerToString(ticker) + e.getMessage()); } + if (coinRPCServer.isAlive()) + LOGGER.warning("[coin] RPC server thread for " + + CoinTickerUtils.tickerToString(ticker) + + " still alive after deinit" + + (interruptedDuringJoin ? " (join was interrupted)" : "")); } } @@ -993,10 +1008,15 @@ public void reloadConfig() { coinRPCServer.start(); // Verify the rebind actually took — a silent bind failure used to - // leave the coin RPC dead while callers already got success. - if (!coinRPCServer.awaitBound(5000)) { + // leave the coin RPC dead while callers already got success. Budget + // exceeds the server's own 20x250ms retry window so a slow but + // successful late rebind cannot trip this alarm. + if (!coinRPCServer.awaitBound(10000)) { LOGGER.severe("[rpc] Failed to rebind JSON-RPC server for coin " - + CoinTickerUtils.tickerToString(getTicker()) + " on port " + getRPCPort() + " after reloadconfig"); + + CoinTickerUtils.tickerToString(getTicker()) + + " on port " + getRPCPort() + " after reloadconfig" + + "; new-server state: " + + coinRPCServer.lifecycleState()); } } diff --git a/src/main/java/io/cloudchains/app/net/api/JSONRPCController.java b/src/main/java/io/cloudchains/app/net/api/JSONRPCController.java index 150c22f..9a4fcfb 100644 --- a/src/main/java/io/cloudchains/app/net/api/JSONRPCController.java +++ b/src/main/java/io/cloudchains/app/net/api/JSONRPCController.java @@ -4,8 +4,12 @@ import io.cloudchains.app.util.ConfigHelper; import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.LogManager; +import java.util.logging.Logger; public class JSONRPCController { + private final static LogManager LOGMANAGER = LogManager.getLogManager(); + private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); private static final ConcurrentHashMap servers = new ConcurrentHashMap<>(); private static JSONRPCMasterServer masterServer = new JSONRPCMasterServer(new ConfigHelper("master").getMasterRpcPort()); @@ -32,12 +36,29 @@ public static void removeRPCServer(CoinInstance coinInstance) { if (server == null) return; + LOGGER.info("[rpc] rebind: retiring server " + server.lifecycleState()); if (server.isAlive()) server.deinit(); // Wait (bounded) for the old listener to release its socket so an - // immediate rebind on the same port cannot lose the race. + // immediate rebind on the same port cannot lose the race. Gate on + // real kernel bindability, not Netty close-completion — the fd can + // lag the closeFuture by ~1s. server.awaitPortRelease(3000); + if (server.port() == coinInstance.getRPCPort()) { + if (!server.awaitPortBindable(5000)) { + LOGGER.warning("[rpc] rebind: port " + server.port() + + " still not bindable after release-wait; retrying at bind"); + } + } else { + // Config changed the port — the old port's bindability is + // irrelevant to where we are about to bind. + LOGGER.finer("[rpc] rebind: RPC port changed " + + server.port() + " -> " + coinInstance.getRPCPort() + + ", skipping old-port bindability wait"); + } + LOGGER.info("[rpc] rebind: after release-wait " + + server.lifecycleState()); servers.remove(coinInstance); } diff --git a/src/main/java/io/cloudchains/app/net/api/JSONRPCMasterServer.java b/src/main/java/io/cloudchains/app/net/api/JSONRPCMasterServer.java index e46e4f9..d8a8160 100644 --- a/src/main/java/io/cloudchains/app/net/api/JSONRPCMasterServer.java +++ b/src/main/java/io/cloudchains/app/net/api/JSONRPCMasterServer.java @@ -8,6 +8,7 @@ import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.util.concurrent.DefaultThreadFactory; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -23,11 +24,14 @@ public class JSONRPCMasterServer extends Thread { private EventLoopGroup workerGroup; JSONRPCMasterServer(int port) { + setDaemon(true); + setName("rpc-master"); this.port = port; } public void run() { - workerGroup = new NioEventLoopGroup(2); + workerGroup = new NioEventLoopGroup(2, + new DefaultThreadFactory("rpc-master", true)); try { LOGGER.info("[rpc] Starting master RPC server on port " + port + "."); diff --git a/src/main/java/io/cloudchains/app/net/api/JSONRPCServer.java b/src/main/java/io/cloudchains/app/net/api/JSONRPCServer.java index 883f1bd..b0d8871 100644 --- a/src/main/java/io/cloudchains/app/net/api/JSONRPCServer.java +++ b/src/main/java/io/cloudchains/app/net/api/JSONRPCServer.java @@ -6,11 +6,17 @@ import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.PooledByteBufAllocator; import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.util.concurrent.DefaultThreadFactory; +import java.net.BindException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.ServerSocket; import java.util.concurrent.TimeUnit; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -21,7 +27,7 @@ public class JSONRPCServer extends Thread { private final CoinInstance coin; private final int port; - private boolean stopping = false; + private volatile boolean stopping = false; private volatile boolean bound = false; private volatile boolean bindFailed = false; @@ -31,23 +37,86 @@ public class JSONRPCServer extends Thread { JSONRPCServer(CoinInstance coin, int port) { this.coin = coin; this.port = port; + // Servant thread: must never block JVM termination on its own + // liveness; lifecycle is governed by deinit()+bounded join. + setDaemon(true); + setName("rpc-coin-" + CoinTickerUtils.tickerToString(coin.getTicker())); + } + + int port() { + return port; } public void run() { - workerGroup = new NioEventLoopGroup(5); + // Daemon threads: the JVM must never be held hostage by an event + // loop draining its graceful-shutdown quiet period after SIGINT + // (DestroyJavaVM waited on these for the full grace window). + workerGroup = new NioEventLoopGroup(5, + new DefaultThreadFactory( + "rpc-coin-" + CoinTickerUtils.tickerToString(coin.getTicker()), true)); try { ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(workerGroup) - .option(ChannelOption.SO_BACKLOG, 128) .option(ChannelOption.SO_REUSEADDR, true) + .option(ChannelOption.SO_BACKLOG, 128) .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) .channel(NioServerSocketChannel.class) .childHandler(new HTTPServerInitializer(coin)); - channel = bootstrap.bind(port).sync().channel(); + // Kernel release of the previous listener can lag Netty's + // closeFuture by up to ~1s (observed: closeDone=true while the + // old LISTEN entry persisted). Retry rather than fail hard. + Channel boundChannel = null; + Exception lastBindError = null; + for (int attempt = 0; attempt < 20 && boundChannel == null && !stopping; attempt++) { + ChannelFuture bindFuture = null; + try { + bindFuture = bootstrap.bind(port); + bindFuture.sync(); + boundChannel = bindFuture.channel(); + } catch (InterruptedException e) { + // Stop requested mid-retry — preserve the flag and bail + // out instead of laundering it into a bind failure. The + // bind may still complete on the event loop, so close + // whatever channel it produced. + Thread.currentThread().interrupt(); + lastBindError = e; + if (bindFuture != null) + bindFuture.channel().close(); + break; + } catch (Exception e) { + lastBindError = e; + if (!isCauseBindException(e)) { + throw e; + } + try { + Thread.sleep(250); + } catch (InterruptedException ie) { + // Thrown from inside this catch, so the sibling + // clause cannot intercept it — preserve the flag + // and bail out here instead of laundering it. + Thread.currentThread().interrupt(); + lastBindError = ie; + break; + } + } + } + if (boundChannel == null) { + if (stopping) { + // Stop requested while retries were pending — a normal + // shutdown, not a bind failure. Do not stamp bindFailed. + LOGGER.finer("[rpc-server] bind retries abandoned: stopping"); + return; + } + throw lastBindError != null ? lastBindError + : new IllegalStateException("bind failed without exception"); + } + channel = boundChannel; // Emitted only after the bind actually succeeded. - LOGGER.info("[rpc] RPC server listening for " + CoinTickerUtils.tickerToString(coin.getTicker()) + " on port " + port + "."); + LOGGER.info("[rpc] RPC server listening for " + + CoinTickerUtils.tickerToString(coin.getTicker()) + + " on port " + port + " at " + channel.localAddress() + "."); bound = true; channel.closeFuture().sync(); @@ -59,14 +128,26 @@ public void run() { } } + private static boolean isCauseBindException(Throwable e) { + Throwable t = e; + while (t != null) { + if (t instanceof BindException) { + return true; + } + t = t.getCause(); + } + return false; + } + /** - * Blocks until the listener has either bound successfully or failed. + * Blocks until the listener has bound successfully, failed outright, + * or stop was requested — whichever comes first. * @return true iff the port is bound and accepting. */ public boolean awaitBound(long timeoutMillis) { long deadline = System.currentTimeMillis() + timeoutMillis; while (System.currentTimeMillis() < deadline) { - if (bound || bindFailed) return bound; + if (bound || bindFailed || stopping) return bound; try { Thread.sleep(25); } catch (InterruptedException e) { @@ -86,6 +167,71 @@ public void awaitPortRelease(long timeoutMillis) { channel.closeFuture().awaitUninterruptibly(timeoutMillis, TimeUnit.MILLISECONDS); } + /** + * Poll ACTUAL kernel bindability of the port. Netty's closeFuture can + * complete while the old LISTEN socket is still being torn down + * (observed ~1s lag), so close-completion alone is not a safe gate for + * an immediate rebind. Succeeds as soon as a probe listener can take + * either wildcard family; gives up after the timeout. + * @return true if the port became bindable in time + */ + public boolean awaitPortBindable(long timeoutMillis) { + return awaitPortBindable(port, timeoutMillis); + } + + /** + * Static form usable without an instance (also unit-testable). + */ + static boolean awaitPortBindable(int port, long timeoutMillis) { + long deadline = System.currentTimeMillis() + timeoutMillis; + while (System.currentTimeMillis() < deadline) { + if (portBindable(port)) { + return true; + } + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + } + return portBindable(port); + } + + /** + * Probe whether a listening socket could be opened on this port right now. + */ + static boolean portBindable(int port) { + for (String host : new String[]{"0.0.0.0", "::"}) { + try (ServerSocket probe = new ServerSocket()) { + probe.setReuseAddress(true); + probe.bind(new InetSocketAddress( + InetAddress.getByName(host), port), 1); + return true; + } catch (Exception e) { + // probe failed (held or blocked) — try the next family + } + } + return false; + } + + /** + * One-line lifecycle summary for diagnostics: thread state, bind flags + * and the recorded channel's open/done/local-address state. + */ + public String lifecycleState() { + Channel ch = channel; + return "srv=" + System.identityHashCode(this) + + " alive=" + isAlive() + + " stopping=" + stopping + + " bound=" + bound + + " bindFailed=" + bindFailed + + " channel=" + (ch == null ? "null" + : "open=" + ch.isOpen() + + " closeDone=" + ch.closeFuture().isDone() + + " local=" + ch.localAddress()); + } + public void deinit() { stopping = true; LOGGER.finer("[json-rpc-server] Interrupting server."); diff --git a/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java b/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java index 8ebe4b2..0d6a4af 100644 --- a/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java +++ b/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java @@ -249,9 +249,13 @@ public void run() { if (!CoinTickerUtils.isActiveTicker(coinInstance.getTicker())) continue; - new Thread(() -> { + Thread historyThread = new Thread(() -> { App.feeUpdateHttpClient.getHistory(coinInstance.getTicker(), 0, (int) System.currentTimeMillis(), 30000); - }).start(); + }, "history-fetch-" + CoinTickerUtils.tickerToString(coinInstance.getTicker())); + // Daemon: a blocking initial-history fetch must never keep the + // JVM alive past shutdown. + historyThread.setDaemon(true); + historyThread.start(); } while (!Thread.currentThread().isInterrupted()) { diff --git a/src/test/java/io/cloudchains/app/net/api/JSONRPCServerPortGateTest.java b/src/test/java/io/cloudchains/app/net/api/JSONRPCServerPortGateTest.java new file mode 100644 index 0000000..16121c8 --- /dev/null +++ b/src/test/java/io/cloudchains/app/net/api/JSONRPCServerPortGateTest.java @@ -0,0 +1,92 @@ +package io.cloudchains.app.net.api; + +import io.cloudchains.app.net.CoinInstance; +import org.junit.jupiter.api.Test; + +import java.net.InetAddress; +import java.net.ServerSocket; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; + +public class JSONRPCServerPortGateTest { + + private static int freePort() throws Exception { + try (ServerSocket s = new ServerSocket(0)) { + return s.getLocalPort(); + } + } + + @Test + public void testPortBindable_TrueOnFreePort() throws Exception { + int port = freePort(); + assertTrue(JSONRPCServer.portBindable(port)); + } + + @Test + public void testPortBindable_FalseWhileHeld() throws Exception { + int port = freePort(); + try (ServerSocket holder = new ServerSocket(port, 1, + InetAddress.getByName("0.0.0.0"))) { + // A plain listener without SO_REUSEADDR must make the gated + // probe report the port as not bindable. + assertFalse(JSONRPCServer.portBindable(port)); + } + assertTrue(JSONRPCServer.portBindable(port)); + } + + @Test + public void testAwaitPortBindable_TimesOutWhileHeld() throws Exception { + int port = freePort(); + try (ServerSocket holder = new ServerSocket(port, 1, + InetAddress.getByName("0.0.0.0"))) { + long t0 = System.currentTimeMillis(); + assertFalse(JSONRPCServer.awaitPortBindable(port, 400)); + assertTrue(System.currentTimeMillis() - t0 >= 350); + } + } + + @Test + public void testAwaitPortBindable_ReturnsOnceReleased() throws Exception { + int port = freePort(); + try (ServerSocket holder = new ServerSocket(port, 1, + InetAddress.getByName("0.0.0.0"))) { + // Release after a short delay. + new Thread(() -> { + try { + Thread.sleep(250); + holder.close(); + } catch (Exception ignored) { + } + }).start(); + long t0 = System.currentTimeMillis(); + assertTrue(JSONRPCServer.awaitPortBindable(port, 3000)); + // Prompt-return proof: success lands well under 2s once released, + // while a gate that misses the release rides to its 3s deadline. + assertTrue(System.currentTimeMillis() - t0 < 2000); + } + } + + @Test + public void testAwaitBound_PromptReturnWhenStopping() throws Exception { + // A stop request is a terminal state for the wait: burning the full + // budget and stamping a bind failure on a benign shutdown race is + // neither prompt nor truthful. + JSONRPCServer server = new JSONRPCServer(mock(CoinInstance.class), freePort()); + server.deinit(); + long t0 = System.currentTimeMillis(); + assertFalse(server.awaitBound(5000)); + assertTrue(System.currentTimeMillis() - t0 < 1000); + } + + @Test + public void testAwaitBound_RidesToDeadlineWhilePending() throws Exception { + // While no terminal state is reached, the wait must keep its budget + // so a slow but successful rebind is not misread as a failure. + JSONRPCServer server = new JSONRPCServer(mock(CoinInstance.class), freePort()); + long t0 = System.currentTimeMillis(); + assertFalse(server.awaitBound(400)); + assertTrue(System.currentTimeMillis() - t0 >= 350); + } +} From a9bfbda3155b15d8f3faa0eb9cc75a48bb1b3ab8 Mon Sep 17 00:00:00 2001 From: tryiou Date: Tue, 25 Aug 2026 22:12:04 +0200 Subject: [PATCH 53/73] feat(config): honor XLITE_DATA_HOME as cross-platform data-root override Unify state-dir resolution behind one precedence chain so sandboxed GUI and daemon runs share a single override on every OS: - XLITE_DATA_HOME (non-blank, absolute-normalized) wins over the per-OS default (win %APPDATA% / mac ~/Library/Application Support / nix ~/.config) - extract pure resolver App.resolveUserConfigDir + precedence matrix unit tests - ConfigHelper.getLocalDataDirectory() now delegates to App.getUserConfigDir() instead of duplicating OS branches; CONFIG_DIR test hook keeps top priority Note: KeyHandlerTest.testGetBaseSeedWrongPassphraseReturnsNull fails in the working tree independent of this change (reproduces without it; introduced by separate in-progress work). --- src/main/java/io/cloudchains/app/App.java | 34 +++++++-- .../io/cloudchains/app/util/ConfigHelper.java | 9 +-- src/test/java/io/cloudchains/app/AppTest.java | 74 +++++++++++++++++++ 3 files changed, 102 insertions(+), 15 deletions(-) create mode 100644 src/test/java/io/cloudchains/app/AppTest.java diff --git a/src/main/java/io/cloudchains/app/App.java b/src/main/java/io/cloudchains/app/App.java index e2b9b5e..ffe6b02 100644 --- a/src/main/java/io/cloudchains/app/App.java +++ b/src/main/java/io/cloudchains/app/App.java @@ -48,16 +48,36 @@ public static String getEnv(String key) { return System.getenv(key); } - public static String getUserConfigDir() { - String OS = (System.getProperty("os.name")).toLowerCase(); + /** + * Resolves the platform application-data root shared by the XLite apps. + *

Precedence: the {@code XLITE_DATA_HOME} environment variable (when + * non-blank, normalized to an absolute path) over the per-OS default. + * Pure function so the precedence matrix stays unit-testable.

+ * + * @param dataHomeEnv value of {@code XLITE_DATA_HOME} (may be null) + * @param osName value of the {@code os.name} system property + * @param userHome value of the {@code user.home} system property + * @param appDataEnv Windows {@code AppData} environment value (may be null) + * @return the resolved config-root directory string + */ + public static String resolveUserConfigDir(String dataHomeEnv, String osName, String userHome, String appDataEnv) { + if (dataHomeEnv != null && !dataHomeEnv.trim().isEmpty()) { + return new File(dataHomeEnv.trim()).getAbsoluteFile().getPath(); + } + String OS = osName.toLowerCase(); if (OS.contains("win")) { - return getEnv("AppData"); - } else if (OS.contains("nix") || OS.contains("nux") || OS.contains("aix")) { - return System.getProperty("user.home") + File.separator + ".config"; + return appDataEnv; } else if (OS.contains("mac")) { - return System.getProperty("user.home") + File.separator + "Library" + File.separator + "Application Support"; + return userHome + File.separator + "Library" + File.separator + "Application Support"; } - return System.getProperty("user.home") + File.separator + ".config"; + return userHome + File.separator + ".config"; + } + + public static String getUserConfigDir() { + return resolveUserConfigDir(getEnv("XLITE_DATA_HOME"), + System.getProperty("os.name"), + System.getProperty("user.home"), + getEnv("AppData")); } private static Level parseLogLevel(String envValue, Level defaultLevel) { diff --git a/src/main/java/io/cloudchains/app/util/ConfigHelper.java b/src/main/java/io/cloudchains/app/util/ConfigHelper.java index 2100940..97dfe5f 100644 --- a/src/main/java/io/cloudchains/app/util/ConfigHelper.java +++ b/src/main/java/io/cloudchains/app/util/ConfigHelper.java @@ -275,14 +275,7 @@ public synchronized void writeConfig() { public static String getLocalDataDirectory() { String baseDir; if (CONFIG_DIR.isEmpty()) { - String os = System.getProperty("os.name").toLowerCase(); - if (os.contains("win")) { - baseDir = App.getEnv("AppData"); - } else if (os.contains("mac")) { - baseDir = System.getProperty("user.home") + File.separator + "Library" + File.separator + "Application Support"; - } else { - baseDir = System.getProperty("user.home") + File.separator + ".config"; - } + baseDir = App.getUserConfigDir(); } else { baseDir = CONFIG_DIR; } diff --git a/src/test/java/io/cloudchains/app/AppTest.java b/src/test/java/io/cloudchains/app/AppTest.java new file mode 100644 index 0000000..c77b0be --- /dev/null +++ b/src/test/java/io/cloudchains/app/AppTest.java @@ -0,0 +1,74 @@ +package io.cloudchains.app; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * Precedence matrix for {@link App#resolveUserConfigDir(String, String, String, String)}. + *

{@code XLITE_DATA_HOME} (non-blank) must win over every per-OS default; + * blank/null env must fall through to the platform root unchanged.

+ */ +class AppTest { + + private static final String USER_HOME = "/home/tester"; + private static final String APPDATA = "C:\\Users\\tester\\AppData\\Roaming"; + + @Test + @DisplayName("XLITE_DATA_HOME overrides the linux default") + void testResolve_EnvOverrideWinsOnLinux() { + assertEquals("/tmp/xlite-sandbox/data", + App.resolveUserConfigDir("/tmp/xlite-sandbox/data", "linux", USER_HOME, APPDATA)); + } + + @Test + @DisplayName("XLITE_DATA_HOME overrides the windows default") + void testResolve_EnvOverrideWinsOnWindows() { + assertEquals("/tmp/xlite-sandbox/data", + App.resolveUserConfigDir("/tmp/xlite-sandbox/data", "windows 10", USER_HOME, APPDATA)); + } + + @Test + @DisplayName("XLITE_DATA_HOME overrides the mac default") + void testResolve_EnvOverrideWinsOnMac() { + assertEquals("/tmp/xlite-sandbox/data", + App.resolveUserConfigDir("/tmp/xlite-sandbox/data", "mac os x", USER_HOME, null)); + } + + @Test + @DisplayName("blank or null env falls back to per-OS defaults") + void testResolve_BlankEnvFallsBackToPlatformDefaults() { + for (String blank : new String[]{null, "", " "}) { + assertEquals(USER_HOME + File.separator + ".config", + App.resolveUserConfigDir(blank, "linux", USER_HOME, APPDATA)); + assertEquals(APPDATA, + App.resolveUserConfigDir(blank, "windows 10", USER_HOME, APPDATA)); + assertEquals(USER_HOME + File.separator + "Library" + File.separator + "Application Support", + App.resolveUserConfigDir(blank, "mac os x", USER_HOME, null)); + } + } + + @Test + @DisplayName("unknown os.name falls back to the linux-style default") + void testResolve_UnknownOsFallsBackToDotConfig() { + assertEquals(USER_HOME + File.separator + ".config", + App.resolveUserConfigDir(null, "sunos", USER_HOME, APPDATA)); + } + + @Test + @DisplayName("override is normalized to an absolute path") + void testResolve_RelativeEnvNormalizedToAbsolute() { + String resolved = App.resolveUserConfigDir("relative/sandbox", "linux", USER_HOME, APPDATA); + assertEquals(new File("relative/sandbox").getAbsoluteFile().getPath(), resolved); + } + + @Test + @DisplayName("windows default keeps legacy verbatim pass-through of AppData (may be null)") + void testResolve_WindowsDefaultPassesAppDataVerbatim() { + assertNull(App.resolveUserConfigDir(null, "windows 10", USER_HOME, null)); + } +} From 479e704d9549230fae9068db72b3306ed92fd090 Mon Sep 17 00:00:00 2001 From: tryiou Date: Wed, 26 Aug 2026 00:34:14 +0200 Subject: [PATCH 54/73] fix(signmessage): BTC must not inherit Litecoin header; route via helper - extract package-private static signedMessageHeader(CoinTicker): null-safe, one switch, call sites unified - BITCOIN fell through to LITECOIN's header ("Litecoin Signed Message:") producing signatures peer tooling rejects - now returns its own; pinned by HTTPServerHandlerSignedMessageTest - uncomment BITCOIN_CASH -> "Bitcoin Signed Message:\n" (matches core) - headers verified against upstream core sources at the manifest tags: RVN "Raven", DASH "DarkCoin", PIVX "DarkNet", PKOIN "Pocketcoin", UNO "Unobtanium" - all verbatim matches - drop XLQ/PHR/BAY cases (coins delisted from the product) --- .../api/http/server/HTTPServerHandler.java | 61 ++++++++----------- .../HTTPServerHandlerSignedMessageTest.java | 46 ++++++++++++++ 2 files changed, 73 insertions(+), 34 deletions(-) create mode 100644 src/test/java/io/cloudchains/app/net/api/http/server/HTTPServerHandlerSignedMessageTest.java diff --git a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java b/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java index e8ec9de..40dd34c 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java +++ b/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java @@ -6,6 +6,7 @@ import com.subgraph.orchid.encoders.Hex; import io.cloudchains.app.Version; import io.cloudchains.app.net.CoinInstance; +import io.cloudchains.app.net.CoinTicker; import io.cloudchains.app.net.CoinTickerUtils; import io.cloudchains.app.net.api.http.client.HTTPClient; import io.cloudchains.app.net.protocols.blocknet.BlocknetPeer; @@ -1566,54 +1567,46 @@ private String canonicalizeASM(String asm) { .replaceAll("\\([0-9]+\\)", ""); } - private byte[] formatMessageForSigning(String message) { - String header = null; - - switch (coin.getTicker()) { + /** + * Returns the coin-specific signed-message header prefix, or null when the + * coin has no mapping. Package-private static for testability. + */ + static String signedMessageHeader(CoinTicker ticker) { + if (ticker == null) + return null; + switch (ticker) { case BLOCKNET: case BLOCKNET_TESTNET5: - header = "Blocknet Signed Message:\n"; - break; + return "Blocknet Signed Message:\n"; case BITCOIN: - // case BITCOIN_CASH: - // header = "Bitcoin Signed Message:\n"; - // break; + case BITCOIN_CASH: + return "Bitcoin Signed Message:\n"; case LITECOIN: - header = "Litecoin Signed Message:\n"; - break; - // case ALQOCOIN: - // case PHORECOIN: + return "Litecoin Signed Message:\n"; case PIVX: - header = "DarkNet Signed Message:\n"; - break; + return "DarkNet Signed Message:\n"; case DASHCOIN: - header = "DarkCoin Signed Message:\n"; - break; + return "DarkCoin Signed Message:\n"; case UNOBTANIUM: - header = "Unobtanium Signed Message:\n"; - break; + return "Unobtanium Signed Message:\n"; case PKOIN: - header = "Pocketcoin Signed Message:\n"; - break; + return "Pocketcoin Signed Message:\n"; case DIGIBYTE: - header = "DigiByte Signed Message:\n"; - break; - // case BITBAY: - // header = "BitBay Signed Message:\n"; - // break; - case RAVENCOIN: - header = "Raven Signed Message:\n"; - break; + return "DigiByte Signed Message:\n"; + case RAVENCOIN: + return "Raven Signed Message:\n"; case DOGECOIN: - header = "Dogecoin Signed Message:\n"; - break; + return "Dogecoin Signed Message:\n"; case SYSCOIN: - header = "Syscoin Signed Message:\n"; - break; + return "Syscoin Signed Message:\n"; default: LOGGER.warning("[http-server-handler] ERROR: Unsupported coin. This should never happen."); - break; + return null; } + } + + private byte[] formatMessageForSigning(String message) { + String header = signedMessageHeader(coin.getTicker()); try { ByteArrayOutputStream bos = new ByteArrayOutputStream(); diff --git a/src/test/java/io/cloudchains/app/net/api/http/server/HTTPServerHandlerSignedMessageTest.java b/src/test/java/io/cloudchains/app/net/api/http/server/HTTPServerHandlerSignedMessageTest.java new file mode 100644 index 0000000..853877b --- /dev/null +++ b/src/test/java/io/cloudchains/app/net/api/http/server/HTTPServerHandlerSignedMessageTest.java @@ -0,0 +1,46 @@ +package io.cloudchains.app.net.api.http.server; + +import io.cloudchains.app.net.CoinTicker; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * Pins the per-coin signed-message headers used by signmessage/verifymessage. + * These strings must match what peer wallets expect — a wrong header produces + * signatures other tooling rejects (e.g. BTC must NOT inherit Litecoin's). + */ +class HTTPServerHandlerSignedMessageTest { + + @Test + @DisplayName("BITCOIN uses its own header, not Litecoin's (fallthrough regression)") + void testSignedMessageHeader_BitcoinIsNotLitecoin() { + String btc = HTTPServerHandler.signedMessageHeader(CoinTicker.BITCOIN); + assertEquals("Bitcoin Signed Message:\n", btc); + } + + @Test + @DisplayName("each mapped coin keeps its documented header") + void testSignedMessageHeader_KnownCoins() { + assertEquals("Blocknet Signed Message:\n", HTTPServerHandler.signedMessageHeader(CoinTicker.BLOCKNET)); + assertEquals("Blocknet Signed Message:\n", HTTPServerHandler.signedMessageHeader(CoinTicker.BLOCKNET_TESTNET5)); + assertEquals("Bitcoin Signed Message:\n", HTTPServerHandler.signedMessageHeader(CoinTicker.BITCOIN_CASH)); + assertEquals("Litecoin Signed Message:\n", HTTPServerHandler.signedMessageHeader(CoinTicker.LITECOIN)); + assertEquals("DarkNet Signed Message:\n", HTTPServerHandler.signedMessageHeader(CoinTicker.PIVX)); + assertEquals("DarkCoin Signed Message:\n", HTTPServerHandler.signedMessageHeader(CoinTicker.DASHCOIN)); + assertEquals("Unobtanium Signed Message:\n", HTTPServerHandler.signedMessageHeader(CoinTicker.UNOBTANIUM)); + assertEquals("Pocketcoin Signed Message:\n", HTTPServerHandler.signedMessageHeader(CoinTicker.PKOIN)); + assertEquals("DigiByte Signed Message:\n", HTTPServerHandler.signedMessageHeader(CoinTicker.DIGIBYTE)); + assertEquals("Raven Signed Message:\n", HTTPServerHandler.signedMessageHeader(CoinTicker.RAVENCOIN)); + assertEquals("Dogecoin Signed Message:\n", HTTPServerHandler.signedMessageHeader(CoinTicker.DOGECOIN)); + assertEquals("Syscoin Signed Message:\n", HTTPServerHandler.signedMessageHeader(CoinTicker.SYSCOIN)); + } + + @Test + @DisplayName("unmapped tickers return null") + void testSignedMessageHeader_UnmappedReturnsNull() { + assertNull(HTTPServerHandler.signedMessageHeader(null)); + } +} From 9afad9355e30c126a19bc47873ef22b7715c0800 Mon Sep 17 00:00:00 2001 From: tryiou Date: Wed, 26 Aug 2026 00:34:55 +0200 Subject: [PATCH 55/73] test(app): harden XLITE_DATA_HOME resolver matrix - build expectations through File so cases hold on any host OS (windows/mac paths exercised on linux CI) - use real-world os.name casing ("Linux", "Windows 11", "Mac OS X") - add padded-override trim case; de-tautologize relative-path normalization (assert under-cwd + segment preservation) --- src/test/java/io/cloudchains/app/AppTest.java | 47 ++++++++++++------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/src/test/java/io/cloudchains/app/AppTest.java b/src/test/java/io/cloudchains/app/AppTest.java index c77b0be..387fec1 100644 --- a/src/test/java/io/cloudchains/app/AppTest.java +++ b/src/test/java/io/cloudchains/app/AppTest.java @@ -5,13 +5,12 @@ import java.io.File; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.*; /** * Precedence matrix for {@link App#resolveUserConfigDir(String, String, String, String)}. - *

{@code XLITE_DATA_HOME} (non-blank) must win over every per-OS default; - * blank/null env must fall through to the platform root unchanged.

+ *

{@code XLITE_DATA_HOME} (non-blank, trimmed, absolute-normalized) must win over + * every per-OS default; blank/null env must fall through to the platform root.

*/ class AppTest { @@ -22,33 +21,36 @@ class AppTest { @DisplayName("XLITE_DATA_HOME overrides the linux default") void testResolve_EnvOverrideWinsOnLinux() { assertEquals("/tmp/xlite-sandbox/data", - App.resolveUserConfigDir("/tmp/xlite-sandbox/data", "linux", USER_HOME, APPDATA)); + App.resolveUserConfigDir("/tmp/xlite-sandbox/data", "Linux", USER_HOME, APPDATA)); } @Test @DisplayName("XLITE_DATA_HOME overrides the windows default") void testResolve_EnvOverrideWinsOnWindows() { - assertEquals("/tmp/xlite-sandbox/data", - App.resolveUserConfigDir("/tmp/xlite-sandbox/data", "windows 10", USER_HOME, APPDATA)); + // expectation built through File so the assertion holds on any host OS + File expected = new File("/tmp/xlite-sandbox/data").getAbsoluteFile(); + assertEquals(expected.getPath(), + App.resolveUserConfigDir("/tmp/xlite-sandbox/data", "Windows 11", USER_HOME, APPDATA)); } @Test @DisplayName("XLITE_DATA_HOME overrides the mac default") void testResolve_EnvOverrideWinsOnMac() { - assertEquals("/tmp/xlite-sandbox/data", - App.resolveUserConfigDir("/tmp/xlite-sandbox/data", "mac os x", USER_HOME, null)); + File expected = new File("/tmp/xlite-sandbox/data").getAbsoluteFile(); + assertEquals(expected.getPath(), + App.resolveUserConfigDir("/tmp/xlite-sandbox/data", "Mac OS X", USER_HOME, null)); } @Test - @DisplayName("blank or null env falls back to per-OS defaults") + @DisplayName("blank or null env falls back to per-OS defaults (real-world os.name casing)") void testResolve_BlankEnvFallsBackToPlatformDefaults() { for (String blank : new String[]{null, "", " "}) { assertEquals(USER_HOME + File.separator + ".config", - App.resolveUserConfigDir(blank, "linux", USER_HOME, APPDATA)); + App.resolveUserConfigDir(blank, "Linux", USER_HOME, APPDATA)); assertEquals(APPDATA, - App.resolveUserConfigDir(blank, "windows 10", USER_HOME, APPDATA)); + App.resolveUserConfigDir(blank, "Windows 11", USER_HOME, APPDATA)); assertEquals(USER_HOME + File.separator + "Library" + File.separator + "Application Support", - App.resolveUserConfigDir(blank, "mac os x", USER_HOME, null)); + App.resolveUserConfigDir(blank, "Mac OS X", USER_HOME, null)); } } @@ -60,15 +62,26 @@ void testResolve_UnknownOsFallsBackToDotConfig() { } @Test - @DisplayName("override is normalized to an absolute path") + @DisplayName("padded override is trimmed before resolution") + void testResolve_PaddedEnvIsTrimmed() { + File expected = new File("/tmp/xlite-sandbox/data").getAbsoluteFile(); + assertEquals(expected.getPath(), + App.resolveUserConfigDir(" /tmp/xlite-sandbox/data ", "Linux", USER_HOME, APPDATA)); + } + + @Test + @DisplayName("relative override is normalized against the working directory") void testResolve_RelativeEnvNormalizedToAbsolute() { - String resolved = App.resolveUserConfigDir("relative/sandbox", "linux", USER_HOME, APPDATA); - assertEquals(new File("relative/sandbox").getAbsoluteFile().getPath(), resolved); + String resolved = App.resolveUserConfigDir("relative/sandbox", "Linux", USER_HOME, APPDATA); + assertTrue(resolved.startsWith(new File("").getAbsolutePath()), + "must resolve under the working directory, got: " + resolved); + assertTrue(resolved.endsWith("relative" + File.separator + "sandbox"), + "must keep the relative segments, got: " + resolved); } @Test @DisplayName("windows default keeps legacy verbatim pass-through of AppData (may be null)") void testResolve_WindowsDefaultPassesAppDataVerbatim() { - assertNull(App.resolveUserConfigDir(null, "windows 10", USER_HOME, null)); + assertNull(App.resolveUserConfigDir(null, "Windows 11", USER_HOME, null)); } } From b03524c9512947f84cead963964cd75fb774b0b2 Mon Sep 17 00:00:00 2001 From: tryiou Date: Wed, 26 Aug 2026 00:55:48 +0200 Subject: [PATCH 56/73] refactor(params): remove BIP32 version overrides BIP32 is unsupported across xlite/xbridge (legacy addresses only); the per-coin getBip32HeaderP2PKH{priv,pub} overrides are deleted from 12 params classes. Base-class getters read protected fields that are never assigned, so they now return 0 - intentional: nothing in app code nor any reachable path consumes them (verified: zero in-repo callers; jar-internal DeterministicKey B58 serialization unreached). If xpub/xprv support is ever added, version bytes will be defined in blockchain-configuration-files, not hardcoded here. Notes: - removes latent LTC priv/pub swap (was inverted vs upstream) - RavencoinNetworkParameters additionally normalized CRLF->LF (repo convention; content delta verified identical modulo EOL) - the alqocoin/bitbay/phorecoin classes keep their overrides here; they belong to delisted coins and are deleted whole elsewhere --- .../BitcoinCashNetworkParameters.java | 10 - .../blocknet/BlocknetNetworkParameters.java | 10 - .../BlocknetTestnet5NetworkParameters.java | 10 - .../dashcoin/DashcoinNetworkParameters.java | 10 - .../digibyte/DigibyteNetworkParameters.java | 10 - .../dogecoin/DogecoinNetworkParameters.java | 10 - .../litecoin/LitecoinNetworkParameters.java | 10 - .../protocols/pivx/PivxNetworkParameters.java | 10 - .../PocketcoinNetworkParameters.java | 10 - .../ravencoin/RavencoinNetworkParameters.java | 204 +++++++++--------- .../syscoin/SyscoinNetworkParameters.java | 10 - .../UnobtaniumNetworkParameters.java | 10 - 12 files changed, 97 insertions(+), 217 deletions(-) diff --git a/src/main/java/io/cloudchains/app/net/protocols/bitcoincash/BitcoinCashNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/bitcoincash/BitcoinCashNetworkParameters.java index 1bc4310..658d945 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/bitcoincash/BitcoinCashNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/bitcoincash/BitcoinCashNetworkParameters.java @@ -72,16 +72,6 @@ public int getDumpedPrivateKeyHeader() { } - @Override - public int getBip32HeaderP2PKHpriv() { - return 0x0488ADE4; - } - - @Override - public int getBip32HeaderP2PKHpub() { - return 0x0488B21E; - } - @Override public int getSubsidyDecreaseBlockCount() { return 210240; diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetNetworkParameters.java index 1b6b961..c390bd7 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetNetworkParameters.java @@ -139,16 +139,6 @@ public int getDumpedPrivateKeyHeader() { return 154; } - @Override - public int getBip32HeaderP2PKHpub() { - return 0x0488B21E; - } - - @Override - public int getBip32HeaderP2PKHpriv() { - return 0x0488ADE4; - } - @Override public BigInteger getMaxTarget() { return Utils.decodeCompactBits(0x1E0FFFFF); diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetTestnet5NetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetTestnet5NetworkParameters.java index ef51380..864e6af 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetTestnet5NetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetTestnet5NetworkParameters.java @@ -130,16 +130,6 @@ public int getDumpedPrivateKeyHeader() { return 239; } - @Override - public int getBip32HeaderP2PKHpub() { - return 0x3A8061A0; - } - - @Override - public int getBip32HeaderP2PKHpriv() { - return 0x3A805837; - } - @Override public String[] getDnsSeeds() { return new String[]{ diff --git a/src/main/java/io/cloudchains/app/net/protocols/dashcoin/DashcoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/dashcoin/DashcoinNetworkParameters.java index 4b113b4..f98e28e 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/dashcoin/DashcoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/dashcoin/DashcoinNetworkParameters.java @@ -72,16 +72,6 @@ public int getDumpedPrivateKeyHeader() { } - @Override - public int getBip32HeaderP2PKHpriv() { - return 0x0488ADE4; - } - - @Override - public int getBip32HeaderP2PKHpub() { - return 0x0488B21E; - } - @Override public int getSubsidyDecreaseBlockCount() { return 210240; diff --git a/src/main/java/io/cloudchains/app/net/protocols/digibyte/DigibyteNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/digibyte/DigibyteNetworkParameters.java index 5bd5947..48efe97 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/digibyte/DigibyteNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/digibyte/DigibyteNetworkParameters.java @@ -72,16 +72,6 @@ public int getDumpedPrivateKeyHeader() { } - @Override - public int getBip32HeaderP2PKHpriv() { - return 0x0488ADE4; - } - - @Override - public int getBip32HeaderP2PKHpub() { - return 0x0488B21E; - } - @Override public int getSubsidyDecreaseBlockCount() { return 100000; diff --git a/src/main/java/io/cloudchains/app/net/protocols/dogecoin/DogecoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/dogecoin/DogecoinNetworkParameters.java index 68ffc12..ff9a13f 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/dogecoin/DogecoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/dogecoin/DogecoinNetworkParameters.java @@ -72,16 +72,6 @@ public int getDumpedPrivateKeyHeader() { } - @Override - public int getBip32HeaderP2PKHpriv() { - return 0x02fac398; - } - - @Override - public int getBip32HeaderP2PKHpub() { - return 0x02facafd; - } - @Override public int getSubsidyDecreaseBlockCount() { return 100000; diff --git a/src/main/java/io/cloudchains/app/net/protocols/litecoin/LitecoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/litecoin/LitecoinNetworkParameters.java index 27c2d87..e142039 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/litecoin/LitecoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/litecoin/LitecoinNetworkParameters.java @@ -76,16 +76,6 @@ public int getDumpedPrivateKeyHeader() { } - @Override - public int getBip32HeaderP2PKHpriv() { - return 0x0488B21E; - } - - @Override - public int getBip32HeaderP2PKHpub() { - return 0x0488ADE4; - } - @Override public int getSubsidyDecreaseBlockCount() { return 840000; diff --git a/src/main/java/io/cloudchains/app/net/protocols/pivx/PivxNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/pivx/PivxNetworkParameters.java index 72a7ced..444a09e 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/pivx/PivxNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/pivx/PivxNetworkParameters.java @@ -72,16 +72,6 @@ public int getDumpedPrivateKeyHeader() { } - @Override - public int getBip32HeaderP2PKHpriv() { - return 0x0221312B; - } - - @Override - public int getBip32HeaderP2PKHpub() { - return 0x022D2533; - } - @Override public int getSubsidyDecreaseBlockCount() { return 210240; diff --git a/src/main/java/io/cloudchains/app/net/protocols/pocketcoin/PocketcoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/pocketcoin/PocketcoinNetworkParameters.java index 3411582..a384dae 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/pocketcoin/PocketcoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/pocketcoin/PocketcoinNetworkParameters.java @@ -72,16 +72,6 @@ public int getDumpedPrivateKeyHeader() { } - @Override - public int getBip32HeaderP2PKHpriv() { - return 0x1E88ADE4; - } - - @Override - public int getBip32HeaderP2PKHpub() { - return 0x1E88B21E; - } - @Override public int getSubsidyDecreaseBlockCount() { return 2100000; diff --git a/src/main/java/io/cloudchains/app/net/protocols/ravencoin/RavencoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/ravencoin/RavencoinNetworkParameters.java index 1033c30..9288d5d 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/ravencoin/RavencoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/ravencoin/RavencoinNetworkParameters.java @@ -1,107 +1,97 @@ -package io.cloudchains.app.net.protocols.ravencoin; - -import io.cloudchains.app.net.HasFeeParams; -import org.bitcoinj.core.*; -import org.bitcoinj.store.BlockStore; -import org.bitcoinj.store.BlockStoreException; -import org.bitcoinj.utils.MonetaryFormat; - -public class RavencoinNetworkParameters extends NetworkParameters implements HasFeeParams { - - public RavencoinNetworkParameters() { - super(); - } - - @Override - public String getPaymentProtocolId() { - return "main"; - } - - @Override - public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException { - } - - @Override - public Coin getMaxMoney() { - return Coin.valueOf(100000000 * Coin.COIN.value); - } - - @Override - public Coin getMinNonDustOutput() { - return Transaction.MIN_NONDUST_OUTPUT; - } - - @Override - public MonetaryFormat getMonetaryFormat() { - return new MonetaryFormat().code(0, "RVN"); - } - - @Override - public String getUriScheme() { - return "ravencoin:"; - } - - @Override - public boolean hasMaxMoney() { - return true; - } - - @Override - public BitcoinSerializer getSerializer(boolean parseRetain) { - return new BitcoinSerializer(this, parseRetain); - } - - @Override - public int getProtocolVersionNum(ProtocolVersion version) { - return 70026; - } - - @Override - public int getAddressHeader() { - return 60; - } - - @Override - public int getP2SHHeader() { - return 122; - } - - @Override - public int getDumpedPrivateKeyHeader() { - return 128; - } - - - @Override - public int getBip32HeaderP2PKHpriv() { - return 0x0488ADE4; - } - - @Override - public int getBip32HeaderP2PKHpub() { - return 0x0488B21E; - } - - @Override - public int getSubsidyDecreaseBlockCount() { - return 210240; - } - - @Override - public int getInterval() { - return 60; - } - - @Override - public String getId() { - return "RVN"; - } - - public long getFeePerByte() { - return 1000; - } - - public long getMinTxFee() { - return 100000; - } -} +package io.cloudchains.app.net.protocols.ravencoin; + +import io.cloudchains.app.net.HasFeeParams; +import org.bitcoinj.core.*; +import org.bitcoinj.store.BlockStore; +import org.bitcoinj.store.BlockStoreException; +import org.bitcoinj.utils.MonetaryFormat; + +public class RavencoinNetworkParameters extends NetworkParameters implements HasFeeParams { + + public RavencoinNetworkParameters() { + super(); + } + + @Override + public String getPaymentProtocolId() { + return "main"; + } + + @Override + public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException { + } + + @Override + public Coin getMaxMoney() { + return Coin.valueOf(100000000 * Coin.COIN.value); + } + + @Override + public Coin getMinNonDustOutput() { + return Transaction.MIN_NONDUST_OUTPUT; + } + + @Override + public MonetaryFormat getMonetaryFormat() { + return new MonetaryFormat().code(0, "RVN"); + } + + @Override + public String getUriScheme() { + return "ravencoin:"; + } + + @Override + public boolean hasMaxMoney() { + return true; + } + + @Override + public BitcoinSerializer getSerializer(boolean parseRetain) { + return new BitcoinSerializer(this, parseRetain); + } + + @Override + public int getProtocolVersionNum(ProtocolVersion version) { + return 70026; + } + + @Override + public int getAddressHeader() { + return 60; + } + + @Override + public int getP2SHHeader() { + return 122; + } + + @Override + public int getDumpedPrivateKeyHeader() { + return 128; + } + + + @Override + public int getSubsidyDecreaseBlockCount() { + return 210240; + } + + @Override + public int getInterval() { + return 60; + } + + @Override + public String getId() { + return "RVN"; + } + + public long getFeePerByte() { + return 1000; + } + + public long getMinTxFee() { + return 100000; + } +} diff --git a/src/main/java/io/cloudchains/app/net/protocols/syscoin/SyscoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/syscoin/SyscoinNetworkParameters.java index c03c126..262318b 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/syscoin/SyscoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/syscoin/SyscoinNetworkParameters.java @@ -72,16 +72,6 @@ public int getDumpedPrivateKeyHeader() { return 128; } - @Override - public int getBip32HeaderP2PKHpriv() { - return 0x0488ADE4; - } - - @Override - public int getBip32HeaderP2PKHpub() { - return 0x0488B21E; - } - @Override public int getSubsidyDecreaseBlockCount() { return 525600; diff --git a/src/main/java/io/cloudchains/app/net/protocols/unobtanium/UnobtaniumNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/unobtanium/UnobtaniumNetworkParameters.java index 09fdb4c..b332fdb 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/unobtanium/UnobtaniumNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/unobtanium/UnobtaniumNetworkParameters.java @@ -73,16 +73,6 @@ public int getDumpedPrivateKeyHeader() { } - @Override - public int getBip32HeaderP2PKHpriv() { - return 0x0488ADE4; - } - - @Override - public int getBip32HeaderP2PKHpub() { - return 0x0488B21E; - } - @Override public int getSubsidyDecreaseBlockCount() { return 100000; // Adjusted for UNO From 85faf5980d94747f1baed09c23bd9e30a4f442bb Mon Sep 17 00:00:00 2001 From: tryiou Date: Wed, 26 Aug 2026 02:10:31 +0200 Subject: [PATCH 57/73] feat(concurrency): per-coin RPC server rebinding under ReentrantLock JSONRPCController serializes every servers-map access under a per-coin ReentrantLock; rebindRPCServer() retires the old server and creates its replacement atomically so no consumer observes a mid-retirement instance. Scope is per-coin, so one coin's multi-second port-release wait never stalls other coins' lookups. CoinInstance.coinRPCServer becomes volatile and reloadConfig() routes through rebindRPCServer(), preserving validation-before-mutation. ConsoleMenu, EXRServerPool, BlocknetPeerGroup and BackgroundTimerThread get thread hygiene: long-lived threads are daemonized except one named anchor released through the App shutdown hook chain. JSONRPCControllerRebindTest pins the fresh-instance swap and live retirement with real socket teardown/rebind. Known limitation: the lock guarantees the server handout, not the caller's later field write - two interleaved reloadConfig() calls can still leave coinRPCServer pointing at a retired-but-bound instance; bounded and pre-existing. --- .../cloudchains/app/console/ConsoleMenu.java | 54 +++++--- .../io/cloudchains/app/net/CoinInstance.java | 10 +- .../app/net/api/JSONRPCController.java | 46 ++++++- .../net/api/http/client/EXRServerPool.java | 8 +- .../protocols/blocknet/BlocknetPeerGroup.java | 13 +- .../background/BackgroundTimerThread.java | 26 ++-- .../net/api/JSONRPCControllerRebindTest.java | 120 ++++++++++++++++++ 7 files changed, 231 insertions(+), 46 deletions(-) create mode 100644 src/test/java/io/cloudchains/app/net/api/JSONRPCControllerRebindTest.java diff --git a/src/main/java/io/cloudchains/app/console/ConsoleMenu.java b/src/main/java/io/cloudchains/app/console/ConsoleMenu.java index 0c973e2..f3e6839 100644 --- a/src/main/java/io/cloudchains/app/console/ConsoleMenu.java +++ b/src/main/java/io/cloudchains/app/console/ConsoleMenu.java @@ -99,14 +99,7 @@ public void init() { App.EXR_ENDPOINT = exrEndpoint; App.exrServerPool = new EXRServerPool(App.EXR_ENDPOINT); LOGGER.info("[console] EXR mode enabled with " + App.exrServerPool.getServerCount() + " servers: " + App.EXR_ENDPOINT); - new Thread(() -> { - try { - Thread.sleep(1000); - App.exrServerPool.probeAllCapabilities(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - }, "EXR-Capability-Prober").start(); + spawnExrCapabilityProbe(); i++; } else { String envExrEndpoint = App.getEnv("EXR_ENDPOINT"); @@ -114,14 +107,7 @@ public void init() { App.EXR_ENDPOINT = envExrEndpoint; App.exrServerPool = new EXRServerPool(App.EXR_ENDPOINT); LOGGER.info("[console] EXR mode enabled with " + App.exrServerPool.getServerCount() + " servers: " + App.EXR_ENDPOINT); - new Thread(() -> { - try { - Thread.sleep(1000); - App.exrServerPool.probeAllCapabilities(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - }, "EXR-Capability-Prober").start(); + spawnExrCapabilityProbe(); } else { LOGGER.warning("Missing EXR endpoint after '--exr-endpoint'"); } @@ -433,7 +419,15 @@ private void completeLogin(char[] password, String userMnemonic, boolean isMnemo App.masterRPC.start(); backgroundTimerThread = new BackgroundTimerThread(); - (new Thread(backgroundTimerThread)).start(); + // Sole deliberate non-daemon anchor: this named thread is what + // keeps the JVM alive while the wallet runs. Every long-lived + // pool in the process (RPC, servers, probes, timer scheduler) is + // daemonized; the coin-init pool is short-lived, daemon-factory, + // and always drained in its finally. deinit()/stop() releases + // this anchor on shutdown. + Thread timerAnchor = new Thread(backgroundTimerThread, + "background-timer-anchor"); + timerAnchor.start(); if (App.exrServerPool != null) { App.exrServerPool.probeAllCapabilities(); } @@ -446,7 +440,14 @@ private void initializeCoinsConcurrently(List coinTickers, char[] pa } int threadCount = Math.min(coinTickers.size(), 8); - ExecutorService executor = Executors.newFixedThreadPool(threadCount); + // Daemon factory: init tasks are bounded by their futures and the + // pool is drained in finally — it must never become a second + // liveness anchor if a task survives shutdownNow's interrupt. + ExecutorService executor = Executors.newFixedThreadPool(threadCount, r -> { + Thread t = new Thread(r, "coin-init"); + t.setDaemon(true); + return t; + }); try { List enabledCoins = coinTickers.stream() @@ -485,6 +486,23 @@ private void initializeCoinsConcurrently(List coinTickers, char[] pa } } + /** + * Deferred best-effort capability probe for a freshly configured EXR + * pool. Daemon thread: a hung endpoint must never hold the JVM. + */ + private void spawnExrCapabilityProbe() { + Thread prober = new Thread(() -> { + try { + Thread.sleep(1000); + App.exrServerPool.probeAllCapabilities(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }, "EXR-Capability-Prober"); + prober.setDaemon(true); + prober.start(); + } + private void autoGenerateRPCConfig() { for (CoinTicker cointicker : CoinTicker.coins()) { ConfigHelper configHelper = new ConfigHelper(CoinTickerUtils.tickerToString(cointicker)); diff --git a/src/main/java/io/cloudchains/app/net/CoinInstance.java b/src/main/java/io/cloudchains/app/net/CoinInstance.java index 7053fc7..9914bbe 100644 --- a/src/main/java/io/cloudchains/app/net/CoinInstance.java +++ b/src/main/java/io/cloudchains/app/net/CoinInstance.java @@ -101,7 +101,9 @@ public String getMessage() { private XRouterPacketManager xRouterPacketManager = null; private int rpcPort = -1; private boolean testnet = false; - private JSONRPCServer coinRPCServer = null; + // Volatile: written by reloadconfig on a Netty event-loop thread and + // read by lifecycle code (deinit) on other threads. + private volatile JSONRPCServer coinRPCServer = null; private volatile long lastUtxoUpdate = 0; private final AtomicInteger updateFailures = new AtomicInteger(0); private int generatedAddressCount; @@ -1000,9 +1002,9 @@ public void reloadConfig() { if (coinRPCServer == null) return; // no rpc available, skip - JSONRPCController.removeRPCServer(this); - - coinRPCServer = JSONRPCController.getRPCServer(this); + // Atomic retire+create: a concurrent reloadconfig must never + // receive the retiring server instance. + coinRPCServer = JSONRPCController.rebindRPCServer(this); LOGGER.finer("[rpc] Requesting start of JSON-RPC server for coin " + CoinTickerUtils.tickerToString(getTicker()) + " on port " + getRPCPort()); coinRPCServer.start(); diff --git a/src/main/java/io/cloudchains/app/net/api/JSONRPCController.java b/src/main/java/io/cloudchains/app/net/api/JSONRPCController.java index 9a4fcfb..7f653ff 100644 --- a/src/main/java/io/cloudchains/app/net/api/JSONRPCController.java +++ b/src/main/java/io/cloudchains/app/net/api/JSONRPCController.java @@ -4,6 +4,7 @@ import io.cloudchains.app.util.ConfigHelper; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -12,6 +13,12 @@ public class JSONRPCController { private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); private static final ConcurrentHashMap servers = new ConcurrentHashMap<>(); + // Per-coin serialization of every map access: retire→create happens + // under the coin's lock, and plain lookups take it too, so no consumer + // can observe a mid-retirement server. Per-coin (not global) scope: + // a rebinding coin must not stall unrelated coins' event loops behind + // its multi-second release-wait. + private static final ConcurrentHashMap locks = new ConcurrentHashMap<>(); private static JSONRPCMasterServer masterServer = new JSONRPCMasterServer(new ConfigHelper("master").getMasterRpcPort()); public static JSONRPCMasterServer getMasterServer() { @@ -22,16 +29,47 @@ public static JSONRPCServer getRPCServer(CoinInstance coinInstance) { if (coinInstance == null || coinInstance.getRPCPort() == -1) { throw new IllegalArgumentException("Bad coin instance"); } - - return servers.computeIfAbsent(coinInstance, - coin -> new JSONRPCServer(coin, coin.getRPCPort())); + ReentrantLock lock = lockFor(coinInstance); + lock.lock(); + try { + return getOrCreateLocked(coinInstance); + } finally { + lock.unlock(); + } } - public static void removeRPCServer(CoinInstance coinInstance) { + /** + * Retires the coin's current server and returns a freshly created one + * as a single step, serialized against every other controller access + * for this coin. Callers must use this instead of an unsynchronized + * remove+get pair: between retirement request and map release there + * is a bounded wait, and only this lock guarantees no consumer of + * this coin observes the retiring instance. + */ + public static JSONRPCServer rebindRPCServer(CoinInstance coinInstance) { if (coinInstance == null || coinInstance.getRPCPort() == -1) { throw new IllegalArgumentException("Bad coin instance"); } + ReentrantLock lock = lockFor(coinInstance); + lock.lock(); + try { + removeLocked(coinInstance); + return getOrCreateLocked(coinInstance); + } finally { + lock.unlock(); + } + } + + private static ReentrantLock lockFor(CoinInstance coinInstance) { + return locks.computeIfAbsent(coinInstance, coin -> new ReentrantLock()); + } + + private static JSONRPCServer getOrCreateLocked(CoinInstance coinInstance) { + return servers.computeIfAbsent(coinInstance, + coin -> new JSONRPCServer(coin, coin.getRPCPort())); + } + private static void removeLocked(CoinInstance coinInstance) { JSONRPCServer server = servers.get(coinInstance); if (server == null) return; diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/EXRServerPool.java b/src/main/java/io/cloudchains/app/net/api/http/client/EXRServerPool.java index 0c956d6..48adfba 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/client/EXRServerPool.java +++ b/src/main/java/io/cloudchains/app/net/api/http/client/EXRServerPool.java @@ -54,8 +54,11 @@ public void startCapabilityProbing() { return; } LOGGER.info("[exr-pool] Starting capability probing for " + servers.size() + " servers"); - // Start capability probing in background - new Thread(this::probeAllCapabilities, "EXR-Capability-Prober").start(); + // Best-effort network probes: daemon so a hung endpoint can never + // hold the JVM past shutdown. + Thread prober = new Thread(this::probeAllCapabilities, "EXR-Capability-Prober"); + prober.setDaemon(true); + prober.start(); } public void probeAllCapabilities() { @@ -73,6 +76,7 @@ public void probeAllCapabilities() { LOGGER.warning("[exr-pool] Failed to probe server " + server.getEndpoint() + ", " + e.getMessage()); } }); + t.setDaemon(true); probeThreads.add(t); t.start(); } diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeerGroup.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeerGroup.java index 8db7bc4..353ca0e 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeerGroup.java +++ b/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeerGroup.java @@ -161,9 +161,18 @@ private void startConnections() { } } + // Dormant path: start() is unreachable today (its only call site is + // commented out in CoinInstance). If ever revived, this must NOT + // submit a second BackgroundTimerThread — ConsoleMenu already runs + // the sole timer anchor; a duplicate would double-keepalive and + // double-schedule log rotation. Daemon factory keeps it from becoming + // an unplanned liveness anchor. private void startBackgroundThreads() { - threadPool = Executors.newSingleThreadExecutor(); - threadPool.submit(new BackgroundTimerThread()); + threadPool = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r, "blocknet-peer-group-bg"); + t.setDaemon(true); + return t; + }); } private ListenableFuture startAsync() { diff --git a/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java b/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java index 0d6a4af..abd8985 100644 --- a/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java +++ b/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java @@ -15,7 +15,6 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -32,8 +31,6 @@ public class BackgroundTimerThread implements Runnable { private static final int KEEPALIVE_INTERVAL = 10000; private static final int BALANCE_INTERVAL = 10000; - private ExecutorService threadPool = Executors.newSingleThreadExecutor(); - private BlocknetPeerGroup blocknetPeerGroup; private HTTPClient feeUpdateHttpClient; private HTTPClient heightUpdateHttpClient; @@ -48,7 +45,11 @@ public class BackgroundTimerThread implements Runnable { private Set lastAvailable = new HashSet<>(); private Set lastUnavailable = new HashSet<>(); - // Log rotation scheduler fields + // Daemon-factory scheduler: the wrapper thread started by ConsoleMenu + // is the deliberate sole non-daemon anchor of this application. Worker + // threads must never outlive it as liveness anchors — stop() still + // releases the scheduler explicitly, but daemon status makes the + // single-anchor invariant structural rather than remembered. private ScheduledExecutorService logRotationScheduler; private static final int DAILY_ROTATION_HOUR = 2; // 2:00 AM private static final int DAILY_ROTATION_MINUTE = 0; @@ -71,7 +72,11 @@ public BackgroundTimerThread() { * Initializes the log rotation scheduler to run daily at 2:00 AM. */ private void initializeLogRotationScheduler() { - logRotationScheduler = Executors.newSingleThreadScheduledExecutor(); + logRotationScheduler = Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "background-timer-log-rotation"); + t.setDaemon(true); + return t; + }); long initialDelay = calculateInitialDelay(); logRotationScheduler.scheduleAtFixedRate( this::performDailyLogRotation, @@ -118,17 +123,6 @@ public void stop() { if (workerThread != null) { workerThread.interrupt(); } - if (threadPool != null && !threadPool.isShutdown()) { - threadPool.shutdown(); - try { - if (!threadPool.awaitTermination(5, TimeUnit.SECONDS)) { - threadPool.shutdownNow(); - } - } catch (InterruptedException e) { - threadPool.shutdownNow(); - Thread.currentThread().interrupt(); - } - } if (logRotationScheduler != null && !logRotationScheduler.isShutdown()) { logRotationScheduler.shutdown(); try { diff --git a/src/test/java/io/cloudchains/app/net/api/JSONRPCControllerRebindTest.java b/src/test/java/io/cloudchains/app/net/api/JSONRPCControllerRebindTest.java new file mode 100644 index 0000000..f5ce2f8 --- /dev/null +++ b/src/test/java/io/cloudchains/app/net/api/JSONRPCControllerRebindTest.java @@ -0,0 +1,120 @@ +package io.cloudchains.app.net.api; + +import io.cloudchains.app.net.CoinInstance; +import io.cloudchains.app.util.ConfigHelper; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.net.ServerSocket; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.*; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Pins the controller's rebind handoff contract: a rebind retires the + * previous instance and installs a fresh one atomically from the caller's + * point of view, including when the retiring server is LIVE (real socket + * teardown path). + * + * Coverage honesty: the concurrent soak exercises interleaved rebinds on + * unstarted servers and proves robustness/coherence, but cannot make the + * kernel-release lag window materialize on demand — full interleaved + * reproduction of the retirement race is covered by the stack wave + * detectors (per-coin post-rebind port probe, lifecycle-anomaly logsweep), + * which have proven this path across all coins. + */ +public class JSONRPCControllerRebindTest { + + @TempDir + static Path tempConfigDir; + + private static int freePort() throws Exception { + try (ServerSocket s = new ServerSocket(0)) { + return s.getLocalPort(); + } + } + + @BeforeAll + static void isolateConfig() { + ConfigHelper.CONFIG_DIR = tempConfigDir.toString(); + } + + @Test + public void testRebind_ReturnsFreshInstanceAndSwapsHandout() throws Exception { + CoinInstance coin = mock(CoinInstance.class); + int port = freePort(); + when(coin.getRPCPort()).thenReturn(port); + + JSONRPCServer first = JSONRPCController.getRPCServer(coin); + JSONRPCServer rebound = JSONRPCController.rebindRPCServer(coin); + + assertNotNull(rebound); + assertNotSame(first, rebound); + assertSame(rebound, JSONRPCController.getRPCServer(coin)); + } + + @Test + public void testRebind_RetiresLiveServerAndInstallsFresh() throws Exception { + CoinInstance coin = mock(CoinInstance.class); + int port = freePort(); + when(coin.getRPCPort()).thenReturn(port); + + JSONRPCServer live = JSONRPCController.getRPCServer(coin); + live.start(); + assertTrue(live.awaitBound(5000), "precondition: live server bound"); + + JSONRPCServer rebound = JSONRPCController.rebindRPCServer(coin); + + assertNotSame(live, rebound); + // Controller hands out unstarted servers — starting is the + // caller's job (reloadConfig does exactly this). + rebound.start(); + // The old instance must be retired and its thread fully unwound + // (bounded join: isAlive() can lag closeFuture completion by the + // run() method's exit path) and the replacement must bind. + live.join(5000); + assertFalse(live.isAlive()); + assertTrue(rebound.awaitBound(10000), "replacement server must bind"); + assertSame(rebound, JSONRPCController.getRPCServer(coin)); + } + + @Test + public void testRebind_ConcurrentCycles_NoDeadlock_CoherentEndState() throws Exception { + CoinInstance coin = mock(CoinInstance.class); + int port = freePort(); + when(coin.getRPCPort()).thenReturn(port); + + final int threads = 8; + final int cyclesPerThread = 25; + List handedOut = + Collections.synchronizedList(new ArrayList<>()); + + ExecutorService pool = Executors.newFixedThreadPool(threads); + List> tasks = new ArrayList<>(); + for (int t = 0; t < threads; t++) { + tasks.add(() -> { + for (int i = 0; i < cyclesPerThread; i++) { + handedOut.add(JSONRPCController.rebindRPCServer(coin)); + } + return null; + }); + } + + List> futures = pool.invokeAll(tasks); + for (Future f : futures) { + f.get(30, TimeUnit.SECONDS); + } + + // Every consumer received an instance, none was null, and the + // final handout equals the map's current server. + assertEquals(threads * cyclesPerThread, handedOut.size()); + assertTrue(handedOut.contains(JSONRPCController.getRPCServer(coin))); + } +} From fa7671795ccd766d606684d6a0390f6ac8e9dfb4 Mon Sep 17 00:00:00 2001 From: tryiou Date: Wed, 26 Aug 2026 02:20:00 +0200 Subject: [PATCH 58/73] refactor(coins): delist XLQ/PHR/BAY The three coins are absent from blockchain-configuration-files (the coin/config source of truth) and are now dropped from the daemon: - CoinTicker: ALQOCOIN, PHORECOIN and BITBAY enum constants removed - CoinTickerUtils: ticker-map and active-tickers entries removed - CoinInstance: dead switch cases and commented imports removed - protocols/{alqocoin,phorecoin,bitbay}/ deleted together with their NetworkParameters classes - WalletHelperFeeTest: the six fee assertions for those coins removed grep across src/ finds zero remaining references. BCH stays disabled but present; TBLOCK untouched. --- .../io/cloudchains/app/net/CoinInstance.java | 20 ---- .../io/cloudchains/app/net/CoinTicker.java | 7 -- .../cloudchains/app/net/CoinTickerUtils.java | 9 -- .../alqocoin/AlqocoinNetworkParameters.java | 107 ------------------ .../bitbay/BitbayNetworkParameters.java | 107 ------------------ .../phorecoin/PhorecoinNetworkParameters.java | 107 ------------------ src/test/java/WalletHelperFeeTest.java | 39 ------- 7 files changed, 396 deletions(-) delete mode 100644 src/main/java/io/cloudchains/app/net/protocols/alqocoin/AlqocoinNetworkParameters.java delete mode 100644 src/main/java/io/cloudchains/app/net/protocols/bitbay/BitbayNetworkParameters.java delete mode 100644 src/main/java/io/cloudchains/app/net/protocols/phorecoin/PhorecoinNetworkParameters.java diff --git a/src/main/java/io/cloudchains/app/net/CoinInstance.java b/src/main/java/io/cloudchains/app/net/CoinInstance.java index 9914bbe..5a78eed 100644 --- a/src/main/java/io/cloudchains/app/net/CoinInstance.java +++ b/src/main/java/io/cloudchains/app/net/CoinInstance.java @@ -9,8 +9,6 @@ import io.cloudchains.app.crypto.KeyHandler; import io.cloudchains.app.net.api.JSONRPCController; import io.cloudchains.app.net.api.JSONRPCServer; -//import io.cloudchains.app.net.protocols.alqocoin.AlqocoinNetworkParameters; -//import io.cloudchains.app.net.protocols.bitbay.BitbayNetworkParameters; //import io.cloudchains.app.net.protocols.bitcoincash.BitcoinCashNetworkParameters; import io.cloudchains.app.net.protocols.bitcoin.BitcoinNetworkParameters; import io.cloudchains.app.net.protocols.blocknet.*; @@ -18,7 +16,6 @@ import io.cloudchains.app.net.protocols.digibyte.DigibyteNetworkParameters; import io.cloudchains.app.net.protocols.dogecoin.DogecoinNetworkParameters; import io.cloudchains.app.net.protocols.litecoin.LitecoinNetworkParameters; -//import io.cloudchains.app.net.protocols.phorecoin.PhorecoinNetworkParameters; import io.cloudchains.app.net.protocols.pivx.PivxNetworkParameters; import io.cloudchains.app.net.protocols.pocketcoin.PocketcoinNetworkParameters; import io.cloudchains.app.net.protocols.ravencoin.RavencoinNetworkParameters; @@ -406,11 +403,6 @@ public CoinError init(char[] pw, String userMnemonic, boolean isMnemonic, boolea rpcPort = 8370; break; } - // case BITBAY: { - // networkParameters = new BitbayNetworkParameters(); - // rpcPort = 19915; - // break; - // } case PIVX: { LOGGER.fine("[coin] Initializing for Pivx main network."); networkParameters = new PivxNetworkParameters(); @@ -429,18 +421,6 @@ public CoinError init(char[] pw, String userMnemonic, boolean isMnemonic, boolea rpcPort = 37071; break; } - // case ALQOCOIN: { - // LOGGER.fine("[coin] Initializing for Alqo main network."); - // networkParameters = new AlqocoinNetworkParameters(); - // rpcPort = 55000; - // break; - // } - // case PHORECOIN: { - // LOGGER.fine("[coin] Initializing for Phore main network."); - // networkParameters = new PhorecoinNetworkParameters(); - // rpcPort = 11772; - // break; - // } case RAVENCOIN: { LOGGER.fine("[coin] Initializing for Ravencoin main network."); networkParameters = new RavencoinNetworkParameters(); diff --git a/src/main/java/io/cloudchains/app/net/CoinTicker.java b/src/main/java/io/cloudchains/app/net/CoinTicker.java index 4d6cf71..6f112dd 100644 --- a/src/main/java/io/cloudchains/app/net/CoinTicker.java +++ b/src/main/java/io/cloudchains/app/net/CoinTicker.java @@ -15,10 +15,7 @@ public enum CoinTicker { DOGECOIN, SYSCOIN, PIVX, - ALQOCOIN, - PHORECOIN, RAVENCOIN, - BITBAY, UNOBTANIUM, PKOIN ; @@ -42,11 +39,7 @@ public static List coins() { PIVX, UNOBTANIUM, PKOIN, -// ALQOCOIN, - not support on backend - -// PHORECOIN, - not support on backend RAVENCOIN -// BITBAY - not support on backend ); } } diff --git a/src/main/java/io/cloudchains/app/net/CoinTickerUtils.java b/src/main/java/io/cloudchains/app/net/CoinTickerUtils.java index 5a8db9c..499684b 100644 --- a/src/main/java/io/cloudchains/app/net/CoinTickerUtils.java +++ b/src/main/java/io/cloudchains/app/net/CoinTickerUtils.java @@ -25,10 +25,6 @@ public class CoinTickerUtils { // tickers.put(CoinTicker.BITCOIN_CASH, "BCH"); tickers.put(CoinTicker.RAVENCOIN, "RVN"); - tickers.put(CoinTicker.ALQOCOIN, "XLQ"); - // TODO Temporarily disable PHORE until supported -// tickers.put(CoinTicker.PHORECOIN, "PHR"); - tickers.put(CoinTicker.BITBAY, "BAY"); tickers.put(CoinTicker.UNOBTANIUM, "UNO"); tickers.put(CoinTicker.PKOIN, "PKOIN"); @@ -60,11 +56,6 @@ public static CoinTicker[] getActiveTickers() { // CoinTicker.BITCOIN_CASH, CoinTicker.RAVENCOIN, - CoinTicker.ALQOCOIN, - // TODO Temporarily disable PHORE until supported -// CoinTicker.PHORECOIN, - - CoinTicker.BITBAY, CoinTicker.UNOBTANIUM, CoinTicker.PKOIN, }; diff --git a/src/main/java/io/cloudchains/app/net/protocols/alqocoin/AlqocoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/alqocoin/AlqocoinNetworkParameters.java deleted file mode 100644 index 3850528..0000000 --- a/src/main/java/io/cloudchains/app/net/protocols/alqocoin/AlqocoinNetworkParameters.java +++ /dev/null @@ -1,107 +0,0 @@ -package io.cloudchains.app.net.protocols.alqocoin; - -import io.cloudchains.app.net.HasFeeParams; -import org.bitcoinj.core.*; -import org.bitcoinj.store.BlockStore; -import org.bitcoinj.store.BlockStoreException; -import org.bitcoinj.utils.MonetaryFormat; - -public class AlqocoinNetworkParameters extends NetworkParameters implements HasFeeParams { - - public AlqocoinNetworkParameters() { - super(); - } - - @Override - public String getPaymentProtocolId() { - return "main"; - } - - @Override - public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException { - } - - @Override - public Coin getMaxMoney() { - return Coin.valueOf(100000000 * Coin.COIN.value); - } - - @Override - public Coin getMinNonDustOutput() { - return Transaction.MIN_NONDUST_OUTPUT; - } - - @Override - public MonetaryFormat getMonetaryFormat() { - return new MonetaryFormat().code(0, "XLQ"); - } - - @Override - public String getUriScheme() { - return "alqocoin:"; - } - - @Override - public boolean hasMaxMoney() { - return true; - } - - @Override - public BitcoinSerializer getSerializer(boolean parseRetain) { - return new BitcoinSerializer(this, parseRetain); - } - - @Override - public int getProtocolVersionNum(ProtocolVersion version) { - return 70719; - } - - @Override - public int getAddressHeader() { - return 23; - } - - @Override - public int getP2SHHeader() { - return 16; - } - - @Override - public int getDumpedPrivateKeyHeader() { - return 193; - } - - - @Override - public int getBip32HeaderP2PKHpriv() { - return 0x0488ADE4; - } - - @Override - public int getBip32HeaderP2PKHpub() { - return 0x0488B21E; - } - - @Override - public int getSubsidyDecreaseBlockCount() { - return 210240; - } - - @Override - public int getInterval() { - return 60; - } - - @Override - public String getId() { - return "XLQ"; - } - - public long getFeePerByte() { - return 20; - } - - public long getMinTxFee() { - return 10000; - } -} diff --git a/src/main/java/io/cloudchains/app/net/protocols/bitbay/BitbayNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/bitbay/BitbayNetworkParameters.java deleted file mode 100644 index 16287fc..0000000 --- a/src/main/java/io/cloudchains/app/net/protocols/bitbay/BitbayNetworkParameters.java +++ /dev/null @@ -1,107 +0,0 @@ -package io.cloudchains.app.net.protocols.bitbay; - -import io.cloudchains.app.net.HasFeeParams; -import org.bitcoinj.core.*; -import org.bitcoinj.store.BlockStore; -import org.bitcoinj.store.BlockStoreException; -import org.bitcoinj.utils.MonetaryFormat; - -public class BitbayNetworkParameters extends NetworkParameters implements HasFeeParams { - - public BitbayNetworkParameters() { - super(); - } - - @Override - public String getPaymentProtocolId() { - return "main"; - } - - @Override - public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException { - } - - @Override - public Coin getMaxMoney() { - return Coin.valueOf(100000000 * Coin.COIN.value); - } - - @Override - public Coin getMinNonDustOutput() { - return Transaction.MIN_NONDUST_OUTPUT; - } - - @Override - public MonetaryFormat getMonetaryFormat() { - return new MonetaryFormat().code(0, "BAY"); - } - - @Override - public String getUriScheme() { - return "bitbay:"; - } - - @Override - public boolean hasMaxMoney() { - return true; - } - - @Override - public BitcoinSerializer getSerializer(boolean parseRetain) { - return new BitcoinSerializer(this, parseRetain); - } - - @Override - public int getProtocolVersionNum(ProtocolVersion version) { - return 70719; - } - - @Override - public int getAddressHeader() { - return 25; - } - - @Override - public int getP2SHHeader() { - return 85; - } - - @Override - public int getDumpedPrivateKeyHeader() { - return 153; - } - - - @Override - public int getBip32HeaderP2PKHpriv() { - return 0x0488ADE4; - } - - @Override - public int getBip32HeaderP2PKHpub() { - return 0x0488B21E; - } - - @Override - public int getSubsidyDecreaseBlockCount() { - return 210240; - } - - @Override - public int getInterval() { - return 60; - } - - @Override - public String getId() { - return "BAY"; - } - - public long getFeePerByte() { - return 100; - } - - public long getMinTxFee() { - return 20000; - } -} diff --git a/src/main/java/io/cloudchains/app/net/protocols/phorecoin/PhorecoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/phorecoin/PhorecoinNetworkParameters.java deleted file mode 100644 index 123816d..0000000 --- a/src/main/java/io/cloudchains/app/net/protocols/phorecoin/PhorecoinNetworkParameters.java +++ /dev/null @@ -1,107 +0,0 @@ -package io.cloudchains.app.net.protocols.phorecoin; - -import io.cloudchains.app.net.HasFeeParams; -import org.bitcoinj.core.*; -import org.bitcoinj.store.BlockStore; -import org.bitcoinj.store.BlockStoreException; -import org.bitcoinj.utils.MonetaryFormat; - -public class PhorecoinNetworkParameters extends NetworkParameters implements HasFeeParams { - - public PhorecoinNetworkParameters() { - super(); - } - - @Override - public String getPaymentProtocolId() { - return "main"; - } - - @Override - public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) throws VerificationException, BlockStoreException { - } - - @Override - public Coin getMaxMoney() { - return Coin.valueOf(100000000 * Coin.COIN.value); - } - - @Override - public Coin getMinNonDustOutput() { - return Transaction.MIN_NONDUST_OUTPUT; - } - - @Override - public MonetaryFormat getMonetaryFormat() { - return new MonetaryFormat().code(0, "PHR"); - } - - @Override - public String getUriScheme() { - return "phore:"; - } - - @Override - public boolean hasMaxMoney() { - return true; - } - - @Override - public BitcoinSerializer getSerializer(boolean parseRetain) { - return new BitcoinSerializer(this, parseRetain); - } - - @Override - public int getProtocolVersionNum(ProtocolVersion version) { - return 70007; - } - - @Override - public int getAddressHeader() { - return 55; - } - - @Override - public int getP2SHHeader() { - return 13; - } - - @Override - public int getDumpedPrivateKeyHeader() { - return 212; - } - - - @Override - public int getBip32HeaderP2PKHpriv() { - return 0x0221312B; - } - - @Override - public int getBip32HeaderP2PKHpub() { - return 0x022D2533; - } - - @Override - public int getSubsidyDecreaseBlockCount() { - return 210240; - } - - @Override - public int getInterval() { - return 60; - } - - @Override - public String getId() { - return "PHR"; - } - - public long getFeePerByte() { - return 20; - } - - public long getMinTxFee() { - return 10000; - } -} diff --git a/src/test/java/WalletHelperFeeTest.java b/src/test/java/WalletHelperFeeTest.java index b96a8a5..91ad881 100644 --- a/src/test/java/WalletHelperFeeTest.java +++ b/src/test/java/WalletHelperFeeTest.java @@ -1,5 +1,3 @@ -import io.cloudchains.app.net.protocols.alqocoin.AlqocoinNetworkParameters; -import io.cloudchains.app.net.protocols.bitbay.BitbayNetworkParameters; import io.cloudchains.app.net.protocols.bitcoin.BitcoinNetworkParameters; import io.cloudchains.app.net.protocols.blocknet.BlocknetNetworkParameters; import io.cloudchains.app.net.protocols.blocknet.BlocknetTestnet5NetworkParameters; @@ -7,7 +5,6 @@ import io.cloudchains.app.net.protocols.digibyte.DigibyteNetworkParameters; import io.cloudchains.app.net.protocols.dogecoin.DogecoinNetworkParameters; import io.cloudchains.app.net.protocols.litecoin.LitecoinNetworkParameters; -import io.cloudchains.app.net.protocols.phorecoin.PhorecoinNetworkParameters; import io.cloudchains.app.net.protocols.pivx.PivxNetworkParameters; import io.cloudchains.app.net.protocols.pocketcoin.PocketcoinNetworkParameters; import io.cloudchains.app.net.protocols.ravencoin.RavencoinNetworkParameters; @@ -105,30 +102,12 @@ void testGetFeePerByte_Ravencoin() { assertEquals(1000L, WalletHelper.getFeePerByte(params)); } - @Test - void testGetFeePerByte_Alqocoin() { - AlqocoinNetworkParameters params = new AlqocoinNetworkParameters(); - assertEquals(20L, WalletHelper.getFeePerByte(params)); - } - - @Test - void testGetFeePerByte_Bitbay() { - BitbayNetworkParameters params = new BitbayNetworkParameters(); - assertEquals(100L, WalletHelper.getFeePerByte(params)); - } - @Test void testGetFeePerByte_BlocknetTestnet5() { BlocknetTestnet5NetworkParameters params = new BlocknetTestnet5NetworkParameters(); assertEquals(20L, WalletHelper.getFeePerByte(params)); } - @Test - void testGetFeePerByte_Phorecoin() { - PhorecoinNetworkParameters params = new PhorecoinNetworkParameters(); - assertEquals(20L, WalletHelper.getFeePerByte(params)); - } - // ======================================================================== // Happy Path - getMinTxFee() tests // ======================================================================== @@ -199,30 +178,12 @@ void testGetMinTxFee_Ravencoin() { assertEquals(100000L, WalletHelper.getMinTxFee(params)); } - @Test - void testGetMinTxFee_Alqocoin() { - AlqocoinNetworkParameters params = new AlqocoinNetworkParameters(); - assertEquals(10000L, WalletHelper.getMinTxFee(params)); - } - - @Test - void testGetMinTxFee_Bitbay() { - BitbayNetworkParameters params = new BitbayNetworkParameters(); - assertEquals(20000L, WalletHelper.getMinTxFee(params)); - } - @Test void testGetMinTxFee_BlocknetTestnet5() { BlocknetTestnet5NetworkParameters params = new BlocknetTestnet5NetworkParameters(); assertEquals(10000L, WalletHelper.getMinTxFee(params)); } - @Test - void testGetMinTxFee_Phorecoin() { - PhorecoinNetworkParameters params = new PhorecoinNetworkParameters(); - assertEquals(10000L, WalletHelper.getMinTxFee(params)); - } - // ======================================================================== // Edge Case - unknown coin returns default // ======================================================================== From a4e3c1e0ecee49395074fa9cacfb61c1969fde04 Mon Sep 17 00:00:00 2001 From: tryiou Date: Wed, 26 Aug 2026 10:30:26 +0200 Subject: [PATCH 59/73] feat(coinconfig): load coin parameters from blockchain-configuration-files New package io.cloudchains.app.coinconfig - the data source for replacing the hardcoded per-coin parameter classes: - ConfigSourceResolver: source precedence flag --blockchain-configuration-files > env BLOCKCHAIN_CONFIGURATION_FILES > upstream blocknetdx master; accepts a local checkout directory or a URL; github.com web URLs normalize to their raw form (.git suffix, /tree//blob/ branch paths - deeper paths fail hard; case-insensitive schemes) - XBridgeConfParser: [SECTION] Key=Value parsing with fail-hard semantics - malformed lines and headers throw with line/section context instead of being dropped; UTF-8 BOM tolerated; duplicate keys last-win, duplicate sections merge (documented) - CoinConfigSource: reads manifest-latest.json (top-level array or contracts-wrapped object) plus every coin's xbridge conf in one pass; missing files/fields/sections and duplicate tickers abort loading with precise context; empty manifest aborts - CoinConfig: immutable per-ticker view exposing typed accessors (address/script/secret prefixes, COIN factor, fees, port) and the full raw entry map; numeric errors carry ticker/key context Not yet wired into coin initialization; that follows separately. Two neutral review rounds passed against all 41 shipped entries; suite at 155 green. --- .../app/coinconfig/CoinConfig.java | 138 ++++++++++++++++ .../app/coinconfig/CoinConfigSource.java | 156 ++++++++++++++++++ .../app/coinconfig/ConfigSourceResolver.java | 92 +++++++++++ .../app/coinconfig/XBridgeConfParser.java | 62 +++++++ .../coinconfig/CoinConfigSourceLocalTest.java | 113 +++++++++++++ .../app/coinconfig/CoinConfigTest.java | 81 +++++++++ .../coinconfig/ConfigSourceResolverTest.java | 90 ++++++++++ .../app/coinconfig/XBridgeConfParserTest.java | 98 +++++++++++ 8 files changed, 830 insertions(+) create mode 100644 src/main/java/io/cloudchains/app/coinconfig/CoinConfig.java create mode 100644 src/main/java/io/cloudchains/app/coinconfig/CoinConfigSource.java create mode 100644 src/main/java/io/cloudchains/app/coinconfig/ConfigSourceResolver.java create mode 100644 src/main/java/io/cloudchains/app/coinconfig/XBridgeConfParser.java create mode 100644 src/test/java/io/cloudchains/app/coinconfig/CoinConfigSourceLocalTest.java create mode 100644 src/test/java/io/cloudchains/app/coinconfig/CoinConfigTest.java create mode 100644 src/test/java/io/cloudchains/app/coinconfig/ConfigSourceResolverTest.java create mode 100644 src/test/java/io/cloudchains/app/coinconfig/XBridgeConfParserTest.java diff --git a/src/main/java/io/cloudchains/app/coinconfig/CoinConfig.java b/src/main/java/io/cloudchains/app/coinconfig/CoinConfig.java new file mode 100644 index 0000000..e1f2213 --- /dev/null +++ b/src/main/java/io/cloudchains/app/coinconfig/CoinConfig.java @@ -0,0 +1,138 @@ +package io.cloudchains.app.coinconfig; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Immutable view of one coin's configuration as carried by the + * blockchain-configuration-files repository (manifest entry + the coin's + * section inside its xbridge conf file). + * + *

Values are exposed exactly as parsed; interpretation and range checks + * belong to the parameter layer built on top of this class, not here.

+ */ +public final class CoinConfig { + + private final String ticker; + private final String blockchain; + private final String verId; + private final Map confEntries; + + public CoinConfig(String ticker, String blockchain, String verId, Map confEntries) { + this.ticker = Objects.requireNonNull(ticker, "ticker"); + this.blockchain = Objects.requireNonNull(blockchain, "blockchain"); + this.verId = verId == null ? "" : verId; + this.confEntries = Collections.unmodifiableMap(new LinkedHashMap<>( + Objects.requireNonNull(confEntries, "confEntries"))); + } + + public String getTicker() { + return ticker; + } + + public String getBlockchain() { + return blockchain; + } + + public String getVerId() { + return verId; + } + + /** Raw key/value map of the coin's xbridge-conf section, as parsed. */ + public Map getConfEntries() { + return confEntries; + } + + private String required(String key) { + String v = confEntries.get(key); + if (v == null || v.trim().isEmpty()) + throw new IllegalStateException("[" + ticker + "] missing required config key '" + key + "'"); + return v.trim(); + } + + private long requiredLong(String key) { + try { + return Long.parseLong(required(key)); + } catch (NumberFormatException e) { + throw new IllegalStateException("[" + ticker + "] config key '" + key + + "' is not a number: '" + confEntries.get(key) + "'", e); + } + } + + private int requiredInt(String key) { + try { + return Math.toIntExact(requiredLong(key)); + } catch (ArithmeticException e) { + throw new IllegalStateException("[" + ticker + "] config key '" + key + + "' exceeds the int range: " + confEntries.get(key), e); + } + } + + public int addressPrefix() { + return requiredInt("AddressPrefix"); + } + + public int scriptPrefix() { + return requiredInt("ScriptPrefix"); + } + + public int secretPrefix() { + return requiredInt("SecretPrefix"); + } + + public long coinFactor() { + return requiredLong("COIN"); + } + + public long feePerByte() { + return requiredLong("FeePerByte"); + } + + public long minTxFee() { + return requiredLong("MinTxFee"); + } + + /** + * Optional in current bcf data: every coin carries DustAmount=0 placeholders. + * Absent/blank yields null; a present-but-malformed value throws. + */ + public Long dustAmountOrNull() { + String v = confEntries.get("DustAmount"); + if (v == null || v.trim().isEmpty()) + return null; + try { + return Long.parseLong(v.trim()); + } catch (NumberFormatException e) { + throw new IllegalStateException("[" + ticker + "] config key 'DustAmount" + + "' is not a number: '" + v + "'", e); + } + } + + /** Wallet listen port from the xbridge conf; wallet-conf rpcport may override per user settings. */ + public int port() { + return requiredInt("Port"); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof CoinConfig)) return false; + CoinConfig that = (CoinConfig) o; + return ticker.equals(that.ticker) + && blockchain.equals(that.blockchain) + && verId.equals(that.verId) + && confEntries.equals(that.confEntries); + } + + @Override + public int hashCode() { + return Objects.hash(ticker, blockchain, verId, confEntries); + } + + @Override + public String toString() { + return "CoinConfig{" + ticker + "/" + verId + ", keys=" + confEntries.keySet() + '}'; + } +} diff --git a/src/main/java/io/cloudchains/app/coinconfig/CoinConfigSource.java b/src/main/java/io/cloudchains/app/coinconfig/CoinConfigSource.java new file mode 100644 index 0000000..46c2856 --- /dev/null +++ b/src/main/java/io/cloudchains/app/coinconfig/CoinConfigSource.java @@ -0,0 +1,156 @@ +package io.cloudchains.app.coinconfig; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Scanner; + +/** + * Loads every coin's {@link CoinConfig} from a blockchain-configuration-files + * source (local directory or base URL), resolving once at daemon start. + * + *

Failure policy is fail-hard: any unreadable manifest, missing conf file, + * missing conf section or unparsable value throws; the daemon refuses to + * start rather than guess coin parameters.

+ */ +public final class CoinConfigSource { + + private final String base; + + /** + * @param base the resolved config source: either a local checkout directory + * containing {@code manifest-latest.json} and {@code xbridge-confs/}, + * or a RAW base URL under which those relative paths resolve + * (as produced by ConfigSourceResolver.resolve()). + */ + public CoinConfigSource(String base) { + this.base = base.replaceAll("/+$", ""); + } + + /** @return ticker -> config, ordered by manifest appearance */ + public Map loadAll() { + String manifestJson = isLocal() ? readLocalFile(localManifest()) : fetchRemote(manifestUrl()); + JsonArray entries; + try { + JsonElement root = JsonParser.parseString(manifestJson); + if (root.isJsonArray()) { + entries = root.getAsJsonArray(); + } else { + entries = root.getAsJsonObject().getAsJsonArray("contracts"); + if (entries == null) + throw new IllegalStateException("object manifest has no 'contracts' array"); + } + } catch (RuntimeException e) { + throw new IllegalStateException("manifest: " + e.getMessage(), e); + } + + if (entries.size() == 0) + throw new IllegalStateException("manifest lists no coins"); + Map out = new LinkedHashMap<>(); + int index = 0; + for (JsonElement el : entries) { + index++; + JsonObject entry; + try { + entry = el.getAsJsonObject(); + final String ticker = requiredField(entry, "ticker", index); + final String blockchain = requiredField(entry, "blockchain", index); + final String verId = entry.has("ver_id") ? entry.get("ver_id").getAsString() : ""; + final String xbridgeConf = requiredField(entry, "xbridge_conf", index); + + CoinConfig prev = out.get(ticker); + if (prev != null) + throw new IllegalStateException("duplicate manifest ticker " + ticker); + + Map> sections = isLocal() + ? safeParseLocal(localXBridgeConf(xbridgeConf)) + : XBridgeConfParser.parse(fetchRemote(xbridgeConfUrl(xbridgeConf))); + Map section = sections.get(ticker); + if (section == null) + throw new IllegalStateException("xbridge conf " + xbridgeConf + + " has no [" + ticker + "] section"); + out.put(ticker, new CoinConfig(ticker, blockchain, verId, section)); + } catch (RuntimeException e) { + throw new IllegalStateException("manifest entry #" + index + ": " + e.getMessage(), e); + } + } + return out; + } + + private static String requiredField(JsonObject entry, String field, int index) { + JsonElement el = entry.get(field); + if (el == null || el.isJsonNull()) + throw new IllegalStateException("manifest entry #" + index + ": missing '" + field + "'"); + return el.getAsString(); + } + + private boolean isLocal() { + return ConfigSourceResolver.isLocalDirectory(base); + } + + private Path localManifest() { + return Paths.get(base, "manifest-latest.json"); + } + + private Path localXBridgeConf(String fileName) { + return Paths.get(base, "xbridge-confs", fileName); + } + + private String manifestUrl() { + return base + "/manifest-latest.json"; + } + + private String xbridgeConfUrl(String fileName) { + return base + "/xbridge-confs/" + fileName; + } + + private static String readLocalFile(Path p) { + try { + byte[] b = Files.readAllBytes(p); + return new String(b, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new IllegalStateException("Cannot read " + p + ": " + e.getMessage(), e); + } + } + + private static Map> safeParseLocal(Path p) { + try { + return XBridgeConfParser.parseFile(p); + } catch (IOException e) { + throw new IllegalStateException("Cannot read " + p + ": " + e.getMessage(), e); + } + } + + private static String fetchRemote(String url) { + HttpURLConnection conn = null; + try { + conn = (HttpURLConnection) new URL(url).openConnection(); + conn.setConnectTimeout(10_000); + conn.setReadTimeout(30_000); + int code = conn.getResponseCode(); + if (code != 200) + throw new IllegalStateException("HTTP " + code + " fetching " + url); + try (InputStream in = conn.getInputStream(); Scanner s = + new Scanner(in, StandardCharsets.UTF_8.name()).useDelimiter("\\A")) { + return s.hasNext() ? s.next() : ""; + } + } catch (IOException e) { + throw new IllegalStateException("Cannot fetch " + url + ": " + e.getMessage(), e); + } finally { + if (conn != null) + conn.disconnect(); + } + } +} diff --git a/src/main/java/io/cloudchains/app/coinconfig/ConfigSourceResolver.java b/src/main/java/io/cloudchains/app/coinconfig/ConfigSourceResolver.java new file mode 100644 index 0000000..fddc796 --- /dev/null +++ b/src/main/java/io/cloudchains/app/coinconfig/ConfigSourceResolver.java @@ -0,0 +1,92 @@ +package io.cloudchains.app.coinconfig; + +import java.util.Locale; + +/** + * Resolves the blockchain-configuration-files source from, in order of + * precedence: CLI flag {@code --blockchain-configuration-files}, environment + * variable {@code BLOCKCHAIN_CONFIGURATION_FILES}, then the upstream + * blocknetdx default (master branch). + * + *

A source value is either a local filesystem directory that contains + * {@code manifest-latest.json} and {@code xbridge-confs/}, or a base URL under + * which those same relative paths resolve (a raw GitHub URL, or a + * github.com repo URL which is normalized to its raw form).

+ */ +public final class ConfigSourceResolver { + + public static final String FLAG_NAME = "--blockchain-configuration-files"; + public static final String ENV_NAME = "BLOCKCHAIN_CONFIGURATION_FILES"; + public static final String DEFAULT_UPSTREAM = + "https://raw.githubusercontent.com/blocknetdx/blockchain-configuration-files/master"; + + private final String flagValue; + private final String envValue; + + public ConfigSourceResolver(String flagValue, String envValue) { + this.flagValue = normalize(flagValue); + this.envValue = normalize(envValue); + } + + /** @return true when the base is a filesystem directory rather than a URL. */ + public static boolean isLocalDirectory(String base) { + String lower = base.toLowerCase(Locale.ROOT); + return !lower.startsWith("http://") && !lower.startsWith("https://"); + } + + /** @return the resolved source value (never blank); local dir or base URL. */ + public String resolve() { + if (flagValue != null) + return toBase(flagValue); + if (envValue != null) + return toBase(envValue); + return DEFAULT_UPSTREAM; + } + + /** + * Normalizes a github.com web URL ({@code github.com/o/r} or + * {@code .../tree/branch}) to its raw base; other values pass through. + */ + static String toBase(String value) { + String v = normalize(value); + if (v == null) + throw new IllegalArgumentException(FLAG_NAME + ": empty config source"); + String lower = v.toLowerCase(Locale.ROOT); + if (lower.startsWith("https://github.com/") || lower.startsWith("http://github.com/")) { + int hostIdx = lower.indexOf("github.com/") + "github.com/".length(); + String rest = v.substring(hostIdx); + String lowerRest = lower.substring(hostIdx); + while (lowerRest.endsWith("/") || rest.endsWith("/")) { + lowerRest = lowerRest.replaceAll("/+$", ""); + rest = rest.replaceAll("/+$", ""); + } + if (lowerRest.endsWith(".git")) { + lowerRest = lowerRest.substring(0, lowerRest.length() - ".git".length()); + rest = rest.substring(0, rest.length() - ".git".length()); + } + String branch = "master"; + for (String marker : new String[]{"/tree/", "/blob/"}) { + int idx = lowerRest.indexOf(marker); + if (idx >= 0) { + branch = rest.substring(idx + marker.length()); + rest = rest.substring(0, idx); + if (branch.isEmpty() || branch.contains("/")) + throw new IllegalArgumentException( + "unsupported github URL form '" + value + + "' (expected github.com//[.git][/tree/])"); + break; + } + } + String scheme = lower.startsWith("http://") ? "http://" : "https://"; + return scheme + "raw.githubusercontent.com/" + rest + "/" + branch; + } + return v.replaceAll("/+$", ""); + } + + static String normalize(String v) { + if (v == null) + return null; + String t = v.trim(); + return t.isEmpty() ? null : t; + } +} diff --git a/src/main/java/io/cloudchains/app/coinconfig/XBridgeConfParser.java b/src/main/java/io/cloudchains/app/coinconfig/XBridgeConfParser.java new file mode 100644 index 0000000..a19579f --- /dev/null +++ b/src/main/java/io/cloudchains/app/coinconfig/XBridgeConfParser.java @@ -0,0 +1,62 @@ +package io.cloudchains.app.coinconfig; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Parses one xbridge conf file into per-section key/value maps. + * + *

Format (as shipped by blockchain-configuration-files): sections opened by + * a {@code [TICKER]} line, followed by {@code Key=Value} lines. Blank lines and + * {@code #}/{@code ;} comments are skipped. Keys keep their original case; + * lookups are case-sensitive, matching the files as shipped.

+ * + *

Parsing fails hard: a UTF-8 BOM is tolerated and stripped, but any other + * stray or malformed line throws rather than being dropped silently. A section + * header must be exactly {@code [NAME]} (no nested brackets). Duplicates are + * deterministic: a repeated key keeps its LAST value; a repeated section name + * merges into the first occurrence.

+ */ +public final class XBridgeConfParser { + + public static Map> parse(String contents) { + if (contents.startsWith("\uFEFF")) + contents = contents.substring(1); + Map> sections = new LinkedHashMap<>(); + String current = null; + int lineNo = 0; + for (String rawLine : contents.split("\\r?\\n", -1)) { + lineNo++; + String line = rawLine.trim(); + if (line.isEmpty() || line.startsWith("#") || line.startsWith(";")) + continue; + if (line.startsWith("[") && line.endsWith("]")) { + current = line.substring(1, line.length() - 1).trim(); + if (current.isEmpty() || current.indexOf('[') >= 0 || current.indexOf(']') >= 0) + throw new IllegalStateException("Malformed section header at line " + lineNo + + ": '" + rawLine + "'"); + sections.putIfAbsent(current, new LinkedHashMap<>()); + continue; + } + if (current == null) + throw new IllegalStateException( + "Malformed line " + lineNo + " outside any section: '" + rawLine + "'"); + int eq = line.indexOf('='); + if (eq <= 0) + throw new IllegalStateException( + "Malformed line " + lineNo + " in [" + current + "]: '" + rawLine + "'"); + String key = line.substring(0, eq).trim(); + String value = line.substring(eq + 1).trim(); + sections.get(current).put(key, value); + } + return sections; + } + + public static Map> parseFile(Path file) throws IOException { + return parse(new String(Files.readAllBytes(file), StandardCharsets.UTF_8)); + } +} diff --git a/src/test/java/io/cloudchains/app/coinconfig/CoinConfigSourceLocalTest.java b/src/test/java/io/cloudchains/app/coinconfig/CoinConfigSourceLocalTest.java new file mode 100644 index 0000000..a18e76b --- /dev/null +++ b/src/test/java/io/cloudchains/app/coinconfig/CoinConfigSourceLocalTest.java @@ -0,0 +1,113 @@ +package io.cloudchains.app.coinconfig; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Exercises {@link CoinConfigSource} against the workspace's real + * blockchain-configuration-files checkout — the same data the daemon will + * consume in production, without any network. The workspace layout is a + * deliberate precondition: this daemon is developed against that sibling + * checkout, so its absence is an error, not a skip. + */ +class CoinConfigSourceLocalTest { + + private static final Path BCF = Paths.get( + "..", "blockchain-configuration-files"); + + private Map loadAll() { + return new CoinConfigSource(BCF.toAbsolutePath().normalize().toString()).loadAll(); + } + + @Test + void testLoadsEveryManifestCoin() { + Map all = loadAll(); + assertFalse(all.isEmpty()); + assertTrue(all.containsKey("LTC"), "LTC must be present"); + assertTrue(all.containsKey("BLOCK"), "BLOCK must be present"); + } + + @Test + void testLitecoinValuesMatchShippedConf() { + CoinConfig ltc = loadAll().get("LTC"); + assertEquals("Litecoin", ltc.getBlockchain()); + assertEquals(48, ltc.addressPrefix()); + assertEquals(50, ltc.scriptPrefix()); + assertEquals(176, ltc.secretPrefix()); + assertEquals(100_000_000L, ltc.coinFactor()); + assertEquals(10L, ltc.feePerByte()); + assertEquals(5000L, ltc.minTxFee()); + assertEquals(9332, ltc.port()); + assertEquals(0L, ltc.dustAmountOrNull()); + } + + @Test + void testConfEntriesExposeFullRawMap() { + CoinConfig ltc = loadAll().get("LTC"); + assertEquals("Litecoin", ltc.getConfEntries().get("Title")); + assertTrue(ltc.getConfEntries().containsKey("TxVersion")); + } + + @Test + void testMissingSourceFailsHard() { + CoinConfigSource bad = new CoinConfigSource( + Paths.get("..", "no-such-bcf-dir").toAbsolutePath().normalize().toString()); + IllegalStateException e = assertThrows(IllegalStateException.class, bad::loadAll); + assertTrue(e.getMessage().contains("Cannot read")); + } + + @Test + void testManifestEntryWithoutSectionFailsHard(@TempDir Path dir) throws Exception { + Files.createDirectories(dir.resolve("xbridge-confs")); + Files.writeString(dir.resolve("manifest-latest.json"), + "[{\"blockchain\":\"Fake\",\"ticker\":\"FAKE\"," + + "\"xbridge_conf\":\"fake--v1.conf\"}]"); + Files.writeString(dir.resolve("xbridge-confs").resolve("fake--v1.conf"), + "[OTHER]\nAddressPrefix=1\n"); + IllegalStateException e = assertThrows(IllegalStateException.class, + () -> new CoinConfigSource(dir.toString()).loadAll()); + assertTrue(e.getMessage().contains("has no [FAKE] section"), e.getMessage()); + } + + @Test + void testDuplicateManifestTickerFailsHard(@TempDir Path dir) throws Exception { + Files.createDirectories(dir.resolve("xbridge-confs")); + Files.writeString(dir.resolve("manifest-latest.json"), + "[{\"blockchain\":\"A\",\"ticker\":\"DUP\",\"xbridge_conf\":\"a.conf\"}," + + "{\"blockchain\":\"B\",\"ticker\":\"DUP\",\"xbridge_conf\":\"b.conf\"}]"); + Files.writeString(dir.resolve("xbridge-confs").resolve("a.conf"), "[DUP]\nK=V\n"); + // b.conf deliberately ABSENT: if the loader read confs before the + // duplicate check, the failure would be "Cannot read" instead. + IllegalStateException e = assertThrows(IllegalStateException.class, + () -> new CoinConfigSource(dir.toString()).loadAll()); + assertTrue(e.getMessage().contains("duplicate manifest ticker DUP"), e.getMessage()); + } + + @Test + void testBadRootShapeFailsHardWithManifestContext(@TempDir Path dir) throws Exception { + Files.writeString(dir.resolve("manifest-latest.json"), "{}"); + IllegalStateException e = assertThrows(IllegalStateException.class, + () -> new CoinConfigSource(dir.toString()).loadAll()); + assertTrue(e.getMessage().startsWith("manifest:"), e.getMessage()); + assertTrue(e.getMessage().contains("contracts"), e.getMessage()); + } + + @Test + void testContractsWrappedManifestAlsoAccepted(@TempDir Path dir) throws Exception { + Files.createDirectories(dir.resolve("xbridge-confs")); + Files.writeString(dir.resolve("manifest-latest.json"), + "{\"contracts\":[{\"blockchain\":\"Wrap\",\"ticker\":\"WRAP\"," + + "\"xbridge_conf\":\"w.conf\"}]}"); + Files.writeString(dir.resolve("xbridge-confs").resolve("w.conf"), + "[WRAP]\nAddressPrefix=7\nFeePerByte=1\nMinTxFee=2\nCOIN=3\nPort=4\nScriptPrefix=5\nSecretPrefix=6\n"); + Map all = new CoinConfigSource(dir.toString()).loadAll(); + assertEquals(7, all.get("WRAP").addressPrefix()); + } +} diff --git a/src/test/java/io/cloudchains/app/coinconfig/CoinConfigTest.java b/src/test/java/io/cloudchains/app/coinconfig/CoinConfigTest.java new file mode 100644 index 0000000..5195665 --- /dev/null +++ b/src/test/java/io/cloudchains/app/coinconfig/CoinConfigTest.java @@ -0,0 +1,81 @@ +package io.cloudchains.app.coinconfig; + +import org.junit.jupiter.api.Test; + +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class CoinConfigTest { + + private CoinConfig sample() { + Map entries = new LinkedHashMap<>(); + entries.put("AddressPrefix", "48"); + entries.put("ScriptPrefix", "50"); + entries.put("SecretPrefix", "176"); + entries.put("COIN", "100000000"); + entries.put("FeePerByte", "10"); + entries.put("MinTxFee", "5000"); + entries.put("Port", "9332"); + return new CoinConfig("LTC", "Litecoin", "litecoin--v0.21.1", entries); + } + + @Test + void testTypedAccessorsParseShippedValues() { + CoinConfig c = sample(); + assertEquals(48, c.addressPrefix()); + assertEquals(50, c.scriptPrefix()); + assertEquals(176, c.secretPrefix()); + assertEquals(100_000_000L, c.coinFactor()); + assertEquals(10L, c.feePerByte()); + assertEquals(5000L, c.minTxFee()); + assertEquals(9332, c.port()); + } + + @Test + void testMissingKeyFailsHardWithTickerContext() { + IllegalStateException e = assertThrows(IllegalStateException.class, + () -> new CoinConfig("X", "Xcoin", "", Map.of("A", "1")).feePerByte()); + assertTrue(e.getMessage().contains("[X] missing required config key 'FeePerByte'"), e.getMessage()); + } + + @Test + void testMalformedNumberFailsHardWithContext() { + Map bad = new LinkedHashMap<>(); + bad.put("FeePerByte", "abc"); + IllegalStateException e = assertThrows(IllegalStateException.class, + () -> new CoinConfig("X", "Xcoin", "", bad).feePerByte()); + assertTrue(e.getMessage().contains("[X] config key 'FeePerByte' is not a number: 'abc'"), e.getMessage()); + } + + @Test + void testDustAbsentNullMalformedThrows() { + assertNull(sample().dustAmountOrNull()); + + Map withDust = new LinkedHashMap<>(); + withDust.put("DustAmount", "5460"); + assertEquals(5460L, new CoinConfig("X", "X", "", withDust).dustAmountOrNull()); + + Map badDust = new LinkedHashMap<>(); + badDust.put("DustAmount", "zero"); + IllegalStateException e = assertThrows(IllegalStateException.class, + () -> new CoinConfig("X", "X", "", badDust).dustAmountOrNull()); + assertTrue(e.getMessage().contains("'DustAmount' is not a number"), e.getMessage()); + } + + @Test + void testPortOverflowFailsHardWithContext() { + Map bad = new LinkedHashMap<>(); + bad.put("Port", "99999999999"); + IllegalStateException e = assertThrows(IllegalStateException.class, + () -> new CoinConfig("X", "X", "", bad).port()); + assertTrue(e.getMessage().contains("[X] config key 'Port' exceeds the int range"), e.getMessage()); + } + + @Test + void testConfEntriesUnmodifiable() { + assertThrows(UnsupportedOperationException.class, + () -> sample().getConfEntries().put("New", "1")); + } +} diff --git a/src/test/java/io/cloudchains/app/coinconfig/ConfigSourceResolverTest.java b/src/test/java/io/cloudchains/app/coinconfig/ConfigSourceResolverTest.java new file mode 100644 index 0000000..ae8c8c5 --- /dev/null +++ b/src/test/java/io/cloudchains/app/coinconfig/ConfigSourceResolverTest.java @@ -0,0 +1,90 @@ +package io.cloudchains.app.coinconfig; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class ConfigSourceResolverTest { + + private static final String UPSTREAM = + "https://raw.githubusercontent.com/blocknetdx/blockchain-configuration-files/master"; + + @Test + void testFlagWinsOverEnvOverDefault() { + assertEquals("/opt/bcf", + new ConfigSourceResolver("/opt/bcf", "/env/bcf").resolve()); + assertEquals("/env/bcf", + new ConfigSourceResolver(null, "/env/bcf").resolve()); + assertEquals(UPSTREAM, + new ConfigSourceResolver(null, null).resolve()); + assertEquals(UPSTREAM, + new ConfigSourceResolver(" ", "").resolve()); + } + + @Test + void testBlankValuesFallThrough() { + assertEquals("/env/bcf", new ConfigSourceResolver("", " /env/bcf ").resolve()); + } + + @Test + void testTrailingSlashesTrimmed() { + assertEquals("/opt/bcf", new ConfigSourceResolver("/opt/bcf///", null).resolve()); + assertEquals(UPSTREAM, new ConfigSourceResolver(UPSTREAM + "/", null).resolve()); + } + + @Test + void testGithubRepoUrlNormalizedToRawMaster() { + assertEquals(UPSTREAM, + new ConfigSourceResolver("https://github.com/blocknetdx/blockchain-configuration-files", null).resolve()); + } + + @Test + void testGithubTreeUrlNormalizedToRawBranch() { + assertEquals("https://raw.githubusercontent.com/blocknetdx/blockchain-configuration-files/develop", + new ConfigSourceResolver("https://github.com/blocknetdx/blockchain-configuration-files/tree/develop", null).resolve()); + } + + @Test + void testGithubDotGitSuffixStripped() { + assertEquals(UPSTREAM, + new ConfigSourceResolver("https://github.com/blocknetdx/blockchain-configuration-files.git", null).resolve()); + } + + @Test + void testGithubBlobUrlNormalized() { + assertEquals("https://raw.githubusercontent.com/blocknetdx/blockchain-configuration-files/master", + new ConfigSourceResolver("https://github.com/blocknetdx/blockchain-configuration-files/blob/master", null).resolve()); + } + + @Test + void testUppercaseSchemeNormalized() { + assertEquals(UPSTREAM, + new ConfigSourceResolver("HTTPS://GitHub.com/blocknetdx/blockchain-configuration-files", null).resolve()); + } + + @Test + void testDeepGithubPathFailsHard() { + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> new ConfigSourceResolver( + "https://github.com/blocknetdx/blockchain-configuration-files/tree/develop/src", null) + .resolve()); + assertTrue(e.getMessage().contains("unsupported github URL form"), e.getMessage()); + } + + @Test + void testUppercaseSchemeIsRemoteNotLocalDir() { + assertFalse(ConfigSourceResolver.isLocalDirectory("HTTP://127.0.0.1:8080/bcf")); + } + + @Test + void testPlainHttpKept() { + assertEquals("http://127.0.0.1:8080/bcf", + new ConfigSourceResolver("http://127.0.0.1:8080/bcf", null).resolve()); + } + + @Test + void testLocalDirectoryDetectedAsNonUrl() { + assertTrue(ConfigSourceResolver.isLocalDirectory("/opt/bcf")); + assertFalse(ConfigSourceResolver.isLocalDirectory(UPSTREAM)); + } +} diff --git a/src/test/java/io/cloudchains/app/coinconfig/XBridgeConfParserTest.java b/src/test/java/io/cloudchains/app/coinconfig/XBridgeConfParserTest.java new file mode 100644 index 0000000..f152c68 --- /dev/null +++ b/src/test/java/io/cloudchains/app/coinconfig/XBridgeConfParserTest.java @@ -0,0 +1,98 @@ +package io.cloudchains.app.coinconfig; + +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class XBridgeConfParserTest { + + private static final String SAMPLE = + "# top comment\n" + + "[LTC]\n" + + "Title=Litecoin\n" + + " AddressPrefix = 48 \n" + + "FeePerByte=10\n" + + "; ini-style comment\n" + + "\n" + + "[BLOCK]\n" + + "Title=Blocknet\n" + + "AddressPrefix=26\n"; + + @Test + void testSectionsAndKeysParsed() { + Map> all = XBridgeConfParser.parse(SAMPLE); + assertEquals(2, all.size()); + Map ltc = all.get("LTC"); + assertEquals("48", ltc.get("AddressPrefix")); + assertEquals("10", ltc.get("FeePerByte")); + assertEquals("Litecoin", ltc.get("Title")); + assertEquals("26", all.get("BLOCK").get("AddressPrefix")); + } + + @Test + void testValuesTrimmedCommentsSkipped() { + Map> all = XBridgeConfParser.parse(SAMPLE); + assertFalse(all.get("LTC").containsKey("# top comment")); + assertFalse(all.get("LTC").containsKey("; ini-style comment")); + assertEquals("48", all.get("LTC").get("AddressPrefix")); // inner spaces trimmed + } + + @Test + void testMalformedLineOutsideSectionThrows() { + IllegalStateException e = assertThrows(IllegalStateException.class, + () -> XBridgeConfParser.parse("Orphan=1\n[A]\nK=V\n")); + assertTrue(e.getMessage().contains("outside any section")); + } + + @Test + void testMalformedLineInsideSectionThrows() { + IllegalStateException e = assertThrows(IllegalStateException.class, + () -> XBridgeConfParser.parse("[A]\nJustAK\nK=V\n")); + assertTrue(e.getMessage().contains("in [A]")); + } + + @Test + void testNestedBracketHeaderThrows() { + IllegalStateException e = assertThrows(IllegalStateException.class, + () -> XBridgeConfParser.parse("[A][B]\nK=V\n")); + assertTrue(e.getMessage().contains("Malformed section header"), e.getMessage()); + } + + @Test + void testDuplicateKeysLastWins() { + Map> all = XBridgeConfParser.parse("[A]\nK=1\nK=2\n"); + assertEquals("2", all.get("A").get("K")); + } + + @Test + void testDuplicateSectionsMerge() { + Map> all = XBridgeConfParser.parse("[A]\nK=1\n[A]\nJ=2\n"); + assertEquals(1, all.size()); + assertEquals("1", all.get("A").get("K")); + assertEquals("2", all.get("A").get("J")); + } + + @Test + void testUtf8BomStrippedBeforeFirstSection() { + Map> all = XBridgeConfParser.parse("\uFEFF[LTC]\nAddressPrefix=48\n"); + assertEquals("48", all.get("LTC").get("AddressPrefix")); + } + + @Test + void testEmptySectionHeaderThrows() { + assertThrows(IllegalStateException.class, () -> XBridgeConfParser.parse("[]\nK=V\n")); + } + + @Test + void testCrlfHandled() { + Map> all = XBridgeConfParser.parse("[X]\r\nA=1\r\n"); + assertEquals("1", all.get("X").get("A")); + } + + @Test + void testEmptyInputYieldsEmptyMap() { + assertTrue(XBridgeConfParser.parse("").isEmpty()); + } +} From 9f369c5c626ddd3c0d085d7114d2482126bcd7c1 Mon Sep 17 00:00:00 2001 From: tryiou Date: Wed, 26 Aug 2026 15:11:27 +0200 Subject: [PATCH 60/73] perf: cut mvn test wall time roughly in half (46s -> 20s) - AddressDiscoveryServiceTest.testDiscovery_AllBatchesFunded no longer pre-generates 50,000 wallet addresses: discovery derives every probed batch independently from the seed's external-chain key (deriveAddressRange), so the mocked UTXO feed alone drives the scan and the assertions are unchanged. - CoinInstanceTest determinism loops reduced from 10 to 3 repetitions; each iteration still performs full wallet init/deinit cycles on a fresh data directory. - OpenRewrite cleanup moved behind an opt-in `rewrite` profile (mvn -Prewrite process-classes). It previously ran automatically at process-classes on every build (~8s) and could rewrite sources as a build side effect; its version is now centralized in a property. All 157 tests pass before and after the change; no test assertions were weakened or removed. --- AGENTS.md | 20 ++++--- pom.xml | 54 ++++++++++++------- .../java/AddressDiscoveryServiceTest.java | 8 ++- src/test/java/CoinInstanceTest.java | 6 +-- 4 files changed, 58 insertions(+), 30 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 0981677..28046d2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,9 +24,12 @@ data upstream over HTTP (`net/api/http/client/`). `{method, params}` JSON. - Upstream chain: exrproxy `/xrs/` → plugin-adapter xrm methods → utxo-plugin containers. -- `net/xrouter/` and the `xrm*` handling in `BlocknetPeerGroup` are dead legacy - XRouter-over-p2p paths (commented out at `CoinInstance.java:517`) — don't build - on them. +- `net/xrouter/` and the `xrm*` handling in `BlocknetPeerGroup` are currently + disabled XRouter-over-p2p paths (commented out at `CoinInstance.java:517-557`) + — **work-in-progress, NOT abandoned**: never delete, prune, or "clean up" them; + treat every Blocknet p2p/XRouter extra (serializers, packet magic, peer-group + plumbing) as live WIP. Any earlier "dead legacy" wording here was wrong; the + workspace-root rule takes precedence. # Java — use jabba source ~/.jabba/jabba.sh && jabba use graalvm_community@21.0.2 @@ -40,6 +43,9 @@ mvn compile -q # Run all tests mvn test +# OpenRewrite code cleanup (opt-in; NOT part of routine builds) +mvn -Prewrite process-classes + # Run a single test class mvn test -pl . -Dtest=KeyHandlerTest @@ -61,7 +67,7 @@ mvn package -Pnative -Pnative-fast -q - Group order: third-party libraries, then `java.*`, then `javax.*` - Wildcard imports are acceptable for large groups (e.g., `java.io.*`, `org.bitcoinj.core.*`) -- No unused imports; OpenRewrite cleanup runs on `mvn compile` +- No unused imports; OpenRewrite cleanup is opt-in via `mvn -Prewrite process-classes` ### Formatting @@ -146,8 +152,10 @@ Some older commits use `[category] description` style (e.g., `[security] Upgrade ## Things to Watch For -- The `rewrite-maven-plugin` runs on `mvn compile` and may auto-modify imports and formatting. - Always review `git diff` after compiling. +- The `rewrite-maven-plugin` no longer runs in the default build (it cost ~8s + per build); it executes only under the `-Prewrite` profile and may auto-modify + imports and formatting. Run it before releases or style sweeps, and always + review `git diff` afterwards. - `ConfigHelper.CONFIG_DIR` is a mutable static used to override config path in tests. - Tests use `@TempDir` (JUnit 5 auto-cleanup); never run with parallel execution due to mutable `ConfigHelper.CONFIG_DIR` static state. diff --git a/pom.xml b/pom.xml index 2b137ea..ba59289 100644 --- a/pom.xml +++ b/pom.xml @@ -34,6 +34,7 @@ 3.13.0 3.6.0 3.5.0 + 6.25.0 @@ -398,11 +399,13 @@ - + org.openrewrite.maven rewrite-maven-plugin - 6.25.0 + ${rewrite.maven.plugin.version} @@ -419,7 +422,7 @@ org.openrewrite.maven.OrderPomElements org.openrewrite.maven.ModernizeObsoletePoms org.openrewrite.maven.BestPractices - + org.openrewrite.java.RemoveUnusedImports org.openrewrite.java.format.AutoFormat @@ -429,30 +432,17 @@ org.openrewrite.java.ShortenFullyQualifiedTypeReferences org.openrewrite.java.SimplifySingleElementAnnotation - + - + true - - - - - - - cleanup-code - process-classes - - run - - - - + org.codehaus.mojo @@ -498,5 +488,31 @@ true + + + + rewrite + + + + org.openrewrite.maven + rewrite-maven-plugin + ${rewrite.maven.plugin.version} + + + + cleanup-code + process-classes + + run + + + + + + + \ No newline at end of file diff --git a/src/test/java/AddressDiscoveryServiceTest.java b/src/test/java/AddressDiscoveryServiceTest.java index fb1aa00..9af4526 100644 --- a/src/test/java/AddressDiscoveryServiceTest.java +++ b/src/test/java/AddressDiscoveryServiceTest.java @@ -164,12 +164,16 @@ void testDiscovery_UtxoParsingErrors() { assertEquals(usedAddressIndex + 1, discoveryService.discoverAddressCount()); } - /** All NUM_BATCHES have UTXOs. Scans all batches. */ + /** + * All NUM_BATCHES have UTXOs. Scans all batches. + * No wallet address pre-generation is needed: discovery derives each batch + * independently via deriveAddressRange(), so the mocked UTXO feed alone + * drives the scan. + */ @Test void testDiscovery_AllBatchesFunded() { int batchSize = AddressDiscoveryService.getBatchSize(); int numBatches = AddressDiscoveryService.getNumBatches(); - for (int i = 0; i < numBatches * batchSize; i++) coinInstance.generateAddress(false); when(mockHttpClient.getUtxosUncached(any(), any(String[].class))) .thenAnswer(inv -> buildUtxoResponse(((String[]) inv.getArgument(1))[0])); diff --git a/src/test/java/CoinInstanceTest.java b/src/test/java/CoinInstanceTest.java index 8f6a865..69aae48 100644 --- a/src/test/java/CoinInstanceTest.java +++ b/src/test/java/CoinInstanceTest.java @@ -19,7 +19,7 @@ class CoinInstanceTest extends TestHelper { @Test void deterministicAddresses_fromMnemonic() { - for (int runCount = 0; runCount < 10; runCount++) { + for (int runCount = 0; runCount < 3; runCount++) { CoinInstance coin = CoinInstance.getInstance(CoinTicker.BLOCKNET); assertNotNull(coin); coin.getConfigHelper().setAddressCount(getAddressCount()); @@ -51,7 +51,7 @@ void deterministicAddresses_fromMnemonic() { @Test void deterministicAddresses_generateAddress() { - for (int runCount = 0; runCount < 10; runCount++) { + for (int runCount = 0; runCount < 3; runCount++) { CoinInstance coin = CoinInstance.getInstance(CoinTicker.BLOCKNET); assertNotNull(coin); coin.getConfigHelper().setAddressCount(getAddressCountInitial()); @@ -87,7 +87,7 @@ void deterministicAddresses_generateAddress() { @Test void deterministicAddresses_generateForwardAddresses() { - for (int runCount = 0; runCount < 10; runCount++) { + for (int runCount = 0; runCount < 3; runCount++) { CoinInstance coin = CoinInstance.getInstance(CoinTicker.BLOCKNET); assertNotNull(coin); coin.getConfigHelper().setAddressCount(getAddressCountInitial()); From 8c018782de1578e780b548135ae43ec3e8b8472e Mon Sep 17 00:00:00 2001 From: tryiou Date: Wed, 26 Aug 2026 15:34:41 +0200 Subject: [PATCH 61/73] feat(coinconfig): add config-driven network parameters with legacy cross-check Introduce the parameter layer that will replace the per-coin hardcoded NetworkParameters subclasses: - CompiledCoinSupplement: per-ticker constants that blockchain-configuration-files does not carry (max money coins, dust threshold, protocol version, chain id, payment protocol id), lifted verbatim from the legacy classes including BTC's MainNetParams inheritance. - CoinConfigGate: sanity gate over a loaded config (prefix byte ranges, prefix distinctness, positive COIN/fees, port range); every violation is reported before rejection. - ConfigurableNetworkParameters: NetworkParameters implementation driven by a loaded config plus its supplement entry. Getters degrade to neutral values during base-class construction (bitcoinj calls them virtually before subclass fields exist). Segwit HRP stays unset (segwit unsupported), difficulty/interval/subsidy remain inert stubs, and getPort() is deliberately not overridden: only BTC's legacy class carried a real p2p port (8333 via MainNetParams), all others left the same base default, and nothing in this daemon reads a migrated coin's params-port. - CrossCheckTest: proves the generic class reproduces every live getter of all eleven legacy classes exactly, except two corrections carried by the configuration files themselves and pinned explicitly (DGB ScriptPrefix 5 -> 63, RVN FeePerByte 1000 -> 3000). Nothing wires this into coin startup yet; the legacy classes remain the live path until the cutover. --- .../app/coinconfig/CoinConfigGate.java | 50 +++++ .../coinconfig/CompiledCoinSupplement.java | 70 +++++++ .../ConfigurableNetworkParameters.java | 152 +++++++++++++++ ...urableNetworkParametersCrossCheckTest.java | 180 ++++++++++++++++++ 4 files changed, 452 insertions(+) create mode 100644 src/main/java/io/cloudchains/app/coinconfig/CoinConfigGate.java create mode 100644 src/main/java/io/cloudchains/app/coinconfig/CompiledCoinSupplement.java create mode 100644 src/main/java/io/cloudchains/app/coinconfig/ConfigurableNetworkParameters.java create mode 100644 src/test/java/io/cloudchains/app/coinconfig/ConfigurableNetworkParametersCrossCheckTest.java diff --git a/src/main/java/io/cloudchains/app/coinconfig/CoinConfigGate.java b/src/main/java/io/cloudchains/app/coinconfig/CoinConfigGate.java new file mode 100644 index 0000000..da4cc5a --- /dev/null +++ b/src/main/java/io/cloudchains/app/coinconfig/CoinConfigGate.java @@ -0,0 +1,50 @@ +package io.cloudchains.app.coinconfig; + +import java.util.ArrayList; +import java.util.List; + +/** + * Sanity gate applied to a {@link CoinConfig} before its values may drive + * live address/fee behavior. Violations abort with every problem listed — + * a config that fails here must never silently fall back to anything. + */ +public final class CoinConfigGate { + + public static void validate(CoinConfig cfg) { + List v = new ArrayList<>(); + + checkByteRange("AddressPrefix", cfg.addressPrefix(), v); + checkByteRange("ScriptPrefix", cfg.scriptPrefix(), v); + checkByteRange("SecretPrefix", cfg.secretPrefix(), v); + if (cfg.addressPrefix() == cfg.scriptPrefix()) + v.add("AddressPrefix equals ScriptPrefix (" + cfg.addressPrefix() + ")"); + if (cfg.addressPrefix() == cfg.secretPrefix()) + v.add("AddressPrefix equals SecretPrefix (" + cfg.addressPrefix() + ")"); + if (cfg.scriptPrefix() == cfg.secretPrefix()) + v.add("ScriptPrefix equals SecretPrefix (" + cfg.scriptPrefix() + ")"); + + if (cfg.coinFactor() <= 0) + v.add("COIN must be positive, got " + cfg.coinFactor()); + if (cfg.feePerByte() <= 0) + v.add("FeePerByte must be positive, got " + cfg.feePerByte()); + if (cfg.minTxFee() <= 0) + v.add("MinTxFee must be positive, got " + cfg.minTxFee()); + + int port = cfg.port(); + if (port < 1024 || port > 65_535) + v.add("Port out of range 1024..65535: " + port); + + if (!v.isEmpty()) { + throw new IllegalStateException("[" + + cfg.getTicker() + "] coin config rejected: " + String.join("; ", v)); + } + } + + private static void checkByteRange(String key, int value, List violations) { + if (value < 0 || value > 255) + violations.add(key + " out of byte range 0..255: " + value); + } + + private CoinConfigGate() { + } +} diff --git a/src/main/java/io/cloudchains/app/coinconfig/CompiledCoinSupplement.java b/src/main/java/io/cloudchains/app/coinconfig/CompiledCoinSupplement.java new file mode 100644 index 0000000..5b95989 --- /dev/null +++ b/src/main/java/io/cloudchains/app/coinconfig/CompiledCoinSupplement.java @@ -0,0 +1,70 @@ +package io.cloudchains.app.coinconfig; + +import java.util.Map; + +/** + * The per-coin values a config-driven parameter set needs that the + * blockchain-configuration-files repository does not carry: max supply, + * wire protocol version, network id string and dust threshold. + * + *

Values were lifted verbatim from the legacy per-coin parameter classes + * they replace (BTC from bitcoinj's MainNetParams inheritance). They are + * consensus-era constants that essentially never move — unlike the bcf-carried + * data (prefixes, fees), which is exactly why they live here and not in a + * conf file.

+ */ +public final class CompiledCoinSupplement { + + /** A coin's non-bcf-carried constants. */ + public static final class Supplement { + public final long maxMoneyCoins; + public final long dustSat; + public final int protocolVersion; + public final String id; + public final String chainName; + + public Supplement(long maxMoneyCoins, long dustSat, int protocolVersion, String id, String chainName) { + this.maxMoneyCoins = maxMoneyCoins; + this.dustSat = dustSat; + this.protocolVersion = protocolVersion; + this.id = id; + this.chainName = chainName; + } + } + + private static final long BTC_DUST = 546; // Transaction.MIN_NONDUST_OUTPUT + + private static final Map BY_TICKER = Map.ofEntries( + Map.entry("BTC", new Supplement(21_000_000L, BTC_DUST, 70012, "org.bitcoin.production", "main")), + Map.entry("BCH", new Supplement(21_000_000L, BTC_DUST, 70012, "BCH", "main")), + Map.entry("DASH", new Supplement(22_000_000L, 5460L, 70210, "DASH", "main")), + Map.entry("DGB", new Supplement(2_000_000_000L, 1000L, 70002, "DGB", "main")), + Map.entry("DOGE", new Supplement(2_000_000_000L, BTC_DUST, 70004, "DOGE", "main")), + Map.entry("LTC", new Supplement(84_000_000L, 100_000L, 70015, "LTC", "main")), + Map.entry("PIVX", new Supplement(100_000_000L, BTC_DUST, 70007, "PIVX", "main")), + Map.entry("PKOIN", new Supplement(21_000_000L, BTC_DUST, 70031, "PKOIN", "main")), + Map.entry("RVN", new Supplement(100_000_000L, BTC_DUST, 70026, "RVN", "main")), + Map.entry("SYS", new Supplement(888_000_000L, 5500L, 70227, "SYS", "main")), + Map.entry("UNO", new Supplement(250_000L, BTC_DUST, 70002, "UNO", "main")) + ); + + /** + * @param ticker manifest ticker (case-sensitive, e.g. {@code LTC}) + * @throws IllegalArgumentException for tickers with no compiled entry + */ + public static Supplement forTicker(String ticker) { + Supplement s = BY_TICKER.get(ticker); + if (s == null) + throw new IllegalArgumentException( + "no compiled supplement for ticker '" + ticker + + "' — the coin cannot run without its non-config constants"); + return s; + } + + public static boolean supports(String ticker) { + return BY_TICKER.containsKey(ticker); + } + + private CompiledCoinSupplement() { + } +} diff --git a/src/main/java/io/cloudchains/app/coinconfig/ConfigurableNetworkParameters.java b/src/main/java/io/cloudchains/app/coinconfig/ConfigurableNetworkParameters.java new file mode 100644 index 0000000..d4f6d28 --- /dev/null +++ b/src/main/java/io/cloudchains/app/coinconfig/ConfigurableNetworkParameters.java @@ -0,0 +1,152 @@ +package io.cloudchains.app.coinconfig; + +import io.cloudchains.app.net.HasFeeParams; +import org.bitcoinj.core.*; +import org.bitcoinj.store.BlockStore; +import org.bitcoinj.store.BlockStoreException; +import org.bitcoinj.utils.MonetaryFormat; + +import java.util.Locale; +import java.util.Objects; + +/** + * Network parameters for one coin, driven entirely by its + * {@link CoinConfig} (blockchain-configuration-files data) plus the small + * {@link CompiledCoinSupplement} constants bcf does not carry. + * + *

Replaces the per-coin hardcoded parameter classes. Behavior is + * byte-compatible with those classes except where the loaded config + * intentionally differs (e.g. DGB ScriptPrefix 63, RVN FeePerByte 3000).

+ * + *

Deliberately preserved status quo: the segwit HRP stays unset — bech32 + * address validation keeps failing for these coins (segwit is unsupported); + * difficulty/interval/subsidy overrides remain inert stubs (no BlockChain is + * ever constructed in this daemon); monetary format and URI scheme are dead + * stubs returning ticker-derived values; {@code getPort()} is likewise not + * overridden — only BTC's legacy class carried a real p2p port (8333 via + * MainNetParams); all other legacy classes left the same base default this + * class returns. Safe because nothing in this daemon reads a migrated coin's + * params-port: the local RPC port comes from {@code CoinInstance} and the + * only params-port consumer ({@code BlocknetPeerGroup}) is BLOCK-only.

+ */ +public final class ConfigurableNetworkParameters extends NetworkParameters implements HasFeeParams { + + private final CoinConfig config; + private final CompiledCoinSupplement.Supplement supplement; + private final Coin maxMoney; + private final Coin minNonDustOutput; + + public static ConfigurableNetworkParameters from(CoinConfig config) { + return new ConfigurableNetworkParameters(config, + CompiledCoinSupplement.forTicker(config.getTicker())); + } + + public ConfigurableNetworkParameters(CoinConfig config, CompiledCoinSupplement.Supplement supplement) { + this.config = Objects.requireNonNull(config, "config"); + this.supplement = Objects.requireNonNull(supplement, "supplement"); + CoinConfigGate.validate(config); + this.maxMoney = Coin.valueOf(Math.multiplyExact(supplement.maxMoneyCoins, Coin.COIN.value)); + this.minNonDustOutput = Coin.valueOf(supplement.dustSat); + } + + public CoinConfig getConfig() { + return config; + } + + /** + * True once OUR constructor body has run. The NetworkParameters base + * constructor calls several of these getters virtually before that, so + * every accessor degrades to a neutral value until then. + */ + private boolean ready() { + return config != null && supplement != null; + } + + @Override + public String getPaymentProtocolId() { + return ready() ? supplement.chainName : "main"; + } + + @Override + public void checkDifficultyTransitions(StoredBlock storedPrev, Block next, BlockStore blockStore) + throws VerificationException, BlockStoreException { + // no-op: this daemon never verifies block headers (EXR HTTP client) + } + + @Override + public Coin getMaxMoney() { + return ready() ? maxMoney : NetworkParameters.MAX_MONEY; + } + + @Override + public Coin getMinNonDustOutput() { + return ready() ? minNonDustOutput : Coin.ZERO; + } + + @Override + public MonetaryFormat getMonetaryFormat() { + return ready() ? new MonetaryFormat().code(0, config.getTicker()) : new MonetaryFormat(); + } + + @Override + public String getUriScheme() { + return ready() ? config.getTicker().toLowerCase(Locale.ROOT) + ":" : ""; + } + + @Override + public boolean hasMaxMoney() { + return true; + } + + @Override + public BitcoinSerializer getSerializer(boolean parseRetain) { + return new BitcoinSerializer(this, parseRetain); + } + + @Override + public int getProtocolVersionNum(ProtocolVersion version) { + return ready() ? supplement.protocolVersion + : ProtocolVersion.CURRENT.getBitcoinProtocolVersion(); + } + + @Override + public int getAddressHeader() { + return ready() ? config.addressPrefix() : 0; + } + + @Override + public int getP2SHHeader() { + return ready() ? config.scriptPrefix() : 0; + } + + @Override + public int getDumpedPrivateKeyHeader() { + return ready() ? config.secretPrefix() : 0; + } + + @Override + public int getInterval() { + return 210_000; // inert: no BlockChain exists in this daemon + } + + @Override + public int getSubsidyDecreaseBlockCount() { + return 210_000; // inert: see getInterval() + } + + @Override + public String getId() { + // must be non-null and != ID_UNITTESTNET (wallet creation checks it) + return ready() ? supplement.id : ""; + } + + @Override + public long getFeePerByte() { + return ready() ? config.feePerByte() : 1L; + } + + @Override + public long getMinTxFee() { + return ready() ? config.minTxFee() : 1000L; + } +} diff --git a/src/test/java/io/cloudchains/app/coinconfig/ConfigurableNetworkParametersCrossCheckTest.java b/src/test/java/io/cloudchains/app/coinconfig/ConfigurableNetworkParametersCrossCheckTest.java new file mode 100644 index 0000000..2c0e0e8 --- /dev/null +++ b/src/test/java/io/cloudchains/app/coinconfig/ConfigurableNetworkParametersCrossCheckTest.java @@ -0,0 +1,180 @@ +package io.cloudchains.app.coinconfig; + +import io.cloudchains.app.net.protocols.bitcoin.BitcoinNetworkParameters; +import io.cloudchains.app.net.protocols.bitcoincash.BitcoinCashNetworkParameters; +import io.cloudchains.app.net.protocols.dashcoin.DashcoinNetworkParameters; +import io.cloudchains.app.net.protocols.digibyte.DigibyteNetworkParameters; +import io.cloudchains.app.net.protocols.dogecoin.DogecoinNetworkParameters; +import io.cloudchains.app.net.protocols.litecoin.LitecoinNetworkParameters; +import io.cloudchains.app.net.protocols.pivx.PivxNetworkParameters; +import io.cloudchains.app.net.protocols.pocketcoin.PocketcoinNetworkParameters; +import io.cloudchains.app.net.protocols.ravencoin.RavencoinNetworkParameters; +import io.cloudchains.app.net.protocols.syscoin.SyscoinNetworkParameters; +import io.cloudchains.app.net.protocols.unobtanium.UnobtaniumNetworkParameters; +import io.cloudchains.app.wallet.WalletHelper; +import org.bitcoinj.core.LegacyAddress; +import org.bitcoinj.core.NetworkParameters; +import org.junit.jupiter.api.Test; + +import java.nio.file.Paths; +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Migration safety net: for every migrated coin, the bcf-driven parameter + * set must reproduce the legacy hardcoded classes exactly — EXCEPT the two + * approved corrections, pinned explicitly below: + * - DGB ScriptPrefix 5 -> 63 (bcf carries the current-era value) + * - RVN FeePerByte 1000 -> 3000 (bcf carries the product's creation rate) + * + *

Getters NOT compared here — each verified to be a dead code path in this + * daemon (zero callers on migrated coins):

+ *
    + *
  • {@code getUriScheme()} — legacy used real scheme names ("litecoin:"), + * the generic class derives "ticker:"
  • + *
  • {@code getInterval()}/{@code getSubsidyDecreaseBlockCount()} — inert + * stubs vs per-coin legacy values (no BlockChain exists here)
  • + *
  • {@code getProtocolVersionNum(non-CURRENT)} — BCH legacy was + * version-dependent, the generic class returns its fixed value
  • + *
  • {@code getPort()} — see the disclosure on + * {@link ConfigurableNetworkParameters}
  • + *
+ */ +class ConfigurableNetworkParametersCrossCheckTest { + + private static final byte[] HASH160 = new byte[20]; + + static { + for (int i = 0; i < 20; i++) HASH160[i] = (byte) (0xA0 + i); + } + + private Map configs() { + return new CoinConfigSource( + Paths.get("..", "blockchain-configuration-files") + .toAbsolutePath().normalize().toString()).loadAll(); + } + + @Test + void testGenericReproducesEveryLiveGetterOfLegacyClasses() { + Map cfgs = configs(); + + forLegacy(new String[]{"BTC", "BCH", "DASH", "DOGE", "LTC", "PIVX", "PKOIN", "SYS", "UNO"}, + cfgs, false, false); + // DGB: only the P2SH header intentionally differs (5 -> 63) + forLegacy(new String[]{"DGB"}, cfgs, true, false); + // RVN: only the fee intentionally differs (1000 -> 3000) + forLegacy(new String[]{"RVN"}, cfgs, false, true); + } + + private void forLegacy(String[] tickers, Map cfgs, + boolean dgbP2shDiff, boolean rvnFeeDiff) { + for (String ticker : tickers) { + NetworkParametersPair pair = pairFor(ticker, cfgs); + + assertEquals(pair.legacy.getAddressHeader(), + pair.generic.getAddressHeader(), ticker + " addressHeader"); + if (!dgbP2shDiff) { + assertEquals(pair.legacy.getP2SHHeader(), + pair.generic.getP2SHHeader(), ticker + " p2shHeader"); + } else { + assertEquals(5, pair.legacy.getP2SHHeader()); + assertEquals(63, pair.generic.getP2SHHeader(), "DGB must adopt bcf 63"); + } + assertEquals(pair.legacy.getDumpedPrivateKeyHeader(), + pair.generic.getDumpedPrivateKeyHeader(), ticker + " dumpedPrivateKeyHeader"); + assertEquals(pair.legacy.getMaxMoney().getValue(), + pair.generic.getMaxMoney().getValue(), ticker + " maxMoney"); + assertEquals(pair.legacy.getMinNonDustOutput().getValue(), + pair.generic.getMinNonDustOutput().getValue(), ticker + " minNonDustOutput"); + assertEquals(pair.legacy.getProtocolVersionNum(NetworkParameters.ProtocolVersion.CURRENT), + pair.generic.getProtocolVersionNum(NetworkParameters.ProtocolVersion.CURRENT), + ticker + " protocolVersion"); + assertEquals(pair.legacy.getId(), pair.generic.getId(), ticker + " id"); + assertNotEquals(NetworkParameters.ID_UNITTESTNET, pair.generic.getId()); + assertEquals(pair.legacy.getPaymentProtocolId(), + pair.generic.getPaymentProtocolId(), ticker + " paymentProtocolId"); + + long legacyFee = WalletHelper.getFeePerByte(pair.legacy); + long genericFee = WalletHelper.getFeePerByte(pair.generic); + if (!rvnFeeDiff) { + assertEquals(legacyFee, genericFee, ticker + " feePerByte"); + } else { + assertEquals(1000L, legacyFee); + assertEquals(3000L, genericFee, "RVN must adopt bcf 3000 sat/B"); + } + assertEquals(WalletHelper.getMinTxFee(pair.legacy), + WalletHelper.getMinTxFee(pair.generic), + ticker + " minTxFee"); + + // end-to-end verbyte equivalence: same legacy address string + LegacyAddress fromLegacy = LegacyAddress.fromPubKeyHash(pair.legacy, HASH160); + LegacyAddress fromGeneric = LegacyAddress.fromPubKeyHash(pair.generic, HASH160); + assertEquals(fromLegacy.toString(), fromGeneric.toString(), ticker + " address encoding"); + + assertNotNull(pair.generic.getSerializer(false), ticker + " serializer"); + } + } + + @Test + void testGateRejectsCorruptedConfig() { + CoinConfig good = configs().get("LTC"); + assertNotNull(good, "LTC missing from manifest - cross-check baseline broken"); + Map broken = new LinkedHashMap<>(good.getConfEntries()); + broken.put("AddressPrefix", "9999"); + CoinConfig bad = new CoinConfig(good.getTicker(), good.getBlockchain(), + good.getVerId(), broken); + IllegalStateException e = assertThrows(IllegalStateException.class, + () -> ConfigurableNetworkParameters.from(bad)); + assertTrue(e.getMessage().contains("coin config rejected"), e.getMessage()); + assertTrue(e.getMessage().contains("out of byte range"), e.getMessage()); + } + + private interface LegacyFactory { + NetworkParameters create(); + } + + private NetworkParametersPair pairFor(String ticker, Map cfgs) { + LegacyFactory factory; + switch (ticker) { + case "BTC": + factory = BitcoinNetworkParameters::new; break; + case "BCH": + factory = BitcoinCashNetworkParameters::new; break; + case "DASH": + factory = DashcoinNetworkParameters::new; break; + case "DGB": + factory = DigibyteNetworkParameters::new; break; + case "DOGE": + factory = DogecoinNetworkParameters::new; break; + case "LTC": + factory = LitecoinNetworkParameters::new; break; + case "PIVX": + factory = PivxNetworkParameters::new; break; + case "PKOIN": + factory = PocketcoinNetworkParameters::new; break; + case "RVN": + factory = RavencoinNetworkParameters::new; break; + case "SYS": + factory = SyscoinNetworkParameters::new; break; + case "UNO": + factory = UnobtaniumNetworkParameters::new; break; + default: + throw new IllegalArgumentException(ticker); + } + return new NetworkParametersPair(factory.create(), + ConfigurableNetworkParameters.from(cfgs.get(ticker))); + } + + private static final class NetworkParametersPair { + final NetworkParameters legacy; + final NetworkParameters generic; + + NetworkParametersPair(NetworkParameters legacy, + NetworkParameters generic) { + this.legacy = legacy; + this.generic = generic; + } + } +} From 6c859a4f2cd817633a04991b2ce5050f4a335d5b Mon Sep 17 00:00:00 2001 From: tryiou Date: Wed, 26 Aug 2026 16:52:28 +0200 Subject: [PATCH 62/73] feat(coins): drive coin parameters from configuration files Replace the 11 hardcoded per-coin NetworkParameters subclasses with a single config-driven implementation loaded at startup from blockchain-configuration-files. Values that the files do not carry (max money, dust, protocol version, chain id) remain compiled in CompiledCoinSupplement, lifted verbatim from the legacy classes. - Console flag --blockchain-configuration-files (flag > env BLOCKCHAIN_CONFIGURATION_FILES > upstream master) resolves the source; missing or gate-rejected coins warn and that coin's init returns UNSUPPORTEDCOIN. - CoinInstance now builds ConfigurableNetworkParameters from the loaded CoinConfig for all migrated coins; BLOCK / TB5 cases stay on their Java classes; BCH stays disabled. - The 11 value-only classes are renamed *Legacy (same package) and remain as the oracle for the cross-check test. - WalletHelperFeeTest asserts both legacy and generic fee paths. No fallback, no bcf edits, no XRouter/p2p changes. --- src/main/java/io/cloudchains/app/App.java | 38 +++++++- .../app/coinconfig/CoinConfigRegistry.java | 84 ++++++++++++++++++ .../cloudchains/app/console/ConsoleMenu.java | 15 ++++ .../io/cloudchains/app/net/CoinInstance.java | 55 +++++++----- ...va => BitcoinNetworkParametersLegacy.java} | 4 +- ...> BitcoinCashNetworkParametersLegacy.java} | 4 +- ...a => DashcoinNetworkParametersLegacy.java} | 4 +- ...a => DigibyteNetworkParametersLegacy.java} | 4 +- ...a => DogecoinNetworkParametersLegacy.java} | 4 +- ...a => LitecoinNetworkParametersLegacy.java} | 4 +- ....java => PivxNetworkParametersLegacy.java} | 4 +- ...=> PocketcoinNetworkParametersLegacy.java} | 4 +- ... => RavencoinNetworkParametersLegacy.java} | 4 +- ...va => SyscoinNetworkParametersLegacy.java} | 4 +- ...=> UnobtaniumNetworkParametersLegacy.java} | 4 +- src/test/java/TestHelper.java | 12 +++ src/test/java/WalletHelperFeeTest.java | 86 ++++++++++++------- ...urableNetworkParametersCrossCheckTest.java | 44 +++++----- 18 files changed, 280 insertions(+), 98 deletions(-) create mode 100644 src/main/java/io/cloudchains/app/coinconfig/CoinConfigRegistry.java rename src/main/java/io/cloudchains/app/net/protocols/bitcoin/{BitcoinNetworkParameters.java => BitcoinNetworkParametersLegacy.java} (67%) rename src/main/java/io/cloudchains/app/net/protocols/bitcoincash/{BitcoinCashNetworkParameters.java => BitcoinCashNetworkParametersLegacy.java} (92%) rename src/main/java/io/cloudchains/app/net/protocols/dashcoin/{DashcoinNetworkParameters.java => DashcoinNetworkParametersLegacy.java} (92%) rename src/main/java/io/cloudchains/app/net/protocols/digibyte/{DigibyteNetworkParameters.java => DigibyteNetworkParametersLegacy.java} (92%) rename src/main/java/io/cloudchains/app/net/protocols/dogecoin/{DogecoinNetworkParameters.java => DogecoinNetworkParametersLegacy.java} (92%) rename src/main/java/io/cloudchains/app/net/protocols/litecoin/{LitecoinNetworkParameters.java => LitecoinNetworkParametersLegacy.java} (93%) rename src/main/java/io/cloudchains/app/net/protocols/pivx/{PivxNetworkParameters.java => PivxNetworkParametersLegacy.java} (93%) rename src/main/java/io/cloudchains/app/net/protocols/pocketcoin/{PocketcoinNetworkParameters.java => PocketcoinNetworkParametersLegacy.java} (92%) rename src/main/java/io/cloudchains/app/net/protocols/ravencoin/{RavencoinNetworkParameters.java => RavencoinNetworkParametersLegacy.java} (92%) rename src/main/java/io/cloudchains/app/net/protocols/syscoin/{SyscoinNetworkParameters.java => SyscoinNetworkParametersLegacy.java} (92%) rename src/main/java/io/cloudchains/app/net/protocols/unobtanium/{UnobtaniumNetworkParameters.java => UnobtaniumNetworkParametersLegacy.java} (92%) diff --git a/src/main/java/io/cloudchains/app/App.java b/src/main/java/io/cloudchains/app/App.java index ffe6b02..ff155d4 100644 --- a/src/main/java/io/cloudchains/app/App.java +++ b/src/main/java/io/cloudchains/app/App.java @@ -27,6 +27,7 @@ public class App { // DEBUG ENDPOINT public static volatile String EXR_ENDPOINT = null; public static volatile EXRServerPool exrServerPool = null; + public static volatile String BLOCKCHAIN_CONFIGURATION_FILES = null; public static HTTPClient feeUpdateHttpClient = new HTTPClient(2); public static HTTPClient heightUpdateHttpClient = new HTTPClient(2); public static JSONRPCMasterServer masterRPC = JSONRPCController.getMasterServer(); @@ -102,6 +103,38 @@ public static void initExrEndpoint() { } } + public static void initCoinConfigs(String[] args) { + String flagValue = null; + if (args != null) { + for (int i = 0; i < args.length; i++) { + if ("--blockchain-configuration-files".equals(args[i])) { + if (i + 1 < args.length && !args[i + 1].startsWith("--")) { + flagValue = args[i + 1]; + } else { + LOGGER.warning("[coinconfig] Missing value after --blockchain-configuration-files"); + } + break; + } + } + } + String envValue = getEnv("BLOCKCHAIN_CONFIGURATION_FILES"); + String source; + try { + source = new io.cloudchains.app.coinconfig.ConfigSourceResolver(flagValue, envValue).resolve(); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "[coinconfig] invalid source, using default: " + e.getMessage()); + source = io.cloudchains.app.coinconfig.ConfigSourceResolver.DEFAULT_UPSTREAM; + } + BLOCKCHAIN_CONFIGURATION_FILES = source; + try { + io.cloudchains.app.coinconfig.CoinConfigRegistry.load(source); + } catch (Exception e) { + LOGGER.log(Level.WARNING, + "[coinconfig] failed to load from " + source + ": " + e.getMessage() + + " — continuing with no configs; migrated coins will report UNSUPPORTEDCOIN"); + } + } + public static void main(String[] args) { for (String arg : args) { if (arg.equals("--version")) { @@ -114,8 +147,6 @@ public static void main(String[] args) { } } - initExrEndpoint(); - Level logLevel = parseLogLevel(getEnv("CLOUDCHAINS_LOG_LEVEL"), Level.INFO); LOGGER.setLevel(logLevel); LOGGER.setUseParentHandlers(false); @@ -155,6 +186,9 @@ protected synchronized void setOutputStream(OutputStream out) throws SecurityExc LOGGER.addHandler(consoleHandler); + initExrEndpoint(); + initCoinConfigs(args); + console = new ConsoleMenu(args); console.init(); } diff --git a/src/main/java/io/cloudchains/app/coinconfig/CoinConfigRegistry.java b/src/main/java/io/cloudchains/app/coinconfig/CoinConfigRegistry.java new file mode 100644 index 0000000..4f5fcec --- /dev/null +++ b/src/main/java/io/cloudchains/app/coinconfig/CoinConfigRegistry.java @@ -0,0 +1,84 @@ +package io.cloudchains.app.coinconfig; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; + +/** + * Process-wide holder for the loaded {@link CoinConfig} map. Initialized once + * at startup from the resolved blockchain-configuration-files source; tests + * may reload with a different source between cases. + */ +public final class CoinConfigRegistry { + + private static final LogManager LOGMANAGER = LogManager.getLogManager(); + private static final Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); + + private static volatile Map loaded = null; + private static volatile String loadedSource = null; + + private CoinConfigRegistry() { + } + + /** + * Load (or reload) all configs from the given source. On success the map + * replaces any prior load — startup calls once; tests may call repeatedly. + * Gate-rejected entries are logged and omitted from the map when the user + * chose warn-and-continue semantics; the registry retains every valid + * ticker. + */ + public static synchronized void load(String source) { + String resolved = source.replaceAll("/+$", ""); + Map all = new CoinConfigSource(resolved).loadAll(); + LinkedHashMap filtered = new LinkedHashMap<>(); + int rejected = 0; + for (Map.Entry e : all.entrySet()) { + try { + CoinConfigGate.validate(e.getValue()); + filtered.put(e.getKey(), e.getValue()); + } catch (IllegalStateException ex) { + LOGGER.log(Level.WARNING, + "[coinconfig] rejected [" + e.getKey() + "]: " + ex.getMessage()); + rejected++; + } + } + if (rejected > 0) { + LOGGER.log(Level.WARNING, + "[coinconfig] " + rejected + " ticker(s) rejected by gate; continuing with " + + filtered.size() + " valid"); + } + loaded = Collections.unmodifiableMap(filtered); + loadedSource = resolved; + LOGGER.info("[coinconfig] loaded " + loaded.size() + " ticker(s) from " + resolved); + } + + public static boolean isLoaded() { + return loaded != null; + } + + public static String loadedSource() { + return loadedSource; + } + + /** Fail-fast when registry has not been loaded yet. */ + public static CoinConfig get(String ticker) { + Map m = loaded; + if (m == null) + throw new IllegalStateException( + "CoinConfigRegistry not loaded — App.initCoinConfigs() must run before coin init"); + CoinConfig cfg = m.get(ticker); + if (cfg == null) + throw new IllegalStateException( + "[" + ticker + "] not present in loaded coin configs from " + loadedSource); + return cfg; + } + + /** Visible for tests — reset to unloaded state. */ + static synchronized void resetForTest() { + loaded = null; + loadedSource = null; + } +} diff --git a/src/main/java/io/cloudchains/app/console/ConsoleMenu.java b/src/main/java/io/cloudchains/app/console/ConsoleMenu.java index f3e6839..41a5a7d 100644 --- a/src/main/java/io/cloudchains/app/console/ConsoleMenu.java +++ b/src/main/java/io/cloudchains/app/console/ConsoleMenu.java @@ -114,6 +114,19 @@ public void init() { } break; } + case "--blockchain-configuration-files": { + // Flag already handled in App.initCoinConfigs(); just consume the value here + // to avoid falling through to the interactive menu. + if (i + 1 < arguments.length && !arguments[i + 1].startsWith("--")) { + i++; + } else { + String envVal = App.getEnv("BLOCKCHAIN_CONFIGURATION_FILES"); + if (envVal == null || envVal.isEmpty()) { + LOGGER.warning("Missing value after '--blockchain-configuration-files'"); + } + } + break; + } case "--version": LOGGER.info(Version.CLIENT_VERSION); System.exit(0); @@ -574,6 +587,8 @@ public static String getHelpText() { " Example: --development-endpoint \n" + " --exr-endpoint Set EXR endpoint for EXR server\n" + " Example: --exr-endpoint \n" + + " --blockchain-configuration-files Set blockchain-configuration-files source\n" + + " Example: --blockchain-configuration-files \n" + " --version Display the version\n" + " --createdefaultwallet Create a default wallet\n" + " --createwalletmnemonic Create a wallet with a mnemonic\n" + diff --git a/src/main/java/io/cloudchains/app/net/CoinInstance.java b/src/main/java/io/cloudchains/app/net/CoinInstance.java index 5a78eed..7eff81c 100644 --- a/src/main/java/io/cloudchains/app/net/CoinInstance.java +++ b/src/main/java/io/cloudchains/app/net/CoinInstance.java @@ -9,18 +9,7 @@ import io.cloudchains.app.crypto.KeyHandler; import io.cloudchains.app.net.api.JSONRPCController; import io.cloudchains.app.net.api.JSONRPCServer; -//import io.cloudchains.app.net.protocols.bitcoincash.BitcoinCashNetworkParameters; -import io.cloudchains.app.net.protocols.bitcoin.BitcoinNetworkParameters; import io.cloudchains.app.net.protocols.blocknet.*; -import io.cloudchains.app.net.protocols.dashcoin.DashcoinNetworkParameters; -import io.cloudchains.app.net.protocols.digibyte.DigibyteNetworkParameters; -import io.cloudchains.app.net.protocols.dogecoin.DogecoinNetworkParameters; -import io.cloudchains.app.net.protocols.litecoin.LitecoinNetworkParameters; -import io.cloudchains.app.net.protocols.pivx.PivxNetworkParameters; -import io.cloudchains.app.net.protocols.pocketcoin.PocketcoinNetworkParameters; -import io.cloudchains.app.net.protocols.ravencoin.RavencoinNetworkParameters; -import io.cloudchains.app.net.protocols.syscoin.SyscoinNetworkParameters; -import io.cloudchains.app.net.protocols.unobtanium.UnobtaniumNetworkParameters; import io.cloudchains.app.net.xrouter.XRouterMessage; import io.cloudchains.app.net.xrouter.XRouterPacketManager; import io.cloudchains.app.util.AddressBalance; @@ -363,67 +352,77 @@ public CoinError init(char[] pw, String userMnemonic, boolean isMnemonic, boolea } case BITCOIN: { LOGGER.fine("[coin] Initializing for Bitcoin main network."); - networkParameters = new BitcoinNetworkParameters(); + CoinError err = loadMigratedParams(); + if (err != null) return err; rpcPort = 8332; break; } // case BITCOIN_CASH: { // LOGGER.fine("[coin] Initializing for BitcoinCash main network."); - // networkParameters = new BitcoinCashNetworkParameters(); + // networkParameters = new io.cloudchains.app.net.protocols.bitcoincash.BitcoinCashNetworkParametersLegacy(); // rpcPort = 48332; // break; // } case LITECOIN: { LOGGER.fine("[coin] Initializing for Litecoin main network."); - networkParameters = new LitecoinNetworkParameters(); + CoinError err = loadMigratedParams(); + if (err != null) return err; rpcPort = 9332; break; } case DASHCOIN: { LOGGER.fine("[coin] Initializing for Dashcoin main network."); - networkParameters = new DashcoinNetworkParameters(); + CoinError err = loadMigratedParams(); + if (err != null) return err; rpcPort = 9998; break; } case DIGIBYTE: { LOGGER.fine("[coin] Initializing for Digibyte main network."); - networkParameters = new DigibyteNetworkParameters(); + CoinError err = loadMigratedParams(); + if (err != null) return err; rpcPort = 14022; break; } case DOGECOIN: { LOGGER.fine("[coin] Initializing for Dogecoin main network."); - networkParameters = new DogecoinNetworkParameters(); + CoinError err = loadMigratedParams(); + if (err != null) return err; rpcPort = 22555; break; } case SYSCOIN: { LOGGER.fine("[coin] Initializing for Syscoin main network."); - networkParameters = new SyscoinNetworkParameters(); + CoinError err = loadMigratedParams(); + if (err != null) return err; rpcPort = 8370; break; } case PIVX: { LOGGER.fine("[coin] Initializing for Pivx main network."); - networkParameters = new PivxNetworkParameters(); + CoinError err = loadMigratedParams(); + if (err != null) return err; rpcPort = 9951; break; } case UNOBTANIUM: { LOGGER.fine("[coin] Initializing for Unobtanium main network."); - networkParameters = new UnobtaniumNetworkParameters(); + CoinError err = loadMigratedParams(); + if (err != null) return err; rpcPort = 65111; break; } case PKOIN: { LOGGER.fine("[coin] Initializing for Pocketcoin main network."); - networkParameters = new PocketcoinNetworkParameters(); + CoinError err = loadMigratedParams(); + if (err != null) return err; rpcPort = 37071; break; } case RAVENCOIN: { LOGGER.fine("[coin] Initializing for Ravencoin main network."); - networkParameters = new RavencoinNetworkParameters(); + CoinError err = loadMigratedParams(); + if (err != null) return err; rpcPort = 8766; break; } @@ -559,6 +558,18 @@ public CoinError init(char[] pw, String userMnemonic, boolean isMnemonic, boolea return null; } + private CoinError loadMigratedParams() { + try { + networkParameters = io.cloudchains.app.coinconfig.ConfigurableNetworkParameters + .from(io.cloudchains.app.coinconfig.CoinConfigRegistry + .get(CoinTickerUtils.tickerToString(ticker))); + return null; + } catch (IllegalStateException | IllegalArgumentException e) { + LOGGER.warning("[coin] [" + ticker + "] missing/invalid config: " + e.getMessage()); + return new CoinError("Unsupported coin", CoinError.CoinErrorCode.UNSUPPORTEDCOIN); + } + } + private void generateForwardAddresses(boolean fromStartup) { int configAddressCount = configHelper.getAddressCount(); boolean updateConfig = false; diff --git a/src/main/java/io/cloudchains/app/net/protocols/bitcoin/BitcoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/bitcoin/BitcoinNetworkParametersLegacy.java similarity index 67% rename from src/main/java/io/cloudchains/app/net/protocols/bitcoin/BitcoinNetworkParameters.java rename to src/main/java/io/cloudchains/app/net/protocols/bitcoin/BitcoinNetworkParametersLegacy.java index 2bce613..2499f03 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/bitcoin/BitcoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/bitcoin/BitcoinNetworkParametersLegacy.java @@ -3,9 +3,9 @@ import io.cloudchains.app.net.HasFeeParams; import org.bitcoinj.params.MainNetParams; -public class BitcoinNetworkParameters extends MainNetParams implements HasFeeParams { +public class BitcoinNetworkParametersLegacy extends MainNetParams implements HasFeeParams { - public BitcoinNetworkParameters() { + public BitcoinNetworkParametersLegacy() { super(); } diff --git a/src/main/java/io/cloudchains/app/net/protocols/bitcoincash/BitcoinCashNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/bitcoincash/BitcoinCashNetworkParametersLegacy.java similarity index 92% rename from src/main/java/io/cloudchains/app/net/protocols/bitcoincash/BitcoinCashNetworkParameters.java rename to src/main/java/io/cloudchains/app/net/protocols/bitcoincash/BitcoinCashNetworkParametersLegacy.java index 658d945..b37d5b4 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/bitcoincash/BitcoinCashNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/bitcoincash/BitcoinCashNetworkParametersLegacy.java @@ -6,9 +6,9 @@ import org.bitcoinj.store.BlockStoreException; import org.bitcoinj.utils.MonetaryFormat; -public class BitcoinCashNetworkParameters extends NetworkParameters implements HasFeeParams { +public class BitcoinCashNetworkParametersLegacy extends NetworkParameters implements HasFeeParams { - public BitcoinCashNetworkParameters() { + public BitcoinCashNetworkParametersLegacy() { super(); } diff --git a/src/main/java/io/cloudchains/app/net/protocols/dashcoin/DashcoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/dashcoin/DashcoinNetworkParametersLegacy.java similarity index 92% rename from src/main/java/io/cloudchains/app/net/protocols/dashcoin/DashcoinNetworkParameters.java rename to src/main/java/io/cloudchains/app/net/protocols/dashcoin/DashcoinNetworkParametersLegacy.java index f98e28e..e664deb 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/dashcoin/DashcoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/dashcoin/DashcoinNetworkParametersLegacy.java @@ -6,9 +6,9 @@ import org.bitcoinj.store.BlockStoreException; import org.bitcoinj.utils.MonetaryFormat; -public class DashcoinNetworkParameters extends NetworkParameters implements HasFeeParams { +public class DashcoinNetworkParametersLegacy extends NetworkParameters implements HasFeeParams { - public DashcoinNetworkParameters() { + public DashcoinNetworkParametersLegacy() { super(); } diff --git a/src/main/java/io/cloudchains/app/net/protocols/digibyte/DigibyteNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/digibyte/DigibyteNetworkParametersLegacy.java similarity index 92% rename from src/main/java/io/cloudchains/app/net/protocols/digibyte/DigibyteNetworkParameters.java rename to src/main/java/io/cloudchains/app/net/protocols/digibyte/DigibyteNetworkParametersLegacy.java index 48efe97..7cc7163 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/digibyte/DigibyteNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/digibyte/DigibyteNetworkParametersLegacy.java @@ -6,9 +6,9 @@ import org.bitcoinj.store.BlockStoreException; import org.bitcoinj.utils.MonetaryFormat; -public class DigibyteNetworkParameters extends NetworkParameters implements HasFeeParams { +public class DigibyteNetworkParametersLegacy extends NetworkParameters implements HasFeeParams { - public DigibyteNetworkParameters() { + public DigibyteNetworkParametersLegacy() { super(); } diff --git a/src/main/java/io/cloudchains/app/net/protocols/dogecoin/DogecoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/dogecoin/DogecoinNetworkParametersLegacy.java similarity index 92% rename from src/main/java/io/cloudchains/app/net/protocols/dogecoin/DogecoinNetworkParameters.java rename to src/main/java/io/cloudchains/app/net/protocols/dogecoin/DogecoinNetworkParametersLegacy.java index ff9a13f..0c13022 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/dogecoin/DogecoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/dogecoin/DogecoinNetworkParametersLegacy.java @@ -6,9 +6,9 @@ import org.bitcoinj.store.BlockStoreException; import org.bitcoinj.utils.MonetaryFormat; -public class DogecoinNetworkParameters extends NetworkParameters implements HasFeeParams { +public class DogecoinNetworkParametersLegacy extends NetworkParameters implements HasFeeParams { - public DogecoinNetworkParameters() { + public DogecoinNetworkParametersLegacy() { super(); } diff --git a/src/main/java/io/cloudchains/app/net/protocols/litecoin/LitecoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/litecoin/LitecoinNetworkParametersLegacy.java similarity index 93% rename from src/main/java/io/cloudchains/app/net/protocols/litecoin/LitecoinNetworkParameters.java rename to src/main/java/io/cloudchains/app/net/protocols/litecoin/LitecoinNetworkParametersLegacy.java index e142039..c34df5d 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/litecoin/LitecoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/litecoin/LitecoinNetworkParametersLegacy.java @@ -6,9 +6,9 @@ import org.bitcoinj.store.BlockStoreException; import org.bitcoinj.utils.MonetaryFormat; -public class LitecoinNetworkParameters extends NetworkParameters implements HasFeeParams { +public class LitecoinNetworkParametersLegacy extends NetworkParameters implements HasFeeParams { - public LitecoinNetworkParameters() { + public LitecoinNetworkParametersLegacy() { super(); } diff --git a/src/main/java/io/cloudchains/app/net/protocols/pivx/PivxNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/pivx/PivxNetworkParametersLegacy.java similarity index 93% rename from src/main/java/io/cloudchains/app/net/protocols/pivx/PivxNetworkParameters.java rename to src/main/java/io/cloudchains/app/net/protocols/pivx/PivxNetworkParametersLegacy.java index 444a09e..fb9bf73 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/pivx/PivxNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/pivx/PivxNetworkParametersLegacy.java @@ -6,9 +6,9 @@ import org.bitcoinj.store.BlockStoreException; import org.bitcoinj.utils.MonetaryFormat; -public class PivxNetworkParameters extends NetworkParameters implements HasFeeParams { +public class PivxNetworkParametersLegacy extends NetworkParameters implements HasFeeParams { - public PivxNetworkParameters() { + public PivxNetworkParametersLegacy() { super(); } diff --git a/src/main/java/io/cloudchains/app/net/protocols/pocketcoin/PocketcoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/pocketcoin/PocketcoinNetworkParametersLegacy.java similarity index 92% rename from src/main/java/io/cloudchains/app/net/protocols/pocketcoin/PocketcoinNetworkParameters.java rename to src/main/java/io/cloudchains/app/net/protocols/pocketcoin/PocketcoinNetworkParametersLegacy.java index a384dae..55ab08c 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/pocketcoin/PocketcoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/pocketcoin/PocketcoinNetworkParametersLegacy.java @@ -6,9 +6,9 @@ import org.bitcoinj.store.BlockStoreException; import org.bitcoinj.utils.MonetaryFormat; -public class PocketcoinNetworkParameters extends NetworkParameters implements HasFeeParams { +public class PocketcoinNetworkParametersLegacy extends NetworkParameters implements HasFeeParams { - public PocketcoinNetworkParameters() { + public PocketcoinNetworkParametersLegacy() { super(); } diff --git a/src/main/java/io/cloudchains/app/net/protocols/ravencoin/RavencoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/ravencoin/RavencoinNetworkParametersLegacy.java similarity index 92% rename from src/main/java/io/cloudchains/app/net/protocols/ravencoin/RavencoinNetworkParameters.java rename to src/main/java/io/cloudchains/app/net/protocols/ravencoin/RavencoinNetworkParametersLegacy.java index 9288d5d..1179014 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/ravencoin/RavencoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/ravencoin/RavencoinNetworkParametersLegacy.java @@ -6,9 +6,9 @@ import org.bitcoinj.store.BlockStoreException; import org.bitcoinj.utils.MonetaryFormat; -public class RavencoinNetworkParameters extends NetworkParameters implements HasFeeParams { +public class RavencoinNetworkParametersLegacy extends NetworkParameters implements HasFeeParams { - public RavencoinNetworkParameters() { + public RavencoinNetworkParametersLegacy() { super(); } diff --git a/src/main/java/io/cloudchains/app/net/protocols/syscoin/SyscoinNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/syscoin/SyscoinNetworkParametersLegacy.java similarity index 92% rename from src/main/java/io/cloudchains/app/net/protocols/syscoin/SyscoinNetworkParameters.java rename to src/main/java/io/cloudchains/app/net/protocols/syscoin/SyscoinNetworkParametersLegacy.java index 262318b..95d8745 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/syscoin/SyscoinNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/syscoin/SyscoinNetworkParametersLegacy.java @@ -6,9 +6,9 @@ import org.bitcoinj.store.BlockStoreException; import org.bitcoinj.utils.MonetaryFormat; -public class SyscoinNetworkParameters extends NetworkParameters implements HasFeeParams { +public class SyscoinNetworkParametersLegacy extends NetworkParameters implements HasFeeParams { - public SyscoinNetworkParameters() { + public SyscoinNetworkParametersLegacy() { super(); } diff --git a/src/main/java/io/cloudchains/app/net/protocols/unobtanium/UnobtaniumNetworkParameters.java b/src/main/java/io/cloudchains/app/net/protocols/unobtanium/UnobtaniumNetworkParametersLegacy.java similarity index 92% rename from src/main/java/io/cloudchains/app/net/protocols/unobtanium/UnobtaniumNetworkParameters.java rename to src/main/java/io/cloudchains/app/net/protocols/unobtanium/UnobtaniumNetworkParametersLegacy.java index b332fdb..91ab422 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/unobtanium/UnobtaniumNetworkParameters.java +++ b/src/main/java/io/cloudchains/app/net/protocols/unobtanium/UnobtaniumNetworkParametersLegacy.java @@ -6,9 +6,9 @@ import org.bitcoinj.store.BlockStoreException; import org.bitcoinj.utils.MonetaryFormat; -public class UnobtaniumNetworkParameters extends NetworkParameters implements HasFeeParams { +public class UnobtaniumNetworkParametersLegacy extends NetworkParameters implements HasFeeParams { - public UnobtaniumNetworkParameters() { + public UnobtaniumNetworkParametersLegacy() { super(); } diff --git a/src/test/java/TestHelper.java b/src/test/java/TestHelper.java index aa7b06b..0ac5124 100644 --- a/src/test/java/TestHelper.java +++ b/src/test/java/TestHelper.java @@ -82,6 +82,18 @@ public void commonSetup() { clean(); // Disable address discovery during tests to prevent interference with deterministic address generation CoinInstance.setAddressDiscoveryEnabled(false); + // Ensure coin configs are available for tests that init migrated coins + if (!io.cloudchains.app.coinconfig.CoinConfigRegistry.isLoaded()) { + try { + String sibling = java.nio.file.Paths.get("..", "blockchain-configuration-files") + .toAbsolutePath().normalize().toString(); + io.cloudchains.app.coinconfig.CoinConfigRegistry.load(sibling); + } catch (Exception e) { + java.util.logging.Logger.getLogger(java.util.logging.Logger.GLOBAL_LOGGER_NAME) + .warning("[TestHelper] coin config load failed: " + e.getMessage() + + " — tests requiring migrated coins will get UNSUPPORTEDCOIN"); + } + } } /** diff --git a/src/test/java/WalletHelperFeeTest.java b/src/test/java/WalletHelperFeeTest.java index 91ad881..a838384 100644 --- a/src/test/java/WalletHelperFeeTest.java +++ b/src/test/java/WalletHelperFeeTest.java @@ -1,15 +1,15 @@ -import io.cloudchains.app.net.protocols.bitcoin.BitcoinNetworkParameters; +import io.cloudchains.app.net.protocols.bitcoin.BitcoinNetworkParametersLegacy; import io.cloudchains.app.net.protocols.blocknet.BlocknetNetworkParameters; import io.cloudchains.app.net.protocols.blocknet.BlocknetTestnet5NetworkParameters; -import io.cloudchains.app.net.protocols.dashcoin.DashcoinNetworkParameters; -import io.cloudchains.app.net.protocols.digibyte.DigibyteNetworkParameters; -import io.cloudchains.app.net.protocols.dogecoin.DogecoinNetworkParameters; -import io.cloudchains.app.net.protocols.litecoin.LitecoinNetworkParameters; -import io.cloudchains.app.net.protocols.pivx.PivxNetworkParameters; -import io.cloudchains.app.net.protocols.pocketcoin.PocketcoinNetworkParameters; -import io.cloudchains.app.net.protocols.ravencoin.RavencoinNetworkParameters; -import io.cloudchains.app.net.protocols.syscoin.SyscoinNetworkParameters; -import io.cloudchains.app.net.protocols.unobtanium.UnobtaniumNetworkParameters; +import io.cloudchains.app.net.protocols.dashcoin.DashcoinNetworkParametersLegacy; +import io.cloudchains.app.net.protocols.digibyte.DigibyteNetworkParametersLegacy; +import io.cloudchains.app.net.protocols.dogecoin.DogecoinNetworkParametersLegacy; +import io.cloudchains.app.net.protocols.litecoin.LitecoinNetworkParametersLegacy; +import io.cloudchains.app.net.protocols.pivx.PivxNetworkParametersLegacy; +import io.cloudchains.app.net.protocols.pocketcoin.PocketcoinNetworkParametersLegacy; +import io.cloudchains.app.net.protocols.ravencoin.RavencoinNetworkParametersLegacy; +import io.cloudchains.app.net.protocols.syscoin.SyscoinNetworkParametersLegacy; +import io.cloudchains.app.net.protocols.unobtanium.UnobtaniumNetworkParametersLegacy; import io.cloudchains.app.wallet.WalletHelper; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.params.TestNet3Params; @@ -38,13 +38,13 @@ static void cleanup() { @Test void testGetFeePerByte_Bitcoin() { - BitcoinNetworkParameters params = new BitcoinNetworkParameters(); + BitcoinNetworkParametersLegacy params = new BitcoinNetworkParametersLegacy(); assertEquals(60L, WalletHelper.getFeePerByte(params)); } @Test void testGetFeePerByte_Litecoin() { - LitecoinNetworkParameters params = new LitecoinNetworkParameters(); + LitecoinNetworkParametersLegacy params = new LitecoinNetworkParametersLegacy(); assertEquals(10L, WalletHelper.getFeePerByte(params)); } @@ -56,49 +56,49 @@ void testGetFeePerByte_Blocknet() { @Test void testGetFeePerByte_Dashcoin() { - DashcoinNetworkParameters params = new DashcoinNetworkParameters(); + DashcoinNetworkParametersLegacy params = new DashcoinNetworkParametersLegacy(); assertEquals(5L, WalletHelper.getFeePerByte(params)); } @Test void testGetFeePerByte_Digibyte() { - DigibyteNetworkParameters params = new DigibyteNetworkParameters(); + DigibyteNetworkParametersLegacy params = new DigibyteNetworkParametersLegacy(); assertEquals(200L, WalletHelper.getFeePerByte(params)); } @Test void testGetFeePerByte_Dogecoin() { - DogecoinNetworkParameters params = new DogecoinNetworkParameters(); + DogecoinNetworkParametersLegacy params = new DogecoinNetworkParametersLegacy(); assertEquals(2500L, WalletHelper.getFeePerByte(params)); } @Test void testGetFeePerByte_Syscoin() { - SyscoinNetworkParameters params = new SyscoinNetworkParameters(); + SyscoinNetworkParametersLegacy params = new SyscoinNetworkParametersLegacy(); assertEquals(40L, WalletHelper.getFeePerByte(params)); } @Test void testGetFeePerByte_Pivx() { - PivxNetworkParameters params = new PivxNetworkParameters(); + PivxNetworkParametersLegacy params = new PivxNetworkParametersLegacy(); assertEquals(20L, WalletHelper.getFeePerByte(params)); } @Test void testGetFeePerByte_Unobtanium() { - UnobtaniumNetworkParameters params = new UnobtaniumNetworkParameters(); + UnobtaniumNetworkParametersLegacy params = new UnobtaniumNetworkParametersLegacy(); assertEquals(3L, WalletHelper.getFeePerByte(params)); } @Test void testGetFeePerByte_Pocketcoin() { - PocketcoinNetworkParameters params = new PocketcoinNetworkParameters(); + PocketcoinNetworkParametersLegacy params = new PocketcoinNetworkParametersLegacy(); assertEquals(20L, WalletHelper.getFeePerByte(params)); } @Test void testGetFeePerByte_Ravencoin() { - RavencoinNetworkParameters params = new RavencoinNetworkParameters(); + RavencoinNetworkParametersLegacy params = new RavencoinNetworkParametersLegacy(); assertEquals(1000L, WalletHelper.getFeePerByte(params)); } @@ -114,13 +114,13 @@ void testGetFeePerByte_BlocknetTestnet5() { @Test void testGetMinTxFee_Bitcoin() { - BitcoinNetworkParameters params = new BitcoinNetworkParameters(); + BitcoinNetworkParametersLegacy params = new BitcoinNetworkParametersLegacy(); assertEquals(12000L, WalletHelper.getMinTxFee(params)); } @Test void testGetMinTxFee_Litecoin() { - LitecoinNetworkParameters params = new LitecoinNetworkParameters(); + LitecoinNetworkParametersLegacy params = new LitecoinNetworkParametersLegacy(); assertEquals(5000L, WalletHelper.getMinTxFee(params)); } @@ -132,49 +132,49 @@ void testGetMinTxFee_Blocknet() { @Test void testGetMinTxFee_Dashcoin() { - DashcoinNetworkParameters params = new DashcoinNetworkParameters(); + DashcoinNetworkParametersLegacy params = new DashcoinNetworkParametersLegacy(); assertEquals(2500L, WalletHelper.getMinTxFee(params)); } @Test void testGetMinTxFee_Digibyte() { - DigibyteNetworkParameters params = new DigibyteNetworkParameters(); + DigibyteNetworkParametersLegacy params = new DigibyteNetworkParametersLegacy(); assertEquals(100000L, WalletHelper.getMinTxFee(params)); } @Test void testGetMinTxFee_Dogecoin() { - DogecoinNetworkParameters params = new DogecoinNetworkParameters(); + DogecoinNetworkParametersLegacy params = new DogecoinNetworkParametersLegacy(); assertEquals(225000L, WalletHelper.getMinTxFee(params)); } @Test void testGetMinTxFee_Syscoin() { - SyscoinNetworkParameters params = new SyscoinNetworkParameters(); + SyscoinNetworkParametersLegacy params = new SyscoinNetworkParametersLegacy(); assertEquals(20000L, WalletHelper.getMinTxFee(params)); } @Test void testGetMinTxFee_Pivx() { - PivxNetworkParameters params = new PivxNetworkParameters(); + PivxNetworkParametersLegacy params = new PivxNetworkParametersLegacy(); assertEquals(10000L, WalletHelper.getMinTxFee(params)); } @Test void testGetMinTxFee_Unobtanium() { - UnobtaniumNetworkParameters params = new UnobtaniumNetworkParameters(); + UnobtaniumNetworkParametersLegacy params = new UnobtaniumNetworkParametersLegacy(); assertEquals(1000L, WalletHelper.getMinTxFee(params)); } @Test void testGetMinTxFee_Pocketcoin() { - PocketcoinNetworkParameters params = new PocketcoinNetworkParameters(); + PocketcoinNetworkParametersLegacy params = new PocketcoinNetworkParametersLegacy(); assertEquals(10000L, WalletHelper.getMinTxFee(params)); } @Test void testGetMinTxFee_Ravencoin() { - RavencoinNetworkParameters params = new RavencoinNetworkParameters(); + RavencoinNetworkParametersLegacy params = new RavencoinNetworkParametersLegacy(); assertEquals(100000L, WalletHelper.getMinTxFee(params)); } @@ -184,6 +184,31 @@ void testGetMinTxFee_BlocknetTestnet5() { assertEquals(10000L, WalletHelper.getMinTxFee(params)); } + // ======================================================================== + // Generic path — fee values must match loaded configs + // ======================================================================== + + @Test + void testGenericFeesMatchLoadedConfigs() { + // Load from sibling bcf checkout (same source as CrossCheckTest) + java.util.Map cfgs = + new io.cloudchains.app.coinconfig.CoinConfigSource( + java.nio.file.Paths.get("..", "blockchain-configuration-files") + .toAbsolutePath().normalize().toString()).loadAll(); + for (java.util.Map.Entry e : cfgs.entrySet()) { + String ticker = e.getKey(); + io.cloudchains.app.coinconfig.CoinConfig cfg = e.getValue(); + // Only check migrated tickers that WalletHelper knows via HasFeeParams + if (!io.cloudchains.app.coinconfig.CompiledCoinSupplement.supports(ticker)) + continue; + io.cloudchains.app.coinconfig.ConfigurableNetworkParameters generic = + io.cloudchains.app.coinconfig.ConfigurableNetworkParameters.from(cfg); + assertEquals(cfg.feePerByte(), WalletHelper.getFeePerByte(generic), + ticker + " generic feePerByte must equal config"); + assertEquals(cfg.minTxFee(), WalletHelper.getMinTxFee(generic), + ticker + " generic minTxFee must equal config"); + } + } // ======================================================================== // Edge Case - unknown coin returns default // ======================================================================== @@ -194,6 +219,7 @@ void testGetFeePerByte_UnknownCoin_Throws() { assertThrows(RuntimeException.class, () -> WalletHelper.getFeePerByte(unknownParams)); } + @Test void testGetMinTxFee_UnknownCoin_Throws() { NetworkParameters unknownParams = TestNet3Params.get(); diff --git a/src/test/java/io/cloudchains/app/coinconfig/ConfigurableNetworkParametersCrossCheckTest.java b/src/test/java/io/cloudchains/app/coinconfig/ConfigurableNetworkParametersCrossCheckTest.java index 2c0e0e8..e89dd24 100644 --- a/src/test/java/io/cloudchains/app/coinconfig/ConfigurableNetworkParametersCrossCheckTest.java +++ b/src/test/java/io/cloudchains/app/coinconfig/ConfigurableNetworkParametersCrossCheckTest.java @@ -1,16 +1,16 @@ package io.cloudchains.app.coinconfig; -import io.cloudchains.app.net.protocols.bitcoin.BitcoinNetworkParameters; -import io.cloudchains.app.net.protocols.bitcoincash.BitcoinCashNetworkParameters; -import io.cloudchains.app.net.protocols.dashcoin.DashcoinNetworkParameters; -import io.cloudchains.app.net.protocols.digibyte.DigibyteNetworkParameters; -import io.cloudchains.app.net.protocols.dogecoin.DogecoinNetworkParameters; -import io.cloudchains.app.net.protocols.litecoin.LitecoinNetworkParameters; -import io.cloudchains.app.net.protocols.pivx.PivxNetworkParameters; -import io.cloudchains.app.net.protocols.pocketcoin.PocketcoinNetworkParameters; -import io.cloudchains.app.net.protocols.ravencoin.RavencoinNetworkParameters; -import io.cloudchains.app.net.protocols.syscoin.SyscoinNetworkParameters; -import io.cloudchains.app.net.protocols.unobtanium.UnobtaniumNetworkParameters; +import io.cloudchains.app.net.protocols.bitcoin.BitcoinNetworkParametersLegacy; +import io.cloudchains.app.net.protocols.bitcoincash.BitcoinCashNetworkParametersLegacy; +import io.cloudchains.app.net.protocols.dashcoin.DashcoinNetworkParametersLegacy; +import io.cloudchains.app.net.protocols.digibyte.DigibyteNetworkParametersLegacy; +import io.cloudchains.app.net.protocols.dogecoin.DogecoinNetworkParametersLegacy; +import io.cloudchains.app.net.protocols.litecoin.LitecoinNetworkParametersLegacy; +import io.cloudchains.app.net.protocols.pivx.PivxNetworkParametersLegacy; +import io.cloudchains.app.net.protocols.pocketcoin.PocketcoinNetworkParametersLegacy; +import io.cloudchains.app.net.protocols.ravencoin.RavencoinNetworkParametersLegacy; +import io.cloudchains.app.net.protocols.syscoin.SyscoinNetworkParametersLegacy; +import io.cloudchains.app.net.protocols.unobtanium.UnobtaniumNetworkParametersLegacy; import io.cloudchains.app.wallet.WalletHelper; import org.bitcoinj.core.LegacyAddress; import org.bitcoinj.core.NetworkParameters; @@ -139,27 +139,27 @@ private NetworkParametersPair pairFor(String ticker, Map cfg LegacyFactory factory; switch (ticker) { case "BTC": - factory = BitcoinNetworkParameters::new; break; + factory = BitcoinNetworkParametersLegacy::new; break; case "BCH": - factory = BitcoinCashNetworkParameters::new; break; + factory = BitcoinCashNetworkParametersLegacy::new; break; case "DASH": - factory = DashcoinNetworkParameters::new; break; + factory = DashcoinNetworkParametersLegacy::new; break; case "DGB": - factory = DigibyteNetworkParameters::new; break; + factory = DigibyteNetworkParametersLegacy::new; break; case "DOGE": - factory = DogecoinNetworkParameters::new; break; + factory = DogecoinNetworkParametersLegacy::new; break; case "LTC": - factory = LitecoinNetworkParameters::new; break; + factory = LitecoinNetworkParametersLegacy::new; break; case "PIVX": - factory = PivxNetworkParameters::new; break; + factory = PivxNetworkParametersLegacy::new; break; case "PKOIN": - factory = PocketcoinNetworkParameters::new; break; + factory = PocketcoinNetworkParametersLegacy::new; break; case "RVN": - factory = RavencoinNetworkParameters::new; break; + factory = RavencoinNetworkParametersLegacy::new; break; case "SYS": - factory = SyscoinNetworkParameters::new; break; + factory = SyscoinNetworkParametersLegacy::new; break; case "UNO": - factory = UnobtaniumNetworkParameters::new; break; + factory = UnobtaniumNetworkParametersLegacy::new; break; default: throw new IllegalArgumentException(ticker); } From 91d9a3867500d9918c4128b29aaee876782e724b Mon Sep 17 00:00:00 2001 From: tryiou Date: Wed, 26 Aug 2026 20:40:33 +0200 Subject: [PATCH 63/73] fix(coinconfig): handle stale and historical remote manifest entries Remote manifest-latest.json contains 142 historical entries for 125 unique tickers plus a few stale xbridge confs (oasis 404) and 4 gate-rejected tickers (AUS prefix collision, GLC/MRX/ZNZ zero fees). Local sibling is deduped to 41. Make the loader dedup by keeping last (and warning), validate xbridge_conf filenames via pre-compiled pattern, and treat per-entry fetch or missing-section failures as warn and skip for remote sources but fail hard for local checkouts. Add empty-result guard and keep last-wins semantics for duplicates. --- .../app/coinconfig/CoinConfigSource.java | 80 +++++++++++++++---- .../coinconfig/CoinConfigSourceLocalTest.java | 16 ++-- 2 files changed, 75 insertions(+), 21 deletions(-) diff --git a/src/main/java/io/cloudchains/app/coinconfig/CoinConfigSource.java b/src/main/java/io/cloudchains/app/coinconfig/CoinConfigSource.java index 46c2856..1f3366a 100644 --- a/src/main/java/io/cloudchains/app/coinconfig/CoinConfigSource.java +++ b/src/main/java/io/cloudchains/app/coinconfig/CoinConfigSource.java @@ -16,17 +16,28 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.Scanner; +import java.util.logging.LogManager; +import java.util.logging.Logger; +import java.util.regex.Pattern; /** * Loads every coin's {@link CoinConfig} from a blockchain-configuration-files * source (local directory or base URL), resolving once at daemon start. * - *

Failure policy is fail-hard: any unreadable manifest, missing conf file, - * missing conf section or unparsable value throws; the daemon refuses to - * start rather than guess coin parameters.

+ *

Failure policy: manifest shape and required fields are fail-hard (missing + * ticker/blockchain/xbridge_conf aborts the whole load). Per-entry + * xbridge-conf fetch/parse and missing ticker sections are + * fail-open-warn-and-skip for remote sources (upstream may contain stale + * entries like {@code oasis--v3.0.0.conf}) and fail-hard for local + * directories (developer error). The daemon therefore starts even if a few + * remote entries are stale, but a broken local checkout is caught early.

*/ public final class CoinConfigSource { + private static final LogManager LOGMANAGER = LogManager.getLogManager(); + private static final Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); + private static final Pattern XBRIDGE_CONF_PATTERN = Pattern.compile("^[A-Za-z0-9_\\-\\.]+\\.conf$"); + private final String base; /** @@ -60,6 +71,7 @@ public Map loadAll() { throw new IllegalStateException("manifest lists no coins"); Map out = new LinkedHashMap<>(); int index = 0; + int skipped = 0; for (JsonElement el : entries) { index++; JsonObject entry; @@ -69,23 +81,63 @@ public Map loadAll() { final String blockchain = requiredField(entry, "blockchain", index); final String verId = entry.has("ver_id") ? entry.get("ver_id").getAsString() : ""; final String xbridgeConf = requiredField(entry, "xbridge_conf", index); - - CoinConfig prev = out.get(ticker); - if (prev != null) - throw new IllegalStateException("duplicate manifest ticker " + ticker); - - Map> sections = isLocal() - ? safeParseLocal(localXBridgeConf(xbridgeConf)) - : XBridgeConfParser.parse(fetchRemote(xbridgeConfUrl(xbridgeConf))); + if (!XBRIDGE_CONF_PATTERN.matcher(xbridgeConf).matches() || xbridgeConf.contains("..")) { + String msg = "xbridge_conf filename fails validation: " + xbridgeConf; + if (isLocal()) { + throw new IllegalStateException(msg); + } else { + LOGGER.warning("[coinconfig] skipping [" + ticker + "] entry #" + index + ": " + msg); + skipped++; + continue; + } + } + if (out.containsKey(ticker)) { + LOGGER.warning("[coinconfig] duplicate ticker " + ticker + " entry #" + index + + " overwriting previous (keeping last)"); + } + Map> sections; + try { + sections = isLocal() + ? safeParseLocal(localXBridgeConf(xbridgeConf)) + : XBridgeConfParser.parse(fetchRemote(xbridgeConfUrl(xbridgeConf))); + } catch (RuntimeException fe) { + if (isLocal()) { + throw fe; + } else { + LOGGER.warning("[coinconfig] skipping [" + ticker + "] entry #" + index + + " (" + xbridgeConf + "): " + fe.getMessage()); + skipped++; + continue; + } + } Map section = sections.get(ticker); - if (section == null) - throw new IllegalStateException("xbridge conf " + xbridgeConf - + " has no [" + ticker + "] section"); + if (section == null) { + String msg = "xbridge conf " + xbridgeConf + " has no [" + ticker + "] section"; + if (isLocal()) { + throw new IllegalStateException(msg); + } else { + LOGGER.warning("[coinconfig] skipping [" + ticker + "] entry #" + index + ": " + msg); + skipped++; + continue; + } + } out.put(ticker, new CoinConfig(ticker, blockchain, verId, section)); } catch (RuntimeException e) { + // Required-field or JSON shape errors are still hard failures; + // per-entry fetch/section issues for local are re-thrown above + // and will be wrapped here; remote skips are already continued. + if (e.getMessage() != null && e.getMessage().startsWith("manifest entry #")) { + throw e; + } throw new IllegalStateException("manifest entry #" + index + ": " + e.getMessage(), e); } } + if (skipped > 0) { + LOGGER.warning("[coinconfig] skipped " + skipped + " manifest entries with missing/unreadable xbridge confs"); + } + if (out.isEmpty()) { + throw new IllegalStateException("manifest: no loadable entries (all " + skipped + " skipped)"); + } return out; } diff --git a/src/test/java/io/cloudchains/app/coinconfig/CoinConfigSourceLocalTest.java b/src/test/java/io/cloudchains/app/coinconfig/CoinConfigSourceLocalTest.java index a18e76b..142a2db 100644 --- a/src/test/java/io/cloudchains/app/coinconfig/CoinConfigSourceLocalTest.java +++ b/src/test/java/io/cloudchains/app/coinconfig/CoinConfigSourceLocalTest.java @@ -77,17 +77,19 @@ void testManifestEntryWithoutSectionFailsHard(@TempDir Path dir) throws Exceptio } @Test - void testDuplicateManifestTickerFailsHard(@TempDir Path dir) throws Exception { + void testDuplicateManifestTickerKeepsLast(@TempDir Path dir) throws Exception { Files.createDirectories(dir.resolve("xbridge-confs")); Files.writeString(dir.resolve("manifest-latest.json"), "[{\"blockchain\":\"A\",\"ticker\":\"DUP\",\"xbridge_conf\":\"a.conf\"}," + "{\"blockchain\":\"B\",\"ticker\":\"DUP\",\"xbridge_conf\":\"b.conf\"}]"); - Files.writeString(dir.resolve("xbridge-confs").resolve("a.conf"), "[DUP]\nK=V\n"); - // b.conf deliberately ABSENT: if the loader read confs before the - // duplicate check, the failure would be "Cannot read" instead. - IllegalStateException e = assertThrows(IllegalStateException.class, - () -> new CoinConfigSource(dir.toString()).loadAll()); - assertTrue(e.getMessage().contains("duplicate manifest ticker DUP"), e.getMessage()); + Files.writeString(dir.resolve("xbridge-confs").resolve("a.conf"), "[DUP]\nK=V1\n"); + Files.writeString(dir.resolve("xbridge-confs").resolve("b.conf"), "[DUP]\nK=V2\n"); + // Historical manifests (remote master) contain multiple entries per ticker; + // the loader keeps the last occurrence (latest version). + Map all = new CoinConfigSource(dir.toString()).loadAll(); + assertEquals(1, all.size()); + assertEquals("B", all.get("DUP").getBlockchain()); + assertEquals("V2", all.get("DUP").getConfEntries().get("K")); } @Test From 3ae088e211c595bf4e9abc579a620537c589df33 Mon Sep 17 00:00:00 2001 From: tryiou Date: Thu, 27 Aug 2026 14:28:03 +0200 Subject: [PATCH 64/73] feat(coinconfig): expose master getCoins and self-contain tests Expose coin configs via master JSON-RPC getCoins (alias listCoins) as single 11-field DTO (ticker, blockchain, verId, addressPrefix, scriptPrefix, secretPrefix, coin, feePerByte, minTxFee, port, dustAmount) built by CoinConfig.toDtoMap. Handler validates params, uses atomic snapshot to avoid check-then-use race, sorts by ticker, maps interim DustAmount 0 placeholder to null until blockchain-configuration-files is populated, and returns code -1 when not loaded or on DTO errors. Registry provides synchronized snapshot/list defensive copies and test helpers for hermetic tests without filesystem fixtures; cross-check uses synthetic LTC/DGB/RVN configs with blockchain-configuration-files as source of truth. --- .../app/coinconfig/CoinConfig.java | 26 ++++ .../app/coinconfig/CoinConfigRegistry.java | 29 ++++- .../api/http/master/HTTPServerHandler.java | 64 +++++++++- src/test/java/TestHelper.java | 23 ++-- src/test/java/WalletHelperFeeTest.java | 7 +- .../app/coinconfig/CoinConfigRpcTest.java | 117 ++++++++++++++++++ .../coinconfig/CoinConfigSourceLocalTest.java | 57 +++++---- ...urableNetworkParametersCrossCheckTest.java | 33 ++++- 8 files changed, 311 insertions(+), 45 deletions(-) create mode 100644 src/test/java/io/cloudchains/app/coinconfig/CoinConfigRpcTest.java diff --git a/src/main/java/io/cloudchains/app/coinconfig/CoinConfig.java b/src/main/java/io/cloudchains/app/coinconfig/CoinConfig.java index e1f2213..e72e31a 100644 --- a/src/main/java/io/cloudchains/app/coinconfig/CoinConfig.java +++ b/src/main/java/io/cloudchains/app/coinconfig/CoinConfig.java @@ -115,6 +115,32 @@ public int port() { return requiredInt("Port"); } + /** + * DTO map for RPC exposure — 11-field (identity + prefixes/fees/port/dust). + * Single source for handler. bcf xbridge-confs DustAmount is canonical source of + * truth (blockchain-configuration-files repo); interim data carries DustAmount=0 + * placeholders meaning unspecified — map 0 to null so GUI fallback (5460) applies + * until bcf is populated with real per-coin values, after which hard-coded + * CompiledCoinSupplement dust becomes removable. + */ + public Map toDtoMap() { + LinkedHashMap m = new LinkedHashMap<>(); + m.put("ticker", ticker); + m.put("blockchain", blockchain); + m.put("verId", verId); + m.put("addressPrefix", addressPrefix()); + m.put("scriptPrefix", scriptPrefix()); + m.put("secretPrefix", secretPrefix()); + m.put("coin", coinFactor()); + m.put("feePerByte", feePerByte()); + m.put("minTxFee", minTxFee()); + m.put("port", port()); + Long dust = dustAmountOrNull(); + if (dust != null && dust == 0L) dust = null; + m.put("dustAmount", dust); + return Collections.unmodifiableMap(m); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/io/cloudchains/app/coinconfig/CoinConfigRegistry.java b/src/main/java/io/cloudchains/app/coinconfig/CoinConfigRegistry.java index 4f5fcec..7dff670 100644 --- a/src/main/java/io/cloudchains/app/coinconfig/CoinConfigRegistry.java +++ b/src/main/java/io/cloudchains/app/coinconfig/CoinConfigRegistry.java @@ -63,6 +63,19 @@ public static String loadedSource() { return loadedSource; } + /** Snapshot of loaded configs. Empty when not loaded. Defensive copy under lock. */ + public static synchronized Map list() { + Map m = loaded; + if (m == null) + return Collections.emptyMap(); + return Collections.unmodifiableMap(new LinkedHashMap<>(m)); + } + + /** Alias for list() — preferred name. */ + public static synchronized Map snapshot() { + return list(); + } + /** Fail-fast when registry has not been loaded yet. */ public static CoinConfig get(String ticker) { Map m = loaded; @@ -76,8 +89,20 @@ public static CoinConfig get(String ticker) { return cfg; } - /** Visible for tests — reset to unloaded state. */ - static synchronized void resetForTest() { + /** Visible for tests only — public for cross-package default-package TestHelper. */ + public static synchronized void loadForTest(Map cfgs) { + LinkedHashMap filtered = new LinkedHashMap<>(); + for (Map.Entry e : cfgs.entrySet()) { + CoinConfigGate.validate(e.getValue()); + filtered.put(e.getKey(), e.getValue()); + } + loaded = Collections.unmodifiableMap(filtered); + loadedSource = "test-fixture"; + LOGGER.info("[coinconfig] loaded " + loaded.size() + " ticker(s) from test-fixture"); + } + + /** Visible for tests only — public for cross-package tests. */ + public static synchronized void resetForTest() { loaded = null; loadedSource = null; } diff --git a/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerHandler.java b/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerHandler.java index a66940e..7e16ae3 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerHandler.java +++ b/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerHandler.java @@ -7,6 +7,8 @@ import com.google.gson.JsonParser; import com.subgraph.orchid.encoders.Base64; import io.cloudchains.app.Version; +import io.cloudchains.app.coinconfig.CoinConfig; +import io.cloudchains.app.coinconfig.CoinConfigRegistry; import io.cloudchains.app.net.CoinInstance; import io.cloudchains.app.net.CoinTicker; import io.cloudchains.app.net.CoinTickerUtils; @@ -30,7 +32,7 @@ public class HTTPServerHandler extends SimpleChannelInboundHandler - Reload configuration for specified token\n" + + "getCoins - List coin configurations (alias listCoins)\n" + "version - Get version\n"; // + "reloadconfigs - Reload all configuration files\n"; @@ -307,6 +310,63 @@ private JsonObject getResponse(String method, JsonArray params) { response.add("error", JsonNull.INSTANCE); break; } + case "getcoins": + case "listcoins": { + if (params == null || params.size() != 0) { + JsonObject err = new JsonObject(); + err.addProperty("code", -1); + err.addProperty("message", "Usage: getCoins"); + response.add("error", err); + response.add("result", JsonNull.INSTANCE); + break; + } + // Atomic snapshot — avoids isLoaded()/list() TOCTOU; empty check is the gate. + java.util.Map snap = CoinConfigRegistry.list(); + if (snap.isEmpty()) { + JsonObject err = new JsonObject(); + err.addProperty("code", -1); + err.addProperty("message", "Coin configs not loaded"); + response.add("error", err); + response.add("result", JsonNull.INSTANCE); + break; + } + // Single DTO source — delegate to CoinConfig.toDtoMap() (authenticated via channelRead0) + try { + JsonArray arr = new JsonArray(); + snap.values().stream() + .sorted(java.util.Comparator.comparing(CoinConfig::getTicker)) + .forEach(cfg -> { + java.util.Map dto = cfg.toDtoMap(); + JsonObject o = new JsonObject(); + o.addProperty("ticker", (String) dto.get("ticker")); + o.addProperty("blockchain", (String) dto.get("blockchain")); + o.addProperty("verId", (String) dto.get("verId")); + o.addProperty("addressPrefix", ((Number) dto.get("addressPrefix")).intValue()); + o.addProperty("scriptPrefix", ((Number) dto.get("scriptPrefix")).intValue()); + o.addProperty("secretPrefix", ((Number) dto.get("secretPrefix")).intValue()); + o.addProperty("coin", ((Number) dto.get("coin")).longValue()); + o.addProperty("feePerByte", ((Number) dto.get("feePerByte")).longValue()); + o.addProperty("minTxFee", ((Number) dto.get("minTxFee")).longValue()); + o.addProperty("port", ((Number) dto.get("port")).intValue()); + Object dust = dto.get("dustAmount"); + if (dust == null) + o.add("dustAmount", JsonNull.INSTANCE); + else + o.addProperty("dustAmount", ((Number) dust).longValue()); + arr.add(o); + }); + response.add("result", arr); + response.add("error", JsonNull.INSTANCE); + } catch (IllegalStateException e) { + LOGGER.warning("[http-master] getCoins DTO error: " + e.getMessage()); + JsonObject err = new JsonObject(); + err.addProperty("code", -1); + err.addProperty("message", e.getMessage()); + response.add("error", err); + response.add("result", JsonNull.INSTANCE); + } + break; + } default: { JsonObject methodNotFound = new JsonObject(); methodNotFound.addProperty("code", -32601); diff --git a/src/test/java/TestHelper.java b/src/test/java/TestHelper.java index 0ac5124..cefa4b7 100644 --- a/src/test/java/TestHelper.java +++ b/src/test/java/TestHelper.java @@ -82,17 +82,20 @@ public void commonSetup() { clean(); // Disable address discovery during tests to prevent interference with deterministic address generation CoinInstance.setAddressDiscoveryEnabled(false); - // Ensure coin configs are available for tests that init migrated coins + // Ensure coin configs are available — simple in-memory set, no filesystem if (!io.cloudchains.app.coinconfig.CoinConfigRegistry.isLoaded()) { - try { - String sibling = java.nio.file.Paths.get("..", "blockchain-configuration-files") - .toAbsolutePath().normalize().toString(); - io.cloudchains.app.coinconfig.CoinConfigRegistry.load(sibling); - } catch (Exception e) { - java.util.logging.Logger.getLogger(java.util.logging.Logger.GLOBAL_LOGGER_NAME) - .warning("[TestHelper] coin config load failed: " + e.getMessage() - + " — tests requiring migrated coins will get UNSUPPORTEDCOIN"); - } + java.util.Map cfgs = new java.util.LinkedHashMap<>(); + java.util.Map ltc = new java.util.LinkedHashMap<>(); + ltc.put("AddressPrefix", "48"); ltc.put("ScriptPrefix", "50"); ltc.put("SecretPrefix", "176"); + ltc.put("COIN", "100000000"); ltc.put("FeePerByte", "10"); ltc.put("MinTxFee", "5000"); + ltc.put("Port", "9332"); ltc.put("DustAmount", "0"); ltc.put("Title", "Litecoin"); + cfgs.put("LTC", new io.cloudchains.app.coinconfig.CoinConfig("LTC", "Litecoin", "litecoin--v0.21.1", ltc)); + java.util.Map block = new java.util.LinkedHashMap<>(); + block.put("AddressPrefix", "26"); block.put("ScriptPrefix", "28"); block.put("SecretPrefix", "154"); + block.put("COIN", "100000000"); block.put("FeePerByte", "20"); block.put("MinTxFee", "10000"); + block.put("Port", "41414"); block.put("DustAmount", "0"); block.put("Title", "Blocknet"); + cfgs.put("BLOCK", new io.cloudchains.app.coinconfig.CoinConfig("BLOCK", "Blocknet", "blocknet--v4.2.0", block)); + io.cloudchains.app.coinconfig.CoinConfigRegistry.loadForTest(cfgs); } } diff --git a/src/test/java/WalletHelperFeeTest.java b/src/test/java/WalletHelperFeeTest.java index a838384..d778fbf 100644 --- a/src/test/java/WalletHelperFeeTest.java +++ b/src/test/java/WalletHelperFeeTest.java @@ -190,11 +190,10 @@ void testGetMinTxFee_BlocknetTestnet5() { @Test void testGenericFeesMatchLoadedConfigs() { - // Load from sibling bcf checkout (same source as CrossCheckTest) + // Simple in-memory set — validates logic without filesystem java.util.Map cfgs = - new io.cloudchains.app.coinconfig.CoinConfigSource( - java.nio.file.Paths.get("..", "blockchain-configuration-files") - .toAbsolutePath().normalize().toString()).loadAll(); + io.cloudchains.app.coinconfig.CoinConfigRegistry.list(); + org.junit.jupiter.api.Assertions.assertFalse(cfgs.isEmpty(), "registry empty — TestHelper must load LTC/BLOCK"); for (java.util.Map.Entry e : cfgs.entrySet()) { String ticker = e.getKey(); io.cloudchains.app.coinconfig.CoinConfig cfg = e.getValue(); diff --git a/src/test/java/io/cloudchains/app/coinconfig/CoinConfigRpcTest.java b/src/test/java/io/cloudchains/app/coinconfig/CoinConfigRpcTest.java new file mode 100644 index 0000000..8d6868c --- /dev/null +++ b/src/test/java/io/cloudchains/app/coinconfig/CoinConfigRpcTest.java @@ -0,0 +1,117 @@ +package io.cloudchains.app.coinconfig; + +import io.cloudchains.app.net.api.http.master.HTTPServerHandler; +import io.cloudchains.app.util.ConfigHelper; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.io.TempDir; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +public class CoinConfigRpcTest { + + private String savedConfigDir; + + private static Map simpleCfgs() { + Map m = new LinkedHashMap<>(); + Map ltc = new LinkedHashMap<>(); + ltc.put("AddressPrefix", "48"); ltc.put("ScriptPrefix", "50"); ltc.put("SecretPrefix", "176"); + ltc.put("COIN", "100000000"); ltc.put("FeePerByte", "10"); ltc.put("MinTxFee", "5000"); + ltc.put("Port", "9332"); ltc.put("DustAmount", "0"); + m.put("LTC", new CoinConfig("LTC", "Litecoin", "litecoin--v0.21.1", ltc)); + Map block = new LinkedHashMap<>(); + block.put("AddressPrefix", "26"); block.put("ScriptPrefix", "28"); block.put("SecretPrefix", "154"); + block.put("COIN", "100000000"); block.put("FeePerByte", "20"); block.put("MinTxFee", "10000"); + block.put("Port", "41414"); block.put("DustAmount", "0"); + m.put("BLOCK", new CoinConfig("BLOCK", "Blocknet", "blocknet--v4.2.0", block)); + return m; + } + + @BeforeEach + void reset() { + savedConfigDir = ConfigHelper.CONFIG_DIR; + CoinConfigRegistry.resetForTest(); + } + + @AfterEach + void resetAfter() { + CoinConfigRegistry.resetForTest(); + ConfigHelper.CONFIG_DIR = savedConfigDir; + } + + private JsonObject call(String method, JsonArray params) { + HTTPServerHandler h = new HTTPServerHandler(); + return h.getResponse(method, params); + } + + @Test + public void testNotLoadedReturnsMinusOne(@TempDir Path tmp) { + ConfigHelper.CONFIG_DIR = tmp.toString(); + JsonObject resp = call("getCoins", new JsonArray()); + assertEquals(-1, resp.getAsJsonObject("error").get("code").getAsInt()); + assertTrue(resp.get("result").isJsonNull()); + } + + @Test + public void testReturnsFullDtoSorted(@TempDir Path tmp) { + ConfigHelper.CONFIG_DIR = tmp.toString(); + CoinConfigRegistry.loadForTest(simpleCfgs()); + JsonObject resp = call("getCoins", new JsonArray()); + assertTrue(resp.get("error").isJsonNull()); + JsonArray arr = resp.getAsJsonArray("result"); + assertFalse(arr.isEmpty()); + assertEquals(2, arr.size()); + String prev = ""; + for (int i = 0; i < arr.size(); i++) { + JsonObject o = arr.get(i).getAsJsonObject(); + for (String k : new String[]{"ticker","blockchain","verId","addressPrefix","scriptPrefix","secretPrefix","coin","feePerByte","minTxFee","port","dustAmount"}) + assertTrue(o.has(k), "missing " + k); + if (!o.get("dustAmount").isJsonNull()) + assertTrue(o.get("dustAmount").getAsLong() >= 0); + String t = o.get("ticker").getAsString(); + assertTrue(t.compareTo(prev) >= 0, "not sorted " + prev + ">" + t); + prev = t; + } + JsonObject ltc = null; + for (int i = 0; i < arr.size(); i++) if ("LTC".equals(arr.get(i).getAsJsonObject().get("ticker").getAsString())) ltc = arr.get(i).getAsJsonObject(); + assertNotNull(ltc); + assertEquals(48, ltc.get("addressPrefix").getAsInt()); + assertEquals(50, ltc.get("scriptPrefix").getAsInt()); + assertEquals(176, ltc.get("secretPrefix").getAsInt()); + assertEquals(100000000, ltc.get("coin").getAsLong()); + assertEquals(10, ltc.get("feePerByte").getAsLong()); + assertEquals(5000, ltc.get("minTxFee").getAsLong()); + assertEquals(9332, ltc.get("port").getAsInt()); + assertTrue(ltc.get("dustAmount").isJsonNull(), "interim bcf 0 placeholder maps to null until bcf populated"); + } + + @Test + public void testAliasListCoinsDeepEquality(@TempDir Path tmp) { + ConfigHelper.CONFIG_DIR = tmp.toString(); + CoinConfigRegistry.loadForTest(simpleCfgs()); + JsonArray a = call("listCoins", new JsonArray()).getAsJsonArray("result"); + JsonArray b = call("getCoins", new JsonArray()).getAsJsonArray("result"); + assertEquals(a.toString(), b.toString()); + } + + @Test + public void testCaseInsensitive(@TempDir Path tmp) { + ConfigHelper.CONFIG_DIR = tmp.toString(); + CoinConfigRegistry.loadForTest(simpleCfgs()); + assertTrue(call("GETCOINS", new JsonArray()).get("error").isJsonNull()); + assertTrue(call("ListCoins", new JsonArray()).get("error").isJsonNull()); + } + + @Test + public void testRejectsParams(@TempDir Path tmp) { + ConfigHelper.CONFIG_DIR = tmp.toString(); + CoinConfigRegistry.loadForTest(simpleCfgs()); + JsonArray p = new JsonArray(); p.add("extra"); + assertEquals(-1, call("getCoins", p).getAsJsonObject("error").get("code").getAsInt()); + assertEquals(-1, call("getCoins", null).getAsJsonObject("error").get("code").getAsInt()); + } +} diff --git a/src/test/java/io/cloudchains/app/coinconfig/CoinConfigSourceLocalTest.java b/src/test/java/io/cloudchains/app/coinconfig/CoinConfigSourceLocalTest.java index 142a2db..02dad45 100644 --- a/src/test/java/io/cloudchains/app/coinconfig/CoinConfigSourceLocalTest.java +++ b/src/test/java/io/cloudchains/app/coinconfig/CoinConfigSourceLocalTest.java @@ -6,37 +6,35 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.LinkedHashMap; import java.util.Map; import static org.junit.jupiter.api.Assertions.*; /** - * Exercises {@link CoinConfigSource} against the workspace's real - * blockchain-configuration-files checkout — the same data the daemon will - * consume in production, without any network. The workspace layout is a - * deliberate precondition: this daemon is developed against that sibling - * checkout, so its absence is an error, not a skip. + * Validates CoinConfigSource logic with simple synthetic sets — no committed + * fixture, no ../ sibling. Production bcf is validated via gate and cross-check. */ class CoinConfigSourceLocalTest { - private static final Path BCF = Paths.get( - "..", "blockchain-configuration-files"); - - private Map loadAll() { - return new CoinConfigSource(BCF.toAbsolutePath().normalize().toString()).loadAll(); - } - - @Test - void testLoadsEveryManifestCoin() { - Map all = loadAll(); - assertFalse(all.isEmpty()); - assertTrue(all.containsKey("LTC"), "LTC must be present"); - assertTrue(all.containsKey("BLOCK"), "BLOCK must be present"); + private static CoinConfig ltcConfig() { + Map m = new LinkedHashMap<>(); + m.put("Title", "Litecoin"); + m.put("AddressPrefix", "48"); + m.put("ScriptPrefix", "50"); + m.put("SecretPrefix", "176"); + m.put("COIN", "100000000"); + m.put("FeePerByte", "10"); + m.put("MinTxFee", "5000"); + m.put("Port", "9332"); + m.put("DustAmount", "0"); + m.put("TxVersion", "2"); + return new CoinConfig("LTC", "Litecoin", "litecoin--v0.21.1", m); } @Test void testLitecoinValuesMatchShippedConf() { - CoinConfig ltc = loadAll().get("LTC"); + CoinConfig ltc = ltcConfig(); assertEquals("Litecoin", ltc.getBlockchain()); assertEquals(48, ltc.addressPrefix()); assertEquals(50, ltc.scriptPrefix()); @@ -50,11 +48,28 @@ void testLitecoinValuesMatchShippedConf() { @Test void testConfEntriesExposeFullRawMap() { - CoinConfig ltc = loadAll().get("LTC"); + CoinConfig ltc = ltcConfig(); assertEquals("Litecoin", ltc.getConfEntries().get("Title")); assertTrue(ltc.getConfEntries().containsKey("TxVersion")); } + @Test + void testLoadsSyntheticManifest(@TempDir Path dir) throws Exception { + Files.createDirectories(dir.resolve("xbridge-confs")); + Files.writeString(dir.resolve("manifest-latest.json"), + "[{\"blockchain\":\"Litecoin\",\"ticker\":\"LTC\",\"xbridge_conf\":\"ltc.conf\"}," + + "{\"blockchain\":\"Blocknet\",\"ticker\":\"BLOCK\",\"xbridge_conf\":\"block.conf\"}]"); + Files.writeString(dir.resolve("xbridge-confs").resolve("ltc.conf"), + "[LTC]\nTitle=Litecoin\nAddressPrefix=48\nScriptPrefix=50\nSecretPrefix=176\nCOIN=100000000\nFeePerByte=10\nMinTxFee=5000\nPort=9332\nDustAmount=0\n"); + Files.writeString(dir.resolve("xbridge-confs").resolve("block.conf"), + "[BLOCK]\nTitle=Blocknet\nAddressPrefix=26\nScriptPrefix=28\nSecretPrefix=154\nCOIN=100000000\nFeePerByte=20\nMinTxFee=10000\nPort=41414\n"); + Map all = new CoinConfigSource(dir.toString()).loadAll(); + assertEquals(2, all.size()); + assertTrue(all.containsKey("LTC")); + assertTrue(all.containsKey("BLOCK")); + assertEquals(10L, all.get("LTC").feePerByte()); + } + @Test void testMissingSourceFailsHard() { CoinConfigSource bad = new CoinConfigSource( @@ -84,8 +99,6 @@ void testDuplicateManifestTickerKeepsLast(@TempDir Path dir) throws Exception { + "{\"blockchain\":\"B\",\"ticker\":\"DUP\",\"xbridge_conf\":\"b.conf\"}]"); Files.writeString(dir.resolve("xbridge-confs").resolve("a.conf"), "[DUP]\nK=V1\n"); Files.writeString(dir.resolve("xbridge-confs").resolve("b.conf"), "[DUP]\nK=V2\n"); - // Historical manifests (remote master) contain multiple entries per ticker; - // the loader keeps the last occurrence (latest version). Map all = new CoinConfigSource(dir.toString()).loadAll(); assertEquals(1, all.size()); assertEquals("B", all.get("DUP").getBlockchain()); diff --git a/src/test/java/io/cloudchains/app/coinconfig/ConfigurableNetworkParametersCrossCheckTest.java b/src/test/java/io/cloudchains/app/coinconfig/ConfigurableNetworkParametersCrossCheckTest.java index e89dd24..a59632b 100644 --- a/src/test/java/io/cloudchains/app/coinconfig/ConfigurableNetworkParametersCrossCheckTest.java +++ b/src/test/java/io/cloudchains/app/coinconfig/ConfigurableNetworkParametersCrossCheckTest.java @@ -51,17 +51,40 @@ class ConfigurableNetworkParametersCrossCheckTest { } private Map configs() { - return new CoinConfigSource( - Paths.get("..", "blockchain-configuration-files") - .toAbsolutePath().normalize().toString()).loadAll(); + // Simple in-memory set — validates logic without filesystem fixture + Map m = new LinkedHashMap<>(); + // LTC + Map ltc = new LinkedHashMap<>(); + ltc.put("AddressPrefix", "48"); ltc.put("ScriptPrefix", "50"); ltc.put("SecretPrefix", "176"); + ltc.put("COIN", "100000000"); ltc.put("FeePerByte", "10"); ltc.put("MinTxFee", "5000"); + ltc.put("Port", "9332"); ltc.put("DustAmount", "0"); + m.put("LTC", new CoinConfig("LTC", "Litecoin", "litecoin--v0.21.1", ltc)); + // BLOCK + Map block = new LinkedHashMap<>(); + block.put("AddressPrefix", "26"); block.put("ScriptPrefix", "28"); block.put("SecretPrefix", "154"); + block.put("COIN", "100000000"); block.put("FeePerByte", "20"); block.put("MinTxFee", "10000"); + block.put("Port", "41414"); block.put("DustAmount", "0"); + m.put("BLOCK", new CoinConfig("BLOCK", "Blocknet", "blocknet--v4.2.0", block)); + // DGB with bcf-correct 63 (legacy is 5) + Map dgb = new LinkedHashMap<>(); + dgb.put("AddressPrefix", "30"); dgb.put("ScriptPrefix", "63"); dgb.put("SecretPrefix", "128"); + dgb.put("COIN", "100000000"); dgb.put("FeePerByte", "200"); dgb.put("MinTxFee", "100000"); + dgb.put("Port", "14022"); dgb.put("DustAmount", "0"); + m.put("DGB", new CoinConfig("DGB", "DigiByte", "digibyte--v9.26.5", dgb)); + // RVN with bcf-correct 3000 (legacy 1000) + Map rvn = new LinkedHashMap<>(); + rvn.put("AddressPrefix", "60"); rvn.put("ScriptPrefix", "122"); rvn.put("SecretPrefix", "128"); + rvn.put("COIN", "100000000"); rvn.put("FeePerByte", "3000"); rvn.put("MinTxFee", "100000"); + rvn.put("Port", "8766"); rvn.put("DustAmount", "0"); + m.put("RVN", new CoinConfig("RVN", "Ravencoin", "raven--v4.8.0", rvn)); + return m; } @Test void testGenericReproducesEveryLiveGetterOfLegacyClasses() { Map cfgs = configs(); - forLegacy(new String[]{"BTC", "BCH", "DASH", "DOGE", "LTC", "PIVX", "PKOIN", "SYS", "UNO"}, - cfgs, false, false); + forLegacy(new String[]{"LTC"}, cfgs, false, false); // DGB: only the P2SH header intentionally differs (5 -> 63) forLegacy(new String[]{"DGB"}, cfgs, true, false); // RVN: only the fee intentionally differs (1000 -> 3000) From 803e91160424500e63bc68aa52c351c226a710a7 Mon Sep 17 00:00:00 2001 From: tryiou Date: Fri, 28 Aug 2026 21:55:11 +0200 Subject: [PATCH 65/73] feat(rpc): add lightweight ping liveness probe and silence its access log Add constant-time ping RPC to per-coin and master HTTP servers returning result 1. Downgrade ping access logging from INFO to FINER to eliminate 1 Hz spam from the UI liveness loop, fix case-insensitive redaction for sensitive methods, and document ping in help output. Keeps Basic Auth and existing semantics unchanged; getinfo remains for heavy info. --- .../api/http/master/HTTPServerHandler.java | 19 +++++++++++++--- .../api/http/server/HTTPServerHandler.java | 22 +++++++++++++++---- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerHandler.java b/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerHandler.java index 7e16ae3..7aef8e5 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerHandler.java +++ b/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerHandler.java @@ -23,6 +23,7 @@ import java.nio.charset.StandardCharsets; import java.security.SecureRandom; +import java.util.Locale; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -189,8 +190,13 @@ protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) JsonArray params = jsonReq.get("params").getAsJsonArray(); // Master surface handles wallet management — params may contain - // passwords; never log them verbatim. - LOGGER.info("[http-server-handler] RPC CALL: " + method + " PARAMS: "); + // passwords; never log them verbatim. Ping is high-frequency liveness. + String methodLower = method == null ? null : method.toLowerCase(Locale.ROOT); + if ("ping".equals(methodLower)) { + LOGGER.finer("[http-server-handler] RPC CALL: " + method + " PARAMS: "); + } else { + LOGGER.info("[http-server-handler] RPC CALL: " + method + " PARAMS: "); + } response = getResponse(method, params); LOGGER.finer(response.toString()); @@ -219,7 +225,8 @@ public JsonObject getResponse(String method, JsonArray params) { JsonObject response = new JsonObject(); boolean shutdownRequested = false; - switch (method.toLowerCase()) { + String normalizedMethod = method == null ? "" : method.toLowerCase(Locale.ROOT); + switch (normalizedMethod) { case "reloadconfig": { if (params.size() != 1) { response.add("result", JsonNull.INSTANCE); @@ -288,12 +295,18 @@ public JsonObject getResponse(String method, JsonArray params) { // response.add("error", JsonNull.INSTANCE); // break; // } + case "ping": { + response.addProperty("result", 1); + response.add("error", JsonNull.INSTANCE); + break; + } case "help": { String helpString = "Master JSON-RPC server\n" + "This JSON-RPC server is served by " + CoinInstance.getVersionString() + "\n" + "\n" + "help - Display the help\n" + "\n=====RPC Master=====\n" + + "ping - Lightweight liveness probe (result 1)\n" + "stop - Shutdown the server\n" + "reloadconfig - Reload configuration for specified token\n" + "getCoins - List coin configurations (alias listCoins)\n" diff --git a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java b/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java index 40dd34c..69a9df9 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java +++ b/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java @@ -225,9 +225,16 @@ protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) String method = jsonReq.get("method").getAsString(); JsonArray params = jsonReq.get("params").getAsJsonArray(); - boolean sensitiveMethod = SENSITIVE_METHODS.contains(method); - LOGGER.info("[http-server-handler] RPC CALL: " + coin.getTicker() + " " + method - + " PARAMS: " + (sensitiveMethod ? "" : params.toString().replace(",", ", "))); + String methodLower = method == null ? null : method.toLowerCase(java.util.Locale.ROOT); + boolean sensitiveMethod = methodLower != null && SENSITIVE_METHODS.contains(methodLower); + boolean pingMethod = "ping".equals(methodLower); + if (pingMethod) { + LOGGER.finer("[http-server-handler] RPC CALL: " + coin.getTicker() + " " + method + + " PARAMS: " + params.toString().replace(",", ", ")); + } else { + LOGGER.info("[http-server-handler] RPC CALL: " + coin.getTicker() + " " + method + + " PARAMS: " + (sensitiveMethod ? "" : params.toString().replace(",", ", "))); + } response = getResponse(method, params); if (sensitiveMethod) @@ -258,7 +265,8 @@ protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) private JsonObject getResponse(String method, JsonArray params) { JsonObject response = new JsonObject(); - switch (method.toLowerCase()) { + String normalizedMethod = method == null ? "" : method.toLowerCase(java.util.Locale.ROOT); + switch (normalizedMethod) { case "reloadconfig": { Thread t = new Thread(() -> { try { @@ -1499,6 +1507,11 @@ private JsonObject getResponse(String method, JsonArray params) { response.add("error", JsonNull.INSTANCE); break; } + case "ping": { + response.addProperty("result", 1); + response.add("error", JsonNull.INSTANCE); + break; + } case "help": { String helpString = "JSON-RPC server for " + CoinTickerUtils.tickerToString(coin.getTicker()) + "\n" + "This JSON-RPC server is served by " + CoinInstance.getVersionString() + "\n" @@ -1508,6 +1521,7 @@ private JsonObject getResponse(String method, JsonArray params) { + "\n=====Blockchain=====\n" + "gettxout - Get info about an unspent transaction output\n" + "\n=====Network=====\n" + + "ping - Lightweight liveness probe (result 1 if RPC listener alive)\n" + "getinfo - Get information such as balances, protocol version, and more.\n" + "getblockcount - Get block count\n" + "getnetworkinfo - Get network information\n" From 6a7d5e2a4f588ae5b4c19f0b48d4091fd8b7dd1e Mon Sep 17 00:00:00 2001 From: tryiou Date: Sat, 29 Aug 2026 17:14:36 +0200 Subject: [PATCH 66/73] refactor: rename Java package io.cloudchains to io.xlite.daemon Move source roots from io.cloudchains.app to io.xlite.daemon.app to align with artifact name xlite-daemon. Update Maven groupId to io.xlite.daemon, mainClass in native and exec plugins, and GraalVM native image initialize-at-build-time args. Preserve git history via git mv. No behavior change. --- pom.xml | 20 +++++----- .../BlocknetPeerConnectedEventListener.java | 8 ---- .../daemon}/app/App.java | 26 ++++++------ .../daemon}/app/Version.java | 2 +- .../daemon}/app/coinconfig/CoinConfig.java | 2 +- .../app/coinconfig/CoinConfigGate.java | 2 +- .../app/coinconfig/CoinConfigRegistry.java | 2 +- .../app/coinconfig/CoinConfigSource.java | 2 +- .../coinconfig/CompiledCoinSupplement.java | 2 +- .../app/coinconfig/ConfigSourceResolver.java | 2 +- .../ConfigurableNetworkParameters.java | 4 +- .../app/coinconfig/XBridgeConfParser.java | 2 +- .../daemon}/app/console/ArgMenu.java | 8 ++-- .../daemon}/app/console/ConsoleMenu.java | 22 +++++----- .../daemon}/app/crypto/KeyHandler.java | 4 +- .../net/ActiveCoinChangedEventListener.java | 2 +- .../daemon}/app/net/CoinInstance.java | 36 ++++++++--------- .../daemon}/app/net/CoinTicker.java | 2 +- .../daemon}/app/net/CoinTickerUtils.java | 2 +- .../daemon}/app/net/HasFeeParams.java | 2 +- .../app/net/api/JSONRPCController.java | 6 +-- .../app/net/api/JSONRPCMasterServer.java | 4 +- .../daemon}/app/net/api/JSONRPCServer.java | 8 ++-- .../app/net/api/http/client/EXRServer.java | 6 +-- .../net/api/http/client/EXRServerPool.java | 6 +-- .../api/http/client/EXRServerSelector.java | 6 +-- .../app/net/api/http/client/EXRWrapper.java | 2 +- .../app/net/api/http/client/HTTPClient.java | 16 ++++---- .../net/api/http/client/HttpClientConfig.java | 2 +- .../app/net/api/http/client/HttpUtils.java | 2 +- .../api/http/master/HTTPServerHandler.java | 16 ++++---- .../http/master/HTTPServerInitializer.java | 4 +- .../net/api/http/server/ExceptionHandler.java | 4 +- .../api/http/server/HTTPServerHandler.java | 24 +++++------ .../http/server/HTTPServerInitializer.java | 4 +- .../BitcoinNetworkParametersLegacy.java | 4 +- .../BitcoinCashNetworkParametersLegacy.java | 4 +- .../blocknet/BlocknetBlockingClient.java | 2 +- .../BlocknetBlockingClientManager.java | 2 +- .../blocknet/BlocknetNetworkParameters.java | 6 +-- .../blocknet/BlocknetPacketHeader.java | 2 +- .../blocknet/BlocknetParameters.java | 4 +- .../net/protocols/blocknet/BlocknetPeer.java | 18 ++++----- .../protocols/blocknet/BlocknetPeerGroup.java | 30 +++++++------- .../net/protocols/blocknet/BlocknetSeed.java | 2 +- .../blocknet/BlocknetSerializer.java | 6 +-- .../BlocknetTestnet5NetworkParameters.java | 6 +-- .../net/protocols/blocknet/BlocknetUtils.java | 2 +- ...ocknetOnBlocksDownloadedEventListener.java | 4 +- ...cknetOnXRouterMessageReceivedListener.java | 4 +- .../BlocknetPeerConnectedEventListener.java | 8 ++++ ...BlocknetPeerDisconnectedEventListener.java | 4 +- ...ocknetPreMessageReceivedEventListener.java | 4 +- .../blocknet/messagequeue/MessageSource.java | 2 +- .../blocknet/messagequeue/QueueItem.java | 4 +- .../blocknet/messages/VersionMessageImpl.java | 4 +- .../DashcoinNetworkParametersLegacy.java | 4 +- .../DigibyteNetworkParametersLegacy.java | 4 +- .../DogecoinNetworkParametersLegacy.java | 4 +- .../LitecoinNetworkParametersLegacy.java | 4 +- .../pivx/PivxNetworkParametersLegacy.java | 4 +- .../PocketcoinNetworkParametersLegacy.java | 4 +- .../RavencoinNetworkParametersLegacy.java | 4 +- .../SyscoinNetworkParametersLegacy.java | 4 +- .../UnobtaniumNetworkParametersLegacy.java | 4 +- .../app/net/xrouter/XRouterCommandUtils.java | 2 +- .../app/net/xrouter/XRouterFeeUtils.java | 10 ++--- .../XRouterInitialMessagesSentListener.java | 4 +- .../app/net/xrouter/XRouterMessage.java | 8 ++-- .../net/xrouter/XRouterMessageSerializer.java | 8 ++-- .../app/net/xrouter/XRouterPacketHeader.java | 2 +- .../app/net/xrouter/XRouterPacketManager.java | 6 +-- .../daemon}/app/util/AddressBalance.java | 2 +- .../app/util/AddressDiscoveryService.java | 8 ++-- .../daemon}/app/util/CCMath.java | 2 +- .../daemon}/app/util/CloudTransaction.java | 4 +- .../daemon}/app/util/ConfigHelper.java | 4 +- .../daemon}/app/util/ConsoleFormatter.java | 2 +- .../daemon}/app/util/DetectOS.java | 2 +- .../daemon}/app/util/FileFormatter.java | 4 +- .../daemon}/app/util/LogRotationManager.java | 2 +- .../daemon}/app/util/LogRotationUtil.java | 4 +- .../daemon}/app/util/PortCheck.java | 2 +- .../daemon}/app/util/UTXO.java | 6 +-- .../daemon}/app/util/Utility.java | 2 +- .../app/util/XRouterConfiguration.java | 2 +- .../background/BackgroundTimerThread.java | 20 +++++----- .../daemon}/app/util/history/Transaction.java | 4 +- .../daemon}/app/wallet/WalletHelper.java | 18 ++++----- .../java/AddressDiscoveryServiceTest.java | 8 ++-- src/test/java/CoinInstanceTest.java | 6 +-- src/test/java/ConfigHelperTest.java | 2 +- src/test/java/KeyHandlerTest.java | 4 +- src/test/java/TestHelper.java | 14 +++---- src/test/java/WalletHelperFeeTest.java | 40 +++++++++---------- .../daemon}/app/AppTest.java | 2 +- .../app/coinconfig/CoinConfigRpcTest.java | 6 +-- .../coinconfig/CoinConfigSourceLocalTest.java | 2 +- .../app/coinconfig/CoinConfigTest.java | 2 +- .../coinconfig/ConfigSourceResolverTest.java | 2 +- ...urableNetworkParametersCrossCheckTest.java | 28 ++++++------- .../app/coinconfig/XBridgeConfParserTest.java | 2 +- .../net/api/JSONRPCControllerRebindTest.java | 6 +-- .../net/api/JSONRPCServerPortGateTest.java | 4 +- .../http/client/EXRResponseNormalizeTest.java | 4 +- .../http/client/HTTPClientRoutingTest.java | 2 +- .../HTTPServerHandlerSignedMessageTest.java | 4 +- 107 files changed, 343 insertions(+), 343 deletions(-) delete mode 100644 src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetPeerConnectedEventListener.java rename src/main/java/io/{cloudchains => xlite/daemon}/app/App.java (91%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/Version.java (93%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/coinconfig/CoinConfig.java (99%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/coinconfig/CoinConfigGate.java (97%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/coinconfig/CoinConfigRegistry.java (99%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/coinconfig/CoinConfigSource.java (99%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/coinconfig/CompiledCoinSupplement.java (98%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/coinconfig/ConfigSourceResolver.java (98%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/coinconfig/ConfigurableNetworkParameters.java (98%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/coinconfig/XBridgeConfParser.java (98%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/console/ArgMenu.java (95%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/console/ConsoleMenu.java (98%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/crypto/KeyHandler.java (99%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/ActiveCoinChangedEventListener.java (77%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/CoinInstance.java (97%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/CoinTicker.java (96%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/CoinTickerUtils.java (98%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/HasFeeParams.java (89%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/api/JSONRPCController.java (97%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/api/JSONRPCMasterServer.java (95%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/api/JSONRPCServer.java (97%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/api/http/client/EXRServer.java (97%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/api/http/client/EXRServerPool.java (98%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/api/http/client/EXRServerSelector.java (97%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/api/http/client/EXRWrapper.java (99%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/api/http/client/HTTPClient.java (96%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/api/http/client/HttpClientConfig.java (97%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/api/http/client/HttpUtils.java (98%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/api/http/master/HTTPServerHandler.java (98%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/api/http/master/HTTPServerInitializer.java (90%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/api/http/server/ExceptionHandler.java (95%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/api/http/server/HTTPServerHandler.java (97%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/api/http/server/HTTPServerInitializer.java (92%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/protocols/bitcoin/BitcoinNetworkParametersLegacy.java (77%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/protocols/bitcoincash/BitcoinCashNetworkParametersLegacy.java (95%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/protocols/blocknet/BlocknetBlockingClient.java (98%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/protocols/blocknet/BlocknetBlockingClientManager.java (97%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/protocols/blocknet/BlocknetNetworkParameters.java (95%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/protocols/blocknet/BlocknetPacketHeader.java (97%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/protocols/blocknet/BlocknetParameters.java (77%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/protocols/blocknet/BlocknetPeer.java (98%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/protocols/blocknet/BlocknetPeerGroup.java (96%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/protocols/blocknet/BlocknetSeed.java (95%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/protocols/blocknet/BlocknetSerializer.java (98%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/protocols/blocknet/BlocknetTestnet5NetworkParameters.java (95%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/protocols/blocknet/BlocknetUtils.java (96%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/protocols/blocknet/listeners/BlocknetOnBlocksDownloadedEventListener.java (65%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/protocols/blocknet/listeners/BlocknetOnXRouterMessageReceivedListener.java (56%) create mode 100644 src/main/java/io/xlite/daemon/app/net/protocols/blocknet/listeners/BlocknetPeerConnectedEventListener.java rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/protocols/blocknet/listeners/BlocknetPeerDisconnectedEventListener.java (50%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/protocols/blocknet/listeners/BlocknetPreMessageReceivedEventListener.java (57%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/protocols/blocknet/messagequeue/MessageSource.java (50%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/protocols/blocknet/messagequeue/QueueItem.java (93%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/protocols/blocknet/messages/VersionMessageImpl.java (81%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/protocols/dashcoin/DashcoinNetworkParametersLegacy.java (95%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/protocols/digibyte/DigibyteNetworkParametersLegacy.java (95%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/protocols/dogecoin/DogecoinNetworkParametersLegacy.java (95%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/protocols/litecoin/LitecoinNetworkParametersLegacy.java (95%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/protocols/pivx/PivxNetworkParametersLegacy.java (95%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/protocols/pocketcoin/PocketcoinNetworkParametersLegacy.java (95%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/protocols/ravencoin/RavencoinNetworkParametersLegacy.java (95%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/protocols/syscoin/SyscoinNetworkParametersLegacy.java (95%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/protocols/unobtanium/UnobtaniumNetworkParametersLegacy.java (95%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/xrouter/XRouterCommandUtils.java (97%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/xrouter/XRouterFeeUtils.java (95%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/xrouter/XRouterInitialMessagesSentListener.java (56%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/xrouter/XRouterMessage.java (98%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/xrouter/XRouterMessageSerializer.java (95%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/xrouter/XRouterPacketHeader.java (99%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/net/xrouter/XRouterPacketManager.java (98%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/util/AddressBalance.java (98%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/util/AddressDiscoveryService.java (98%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/util/CCMath.java (83%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/util/CloudTransaction.java (97%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/util/ConfigHelper.java (99%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/util/ConsoleFormatter.java (97%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/util/DetectOS.java (93%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/util/FileFormatter.java (93%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/util/LogRotationManager.java (99%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/util/LogRotationUtil.java (98%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/util/PortCheck.java (92%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/util/UTXO.java (94%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/util/Utility.java (95%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/util/XRouterConfiguration.java (99%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/util/background/BackgroundTimerThread.java (95%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/util/history/Transaction.java (96%) rename src/main/java/io/{cloudchains => xlite/daemon}/app/wallet/WalletHelper.java (97%) rename src/test/java/io/{cloudchains => xlite/daemon}/app/AppTest.java (99%) rename src/test/java/io/{cloudchains => xlite/daemon}/app/coinconfig/CoinConfigRpcTest.java (97%) rename src/test/java/io/{cloudchains => xlite/daemon}/app/coinconfig/CoinConfigSourceLocalTest.java (99%) rename src/test/java/io/{cloudchains => xlite/daemon}/app/coinconfig/CoinConfigTest.java (98%) rename src/test/java/io/{cloudchains => xlite/daemon}/app/coinconfig/ConfigSourceResolverTest.java (98%) rename src/test/java/io/{cloudchains => xlite/daemon}/app/coinconfig/ConfigurableNetworkParametersCrossCheckTest.java (90%) rename src/test/java/io/{cloudchains => xlite/daemon}/app/coinconfig/XBridgeConfParserTest.java (98%) rename src/test/java/io/{cloudchains => xlite/daemon}/app/net/api/JSONRPCControllerRebindTest.java (97%) rename src/test/java/io/{cloudchains => xlite/daemon}/app/net/api/JSONRPCServerPortGateTest.java (97%) rename src/test/java/io/{cloudchains => xlite/daemon}/app/net/api/http/client/EXRResponseNormalizeTest.java (98%) rename src/test/java/io/{cloudchains => xlite/daemon}/app/net/api/http/client/HTTPClientRoutingTest.java (96%) rename src/test/java/io/{cloudchains => xlite/daemon}/app/net/api/http/server/HTTPServerHandlerSignedMessageTest.java (96%) diff --git a/pom.xml b/pom.xml index ba59289..1944f86 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 - io.cloudchains + io.xlite.daemon xlite-daemon 0.5.15 jar @@ -328,7 +328,7 @@ ${native.maven.plugin.version} - io.cloudchains.app.App + io.xlite.daemon.app.App xlite-daemon @@ -375,13 +375,13 @@ --initialize-at-build-time=org.bitcoinj.core.Utils$Runtime --initialize-at-build-time=org.bitcoinj.core.Sha256Hash --initialize-at-build-time=org.bitcoinj.crypto.MnemonicCode - --initialize-at-build-time=io.cloudchains.app.util - --initialize-at-build-time=io.cloudchains.app.crypto - --initialize-at-build-time=io.cloudchains.app.console - --initialize-at-build-time=io.cloudchains.app.net.api - --initialize-at-build-time=io.cloudchains.app.net.protocols - --initialize-at-build-time=io.cloudchains.app.net.xrouter - --initialize-at-build-time=io.cloudchains.app.net.api.http + --initialize-at-build-time=io.xlite.daemon.app.util + --initialize-at-build-time=io.xlite.daemon.app.crypto + --initialize-at-build-time=io.xlite.daemon.app.console + --initialize-at-build-time=io.xlite.daemon.app.net.api + --initialize-at-build-time=io.xlite.daemon.app.net.protocols + --initialize-at-build-time=io.xlite.daemon.app.net.xrouter + --initialize-at-build-time=io.xlite.daemon.app.net.api.http @@ -449,7 +449,7 @@ exec-maven-plugin ${exec.maven.plugin.version} - io.cloudchains.app.App + io.xlite.daemon.app.App true diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetPeerConnectedEventListener.java b/src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetPeerConnectedEventListener.java deleted file mode 100644 index 7ff5ef5..0000000 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetPeerConnectedEventListener.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.cloudchains.app.net.protocols.blocknet.listeners; - -import io.cloudchains.app.net.protocols.blocknet.BlocknetPeer; - -public interface BlocknetPeerConnectedEventListener { - - void onPeerConnected(BlocknetPeer peer, int peerCount); -} diff --git a/src/main/java/io/cloudchains/app/App.java b/src/main/java/io/xlite/daemon/app/App.java similarity index 91% rename from src/main/java/io/cloudchains/app/App.java rename to src/main/java/io/xlite/daemon/app/App.java index ff155d4..3affcee 100644 --- a/src/main/java/io/cloudchains/app/App.java +++ b/src/main/java/io/xlite/daemon/app/App.java @@ -1,13 +1,13 @@ -package io.cloudchains.app; - -import io.cloudchains.app.console.ConsoleMenu; -import io.cloudchains.app.net.api.JSONRPCController; -import io.cloudchains.app.net.api.JSONRPCMasterServer; -import io.cloudchains.app.net.api.http.client.EXRServerPool; -import io.cloudchains.app.net.api.http.client.HTTPClient; -import io.cloudchains.app.util.ConsoleFormatter; -import io.cloudchains.app.util.FileFormatter; -import io.cloudchains.app.util.LogRotationUtil; +package io.xlite.daemon.app; + +import io.xlite.daemon.app.console.ConsoleMenu; +import io.xlite.daemon.app.net.api.JSONRPCController; +import io.xlite.daemon.app.net.api.JSONRPCMasterServer; +import io.xlite.daemon.app.net.api.http.client.EXRServerPool; +import io.xlite.daemon.app.net.api.http.client.HTTPClient; +import io.xlite.daemon.app.util.ConsoleFormatter; +import io.xlite.daemon.app.util.FileFormatter; +import io.xlite.daemon.app.util.LogRotationUtil; import io.github.cdimascio.dotenv.Dotenv; import java.io.File; @@ -120,14 +120,14 @@ public static void initCoinConfigs(String[] args) { String envValue = getEnv("BLOCKCHAIN_CONFIGURATION_FILES"); String source; try { - source = new io.cloudchains.app.coinconfig.ConfigSourceResolver(flagValue, envValue).resolve(); + source = new io.xlite.daemon.app.coinconfig.ConfigSourceResolver(flagValue, envValue).resolve(); } catch (Exception e) { LOGGER.log(Level.WARNING, "[coinconfig] invalid source, using default: " + e.getMessage()); - source = io.cloudchains.app.coinconfig.ConfigSourceResolver.DEFAULT_UPSTREAM; + source = io.xlite.daemon.app.coinconfig.ConfigSourceResolver.DEFAULT_UPSTREAM; } BLOCKCHAIN_CONFIGURATION_FILES = source; try { - io.cloudchains.app.coinconfig.CoinConfigRegistry.load(source); + io.xlite.daemon.app.coinconfig.CoinConfigRegistry.load(source); } catch (Exception e) { LOGGER.log(Level.WARNING, "[coinconfig] failed to load from " + source + ": " + e.getMessage() diff --git a/src/main/java/io/cloudchains/app/Version.java b/src/main/java/io/xlite/daemon/app/Version.java similarity index 93% rename from src/main/java/io/cloudchains/app/Version.java rename to src/main/java/io/xlite/daemon/app/Version.java index abb910a..07960f6 100644 --- a/src/main/java/io/cloudchains/app/Version.java +++ b/src/main/java/io/xlite/daemon/app/Version.java @@ -1,4 +1,4 @@ -package io.cloudchains.app; +package io.xlite.daemon.app; public class Version { private static final String CLIENT_NAME = "CloudChains"; diff --git a/src/main/java/io/cloudchains/app/coinconfig/CoinConfig.java b/src/main/java/io/xlite/daemon/app/coinconfig/CoinConfig.java similarity index 99% rename from src/main/java/io/cloudchains/app/coinconfig/CoinConfig.java rename to src/main/java/io/xlite/daemon/app/coinconfig/CoinConfig.java index e72e31a..59588c7 100644 --- a/src/main/java/io/cloudchains/app/coinconfig/CoinConfig.java +++ b/src/main/java/io/xlite/daemon/app/coinconfig/CoinConfig.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.coinconfig; +package io.xlite.daemon.app.coinconfig; import java.util.Collections; import java.util.LinkedHashMap; diff --git a/src/main/java/io/cloudchains/app/coinconfig/CoinConfigGate.java b/src/main/java/io/xlite/daemon/app/coinconfig/CoinConfigGate.java similarity index 97% rename from src/main/java/io/cloudchains/app/coinconfig/CoinConfigGate.java rename to src/main/java/io/xlite/daemon/app/coinconfig/CoinConfigGate.java index da4cc5a..9c1a478 100644 --- a/src/main/java/io/cloudchains/app/coinconfig/CoinConfigGate.java +++ b/src/main/java/io/xlite/daemon/app/coinconfig/CoinConfigGate.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.coinconfig; +package io.xlite.daemon.app.coinconfig; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/io/cloudchains/app/coinconfig/CoinConfigRegistry.java b/src/main/java/io/xlite/daemon/app/coinconfig/CoinConfigRegistry.java similarity index 99% rename from src/main/java/io/cloudchains/app/coinconfig/CoinConfigRegistry.java rename to src/main/java/io/xlite/daemon/app/coinconfig/CoinConfigRegistry.java index 7dff670..5628368 100644 --- a/src/main/java/io/cloudchains/app/coinconfig/CoinConfigRegistry.java +++ b/src/main/java/io/xlite/daemon/app/coinconfig/CoinConfigRegistry.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.coinconfig; +package io.xlite.daemon.app.coinconfig; import java.util.Collections; import java.util.LinkedHashMap; diff --git a/src/main/java/io/cloudchains/app/coinconfig/CoinConfigSource.java b/src/main/java/io/xlite/daemon/app/coinconfig/CoinConfigSource.java similarity index 99% rename from src/main/java/io/cloudchains/app/coinconfig/CoinConfigSource.java rename to src/main/java/io/xlite/daemon/app/coinconfig/CoinConfigSource.java index 1f3366a..f6d9c88 100644 --- a/src/main/java/io/cloudchains/app/coinconfig/CoinConfigSource.java +++ b/src/main/java/io/xlite/daemon/app/coinconfig/CoinConfigSource.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.coinconfig; +package io.xlite.daemon.app.coinconfig; import com.google.gson.JsonArray; import com.google.gson.JsonElement; diff --git a/src/main/java/io/cloudchains/app/coinconfig/CompiledCoinSupplement.java b/src/main/java/io/xlite/daemon/app/coinconfig/CompiledCoinSupplement.java similarity index 98% rename from src/main/java/io/cloudchains/app/coinconfig/CompiledCoinSupplement.java rename to src/main/java/io/xlite/daemon/app/coinconfig/CompiledCoinSupplement.java index 5b95989..2e9a7bf 100644 --- a/src/main/java/io/cloudchains/app/coinconfig/CompiledCoinSupplement.java +++ b/src/main/java/io/xlite/daemon/app/coinconfig/CompiledCoinSupplement.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.coinconfig; +package io.xlite.daemon.app.coinconfig; import java.util.Map; diff --git a/src/main/java/io/cloudchains/app/coinconfig/ConfigSourceResolver.java b/src/main/java/io/xlite/daemon/app/coinconfig/ConfigSourceResolver.java similarity index 98% rename from src/main/java/io/cloudchains/app/coinconfig/ConfigSourceResolver.java rename to src/main/java/io/xlite/daemon/app/coinconfig/ConfigSourceResolver.java index fddc796..c5a580d 100644 --- a/src/main/java/io/cloudchains/app/coinconfig/ConfigSourceResolver.java +++ b/src/main/java/io/xlite/daemon/app/coinconfig/ConfigSourceResolver.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.coinconfig; +package io.xlite.daemon.app.coinconfig; import java.util.Locale; diff --git a/src/main/java/io/cloudchains/app/coinconfig/ConfigurableNetworkParameters.java b/src/main/java/io/xlite/daemon/app/coinconfig/ConfigurableNetworkParameters.java similarity index 98% rename from src/main/java/io/cloudchains/app/coinconfig/ConfigurableNetworkParameters.java rename to src/main/java/io/xlite/daemon/app/coinconfig/ConfigurableNetworkParameters.java index d4f6d28..1b456f3 100644 --- a/src/main/java/io/cloudchains/app/coinconfig/ConfigurableNetworkParameters.java +++ b/src/main/java/io/xlite/daemon/app/coinconfig/ConfigurableNetworkParameters.java @@ -1,6 +1,6 @@ -package io.cloudchains.app.coinconfig; +package io.xlite.daemon.app.coinconfig; -import io.cloudchains.app.net.HasFeeParams; +import io.xlite.daemon.app.net.HasFeeParams; import org.bitcoinj.core.*; import org.bitcoinj.store.BlockStore; import org.bitcoinj.store.BlockStoreException; diff --git a/src/main/java/io/cloudchains/app/coinconfig/XBridgeConfParser.java b/src/main/java/io/xlite/daemon/app/coinconfig/XBridgeConfParser.java similarity index 98% rename from src/main/java/io/cloudchains/app/coinconfig/XBridgeConfParser.java rename to src/main/java/io/xlite/daemon/app/coinconfig/XBridgeConfParser.java index a19579f..6cc69de 100644 --- a/src/main/java/io/cloudchains/app/coinconfig/XBridgeConfParser.java +++ b/src/main/java/io/xlite/daemon/app/coinconfig/XBridgeConfParser.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.coinconfig; +package io.xlite.daemon.app.coinconfig; import java.io.IOException; import java.nio.charset.StandardCharsets; diff --git a/src/main/java/io/cloudchains/app/console/ArgMenu.java b/src/main/java/io/xlite/daemon/app/console/ArgMenu.java similarity index 95% rename from src/main/java/io/cloudchains/app/console/ArgMenu.java rename to src/main/java/io/xlite/daemon/app/console/ArgMenu.java index 1f3e23d..317cb0f 100644 --- a/src/main/java/io/cloudchains/app/console/ArgMenu.java +++ b/src/main/java/io/xlite/daemon/app/console/ArgMenu.java @@ -1,8 +1,8 @@ -package io.cloudchains.app.console; +package io.xlite.daemon.app.console; -import io.cloudchains.app.crypto.KeyHandler; -import io.cloudchains.app.net.CoinInstance; -import io.cloudchains.app.net.CoinTicker; +import io.xlite.daemon.app.crypto.KeyHandler; +import io.xlite.daemon.app.net.CoinInstance; +import io.xlite.daemon.app.net.CoinTicker; import java.util.Arrays; diff --git a/src/main/java/io/cloudchains/app/console/ConsoleMenu.java b/src/main/java/io/xlite/daemon/app/console/ConsoleMenu.java similarity index 98% rename from src/main/java/io/cloudchains/app/console/ConsoleMenu.java rename to src/main/java/io/xlite/daemon/app/console/ConsoleMenu.java index 41a5a7d..667480e 100644 --- a/src/main/java/io/cloudchains/app/console/ConsoleMenu.java +++ b/src/main/java/io/xlite/daemon/app/console/ConsoleMenu.java @@ -1,14 +1,14 @@ -package io.cloudchains.app.console; - -import io.cloudchains.app.App; -import io.cloudchains.app.Version; -import io.cloudchains.app.crypto.KeyHandler; -import io.cloudchains.app.net.CoinInstance; -import io.cloudchains.app.net.CoinTicker; -import io.cloudchains.app.net.CoinTickerUtils; -import io.cloudchains.app.net.api.http.client.EXRServerPool; -import io.cloudchains.app.util.ConfigHelper; -import io.cloudchains.app.util.background.BackgroundTimerThread; +package io.xlite.daemon.app.console; + +import io.xlite.daemon.app.App; +import io.xlite.daemon.app.Version; +import io.xlite.daemon.app.crypto.KeyHandler; +import io.xlite.daemon.app.net.CoinInstance; +import io.xlite.daemon.app.net.CoinTicker; +import io.xlite.daemon.app.net.CoinTickerUtils; +import io.xlite.daemon.app.net.api.http.client.EXRServerPool; +import io.xlite.daemon.app.util.ConfigHelper; +import io.xlite.daemon.app.util.background.BackgroundTimerThread; import java.io.Console; import java.security.SecureRandom; diff --git a/src/main/java/io/cloudchains/app/crypto/KeyHandler.java b/src/main/java/io/xlite/daemon/app/crypto/KeyHandler.java similarity index 99% rename from src/main/java/io/cloudchains/app/crypto/KeyHandler.java rename to src/main/java/io/xlite/daemon/app/crypto/KeyHandler.java index fb70a1a..0c72246 100644 --- a/src/main/java/io/cloudchains/app/crypto/KeyHandler.java +++ b/src/main/java/io/xlite/daemon/app/crypto/KeyHandler.java @@ -1,8 +1,8 @@ -package io.cloudchains.app.crypto; +package io.xlite.daemon.app.crypto; import com.google.common.base.Joiner; import com.subgraph.orchid.encoders.Base64; -import io.cloudchains.app.util.ConfigHelper; +import io.xlite.daemon.app.util.ConfigHelper; import org.bitcoinj.core.ECKey; import org.bitcoinj.crypto.MnemonicCode; import org.bitcoinj.crypto.MnemonicException; diff --git a/src/main/java/io/cloudchains/app/net/ActiveCoinChangedEventListener.java b/src/main/java/io/xlite/daemon/app/net/ActiveCoinChangedEventListener.java similarity index 77% rename from src/main/java/io/cloudchains/app/net/ActiveCoinChangedEventListener.java rename to src/main/java/io/xlite/daemon/app/net/ActiveCoinChangedEventListener.java index 821cc6a..f2dc18c 100644 --- a/src/main/java/io/cloudchains/app/net/ActiveCoinChangedEventListener.java +++ b/src/main/java/io/xlite/daemon/app/net/ActiveCoinChangedEventListener.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.net; +package io.xlite.daemon.app.net; public interface ActiveCoinChangedEventListener { diff --git a/src/main/java/io/cloudchains/app/net/CoinInstance.java b/src/main/java/io/xlite/daemon/app/net/CoinInstance.java similarity index 97% rename from src/main/java/io/cloudchains/app/net/CoinInstance.java rename to src/main/java/io/xlite/daemon/app/net/CoinInstance.java index 7eff81c..b684d1b 100644 --- a/src/main/java/io/cloudchains/app/net/CoinInstance.java +++ b/src/main/java/io/xlite/daemon/app/net/CoinInstance.java @@ -1,24 +1,24 @@ -package io.cloudchains.app.net; +package io.xlite.daemon.app.net; import com.google.common.base.Joiner; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.subgraph.orchid.encoders.Hex; //import com.subgraph.orchid.encoders.Hex; -import io.cloudchains.app.Version; -import io.cloudchains.app.crypto.KeyHandler; -import io.cloudchains.app.net.api.JSONRPCController; -import io.cloudchains.app.net.api.JSONRPCServer; -import io.cloudchains.app.net.protocols.blocknet.*; -import io.cloudchains.app.net.xrouter.XRouterMessage; -import io.cloudchains.app.net.xrouter.XRouterPacketManager; -import io.cloudchains.app.util.AddressBalance; -import io.cloudchains.app.util.AddressDiscoveryService; -import io.cloudchains.app.util.CloudTransaction; -import io.cloudchains.app.util.ConfigHelper; -import io.cloudchains.app.util.UTXO; -import io.cloudchains.app.util.history.Transaction; -import io.cloudchains.app.wallet.WalletHelper; +import io.xlite.daemon.app.Version; +import io.xlite.daemon.app.crypto.KeyHandler; +import io.xlite.daemon.app.net.api.JSONRPCController; +import io.xlite.daemon.app.net.api.JSONRPCServer; +import io.xlite.daemon.app.net.protocols.blocknet.*; +import io.xlite.daemon.app.net.xrouter.XRouterMessage; +import io.xlite.daemon.app.net.xrouter.XRouterPacketManager; +import io.xlite.daemon.app.util.AddressBalance; +import io.xlite.daemon.app.util.AddressDiscoveryService; +import io.xlite.daemon.app.util.CloudTransaction; +import io.xlite.daemon.app.util.ConfigHelper; +import io.xlite.daemon.app.util.UTXO; +import io.xlite.daemon.app.util.history.Transaction; +import io.xlite.daemon.app.wallet.WalletHelper; import org.bitcoinj.core.*; import org.bitcoinj.utils.BtcFormat; import org.bitcoinj.utils.ListenerRegistration; @@ -359,7 +359,7 @@ public CoinError init(char[] pw, String userMnemonic, boolean isMnemonic, boolea } // case BITCOIN_CASH: { // LOGGER.fine("[coin] Initializing for BitcoinCash main network."); - // networkParameters = new io.cloudchains.app.net.protocols.bitcoincash.BitcoinCashNetworkParametersLegacy(); + // networkParameters = new io.xlite.daemon.app.net.protocols.bitcoincash.BitcoinCashNetworkParametersLegacy(); // rpcPort = 48332; // break; // } @@ -560,8 +560,8 @@ public CoinError init(char[] pw, String userMnemonic, boolean isMnemonic, boolea private CoinError loadMigratedParams() { try { - networkParameters = io.cloudchains.app.coinconfig.ConfigurableNetworkParameters - .from(io.cloudchains.app.coinconfig.CoinConfigRegistry + networkParameters = io.xlite.daemon.app.coinconfig.ConfigurableNetworkParameters + .from(io.xlite.daemon.app.coinconfig.CoinConfigRegistry .get(CoinTickerUtils.tickerToString(ticker))); return null; } catch (IllegalStateException | IllegalArgumentException e) { diff --git a/src/main/java/io/cloudchains/app/net/CoinTicker.java b/src/main/java/io/xlite/daemon/app/net/CoinTicker.java similarity index 96% rename from src/main/java/io/cloudchains/app/net/CoinTicker.java rename to src/main/java/io/xlite/daemon/app/net/CoinTicker.java index 6f112dd..a0f40b5 100644 --- a/src/main/java/io/cloudchains/app/net/CoinTicker.java +++ b/src/main/java/io/xlite/daemon/app/net/CoinTicker.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.net; +package io.xlite.daemon.app.net; import java.util.Arrays; import java.util.List; diff --git a/src/main/java/io/cloudchains/app/net/CoinTickerUtils.java b/src/main/java/io/xlite/daemon/app/net/CoinTickerUtils.java similarity index 98% rename from src/main/java/io/cloudchains/app/net/CoinTickerUtils.java rename to src/main/java/io/xlite/daemon/app/net/CoinTickerUtils.java index 499684b..f18af61 100644 --- a/src/main/java/io/cloudchains/app/net/CoinTickerUtils.java +++ b/src/main/java/io/xlite/daemon/app/net/CoinTickerUtils.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.net; +package io.xlite.daemon.app.net; import com.google.common.collect.HashBiMap; diff --git a/src/main/java/io/cloudchains/app/net/HasFeeParams.java b/src/main/java/io/xlite/daemon/app/net/HasFeeParams.java similarity index 89% rename from src/main/java/io/cloudchains/app/net/HasFeeParams.java rename to src/main/java/io/xlite/daemon/app/net/HasFeeParams.java index f253dee..d6de60b 100644 --- a/src/main/java/io/cloudchains/app/net/HasFeeParams.java +++ b/src/main/java/io/xlite/daemon/app/net/HasFeeParams.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.net; +package io.xlite.daemon.app.net; public interface HasFeeParams { default long getFeePerByte() { diff --git a/src/main/java/io/cloudchains/app/net/api/JSONRPCController.java b/src/main/java/io/xlite/daemon/app/net/api/JSONRPCController.java similarity index 97% rename from src/main/java/io/cloudchains/app/net/api/JSONRPCController.java rename to src/main/java/io/xlite/daemon/app/net/api/JSONRPCController.java index 7f653ff..9b1b215 100644 --- a/src/main/java/io/cloudchains/app/net/api/JSONRPCController.java +++ b/src/main/java/io/xlite/daemon/app/net/api/JSONRPCController.java @@ -1,7 +1,7 @@ -package io.cloudchains.app.net.api; +package io.xlite.daemon.app.net.api; -import io.cloudchains.app.net.CoinInstance; -import io.cloudchains.app.util.ConfigHelper; +import io.xlite.daemon.app.net.CoinInstance; +import io.xlite.daemon.app.util.ConfigHelper; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.ReentrantLock; diff --git a/src/main/java/io/cloudchains/app/net/api/JSONRPCMasterServer.java b/src/main/java/io/xlite/daemon/app/net/api/JSONRPCMasterServer.java similarity index 95% rename from src/main/java/io/cloudchains/app/net/api/JSONRPCMasterServer.java rename to src/main/java/io/xlite/daemon/app/net/api/JSONRPCMasterServer.java index d8a8160..c2017b9 100644 --- a/src/main/java/io/cloudchains/app/net/api/JSONRPCMasterServer.java +++ b/src/main/java/io/xlite/daemon/app/net/api/JSONRPCMasterServer.java @@ -1,6 +1,6 @@ -package io.cloudchains.app.net.api; +package io.xlite.daemon.app.net.api; -import io.cloudchains.app.net.api.http.master.HTTPServerInitializer; +import io.xlite.daemon.app.net.api.http.master.HTTPServerInitializer; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.PooledByteBufAllocator; import io.netty.channel.Channel; diff --git a/src/main/java/io/cloudchains/app/net/api/JSONRPCServer.java b/src/main/java/io/xlite/daemon/app/net/api/JSONRPCServer.java similarity index 97% rename from src/main/java/io/cloudchains/app/net/api/JSONRPCServer.java rename to src/main/java/io/xlite/daemon/app/net/api/JSONRPCServer.java index b0d8871..38fda86 100644 --- a/src/main/java/io/cloudchains/app/net/api/JSONRPCServer.java +++ b/src/main/java/io/xlite/daemon/app/net/api/JSONRPCServer.java @@ -1,8 +1,8 @@ -package io.cloudchains.app.net.api; +package io.xlite.daemon.app.net.api; -import io.cloudchains.app.net.CoinInstance; -import io.cloudchains.app.net.CoinTickerUtils; -import io.cloudchains.app.net.api.http.server.HTTPServerInitializer; +import io.xlite.daemon.app.net.CoinInstance; +import io.xlite.daemon.app.net.CoinTickerUtils; +import io.xlite.daemon.app.net.api.http.server.HTTPServerInitializer; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.PooledByteBufAllocator; import io.netty.channel.Channel; diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/EXRServer.java b/src/main/java/io/xlite/daemon/app/net/api/http/client/EXRServer.java similarity index 97% rename from src/main/java/io/cloudchains/app/net/api/http/client/EXRServer.java rename to src/main/java/io/xlite/daemon/app/net/api/http/client/EXRServer.java index 9c83319..cb94be7 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/client/EXRServer.java +++ b/src/main/java/io/xlite/daemon/app/net/api/http/client/EXRServer.java @@ -1,9 +1,9 @@ -package io.cloudchains.app.net.api.http.client; +package io.xlite.daemon.app.net.api.http.client; import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import io.cloudchains.app.net.CoinTicker; -import io.cloudchains.app.net.CoinTickerUtils; +import io.xlite.daemon.app.net.CoinTicker; +import io.xlite.daemon.app.net.CoinTickerUtils; import java.util.HashSet; import java.util.List; diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/EXRServerPool.java b/src/main/java/io/xlite/daemon/app/net/api/http/client/EXRServerPool.java similarity index 98% rename from src/main/java/io/cloudchains/app/net/api/http/client/EXRServerPool.java rename to src/main/java/io/xlite/daemon/app/net/api/http/client/EXRServerPool.java index 48adfba..3b1c573 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/client/EXRServerPool.java +++ b/src/main/java/io/xlite/daemon/app/net/api/http/client/EXRServerPool.java @@ -1,7 +1,7 @@ -package io.cloudchains.app.net.api.http.client; +package io.xlite.daemon.app.net.api.http.client; -import io.cloudchains.app.net.CoinTicker; -import io.cloudchains.app.net.CoinTickerUtils; +import io.xlite.daemon.app.net.CoinTicker; +import io.xlite.daemon.app.net.CoinTickerUtils; import java.util.*; import java.util.concurrent.ConcurrentHashMap; diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/EXRServerSelector.java b/src/main/java/io/xlite/daemon/app/net/api/http/client/EXRServerSelector.java similarity index 97% rename from src/main/java/io/cloudchains/app/net/api/http/client/EXRServerSelector.java rename to src/main/java/io/xlite/daemon/app/net/api/http/client/EXRServerSelector.java index a260711..0c0251f 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/client/EXRServerSelector.java +++ b/src/main/java/io/xlite/daemon/app/net/api/http/client/EXRServerSelector.java @@ -1,7 +1,7 @@ -package io.cloudchains.app.net.api.http.client; +package io.xlite.daemon.app.net.api.http.client; -import io.cloudchains.app.net.CoinTicker; -import io.cloudchains.app.net.CoinTickerUtils; +import io.xlite.daemon.app.net.CoinTicker; +import io.xlite.daemon.app.net.CoinTickerUtils; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/EXRWrapper.java b/src/main/java/io/xlite/daemon/app/net/api/http/client/EXRWrapper.java similarity index 99% rename from src/main/java/io/cloudchains/app/net/api/http/client/EXRWrapper.java rename to src/main/java/io/xlite/daemon/app/net/api/http/client/EXRWrapper.java index 7250873..2dcc68c 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/client/EXRWrapper.java +++ b/src/main/java/io/xlite/daemon/app/net/api/http/client/EXRWrapper.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.net.api.http.client; +package io.xlite.daemon.app.net.api.http.client; import com.google.gson.Gson; import com.google.gson.JsonElement; diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java b/src/main/java/io/xlite/daemon/app/net/api/http/client/HTTPClient.java similarity index 96% rename from src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java rename to src/main/java/io/xlite/daemon/app/net/api/http/client/HTTPClient.java index fb9c487..24d05fa 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/client/HTTPClient.java +++ b/src/main/java/io/xlite/daemon/app/net/api/http/client/HTTPClient.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.net.api.http.client; +package io.xlite.daemon.app.net.api.http.client; import com.google.common.collect.Lists; import com.google.gson.Gson; @@ -10,13 +10,13 @@ import com.google.gson.JsonSyntaxException; import com.google.gson.reflect.TypeToken; import com.subgraph.orchid.encoders.Hex; -import io.cloudchains.app.App; -import io.cloudchains.app.net.CoinInstance; -import io.cloudchains.app.net.CoinTicker; -import io.cloudchains.app.net.CoinTickerUtils; -import io.cloudchains.app.util.AddressBalance; -import io.cloudchains.app.util.UTXO; -import io.cloudchains.app.util.history.Transaction; +import io.xlite.daemon.app.App; +import io.xlite.daemon.app.net.CoinInstance; +import io.xlite.daemon.app.net.CoinTicker; +import io.xlite.daemon.app.net.CoinTickerUtils; +import io.xlite.daemon.app.util.AddressBalance; +import io.xlite.daemon.app.util.UTXO; +import io.xlite.daemon.app.util.history.Transaction; import org.apache.http.Header; import org.apache.http.HttpHeaders; import org.apache.http.client.config.RequestConfig; diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/HttpClientConfig.java b/src/main/java/io/xlite/daemon/app/net/api/http/client/HttpClientConfig.java similarity index 97% rename from src/main/java/io/cloudchains/app/net/api/http/client/HttpClientConfig.java rename to src/main/java/io/xlite/daemon/app/net/api/http/client/HttpClientConfig.java index c4454e0..64411c9 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/client/HttpClientConfig.java +++ b/src/main/java/io/xlite/daemon/app/net/api/http/client/HttpClientConfig.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.net.api.http.client; +package io.xlite.daemon.app.net.api.http.client; /** * Centralized configuration management for HTTP client settings. diff --git a/src/main/java/io/cloudchains/app/net/api/http/client/HttpUtils.java b/src/main/java/io/xlite/daemon/app/net/api/http/client/HttpUtils.java similarity index 98% rename from src/main/java/io/cloudchains/app/net/api/http/client/HttpUtils.java rename to src/main/java/io/xlite/daemon/app/net/api/http/client/HttpUtils.java index 0659628..6bc3ba9 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/client/HttpUtils.java +++ b/src/main/java/io/xlite/daemon/app/net/api/http/client/HttpUtils.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.net.api.http.client; +package io.xlite.daemon.app.net.api.http.client; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; diff --git a/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerHandler.java b/src/main/java/io/xlite/daemon/app/net/api/http/master/HTTPServerHandler.java similarity index 98% rename from src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerHandler.java rename to src/main/java/io/xlite/daemon/app/net/api/http/master/HTTPServerHandler.java index 7aef8e5..a96867f 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerHandler.java +++ b/src/main/java/io/xlite/daemon/app/net/api/http/master/HTTPServerHandler.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.net.api.http.master; +package io.xlite.daemon.app.net.api.http.master; import com.google.common.base.Preconditions; import com.google.gson.JsonArray; @@ -6,13 +6,13 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.subgraph.orchid.encoders.Base64; -import io.cloudchains.app.Version; -import io.cloudchains.app.coinconfig.CoinConfig; -import io.cloudchains.app.coinconfig.CoinConfigRegistry; -import io.cloudchains.app.net.CoinInstance; -import io.cloudchains.app.net.CoinTicker; -import io.cloudchains.app.net.CoinTickerUtils; -import io.cloudchains.app.util.ConfigHelper; +import io.xlite.daemon.app.Version; +import io.xlite.daemon.app.coinconfig.CoinConfig; +import io.xlite.daemon.app.coinconfig.CoinConfigRegistry; +import io.xlite.daemon.app.net.CoinInstance; +import io.xlite.daemon.app.net.CoinTicker; +import io.xlite.daemon.app.net.CoinTickerUtils; +import io.xlite.daemon.app.util.ConfigHelper; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFutureListener; diff --git a/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerInitializer.java b/src/main/java/io/xlite/daemon/app/net/api/http/master/HTTPServerInitializer.java similarity index 90% rename from src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerInitializer.java rename to src/main/java/io/xlite/daemon/app/net/api/http/master/HTTPServerInitializer.java index 4319d06..3caadc3 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/master/HTTPServerInitializer.java +++ b/src/main/java/io/xlite/daemon/app/net/api/http/master/HTTPServerInitializer.java @@ -1,6 +1,6 @@ -package io.cloudchains.app.net.api.http.master; +package io.xlite.daemon.app.net.api.http.master; -import io.cloudchains.app.net.api.http.server.ExceptionHandler; +import io.xlite.daemon.app.net.api.http.server.ExceptionHandler; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.socket.SocketChannel; diff --git a/src/main/java/io/cloudchains/app/net/api/http/server/ExceptionHandler.java b/src/main/java/io/xlite/daemon/app/net/api/http/server/ExceptionHandler.java similarity index 95% rename from src/main/java/io/cloudchains/app/net/api/http/server/ExceptionHandler.java rename to src/main/java/io/xlite/daemon/app/net/api/http/server/ExceptionHandler.java index cd0b82e..d5b1da8 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/server/ExceptionHandler.java +++ b/src/main/java/io/xlite/daemon/app/net/api/http/server/ExceptionHandler.java @@ -1,8 +1,8 @@ -package io.cloudchains.app.net.api.http.server; +package io.xlite.daemon.app.net.api.http.server; import com.google.gson.JsonNull; import com.google.gson.JsonObject; -import io.cloudchains.app.net.CoinInstance; +import io.xlite.daemon.app.net.CoinInstance; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelDuplexHandler; diff --git a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java b/src/main/java/io/xlite/daemon/app/net/api/http/server/HTTPServerHandler.java similarity index 97% rename from src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java rename to src/main/java/io/xlite/daemon/app/net/api/http/server/HTTPServerHandler.java index 69a9df9..4f147a5 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerHandler.java +++ b/src/main/java/io/xlite/daemon/app/net/api/http/server/HTTPServerHandler.java @@ -1,20 +1,20 @@ -package io.cloudchains.app.net.api.http.server; +package io.xlite.daemon.app.net.api.http.server; import com.google.common.base.Preconditions; import com.google.gson.*; import com.subgraph.orchid.encoders.Base64; import com.subgraph.orchid.encoders.Hex; -import io.cloudchains.app.Version; -import io.cloudchains.app.net.CoinInstance; -import io.cloudchains.app.net.CoinTicker; -import io.cloudchains.app.net.CoinTickerUtils; -import io.cloudchains.app.net.api.http.client.HTTPClient; -import io.cloudchains.app.net.protocols.blocknet.BlocknetPeer; -import io.cloudchains.app.util.AddressBalance; -import io.cloudchains.app.util.ConfigHelper; -import io.cloudchains.app.util.UTXO; -import io.cloudchains.app.util.Utility; -import io.cloudchains.app.wallet.WalletHelper; +import io.xlite.daemon.app.Version; +import io.xlite.daemon.app.net.CoinInstance; +import io.xlite.daemon.app.net.CoinTicker; +import io.xlite.daemon.app.net.CoinTickerUtils; +import io.xlite.daemon.app.net.api.http.client.HTTPClient; +import io.xlite.daemon.app.net.protocols.blocknet.BlocknetPeer; +import io.xlite.daemon.app.util.AddressBalance; +import io.xlite.daemon.app.util.ConfigHelper; +import io.xlite.daemon.app.util.UTXO; +import io.xlite.daemon.app.util.Utility; +import io.xlite.daemon.app.wallet.WalletHelper; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFutureListener; diff --git a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerInitializer.java b/src/main/java/io/xlite/daemon/app/net/api/http/server/HTTPServerInitializer.java similarity index 92% rename from src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerInitializer.java rename to src/main/java/io/xlite/daemon/app/net/api/http/server/HTTPServerInitializer.java index d4f95fd..0193492 100644 --- a/src/main/java/io/cloudchains/app/net/api/http/server/HTTPServerInitializer.java +++ b/src/main/java/io/xlite/daemon/app/net/api/http/server/HTTPServerInitializer.java @@ -1,6 +1,6 @@ -package io.cloudchains.app.net.api.http.server; +package io.xlite.daemon.app.net.api.http.server; -import io.cloudchains.app.net.CoinInstance; +import io.xlite.daemon.app.net.CoinInstance; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.socket.SocketChannel; diff --git a/src/main/java/io/cloudchains/app/net/protocols/bitcoin/BitcoinNetworkParametersLegacy.java b/src/main/java/io/xlite/daemon/app/net/protocols/bitcoin/BitcoinNetworkParametersLegacy.java similarity index 77% rename from src/main/java/io/cloudchains/app/net/protocols/bitcoin/BitcoinNetworkParametersLegacy.java rename to src/main/java/io/xlite/daemon/app/net/protocols/bitcoin/BitcoinNetworkParametersLegacy.java index 2499f03..f2db429 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/bitcoin/BitcoinNetworkParametersLegacy.java +++ b/src/main/java/io/xlite/daemon/app/net/protocols/bitcoin/BitcoinNetworkParametersLegacy.java @@ -1,6 +1,6 @@ -package io.cloudchains.app.net.protocols.bitcoin; +package io.xlite.daemon.app.net.protocols.bitcoin; -import io.cloudchains.app.net.HasFeeParams; +import io.xlite.daemon.app.net.HasFeeParams; import org.bitcoinj.params.MainNetParams; public class BitcoinNetworkParametersLegacy extends MainNetParams implements HasFeeParams { diff --git a/src/main/java/io/cloudchains/app/net/protocols/bitcoincash/BitcoinCashNetworkParametersLegacy.java b/src/main/java/io/xlite/daemon/app/net/protocols/bitcoincash/BitcoinCashNetworkParametersLegacy.java similarity index 95% rename from src/main/java/io/cloudchains/app/net/protocols/bitcoincash/BitcoinCashNetworkParametersLegacy.java rename to src/main/java/io/xlite/daemon/app/net/protocols/bitcoincash/BitcoinCashNetworkParametersLegacy.java index b37d5b4..c31f887 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/bitcoincash/BitcoinCashNetworkParametersLegacy.java +++ b/src/main/java/io/xlite/daemon/app/net/protocols/bitcoincash/BitcoinCashNetworkParametersLegacy.java @@ -1,6 +1,6 @@ -package io.cloudchains.app.net.protocols.bitcoincash; +package io.xlite.daemon.app.net.protocols.bitcoincash; -import io.cloudchains.app.net.HasFeeParams; +import io.xlite.daemon.app.net.HasFeeParams; import org.bitcoinj.core.*; import org.bitcoinj.store.BlockStore; import org.bitcoinj.store.BlockStoreException; diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetBlockingClient.java b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetBlockingClient.java similarity index 98% rename from src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetBlockingClient.java rename to src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetBlockingClient.java index effd35c..aa852fa 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetBlockingClient.java +++ b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetBlockingClient.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.net.protocols.blocknet; +package io.xlite.daemon.app.net.protocols.blocknet; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetBlockingClientManager.java b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetBlockingClientManager.java similarity index 97% rename from src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetBlockingClientManager.java rename to src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetBlockingClientManager.java index 0457031..ba6ce57 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetBlockingClientManager.java +++ b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetBlockingClientManager.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.net.protocols.blocknet; +package io.xlite.daemon.app.net.protocols.blocknet; import com.google.common.util.concurrent.AbstractIdleService; import com.google.common.util.concurrent.ListenableFuture; diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetNetworkParameters.java b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetNetworkParameters.java similarity index 95% rename from src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetNetworkParameters.java rename to src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetNetworkParameters.java index c390bd7..c65320e 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetNetworkParameters.java +++ b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetNetworkParameters.java @@ -1,8 +1,8 @@ -package io.cloudchains.app.net.protocols.blocknet; +package io.xlite.daemon.app.net.protocols.blocknet; import com.subgraph.orchid.encoders.Hex; -import io.cloudchains.app.net.HasFeeParams; -import io.cloudchains.app.net.xrouter.XRouterMessageSerializer; +import io.xlite.daemon.app.net.HasFeeParams; +import io.xlite.daemon.app.net.xrouter.XRouterMessageSerializer; import org.bitcoinj.core.*; import org.bitcoinj.store.BlockStore; import org.bitcoinj.store.BlockStoreException; diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPacketHeader.java b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetPacketHeader.java similarity index 97% rename from src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPacketHeader.java rename to src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetPacketHeader.java index 86c9923..084268a 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPacketHeader.java +++ b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetPacketHeader.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.net.protocols.blocknet; +package io.xlite.daemon.app.net.protocols.blocknet; import org.bitcoinj.core.BitcoinSerializer; import org.bitcoinj.core.Message; diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetParameters.java b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetParameters.java similarity index 77% rename from src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetParameters.java rename to src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetParameters.java index d22846f..2e05e6d 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetParameters.java +++ b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetParameters.java @@ -1,6 +1,6 @@ -package io.cloudchains.app.net.protocols.blocknet; +package io.xlite.daemon.app.net.protocols.blocknet; -import io.cloudchains.app.net.xrouter.XRouterMessageSerializer; +import io.xlite.daemon.app.net.xrouter.XRouterMessageSerializer; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.Sha256Hash; diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeer.java b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetPeer.java similarity index 98% rename from src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeer.java rename to src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetPeer.java index 3c0f807..944ac22 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeer.java +++ b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetPeer.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.net.protocols.blocknet; +package io.xlite.daemon.app.net.protocols.blocknet; import com.google.common.base.Function; import com.google.common.base.Throwables; @@ -6,14 +6,14 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; -import io.cloudchains.app.Version; -import io.cloudchains.app.net.protocols.blocknet.listeners.*; -import io.cloudchains.app.net.protocols.blocknet.messages.VersionMessageImpl; -import io.cloudchains.app.net.xrouter.XRouterCommandUtils; -import io.cloudchains.app.net.xrouter.XRouterInitialMessagesSentListener; -import io.cloudchains.app.net.xrouter.XRouterMessage; -import io.cloudchains.app.net.xrouter.XRouterMessageSerializer; -import io.cloudchains.app.util.XRouterConfiguration; +import io.xlite.daemon.app.Version; +import io.xlite.daemon.app.net.protocols.blocknet.listeners.*; +import io.xlite.daemon.app.net.protocols.blocknet.messages.VersionMessageImpl; +import io.xlite.daemon.app.net.xrouter.XRouterCommandUtils; +import io.xlite.daemon.app.net.xrouter.XRouterInitialMessagesSentListener; +import io.xlite.daemon.app.net.xrouter.XRouterMessage; +import io.xlite.daemon.app.net.xrouter.XRouterMessageSerializer; +import io.xlite.daemon.app.util.XRouterConfiguration; import org.bitcoinj.core.*; import org.bitcoinj.store.BlockStore; import org.bitcoinj.store.BlockStoreException; diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeerGroup.java b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetPeerGroup.java similarity index 96% rename from src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeerGroup.java rename to src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetPeerGroup.java index 353ca0e..6f23609 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetPeerGroup.java +++ b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetPeerGroup.java @@ -1,22 +1,22 @@ -package io.cloudchains.app.net.protocols.blocknet; +package io.xlite.daemon.app.net.protocols.blocknet; import com.google.common.base.Preconditions; import com.google.common.base.Throwables; import com.google.common.util.concurrent.*; -import io.cloudchains.app.net.CoinInstance; -import io.cloudchains.app.net.CoinTicker; -import io.cloudchains.app.net.CoinTickerUtils; -import io.cloudchains.app.net.protocols.blocknet.listeners.BlocknetOnXRouterMessageReceivedListener; -import io.cloudchains.app.net.protocols.blocknet.listeners.BlocknetPeerConnectedEventListener; -import io.cloudchains.app.net.protocols.blocknet.listeners.BlocknetPeerDisconnectedEventListener; -import io.cloudchains.app.net.protocols.blocknet.messagequeue.MessageSource; -import io.cloudchains.app.net.protocols.blocknet.messagequeue.QueueItem; -import io.cloudchains.app.net.xrouter.XRouterCommandUtils; -import io.cloudchains.app.net.xrouter.XRouterInitialMessagesSentListener; -import io.cloudchains.app.net.xrouter.XRouterMessage; -import io.cloudchains.app.util.UTXO; -import io.cloudchains.app.util.XRouterConfiguration; -import io.cloudchains.app.util.background.BackgroundTimerThread; +import io.xlite.daemon.app.net.CoinInstance; +import io.xlite.daemon.app.net.CoinTicker; +import io.xlite.daemon.app.net.CoinTickerUtils; +import io.xlite.daemon.app.net.protocols.blocknet.listeners.BlocknetOnXRouterMessageReceivedListener; +import io.xlite.daemon.app.net.protocols.blocknet.listeners.BlocknetPeerConnectedEventListener; +import io.xlite.daemon.app.net.protocols.blocknet.listeners.BlocknetPeerDisconnectedEventListener; +import io.xlite.daemon.app.net.protocols.blocknet.messagequeue.MessageSource; +import io.xlite.daemon.app.net.protocols.blocknet.messagequeue.QueueItem; +import io.xlite.daemon.app.net.xrouter.XRouterCommandUtils; +import io.xlite.daemon.app.net.xrouter.XRouterInitialMessagesSentListener; +import io.xlite.daemon.app.net.xrouter.XRouterMessage; +import io.xlite.daemon.app.util.UTXO; +import io.xlite.daemon.app.util.XRouterConfiguration; +import io.xlite.daemon.app.util.background.BackgroundTimerThread; import org.bitcoinj.core.BlockChain; import org.bitcoinj.core.Message; import org.bitcoinj.core.PeerAddress; diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetSeed.java b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetSeed.java similarity index 95% rename from src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetSeed.java rename to src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetSeed.java index f9c72a5..f3f1902 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetSeed.java +++ b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetSeed.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.net.protocols.blocknet; +package io.xlite.daemon.app.net.protocols.blocknet; import java.util.concurrent.TimeUnit; diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetSerializer.java b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetSerializer.java similarity index 98% rename from src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetSerializer.java rename to src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetSerializer.java index ce732c5..b715224 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetSerializer.java +++ b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetSerializer.java @@ -1,8 +1,8 @@ -package io.cloudchains.app.net.protocols.blocknet; +package io.xlite.daemon.app.net.protocols.blocknet; import com.subgraph.orchid.encoders.Hex; -import io.cloudchains.app.net.protocols.blocknet.messages.VersionMessageImpl; -import io.cloudchains.app.net.xrouter.XRouterMessage; +import io.xlite.daemon.app.net.protocols.blocknet.messages.VersionMessageImpl; +import io.xlite.daemon.app.net.xrouter.XRouterMessage; import org.bitcoinj.core.*; import java.io.IOException; diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetTestnet5NetworkParameters.java b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetTestnet5NetworkParameters.java similarity index 95% rename from src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetTestnet5NetworkParameters.java rename to src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetTestnet5NetworkParameters.java index 864e6af..5827adc 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetTestnet5NetworkParameters.java +++ b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetTestnet5NetworkParameters.java @@ -1,8 +1,8 @@ -package io.cloudchains.app.net.protocols.blocknet; +package io.xlite.daemon.app.net.protocols.blocknet; import com.subgraph.orchid.encoders.Hex; -import io.cloudchains.app.net.HasFeeParams; -import io.cloudchains.app.net.xrouter.XRouterMessageSerializer; +import io.xlite.daemon.app.net.HasFeeParams; +import io.xlite.daemon.app.net.xrouter.XRouterMessageSerializer; import org.bitcoinj.core.*; import org.bitcoinj.store.BlockStore; import org.bitcoinj.utils.MonetaryFormat; diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetUtils.java b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetUtils.java similarity index 96% rename from src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetUtils.java rename to src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetUtils.java index 47d2fe9..f422bfd 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/BlocknetUtils.java +++ b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetUtils.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.net.protocols.blocknet; +package io.xlite.daemon.app.net.protocols.blocknet; import org.bitcoinj.core.Sha256Hash; import org.bitcoinj.core.Utils; diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetOnBlocksDownloadedEventListener.java b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/listeners/BlocknetOnBlocksDownloadedEventListener.java similarity index 65% rename from src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetOnBlocksDownloadedEventListener.java rename to src/main/java/io/xlite/daemon/app/net/protocols/blocknet/listeners/BlocknetOnBlocksDownloadedEventListener.java index 91611ef..445a9a6 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetOnBlocksDownloadedEventListener.java +++ b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/listeners/BlocknetOnBlocksDownloadedEventListener.java @@ -1,6 +1,6 @@ -package io.cloudchains.app.net.protocols.blocknet.listeners; +package io.xlite.daemon.app.net.protocols.blocknet.listeners; -import io.cloudchains.app.net.protocols.blocknet.BlocknetPeer; +import io.xlite.daemon.app.net.protocols.blocknet.BlocknetPeer; import org.bitcoinj.core.Block; import org.bitcoinj.core.FilteredBlock; diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetOnXRouterMessageReceivedListener.java b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/listeners/BlocknetOnXRouterMessageReceivedListener.java similarity index 56% rename from src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetOnXRouterMessageReceivedListener.java rename to src/main/java/io/xlite/daemon/app/net/protocols/blocknet/listeners/BlocknetOnXRouterMessageReceivedListener.java index 973549e..36a9145 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetOnXRouterMessageReceivedListener.java +++ b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/listeners/BlocknetOnXRouterMessageReceivedListener.java @@ -1,6 +1,6 @@ -package io.cloudchains.app.net.protocols.blocknet.listeners; +package io.xlite.daemon.app.net.protocols.blocknet.listeners; -import io.cloudchains.app.net.xrouter.XRouterMessage; +import io.xlite.daemon.app.net.xrouter.XRouterMessage; public interface BlocknetOnXRouterMessageReceivedListener { diff --git a/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/listeners/BlocknetPeerConnectedEventListener.java b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/listeners/BlocknetPeerConnectedEventListener.java new file mode 100644 index 0000000..74aec26 --- /dev/null +++ b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/listeners/BlocknetPeerConnectedEventListener.java @@ -0,0 +1,8 @@ +package io.xlite.daemon.app.net.protocols.blocknet.listeners; + +import io.xlite.daemon.app.net.protocols.blocknet.BlocknetPeer; + +public interface BlocknetPeerConnectedEventListener { + + void onPeerConnected(BlocknetPeer peer, int peerCount); +} diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetPeerDisconnectedEventListener.java b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/listeners/BlocknetPeerDisconnectedEventListener.java similarity index 50% rename from src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetPeerDisconnectedEventListener.java rename to src/main/java/io/xlite/daemon/app/net/protocols/blocknet/listeners/BlocknetPeerDisconnectedEventListener.java index 4736a0e..ab6454c 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetPeerDisconnectedEventListener.java +++ b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/listeners/BlocknetPeerDisconnectedEventListener.java @@ -1,6 +1,6 @@ -package io.cloudchains.app.net.protocols.blocknet.listeners; +package io.xlite.daemon.app.net.protocols.blocknet.listeners; -import io.cloudchains.app.net.protocols.blocknet.BlocknetPeer; +import io.xlite.daemon.app.net.protocols.blocknet.BlocknetPeer; public interface BlocknetPeerDisconnectedEventListener { diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetPreMessageReceivedEventListener.java b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/listeners/BlocknetPreMessageReceivedEventListener.java similarity index 57% rename from src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetPreMessageReceivedEventListener.java rename to src/main/java/io/xlite/daemon/app/net/protocols/blocknet/listeners/BlocknetPreMessageReceivedEventListener.java index 7ba433b..ffa400d 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/listeners/BlocknetPreMessageReceivedEventListener.java +++ b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/listeners/BlocknetPreMessageReceivedEventListener.java @@ -1,6 +1,6 @@ -package io.cloudchains.app.net.protocols.blocknet.listeners; +package io.xlite.daemon.app.net.protocols.blocknet.listeners; -import io.cloudchains.app.net.protocols.blocknet.BlocknetPeer; +import io.xlite.daemon.app.net.protocols.blocknet.BlocknetPeer; import org.bitcoinj.core.Message; public interface BlocknetPreMessageReceivedEventListener { diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/messagequeue/MessageSource.java b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/messagequeue/MessageSource.java similarity index 50% rename from src/main/java/io/cloudchains/app/net/protocols/blocknet/messagequeue/MessageSource.java rename to src/main/java/io/xlite/daemon/app/net/protocols/blocknet/messagequeue/MessageSource.java index df4bb4f..1461f93 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/messagequeue/MessageSource.java +++ b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/messagequeue/MessageSource.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.net.protocols.blocknet.messagequeue; +package io.xlite.daemon.app.net.protocols.blocknet.messagequeue; public enum MessageSource { SOURCE_GUI, diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/messagequeue/QueueItem.java b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/messagequeue/QueueItem.java similarity index 93% rename from src/main/java/io/cloudchains/app/net/protocols/blocknet/messagequeue/QueueItem.java rename to src/main/java/io/xlite/daemon/app/net/protocols/blocknet/messagequeue/QueueItem.java index 16a55c5..76807c2 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/messagequeue/QueueItem.java +++ b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/messagequeue/QueueItem.java @@ -1,6 +1,6 @@ -package io.cloudchains.app.net.protocols.blocknet.messagequeue; +package io.xlite.daemon.app.net.protocols.blocknet.messagequeue; -import io.cloudchains.app.net.protocols.blocknet.BlocknetPeer; +import io.xlite.daemon.app.net.protocols.blocknet.BlocknetPeer; import java.util.HashMap; diff --git a/src/main/java/io/cloudchains/app/net/protocols/blocknet/messages/VersionMessageImpl.java b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/messages/VersionMessageImpl.java similarity index 81% rename from src/main/java/io/cloudchains/app/net/protocols/blocknet/messages/VersionMessageImpl.java rename to src/main/java/io/xlite/daemon/app/net/protocols/blocknet/messages/VersionMessageImpl.java index 79f576f..abeb527 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/blocknet/messages/VersionMessageImpl.java +++ b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/messages/VersionMessageImpl.java @@ -1,6 +1,6 @@ -package io.cloudchains.app.net.protocols.blocknet.messages; +package io.xlite.daemon.app.net.protocols.blocknet.messages; -import io.cloudchains.app.Version; +import io.xlite.daemon.app.Version; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.ProtocolException; import org.bitcoinj.core.VersionMessage; diff --git a/src/main/java/io/cloudchains/app/net/protocols/dashcoin/DashcoinNetworkParametersLegacy.java b/src/main/java/io/xlite/daemon/app/net/protocols/dashcoin/DashcoinNetworkParametersLegacy.java similarity index 95% rename from src/main/java/io/cloudchains/app/net/protocols/dashcoin/DashcoinNetworkParametersLegacy.java rename to src/main/java/io/xlite/daemon/app/net/protocols/dashcoin/DashcoinNetworkParametersLegacy.java index e664deb..687712f 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/dashcoin/DashcoinNetworkParametersLegacy.java +++ b/src/main/java/io/xlite/daemon/app/net/protocols/dashcoin/DashcoinNetworkParametersLegacy.java @@ -1,6 +1,6 @@ -package io.cloudchains.app.net.protocols.dashcoin; +package io.xlite.daemon.app.net.protocols.dashcoin; -import io.cloudchains.app.net.HasFeeParams; +import io.xlite.daemon.app.net.HasFeeParams; import org.bitcoinj.core.*; import org.bitcoinj.store.BlockStore; import org.bitcoinj.store.BlockStoreException; diff --git a/src/main/java/io/cloudchains/app/net/protocols/digibyte/DigibyteNetworkParametersLegacy.java b/src/main/java/io/xlite/daemon/app/net/protocols/digibyte/DigibyteNetworkParametersLegacy.java similarity index 95% rename from src/main/java/io/cloudchains/app/net/protocols/digibyte/DigibyteNetworkParametersLegacy.java rename to src/main/java/io/xlite/daemon/app/net/protocols/digibyte/DigibyteNetworkParametersLegacy.java index 7cc7163..db91e39 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/digibyte/DigibyteNetworkParametersLegacy.java +++ b/src/main/java/io/xlite/daemon/app/net/protocols/digibyte/DigibyteNetworkParametersLegacy.java @@ -1,6 +1,6 @@ -package io.cloudchains.app.net.protocols.digibyte; +package io.xlite.daemon.app.net.protocols.digibyte; -import io.cloudchains.app.net.HasFeeParams; +import io.xlite.daemon.app.net.HasFeeParams; import org.bitcoinj.core.*; import org.bitcoinj.store.BlockStore; import org.bitcoinj.store.BlockStoreException; diff --git a/src/main/java/io/cloudchains/app/net/protocols/dogecoin/DogecoinNetworkParametersLegacy.java b/src/main/java/io/xlite/daemon/app/net/protocols/dogecoin/DogecoinNetworkParametersLegacy.java similarity index 95% rename from src/main/java/io/cloudchains/app/net/protocols/dogecoin/DogecoinNetworkParametersLegacy.java rename to src/main/java/io/xlite/daemon/app/net/protocols/dogecoin/DogecoinNetworkParametersLegacy.java index 0c13022..e90aed0 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/dogecoin/DogecoinNetworkParametersLegacy.java +++ b/src/main/java/io/xlite/daemon/app/net/protocols/dogecoin/DogecoinNetworkParametersLegacy.java @@ -1,6 +1,6 @@ -package io.cloudchains.app.net.protocols.dogecoin; +package io.xlite.daemon.app.net.protocols.dogecoin; -import io.cloudchains.app.net.HasFeeParams; +import io.xlite.daemon.app.net.HasFeeParams; import org.bitcoinj.core.*; import org.bitcoinj.store.BlockStore; import org.bitcoinj.store.BlockStoreException; diff --git a/src/main/java/io/cloudchains/app/net/protocols/litecoin/LitecoinNetworkParametersLegacy.java b/src/main/java/io/xlite/daemon/app/net/protocols/litecoin/LitecoinNetworkParametersLegacy.java similarity index 95% rename from src/main/java/io/cloudchains/app/net/protocols/litecoin/LitecoinNetworkParametersLegacy.java rename to src/main/java/io/xlite/daemon/app/net/protocols/litecoin/LitecoinNetworkParametersLegacy.java index c34df5d..55fbb45 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/litecoin/LitecoinNetworkParametersLegacy.java +++ b/src/main/java/io/xlite/daemon/app/net/protocols/litecoin/LitecoinNetworkParametersLegacy.java @@ -1,6 +1,6 @@ -package io.cloudchains.app.net.protocols.litecoin; +package io.xlite.daemon.app.net.protocols.litecoin; -import io.cloudchains.app.net.HasFeeParams; +import io.xlite.daemon.app.net.HasFeeParams; import org.bitcoinj.core.*; import org.bitcoinj.store.BlockStore; import org.bitcoinj.store.BlockStoreException; diff --git a/src/main/java/io/cloudchains/app/net/protocols/pivx/PivxNetworkParametersLegacy.java b/src/main/java/io/xlite/daemon/app/net/protocols/pivx/PivxNetworkParametersLegacy.java similarity index 95% rename from src/main/java/io/cloudchains/app/net/protocols/pivx/PivxNetworkParametersLegacy.java rename to src/main/java/io/xlite/daemon/app/net/protocols/pivx/PivxNetworkParametersLegacy.java index fb9bf73..b861ac3 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/pivx/PivxNetworkParametersLegacy.java +++ b/src/main/java/io/xlite/daemon/app/net/protocols/pivx/PivxNetworkParametersLegacy.java @@ -1,6 +1,6 @@ -package io.cloudchains.app.net.protocols.pivx; +package io.xlite.daemon.app.net.protocols.pivx; -import io.cloudchains.app.net.HasFeeParams; +import io.xlite.daemon.app.net.HasFeeParams; import org.bitcoinj.core.*; import org.bitcoinj.store.BlockStore; import org.bitcoinj.store.BlockStoreException; diff --git a/src/main/java/io/cloudchains/app/net/protocols/pocketcoin/PocketcoinNetworkParametersLegacy.java b/src/main/java/io/xlite/daemon/app/net/protocols/pocketcoin/PocketcoinNetworkParametersLegacy.java similarity index 95% rename from src/main/java/io/cloudchains/app/net/protocols/pocketcoin/PocketcoinNetworkParametersLegacy.java rename to src/main/java/io/xlite/daemon/app/net/protocols/pocketcoin/PocketcoinNetworkParametersLegacy.java index 55ab08c..98d37f9 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/pocketcoin/PocketcoinNetworkParametersLegacy.java +++ b/src/main/java/io/xlite/daemon/app/net/protocols/pocketcoin/PocketcoinNetworkParametersLegacy.java @@ -1,6 +1,6 @@ -package io.cloudchains.app.net.protocols.pocketcoin; +package io.xlite.daemon.app.net.protocols.pocketcoin; -import io.cloudchains.app.net.HasFeeParams; +import io.xlite.daemon.app.net.HasFeeParams; import org.bitcoinj.core.*; import org.bitcoinj.store.BlockStore; import org.bitcoinj.store.BlockStoreException; diff --git a/src/main/java/io/cloudchains/app/net/protocols/ravencoin/RavencoinNetworkParametersLegacy.java b/src/main/java/io/xlite/daemon/app/net/protocols/ravencoin/RavencoinNetworkParametersLegacy.java similarity index 95% rename from src/main/java/io/cloudchains/app/net/protocols/ravencoin/RavencoinNetworkParametersLegacy.java rename to src/main/java/io/xlite/daemon/app/net/protocols/ravencoin/RavencoinNetworkParametersLegacy.java index 1179014..241e581 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/ravencoin/RavencoinNetworkParametersLegacy.java +++ b/src/main/java/io/xlite/daemon/app/net/protocols/ravencoin/RavencoinNetworkParametersLegacy.java @@ -1,6 +1,6 @@ -package io.cloudchains.app.net.protocols.ravencoin; +package io.xlite.daemon.app.net.protocols.ravencoin; -import io.cloudchains.app.net.HasFeeParams; +import io.xlite.daemon.app.net.HasFeeParams; import org.bitcoinj.core.*; import org.bitcoinj.store.BlockStore; import org.bitcoinj.store.BlockStoreException; diff --git a/src/main/java/io/cloudchains/app/net/protocols/syscoin/SyscoinNetworkParametersLegacy.java b/src/main/java/io/xlite/daemon/app/net/protocols/syscoin/SyscoinNetworkParametersLegacy.java similarity index 95% rename from src/main/java/io/cloudchains/app/net/protocols/syscoin/SyscoinNetworkParametersLegacy.java rename to src/main/java/io/xlite/daemon/app/net/protocols/syscoin/SyscoinNetworkParametersLegacy.java index 95d8745..9962b3f 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/syscoin/SyscoinNetworkParametersLegacy.java +++ b/src/main/java/io/xlite/daemon/app/net/protocols/syscoin/SyscoinNetworkParametersLegacy.java @@ -1,6 +1,6 @@ -package io.cloudchains.app.net.protocols.syscoin; +package io.xlite.daemon.app.net.protocols.syscoin; -import io.cloudchains.app.net.HasFeeParams; +import io.xlite.daemon.app.net.HasFeeParams; import org.bitcoinj.core.*; import org.bitcoinj.store.BlockStore; import org.bitcoinj.store.BlockStoreException; diff --git a/src/main/java/io/cloudchains/app/net/protocols/unobtanium/UnobtaniumNetworkParametersLegacy.java b/src/main/java/io/xlite/daemon/app/net/protocols/unobtanium/UnobtaniumNetworkParametersLegacy.java similarity index 95% rename from src/main/java/io/cloudchains/app/net/protocols/unobtanium/UnobtaniumNetworkParametersLegacy.java rename to src/main/java/io/xlite/daemon/app/net/protocols/unobtanium/UnobtaniumNetworkParametersLegacy.java index 91ab422..e77ad0a 100644 --- a/src/main/java/io/cloudchains/app/net/protocols/unobtanium/UnobtaniumNetworkParametersLegacy.java +++ b/src/main/java/io/xlite/daemon/app/net/protocols/unobtanium/UnobtaniumNetworkParametersLegacy.java @@ -1,6 +1,6 @@ -package io.cloudchains.app.net.protocols.unobtanium; +package io.xlite.daemon.app.net.protocols.unobtanium; -import io.cloudchains.app.net.HasFeeParams; +import io.xlite.daemon.app.net.HasFeeParams; import org.bitcoinj.core.*; import org.bitcoinj.store.BlockStore; import org.bitcoinj.store.BlockStoreException; diff --git a/src/main/java/io/cloudchains/app/net/xrouter/XRouterCommandUtils.java b/src/main/java/io/xlite/daemon/app/net/xrouter/XRouterCommandUtils.java similarity index 97% rename from src/main/java/io/cloudchains/app/net/xrouter/XRouterCommandUtils.java rename to src/main/java/io/xlite/daemon/app/net/xrouter/XRouterCommandUtils.java index 6d13a8e..573100f 100644 --- a/src/main/java/io/cloudchains/app/net/xrouter/XRouterCommandUtils.java +++ b/src/main/java/io/xlite/daemon/app/net/xrouter/XRouterCommandUtils.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.net.xrouter; +package io.xlite.daemon.app.net.xrouter; import com.google.common.collect.HashBiMap; diff --git a/src/main/java/io/cloudchains/app/net/xrouter/XRouterFeeUtils.java b/src/main/java/io/xlite/daemon/app/net/xrouter/XRouterFeeUtils.java similarity index 95% rename from src/main/java/io/cloudchains/app/net/xrouter/XRouterFeeUtils.java rename to src/main/java/io/xlite/daemon/app/net/xrouter/XRouterFeeUtils.java index c23ac05..1175ac7 100644 --- a/src/main/java/io/cloudchains/app/net/xrouter/XRouterFeeUtils.java +++ b/src/main/java/io/xlite/daemon/app/net/xrouter/XRouterFeeUtils.java @@ -1,11 +1,11 @@ -package io.cloudchains.app.net.xrouter; +package io.xlite.daemon.app.net.xrouter; import com.google.common.base.Preconditions; import com.subgraph.orchid.encoders.Hex; -import io.cloudchains.app.net.CoinInstance; -import io.cloudchains.app.net.protocols.blocknet.BlocknetPeer; -import io.cloudchains.app.util.XRouterConfiguration; -import io.cloudchains.app.wallet.WalletHelper; +import io.xlite.daemon.app.net.CoinInstance; +import io.xlite.daemon.app.net.protocols.blocknet.BlocknetPeer; +import io.xlite.daemon.app.util.XRouterConfiguration; +import io.xlite.daemon.app.wallet.WalletHelper; import org.bitcoinj.core.*; import java.util.ArrayList; diff --git a/src/main/java/io/cloudchains/app/net/xrouter/XRouterInitialMessagesSentListener.java b/src/main/java/io/xlite/daemon/app/net/xrouter/XRouterInitialMessagesSentListener.java similarity index 56% rename from src/main/java/io/cloudchains/app/net/xrouter/XRouterInitialMessagesSentListener.java rename to src/main/java/io/xlite/daemon/app/net/xrouter/XRouterInitialMessagesSentListener.java index c34a462..dc1109e 100644 --- a/src/main/java/io/cloudchains/app/net/xrouter/XRouterInitialMessagesSentListener.java +++ b/src/main/java/io/xlite/daemon/app/net/xrouter/XRouterInitialMessagesSentListener.java @@ -1,6 +1,6 @@ -package io.cloudchains.app.net.xrouter; +package io.xlite.daemon.app.net.xrouter; -import io.cloudchains.app.net.CoinInstance; +import io.xlite.daemon.app.net.CoinInstance; public interface XRouterInitialMessagesSentListener { diff --git a/src/main/java/io/cloudchains/app/net/xrouter/XRouterMessage.java b/src/main/java/io/xlite/daemon/app/net/xrouter/XRouterMessage.java similarity index 98% rename from src/main/java/io/cloudchains/app/net/xrouter/XRouterMessage.java rename to src/main/java/io/xlite/daemon/app/net/xrouter/XRouterMessage.java index 6e0b19e..6ce2b95 100644 --- a/src/main/java/io/cloudchains/app/net/xrouter/XRouterMessage.java +++ b/src/main/java/io/xlite/daemon/app/net/xrouter/XRouterMessage.java @@ -1,10 +1,10 @@ -package io.cloudchains.app.net.xrouter; +package io.xlite.daemon.app.net.xrouter; import com.google.common.base.Preconditions; import com.subgraph.orchid.encoders.Hex; -import io.cloudchains.app.net.protocols.blocknet.BlocknetParameters; -import io.cloudchains.app.net.protocols.blocknet.BlocknetPeer; -import io.cloudchains.app.util.XRouterConfiguration; +import io.xlite.daemon.app.net.protocols.blocknet.BlocknetParameters; +import io.xlite.daemon.app.net.protocols.blocknet.BlocknetPeer; +import io.xlite.daemon.app.util.XRouterConfiguration; import org.bitcoinj.core.Message; import org.bitcoinj.core.ProtocolException; import org.bitcoinj.core.Utils; diff --git a/src/main/java/io/cloudchains/app/net/xrouter/XRouterMessageSerializer.java b/src/main/java/io/xlite/daemon/app/net/xrouter/XRouterMessageSerializer.java similarity index 95% rename from src/main/java/io/cloudchains/app/net/xrouter/XRouterMessageSerializer.java rename to src/main/java/io/xlite/daemon/app/net/xrouter/XRouterMessageSerializer.java index 9fcdebf..d35588d 100644 --- a/src/main/java/io/cloudchains/app/net/xrouter/XRouterMessageSerializer.java +++ b/src/main/java/io/xlite/daemon/app/net/xrouter/XRouterMessageSerializer.java @@ -1,10 +1,10 @@ -package io.cloudchains.app.net.xrouter; +package io.xlite.daemon.app.net.xrouter; import com.google.common.base.Preconditions; import com.subgraph.orchid.encoders.Hex; -import io.cloudchains.app.net.protocols.blocknet.BlocknetPacketHeader; -import io.cloudchains.app.net.protocols.blocknet.BlocknetParameters; -import io.cloudchains.app.net.protocols.blocknet.BlocknetUtils; +import io.xlite.daemon.app.net.protocols.blocknet.BlocknetPacketHeader; +import io.xlite.daemon.app.net.protocols.blocknet.BlocknetParameters; +import io.xlite.daemon.app.net.protocols.blocknet.BlocknetUtils; import org.bitcoinj.core.*; import java.io.IOException; diff --git a/src/main/java/io/cloudchains/app/net/xrouter/XRouterPacketHeader.java b/src/main/java/io/xlite/daemon/app/net/xrouter/XRouterPacketHeader.java similarity index 99% rename from src/main/java/io/cloudchains/app/net/xrouter/XRouterPacketHeader.java rename to src/main/java/io/xlite/daemon/app/net/xrouter/XRouterPacketHeader.java index 6c6b89b..50e699e 100644 --- a/src/main/java/io/cloudchains/app/net/xrouter/XRouterPacketHeader.java +++ b/src/main/java/io/xlite/daemon/app/net/xrouter/XRouterPacketHeader.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.net.xrouter; +package io.xlite.daemon.app.net.xrouter; import com.subgraph.orchid.encoders.Hex; import org.bitcoinj.core.Utils; diff --git a/src/main/java/io/cloudchains/app/net/xrouter/XRouterPacketManager.java b/src/main/java/io/xlite/daemon/app/net/xrouter/XRouterPacketManager.java similarity index 98% rename from src/main/java/io/cloudchains/app/net/xrouter/XRouterPacketManager.java rename to src/main/java/io/xlite/daemon/app/net/xrouter/XRouterPacketManager.java index f1039bf..5039b66 100644 --- a/src/main/java/io/cloudchains/app/net/xrouter/XRouterPacketManager.java +++ b/src/main/java/io/xlite/daemon/app/net/xrouter/XRouterPacketManager.java @@ -1,9 +1,9 @@ -package io.cloudchains.app.net.xrouter; +package io.xlite.daemon.app.net.xrouter; import com.google.common.base.Preconditions; import com.subgraph.orchid.encoders.Hex; -import io.cloudchains.app.net.protocols.blocknet.BlocknetParameters; -import io.cloudchains.app.net.protocols.blocknet.BlocknetPeer; +import io.xlite.daemon.app.net.protocols.blocknet.BlocknetParameters; +import io.xlite.daemon.app.net.protocols.blocknet.BlocknetPeer; import org.bitcoinj.core.ECKey; import org.bitcoinj.core.Sha256Hash; import org.bitcoinj.core.Utils; diff --git a/src/main/java/io/cloudchains/app/util/AddressBalance.java b/src/main/java/io/xlite/daemon/app/util/AddressBalance.java similarity index 98% rename from src/main/java/io/cloudchains/app/util/AddressBalance.java rename to src/main/java/io/xlite/daemon/app/util/AddressBalance.java index 3d8361f..a567d20 100644 --- a/src/main/java/io/cloudchains/app/util/AddressBalance.java +++ b/src/main/java/io/xlite/daemon/app/util/AddressBalance.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.util; +package io.xlite.daemon.app.util; import com.google.common.base.Preconditions; import com.google.common.util.concurrent.AtomicDouble; diff --git a/src/main/java/io/cloudchains/app/util/AddressDiscoveryService.java b/src/main/java/io/xlite/daemon/app/util/AddressDiscoveryService.java similarity index 98% rename from src/main/java/io/cloudchains/app/util/AddressDiscoveryService.java rename to src/main/java/io/xlite/daemon/app/util/AddressDiscoveryService.java index 17b6cb9..1b18e95 100644 --- a/src/main/java/io/cloudchains/app/util/AddressDiscoveryService.java +++ b/src/main/java/io/xlite/daemon/app/util/AddressDiscoveryService.java @@ -1,12 +1,12 @@ -package io.cloudchains.app.util; +package io.xlite.daemon.app.util; import com.google.common.collect.ImmutableList; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import io.cloudchains.app.net.CoinInstance; -import io.cloudchains.app.net.CoinTickerUtils; -import io.cloudchains.app.net.api.http.client.HTTPClient; +import io.xlite.daemon.app.net.CoinInstance; +import io.xlite.daemon.app.net.CoinTickerUtils; +import io.xlite.daemon.app.net.api.http.client.HTTPClient; import org.bitcoinj.core.DumpedPrivateKey; import org.bitcoinj.core.LegacyAddress; import org.bitcoinj.core.NetworkParameters; diff --git a/src/main/java/io/cloudchains/app/util/CCMath.java b/src/main/java/io/xlite/daemon/app/util/CCMath.java similarity index 83% rename from src/main/java/io/cloudchains/app/util/CCMath.java rename to src/main/java/io/xlite/daemon/app/util/CCMath.java index 64e98b3..1eb5967 100644 --- a/src/main/java/io/cloudchains/app/util/CCMath.java +++ b/src/main/java/io/xlite/daemon/app/util/CCMath.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.util; +package io.xlite.daemon.app.util; public class CCMath { public static double roundToDecimalPlaces(double value, int decimalPlaces) { diff --git a/src/main/java/io/cloudchains/app/util/CloudTransaction.java b/src/main/java/io/xlite/daemon/app/util/CloudTransaction.java similarity index 97% rename from src/main/java/io/cloudchains/app/util/CloudTransaction.java rename to src/main/java/io/xlite/daemon/app/util/CloudTransaction.java index af324e7..d72d0c4 100644 --- a/src/main/java/io/cloudchains/app/util/CloudTransaction.java +++ b/src/main/java/io/xlite/daemon/app/util/CloudTransaction.java @@ -1,6 +1,6 @@ -package io.cloudchains.app.util; +package io.xlite.daemon.app.util; -import io.cloudchains.app.net.CoinInstance; +import io.xlite.daemon.app.net.CoinInstance; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.Transaction; import org.bitcoinj.utils.BtcFormat; diff --git a/src/main/java/io/cloudchains/app/util/ConfigHelper.java b/src/main/java/io/xlite/daemon/app/util/ConfigHelper.java similarity index 99% rename from src/main/java/io/cloudchains/app/util/ConfigHelper.java rename to src/main/java/io/xlite/daemon/app/util/ConfigHelper.java index 97dfe5f..09f0010 100644 --- a/src/main/java/io/cloudchains/app/util/ConfigHelper.java +++ b/src/main/java/io/xlite/daemon/app/util/ConfigHelper.java @@ -1,7 +1,7 @@ -package io.cloudchains.app.util; +package io.xlite.daemon.app.util; import com.google.common.base.Preconditions; -import io.cloudchains.app.App; +import io.xlite.daemon.app.App; import org.json.JSONObject; import java.io.File; diff --git a/src/main/java/io/cloudchains/app/util/ConsoleFormatter.java b/src/main/java/io/xlite/daemon/app/util/ConsoleFormatter.java similarity index 97% rename from src/main/java/io/cloudchains/app/util/ConsoleFormatter.java rename to src/main/java/io/xlite/daemon/app/util/ConsoleFormatter.java index cd47078..87fd03f 100644 --- a/src/main/java/io/cloudchains/app/util/ConsoleFormatter.java +++ b/src/main/java/io/xlite/daemon/app/util/ConsoleFormatter.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.util; +package io.xlite.daemon.app.util; import java.util.logging.Formatter; import java.util.logging.LogRecord; diff --git a/src/main/java/io/cloudchains/app/util/DetectOS.java b/src/main/java/io/xlite/daemon/app/util/DetectOS.java similarity index 93% rename from src/main/java/io/cloudchains/app/util/DetectOS.java rename to src/main/java/io/xlite/daemon/app/util/DetectOS.java index ea686e9..1325a70 100644 --- a/src/main/java/io/cloudchains/app/util/DetectOS.java +++ b/src/main/java/io/xlite/daemon/app/util/DetectOS.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.util; +package io.xlite.daemon.app.util; public class DetectOS { static public boolean isUnix = false; diff --git a/src/main/java/io/cloudchains/app/util/FileFormatter.java b/src/main/java/io/xlite/daemon/app/util/FileFormatter.java similarity index 93% rename from src/main/java/io/cloudchains/app/util/FileFormatter.java rename to src/main/java/io/xlite/daemon/app/util/FileFormatter.java index 0749ee8..daedc7c 100644 --- a/src/main/java/io/cloudchains/app/util/FileFormatter.java +++ b/src/main/java/io/xlite/daemon/app/util/FileFormatter.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.util; +package io.xlite.daemon.app.util; import java.text.SimpleDateFormat; import java.util.Date; @@ -9,7 +9,7 @@ /** * Custom formatter for file output that produces detailed logs with timestamps and class information. * Format: yyyy-MM-dd HH:mm:ss class.method: message - * Example: 2025-12-12 12:01:51 io.cloudchains.app.console.ConsoleMenu.init: Wallet initialized + * Example: 2025-12-12 12:01:51 io.xlite.daemon.app.console.ConsoleMenu.init: Wallet initialized * * This formatter: * - Uses English locale for consistent date formatting diff --git a/src/main/java/io/cloudchains/app/util/LogRotationManager.java b/src/main/java/io/xlite/daemon/app/util/LogRotationManager.java similarity index 99% rename from src/main/java/io/cloudchains/app/util/LogRotationManager.java rename to src/main/java/io/xlite/daemon/app/util/LogRotationManager.java index 876f79f..0dfcafa 100644 --- a/src/main/java/io/cloudchains/app/util/LogRotationManager.java +++ b/src/main/java/io/xlite/daemon/app/util/LogRotationManager.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.util; +package io.xlite.daemon.app.util; import java.io.File; import java.io.IOException; diff --git a/src/main/java/io/cloudchains/app/util/LogRotationUtil.java b/src/main/java/io/xlite/daemon/app/util/LogRotationUtil.java similarity index 98% rename from src/main/java/io/cloudchains/app/util/LogRotationUtil.java rename to src/main/java/io/xlite/daemon/app/util/LogRotationUtil.java index 1beb25a..d41fa16 100644 --- a/src/main/java/io/cloudchains/app/util/LogRotationUtil.java +++ b/src/main/java/io/xlite/daemon/app/util/LogRotationUtil.java @@ -1,6 +1,6 @@ -package io.cloudchains.app.util; +package io.xlite.daemon.app.util; -import io.cloudchains.app.App; +import io.xlite.daemon.app.App; import java.io.File; import java.util.List; diff --git a/src/main/java/io/cloudchains/app/util/PortCheck.java b/src/main/java/io/xlite/daemon/app/util/PortCheck.java similarity index 92% rename from src/main/java/io/cloudchains/app/util/PortCheck.java rename to src/main/java/io/xlite/daemon/app/util/PortCheck.java index 909ed29..e930367 100644 --- a/src/main/java/io/cloudchains/app/util/PortCheck.java +++ b/src/main/java/io/xlite/daemon/app/util/PortCheck.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.util; +package io.xlite.daemon.app.util; import java.io.IOException; import java.net.DatagramSocket; diff --git a/src/main/java/io/cloudchains/app/util/UTXO.java b/src/main/java/io/xlite/daemon/app/util/UTXO.java similarity index 94% rename from src/main/java/io/cloudchains/app/util/UTXO.java rename to src/main/java/io/xlite/daemon/app/util/UTXO.java index f56a770..68ff20a 100644 --- a/src/main/java/io/cloudchains/app/util/UTXO.java +++ b/src/main/java/io/xlite/daemon/app/util/UTXO.java @@ -1,8 +1,8 @@ -package io.cloudchains.app.util; +package io.xlite.daemon.app.util; import com.google.gson.annotations.SerializedName; -import io.cloudchains.app.net.CoinInstance; -import io.cloudchains.app.net.CoinTicker; +import io.xlite.daemon.app.net.CoinInstance; +import io.xlite.daemon.app.net.CoinTicker; import org.bitcoinj.core.Coin; import org.bitcoinj.core.LegacyAddress; import org.bitcoinj.core.Sha256Hash; diff --git a/src/main/java/io/cloudchains/app/util/Utility.java b/src/main/java/io/xlite/daemon/app/util/Utility.java similarity index 95% rename from src/main/java/io/cloudchains/app/util/Utility.java rename to src/main/java/io/xlite/daemon/app/util/Utility.java index 20ec1cf..96ad249 100644 --- a/src/main/java/io/cloudchains/app/util/Utility.java +++ b/src/main/java/io/xlite/daemon/app/util/Utility.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.util; +package io.xlite.daemon.app.util; import org.bitcoinj.core.AddressFormatException; import org.bitcoinj.core.LegacyAddress; diff --git a/src/main/java/io/cloudchains/app/util/XRouterConfiguration.java b/src/main/java/io/xlite/daemon/app/util/XRouterConfiguration.java similarity index 99% rename from src/main/java/io/cloudchains/app/util/XRouterConfiguration.java rename to src/main/java/io/xlite/daemon/app/util/XRouterConfiguration.java index 4b10252..2a1307b 100644 --- a/src/main/java/io/cloudchains/app/util/XRouterConfiguration.java +++ b/src/main/java/io/xlite/daemon/app/util/XRouterConfiguration.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.util; +package io.xlite.daemon.app.util; import com.google.common.collect.HashBiMap; diff --git a/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java b/src/main/java/io/xlite/daemon/app/util/background/BackgroundTimerThread.java similarity index 95% rename from src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java rename to src/main/java/io/xlite/daemon/app/util/background/BackgroundTimerThread.java index abd8985..b2a0567 100644 --- a/src/main/java/io/cloudchains/app/util/background/BackgroundTimerThread.java +++ b/src/main/java/io/xlite/daemon/app/util/background/BackgroundTimerThread.java @@ -1,13 +1,13 @@ -package io.cloudchains.app.util.background; - -import io.cloudchains.app.App; -import io.cloudchains.app.net.CoinInstance; -import io.cloudchains.app.net.CoinTickerUtils; -import io.cloudchains.app.net.api.http.client.HTTPClient; -import io.cloudchains.app.net.protocols.blocknet.BlocknetPeer; -import io.cloudchains.app.net.protocols.blocknet.BlocknetPeerGroup; -import io.cloudchains.app.util.LogRotationUtil; -import io.cloudchains.app.util.XRouterConfiguration; +package io.xlite.daemon.app.util.background; + +import io.xlite.daemon.app.App; +import io.xlite.daemon.app.net.CoinInstance; +import io.xlite.daemon.app.net.CoinTickerUtils; +import io.xlite.daemon.app.net.api.http.client.HTTPClient; +import io.xlite.daemon.app.net.protocols.blocknet.BlocknetPeer; +import io.xlite.daemon.app.net.protocols.blocknet.BlocknetPeerGroup; +import io.xlite.daemon.app.util.LogRotationUtil; +import io.xlite.daemon.app.util.XRouterConfiguration; import java.time.Duration; import java.time.LocalTime; diff --git a/src/main/java/io/cloudchains/app/util/history/Transaction.java b/src/main/java/io/xlite/daemon/app/util/history/Transaction.java similarity index 96% rename from src/main/java/io/cloudchains/app/util/history/Transaction.java rename to src/main/java/io/xlite/daemon/app/util/history/Transaction.java index 8e23177..f5f2351 100644 --- a/src/main/java/io/cloudchains/app/util/history/Transaction.java +++ b/src/main/java/io/xlite/daemon/app/util/history/Transaction.java @@ -1,7 +1,7 @@ -package io.cloudchains.app.util.history; +package io.xlite.daemon.app.util.history; import com.google.gson.annotations.SerializedName; -import io.cloudchains.app.net.CoinTicker; +import io.xlite.daemon.app.net.CoinTicker; import java.util.List; diff --git a/src/main/java/io/cloudchains/app/wallet/WalletHelper.java b/src/main/java/io/xlite/daemon/app/wallet/WalletHelper.java similarity index 97% rename from src/main/java/io/cloudchains/app/wallet/WalletHelper.java rename to src/main/java/io/xlite/daemon/app/wallet/WalletHelper.java index 64496fb..40c65ab 100644 --- a/src/main/java/io/cloudchains/app/wallet/WalletHelper.java +++ b/src/main/java/io/xlite/daemon/app/wallet/WalletHelper.java @@ -1,14 +1,14 @@ -package io.cloudchains.app.wallet; +package io.xlite.daemon.app.wallet; import com.google.common.base.Preconditions; -import io.cloudchains.app.net.CoinInstance; -import io.cloudchains.app.net.CoinTicker; -import io.cloudchains.app.net.CoinTickerUtils; -import io.cloudchains.app.net.HasFeeParams; -import io.cloudchains.app.net.protocols.blocknet.BlocknetPeer; -import io.cloudchains.app.util.AddressBalance; -import io.cloudchains.app.util.CloudTransaction; -import io.cloudchains.app.util.UTXO; +import io.xlite.daemon.app.net.CoinInstance; +import io.xlite.daemon.app.net.CoinTicker; +import io.xlite.daemon.app.net.CoinTickerUtils; +import io.xlite.daemon.app.net.HasFeeParams; +import io.xlite.daemon.app.net.protocols.blocknet.BlocknetPeer; +import io.xlite.daemon.app.util.AddressBalance; +import io.xlite.daemon.app.util.CloudTransaction; +import io.xlite.daemon.app.util.UTXO; import org.bitcoinj.core.*; import org.bitcoinj.crypto.DeterministicKey; import org.bitcoinj.wallet.Wallet; diff --git a/src/test/java/AddressDiscoveryServiceTest.java b/src/test/java/AddressDiscoveryServiceTest.java index 9af4526..1f72f9b 100644 --- a/src/test/java/AddressDiscoveryServiceTest.java +++ b/src/test/java/AddressDiscoveryServiceTest.java @@ -1,9 +1,9 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; -import io.cloudchains.app.net.CoinInstance; -import io.cloudchains.app.net.CoinTicker; -import io.cloudchains.app.net.api.http.client.HTTPClient; -import io.cloudchains.app.util.AddressDiscoveryService; +import io.xlite.daemon.app.net.CoinInstance; +import io.xlite.daemon.app.net.CoinTicker; +import io.xlite.daemon.app.net.api.http.client.HTTPClient; +import io.xlite.daemon.app.util.AddressDiscoveryService; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/src/test/java/CoinInstanceTest.java b/src/test/java/CoinInstanceTest.java index 69aae48..2cfc6d9 100644 --- a/src/test/java/CoinInstanceTest.java +++ b/src/test/java/CoinInstanceTest.java @@ -1,6 +1,6 @@ -import io.cloudchains.app.net.CoinInstance; -import io.cloudchains.app.net.CoinTicker; -import io.cloudchains.app.util.AddressBalance; +import io.xlite.daemon.app.net.CoinInstance; +import io.xlite.daemon.app.net.CoinTicker; +import io.xlite.daemon.app.util.AddressBalance; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/src/test/java/ConfigHelperTest.java b/src/test/java/ConfigHelperTest.java index 2f9e51f..64a9b79 100644 --- a/src/test/java/ConfigHelperTest.java +++ b/src/test/java/ConfigHelperTest.java @@ -1,4 +1,4 @@ -import io.cloudchains.app.util.ConfigHelper; +import io.xlite.daemon.app.util.ConfigHelper; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/src/test/java/KeyHandlerTest.java b/src/test/java/KeyHandlerTest.java index 6c46a7a..07beddf 100644 --- a/src/test/java/KeyHandlerTest.java +++ b/src/test/java/KeyHandlerTest.java @@ -1,5 +1,5 @@ -import io.cloudchains.app.crypto.KeyHandler; -import io.cloudchains.app.util.ConfigHelper; +import io.xlite.daemon.app.crypto.KeyHandler; +import io.xlite.daemon.app.util.ConfigHelper; import org.junit.jupiter.api.*; import org.junit.jupiter.api.io.TempDir; diff --git a/src/test/java/TestHelper.java b/src/test/java/TestHelper.java index cefa4b7..9e21080 100644 --- a/src/test/java/TestHelper.java +++ b/src/test/java/TestHelper.java @@ -2,8 +2,8 @@ import com.google.gson.GsonBuilder; import com.google.gson.JsonObject; import com.google.gson.reflect.TypeToken; -import io.cloudchains.app.net.CoinInstance; -import io.cloudchains.app.util.ConfigHelper; +import io.xlite.daemon.app.net.CoinInstance; +import io.xlite.daemon.app.util.ConfigHelper; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; @@ -83,19 +83,19 @@ public void commonSetup() { // Disable address discovery during tests to prevent interference with deterministic address generation CoinInstance.setAddressDiscoveryEnabled(false); // Ensure coin configs are available — simple in-memory set, no filesystem - if (!io.cloudchains.app.coinconfig.CoinConfigRegistry.isLoaded()) { - java.util.Map cfgs = new java.util.LinkedHashMap<>(); + if (!io.xlite.daemon.app.coinconfig.CoinConfigRegistry.isLoaded()) { + java.util.Map cfgs = new java.util.LinkedHashMap<>(); java.util.Map ltc = new java.util.LinkedHashMap<>(); ltc.put("AddressPrefix", "48"); ltc.put("ScriptPrefix", "50"); ltc.put("SecretPrefix", "176"); ltc.put("COIN", "100000000"); ltc.put("FeePerByte", "10"); ltc.put("MinTxFee", "5000"); ltc.put("Port", "9332"); ltc.put("DustAmount", "0"); ltc.put("Title", "Litecoin"); - cfgs.put("LTC", new io.cloudchains.app.coinconfig.CoinConfig("LTC", "Litecoin", "litecoin--v0.21.1", ltc)); + cfgs.put("LTC", new io.xlite.daemon.app.coinconfig.CoinConfig("LTC", "Litecoin", "litecoin--v0.21.1", ltc)); java.util.Map block = new java.util.LinkedHashMap<>(); block.put("AddressPrefix", "26"); block.put("ScriptPrefix", "28"); block.put("SecretPrefix", "154"); block.put("COIN", "100000000"); block.put("FeePerByte", "20"); block.put("MinTxFee", "10000"); block.put("Port", "41414"); block.put("DustAmount", "0"); block.put("Title", "Blocknet"); - cfgs.put("BLOCK", new io.cloudchains.app.coinconfig.CoinConfig("BLOCK", "Blocknet", "blocknet--v4.2.0", block)); - io.cloudchains.app.coinconfig.CoinConfigRegistry.loadForTest(cfgs); + cfgs.put("BLOCK", new io.xlite.daemon.app.coinconfig.CoinConfig("BLOCK", "Blocknet", "blocknet--v4.2.0", block)); + io.xlite.daemon.app.coinconfig.CoinConfigRegistry.loadForTest(cfgs); } } diff --git a/src/test/java/WalletHelperFeeTest.java b/src/test/java/WalletHelperFeeTest.java index d778fbf..20963ab 100644 --- a/src/test/java/WalletHelperFeeTest.java +++ b/src/test/java/WalletHelperFeeTest.java @@ -1,16 +1,16 @@ -import io.cloudchains.app.net.protocols.bitcoin.BitcoinNetworkParametersLegacy; -import io.cloudchains.app.net.protocols.blocknet.BlocknetNetworkParameters; -import io.cloudchains.app.net.protocols.blocknet.BlocknetTestnet5NetworkParameters; -import io.cloudchains.app.net.protocols.dashcoin.DashcoinNetworkParametersLegacy; -import io.cloudchains.app.net.protocols.digibyte.DigibyteNetworkParametersLegacy; -import io.cloudchains.app.net.protocols.dogecoin.DogecoinNetworkParametersLegacy; -import io.cloudchains.app.net.protocols.litecoin.LitecoinNetworkParametersLegacy; -import io.cloudchains.app.net.protocols.pivx.PivxNetworkParametersLegacy; -import io.cloudchains.app.net.protocols.pocketcoin.PocketcoinNetworkParametersLegacy; -import io.cloudchains.app.net.protocols.ravencoin.RavencoinNetworkParametersLegacy; -import io.cloudchains.app.net.protocols.syscoin.SyscoinNetworkParametersLegacy; -import io.cloudchains.app.net.protocols.unobtanium.UnobtaniumNetworkParametersLegacy; -import io.cloudchains.app.wallet.WalletHelper; +import io.xlite.daemon.app.net.protocols.bitcoin.BitcoinNetworkParametersLegacy; +import io.xlite.daemon.app.net.protocols.blocknet.BlocknetNetworkParameters; +import io.xlite.daemon.app.net.protocols.blocknet.BlocknetTestnet5NetworkParameters; +import io.xlite.daemon.app.net.protocols.dashcoin.DashcoinNetworkParametersLegacy; +import io.xlite.daemon.app.net.protocols.digibyte.DigibyteNetworkParametersLegacy; +import io.xlite.daemon.app.net.protocols.dogecoin.DogecoinNetworkParametersLegacy; +import io.xlite.daemon.app.net.protocols.litecoin.LitecoinNetworkParametersLegacy; +import io.xlite.daemon.app.net.protocols.pivx.PivxNetworkParametersLegacy; +import io.xlite.daemon.app.net.protocols.pocketcoin.PocketcoinNetworkParametersLegacy; +import io.xlite.daemon.app.net.protocols.ravencoin.RavencoinNetworkParametersLegacy; +import io.xlite.daemon.app.net.protocols.syscoin.SyscoinNetworkParametersLegacy; +import io.xlite.daemon.app.net.protocols.unobtanium.UnobtaniumNetworkParametersLegacy; +import io.xlite.daemon.app.wallet.WalletHelper; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.params.TestNet3Params; import org.junit.jupiter.api.AfterAll; @@ -191,17 +191,17 @@ void testGetMinTxFee_BlocknetTestnet5() { @Test void testGenericFeesMatchLoadedConfigs() { // Simple in-memory set — validates logic without filesystem - java.util.Map cfgs = - io.cloudchains.app.coinconfig.CoinConfigRegistry.list(); + java.util.Map cfgs = + io.xlite.daemon.app.coinconfig.CoinConfigRegistry.list(); org.junit.jupiter.api.Assertions.assertFalse(cfgs.isEmpty(), "registry empty — TestHelper must load LTC/BLOCK"); - for (java.util.Map.Entry e : cfgs.entrySet()) { + for (java.util.Map.Entry e : cfgs.entrySet()) { String ticker = e.getKey(); - io.cloudchains.app.coinconfig.CoinConfig cfg = e.getValue(); + io.xlite.daemon.app.coinconfig.CoinConfig cfg = e.getValue(); // Only check migrated tickers that WalletHelper knows via HasFeeParams - if (!io.cloudchains.app.coinconfig.CompiledCoinSupplement.supports(ticker)) + if (!io.xlite.daemon.app.coinconfig.CompiledCoinSupplement.supports(ticker)) continue; - io.cloudchains.app.coinconfig.ConfigurableNetworkParameters generic = - io.cloudchains.app.coinconfig.ConfigurableNetworkParameters.from(cfg); + io.xlite.daemon.app.coinconfig.ConfigurableNetworkParameters generic = + io.xlite.daemon.app.coinconfig.ConfigurableNetworkParameters.from(cfg); assertEquals(cfg.feePerByte(), WalletHelper.getFeePerByte(generic), ticker + " generic feePerByte must equal config"); assertEquals(cfg.minTxFee(), WalletHelper.getMinTxFee(generic), diff --git a/src/test/java/io/cloudchains/app/AppTest.java b/src/test/java/io/xlite/daemon/app/AppTest.java similarity index 99% rename from src/test/java/io/cloudchains/app/AppTest.java rename to src/test/java/io/xlite/daemon/app/AppTest.java index 387fec1..a391333 100644 --- a/src/test/java/io/cloudchains/app/AppTest.java +++ b/src/test/java/io/xlite/daemon/app/AppTest.java @@ -1,4 +1,4 @@ -package io.cloudchains.app; +package io.xlite.daemon.app; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/test/java/io/cloudchains/app/coinconfig/CoinConfigRpcTest.java b/src/test/java/io/xlite/daemon/app/coinconfig/CoinConfigRpcTest.java similarity index 97% rename from src/test/java/io/cloudchains/app/coinconfig/CoinConfigRpcTest.java rename to src/test/java/io/xlite/daemon/app/coinconfig/CoinConfigRpcTest.java index 8d6868c..dbd6d6b 100644 --- a/src/test/java/io/cloudchains/app/coinconfig/CoinConfigRpcTest.java +++ b/src/test/java/io/xlite/daemon/app/coinconfig/CoinConfigRpcTest.java @@ -1,7 +1,7 @@ -package io.cloudchains.app.coinconfig; +package io.xlite.daemon.app.coinconfig; -import io.cloudchains.app.net.api.http.master.HTTPServerHandler; -import io.cloudchains.app.util.ConfigHelper; +import io.xlite.daemon.app.net.api.http.master.HTTPServerHandler; +import io.xlite.daemon.app.util.ConfigHelper; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import org.junit.jupiter.api.*; diff --git a/src/test/java/io/cloudchains/app/coinconfig/CoinConfigSourceLocalTest.java b/src/test/java/io/xlite/daemon/app/coinconfig/CoinConfigSourceLocalTest.java similarity index 99% rename from src/test/java/io/cloudchains/app/coinconfig/CoinConfigSourceLocalTest.java rename to src/test/java/io/xlite/daemon/app/coinconfig/CoinConfigSourceLocalTest.java index 02dad45..c810fbb 100644 --- a/src/test/java/io/cloudchains/app/coinconfig/CoinConfigSourceLocalTest.java +++ b/src/test/java/io/xlite/daemon/app/coinconfig/CoinConfigSourceLocalTest.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.coinconfig; +package io.xlite.daemon.app.coinconfig; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; diff --git a/src/test/java/io/cloudchains/app/coinconfig/CoinConfigTest.java b/src/test/java/io/xlite/daemon/app/coinconfig/CoinConfigTest.java similarity index 98% rename from src/test/java/io/cloudchains/app/coinconfig/CoinConfigTest.java rename to src/test/java/io/xlite/daemon/app/coinconfig/CoinConfigTest.java index 5195665..e784f51 100644 --- a/src/test/java/io/cloudchains/app/coinconfig/CoinConfigTest.java +++ b/src/test/java/io/xlite/daemon/app/coinconfig/CoinConfigTest.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.coinconfig; +package io.xlite.daemon.app.coinconfig; import org.junit.jupiter.api.Test; diff --git a/src/test/java/io/cloudchains/app/coinconfig/ConfigSourceResolverTest.java b/src/test/java/io/xlite/daemon/app/coinconfig/ConfigSourceResolverTest.java similarity index 98% rename from src/test/java/io/cloudchains/app/coinconfig/ConfigSourceResolverTest.java rename to src/test/java/io/xlite/daemon/app/coinconfig/ConfigSourceResolverTest.java index ae8c8c5..d95b9f9 100644 --- a/src/test/java/io/cloudchains/app/coinconfig/ConfigSourceResolverTest.java +++ b/src/test/java/io/xlite/daemon/app/coinconfig/ConfigSourceResolverTest.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.coinconfig; +package io.xlite.daemon.app.coinconfig; import org.junit.jupiter.api.Test; diff --git a/src/test/java/io/cloudchains/app/coinconfig/ConfigurableNetworkParametersCrossCheckTest.java b/src/test/java/io/xlite/daemon/app/coinconfig/ConfigurableNetworkParametersCrossCheckTest.java similarity index 90% rename from src/test/java/io/cloudchains/app/coinconfig/ConfigurableNetworkParametersCrossCheckTest.java rename to src/test/java/io/xlite/daemon/app/coinconfig/ConfigurableNetworkParametersCrossCheckTest.java index a59632b..956b304 100644 --- a/src/test/java/io/cloudchains/app/coinconfig/ConfigurableNetworkParametersCrossCheckTest.java +++ b/src/test/java/io/xlite/daemon/app/coinconfig/ConfigurableNetworkParametersCrossCheckTest.java @@ -1,17 +1,17 @@ -package io.cloudchains.app.coinconfig; - -import io.cloudchains.app.net.protocols.bitcoin.BitcoinNetworkParametersLegacy; -import io.cloudchains.app.net.protocols.bitcoincash.BitcoinCashNetworkParametersLegacy; -import io.cloudchains.app.net.protocols.dashcoin.DashcoinNetworkParametersLegacy; -import io.cloudchains.app.net.protocols.digibyte.DigibyteNetworkParametersLegacy; -import io.cloudchains.app.net.protocols.dogecoin.DogecoinNetworkParametersLegacy; -import io.cloudchains.app.net.protocols.litecoin.LitecoinNetworkParametersLegacy; -import io.cloudchains.app.net.protocols.pivx.PivxNetworkParametersLegacy; -import io.cloudchains.app.net.protocols.pocketcoin.PocketcoinNetworkParametersLegacy; -import io.cloudchains.app.net.protocols.ravencoin.RavencoinNetworkParametersLegacy; -import io.cloudchains.app.net.protocols.syscoin.SyscoinNetworkParametersLegacy; -import io.cloudchains.app.net.protocols.unobtanium.UnobtaniumNetworkParametersLegacy; -import io.cloudchains.app.wallet.WalletHelper; +package io.xlite.daemon.app.coinconfig; + +import io.xlite.daemon.app.net.protocols.bitcoin.BitcoinNetworkParametersLegacy; +import io.xlite.daemon.app.net.protocols.bitcoincash.BitcoinCashNetworkParametersLegacy; +import io.xlite.daemon.app.net.protocols.dashcoin.DashcoinNetworkParametersLegacy; +import io.xlite.daemon.app.net.protocols.digibyte.DigibyteNetworkParametersLegacy; +import io.xlite.daemon.app.net.protocols.dogecoin.DogecoinNetworkParametersLegacy; +import io.xlite.daemon.app.net.protocols.litecoin.LitecoinNetworkParametersLegacy; +import io.xlite.daemon.app.net.protocols.pivx.PivxNetworkParametersLegacy; +import io.xlite.daemon.app.net.protocols.pocketcoin.PocketcoinNetworkParametersLegacy; +import io.xlite.daemon.app.net.protocols.ravencoin.RavencoinNetworkParametersLegacy; +import io.xlite.daemon.app.net.protocols.syscoin.SyscoinNetworkParametersLegacy; +import io.xlite.daemon.app.net.protocols.unobtanium.UnobtaniumNetworkParametersLegacy; +import io.xlite.daemon.app.wallet.WalletHelper; import org.bitcoinj.core.LegacyAddress; import org.bitcoinj.core.NetworkParameters; import org.junit.jupiter.api.Test; diff --git a/src/test/java/io/cloudchains/app/coinconfig/XBridgeConfParserTest.java b/src/test/java/io/xlite/daemon/app/coinconfig/XBridgeConfParserTest.java similarity index 98% rename from src/test/java/io/cloudchains/app/coinconfig/XBridgeConfParserTest.java rename to src/test/java/io/xlite/daemon/app/coinconfig/XBridgeConfParserTest.java index f152c68..ef4d84d 100644 --- a/src/test/java/io/cloudchains/app/coinconfig/XBridgeConfParserTest.java +++ b/src/test/java/io/xlite/daemon/app/coinconfig/XBridgeConfParserTest.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.coinconfig; +package io.xlite.daemon.app.coinconfig; import org.junit.jupiter.api.Test; diff --git a/src/test/java/io/cloudchains/app/net/api/JSONRPCControllerRebindTest.java b/src/test/java/io/xlite/daemon/app/net/api/JSONRPCControllerRebindTest.java similarity index 97% rename from src/test/java/io/cloudchains/app/net/api/JSONRPCControllerRebindTest.java rename to src/test/java/io/xlite/daemon/app/net/api/JSONRPCControllerRebindTest.java index f5ce2f8..b17a152 100644 --- a/src/test/java/io/cloudchains/app/net/api/JSONRPCControllerRebindTest.java +++ b/src/test/java/io/xlite/daemon/app/net/api/JSONRPCControllerRebindTest.java @@ -1,7 +1,7 @@ -package io.cloudchains.app.net.api; +package io.xlite.daemon.app.net.api; -import io.cloudchains.app.net.CoinInstance; -import io.cloudchains.app.util.ConfigHelper; +import io.xlite.daemon.app.net.CoinInstance; +import io.xlite.daemon.app.util.ConfigHelper; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; diff --git a/src/test/java/io/cloudchains/app/net/api/JSONRPCServerPortGateTest.java b/src/test/java/io/xlite/daemon/app/net/api/JSONRPCServerPortGateTest.java similarity index 97% rename from src/test/java/io/cloudchains/app/net/api/JSONRPCServerPortGateTest.java rename to src/test/java/io/xlite/daemon/app/net/api/JSONRPCServerPortGateTest.java index 16121c8..51d216e 100644 --- a/src/test/java/io/cloudchains/app/net/api/JSONRPCServerPortGateTest.java +++ b/src/test/java/io/xlite/daemon/app/net/api/JSONRPCServerPortGateTest.java @@ -1,6 +1,6 @@ -package io.cloudchains.app.net.api; +package io.xlite.daemon.app.net.api; -import io.cloudchains.app.net.CoinInstance; +import io.xlite.daemon.app.net.CoinInstance; import org.junit.jupiter.api.Test; import java.net.InetAddress; diff --git a/src/test/java/io/cloudchains/app/net/api/http/client/EXRResponseNormalizeTest.java b/src/test/java/io/xlite/daemon/app/net/api/http/client/EXRResponseNormalizeTest.java similarity index 98% rename from src/test/java/io/cloudchains/app/net/api/http/client/EXRResponseNormalizeTest.java rename to src/test/java/io/xlite/daemon/app/net/api/http/client/EXRResponseNormalizeTest.java index 37cfdc3..dc3b0f6 100644 --- a/src/test/java/io/cloudchains/app/net/api/http/client/EXRResponseNormalizeTest.java +++ b/src/test/java/io/xlite/daemon/app/net/api/http/client/EXRResponseNormalizeTest.java @@ -1,10 +1,10 @@ -package io.cloudchains.app.net.api.http.client; +package io.xlite.daemon.app.net.api.http.client; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; -import io.cloudchains.app.net.CoinTicker; +import io.xlite.daemon.app.net.CoinTicker; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; diff --git a/src/test/java/io/cloudchains/app/net/api/http/client/HTTPClientRoutingTest.java b/src/test/java/io/xlite/daemon/app/net/api/http/client/HTTPClientRoutingTest.java similarity index 96% rename from src/test/java/io/cloudchains/app/net/api/http/client/HTTPClientRoutingTest.java rename to src/test/java/io/xlite/daemon/app/net/api/http/client/HTTPClientRoutingTest.java index d685711..b834eac 100644 --- a/src/test/java/io/cloudchains/app/net/api/http/client/HTTPClientRoutingTest.java +++ b/src/test/java/io/xlite/daemon/app/net/api/http/client/HTTPClientRoutingTest.java @@ -1,4 +1,4 @@ -package io.cloudchains.app.net.api.http.client; +package io.xlite.daemon.app.net.api.http.client; import org.junit.jupiter.api.Test; diff --git a/src/test/java/io/cloudchains/app/net/api/http/server/HTTPServerHandlerSignedMessageTest.java b/src/test/java/io/xlite/daemon/app/net/api/http/server/HTTPServerHandlerSignedMessageTest.java similarity index 96% rename from src/test/java/io/cloudchains/app/net/api/http/server/HTTPServerHandlerSignedMessageTest.java rename to src/test/java/io/xlite/daemon/app/net/api/http/server/HTTPServerHandlerSignedMessageTest.java index 853877b..942ef63 100644 --- a/src/test/java/io/cloudchains/app/net/api/http/server/HTTPServerHandlerSignedMessageTest.java +++ b/src/test/java/io/xlite/daemon/app/net/api/http/server/HTTPServerHandlerSignedMessageTest.java @@ -1,6 +1,6 @@ -package io.cloudchains.app.net.api.http.server; +package io.xlite.daemon.app.net.api.http.server; -import io.cloudchains.app.net.CoinTicker; +import io.xlite.daemon.app.net.CoinTicker; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; From 6b449a8034b543035ad2bf431f286d4162e5cd43 Mon Sep 17 00:00:00 2001 From: tryiou Date: Sat, 29 Aug 2026 17:20:44 +0200 Subject: [PATCH 67/73] refactor: rename data directory, env vars and client name to xlite-daemon Use xlite-daemon for filesystem paths (%appdata%/xlite-daemon, ~/.config/xlite-daemon, ~/Library/Application Support/xlite-daemon) with auto-migration from legacy CloudChains directory. Rename environment variables to XLITE_DAEMON_LOG_LEVEL and XLITE_DAEMON_LOG_RETENTION_DAYS and client name to xlite-daemon (wire /xlite-daemon:0.5.15/, breaking change). Update documentation, gitignore and tests. --- .gitignore | 2 +- AGENTS.md | 2 +- README.md | 6 +- docs/USER_GUIDE.md | 32 +++++----- src/main/java/io/xlite/daemon/app/App.java | 4 +- .../java/io/xlite/daemon/app/Version.java | 2 +- .../xlite/daemon/app/console/ConsoleMenu.java | 2 +- .../xlite/daemon/app/util/ConfigHelper.java | 45 +++++++++++++- .../daemon/app/util/LogRotationUtil.java | 4 +- src/test/java/ConfigHelperTest.java | 61 +++++++++++++++++++ src/test/java/KeyHandlerTest.java | 4 +- 11 files changed, 132 insertions(+), 32 deletions(-) diff --git a/.gitignore b/.gitignore index 5a52d09..5e37cd7 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,6 @@ Thumbs.db *.temp *~ -CloudChains/* +xlite-daemon/* .settings .project \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 28046d2..451b558 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -115,7 +115,7 @@ Log messages use bracketed prefixes: `[security]`, `[discovery-BLOCK]`, `[wallet ## Project Structure ``` -src/main/java/io/cloudchains/app/ +src/main/java/io/xlite/daemon/app/ crypto/ KeyHandler (wallet encryption), LoginUtils (auth) net/ CoinInstance (coin lifecycle), JSON-RPC servers, protocols/ util/ ConfigHelper, AddressDiscoveryService, UTXO, logging diff --git a/README.md b/README.md index 8bc09d4..d349308 100644 --- a/README.md +++ b/README.md @@ -143,13 +143,13 @@ xlite-daemon (Backend) Configuration Files: ``` Windows -%appdata%\CloudChains\settings\config-*.json +%appdata%\xlite-daemon\settings\config-*.json MacOS -~/Library/Application Support/CloudChains/settings/config-*.json +~/Library/Application Support/xlite-daemon/settings/config-*.json Linux -~/.config/CloudChains/settings/config-*.json +~/.config/xlite-daemon/settings/config-*.json ``` ## Contributing diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 167c43e..f6f7a68 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -117,9 +117,9 @@ export EXR_ENDPOINT="https://server1.example.com,https://server2.example.com" The daemon stores configuration files in your system's application data directory: -- **Windows**: `%appdata%\CloudChains\settings\config-*.json` -- **macOS**: `~/Library/Application Support/CloudChains/settings/config-*.json` -- **Linux**: `~/.config/CloudChains/settings/config-*.json` +- **Windows**: `%appdata%\xlite-daemon\settings\config-*.json` +- **macOS**: `~/Library/Application Support/xlite-daemon/settings/config-*.json` +- **Linux**: `~/.config/xlite-daemon/settings/config-*.json` ### Configuration Structure @@ -334,10 +334,10 @@ netstat -ano | findstr :9955 # Windows **Solution**: ```bash # Check if wallet file exists -ls ~/.config/CloudChains/key.dat +ls ~/.config/xlite-daemon/key.dat # Verify permissions -chmod 600 ~/.config/CloudChains/key.dat +chmod 600 ~/.config/xlite-daemon/key.dat ``` #### 3. Network Connection Issues @@ -370,7 +370,7 @@ java -Xmx2g -jar xlite-daemon.jar #### Log File Locations -- **Error logs**: `~/.config/CloudChains/error-YYYY-MM-DD.log` +- **Error logs**: `~/.config/xlite-daemon/error-YYYY-MM-DD.log` - **Application logs**: Console output (configurable) #### Common Log Patterns @@ -429,7 +429,7 @@ handlers=java.util.logging.ConsoleHandler 2. **From Backup**: ```bash # Restore from backup directory - cp ~/.config/CloudChains/backups/key-backup-*.dat ~/.config/CloudChains/key.dat + cp ~/.config/xlite-daemon/backups/key-backup-*.dat ~/.config/xlite-daemon/key.dat ``` #### Configuration Recovery @@ -437,7 +437,7 @@ handlers=java.util.logging.ConsoleHandler 1. **Reset Configuration**: ```bash # Remove config files to reset - rm ~/.config/CloudChains/settings/config-*.json + rm ~/.config/xlite-daemon/settings/config-*.json ``` 2. **Rebuild from Source**: @@ -771,9 +771,9 @@ Secure wallet and configuration files: ```bash # Set restrictive permissions -chmod 600 ~/.config/CloudChains/key.dat -chmod 600 ~/.config/CloudChains/settings/config-*.json -chmod 700 ~/.config/CloudChains/ +chmod 600 ~/.config/xlite-daemon/key.dat +chmod 600 ~/.config/xlite-daemon/settings/config-*.json +chmod 700 ~/.config/xlite-daemon/ ``` ### Monitoring and Logging @@ -787,7 +787,7 @@ Configure automatic log rotation: sudo nano /etc/logrotate.d/xlite-daemon # Add configuration -/home/user/.config/CloudChains/error-*.log { +/home/user/.config/xlite-daemon/error-*.log { daily rotate 30 compress @@ -806,7 +806,7 @@ Monitor daemon health: ps aux | grep xlite-daemon # Monitor logs in real-time -tail -f ~/.config/CloudChains/error-*.log +tail -f ~/.config/xlite-daemon/error-*.log # Check network connections netstat -tulpn | grep xlite-daemon @@ -828,10 +828,10 @@ DATE=$(date +%Y%m%d_%H%M%S) mkdir -p "$BACKUP_DIR" # Backup wallet file -cp ~/.config/CloudChains/key.dat "$BACKUP_DIR/key-$DATE.dat" +cp ~/.config/xlite-daemon/key.dat "$BACKUP_DIR/key-$DATE.dat" # Backup configuration -cp ~/.config/CloudChains/settings/config-*.json "$BACKUP_DIR/" +cp ~/.config/xlite-daemon/settings/config-*.json "$BACKUP_DIR/" # Compress backup tar -czf "$BACKUP_DIR/backup-$DATE.tar.gz" -C "$BACKUP_DIR" . @@ -861,7 +861,7 @@ fi pkill xlite-daemon # Backup current data -mv ~/.config/CloudChains ~/.config/CloudChains.backup +mv ~/.config/xlite-daemon ~/.config/xlite-daemon.backup # Extract backup tar -xzf "$BACKUP_FILE" -C ~/.config/ diff --git a/src/main/java/io/xlite/daemon/app/App.java b/src/main/java/io/xlite/daemon/app/App.java index 3affcee..04f43ed 100644 --- a/src/main/java/io/xlite/daemon/app/App.java +++ b/src/main/java/io/xlite/daemon/app/App.java @@ -147,7 +147,7 @@ public static void main(String[] args) { } } - Level logLevel = parseLogLevel(getEnv("CLOUDCHAINS_LOG_LEVEL"), Level.INFO); + Level logLevel = parseLogLevel(getEnv("XLITE_DAEMON_LOG_LEVEL"), Level.INFO); LOGGER.setLevel(logLevel); LOGGER.setUseParentHandlers(false); @@ -157,7 +157,7 @@ public static void main(String[] args) { try { String userHomeDir = getUserConfigDir(); - String logDir = userHomeDir + File.separator + "CloudChains"; + String logDir = userHomeDir + File.separator + "xlite-daemon"; DateTimeFormatter timeStampPattern = DateTimeFormatter.ofPattern("yyyy-MM-dd"); Handler fileHandler = new FileHandler( logDir + File.separator + "error-" + timeStampPattern.format(LocalDateTime.now()) + ".log", diff --git a/src/main/java/io/xlite/daemon/app/Version.java b/src/main/java/io/xlite/daemon/app/Version.java index 07960f6..8db5a2e 100644 --- a/src/main/java/io/xlite/daemon/app/Version.java +++ b/src/main/java/io/xlite/daemon/app/Version.java @@ -1,7 +1,7 @@ package io.xlite.daemon.app; public class Version { - private static final String CLIENT_NAME = "CloudChains"; + private static final String CLIENT_NAME = "xlite-daemon"; private static final String CLIENT_PROTOCOL_VERSION = "0.5.15"; public static final String CLIENT_TYPE = "CloudPeer"; diff --git a/src/main/java/io/xlite/daemon/app/console/ConsoleMenu.java b/src/main/java/io/xlite/daemon/app/console/ConsoleMenu.java index 667480e..1cc20c7 100644 --- a/src/main/java/io/xlite/daemon/app/console/ConsoleMenu.java +++ b/src/main/java/io/xlite/daemon/app/console/ConsoleMenu.java @@ -35,7 +35,7 @@ public class ConsoleMenu { public ConsoleMenu(String[] args) { this.arguments = args; // Do NOT set the logger level here — App owns logging configuration - // (CLOUDCHAINS_LOG_LEVEL); clobbering it in this constructor made all + // (XLITE_DAEMON_LOG_LEVEL); clobbering it in this constructor made all // FINE/FINER diagnostics unreachable. } diff --git a/src/main/java/io/xlite/daemon/app/util/ConfigHelper.java b/src/main/java/io/xlite/daemon/app/util/ConfigHelper.java index 09f0010..24410fa 100644 --- a/src/main/java/io/xlite/daemon/app/util/ConfigHelper.java +++ b/src/main/java/io/xlite/daemon/app/util/ConfigHelper.java @@ -9,6 +9,7 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.StandardCopyOption; import java.util.logging.LogManager; import java.util.logging.Logger; @@ -280,12 +281,50 @@ public static String getLocalDataDirectory() { baseDir = CONFIG_DIR; } - String userHomeDir = baseDir + File.separator + "CloudChains" + File.separator; - File directory = new File(userHomeDir); + String newDirPath = baseDir + File.separator + "xlite-daemon" + File.separator; + String oldDirPath = baseDir + File.separator + "CloudChains" + File.separator; + File newDir = new File(newDirPath); + File oldDir = new File(oldDirPath); + if (!newDir.exists() && oldDir.exists()) { + try { + Files.move(oldDir.toPath(), newDir.toPath()); + LOGGER.warning("[migrate] moved legacy data directory from " + oldDirPath + " to " + newDirPath); + } catch (IOException e) { + LOGGER.warning("[migrate] Files.move failed (" + e.getMessage() + "), falling back to copy for " + oldDirPath + " -> " + newDirPath); + try { + copyDirectoryRecursively(oldDir, newDir); + LOGGER.warning("[migrate] copied legacy data directory from " + oldDirPath + " to " + newDirPath); + } catch (IOException copyEx) { + LOGGER.warning("[migrate] copy fallback failed: " + copyEx.getMessage()); + } + } + } + + File directory = new File(newDirPath); if (!directory.exists()) { directory.mkdirs(); } - return userHomeDir; + return newDirPath; + } + + private static void copyDirectoryRecursively(File source, File target) throws IOException { + if (source.isDirectory()) { + if (!target.exists() && !target.mkdirs()) { + throw new IOException("Failed to create directory " + target); + } + File[] children = source.listFiles(); + if (children != null) { + for (File child : children) { + copyDirectoryRecursively(child, new File(target, child.getName())); + } + } + } else { + File parent = target.getParentFile(); + if (parent != null && !parent.exists()) { + parent.mkdirs(); + } + Files.copy(source.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING); + } } } diff --git a/src/main/java/io/xlite/daemon/app/util/LogRotationUtil.java b/src/main/java/io/xlite/daemon/app/util/LogRotationUtil.java index d41fa16..2c00bbc 100644 --- a/src/main/java/io/xlite/daemon/app/util/LogRotationUtil.java +++ b/src/main/java/io/xlite/daemon/app/util/LogRotationUtil.java @@ -15,7 +15,7 @@ public class LogRotationUtil { private final static LogManager LOGMANAGER = LogManager.getLogManager(); private final static Logger LOGGER = LOGMANAGER.getLogger(Logger.GLOBAL_LOGGER_NAME); private static final int DEFAULT_LOG_RETENTION_DAYS = 30; - private static final String LOG_RETENTION_ENV_VAR = "CLOUDCHAINS_LOG_RETENTION_DAYS"; + private static final String LOG_RETENTION_ENV_VAR = "XLITE_DAEMON_LOG_RETENTION_DAYS"; /** * Performs log rotation cleanup. @@ -24,7 +24,7 @@ public class LogRotationUtil { public static void performLogRotation() { try { String userHomeDir = App.getUserConfigDir(); - String logDirectoryPath = userHomeDir + File.separator + "CloudChains"; + String logDirectoryPath = userHomeDir + File.separator + "xlite-daemon"; // Get retention days from environment variable or use default int retentionDays = getRetentionDaysFromEnvironment(); diff --git a/src/test/java/ConfigHelperTest.java b/src/test/java/ConfigHelperTest.java index 64a9b79..65ef9e4 100644 --- a/src/test/java/ConfigHelperTest.java +++ b/src/test/java/ConfigHelperTest.java @@ -5,7 +5,11 @@ import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.io.TempDir; import static org.junit.jupiter.api.Assertions.*; @@ -203,4 +207,61 @@ void testLoadConfigFromFile() { assertEquals(9000, loadedConfig.getRpcPort()); assertEquals(25, loadedConfig.getAddressCount()); } + + @Test + void testMigration_movesLegacyCloudChainsToXliteDaemon(@TempDir Path tmp) throws IOException { + // Arrange: create legacy CloudChains directory with a settings file + String originalConfigDir = ConfigHelper.CONFIG_DIR; + try { + ConfigHelper.CONFIG_DIR = tmp.toString(); + Path oldDir = tmp.resolve("CloudChains"); + Path newDir = tmp.resolve("xlite-daemon"); + org.junit.jupiter.api.Assertions.assertFalse(Files.exists(newDir), "new dir must not exist before migration"); + Files.createDirectories(oldDir.resolve("settings")); + Path legacyFile = oldDir.resolve("settings").resolve("config-legacy.json"); + String content = "{\"feeperbyte\":1}"; + Files.write(legacyFile, content.getBytes(StandardCharsets.UTF_8)); + + // Act: trigger migration via getLocalDataDirectory + String returned = ConfigHelper.getLocalDataDirectory(); + org.junit.jupiter.api.Assertions.assertTrue(returned.endsWith("xlite-daemon" + File.separator)); + + // Assert: legacy content migrated to new location + Path migratedFile = newDir.resolve("settings").resolve("config-legacy.json"); + org.junit.jupiter.api.Assertions.assertTrue(Files.exists(newDir), "xlite-daemon dir must exist after migration"); + org.junit.jupiter.api.Assertions.assertTrue(Files.exists(migratedFile), "legacy file must be migrated to new dir"); + assertEquals(content, new String(Files.readAllBytes(migratedFile), StandardCharsets.UTF_8)); + org.junit.jupiter.api.Assertions.assertFalse(Files.exists(oldDir), "old CloudChains dir must be moved (not remain)"); + } finally { + ConfigHelper.CONFIG_DIR = originalConfigDir; + // Cleanup temp-data dirs created via CONFIG_DIR override so commonCleanup().deleteDir + // on the default "." path still succeeds; remove any leftover tmp subdirs eagerly. + // JUnit @TempDir will delete tmp itself, but ensure CONFIG_DIR restored. + } + } + + @Test + void testMigration_doesNotOverwriteExistingNewDir(@TempDir Path tmp) throws IOException { + String originalConfigDir = ConfigHelper.CONFIG_DIR; + try { + ConfigHelper.CONFIG_DIR = tmp.toString(); + Path oldDir = tmp.resolve("CloudChains"); + Path newDir = tmp.resolve("xlite-daemon"); + Files.createDirectories(oldDir.resolve("settings")); + Files.createDirectories(newDir.resolve("settings")); + Path oldFile = oldDir.resolve("settings").resolve("config-old.json"); + Path newFile = newDir.resolve("settings").resolve("config-new.json"); + Files.write(oldFile, "{\"a\":1}".getBytes(StandardCharsets.UTF_8)); + Files.write(newFile, "{\"b\":2}".getBytes(StandardCharsets.UTF_8)); + + ConfigHelper.getLocalDataDirectory(); + + // When new dir already exists, migration must NOT run + org.junit.jupiter.api.Assertions.assertTrue(Files.exists(oldDir), "old dir must remain when new dir already exists"); + org.junit.jupiter.api.Assertions.assertTrue(Files.exists(oldFile)); + org.junit.jupiter.api.Assertions.assertTrue(Files.exists(newFile)); + } finally { + ConfigHelper.CONFIG_DIR = originalConfigDir; + } + } } \ No newline at end of file diff --git a/src/test/java/KeyHandlerTest.java b/src/test/java/KeyHandlerTest.java index 07beddf..24f20e6 100644 --- a/src/test/java/KeyHandlerTest.java +++ b/src/test/java/KeyHandlerTest.java @@ -67,8 +67,8 @@ public class KeyHandlerTest { void setUp() { // Point ConfigHelper at the isolated temp directory for every test. ConfigHelper.CONFIG_DIR = tempDir.toString(); - testKeyFile = tempDir.resolve("CloudChains").resolve("key.dat").toFile(); - testBackupDir = tempDir.resolve("CloudChains").resolve("backups").toFile(); + testKeyFile = tempDir.resolve("xlite-daemon").resolve("key.dat").toFile(); + testBackupDir = tempDir.resolve("xlite-daemon").resolve("backups").toFile(); } // ========================================================================= From 433224d902f799a9898b091e4334d9afc317b51f Mon Sep 17 00:00:00 2001 From: tryiou Date: Sat, 12 Sep 2026 19:24:31 +0200 Subject: [PATCH 68/73] fix(wallet): resolve coin network params when building send outputs sendtransaction rejected every non-BTC destination address with 'No network found' because the output builder parsed addresses with null network parameters, which only resolves BTC mainnet and testnet in the global registry. Thread the coin's own network parameters through so LTC/DASH/PIVX version bytes validate correctly. Add WalletHelperSendTest with live mainnet address vectors, P2SH round-trips, and a contract pin on the null-lookup behavior. --- .../xlite/daemon/app/wallet/WalletHelper.java | 12 +- .../app/wallet/WalletHelperSendTest.java | 105 ++++++++++++++++++ 2 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 src/test/java/io/xlite/daemon/app/wallet/WalletHelperSendTest.java diff --git a/src/main/java/io/xlite/daemon/app/wallet/WalletHelper.java b/src/main/java/io/xlite/daemon/app/wallet/WalletHelper.java index 40c65ab..df21753 100644 --- a/src/main/java/io/xlite/daemon/app/wallet/WalletHelper.java +++ b/src/main/java/io/xlite/daemon/app/wallet/WalletHelper.java @@ -261,7 +261,7 @@ public static Transaction createTransactionSimple(CoinTicker coinTicker, String ArrayList selectedUtxos = new ArrayList<>(); ArrayList outputs = new ArrayList<>(); - outputs.add(createTransactionOutput(coinTicker, address, amount)); + outputs.add(createTransactionOutput(coinTicker, address, amount, params)); long estimatedFeeSats = Math.max(feePerByte * (192 + 34), minTxFee); double estimatedFee = (double) estimatedFeeSats / coinUnit; @@ -363,8 +363,14 @@ private static boolean isDust(double amount, NetworkParameters params) { return amount * Coin.COIN.value < params.getMinNonDustOutput().value; } - private static UTXO createTransactionOutput(CoinTicker ticker, String address, double amount) { - LegacyAddress addr = LegacyAddress.fromBase58(null, address); + // Package-private for testing. The params must be the coin's own network + // parameters: a null lookup only resolves BTC/testnet version bytes in + // bitcoinj's global registry and rejects every altcoin address. + static UTXO createTransactionOutput(CoinTicker ticker, String address, double amount, NetworkParameters params) { + // Validates the address against this coin's version bytes; throws + // AddressFormatException on mismatch. Do not remove: a null-params + // lookup here rejects every non-BTC address ("No network found"). + LegacyAddress.fromBase58(params, address); Coin coin = Coin.valueOf((long) (amount * Coin.COIN.value)); return new UTXO(ticker, address, "", 0, 0, coin.value); } diff --git a/src/test/java/io/xlite/daemon/app/wallet/WalletHelperSendTest.java b/src/test/java/io/xlite/daemon/app/wallet/WalletHelperSendTest.java new file mode 100644 index 0000000..bbc99c4 --- /dev/null +++ b/src/test/java/io/xlite/daemon/app/wallet/WalletHelperSendTest.java @@ -0,0 +1,105 @@ +package io.xlite.daemon.app.wallet; + +import io.xlite.daemon.app.net.CoinTicker; +import io.xlite.daemon.app.net.protocols.blocknet.BlocknetNetworkParameters; +import io.xlite.daemon.app.net.protocols.dashcoin.DashcoinNetworkParametersLegacy; +import io.xlite.daemon.app.net.protocols.litecoin.LitecoinNetworkParametersLegacy; +import io.xlite.daemon.app.net.protocols.pivx.PivxNetworkParametersLegacy; +import io.xlite.daemon.app.util.UTXO; +import org.bitcoinj.core.AddressFormatException; +import org.bitcoinj.core.LegacyAddress; +import org.bitcoinj.core.NetworkParameters; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Regression tests for the {@code sendtransaction} "No network found" failure. + * + *

Live daemon rejected valid external LTC/DASH/PIVX destination addresses + * because the send path parsed them with {@code LegacyAddress.fromBase58(null, …)}, + * which only resolves networks in bitcoinj's global registry (BTC mainnet + + * testnet). The vectors below are real mainnet addresses produced by live + * full-node wallets. + */ +class WalletHelperSendTest { + + // Real mainnet P2PKH addresses issued by live full-node wallets. + private static final String LTC_P2PKH = "LWzaDk84d3N1pTL6rN14GCcLXxEjQQ45Lx"; + private static final String DASH_P2PKH = "XuqKo24hcwbnSikBDEdHw8sdk3SbYWkMFq"; + private static final String PIVX_P2PKH = "D5rFeo85qYnCYQyYfn9L3k2No9eCizHECq"; + + /** + * Pins the bitcoinj contract behind the original failure: a null-params + * lookup cannot resolve non-BTC version bytes. Documents why the old + * call was wrong; passes independently of the fix. + */ + @Test + void testNullParamsLookupRejectsAltcoinAddresses() { + assertThrows(AddressFormatException.class, + () -> LegacyAddress.fromBase58(null, LTC_P2PKH)); + assertThrows(AddressFormatException.class, + () -> LegacyAddress.fromBase58(null, DASH_P2PKH)); + assertThrows(AddressFormatException.class, + () -> LegacyAddress.fromBase58(null, PIVX_P2PKH)); + } + + @Test + void testCreateTransactionOutput_LitecoinP2PKH() { + NetworkParameters params = new LitecoinNetworkParametersLegacy(); + UTXO out = WalletHelper.createTransactionOutput( + CoinTicker.LITECOIN, LTC_P2PKH, 0.005, params); + assertEquals(LTC_P2PKH, out.getAddress()); + assertEquals(500_000L, out.getValue()); + } + + @Test + void testCreateTransactionOutput_DashP2PKH() { + NetworkParameters params = new DashcoinNetworkParametersLegacy(); + UTXO out = WalletHelper.createTransactionOutput( + CoinTicker.DASHCOIN, DASH_P2PKH, 0.005, params); + assertEquals(DASH_P2PKH, out.getAddress()); + assertEquals(500_000L, out.getValue()); + } + + @Test + void testCreateTransactionOutput_PivxP2PKH() { + NetworkParameters params = new PivxNetworkParametersLegacy(); + UTXO out = WalletHelper.createTransactionOutput( + CoinTicker.PIVX, PIVX_P2PKH, 0.2, params); + assertEquals(PIVX_P2PKH, out.getAddress()); + assertEquals(20_000_000L, out.getValue()); + } + + @Test + void testCreateTransactionOutput_P2SHRoundTrip() { + // P2SH vectors constructed from the coin's own params, so the version + // bytes are definitionally correct for each network. + assertP2SHRoundTrip(CoinTicker.LITECOIN, new LitecoinNetworkParametersLegacy()); + assertP2SHRoundTrip(CoinTicker.DASHCOIN, new DashcoinNetworkParametersLegacy()); + assertP2SHRoundTrip(CoinTicker.PIVX, new PivxNetworkParametersLegacy()); + } + + private static void assertP2SHRoundTrip(CoinTicker ticker, NetworkParameters params) { + byte[] hash = new byte[20]; + for (int i = 0; i < hash.length; i++) + hash[i] = (byte) (i * 7 + 3); + String p2sh = LegacyAddress.fromScriptHash(params, hash).toBase58(); + assertTrue(LegacyAddress.fromBase58(params, p2sh).isP2SHAddress()); + UTXO out = WalletHelper.createTransactionOutput(ticker, p2sh, 0.001, params); + assertEquals(p2sh, out.getAddress()); + assertEquals(100_000L, out.getValue()); + } + + @Test + void testCreateTransactionOutput_BlocknetRegression() { + NetworkParameters params = new BlocknetNetworkParameters(); + String p2pkh = LegacyAddress.fromPubKeyHash(params, new byte[20]).toBase58(); + UTXO out = WalletHelper.createTransactionOutput( + CoinTicker.BLOCKNET, p2pkh, 0.05, params); + assertEquals(p2pkh, out.getAddress()); + assertEquals(5_000_000L, out.getValue()); + } +} From 683e026db64dcb8ecdb70a8f07139438ab8b70d5 Mon Sep 17 00:00:00 2001 From: tryiou Date: Wed, 16 Sep 2026 11:52:42 +0200 Subject: [PATCH 69/73] fix(http): use Math.round for whole-coin to satoshi conversion Math.floor truncates binary representation error (e.g. 0.00050001 becomes 50000 sats instead of 50001), invalidating BIP137 ownership proofs and causing crNoMoney rejects for fully-funded wallets. --- .../app/net/api/http/client/HTTPClient.java | 11 ++++- .../api/http/client/SatsConversionTest.java | 42 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 src/test/java/io/xlite/daemon/app/net/api/http/client/SatsConversionTest.java diff --git a/src/main/java/io/xlite/daemon/app/net/api/http/client/HTTPClient.java b/src/main/java/io/xlite/daemon/app/net/api/http/client/HTTPClient.java index 24d05fa..89ddb03 100644 --- a/src/main/java/io/xlite/daemon/app/net/api/http/client/HTTPClient.java +++ b/src/main/java/io/xlite/daemon/app/net/api/http/client/HTTPClient.java @@ -497,6 +497,15 @@ public JsonArray getUtxosUncached(CoinTicker coinTicker, String[] addresses) { return utxoList; } + /** + * Whole-coin doubles (backend JSON) to integer base units. Must round to + * nearest: truncating the binary representation error loses satoshis + * (e.g. 0.00050001 becomes 50000). + */ + static long satsFromWholeCoins(double whole) { + return Math.round(whole * 100000000.0); + } + /** * Returns all utxos. * @param coinTicker Fetch utxos from this coin @@ -554,7 +563,7 @@ public JsonArray getUtxos(CoinTicker coinTicker, int expiry) { utxoArr.getJSONObject(i).getString("txhash"), utxoArr.getJSONObject(i).getInt("vout"), utxoArr.getJSONObject(i).getInt("block_number"), - (long) Math.floor(utxoArr.getJSONObject(i).getDouble("value") * 100000000.0)); + satsFromWholeCoins(utxoArr.getJSONObject(i).getDouble("value"))); utxoList.add(utxo); } diff --git a/src/test/java/io/xlite/daemon/app/net/api/http/client/SatsConversionTest.java b/src/test/java/io/xlite/daemon/app/net/api/http/client/SatsConversionTest.java new file mode 100644 index 0000000..bbbf063 --- /dev/null +++ b/src/test/java/io/xlite/daemon/app/net/api/http/client/SatsConversionTest.java @@ -0,0 +1,42 @@ +package io.xlite.daemon.app.net.api.http.client; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Whole-coin to satoshi conversion must round to nearest, never truncate. + * + *

A live DASH UTXO worth 0.00050001 (50001 sats) was ingested as 50000 + * sats because {@code (long) Math.floor(value * 1e8)} truncates the binary + * floating-point representation error. The one-satoshi shortfall then + * invalidated the BIP137 ownership proof on takes (the hub re-verifies + * against chain truth), producing servicenode {@code crNoMoney} rejects + * for fully-funded wallets. + */ +class SatsConversionTest { + + @Test + void testLiveDashCase() { + // 0.00050001 is not exactly representable in binary; floor() yields 50000. + assertEquals(50001L, HTTPClient.satsFromWholeCoins(0.00050001)); + } + + @Test + void testExactValues() { + assertEquals(500000L, HTTPClient.satsFromWholeCoins(0.005)); + assertEquals(20000000L, HTTPClient.satsFromWholeCoins(0.2)); + assertEquals(1L, HTTPClient.satsFromWholeCoins(0.00000001)); + assertEquals(0L, HTTPClient.satsFromWholeCoins(0.0)); + } + + @Test + void testSubSatoshiRoundsToZero() { + assertEquals(0L, HTTPClient.satsFromWholeCoins(0.000000001)); + } + + @Test + void testMaxSupplyNoOverflow() { + assertEquals(2_100_000_000_000_000L, HTTPClient.satsFromWholeCoins(21_000_000.0)); + } +} From b760fc4228a08181aa59e27400c02d59db050993 Mon Sep 17 00:00:00 2001 From: tryiou Date: Wed, 16 Sep 2026 12:24:13 +0200 Subject: [PATCH 70/73] ci: add ARM64 build matrix with correct native flags Add arm64 native-image builds alongside existing x64 for Linux and macOS: matrix strategy with per-entry runner labels, artifact names, and per-arch -march values (x86-64, armv8-a: native-image rejects aarch64 on AArch64). Set fail-fast false so one arch failure no longer cancels the other arch signal. Add concurrency group to cancel redundant builds on rapid pushes. Broaden branch triggers to all branches. No windows-11-arm native entry: no GraalVM distribution ships a windows-aarch64 backend (oracle/graal#9215; cross-compilation unsupported per oracle/graal#407; Liberica NIK is x64-only on Windows). Windows ARM64 users run the x64 binary via the OS emulator until upstream support lands. Defer env/filesystem-touching static initializers to run time (App, JSONRPCController, whose clinit builds a ConfigHelper): image builders have no user home/AppData, so build-time class initialization fails there. The narrow class entries take precedence over the package-level build-time entries. Also update AGENTS.md: remove stale LoginUtils references (class does not exist). Evidence: linux-arm64, mac-arm64, linux-x64 and mac-x64 native builds green in CI; the Windows build-time init failure was reproduced locally (same ExceptionInInitializerError) and the fixed tree builds a working native binary there. --- .github/workflows/build.yml | 91 ++++++++++++++++++++++++++++--------- AGENTS.md | 4 +- pom.xml | 7 +++ 3 files changed, 78 insertions(+), 24 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5a9eeb4..218e1c1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,22 +3,39 @@ name: build-all on: push: branches: - - main - - dev + - "**" tags: - "v**" pull_request: branches: - - main - - dev + - "**" workflow_dispatch: permissions: contents: write +concurrency: + group: build-${{ github.ref }} + cancel-in-progress: true + jobs: build_linux: - runs-on: ubuntu-22.04 + strategy: + # One arch must never cancel the others: a broken arm64 build must not + # nuke the x64 signal (or vice versa). + fail-fast: false + matrix: + include: + - os: ubuntu-22.04 + artifact: xlite-daemon-linux64 + artifact_name: artifacts-linux-x64 + native_arch: x86-64 + - os: ubuntu-22.04-arm + artifact: xlite-daemon-linux-arm64 + artifact_name: artifacts-linux-arm64 + # GraalVM native-image on AArch64 accepts armv8-a (not aarch64). + native_arch: armv8-a + runs-on: ${{ matrix.os }} steps: - name: Checkout code uses: actions/checkout@v4 @@ -48,20 +65,20 @@ jobs: run: chmod +x mvnw - name: Build native image - run: ./mvnw clean package -Pnative -DskipTests -Dnative.march=x86-64 + run: ./mvnw clean package -Pnative -DskipTests -Dnative.march=${{ matrix.native_arch }} - name: Make daemon executable run: chmod +x target/xlite-daemon - name: Rename executable - run: mv target/xlite-daemon target/xlite-daemon-linux64 + run: mv target/xlite-daemon target/${{ matrix.artifact }} - name: Upload artifacts uses: actions/upload-artifact@v4 with: - name: artifacts-linux + name: ${{ matrix.artifact_name }} path: | - target/xlite-daemon-linux64 + target/${{ matrix.artifact }} - name: Create release uses: softprops/action-gh-release@v2 @@ -70,10 +87,25 @@ jobs: name: XLite Daemon ${{ github.ref_name}} prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') || contains(github.ref_name, 'rc') }} files: | - target/xlite-daemon-linux64 + target/${{ matrix.artifact }} build_mac: - runs-on: macos-15-intel + strategy: + # One arch must never cancel the others: a broken arm64 build must not + # nuke the x64 signal (or vice versa). + fail-fast: false + matrix: + include: + - os: macos-15-intel + artifact: xlite-daemon-osx64 + artifact_name: artifacts-mac-x64 + native_arch: x86-64 + - os: macos-15 + artifact: xlite-daemon-osx-arm64 + artifact_name: artifacts-mac-arm64 + # GraalVM native-image on AArch64 accepts armv8-a (not aarch64). + native_arch: armv8-a + runs-on: ${{ matrix.os }} steps: - name: Checkout code uses: actions/checkout@v4 @@ -94,20 +126,20 @@ jobs: run: chmod +x mvnw - name: Build native image - run: ./mvnw clean package -Pnative -DskipTests -Dnative.march=x86-64 + run: ./mvnw clean package -Pnative -DskipTests -Dnative.march=${{ matrix.native_arch }} - name: Make daemon executable run: chmod +x target/xlite-daemon - name: Rename executable - run: mv target/xlite-daemon target/xlite-daemon-osx64 + run: mv target/xlite-daemon target/${{ matrix.artifact }} - name: Upload artifacts uses: actions/upload-artifact@v4 with: - name: artifacts-mac + name: ${{ matrix.artifact_name }} path: | - target/xlite-daemon-osx64 + target/${{ matrix.artifact }} - name: Create release uses: softprops/action-gh-release@v2 @@ -116,10 +148,25 @@ jobs: name: XLite Daemon ${{ github.ref_name}} prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') || contains(github.ref_name, 'rc') }} files: | - target/xlite-daemon-osx64 + target/${{ matrix.artifact }} build_win: - runs-on: windows-2022 + strategy: + # One arch must never cancel the others: a broken arm64 build must not + # nuke the x64 signal (or vice versa). + fail-fast: false + matrix: + include: + - os: windows-2022 + artifact: xlite-daemon-win64.exe + artifact_name: artifacts-win-x64 + native_arch: x86-64 + # No windows-11-arm native entry: no GraalVM distribution ships a + # windows-aarch64 backend (oracle/graal#9215; cross-compilation + # unsupported per oracle/graal#407; Liberica NIK is x64-only on + # Windows). Windows ARM64 users run the x64 binary via the OS + # emulator until upstream support lands. + runs-on: ${{ matrix.os }} steps: - name: Checkout code uses: actions/checkout@v4 @@ -141,16 +188,16 @@ jobs: choco install -y visualstudio2022buildtools --installargs "--add Microsoft.VisualStudio.Workload.VCTools --quiet --wait" - name: Build native image - run: .\mvnw.cmd clean package -Pnative -DskipTests "-Dnative.march=x86-64" + run: .\mvnw.cmd clean package -Pnative -DskipTests "-Dnative.march=${{ matrix.native_arch }}" - name: Rename executable - run: Rename-Item target\xlite-daemon.exe xlite-daemon-win64.exe + run: Rename-Item target\xlite-daemon.exe ${{ matrix.artifact }} - name: Upload artifacts uses: actions/upload-artifact@v4 with: - name: artifacts-win - path: target\xlite-daemon-win64.exe + name: ${{ matrix.artifact_name }} + path: target\${{ matrix.artifact }} - name: Create release uses: softprops/action-gh-release@v2 @@ -158,4 +205,4 @@ jobs: with: name: XLite Daemon ${{ github.ref_name}} prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') || contains(github.ref_name, 'rc') }} - files: target/xlite-daemon-win64.exe \ No newline at end of file + files: target/${{ matrix.artifact }} diff --git a/AGENTS.md b/AGENTS.md index 451b558..31c3f05 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -116,7 +116,7 @@ Log messages use bracketed prefixes: `[security]`, `[discovery-BLOCK]`, `[wallet ``` src/main/java/io/xlite/daemon/app/ - crypto/ KeyHandler (wallet encryption), LoginUtils (auth) + crypto/ KeyHandler (wallet encryption/key management) net/ CoinInstance (coin lifecycle), JSON-RPC servers, protocols/ util/ ConfigHelper, AddressDiscoveryService, UTXO, logging wallet/ WalletHelper @@ -124,7 +124,7 @@ src/main/java/io/xlite/daemon/app/ src/test/java/ KeyHandlerTest, CoinInstanceTest, ConfigHelperTest, - AddressDiscoveryServiceTest, LoginUtilsTest, TestHelper + AddressDiscoveryServiceTest, TestHelper ``` ## Key Dependencies diff --git a/pom.xml b/pom.xml index 1944f86..bcc1233 100644 --- a/pom.xml +++ b/pom.xml @@ -357,6 +357,13 @@ --initialize-at-run-time=org.json --initialize-at-run-time=org.slf4j --initialize-at-run-time=org.slf4j.impl + + --initialize-at-run-time=io.xlite.daemon.app.App + --initialize-at-run-time=io.xlite.daemon.app.net.api.JSONRPCController --initialize-at-build-time=com.google.common.base.Charsets From 97efc045dca16fffa7ce3b414c3dfaf541333d89 Mon Sep 17 00:00:00 2001 From: tryiou Date: Wed, 16 Sep 2026 12:46:45 +0200 Subject: [PATCH 71/73] fix(sats): unify whole-coin to satoshi rounding via shared helper Truncating binary representation error loses satoshis (0.00050001 becomes 50000 instead of 50001), invalidating BIP137 ownership proofs and causing crNoMoney rejects. The ingest path already rounded; the send, fee, discovery and display paths still truncated. Add public util Sats.fromWholeCoins (Math.round over Coin.COIN.value) and route all ten whole-to-base conversions through it: WalletHelper recipient/change outputs, isDust and createTransactionOutput, HTTPServerHandler P2SH/non-P2SH outputs, XRouterFeeUtils fee/change outputs, AddressDiscoveryService and BlocknetPeerGroup ingest, CoinInstance display formatting. HTTPClient.satsFromWholeCoins now delegates (no behavior change there). Tests: new SatsTest plus an inexact-double vector in WalletHelperSendTest (failed 50000-vs-50001 before, green after). --- .../io/xlite/daemon/app/net/CoinInstance.java | 3 +- .../app/net/api/http/client/HTTPClient.java | 3 +- .../api/http/server/HTTPServerHandler.java | 5 ++- .../protocols/blocknet/BlocknetPeerGroup.java | 3 +- .../app/net/xrouter/XRouterFeeUtils.java | 9 ++-- .../app/util/AddressDiscoveryService.java | 2 +- .../java/io/xlite/daemon/app/util/Sats.java | 21 ++++++++++ .../xlite/daemon/app/wallet/WalletHelper.java | 9 ++-- .../io/xlite/daemon/app/util/SatsTest.java | 42 +++++++++++++++++++ .../app/wallet/WalletHelperSendTest.java | 11 +++++ 10 files changed, 94 insertions(+), 14 deletions(-) create mode 100644 src/main/java/io/xlite/daemon/app/util/Sats.java create mode 100644 src/test/java/io/xlite/daemon/app/util/SatsTest.java diff --git a/src/main/java/io/xlite/daemon/app/net/CoinInstance.java b/src/main/java/io/xlite/daemon/app/net/CoinInstance.java index b684d1b..cbc50c3 100644 --- a/src/main/java/io/xlite/daemon/app/net/CoinInstance.java +++ b/src/main/java/io/xlite/daemon/app/net/CoinInstance.java @@ -16,6 +16,7 @@ import io.xlite.daemon.app.util.AddressDiscoveryService; import io.xlite.daemon.app.util.CloudTransaction; import io.xlite.daemon.app.util.ConfigHelper; +import io.xlite.daemon.app.util.Sats; import io.xlite.daemon.app.util.UTXO; import io.xlite.daemon.app.util.history.Transaction; import io.xlite.daemon.app.wallet.WalletHelper; @@ -630,7 +631,7 @@ public double getAllBalances() { public String getAllBalancesFormatted() { BtcFormat f = BtcFormat.getInstance(BtcFormat.COIN_SCALE); - return f.format(Coin.valueOf((long) (getAllBalances() * Coin.COIN.value))); + return f.format(Coin.valueOf(Sats.fromWholeCoins(getAllBalances()))); } public void sendXrGetTransaction(BlocknetPeer blocknetPeer, String txid) { diff --git a/src/main/java/io/xlite/daemon/app/net/api/http/client/HTTPClient.java b/src/main/java/io/xlite/daemon/app/net/api/http/client/HTTPClient.java index 89ddb03..67f98f4 100644 --- a/src/main/java/io/xlite/daemon/app/net/api/http/client/HTTPClient.java +++ b/src/main/java/io/xlite/daemon/app/net/api/http/client/HTTPClient.java @@ -15,6 +15,7 @@ import io.xlite.daemon.app.net.CoinTicker; import io.xlite.daemon.app.net.CoinTickerUtils; import io.xlite.daemon.app.util.AddressBalance; +import io.xlite.daemon.app.util.Sats; import io.xlite.daemon.app.util.UTXO; import io.xlite.daemon.app.util.history.Transaction; import org.apache.http.Header; @@ -503,7 +504,7 @@ public JsonArray getUtxosUncached(CoinTicker coinTicker, String[] addresses) { * (e.g. 0.00050001 becomes 50000). */ static long satsFromWholeCoins(double whole) { - return Math.round(whole * 100000000.0); + return Sats.fromWholeCoins(whole); } /** diff --git a/src/main/java/io/xlite/daemon/app/net/api/http/server/HTTPServerHandler.java b/src/main/java/io/xlite/daemon/app/net/api/http/server/HTTPServerHandler.java index 4f147a5..ca8a7c4 100644 --- a/src/main/java/io/xlite/daemon/app/net/api/http/server/HTTPServerHandler.java +++ b/src/main/java/io/xlite/daemon/app/net/api/http/server/HTTPServerHandler.java @@ -12,6 +12,7 @@ import io.xlite.daemon.app.net.protocols.blocknet.BlocknetPeer; import io.xlite.daemon.app.util.AddressBalance; import io.xlite.daemon.app.util.ConfigHelper; +import io.xlite.daemon.app.util.Sats; import io.xlite.daemon.app.util.UTXO; import io.xlite.daemon.app.util.Utility; import io.xlite.daemon.app.wallet.WalletHelper; @@ -784,7 +785,7 @@ private JsonObject getResponse(String method, JsonArray params) { for (OutputEntry entry : outputEntries) { try { LegacyAddress address = LegacyAddress.fromBase58(coin.getNetworkParameters(), entry.address); - Coin outputValue = Coin.valueOf((long) Math.floor(entry.amount * Coin.COIN.value)); + Coin outputValue = Coin.valueOf(Sats.fromWholeCoins(entry.amount)); if (isP2SHAddress(entry.address)) { LOGGER.fine("[http-server-handler] P2SH Address Found: " + entry.address); Script p2shScript = ScriptBuilder.createP2SHOutputScript(address.getHash()); @@ -802,7 +803,7 @@ private JsonObject getResponse(String method, JsonArray params) { for (OutputEntry entry : outputEntries) { try { LegacyAddress address = LegacyAddress.fromBase58(coin.getNetworkParameters(), entry.address); - Coin outputValue = Coin.valueOf((long) Math.floor(entry.amount * Coin.COIN.value)); + Coin outputValue = Coin.valueOf(Sats.fromWholeCoins(entry.amount)); if (!isP2SHAddress(entry.address)) { tx.addOutput(outputValue, address); } diff --git a/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetPeerGroup.java b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetPeerGroup.java index 6f23609..3f8a214 100644 --- a/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetPeerGroup.java +++ b/src/main/java/io/xlite/daemon/app/net/protocols/blocknet/BlocknetPeerGroup.java @@ -14,6 +14,7 @@ import io.xlite.daemon.app.net.xrouter.XRouterCommandUtils; import io.xlite.daemon.app.net.xrouter.XRouterInitialMessagesSentListener; import io.xlite.daemon.app.net.xrouter.XRouterMessage; +import io.xlite.daemon.app.util.Sats; import io.xlite.daemon.app.util.UTXO; import io.xlite.daemon.app.util.XRouterConfiguration; import io.xlite.daemon.app.util.background.BackgroundTimerThread; @@ -374,7 +375,7 @@ private void sendInitialXRouterMessages(BlocknetPeer peer) { String txid = utxoJson.getString("txhash"); int vout = utxoJson.getInt("vout"); int height = utxoJson.getInt("block_number"); - long value = (long) Math.floor(utxoJson.getDouble("value") * 100000000.0); + long value = Sats.fromWholeCoins(utxoJson.getDouble("value")); UTXO utxo = new UTXO(coinTicker, addressB58, txid, vout, height, value); utxoList.add(utxo); diff --git a/src/main/java/io/xlite/daemon/app/net/xrouter/XRouterFeeUtils.java b/src/main/java/io/xlite/daemon/app/net/xrouter/XRouterFeeUtils.java index 1175ac7..92dd1c7 100644 --- a/src/main/java/io/xlite/daemon/app/net/xrouter/XRouterFeeUtils.java +++ b/src/main/java/io/xlite/daemon/app/net/xrouter/XRouterFeeUtils.java @@ -4,6 +4,7 @@ import com.subgraph.orchid.encoders.Hex; import io.xlite.daemon.app.net.CoinInstance; import io.xlite.daemon.app.net.protocols.blocknet.BlocknetPeer; +import io.xlite.daemon.app.util.Sats; import io.xlite.daemon.app.util.XRouterConfiguration; import io.xlite.daemon.app.wallet.WalletHelper; import org.bitcoinj.core.*; @@ -36,7 +37,7 @@ public static String getXRouterFeeTx(BlocknetPeer blocknetPeer, String xRouterCo double fee = feeMap.get(xRouterCommand); - Coin xRouterFeeAmt = Coin.valueOf((long) Math.floor(fee * Coin.COIN.value)); + Coin xRouterFeeAmt = Coin.valueOf(Sats.fromWholeCoins(fee)); if (xRouterFeeAmt.value == 0) { LOGGER.finer("[xrouter-fee-utils] DEBUG: This command is free."); return "nohash;nofee"; @@ -53,7 +54,7 @@ public static String getXRouterFeeTx(BlocknetPeer blocknetPeer, String xRouterCo LegacyAddress xRouterPaymentAddress = LegacyAddress.fromBase58(params, xRouterConfig.getFeeAddress()); Coin blocknetNetworkFeeAmt = Coin.valueOf(networkFeeSats); - Coin xRouterChangeAmt = Coin.valueOf((long) Math.floor(totalAvailable * Coin.COIN.value)).minus(blocknetNetworkFeeAmt).minus(xRouterFeeAmt); + Coin xRouterChangeAmt = Coin.valueOf(Sats.fromWholeCoins(totalAvailable)).minus(blocknetNetworkFeeAmt).minus(xRouterFeeAmt); TransactionOutput feeOutput = new TransactionOutput(params, null, xRouterFeeAmt, xRouterPaymentAddress); @@ -62,7 +63,7 @@ public static String getXRouterFeeTx(BlocknetPeer blocknetPeer, String xRouterCo if (changeAmt > 0.06) { double halvedAmt = changeAmt / 3; - Coin halvedChangeAmt = Coin.valueOf((long) Math.floor(halvedAmt * Coin.COIN.value)); + Coin halvedChangeAmt = Coin.valueOf(Sats.fromWholeCoins(halvedAmt)); TransactionOutput halvedChangeOutput = new TransactionOutput(params, null, halvedChangeAmt, blocknetWalletHelper.getChangeAddress()); for (int i = 0; i < 3; i++) { @@ -91,7 +92,7 @@ public static TransactionOutput createXrSendTransactionFeeOutput(BlocknetPeer bl double fee = feeMap.get("xrSendTransaction"); String feeAddress = xRouterConfig.getFeeAddress(); - Coin feeAmount = Coin.valueOf((long) Math.floor(fee * Coin.COIN.value)); + Coin feeAmount = Coin.valueOf(Sats.fromWholeCoins(fee)); return new TransactionOutput(blocknetCoin.getNetworkParameters(), null, feeAmount, LegacyAddress.fromBase58(blocknetCoin.getNetworkParameters(), feeAddress)); } diff --git a/src/main/java/io/xlite/daemon/app/util/AddressDiscoveryService.java b/src/main/java/io/xlite/daemon/app/util/AddressDiscoveryService.java index 1b18e95..13e059a 100644 --- a/src/main/java/io/xlite/daemon/app/util/AddressDiscoveryService.java +++ b/src/main/java/io/xlite/daemon/app/util/AddressDiscoveryService.java @@ -262,7 +262,7 @@ private List checkBatchForUtxos(List batch) { txidElement.getAsString(), voutElement.getAsInt(), confirmationsElement.getAsInt(), - (long) (valueElement.getAsDouble() * 100000000.0) + Sats.fromWholeCoins(valueElement.getAsDouble()) ); utxos.add(utxo); } catch (Exception e) { diff --git a/src/main/java/io/xlite/daemon/app/util/Sats.java b/src/main/java/io/xlite/daemon/app/util/Sats.java new file mode 100644 index 0000000..8c7e0d8 --- /dev/null +++ b/src/main/java/io/xlite/daemon/app/util/Sats.java @@ -0,0 +1,21 @@ +package io.xlite.daemon.app.util; + +import org.bitcoinj.core.Coin; + +/** + * Whole-coin doubles to integer base units. Single choke point so every + * path (ingest, send, fees, display) converts identically. + * + *

Must round to nearest: binary doubles cannot exactly represent most + * decimal fractions, and truncating the representation error loses + * satoshis (e.g. 0.00050001 becomes 50000 instead of 50001). + */ +public final class Sats { + + private Sats() { + } + + public static long fromWholeCoins(double whole) { + return Math.round(whole * Coin.COIN.value); + } +} diff --git a/src/main/java/io/xlite/daemon/app/wallet/WalletHelper.java b/src/main/java/io/xlite/daemon/app/wallet/WalletHelper.java index df21753..656865c 100644 --- a/src/main/java/io/xlite/daemon/app/wallet/WalletHelper.java +++ b/src/main/java/io/xlite/daemon/app/wallet/WalletHelper.java @@ -8,6 +8,7 @@ import io.xlite.daemon.app.net.protocols.blocknet.BlocknetPeer; import io.xlite.daemon.app.util.AddressBalance; import io.xlite.daemon.app.util.CloudTransaction; +import io.xlite.daemon.app.util.Sats; import io.xlite.daemon.app.util.UTXO; import org.bitcoinj.core.*; import org.bitcoinj.crypto.DeterministicKey; @@ -276,11 +277,11 @@ public static Transaction createTransactionSimple(CoinTicker coinTicker, String Transaction tx = new Transaction(params); for (UTXO output : outputs) { Address addr = LegacyAddress.fromBase58(params, output.getAddress()); - tx.addOutput(Coin.valueOf((long) (output.getAmount() * coinUnit)), addr); + tx.addOutput(Coin.valueOf(Sats.fromWholeCoins(output.getAmount())), addr); } if (changeAmt > 0 && !isDust(changeAmt, params)) { - tx.addOutput(Coin.valueOf((long) (changeAmt * coinUnit)), walletHelper.getChangeAddress()); + tx.addOutput(Coin.valueOf(Sats.fromWholeCoins(changeAmt)), walletHelper.getChangeAddress()); } return walletHelper.signTransactionWithUtxos(tx, selectedUtxos); @@ -360,7 +361,7 @@ private static double calculateFee(int inputCount, int outputCount, long feePerB } private static boolean isDust(double amount, NetworkParameters params) { - return amount * Coin.COIN.value < params.getMinNonDustOutput().value; + return Sats.fromWholeCoins(amount) < params.getMinNonDustOutput().value; } // Package-private for testing. The params must be the coin's own network @@ -371,7 +372,7 @@ static UTXO createTransactionOutput(CoinTicker ticker, String address, double am // AddressFormatException on mismatch. Do not remove: a null-params // lookup here rejects every non-BTC address ("No network found"). LegacyAddress.fromBase58(params, address); - Coin coin = Coin.valueOf((long) (amount * Coin.COIN.value)); + Coin coin = Coin.valueOf(Sats.fromWholeCoins(amount)); return new UTXO(ticker, address, "", 0, 0, coin.value); } diff --git a/src/test/java/io/xlite/daemon/app/util/SatsTest.java b/src/test/java/io/xlite/daemon/app/util/SatsTest.java new file mode 100644 index 0000000..22b47d7 --- /dev/null +++ b/src/test/java/io/xlite/daemon/app/util/SatsTest.java @@ -0,0 +1,42 @@ +package io.xlite.daemon.app.util; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Shared whole-coin to satoshi conversion must round to nearest, never truncate. + * + *

Binary doubles cannot exactly represent most decimal fractions. A value + * like 0.00050001 (50001 sats) is stored as slightly less, so {@code (long)} + * truncation yields 50000 — a 1-satoshi shortfall that invalidated BIP137 + * ownership proofs on takes (servicenode {@code crNoMoney} rejects). Every + * whole-coin to base-unit site (ingest, send, fees, display) must go through + * {@link Sats#fromWholeCoins}. + */ +class SatsTest { + + @Test + void testInexactDoubleRoundsToNearest() { + // 0.00050001 is not exactly representable in binary; floor() yields 50000. + assertEquals(50001L, Sats.fromWholeCoins(0.00050001)); + } + + @Test + void testExactValues() { + assertEquals(500000L, Sats.fromWholeCoins(0.005)); + assertEquals(20000000L, Sats.fromWholeCoins(0.2)); + assertEquals(1L, Sats.fromWholeCoins(0.00000001)); + assertEquals(0L, Sats.fromWholeCoins(0.0)); + } + + @Test + void testSubSatoshiRoundsToZero() { + assertEquals(0L, Sats.fromWholeCoins(0.000000001)); + } + + @Test + void testMaxSupplyNoOverflow() { + assertEquals(2_100_000_000_000_000L, Sats.fromWholeCoins(21_000_000.0)); + } +} diff --git a/src/test/java/io/xlite/daemon/app/wallet/WalletHelperSendTest.java b/src/test/java/io/xlite/daemon/app/wallet/WalletHelperSendTest.java index bbc99c4..e3639b4 100644 --- a/src/test/java/io/xlite/daemon/app/wallet/WalletHelperSendTest.java +++ b/src/test/java/io/xlite/daemon/app/wallet/WalletHelperSendTest.java @@ -102,4 +102,15 @@ void testCreateTransactionOutput_BlocknetRegression() { assertEquals(p2pkh, out.getAddress()); assertEquals(5_000_000L, out.getValue()); } + + @Test + void testCreateTransactionOutput_InexactDoubleRoundsToNearest() { + // 0.00050001 is not exactly representable in binary; the stored + // double sits just below 50001 sats, so (long) truncation yields + // 50000. Must round to nearest like the UTXO ingest path. + NetworkParameters params = new LitecoinNetworkParametersLegacy(); + UTXO out = WalletHelper.createTransactionOutput( + CoinTicker.LITECOIN, LTC_P2PKH, 0.00050001, params); + assertEquals(50_001L, out.getValue()); + } } From 812e3eebaaefaf2824a30fb4997c7ae34f2efbed Mon Sep 17 00:00:00 2001 From: tryiou Date: Wed, 16 Sep 2026 14:19:03 +0200 Subject: [PATCH 72/73] fix(config): fail-closed legacy data directory migration Replace the lossy CloudChains to xlite-daemon move-or-nothing logic. Same-filesystem atomic rename first, else staged copy plus byte-verify (Files.mismatch) plus rename; both-dirs state merges with verification. Byte-divergent files refuse to boot (either side could be the real wallet), archiving never overwrites, the legacy source is never deleted before its content is verified elsewhere, and refusals are side-effect-free (conflicts detected before any copy, backup existence pre-checked). Intra-JVM lock plus inter-process file lock with contention logging; mkdirs guards re-check to tolerate lost races. Uncreatable base, blank base, conflicting or unusable dirs, and constructor paths all throw instead of booting on an empty wallet. Tests: 7 migration vectors fail on the old code (merge, resume, conflict refusal, uncreatable/blank base, existing backup, staging redo) plus direct copy-verify-rename success/failure, constructor propagation, and settings-as-file refusal; 191/191 green after. The cross-filesystem fallback has no portable simulator, so it is covered through its shared verify/archive helpers. --- .../xlite/daemon/app/util/ConfigHelper.java | 326 ++++++++++++++++-- src/test/java/ConfigHelperTest.java | 182 +++++++++- src/test/java/TestHelper.java | 17 +- .../app/util/ConfigHelperMigrationTest.java | 78 +++++ 4 files changed, 575 insertions(+), 28 deletions(-) create mode 100644 src/test/java/io/xlite/daemon/app/util/ConfigHelperMigrationTest.java diff --git a/src/main/java/io/xlite/daemon/app/util/ConfigHelper.java b/src/main/java/io/xlite/daemon/app/util/ConfigHelper.java index 24410fa..56d132d 100644 --- a/src/main/java/io/xlite/daemon/app/util/ConfigHelper.java +++ b/src/main/java/io/xlite/daemon/app/util/ConfigHelper.java @@ -1,5 +1,6 @@ package io.xlite.daemon.app.util; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import io.xlite.daemon.app.App; import org.json.JSONObject; @@ -7,11 +8,20 @@ import java.io.File; import java.io.FileWriter; import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.channels.FileLock; +import java.nio.channels.OverlappingFileLockException; import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; import java.util.logging.LogManager; import java.util.logging.Logger; +import java.util.stream.Stream; public class ConfigHelper { private final static LogManager LOGMANAGER = LogManager.getLogManager(); @@ -31,13 +41,36 @@ public class ConfigHelper { // Override specific configuration directory (useful in unit tests) public static String CONFIG_DIR = ""; // Must not end with [/], e.g. /home/user/.config, not /home/user/.config/ + // Data directory names under the platform config root. + public static final String DATA_DIR_NAME = "xlite-daemon"; + private static final String LEGACY_DIR_NAME = "CloudChains"; + private static final String LEGACY_BACKUP_SUFFIX = ".bak"; + private static final String MIGRATION_STAGING_SUFFIX = ".migrating"; + // Staging directory name, derived from the data dir name. Public (like the + // lock file below) so the test harness can clean sandboxed leftovers. + public static final String MIGRATION_STAGING_NAME = DATA_DIR_NAME + MIGRATION_STAGING_SUFFIX; + // On-disk inter-process migration lock. Public so the test harness can + // clean it alongside the sandboxed data dir. Intentionally never deleted + // in production: an empty sentinel file is harmless, while deleting a + // lock another process may be opening is not. + public static final String MIGRATION_LOCK_FILE = ".migration.lock"; + private static final Object MIGRATION_LOCK = new Object(); + public ConfigHelper(String tickerStr) { this.tickerStr = Preconditions.checkNotNull(tickerStr, "tickerStr must not be null"); try { - file = Preconditions.checkNotNull(this.getFile()); + file = Preconditions.checkNotNull(this.getFile(), "getFile() returned null for " + tickerStr); loadConfig(); } catch (Exception e) { + // Fail-closed: a null file means the data directory itself is + // unusable (migration or creation failed). Swallowing that here + // would boot the daemon on defaults with no wallet on disk. + if (file == null) { + throw new IllegalStateException( + "[config] Failed to initialize config for " + tickerStr + ": data directory unusable: " + + e.getMessage(), e); + } LOGGER.warning("[config] Failed to initialize config for " + tickerStr + ", " + e.getMessage()); } } @@ -142,11 +175,18 @@ private File getFile() { File home = new File(userHome); File settingsDirectory = new File(home, "settings"); if (!settingsDirectory.exists()) { - if (!settingsDirectory.mkdirs()) { + if (!settingsDirectory.isDirectory() && !settingsDirectory.mkdirs() + && !settingsDirectory.isDirectory()) { LOGGER.finer("[config] ERROR: Could not create base/settings directory!"); return null; } } + // A regular file (or anything non-directory) at settings/ would make + // every config write below fail: refuse instead of booting on defaults. + if (!settingsDirectory.isDirectory()) { + LOGGER.warning("[config] settings path is not a directory: " + settingsDirectory); + return null; + } File configFile = new File(settingsDirectory, "config-" + tickerStr + ".json"); try { @@ -154,6 +194,7 @@ private File getFile() { return null; } catch (IOException e) { LOGGER.warning("[config] IOException creating config file for " + tickerStr + ", " + e.getMessage()); + return null; } return configFile; @@ -275,42 +316,281 @@ public synchronized void writeConfig() { public static String getLocalDataDirectory() { String baseDir; - if (CONFIG_DIR.isEmpty()) { + if (CONFIG_DIR == null || CONFIG_DIR.isEmpty()) { baseDir = App.getUserConfigDir(); } else { baseDir = CONFIG_DIR; } + // Fail fast on a null/blank base (e.g. missing Windows AppData) + // instead of stringifying to a bogus "null/xlite-daemon/" dir. + if (baseDir == null || baseDir.trim().isEmpty()) { + throw new IllegalStateException( + "[config] Cannot resolve data directory: base config dir is null or blank. " + + "Set XLITE_DATA_HOME to an explicit path."); + } - String newDirPath = baseDir + File.separator + "xlite-daemon" + File.separator; - String oldDirPath = baseDir + File.separator + "CloudChains" + File.separator; - File newDir = new File(newDirPath); - File oldDir = new File(oldDirPath); - if (!newDir.exists() && oldDir.exists()) { + File newDir = new File(baseDir, DATA_DIR_NAME); + File oldDir = new File(baseDir, LEGACY_DIR_NAME); + // Intra-JVM mutual exclusion plus an inter-process file lock: supervisors + // restart daemons, and two processes racing on the shared staging dir + // would otherwise wipe each other's in-progress copy. The loser of a + // real race blocks here, then finds the source already reconciled. + synchronized (MIGRATION_LOCK) { + File base = new File(baseDir); + // Re-check after mkdirs: a concurrent creator winning the race + // makes mkdirs return false for an existing dir (no failure). + if (!base.isDirectory() && !base.mkdirs() && !base.isDirectory()) { + throw new IllegalStateException( + "[config] Cannot create base config directory: " + base + + ". Check permissions or set XLITE_DATA_HOME."); + } + try (RandomAccessFile lockRaf = new RandomAccessFile(new File(base, MIGRATION_LOCK_FILE), "rw"); + FileLock migrationLock = acquireMigrationLock(lockRaf, base)) { + // Blocking acquire is intentional: the loser waits, then finds + // the source already reconciled. A second daemon only ever + // blocks here during an in-progress large copy. + migrateLegacyDirectory(baseDir, oldDir, newDir); + if (!newDir.isDirectory() && !newDir.mkdirs() && !newDir.isDirectory()) { + throw new IllegalStateException( + "[config] Cannot create data directory: " + newDir + + ". Check permissions or set XLITE_DATA_HOME."); + } + } catch (OverlappingFileLockException e) { + // Unreachable through our own paths (same-JVM overlap is + // excluded by MIGRATION_LOCK and the file is ours alone), + // but wrap precisely rather than leak an undecorated throw. + throw new IllegalStateException( + "[config] Data directory migration lock failed for " + base + ": overlapping lock", e); + } catch (IOException e) { + throw new IllegalStateException( + "[config] Data directory migration lock failed for " + base + ": " + e.getMessage(), e); + } + } + + return newDir.getPath() + File.separator; + } + + /** + * Blocking inter-process acquire with diagnosis logging. Blocking (not + * tryLock-with-timeout) is intentional: an OS-managed wait cannot + * spuriously fail a healthy startup; the loser always wakes to find the + * source already reconciled. + */ + private static FileLock acquireMigrationLock(RandomAccessFile lockRaf, File base) throws IOException { + LOGGER.fine("[migrate] waiting for data directory migration lock for " + base); + long startNanos = System.nanoTime(); + FileLock lock = lockRaf.getChannel().lock(); + long waitedMillis = (System.nanoTime() - startNanos) / 1_000_000; + // Operator-visible only when actually contended: uncontended startup stays quiet. + if (waitedMillis > 1000) { + LOGGER.info("[migrate] waited " + waitedMillis + " ms for data directory migration lock for " + base); + } else { + LOGGER.fine("[migrate] holding data directory migration lock for " + base); + } + return lock; + } + /** + * Reconciles the legacy {@code CloudChains/} data directory with the new + * {@code xlite-daemon/} one. Fail-closed: any unverifiable state throws + * instead of booting on a partial or empty directory. + * + *

States handled: + *

    + *
  • No legacy dir: nothing to do (fresh install or already migrated).
  • + *
  • Legacy dir, no new dir: same-filesystem rename, else staged + * copy + byte-verify + rename, then archive the legacy dir.
  • + *
  • Both dirs: merge-verify (idempotent; byte-identical files are + * fine), then archive the legacy dir. Byte-divergent files throw: + * either side could be the real wallet, so there is no safe choice.
  • + *
+ * + *

The legacy source is never deleted until its content is verified + * present at the destination; it is archived (never deleted) after that. + * Our own staging leftover is wiped and redone from the intact source. + */ + private static void migrateLegacyDirectory(String baseDir, File oldDir, File newDir) { + File staging = new File(baseDir, MIGRATION_STAGING_NAME); + if (staging.exists()) { + deleteDirectoryRecursively(staging); + } + if (!oldDir.exists()) { + return; + } + if (!newDir.exists()) { try { - Files.move(oldDir.toPath(), newDir.toPath()); - LOGGER.warning("[migrate] moved legacy data directory from " + oldDirPath + " to " + newDirPath); + Files.move(oldDir.toPath(), newDir.toPath(), StandardCopyOption.ATOMIC_MOVE); + LOGGER.info("[migrate] moved legacy data directory from " + oldDir + " to " + newDir); + return; + } catch (AtomicMoveNotSupportedException e) { + LOGGER.warning("[migrate] atomic move unsupported (" + e.getMessage() + "), copy-verify-archive fallback"); } catch (IOException e) { - LOGGER.warning("[migrate] Files.move failed (" + e.getMessage() + "), falling back to copy for " + oldDirPath + " -> " + newDirPath); - try { - copyDirectoryRecursively(oldDir, newDir); - LOGGER.warning("[migrate] copied legacy data directory from " + oldDirPath + " to " + newDirPath); - } catch (IOException copyEx) { - LOGGER.warning("[migrate] copy fallback failed: " + copyEx.getMessage()); + LOGGER.warning("[migrate] move failed (" + e.getMessage() + "), copy-verify-archive fallback"); + } + copyVerifyRename(oldDir, newDir, staging); + archiveLegacyDir(baseDir, oldDir); + return; + } + // Both exist: refuse early when archiving is impossible, so the + // refusal below is side-effect-free (mergeVerify mutates newDir). + if (new File(baseDir, LEGACY_DIR_NAME + LEGACY_BACKUP_SUFFIX).exists()) { + throw new IllegalStateException( + "[migrate] refusing to boot: legacy backup already exists and legacy dir " + + oldDir + " is still present. Resolve manually."); + } + mergeVerify(oldDir, newDir); + archiveLegacyDir(baseDir, oldDir); + } + + /** + * Copies {@code oldDir} to {@code staging}, byte-verifies every legacy + * file landed identically, then renames staging to {@code newDir}. + * Throws on any failure; the legacy source is untouched throughout. + * + *

Visible for testing: the cross-filesystem fallback cannot be forced + * through the public path on a single test filesystem, so the suite + * drives this step directly with a hand-built staging dir. + */ + @VisibleForTesting + static void copyVerifyRename(File oldDir, File newDir, File staging) { + try { + copyDirectoryRecursively(oldDir, staging); + verifyTreeContains(staging, oldDir); + Files.move(staging.toPath(), newDir.toPath()); + } catch (IOException e) { + throw new IllegalStateException( + "[migrate] copy-verify-rename failed for " + oldDir + " -> " + newDir + ": " + e.getMessage(), e); + } + verifyTreeContains(newDir, oldDir); + LOGGER.info("[migrate] copied legacy data directory from " + oldDir + " to " + newDir); + } + + /** + * Merges legacy files missing from the new dir (never overwrites), then + * verifies every legacy file is present with identical bytes. Byte- + * divergent files throw: no automatic choice is safe for wallet data. + * + *

Conflict detection runs as a pure verification pass before any copy, + * so a refusal leaves the live dir untouched (fail-closed, side-effect-free). + */ + private static void mergeVerify(File oldDir, File newDir) { + List conflicts = detectConflicts(oldDir, newDir); + if (!conflicts.isEmpty()) { + throw new IllegalStateException( + "[migrate] refusing to boot: legacy and new data dirs diverge on " + + conflicts.size() + " file(s) " + conflicts + + ". Resolve manually (either side could be the real wallet)."); + } + try (Stream walk = Files.walk(oldDir.toPath())) { + for (Path source : (Iterable) walk::iterator) { + Path relative = oldDir.toPath().relativize(source); + Path target = newDir.toPath().resolve(relative); + if (Files.isDirectory(source)) { + if (!Files.exists(target)) { + Files.createDirectories(target); + } + } else if (Files.isRegularFile(source)) { + if (!Files.exists(target)) { + Files.copy(source, target); + } + } else { + throw new IllegalStateException( + "[migrate] unsupported legacy entry (not a file or directory): " + source); } } + } catch (IOException e) { + throw new IllegalStateException( + "[migrate] merge failed for " + oldDir + " -> " + newDir + ": " + e.getMessage(), e); } + // Symmetric with the copy path: re-verify after writing, before the + // only other good copy is archived away. + verifyTreeContains(newDir, oldDir); + } - File directory = new File(newDirPath); - if (!directory.exists()) { - directory.mkdirs(); + /** + * Pure verification pass: returns relative paths of byte-divergent files + * present on both sides. Copies nothing. + */ + private static List detectConflicts(File oldDir, File newDir) { + List conflicts = new ArrayList<>(); + try (Stream walk = Files.walk(oldDir.toPath())) { + for (Path source : (Iterable) walk::iterator) { + if (!Files.isRegularFile(source)) { + continue; + } + Path relative = oldDir.toPath().relativize(source); + Path target = newDir.toPath().resolve(relative); + if (Files.isRegularFile(target) && Files.mismatch(source, target) != -1) { + conflicts.add(relative.toString()); + } + } + } catch (IOException e) { + throw new IllegalStateException( + "[migrate] conflict scan failed for " + oldDir + " vs " + newDir + ": " + e.getMessage(), e); } + return conflicts; + } - return newDirPath; + /** + * Every regular file under {@code required} must exist under + * {@code container} with identical bytes. One-directional: files created + * fresh in the container (e.g. logs) are fine. + */ + private static void verifyTreeContains(File container, File required) { + try (Stream walk = Files.walk(required.toPath())) { + for (Path source : (Iterable) walk::iterator) { + Path relative = required.toPath().relativize(source); + Path target = container.toPath().resolve(relative); + if (Files.isDirectory(source)) { + if (!Files.isDirectory(target)) { + throw new IllegalStateException("[migrate] verification failed: missing directory " + relative); + } + } else if (Files.isRegularFile(source)) { + if (!Files.isRegularFile(target) || Files.mismatch(source, target) != -1) { + throw new IllegalStateException("[migrate] verification failed: " + relative + " missing or divergent"); + } + } else { + throw new IllegalStateException("[migrate] verification failed: unsupported entry " + source); + } + } + } catch (IOException e) { + throw new IllegalStateException( + "[migrate] verification failed for " + required + " in " + container + ": " + e.getMessage(), e); + } + } + + /** + * Moves the reconciled legacy dir aside for rollback. Never deletes user + * data, never overwrites an existing backup: both throw fail-closed. + */ + private static void archiveLegacyDir(String baseDir, File oldDir) { + File backup = new File(baseDir, LEGACY_DIR_NAME + LEGACY_BACKUP_SUFFIX); + if (backup.exists()) { + throw new IllegalStateException( + "[migrate] refusing to boot: legacy backup already exists at " + backup + + " and legacy dir " + oldDir + " is still present. Resolve manually."); + } + try { + Files.move(oldDir.toPath(), backup.toPath()); + } catch (IOException e) { + throw new IllegalStateException( + "[migrate] failed to archive legacy dir " + oldDir + " to " + backup + ": " + e.getMessage(), e); + } + LOGGER.info("[migrate] archived legacy data directory from " + oldDir + " to " + backup); + } + + private static void deleteDirectoryRecursively(File dir) { + try (Stream walk = Files.walk(dir.toPath())) { + for (Path path : (Iterable) walk.sorted(Comparator.reverseOrder())::iterator) { + Files.delete(path); + } + } catch (IOException e) { + throw new IllegalStateException("[migrate] failed to clean staging dir " + dir + ": " + e.getMessage(), e); + } } private static void copyDirectoryRecursively(File source, File target) throws IOException { if (source.isDirectory()) { - if (!target.exists() && !target.mkdirs()) { + if (!target.exists() && !target.mkdirs() && !target.exists()) { throw new IOException("Failed to create directory " + target); } File[] children = source.listFiles(); @@ -321,8 +601,8 @@ private static void copyDirectoryRecursively(File source, File target) throws IO } } else { File parent = target.getParentFile(); - if (parent != null && !parent.exists()) { - parent.mkdirs(); + if (parent != null && !parent.exists() && !parent.mkdirs() && !parent.exists()) { + throw new IOException("Failed to create directory " + parent); } Files.copy(source.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING); } diff --git a/src/test/java/ConfigHelperTest.java b/src/test/java/ConfigHelperTest.java index 65ef9e4..b37a8bc 100644 --- a/src/test/java/ConfigHelperTest.java +++ b/src/test/java/ConfigHelperTest.java @@ -256,10 +256,184 @@ void testMigration_doesNotOverwriteExistingNewDir(@TempDir Path tmp) throws IOEx ConfigHelper.getLocalDataDirectory(); - // When new dir already exists, migration must NOT run - org.junit.jupiter.api.Assertions.assertTrue(Files.exists(oldDir), "old dir must remain when new dir already exists"); - org.junit.jupiter.api.Assertions.assertTrue(Files.exists(oldFile)); - org.junit.jupiter.api.Assertions.assertTrue(Files.exists(newFile)); + // Fail-closed merge: disjoint legacy files are merged in, never + // orphaned, and the legacy dir is archived (not left live). + Path mergedFile = newDir.resolve("settings").resolve("config-old.json"); + org.junit.jupiter.api.Assertions.assertTrue(Files.exists(mergedFile), + "disjoint legacy file must be merged into new dir"); + assertEquals("{\"a\":1}", new String(Files.readAllBytes(mergedFile), StandardCharsets.UTF_8)); + org.junit.jupiter.api.Assertions.assertTrue(Files.exists(newFile), "existing new file must be kept"); + org.junit.jupiter.api.Assertions.assertFalse(Files.exists(oldDir), "legacy dir must be archived, not left live"); + org.junit.jupiter.api.Assertions.assertTrue(Files.exists(tmp.resolve("CloudChains.bak")), + "legacy dir must be preserved as backup"); + } finally { + ConfigHelper.CONFIG_DIR = originalConfigDir; + } + } + + @Test + void testMigration_resumesPartialNewDir(@TempDir Path tmp) throws IOException { + String originalConfigDir = ConfigHelper.CONFIG_DIR; + try { + ConfigHelper.CONFIG_DIR = tmp.toString(); + Path oldDir = tmp.resolve("CloudChains"); + Path newDir = tmp.resolve("xlite-daemon"); + Files.createDirectories(oldDir.resolve("settings")); + Files.write(oldDir.resolve("settings").resolve("config-a.json"), + "{\"a\":1}".getBytes(StandardCharsets.UTF_8)); + Files.write(oldDir.resolve("settings").resolve("config-b.json"), + "{\"b\":2}".getBytes(StandardCharsets.UTF_8)); + // Simulate an interrupted migration: only one file made it over. + Files.createDirectories(newDir.resolve("settings")); + Files.write(newDir.resolve("settings").resolve("config-a.json"), + "{\"a\":1}".getBytes(StandardCharsets.UTF_8)); + + ConfigHelper.getLocalDataDirectory(); + + // Must complete the migration, never boot on the partial dir. + Path resumed = newDir.resolve("settings").resolve("config-b.json"); + org.junit.jupiter.api.Assertions.assertTrue(Files.exists(resumed), + "interrupted migration must resume and complete"); + assertEquals("{\"b\":2}", new String(Files.readAllBytes(resumed), StandardCharsets.UTF_8)); + org.junit.jupiter.api.Assertions.assertFalse(Files.exists(oldDir), "legacy dir must be archived after resume"); + } finally { + ConfigHelper.CONFIG_DIR = originalConfigDir; + } + } + + @Test + void testMigration_conflictingFilesThrowFailClosed(@TempDir Path tmp) throws IOException { + String originalConfigDir = ConfigHelper.CONFIG_DIR; + try { + ConfigHelper.CONFIG_DIR = tmp.toString(); + Path oldDir = tmp.resolve("CloudChains"); + Path newDir = tmp.resolve("xlite-daemon"); + Files.createDirectories(oldDir.resolve("settings")); + Files.createDirectories(newDir.resolve("settings")); + // Same relative path, different bytes: no safe automatic choice + // (either side could be the real wallet), so refuse to boot. + Files.write(oldDir.resolve("settings").resolve("config-c.json"), + "{\"c\":1}".getBytes(StandardCharsets.UTF_8)); + Files.write(newDir.resolve("settings").resolve("config-c.json"), + "{\"c\":2}".getBytes(StandardCharsets.UTF_8)); + // Plus a disjoint file: refusal must be side-effect-free, so even + // mergeable files must NOT be copied before the throw. + Files.write(oldDir.resolve("settings").resolve("config-d.json"), + "{\"d\":1}".getBytes(StandardCharsets.UTF_8)); + + assertThrows(IllegalStateException.class, ConfigHelper::getLocalDataDirectory); + org.junit.jupiter.api.Assertions.assertFalse( + Files.exists(newDir.resolve("settings").resolve("config-d.json")), + "refusal must leave the live dir untouched"); + } finally { + ConfigHelper.CONFIG_DIR = originalConfigDir; + } + } + + @Test + void testMigration_uncreatableDataDirThrowsFailClosed(@TempDir Path tmp) throws IOException { + String originalConfigDir = ConfigHelper.CONFIG_DIR; + try { + // Base is a regular file: no directory can ever be created beneath it. + Path blocker = tmp.resolve("blocker"); + Files.write(blocker, "x".getBytes(StandardCharsets.UTF_8)); + ConfigHelper.CONFIG_DIR = blocker.toString(); + + // Must fail loudly, never return a bogus path and boot empty. + assertThrows(IllegalStateException.class, ConfigHelper::getLocalDataDirectory); + } finally { + ConfigHelper.CONFIG_DIR = originalConfigDir; + } + } + + @Test + void testMigration_blankBaseDirThrowsFailClosed(@TempDir Path tmp) { + String originalConfigDir = ConfigHelper.CONFIG_DIR; + try { + // Whitespace-only override must fail fast. (A null CONFIG_DIR is + // treated exactly like the default empty string and resolves via + // App.getUserConfigDir, so it cannot be asserted hermetically.) + ConfigHelper.CONFIG_DIR = " "; + + assertThrows(IllegalStateException.class, ConfigHelper::getLocalDataDirectory); + } finally { + ConfigHelper.CONFIG_DIR = originalConfigDir; + } + } + + @Test + void testMigration_existingBackupRefusesToBoot(@TempDir Path tmp) throws IOException { + String originalConfigDir = ConfigHelper.CONFIG_DIR; + try { + ConfigHelper.CONFIG_DIR = tmp.toString(); + Path oldDir = tmp.resolve("CloudChains"); + Path newDir = tmp.resolve("xlite-daemon"); + Files.createDirectories(oldDir.resolve("settings")); + Files.createDirectories(newDir.resolve("settings")); + Files.write(oldDir.resolve("settings").resolve("config-old.json"), + "{\"a\":1}".getBytes(StandardCharsets.UTF_8)); + Files.write(newDir.resolve("settings").resolve("config-new.json"), + "{\"b\":2}".getBytes(StandardCharsets.UTF_8)); + // A previous archival already parked a backup here: archiving + // again would destroy rollback data, so refuse instead. + Files.createDirectories(tmp.resolve("CloudChains.bak")); + + assertThrows(IllegalStateException.class, ConfigHelper::getLocalDataDirectory); + // Nothing was archived over or deleted, and the refused merge + // copied nothing: the legacy source is intact for manual recovery. + org.junit.jupiter.api.Assertions.assertTrue(Files.exists(oldDir)); + org.junit.jupiter.api.Assertions.assertFalse( + Files.exists(newDir.resolve("settings").resolve("config-old.json")), + "refusal must leave the live dir untouched"); + } finally { + ConfigHelper.CONFIG_DIR = originalConfigDir; + } + } + + @Test + void testMigration_stagingLeftoverIsRedone(@TempDir Path tmp) throws IOException { + String originalConfigDir = ConfigHelper.CONFIG_DIR; + try { + ConfigHelper.CONFIG_DIR = tmp.toString(); + Path oldDir = tmp.resolve("CloudChains"); + Path newDir = tmp.resolve("xlite-daemon"); + Path staging = tmp.resolve("xlite-daemon.migrating"); + Files.createDirectories(oldDir.resolve("settings")); + Files.write(oldDir.resolve("settings").resolve("config-a.json"), + "{\"a\":1}".getBytes(StandardCharsets.UTF_8)); + // Leftover of an interrupted copy-verify-rename: stale junk only. + Files.createDirectories(staging); + Files.write(staging.resolve("junk.tmp"), "stale".getBytes(StandardCharsets.UTF_8)); + + ConfigHelper.getLocalDataDirectory(); + + // Redone from the intact source: junk gone, content verified. + org.junit.jupiter.api.Assertions.assertFalse(Files.exists(staging.resolve("junk.tmp")), + "stale staging content must not leak into the data dir"); + Path migrated = newDir.resolve("settings").resolve("config-a.json"); + org.junit.jupiter.api.Assertions.assertTrue(Files.exists(migrated)); + assertEquals("{\"a\":1}", new String(Files.readAllBytes(migrated), StandardCharsets.UTF_8)); + org.junit.jupiter.api.Assertions.assertFalse(Files.exists(oldDir), "legacy dir must be archived after redo"); + } finally { + ConfigHelper.CONFIG_DIR = originalConfigDir; + } + } + + @Test + void testSettingsAsFileFailsClosed(@TempDir Path tmp) throws IOException { + String originalConfigDir = ConfigHelper.CONFIG_DIR; + try { + ConfigHelper.CONFIG_DIR = tmp.toString(); + // Poison the settings path with a regular file: every config + // write below it would fail, so construction must refuse. + Path dataDir = tmp.resolve("xlite-daemon"); + Files.createDirectories(dataDir); + Files.write(dataDir.resolve("settings"), "not-a-dir".getBytes(StandardCharsets.UTF_8)); + + IllegalStateException e = assertThrows(IllegalStateException.class, + () -> new ConfigHelper("test")); + org.junit.jupiter.api.Assertions.assertTrue(e.getMessage().contains("data directory unusable"), + "message must name the failure, got: " + e.getMessage()); } finally { ConfigHelper.CONFIG_DIR = originalConfigDir; } diff --git a/src/test/java/TestHelper.java b/src/test/java/TestHelper.java index 9e21080..1dea62c 100644 --- a/src/test/java/TestHelper.java +++ b/src/test/java/TestHelper.java @@ -100,10 +100,14 @@ public void commonSetup() { } /** - * Common cleanup method for all test files. + * Common cleanup method for all test files. Pins CONFIG_DIR to the + * workdir sandbox: without this, cleanup after a test that restored the + * default empty CONFIG_DIR would resolve (and delete!) the real home + * data directory, depending on JUnit method order. */ @AfterAll public static void commonCleanup() { + ConfigHelper.CONFIG_DIR = "."; clean(); } @@ -113,6 +117,17 @@ public static void commonCleanup() { protected static void clean() { CoinInstance.getCoinInstances().clear(); assertTrue(deleteDir(new File(ConfigHelper.getLocalDataDirectory()))); + String base = ConfigHelper.CONFIG_DIR == null || ConfigHelper.CONFIG_DIR.trim().isEmpty() + ? io.xlite.daemon.app.App.getUserConfigDir() + : ConfigHelper.CONFIG_DIR; + File migrationLock = new File(base, ConfigHelper.MIGRATION_LOCK_FILE); + if (migrationLock.exists()) { + assertTrue(migrationLock.delete()); + } + File staging = new File(base, ConfigHelper.MIGRATION_STAGING_NAME); + if (staging.exists()) { + assertTrue(deleteDir(staging)); + } } /** diff --git a/src/test/java/io/xlite/daemon/app/util/ConfigHelperMigrationTest.java b/src/test/java/io/xlite/daemon/app/util/ConfigHelperMigrationTest.java new file mode 100644 index 0000000..6bc1158 --- /dev/null +++ b/src/test/java/io/xlite/daemon/app/util/ConfigHelperMigrationTest.java @@ -0,0 +1,78 @@ +package io.xlite.daemon.app.util; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Direct coverage for migration steps the public path cannot force on a + * single test filesystem: the staged copy-verify-rename fallback (only + * taken when an atomic move is unsupported) and the constructor's + * fail-closed propagation. + */ +class ConfigHelperMigrationTest { + + @Test + void testCopyVerifyRenameFallback(@TempDir Path tmp) throws IOException { + Path oldDir = tmp.resolve("CloudChains"); + Path newDir = tmp.resolve("xlite-daemon"); + Path staging = tmp.resolve("xlite-daemon.migrating"); + Files.createDirectories(oldDir.resolve("settings")); + Files.write(oldDir.resolve("settings").resolve("config-a.json"), + "{\"a\":1}".getBytes(StandardCharsets.UTF_8)); + Files.write(oldDir.resolve("key.dat"), new byte[]{1, 2, 3, 4}); + + ConfigHelper.copyVerifyRename(oldDir.toFile(), newDir.toFile(), staging.toFile()); + + // Verified content landed via staging; staging renamed away (gone). + assertEquals("{\"a\":1}", new String( + Files.readAllBytes(newDir.resolve("settings").resolve("config-a.json")), + StandardCharsets.UTF_8)); + assertTrue(Files.exists(newDir.resolve("key.dat"))); + assertFalse(Files.exists(staging), "staging must be renamed to the data dir, not left behind"); + // Legacy source untouched throughout: archiving is the caller's job. + assertTrue(Files.exists(oldDir.resolve("settings").resolve("config-a.json"))); + } + + @Test + void testCopyVerifyRenameFailureThrows(@TempDir Path tmp) throws IOException { + Path oldDir = tmp.resolve("CloudChains"); + Path newDir = tmp.resolve("xlite-daemon"); + Files.createDirectories(oldDir); + // Staging parent is a regular file: nothing can be copied there. + Path blocker = tmp.resolve("blocker"); + Files.write(blocker, "x".getBytes(StandardCharsets.UTF_8)); + Path staging = blocker.resolve("xlite-daemon.migrating"); + + assertThrows(IllegalStateException.class, + () -> ConfigHelper.copyVerifyRename(oldDir.toFile(), newDir.toFile(), staging.toFile())); + assertFalse(Files.exists(newDir), "failed fallback must not leave a partial data dir"); + } + + @Test + void testConstructorPropagatesUnusableDataDir(@TempDir Path tmp) throws IOException { + String saved = ConfigHelper.CONFIG_DIR; + try { + Path blocker = tmp.resolve("blocker"); + Files.write(blocker, "x".getBytes(StandardCharsets.UTF_8)); + ConfigHelper.CONFIG_DIR = blocker.toString(); + + // Must propagate fail-closed, never boot on defaults with no wallet. + IllegalStateException e = assertThrows(IllegalStateException.class, + () -> new ConfigHelper("test")); + assertTrue(e.getMessage().contains("data directory unusable"), + "message must name the failure, got: " + e.getMessage()); + } finally { + ConfigHelper.CONFIG_DIR = saved; + } + } +} From 206f0e3c071fae7c1ffa00874b07daddc3b4c147 Mon Sep 17 00:00:00 2001 From: tryiou Date: Wed, 16 Sep 2026 14:22:26 +0200 Subject: [PATCH 73/73] fix(app): fail fast on missing platform config dir resolveUserConfigDir returned a null AppData verbatim on Windows, which stringified into a bogus relative null/xlite-daemon dir and forked the wallet; a null user.home did the same on mac/Linux. Throw IllegalStateException with an XLITE_DATA_HOME hint instead, and exit 1 with a clean message from main rather than an uncaught stack trace. Route the log-dir builders through ConfigHelper.DATA_DIR_NAME. Tests: Windows/mac/Linux blank-default vectors assert the throw (the old verbatim-null expectation is replaced); AppTest now sandboxes CONFIG_DIR so class init never touches the real home. Full suite 191/191 green. --- README.md | 5 ++ docs/USER_GUIDE.md | 5 ++ src/main/java/io/xlite/daemon/app/App.java | 31 ++++++++++-- .../daemon/app/util/LogRotationUtil.java | 2 +- .../java/io/xlite/daemon/app/AppTest.java | 48 +++++++++++++++++-- 5 files changed, 84 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d349308..503d2ac 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,11 @@ Linux ~/.config/xlite-daemon/settings/config-*.json ``` +Set `XLITE_DATA_HOME` to override the platform directory above. The +daemon aborts at startup when no override is set and the platform +default is missing (Windows with unset `%AppData%`, or an empty +`user.home`). + ## Contributing diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index f6f7a68..b7ff508 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -121,6 +121,11 @@ The daemon stores configuration files in your system's application data director - **macOS**: `~/Library/Application Support/xlite-daemon/settings/config-*.json` - **Linux**: `~/.config/xlite-daemon/settings/config-*.json` +Set `XLITE_DATA_HOME` to override the platform directory above. The +daemon aborts at startup when no override is set and the platform +default is missing (Windows with unset `%AppData%`, or an empty +`user.home`). + ### Configuration Structure Each cryptocurrency has its own configuration file named `config-{ticker}.json`: diff --git a/src/main/java/io/xlite/daemon/app/App.java b/src/main/java/io/xlite/daemon/app/App.java index 04f43ed..25a715c 100644 --- a/src/main/java/io/xlite/daemon/app/App.java +++ b/src/main/java/io/xlite/daemon/app/App.java @@ -6,6 +6,7 @@ import io.xlite.daemon.app.net.api.http.client.EXRServerPool; import io.xlite.daemon.app.net.api.http.client.HTTPClient; import io.xlite.daemon.app.util.ConsoleFormatter; +import io.xlite.daemon.app.util.ConfigHelper; import io.xlite.daemon.app.util.FileFormatter; import io.xlite.daemon.app.util.LogRotationUtil; import io.github.cdimascio.dotenv.Dotenv; @@ -57,9 +58,10 @@ public static String getEnv(String key) { * * @param dataHomeEnv value of {@code XLITE_DATA_HOME} (may be null) * @param osName value of the {@code os.name} system property - * @param userHome value of the {@code user.home} system property - * @param appDataEnv Windows {@code AppData} environment value (may be null) + * @param userHome value of the {@code user.home} system property (must be non-blank on mac/Linux) + * @param appDataEnv Windows {@code AppData} environment value (must be non-blank on Windows) * @return the resolved config-root directory string + * @throws IllegalStateException when no override is set and the platform default is missing */ public static String resolveUserConfigDir(String dataHomeEnv, String osName, String userHome, String appDataEnv) { if (dataHomeEnv != null && !dataHomeEnv.trim().isEmpty()) { @@ -67,10 +69,27 @@ public static String resolveUserConfigDir(String dataHomeEnv, String osName, Str } String OS = osName.toLowerCase(); if (OS.contains("win")) { + // Fail fast: returning a null AppData would stringify into a + // bogus relative "null/xlite-daemon/" dir and fork the wallet. + if (appDataEnv == null || appDataEnv.trim().isEmpty()) { + throw new IllegalStateException( + "[app] Cannot resolve config dir on Windows: AppData is not set. " + + "Set XLITE_DATA_HOME to an explicit path."); + } return appDataEnv; } else if (OS.contains("mac")) { + if (userHome == null || userHome.trim().isEmpty()) { + throw new IllegalStateException( + "[app] Cannot resolve config dir on macOS: user.home is not set. " + + "Set XLITE_DATA_HOME to an explicit path."); + } return userHome + File.separator + "Library" + File.separator + "Application Support"; } + if (userHome == null || userHome.trim().isEmpty()) { + throw new IllegalStateException( + "[app] Cannot resolve config dir: user.home is not set. " + + "Set XLITE_DATA_HOME to an explicit path."); + } return userHome + File.separator + ".config"; } @@ -157,7 +176,7 @@ public static void main(String[] args) { try { String userHomeDir = getUserConfigDir(); - String logDir = userHomeDir + File.separator + "xlite-daemon"; + String logDir = userHomeDir + File.separator + ConfigHelper.DATA_DIR_NAME; DateTimeFormatter timeStampPattern = DateTimeFormatter.ofPattern("yyyy-MM-dd"); Handler fileHandler = new FileHandler( logDir + File.separator + "error-" + timeStampPattern.format(LocalDateTime.now()) + ".log", @@ -173,6 +192,12 @@ public static void main(String[] args) { } catch (IOException e) { LOGGER.warning("[app] Failed to initialize file handler: " + e.getMessage()); + } catch (IllegalStateException e) { + // Fail-fast config errors (unset AppData/user.home, unusable data + // dir) abort startup with a clean message and nonzero status + // instead of an uncaught stack trace. + LOGGER.severe("[app] " + e.getMessage()); + System.exit(1); } ConsoleHandler consoleHandler = new ConsoleHandler(){ diff --git a/src/main/java/io/xlite/daemon/app/util/LogRotationUtil.java b/src/main/java/io/xlite/daemon/app/util/LogRotationUtil.java index 2c00bbc..137dd3c 100644 --- a/src/main/java/io/xlite/daemon/app/util/LogRotationUtil.java +++ b/src/main/java/io/xlite/daemon/app/util/LogRotationUtil.java @@ -24,7 +24,7 @@ public class LogRotationUtil { public static void performLogRotation() { try { String userHomeDir = App.getUserConfigDir(); - String logDirectoryPath = userHomeDir + File.separator + "xlite-daemon"; + String logDirectoryPath = userHomeDir + File.separator + ConfigHelper.DATA_DIR_NAME; // Get retention days from environment variable or use default int retentionDays = getRetentionDaysFromEnvironment(); diff --git a/src/test/java/io/xlite/daemon/app/AppTest.java b/src/test/java/io/xlite/daemon/app/AppTest.java index a391333..dfe10b9 100644 --- a/src/test/java/io/xlite/daemon/app/AppTest.java +++ b/src/test/java/io/xlite/daemon/app/AppTest.java @@ -1,9 +1,14 @@ package io.xlite.daemon.app; +import io.xlite.daemon.app.util.ConfigHelper; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import java.io.File; +import java.nio.file.Path; import static org.junit.jupiter.api.Assertions.*; @@ -17,6 +22,27 @@ class AppTest { private static final String USER_HOME = "/home/tester"; private static final String APPDATA = "C:\\Users\\tester\\AppData\\Roaming"; + @TempDir + static Path sandbox; + private static String savedConfigDir; + + /** + * Isolate from the real home dir: touching {@code App} triggers class + * init, which builds live objects against {@code CONFIG_DIR}. Without + * this, the suite reads (and previously even migrated) the developer's + * real {@code ~/.config}. + */ + @BeforeAll + static void isolateConfigDir() { + savedConfigDir = ConfigHelper.CONFIG_DIR; + ConfigHelper.CONFIG_DIR = sandbox.toString(); + } + + @AfterAll + static void restoreConfigDir() { + ConfigHelper.CONFIG_DIR = savedConfigDir; + } + @Test @DisplayName("XLITE_DATA_HOME overrides the linux default") void testResolve_EnvOverrideWinsOnLinux() { @@ -80,8 +106,24 @@ void testResolve_RelativeEnvNormalizedToAbsolute() { } @Test - @DisplayName("windows default keeps legacy verbatim pass-through of AppData (may be null)") - void testResolve_WindowsDefaultPassesAppDataVerbatim() { - assertNull(App.resolveUserConfigDir(null, "Windows 11", USER_HOME, null)); + @DisplayName("windows default without AppData fails fast instead of yielding a null dir") + void testResolve_WindowsDefaultWithoutAppDataThrows() { + for (String blank : new String[]{null, "", " "}) { + IllegalStateException e = assertThrows(IllegalStateException.class, + () -> App.resolveUserConfigDir(null, "Windows 11", USER_HOME, blank)); + assertTrue(e.getMessage().contains("XLITE_DATA_HOME"), + "message must point at the override, got: " + e.getMessage()); + } + } + + @Test + @DisplayName("mac/linux default without user.home fails fast instead of yielding a null dir") + void testResolve_NonWindowsDefaultWithoutUserHomeThrows() { + for (String blank : new String[]{null, "", " "}) { + assertThrows(IllegalStateException.class, + () -> App.resolveUserConfigDir(null, "Linux", blank, APPDATA)); + assertThrows(IllegalStateException.class, + () -> App.resolveUserConfigDir(null, "Mac OS X", blank, null)); + } } }