forked from Qortal/qortal
add RVN wallet
This commit is contained in:
parent
311f41c610
commit
390b359761
@ -0,0 +1,29 @@
|
|||||||
|
package org.qortal.api.model.crosschain;
|
||||||
|
|
||||||
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
public class RavencoinSendRequest {
|
||||||
|
|
||||||
|
@Schema(description = "Ravencoin BIP32 extended private key", example = "tprv___________________________________________________________________________________________________________")
|
||||||
|
public String xprv58;
|
||||||
|
|
||||||
|
@Schema(description = "Recipient's Ravencoin address ('legacy' P2PKH only)", example = "1RvnCoinEaterAddressDontSendf59kuE")
|
||||||
|
public String receivingAddress;
|
||||||
|
|
||||||
|
@Schema(description = "Amount of RVN to send", type = "number")
|
||||||
|
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||||
|
public long ravencoinAmount;
|
||||||
|
|
||||||
|
@Schema(description = "Transaction fee per byte (optional). Default is 0.00000100 RVN (100 sats) per byte", example = "0.00000100", type = "number")
|
||||||
|
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||||
|
public Long feePerByte;
|
||||||
|
|
||||||
|
public RavencoinSendRequest() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,177 @@
|
|||||||
|
package org.qortal.api.resource;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.ws.rs.HeaderParam;
|
||||||
|
import javax.ws.rs.POST;
|
||||||
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.core.Context;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
|
||||||
|
import org.bitcoinj.core.Transaction;
|
||||||
|
import org.qortal.api.ApiError;
|
||||||
|
import org.qortal.api.ApiErrors;
|
||||||
|
import org.qortal.api.ApiExceptionFactory;
|
||||||
|
import org.qortal.api.Security;
|
||||||
|
import org.qortal.api.model.crosschain.RavencoinSendRequest;
|
||||||
|
import org.qortal.crosschain.Ravencoin;
|
||||||
|
import org.qortal.crosschain.ForeignBlockchainException;
|
||||||
|
import org.qortal.crosschain.SimpleTransaction;
|
||||||
|
|
||||||
|
@Path("/crosschain/rvn")
|
||||||
|
@Tag(name = "Cross-Chain (Ravencoin)")
|
||||||
|
public class CrossChainRavencoinResource {
|
||||||
|
|
||||||
|
@Context
|
||||||
|
HttpServletRequest request;
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/walletbalance")
|
||||||
|
@Operation(
|
||||||
|
summary = "Returns RVN balance for hierarchical, deterministic BIP32 wallet",
|
||||||
|
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
|
||||||
|
requestBody = @RequestBody(
|
||||||
|
required = true,
|
||||||
|
content = @Content(
|
||||||
|
mediaType = MediaType.TEXT_PLAIN,
|
||||||
|
schema = @Schema(
|
||||||
|
type = "string",
|
||||||
|
description = "BIP32 'm' private/public key in base58",
|
||||||
|
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", description = "balance (satoshis)"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
|
||||||
|
@SecurityRequirement(name = "apiKey")
|
||||||
|
public String getRavencoinWalletBalance(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
|
||||||
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
|
Ravencoin ravencoin = Ravencoin.getInstance();
|
||||||
|
|
||||||
|
if (!ravencoin.isValidDeterministicKey(key58))
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Long balance = ravencoin.getWalletBalanceFromTransactions(key58);
|
||||||
|
if (balance == null)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||||
|
|
||||||
|
return balance.toString();
|
||||||
|
|
||||||
|
} catch (ForeignBlockchainException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/wallettransactions")
|
||||||
|
@Operation(
|
||||||
|
summary = "Returns transactions for hierarchical, deterministic BIP32 wallet",
|
||||||
|
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
|
||||||
|
requestBody = @RequestBody(
|
||||||
|
required = true,
|
||||||
|
content = @Content(
|
||||||
|
mediaType = MediaType.TEXT_PLAIN,
|
||||||
|
schema = @Schema(
|
||||||
|
type = "string",
|
||||||
|
description = "BIP32 'm' private/public key in base58",
|
||||||
|
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
|
||||||
|
@SecurityRequirement(name = "apiKey")
|
||||||
|
public List<SimpleTransaction> getRavencoinWalletTransactions(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
|
||||||
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
|
Ravencoin ravencoin = Ravencoin.getInstance();
|
||||||
|
|
||||||
|
if (!ravencoin.isValidDeterministicKey(key58))
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return ravencoin.getWalletTransactions(key58);
|
||||||
|
} catch (ForeignBlockchainException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/send")
|
||||||
|
@Operation(
|
||||||
|
summary = "Sends RVN from hierarchical, deterministic BIP32 wallet to specific address",
|
||||||
|
description = "Currently only supports 'legacy' P2PKH Ravencoin addresses. Supply BIP32 'm' private key in base58, starting with 'xprv' for mainnet, 'tprv' for testnet",
|
||||||
|
requestBody = @RequestBody(
|
||||||
|
required = true,
|
||||||
|
content = @Content(
|
||||||
|
mediaType = MediaType.APPLICATION_JSON,
|
||||||
|
schema = @Schema(
|
||||||
|
implementation = RavencoinSendRequest.class
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", description = "transaction hash"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
|
||||||
|
@SecurityRequirement(name = "apiKey")
|
||||||
|
public String sendBitcoin(@HeaderParam(Security.API_KEY_HEADER) String apiKey, RavencoinSendRequest ravencoinSendRequest) {
|
||||||
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
|
if (ravencoinSendRequest.ravencoinAmount <= 0)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
|
if (ravencoinSendRequest.feePerByte != null && ravencoinSendRequest.feePerByte <= 0)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
|
Ravencoin ravencoin = Ravencoin.getInstance();
|
||||||
|
|
||||||
|
if (!ravencoin.isValidAddress(ravencoinSendRequest.receivingAddress))
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||||
|
|
||||||
|
if (!ravencoin.isValidDeterministicKey(ravencoinSendRequest.xprv58))
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||||
|
|
||||||
|
Transaction spendTransaction = ravencoin.buildSpend(ravencoinSendRequest.xprv58,
|
||||||
|
ravencoinSendRequest.receivingAddress,
|
||||||
|
ravencoinSendRequest.ravencoinAmount,
|
||||||
|
ravencoinSendRequest.feePerByte);
|
||||||
|
|
||||||
|
if (spendTransaction == null)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE);
|
||||||
|
|
||||||
|
try {
|
||||||
|
ravencoin.broadcastTransaction(spendTransaction);
|
||||||
|
} catch (ForeignBlockchainException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return spendTransaction.getTxId().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
175
src/main/java/org/qortal/crosschain/Ravencoin.java
Normal file
175
src/main/java/org/qortal/crosschain/Ravencoin.java
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
package org.qortal.crosschain;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.bitcoinj.core.Coin;
|
||||||
|
import org.bitcoinj.core.Context;
|
||||||
|
import org.bitcoinj.core.NetworkParameters;
|
||||||
|
import org.bitcoinj.params.MainNetParams;
|
||||||
|
import org.bitcoinj.params.RegTestParams;
|
||||||
|
import org.bitcoinj.params.TestNet3Params;
|
||||||
|
import org.qortal.crosschain.ElectrumX.Server;
|
||||||
|
import org.qortal.crosschain.ElectrumX.Server.ConnectionType;
|
||||||
|
import org.qortal.settings.Settings;
|
||||||
|
|
||||||
|
public class Ravencoin extends Bitcoiny {
|
||||||
|
|
||||||
|
public static final String CURRENCY_CODE = "RVN";
|
||||||
|
|
||||||
|
private static final Coin DEFAULT_FEE_PER_KB = Coin.valueOf(1125000); // 0.01125 RVN per 1000 bytes
|
||||||
|
|
||||||
|
private static final long MINIMUM_ORDER_AMOUNT = 1000000; // 0.01 RVN minimum order, to avoid dust errors
|
||||||
|
|
||||||
|
// Temporary values until a dynamic fee system is written.
|
||||||
|
private static final long MAINNET_FEE = 1000000L;
|
||||||
|
private static final long NON_MAINNET_FEE = 1000000L; // enough for TESTNET3 and should be OK for REGTEST
|
||||||
|
|
||||||
|
private static final Map<ConnectionType, Integer> DEFAULT_ELECTRUMX_PORTS = new EnumMap<>(ConnectionType.class);
|
||||||
|
static {
|
||||||
|
DEFAULT_ELECTRUMX_PORTS.put(ConnectionType.TCP, 50001);
|
||||||
|
DEFAULT_ELECTRUMX_PORTS.put(ConnectionType.SSL, 50002);
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum RavencoinNet {
|
||||||
|
MAIN {
|
||||||
|
@Override
|
||||||
|
public NetworkParameters getParams() {
|
||||||
|
return MainNetParams.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<Server> getServers() {
|
||||||
|
return Arrays.asList(
|
||||||
|
// Servers chosen on NO BASIS WHATSOEVER from various sources!
|
||||||
|
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=rvn
|
||||||
|
//new Server("aethyn.com", ConnectionType.SSL, 50002),
|
||||||
|
//new Server("electrum2.rvn.rocks", ConnectionType.SSL, 50002),
|
||||||
|
//new Server("rvn-dashboard.com", ConnectionType.SSL, 50002),
|
||||||
|
//new Server("rvn4lyfe.com", ConnectionType.SSL, 50002),
|
||||||
|
new Server("electrum1.cipig.net", ConnectionType.SSL, 20051),
|
||||||
|
new Server("electrum2.cipig.net", ConnectionType.SSL, 20051),
|
||||||
|
new Server("electrum3.cipig.net", ConnectionType.SSL, 20051));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getGenesisHash() {
|
||||||
|
return "0000006b444bc2f2ffe627be9d9e7e7a0730000870ef6eb6da46c8eae389df90";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getP2shFee(Long timestamp) {
|
||||||
|
// TODO: This will need to be replaced with something better in the near future!
|
||||||
|
return MAINNET_FEE;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
TEST3 {
|
||||||
|
@Override
|
||||||
|
public NetworkParameters getParams() {
|
||||||
|
return TestNet3Params.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<Server> getServers() {
|
||||||
|
return Arrays.asList(); // TODO: find testnet servers
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getGenesisHash() {
|
||||||
|
return "000000ecfc5e6324a079542221d00e10362bdc894d56500c414060eea8a3ad5a";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getP2shFee(Long timestamp) {
|
||||||
|
return NON_MAINNET_FEE;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
REGTEST {
|
||||||
|
@Override
|
||||||
|
public NetworkParameters getParams() {
|
||||||
|
return RegTestParams.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<Server> getServers() {
|
||||||
|
return Arrays.asList(
|
||||||
|
new Server("localhost", ConnectionType.TCP, 50001),
|
||||||
|
new Server("localhost", ConnectionType.SSL, 50002));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getGenesisHash() {
|
||||||
|
// This is unique to each regtest instance
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getP2shFee(Long timestamp) {
|
||||||
|
return NON_MAINNET_FEE;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public abstract NetworkParameters getParams();
|
||||||
|
public abstract Collection<Server> getServers();
|
||||||
|
public abstract String getGenesisHash();
|
||||||
|
public abstract long getP2shFee(Long timestamp) throws ForeignBlockchainException;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Ravencoin instance;
|
||||||
|
|
||||||
|
private final RavencoinNet ravencoinNet;
|
||||||
|
|
||||||
|
// Constructors and instance
|
||||||
|
|
||||||
|
private Ravencoin(RavencoinNet ravencoinNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
|
||||||
|
super(blockchain, bitcoinjContext, currencyCode);
|
||||||
|
this.ravencoinNet = ravencoinNet;
|
||||||
|
|
||||||
|
LOGGER.info(() -> String.format("Starting Ravencoin support using %s", this.ravencoinNet.name()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized Ravencoin getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
RavencoinNet ravencoinNet = Settings.getInstance().getRavencoinNet();
|
||||||
|
|
||||||
|
BitcoinyBlockchainProvider electrumX = new ElectrumX("Ravencoin-" + ravencoinNet.name(), ravencoinNet.getGenesisHash(), ravencoinNet.getServers(), DEFAULT_ELECTRUMX_PORTS);
|
||||||
|
Context bitcoinjContext = new Context(ravencoinNet.getParams());
|
||||||
|
|
||||||
|
instance = new Ravencoin(ravencoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters & setters
|
||||||
|
|
||||||
|
public static synchronized void resetForTesting() {
|
||||||
|
instance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actual useful methods for use by other classes
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Coin getFeePerKb() {
|
||||||
|
return DEFAULT_FEE_PER_KB;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getMinimumOrderAmount() {
|
||||||
|
return MINIMUM_ORDER_AMOUNT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns estimated RVN fee, in sats per 1000bytes, optionally for historic timestamp.
|
||||||
|
*
|
||||||
|
* @param timestamp optional milliseconds since epoch, or null for 'now'
|
||||||
|
* @return sats per 1000bytes, or throws ForeignBlockchainException if something went wrong
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public long getP2shFee(Long timestamp) throws ForeignBlockchainException {
|
||||||
|
return this.ravencoinNet.getP2shFee(timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -26,6 +26,7 @@ import org.qortal.controller.arbitrary.ArbitraryDataStorageManager.*;
|
|||||||
import org.qortal.crosschain.Bitcoin.BitcoinNet;
|
import org.qortal.crosschain.Bitcoin.BitcoinNet;
|
||||||
import org.qortal.crosschain.Litecoin.LitecoinNet;
|
import org.qortal.crosschain.Litecoin.LitecoinNet;
|
||||||
import org.qortal.crosschain.Dogecoin.DogecoinNet;
|
import org.qortal.crosschain.Dogecoin.DogecoinNet;
|
||||||
|
import org.qortal.crosschain.Ravencoin.RavencoinNet;
|
||||||
import org.qortal.utils.EnumUtils;
|
import org.qortal.utils.EnumUtils;
|
||||||
|
|
||||||
// All properties to be converted to JSON via JAXB
|
// All properties to be converted to JSON via JAXB
|
||||||
@ -222,6 +223,7 @@ public class Settings {
|
|||||||
private BitcoinNet bitcoinNet = BitcoinNet.MAIN;
|
private BitcoinNet bitcoinNet = BitcoinNet.MAIN;
|
||||||
private LitecoinNet litecoinNet = LitecoinNet.MAIN;
|
private LitecoinNet litecoinNet = LitecoinNet.MAIN;
|
||||||
private DogecoinNet dogecoinNet = DogecoinNet.MAIN;
|
private DogecoinNet dogecoinNet = DogecoinNet.MAIN;
|
||||||
|
private RavencoinNet ravencoinNet = RavencoinNet.MAIN;
|
||||||
// Also crosschain-related:
|
// Also crosschain-related:
|
||||||
/** Whether to show SysTray pop-up notifications when trade-bot entries change state */
|
/** Whether to show SysTray pop-up notifications when trade-bot entries change state */
|
||||||
private boolean tradebotSystrayEnabled = false;
|
private boolean tradebotSystrayEnabled = false;
|
||||||
@ -680,6 +682,10 @@ public class Settings {
|
|||||||
return this.dogecoinNet;
|
return this.dogecoinNet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RavencoinNet getRavencoinNet() {
|
||||||
|
return this.ravencoinNet;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isTradebotSystrayEnabled() {
|
public boolean isTradebotSystrayEnabled() {
|
||||||
return this.tradebotSystrayEnabled;
|
return this.tradebotSystrayEnabled;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user