strat.mining.multipool.stats.service.impl.DonationServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for strat.mining.multipool.stats.service.impl.DonationServiceImpl.java

Source

/**
 * multipool-stats-backend is a web application which collects statistics
 * on several Switching-profit crypto-currencies mining pools and display
 * then in a Browser.
 * Copyright (C) 2014  Stratehm (stratehm@hotmail.com)
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with multipool-stats-backend. If not, see <http://www.gnu.org/licenses/>.
 */
package strat.mining.multipool.stats.service.impl;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;

import org.apache.commons.collections.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import strat.mining.multipool.stats.dto.CurrencyTickerDTO;
import strat.mining.multipool.stats.dto.DonationDetailsDTO;
import strat.mining.multipool.stats.dto.DonationTransactionDetailsDTO;
import strat.mining.multipool.stats.jersey.client.iface.BlockchainRestClient;
import strat.mining.multipool.stats.jersey.model.blockchain.BlockChainSingleAddress;
import strat.mining.multipool.stats.jersey.model.blockchain.BlockHeight;
import strat.mining.multipool.stats.jersey.model.blockchain.Inputs;
import strat.mining.multipool.stats.jersey.model.blockchain.Out;
import strat.mining.multipool.stats.jersey.model.blockchain.Txs;
import strat.mining.multipool.stats.persistence.dao.donation.iface.AddressDAO;
import strat.mining.multipool.stats.persistence.dao.donation.iface.TransactionDAO;
import strat.mining.multipool.stats.persistence.model.donation.Address;
import strat.mining.multipool.stats.persistence.model.donation.Transaction;
import strat.mining.multipool.stats.service.iface.CurrencyService;
import strat.mining.multipool.stats.service.iface.DonationService;
import strat.mining.multipool.stats.service.impl.place.BitcoinCentralProvider;

@Component("donationService")
public class DonationServiceImpl implements DonationService {

    private static final Logger LOGGER = LoggerFactory.getLogger(DonationServiceImpl.class);

    private static final float DEFAULT_BTC_PRICE = 450;

    private static final float SERVER_RENT_PRICE = 30;

    private static final String DONATION_BITCOIN_ADDRESS = "19wv8FQKv3NkwTdzBCQn1AGsb9ghqBPWXi";

    private volatile DonationDetailsDTO donationDetails;

    private volatile Map<String, List<Transaction>> transactionsNotThankedByAddress;

    @Resource
    private BlockchainRestClient blockchainRestClient;

    @Resource
    private CurrencyService currencyService;

    @Resource
    private AddressDAO donationAddressDao;

    @Resource
    private TransactionDAO donationTransactionDao;

    @PostConstruct
    public void init() {
        transactionsNotThankedByAddress = Collections.synchronizedMap(new HashMap<String, List<Transaction>>());
        updateDonations();
    }

    @Override
    public DonationDetailsDTO getDonationDetails() {
        return donationDetails;
    }

    /**
     * update donations details every 10 minutes
     */
    @Scheduled(cron = "0 2/10 * * * *")
    private void updateDonations() {
        DonationDetailsDTO newDetails = new DonationDetailsDTO();
        newDetails.setTransactions(new ArrayList<DonationTransactionDetailsDTO>());

        CurrencyTickerDTO currencyTicker = currencyService.getCurrencyTicker(BitcoinCentralProvider.EXCHANGE_NAME,
                "EUR");

        if (currencyTicker == null) {
            currencyTicker = new CurrencyTickerDTO();
            currencyTicker.setLast(DEFAULT_BTC_PRICE);
        }

        boolean hasErrorOccured = extractTransactions(newDetails);

        newDetails.setLastMonthDonationsValue(getLastMonthDonationsValue());

        if (!hasErrorOccured) {
            float rentValueInBTC = SERVER_RENT_PRICE / currencyTicker.getLast();
            newDetails.setDonationsNeeded(rentValueInBTC);
            newDetails.setBtcPriceInEuro(currencyTicker.getLast());
            newDetails.setDonationBtcAddress(DONATION_BITCOIN_ADDRESS);
            newDetails.setRentPriceInEuro(SERVER_RENT_PRICE);
            donationDetails = newDetails;
        } else {
            donationDetails = null;
        }

        updateDonationsNotThanked();

    }

