导航菜单

页面标题

页面副标题

imToken v3.28.8 - PaymentChannelClient.java 源代码

正在查看: imToken v3.28.8 应用的 PaymentChannelClient.java JAVA 源代码文件

本页面展示 JAVA 反编译生成的源代码文件,支持语法高亮显示。 仅供安全研究与技术分析使用,严禁用于任何非法用途。请遵守相关法律法规。


package org.bitcoinj.protocols.channels;

import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import com.google.protobuf.ByteString;
import java.util.concurrent.locks.ReentrantLock;
import javax.annotation.Nullable;
import net.jcip.annotations.GuardedBy;
import org.bitcoin.paymentchannel.Protos;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.InsufficientMoneyException;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.Utils;
import org.bitcoinj.core.VerificationException;
import org.bitcoinj.protocols.channels.IPaymentChannelClient;
import org.bitcoinj.protocols.channels.PaymentChannelClientState;
import org.bitcoinj.protocols.channels.PaymentChannelCloseException;
import org.bitcoinj.utils.Threading;
import org.bitcoinj.wallet.Wallet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.params.KeyParameter;

public class PaymentChannelClient implements IPaymentChannelClient {
    public static final long DEFAULT_TIME_WINDOW = 86340;
    private static final Logger log = LoggerFactory.getLogger(PaymentChannelClient.class);

    @GuardedBy("lock")
    private final IPaymentChannelClient.ClientConnection conn;

    @GuardedBy("lock")
    boolean connectionOpen;

    @GuardedBy("lock")
    SettableFuture<PaymentIncrementAck> increasePaymentFuture;

    @GuardedBy("lock")
    Coin lastPaymentActualAmount;
    protected final ReentrantLock lock;

    @GuardedBy("lock")
    private int majorVersion;
    private final Coin maxValue;

    @GuardedBy("lock")
    private long minPayment;
    private Coin missing;
    private final ECKey myKey;
    private final Sha256Hash serverId;

    @GuardedBy("lock")
    private PaymentChannelClientState state;

    @GuardedBy("lock")
    private InitStep step;
    private StoredClientChannel storedChannel;
    private final long timeWindow;
    private KeyParameter userKeySetup;
    private final VersionSelector versionSelector;
    private final Wallet wallet;

    private enum InitStep {
        WAITING_FOR_CONNECTION_OPEN,
        WAITING_FOR_VERSION_NEGOTIATION,
        WAITING_FOR_INITIATE,
        WAITING_FOR_REFUND_RETURN,
        WAITING_FOR_CHANNEL_OPEN,
        CHANNEL_OPEN,
        WAITING_FOR_CHANNEL_CLOSE,
        CHANNEL_CLOSED
    }

    public enum VersionSelector {
        VERSION_1,
        VERSION_2_ALLOW_1,
        VERSION_2;

        public int getRequestedMinorVersion() {
            return 0;
        }

        public int getRequestedMajorVersion() {
            return AnonymousClass2.$SwitchMap$org$bitcoinj$protocols$channels$PaymentChannelClient$VersionSelector[ordinal()] != 1 ? 2 : 1;
        }

        public boolean isServerVersionAccepted(int i, int i2) {
            int i3 = AnonymousClass2.$SwitchMap$org$bitcoinj$protocols$channels$PaymentChannelClient$VersionSelector[ordinal()];
            return i3 != 1 ? i3 != 2 ? i3 == 3 && i == 2 : i == 1 || i == 2 : i == 1;
        }
    }

    public PaymentChannelClient(Wallet wallet, ECKey eCKey, Coin coin, Sha256Hash sha256Hash, IPaymentChannelClient.ClientConnection clientConnection) {
        this(wallet, eCKey, coin, sha256Hash, clientConnection, VersionSelector.VERSION_2_ALLOW_1);
    }

    public PaymentChannelClient(Wallet wallet, ECKey eCKey, Coin coin, Sha256Hash sha256Hash, IPaymentChannelClient.ClientConnection clientConnection, VersionSelector versionSelector) {
        this(wallet, eCKey, coin, sha256Hash, DEFAULT_TIME_WINDOW, null, clientConnection, versionSelector);
    }

