com.coinblesk.server.controller.PaymentController.java Source code

Java tutorial

Introduction

Here is the source code for com.coinblesk.server.controller.PaymentController.java

Source

/*
 * Copyright 2016 The Coinblesk team and the CSG Group at University of Zurich
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.coinblesk.server.controller;

import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
import static org.springframework.http.HttpStatus.OK;
import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8_VALUE;
import static org.springframework.web.bind.annotation.RequestMethod.GET;
import static org.springframework.web.bind.annotation.RequestMethod.POST;

import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import com.coinblesk.server.dto.ErrorDTO;
import com.coinblesk.server.dto.KeyExchangeRequestDTO;
import com.coinblesk.server.dto.CreateAddressRequestDTO;
import com.coinblesk.server.dto.SignedDTO;
import com.coinblesk.server.exceptions.InvalidLockTimeException;
import com.coinblesk.server.exceptions.InvalidSignatureException;
import com.coinblesk.server.exceptions.MissingFieldException;
import com.coinblesk.server.exceptions.UserNotFoundException;
import com.coinblesk.server.utils.DTOUtils;
import com.coinblesk.server.utils.SignatureUtils;
import com.google.common.io.BaseEncoding;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.AddressFormatException;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionConfidence;
import org.bitcoinj.core.TransactionOutPoint;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.crypto.TransactionSignature;
import org.bitcoinj.script.Script;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import com.coinblesk.bitcoin.TimeLockedAddress;
import com.coinblesk.json.v1.BalanceTO;
import com.coinblesk.json.v1.KeyTO;
import com.coinblesk.json.v1.RefundTO;
import com.coinblesk.json.v1.SignTO;
import com.coinblesk.json.v1.SignVerifyTO;
import com.coinblesk.json.v1.TxSig;
import com.coinblesk.json.v1.Type;
import com.coinblesk.json.v1.VerifyTO;
import com.coinblesk.server.config.AppConfig;
import com.coinblesk.server.entity.Keys;
import com.coinblesk.server.service.KeyService;
import com.coinblesk.server.service.TransactionService;
import com.coinblesk.server.service.WalletService;
import com.coinblesk.server.utils.ApiVersion;
import com.coinblesk.server.utils.ToUtils;
import com.coinblesk.util.BitcoinUtils;
import com.coinblesk.util.CoinbleskException;
import com.coinblesk.util.InsufficientFunds;
import com.coinblesk.util.Pair;
import com.coinblesk.util.SerializeUtils;

import javax.validation.Valid;

/**
 *
 * @author Alessandro Di Carli
 * @author Thomas Bocek
 * @author Andreas Albrecht
 *
 */
@RestController
@RequestMapping(value = "/payment")
@ApiVersion({ "v1", "" })
public class PaymentController {

    private final static Logger LOG = LoggerFactory.getLogger(PaymentController.class);
    private final static Set<String> CONCURRENCY = Collections.synchronizedSet(new HashSet<>());

    private final AppConfig appConfig;

    private final WalletService walletService;

    private final KeyService keyService;

    private final TransactionService txService;

    @Autowired
    public PaymentController(AppConfig appConfig, WalletService walletService, KeyService keyService,
            TransactionService txService) {
        this.appConfig = appConfig;
        this.walletService = walletService;
        this.keyService = keyService;
        this.txService = txService;
    }

