Merge pull request #6306 from chimp1984/update_dao_node

Update dao node
This commit is contained in:
Christoph Atteneder 2022-08-07 20:40:26 +02:00 committed by GitHub
commit ffcd96673e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 570 additions and 49 deletions

View File

@ -287,7 +287,7 @@ public class AccountAgeWitnessService {
}
public Optional<AccountAgeWitness> findWitness(PaymentAccountPayload paymentAccountPayload,
PubKeyRing pubKeyRing) {
PubKeyRing pubKeyRing) {
if (paymentAccountPayload == null) {
return Optional.empty();
}
@ -315,7 +315,7 @@ public class AccountAgeWitnessService {
findWitness(tradingPeer.getPaymentAccountPayload(), tradingPeer.getPubKeyRing());
}
private Optional<AccountAgeWitness> getWitnessByHash(byte[] hash) {
public Optional<AccountAgeWitness> getWitnessByHash(byte[] hash) {
P2PDataStorage.ByteArray hashAsByteArray = new P2PDataStorage.ByteArray(hash);
// First we look up in our fast lookup cache
@ -335,7 +335,7 @@ public class AccountAgeWitnessService {
return Optional.empty();
}
private Optional<AccountAgeWitness> getWitnessByHashAsHex(String hashAsHex) {
public Optional<AccountAgeWitness> getWitnessByHashAsHex(String hashAsHex) {
return getWitnessByHash(Utilities.decodeFromHex(hashAsHex));
}
@ -381,6 +381,15 @@ public class AccountAgeWitnessService {
}
}
public long getWitnessSignDate(AccountAgeWitness accountAgeWitness) {
List<Long> dates = signedWitnessService.getVerifiedWitnessDateList(accountAgeWitness);
if (dates.isEmpty()) {
return -1L;
} else {
return dates.get(0);
}
}
// Return -1 if not signed
public long getWitnessSignAge(Offer offer, Date now) {
return findWitness(offer)
@ -951,4 +960,8 @@ public class AccountAgeWitnessService {
return null;
}
}
public boolean isFilteredWitness(AccountAgeWitness accountAgeWitness) {
return signedWitnessService.isFilteredWitness(accountAgeWitness);
}
}

View File

@ -19,25 +19,36 @@ package bisq.core.account.witness;
import bisq.core.account.sign.SignedWitness;
import bisq.core.account.sign.SignedWitnessService;
import bisq.core.payment.PaymentAccount;
import bisq.core.payment.payload.PaymentAccountPayload;
import bisq.core.trade.model.bisq_v1.Trade;
import bisq.core.util.JsonUtil;
import bisq.network.p2p.storage.P2PDataStorage;
import bisq.common.app.DevEnv;
import bisq.common.crypto.CryptoException;
import bisq.common.crypto.Hash;
import bisq.common.crypto.KeyRing;
import bisq.common.crypto.PubKeyRing;
import bisq.common.crypto.Sig;
import bisq.common.util.Hex;
import bisq.common.util.Utilities;
import java.security.KeyPair;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.Optional;
import java.util.Stack;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j
@ -168,4 +179,109 @@ public class AccountAgeWitnessUtils {
accountAgeWitnessService.tradeAmountIsSufficient(trade.getAmount()),
isSignWitnessTrade);
}
static class AccountAgeWitnessDto {
private final String profileId;
private final String hashAsHex;
private final long date;
private final String pubKeyBase64;
private final String signatureBase64;
public AccountAgeWitnessDto(String profileId,
String hashAsHex,
long date,
String pubKeyBase64,
String signatureBase64) {
this.profileId = profileId;
this.hashAsHex = hashAsHex;
this.date = date;
this.pubKeyBase64 = pubKeyBase64;
this.signatureBase64 = signatureBase64;
}
}
static class SignedWitnessDto {
private final String profileId;
private final String hashAsHex;
private final long accountAgeWitnessDate;
private final long witnessSignDate;
private final String pubKeyBase64;
private final String signatureBase64;
public SignedWitnessDto(String profileId,
String hashAsHex,
long accountAgeWitnessDate,
long witnessSignDate,
String pubKeyBase64,
String signatureBase64) {
this.profileId = profileId;
this.hashAsHex = hashAsHex;
this.accountAgeWitnessDate = accountAgeWitnessDate;
this.witnessSignDate = witnessSignDate;
this.pubKeyBase64 = pubKeyBase64;
this.signatureBase64 = signatureBase64;
}
}
public static Optional<String> signAccountAgeAndBisq2ProfileId(AccountAgeWitnessService accountAgeWitnessService,
PaymentAccount account,
KeyRing keyRing,
String profileId) {
return accountAgeWitnessService.findWitness(account.getPaymentAccountPayload(), keyRing.getPubKeyRing())
.map(accountAgeWitness -> {
try {
checkArgument(!accountAgeWitnessService.isFilteredWitness(accountAgeWitness), "Invalid account age witness");
String hashAsHex = Hex.encode(accountAgeWitness.getHash());
long date = accountAgeWitness.getDate();
checkArgument(date > 0, "Date must be > 0");
String message = profileId + hashAsHex + date;
KeyPair signatureKeyPair = keyRing.getSignatureKeyPair();
String signatureBase64 = Sig.sign(signatureKeyPair.getPrivate(), message);
String pubKeyBase64 = Base64.getEncoder().encodeToString(Sig.getPublicKeyBytes(signatureKeyPair.getPublic()));
AccountAgeWitnessDto dto = new AccountAgeWitnessDto(profileId,
hashAsHex,
date,
pubKeyBase64,
signatureBase64);
return JsonUtil.objectToJson(dto);
} catch (CryptoException e) {
throw new RuntimeException(e);
}
});
}
public static Optional<String> signSignedWitnessAndBisq2ProfileId(AccountAgeWitnessService accountAgeWitnessService,
PaymentAccount account,
KeyRing keyRing,
String profileId) {
return accountAgeWitnessService.findWitness(account.getPaymentAccountPayload(), keyRing.getPubKeyRing())
.map(accountAgeWitness -> {
try {
checkArgument(!accountAgeWitnessService.isFilteredWitness(accountAgeWitness), "Invalid account age witness");
long witnessSignDate = accountAgeWitnessService.getWitnessSignDate(accountAgeWitness);
long ageInDays = (System.currentTimeMillis() - witnessSignDate) / TimeUnit.DAYS.toMillis(1);
if (!DevEnv.isDevMode()) {
checkArgument(witnessSignDate > 0, "Account is not signed yet");
checkArgument(ageInDays > 60, "Account must have been signed at least 61 days ago");
}
String hashAsHex = Hex.encode(accountAgeWitness.getHash());
long date = accountAgeWitness.getDate();
checkArgument(date > 0, "AccountAgeWitness date must be > 0");
String message = profileId + hashAsHex + date + witnessSignDate;
KeyPair signatureKeyPair = keyRing.getSignatureKeyPair();
String signatureBase64 = Sig.sign(signatureKeyPair.getPrivate(), message);
String pubKeyBase64 = Base64.getEncoder().encodeToString(Sig.getPublicKeyBytes(signatureKeyPair.getPublic()));
SignedWitnessDto dto = new SignedWitnessDto(profileId,
hashAsHex,
date,
witnessSignDate,
pubKeyBase64,
signatureBase64);
return JsonUtil.objectToJson(dto);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
}

View File

@ -167,7 +167,7 @@ public class ProofOfBurnService implements DaoSetupService, DaoStateListener {
myProofOfBurnListService.addMyProofOfBurn(myProofOfBurn);
}
public byte[] getHashFromOpReturnData(Tx tx) {
public static byte[] getHashFromOpReturnData(Tx tx) {
return ProofOfBurnConsensus.getHashFromOpReturnData(tx.getLastTxOutput().getOpReturnData());
}

View File

@ -1818,7 +1818,14 @@ providing cryptographic proof to the mediator or refund agent.\n\n\
If you do not understand these requirements, do not trade L-BTC on Bisq.
account.fiat.yourFiatAccounts=Your national currency accounts
account.fiat.exportAccountAge=Export account age for Bisq 2
account.fiat.exportAccountAge.popup=Your 'account age' and Bisq 2 profile ID got signed and the data is copied \
to the clipboard.\n\n\This is the data in json format:\n\n\{0}\n\n\
Go back to Bisq 2 and follow the instructions there.
account.fiat.signedWitness=Export signed witness for Bisq 2
account.fiat.signedWitness.popup=Your 'signed account age witness' and Bisq 2 profile ID got signed and the data is copied \
to the clipboard.\n\n\This is the data in json format:\n\n\{0}\n\n\
Go back to Bisq 2 and follow the instructions there.
account.backup.title=Backup wallet
account.backup.location=Backup location
account.backup.selectLocation=Select backup location

View File

@ -28,7 +28,10 @@ import lombok.extern.slf4j.Slf4j;
import bisq.daonode.endpoints.AccountAgeApi;
import bisq.daonode.endpoints.BondedReputationApi;
import bisq.daonode.endpoints.ProofOfBurnApi;
import bisq.daonode.endpoints.SignedWitnessApi;
import bisq.daonode.error.CustomExceptionMapper;
import bisq.daonode.error.StatusException;
import bisq.daonode.util.StaticFileHandler;
@ -54,6 +57,9 @@ public class DaoNodeRestApiApplication extends ResourceConfig {
.register(CustomExceptionMapper.class)
.register(StatusException.StatusExceptionMapper.class)
.register(ProofOfBurnApi.class)
.register(BondedReputationApi.class)
.register(AccountAgeApi.class)
.register(SignedWitnessApi.class)
.register(SwaggerResolution.class);
daoNodeRestApiApplication.startServer(config.daoNodeApiUrl, config.daoNodeApiPort);
});
@ -61,17 +67,18 @@ public class DaoNodeRestApiApplication extends ResourceConfig {
@Getter
private final DaoNode daoNode;
private final ServiceNode serviceNode;
private HttpServer httpServer;
public DaoNodeRestApiApplication() {
daoNode = new DaoNode();
serviceNode = new ServiceNode();
}
private void startDaoNode(String[] args, Consumer<Config> configConsumer) {
new Thread(() -> {
daoNode.execute(args);
configConsumer.accept(daoNode.getConfig());
serviceNode.execute(args);
configConsumer.accept(serviceNode.getConfig());
try {
// Keep running
Thread.currentThread().setName("daoNodeThread");
@ -106,8 +113,8 @@ public class DaoNodeRestApiApplication extends ResourceConfig {
}
private void shutDown() {
if (daoNode != null) {
daoNode.gracefulShutDown(this::stopServer);
if (serviceNode != null) {
serviceNode.gracefulShutDown(this::stopServer);
} else {
stopServer();
}

View File

@ -18,10 +18,12 @@
package bisq.daonode;
import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.app.TorSetup;
import bisq.core.app.misc.AppSetupWithP2PAndDAO;
import bisq.core.app.misc.ExecutableForAppWithP2p;
import bisq.core.app.misc.ModuleForAppWithP2p;
import bisq.core.dao.governance.bond.reputation.BondedReputationRepository;
import bisq.core.dao.state.DaoStateService;
import bisq.core.dao.state.DaoStateSnapshotService;
import bisq.core.user.Cookie;
@ -49,14 +51,18 @@ import lombok.extern.slf4j.Slf4j;
//todo not sure if the restart handling from seed nodes is required
@Slf4j
public class DaoNode extends ExecutableForAppWithP2p {
public class ServiceNode extends ExecutableForAppWithP2p {
private static final long CHECK_CONNECTION_LOSS_SEC = 30;
private Timer checkConnectionLossTime;
@Getter
private DaoStateService daoStateService;
@Getter
private BondedReputationRepository bondedReputationRepository;
@Getter
private AccountAgeWitnessService accountAgeWitnessService;
public DaoNode() {
public ServiceNode() {
super("Bisq Dao Node", "bisq-dao-node", "bisq_dao_node", Version.VERSION);
}
@ -117,6 +123,8 @@ public class DaoNode extends ExecutableForAppWithP2p {
injector.getInstance(AppSetupWithP2PAndDAO.class).start();
daoStateService = injector.getInstance(DaoStateService.class);
accountAgeWitnessService = injector.getInstance(AccountAgeWitnessService.class);
bondedReputationRepository = injector.getInstance(BondedReputationRepository.class);
injector.getInstance(P2PService.class).addP2PServiceListener(new P2PServiceListener() {
@Override
@ -149,9 +157,11 @@ public class DaoNode extends ExecutableForAppWithP2p {
boolean preventPeriodicShutdownAtSeedNode = injector.getInstance(Key.get(boolean.class,
Names.named(Config.PREVENT_PERIODIC_SHUTDOWN_AT_SEED_NODE)));
if (!preventPeriodicShutdownAtSeedNode) {
startShutDownInterval(DaoNode.this);
startShutDownInterval(ServiceNode.this);
}
UserThread.runAfter(() -> setupConnectionLossCheck(), 60);
accountAgeWitnessService.onAllServicesInitialized();
}
@Override

View File

@ -0,0 +1,48 @@
/*
* 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.daonode.dto;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* Minimal data required for Bisq 2 bonded reputation use case.
* Need to be in sync with the Bisq 2 BondedReputationDto class.
*/
@Getter
@Slf4j
@Schema(title = "BondedReputation")
public class BondedReputationDto {
private final long amount;
private final long time;
private final String hash;
private final int blockHeight;
private final int lockTime;
public BondedReputationDto(long amount, long time, String hash, int blockHeight, int lockTime) {
this.amount = amount;
this.time = time;
this.hash = hash;
this.blockHeight = blockHeight;
this.lockTime = lockTime;
}
}

View File

@ -24,23 +24,21 @@ import lombok.Getter;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* Minimal data required for Bisq 2 reputation use case.
* Minimal data required for Bisq 2 proof of burn use case.
* Need to be in sync with the Bisq 2 ProofOfBurnDto class.
*/
@Getter
@Schema(title = "ProofOfBurn")
public class ProofOfBurnDto {
private final String txId;
private final long burnedAmount;
private final int blockHeight;
private final long amount;
private final long time;
private final String hash;
private final int blockHeight;
public ProofOfBurnDto(String txId, long burnedAmount, int blockHeight, long time, String hash) {
this.txId = txId;
this.burnedAmount = burnedAmount;
this.blockHeight = blockHeight;
public ProofOfBurnDto(long amount, long time, String hash, int blockHeight) {
this.amount = amount;
this.time = time;
this.hash = hash;
this.blockHeight = blockHeight;
}
}

View File

@ -0,0 +1,76 @@
/*
* 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.daonode.endpoints;
import bisq.core.account.witness.AccountAgeWitness;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkNotNull;
import bisq.daonode.DaoNodeRestApiApplication;
import bisq.daonode.ServiceNode;
import bisq.daonode.dto.ProofOfBurnDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Application;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
/**
* Endpoint for getting the account age date from a given hash as hex string.
* Used for reputation system in Bisq 2.
* <a href="http://localhost:8082/api/v1/account-age/get-date/dd75a7175c7c83fe9a4729e36b85f5fbc44e29ae">Request with hash</a>
*/
@Slf4j
@Path("/account-age")
@Produces(MediaType.TEXT_PLAIN)
@Tag(name = "Account age API")
public class AccountAgeApi {
private static final String DESC_HASH = "The hash of the account age witness as hex string";
private final ServiceNode serviceNode;
public AccountAgeApi(@Context Application application) {
serviceNode = ((DaoNodeRestApiApplication) application).getServiceNode();
}
@Operation(description = "Request the account age date")
@ApiResponse(responseCode = "200", description = "The account age date",
content = {@Content(mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(allOf = ProofOfBurnDto.class))}
)
@GET
@Path("get-date/{hash}")
public Long getDate(@Parameter(description = DESC_HASH)
@PathParam("hash")
String hash) {
return checkNotNull(serviceNode.getAccountAgeWitnessService()).getWitnessByHashAsHex(hash)
.map(AccountAgeWitness::getDate)
.orElse(-1L);
}
}

View File

@ -0,0 +1,103 @@
/*
* 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.daonode.endpoints;
import bisq.core.dao.governance.bond.reputation.BondedReputationRepository;
import bisq.core.dao.state.DaoStateService;
import bisq.core.dao.state.model.blockchain.Tx;
import bisq.common.util.Hex;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import bisq.daonode.DaoNodeRestApiApplication;
import bisq.daonode.ServiceNode;
import bisq.daonode.dto.BondedReputationDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Application;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
/**
* Endpoint for getting the bonded reputation data from a given block height.
* Used for reputation system in Bisq 2.
* <a href="http://localhost:8082/api/v1/bonded-reputation/get-bonded-reputation/0">Request with block height 0</a>
*/
@Slf4j
@Path("/bonded-reputation")
@Produces(MediaType.APPLICATION_JSON)
@Tag(name = "Bonded reputation API")
public class BondedReputationApi {
private static final String DESC_BLOCK_HEIGHT = "The block height from which we request the bonded reputation data";
private final BondedReputationRepository bondedReputationRepository;
private final DaoStateService daoStateService;
public BondedReputationApi(@Context Application application) {
ServiceNode serviceNode = ((DaoNodeRestApiApplication) application).getServiceNode();
daoStateService = serviceNode.getDaoStateService();
bondedReputationRepository = serviceNode.getBondedReputationRepository();
}
@Operation(description = "Request the bonded reputation data")
@ApiResponse(responseCode = "200", description = "The bonded reputation data",
content = {@Content(mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(allOf = BondedReputationDto.class))}
)
@GET
@Path("get-bonded-reputation/{block-height}")
public List<BondedReputationDto> getBondedReputation(@Parameter(description = DESC_BLOCK_HEIGHT)
@PathParam("block-height")
int fromBlockHeight) {
return bondedReputationRepository.getActiveBonds().stream()
.map(bondedReputation -> {
Optional<Tx> optionalTx = daoStateService.getTx(bondedReputation.getLockupTxId());
if (optionalTx.isEmpty()) {
return null;
}
Tx tx = optionalTx.get();
int blockHeight = tx.getBlockHeight();
if (blockHeight >= fromBlockHeight) {
return new BondedReputationDto(bondedReputation.getAmount(),
tx.getTime(),
Hex.encode(bondedReputation.getBondedAsset().getHash()),
blockHeight,
bondedReputation.getLockTime()
);
} else {
return null;
}
}).filter(Objects::nonNull)
.collect(Collectors.toList());
}
}

View File

@ -17,11 +17,10 @@
package bisq.daonode.endpoints;
import bisq.core.dao.state.model.blockchain.Tx;
import bisq.core.dao.governance.proofofburn.ProofOfBurnService;
import bisq.common.util.Hex;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@ -31,8 +30,8 @@ import static com.google.common.base.Preconditions.checkNotNull;
import bisq.daonode.DaoNode;
import bisq.daonode.DaoNodeRestApiApplication;
import bisq.daonode.ServiceNode;
import bisq.daonode.dto.ProofOfBurnDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@ -48,16 +47,21 @@ import jakarta.ws.rs.core.Application;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
/**
* Endpoint for getting the proof of burn data from a given block height.
* Used for reputation system in Bisq 2.
* <a href="http://localhost:8082/api/v1/proof-of-burn/get-proof-of-burn/0">Request with block height 0</a>
*/
@Slf4j
@Path("/proof-of-burn")
@Produces(MediaType.APPLICATION_JSON)
@Tag(name = "Proof of burn API")
public class ProofOfBurnApi {
private static final String DESC_BLOCK_HEIGHT = "The block height from which we request the proof of burn data";
private final DaoNode daoNode;
private final ServiceNode serviceNode;
public ProofOfBurnApi(@Context Application application) {
daoNode = ((DaoNodeRestApiApplication) application).getDaoNode();
serviceNode = ((DaoNodeRestApiApplication) application).getServiceNode();
}
@Operation(description = "Request the proof of burn data")
@ -69,23 +73,13 @@ public class ProofOfBurnApi {
@Path("get-proof-of-burn/{block-height}")
public List<ProofOfBurnDto> getProofOfBurn(@Parameter(description = DESC_BLOCK_HEIGHT)
@PathParam("block-height")
int blockHeight) {
return checkNotNull(daoNode.getDaoStateService()).getProofOfBurnTxs().stream()
.filter(tx -> tx.getBlockHeight() >= blockHeight)
.map(tx -> new ProofOfBurnDto(tx.getId(),
tx.getBurntBsq(),
tx.getBlockHeight(),
int fromBlockHeight) {
return checkNotNull(serviceNode.getDaoStateService()).getProofOfBurnTxs().stream()
.filter(tx -> tx.getBlockHeight() >= fromBlockHeight)
.map(tx -> new ProofOfBurnDto(tx.getBurntBsq(),
tx.getTime(),
getHash(tx)))
Hex.encode(ProofOfBurnService.getHashFromOpReturnData(tx)),
tx.getBlockHeight()))
.collect(Collectors.toList());
}
// We strip out the version bytes
private String getHash(Tx tx) {
byte[] opReturnData = tx.getLastTxOutput().getOpReturnData();
if (opReturnData == null) {
return "";
}
return Hex.encode(Arrays.copyOfRange(opReturnData, 2, 22));
}
}

View File

@ -0,0 +1,77 @@
/*
* 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.daonode.endpoints;
import bisq.core.account.witness.AccountAgeWitnessService;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkNotNull;
import bisq.daonode.DaoNodeRestApiApplication;
import bisq.daonode.ServiceNode;
import bisq.daonode.dto.ProofOfBurnDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Application;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
/**
* Endpoint for getting the signed witness date from a given hash as hex string.
* Used for reputation system in Bisq 2.
* <a href="http://localhost:8082/api/v1/signed-witness/get-date/dd75a7175c7c83fe9a4729e36b85f5fbc44e29ae">Request with hash</a>
*/
@Slf4j
@Path("/signed-witness")
@Produces(MediaType.TEXT_PLAIN)
@Tag(name = "Signed witness API")
public class SignedWitnessApi {
private static final String DESC_HASH = "The hash of the signed account age witness as hex string";
private final ServiceNode serviceNode;
public SignedWitnessApi(@Context Application application) {
serviceNode = ((DaoNodeRestApiApplication) application).getServiceNode();
}
@Operation(description = "Request the signed witness date")
@ApiResponse(responseCode = "200", description = "The signed witness date",
content = {@Content(mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(allOf = ProofOfBurnDto.class))}
)
@GET
@Path("get-date/{hash}")
public Long getDate(@Parameter(description = DESC_HASH)
@PathParam("hash")
String hash) {
AccountAgeWitnessService accountAgeWitnessService = checkNotNull(serviceNode.getAccountAgeWitnessService());
return accountAgeWitnessService.getWitnessByHashAsHex(hash)
.map(accountAgeWitnessService::getWitnessSignDate)
.orElse(-1L);
}
}

View File

@ -18,6 +18,7 @@
package bisq.desktop.main.account.content.fiataccounts;
import bisq.desktop.common.view.FxmlView;
import bisq.desktop.components.AutoTooltipButton;
import bisq.desktop.components.TitledGroupBg;
import bisq.desktop.components.paymentmethods.AchTransferForm;
import bisq.desktop.components.paymentmethods.AdvancedCashForm;
@ -100,6 +101,7 @@ import bisq.desktop.util.validation.UpholdValidator;
import bisq.desktop.util.validation.WeChatPayValidator;
import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.account.witness.AccountAgeWitnessUtils;
import bisq.core.locale.Res;
import bisq.core.offer.OfferRestrictions;
import bisq.core.payment.AmazonGiftCardAccount;
@ -120,6 +122,8 @@ import bisq.core.util.FormattingUtils;
import bisq.core.util.coin.CoinFormatter;
import bisq.common.config.Config;
import bisq.common.crypto.KeyRing;
import bisq.common.util.Hex;
import bisq.common.util.Tuple2;
import bisq.common.util.Tuple3;
import bisq.common.util.Utilities;
@ -135,7 +139,10 @@ import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.input.Clipboard;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.collections.FXCollections;
@ -157,6 +164,7 @@ public class FiatAccountsView extends PaymentAccountsView<GridPane, FiatAccounts
private final BICValidator bicValidator;
private final CapitualValidator capitualValidator;
private final LengthValidator inputValidator;
private final KeyRing keyRing;
private final UpholdValidator upholdValidator;
private final MoneyBeamValidator moneyBeamValidator;
private final PopmoneyValidator popmoneyValidator;
@ -208,12 +216,14 @@ public class FiatAccountsView extends PaymentAccountsView<GridPane, FiatAccounts
AdvancedCashValidator advancedCashValidator,
TransferwiseValidator transferwiseValidator,
AccountAgeWitnessService accountAgeWitnessService,
KeyRing keyRing,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter) {
super(model, accountAgeWitnessService);
this.bicValidator = bicValidator;
this.capitualValidator = capitualValidator;
this.inputValidator = inputValidator;
this.keyRing = keyRing;
this.inputValidator.setMaxLength(100); // restrict general field entry length
this.inputValidator.setMinLength(2);
this.upholdValidator = upholdValidator;
@ -296,9 +306,9 @@ public class FiatAccountsView extends PaymentAccountsView<GridPane, FiatAccounts
}
new Popup().information(Res.get(limitsInfoKey,
initialLimit,
formatter.formatCoinWithCode(maxTradeLimitSecondMonth),
formatter.formatCoinWithCode(maxTradeLimitAsCoin)))
initialLimit,
formatter.formatCoinWithCode(maxTradeLimitSecondMonth),
formatter.formatCoinWithCode(maxTradeLimitAsCoin)))
.width(700)
.closeButtonText(Res.get("shared.cancel"))
.actionButtonText(Res.get("shared.iUnderstand"))
@ -490,6 +500,7 @@ public class FiatAccountsView extends PaymentAccountsView<GridPane, FiatAccounts
Res.get("shared.deleteAccount"),
Res.get("shared.cancel")
);
Button updateButton = tuple.first;
updateButton.setOnAction(event -> onUpdateAccount(paymentMethodForm.getPaymentAccount()));
Button deleteAccountButton = tuple.second;
@ -497,10 +508,69 @@ public class FiatAccountsView extends PaymentAccountsView<GridPane, FiatAccounts
Button cancelButton = tuple.third;
cancelButton.setOnAction(event -> onCancelSelectedAccount(paymentMethodForm.getPaymentAccount()));
GridPane.setRowSpan(accountTitledGroupBg, paymentMethodForm.getRowSpan());
Button exportAccountAgeButton = new AutoTooltipButton(Res.get("account.fiat.exportAccountAge"));
exportAccountAgeButton.setOnAction(event -> onExportAccountAgeForBisq2(paymentMethodForm.getPaymentAccount()));
exportAccountAgeButton.setMaxWidth(Double.MAX_VALUE);
HBox.setHgrow(exportAccountAgeButton, Priority.ALWAYS);
Button signedWitnessButton = new AutoTooltipButton(Res.get("account.fiat.signedWitness"));
signedWitnessButton.setOnAction(event -> onExportSignedWitnessForBisq2(paymentMethodForm.getPaymentAccount()));
signedWitnessButton.setMaxWidth(Double.MAX_VALUE);
HBox.setHgrow(signedWitnessButton, Priority.ALWAYS);
HBox hBox = (HBox) cancelButton.getParent();
hBox.getChildren().addAll(exportAccountAgeButton, signedWitnessButton);
model.onSelectAccount(current);
}
}
private void onExportAccountAgeForBisq2(PaymentAccount paymentAccount) {
String prefix = "BISQ2_ACCOUNT_AGE:";
try {
String profileId = getProfileIdFromClipBoard(prefix);
AccountAgeWitnessUtils.signAccountAgeAndBisq2ProfileId(accountAgeWitnessService, paymentAccount, keyRing, profileId)
.ifPresent(json -> {
Utilities.copyToClipboard(prefix + json);
new Popup().information(Res.get("account.fiat.exportAccountAge.popup", json))
.width(900).show();
});
} catch (Exception e) {
String error = e.getCause() != null ? e.getCause().getMessage() : e.getMessage();
new Popup().warning(error).show();
}
}
private void onExportSignedWitnessForBisq2(PaymentAccount paymentAccount) {
String prefix = "BISQ2_SIGNED_WITNESS:";
try {
String profileId = getProfileIdFromClipBoard(prefix);
AccountAgeWitnessUtils.signSignedWitnessAndBisq2ProfileId(accountAgeWitnessService, paymentAccount, keyRing, profileId)
.ifPresent(json -> {
Utilities.copyToClipboard(prefix + json);
new Popup().information(Res.get("account.fiat.signedWitness.popup", json))
.width(900).show();
});
} catch (Exception e) {
String error = e.getCause() != null ? e.getCause().getMessage() : e.getMessage();
new Popup().warning(error).show();
}
}
private String getProfileIdFromClipBoard(String prefix) {
String clipboardText = Clipboard.getSystemClipboard().getString();
if (clipboardText != null && clipboardText.startsWith(prefix)) {
String profileId = clipboardText.replace(prefix, "");
if (profileId.length() == 40) {
Hex.decode(profileId);
return profileId;
}
}
throw new RuntimeException("Clipboard text not in expected format. " + clipboardText);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Utils

View File

@ -58,7 +58,7 @@ class MyProofOfBurnListItem {
amount = proofOfBurnService.getAmount(tx);
amountAsString = bsqFormatter.formatCoinWithCode(Coin.valueOf(amount));
txId = tx.getId();
hashAsHex = Utilities.bytesAsHexString(proofOfBurnService.getHashFromOpReturnData(tx));
hashAsHex = Utilities.bytesAsHexString(ProofOfBurnService.getHashFromOpReturnData(tx));
pubKey = Utilities.bytesAsHexString(proofOfBurnService.getPubKey(txId));
} else {
amount = 0;

View File

@ -45,7 +45,7 @@ class ProofOfBurnListItem {
amount = proofOfBurnService.getAmount(tx);
amountAsString = bsqFormatter.formatCoinWithCode(Coin.valueOf(amount));
txId = tx.getId();
hashAsHex = Utilities.bytesAsHexString(proofOfBurnService.getHashFromOpReturnData(tx));
hashAsHex = Utilities.bytesAsHexString(ProofOfBurnService.getHashFromOpReturnData(tx));
pubKey = Utilities.bytesAsHexString(proofOfBurnService.getPubKey(txId));
date = new Date(tx.getTime());
dateAsString = DisplayUtils.formatDateTime(date);

View File

@ -843,7 +843,9 @@ public class GUIUtil {
return true;
}
public static boolean canCreateOrTakeOfferOrShowPopup(User user, Navigation navigation, @Nullable TradeCurrency currency) {
public static boolean canCreateOrTakeOfferOrShowPopup(User user,
Navigation navigation,
@Nullable TradeCurrency currency) {
if (currency != null && currency.getCode().equals("BSQ")) {
return true;
}