    public PaymentChannelClient(Wallet wallet, ECKey eCKey, Coin coin, Sha256Hash sha256Hash, long j, @Nullable KeyParameter keyParameter, IPaymentChannelClient.ClientConnection clientConnection) {
        this(wallet, eCKey, coin, sha256Hash, j, keyParameter, clientConnection, VersionSelector.VERSION_2_ALLOW_1);
    }

    public PaymentChannelClient(Wallet wallet, ECKey eCKey, Coin coin, Sha256Hash sha256Hash, long j, @Nullable KeyParameter keyParameter, IPaymentChannelClient.ClientConnection clientConnection, VersionSelector versionSelector) {
        this.lock = Threading.lock("channelclient");
        this.connectionOpen = false;
        this.step = InitStep.WAITING_FOR_CONNECTION_OPEN;
        this.wallet = (Wallet) Preconditions.checkNotNull(wallet);
        this.myKey = (ECKey) Preconditions.checkNotNull(eCKey);
        this.maxValue = (Coin) Preconditions.checkNotNull(coin);
        this.serverId = (Sha256Hash) Preconditions.checkNotNull(sha256Hash);
        Preconditions.checkState(j >= 0);
        this.timeWindow = j;
        this.conn = (IPaymentChannelClient.ClientConnection) Preconditions.checkNotNull(clientConnection);
        this.userKeySetup = keyParameter;
        this.versionSelector = versionSelector;
    }

    public Coin getMissing() {
        return this.missing;
    }