    @RequestMapping(value = "/createTimeLockedAddress", method = POST, consumes = APPLICATION_JSON_UTF8_VALUE, produces = APPLICATION_JSON_UTF8_VALUE)
    @ResponseBody
    @CrossOrigin
    public ResponseEntity createTimeLockedAddress(@RequestBody @Valid SignedDTO request) {

        // Get the embedded payload and check signature
        final CreateAddressRequestDTO createAddressRequestDTO;
        try {
            createAddressRequestDTO = DTOUtils.parseAndValidatePayload(request, CreateAddressRequestDTO.class);
        } catch (MissingFieldException | InvalidSignatureException e) {
            return new ResponseEntity<>(new ErrorDTO(e.getMessage()), BAD_REQUEST);
        } catch (Throwable e) {
            return new ResponseEntity<>(new ErrorDTO("Bad request"), BAD_REQUEST);
        }

        // The client we want to create an address for
        final ECKey clientPublicKey = SignatureUtils
                .getECKeyFromHexPublicKey(createAddressRequestDTO.getPublicKey());
        final long lockTime = createAddressRequestDTO.getLockTime();

        TimeLockedAddress address = null;
        try {
            address = keyService.createTimeLockedAddress(clientPublicKey, lockTime);
        } catch (UserNotFoundException | InvalidLockTimeException e) {
            return new ResponseEntity<>(new ErrorDTO(e.getMessage()), BAD_REQUEST);
        } catch (Throwable e) {
            return new ResponseEntity<>(new ErrorDTO(e.getMessage()), INTERNAL_SERVER_ERROR);
        }

        if (address == null)
            System.out.println("asdf");

        // Start watching the address
        walletService.addWatching(address.createPubkeyScript());

        // Create response
        // TODO: Wrap address and other information (check android) needed into SignedDTO
        return new ResponseEntity("ok", OK);
    }

    @RequestMapping(value = "/signverify", method = POST, consumes = APPLICATION_JSON_UTF8_VALUE, produces = APPLICATION_JSON_UTF8_VALUE)
    @ResponseBody
    public SignVerifyTO signVerify(@RequestBody SignVerifyTO request) {
        final String tag = "{signverify}";
        final Instant startTime = Instant.now();
        String clientPubKeyHex = "(UNKNOWN)";

        try {
            final NetworkParameters params = appConfig.getNetworkParameters();
            final Keys keys;
            final ECKey clientKey = ECKey.fromPublicOnly(request.publicKey());
            clientPubKeyHex = clientKey.getPublicKeyAsHex();
            final ECKey serverKey;
            final Transaction transaction;
            final List<TransactionSignature> clientSigs;

            // clear payeeSig since client sig does not cover it
            final TxSig payeeSigInput = request.payeeMessageSig();
            request.payeeMessageSig(null);
            final byte[] payeePubKey = request.payeePublicKey();
            request.payeePublicKey(null);

            // NFC has a hard limit of 245, thus we have no space for the date
            // yet.
            final SignVerifyTO error = ToUtils.checkInput(request, false);
            if (error != null) {
                LOG.info("{} - clientPubKey={} - input error - type={}", tag, clientPubKeyHex, error.type());
                return error;
            }

            LOG.debug("{} - clientPubKey={} - request", tag, clientPubKeyHex);
            keys = keyService.getByClientPublicKey(clientKey.getPubKey());
            if (keys == null || keys.clientPublicKey() == null || keys.serverPrivateKey() == null
                    || keys.serverPublicKey() == null) {
                LOG.debug("{} - clientPubKey={} - KEYS_NOT_FOUND", tag, clientPubKeyHex);
                return ToUtils.newInstance(SignVerifyTO.class, Type.KEYS_NOT_FOUND);
            }
            serverKey = ECKey.fromPrivateAndPrecalculatedPublic(keys.serverPrivateKey(), keys.serverPublicKey());

            if (keys.timeLockedAddresses().isEmpty()) {
                LOG.debug("{} - clientPubKey={} - ADDRESS_EMPTY", tag, clientPubKeyHex);
                return ToUtils.newInstance(SignVerifyTO.class, Type.ADDRESS_EMPTY, serverKey);
            }

            /*
             * Got a transaction in the request - sign
             */
            if (request.transaction() != null) {
                transaction = new Transaction(params, request.transaction());
                LOG.debug("{} - clientPubKey={} - transaction from input: \n{}", tag, clientPubKeyHex, transaction);

                List<TransactionOutput> outputsToAdd = new ArrayList<>();
                // if amount to spend && address provided, add corresponding
                // output
                if (request.amountToSpend() > 0 && request.addressTo() != null && !request.addressTo().isEmpty()) {
                    TransactionOutput txOut = transaction.addOutput(Coin.valueOf(request.amountToSpend()),
                            Address.fromBase58(params, request.addressTo()));
                    outputsToAdd.add(txOut);
                    LOG.debug("{} - added output={} to Tx={}", tag, txOut, transaction.getHash());
                }

                // if change amount is provided, we add an output to the most
                // recently created address of the client.
                if (request.amountChange() > 0) {
                    Address changeAddress = keys.latestTimeLockedAddresses().toAddress(params);
                    Coin changeAmount = Coin.valueOf(request.amountChange());
                    TransactionOutput changeOut = transaction.addOutput(changeAmount, changeAddress);
                    outputsToAdd.add(changeOut);
                    LOG.debug("{} - added change output={} to Tx={}", tag, changeOut, transaction.getHash());
                }

                if (!outputsToAdd.isEmpty()) {
                    outputsToAdd = BitcoinUtils.sortOutputs(outputsToAdd);
                    transaction.clearOutputs();
                    for (TransactionOutput to : outputsToAdd) {
                        transaction.addOutput(to);
                    }
                }

            } else {
                LOG.debug("{} - clientPubKey={} - INPUT_MISMATCH", tag, clientPubKeyHex);
                return ToUtils.newInstance(SignVerifyTO.class, Type.INPUT_MISMATCH, serverKey);
            }

            // check signatures
            if (request.signatures() == null || (request.signatures().size() != transaction.getInputs().size())) {
                LOG.debug(
                        "{} - clientPubKey={} - INPUT_MISMATCH - number of signatures ({}) != number of inputs ({})",
                        tag, clientPubKeyHex, request.signatures().size(), transaction.getInputs().size());
                return ToUtils.newInstance(SignVerifyTO.class, Type.INPUT_MISMATCH, serverKey);
            }

            clientSigs = SerializeUtils.deserializeSignatures(request.signatures());
            SignVerifyTO responseTO = txService.signVerifyTransaction(transaction, clientKey, serverKey,
                    clientSigs);
            SerializeUtils.signJSON(responseTO, serverKey);

            // if we know the receiver, we create an additional signature with
            // the key of the payee.
            if (maybeAppendPayeeSignature(request, payeePubKey, payeeSigInput, responseTO)) {
                LOG.debug("{} - created additional signature for payee.", tag);
            } else {
                LOG.debug("{} - payee unknown (no additional signature)");
            }

            return responseTO;
        } catch (Exception e) {
            LOG.error("{} - clientPubKey={} - SERVER_ERROR: ", tag, clientPubKeyHex, e);
            return new SignVerifyTO().currentDate(System.currentTimeMillis()).type(Type.SERVER_ERROR)
                    .message(e.getMessage());
        } finally {
            LOG.debug("{} - clientPubKey={} - finished in {} ms", tag, clientPubKeyHex,
                    Duration.between(startTime, Instant.now()).toMillis());
        }
    }

