forked from Qortal/qortal
Added DOGE wallet.
This commit is contained in:
parent
d6e65a3d63
commit
50e4e71abb
@ -0,0 +1,29 @@
|
|||||||
|
package org.qortal.api.model.crosschain;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||||
|
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
public class DogecoinSendRequest {
|
||||||
|
|
||||||
|
@Schema(description = "Dogecoin BIP32 extended private key", example = "tprv___________________________________________________________________________________________________________")
|
||||||
|
public String xprv58;
|
||||||
|
|
||||||
|
@Schema(description = "Recipient's Dogecoin address ('legacy' P2PKH only)", example = "DoGecoinEaterAddressDontSendhLfzKD")
|
||||||
|
public String receivingAddress;
|
||||||
|
|
||||||
|
@Schema(description = "Amount of DOGE to send", type = "number")
|
||||||
|
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||||
|
public long dogecoinAmount;
|
||||||
|
|
||||||
|
@Schema(description = "Transaction fee per byte (optional). Default is 0.00000100 DOGE (100 sats) per byte", example = "0.00000100", type = "number")
|
||||||
|
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||||
|
public Long feePerByte;
|
||||||
|
|
||||||
|
public DogecoinSendRequest() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,165 @@
|
|||||||
|
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.tags.Tag;
|
||||||
|
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.DogecoinSendRequest;
|
||||||
|
import org.qortal.crosschain.ForeignBlockchainException;
|
||||||
|
import org.qortal.crosschain.Dogecoin;
|
||||||
|
import org.qortal.crosschain.SimpleTransaction;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.ws.rs.POST;
|
||||||
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.core.Context;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Path("/crosschain/doge")
|
||||||
|
@Tag(name = "Cross-Chain (Dogecoin)")
|
||||||
|
public class CrossChainDogecoinResource {
|
||||||
|
|
||||||
|
@Context
|
||||||
|
HttpServletRequest request;
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/walletbalance")
|
||||||
|
@Operation(
|
||||||
|
summary = "Returns DOGE 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})
|
||||||
|
public String getDogecoinWalletBalance(String key58) {
|
||||||
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
|
Dogecoin dogecoin = Dogecoin.getInstance();
|
||||||
|
|
||||||
|
if (!dogecoin.isValidDeterministicKey(key58))
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||||
|
|
||||||
|
Long balance = dogecoin.getWalletBalance(key58);
|
||||||
|
if (balance == null)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||||
|
|
||||||
|
return balance.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@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})
|
||||||
|
public List<SimpleTransaction> getDogecoinWalletTransactions(String key58) {
|
||||||
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
|
Dogecoin dogecoin = Dogecoin.getInstance();
|
||||||
|
|
||||||
|
if (!dogecoin.isValidDeterministicKey(key58))
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return dogecoin.getWalletTransactions(key58);
|
||||||
|
} catch (ForeignBlockchainException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/send")
|
||||||
|
@Operation(
|
||||||
|
summary = "Sends DOGE from hierarchical, deterministic BIP32 wallet to specific address",
|
||||||
|
description = "Currently only supports 'legacy' P2PKH Dogecoin 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 = DogecoinSendRequest.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})
|
||||||
|
public String sendBitcoin(DogecoinSendRequest dogecoinSendRequest) {
|
||||||
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
|
if (dogecoinSendRequest.dogecoinAmount <= 0)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
|
if (dogecoinSendRequest.feePerByte != null && dogecoinSendRequest.feePerByte <= 0)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
|
Dogecoin dogecoin = Dogecoin.getInstance();
|
||||||
|
|
||||||
|
if (!dogecoin.isValidAddress(dogecoinSendRequest.receivingAddress))
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||||
|
|
||||||
|
if (!dogecoin.isValidDeterministicKey(dogecoinSendRequest.xprv58))
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||||
|
|
||||||
|
Transaction spendTransaction = dogecoin.buildSpend(dogecoinSendRequest.xprv58,
|
||||||
|
dogecoinSendRequest.receivingAddress,
|
||||||
|
dogecoinSendRequest.dogecoinAmount,
|
||||||
|
dogecoinSendRequest.feePerByte);
|
||||||
|
|
||||||
|
if (spendTransaction == null)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE);
|
||||||
|
|
||||||
|
try {
|
||||||
|
dogecoin.broadcastTransaction(spendTransaction);
|
||||||
|
} catch (ForeignBlockchainException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return spendTransaction.getTxId().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
165
src/main/java/org/qortal/crosschain/Dogecoin.java
Normal file
165
src/main/java/org/qortal/crosschain/Dogecoin.java
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
package org.qortal.crosschain;
|
||||||
|
|
||||||
|
import org.bitcoinj.core.Coin;
|
||||||
|
import org.bitcoinj.core.Context;
|
||||||
|
import org.bitcoinj.core.NetworkParameters;
|
||||||
|
import org.libdohj.params.DogecoinMainNetParams;
|
||||||
|
//import org.libdohj.params.DogecoinRegTestParams;
|
||||||
|
import org.libdohj.params.DogecoinTestNet3Params;
|
||||||
|
import org.qortal.crosschain.ElectrumX.Server;
|
||||||
|
import org.qortal.crosschain.ElectrumX.Server.ConnectionType;
|
||||||
|
import org.qortal.settings.Settings;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class Dogecoin extends Bitcoiny {
|
||||||
|
|
||||||
|
public static final String CURRENCY_CODE = "DOGE";
|
||||||
|
|
||||||
|
private static final Coin DEFAULT_FEE_PER_KB = Coin.valueOf(10000); // 0.0001 LTC per 1000 bytes
|
||||||
|
|
||||||
|
// Temporary values until a dynamic fee system is written.
|
||||||
|
private static final long MAINNET_FEE = 1000L;
|
||||||
|
private static final long NON_MAINNET_FEE = 1000L; // 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 DogecoinNet {
|
||||||
|
MAIN {
|
||||||
|
@Override
|
||||||
|
public NetworkParameters getParams() {
|
||||||
|
return DogecoinMainNetParams.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<Server> getServers() {
|
||||||
|
return Arrays.asList(
|
||||||
|
// Servers chosen on NO BASIS WHATSOEVER from various sources!
|
||||||
|
new Server("electrum1.cipig.net", ConnectionType.TCP, 10060),
|
||||||
|
new Server("electrum2.cipig.net", ConnectionType.TCP, 10060),
|
||||||
|
new Server("electrum3.cipig.net", ConnectionType.TCP, 10060));
|
||||||
|
// TODO: add more mainnet servers. It's too centralized.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getGenesisHash() {
|
||||||
|
return "1a91e3dace36e2be3bf030a65679fe821aa1d6ef92e7c9902eb318182c355691";
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 DogecoinTestNet3Params.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<Server> getServers() {
|
||||||
|
return Arrays.asList(); // TODO: find testnet servers
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getGenesisHash() {
|
||||||
|
return "4966625a4b2851d9fdee139e56211a0d88575f59ed816ff5e6a63deb4e3e29a0";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getP2shFee(Long timestamp) {
|
||||||
|
return NON_MAINNET_FEE;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
REGTEST {
|
||||||
|
@Override
|
||||||
|
public NetworkParameters getParams() {
|
||||||
|
return null; // TODO: DogecoinRegTestParams.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 Dogecoin instance;
|
||||||
|
|
||||||
|
private final DogecoinNet dogecoinNet;
|
||||||
|
|
||||||
|
// Constructors and instance
|
||||||
|
|
||||||
|
private Dogecoin(DogecoinNet dogecoinNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
|
||||||
|
super(blockchain, bitcoinjContext, currencyCode);
|
||||||
|
this.dogecoinNet = dogecoinNet;
|
||||||
|
|
||||||
|
LOGGER.info(() -> String.format("Starting Dogecoin support using %s", this.dogecoinNet.name()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized Dogecoin getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
DogecoinNet dogecoinNet = Settings.getInstance().getDogecoinNet();
|
||||||
|
|
||||||
|
BitcoinyBlockchainProvider electrumX = new ElectrumX("Dogecoin-" + dogecoinNet.name(), dogecoinNet.getGenesisHash(), dogecoinNet.getServers(), DEFAULT_ELECTRUMX_PORTS);
|
||||||
|
Context bitcoinjContext = new Context(dogecoinNet.getParams());
|
||||||
|
|
||||||
|
instance = new Dogecoin(dogecoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters & setters
|
||||||
|
|
||||||
|
public static synchronized void resetForTesting() {
|
||||||
|
instance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actual useful methods for use by other classes
|
||||||
|
|
||||||
|
/** Default Dogecoin fee is lower than Bitcoin: only 10sats/byte. */
|
||||||
|
@Override
|
||||||
|
public Coin getFeePerKb() {
|
||||||
|
return DEFAULT_FEE_PER_KB;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns estimated LTC 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.dogecoinNet.getP2shFee(timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -518,6 +518,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Failed to perform RPC - maybe lack of servers?
|
// Failed to perform RPC - maybe lack of servers?
|
||||||
|
LOGGER.info("Error: No connected Electrum servers when trying to make RPC call");
|
||||||
throw new ForeignBlockchainException.NetworkException(String.format("Failed to perform ElectrumX RPC %s", method));
|
throw new ForeignBlockchainException.NetworkException(String.format("Failed to perform ElectrumX RPC %s", method));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ import org.eclipse.persistence.jaxb.UnmarshallerProperties;
|
|||||||
import org.qortal.block.BlockChain;
|
import org.qortal.block.BlockChain;
|
||||||
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;
|
||||||
|
|
||||||
// All properties to be converted to JSON via JAXB
|
// All properties to be converted to JSON via JAXB
|
||||||
@XmlAccessorType(XmlAccessType.FIELD)
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
@ -141,6 +142,7 @@ public class Settings {
|
|||||||
private String blockchainConfig = null; // use default from resources
|
private String blockchainConfig = null; // use default from resources
|
||||||
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;
|
||||||
// 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;
|
||||||
@ -444,6 +446,10 @@ public class Settings {
|
|||||||
return this.litecoinNet;
|
return this.litecoinNet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DogecoinNet getDogecoinNet() {
|
||||||
|
return this.dogecoinNet;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isTradebotSystrayEnabled() {
|
public boolean isTradebotSystrayEnabled() {
|
||||||
return this.tradebotSystrayEnabled;
|
return this.tradebotSystrayEnabled;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user