Add more validations

Signed-off-by: HenrikJannsen <boilingfrog@gmx.com>
This commit is contained in:
HenrikJannsen 2023-03-31 15:07:24 +07:00 committed by Alejandro García
parent 2669d736b6
commit 874f6b4aa5
No known key found for this signature in database
GPG Key ID: F806F422E222AA02
6 changed files with 198 additions and 20 deletions

View File

@ -19,4 +19,6 @@ package bisq.core.trade.txproof;
public interface AssetTxProofParser<R extends AssetTxProofRequest.Result, T extends AssetTxProofModel> {
R parse(T model, String jsonTxt);
R parse(String jsonTxt);
}

View File

@ -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()));
}
}
}

View File

@ -45,6 +45,11 @@ public class XmrTxProofParser implements AssetTxProofParser<XmrTxProofRequest.Re
// API
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public XmrTxProofRequest.Result parse(String jsonTxt) {
throw new UnsupportedOperationException("This method is not supported for this parser");
}
@SuppressWarnings("SpellCheckingInspection")
@Override
public XmrTxProofRequest.Result parse(XmrTxProofModel model, String jsonTxt) {

View File

@ -37,6 +37,8 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
@ -101,6 +103,7 @@ class XmrTxProofRequest implements AssetTxProofRequest<XmrTxProofRequest.Result>
NO_MATCH_FOUND,
AMOUNT_NOT_MATCHING,
TRADE_DATE_NOT_MATCHING,
INVALID_UNLOCK_TIME,
NO_RESULTS_TIMEOUT;
@Getter
@ -143,7 +146,8 @@ class XmrTxProofRequest implements AssetTxProofRequest<XmrTxProofRequest.Result>
private final ListeningExecutorService executorService = Utilities.getListeningExecutorService(
"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 AssetTxProofHttpClient httpClient;
private final long firstRequest;
@ -160,7 +164,8 @@ class XmrTxProofRequest implements AssetTxProofRequest<XmrTxProofRequest.Result>
XmrTxProofRequest(Socks5ProxyProvider socks5ProxyProvider,
XmrTxProofModel model) {
this.parser = new XmrTxProofParser();
txProofParser = new XmrTxProofParser();
rawTxParser = new XmrRawTxParser();
this.model = model;
httpClient = new XmrTxProofHttpClient(socks5ProxyProvider);
@ -206,24 +211,20 @@ class XmrTxProofRequest implements AssetTxProofRequest<XmrTxProofRequest.Result>
ListenableFuture<Result> future = executorService.submit(() -> {
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
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 = getResultFromRawTxRequest();
if (result != Result.SUCCESS) {
return result;
}
Result result = parser.parse(model, json);
log.info("Result from {}\n{}", this, result);
return result;
if (terminated) {
return null;
}
// 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<>() {
@ -265,7 +266,7 @@ class XmrTxProofRequest implements AssetTxProofRequest<XmrTxProofRequest.Result>
}
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);
UserThread.execute(() ->
resultHandler.accept(XmrTxProofRequest.Result.ERROR.with(Detail.CONNECTION_FAILURE.error(errorMessage))));
@ -273,6 +274,45 @@ class XmrTxProofRequest implements AssetTxProofRequest<XmrTxProofRequest.Result>
}, 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
public void terminate() {
executorService.shutdown();

View File

@ -156,7 +156,7 @@ class XmrTxProofRequestsPerTrade implements AssetTxProofRequestsPerTrade {
request.requestFromService(result -> {
// 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()) {
return;
}

View File

@ -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);
}
}