    /**
     * Return the value of all transactions of the previous month.
     * 
     * @return
     */
    private Float getLastMonthDonationsValue() {
        Calendar start = new GregorianCalendar();
        start.add(Calendar.MONTH, -1);
        start.set(Calendar.DAY_OF_MONTH, 1);
        start.set(Calendar.HOUR, 0);
        start.set(Calendar.MINUTE, 0);
        start.set(Calendar.SECOND, 0);
        start.set(Calendar.MILLISECOND, 0);
        List<Transaction> transactions = donationTransactionDao.getTransactionsBetween(start.getTime(),
                getFirstDayOfMonthDate());

        Float result = 0f;
        if (CollectionUtils.isNotEmpty(transactions)) {
            for (Transaction transaction : transactions) {
                result += transaction.getAmount();
            }
        }

        return result;
    }

    /**
     * Fill the given details with transactions details
     * 
     * @param newDetails
     * @return
     */
    private boolean extractTransactions(DonationDetailsDTO newDetails) {
        BlockChainSingleAddress blockChainInfo = null;
        Date month = getFirstDayOfMonthDate();

        int totalDonationsValueInMonth = 0;
        boolean isMonthComplete = false;
        boolean noMoreTxs = false;
        boolean hasErrorOccured = false;

        int currentPage = 0;
        int pageSize = 10;

        while (!isMonthComplete && !noMoreTxs && !hasErrorOccured) {
            blockChainInfo = blockchainRestClient.getBlockChainSingleAddress(DONATION_BITCOIN_ADDRESS, pageSize,
                    currentPage++ * pageSize);

            hasErrorOccured = blockChainInfo == null;

            // Iterate on transactions until a transaction is before the first
            // day of the current month. If this transaction is not found, then
            // retrieve the next transaction page.
            if (blockChainInfo != null && blockChainInfo.getTxs() != null && blockChainInfo.getTxs().size() > 0) {
                for (Txs tx : blockChainInfo.getTxs()) {
                    Date txDate = new Date();
                    Float txValue = 0f;

                    if (tx.getTime() != null) {
                        txDate = new Date(tx.getTime().longValue() * 1000);
                    } else {
                        BlockHeight blockHeight = blockchainRestClient
                                .getBlockHeight(tx.getBlock_height().toString());
                        if (CollectionUtils.isNotEmpty(blockHeight.getBlocks())
                                && blockHeight.getBlocks().get(0).getTime() != null) {
                            txDate = new Date(blockHeight.getBlocks().get(0).getTime().longValue() * 1000);
                        }
                    }

                    if (txDate.after(month)) {
                        LOGGER.debug("TX kept for date {}", txDate);
                        DonationTransactionDetailsDTO txDetails = new DonationTransactionDetailsDTO();

                        // Look for the output with my address to get the
                        // donation value. The transaction is a donation only if
                        // the donation address is in the outputs (else, it is a
                        // payement done with this address)
                        boolean isDonation = false;
                        for (Out out : tx.getOut()) {
                            if (DONATION_BITCOIN_ADDRESS.equals(out.getAddr())) {
                                txValue = out.getValue().floatValue() / 100000000F;
                                LOGGER.debug("Value: {}", txValue);
                                txDetails.setValue(txValue);
                                totalDonationsValueInMonth += out.getValue().intValue();
                                isDonation = true;
                                break;
                            }
                        }

                        // Add the transaction only if it is a donation.
                        if (isDonation) {
                            txDetails.setSrcAddresses(new ArrayList<String>());
                            for (Inputs input : tx.getInputs()) {
                                txDetails.getSrcAddresses().add(input.getPrev_out().getAddr());
                            }

                            txDetails.setTime(txDate);

                            newDetails.getTransactions().add(txDetails);

                            saveTransaction(tx, txDate, txValue);
                        }
                    } else {
                        // Stop to retrieve transactions
                        isMonthComplete = true;
                        break;
                    }
                }
            } else {
                noMoreTxs = true;
            }
        }

        newDetails.setDonationsInBTC((float) totalDonationsValueInMonth / 100000000F);

        return hasErrorOccured;
    }