    @GuardedBy("lock")
    @Nullable
    private PaymentChannelCloseException.CloseReason receiveInitiate(Protos.Initiate initiate, Coin coin, Protos.Error.Builder builder) throws VerificationException, InsufficientMoneyException, ECKey.KeyIsEncryptedException {
        log.info("Got INITIATE message:\n{}", initiate.toString());
        if (this.wallet.isEncrypted() && this.userKeySetup == null) {
            throw new ECKey.KeyIsEncryptedException();
        }
        long expireTimeSecs = initiate.getExpireTimeSecs();
        Preconditions.checkState(expireTimeSecs >= 0 && initiate.getMinAcceptedChannelSize() >= 0);
        if (!this.conn.acceptExpireTime(expireTimeSecs)) {
            log.error("Server suggested expire time was out of our allowed bounds: {} ({} s)", Utils.dateTimeFormat(1000 * expireTimeSecs), Long.valueOf(expireTimeSecs));
            builder.setCode(Protos.Error.ErrorCode.TIME_WINDOW_UNACCEPTABLE);
            return PaymentChannelCloseException.CloseReason.TIME_WINDOW_UNACCEPTABLE;
        }
        Coin valueOf = Coin.valueOf(initiate.getMinAcceptedChannelSize());
        if (coin.compareTo(valueOf) < 0) {
            log.error("Server requested too much value");
            builder.setCode(Protos.Error.ErrorCode.CHANNEL_VALUE_TOO_LARGE);
            this.missing = valueOf.subtract(coin);
            return PaymentChannelCloseException.CloseReason.SERVER_REQUESTED_TOO_MUCH_VALUE;
        }
        long j = Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.value;
        if (initiate.getMinPayment() != j) {
            log.error("Server requested a min payment of {} but we expected {}", Long.valueOf(initiate.getMinPayment()), Long.valueOf(j));
            builder.setCode(Protos.Error.ErrorCode.MIN_PAYMENT_TOO_LARGE);
            builder.setExpectedValue(j);
            this.missing = Coin.valueOf(initiate.getMinPayment() - j);
            return PaymentChannelCloseException.CloseReason.SERVER_REQUESTED_TOO_MUCH_VALUE;
        }
        byte[] byteArray = initiate.getMultisigKey().toByteArray();
        if (!ECKey.isPubKeyCanonical(byteArray)) {
            throw new VerificationException("Server gave us a non-canonical public key, protocol error.");
        }
        int i = this.majorVersion;
        if (i == 1) {
            this.state = new PaymentChannelV1ClientState(this.wallet, this.myKey, ECKey.fromPublicOnly(byteArray), coin, expireTimeSecs);
        } else if (i == 2) {
            this.state = new PaymentChannelV2ClientState(this.wallet, this.myKey, ECKey.fromPublicOnly(byteArray), coin, expireTimeSecs);
        } else {
            return PaymentChannelCloseException.CloseReason.NO_ACCEPTABLE_VERSION;
        }
        try {
            this.state.initiate(this.userKeySetup);
            this.minPayment = initiate.getMinPayment();
            int i2 = this.majorVersion;
            if (i2 == 1) {
                this.step = InitStep.WAITING_FOR_REFUND_RETURN;
                this.conn.sendToServer(Protos.TwoWayChannelMessage.newBuilder().setProvideRefund(Protos.ProvideRefund.newBuilder().setMultisigKey(ByteString.copyFrom(this.myKey.getPubKey())).setTx(ByteString.copyFrom(((PaymentChannelV1ClientState) this.state).getIncompleteRefundTransaction().unsafeBitcoinSerialize()))).setType(Protos.TwoWayChannelMessage.MessageType.PROVIDE_REFUND).m1809build());
            } else if (i2 == 2) {
                this.step = InitStep.WAITING_FOR_CHANNEL_OPEN;
                this.state.storeChannelInWallet(this.serverId);
                Protos.ProvideContract.Builder clientKey = Protos.ProvideContract.newBuilder().setTx(ByteString.copyFrom(this.state.getContract().unsafeBitcoinSerialize())).setClientKey(ByteString.copyFrom(this.myKey.getPubKey()));
                try {
                    PaymentChannelClientState.IncrementedPayment incrementPaymentBy = state().incrementPaymentBy(Coin.valueOf(this.minPayment), this.userKeySetup);
                    Protos.UpdatePayment.Builder initialPaymentBuilder = clientKey.getInitialPaymentBuilder();
                    initialPaymentBuilder.setSignature(ByteString.copyFrom(incrementPaymentBy.signature.encodeToBitcoin()));
                    initialPaymentBuilder.setClientChangeValue(this.state.getValueRefunded().value);
                    this.userKeySetup = null;
                    Protos.TwoWayChannelMessage.Builder newBuilder = Protos.TwoWayChannelMessage.newBuilder();
                    newBuilder.setProvideContract(clientKey);
                    newBuilder.setType(Protos.TwoWayChannelMessage.MessageType.PROVIDE_CONTRACT);
                    this.conn.sendToServer(newBuilder.m1809build());
                } catch (ValueOutOfRangeException e) {
                    throw new IllegalStateException(e);
                }
            } else {
                return PaymentChannelCloseException.CloseReason.NO_ACCEPTABLE_VERSION;
            }
            return null;
        } catch (ValueOutOfRangeException e2) {
            log.error("Value out of range when trying to initiate", e2);
            builder.setCode(Protos.Error.ErrorCode.CHANNEL_VALUE_TOO_LARGE);
            return PaymentChannelCloseException.CloseReason.SERVER_REQUESTED_TOO_MUCH_VALUE;
        }
    }

    @GuardedBy("lock")
    private void receiveRefund(Protos.TwoWayChannelMessage twoWayChannelMessage, @Nullable KeyParameter keyParameter) throws VerificationException {
        boolean z = false;
        Preconditions.checkState(this.majorVersion == 1);
        if (this.step == InitStep.WAITING_FOR_REFUND_RETURN && twoWayChannelMessage.hasReturnRefund()) {
            z = true;
        }
        Preconditions.checkState(z);
        log.info("Got RETURN_REFUND message, providing signed contract");
        ((PaymentChannelV1ClientState) this.state).provideRefundSignature(twoWayChannelMessage.getReturnRefund().getSignature().toByteArray(), keyParameter);
        this.step = InitStep.WAITING_FOR_CHANNEL_OPEN;
        this.state.storeChannelInWallet(this.serverId);
        Protos.ProvideContract.Builder tx = Protos.ProvideContract.newBuilder().setTx(ByteString.copyFrom(this.state.getContract().unsafeBitcoinSerialize()));
        try {
            PaymentChannelClientState.IncrementedPayment incrementPaymentBy = state().incrementPaymentBy(Coin.valueOf(this.minPayment), keyParameter);
            Protos.UpdatePayment.Builder initialPaymentBuilder = tx.getInitialPaymentBuilder();
            initialPaymentBuilder.setSignature(ByteString.copyFrom(incrementPaymentBy.signature.encodeToBitcoin()));
            initialPaymentBuilder.setClientChangeValue(this.state.getValueRefunded().value);
            Protos.TwoWayChannelMessage.Builder newBuilder = Protos.TwoWayChannelMessage.newBuilder();
            newBuilder.setProvideContract(tx);
            newBuilder.setType(Protos.TwoWayChannelMessage.MessageType.PROVIDE_CONTRACT);
            this.conn.sendToServer(newBuilder.m1809build());
        } catch (ValueOutOfRangeException e) {
            throw new IllegalStateException(e);
        }
    }

    @GuardedBy("lock")
    private void receiveChannelOpen() throws VerificationException {
        boolean z = false;
        Preconditions.checkState(this.step == InitStep.WAITING_FOR_CHANNEL_OPEN || (this.step == InitStep.WAITING_FOR_INITIATE && this.storedChannel != null), this.step);
        log.info("Got CHANNEL_OPEN message, ready to pay");
        if (this.step == InitStep.WAITING_FOR_INITIATE) {
            int i = this.majorVersion;
            if (i == 1) {
                this.state = new PaymentChannelV1ClientState(this.storedChannel, this.wallet);
            } else if (i == 2) {
                this.state = new PaymentChannelV2ClientState(this.storedChannel, this.wallet);
            } else {
                throw new IllegalStateException("Invalid version number " + this.majorVersion);
            }
        } else {
            z = true;
        }
        this.step = InitStep.CHANNEL_OPEN;
        this.conn.channelOpen(z);
    }

    @Override
    public void receiveMessage(Protos.TwoWayChannelMessage twoWayChannelMessage) throws InsufficientMoneyException {
        Protos.Error.Builder explanation;
        PaymentChannelCloseException.CloseReason closeReason;
        this.lock.lock();
        try {
            Preconditions.checkState(this.connectionOpen);
            try {
                boolean z = true;
                switch (AnonymousClass2.$SwitchMap$org$bitcoin$paymentchannel$Protos$TwoWayChannelMessage$MessageType[twoWayChannelMessage.getType().ordinal()]) {
                    case 1:
                        if (this.step != InitStep.WAITING_FOR_VERSION_NEGOTIATION || !twoWayChannelMessage.hasServerVersion()) {
                            z = false;
                        }
                        Preconditions.checkState(z);
                        int major = twoWayChannelMessage.getServerVersion().getMajor();
                        this.majorVersion = major;
                        if (!this.versionSelector.isServerVersionAccepted(major, twoWayChannelMessage.getServerVersion().getMinor())) {
                            explanation = Protos.Error.newBuilder().setCode(Protos.Error.ErrorCode.NO_ACCEPTABLE_VERSION);
                            closeReason = PaymentChannelCloseException.CloseReason.NO_ACCEPTABLE_VERSION;
                            break;
                        } else {
                            log.info("Got version handshake, awaiting INITIATE or resume CHANNEL_OPEN");
                            this.step = InitStep.WAITING_FOR_INITIATE;
                            return;
                        }
                    case 2:
                        if (this.step != InitStep.WAITING_FOR_INITIATE || !twoWayChannelMessage.hasInitiate()) {
                            z = false;
                        }
                        Preconditions.checkState(z);
                        Protos.Initiate initiate = twoWayChannelMessage.getInitiate();
                        explanation = Protos.Error.newBuilder();
                        closeReason = receiveInitiate(initiate, this.maxValue, explanation);
                        if (closeReason != null) {
                            log.error("Initiate failed with error: {}", explanation.m1568build().toString());
                            break;
                        } else {
                            return;
                        }
                    case 3:
                        receiveRefund(twoWayChannelMessage, this.userKeySetup);
                        this.userKeySetup = null;
                        return;
                    case 4:
                        receiveChannelOpen();
                        return;
                    case 5:
                        receivePaymentAck(twoWayChannelMessage.getPaymentAck());
                        return;
                    case 6:
                        receiveClose(twoWayChannelMessage);
                        return;
                    case 7:
                        Preconditions.checkState(twoWayChannelMessage.hasError());
                        log.error("Server sent ERROR {} with explanation {}", twoWayChannelMessage.getError().getCode().name(), twoWayChannelMessage.getError().hasExplanation() ? twoWayChannelMessage.getError().getExplanation() : "");
                        setIncreasePaymentFutureIfNeeded(PaymentChannelCloseException.CloseReason.REMOTE_SENT_ERROR, twoWayChannelMessage.getError().getCode().name());
                        this.conn.destroyConnection(PaymentChannelCloseException.CloseReason.REMOTE_SENT_ERROR);
                        return;
                    default:
                        log.error("Got unknown message type or type that doesn't apply to clients.");
                        explanation = Protos.Error.newBuilder().setCode(Protos.Error.ErrorCode.SYNTAX_ERROR);
                        setIncreasePaymentFutureIfNeeded(PaymentChannelCloseException.CloseReason.REMOTE_SENT_INVALID_MESSAGE, "");
                        closeReason = PaymentChannelCloseException.CloseReason.REMOTE_SENT_INVALID_MESSAGE;
                        break;
                }
            } catch (IllegalStateException e) {
                log.error("Caught illegal state exception handling message from server", e);
                explanation = Protos.Error.newBuilder().setCode(Protos.Error.ErrorCode.SYNTAX_ERROR);
                closeReason = PaymentChannelCloseException.CloseReason.REMOTE_SENT_INVALID_MESSAGE;
            } catch (VerificationException e2) {
                log.error("Caught verification exception handling message from server", e2);
                explanation = Protos.Error.newBuilder().setCode(Protos.Error.ErrorCode.BAD_TRANSACTION).setExplanation(e2.getMessage());
                closeReason = PaymentChannelCloseException.CloseReason.REMOTE_SENT_INVALID_MESSAGE;
            }
            this.conn.sendToServer(Protos.TwoWayChannelMessage.newBuilder().setError(explanation).setType(Protos.TwoWayChannelMessage.MessageType.ERROR).m1809build());
            this.conn.destroyConnection(closeReason);
        } finally {
            this.lock.unlock();
        }
    }

    static class AnonymousClass2 {
        static final int[] $SwitchMap$org$bitcoin$paymentchannel$Protos$TwoWayChannelMessage$MessageType;
        static final int[] $SwitchMap$org$bitcoinj$protocols$channels$PaymentChannelClient$VersionSelector;

        static {
            int[] iArr = new int[Protos.TwoWayChannelMessage.MessageType.values().length];
            $SwitchMap$org$bitcoin$paymentchannel$Protos$TwoWayChannelMessage$MessageType = iArr;
            try {
                iArr[Protos.TwoWayChannelMessage.MessageType.SERVER_VERSION.ordinal()] = 1;
            } catch (NoSuchFieldError unused) {
            }
            try {
                $SwitchMap$org$bitcoin$paymentchannel$Protos$TwoWayChannelMessage$MessageType[Protos.TwoWayChannelMessage.MessageType.INITIATE.ordinal()] = 2;
            } catch (NoSuchFieldError unused2) {
            }
            try {
                $SwitchMap$org$bitcoin$paymentchannel$Protos$TwoWayChannelMessage$MessageType[Protos.TwoWayChannelMessage.MessageType.RETURN_REFUND.ordinal()] = 3;
            } catch (NoSuchFieldError unused3) {
            }
            try {
                $SwitchMap$org$bitcoin$paymentchannel$Protos$TwoWayChannelMessage$MessageType[Protos.TwoWayChannelMessage.MessageType.CHANNEL_OPEN.ordinal()] = 4;
            } catch (NoSuchFieldError unused4) {
            }
            try {
                $SwitchMap$org$bitcoin$paymentchannel$Protos$TwoWayChannelMessage$MessageType[Protos.TwoWayChannelMessage.MessageType.PAYMENT_ACK.ordinal()] = 5;
            } catch (NoSuchFieldError unused5) {
            }
            try {
                $SwitchMap$org$bitcoin$paymentchannel$Protos$TwoWayChannelMessage$MessageType[Protos.TwoWayChannelMessage.MessageType.CLOSE.ordinal()] = 6;
            } catch (NoSuchFieldError unused6) {
            }
            try {
                $SwitchMap$org$bitcoin$paymentchannel$Protos$TwoWayChannelMessage$MessageType[Protos.TwoWayChannelMessage.MessageType.ERROR.ordinal()] = 7;
            } catch (NoSuchFieldError unused7) {
            }
            int[] iArr2 = new int[VersionSelector.values().length];
            $SwitchMap$org$bitcoinj$protocols$channels$PaymentChannelClient$VersionSelector = iArr2;
            try {
                iArr2[VersionSelector.VERSION_1.ordinal()] = 1;
            } catch (NoSuchFieldError unused8) {
            }
            try {
                $SwitchMap$org$bitcoinj$protocols$channels$PaymentChannelClient$VersionSelector[VersionSelector.VERSION_2_ALLOW_1.ordinal()] = 2;
            } catch (NoSuchFieldError unused9) {
            }
            try {
                $SwitchMap$org$bitcoinj$protocols$channels$PaymentChannelClient$VersionSelector[VersionSelector.VERSION_2.ordinal()] = 3;
            } catch (NoSuchFieldError unused10) {
            }
        }
    }

