forked from Qortal/qortal
QuickMythril
2 years ago
4 changed files with 383 additions and 0 deletions
@ -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 DigibyteSendRequest { |
||||
|
||||
@Schema(description = "Digibyte BIP32 extended private key", example = "tprv___________________________________________________________________________________________________________") |
||||
public String xprv58; |
||||
|
||||
@Schema(description = "Recipient's Digibyte address ('legacy' P2PKH only)", example = "1DigByteEaterAddressDontSendf59kuE") |
||||
public String receivingAddress; |
||||
|
||||
@Schema(description = "Amount of DGB to send", type = "number") |
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) |
||||
public long digibyteAmount; |
||||
|
||||
@Schema(description = "Transaction fee per byte (optional). Default is 0.00000100 DGB (100 sats) per byte", example = "0.00000100", type = "number") |
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) |
||||
public Long feePerByte; |
||||
|
||||
public DigibyteSendRequest() { |
||||
} |
||||
|
||||
} |
@ -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.DigibyteSendRequest; |
||||
import org.qortal.crosschain.Digibyte; |
||||
import org.qortal.crosschain.ForeignBlockchainException; |
||||
import org.qortal.crosschain.SimpleTransaction; |
||||
|
||||
@Path("/crosschain/dgb") |
||||
@Tag(name = "Cross-Chain (Digibyte)") |
||||
public class CrossChainDigibyteResource { |
||||
|
||||
@Context |
||||
HttpServletRequest request; |
||||
|
||||
@POST |
||||
@Path("/walletbalance") |
||||
@Operation( |
||||
summary = "Returns DGB 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 getDigibyteWalletBalance(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) { |
||||
Security.checkApiCallAllowed(request); |
||||
|
||||
Digibyte digibyte = Digibyte.getInstance(); |
||||
|
||||
if (!digibyte.isValidDeterministicKey(key58)) |
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY); |
||||
|
||||
try { |
||||
Long balance = digibyte.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> getDigibyteWalletTransactions(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) { |
||||
Security.checkApiCallAllowed(request); |
||||
|
||||
Digibyte digibyte = Digibyte.getInstance(); |
||||
|
||||
if (!digibyte.isValidDeterministicKey(key58)) |
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY); |
||||
|
||||
try { |
||||
return digibyte.getWalletTransactions(key58); |
||||
} catch (ForeignBlockchainException e) { |
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE); |
||||
} |
||||
} |
||||
|
||||
@POST |
||||
@Path("/send") |
||||
@Operation( |
||||
summary = "Sends DGB from hierarchical, deterministic BIP32 wallet to specific address", |
||||
description = "Currently supports 'legacy' P2PKH Digibyte addresses and Native SegWit (P2WPKH) 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 = DigibyteSendRequest.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, DigibyteSendRequest digibyteSendRequest) { |
||||
Security.checkApiCallAllowed(request); |
||||
|
||||
if (digibyteSendRequest.digibyteAmount <= 0) |
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); |
||||
|
||||
if (digibyteSendRequest.feePerByte != null && digibyteSendRequest.feePerByte <= 0) |
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); |
||||
|
||||
Digibyte digibyte = Digibyte.getInstance(); |
||||
|
||||
if (!digibyte.isValidAddress(digibyteSendRequest.receivingAddress)) |
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); |
||||
|
||||
if (!digibyte.isValidDeterministicKey(digibyteSendRequest.xprv58)) |
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY); |
||||
|
||||
Transaction spendTransaction = digibyte.buildSpend(digibyteSendRequest.xprv58, |
||||
digibyteSendRequest.receivingAddress, |
||||
digibyteSendRequest.digibyteAmount, |
||||
digibyteSendRequest.feePerByte); |
||||
|
||||
if (spendTransaction == null) |
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE); |
||||
|
||||
try { |
||||
digibyte.broadcastTransaction(spendTransaction); |
||||
} catch (ForeignBlockchainException e) { |
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE); |
||||
} |
||||
|
||||
return spendTransaction.getTxId().toString(); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,171 @@
|
||||
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 Digibyte extends Bitcoiny { |
||||
|
||||
public static final String CURRENCY_CODE = "DGB"; |
||||
|
||||
private static final Coin DEFAULT_FEE_PER_KB = Coin.valueOf(100000); // 0.001 DGB per 1000 bytes
|
||||
|
||||
private static final long MINIMUM_ORDER_AMOUNT = 1000000; // 0.01 DGB minimum order, to avoid dust errors
|
||||
|
||||
// Temporary values until a dynamic fee system is written.
|
||||
private static final long MAINNET_FEE = 10000L; |
||||
private static final long NON_MAINNET_FEE = 10000L; // 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 DigibyteNet { |
||||
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=dgb
|
||||
new Server("electrum1.cipig.net", ConnectionType.SSL, 20059), |
||||
new Server("electrum2.cipig.net", ConnectionType.SSL, 20059), |
||||
new Server("electrum3.cipig.net", ConnectionType.SSL, 20059)); |
||||
} |
||||
|
||||
@Override |
||||
public String getGenesisHash() { |
||||
return "7497ea1b465eb39f1c8f507bc877078fe016d6fcb6dfad3a64c98dcc6e1e8496"; |
||||
} |
||||
|
||||
@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 "308ea0711d5763be2995670dd9ca9872753561285a84da1d58be58acaa822252"; |
||||
} |
||||
|
||||
@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 Digibyte instance; |
||||
|
||||
private final DigibyteNet digibyteNet; |
||||
|
||||
// Constructors and instance
|
||||
|
||||
private Digibyte(DigibyteNet digibyteNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) { |
||||
super(blockchain, bitcoinjContext, currencyCode); |
||||
this.digibyteNet = digibyteNet; |
||||
|
||||
LOGGER.info(() -> String.format("Starting Digibyte support using %s", this.digibyteNet.name())); |
||||
} |
||||
|
||||
public static synchronized Digibyte getInstance() { |
||||
if (instance == null) { |
||||
DigibyteNet digibyteNet = Settings.getInstance().getDigibyteNet(); |
||||
|
||||
BitcoinyBlockchainProvider electrumX = new ElectrumX("Digibyte-" + digibyteNet.name(), digibyteNet.getGenesisHash(), digibyteNet.getServers(), DEFAULT_ELECTRUMX_PORTS); |
||||
Context bitcoinjContext = new Context(digibyteNet.getParams()); |
||||
|
||||
instance = new Digibyte(digibyteNet, 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 DGB 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.digibyteNet.getP2shFee(timestamp); |
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue