mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-24 12:46:45 +00:00
fetch fee rates from configurable external sources
This commit is contained in:
parent
02c8415a7b
commit
ee7b741a69
12 changed files with 215 additions and 59 deletions
|
@ -1495,6 +1495,15 @@ public class AppController implements Initializable {
|
|||
selectedToggle.ifPresent(toggle -> bitcoinUnit.selectToggle(toggle));
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void feeRateSourceChanged(FeeRatesSourceChangedEvent event) {
|
||||
ElectrumServer.FeeRatesService feeRatesService = new ElectrumServer.FeeRatesService();
|
||||
feeRatesService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(feeRatesService.getValue());
|
||||
});
|
||||
feeRatesService.start();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void fiatCurrencySelected(FiatCurrencySelectedEvent event) {
|
||||
ratesService.cancel();
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.sparrow.wallet.FeeRateSelection;
|
||||
|
||||
public class FeeRateSelectionChangedEvent {
|
||||
private final FeeRateSelection feeRateSelection;
|
||||
|
||||
public FeeRateSelectionChangedEvent(FeeRateSelection feeRateSelection) {
|
||||
this.feeRateSelection = feeRateSelection;
|
||||
}
|
||||
|
||||
public FeeRateSelection getFeeRateSelection() {
|
||||
return feeRateSelection;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.sparrow.wallet.FeeRatesSelection;
|
||||
|
||||
public class FeeRatesSelectionChangedEvent {
|
||||
private final FeeRatesSelection feeRatesSelection;
|
||||
|
||||
public FeeRatesSelectionChangedEvent(FeeRatesSelection feeRatesSelection) {
|
||||
this.feeRatesSelection = feeRatesSelection;
|
||||
}
|
||||
|
||||
public FeeRatesSelection getFeeRateSelection() {
|
||||
return feeRatesSelection;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.sparrow.net.FeeRatesSource;
|
||||
|
||||
public class FeeRatesSourceChangedEvent {
|
||||
private final FeeRatesSource feeRatesSource;
|
||||
|
||||
public FeeRatesSourceChangedEvent(FeeRatesSource feeRatesSource) {
|
||||
this.feeRatesSource = feeRatesSource;
|
||||
}
|
||||
|
||||
public FeeRatesSource getFeeRateSource() {
|
||||
return feeRatesSource;
|
||||
}
|
||||
}
|
|
@ -5,7 +5,8 @@ import com.sparrowwallet.drongo.BitcoinUnit;
|
|||
import com.sparrowwallet.sparrow.Mode;
|
||||
import com.sparrowwallet.sparrow.Theme;
|
||||
import com.sparrowwallet.sparrow.net.ExchangeSource;
|
||||
import com.sparrowwallet.sparrow.wallet.FeeRateSelection;
|
||||
import com.sparrowwallet.sparrow.net.FeeRatesSource;
|
||||
import com.sparrowwallet.sparrow.wallet.FeeRatesSelection;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -21,7 +22,8 @@ public class Config {
|
|||
|
||||
private Mode mode;
|
||||
private BitcoinUnit bitcoinUnit;
|
||||
private FeeRateSelection feeRateSelection;
|
||||
private FeeRatesSource feeRatesSource;
|
||||
private FeeRatesSelection feeRatesSelection;
|
||||
private Currency fiatCurrency;
|
||||
private ExchangeSource exchangeSource;
|
||||
private boolean groupByAddress = true;
|
||||
|
@ -101,12 +103,21 @@ public class Config {
|
|||
flush();
|
||||
}
|
||||
|
||||
public FeeRateSelection getFeeRateSelection() {
|
||||
return feeRateSelection;
|
||||
public FeeRatesSource getFeeRatesSource() {
|
||||
return feeRatesSource;
|
||||
}
|
||||
|
||||
public void setFeeRateSelection(FeeRateSelection feeRateSelection) {
|
||||
this.feeRateSelection = feeRateSelection;
|
||||
public void setFeeRatesSource(FeeRatesSource feeRatesSource) {
|
||||
this.feeRatesSource = feeRatesSource;
|
||||
flush();
|
||||
}
|
||||
|
||||
public FeeRatesSelection getFeeRatesSelection() {
|
||||
return feeRatesSelection;
|
||||
}
|
||||
|
||||
public void setFeeRatesSelection(FeeRatesSelection feeRatesSelection) {
|
||||
this.feeRatesSelection = feeRatesSelection;
|
||||
flush();
|
||||
}
|
||||
|
||||
|
|
|
@ -604,6 +604,11 @@ public class ElectrumServer {
|
|||
targetBlocksFeeRatesSats.put(target, minFeeRateSatsKb / 1000d);
|
||||
}
|
||||
|
||||
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
|
||||
if(feeRatesSource != null) {
|
||||
targetBlocksFeeRatesSats.putAll(feeRatesSource.getBlockTargetFeeRates(targetBlocksFeeRatesSats));
|
||||
}
|
||||
|
||||
return targetBlocksFeeRatesSats;
|
||||
} catch(ElectrumServerRpcException e) {
|
||||
throw new ServerException(e.getMessage(), e);
|
||||
|
@ -718,7 +723,7 @@ public class ElectrumServer {
|
|||
}
|
||||
|
||||
public static class ConnectionService extends ScheduledService<FeeRatesUpdatedEvent> implements Thread.UncaughtExceptionHandler {
|
||||
private static final int FEE_RATES_PERIOD = 1 * 60 * 1000;
|
||||
private static final int FEE_RATES_PERIOD = 30 * 1000;
|
||||
|
||||
private final boolean subscribe;
|
||||
private boolean firstCall = true;
|
||||
|
@ -1014,4 +1019,18 @@ public class ElectrumServer {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class FeeRatesService extends Service<FeeRatesUpdatedEvent> {
|
||||
@Override
|
||||
protected Task<FeeRatesUpdatedEvent> createTask() {
|
||||
return new Task<>() {
|
||||
protected FeeRatesUpdatedEvent call() throws ServerException {
|
||||
ElectrumServer electrumServer = new ElectrumServer();
|
||||
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(SendController.TARGET_BLOCKS_RANGE);
|
||||
Set<MempoolRateSize> mempoolRateSizes = electrumServer.getMempoolRateSizes();
|
||||
return new FeeRatesUpdatedEvent(blockTargetFeeRates, mempoolRateSizes);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
102
src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java
Normal file
102
src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java
Normal file
|
@ -0,0 +1,102 @@
|
|||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.google.gson.Gson;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Proxy;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public enum FeeRatesSource {
|
||||
ELECTRUM_SERVER("Electrum Server") {
|
||||
@Override
|
||||
public Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
},
|
||||
MEMPOOL_SPACE("mempool.space") {
|
||||
@Override
|
||||
public Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
|
||||
String url = "https://mempool.space/api/v1/fees/recommended";
|
||||
return getThreeTierFeeRates(defaultblockTargetFeeRates, url);
|
||||
}
|
||||
},
|
||||
BITCOINFEES_EARN_COM("bitcoinfees.earn.com") {
|
||||
@Override
|
||||
public Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
|
||||
String url = "https://bitcoinfees.earn.com/api/v1/fees/recommended";
|
||||
return getThreeTierFeeRates(defaultblockTargetFeeRates, url);
|
||||
}
|
||||
};
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(FeeRatesSource.class);
|
||||
|
||||
private final String name;
|
||||
|
||||
FeeRatesSource(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public abstract Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates);
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
private static Map<Integer, Double> getThreeTierFeeRates(Map<Integer, Double> defaultblockTargetFeeRates, String url) {
|
||||
Proxy proxy = getProxy();
|
||||
|
||||
Map<Integer, Double> blockTargetFeeRates = new LinkedHashMap<>();
|
||||
try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream()); Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {
|
||||
Gson gson = new Gson();
|
||||
ThreeTierRates threeTierRates = gson.fromJson(reader, ThreeTierRates.class);
|
||||
for(Integer blockTarget : defaultblockTargetFeeRates.keySet()) {
|
||||
if(blockTarget < 3) {
|
||||
blockTargetFeeRates.put(blockTarget, threeTierRates.fastestFee);
|
||||
} else if(blockTarget < 6) {
|
||||
blockTargetFeeRates.put(blockTarget, threeTierRates.halfHourFee);
|
||||
} else if(blockTarget <= 10 || defaultblockTargetFeeRates.get(blockTarget) > threeTierRates.hourFee) {
|
||||
blockTargetFeeRates.put(blockTarget, threeTierRates.hourFee);
|
||||
} else {
|
||||
blockTargetFeeRates.put(blockTarget, defaultblockTargetFeeRates.get(blockTarget));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Error retrieving recommended fee rates from " + url, e);
|
||||
}
|
||||
|
||||
return blockTargetFeeRates;
|
||||
}
|
||||
|
||||
private static Proxy getProxy() {
|
||||
Config config = Config.get();
|
||||
if(config.isUseProxy()) {
|
||||
HostAndPort proxy = HostAndPort.fromString(config.getProxyServer());
|
||||
InetSocketAddress proxyAddress = new InetSocketAddress(proxy.getHost(), proxy.getPortOrDefault(ProxyTcpOverTlsTransport.DEFAULT_PROXY_PORT));
|
||||
return new Proxy(Proxy.Type.SOCKS, proxyAddress);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
|
||||
private static class ThreeTierRates {
|
||||
Double fastestFee;
|
||||
Double halfHourFee;
|
||||
Double hourFee;
|
||||
}
|
||||
}
|
|
@ -3,13 +3,10 @@ package com.sparrowwallet.sparrow.preferences;
|
|||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch;
|
||||
import com.sparrowwallet.sparrow.event.BitcoinUnitChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.FeeRateSelectionChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.FiatCurrencySelectedEvent;
|
||||
import com.sparrowwallet.sparrow.event.VersionCheckStatusEvent;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.net.ExchangeSource;
|
||||
import com.sparrowwallet.sparrow.wallet.FeeRateSelection;
|
||||
import com.sparrowwallet.sparrow.net.FeeRatesSource;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.fxml.FXML;
|
||||
|
@ -27,7 +24,7 @@ public class GeneralPreferencesController extends PreferencesDetailController {
|
|||
private ComboBox<BitcoinUnit> bitcoinUnit;
|
||||
|
||||
@FXML
|
||||
private ComboBox<FeeRateSelection> feeRateSelection;
|
||||
private ComboBox<FeeRatesSource> feeRatesSource;
|
||||
|
||||
@FXML
|
||||
private ComboBox<Currency> fiatCurrency;
|
||||
|
@ -68,16 +65,16 @@ public class GeneralPreferencesController extends PreferencesDetailController {
|
|||
EventManager.get().post(new BitcoinUnitChangedEvent(newValue));
|
||||
});
|
||||
|
||||
if(config.getFeeRateSelection() != null) {
|
||||
feeRateSelection.setValue(config.getFeeRateSelection());
|
||||
if(config.getFeeRatesSource() != null) {
|
||||
feeRatesSource.setValue(config.getFeeRatesSource());
|
||||
} else {
|
||||
feeRateSelection.getSelectionModel().select(0);
|
||||
config.setFeeRateSelection(FeeRateSelection.BLOCK_TARGET);
|
||||
feeRatesSource.getSelectionModel().select(1);
|
||||
config.setFeeRatesSource(feeRatesSource.getValue());
|
||||
}
|
||||
|
||||
feeRateSelection.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
config.setFeeRateSelection(newValue);
|
||||
EventManager.get().post(new FeeRateSelectionChangedEvent(newValue));
|
||||
feeRatesSource.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
config.setFeeRatesSource(newValue);
|
||||
EventManager.get().post(new FeeRatesSourceChangedEvent(newValue));
|
||||
});
|
||||
|
||||
if(config.getExchangeSource() != null) {
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
package com.sparrowwallet.sparrow.wallet;
|
||||
|
||||
public enum FeeRateSelection {
|
||||
public enum FeeRatesSelection {
|
||||
BLOCK_TARGET("Block Target"), MEMPOOL_SIZE("Mempool Size");
|
||||
|
||||
private final String name;
|
||||
|
||||
private FeeRateSelection(String name) {
|
||||
private FeeRatesSelection(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
|
@ -269,13 +269,14 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
mempoolSizeFeeRatesChart.update(mempoolHistogram);
|
||||
}
|
||||
|
||||
FeeRateSelection feeRateSelection = Config.get().getFeeRateSelection();
|
||||
updateFeeRateSelection(feeRateSelection);
|
||||
feeSelectionToggleGroup.selectToggle(feeRateSelection == FeeRateSelection.BLOCK_TARGET ? targetBlocksToggle : mempoolSizeToggle);
|
||||
FeeRatesSelection feeRatesSelection = Config.get().getFeeRatesSelection();
|
||||
feeRatesSelection = (feeRatesSelection == null ? FeeRatesSelection.BLOCK_TARGET : feeRatesSelection);
|
||||
updateFeeRateSelection(feeRatesSelection);
|
||||
feeSelectionToggleGroup.selectToggle(feeRatesSelection == FeeRatesSelection.BLOCK_TARGET ? targetBlocksToggle : mempoolSizeToggle);
|
||||
feeSelectionToggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
|
||||
FeeRateSelection newFeeRateSelection = (FeeRateSelection)newValue.getUserData();
|
||||
Config.get().setFeeRateSelection(newFeeRateSelection);
|
||||
EventManager.get().post(new FeeRateSelectionChangedEvent(newFeeRateSelection));
|
||||
FeeRatesSelection newFeeRatesSelection = (FeeRatesSelection)newValue.getUserData();
|
||||
Config.get().setFeeRatesSelection(newFeeRatesSelection);
|
||||
EventManager.get().post(new FeeRatesSelectionChangedEvent(newFeeRatesSelection));
|
||||
});
|
||||
|
||||
fee.setTextFormatter(new CoinTextFormatter());
|
||||
|
@ -472,8 +473,8 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private void updateFeeRateSelection(FeeRateSelection feeRateSelection) {
|
||||
boolean blockTargetSelection = (feeRateSelection == FeeRateSelection.BLOCK_TARGET);
|
||||
private void updateFeeRateSelection(FeeRatesSelection feeRatesSelection) {
|
||||
boolean blockTargetSelection = (feeRatesSelection == FeeRatesSelection.BLOCK_TARGET);
|
||||
targetBlocksField.setVisible(blockTargetSelection);
|
||||
blockTargetFeeRatesChart.setVisible(blockTargetSelection);
|
||||
setDefaultFeeRate();
|
||||
|
@ -481,14 +482,15 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
}
|
||||
|
||||
private void setDefaultFeeRate() {
|
||||
int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1);
|
||||
int index = TARGET_BLOCKS_RANGE.indexOf(defaultTarget);
|
||||
Double defaultRate = getTargetBlocksFeeRates().get(defaultTarget);
|
||||
if(targetBlocksField.isVisible()) {
|
||||
int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1);
|
||||
int index = TARGET_BLOCKS_RANGE.indexOf(defaultTarget);
|
||||
targetBlocks.setValue(index);
|
||||
blockTargetFeeRatesChart.select(defaultTarget);
|
||||
setFeeRate(getTargetBlocksFeeRates().get(getTargetBlocks()));
|
||||
setFeeRate(defaultRate);
|
||||
} else {
|
||||
feeRange.setValue(5.0);
|
||||
feeRange.setValue(Math.log(defaultRate) / Math.log(2));
|
||||
setFeeRate(getFeeRangeRate());
|
||||
}
|
||||
}
|
||||
|
@ -771,7 +773,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
}
|
||||
|
||||
@Subscribe
|
||||
public void feeRateSelectionChanged(FeeRateSelectionChangedEvent event) {
|
||||
public void feeRateSelectionChanged(FeeRatesSelectionChangedEvent event) {
|
||||
updateFeeRateSelection(event.getFeeRateSelection());
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<?import com.sparrowwallet.sparrow.net.ExchangeSource?>
|
||||
<?import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch?>
|
||||
<?import com.sparrowwallet.sparrow.control.HelpLabel?>
|
||||
<?import com.sparrowwallet.sparrow.wallet.FeeRateSelection?>
|
||||
<?import com.sparrowwallet.sparrow.net.FeeRatesSource?>
|
||||
|
||||
<GridPane hgap="10.0" vgap="10.0" stylesheets="@preferences.css, @../general.css" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.preferences.GeneralPreferencesController">
|
||||
<padding>
|
||||
|
@ -42,16 +42,17 @@
|
|||
</ComboBox>
|
||||
<HelpLabel helpText="Display unit for bitcoin amounts.\nAuto displays amounts over 1 BTC in BTC, and amounts under that in satoshis."/>
|
||||
</Field>
|
||||
<Field text="Fee rate selection:">
|
||||
<ComboBox fx:id="feeRateSelection">
|
||||
<Field text="Fee rates source:">
|
||||
<ComboBox fx:id="feeRatesSource">
|
||||
<items>
|
||||
<FXCollections fx:factory="observableArrayList">
|
||||
<FeeRateSelection fx:constant="BLOCK_TARGET" />
|
||||
<FeeRateSelection fx:constant="MEMPOOL_SIZE" />
|
||||
<FeeRatesSource fx:constant="ELECTRUM_SERVER" />
|
||||
<FeeRatesSource fx:constant="MEMPOOL_SPACE" />
|
||||
<FeeRatesSource fx:constant="BITCOINFEES_EARN_COM" />
|
||||
</FXCollections>
|
||||
</items>
|
||||
</ComboBox>
|
||||
<HelpLabel helpText="Fee rate selection can be done either by estimating the number of blocks until a transaction is mined, or examining the current size of the mempool."/>
|
||||
<HelpLabel helpText="Recommended fee rates can be sourced from your connected Electrum server, or an external source."/>
|
||||
</Field>
|
||||
</Fieldset>
|
||||
<Fieldset inputGrow="SOMETIMES" text="Fiat" styleClass="wideLabelFieldSet">
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
<?import org.controlsfx.glyphfont.Glyph?>
|
||||
<?import com.sparrowwallet.sparrow.control.MempoolSizeFeeRatesChart?>
|
||||
<?import org.controlsfx.control.SegmentedButton?>
|
||||
<?import com.sparrowwallet.sparrow.wallet.FeeRateSelection?>
|
||||
<?import com.sparrowwallet.sparrow.wallet.FeeRatesSelection?>
|
||||
|
||||
<BorderPane stylesheets="@send.css, @wallet.css, @../script.css, @../general.css" styleClass="wallet-pane" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.wallet.SendController">
|
||||
<center>
|
||||
|
@ -67,7 +67,7 @@
|
|||
<Tooltip text="Determine fee via estimated number of blocks"/>
|
||||
</tooltip>
|
||||
<userData>
|
||||
<FeeRateSelection fx:constant="BLOCK_TARGET"/>
|
||||
<FeeRatesSelection fx:constant="BLOCK_TARGET"/>
|
||||
</userData>
|
||||
</ToggleButton>
|
||||
<ToggleButton fx:id="mempoolSizeToggle" text="Mempool Size" toggleGroup="$feeSelectionToggleGroup">
|
||||
|
@ -75,7 +75,7 @@
|
|||
<Tooltip text="Determine fee via current mempool size"/>
|
||||
</tooltip>
|
||||
<userData>
|
||||
<FeeRateSelection fx:constant="MEMPOOL_SIZE"/>
|
||||
<FeeRatesSelection fx:constant="MEMPOOL_SIZE"/>
|
||||
</userData>
|
||||
</ToggleButton>
|
||||
</buttons>
|
||||
|
|
Loading…
Reference in a new issue