Savings wallet (WIP)

This commit is contained in:
Manfred Karrer 2016-04-02 00:32:03 +02:00
parent f07aa9ba6a
commit 091eae4965
19 changed files with 288 additions and 169 deletions

View file

@ -36,12 +36,12 @@ public class FeePolicy {
// disputed payout tx: 408 bytes
// We set a fixed fee to make the needed amounts in the trade predictable.
// We use 0.0003 BTC (0.12 EUR @ 400 EUR/BTC) which is for our tx sizes about 75-150 satoshi/byte
// We use 0.0002 BTC (0.08 EUR @ 400 EUR/BTC) which is for our tx sizes about 50-90 satoshi/byte
// We cannot make that user defined as it need to be the same for both users, so we can only change that in
// software updates
// TODO before Beta we should get a good future proof guess as a change causes incompatible versions
public static Coin getFixedTxFeeForTrades() {
return Coin.valueOf(30_000);
return Coin.valueOf(20_000);
}
// For non trade transactions (withdrawal) we use the default fee calculation
@ -50,7 +50,7 @@ public class FeePolicy {
// The BitcoinJ fee calculation use kb so a tx size < 1kb will still pay the fee for a kb tx.
// Our payout tx has about 370 bytes so we get a fee/kb value of about 90 satoshi/byte making it high priority
// Other payout transactions (E.g. arbitrators many collected transactions) will go with 30 satoshi/byte if > 1kb
private static Coin FEE_PER_KB = Coin.valueOf(30_000); // 0.0003 BTC about 0.12 EUR @ 400 EUR/BTC
private static Coin FEE_PER_KB = Coin.valueOf(20_000); // 0.0002 BTC about 0.08 EUR @ 400 EUR/BTC
public static void setFeePerKb(Coin feePerKb) {
FEE_PER_KB = feePerKb;
@ -84,8 +84,8 @@ public class FeePolicy {
// TODO will be increased once we get higher limits
// 0.01 BTC; about 0.4 EUR @ 400 EUR/BTC
// 0.02 BTC; about 8 EUR @ 400 EUR/BTC
public static Coin getSecurityDeposit() {
return Coin.valueOf(1_000_000);
return Coin.valueOf(2_000_000);
}
}

View file

@ -333,6 +333,12 @@ public class WalletService {
.findAny();
}
public List<AddressEntry> getTradeAddressEntryList() {
return getAddressEntryList().stream()
.filter(e -> e.getContext().equals(AddressEntry.Context.TRADE))
.collect(Collectors.toList());
}
///////////////////////////////////////////////////////////////////////////////////////////
// SavingsAddressEntry
@ -493,6 +499,10 @@ public class WalletService {
return wallet != null ? getBalance(wallet.calculateAllSpendCandidates(), address) : Coin.ZERO;
}
public Coin getBalanceForAddressEntryWithTradeId(String tradeId) {
return getBalanceForAddress(getTradeAddressEntry(tradeId).getAddress());
}
private Coin getBalance(List<TransactionOutput> transactionOutputs, Address address) {
Coin balance = Coin.ZERO;
for (TransactionOutput transactionOutput : transactionOutputs) {
@ -539,7 +549,8 @@ public class WalletService {
Coin fee;
try {
wallet.completeTx(getSendRequest(fromAddress, toAddress, amount, aesKey));
fee = Coin.ZERO;
// We use the min fee for now as the mix of savingswallet/trade wallet has some nasty edge cases...
fee = FeePolicy.getFixedTxFeeForTrades();
} catch (InsufficientMoneyException e) {
log.info("The amount to be transferred is not enough to pay the transaction fees of {}. " +
"We subtract that fee from the receivers amount to make the transaction possible.");
@ -556,7 +567,8 @@ public class WalletService {
Coin fee;
try {
wallet.completeTx(getSendRequestForMultipleAddresses(fromAddresses, toAddress, amount, null, aesKey));
fee = Coin.ZERO;
// We use the min fee for now as the mix of savingswallet/trade wallet has some nasty edge cases...
fee = FeePolicy.getFixedTxFeeForTrades();
} catch (InsufficientMoneyException e) {
log.info("The amount to be transferred is not enough to pay the transaction fees of {}. " +
"We subtract that fee from the receivers amount to make the transaction possible.");

View file

@ -67,18 +67,18 @@ public final class PaymentMethod implements Persistable, Comparable {
public static PaymentMethod BLOCK_CHAINS;
public static final List<PaymentMethod> ALL_VALUES = new ArrayList<>(Arrays.asList(
OK_PAY = new PaymentMethod(OK_PAY_ID, 0, DAY, Coin.parseCoin("0.4")), // tx instant so min. wait time
PERFECT_MONEY = new PaymentMethod(PERFECT_MONEY_ID, 0, DAY, Coin.parseCoin("0.4")),
SEPA = new PaymentMethod(SEPA_ID, 0, 8 * DAY, Coin.parseCoin("0.3")), // sepa takes 1-3 business days. We use 8 days to include safety for holidays
NATIONAL_BANK = new PaymentMethod(NATIONAL_BANK_ID, 0, 4 * DAY, Coin.parseCoin("0.3")),
SAME_BANK = new PaymentMethod(SAME_BANK_ID, 0, 2 * DAY, Coin.parseCoin("0.3")),
SPECIFIC_BANKS = new PaymentMethod(SPECIFIC_BANKS_ID, 0, 4 * DAY, Coin.parseCoin("0.3")),
SWISH = new PaymentMethod(SWISH_ID, 0, DAY, Coin.parseCoin("0.4")),
ALI_PAY = new PaymentMethod(ALI_PAY_ID, 0, DAY, Coin.parseCoin("0.4")),
OK_PAY = new PaymentMethod(OK_PAY_ID, 0, DAY, Coin.parseCoin("1")), // tx instant so min. wait time
SEPA = new PaymentMethod(SEPA_ID, 0, 8 * DAY, Coin.parseCoin("0.5")), // sepa takes 1-3 business days. We use 8 days to include safety for holidays
NATIONAL_BANK = new PaymentMethod(NATIONAL_BANK_ID, 0, 4 * DAY, Coin.parseCoin("0.5")),
SAME_BANK = new PaymentMethod(SAME_BANK_ID, 0, 2 * DAY, Coin.parseCoin("0.5")),
SPECIFIC_BANKS = new PaymentMethod(SPECIFIC_BANKS_ID, 0, 4 * DAY, Coin.parseCoin("0.5")),
PERFECT_MONEY = new PaymentMethod(PERFECT_MONEY_ID, 0, DAY, Coin.parseCoin("0.75")),
SWISH = new PaymentMethod(SWISH_ID, 0, DAY, Coin.parseCoin("0.75")),
ALI_PAY = new PaymentMethod(ALI_PAY_ID, 0, DAY, Coin.parseCoin("0.75")),
/* FED_WIRE = new PaymentMethod(FED_WIRE_ID, 0, DAY, Coin.parseCoin("0.1")),*/
/* TRANSFER_WISE = new PaymentMethod(TRANSFER_WISE_ID, 0, DAY, Coin.parseCoin("0.1")),*/
/* US_POSTAL_MONEY_ORDER = new PaymentMethod(US_POSTAL_MONEY_ORDER_ID, 0, DAY, Coin.parseCoin("0.1")),*/
BLOCK_CHAINS = new PaymentMethod(BLOCK_CHAINS_ID, 0, DAY, Coin.parseCoin("0.5"))
BLOCK_CHAINS = new PaymentMethod(BLOCK_CHAINS_ID, 0, DAY, Coin.parseCoin("1"))
));

View file

@ -1,36 +0,0 @@
package io.bitsquare.trade;
import io.bitsquare.btc.AddressEntry;
import io.bitsquare.btc.WalletService;
import io.bitsquare.trade.offer.OpenOfferManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class TradableCollections {
private static final Logger log = LoggerFactory.getLogger(TradableCollections.class);
public static List<AddressEntry> getAddressEntriesForAvailableBalance(OpenOfferManager openOfferManager, TradeManager tradeManager, WalletService walletService) {
Set<String> reservedTrades = getNotCompletedTradableItems(openOfferManager, tradeManager).stream()
.map(tradable -> tradable.getOffer().getId())
.collect(Collectors.toSet());
List<AddressEntry> list = new ArrayList<>();
list.addAll(walletService.getAddressEntryList().stream()
.filter(e -> walletService.getBalanceForAddress(e.getAddress()).isPositive())
.filter(e -> !reservedTrades.contains(e.getOfferId()))
.collect(Collectors.toList()));
return list;
}
public static Set<Tradable> getNotCompletedTradableItems(OpenOfferManager openOfferManager, TradeManager tradeManager) {
return Stream.concat(openOfferManager.getOpenOffers().stream(), tradeManager.getTrades().stream())
.filter(tradable -> !(tradable instanceof Trade) || ((Trade) tradable).getState().getPhase() != Trade.Phase.PAYOUT_PAID)
.collect(Collectors.toSet());
}
}

View file

@ -0,0 +1,110 @@
package io.bitsquare.trade;
import io.bitsquare.btc.AddressEntry;
import io.bitsquare.btc.FeePolicy;
import io.bitsquare.btc.WalletService;
import io.bitsquare.trade.closed.ClosedTradableManager;
import io.bitsquare.trade.failed.FailedTradesManager;
import io.bitsquare.trade.offer.Offer;
import io.bitsquare.trade.offer.OpenOffer;
import io.bitsquare.trade.offer.OpenOfferManager;
import org.bitcoinj.core.Coin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class TradableHelper {
private static final Logger log = LoggerFactory.getLogger(TradableHelper.class);
public static List<AddressEntry> getAddressEntriesForAvailableBalance(OpenOfferManager openOfferManager, TradeManager tradeManager, WalletService walletService) {
Set<String> reservedTradeIds = getNotCompletedTradableItems(openOfferManager, tradeManager).stream()
.map(tradable -> tradable.getOffer().getId())
.collect(Collectors.toSet());
return walletService.getAddressEntryList().stream()
.filter(e -> walletService.getBalanceForAddress(e.getAddress()).isPositive())
.filter(e -> !reservedTradeIds.contains(e.getOfferId()))
.collect(Collectors.toList());
}
public static Set<Tradable> getNotCompletedTradableItems(OpenOfferManager openOfferManager, TradeManager tradeManager) {
return Stream.concat(openOfferManager.getOpenOffers().stream(), tradeManager.getTrades().stream())
.filter(tradable -> !(tradable instanceof Trade) || ((Trade) tradable).getState().getPhase() != Trade.Phase.PAYOUT_PAID)
.collect(Collectors.toSet());
}
public static Coin getBalanceInOpenOffer(OpenOffer openOffer) {
Offer offer = openOffer.getOffer();
Coin balance = FeePolicy.getSecurityDeposit().add(FeePolicy.getFeePerKb());
// For the seller we add the trade amount
if (offer.getDirection() == Offer.Direction.SELL)
balance = balance.add(offer.getAmount());
return balance;
}
public static Coin getBalanceInTrade(Trade trade, WalletService walletService) {
AddressEntry addressEntry = walletService.getTradeAddressEntry(trade.getId());
Coin balance = FeePolicy.getSecurityDeposit().add(FeePolicy.getFeePerKb());
// For the seller we add the trade amount
if (trade.getContract() != null &&
trade.getTradeAmount() != null &&
trade.getContract().getSellerPayoutAddressString().equals(addressEntry.getAddressString()))
balance = balance.add(trade.getTradeAmount());
return balance;
}
public static Coin getAvailableBalance(AddressEntry addressEntry, WalletService walletService,
OpenOfferManager openOfferManager, TradeManager tradeManager,
ClosedTradableManager closedTradableManager,
FailedTradesManager failedTradesManager) {
Coin balance;
Coin totalBalance = walletService.getBalanceForAddress(addressEntry.getAddress());
String id = addressEntry.getOfferId();
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(id);
Optional<Trade> tradeOptional = tradeManager.getTradeById(id);
Optional<Tradable> closedTradableOptional = closedTradableManager.getTradableById(id);
Optional<Trade> failedTradesOptional = failedTradesManager.getTradeById(id);
if (openOfferOptional.isPresent()) {
balance = totalBalance.subtract(TradableHelper.getBalanceInOpenOffer(openOfferOptional.get()));
} else if (tradeOptional.isPresent()) {
Trade trade = tradeOptional.get();
if (trade.getState().getPhase() != Trade.Phase.PAYOUT_PAID)
balance = totalBalance.subtract(TradableHelper.getBalanceInTrade(trade, walletService));
else
balance = totalBalance;
} else if (closedTradableOptional.isPresent()) {
Tradable tradable = closedTradableOptional.get();
Coin balanceInTrade = Coin.ZERO;
if (tradable instanceof OpenOffer)
balanceInTrade = TradableHelper.getBalanceInOpenOffer((OpenOffer) tradable);
else if (tradable instanceof Trade)
balanceInTrade = TradableHelper.getBalanceInTrade((Trade) tradable, walletService);
balance = totalBalance.subtract(balanceInTrade);
} else if (failedTradesOptional.isPresent()) {
balance = totalBalance.subtract(TradableHelper.getBalanceInTrade(failedTradesOptional.get(), walletService));
} else {
balance = totalBalance;
}
return balance;
}
public static Coin getReservedBalance(Tradable tradable, WalletService walletService) {
AddressEntry addressEntry = walletService.getTradeAddressEntry(tradable.getId());
Coin balance = walletService.getBalanceForAddress(addressEntry.getAddress());
if (tradable instanceof Trade) {
Trade trade = (Trade) tradable;
if (trade.getState().getPhase().ordinal() < Trade.Phase.PAYOUT_PAID.ordinal())
balance = TradableHelper.getBalanceInTrade(trade, walletService);
} else if (tradable instanceof OpenOffer) {
balance = TradableHelper.getBalanceInOpenOffer((OpenOffer) tradable);
}
return balance;
}
}

View file

@ -147,7 +147,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
public void closeAllOpenOffers(@Nullable Runnable completeHandler) {
openOffers.forEach(openOffer -> offerBookService.removeOfferAtShutDown(openOffer.getOffer()));
if (completeHandler != null)
UserThread.runAfter(completeHandler::run, openOffers.size() * 200 + 300, TimeUnit.MILLISECONDS);
UserThread.runAfter(completeHandler::run, openOffers.size() * 100 + 200, TimeUnit.MILLISECONDS);
}
///////////////////////////////////////////////////////////////////////////////////////////