mirror of
https://github.com/bisq-network/bisq.git
synced 2024-11-19 09:52:23 +01:00
Merge pull request #1718 from sqrrm/refactor-dao-parser
[WIP] Refactor dao parser
This commit is contained in:
commit
126d279e6e
@ -24,9 +24,12 @@ import bisq.core.dao.state.blockchain.TempTx;
|
||||
import bisq.core.dao.state.blockchain.TempTxOutput;
|
||||
import bisq.core.dao.state.blockchain.TxOutput;
|
||||
import bisq.core.dao.state.blockchain.TxOutputType;
|
||||
import bisq.core.dao.state.blockchain.TxType;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import lombok.Getter;
|
||||
@ -47,6 +50,8 @@ public class TxOutputParser {
|
||||
@Getter
|
||||
@Setter
|
||||
private long availableInputValue = 0;
|
||||
private int lockTime;
|
||||
private List<TempTxOutput> bsqOutputs = new ArrayList<>();
|
||||
@Setter
|
||||
private int unlockBlockHeight;
|
||||
@Setter
|
||||
@ -58,8 +63,6 @@ public class TxOutputParser {
|
||||
@Getter
|
||||
private boolean bsqOutputFound;
|
||||
@Getter
|
||||
private Optional<OpReturnType> optionalOpReturnTypeCandidate = Optional.empty();
|
||||
@Getter
|
||||
private Optional<OpReturnType> optionalVerifiedOpReturnType = Optional.empty();
|
||||
@Getter
|
||||
private Optional<TempTxOutput> optionalIssuanceCandidate = Optional.empty();
|
||||
@ -74,15 +77,41 @@ public class TxOutputParser {
|
||||
this.bsqStateService = bsqStateService;
|
||||
}
|
||||
|
||||
public void commitTxOutputs() {
|
||||
bsqOutputs.forEach(bsqOutput -> {
|
||||
bsqStateService.addUnspentTxOutput(TxOutput.fromTempOutput(bsqOutput));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This sets all outputs to BTC_OUTPUT and doesn't add any txOutputs to the bsqStateService
|
||||
*/
|
||||
public void commitTxOutputsForInvalidTx() {
|
||||
bsqOutputs.forEach(bsqOutput -> {
|
||||
bsqOutput.setTxOutputType(TxOutputType.BTC_OUTPUT);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public void processGenesisTxOutput(TempTx genesisTx) {
|
||||
for (int i = 0; i < genesisTx.getTempTxOutputs().size(); ++i) {
|
||||
TempTxOutput tempTxOutput = genesisTx.getTempTxOutputs().get(i);
|
||||
bsqStateService.addUnspentTxOutput(TxOutput.fromTempOutput(tempTxOutput));
|
||||
bsqOutputs.add(tempTxOutput);
|
||||
}
|
||||
commitTxOutputs();
|
||||
}
|
||||
|
||||
void processOpReturnCandidate(TempTxOutput txOutput) {
|
||||
optionalOpReturnTypeCandidate = OpReturnParser.getOptionalOpReturnTypeCandidate(txOutput);
|
||||
boolean isOpReturnOutput(TempTxOutput txOutput) {
|
||||
return txOutput.getOpReturnData() != null;
|
||||
}
|
||||
|
||||
void processOpReturnOutput(boolean isLastOutput, TempTxOutput tempTxOutput) {
|
||||
byte[] opReturnData = tempTxOutput.getOpReturnData();
|
||||
if (opReturnData != null) {
|
||||
handleOpReturnOutput(tempTxOutput, isLastOutput);
|
||||
} else {
|
||||
log.error("This should be an opReturn output");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -97,7 +126,10 @@ public class TxOutputParser {
|
||||
byte[] opReturnData = tempTxOutput.getOpReturnData();
|
||||
if (opReturnData == null) {
|
||||
long txOutputValue = tempTxOutput.getValue();
|
||||
if (isUnlockBondTx(tempTxOutput.getValue(), index)) {
|
||||
if (tempTx.getTxType() == TxType.INVALID) {
|
||||
// Set all non opReturn outputs to BTC_OUTPUT if the tx is invalid
|
||||
tempTxOutput.setTxOutputType(TxOutputType.BTC_OUTPUT);
|
||||
} else if (isUnlockBondTx(tempTxOutput.getValue(), index)) {
|
||||
// We need to handle UNLOCK transactions separately as they don't follow the pattern on spending BSQ
|
||||
// The LOCKUP BSQ is burnt unless the output exactly matches the input, that would cause the
|
||||
// output to not be BSQ output at all
|
||||
@ -108,14 +140,10 @@ public class TxOutputParser {
|
||||
handleBtcOutput(tempTxOutput, index);
|
||||
}
|
||||
} else {
|
||||
handleOpReturnOutput(tempTxOutput, isLastOutput);
|
||||
log.error("This should not be an opReturn output");
|
||||
}
|
||||
}
|
||||
|
||||
boolean isOpReturnOutput(TempTxOutput txOutput) {
|
||||
return txOutput.getOpReturnData() != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a transaction is a valid unlock bond transaction or not.
|
||||
*
|
||||
@ -136,7 +164,7 @@ public class TxOutputParser {
|
||||
availableInputValue -= optionalSpentLockupTxOutput.get().getValue();
|
||||
|
||||
txOutput.setTxOutputType(TxOutputType.UNLOCK);
|
||||
bsqStateService.addUnspentTxOutput(TxOutput.fromTempOutput(txOutput));
|
||||
bsqOutputs.add(txOutput);
|
||||
|
||||
//TODO move up to TxParser
|
||||
// We should add unlockBlockHeight to TempTxOutput and remove unlockBlockHeight from tempTx
|
||||
@ -152,8 +180,8 @@ public class TxOutputParser {
|
||||
boolean isFirstOutput = index == 0;
|
||||
|
||||
OpReturnType opReturnTypeCandidate = null;
|
||||
if (optionalOpReturnTypeCandidate.isPresent())
|
||||
opReturnTypeCandidate = optionalOpReturnTypeCandidate.get();
|
||||
if (optionalVerifiedOpReturnType.isPresent())
|
||||
opReturnTypeCandidate = optionalVerifiedOpReturnType.get();
|
||||
|
||||
TxOutputType bsqOutput;
|
||||
if (isFirstOutput && opReturnTypeCandidate == OpReturnType.BLIND_VOTE) {
|
||||
@ -164,12 +192,13 @@ public class TxOutputParser {
|
||||
optionalVoteRevealUnlockStakeOutput = Optional.of(txOutput);
|
||||
} else if (isFirstOutput && opReturnTypeCandidate == OpReturnType.LOCKUP) {
|
||||
bsqOutput = TxOutputType.LOCKUP;
|
||||
txOutput.setLockTime(lockTime);
|
||||
optionalLockupOutput = Optional.of(txOutput);
|
||||
} else {
|
||||
bsqOutput = TxOutputType.BSQ_OUTPUT;
|
||||
}
|
||||
txOutput.setTxOutputType(bsqOutput);
|
||||
bsqStateService.addUnspentTxOutput(TxOutput.fromTempOutput(txOutput));
|
||||
bsqOutputs.add(txOutput);
|
||||
|
||||
bsqOutputFound = true;
|
||||
}
|
||||
@ -179,8 +208,8 @@ public class TxOutputParser {
|
||||
// candidate to the parsingModel and we don't apply the TxOutputType as we do that later as the OpReturn check.
|
||||
if (availableInputValue > 0 &&
|
||||
index == 1 &&
|
||||
optionalOpReturnTypeCandidate.isPresent() &&
|
||||
optionalOpReturnTypeCandidate.get() == OpReturnType.COMPENSATION_REQUEST) {
|
||||
optionalVerifiedOpReturnType.isPresent() &&
|
||||
optionalVerifiedOpReturnType.get() == OpReturnType.COMPENSATION_REQUEST) {
|
||||
// We don't set the txOutputType yet as we have not fully validated the tx but put the candidate
|
||||
// into our local optionalIssuanceCandidate.
|
||||
|
||||
@ -199,8 +228,7 @@ public class TxOutputParser {
|
||||
.ifPresent(verifiedOpReturnType -> {
|
||||
byte[] opReturnData = tempTxOutput.getOpReturnData();
|
||||
checkNotNull(opReturnData, "opReturnData must not be null");
|
||||
int lockTime = BondingConsensus.getLockTime(opReturnData);
|
||||
tempTxOutput.setLockTime(lockTime);
|
||||
lockTime = BondingConsensus.getLockTime(opReturnData);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -66,7 +66,7 @@ public class TxParser {
|
||||
}
|
||||
|
||||
// Apply state changes to tx, inputs and outputs
|
||||
// return true if any input contained BSQ
|
||||
// return Tx if any input contained BSQ
|
||||
// Any tx with BSQ input is a BSQ tx (except genesis tx but that is not handled in
|
||||
// that class).
|
||||
// There might be txs without any valid BSQ txOutput but we still keep track of it,
|
||||
@ -101,6 +101,15 @@ public class TxParser {
|
||||
|
||||
long accumulatedInputValue = txInputParser.getAccumulatedInputValue();
|
||||
txOutputParser.setAvailableInputValue(accumulatedInputValue);
|
||||
|
||||
// We don't allow multiple opReturn outputs (they are non-standard but to be safe lets check it)
|
||||
long numOpReturnOutputs = tempTx.getTempTxOutputs().stream().filter(txOutputParser::isOpReturnOutput).count();
|
||||
if (numOpReturnOutputs > 1) {
|
||||
tempTx.setTxType(TxType.INVALID);
|
||||
String msg = "Invalid tx. We have multiple opReturn outputs. tx=" + tempTx;
|
||||
log.warn(msg);
|
||||
}
|
||||
|
||||
txOutputParser.setUnlockBlockHeight(txInputParser.getUnlockBlockHeight());
|
||||
txOutputParser.setOptionalSpentLockupTxOutput(txInputParser.getOptionalSpentLockupTxOutput());
|
||||
txOutputParser.setTempTx(tempTx); //TODO remove
|
||||
@ -115,11 +124,16 @@ public class TxParser {
|
||||
// We keep the temporary opReturn type in the parsingModel object.
|
||||
checkArgument(!outputs.isEmpty(), "outputs must not be empty");
|
||||
int lastIndex = outputs.size() - 1;
|
||||
txOutputParser.processOpReturnCandidate(outputs.get(lastIndex));
|
||||
int lastNonOpReturnIndex = lastIndex;
|
||||
if (txOutputParser.isOpReturnOutput(outputs.get(lastIndex))) {
|
||||
// TODO(SQ): perhaps the check for isLastOutput could be skipped
|
||||
txOutputParser.processOpReturnOutput(true, outputs.get(lastIndex));
|
||||
lastNonOpReturnIndex -= 1;
|
||||
}
|
||||
|
||||
// We use order of output index. An output is a BSQ utxo as long there is enough input value
|
||||
// We iterate all outputs including the opReturn to do a full validation including the BSQ fee
|
||||
for (int index = 0; index < outputs.size(); index++) {
|
||||
for (int index = 0; index <= lastNonOpReturnIndex; index++) {
|
||||
boolean isLastOutput = index == lastIndex;
|
||||
txOutputParser.processTxOutput(isLastOutput,
|
||||
outputs.get(index),
|
||||
@ -131,9 +145,12 @@ public class TxParser {
|
||||
|
||||
processOpReturnType(blockHeight, tempTx);
|
||||
|
||||
// We don't allow multiple opReturn outputs (they are non-standard but to be safe lets check it)
|
||||
long numOpReturnOutputs = tempTx.getTempTxOutputs().stream().filter(txOutputParser::isOpReturnOutput).count();
|
||||
if (numOpReturnOutputs <= 1) {
|
||||
// TODO(SQ): Should the destroyed BSQ from an INVALID tx be considered as burnt fee?
|
||||
if (remainingInputValue > 0)
|
||||
tempTx.setBurntFee(remainingInputValue);
|
||||
|
||||
// Process the type of transaction if not already determined to be INVALID
|
||||
if (tempTx.getTxType() != TxType.INVALID) {
|
||||
boolean isAnyTxOutputTypeUndefined = tempTx.getTempTxOutputs().stream()
|
||||
.anyMatch(txOutput -> TxOutputType.UNDEFINED == txOutput.getTxOutputType());
|
||||
if (!isAnyTxOutputTypeUndefined) {
|
||||
@ -141,23 +158,22 @@ public class TxParser {
|
||||
// use RawTx?
|
||||
TxType txType = TxParser.getBisqTxType(
|
||||
tempTx,
|
||||
txOutputParser.getOptionalOpReturnTypeCandidate().isPresent(),
|
||||
txOutputParser.getOptionalVerifiedOpReturnType().isPresent(),
|
||||
remainingInputValue,
|
||||
getOptionalOpReturnType()
|
||||
);
|
||||
tempTx.setTxType(txType);
|
||||
if (remainingInputValue > 0)
|
||||
tempTx.setBurntFee(remainingInputValue);
|
||||
} else {
|
||||
tempTx.setTxType(TxType.INVALID);
|
||||
String msg = "We have undefined txOutput types which must not happen. tx=" + tempTx;
|
||||
DevEnv.logErrorAndThrowIfDevMode(msg);
|
||||
}
|
||||
}
|
||||
|
||||
if (tempTx.getTxType() != TxType.INVALID){
|
||||
txOutputParser.commitTxOutputs();
|
||||
} else {
|
||||
// We don't consider a tx with multiple OpReturn outputs valid.
|
||||
tempTx.setTxType(TxType.INVALID);
|
||||
String msg = "Invalid tx. We have multiple opReturn outputs. tx=" + tempTx;
|
||||
log.warn(msg);
|
||||
txOutputParser.commitTxOutputsForInvalidTx();
|
||||
}
|
||||
}
|
||||
|
||||
@ -186,55 +202,16 @@ public class TxParser {
|
||||
boolean isFeeAndPhaseValid;
|
||||
switch (verifiedOpReturnType) {
|
||||
case PROPOSAL:
|
||||
isFeeAndPhaseValid = isFeeAndPhaseValid(blockHeight, bsqFee, DaoPhase.Phase.PROPOSAL, Param.PROPOSAL_FEE);
|
||||
if (!isFeeAndPhaseValid) {
|
||||
tempTx.setTxType(TxType.INVALID);
|
||||
}
|
||||
processProposal(blockHeight, tempTx, bsqFee);
|
||||
break;
|
||||
case COMPENSATION_REQUEST:
|
||||
isFeeAndPhaseValid = isFeeAndPhaseValid(blockHeight, bsqFee, DaoPhase.Phase.PROPOSAL, Param.PROPOSAL_FEE);
|
||||
Optional<TempTxOutput> optionalIssuanceCandidate = txOutputParser.getOptionalIssuanceCandidate();
|
||||
if (isFeeAndPhaseValid) {
|
||||
if (optionalIssuanceCandidate.isPresent()) {
|
||||
// Now after we have validated the opReturn data we will apply the TxOutputType
|
||||
optionalIssuanceCandidate.get().setTxOutputType(TxOutputType.ISSUANCE_CANDIDATE_OUTPUT);
|
||||
} else {
|
||||
log.warn("It can be that we have a opReturn which is correct from its structure but the whole tx " +
|
||||
"in not valid as the issuanceCandidate in not there. " +
|
||||
"As the BSQ fee is set it must be either a buggy tx or an manually crafted invalid tx.");
|
||||
}
|
||||
} else {
|
||||
tempTx.setTxType(TxType.INVALID);
|
||||
optionalIssuanceCandidate.ifPresent(tempTxOutput -> tempTxOutput.setTxOutputType(TxOutputType.BTC_OUTPUT));
|
||||
// Empty Optional case is a possible valid case where a random tx matches our opReturn rules but it is not a
|
||||
// valid BSQ tx.
|
||||
}
|
||||
processCompensationRequest(blockHeight, tempTx, bsqFee);
|
||||
break;
|
||||
case BLIND_VOTE:
|
||||
isFeeAndPhaseValid = isFeeAndPhaseValid(blockHeight, bsqFee, DaoPhase.Phase.BLIND_VOTE, Param.BLIND_VOTE_FEE);
|
||||
if (!isFeeAndPhaseValid) {
|
||||
tempTx.setTxType(TxType.INVALID);
|
||||
Optional<TempTxOutput> optionalBlindVoteLockStakeOutput = txOutputParser.getOptionalBlindVoteLockStakeOutput();
|
||||
optionalBlindVoteLockStakeOutput.ifPresent(tempTxOutput -> tempTxOutput.setTxOutputType(TxOutputType.BTC_OUTPUT));
|
||||
// Empty Optional case is a possible valid case where a random tx matches our opReturn rules but it is not a
|
||||
// valid BSQ tx.
|
||||
}
|
||||
processBlindVote(blockHeight, tempTx, bsqFee);
|
||||
break;
|
||||
case VOTE_REVEAL:
|
||||
boolean isPhaseValid = isPhaseValid(blockHeight, DaoPhase.Phase.VOTE_REVEAL);
|
||||
boolean isVoteRevealInputInValid = txInputParser.getVoteRevealInputState() != TxInputParser.VoteRevealInputState.VALID;
|
||||
if (!isPhaseValid) {
|
||||
tempTx.setTxType(TxType.INVALID);
|
||||
}
|
||||
if (!isPhaseValid || isVoteRevealInputInValid) {
|
||||
Optional<TempTxOutput> optionalVoteRevealUnlockStakeOutput = txOutputParser
|
||||
.getOptionalVoteRevealUnlockStakeOutput();
|
||||
optionalVoteRevealUnlockStakeOutput.ifPresent(
|
||||
tempTxOutput -> tempTxOutput
|
||||
.setTxOutputType(TxOutputType.BTC_OUTPUT));
|
||||
// Empty Optional case is a possible valid case where a random tx matches our opReturn rules but it is not a
|
||||
// valid BSQ tx.
|
||||
}
|
||||
processVoteReveal(blockHeight, tempTx);
|
||||
break;
|
||||
case LOCKUP:
|
||||
// do nothing
|
||||
@ -261,6 +238,64 @@ public class TxParser {
|
||||
}
|
||||
}
|
||||
|
||||
private void processVoteReveal(int blockHeight, TempTx tempTx) {
|
||||
boolean isPhaseValid = isPhaseValid(blockHeight, DaoPhase.Phase.VOTE_REVEAL);
|
||||
boolean isVoteRevealInputInValid = txInputParser.getVoteRevealInputState() != TxInputParser.VoteRevealInputState.VALID;
|
||||
if (!isPhaseValid) {
|
||||
tempTx.setTxType(TxType.INVALID);
|
||||
}
|
||||
if (!isPhaseValid || isVoteRevealInputInValid) {
|
||||
Optional<TempTxOutput> optionalVoteRevealUnlockStakeOutput = txOutputParser
|
||||
.getOptionalVoteRevealUnlockStakeOutput();
|
||||
optionalVoteRevealUnlockStakeOutput.ifPresent(
|
||||
tempTxOutput -> tempTxOutput
|
||||
.setTxOutputType(TxOutputType.BTC_OUTPUT));
|
||||
// Empty Optional case is a possible valid case where a random tx matches our opReturn rules but it is not a
|
||||
// valid BSQ tx.
|
||||
}
|
||||
}
|
||||
|
||||
private void processBlindVote(int blockHeight, TempTx tempTx, long bsqFee) {
|
||||
boolean isFeeAndPhaseValid;
|
||||
isFeeAndPhaseValid = isFeeAndPhaseValid(blockHeight, bsqFee, DaoPhase.Phase.BLIND_VOTE, Param.BLIND_VOTE_FEE);
|
||||
if (!isFeeAndPhaseValid) {
|
||||
tempTx.setTxType(TxType.INVALID);
|
||||
Optional<TempTxOutput> optionalBlindVoteLockStakeOutput = txOutputParser.getOptionalBlindVoteLockStakeOutput();
|
||||
optionalBlindVoteLockStakeOutput.ifPresent(tempTxOutput -> tempTxOutput.setTxOutputType(TxOutputType.BTC_OUTPUT));
|
||||
// Empty Optional case is a possible valid case where a random tx matches our opReturn rules but it is not a
|
||||
// valid BSQ tx.
|
||||
}
|
||||
}
|
||||
|
||||
private void processCompensationRequest(int blockHeight, TempTx tempTx, long bsqFee) {
|
||||
boolean isFeeAndPhaseValid =
|
||||
isFeeAndPhaseValid(blockHeight, bsqFee, DaoPhase.Phase.PROPOSAL, Param.PROPOSAL_FEE);
|
||||
Optional<TempTxOutput> optionalIssuanceCandidate = txOutputParser.getOptionalIssuanceCandidate();
|
||||
if (isFeeAndPhaseValid) {
|
||||
if (optionalIssuanceCandidate.isPresent()) {
|
||||
// Now after we have validated the opReturn data we will apply the TxOutputType
|
||||
optionalIssuanceCandidate.get().setTxOutputType(TxOutputType.ISSUANCE_CANDIDATE_OUTPUT);
|
||||
} else {
|
||||
log.warn("It can be that we have a opReturn which is correct from its structure but the whole tx " +
|
||||
"in not valid as the issuanceCandidate in not there. " +
|
||||
"As the BSQ fee is set it must be either a buggy tx or an manually crafted invalid tx.");
|
||||
}
|
||||
} else {
|
||||
tempTx.setTxType(TxType.INVALID);
|
||||
optionalIssuanceCandidate.ifPresent(tempTxOutput -> tempTxOutput.setTxOutputType(TxOutputType.BTC_OUTPUT));
|
||||
// Empty Optional case is a possible valid case where a random tx matches our opReturn rules but it is not a
|
||||
// valid BSQ tx.
|
||||
}
|
||||
}
|
||||
|
||||
private void processProposal(int blockHeight, TempTx tempTx, long bsqFee) {
|
||||
boolean isFeeAndPhaseValid =
|
||||
isFeeAndPhaseValid(blockHeight, bsqFee, DaoPhase.Phase.PROPOSAL, Param.PROPOSAL_FEE);
|
||||
if (!isFeeAndPhaseValid) {
|
||||
tempTx.setTxType(TxType.INVALID);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the BSQ fee and phase is valid for a transaction.
|
||||
*
|
||||
@ -369,20 +404,7 @@ public class TxParser {
|
||||
*/
|
||||
private Optional<OpReturnType> getOptionalOpReturnType() {
|
||||
if (txOutputParser.isBsqOutputFound()) {
|
||||
// We want to be sure that the initial assumption of the opReturn type was matching the result after full
|
||||
// validation.
|
||||
Optional<OpReturnType> optionalOpReturnTypeCandidate = txOutputParser.getOptionalOpReturnTypeCandidate();
|
||||
Optional<OpReturnType> optionalVerifiedOpReturnType = txOutputParser.getOptionalVerifiedOpReturnType();
|
||||
if (optionalOpReturnTypeCandidate.isPresent() && optionalVerifiedOpReturnType.isPresent()) {
|
||||
if (optionalOpReturnTypeCandidate.get() == optionalVerifiedOpReturnType.get()) {
|
||||
return optionalVerifiedOpReturnType;
|
||||
} else {
|
||||
String msg = "We got a different opReturn type after validation as we expected initially. " +
|
||||
"optionalOpReturnTypeCandidate=" + optionalOpReturnTypeCandidate +
|
||||
", optionalVerifiedOpReturnType=" + txOutputParser.getOptionalVerifiedOpReturnType();
|
||||
log.warn(msg);
|
||||
}
|
||||
}
|
||||
return txOutputParser.getOptionalVerifiedOpReturnType();
|
||||
} else {
|
||||
String msg = "We got a tx without any valid BSQ output but with burned BSQ. " +
|
||||
"Burned fee=" + remainingInputValue / 100D + " BSQ.";
|
||||
|
@ -138,14 +138,12 @@ public final class Tx extends BaseTx implements PersistablePayload {
|
||||
|
||||
|
||||
/**
|
||||
* OpReturn output might contain the lockTime in case of a LockTx. It has to be the last output.
|
||||
* We store technically the lockTime there as is is stored in the OpReturn data but conceptually we want to provide
|
||||
* it from the transaction.
|
||||
* The locktime is stored in the LOCKUP txOutput, which is the first txOutput.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public int getLockTime() {
|
||||
return getLastTxOutput().getLockTime();
|
||||
return txOutputs.get(0).getLockTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
Loading…
Reference in New Issue
Block a user