mirror of https://github.com/qortal/qortal
QuickMythril
2 years ago
4 changed files with 387 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 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(); |
||||
} |
||||
|
||||
} |
@ -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); |
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue