package network.thunder.core.communication.objects.messages.impl;
import network.thunder.core.communication.objects.lightning.subobjects.PaymentData;
import network.thunder.core.communication.objects.messages.impl.message.lnpayment.PeeledOnion;
import network.thunder.core.communication.objects.messages.interfaces.factories.ContextFactory;
import network.thunder.core.communication.objects.messages.interfaces.helper.LNEventHelper;
import network.thunder.core.communication.objects.messages.interfaces.helper.LNOnionHelper;
import network.thunder.core.communication.objects.messages.interfaces.helper.LNPaymentHelper;
import network.thunder.core.communication.objects.subobjects.PaymentSecret;
import network.thunder.core.communication.processor.exceptions.LNPaymentException;
import network.thunder.core.communication.processor.interfaces.lnpayment.LNPaymentProcessor;
import network.thunder.core.database.DBHandler;
import network.thunder.core.etc.Tools;
import network.thunder.core.mesh.LNConfiguration;
import network.thunder.core.mesh.NodeServer;
import org.bitcoinj.core.ECKey;
import java.util.ArrayList;
import java.util.List;
/**
* Created by matsjerratsch on 02/02/2016.
*/
public class LNPaymentHelperImpl implements LNPaymentHelper {
LNOnionHelper onionHelper;
DBHandler dbHandler;
LNEventHelper eventHelper;
NodeServer nodeServer;
LNConfiguration configuration;
List<LNPaymentProcessor> processorList = new ArrayList<>();
public LNPaymentHelperImpl (ContextFactory contextFactory, DBHandler dbHandler) {
this.onionHelper = contextFactory.getOnionHelper();
this.dbHandler = dbHandler;
this.eventHelper = contextFactory.getEventHelper();
this.nodeServer = contextFactory.getServerSettings();
configuration = nodeServer.configuration;
}
@Override
public void addProcessor (LNPaymentProcessor processor) {
processorList.add(processor);
}
@Override
public void removeProcessor (LNPaymentProcessor processor) {
processorList.remove(processor);
}
@Override
public synchronized void relayPayment (LNPaymentProcessor processorSent, PaymentData paymentData) {
try {
PeeledOnion peeledOnion = getPeeledOnion(paymentData);
saveReceiverToDatabase(paymentData, peeledOnion);
if (peeledOnion.isLastHop) {
PaymentSecret secret = dbHandler.getPaymentSecret(paymentData.secret);
if (secret == null) {
System.out.println("Can't redeem payment - refund!");
processorSent.refundPayment(paymentData);
} else {
System.out.println("Received money!");
eventHelper.onPaymentCompleted(secret);
processorSent.redeemPayment(secret);
}
} else {
paymentData.onionObject = peeledOnion.onionObject;
ECKey nextHop = peeledOnion.nextHop;
if (!relayPaymentToCorrectProcessor(paymentData, nextHop)) {
//TODO Can't connect the payment right now. Check the DB if we even have a channel with
// the next node, refund the payment back if we don't...
System.out.println("Currently not connected with " + Tools.bytesToHex(nextHop.getPubKey()) + ". Refund...");
processorSent.refundPayment(paymentData);
}
}
} catch (Exception e) {
e.printStackTrace();
processorSent.refundPayment(paymentData);
}
}
@Override
public void makePayment (PaymentData paymentData) {
try {
PeeledOnion peeledOnion = getPeeledOnion(paymentData);
saveReceiverToDatabase(paymentData, peeledOnion);
paymentData.onionObject = peeledOnion.onionObject;
ECKey nextHop = peeledOnion.nextHop;
if (!relayPaymentToCorrectProcessor(paymentData, nextHop)) {
throw new LNPaymentException("Not connected to next hop " + nextHop);
}
} catch (Exception e) {
e.printStackTrace();
throw new LNPaymentException(e);
}
}
private boolean relayPaymentToCorrectProcessor (PaymentData paymentData, ECKey nextHop) {
System.out.println("Next Hop: " + nextHop);
for (LNPaymentProcessor processor : processorList) {
if (processor.connectsToNodeId(nextHop.getPubKey())) {
PaymentData copy = paymentData.cloneObject();
copy.sending = true;
copy.timestampOpen = Tools.currentTime();
copy.timestampRefund -= configuration.getTimeToReduceWhenRelayingPayment();
//Last check to see if there is sufficient refund time left..
if ((copy.timestampRefund - Tools.currentTime()) < (configuration.MIN_OVERLAY_REFUND * configuration.MIN_REFUND_DELAY)) {
System.out.println("Not sufficient refund time left - refund!");
return false;
}
processor.makePayment(copy);
return true;
}
}
return false;
}
private PeeledOnion getPeeledOnion (PaymentData paymentData) {
return onionHelper.loadMessage(nodeServer.pubKeyServer, paymentData.onionObject);
}
private void saveReceiverToDatabase (PaymentData payment, PeeledOnion peeledOnion) {
if (peeledOnion.isLastHop) {
dbHandler.updatePaymentAddReceiverAddress(payment.secret, new byte[0]);
} else {
dbHandler.updatePaymentAddReceiverAddress(payment.secret, peeledOnion.nextHop.getPubKey());
}
}
@Override
public void paymentRedeemed (PaymentSecret paymentSecret) {
System.out.println("Payment redeemed: " + paymentSecret);
byte[] sender = dbHandler.getSenderOfPayment(paymentSecret);
if (isEmptyByte(sender)) {
System.out.println("Payment was redeemed: " + paymentSecret);
eventHelper.onPaymentCompleted(paymentSecret);
return;
} else {
System.out.println("Sender of payment: " + Tools.bytesToHex(sender));
for (LNPaymentProcessor processor : processorList) {
if (processor.connectsToNodeId(sender)) {
processor.redeemPayment(paymentSecret);
return;
}
}
System.out.println("Aren't connected to redeem payment..");
//TODO sender of payment is offline right now - have to close channel if he does not come back online in time..
//TODO redeem payment from other party on blockchain if channel is closed already..
}
}
@Override
public void paymentRefunded (PaymentData paymentData) {
byte[] sender = dbHandler.getSenderOfPayment(paymentData.secret);
if (sender == null) {
System.out.println("Can't find sender when trying to refund..?");
//TODO ??? - this should not happen - but can't really resolve it either..
}
for (LNPaymentProcessor processor : processorList) {
if (processor.connectsToNodeId(sender)) {
System.out.println("refundPayment...");
processor.refundPayment(paymentData);
return;
}
}
//TODO sender of payment is offline right now - he will close the channel if we can't get back to him in time..
}
private static boolean isEmptyByte (byte[] bytes) {
return bytes.length == 0;
}
}