package lighthouse.subwindows; import com.google.common.util.concurrent.*; import javafx.application.*; import javafx.event.*; import javafx.fxml.*; import javafx.scene.control.*; import lighthouse.*; import lighthouse.protocol.*; import lighthouse.wallet.*; import org.bitcoinj.core.*; import org.slf4j.*; import org.spongycastle.crypto.params.*; import javax.annotation.*; import java.util.*; import static com.google.common.base.Preconditions.*; import static lighthouse.utils.GuiUtils.*; import static lighthouse.utils.I18nUtil.*; /** * Tells the user there's a fee to pay and shows a progress bar that tracks network propagation. Possibly request the * users password first. This is used for both revocation and claiming the contract. */ public class RevokeAndClaimWindow { private static final Logger log = LoggerFactory.getLogger(RevokeAndClaimWindow.class); // Either this ... public LHProtos.Pledge pledgeToRevoke; // Or these ... public Project projectToClaim; public Set<LHProtos.Pledge> pledgesToClaim; // ... are set. pledgesToClaim is IGNORED in server assisted projects however because one of the pledges might have // been revoked whilst the user was sitting on the claim screen. So we always refetch the status and recheck just // before doing the claim. public Runnable onSuccess; public Main.OverlayUI overlayUI; public Button cancelBtn; public Button confirmBtn; public Label explanationLabel; @FXML ProgressBar progressBar; public static Main.OverlayUI<RevokeAndClaimWindow> openForRevoke(LHProtos.Pledge pledgeToRevoke) { Main.OverlayUI<RevokeAndClaimWindow> overlay = Main.instance.overlayUI("subwindows/revoke_and_claim.fxml", tr("Revoke pledge")); overlay.controller.setForRevoke(pledgeToRevoke); return overlay; } public static Main.OverlayUI<RevokeAndClaimWindow> openForClaim(Project project, Set<LHProtos.Pledge> pledgesToClaim) { Main.OverlayUI<RevokeAndClaimWindow> overlay = Main.instance.overlayUI("subwindows/revoke_and_claim.fxml", tr("Claim pledges")); overlay.controller.setForClaim(project, pledgesToClaim); return overlay; } private void setForClaim(Project project, Set<LHProtos.Pledge> claim) { projectToClaim = project; pledgesToClaim = claim; explanationLabel.setText(tr("Claiming a project sends all the pledged money to the project's goal address. ") + explanationLabel.getText()); } private void setForRevoke(LHProtos.Pledge revoke) { pledgeToRevoke = revoke; explanationLabel.setText(tr("Revoking a pledge returns the money to your wallet. ") + explanationLabel.getText()); } @FXML public void confirmClicked(ActionEvent event) { // runLater: shitty hack around RT-37821 (consider upgrading to 8u40 when available and/or applying fix locally) // otherwise pressing enter can cause a crash here when we open a new window with a default button Platform.runLater(this::confirmClicked); } private void confirmClicked() { if (Main.wallet.isEncrypted()) { log.info("Wallet is encrypted, requesting password"); WalletPasswordController.requestPasswordWithNextWindow(key -> { Main.OverlayUI<RevokeAndClaimWindow> screen; if (projectToClaim != null) { screen = openForClaim(projectToClaim, pledgesToClaim); } else { screen = openForRevoke(pledgeToRevoke); } screen.controller.onSuccess = onSuccess; screen.controller.go(key); }); } else { go(null); } } private void go(@Nullable KeyParameter aesKey) { confirmBtn.setDisable(true); cancelBtn.setDisable(true); if (pledgeToRevoke != null) { revoke(aesKey); } else { checkState(projectToClaim != null); claim(aesKey); } } private void claim(@Nullable KeyParameter key) { if (projectToClaim.getPaymentURL() != null) { claimServerAssisted(key); } else { log.info("Attempting to claim serverless project, proceeding to merge and broadcast"); broadcastClaim(pledgesToClaim, key); } } private void claimServerAssisted(@Nullable KeyParameter key) { // Need to refresh here because we're polling the server and might be lagging behind the true state. // This is kind of a hack. Better solutions would be: // // 1) Check the pledges returned ourselves against p2p network using getutxo: lowers trust in the server // 2) Have server stream updates to client so we are never more than a second or two behind, instead of // a block interval like today. log.info("Attempting to claim server assisted project, refreshing"); progressBar.setProgress(-1); Main.backend.refreshProjectStatusFromServer(projectToClaim, key).handleAsync((status, ex) -> { // On backend thread. if (ex != null) { log.error("Unable to fetch project status", ex); informationalAlert(tr("Unable to claim"), // TRANS: %s = error message tr("Could not fetch project status from server: %s"), ex); overlayUI.done(); } else { HashSet<LHProtos.Pledge> newPledges = new HashSet<>(status.getPledgesList()); if (status.getValuePledgedSoFar() < projectToClaim.getGoalAmount().value) { log.error("Refreshed project status indicates value has changed, is now {}", status.getValuePledgedSoFar()); informationalAlert(tr("Unable to claim"), tr("One or more pledges have been revoked whilst you were waiting.")); overlayUI.done(); } else { // Must use newPledges here because a pledge might have been revoked and replaced in the // waiting interval. broadcastClaim(newPledges, key); } } return null; }, Platform::runLater); } private void broadcastClaim(Set<LHProtos.Pledge> pledges, @Nullable KeyParameter key) { try { Main.wallet.completeContractWithFee(projectToClaim, pledges, key, // Progress (val) -> { progressBar.setProgress(val); if (val >= 1.0) overlayUI.done(); }, // Error (ex) -> { overlayUI.done(); informationalAlert(tr("Transaction acceptance issue"), // TRANS: %s = error message tr("The Bitcoin network has rejected the claim: %s"), ex); }, Platform::runLater); } catch (Ex.ValueMismatch e) { // TODO: Solve value mismatch errors. We have a few options. // 1) Try taking away pledges to see if we can get precisely to the target value, e.g. this can // help if everyone agrees up front to pledge 1 BTC exactly, and the goal is 10, but nobody // knows how many people will pledge so we might end up with 11 or 12 BTC. In this situation // we can just randomly drop pledges until we get to the right amount (or allow the user to choose). // 2) Find a way to extend the Bitcoin protocol so the additional money can be allocated to the // project owner and not miners. For instance by allowing new SIGHASH modes that control which // parts of which outputs are signed. This would require a Script 2.0 effort though. // // This should never happen in server assisted mode. log.error("Value mismatch: " + e); informationalAlert(tr("Too much money"), // TRANS: %s = difference in BTC tr("You have gathered pledges that add up to more than the goal. The excess cannot be " + "redeemed in the current version of the software and would end up being paid completely " + "to miners fees. Please remove some pledges and try to hit the goal amount exactly. " + "There is %s too much."), Coin.valueOf(e.byAmount).toFriendlyString()); overlayUI.done(); } catch (InsufficientMoneyException e) { log.error("Insufficient money to claim", e); informationalAlert(tr("Cannot claim pledges"), tr("Closing the contract requires paying Bitcoin network fees, but you don't have enough " + "money in the wallet. Add more money and try again.") ); overlayUI.done(); } } private void revoke(@Nullable KeyParameter aesKey) { try { PledgingWallet.Revocation revocation = Main.wallet.revokePledge(pledgeToRevoke, aesKey); progressBar.setProgress(-1); revocation.broadcast.setProgressCallback(progressBar::setProgress, Platform::runLater); Futures.addCallback(revocation.broadcast.future(), new FutureCallback<Transaction>() { @Override public void onSuccess(@Nullable Transaction result) { onSuccess.run(); overlayUI.done(); } @Override public void onFailure(Throwable t) { informationalAlert(tr("Transaction acceptance issue"), // TRANS: %s = error message tr("The Bitcoin network has rejected the revocation transaction: %s"), t); log.error("Revocation failed", t); overlayUI.done(); } }, Platform::runLater); } catch (InsufficientMoneyException e) { // This really sucks. In future we should make it a free tx, when we know if we have sufficient // priority to meet the relay rules. log.error("Could not revoke due to insufficient money to pay the fee", e); informationalAlert(tr("Cannot revoke pledge"), tr("Revoking a pledge requires making another Bitcoin transaction on the block chain, but " + "you don't have sufficient funds to pay the required fee. Add more money and try again.") ); } } @FXML public void cancelClicked(ActionEvent event) { overlayUI.done(); } }