    private void setIncreasePaymentFutureIfNeeded(PaymentChannelCloseException.CloseReason closeReason, String str) {
        SettableFuture<PaymentIncrementAck> settableFuture = this.increasePaymentFuture;
        if (settableFuture == null || settableFuture.isDone()) {
            return;
        }
        this.increasePaymentFuture.setException(new PaymentChannelCloseException(str, closeReason));
    }

    @GuardedBy("lock")
    private void receiveClose(Protos.TwoWayChannelMessage twoWayChannelMessage) throws VerificationException {
        Preconditions.checkState(this.lock.isHeldByCurrentThread());
        if (twoWayChannelMessage.hasSettlement()) {
            Transaction makeTransaction = this.wallet.getParams().getDefaultSerializer().makeTransaction(twoWayChannelMessage.getSettlement().getTx().toByteArray());
            log.info("CLOSE message received with settlement tx {}", makeTransaction.getHash());
            if (this.state != null && state().isSettlementTransaction(makeTransaction)) {
                this.wallet.receivePending(makeTransaction, null);
            }
        } else {
            log.info("CLOSE message received without settlement tx");
        }
        if (this.step == InitStep.WAITING_FOR_CHANNEL_CLOSE) {
            this.conn.destroyConnection(PaymentChannelCloseException.CloseReason.CLIENT_REQUESTED_CLOSE);
        } else {
            this.conn.destroyConnection(PaymentChannelCloseException.CloseReason.SERVER_REQUESTED_CLOSE);
        }
        this.step = InitStep.CHANNEL_CLOSED;
    }

    @Override
    public void connectionClosed() {
        this.lock.lock();
        try {
            this.connectionOpen = false;
            if (this.state != null) {
                this.state.disconnectFromChannel();
            }
        } finally {
            this.lock.unlock();
        }
    }

    @Override
    public void settle() throws IllegalStateException {
        this.lock.lock();
        try {
            Preconditions.checkState(this.connectionOpen);
            this.step = InitStep.WAITING_FOR_CHANNEL_CLOSE;
            log.info("Sending a CLOSE message to the server and waiting for response indicating successful settlement.");
            this.conn.sendToServer(Protos.TwoWayChannelMessage.newBuilder().setType(Protos.TwoWayChannelMessage.MessageType.CLOSE).m1809build());
        } finally {
            this.lock.unlock();
        }
    }

