diff --git a/src/main/java/org/qortal/api/model/BitcoinSendRequest.java b/src/main/java/org/qortal/api/model/BitcoinSendRequest.java new file mode 100644 index 00000000..f169fe33 --- /dev/null +++ b/src/main/java/org/qortal/api/model/BitcoinSendRequest.java @@ -0,0 +1,25 @@ +package org.qortal.api.model; + +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 BitcoinSendRequest { + + @Schema(description = "Bitcoin BIP32 extended private key", example = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TbTVGajEB55L1HYLg2aQMecZLXLre5YJcawpdFG66STVAWPJ") + public String xprv58; + + @Schema(description = "Recipient's Bitcoin address ('legacy' P2PKH only)", example = "1BitcoinEaterAddressDontSendf59kuE") + public String receivingAddress; + + @Schema(description = "Amount of BTC to send") + @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) + public long bitcoinAmount; + + public BitcoinSendRequest() { + } + +} diff --git a/src/main/java/org/qortal/api/model/TradeBotCreateRequest.java b/src/main/java/org/qortal/api/model/TradeBotCreateRequest.java index adc319e3..622262b0 100644 --- a/src/main/java/org/qortal/api/model/TradeBotCreateRequest.java +++ b/src/main/java/org/qortal/api/model/TradeBotCreateRequest.java @@ -27,7 +27,7 @@ public class TradeBotCreateRequest { @Schema(description = "Suggested trade timeout (minutes)", example = "10080") public int tradeTimeout; - @Schema(description = "Bitcoin address for receiving", example = "1NCTG9oLk41bU6pcehLNo9DVJup77EHAVx") + @Schema(description = "Bitcoin address for receiving", example = "1BitcoinEaterAddressDontSendf59kuE") public String receivingAddress; public TradeBotCreateRequest() { diff --git a/src/main/java/org/qortal/api/resource/CrossChainResource.java b/src/main/java/org/qortal/api/resource/CrossChainResource.java index 6901af06..3f02631a 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainResource.java @@ -46,6 +46,7 @@ import org.qortal.api.model.CrossChainTradeRequest; import org.qortal.api.model.CrossChainTradeSummary; import org.qortal.api.model.TradeBotCreateRequest; import org.qortal.api.model.TradeBotRespondRequest; +import org.qortal.api.model.BitcoinSendRequest; import org.qortal.api.model.CrossChainBitcoinP2SHStatus; import org.qortal.api.model.CrossChainBitcoinRedeemRequest; import org.qortal.api.model.CrossChainBitcoinRefundRequest; @@ -947,6 +948,57 @@ public class CrossChainResource { return balance.toString(); } + @POST + @Path("/btc/send") + @Operation( + summary = "Sends BTC from BIP32 wallet to specific address", + description = "Currently only supports 'legacy' P2PKH Bitcoin 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 = BitcoinSendRequest.class + ) + ) + ), + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string")) + ) + } + ) + @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE}) + public String sendBitcoin(BitcoinSendRequest bitcoinSendRequest) { + Security.checkApiCallAllowed(request); + + if (bitcoinSendRequest.bitcoinAmount <= 0) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + + Address receivingAddress; + try { + receivingAddress = Address.fromString(BTC.getInstance().getNetworkParameters(), bitcoinSendRequest.receivingAddress); + } catch (AddressFormatException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + } + + // We only support P2PKH addresses at this time + if (receivingAddress.getOutputScriptType() != ScriptType.P2PKH) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + + if (!BTC.getInstance().isValidXprv(bitcoinSendRequest.xprv58)) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY); + + org.bitcoinj.core.Transaction spendTransaction = BTC.getInstance().buildSpend(bitcoinSendRequest.xprv58, bitcoinSendRequest.receivingAddress, bitcoinSendRequest.bitcoinAmount); + if (spendTransaction == null) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_BALANCE_ISSUE); + + if (!BTC.getInstance().broadcastTransaction(spendTransaction)) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE); + + return "true"; + } + @GET @Path("/tradebot") @Operation(