    @Override
    public List<DonationTransactionDetailsDTO> getAddressDonationsNotThanked(String bitcoinAddress) {
        List<DonationTransactionDetailsDTO> result = null;
        List<Transaction> transactions = transactionsNotThankedByAddress.remove(bitcoinAddress);
        if (CollectionUtils.isNotEmpty(transactions)) {
            result = new ArrayList<>();
            for (Transaction transaction : transactions) {
                DonationTransactionDetailsDTO details = new DonationTransactionDetailsDTO();
                details.setTime(transaction.getDate());
                details.setValue(transaction.getAmount());
                result.add(details);

                transaction.setHasBeenThanked(true);
                donationTransactionDao.updateTransaction(transaction);
            }
        }

        return result;
    }

    /**
     * Update the map of donation transactions that have not been thanked yet.
     */
    private void updateDonationsNotThanked() {
        Map<String, List<Transaction>> transactionsByAddress = Collections
                .synchronizedMap(new HashMap<String, List<Transaction>>());

        List<Address> donatorsAddresses = donationAddressDao.getAllAddressesSince(getFirstDayOfMonthDate());

        if (CollectionUtils.isNotEmpty(donatorsAddresses)) {
            for (Address address : donatorsAddresses) {
                List<Transaction> notThankedDonationTransactions = new ArrayList<>();

                if (address != null && CollectionUtils.isNotEmpty(address.getTransactionIds())) {
                    for (String transactionId : address.getTransactionIds()) {
                        Transaction transaction = donationTransactionDao.getTransaction(transactionId);
                        if (transaction != null && !transaction.getHasBeenThanked()) {
                            notThankedDonationTransactions.add(transaction);
                        }
                    }
                }

                // If the current address has some transactions not thanked. add
                // it.
                if (CollectionUtils.isNotEmpty(notThankedDonationTransactions)) {
                    transactionsByAddress.put(address.getAddress(), notThankedDonationTransactions);
                }
            }
        }

        transactionsNotThankedByAddress = transactionsByAddress;
    }

    /**
     * Return the date object that represent the first day of month
     * 
     * @return
     */
    private Date getFirstDayOfMonthDate() {
        Calendar calendar = new GregorianCalendar();
        calendar.set(Calendar.MILLISECOND, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.HOUR, 0);
        calendar.set(Calendar.DAY_OF_MONTH, 1);
        return calendar.getTime();
    }

    /**
     * Save the transaction (if not already done).
     * 
     * @param tx
     */
    private void saveTransaction(Txs tx, Date txDate, Float amount) {
        Transaction transaction = donationTransactionDao.getTransaction(tx.getHash());

        // Save the transation if not already present.
        if (transaction == null && CollectionUtils.isNotEmpty(tx.getInputs())) {
            Set<Integer> inputAddressId = new HashSet<>();
            for (Inputs input : tx.getInputs()) {
                String inputAddress = input.getPrev_out().getAddr();
                if (inputAddress != null) {
                    Address address = donationAddressDao.getAddress(inputAddress);
                    Set<String> transactions = new HashSet<>();

                    if (address == null) {
                        address = new Address();
                        address.setAddress(inputAddress);
                        address.setLastUpdated(new Date());
                        transactions.add(tx.getHash());
                        address.setTransactionIds(transactions);
                        donationAddressDao.insertAddress(address);
                    } else {
                        address.setLastUpdated(new Date());
                        transactions.add(tx.getHash());
                        if (address.getTransactionIds() != null) {
                            transactions.addAll(address.getTransactionIds());
                        }
                        address.setTransactionIds(transactions);
                        donationAddressDao.updateAddress(address);
                    }

                    inputAddressId.add(address.getId());
                }
            }

            transaction = new Transaction();
            transaction.setTransactionId(tx.getHash());
            transaction.setHasBeenThanked(false);
            transaction.setDate(txDate);
            transaction.setSourceAddressesId(inputAddressId);
            transaction.setAmount(amount);
            donationTransactionDao.insertTransaction(transaction);
        }
    }
}