    private boolean maybeAppendPayeeSignature(SignVerifyTO request, byte[] payeePubKey, TxSig payeeTxSig,
            SignVerifyTO response) {
        if (payeePubKey == null || !ECKey.isPubKeyCanonical(payeePubKey) || payeeTxSig == null) {
            return false;
        }

        Keys payeeKeys = keyService.getByClientPublicKey(payeePubKey);
        if (payeeKeys == null) {
            return false; // payee unknown / external user.
        }
        ECKey payeeClientKey = ECKey.fromPublicOnly(payeePubKey);
        ECKey payeeServerKey = ECKey.fromPrivateAndPrecalculatedPublic(payeeKeys.serverPrivateKey(),
                payeeKeys.serverPublicKey());

        // check that payee signature is valid
        request.payeePublicKey(payeePubKey);
        if (!SerializeUtils.verifyJSONSignatureRaw(request, payeeTxSig, payeeClientKey)) {
            return false;
        }
        request.payeePublicKey(null);

        // all checks OK - sign and append signature
        response.payeePublicKey(payeeServerKey.getPubKey());
        TxSig payeeSigOutput = SerializeUtils.signJSONRaw(response, payeeServerKey);
        response.payeeMessageSig(payeeSigOutput);
        return true;
    }

    /**
     * Input is the KeyTO with the client public key. The server will create for
     * this client public key its own server keypair and return the server
     * public key, or indicate an error in KeyTO (or via status code). Make sure
     * to check for isSuccess(). Internally the server will hash the client
     * public key with SHA-256 (UUID) and clients need to identify themselfs
     * with this UUID for subsequent calls.
     *
     */
    @RequestMapping(value = "/key-exchange", method = POST, consumes = APPLICATION_JSON_UTF8_VALUE, produces = APPLICATION_JSON_UTF8_VALUE)
    @ResponseBody
    @CrossOrigin
    public KeyTO keyExchange(@RequestBody @Valid KeyExchangeRequestDTO request) {
        final long startTime = System.currentTimeMillis();
        final String tag = "{key-exchange}";

        try {
            final byte[] publicKey = BaseEncoding.base16().decode(request.getPublicKey().toUpperCase());

            if (publicKey.length != 33 || !ECKey.isPubKeyCanonical(publicKey)) {
                LOG.debug("{} - INPUT_MISMATCH", tag);
                return ToUtils.newInstance(KeyTO.class, Type.INPUT_MISMATCH);
            }

            LOG.debug("{} - clientPubKey={}", tag, SerializeUtils.bytesToHex(publicKey));
            // no input checking as input may not be signed
            final byte[] clientPublicKey = publicKey;
            final ECKey serverEcKey = new ECKey();
            final List<ECKey> keyList = new ArrayList<>(2);
            keyList.add(ECKey.fromPublicOnly(clientPublicKey));
            keyList.add(serverEcKey);

            // 2-of-2 multisig
            final Script script = BitcoinUtils.createP2SHOutputScript(2, keyList);
            final Pair<Boolean, Keys> retVal = keyService.storeKeysAndAddress(clientPublicKey,
                    serverEcKey.getPubKey(), serverEcKey.getPrivKeyBytes());

            final KeyTO serverKeyTO = new KeyTO().currentDate(System.currentTimeMillis());
            if (retVal.element0()) {
                walletService.addWatching(script);
                serverKeyTO.publicKey(serverEcKey.getPubKey());
                serverKeyTO.setSuccess();
                SerializeUtils.signJSON(serverKeyTO, serverEcKey);
            } else {
                Keys keys = retVal.element1();
                serverKeyTO.publicKey(keys.serverPublicKey());
                serverKeyTO.type(Type.SUCCESS_BUT_KEY_ALREADY_EXISTS);
                ECKey existingServerKey = ECKey.fromPrivateAndPrecalculatedPublic(keys.serverPrivateKey(),
                        keys.serverPublicKey());
                SerializeUtils.signJSON(serverKeyTO, existingServerKey);
            }
            LOG.debug("{} - done - {}", serverKeyTO.type().toString());
            return serverKeyTO;
        } catch (Exception e) {
            LOG.error("{} - SERVER_ERROR: ", e);
            return new KeyTO().currentDate(System.currentTimeMillis()).type(Type.SERVER_ERROR)
                    .message(e.getMessage());
        } finally {
            LOG.debug("{} - finished in {} ms", tag, (System.currentTimeMillis() - startTime));
        }
    }

    @RequestMapping(value = "/balance", method = GET, consumes = APPLICATION_JSON_UTF8_VALUE, produces = APPLICATION_JSON_UTF8_VALUE)
    @ResponseBody
    public BalanceTO balance(@RequestBody BalanceTO input) {
        final long start = System.currentTimeMillis();
        try {
            if (input.publicKey() == null || input.publicKey().length == 0) {
                return new BalanceTO().type(Type.KEYS_NOT_FOUND);
            }
            LOG.debug("{balance} clientHash for {}", SerializeUtils.bytesToHex(input.publicKey()));
            final BalanceTO error = ToUtils.checkInput(input);
            if (error != null) {
                return error;
            }
            final NetworkParameters params = appConfig.getNetworkParameters();
            final List<ECKey> keys = keyService.getPublicECKeysByClientPublicKey(input.publicKey());
            final Script script = BitcoinUtils.createP2SHOutputScript(2, keys);
            final Address p2shAddressFrom = script.getToAddress(params);
            List<TransactionOutput> outputs = walletService.verifiedOutputs(params, p2shAddressFrom);
            LOG.debug("{balance} nr. of outputs from network {} for {}. Full list: {}", outputs.size(), "tdb",
                    outputs);
            long total = 0;
            for (TransactionOutput transactionOutput : outputs) {
                total += transactionOutput.getValue().value;
            }
            LOG.debug("{balance}:{} done", (System.currentTimeMillis() - start));
            return new BalanceTO().balance(total);

        } catch (Exception e) {
            LOG.error("{balance} keys error", e);
            return new BalanceTO().type(Type.SERVER_ERROR).message(e.getMessage());
        }
    }