    @Override
    public void connectionOpen() {
        this.lock.lock();
        try {
            this.connectionOpen = true;
            StoredPaymentChannelClientStates storedPaymentChannelClientStates = (StoredPaymentChannelClientStates) this.wallet.getExtensions().get(StoredPaymentChannelClientStates.EXTENSION_ID);
            if (storedPaymentChannelClientStates != null) {
                this.storedChannel = storedPaymentChannelClientStates.getUsableChannelForServerID(this.serverId);
            }
            this.step = InitStep.WAITING_FOR_VERSION_NEGOTIATION;
            Protos.ClientVersion.Builder timeWindowSecs = Protos.ClientVersion.newBuilder().setMajor(this.versionSelector.getRequestedMajorVersion()).setMinor(this.versionSelector.getRequestedMinorVersion()).setTimeWindowSecs(this.timeWindow);
            if (this.storedChannel != null) {
                timeWindowSecs.setPreviousChannelContractHash(ByteString.copyFrom(this.storedChannel.contract.getHash().getBytes()));
                log.info("Begun version handshake, attempting to reopen channel with contract hash {}", this.storedChannel.contract.getHash());
            } else {
                log.info("Begun version handshake creating new channel");
            }
            this.conn.sendToServer(Protos.TwoWayChannelMessage.newBuilder().setType(Protos.TwoWayChannelMessage.MessageType.CLIENT_VERSION).setClientVersion(timeWindowSecs).m1809build());
        } finally {
            this.lock.unlock();
        }
    }

    public PaymentChannelClientState state() {
        this.lock.lock();
        try {
            return this.state;
        } finally {
            this.lock.unlock();
        }
    }

    public ListenableFuture<PaymentIncrementAck> incrementPayment(Coin coin) throws ValueOutOfRangeException, IllegalStateException {
        return incrementPayment(coin, null, null);
    }

    @Override
    public ListenableFuture<PaymentIncrementAck> incrementPayment(Coin coin, @Nullable ByteString byteString, @Nullable KeyParameter keyParameter) throws ValueOutOfRangeException, IllegalStateException, ECKey.KeyIsEncryptedException {
        this.lock.lock();
        try {
            if (state() == null || !this.connectionOpen || this.step != InitStep.CHANNEL_OPEN) {
                throw new IllegalStateException("Channel is not fully initialized/has already been closed");
            }
            if (this.increasePaymentFuture != null) {
                throw new IllegalStateException("Already incrementing paying, wait for previous payment to complete.");
            }
            if (this.wallet.isEncrypted() && keyParameter == null) {
                throw new ECKey.KeyIsEncryptedException();
            }
            PaymentChannelClientState.IncrementedPayment incrementPaymentBy = state().incrementPaymentBy(coin, keyParameter);
            Protos.UpdatePayment.Builder clientChangeValue = Protos.UpdatePayment.newBuilder().setSignature(ByteString.copyFrom(incrementPaymentBy.signature.encodeToBitcoin())).setClientChangeValue(this.state.getValueRefunded().value);
            if (byteString != null) {
                clientChangeValue.setInfo(byteString);
            }
            SettableFuture<PaymentIncrementAck> create = SettableFuture.create();
            this.increasePaymentFuture = create;
            create.addListener(new Runnable() {
                @Override
                public void run() {
                    PaymentChannelClient.this.lock.lock();
                    PaymentChannelClient.this.increasePaymentFuture = null;
                    PaymentChannelClient.this.lock.unlock();
                }
            }, MoreExecutors.sameThreadExecutor());
            this.conn.sendToServer(Protos.TwoWayChannelMessage.newBuilder().setUpdatePayment(clientChangeValue).setType(Protos.TwoWayChannelMessage.MessageType.UPDATE_PAYMENT).m1809build());
            this.lastPaymentActualAmount = incrementPaymentBy.amount;
            return this.increasePaymentFuture;
        } finally {
            this.lock.unlock();
        }
    }

    private void receivePaymentAck(Protos.PaymentAck paymentAck) {
        this.lock.lock();
        try {
            if (this.increasePaymentFuture == null) {
                return;
            }
            Preconditions.checkNotNull(this.increasePaymentFuture, "Server sent a PAYMENT_ACK with no outstanding payment");
            log.info("Received a PAYMENT_ACK from the server");
            SettableFuture<PaymentIncrementAck> settableFuture = this.increasePaymentFuture;
            Coin coin = this.lastPaymentActualAmount;
            this.lock.unlock();
            settableFuture.set(new PaymentIncrementAck(coin, paymentAck.getInfo()));
        } finally {
            this.lock.unlock();
        }
    }
}