remove pricenode from main repo
This commit is contained in:
parent
15d2c24a82
commit
c194e0b5ed
78
build.gradle
78
build.gradle
@ -64,7 +64,6 @@ configure(subprojects) {
|
||||
junitVersion = '4.12'
|
||||
jupiterVersion = '5.7.0'
|
||||
kotlinVersion = '1.3.41'
|
||||
knowmXchangeVersion = '5.0.13'
|
||||
langVersion = '3.11'
|
||||
logbackVersion = '1.1.11'
|
||||
loggingVersion = '1.2'
|
||||
@ -77,7 +76,6 @@ configure(subprojects) {
|
||||
qrgenVersion = '1.3'
|
||||
slf4jVersion = '1.7.30'
|
||||
sparkVersion = '2.5.2'
|
||||
springBootVersion = '2.5.6'
|
||||
|
||||
os = osdetector.os == 'osx' ? 'mac' : osdetector.os == 'windows' ? 'win' : osdetector.os
|
||||
}
|
||||
@ -115,7 +113,6 @@ configure([project(':cli'),
|
||||
project(':relay'),
|
||||
project(':seednode'),
|
||||
project(':statsnode'),
|
||||
project(':pricenode'),
|
||||
project(':inventory'),
|
||||
project(':apitest')]) {
|
||||
|
||||
@ -678,81 +675,6 @@ configure(project(':monitor')) {
|
||||
}
|
||||
|
||||
|
||||
configure(project(':pricenode')) {
|
||||
apply plugin: "org.springframework.boot"
|
||||
|
||||
apply plugin: 'io.spring.dependency-management'
|
||||
|
||||
mainClassName = 'bisq.price.Main'
|
||||
|
||||
version = file("src/main/resources/version.txt").text.trim()
|
||||
|
||||
jar.manifest.attributes(
|
||||
"Implementation-Title": project.name,
|
||||
"Implementation-Version": version)
|
||||
|
||||
ext['log4j2.version'] = '2.17.0'
|
||||
|
||||
dependencies {
|
||||
implementation project(":common")
|
||||
implementation project(":core")
|
||||
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
|
||||
compileOnly "org.projectlombok:lombok:$lombokVersion"
|
||||
implementation "com.google.code.gson:gson:$gsonVersion"
|
||||
implementation "com.google.guava:guava:$guavaVersion"
|
||||
implementation "commons-codec:commons-codec:$codecVersion"
|
||||
implementation "org.apache.httpcomponents:httpcore:$httpcoreVersion"
|
||||
implementation("org.apache.httpcomponents:httpclient:$httpclientVersion") {
|
||||
exclude(module: 'commons-codec')
|
||||
}
|
||||
implementation("org.knowm.xchange:xchange-binance:$knowmXchangeVersion")
|
||||
implementation("org.knowm.xchange:xchange-bitbay:$knowmXchangeVersion")
|
||||
implementation("org.knowm.xchange:xchange-bitfinex:$knowmXchangeVersion")
|
||||
implementation("org.knowm.xchange:xchange-bitflyer:$knowmXchangeVersion")
|
||||
implementation("org.knowm.xchange:xchange-bitstamp:$knowmXchangeVersion")
|
||||
implementation("org.knowm.xchange:xchange-btcmarkets:$knowmXchangeVersion")
|
||||
implementation("org.knowm.xchange:xchange-cexio:$knowmXchangeVersion")
|
||||
implementation("org.knowm.xchange:xchange-coinbasepro:$knowmXchangeVersion")
|
||||
implementation("org.knowm.xchange:xchange-coinmarketcap:$knowmXchangeVersion")
|
||||
implementation("org.knowm.xchange:xchange-coinmate:$knowmXchangeVersion")
|
||||
implementation("org.knowm.xchange:xchange-coinone:$knowmXchangeVersion")
|
||||
implementation("org.knowm.xchange:xchange-exmo:$knowmXchangeVersion")
|
||||
implementation("org.knowm.xchange:xchange-hitbtc:$knowmXchangeVersion")
|
||||
implementation("org.knowm.xchange:xchange-huobi:$knowmXchangeVersion")
|
||||
implementation("org.knowm.xchange:xchange-independentreserve:$knowmXchangeVersion")
|
||||
implementation("org.knowm.xchange:xchange-kraken:$knowmXchangeVersion")
|
||||
implementation("org.knowm.xchange:xchange-luno:$knowmXchangeVersion")
|
||||
implementation("org.knowm.xchange:xchange-mercadobitcoin:$knowmXchangeVersion")
|
||||
implementation("org.knowm.xchange:xchange-paribu:$knowmXchangeVersion")
|
||||
implementation("org.knowm.xchange:xchange-poloniex:$knowmXchangeVersion")
|
||||
implementation("org.knowm.xchange:xchange-quoine:$knowmXchangeVersion")
|
||||
implementation("org.springframework.boot:spring-boot-starter-web:$springBootVersion")
|
||||
testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion"
|
||||
testCompileOnly "org.projectlombok:lombok:$lombokVersion"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api:$jupiterVersion"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-params:$jupiterVersion"
|
||||
testImplementation "org.mockito:mockito-core:$mockitoVersion"
|
||||
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$jupiterVersion")
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
|
||||
// Disabled by default, since spot provider tests include connections to external API endpoints
|
||||
// Can be enabled by adding -Dtest.pricenode.includeSpotProviderTests=true to the gradle command:
|
||||
// ./gradlew test -Dtest.pricenode.includeSpotProviderTests=true
|
||||
if (System.properties['test.pricenode.includeSpotProviderTests'] != 'true') {
|
||||
project.logger.lifecycle('Pricenode: Skipping spot provider tests')
|
||||
exclude 'bisq/price/spot/providers/**'
|
||||
}
|
||||
}
|
||||
|
||||
task stage {
|
||||
dependsOn assemble
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
configure(project(':relay')) {
|
||||
mainClassName = 'bisq.relay.RelayMain'
|
||||
|
||||
|
@ -1 +0,0 @@
|
||||
web: if [ "$HIDDEN" == true ]; then ./tor/bin/run_tor java -jar -Dserver.port=$PORT build/libs/haveno-pricenode.jar; else java -jar -Dserver.port=$PORT build/libs/haveno-pricenode.jar; fi
|
@ -1,42 +0,0 @@
|
||||
Deploy on Heroku
|
||||
--------
|
||||
|
||||
Run the following commands:
|
||||
|
||||
heroku create
|
||||
heroku buildpacks:add heroku/gradle
|
||||
git push heroku master
|
||||
curl https://your-app-123456.herokuapp.com/getAllMarketPrices
|
||||
|
||||
To register the node as a Tor hidden service, first install the Heroku Tor buildpack:
|
||||
|
||||
heroku buildpacks:add https://github.com/cbeams/heroku-buildpack-tor.git
|
||||
git push heroku master
|
||||
|
||||
> NOTE: this deployment will take a while, because the new buildpack must download and build Tor from source.
|
||||
|
||||
Next, generate your Tor hidden service private key and .onion address:
|
||||
|
||||
heroku run bash
|
||||
./tor/bin/tor -f torrc
|
||||
|
||||
When the process reports that it is "100% bootstrapped", kill it, then copy the generated private key and .onion hostname values:
|
||||
|
||||
cat build/tor-hidden-service/hostname
|
||||
cat build/tor-hidden-service/private_key
|
||||
exit
|
||||
|
||||
> IMPORTANT: Save the private key value in a secure location so that this node can be re-created elsewhere with the same .onion address in the future if necessary.
|
||||
|
||||
Now configure the hostname and private key values as environment variables for your Heroku app:
|
||||
|
||||
heroku config:set HIDDEN=true HIDDEN_DOT_ONION=[your .onion] HIDDEN_PRIVATE_KEY="[your tor privkey]"
|
||||
git push heroku master
|
||||
|
||||
When the application finishes restarting, you should still be able to access it via the clearnet, e.g. with:
|
||||
|
||||
curl https://your-app-123456.herokuapp.com/getAllMarketPrices
|
||||
|
||||
And via your Tor Browser at:
|
||||
|
||||
http://$YOUR_ONION/getAllMarketPrices
|
@ -1,84 +0,0 @@
|
||||
# Haveno-pricenode
|
||||
|
||||
## Overview
|
||||
|
||||
The Haveno pricenode is a simple HTTP service that fetches, transforms and relays data from third-party price providers to Haveno exchange clients on request. Available prices include:
|
||||
|
||||
- Bitcoin exchange rates, available at `/getAllMarketPrices`, and
|
||||
- Bitcoin mining fee rates, available at `/getFees`
|
||||
|
||||
Pricenodes are deployed in production as Tor hidden services. This is not because the location of these nodes needs to be kept secret, but rather so that Haveno exchange clients do not need to exit the Tor network in order to get price data.
|
||||
|
||||
Anyone can run a pricenode, but it must be _discoverable_ in order for it to do any good. For exchange clients to discover your pricenode, its .onion address must be hard-coded in the Haveno exchange client's `ProvidersRepository` class. Alternatively, users can point explicitly to given pricenode (or set of pricenodes) with the exchange client's `--providers` command line option.
|
||||
|
||||
Pricenodes can be deployed anywhere Java and Tor binaries can be run. Instructions below cover deployment on localhost, and instructions [how to deploy on Heroku](README-HEROKU.md) are also available.
|
||||
|
||||
Pricenodes should be cheap to run with regard to both time and money. The application itself is non-resource intensive and can be run on the low-end of most providers' paid tiers.
|
||||
|
||||
A pricenode operator's main responsibilities are to ensure their node(s) are available and up-to-date. Releases are currently source-only, with the assumption that most operators will favor Git-based "push to deploy" workflows.
|
||||
|
||||
## Prerequisites for running a pricenode
|
||||
|
||||
To run a pricenode, you will need:
|
||||
|
||||
- JDK 8 if you want to build and run a node locally.
|
||||
- The `tor` binary (e.g. `brew install tor`) if you want to run a hidden service locally.
|
||||
|
||||
## How to deploy for production
|
||||
|
||||
### Install
|
||||
|
||||
Run the one-command installer:
|
||||
|
||||
```bash
|
||||
curl -s https://raw.githubusercontent.com/haveno-dex/haveno/master/pricenode/install_pricenode_debian.sh | sudo bash
|
||||
```
|
||||
|
||||
This will install the pricenode under the user `pricenode`. At the end of the installer script, it should print your Tor onion hostname.
|
||||
|
||||
### Test
|
||||
|
||||
To manually test endpoints, run each of the following:
|
||||
|
||||
``` bash
|
||||
curl http://localhost:8078/getAllMarketPrices
|
||||
curl http://localhost:8078/getFees
|
||||
curl http://localhost:8078/getParams
|
||||
curl http://localhost:8078/info
|
||||
```
|
||||
|
||||
### Monitoring
|
||||
|
||||
If you run a main pricenode, you also are obliged to activate the monitoring feed by running
|
||||
|
||||
```bash
|
||||
bash <(curl -s https://raw.githubusercontent.com/haveno-dex/haveno/master/monitor/install_collectd_debian.sh)
|
||||
```
|
||||
|
||||
Furthermore, you are obliged to provide network size data to the monitor by running
|
||||
|
||||
```bash
|
||||
curl -s https://raw.githubusercontent.com/haveno-dex/Haveno/master/pricenode/install_networksize_debian.sh | sudo bash
|
||||
```
|
||||
|
||||
### Updating
|
||||
|
||||
Update your Haveno code in /Haveno with ```git pull```
|
||||
|
||||
Then build an updated pricenode:
|
||||
|
||||
```./gradlew :pricenode:installDist -x test```
|
||||
|
||||
## How to deploy elsewhere
|
||||
|
||||
- [README-HEROKU.md](README-HEROKU.md)
|
||||
- [docker/README.md](docker/README.md)
|
||||
|
||||
|
||||
## Bitcoin mining fee estimates
|
||||
|
||||
The pricenode exposes a service API to Haveno clients under `/getFees`.
|
||||
|
||||
This API returns a mining fee rate estimate, representing an average of several mining fee rate values retrieved from different `mempool.space` instances.
|
||||
|
||||
To configure which `mempool.space` instances are queried to calculate this average, see the relevant section in the file `application.properties`.
|
@ -1,5 +0,0 @@
|
||||
LoadPlugin exec
|
||||
|
||||
<Plugin exec>
|
||||
Exec "__USER_GROUP__" "__SCRAPERSCRIPT__"
|
||||
</Plugin>
|
@ -1,33 +0,0 @@
|
||||
###
|
||||
# Haveno pricenode dockerfile
|
||||
###
|
||||
|
||||
# pull base image
|
||||
FROM openjdk:11-jdk
|
||||
|
||||
# install tor
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
tor && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# copy tor configuration file
|
||||
COPY torrc /etc/tor/
|
||||
# give proper permissions for tor configuration file
|
||||
RUN chown debian-tor:debian-tor /etc/tor/torrc
|
||||
# add haveno user
|
||||
RUN useradd -d /haveno -G debian-tor haveno
|
||||
# make haveno directory
|
||||
RUN mkdir -p /haveno
|
||||
# give haveno user proper permissions
|
||||
RUN chown haveno:haveno /haveno
|
||||
# clone haveno repository
|
||||
RUN git clone https://github.com/haveno-dex/haveno.git /haveno/haveno
|
||||
# build pricenode
|
||||
WORKDIR /haveno/haveno
|
||||
RUN ./gradlew :pricenode:installDist -x test
|
||||
# set proper java options
|
||||
ENV JAVA_OPTS=""
|
||||
# expose ports
|
||||
EXPOSE 80
|
||||
EXPOSE 8078
|
||||
# set launch command (tor and pricenode)
|
||||
CMD tor && /haveno/haveno/haveno-pricenode 2
|
@ -1,26 +0,0 @@
|
||||
Needed software to start a pricenode
|
||||
==
|
||||
|
||||
* docker
|
||||
* docker-compose
|
||||
|
||||
How to start
|
||||
==
|
||||
|
||||
`docker compose up -d`
|
||||
|
||||
|
||||
How to monitor
|
||||
==
|
||||
|
||||
See if it's running: `docker ps`
|
||||
|
||||
Check the logs: `docker-compose logs`
|
||||
|
||||
Check the tor hostname: `docker exec docker_pricenode_1 cat /var/lib/tor/pricenode/hostname`
|
||||
|
||||
|
||||
How to test
|
||||
==
|
||||
|
||||
Refer to the main pricenode [README](../README.md).
|
@ -1,10 +0,0 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
pricenode:
|
||||
restart: unless-stopped
|
||||
build:
|
||||
context: .
|
||||
ports:
|
||||
- 80:80
|
||||
- 8078:8078
|
@ -1,4 +0,0 @@
|
||||
HiddenServiceDir /var/lib/tor/pricenode/
|
||||
HiddenServicePort 80 127.0.0.1:8078
|
||||
HiddenServiceVersion 3
|
||||
RunAsDaemon 1
|
@ -1 +0,0 @@
|
||||
JAVA_OPTS="-XX:+ExitOnOutOfMemoryError"
|
@ -1,25 +0,0 @@
|
||||
[Unit]
|
||||
Description=Haveno Price Node
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
SyslogIdentifier=Haveno-pricenode
|
||||
EnvironmentFile=/etc/default/haveno-pricenode.env
|
||||
ExecStart=/pricenode/haveno/haveno-pricenode 2
|
||||
ExecStop=/bin/kill -TERM ${MAINPID}
|
||||
Restart=on-failure
|
||||
|
||||
User=pricenode
|
||||
Group=pricenode
|
||||
|
||||
PrivateTmp=true
|
||||
ProtectSystem=full
|
||||
NoNewPrivileges=true
|
||||
PrivateDevices=true
|
||||
MemoryDenyWriteExecute=false
|
||||
ProtectControlGroups=true
|
||||
ProtectKernelTunables=true
|
||||
RestrictSUIDSGID=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
@ -1,41 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
echo "[*] Network Size Monitoring installation script"
|
||||
|
||||
##### change paths if necessary for your system
|
||||
|
||||
ROOT_USER=root
|
||||
|
||||
SCRAPER_HOME=/journalreader
|
||||
SCRAPER_USER=journalreader
|
||||
SCRAPER_GROUP=systemd-journal
|
||||
|
||||
#####
|
||||
echo "[*] Checking environment..."
|
||||
if [ ! -f "/etc/collectd/collectd.conf" ]; then
|
||||
echo 'Collectd is not installed. Did you do the install_monitoring_debian.sh?'
|
||||
echo 'Exiting...'
|
||||
exit
|
||||
fi
|
||||
if ! grep -q "journalreader" /etc/passwd; then
|
||||
echo 'User not found. Did you run the install_networksize_debian.sh?'
|
||||
echo 'Exiting...'
|
||||
exit
|
||||
fi
|
||||
|
||||
echo "[*] Installing journal parser script"
|
||||
curl -s https://raw.githubusercontent.com/bisq-network/bisq/master/pricenode/journalscraper_hsversion.sh > /tmp/journalscraper_hsversion.sh
|
||||
sudo -H -i -u "${ROOT_USER}" install -c -o "${SCRAPER_USER}" -g "${SCRAPER_GROUP}" -m 744 /tmp/journalscraper_hsversion.sh "${SCRAPER_HOME}/scraperscript_hsversion.sh"
|
||||
|
||||
echo "[*] Installing collectd config"
|
||||
curl -s https://raw.githubusercontent.com/bisq-network/bisq/master/pricenode/collectd.conf.snippet > /tmp/collectd.conf.snippet
|
||||
sudo -H -i -u "${ROOT_USER}" sed -i -e "s/LoadPlugin exec//" /tmp/collectd.conf.snippet
|
||||
sudo -H -i -u "${ROOT_USER}" /bin/sh -c "cat /tmp/collectd.conf.snippet >> /etc/collectd/collectd.conf"
|
||||
sudo -H -i -u "${ROOT_USER}" sed -i -e "s/__USER_GROUP__/${SCRAPER_USER}:${SCRAPER_GROUP}/" /etc/collectd/collectd.conf
|
||||
sudo -H -i -u "${ROOT_USER}" sed -i -e "s!__SCRAPERSCRIPT__!${SCRAPER_HOME}/scraperscript_hsversion.sh!" /etc/collectd/collectd.conf
|
||||
|
||||
echo "[*] Restarting services"
|
||||
sudo -H -i -u "${ROOT_USER}" systemctl restart collectd.service
|
||||
|
||||
echo '[*] Done!'
|
@ -1,42 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
echo "[*] Network Size Monitoring installation script"
|
||||
|
||||
##### change paths if necessary for your system
|
||||
|
||||
ROOT_USER=root
|
||||
|
||||
SCRAPER_HOME=/journalreader
|
||||
SCRAPER_USER=journalreader
|
||||
SCRAPER_GROUP=systemd-journal
|
||||
|
||||
#####
|
||||
echo "[*] Checking environment..."
|
||||
if [ ! -f "/etc/collectd/collectd.conf" ]; then
|
||||
echo 'Collectd is not installed. Did you do the install_monitoring_debian.sh?'
|
||||
echo 'Exiting...'
|
||||
exit
|
||||
fi
|
||||
|
||||
echo "[*] Creating journal reader user"
|
||||
sudo -H -i -u "${ROOT_USER}" useradd -d "${SCRAPER_HOME}" -G "${SCRAPER_GROUP}" "${SCRAPER_USER}"
|
||||
sudo -H -i -u "${ROOT_USER}" mkdir -p "${SCRAPER_HOME}"
|
||||
sudo -H -i -u "${ROOT_USER}" chown "${SCRAPER_USER}":"${SCRAPER_GROUP}" ${SCRAPER_HOME}
|
||||
|
||||
echo "[*] Installing journal parser script"
|
||||
curl -s https://raw.githubusercontent.com/bisq-network/bisq/master/pricenode/journalscraper.sh > /tmp/journalscraper.sh
|
||||
sudo -H -i -u "${ROOT_USER}" install -c -o "${SCRAPER_USER}" -g "${SCRAPER_GROUP}" -m 744 /tmp/journalscraper.sh "${SCRAPER_HOME}/scraperscript.sh"
|
||||
|
||||
echo "[*] Installing collectd config"
|
||||
curl -s https://raw.githubusercontent.com/bisq-network/bisq/master/pricenode/collectd.conf.snippet > /tmp/collectd.conf.snippet
|
||||
sudo -H -i -u "${ROOT_USER}" /bin/sh -c "cat /tmp/collectd.conf.snippet >> /etc/collectd/collectd.conf"
|
||||
sudo -H -i -u "${ROOT_USER}" sed -i -e "s/__USER_GROUP__/${SCRAPER_USER}:${SCRAPER_GROUP}/" /etc/collectd/collectd.conf
|
||||
sudo -H -i -u "${ROOT_USER}" sed -i -e "s!__SCRAPERSCRIPT__!${SCRAPER_HOME}/scraperscript.sh!" /etc/collectd/collectd.conf
|
||||
|
||||
sudo -H -i -u "${ROOT_USER}" systemctl enable collectd.service
|
||||
|
||||
echo "[*] Restarting services"
|
||||
sudo -H -i -u "${ROOT_USER}" systemctl restart collectd.service
|
||||
|
||||
echo '[*] Done!'
|
@ -1,90 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
echo "[*] haveno-pricenode installation script"
|
||||
|
||||
##### change as necessary for your system
|
||||
|
||||
SYSTEMD_SERVICE_HOME=/etc/systemd/system
|
||||
SYSTEMD_ENV_HOME=/etc/default
|
||||
|
||||
ROOT_USER=root
|
||||
ROOT_GROUP=root
|
||||
#ROOT_HOME=/root
|
||||
|
||||
HAVENO_USER=pricenode
|
||||
HAVENO_GROUP=pricenode
|
||||
HAVENO_HOME=/pricenode
|
||||
|
||||
HAVENO_REPO_URL=https://github.com/haveno-dex/haveno
|
||||
HAVENO_REPO_NAME=haveno
|
||||
HAVENO_REPO_TAG=master
|
||||
HAVENO_LATEST_RELEASE=master
|
||||
HAVENO_TORHS=pricenode
|
||||
|
||||
TOR_PKG="tor"
|
||||
#TOR_USER=debian-tor
|
||||
TOR_GROUP=debian-tor
|
||||
TOR_CONF=/etc/tor/torrc
|
||||
TOR_RESOURCES=/var/lib/tor
|
||||
|
||||
#####
|
||||
|
||||
echo "[*] Upgrading apt packages"
|
||||
sudo -H -i -u "${ROOT_USER}" DEBIAN_FRONTEND=noninteractive apt-get update -q
|
||||
sudo -H -i -u "${ROOT_USER}" DEBIAN_FRONTEND=noninteractive apt-get upgrade -qq -y
|
||||
|
||||
echo "[*] Installing Tor"
|
||||
sudo -H -i -u "${ROOT_USER}" DEBIAN_FRONTEND=noninteractive apt-get install -qq -y "${TOR_PKG}"
|
||||
|
||||
echo "[*] Adding Tor configuration"
|
||||
if ! grep "${HAVENO_TORHS}" /etc/tor/torrc >/dev/null 2>&1;then
|
||||
sudo -H -i -u "${ROOT_USER}" sh -c "echo HiddenServiceDir ${TOR_RESOURCES}/${HAVENO_TORHS}/ >> ${TOR_CONF}"
|
||||
sudo -H -i -u "${ROOT_USER}" sh -c "echo HiddenServicePort 80 127.0.0.1:8078 >> ${TOR_CONF}"
|
||||
sudo -H -i -u "${ROOT_USER}" sh -c "echo HiddenServiceVersion 3 >> ${TOR_CONF}"
|
||||
fi
|
||||
|
||||
echo "[*] Creating Haveno user with Tor access"
|
||||
sudo -H -i -u "${ROOT_USER}" useradd -d "${HAVENO_HOME}" -G "${TOR_GROUP}" "${HAVENO_USER}"
|
||||
|
||||
echo "[*] Creating Haveno homedir"
|
||||
sudo -H -i -u "${ROOT_USER}" mkdir -p "${HAVENO_HOME}"
|
||||
sudo -H -i -u "${ROOT_USER}" chown "${HAVENO_USER}":"${HAVENO_GROUP}" ${HAVENO_HOME}
|
||||
|
||||
echo "[*] Cloning Haveno repo"
|
||||
sudo -H -i -u "${HAVENO_USER}" git config --global advice.detachedHead false
|
||||
sudo -H -i -u "${HAVENO_USER}" git clone --branch "${HAVENO_REPO_TAG}" "${HAVENO_REPO_URL}" "${HAVENO_HOME}/${HAVENO_REPO_NAME}"
|
||||
|
||||
echo "[*] Installing OpenJDK 11"
|
||||
sudo -H -i -u "${ROOT_USER}" apt-get install -qq -y openjdk-11-jdk
|
||||
|
||||
echo "[*] Checking out Haveno ${HAVENO_LATEST_RELEASE}"
|
||||
sudo -H -i -u "${HAVENO_USER}" sh -c "cd ${HAVENO_HOME}/${HAVENO_REPO_NAME} && git checkout ${HAVENO_LATEST_RELEASE}"
|
||||
|
||||
echo "[*] Building Haveno from source"
|
||||
sudo -H -i -u "${HAVENO_USER}" sh -c "cd ${HAVENO_HOME}/${HAVENO_REPO_NAME} && ./gradlew :pricenode:installDist -x test < /dev/null" # redirect from /dev/null is necessary to workaround gradlew non-interactive shell hanging issue
|
||||
|
||||
echo "[*] Installing haveno-pricenode systemd service"
|
||||
sudo -H -i -u "${ROOT_USER}" install -c -o "${ROOT_USER}" -g "${ROOT_GROUP}" -m 644 "${HAVENO_HOME}/${HAVENO_REPO_NAME}/pricenode/haveno-pricenode.service" "${SYSTEMD_SERVICE_HOME}"
|
||||
sudo -H -i -u "${ROOT_USER}" install -c -o "${ROOT_USER}" -g "${ROOT_GROUP}" -m 644 "${HAVENO_HOME}/${HAVENO_REPO_NAME}/pricenode/haveno-pricenode.env" "${SYSTEMD_ENV_HOME}"
|
||||
|
||||
echo "[*] Reloading systemd daemon configuration"
|
||||
sudo -H -i -u "${ROOT_USER}" systemctl daemon-reload
|
||||
|
||||
echo "[*] Enabling haveno-pricenode service"
|
||||
sudo -H -i -u "${ROOT_USER}" systemctl enable haveno-pricenode.service
|
||||
|
||||
echo "[*] Starting haveno-pricenode service"
|
||||
sudo -H -i -u "${ROOT_USER}" systemctl start haveno-pricenode.service
|
||||
sleep 5
|
||||
sudo -H -i -u "${ROOT_USER}" journalctl --no-pager --unit haveno-pricenode
|
||||
|
||||
echo "[*] Restarting Tor"
|
||||
sudo -H -i -u "${ROOT_USER}" service tor restart
|
||||
sleep 5
|
||||
|
||||
echo '[*] Done!'
|
||||
echo -n '[*] Access your pricenode at http://'
|
||||
cat "${TOR_RESOURCES}/${HAVENO_TORHS}/hostname"
|
||||
|
||||
exit 0
|
@ -1,20 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
HOSTNAME="${COLLECTD_HOSTNAME:-localhost}"
|
||||
INTERVAL=750
|
||||
|
||||
last=$(date +"%F %T" -d "$INTERVAL seconds ago")
|
||||
while true;
|
||||
do
|
||||
now=$(date +"%F %T")
|
||||
|
||||
journalctl -u bisq-pricenode --since="$last" --until="$now" | grep -Eo "getAllMarketPrices.*bisq/[0-9].[0-9].[0-9]" | cut -d / -f 2 | sort | uniq -c | while read -r line; do
|
||||
number=$(echo "${line}" | cut -d ' ' -f 1);
|
||||
version=$(echo "${line}" | cut -d \ -f 2);
|
||||
version=${version//./_};
|
||||
echo "PUTVAL $HOSTNAME/requestsPer750Seconds/gauge-v$version interval=$INTERVAL N:$number";
|
||||
done
|
||||
last=$now
|
||||
|
||||
sleep $INTERVAL
|
||||
done
|
@ -1,20 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
HOSTNAME="${COLLECTD_HOSTNAME:-localhost}"
|
||||
INTERVAL=750
|
||||
|
||||
last=$(date +"%F %T" -d "$INTERVAL seconds ago")
|
||||
while true;
|
||||
do
|
||||
now=$(date +"%F %T")
|
||||
|
||||
journalctl -u bisq-pricenode --since="$last" --until="$now" | grep -Eo "getAllMarketPrices.*HSv[0-9]" | grep -o "HSv[0-9]" | sort | uniq -c | while read -r line; do
|
||||
number=$(echo "${line}" | cut -d ' ' -f 1);
|
||||
version=$(echo "${line}" | cut -d \ -f 2);
|
||||
version=${version//./_};
|
||||
echo "PUTVAL $HOSTNAME/hsversionStats/gauge-$version interval=$INTERVAL N:$number";
|
||||
done
|
||||
last=$now
|
||||
|
||||
sleep $INTERVAL
|
||||
done
|
@ -1,51 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price;
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
@SpringBootApplication
|
||||
public class Main {
|
||||
|
||||
public static void main(String[] args) {
|
||||
new SpringApplicationBuilder(Main.class)
|
||||
.properties(bisqProperties())
|
||||
.run(args);
|
||||
}
|
||||
|
||||
private static Properties bisqProperties() {
|
||||
Properties props = new Properties();
|
||||
File propsFile = new File(System.getenv("HOME"), ".config/haveno.properties");
|
||||
if (propsFile.exists()) {
|
||||
try {
|
||||
props.load(new FileInputStream(propsFile));
|
||||
} catch (IOException ex) {
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
}
|
||||
return props;
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price;
|
||||
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public abstract class PriceController {
|
||||
|
||||
protected final Logger log = LoggerFactory.getLogger(this.getClass());
|
||||
|
||||
@ModelAttribute
|
||||
public void logRequest(HttpServletRequest request) {
|
||||
log.info("Incoming {} request from: {}", request.getServletPath(), request.getHeader("User-Agent"));
|
||||
}
|
||||
}
|
@ -1,121 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price;
|
||||
|
||||
import bisq.common.UserThread;
|
||||
|
||||
import org.springframework.context.SmartLifecycle;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public abstract class PriceProvider<T> implements SmartLifecycle, Supplier<T> {
|
||||
|
||||
protected final Logger log = LoggerFactory.getLogger(this.getClass());
|
||||
|
||||
private final Timer timer = new Timer(true);
|
||||
|
||||
protected final Duration refreshInterval;
|
||||
|
||||
private T cachedResult;
|
||||
|
||||
public PriceProvider(Duration refreshInterval) {
|
||||
this.refreshInterval = refreshInterval;
|
||||
log.info("will refresh every {}", refreshInterval);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final T get() {
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void start() {
|
||||
// do the initial refresh asynchronously
|
||||
UserThread.runAfter(() -> {
|
||||
try {
|
||||
refresh();
|
||||
} catch (Throwable t) {
|
||||
log.warn("initial refresh failed", t);
|
||||
}
|
||||
}, 1, TimeUnit.MILLISECONDS);
|
||||
|
||||
timer.scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
refresh();
|
||||
} catch (Throwable t) {
|
||||
// we only log scheduled calls to refresh that fail to ensure that
|
||||
// the application does *not* halt, assuming the failure is temporary
|
||||
// and on the side of the upstream price provider, eg. BitcoinAverage
|
||||
log.warn("refresh failed", t);
|
||||
}
|
||||
}
|
||||
}, refreshInterval.toMillis(), refreshInterval.toMillis());
|
||||
}
|
||||
|
||||
private void refresh() {
|
||||
try {
|
||||
long ts = System.currentTimeMillis();
|
||||
cachedResult = doGet();
|
||||
log.info("refresh took {} ms.", (System.currentTimeMillis() - ts));
|
||||
onRefresh();
|
||||
} catch (Exception e) {
|
||||
log.warn("Error refreshing price provider {}", getClass());
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract T doGet();
|
||||
|
||||
protected void onRefresh() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
timer.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop(Runnable callback) {
|
||||
stop();
|
||||
callback.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAutoStartup() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return cachedResult != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPhase() {
|
||||
return 0;
|
||||
}
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.mining;
|
||||
|
||||
/**
|
||||
* A value object representing the mining fee rate for a given base currency.
|
||||
*/
|
||||
public class FeeRate {
|
||||
|
||||
private final String currency;
|
||||
private final long price;
|
||||
private final long minimumFee;
|
||||
private final long timestamp;
|
||||
|
||||
public FeeRate(String currency, long price, long minimumFee, long timestamp) {
|
||||
this.currency = currency;
|
||||
this.price = price;
|
||||
this.minimumFee = minimumFee;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public String getCurrency() {
|
||||
return currency;
|
||||
}
|
||||
|
||||
public long getPrice() {
|
||||
return price;
|
||||
}
|
||||
|
||||
public long getMinimumFee() {
|
||||
return minimumFee;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.mining;
|
||||
|
||||
import bisq.price.PriceController;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
class FeeRateController extends PriceController {
|
||||
|
||||
private final FeeRateService feeRateService;
|
||||
|
||||
public FeeRateController(FeeRateService feeRateService) {
|
||||
this.feeRateService = feeRateService;
|
||||
}
|
||||
|
||||
@GetMapping(path = "/getFees")
|
||||
public Map<String, Object> getFees() {
|
||||
return feeRateService.getFees();
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.mining;
|
||||
|
||||
import bisq.price.PriceProvider;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* Abstract base class for providers of mining {@link FeeRate} data.
|
||||
*/
|
||||
public abstract class FeeRateProvider extends PriceProvider<FeeRate> {
|
||||
|
||||
public static final long MIN_FEE_RATE_FOR_WITHDRAWAL = 2; // satoshi/vbyte
|
||||
public static final long MIN_FEE_RATE_FOR_TRADING = 10; // satoshi/vbyte
|
||||
public static final long MAX_FEE_RATE = 1000;
|
||||
|
||||
public FeeRateProvider(Duration refreshInterval) {
|
||||
super(refreshInterval);
|
||||
}
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.mining;
|
||||
|
||||
import bisq.common.config.Config;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* High-level mining {@link FeeRate} operations.
|
||||
*/
|
||||
@Service
|
||||
public class FeeRateService {
|
||||
|
||||
private final List<FeeRateProvider> providers;
|
||||
protected final Logger log = LoggerFactory.getLogger(this.getClass());
|
||||
|
||||
/**
|
||||
* Construct a {@link FeeRateService} with a list of all {@link FeeRateProvider}
|
||||
* implementations discovered via classpath scanning.
|
||||
*
|
||||
* @param providers all {@link FeeRateProvider} implementations in ascending
|
||||
* order of precedence
|
||||
*/
|
||||
public FeeRateService(List<FeeRateProvider> providers) {
|
||||
this.providers = providers;
|
||||
}
|
||||
|
||||
public Map<String, Object> getFees() {
|
||||
Map<String, Long> metadata = new HashMap<>();
|
||||
Map<String, Long> allFeeRates = new HashMap<>();
|
||||
|
||||
AtomicLong sumOfAllFeeRates = new AtomicLong();
|
||||
AtomicLong sumOfAllMinFeeRates = new AtomicLong();
|
||||
AtomicInteger amountOfFeeRates = new AtomicInteger();
|
||||
|
||||
// Process each provider, retrieve and store their fee rate
|
||||
providers.forEach(p -> {
|
||||
FeeRate feeRate = p.get();
|
||||
if (feeRate == null) {
|
||||
log.warn("feeRate is null, provider={} ", p.toString());
|
||||
return;
|
||||
}
|
||||
String currency = feeRate.getCurrency();
|
||||
if ("BTC".equals(currency)) {
|
||||
sumOfAllFeeRates.getAndAdd(feeRate.getPrice());
|
||||
sumOfAllMinFeeRates.getAndAdd(feeRate.getMinimumFee());
|
||||
amountOfFeeRates.getAndAdd(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate the average
|
||||
long averageFeeRate = (amountOfFeeRates.intValue() > 0)
|
||||
? sumOfAllFeeRates.longValue() / amountOfFeeRates.intValue()
|
||||
: FeeRateProvider.MIN_FEE_RATE_FOR_TRADING;
|
||||
long averageMinFeeRate = (amountOfFeeRates.intValue() > 0)
|
||||
? sumOfAllMinFeeRates.longValue() / amountOfFeeRates.intValue()
|
||||
: FeeRateProvider.MIN_FEE_RATE_FOR_WITHDRAWAL;
|
||||
|
||||
// Make sure the returned value is within the min-max range
|
||||
averageFeeRate = Math.max(averageFeeRate, FeeRateProvider.MIN_FEE_RATE_FOR_TRADING);
|
||||
averageFeeRate = Math.min(averageFeeRate, FeeRateProvider.MAX_FEE_RATE);
|
||||
averageMinFeeRate = Math.max(averageMinFeeRate, FeeRateProvider.MIN_FEE_RATE_FOR_WITHDRAWAL);
|
||||
averageMinFeeRate = Math.min(averageMinFeeRate, FeeRateProvider.MAX_FEE_RATE);
|
||||
|
||||
// Prepare response: Add timestamp of now
|
||||
// Since this is an average, the timestamp is associated with when the moment in
|
||||
// time when the avg was computed
|
||||
metadata.put(Config.BTC_FEES_TS, Instant.now().getEpochSecond());
|
||||
|
||||
// Prepare response: Add the fee average
|
||||
allFeeRates.put(Config.BTC_TX_FEE, averageFeeRate);
|
||||
allFeeRates.put(Config.BTC_MIN_TX_FEE, averageMinFeeRate);
|
||||
|
||||
// Build response
|
||||
return new HashMap<>() {{
|
||||
putAll(metadata);
|
||||
put(Config.LEGACY_FEE_DATAMAP, allFeeRates);
|
||||
}};
|
||||
}
|
||||
}
|
@ -1,229 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.mining.providers;
|
||||
|
||||
import bisq.price.PriceController;
|
||||
import bisq.price.mining.FeeRate;
|
||||
import bisq.price.mining.FeeRateProvider;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.core.env.CommandLinePropertySource;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.http.RequestEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* {@link FeeRateProvider} that interprets the Mempool API format to retrieve a mining
|
||||
* fee estimate. See https://mempool.space.
|
||||
*/
|
||||
abstract class MempoolFeeRateProvider extends FeeRateProvider {
|
||||
|
||||
private static final int DEFAULT_MAX_BLOCKS = 2;
|
||||
private static final int DEFAULT_REFRESH_INTERVAL = 2;
|
||||
|
||||
// Keys of properties defining the available Mempool API endpoints. To enable them,
|
||||
// simply uncomment and adjust the corresponding lines in application.properties
|
||||
private static final String MEMPOOL_HOSTNAME_KEY_1 = "bisq.price.mining.providers.mempoolHostname.1";
|
||||
private static final String MEMPOOL_HOSTNAME_KEY_2 = "bisq.price.mining.providers.mempoolHostname.2";
|
||||
private static final String MEMPOOL_HOSTNAME_KEY_3 = "bisq.price.mining.providers.mempoolHostname.3";
|
||||
private static final String MEMPOOL_HOSTNAME_KEY_4 = "bisq.price.mining.providers.mempoolHostname.4";
|
||||
private static final String MEMPOOL_HOSTNAME_KEY_5 = "bisq.price.mining.providers.mempoolHostname.5";
|
||||
|
||||
private static final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
// TODO: As of the switch to the mempool.space API this field and related members are
|
||||
// now dead code and should be removed, including removing the positional
|
||||
// command-line argument from startup scripts. Operators need to be notified of this
|
||||
// when it happens.
|
||||
private final int maxBlocks;
|
||||
|
||||
protected Environment env;
|
||||
|
||||
public MempoolFeeRateProvider(Environment env) {
|
||||
super(Duration.ofMinutes(refreshInterval(env)));
|
||||
this.env = env;
|
||||
this.maxBlocks = maxBlocks(env);
|
||||
}
|
||||
|
||||
protected FeeRate doGet() {
|
||||
// Default value is the minimum rate. If the connection to the fee estimate
|
||||
// provider fails, we fall back to this value.
|
||||
try {
|
||||
return getEstimatedFeeRate();
|
||||
}
|
||||
catch (Exception e) {
|
||||
// Something happened with the connection
|
||||
log.error("Error retrieving bitcoin mining fee estimation: " + e.getMessage());
|
||||
}
|
||||
|
||||
return new FeeRate("BTC", MIN_FEE_RATE_FOR_TRADING, MIN_FEE_RATE_FOR_WITHDRAWAL, Instant.now().getEpochSecond());
|
||||
}
|
||||
|
||||
private FeeRate getEstimatedFeeRate() {
|
||||
Set<Map.Entry<String, Long>> feeRatePredictions = getFeeRatePredictions();
|
||||
long estimatedFeeRate = feeRatePredictions.stream()
|
||||
.filter(p -> p.getKey().equalsIgnoreCase("halfHourFee"))
|
||||
.map(Map.Entry::getValue)
|
||||
.findFirst()
|
||||
.map(r -> Math.max(r, MIN_FEE_RATE_FOR_TRADING))
|
||||
.map(r -> Math.min(r, MAX_FEE_RATE))
|
||||
.orElse(MIN_FEE_RATE_FOR_TRADING);
|
||||
long minimumFee = feeRatePredictions.stream()
|
||||
.filter(p -> p.getKey().equalsIgnoreCase("minimumFee"))
|
||||
.map(Map.Entry::getValue)
|
||||
.findFirst()
|
||||
.map(r -> Math.multiplyExact(r, 2)) // multiply the minimumFee by 2 (per wiz)
|
||||
.orElse(MIN_FEE_RATE_FOR_WITHDRAWAL);
|
||||
log.info("Retrieved estimated mining fee of {} sat/vB and minimumFee of {} sat/vB from {}", estimatedFeeRate, minimumFee, getMempoolApiHostname());
|
||||
return new FeeRate("BTC", estimatedFeeRate, minimumFee, Instant.now().getEpochSecond());
|
||||
}
|
||||
|
||||
private Set<Map.Entry<String, Long>> getFeeRatePredictions() {
|
||||
return restTemplate.exchange(
|
||||
RequestEntity
|
||||
.get(UriComponentsBuilder
|
||||
// See https://github.com/bisq-network/projects/issues/27
|
||||
.fromUriString("https://" + getMempoolApiHostname() + "/api/v1/fees/recommended")
|
||||
.build().toUri())
|
||||
.build(),
|
||||
new ParameterizedTypeReference<Map<String, Long>>() { }
|
||||
).getBody().entrySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the hostname of the fee estimation API endpoint. No prefix (https://), no
|
||||
* suffix (trailing slashes, etc).
|
||||
*/
|
||||
protected abstract String getMempoolApiHostname();
|
||||
|
||||
private static Optional<String[]> args(Environment env) {
|
||||
return Optional.ofNullable(
|
||||
env.getProperty(CommandLinePropertySource.DEFAULT_NON_OPTION_ARGS_PROPERTY_NAME, String[].class));
|
||||
}
|
||||
|
||||
private static int maxBlocks(Environment env) {
|
||||
return args(env)
|
||||
.filter(args -> args.length >= 1)
|
||||
.map(args -> Integer.valueOf(args[0]))
|
||||
.orElse(DEFAULT_MAX_BLOCKS);
|
||||
}
|
||||
|
||||
private static long refreshInterval(Environment env) {
|
||||
return args(env)
|
||||
.filter(args -> args.length >= 2)
|
||||
.map(args -> Integer.valueOf(args[1]))
|
||||
.orElse(DEFAULT_REFRESH_INTERVAL);
|
||||
}
|
||||
|
||||
@Primary
|
||||
@Component
|
||||
@Order(1)
|
||||
public static class First extends MempoolFeeRateProvider {
|
||||
|
||||
public First(Environment env) {
|
||||
super(env);
|
||||
}
|
||||
|
||||
protected String getMempoolApiHostname() {
|
||||
// This is the primary instance, so if no API point is set in
|
||||
// application.properties file, then it defaults to mempool.space
|
||||
// This ensures there is at least one provider attempting to connect,
|
||||
// even if the properties file is corrupt or empty
|
||||
return env.getProperty(MEMPOOL_HOSTNAME_KEY_1, "mempool.space");
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
@Order(2)
|
||||
@ConditionalOnProperty(name = MEMPOOL_HOSTNAME_KEY_2)
|
||||
public static class Second extends MempoolFeeRateProvider {
|
||||
|
||||
public Second(Environment env) {
|
||||
super(env);
|
||||
}
|
||||
|
||||
protected String getMempoolApiHostname() {
|
||||
return env.getProperty(MEMPOOL_HOSTNAME_KEY_2);
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
@Order(3)
|
||||
@ConditionalOnProperty(name = MEMPOOL_HOSTNAME_KEY_3)
|
||||
public static class Third extends MempoolFeeRateProvider {
|
||||
|
||||
public Third(Environment env) {
|
||||
super(env);
|
||||
}
|
||||
|
||||
protected String getMempoolApiHostname() {
|
||||
return env.getProperty(MEMPOOL_HOSTNAME_KEY_3);
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
@Order(4)
|
||||
@ConditionalOnProperty(name = MEMPOOL_HOSTNAME_KEY_4)
|
||||
public static class Fourth extends MempoolFeeRateProvider {
|
||||
|
||||
public Fourth(Environment env) {
|
||||
super(env);
|
||||
}
|
||||
|
||||
protected String getMempoolApiHostname() {
|
||||
return env.getProperty(MEMPOOL_HOSTNAME_KEY_4);
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
@Order(5)
|
||||
@ConditionalOnProperty(name = MEMPOOL_HOSTNAME_KEY_5)
|
||||
public static class Fifth extends MempoolFeeRateProvider {
|
||||
|
||||
public Fifth(Environment env) {
|
||||
super(env);
|
||||
}
|
||||
|
||||
protected String getMempoolApiHostname() {
|
||||
return env.getProperty(MEMPOOL_HOSTNAME_KEY_5);
|
||||
}
|
||||
}
|
||||
|
||||
@RestController
|
||||
class Controller extends PriceController {
|
||||
|
||||
@GetMapping(path = "/getParams")
|
||||
public String getParams() {
|
||||
return String.format("%s;%s", maxBlocks, refreshInterval.toMillis());
|
||||
}
|
||||
}
|
||||
}
|
@ -1,99 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A value object representing the spot price in bitcoin for a given currency at a given
|
||||
* time as reported by a given provider.
|
||||
*/
|
||||
public class ExchangeRate {
|
||||
|
||||
private final String currency;
|
||||
private final double price;
|
||||
private final long timestamp;
|
||||
private final String provider;
|
||||
|
||||
public ExchangeRate(String currency, BigDecimal price, Date timestamp, String provider) {
|
||||
this(
|
||||
currency,
|
||||
price.doubleValue(),
|
||||
timestamp.getTime(),
|
||||
provider
|
||||
);
|
||||
}
|
||||
|
||||
public ExchangeRate(String currency, double price, long timestamp, String provider) {
|
||||
this.currency = currency;
|
||||
this.price = price;
|
||||
this.timestamp = timestamp;
|
||||
this.provider = provider;
|
||||
}
|
||||
|
||||
@JsonProperty(value = "currencyCode", index = 1)
|
||||
public String getCurrency() {
|
||||
return currency;
|
||||
}
|
||||
|
||||
@JsonProperty(value = "price", index = 2)
|
||||
public double getPrice() {
|
||||
return this.price;
|
||||
}
|
||||
|
||||
@JsonProperty(value = "timestampSec", index = 3)
|
||||
public long getTimestamp() {
|
||||
return this.timestamp;
|
||||
}
|
||||
|
||||
@JsonProperty(value = "provider", index = 4)
|
||||
public String getProvider() {
|
||||
return provider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ExchangeRate exchangeRate = (ExchangeRate) o;
|
||||
return Double.compare(exchangeRate.price, price) == 0 &&
|
||||
timestamp == exchangeRate.timestamp &&
|
||||
Objects.equals(currency, exchangeRate.currency) &&
|
||||
Objects.equals(provider, exchangeRate.provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(currency, price, timestamp, provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ExchangeRate{" +
|
||||
"currency='" + currency + '\'' +
|
||||
", price=" + price +
|
||||
", timestamp=" + timestamp +
|
||||
", provider=" + provider +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot;
|
||||
|
||||
import bisq.price.PriceController;
|
||||
import bisq.price.mining.FeeRateService;
|
||||
|
||||
import bisq.common.config.Config;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
class ExchangeRateController extends PriceController {
|
||||
|
||||
private final ExchangeRateService exchangeRateService;
|
||||
private final FeeRateService feeRateService;
|
||||
|
||||
public ExchangeRateController(ExchangeRateService exchangeRateService, FeeRateService feeRateService) {
|
||||
this.exchangeRateService = exchangeRateService;
|
||||
this.feeRateService = feeRateService;
|
||||
}
|
||||
|
||||
@GetMapping(path = "/getAllMarketPrices")
|
||||
public Map<String, Object> getAllMarketPrices() {
|
||||
Map<String, Object> retVal = exchangeRateService.getAllMarketPrices();
|
||||
|
||||
// add the fee info to results
|
||||
feeRateService.getFees().forEach((key, value) -> {
|
||||
retVal.put(translateFieldName(key), value);
|
||||
});
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
static String translateFieldName(String name) {
|
||||
if (name.equals(Config.LEGACY_FEE_DATAMAP))
|
||||
name = Config.BTC_FEE_INFO; // name changed for clarity
|
||||
return name;
|
||||
}
|
||||
}
|
@ -1,339 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot;
|
||||
|
||||
import bisq.price.PriceProvider;
|
||||
|
||||
import bisq.core.locale.CurrencyUtil;
|
||||
import bisq.core.locale.TradeCurrency;
|
||||
|
||||
import org.knowm.xchange.Exchange;
|
||||
import org.knowm.xchange.ExchangeFactory;
|
||||
import org.knowm.xchange.currency.Currency;
|
||||
import org.knowm.xchange.currency.CurrencyPair;
|
||||
import org.knowm.xchange.dto.marketdata.Ticker;
|
||||
import org.knowm.xchange.exceptions.ExchangeException;
|
||||
import org.knowm.xchange.exceptions.NotYetImplementedForExchangeException;
|
||||
import org.knowm.xchange.service.marketdata.MarketDataService;
|
||||
import org.knowm.xchange.service.marketdata.params.CurrencyPairsParam;
|
||||
import org.knowm.xchange.service.marketdata.params.Params;
|
||||
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Abstract base class for providers of bitcoin {@link ExchangeRate} data. Implementations
|
||||
* are marked with the {@link org.springframework.stereotype.Component} annotation in
|
||||
* order to be discovered via classpath scanning. If multiple
|
||||
* {@link ExchangeRateProvider}s retrieve rates for the same currency, then the
|
||||
* {@link ExchangeRateService} will average them out and expose an aggregate rate.
|
||||
*
|
||||
* @see ExchangeRateService#getAllMarketPrices()
|
||||
*/
|
||||
public abstract class ExchangeRateProvider extends PriceProvider<Set<ExchangeRate>> {
|
||||
|
||||
private static Set<String> SUPPORTED_CRYPTO_CURRENCIES = new HashSet<>();
|
||||
private static Set<String> SUPPORTED_FIAT_CURRENCIES = new HashSet<>();
|
||||
private Set<String> providerExclusionList = new HashSet<>();
|
||||
private final String name;
|
||||
private final String prefix;
|
||||
private final Environment env;
|
||||
|
||||
public ExchangeRateProvider(Environment env, String name, String prefix, Duration refreshInterval) {
|
||||
super(refreshInterval);
|
||||
this.name = name;
|
||||
this.prefix = prefix;
|
||||
this.env = env;
|
||||
List<String> excludedByProvider =
|
||||
Arrays.asList(env.getProperty("bisq.price.fiatcurrency.excludedByProvider", "")
|
||||
.toUpperCase().trim().split("\\s*,\\s*"));
|
||||
for (String s: excludedByProvider) {
|
||||
String[] splits = s.split(":");
|
||||
if (splits.length == 2 && splits[0].equalsIgnoreCase(name) && CurrencyUtil.isFiatCurrency(splits[1])) {
|
||||
providerExclusionList.add(splits[1]);
|
||||
}
|
||||
}
|
||||
if (providerExclusionList.size() > 0) {
|
||||
log.info("{} specific exclusion list={}", name, providerExclusionList.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public Set<String> getSupportedFiatCurrencies() {
|
||||
if (SUPPORTED_FIAT_CURRENCIES.isEmpty()) { // one-time initialization
|
||||
List<String> excludedFiatCurrencies =
|
||||
Arrays.asList(env.getProperty("bisq.price.fiatcurrency.excluded", "")
|
||||
.toUpperCase().trim().split("\\s*,\\s*"));
|
||||
String validatedExclusionList = excludedFiatCurrencies.stream()
|
||||
.filter(ccy -> !ccy.isEmpty())
|
||||
.filter(CurrencyUtil::isFiatCurrency)
|
||||
.collect(Collectors.toList()).toString();
|
||||
SUPPORTED_FIAT_CURRENCIES = CurrencyUtil.getAllSortedFiatCurrencies().stream()
|
||||
.map(TradeCurrency::getCode)
|
||||
.filter(ccy -> !validatedExclusionList.contains(ccy.toUpperCase()))
|
||||
.collect(Collectors.toSet());
|
||||
log.info("fiat currencies excluded: {}", validatedExclusionList);
|
||||
log.info("fiat currencies supported: {}", SUPPORTED_FIAT_CURRENCIES.size());
|
||||
}
|
||||
// filter out any provider specific ccy exclusions
|
||||
return SUPPORTED_FIAT_CURRENCIES.stream()
|
||||
.filter(ccy -> !providerExclusionList.contains(ccy.toUpperCase()))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public Set<String> getSupportedCryptoCurrencies() {
|
||||
if (SUPPORTED_CRYPTO_CURRENCIES.isEmpty()) { // one-time initialization
|
||||
List<String> excludedCryptoCurrencies =
|
||||
Arrays.asList(env.getProperty("bisq.price.cryptocurrency.excluded", "")
|
||||
.toUpperCase().trim().split("\\s*,\\s*"));
|
||||
String validatedExclusionList = excludedCryptoCurrencies.stream()
|
||||
.filter(ccy -> !ccy.isEmpty())
|
||||
.filter(CurrencyUtil::isCryptoCurrency)
|
||||
.collect(Collectors.toList()).toString();
|
||||
SUPPORTED_CRYPTO_CURRENCIES = CurrencyUtil.getAllSortedCryptoCurrencies().stream()
|
||||
.map(TradeCurrency::getCode)
|
||||
.filter(ccy -> !validatedExclusionList.contains(ccy.toUpperCase()))
|
||||
.collect(Collectors.toSet());
|
||||
SUPPORTED_CRYPTO_CURRENCIES.add("XMR"); // XMR is skipped because it's a base currency
|
||||
log.info("crypto currencies excluded: {}", validatedExclusionList);
|
||||
log.info("crypto currencies supported: {}", SUPPORTED_CRYPTO_CURRENCIES.size());
|
||||
}
|
||||
return SUPPORTED_CRYPTO_CURRENCIES;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getPrefix() {
|
||||
return prefix;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRefresh() {
|
||||
get().stream()
|
||||
.filter(e -> "USD".equals(e.getCurrency()) || "XMR".equals(e.getCurrency()) || "ETH".equals(e.getCurrency()) || "BCH".equals(e.getCurrency()))
|
||||
.forEach(e -> log.info("BTC/{}: {}", e.getCurrency(), e.getPrice()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param exchangeClass Class of the {@link Exchange} for which the rates should be
|
||||
* polled
|
||||
* @return Exchange rates for Bisq-supported fiat currencies and altcoins in the
|
||||
* specified {@link Exchange}
|
||||
*
|
||||
* @see CurrencyUtil#getAllSortedFiatCurrencies()
|
||||
* @see CurrencyUtil#getAllSortedCryptoCurrencies()
|
||||
*/
|
||||
protected Set<ExchangeRate> doGet(Class<? extends Exchange> exchangeClass) {
|
||||
Set<ExchangeRate> result = new HashSet<ExchangeRate>();
|
||||
|
||||
// Initialize XChange objects
|
||||
Exchange exchange = ExchangeFactory.INSTANCE.createExchange(exchangeClass.getName());
|
||||
MarketDataService marketDataService = exchange.getMarketDataService();
|
||||
|
||||
// Retrieve all currency pairs supported by the exchange
|
||||
List<CurrencyPair> allCurrencyPairsOnExchange = exchange.getExchangeSymbols();
|
||||
|
||||
// Find out which currency pairs we are interested in polling ("desired pairs")
|
||||
// This will be the intersection of:
|
||||
// 1) the pairs available on the exchange, and
|
||||
// 2) the pairs Bisq considers relevant / valid
|
||||
// This will result in two lists of desired pairs (fiat and alts)
|
||||
|
||||
// Find the desired fiat pairs (pair format is BTC-FIAT)
|
||||
List<CurrencyPair> desiredFiatPairs = allCurrencyPairsOnExchange.stream()
|
||||
.filter(cp -> cp.base.equals(Currency.BTC))
|
||||
.filter(cp -> getSupportedFiatCurrencies().contains(cp.counter.getCurrencyCode()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Find the desired altcoin pairs (pair format is ALT-BTC)
|
||||
List<CurrencyPair> desiredCryptoPairs = allCurrencyPairsOnExchange.stream()
|
||||
.filter(cp -> cp.counter.equals(Currency.BTC))
|
||||
.filter(cp -> getSupportedCryptoCurrencies().contains(cp.base.getCurrencyCode()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Retrieve in bulk all tickers offered by the exchange
|
||||
// The benefits of this approach (vs polling each ticker) are twofold:
|
||||
// 1) the polling of the exchange is faster (one HTTP call vs several)
|
||||
// 2) it's easier to stay below any API rate limits the exchange might have
|
||||
List<Ticker> tickersRetrievedFromExchange = new ArrayList<>();
|
||||
try {
|
||||
tickersRetrievedFromExchange = marketDataService.getTickers(new CurrencyPairsParam() {
|
||||
|
||||
/**
|
||||
* The {@link MarketDataService#getTickers(Params)} interface requires a
|
||||
* {@link CurrencyPairsParam} argument when polling for tickers in bulk.
|
||||
* This parameter is meant to indicate a list of currency pairs for which
|
||||
* the tickers should be polled. However, the actual implementations for
|
||||
* the different exchanges differ, for example:
|
||||
* - some will ignore it (and retrieve all available tickers)
|
||||
* - some will require it (and will fail if a null or empty list is given)
|
||||
* - some will properly handle it
|
||||
*
|
||||
* We take a simplistic approach, namely:
|
||||
* - for providers that require such a filter, specify one
|
||||
* - for all others, do not specify one
|
||||
*
|
||||
* We make this distinction using
|
||||
* {@link ExchangeRateProvider#requiresFilterDuringBulkTickerRetrieval}
|
||||
*
|
||||
* @return Filter (list of desired currency pairs) to be used during bulk
|
||||
* ticker retrieval
|
||||
*/
|
||||
@Override
|
||||
public Collection<CurrencyPair> getCurrencyPairs() {
|
||||
// If required by the exchange implementation, specify a filter
|
||||
// (list of pairs which should be retrieved)
|
||||
if (requiresFilterDuringBulkTickerRetrieval()) {
|
||||
return Stream.of(desiredFiatPairs, desiredCryptoPairs)
|
||||
.flatMap(Collection::stream)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// Otherwise, specify an empty list, indicating that the API should
|
||||
// simply return all available tickers
|
||||
return Collections.emptyList();
|
||||
}
|
||||
});
|
||||
|
||||
if (tickersRetrievedFromExchange.isEmpty()) {
|
||||
// If the bulk ticker retrieval went through, but no tickers were
|
||||
// retrieved, this is a strong indication that this specific exchange
|
||||
// needs a specific list of pairs given as argument, for bulk retrieval to
|
||||
// work. See requiresFilterDuringBulkTickerRetrieval()
|
||||
throw new IllegalArgumentException("No tickers retrieved, " +
|
||||
"exchange requires explicit filter argument during bulk retrieval?");
|
||||
}
|
||||
} catch (NotYetImplementedForExchangeException e) {
|
||||
// Thrown when a provider has no marketDataService.getTickers() implementation
|
||||
// either because the exchange API does not provide it, or because it has not
|
||||
// been implemented yet in the knowm xchange library
|
||||
|
||||
// In this case (retrieval of bulk tickers is not possible) retrieve the
|
||||
// tickers one by one
|
||||
List<Ticker> finalTickersRetrievedFromExchange = tickersRetrievedFromExchange;
|
||||
Stream.of(desiredFiatPairs, desiredCryptoPairs)
|
||||
.flatMap(Collection::stream)
|
||||
.collect(Collectors.toList())
|
||||
.forEach(cp -> {
|
||||
try {
|
||||
|
||||
// This is done in a loop, and can therefore result in a burst
|
||||
// of API calls. Some exchanges do not allow bursts
|
||||
// A simplistic solution is to delay every call by 1 second
|
||||
// TODO Switch to using a more elegant solution (per exchange)
|
||||
// like ResilienceSpecification (needs knowm xchange libs v5)
|
||||
if (getMarketDataCallDelay() > 0) {
|
||||
Thread.sleep(getMarketDataCallDelay());
|
||||
}
|
||||
|
||||
Ticker ticker = marketDataService.getTicker(cp);
|
||||
finalTickersRetrievedFromExchange.add(ticker);
|
||||
|
||||
} catch (IOException | InterruptedException ioException) {
|
||||
ioException.printStackTrace();
|
||||
log.error("Could not query tickers for " + getName(), e);
|
||||
}
|
||||
});
|
||||
} catch (ExchangeException | // Errors reported by the exchange (rate limit, etc)
|
||||
IOException | // Errors while trying to connect to the API (timeouts, etc)
|
||||
// Potential error when integrating new exchange (hints that exchange
|
||||
// provider implementation needs to overwrite
|
||||
// requiresFilterDuringBulkTickerRetrieval() and have it return true )
|
||||
IllegalArgumentException e) {
|
||||
// Catch and handle all other possible exceptions
|
||||
// If there was a problem with polling this exchange, return right away,
|
||||
// since there are no results to parse and process
|
||||
log.error("Could not query tickers for provider " + getName(), e);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Create an ExchangeRate for each desired currency pair ticker that was retrieved
|
||||
Predicate<Ticker> isDesiredFiatPair = t -> desiredFiatPairs.contains(t.getCurrencyPair());
|
||||
Predicate<Ticker> isDesiredCryptoPair = t -> desiredCryptoPairs.contains(t.getCurrencyPair());
|
||||
tickersRetrievedFromExchange.stream()
|
||||
.filter(isDesiredFiatPair.or(isDesiredCryptoPair)) // Only consider desired pairs
|
||||
.forEach(t -> {
|
||||
// All tickers here match all requirements
|
||||
|
||||
// We have two kinds of currency pairs, BTC-FIAT and ALT-BTC
|
||||
// In the first one, BTC is the first currency of the pair
|
||||
// In the second type, BTC is listed as the second currency
|
||||
// Distinguish between the two and create ExchangeRates accordingly
|
||||
|
||||
// In every Bisq ExchangeRate, BTC is one currency in the pair
|
||||
// Extract the other currency from the ticker, to create ExchangeRates
|
||||
String otherExchangeRateCurrency;
|
||||
if (t.getCurrencyPair().base.equals(Currency.BTC)) {
|
||||
otherExchangeRateCurrency = t.getCurrencyPair().counter.getCurrencyCode();
|
||||
} else {
|
||||
otherExchangeRateCurrency = t.getCurrencyPair().base.getCurrencyCode();
|
||||
}
|
||||
|
||||
result.add(new ExchangeRate(
|
||||
otherExchangeRateCurrency,
|
||||
t.getLast(),
|
||||
// Some exchanges do not provide timestamps
|
||||
t.getTimestamp() == null ? new Date() : t.getTimestamp(),
|
||||
this.getName()
|
||||
));
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies optional delay between certain kind of API calls that can result in
|
||||
* bursts. We want to avoid bursts, because this can cause certain exchanges to
|
||||
* temporarily restrict access to the pricenode IP.
|
||||
*
|
||||
* @return Amount of milliseconds of delay between marketDataService.getTicker calls.
|
||||
* By default 0, but can be overwritten by each provider.
|
||||
*/
|
||||
protected long getMarketDataCallDelay() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Whether or not the bulk retrieval of tickers from the exchange requires an
|
||||
* explicit filter (list of desired pairs) or not. If true, the
|
||||
* {@link MarketDataService#getTickers(Params)} call will be constructed and given as
|
||||
* argument, which acts as a filter indicating for which pairs the ticker should be
|
||||
* retrieved. If false, {@link MarketDataService#getTickers(Params)} will be called
|
||||
* with an empty argument, indicating that the API should simply return all available
|
||||
* tickers on the exchange
|
||||
*/
|
||||
protected boolean requiresFilterDuringBulkTickerRetrieval() {
|
||||
return false;
|
||||
}
|
||||
}
|
@ -1,187 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.OptionalDouble;
|
||||
import java.util.Set;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
/**
|
||||
* High-level {@link ExchangeRate} data operations.
|
||||
*/
|
||||
@Service
|
||||
class ExchangeRateService {
|
||||
protected final Logger log = LoggerFactory.getLogger(this.getClass());
|
||||
|
||||
private final List<ExchangeRateProvider> providers;
|
||||
|
||||
/**
|
||||
* Construct an {@link ExchangeRateService} with a list of all
|
||||
* {@link ExchangeRateProvider} implementations discovered via classpath scanning.
|
||||
*
|
||||
* @param providers all {@link ExchangeRateProvider} implementations in ascending
|
||||
* order of precedence
|
||||
*/
|
||||
public ExchangeRateService(List<ExchangeRateProvider> providers) {
|
||||
this.providers = providers;
|
||||
}
|
||||
|
||||
public Map<String, Object> getAllMarketPrices() {
|
||||
Map<String, Object> metadata = new LinkedHashMap<>();
|
||||
Map<String, ExchangeRate> aggregateExchangeRates = getAggregateExchangeRates();
|
||||
|
||||
providers.forEach(p -> {
|
||||
if (p.get() == null)
|
||||
return;
|
||||
Set<ExchangeRate> exchangeRates = p.get();
|
||||
|
||||
// Specific metadata fields for specific providers are expected by the client,
|
||||
// mostly for historical reasons
|
||||
// Therefore, add metadata fields for all known providers
|
||||
// Rates are encapsulated in the "data" map below
|
||||
metadata.putAll(getMetadata(p, exchangeRates));
|
||||
});
|
||||
|
||||
LinkedHashMap<String, Object> result = new LinkedHashMap<>();
|
||||
result.putAll(metadata);
|
||||
// Use a sorted list by currency code to make comparision of json data between
|
||||
// different price nodes easier
|
||||
List<ExchangeRate> values = new ArrayList<>(aggregateExchangeRates.values());
|
||||
values.sort(Comparator.comparing(ExchangeRate::getCurrency));
|
||||
result.put("data", values);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* For each currency, create an aggregate {@link ExchangeRate} based on the currency's
|
||||
* rates from all providers. If multiple providers have rates for the currency, then
|
||||
* aggregate price = average of retrieved prices. If a single provider has rates for
|
||||
* the currency, then aggregate price = the rate from that provider.
|
||||
*
|
||||
* @return Aggregate {@link ExchangeRate}s based on info from all providers, indexed
|
||||
* by currency code
|
||||
*/
|
||||
private Map<String, ExchangeRate> getAggregateExchangeRates() {
|
||||
Map<String, ExchangeRate> aggregateExchangeRates = new HashMap<>();
|
||||
|
||||
// Query all providers and collect all exchange rates, grouped by currency code
|
||||
// key = currency code
|
||||
// value = list of exchange rates
|
||||
Map<String, List<ExchangeRate>> currencyCodeToExchangeRates = getCurrencyCodeToExchangeRates();
|
||||
|
||||
// For each currency code, calculate aggregate rate
|
||||
currencyCodeToExchangeRates.forEach((currencyCode, exchangeRateList) -> {
|
||||
if (exchangeRateList.isEmpty()) {
|
||||
// If the map was built incorrectly and this currency points to an empty
|
||||
// list of rates, skip it
|
||||
return;
|
||||
}
|
||||
|
||||
ExchangeRate aggregateExchangeRate;
|
||||
if (exchangeRateList.size() == 1) {
|
||||
// If a single provider has rates for this currency, then aggregate = rate
|
||||
// from that provider
|
||||
aggregateExchangeRate = exchangeRateList.get(0);
|
||||
} else {
|
||||
// If multiple providers have rates for this currency, then
|
||||
// aggregate = average of the rates
|
||||
OptionalDouble opt = exchangeRateList.stream().mapToDouble(ExchangeRate::getPrice).average();
|
||||
// List size > 1, so opt is always set
|
||||
double priceAvg = opt.orElseThrow(IllegalStateException::new);
|
||||
|
||||
aggregateExchangeRate = new ExchangeRate(
|
||||
currencyCode,
|
||||
BigDecimal.valueOf(priceAvg),
|
||||
new Date(), // timestamp = time when avg is calculated
|
||||
"Bisq-Aggregate");
|
||||
}
|
||||
aggregateExchangeRates.put(aggregateExchangeRate.getCurrency(), aggregateExchangeRate);
|
||||
});
|
||||
|
||||
return aggregateExchangeRates;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return All {@link ExchangeRate}s from all providers, grouped by currency code
|
||||
*/
|
||||
private Map<String, List<ExchangeRate>> getCurrencyCodeToExchangeRates() {
|
||||
Map<String, List<ExchangeRate>> currencyCodeToExchangeRates = new HashMap<>();
|
||||
for (ExchangeRateProvider p : providers) {
|
||||
if (p.get() == null)
|
||||
continue;
|
||||
for (ExchangeRate exchangeRate : p.get()) {
|
||||
String currencyCode = exchangeRate.getCurrency();
|
||||
if (currencyCodeToExchangeRates.containsKey(currencyCode)) {
|
||||
List<ExchangeRate> l = new ArrayList<>(currencyCodeToExchangeRates.get(currencyCode));
|
||||
l.add(exchangeRate);
|
||||
currencyCodeToExchangeRates.put(currencyCode, l);
|
||||
} else {
|
||||
currencyCodeToExchangeRates.put(currencyCode, asList(exchangeRate));
|
||||
}
|
||||
}
|
||||
}
|
||||
return currencyCodeToExchangeRates;
|
||||
}
|
||||
|
||||
private Map<String, Object> getMetadata(ExchangeRateProvider provider, Set<ExchangeRate> exchangeRates) {
|
||||
Map<String, Object> metadata = new LinkedHashMap<>();
|
||||
|
||||
// In case a provider is not available we still want to deliver the data of the
|
||||
// other providers, so we catch a possible exception and leave timestamp at 0. The
|
||||
// Bisq app will check if the timestamp is in a tolerance window and if it is too
|
||||
// old it will show that the price is not available.
|
||||
long timestamp = 0;
|
||||
try {
|
||||
timestamp = getTimestamp(provider, exchangeRates);
|
||||
} catch (Throwable t) {
|
||||
log.error(t.toString());
|
||||
if (log.isDebugEnabled())
|
||||
t.printStackTrace();
|
||||
}
|
||||
|
||||
String prefix = provider.getPrefix();
|
||||
metadata.put(prefix + "Ts", timestamp);
|
||||
metadata.put(prefix + "Count", exchangeRates.size());
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private long getTimestamp(ExchangeRateProvider provider, Set<ExchangeRate> exchangeRates) {
|
||||
return exchangeRates.stream()
|
||||
.filter(e -> provider.getName().equals(e.getProvider()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("No exchange rate data found for " + provider.getName()))
|
||||
.getTimestamp();
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot.providers;
|
||||
|
||||
import bisq.price.spot.ExchangeRate;
|
||||
import bisq.price.spot.ExchangeRateProvider;
|
||||
|
||||
import org.knowm.xchange.btcmarkets.BTCMarketsExchange;
|
||||
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@Component
|
||||
class BTCMarkets extends ExchangeRateProvider {
|
||||
|
||||
public BTCMarkets(Environment env) {
|
||||
super(env, "BTCMARKETS", "btcmarkets", Duration.ofMinutes(1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ExchangeRate> doGet() {
|
||||
// Supported fiat: AUD
|
||||
// Supported alts: ETH, LTC
|
||||
return doGet(BTCMarketsExchange.class);
|
||||
}
|
||||
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot.providers;
|
||||
|
||||
import bisq.price.spot.ExchangeRate;
|
||||
import bisq.price.spot.ExchangeRateProvider;
|
||||
|
||||
import org.knowm.xchange.binance.BinanceExchange;
|
||||
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@Component
|
||||
class Binance extends ExchangeRateProvider {
|
||||
|
||||
public Binance(Environment env) {
|
||||
super(env, "BINANCE", "binance", Duration.ofMinutes(1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ExchangeRate> doGet() {
|
||||
// Supported fiat: EUR, GBP, NGN, RUB, TRY, UAH, ZAR
|
||||
// Supported alts: BEAM, DAI, DASH, DCR, DOGE, ETC, ETH, LTC, NAV, PIVX, XMR, XZC,
|
||||
// ZEC, ZEN
|
||||
return doGet(BinanceExchange.class);
|
||||
}
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot.providers;
|
||||
|
||||
import bisq.price.spot.ExchangeRate;
|
||||
import bisq.price.spot.ExchangeRateProvider;
|
||||
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Stub implementation (similar to #CoinMarketCap) for backward compatibility with legacy
|
||||
* Bisq clients
|
||||
*/
|
||||
@Component
|
||||
class BitcoinAverage extends ExchangeRateProvider {
|
||||
|
||||
public BitcoinAverage(Environment env) {
|
||||
// Simulate a deactivated BitcoinAverage provider
|
||||
// We still need the class to exist and be registered as a provider though,
|
||||
// because the returned data structure must contain the "btcAverageTs" key
|
||||
// for backward compatibility with Bisq clients which hardcode that key
|
||||
super(env, "BA", "btcAverage", Duration.ofMinutes(100));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see CoinMarketCap#doGet()
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public Set<ExchangeRate> doGet() {
|
||||
|
||||
HashSet<ExchangeRate> exchangeRates = new HashSet<>();
|
||||
exchangeRates.add(new ExchangeRate("NON_EXISTING_SYMBOL_BA", 0, 0L, getName()));
|
||||
return exchangeRates;
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot.providers;
|
||||
|
||||
import bisq.price.spot.ExchangeRate;
|
||||
import bisq.price.spot.ExchangeRateProvider;
|
||||
|
||||
import org.knowm.xchange.bitfinex.BitfinexExchange;
|
||||
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@Component
|
||||
class Bitfinex extends ExchangeRateProvider {
|
||||
|
||||
public Bitfinex(Environment env) {
|
||||
super(env, "BITFINEX", "bitfinex", Duration.ofMinutes(1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ExchangeRate> doGet() {
|
||||
// Supported fiat: EUR, GBP, JPY, USD
|
||||
// Supported alts: DAI, ETC, ETH, LTC, XMR, ZEC
|
||||
return doGet(BitfinexExchange.class);
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot.providers;
|
||||
|
||||
import bisq.price.spot.ExchangeRate;
|
||||
import bisq.price.spot.ExchangeRateProvider;
|
||||
|
||||
import org.knowm.xchange.bitflyer.BitflyerExchange;
|
||||
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@Component
|
||||
class Bitflyer extends ExchangeRateProvider {
|
||||
|
||||
public Bitflyer(Environment env) {
|
||||
super(env, "BITFLYER", "bitflyer", Duration.ofMinutes(1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ExchangeRate> doGet() {
|
||||
// Supported fiat: JPY
|
||||
// Supported alts: ETH
|
||||
return doGet(BitflyerExchange.class);
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot.providers;
|
||||
|
||||
import bisq.price.spot.ExchangeRate;
|
||||
import bisq.price.spot.ExchangeRateProvider;
|
||||
|
||||
import org.knowm.xchange.bitstamp.BitstampExchange;
|
||||
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@Component
|
||||
class Bitstamp extends ExchangeRateProvider {
|
||||
|
||||
public Bitstamp(Environment env) {
|
||||
super(env, "BITSTAMP", "bitstamp", Duration.ofMinutes(1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ExchangeRate> doGet() {
|
||||
// Supported fiat: EUR, GBP, USD
|
||||
// Supported alts: ETH, LTC
|
||||
return doGet(BitstampExchange.class);
|
||||
}
|
||||
}
|
@ -1,105 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot.providers;
|
||||
|
||||
import bisq.price.spot.ExchangeRate;
|
||||
import bisq.price.spot.ExchangeRateProvider;
|
||||
import bisq.price.util.coingecko.CoinGeckoMarketData;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.http.RequestEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Component
|
||||
class CoinGecko extends ExchangeRateProvider {
|
||||
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
public CoinGecko(Environment env) {
|
||||
super(env, "COINGECKO", "coingecko", Duration.ofMinutes(1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ExchangeRate> doGet() {
|
||||
|
||||
// Rate limit for the CoinGecko API is 10 calls each second per IP address
|
||||
// We retrieve all rates in bulk, so we only make 1 call per provider poll
|
||||
|
||||
Set<ExchangeRate> result = new HashSet<ExchangeRate>();
|
||||
|
||||
Predicate<Map.Entry> isDesiredFiatPair = t -> getSupportedFiatCurrencies().contains(t.getKey());
|
||||
Predicate<Map.Entry> isDesiredCryptoPair = t -> getSupportedCryptoCurrencies().contains(t.getKey());
|
||||
|
||||
getMarketData().getRates().entrySet().stream()
|
||||
.filter(isDesiredFiatPair.or(isDesiredCryptoPair))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))
|
||||
.forEach((key, ticker) -> {
|
||||
|
||||
boolean useInverseRate = false;
|
||||
if (getSupportedCryptoCurrencies().contains(key)) {
|
||||
// Use inverse rate for alts, because the API returns the
|
||||
// conversion rate in the opposite direction than what we need
|
||||
// API returns the BTC/Alt rate, we need the Alt/BTC rate
|
||||
useInverseRate = true;
|
||||
}
|
||||
|
||||
BigDecimal rate = ticker.getValue();
|
||||
// Find the inverse rate, while using enough decimals to reflect very
|
||||
// small exchange rates
|
||||
BigDecimal inverseRate = (rate.compareTo(BigDecimal.ZERO) > 0) ?
|
||||
BigDecimal.ONE.divide(rate, 8, RoundingMode.HALF_UP) :
|
||||
BigDecimal.ZERO;
|
||||
|
||||
result.add(new ExchangeRate(
|
||||
key,
|
||||
(useInverseRate ? inverseRate : rate),
|
||||
new Date(),
|
||||
this.getName()
|
||||
));
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private CoinGeckoMarketData getMarketData() {
|
||||
return restTemplate.exchange(
|
||||
RequestEntity
|
||||
.get(UriComponentsBuilder
|
||||
.fromUriString("https://api.coingecko.com/api/v3/exchange_rates").build()
|
||||
.toUri())
|
||||
.build(),
|
||||
new ParameterizedTypeReference<CoinGeckoMarketData>() {
|
||||
}
|
||||
).getBody();
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot.providers;
|
||||
|
||||
import bisq.price.spot.ExchangeRate;
|
||||
import bisq.price.spot.ExchangeRateProvider;
|
||||
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Stub implementation of CoinMarketCap price provider to prevent NullPointerExceptions within legacy clients
|
||||
*/
|
||||
@Component
|
||||
class CoinMarketCap extends ExchangeRateProvider {
|
||||
|
||||
public CoinMarketCap(Environment env) {
|
||||
super(env, "CMC", "coinmarketcap", Duration.ofMinutes(5)); // large data structure, so don't request it too often
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Set with a non existing symbol for the CoinMarketCap price provider.
|
||||
* Price data of CMC provider is not used in the client anymore, except for the last update timestamp.
|
||||
* To prevent a unnecessary warning log in that case we have to pass at least one element.
|
||||
*
|
||||
* @return Empty Set
|
||||
*/
|
||||
@Override
|
||||
public Set<ExchangeRate> doGet() {
|
||||
HashSet<ExchangeRate> exchangeRates = new HashSet<>();
|
||||
exchangeRates.add(new ExchangeRate("NON_EXISTING_SYMBOL", 0, 0L, "CMC"));
|
||||
return exchangeRates;
|
||||
}
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package bisq.price.spot.providers;
|
||||
|
||||
import bisq.price.spot.ExchangeRate;
|
||||
import bisq.price.spot.ExchangeRateProvider;
|
||||
|
||||
import org.knowm.xchange.coinbasepro.CoinbaseProExchange;
|
||||
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@Component
|
||||
class CoinbasePro extends ExchangeRateProvider {
|
||||
|
||||
public CoinbasePro(Environment env) {
|
||||
super(env, "COINBASEPRO", "coinbasepro", Duration.ofMinutes(1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ExchangeRate> doGet() {
|
||||
// Supported fiat: EUR, USD, GBP
|
||||
// Supported alts: DASH, DOGE, ETC, ETH, LTC, ZEC, ZEN
|
||||
return doGet(CoinbaseProExchange.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean requiresFilterDuringBulkTickerRetrieval() {
|
||||
return true;
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot.providers;
|
||||
|
||||
import bisq.price.spot.ExchangeRate;
|
||||
import bisq.price.spot.ExchangeRateProvider;
|
||||
|
||||
import org.knowm.xchange.coinone.CoinoneExchange;
|
||||
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@Component
|
||||
class Coinone extends ExchangeRateProvider {
|
||||
|
||||
public Coinone(Environment env) {
|
||||
super(env, "COINONE", "coinone", Duration.ofMinutes(1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ExchangeRate> doGet() {
|
||||
// Supported fiat: KRW
|
||||
// Supported alts: -
|
||||
return doGet(CoinoneExchange.class);
|
||||
}
|
||||
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot.providers;
|
||||
|
||||
import bisq.price.spot.ExchangeRate;
|
||||
import bisq.price.spot.ExchangeRateProvider;
|
||||
|
||||
import org.knowm.xchange.independentreserve.IndependentReserveExchange;
|
||||
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@Component
|
||||
class IndependentReserve extends ExchangeRateProvider {
|
||||
|
||||
public IndependentReserve(Environment env) {
|
||||
super(env, "IndependentReserve", "independentreserve", Duration.ofMinutes(1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ExchangeRate> doGet() {
|
||||
// Supported fiat: AUD, NZD (New Zealand Dollar), USD
|
||||
// Supported alts: -
|
||||
return doGet(IndependentReserveExchange.class);
|
||||
}
|
||||
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot.providers;
|
||||
|
||||
import bisq.price.spot.ExchangeRate;
|
||||
import bisq.price.spot.ExchangeRateProvider;
|
||||
|
||||
import org.knowm.xchange.kraken.KrakenExchange;
|
||||
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@Component
|
||||
class Kraken extends ExchangeRateProvider {
|
||||
|
||||
public Kraken(Environment env) {
|
||||
super(env, "KRAKEN", "kraken", Duration.ofMinutes(1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ExchangeRate> doGet() {
|
||||
// Supported fiat: AUD, CAD, CHF, EUR, GBP, JPY, USD
|
||||
// Supported alts: DASH, DOGE, ETC, ETH, LTC, XMR, ZEC
|
||||
return doGet(KrakenExchange.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean requiresFilterDuringBulkTickerRetrieval() {
|
||||
return true;
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot.providers;
|
||||
|
||||
import bisq.price.spot.ExchangeRate;
|
||||
import bisq.price.spot.ExchangeRateProvider;
|
||||
|
||||
import org.knowm.xchange.luno.LunoExchange;
|
||||
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@Component
|
||||
class Luno extends ExchangeRateProvider {
|
||||
|
||||
public Luno(Environment env) {
|
||||
super(env, "LUNO", "luno", Duration.ofMinutes(1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ExchangeRate> doGet() {
|
||||
// Supported fiat: IDR (Indonesian rupiah), MYR (Malaysian ringgit),
|
||||
// NGN (Nigerian Naira), ZAR (South African rand)
|
||||
// Supported alts: -
|
||||
return doGet(LunoExchange.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long getMarketDataCallDelay() {
|
||||
// Luno allows only 1 MarketData call per second
|
||||
// (see https://www.luno.com/en/developers/api )
|
||||
return 1000;
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot.providers;
|
||||
|
||||
import bisq.price.spot.ExchangeRate;
|
||||
import bisq.price.spot.ExchangeRateProvider;
|
||||
|
||||
import org.knowm.xchange.mercadobitcoin.MercadoBitcoinExchange;
|
||||
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@Component
|
||||
class MercadoBitcoin extends ExchangeRateProvider {
|
||||
|
||||
public MercadoBitcoin(Environment env) {
|
||||
super(env, "MercadoBitcoin", "mercadobitcoin", Duration.ofMinutes(1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ExchangeRate> doGet() {
|
||||
// Supported fiat: BRL (Brazilian Real)
|
||||
// Supported alts: -
|
||||
return doGet(MercadoBitcoinExchange.class);
|
||||
}
|
||||
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot.providers;
|
||||
|
||||
import bisq.price.spot.ExchangeRate;
|
||||
import bisq.price.spot.ExchangeRateProvider;
|
||||
|
||||
import org.knowm.xchange.paribu.ParibuExchange;
|
||||
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@Component
|
||||
class Paribu extends ExchangeRateProvider {
|
||||
|
||||
public Paribu(Environment env) {
|
||||
super(env, "PARIBU", "paribu", Duration.ofMinutes(1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ExchangeRate> doGet() {
|
||||
// Supported fiat: TRY (Turkish Lira)
|
||||
// Supported alts: -
|
||||
return doGet(ParibuExchange.class);
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot.providers;
|
||||
|
||||
import bisq.price.spot.ExchangeRate;
|
||||
import bisq.price.spot.ExchangeRateProvider;
|
||||
|
||||
import org.knowm.xchange.poloniex.PoloniexExchange;
|
||||
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@Component
|
||||
class Poloniex extends ExchangeRateProvider {
|
||||
|
||||
public Poloniex(Environment env) {
|
||||
super(env, "POLO", "poloniex", Duration.ofMinutes(1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ExchangeRate> doGet() {
|
||||
// Supported fiat: -
|
||||
// Supported alts: DASH, DCR, DOGE, ETC, ETH, LTC, XMR, ZEC
|
||||
return doGet(PoloniexExchange.class);
|
||||
}
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot.providers;
|
||||
|
||||
import bisq.price.spot.ExchangeRate;
|
||||
import bisq.price.spot.ExchangeRateProvider;
|
||||
|
||||
import org.knowm.xchange.quoine.QuoineExchange;
|
||||
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@Component
|
||||
class Quoine extends ExchangeRateProvider {
|
||||
|
||||
public Quoine(Environment env) {
|
||||
super(env, "QUOINE", "quoine", Duration.ofMinutes(1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ExchangeRate> doGet() {
|
||||
// Supported fiat: AUD, CNY, EUR, HKD, IDR, INR, JPY, PHP, SGD, USD
|
||||
// Supported alts: ETH
|
||||
return doGet(QuoineExchange.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean requiresFilterDuringBulkTickerRetrieval() {
|
||||
return true;
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.util;
|
||||
|
||||
import bisq.core.locale.CurrencyUtil;
|
||||
import bisq.core.locale.TradeCurrency;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public abstract class Altcoins {
|
||||
|
||||
public static final Set<String> ALL_SUPPORTED =
|
||||
CurrencyUtil.getAllSortedCryptoCurrencies().stream()
|
||||
.map(TradeCurrency::getCode)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
package bisq.price.util.bitpay;
|
||||
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class BitpayMarketData {
|
||||
|
||||
private BitpayTicker[] data;
|
||||
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
package bisq.price.util.bitpay;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class BitpayTicker {
|
||||
|
||||
private String code;
|
||||
|
||||
private String name;
|
||||
|
||||
private BigDecimal rate;
|
||||
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
package bisq.price.util.coingecko;
|
||||
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class CoinGeckoMarketData {
|
||||
|
||||
private Map<String, CoinGeckoTicker> rates;
|
||||
|
||||
public void setRates(Map<String, CoinGeckoTicker> rates) {
|
||||
// Convert keys to uppercase ("usd" -> "USD") when deserializing API response
|
||||
this.rates = rates.entrySet().stream()
|
||||
.collect(Collectors.toMap(entry -> entry.getKey().toUpperCase(), entry -> entry.getValue()));
|
||||
}
|
||||
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
package bisq.price.util.coingecko;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class CoinGeckoTicker {
|
||||
|
||||
private String name;
|
||||
|
||||
private String unit;
|
||||
|
||||
private BigDecimal value;
|
||||
|
||||
private String type;
|
||||
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
package bisq.price.util.coinpaprika;
|
||||
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class CoinpaprikaMarketData {
|
||||
|
||||
// All other json fields can be ignored, we don't need them
|
||||
|
||||
private Map<String, CoinpaprikaTicker> quotes;
|
||||
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
package bisq.price.util.coinpaprika;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class CoinpaprikaTicker {
|
||||
|
||||
// All other json fields can be ignored, we don't need them
|
||||
|
||||
private BigDecimal price;
|
||||
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
server.port=8078
|
||||
spring.jackson.serialization.indent_output=true
|
||||
|
||||
# To enable another fee estimation endpoint, simply uncomment one of the following lines
|
||||
# and set it to hostname exposing the fee estimation API
|
||||
bisq.price.mining.providers.mempoolHostname.1=mempool.space
|
||||
bisq.price.mining.providers.mempoolHostname.2=mempool.emzy.de
|
||||
bisq.price.mining.providers.mempoolHostname.3=mempool.ninja
|
||||
bisq.price.mining.providers.mempoolHostname.4=mempool.bisq.services
|
||||
# bisq.price.mining.providers.mempoolHostname.5=someHostOrIP
|
||||
bisq.price.fiatcurrency.excluded=LBP,ARS
|
||||
bisq.price.fiatcurrency.excludedByProvider=HUOBI:BRL
|
||||
bisq.price.cryptocurrency.excluded=
|
@ -1,6 +0,0 @@
|
||||
__ _ _ __
|
||||
/ /_ (_)________ _ ____ _____(_)_______ ____ ____ ____/ /__
|
||||
/ __ \/ / ___/ __ `/_____/ __ \/ ___/ / ___/ _ \/ __ \/ __ \/ __ / _ \
|
||||
/ /_/ / (__ ) /_/ /_____/ /_/ / / / / /__/ __/ / / / /_/ / /_/ / __/
|
||||
/_.___/_/____/\__, / / .___/_/ /_/\___/\___/_/ /_/\____/\__,_/\___/
|
||||
/_/ /_/ ${application.formatted-version}
|
@ -1,16 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<appender name="CONSOLE_APPENDER" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{15}: %msg %xEx%n)</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="WARN">
|
||||
<appender-ref ref="CONSOLE_APPENDER"/>
|
||||
</root>
|
||||
|
||||
<logger name="bisq" level="INFO"/>
|
||||
<logger name="org.springframework.boot.context.embedded.tomcat" level="INFO"/>
|
||||
|
||||
</configuration>
|
@ -1 +0,0 @@
|
||||
0.7.2-SNAPSHOT
|
@ -1,66 +0,0 @@
|
||||
package bisq.price;
|
||||
|
||||
import bisq.price.spot.ExchangeRate;
|
||||
import bisq.price.spot.ExchangeRateProvider;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
@Slf4j
|
||||
public abstract class AbstractExchangeRateProviderTest {
|
||||
|
||||
protected void doGet_successfulCall(ExchangeRateProvider exchangeProvider) {
|
||||
|
||||
// Use the XChange library to call the provider API, in order to retrieve the
|
||||
// exchange rates. If the API call fails, or the response body cannot be parsed,
|
||||
// the test will fail with an exception
|
||||
Set<ExchangeRate> retrievedExchangeRates = exchangeProvider.doGet();
|
||||
|
||||
// Log the valid exchange rates which were retrieved
|
||||
// Useful when running the tests, to easily identify which exchanges provide
|
||||
// useful pairs
|
||||
retrievedExchangeRates.forEach(e -> log.info("Found exchange rate " + e.toString()));
|
||||
|
||||
// Sanity checks
|
||||
assertTrue(retrievedExchangeRates.size() > 0);
|
||||
checkProviderCurrencyPairs(exchangeProvider, retrievedExchangeRates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that every retrieved currency pair is between BTC and either
|
||||
* A) a fiat currency on the list of Bisq-supported fiat currencies, or
|
||||
* B) an altcoin on the list of Bisq-supported altcoins
|
||||
*
|
||||
* @param retrievedExchangeRates Exchange rates retrieved from the provider
|
||||
*/
|
||||
private void checkProviderCurrencyPairs(ExchangeRateProvider exchangeProvider, Set<ExchangeRate> retrievedExchangeRates) {
|
||||
Set<String> retrievedRatesCurrencies = retrievedExchangeRates.stream()
|
||||
.map(ExchangeRate::getCurrency)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
Set<String> supportedFiatCurrenciesRetrieved = exchangeProvider.getSupportedFiatCurrencies().stream()
|
||||
.filter(f -> retrievedRatesCurrencies.contains(f))
|
||||
.collect(Collectors.toCollection(TreeSet::new));
|
||||
log.info("Retrieved rates for supported fiat currencies: " + supportedFiatCurrenciesRetrieved);
|
||||
|
||||
Set<String> supportedCryptoCurrenciesRetrieved = exchangeProvider.getSupportedCryptoCurrencies().stream()
|
||||
.filter(c -> retrievedRatesCurrencies.contains(c))
|
||||
.collect(Collectors.toCollection(TreeSet::new));
|
||||
log.info("Retrieved rates for supported altcoins: " + supportedCryptoCurrenciesRetrieved);
|
||||
|
||||
Set<String> supportedCurrencies = Sets.union(
|
||||
exchangeProvider.getSupportedCryptoCurrencies(),
|
||||
exchangeProvider.getSupportedFiatCurrencies());
|
||||
|
||||
Set unsupportedCurrencies = Sets.difference(retrievedRatesCurrencies, supportedCurrencies);
|
||||
assertTrue("Retrieved exchange rates contain unsupported currencies: " + unsupportedCurrencies,
|
||||
unsupportedCurrencies.isEmpty());
|
||||
}
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
package bisq.price.mining;
|
||||
|
||||
import bisq.price.mining.providers.MempoolFeeRateProviderTest;
|
||||
|
||||
import bisq.common.config.Config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static bisq.price.mining.providers.MempoolFeeRateProviderTest.buildDummyReachableMempoolFeeRateProvider;
|
||||
import static bisq.price.mining.providers.MempoolFeeRateProviderTest.buildDummyUnreachableMempoolFeeRateProvider;
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
|
||||
/**
|
||||
* Tests the {@link bisq.price.mining.FeeRateService}, which can aggregate data from
|
||||
* several {@link FeeRateProvider}s.
|
||||
* @see MempoolFeeRateProviderTest
|
||||
*/
|
||||
public class FeeRateServiceTest {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(FeeRateServiceTest.class);
|
||||
|
||||
@Test
|
||||
public void getFees_noWorkingProvider() {
|
||||
// Several providers, but all unreachable
|
||||
List<FeeRateProvider> listOfProviders = new ArrayList<>();
|
||||
for (int i = 0; i < 3; i++) {
|
||||
try {
|
||||
listOfProviders.add(buildDummyUnreachableMempoolFeeRateProvider());
|
||||
} catch (Exception e) {
|
||||
// Expected
|
||||
log.info("We encountered an expected exception: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
FeeRateService service = new FeeRateService(listOfProviders);
|
||||
|
||||
Map<String, Object> retrievedData = service.getFees();
|
||||
|
||||
// Even with no working providers, we expect the service to return pre-configured
|
||||
// minimum fee rate
|
||||
doSanityChecksForRetrievedData(retrievedData, FeeRateProvider.MIN_FEE_RATE_FOR_TRADING);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFees_singleProvider_feeBelowMin() {
|
||||
// One working provider, which returns a fee lower than the minimum
|
||||
long providerFee = FeeRateProvider.MIN_FEE_RATE_FOR_TRADING - 3;
|
||||
FeeRateService service = new FeeRateService(
|
||||
Collections.singletonList(
|
||||
buildDummyReachableMempoolFeeRateProvider(providerFee)));
|
||||
|
||||
Map<String, Object> retrievedData = service.getFees();
|
||||
|
||||
// When the provider returns a value below the expected min, the service should
|
||||
// return the min
|
||||
doSanityChecksForRetrievedData(retrievedData, FeeRateProvider.MIN_FEE_RATE_FOR_TRADING);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFees_singleProvider_feeAboveMax() {
|
||||
// One working provider, which returns a fee greater than the maximum
|
||||
long providerFee = FeeRateProvider.MAX_FEE_RATE + 13;
|
||||
FeeRateService service = new FeeRateService(
|
||||
Collections.singletonList(
|
||||
buildDummyReachableMempoolFeeRateProvider(providerFee)));
|
||||
|
||||
Map<String, Object> retrievedData = service.getFees();
|
||||
|
||||
// When the provider returns a value above the expected max, the service should
|
||||
// return the max
|
||||
doSanityChecksForRetrievedData(retrievedData, FeeRateProvider.MAX_FEE_RATE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getFees_multipleProviders() {
|
||||
// 3 providers, returning 1xMIN, 2xMIN, 3xMIN
|
||||
FeeRateService service = new FeeRateService(asList(
|
||||
buildDummyReachableMempoolFeeRateProvider(FeeRateProvider.MIN_FEE_RATE_FOR_TRADING * 1),
|
||||
buildDummyReachableMempoolFeeRateProvider(FeeRateProvider.MIN_FEE_RATE_FOR_TRADING * 2),
|
||||
buildDummyReachableMempoolFeeRateProvider(FeeRateProvider.MIN_FEE_RATE_FOR_TRADING * 3)));
|
||||
|
||||
Map<String, Object> retrievedData = service.getFees();
|
||||
|
||||
// The service should then return the average, which is 2xMIN
|
||||
doSanityChecksForRetrievedData(retrievedData, FeeRateProvider.MIN_FEE_RATE_FOR_TRADING * 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a few basic sanity checks on the returned data object
|
||||
*/
|
||||
private void doSanityChecksForRetrievedData(Map<String, Object> retrievedData, long expectedFeeRate) {
|
||||
// Check if the response has the expected format. Since the timestamp is that of
|
||||
// the average (not that of the individual fee rates reported by the individual
|
||||
// providers), we always expect a non-zero timestamp
|
||||
assertNotEquals(0L, retrievedData.get(Config.BTC_FEES_TS));
|
||||
|
||||
Map<String, String> retrievedDataMap = (Map<String, String>) retrievedData.get(Config.LEGACY_FEE_DATAMAP);
|
||||
assertEquals(2, retrievedDataMap.size());
|
||||
assertEquals(expectedFeeRate, retrievedDataMap.get(Config.BTC_TX_FEE));
|
||||
}
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.mining.providers;
|
||||
|
||||
import bisq.price.mining.FeeRate;
|
||||
import bisq.price.mining.FeeRateProvider;
|
||||
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.env.StandardEnvironment;
|
||||
import org.springframework.web.client.RestClientException;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static java.lang.Thread.sleep;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* Tests specific to a {@link MempoolFeeRateProvider} which queries one API endpoint. For
|
||||
* tests related to managing parallel fee API endpoints, see
|
||||
* {@link bisq.price.mining.FeeRateServiceTest}
|
||||
*/
|
||||
public class MempoolFeeRateProviderTest {
|
||||
|
||||
private static final Environment env = new StandardEnvironment();
|
||||
|
||||
@Test
|
||||
public void doGet_successfulCall() {
|
||||
MempoolFeeRateProvider feeRateProvider = new MempoolFeeRateProvider.First(env);
|
||||
|
||||
// Make a call to the API, retrieve the recommended fee rate
|
||||
// If the API call fails, or the response body cannot be parsed, the test will
|
||||
// fail with an exception
|
||||
FeeRate retrievedFeeRate = feeRateProvider.doGet();
|
||||
|
||||
// Check that the FeeRateProvider returns a fee within the defined parameters
|
||||
assertTrue(retrievedFeeRate.getPrice() >= FeeRateProvider.MIN_FEE_RATE_FOR_TRADING);
|
||||
assertTrue(retrievedFeeRate.getPrice() <= FeeRateProvider.MAX_FEE_RATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates a reachable provider, which successfully returns an API response
|
||||
*/
|
||||
public static FeeRateProvider buildDummyReachableMempoolFeeRateProvider(long feeRate) {
|
||||
MempoolFeeRateProvider dummyProvider = new MempoolFeeRateProvider.First(env) {
|
||||
@Override
|
||||
protected FeeRate doGet() {
|
||||
return new FeeRate("BTC", feeRate, MIN_FEE_RATE_FOR_WITHDRAWAL, Instant.now().getEpochSecond());
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize provider
|
||||
dummyProvider.start();
|
||||
try {
|
||||
sleep(1000);
|
||||
} catch (InterruptedException e) { }
|
||||
dummyProvider.stop();
|
||||
|
||||
return dummyProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates an unreachable provider, which for whatever reason cannot deliver a
|
||||
* response to the API. Reasons for that could be: host went offline, connection
|
||||
* timeout, connection cannot be established (expired certificate), etc.
|
||||
*/
|
||||
public static FeeRateProvider buildDummyUnreachableMempoolFeeRateProvider() throws RestClientException {
|
||||
MempoolFeeRateProvider dummyProvider = new MempoolFeeRateProvider.First(env) {
|
||||
@Override
|
||||
protected FeeRate doGet() {
|
||||
throw new RestClientException("Simulating connection error when trying to reach API endpoint");
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize provider
|
||||
dummyProvider.start();
|
||||
try {
|
||||
sleep(1000);
|
||||
} catch (InterruptedException e) { }
|
||||
dummyProvider.stop();
|
||||
|
||||
return dummyProvider;
|
||||
}
|
||||
}
|
@ -1,414 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot;
|
||||
|
||||
import bisq.core.locale.CurrencyUtil;
|
||||
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.env.StandardEnvironment;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.apache.commons.lang3.RandomUtils;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.OptionalDouble;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ch.qos.logback.classic.Level;
|
||||
import ch.qos.logback.classic.Logger;
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||
import ch.qos.logback.core.read.ListAppender;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static java.lang.Thread.sleep;
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
public class ExchangeRateServiceTest {
|
||||
|
||||
/**
|
||||
* Logback version of the Slf4j logger used by {@link ExchangeRateService}. This
|
||||
* allows us to test if specific messages were logged.
|
||||
* See https://stackoverflow.com/a/52229629
|
||||
*/
|
||||
private static Logger exchangeRateServiceLogger;
|
||||
private static final String LIST_APPENDER_NAME = "testListAppender";
|
||||
|
||||
@BeforeAll
|
||||
static void setup() {
|
||||
// Get the logger object for logs in ExchangeRateService
|
||||
exchangeRateServiceLogger = (Logger) LoggerFactory.getLogger(ExchangeRateService.class);
|
||||
|
||||
// Initiate and append a ListAppender, which allows us to programmatically inspect
|
||||
// log messages
|
||||
ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
|
||||
listAppender.setName(LIST_APPENDER_NAME);
|
||||
listAppender.start();
|
||||
exchangeRateServiceLogger.addAppender(listAppender);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAllMarketPrices_withNoExchangeRates_logs_Exception() {
|
||||
int numberOfCurrencyPairsOnExchange = 0;
|
||||
ExchangeRateProvider dummyProvider = buildDummyExchangeRateProvider(numberOfCurrencyPairsOnExchange);
|
||||
ExchangeRateService service = new ExchangeRateService(Collections.singletonList(dummyProvider));
|
||||
|
||||
Map<String, Object> retrievedData = service.getAllMarketPrices();
|
||||
|
||||
doSanityChecksForRetrievedDataSingleProvider(retrievedData, dummyProvider, numberOfCurrencyPairsOnExchange);
|
||||
|
||||
// No exchange rates provided by this exchange, two things should happen
|
||||
// A) the timestamp should be set to 0
|
||||
// B) an error should be logged
|
||||
|
||||
// A) Check that timestamp was set to 0
|
||||
// This allows Bisq clients to eventually disregard data from this provider
|
||||
assertEquals(0L, retrievedData.get(dummyProvider.getPrefix() + "Ts"));
|
||||
|
||||
// B) Check that an error is logged
|
||||
// Log msg has the format: java.lang.IllegalStateException: No exchange rate data
|
||||
// found for ExchangeName-JzfP1
|
||||
List<ILoggingEvent> logsList = ((ListAppender) exchangeRateServiceLogger.getAppender(LIST_APPENDER_NAME)).list;
|
||||
assertEquals(Level.ERROR, logsList.get(0).getLevel());
|
||||
assertTrue(logsList.get(0).getMessage().endsWith("No exchange rate data found for " + dummyProvider.getName()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAllMarketPrices_withSingleExchangeRate() {
|
||||
int numberOfCurrencyPairsOnExchange = 1;
|
||||
ExchangeRateProvider dummyProvider = buildDummyExchangeRateProvider(numberOfCurrencyPairsOnExchange);
|
||||
ExchangeRateService service = new ExchangeRateService(Collections.singletonList(dummyProvider));
|
||||
|
||||
Map<String, Object> retrievedData = service.getAllMarketPrices();
|
||||
|
||||
doSanityChecksForRetrievedDataSingleProvider(retrievedData, dummyProvider, numberOfCurrencyPairsOnExchange);
|
||||
|
||||
// One rate was provided by this provider, so the timestamp should not be 0
|
||||
assertNotEquals(0L, retrievedData.get(dummyProvider.getPrefix() + "Ts"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAllMarketPrices_withMultipleProviders_differentCurrencyCodes() {
|
||||
int numberOfCurrencyPairsOnExchange = 1;
|
||||
ExchangeRateProvider dummyProvider1 = buildDummyExchangeRateProvider(numberOfCurrencyPairsOnExchange);
|
||||
ExchangeRateProvider dummyProvider2 = buildDummyExchangeRateProvider(numberOfCurrencyPairsOnExchange);
|
||||
ExchangeRateService service = new ExchangeRateService(asList(dummyProvider1, dummyProvider2));
|
||||
|
||||
Map<String, Object> retrievedData = service.getAllMarketPrices();
|
||||
|
||||
doSanityChecksForRetrievedDataMultipleProviders(retrievedData, asList(dummyProvider1, dummyProvider2));
|
||||
|
||||
// One rate was provided by each provider in this service, so the timestamp
|
||||
// (for both providers) should not be 0
|
||||
assertNotEquals(0L, retrievedData.get(dummyProvider1.getPrefix() + "Ts"));
|
||||
assertNotEquals(0L, retrievedData.get(dummyProvider2.getPrefix() + "Ts"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the scenario when multiple providers have rates for the same currencies
|
||||
*/
|
||||
@Test
|
||||
public void getAllMarketPrices_withMultipleProviders_overlappingCurrencyCodes() {
|
||||
|
||||
// List of currencies for which multiple providers will have exchange rates
|
||||
Set<String> rateCurrencyCodes = Sets.newHashSet("CURRENCY-1", "CURRENCY-2", "CURRENCY-3");
|
||||
|
||||
// Create several dummy providers, each providing their own rates for the same set of currencies
|
||||
ExchangeRateProvider dummyProvider1 = buildDummyExchangeRateProvider(rateCurrencyCodes);
|
||||
ExchangeRateProvider dummyProvider2 = buildDummyExchangeRateProvider(rateCurrencyCodes);
|
||||
|
||||
ExchangeRateService service = new ExchangeRateService(asList(dummyProvider1, dummyProvider2));
|
||||
|
||||
Map<String, Object> retrievedData = service.getAllMarketPrices();
|
||||
|
||||
doSanityChecksForRetrievedDataMultipleProviders(retrievedData, asList(dummyProvider1, dummyProvider2));
|
||||
|
||||
// At least one rate was provided by each provider in this service, so the
|
||||
// timestamp (for both providers) should not be 0
|
||||
assertNotEquals(0L, retrievedData.get(dummyProvider1.getPrefix() + "Ts"));
|
||||
assertNotEquals(0L, retrievedData.get(dummyProvider2.getPrefix() + "Ts"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the scenario when currencies are excluded from the PriceNode feed via configuration settings
|
||||
*/
|
||||
@Test
|
||||
public void getAllMarketPrices_withMultipleProviders_excludedCurrencyCodes() {
|
||||
String excludedCcyString = "LBP,USD,EUR";
|
||||
String providerExcludedCcyString = "HUOBI:BRL,BINANCE:GBP,BINANCE:SEK";
|
||||
Environment mockedEnvironment = mock(Environment.class);
|
||||
when(mockedEnvironment.getProperty(eq("bisq.price.fiatcurrency.excluded"), anyString())).thenReturn(excludedCcyString);
|
||||
when(mockedEnvironment.getProperty(eq("bisq.price.fiatcurrency.excludedByProvider"), anyString())).thenReturn(providerExcludedCcyString);
|
||||
|
||||
class MockedExchangeRateProvider extends ExchangeRateProvider {
|
||||
MockedExchangeRateProvider() {
|
||||
super(mockedEnvironment, "ExchangeName", "EXCH", Duration.ofDays(1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ExchangeRate> doGet() {
|
||||
HashSet<ExchangeRate> exchangeRates = new HashSet<>();
|
||||
// Simulate rates for all the supported ccys
|
||||
for (String rateCurrencyCode : getSupportedFiatCurrencies()) {
|
||||
exchangeRates.add(new ExchangeRate(
|
||||
rateCurrencyCode,
|
||||
RandomUtils.nextDouble(1, 1000), // random price
|
||||
System.currentTimeMillis(),
|
||||
getName())); // ExchangeRateProvider name
|
||||
}
|
||||
return exchangeRates;
|
||||
}
|
||||
}
|
||||
|
||||
Logger exchangeRateProviderLogger;
|
||||
String LIST_APPENDER_NAME2 = "testListAppender2";
|
||||
exchangeRateProviderLogger = (Logger) LoggerFactory.getLogger(MockedExchangeRateProvider.class);
|
||||
ListAppender<ILoggingEvent> listAppender2 = new ListAppender<>();
|
||||
listAppender2.setName(LIST_APPENDER_NAME2);
|
||||
listAppender2.start();
|
||||
exchangeRateProviderLogger.addAppender(listAppender2);
|
||||
|
||||
// we request rates for all currencies, and check that:
|
||||
// (a) the provider supplies more currency rates than the number of currencies we are trying to exclude (sanity test),
|
||||
// (b) the number of missing currency rates equals the number of currencies we told PriceNode to exclude,
|
||||
// (c) none of the rates supplied are for an excluded currency.
|
||||
|
||||
Set<String> excludedFiatCurrencies = new HashSet<>(asList(excludedCcyString.split(",")));
|
||||
MockedExchangeRateProvider mockedExchangeRateProvider = new MockedExchangeRateProvider();
|
||||
Set<ExchangeRate> exchangeRates = mockedExchangeRateProvider.doGet();
|
||||
assertTrue(exchangeRates.size() > excludedFiatCurrencies.size());
|
||||
int numSortedFiatCurrencies = CurrencyUtil.getAllSortedFiatCurrencies().size();
|
||||
int numCurrenciesFromProvider = mockedExchangeRateProvider.getSupportedFiatCurrencies().size();
|
||||
int missingCurrencyCount = numSortedFiatCurrencies - numCurrenciesFromProvider;
|
||||
assertEquals(missingCurrencyCount, excludedFiatCurrencies.size());
|
||||
for (ExchangeRate exchangeRate : exchangeRates) {
|
||||
assertFalse(excludedCcyString.contains(exchangeRate.getCurrency()));
|
||||
}
|
||||
List<ILoggingEvent> logsList = ((ListAppender) exchangeRateProviderLogger.getAppender(LIST_APPENDER_NAME2)).list;
|
||||
assertEquals(3, logsList.size());
|
||||
assertEquals(Level.INFO, logsList.get(1).getLevel());
|
||||
assertTrue(logsList.get(0).getFormattedMessage().endsWith("will refresh every PT24H"));
|
||||
assertTrue(logsList.get(1).getFormattedMessage().endsWith("fiat currencies excluded: [LBP, USD, EUR]"));
|
||||
assertTrue(logsList.get(2).getFormattedMessage().endsWith("fiat currencies supported: " + numCurrenciesFromProvider));
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs generic sanity checks on the response format and contents.
|
||||
*
|
||||
* @param retrievedData Response data retrieved from the {@link ExchangeRateService}
|
||||
* @param provider {@link ExchangeRateProvider} available to the
|
||||
* {@link ExchangeRateService}
|
||||
* @param numberOfCurrencyPairsOnExchange Number of currency pairs this exchange was
|
||||
* initiated with
|
||||
*/
|
||||
private void doSanityChecksForRetrievedDataSingleProvider(Map<String, Object> retrievedData,
|
||||
ExchangeRateProvider provider,
|
||||
int numberOfCurrencyPairsOnExchange) {
|
||||
// Check response structure
|
||||
doSanityChecksForRetrievedDataMultipleProviders(retrievedData, asList(provider));
|
||||
|
||||
// Check that the amount of provided exchange rates matches expected value
|
||||
// For one provider, the amount of rates of that provider should be the total
|
||||
// amount of rates in the response
|
||||
List<String> retrievedMarketPricesData = (List<String>) retrievedData.get("data");
|
||||
assertEquals(numberOfCurrencyPairsOnExchange, retrievedMarketPricesData.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs generic sanity checks on the response format and contents.
|
||||
*
|
||||
* @param retrievedData Response data retrieved from the {@link ExchangeRateService}
|
||||
* @param providers List of all {@link ExchangeRateProvider#getPrefix()} the
|
||||
* {@link ExchangeRateService} uses
|
||||
*/
|
||||
private void doSanityChecksForRetrievedDataMultipleProviders(Map<String, Object> retrievedData,
|
||||
List<ExchangeRateProvider> providers) {
|
||||
// Check the correct amount of entries were present in the service response:
|
||||
// The timestamp and the count fields are per provider, so N providers means N
|
||||
// times those fields timestamp (x N) + count (x N) + price data (stored as a list
|
||||
// under the key "data"). So expected size is Nx2 + 1.
|
||||
int n = providers.size();
|
||||
assertEquals(n * 2 + 1, retrievedData.size());
|
||||
for (ExchangeRateProvider provider : providers) {
|
||||
String providerPrefix = provider.getPrefix();
|
||||
assertNotNull(retrievedData.get(providerPrefix + "Ts"));
|
||||
assertNotNull(retrievedData.get(providerPrefix + "Count"));
|
||||
}
|
||||
|
||||
// Check validity of the data field
|
||||
List<ExchangeRate> retrievedRates = (List<ExchangeRate>) retrievedData.get("data");
|
||||
assertNotNull(retrievedRates);
|
||||
|
||||
// It should contain no duplicate ExchangeRate objects
|
||||
int uniqueRates = Sets.newHashSet(retrievedRates).size();
|
||||
int totalRates = retrievedRates.size();
|
||||
assertEquals(uniqueRates, totalRates, "Found duplicate rates in data field");
|
||||
|
||||
// There should be only one ExchangeRate per currency
|
||||
// In other words, even if multiple providers return rates for the same currency,
|
||||
// the ExchangeRateService should expose only one (aggregate) ExchangeRate for
|
||||
// that currency
|
||||
Map<String, ExchangeRate> currencyCodeToExchangeRateFromService = retrievedRates.stream()
|
||||
.collect(Collectors.toMap(
|
||||
ExchangeRate::getCurrency, exchangeRate -> exchangeRate
|
||||
));
|
||||
int uniqueCurrencyCodes = currencyCodeToExchangeRateFromService.keySet().size();
|
||||
assertEquals(uniqueCurrencyCodes, uniqueRates, "Found currency code with multiple exchange rates");
|
||||
|
||||
// Collect all ExchangeRates from all providers and group them by currency code
|
||||
Map<String, List<ExchangeRate>> currencyCodeToExchangeRatesFromProviders = new HashMap<>();
|
||||
for (ExchangeRateProvider p : providers) {
|
||||
for (ExchangeRate exchangeRate : p.get()) {
|
||||
String currencyCode = exchangeRate.getCurrency();
|
||||
if (currencyCodeToExchangeRatesFromProviders.containsKey(currencyCode)) {
|
||||
List<ExchangeRate> l = new ArrayList<>(currencyCodeToExchangeRatesFromProviders.get(currencyCode));
|
||||
l.add(exchangeRate);
|
||||
currencyCodeToExchangeRatesFromProviders.put(currencyCode, l);
|
||||
} else {
|
||||
currencyCodeToExchangeRatesFromProviders.put(currencyCode, asList(exchangeRate));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For each ExchangeRate which is covered by multiple providers, ensure the rate
|
||||
// value is an average
|
||||
currencyCodeToExchangeRatesFromProviders.forEach((currencyCode, exchangeRateList) -> {
|
||||
ExchangeRate rateFromService = currencyCodeToExchangeRateFromService.get(currencyCode);
|
||||
double priceFromService = rateFromService.getPrice();
|
||||
|
||||
OptionalDouble opt = exchangeRateList.stream().mapToDouble(ExchangeRate::getPrice).average();
|
||||
double priceAvgFromProviders = opt.getAsDouble();
|
||||
|
||||
// Ensure that the ExchangeRateService correctly aggregates exchange rates
|
||||
// from multiple providers. If multiple providers contain rates for a
|
||||
// currency, the service should return a single aggregate rate
|
||||
// Expected value for aggregate rate = avg(provider rates)
|
||||
// This formula works for any number of providers for a specific currency
|
||||
assertEquals(priceFromService, priceAvgFromProviders, "Service returned incorrect aggregate rate");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param numberOfRatesAvailable Number of exchange rates this provider returns
|
||||
* @return Dummy {@link ExchangeRateProvider} providing rates for
|
||||
* "numberOfRatesAvailable" random currency codes
|
||||
*/
|
||||
private ExchangeRateProvider buildDummyExchangeRateProvider(int numberOfRatesAvailable) {
|
||||
ExchangeRateProvider dummyProvider = new ExchangeRateProvider(
|
||||
new StandardEnvironment(),
|
||||
"ExchangeName-" + getRandomAlphaNumericString(5),
|
||||
"EXCH-" + getRandomAlphaNumericString(3),
|
||||
Duration.ofDays(1)) {
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Set<ExchangeRate> doGet() {
|
||||
HashSet<ExchangeRate> exchangeRates = new HashSet<>();
|
||||
|
||||
// Simulate the required amount of rates
|
||||
for (int i = 0; i < numberOfRatesAvailable; i++) {
|
||||
exchangeRates.add(new ExchangeRate(
|
||||
// random symbol, avoid duplicates
|
||||
"DUM-" + getRandomAlphaNumericString(3),
|
||||
RandomUtils.nextDouble(1, 1000), // random price
|
||||
System.currentTimeMillis(),
|
||||
getName())); // ExchangeRateProvider name
|
||||
}
|
||||
|
||||
return exchangeRates;
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize provider
|
||||
dummyProvider.start();
|
||||
try {
|
||||
sleep(1000);
|
||||
} catch (InterruptedException e) { }
|
||||
dummyProvider.stop();
|
||||
|
||||
return dummyProvider;
|
||||
}
|
||||
|
||||
private ExchangeRateProvider buildDummyExchangeRateProvider(Set<String> rateCurrencyCodes) {
|
||||
ExchangeRateProvider dummyProvider = new ExchangeRateProvider(
|
||||
new StandardEnvironment(),
|
||||
"ExchangeName-" + getRandomAlphaNumericString(5),
|
||||
"EXCH-" + getRandomAlphaNumericString(3),
|
||||
Duration.ofDays(1)) {
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Set<ExchangeRate> doGet() {
|
||||
HashSet<ExchangeRate> exchangeRates = new HashSet<>();
|
||||
|
||||
// Simulate the required amount of rates
|
||||
for (String rateCurrencyCode : rateCurrencyCodes) {
|
||||
exchangeRates.add(new ExchangeRate(
|
||||
rateCurrencyCode,
|
||||
RandomUtils.nextDouble(1, 1000), // random price
|
||||
System.currentTimeMillis(),
|
||||
getName())); // ExchangeRateProvider name
|
||||
}
|
||||
|
||||
return exchangeRates;
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize provider
|
||||
dummyProvider.start();
|
||||
try {
|
||||
sleep(1000);
|
||||
} catch (InterruptedException e) { }
|
||||
dummyProvider.stop();
|
||||
|
||||
return dummyProvider;
|
||||
}
|
||||
|
||||
private static String getRandomAlphaNumericString(int length) {
|
||||
return RandomStringUtils.random(length, true, true);
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot.providers;
|
||||
|
||||
import bisq.price.AbstractExchangeRateProviderTest;
|
||||
|
||||
import org.springframework.core.env.StandardEnvironment;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@Slf4j
|
||||
public class BTCMarketsTest extends AbstractExchangeRateProviderTest {
|
||||
|
||||
@Test
|
||||
public void doGet_successfulCall() {
|
||||
doGet_successfulCall(new BTCMarkets(new StandardEnvironment()));
|
||||
}
|
||||
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot.providers;
|
||||
|
||||
import bisq.price.AbstractExchangeRateProviderTest;
|
||||
|
||||
import org.springframework.core.env.StandardEnvironment;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@Slf4j
|
||||
public class BinanceTest extends AbstractExchangeRateProviderTest {
|
||||
|
||||
@Test
|
||||
public void doGet_successfulCall() {
|
||||
doGet_successfulCall(new Binance(new StandardEnvironment()));
|
||||
}
|
||||
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot.providers;
|
||||
|
||||
import bisq.price.AbstractExchangeRateProviderTest;
|
||||
|
||||
import org.springframework.core.env.StandardEnvironment;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@Slf4j
|
||||
public class BitfinexTest extends AbstractExchangeRateProviderTest {
|
||||
|
||||
@Test
|
||||
public void doGet_successfulCall() {
|
||||
doGet_successfulCall(new Bitfinex(new StandardEnvironment()));
|
||||
}
|
||||
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot.providers;
|
||||
|
||||
import bisq.price.AbstractExchangeRateProviderTest;
|
||||
|
||||
import org.springframework.core.env.StandardEnvironment;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@Slf4j
|
||||
public class BitflyerTest extends AbstractExchangeRateProviderTest {
|
||||
|
||||
@Test
|
||||
public void doGet_successfulCall() {
|
||||
doGet_successfulCall(new Bitflyer(new StandardEnvironment()));
|
||||
}
|
||||
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot.providers;
|
||||
|
||||
import bisq.price.AbstractExchangeRateProviderTest;
|
||||
|
||||
import org.springframework.core.env.StandardEnvironment;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@Slf4j
|
||||
public class BitstampTest extends AbstractExchangeRateProviderTest {
|
||||
|
||||
@Test
|
||||
public void doGet_successfulCall() {
|
||||
doGet_successfulCall(new Bitstamp(new StandardEnvironment()));
|
||||
}
|
||||
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot.providers;
|
||||
|
||||
import bisq.price.AbstractExchangeRateProviderTest;
|
||||
|
||||
import org.springframework.core.env.StandardEnvironment;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@Slf4j
|
||||
public class CoinGeckoTest extends AbstractExchangeRateProviderTest {
|
||||
|
||||
@Test
|
||||
public void doGet_successfulCall() {
|
||||
doGet_successfulCall(new CoinGecko(new StandardEnvironment()));
|
||||
}
|
||||
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot.providers;
|
||||
|
||||
import bisq.price.AbstractExchangeRateProviderTest;
|
||||
|
||||
import org.springframework.core.env.StandardEnvironment;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@Slf4j
|
||||
public class CoinbaseProTest extends AbstractExchangeRateProviderTest {
|
||||
|
||||
@Test
|
||||
public void doGet_successfulCall() {
|
||||
doGet_successfulCall(new CoinbasePro(new StandardEnvironment()));
|
||||
}
|
||||
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot.providers;
|
||||
|
||||
import bisq.price.AbstractExchangeRateProviderTest;
|
||||
|
||||
import org.springframework.core.env.StandardEnvironment;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@Slf4j
|
||||
public class CoinoneTest extends AbstractExchangeRateProviderTest {
|
||||
|
||||
@Test
|
||||
public void doGet_successfulCall() {
|
||||
doGet_successfulCall(new Coinone(new StandardEnvironment()));
|
||||
}
|
||||
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot.providers;
|
||||
|
||||
import bisq.price.AbstractExchangeRateProviderTest;
|
||||
|
||||
import org.springframework.core.env.StandardEnvironment;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@Slf4j
|
||||
public class IndependentReserveTest extends AbstractExchangeRateProviderTest {
|
||||
|
||||
@Test
|
||||
public void doGet_successfulCall() {
|
||||
doGet_successfulCall(new IndependentReserve(new StandardEnvironment()));
|
||||
}
|
||||
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot.providers;
|
||||
|
||||
import bisq.price.AbstractExchangeRateProviderTest;
|
||||
|
||||
import org.springframework.core.env.StandardEnvironment;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@Slf4j
|
||||
public class KrakenTest extends AbstractExchangeRateProviderTest {
|
||||
|
||||
@Test
|
||||
public void doGet_successfulCall() {
|
||||
doGet_successfulCall(new Kraken(new StandardEnvironment()));
|
||||
}
|
||||
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot.providers;
|
||||
|
||||
import bisq.price.AbstractExchangeRateProviderTest;
|
||||
|
||||
import org.springframework.core.env.StandardEnvironment;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@Slf4j
|
||||
public class LunoTest extends AbstractExchangeRateProviderTest {
|
||||
|
||||
@Test
|
||||
public void doGet_successfulCall() {
|
||||
doGet_successfulCall(new Luno(new StandardEnvironment()));
|
||||
}
|
||||
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot.providers;
|
||||
|
||||
import bisq.price.AbstractExchangeRateProviderTest;
|
||||
|
||||
import org.springframework.core.env.StandardEnvironment;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@Slf4j
|
||||
public class MercadoBitcoinTest extends AbstractExchangeRateProviderTest {
|
||||
|
||||
@Test
|
||||
public void doGet_successfulCall() {
|
||||
doGet_successfulCall(new MercadoBitcoin(new StandardEnvironment()));
|
||||
}
|
||||
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot.providers;
|
||||
|
||||
import bisq.price.AbstractExchangeRateProviderTest;
|
||||
|
||||
import org.springframework.core.env.StandardEnvironment;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@Slf4j
|
||||
public class ParibuTest extends AbstractExchangeRateProviderTest {
|
||||
|
||||
@Test
|
||||
public void doGet_successfulCall() {
|
||||
doGet_successfulCall(new Paribu(new StandardEnvironment()));
|
||||
}
|
||||
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot.providers;
|
||||
|
||||
import bisq.price.AbstractExchangeRateProviderTest;
|
||||
|
||||
import org.springframework.core.env.StandardEnvironment;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@Slf4j
|
||||
public class PoloniexTest extends AbstractExchangeRateProviderTest {
|
||||
|
||||
@Test
|
||||
public void doGet_successfulCall() {
|
||||
doGet_successfulCall(new Poloniex(new StandardEnvironment()));
|
||||
}
|
||||
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot.providers;
|
||||
|
||||
import bisq.price.AbstractExchangeRateProviderTest;
|
||||
|
||||
import org.springframework.core.env.StandardEnvironment;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@Slf4j
|
||||
public class QuoineTest extends AbstractExchangeRateProviderTest {
|
||||
|
||||
@Test
|
||||
public void doGet_successfulCall() {
|
||||
doGet_successfulCall(new Quoine(new StandardEnvironment()));
|
||||
}
|
||||
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
HiddenServiceDir build/tor-hidden-service
|
||||
HiddenServicePort 80 127.0.0.1:8080
|
@ -1,29 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
echo "[*] Network Size Monitoring removal script"
|
||||
|
||||
##### change paths if necessary for your system
|
||||
|
||||
ROOT_USER=root
|
||||
|
||||
SCRAPER_HOME=/journalreader
|
||||
|
||||
#####
|
||||
echo "[*] Checking environment..."
|
||||
if [ ! -f "${SCRAPER_HOME}/scraperscript_hsversion.sh" ]; then
|
||||
echo 'There is nothing to be removed.'
|
||||
echo 'Exiting...'
|
||||
exit
|
||||
fi
|
||||
|
||||
echo "[*] Removing journal parser script"
|
||||
sudo -H -i -u "${ROOT_USER}" rm "${SCRAPER_HOME}/scraperscript_hsversion.sh"
|
||||
|
||||
echo "[*] Reverting collectd config"
|
||||
sudo -H -i -u "${ROOT_USER}" sed -i '/<Plugin exec>.*/ {N;N; s/<Plugin exec>.*scraperscript_hsversion.sh.*<.Plugin>//g}' /etc/collectd/collectd.conf
|
||||
|
||||
echo "[*] Restarting services"
|
||||
sudo -H -i -u "${ROOT_USER}" systemctl restart collectd.service
|
||||
|
||||
echo '[*] Done!'
|
@ -7,7 +7,6 @@ include 'cli'
|
||||
include 'daemon'
|
||||
include 'desktop'
|
||||
include 'monitor'
|
||||
include 'pricenode'
|
||||
include 'relay'
|
||||
include 'seednode'
|
||||
include 'statsnode'
|
||||
|
Loading…
Reference in New Issue
Block a user