    @RequestMapping(value = "/refund", method = POST, consumes = APPLICATION_JSON_UTF8_VALUE, produces = APPLICATION_JSON_UTF8_VALUE)
    @ResponseBody
    public RefundTO refund(@RequestBody RefundTO input) {
        final long start = System.currentTimeMillis();
        try {
            if (input.publicKey() == null || input.publicKey().length == 0) {
                return new RefundTO().type(Type.KEYS_NOT_FOUND);
            }
            LOG.debug("{refund} for {}", SerializeUtils.bytesToHex(input.publicKey()));
            final RefundTO error = ToUtils.checkInput(input);
            if (error != null) {
                return error;
            }
            final List<ECKey> keys = keyService.getECKeysByClientPublicKey(input.publicKey());
            if (keys == null || keys.size() != 2) {
                return new RefundTO().type(Type.KEYS_NOT_FOUND);
            }
            final NetworkParameters params = appConfig.getNetworkParameters();
            final ECKey serverKey = keys.get(1);
            final ECKey clientKey = keys.get(0);
            final Script redeemScript = BitcoinUtils.createRedeemScript(2, keys);
            // this is how the client sees the tx
            final Transaction refundTransaction;

            // choice 1 - full refund tx
            if (input.refundTransaction() != null) {
                refundTransaction = new Transaction(params, input.refundTransaction());
            }
            // choice 2 - send outpoints, coins, where to send btc to, and
            // amount
            else if (input.outpointsCoinPair() != null && !input.outpointsCoinPair().isEmpty()
                    && input.lockTimeSeconds() > 0 && input.refundSendTo() != null) {
                final List<Pair<TransactionOutPoint, Coin>> refundClientPoints = SerializeUtils
                        .deserializeOutPointsCoin(params, input.outpointsCoinPair());
                try {
                    Address refundSendTo = Address.fromBase58(params, input.refundSendTo());
                    refundTransaction = BitcoinUtils.createRefundTx(params, refundClientPoints, redeemScript,
                            refundSendTo, input.lockTimeSeconds());
                } catch (AddressFormatException e) {
                    LOG.debug("{refund}:{} empty address for", (System.currentTimeMillis() - start));
                    return new RefundTO().type(Type.ADDRESS_EMPTY).message(e.getMessage());
                }
            }
            // wrong choice
            else {
                return new RefundTO().type(Type.INPUT_MISMATCH);
            }

            // sanity check
            refundTransaction.verify();

            List<TransactionSignature> clientSigs = SerializeUtils.deserializeSignatures(input.clientSignatures());

            // now we can check the client sigs
            if (!SerializeUtils.verifyTxSignatures(refundTransaction, clientSigs, redeemScript, clientKey)) {
                LOG.debug("{refund} signature mismatch for tx {} with sigs {}", refundTransaction, clientSigs);
                return new RefundTO().type(Type.SIGNATURE_ERROR);
            } else {
                LOG.debug("{refund} signature good! for tx {} with sigs", refundTransaction, clientSigs);
            }
            // also check the server sigs, that the reedemscript has our public
            // key

            Collections.sort(keys, ECKey.PUBKEY_COMPARATOR);

            List<TransactionSignature> serverSigs = BitcoinUtils.partiallySign(refundTransaction, redeemScript,
                    serverKey);
            boolean clientFirst = BitcoinUtils.clientFirst(keys, clientKey);
            BitcoinUtils.applySignatures(refundTransaction, redeemScript, clientSigs, serverSigs, clientFirst);
            input.serverSignatures(SerializeUtils.serializeSignatures(serverSigs));
            // TODO: enable
            // refundTransaction.verify(); make sure those inputs are from the
            // known p2sh address (min conf)
            byte[] refundTx = refundTransaction.unsafeBitcoinSerialize();
            txService.addTransaction(input.publicKey(), refundTx, refundTransaction.getHash().getBytes(), false);
            LOG.debug("{refund}:{} done", (System.currentTimeMillis() - start));
            return new RefundTO().setSuccess().refundTransaction(refundTx)
                    .serverSignatures(SerializeUtils.serializeSignatures(serverSigs));
        } catch (Exception e) {
            LOG.error("register keys error", e);
            return new RefundTO().type(Type.SERVER_ERROR).message(e.getMessage());
        }
    }

