mirror of
https://github.com/bisq-network/bisq.git
synced 2024-11-19 09:52:23 +01:00
Merge pull request #6632 from HenrikJannsen/improve_validations
Add more validations
This commit is contained in:
commit
f49be9ab3c
@ -19,4 +19,6 @@ package bisq.core.trade.txproof;
|
|||||||
|
|
||||||
public interface AssetTxProofParser<R extends AssetTxProofRequest.Result, T extends AssetTxProofModel> {
|
public interface AssetTxProofParser<R extends AssetTxProofRequest.Result, T extends AssetTxProofModel> {
|
||||||
R parse(T model, String jsonTxt);
|
R parse(T model, String jsonTxt);
|
||||||
|
|
||||||
|
R parse(String jsonTxt);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of Bisq.
|
||||||
|
*
|
||||||
|
* Bisq is free software: you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or (at
|
||||||
|
* your option) any later version.
|
||||||
|
*
|
||||||
|
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||||
|
* License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.trade.txproof.xmr;
|
||||||
|
|
||||||
|
import bisq.core.trade.txproof.AssetTxProofParser;
|
||||||
|
|
||||||
|
import bisq.common.app.DevEnv;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParseException;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class XmrRawTxParser implements AssetTxProofParser<XmrTxProofRequest.Result, XmrTxProofModel> {
|
||||||
|
XmrRawTxParser() {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// API
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public XmrTxProofRequest.Result parse(XmrTxProofModel model, String jsonTxt) {
|
||||||
|
return parse(jsonTxt);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("SpellCheckingInspection")
|
||||||
|
@Override
|
||||||
|
public XmrTxProofRequest.Result parse(String jsonTxt) {
|
||||||
|
try {
|
||||||
|
JsonObject json = new Gson().fromJson(jsonTxt, JsonObject.class);
|
||||||
|
if (json == null) {
|
||||||
|
return XmrTxProofRequest.Result.ERROR.with(XmrTxProofRequest.Detail.API_INVALID.error("Empty json"));
|
||||||
|
}
|
||||||
|
// there should always be "data" and "status" at the top level
|
||||||
|
if (json.get("data") == null || !json.get("data").isJsonObject() || json.get("status") == null) {
|
||||||
|
return XmrTxProofRequest.Result.ERROR.with(XmrTxProofRequest.Detail.API_INVALID.error("Missing data / status fields"));
|
||||||
|
}
|
||||||
|
JsonObject jsonData = json.get("data").getAsJsonObject();
|
||||||
|
String jsonStatus = json.get("status").getAsString();
|
||||||
|
if (jsonStatus.matches("fail")) {
|
||||||
|
// The API returns "fail" until the transaction has successfully reached the mempool or if request
|
||||||
|
// contained invalid data.
|
||||||
|
// We return TX_NOT_FOUND which will cause a retry later
|
||||||
|
return XmrTxProofRequest.Result.PENDING.with(XmrTxProofRequest.Detail.TX_NOT_FOUND);
|
||||||
|
} else if (!jsonStatus.matches("success")) {
|
||||||
|
return XmrTxProofRequest.Result.ERROR.with(XmrTxProofRequest.Detail.API_INVALID.error("Unhandled status value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonElement jsonUnlockTime = jsonData.get("unlock_time");
|
||||||
|
if (jsonUnlockTime == null) {
|
||||||
|
return XmrTxProofRequest.Result.ERROR.with(XmrTxProofRequest.Detail.API_INVALID.error("Missing unlock_time field"));
|
||||||
|
} else {
|
||||||
|
long unlockTime = jsonUnlockTime.getAsLong();
|
||||||
|
if (unlockTime != 0 && !DevEnv.isDevMode()) {
|
||||||
|
log.warn("Invalid unlock_time {}", unlockTime);
|
||||||
|
return XmrTxProofRequest.Result.FAILED.with(XmrTxProofRequest.Detail.INVALID_UNLOCK_TIME.error("Invalid unlock_time"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return XmrTxProofRequest.Result.SUCCESS.with(XmrTxProofRequest.Detail.SUCCESS);
|
||||||
|
} catch (JsonParseException | NullPointerException e) {
|
||||||
|
return XmrTxProofRequest.Result.ERROR.with(XmrTxProofRequest.Detail.API_INVALID.error(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -45,6 +45,11 @@ public class XmrTxProofParser implements AssetTxProofParser<XmrTxProofRequest.Re
|
|||||||
// API
|
// API
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public XmrTxProofRequest.Result parse(String jsonTxt) {
|
||||||
|
throw new UnsupportedOperationException("This method is not supported for this parser");
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("SpellCheckingInspection")
|
@SuppressWarnings("SpellCheckingInspection")
|
||||||
@Override
|
@Override
|
||||||
public XmrTxProofRequest.Result parse(XmrTxProofModel model, String jsonTxt) {
|
public XmrTxProofRequest.Result parse(XmrTxProofModel model, String jsonTxt) {
|
||||||
|
@ -37,6 +37,8 @@ import com.google.common.util.concurrent.ListenableFuture;
|
|||||||
import com.google.common.util.concurrent.ListeningExecutorService;
|
import com.google.common.util.concurrent.ListeningExecutorService;
|
||||||
import com.google.common.util.concurrent.MoreExecutors;
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
@ -101,6 +103,7 @@ class XmrTxProofRequest implements AssetTxProofRequest<XmrTxProofRequest.Result>
|
|||||||
NO_MATCH_FOUND,
|
NO_MATCH_FOUND,
|
||||||
AMOUNT_NOT_MATCHING,
|
AMOUNT_NOT_MATCHING,
|
||||||
TRADE_DATE_NOT_MATCHING,
|
TRADE_DATE_NOT_MATCHING,
|
||||||
|
INVALID_UNLOCK_TIME,
|
||||||
NO_RESULTS_TIMEOUT;
|
NO_RESULTS_TIMEOUT;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@ -143,7 +146,8 @@ class XmrTxProofRequest implements AssetTxProofRequest<XmrTxProofRequest.Result>
|
|||||||
private final ListeningExecutorService executorService = Utilities.getListeningExecutorService(
|
private final ListeningExecutorService executorService = Utilities.getListeningExecutorService(
|
||||||
"XmrTransferProofRequester", 3, 5, 10 * 60);
|
"XmrTransferProofRequester", 3, 5, 10 * 60);
|
||||||
|
|
||||||
private final AssetTxProofParser<XmrTxProofRequest.Result, XmrTxProofModel> parser;
|
private final AssetTxProofParser<XmrTxProofRequest.Result, XmrTxProofModel> txProofParser;
|
||||||
|
private final AssetTxProofParser<XmrTxProofRequest.Result, XmrTxProofModel> rawTxParser;
|
||||||
private final XmrTxProofModel model;
|
private final XmrTxProofModel model;
|
||||||
private final AssetTxProofHttpClient httpClient;
|
private final AssetTxProofHttpClient httpClient;
|
||||||
private final long firstRequest;
|
private final long firstRequest;
|
||||||
@ -160,7 +164,8 @@ class XmrTxProofRequest implements AssetTxProofRequest<XmrTxProofRequest.Result>
|
|||||||
|
|
||||||
XmrTxProofRequest(Socks5ProxyProvider socks5ProxyProvider,
|
XmrTxProofRequest(Socks5ProxyProvider socks5ProxyProvider,
|
||||||
XmrTxProofModel model) {
|
XmrTxProofModel model) {
|
||||||
this.parser = new XmrTxProofParser();
|
txProofParser = new XmrTxProofParser();
|
||||||
|
rawTxParser = new XmrRawTxParser();
|
||||||
this.model = model;
|
this.model = model;
|
||||||
|
|
||||||
httpClient = new XmrTxProofHttpClient(socks5ProxyProvider);
|
httpClient = new XmrTxProofHttpClient(socks5ProxyProvider);
|
||||||
@ -206,24 +211,20 @@ class XmrTxProofRequest implements AssetTxProofRequest<XmrTxProofRequest.Result>
|
|||||||
|
|
||||||
ListenableFuture<Result> future = executorService.submit(() -> {
|
ListenableFuture<Result> future = executorService.submit(() -> {
|
||||||
Thread.currentThread().setName("XmrTransferProofRequest-" + this.getShortId());
|
Thread.currentThread().setName("XmrTransferProofRequest-" + this.getShortId());
|
||||||
// The API use the viewkey param for txKey if txprove is true
|
|
||||||
// https://github.com/moneroexamples/onion-monero-blockchain-explorer/blob/9a37839f37abef0b8b94ceeba41ab51a41f3fbd8/src/page.h#L5254
|
Result result = getResultFromRawTxRequest();
|
||||||
String param = "/api/outputs?txhash=" + model.getTxHash() +
|
if (result != Result.SUCCESS) {
|
||||||
"&address=" + model.getRecipientAddress() +
|
return result;
|
||||||
"&viewkey=" + model.getTxKey() +
|
|
||||||
"&txprove=1";
|
|
||||||
log.info("Param {} for {}", param, this);
|
|
||||||
String json = httpClient.get(param, "User-Agent", "bisq/" + Version.VERSION);
|
|
||||||
try {
|
|
||||||
String prettyJson = new GsonBuilder().setPrettyPrinting().create().toJson(new JsonParser().parse(json));
|
|
||||||
log.info("Response json from {}\n{}", this, prettyJson);
|
|
||||||
} catch (Throwable error) {
|
|
||||||
log.error("Pretty print caused a {}: raw json={}", error, json);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Result result = parser.parse(model, json);
|
if (terminated) {
|
||||||
log.info("Result from {}\n{}", this, result);
|
return null;
|
||||||
return result;
|
}
|
||||||
|
|
||||||
|
// Only if the rawTx request succeeded we go on to the tx proof request.
|
||||||
|
// The result from the rawTx request does not contain any detail data in the
|
||||||
|
// success case, so we drop it.
|
||||||
|
return getResultFromTxProofRequest();
|
||||||
});
|
});
|
||||||
|
|
||||||
Futures.addCallback(future, new FutureCallback<>() {
|
Futures.addCallback(future, new FutureCallback<>() {
|
||||||
@ -265,7 +266,7 @@ class XmrTxProofRequest implements AssetTxProofRequest<XmrTxProofRequest.Result>
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void onFailure(@NotNull Throwable throwable) {
|
public void onFailure(@NotNull Throwable throwable) {
|
||||||
String errorMessage = this + " failed with error " + throwable.toString();
|
String errorMessage = this + " failed with error " + throwable;
|
||||||
faultHandler.handleFault(errorMessage, throwable);
|
faultHandler.handleFault(errorMessage, throwable);
|
||||||
UserThread.execute(() ->
|
UserThread.execute(() ->
|
||||||
resultHandler.accept(XmrTxProofRequest.Result.ERROR.with(Detail.CONNECTION_FAILURE.error(errorMessage))));
|
resultHandler.accept(XmrTxProofRequest.Result.ERROR.with(Detail.CONNECTION_FAILURE.error(errorMessage))));
|
||||||
@ -273,6 +274,45 @@ class XmrTxProofRequest implements AssetTxProofRequest<XmrTxProofRequest.Result>
|
|||||||
}, MoreExecutors.directExecutor());
|
}, MoreExecutors.directExecutor());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Result getResultFromRawTxRequest() throws IOException {
|
||||||
|
// The rawtransaction endpoint is not documented in explorer docs.
|
||||||
|
// Example request: https://xmrblocks.bisq.services/api/rawtransaction/5e665addf6d7c6300670e8a89564ed12b5c1a21c336408e2835668f9a6a0d802
|
||||||
|
String param = "/api/rawtransaction/" + model.getTxHash();
|
||||||
|
log.info("Param {} for rawtransaction request {}", param, this);
|
||||||
|
String json = httpClient.get(param, "User-Agent", "bisq/" + Version.VERSION);
|
||||||
|
try {
|
||||||
|
String prettyJson = new GsonBuilder().setPrettyPrinting().create().toJson(new JsonParser().parse(json));
|
||||||
|
log.info("Response json from rawtransaction request {}\n{}", this, prettyJson);
|
||||||
|
} catch (Throwable error) {
|
||||||
|
log.error("Pretty print caused a {}: raw json={}", error, json);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result result = rawTxParser.parse(json);
|
||||||
|
log.info("Result from rawtransaction request {}\n{}", this, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result getResultFromTxProofRequest() throws IOException {
|
||||||
|
// The API use the viewkey param for txKey if txprove is true
|
||||||
|
// https://github.com/moneroexamples/onion-monero-blockchain-explorer/blob/9a37839f37abef0b8b94ceeba41ab51a41f3fbd8/src/page.h#L5254
|
||||||
|
String param = "/api/outputs?txhash=" + model.getTxHash() +
|
||||||
|
"&address=" + model.getRecipientAddress() +
|
||||||
|
"&viewkey=" + model.getTxKey() +
|
||||||
|
"&txprove=1";
|
||||||
|
log.info("Param {} for {}", param, this);
|
||||||
|
String json = httpClient.get(param, "User-Agent", "bisq/" + Version.VERSION);
|
||||||
|
try {
|
||||||
|
String prettyJson = new GsonBuilder().setPrettyPrinting().create().toJson(new JsonParser().parse(json));
|
||||||
|
log.info("Response json from {}\n{}", this, prettyJson);
|
||||||
|
} catch (Throwable error) {
|
||||||
|
log.error("Pretty print caused a {}: raw json={}", error, json);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result result = txProofParser.parse(model, json);
|
||||||
|
log.info("Result from {}\n{}", this, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void terminate() {
|
public void terminate() {
|
||||||
executorService.shutdown();
|
executorService.shutdown();
|
||||||
|
@ -156,7 +156,7 @@ class XmrTxProofRequestsPerTrade implements AssetTxProofRequestsPerTrade {
|
|||||||
|
|
||||||
request.requestFromService(result -> {
|
request.requestFromService(result -> {
|
||||||
// If we ever received an error or failed result we terminate and do not process any
|
// If we ever received an error or failed result we terminate and do not process any
|
||||||
// future result anymore to avoid that we overwrite out state with success.
|
// future result anymore to avoid that we overwrite our state with success.
|
||||||
if (wasTerminated()) {
|
if (wasTerminated()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,47 @@
|
|||||||
|
package bisq.core.trade.txproof.xmr;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
|
||||||
|
public class XmrRawTxParserTest {
|
||||||
|
private final XmrRawTxParser parser = new XmrRawTxParser();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testJsonRoot() {
|
||||||
|
// checking what happens when bad input is provided
|
||||||
|
assertSame(parser.parse("invalid json data").getDetail(), XmrTxProofRequest.Detail.API_INVALID);
|
||||||
|
assertSame(parser.parse("").getDetail(), XmrTxProofRequest.Detail.API_INVALID);
|
||||||
|
assertSame(parser.parse("[]").getDetail(), XmrTxProofRequest.Detail.API_INVALID);
|
||||||
|
assertSame(parser.parse("{}").getDetail(), XmrTxProofRequest.Detail.API_INVALID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testJsonTopLevel() {
|
||||||
|
// testing the top level fields: data and status
|
||||||
|
assertSame(parser.parse("{'data':{'title':''},'status':'fail'}")
|
||||||
|
.getDetail(), XmrTxProofRequest.Detail.TX_NOT_FOUND);
|
||||||
|
assertSame(parser.parse("{'data':{'title':''},'missingstatus':'success'}")
|
||||||
|
.getDetail(), XmrTxProofRequest.Detail.API_INVALID);
|
||||||
|
assertSame(parser.parse("{'missingdata':{'title':''},'status':'success'}")
|
||||||
|
.getDetail(), XmrTxProofRequest.Detail.API_INVALID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testJsonTxUnlockTime() {
|
||||||
|
String missing_tx_timestamp = "{'data':{'version':'2'}, 'status':'success'}";
|
||||||
|
assertSame(parser.parse(missing_tx_timestamp).getDetail(), XmrTxProofRequest.Detail.API_INVALID);
|
||||||
|
|
||||||
|
String invalid_unlock_time = "{'data':{'unlock_time':'1'}, 'status':'success'}";
|
||||||
|
assertSame(parser.parse(invalid_unlock_time).getDetail(), XmrTxProofRequest.Detail.INVALID_UNLOCK_TIME);
|
||||||
|
|
||||||
|
String valid_unlock_time = "{'data':{'unlock_time':'0'}, 'status':'success'}";
|
||||||
|
assertSame(parser.parse(valid_unlock_time).getDetail(), XmrTxProofRequest.Detail.SUCCESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testJsonFail() {
|
||||||
|
String failedJson = "{\"data\":null,\"message\":\"Cant parse tx hash: a\",\"status\":\"error\"}";
|
||||||
|
assertSame(parser.parse(failedJson).getDetail(), XmrTxProofRequest.Detail.API_INVALID);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user