package lighthouse.model;
import com.google.protobuf.*;
import javafx.beans.*;
import javafx.beans.property.*;
import lighthouse.protocol.*;
import lighthouse.wallet.*;
import org.bitcoinj.core.*;
import org.bitcoinj.script.*;
import javax.annotation.*;
import static lighthouse.protocol.LHUtils.*;
import static lighthouse.utils.I18nUtil.*;
/**
* This class wraps a LHProtos.Project and exposes JFX properties for things that users are interested in. It performs
* some validation and links the properties to a protobuf builder.
*/
public class ProjectModel {
public final StringProperty title = new SimpleStringProperty();
public final StringProperty email = new SimpleStringProperty();
public final StringProperty memo = new SimpleStringProperty();
public final StringProperty serverName = new SimpleStringProperty();
public final StringProperty address = new SimpleStringProperty();
// Value in satoshis.
public final LongProperty goalAmount = new SimpleLongProperty();
public final ObjectProperty<ByteString> image = new SimpleObjectProperty<>();
private final LongProperty minPledgeAmount = new SimpleLongProperty();
private LHProtos.ProjectDetails.Builder proto;
// Pointer to the original Project object that this model is based on, if editing.
@Nullable public final Project originalProject;
public static final int ESTIMATED_INPUT_SIZE = Script.SIG_SIZE + 35 /* bytes for a compressed pubkey */ + 32 /* hash */ + 4;
public static final int MAX_NUM_INPUTS = (Transaction.MAX_STANDARD_TX_SIZE - 64) /* for output */ / ESTIMATED_INPUT_SIZE;
public ProjectModel(PledgingWallet wallet) {
this(Project.makeDetails(wallet.getParams(), "", "", wallet.freshReceiveAddress(), Coin.SATOSHI, wallet.freshAuthKey(),
wallet.getKeyChainGroupLookaheadSize()));
}
public ProjectModel(Project editing) {
this(editing.getProtoDetails().toBuilder(), editing);
}
public ProjectModel(LHProtos.ProjectDetails.Builder liveProto) {
this(liveProto, null);
}
public ProjectModel(LHProtos.ProjectDetails.Builder liveProto, @Nullable Project editing) {
this.proto = liveProto;
this.originalProject = editing;
final LHProtos.Project.Builder wrapper = LHProtos.Project.newBuilder().setSerializedPaymentDetails(liveProto.build().toByteString());
Project project = unchecked(() -> new Project(wrapper.build()));
title.set(project.getTitle());
memo.set(project.getMemo());
goalAmount.set(project.getGoalAmount().value);
if (liveProto.getExtraDetails().hasMinPledgeSize())
minPledgeAmount.set(project.getMinPledgeAmount().value);
else
minPledgeAmount.set(recalculateMinPledgeAmount(goalAmount.longValue()));
if (liveProto.hasPaymentUrl()) {
String host = LHUtils.validateServerPath(liveProto.getPaymentUrl());
if (host == null)
// TRANS: %s = payment URL
throw new IllegalArgumentException(String.format(tr("Server path not valid for Lighthouse protocol: %s"), liveProto.getPaymentUrl()));
serverName.set(host);
}
// Connect the properties.
InvalidationListener pathSetter = o -> {
final String name = serverName.get();
if (name == null || name.isEmpty())
proto.clearPaymentUrl();
else
proto.setPaymentUrl(LHUtils.makeServerPath(name, LHUtils.titleToUrlString(title.get())));
};
serverName.addListener(pathSetter);
title.addListener(o -> {
proto.getExtraDetailsBuilder().setTitle(title.get());
pathSetter.invalidated(null);
});
email.set(project.getEmail());
email.addListener(o -> proto.getExtraDetailsBuilder().setEmail(email.get()));
memo.addListener(o -> proto.setMemo(memo.get()));
// Just adjust the first output. GUI doesn't handle multioutput contracts right now (they're useless anyway).
goalAmount.addListener(o -> {
long value = goalAmount.longValue();
minPledgeAmount.set(recalculateMinPledgeAmount(value));
proto.getOutputsBuilder(0).setAmount(value);
});
minPledgeAmount.addListener(o -> proto.getExtraDetailsBuilder().setMinPledgeSize(minPledgeAmountProperty().get()));
TransactionOutput output = project.getOutputs().get(0);
Address addr = output.getAddressFromP2PKHScript(project.getParams());
if (addr == null)
addr = output.getAddressFromP2SH(project.getParams());
if (addr == null)
// TRANS: %s = transaction output
throw new IllegalArgumentException(String.format(tr("Output type is not pay to address/p2sh: %s"), output));
address.set(addr.toString());
address.addListener(o -> {
try {
Address addr2 = new Address(project.getParams(), address.get());
proto.getOutputsBuilder(0).setScript(ByteString.copyFrom(ScriptBuilder.createOutputScript(addr2).getProgram()));
} catch (AddressFormatException e) {
// Ignored: wait for the user to make it valid.
}
});
if (proto.getExtraDetailsBuilder().hasCoverImage())
image.set(proto.getExtraDetailsBuilder().getCoverImage());
image.addListener(o -> {
proto.getExtraDetailsBuilder().setCoverImage(image.get());
});
}
private long recalculateMinPledgeAmount(long value) {
// This isn't a perfect estimation by any means especially as we allow P2SH outputs to be pledged, which
// could end up gobbling up a lot of space in the contract, but it'll do for now. How many pledges can we
// have assuming Lighthouse makes them all i.e. pay to address?
return Math.max(value / MAX_NUM_INPUTS, Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.multiply(4).value);
}
public LHProtos.Project.Builder getProto() {
LHProtos.Project.Builder proto = LHProtos.Project.newBuilder();
proto.setSerializedPaymentDetails(getDetailsProto().build().toByteString());
return proto;
}
public Project getProject() {
return unchecked(() -> new Project(getProto().build()));
}
public LHProtos.ProjectDetails.Builder getDetailsProto() {
return proto;
}
public Coin getMinPledgeAmount() {
return Coin.valueOf(minPledgeAmount.get());
}
public void setMinPledgeAmount(Coin value) { minPledgeAmount.setValue(value.value);}
public void resetMinPledgeAmount() {
minPledgeAmount.set(recalculateMinPledgeAmount(goalAmount.longValue()));
}
public ReadOnlyLongProperty minPledgeAmountProperty() {
return minPledgeAmount;
}
}