    @RequestMapping(value = "/sign", method = POST, consumes = APPLICATION_JSON_UTF8_VALUE, produces = APPLICATION_JSON_UTF8_VALUE)
    @ApiVersion("v2")
    @ResponseBody
    public SignTO sign(@RequestBody SignTO input) {
        final long start = System.currentTimeMillis();
        if (input.publicKey() == null || input.publicKey().length == 0) {
            return new SignTO().type(Type.KEYS_NOT_FOUND);
        }
        final String key = SerializeUtils.bytesToHex(input.publicKey());
        try {
            LOG.debug("{sign} for {}", key);
            if (!CONCURRENCY.add(key)) {
                return new SignTO().type(Type.CONCURRENCY_ERROR);
            }
            final SignTO error = ToUtils.checkInput(input);
            if (error != null) {
                return error;
            }
            final NetworkParameters params = appConfig.getNetworkParameters();
            final List<ECKey> keys = keyService.getECKeysByClientPublicKey(input.publicKey());
            if (keys == null || keys.size() != 2) {
                return new SignTO().type(Type.KEYS_NOT_FOUND);
            }

            final ECKey serverKey = keys.get(1);
            final Script redeemScript = BitcoinUtils.createRedeemScript(2, keys);
            final Script p2SHOutputScript = BitcoinUtils.createP2SHOutputScript(2, keys);
            Collections.sort(keys, ECKey.PUBKEY_COMPARATOR);
            final Address p2shAddressFrom = p2SHOutputScript.getToAddress(params);

            final Transaction transaction;
            // choice 1 - full tx, without sigs
            if (input.transaction() != null) {
                LOG.debug("{sign}:{} got transaction from input", (System.currentTimeMillis() - start));
                transaction = new Transaction(appConfig.getNetworkParameters(), input.transaction());
            }
            // choice 2 - send outpoints, coins, where to send btc to, and
            // amount
            else if (input.outpointsCoinPair() != null && !input.outpointsCoinPair().isEmpty()
                    && input.p2shAddressTo() != null && input.amountToSpend() != 0) {
                // having the coins and the output allows us to create the tx
                // without looking into our wallet
                try {
                    transaction = createTx(params, input.p2shAddressTo(), p2shAddressFrom,
                            input.outpointsCoinPair(), input.amountToSpend(), redeemScript);
                } catch (AddressFormatException e) {
                    LOG.debug("{sign}:{} empty address for", (System.currentTimeMillis() - start));
                    return new SignTO().type(Type.ADDRESS_EMPTY).message(e.getMessage());
                } catch (CoinbleskException e) {
                    LOG.warn("{sign} could not create tx", e);
                    return new SignTO().type(Type.TX_ERROR).message(e.getMessage());
                } catch (InsufficientFunds e) {
                    LOG.debug("{sign} not enough coins or amount too small");
                    return new SignTO().type(Type.NOT_ENOUGH_COINS);
                }
            }
            // wrong choice
            else {
                return new SignTO().type(Type.INPUT_MISMATCH);
            }

            // sanity check
            transaction.verify();

            if (txService.isBurned(params, input.publicKey(), transaction)) {
                return new SignTO().type(Type.BURNED_OUTPUTS);
            }

            final List<TransactionSignature> serverSigs = BitcoinUtils.partiallySign(transaction, redeemScript,
                    serverKey);

            final byte[] serializedTransaction = transaction.unsafeBitcoinSerialize();
            txService.addTransaction(input.publicKey(), serializedTransaction, transaction.getHash().getBytes(),
                    false);
            LOG.debug("{sign}:tx-hash {} in {} done", transaction.getHash(), (System.currentTimeMillis() - start));
            return new SignTO().setSuccess().transaction(serializedTransaction)
                    .signatures(SerializeUtils.serializeSignatures(serverSigs));

        } catch (Exception e) {
            LOG.error("Sign keys error", e);
            return new SignTO().type(Type.SERVER_ERROR).message(e.getMessage());
        } finally {
            CONCURRENCY.remove(key);
        }
    }

