mirror of
https://github.com/bisq-network/bisq.git
synced 2024-11-19 01:41:11 +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> {
|
||||
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
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@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) {
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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