    @RequestMapping(value = "/verify", method = POST, consumes = APPLICATION_JSON_UTF8_VALUE, produces = APPLICATION_JSON_UTF8_VALUE)
    @ApiVersion("v2")
    @ResponseBody
    public VerifyTO verify(@RequestBody VerifyTO input) {
        final long start = System.currentTimeMillis();
        if (input.publicKey() == null || input.publicKey().length == 0) {
            return new VerifyTO().type(Type.KEYS_NOT_FOUND);
        }
        final String key = SerializeUtils.bytesToHex(input.publicKey());
        try {
            LOG.debug("{verify} for {}", key);
            if (!CONCURRENCY.add(key)) {
                return new VerifyTO().type(Type.CONCURRENCY_ERROR);
            }
            final VerifyTO error = ToUtils.checkInput(input);
            if (error != null) {
                return error;
            }
            final NetworkParameters params = appConfig.getNetworkParameters();
            final List<ECKey> keys = keyService.getECKeysByClientPublicKey(input.publicKey());
            if (keys == null || keys.size() != 2) {
                return new VerifyTO().type(Type.KEYS_NOT_FOUND);
            }
            final ECKey clientKey = keys.get(0);
            final ECKey serverKey = keys.get(1);
            final Script redeemScript = BitcoinUtils.createRedeemScript(2, keys);
            final Script p2SHOutputScript = BitcoinUtils.createP2SHOutputScript(2, keys);
            Collections.sort(keys, ECKey.PUBKEY_COMPARATOR);
            final Address p2shAddressFrom = p2SHOutputScript.getToAddress(params);

            final Transaction fullTx;
            // choice 1 - full tx
            if (input.transaction() != null) {
                LOG.debug("{verify}:{} got transaction from input", (System.currentTimeMillis() - start));
                fullTx = new Transaction(appConfig.getNetworkParameters(), input.transaction());
                LOG.debug("{verify}:{} tx1 created {}", (System.currentTimeMillis() - start), fullTx);
                // TODO: verify that this was sent from us
            }
            // choice 2 - send outpoints, coins, where to send btc to, and
            // amount
            else if (input.outpointsCoinPair() != null && !input.outpointsCoinPair().isEmpty()
                    && input.p2shAddressTo() != null && input.amountToSpend() != 0
                    && input.clientSignatures() != null && input.serverSignatures() != null) {
                try {
                    fullTx = createTx(params, input.p2shAddressTo(), p2shAddressFrom, input.outpointsCoinPair(),
                            input.amountToSpend(), redeemScript);
                    List<TransactionSignature> clientSigs = SerializeUtils
                            .deserializeSignatures(input.clientSignatures());
                    List<TransactionSignature> severSigs = SerializeUtils
                            .deserializeSignatures(input.serverSignatures());
                    final boolean clientFirst = BitcoinUtils.clientFirst(keys, clientKey);
                    BitcoinUtils.applySignatures(fullTx, redeemScript, clientSigs, severSigs, clientFirst);

                    LOG.debug("{verify}:{} tx2 created {}", (System.currentTimeMillis() - start), fullTx);

                    if (!SerializeUtils.verifyTxSignatures(fullTx, clientSigs, redeemScript, clientKey)) {
                        LOG.debug("{verify} signature mismatch for client-sigs tx {} with sigs {}", fullTx,
                                clientSigs);
                        return new VerifyTO().type(Type.SIGNATURE_ERROR);
                    } else {
                        LOG.debug("{verify} signature good! for client-sigs tx {} with sigs", fullTx, clientSigs);
                    }

                    if (!SerializeUtils.verifyTxSignatures(fullTx, severSigs, redeemScript, serverKey)) {
                        LOG.debug("{verify} signature mismatch for server-sigs tx {} with sigs {}", fullTx,
                                severSigs);
                        return new VerifyTO().type(Type.SIGNATURE_ERROR);
                    } else {
                        LOG.debug("{verify} signature good! for server-sigs tx {} with sigs", fullTx, clientSigs);
                    }

                } catch (AddressFormatException e) {
                    LOG.debug("{verify}:{} empty address for", (System.currentTimeMillis() - start));
                    return new VerifyTO().type(Type.ADDRESS_EMPTY).message(e.getMessage());
                } catch (CoinbleskException e) {
                    LOG.warn("{verify} could not create tx", e);
                    return new VerifyTO().type(Type.SERVER_ERROR).message(e.getMessage());
                } catch (InsufficientFunds e) {
                    LOG.debug("{verify} not enough coins or amount too small");
                    return new VerifyTO().type(Type.NOT_ENOUGH_COINS);
                }
            }
            // wrong choice
            else {
                return new VerifyTO().type(Type.INPUT_MISMATCH);
            }

            // sanity check
            fullTx.verify();

            final VerifyTO output = new VerifyTO();
            output.transaction(fullTx.unsafeBitcoinSerialize());

            LOG.debug("{verify}:{} received {}", (System.currentTimeMillis() - start), fullTx);

            // ok, refunds are locked or no refund found
            fullTx.getConfidence().setSource(TransactionConfidence.Source.SELF);
            walletService.receivePending(fullTx);
            walletService.broadcast(fullTx);

            LOG.debug("{verify}:{} broadcast done", (System.currentTimeMillis() - start));

            if (txService.isTransactionInstant(params, input.publicKey(), fullTx)) {
                LOG.debug("{verify}:{} instant payment **OK**", (System.currentTimeMillis() - start));
                return output.setSuccess();
            } else {
                LOG.debug("{verify}:{} instant payment NOTOK", (System.currentTimeMillis() - start));
                return output.type(Type.SUCCESS_BUT_NO_INSTANT_PAYMENT);
            }

        } catch (Exception e) {
            LOG.error("{verify} register keys error: ", e);
            return new VerifyTO().type(Type.SERVER_ERROR).message(e.getMessage());
        } finally {
            CONCURRENCY.remove(key);
        }
    }

    @RequestMapping(value = {
            "/virtualbalance" }, method = POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
    @ResponseBody
    public BalanceTO virtualBalance(@RequestBody BalanceTO input) {
        if (input.publicKey() == null || input.publicKey().length == 0) {
            return new BalanceTO().type(Type.KEYS_NOT_FOUND);
        }

        // Check if message is signed correctly
        final BalanceTO error = ToUtils.checkInput(input);
        if (error != null) {
            return error;
        }

        // Fetch actual balance
        final long balance = keyService.getVirtualBalanceByClientPublicKey(input.publicKey());

        // Construct response
        BalanceTO balanceDTO = new BalanceTO().balance(balance);

        // Sign it
        Keys keys = keyService.getByClientPublicKey(input.publicKey());
        ECKey existingServerKey = ECKey.fromPrivateAndPrecalculatedPublic(keys.serverPrivateKey(),
                keys.serverPublicKey());
        return SerializeUtils.signJSON(balanceDTO, existingServerKey);
    }

    private static Transaction createTx(NetworkParameters params, String p2shAddressTo, Address p2shAddressFrom,
            List<Pair<byte[], Long>> outpointsCoinPair, long amountToSpend, Script redeemScript)
            throws AddressFormatException, CoinbleskException, InsufficientFunds {
        final Address p2shAddressTo1 = new Address(params, p2shAddressTo);

        // we now get from the client the outpoints for the refund tx (including
        // hash)
        final List<Pair<TransactionOutPoint, Coin>> refundClientPoints = SerializeUtils
                .deserializeOutPointsCoin(params, outpointsCoinPair);

        return BitcoinUtils.createTx(params, refundClientPoints, redeemScript, p2shAddressFrom, p2shAddressTo1,
                amountToSpend, true);
    }
}