diff --git a/README.md b/README.md index e475fb3..1101cbd 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ To release the app follow the steps. * in build.gradle the package from "com.coinomi.wallet.dev" to "com.coinomi.wallet" * in AndroidManifest.xml the android:icon to "ic_launcher" and all "com.coinomi.wallet.dev.*" to "com.coinomi.wallet.*" * remove all ic_launcher_dev icons with `rm wallet/src/main/res/drawable*/ic_launcher_dev.png` -* setup ACRA +* setup ACRA and ShapeShift 2) Then in the Android Studio go to: diff --git a/core/build.gradle b/core/build.gradle index 302de7c..38ba2b6 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -9,21 +9,22 @@ dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.google.guava:guava:17.0' - // compile 'com.google:bitcoinj:0.11.2' compile 'com.google.code.findbugs:jsr305:1.3.9' compile 'com.madgag.spongycastle:core:1.51.0.0' compile 'com.lambdaworks:scrypt:1.4.0' compile 'com.google.protobuf:protobuf-java:2.5.0' - // compile 'net.jcip:jcip-annotations:1.0' compile 'org.slf4j:slf4j-jdk14:1.7.6' // compile 'org.json:json:20140107' compile 'org.json:json:20080701' + compile 'com.squareup.okhttp:okhttp:2.3.0' + testCompile 'junit:junit:4.11' testCompile 'org.mockito:mockito-all:1.9.5' + testCompile 'com.squareup.okhttp:mockwebserver:2.3.0' } sourceSets { diff --git a/core/libs/bitcoinj-core-0.12.2-COINOMI-2.jar b/core/libs/bitcoinj-core-0.12.2-COINOMI-3.jar similarity index 92% rename from core/libs/bitcoinj-core-0.12.2-COINOMI-2.jar rename to core/libs/bitcoinj-core-0.12.2-COINOMI-3.jar index 213d497..c4aeb07 100644 Binary files a/core/libs/bitcoinj-core-0.12.2-COINOMI-2.jar and b/core/libs/bitcoinj-core-0.12.2-COINOMI-3.jar differ diff --git a/core/src/main/java/com/coinomi/core/coins/BitcoinMain.java b/core/src/main/java/com/coinomi/core/coins/BitcoinMain.java index f512346..f85fbb1 100644 --- a/core/src/main/java/com/coinomi/core/coins/BitcoinMain.java +++ b/core/src/main/java/com/coinomi/core/coins/BitcoinMain.java @@ -20,9 +20,9 @@ private BitcoinMain() { uriScheme = "bitcoin"; bip44Index = 0; unitExponent = 8; - feePerKb = Coin.valueOf(10000); - minNonDust = Coin.valueOf(5460); - softDustLimit = Coin.valueOf(1000000); // 0.01 BTC + feePerKb = value(10000); + minNonDust = value(5460); + softDustLimit = value(1000000); // 0.01 BTC softDustPolicy = SoftDustPolicy.AT_LEAST_BASE_FEE_IF_SOFT_DUST_TXO_PRESENT; } diff --git a/core/src/main/java/com/coinomi/core/coins/BitcoinTest.java b/core/src/main/java/com/coinomi/core/coins/BitcoinTest.java index 9c92f5b..82eb356 100644 --- a/core/src/main/java/com/coinomi/core/coins/BitcoinTest.java +++ b/core/src/main/java/com/coinomi/core/coins/BitcoinTest.java @@ -16,13 +16,13 @@ private BitcoinTest() { dumpedPrivateKeyHeader = 239; name = "Bitcoin Test"; - symbol = "BTCTEST"; + symbol = "BTCt"; uriScheme = "bitcoin"; bip44Index = 1; unitExponent = 8; - feePerKb = Coin.valueOf(10000); - minNonDust = Coin.valueOf(5460); - softDustLimit = Coin.valueOf(1000000); // 0.01 BTC + feePerKb = value(10000); + minNonDust = value(5460); + softDustLimit = value(1000000); // 0.01 BTC softDustPolicy = SoftDustPolicy.AT_LEAST_BASE_FEE_IF_SOFT_DUST_TXO_PRESENT; } diff --git a/core/src/main/java/com/coinomi/core/coins/BlackcoinMain.java b/core/src/main/java/com/coinomi/core/coins/BlackcoinMain.java index dac156f..47a6d9e 100644 --- a/core/src/main/java/com/coinomi/core/coins/BlackcoinMain.java +++ b/core/src/main/java/com/coinomi/core/coins/BlackcoinMain.java @@ -19,9 +19,9 @@ private BlackcoinMain() { uriScheme = "blackcoin"; bip44Index = 10; unitExponent = 8; - feePerKb = Coin.valueOf(10000); // 0.0001 BLK - minNonDust = Coin.valueOf(1); - softDustLimit = Coin.valueOf(1000000); // 0.01 BLK + feePerKb = value(10000); // 0.0001 BLK + minNonDust = value(1); + softDustLimit = value(1000000); // 0.01 BLK softDustPolicy = SoftDustPolicy.AT_LEAST_BASE_FEE_IF_SOFT_DUST_TXO_PRESENT; } diff --git a/core/src/main/java/com/coinomi/core/coins/CannacoinMain.java b/core/src/main/java/com/coinomi/core/coins/CannacoinMain.java index 4f2af34..941ecda 100644 --- a/core/src/main/java/com/coinomi/core/coins/CannacoinMain.java +++ b/core/src/main/java/com/coinomi/core/coins/CannacoinMain.java @@ -21,9 +21,9 @@ private CannacoinMain() { uriScheme = "cannacoin"; bip44Index = 19; unitExponent = 8; - feePerKb = Coin.valueOf(100000); - minNonDust = Coin.valueOf(1000000); - softDustLimit = Coin.valueOf(100000000); + feePerKb = value(100000); + minNonDust = value(1000000); + softDustLimit = value(100000000); softDustPolicy = SoftDustPolicy.BASE_FEE_FOR_EACH_SOFT_DUST_TXO; } diff --git a/core/src/main/java/com/coinomi/core/coins/CoinID.java b/core/src/main/java/com/coinomi/core/coins/CoinID.java index 09e1474..3f5b5be 100644 --- a/core/src/main/java/com/coinomi/core/coins/CoinID.java +++ b/core/src/main/java/com/coinomi/core/coins/CoinID.java @@ -10,6 +10,7 @@ import com.coinomi.core.uri.CoinURIParseException; import com.google.common.collect.ImmutableList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -36,8 +37,10 @@ public enum CoinID { URO_MAIN(UroMain.get()), DIGITALCOIN_MAIN(DigitalcoinMain.get()), CANNACOIN_MAIN(CannacoinMain.get()), - DIGIBYTE_MAIN(DigibyteMain.get()) - ; + DIGIBYTE_MAIN(DigibyteMain.get()),; + + private static HashMap idLookup = new HashMap<>(); + private static HashMap symbolLookup = new HashMap<>(); static { Set bitcoinjNetworks = Networks.get(); @@ -49,14 +52,18 @@ public enum CoinID { Networks.register(id.type); } - // Test if currency codes are unique - HashSet codes = new HashSet<>(); for (CoinID id : values()) { - if (codes.contains(id.type.symbol)) { + if (symbolLookup.containsKey(id.type.symbol)) { throw new IllegalStateException( "Coin currency codes must be unique, double found: " + id.type.symbol); } - codes.add(id.type.symbol); + symbolLookup.put(id.type.symbol, id.type); + + if (idLookup.containsKey(id.type.getId())) { + throw new IllegalStateException( + "Coin IDs must be unique, double found: " + id.type.getId()); + } + idLookup.put(id.type.getId(), id.type); } } @@ -76,29 +83,22 @@ public CoinType getCoinType() { } public static List getSupportedCoins() { - ImmutableList.Builder builder = ImmutableList.builder(); - for (CoinID id : values()) { - builder.add(id.type); - } - return builder.build(); + return ImmutableList.copyOf(idLookup.values()); } public static CoinType typeFromId(String stringId) { - return fromId(stringId).type; - } - - public static CoinID fromId(String stringId) { - for(CoinID id : values()) { - if (id.type.getId().equalsIgnoreCase(stringId)) return id; + if (idLookup.containsKey(stringId)) { + return idLookup.get(stringId); + } else { + throw new IllegalArgumentException("Unsupported ID: " + stringId); } - throw new IllegalArgumentException("Unsupported ID: " + stringId); } public static CoinID fromUri(String input) { - for(CoinID id : values()) { + for (CoinID id : values()) { if (input.startsWith(id.getCoinType().getUriScheme() + "://")) { return id; - } else if (input.startsWith(id.getCoinType().getUriScheme()+":")) { + } else if (input.startsWith(id.getCoinType().getUriScheme() + ":")) { return id; } } @@ -114,10 +114,15 @@ public static CoinType typeFromAddress(String address) throws AddressFormatExcep } } + public static boolean isSymbolSupported(String symbol) { + return symbolLookup.containsKey(symbol); + } + public static CoinType typeFromSymbol(String symbol) { - for(CoinID id : values()) { - if (id.type.getSymbol().equalsIgnoreCase(symbol)) return id.type; + if (symbolLookup.containsKey(symbol.toUpperCase())) { + return symbolLookup.get(symbol.toUpperCase()); + } else { + throw new IllegalArgumentException("Unsupported coin symbol: " + symbol); } - throw new IllegalArgumentException("Unsupported coin symbol: " + symbol); } } \ No newline at end of file diff --git a/core/src/main/java/com/coinomi/core/coins/CoinType.java b/core/src/main/java/com/coinomi/core/coins/CoinType.java index d9a9c09..8a11bda 100644 --- a/core/src/main/java/com/coinomi/core/coins/CoinType.java +++ b/core/src/main/java/com/coinomi/core/coins/CoinType.java @@ -3,6 +3,8 @@ import com.coinomi.core.util.MonetaryFormat; +import org.bitcoinj.core.Address; +import org.bitcoinj.core.AddressFormatException; import org.bitcoinj.core.Coin; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.crypto.ChildNumber; @@ -28,14 +30,14 @@ abstract public class CoinType extends NetworkParameters implements ValueType, S protected String uriScheme; protected Integer bip44Index; protected Integer unitExponent; - protected Coin feePerKb; - protected Coin minNonDust; - protected Coin softDustLimit; + protected Value feePerKb; + protected Value minNonDust; + protected Value softDustLimit; protected SoftDustPolicy softDustPolicy; - private MonetaryFormat friendlyFormat; - private MonetaryFormat plainFormat; - private Value oneCoin; + private transient MonetaryFormat friendlyFormat; + private transient MonetaryFormat plainFormat; + private transient Value oneCoin; @Override public String getName() { @@ -60,15 +62,31 @@ public int getUnitExponent() { return checkNotNull(unitExponent, "A coin failed to set a unit exponent"); } + @Deprecated public Coin getFeePerKb() { + return feePerKb().toCoin(); + } + + public Value feePerKb() { return checkNotNull(feePerKb, "A coin failed to set a fee per kilobyte"); } + @Deprecated public Coin getMinNonDust() { + return minNonDust().toCoin(); + } + + @Override + public Value minNonDust() { return checkNotNull(minNonDust, "A coin failed to set a minimum amount to be considered not dust"); } + @Deprecated public Coin getSoftDustLimit() { + return softDustLimit().toCoin(); + } + + public Value softDustLimit() { return checkNotNull(softDustLimit, "A coin failed to set a soft dust limit"); } @@ -81,6 +99,10 @@ public List getBip44Path(int account) { return HDUtils.parsePath(path); } + public Address address(String addressStr) throws AddressFormatException { + return new Address(this, addressStr); + } + /** * Returns a 1 coin of this type with the correct amount of units (satoshis) * Use {@link com.coinomi.core.coins.CoinType:oneCoin} @@ -100,6 +122,21 @@ public Value oneCoin() { return oneCoin; } + @Override + public Value value(String string) { + return Value.parse(this, string); + } + + @Override + public Value value(Coin coin) { + return Value.valueOf(this, coin); + } + + @Override + public Value value(long units) { + return Value.valueOf(this, units); + } + @Override public String getPaymentProtocolId() { throw new RuntimeException("Method not implemented"); @@ -118,7 +155,7 @@ public String toString() { public MonetaryFormat getMonetaryFormat() { if (friendlyFormat == null) { friendlyFormat = new MonetaryFormat() - .shift(0).minDecimals(2).noCode().code(0, symbol).postfixCode(); + .shift(0).minDecimals(2).code(0, symbol).postfixCode(); switch (unitExponent) { case 8: friendlyFormat = friendlyFormat.optionalDecimals(2, 2, 2); diff --git a/core/src/main/java/com/coinomi/core/coins/DarkcoinMain.java b/core/src/main/java/com/coinomi/core/coins/DarkcoinMain.java index b05abda..0c8fb74 100644 --- a/core/src/main/java/com/coinomi/core/coins/DarkcoinMain.java +++ b/core/src/main/java/com/coinomi/core/coins/DarkcoinMain.java @@ -19,9 +19,9 @@ private DarkcoinMain() { uriScheme = "darkcoin"; bip44Index = 5; unitExponent = 8; - feePerKb = Coin.valueOf(100000); - minNonDust = Coin.valueOf(1000); // 0.00001 DRK mininput - softDustLimit = Coin.valueOf(100000); // 0.001 DRK + feePerKb = value(100000); + minNonDust = value(1000); // 0.00001 DRK mininput + softDustLimit = value(100000); // 0.001 DRK softDustPolicy = SoftDustPolicy.BASE_FEE_FOR_EACH_SOFT_DUST_TXO; } diff --git a/core/src/main/java/com/coinomi/core/coins/DarkcoinTest.java b/core/src/main/java/com/coinomi/core/coins/DarkcoinTest.java index d2e7c3e..18e1c6f 100644 --- a/core/src/main/java/com/coinomi/core/coins/DarkcoinTest.java +++ b/core/src/main/java/com/coinomi/core/coins/DarkcoinTest.java @@ -19,9 +19,9 @@ private DarkcoinTest() { uriScheme = "darkcoin"; bip44Index = 1; unitExponent = 8; - feePerKb = Coin.valueOf(100000); - minNonDust = Coin.valueOf(1000); // 0.00001 DRK mininput - softDustLimit = Coin.valueOf(100000); // 0.001 DRK + feePerKb = value(100000); + minNonDust = value(1000); // 0.00001 DRK mininput + softDustLimit = value(100000); // 0.001 DRK softDustPolicy = SoftDustPolicy.BASE_FEE_FOR_EACH_SOFT_DUST_TXO; } diff --git a/core/src/main/java/com/coinomi/core/coins/DigibyteMain.java b/core/src/main/java/com/coinomi/core/coins/DigibyteMain.java index 0f7f6f3..b42cdc3 100644 --- a/core/src/main/java/com/coinomi/core/coins/DigibyteMain.java +++ b/core/src/main/java/com/coinomi/core/coins/DigibyteMain.java @@ -20,9 +20,9 @@ private DigibyteMain() { uriScheme = "digibyte"; bip44Index = 20; unitExponent = 8; - feePerKb = Coin.valueOf(10000); - minNonDust = Coin.valueOf(5460); - softDustLimit = Coin.valueOf(100000); + feePerKb = value(10000); + minNonDust = value(5460); + softDustLimit = value(100000); softDustPolicy = SoftDustPolicy.BASE_FEE_FOR_EACH_SOFT_DUST_TXO; } diff --git a/core/src/main/java/com/coinomi/core/coins/DigitalcoinMain.java b/core/src/main/java/com/coinomi/core/coins/DigitalcoinMain.java index 4c70830..6960b07 100644 --- a/core/src/main/java/com/coinomi/core/coins/DigitalcoinMain.java +++ b/core/src/main/java/com/coinomi/core/coins/DigitalcoinMain.java @@ -19,9 +19,9 @@ private DigitalcoinMain() { uriScheme = "digitalcoin"; bip44Index = 18; unitExponent = 8; - feePerKb = Coin.valueOf(10000); // 0.0001 DGC - minNonDust = Coin.valueOf(1); - softDustLimit = Coin.valueOf(1000000); // 0.01 DGC + feePerKb = value(10000); // 0.0001 DGC + minNonDust = value(1); + softDustLimit = value(1000000); // 0.01 DGC softDustPolicy = SoftDustPolicy.BASE_FEE_FOR_EACH_SOFT_DUST_TXO; } diff --git a/core/src/main/java/com/coinomi/core/coins/DogecoinMain.java b/core/src/main/java/com/coinomi/core/coins/DogecoinMain.java index 2604e64..2eed712 100644 --- a/core/src/main/java/com/coinomi/core/coins/DogecoinMain.java +++ b/core/src/main/java/com/coinomi/core/coins/DogecoinMain.java @@ -19,9 +19,9 @@ private DogecoinMain() { uriScheme = "dogecoin"; bip44Index = 3; unitExponent = 8; - feePerKb = Coin.valueOf(100000000L); - minNonDust = Coin.valueOf(1); - softDustLimit = Coin.valueOf(100000000L); // 1 DOGE + feePerKb = value(100000000L); + minNonDust = value(1); + softDustLimit = value(100000000L); // 1 DOGE softDustPolicy = SoftDustPolicy.BASE_FEE_FOR_EACH_SOFT_DUST_TXO; } diff --git a/core/src/main/java/com/coinomi/core/coins/DogecoinTest.java b/core/src/main/java/com/coinomi/core/coins/DogecoinTest.java index d046935..82aee01 100644 --- a/core/src/main/java/com/coinomi/core/coins/DogecoinTest.java +++ b/core/src/main/java/com/coinomi/core/coins/DogecoinTest.java @@ -15,13 +15,13 @@ private DogecoinTest() { spendableCoinbaseDepth = 240; // COINBASE_MATURITY_NEW name = "Dogecoin Test"; - symbol = "DOGETEST"; + symbol = "DOGEt"; uriScheme = "dogecoin"; bip44Index = 1; unitExponent = 8; - feePerKb = Coin.valueOf(100000000L); - minNonDust = Coin.valueOf(1); - softDustLimit = Coin.valueOf(100000000L); // 1 DOGE + feePerKb = value(100000000L); + minNonDust = value(1); + softDustLimit = value(100000000L); // 1 DOGE softDustPolicy = SoftDustPolicy.BASE_FEE_FOR_EACH_SOFT_DUST_TXO; } diff --git a/core/src/main/java/com/coinomi/core/coins/FeathercoinMain.java b/core/src/main/java/com/coinomi/core/coins/FeathercoinMain.java index 9e79aff..00fcd4e 100644 --- a/core/src/main/java/com/coinomi/core/coins/FeathercoinMain.java +++ b/core/src/main/java/com/coinomi/core/coins/FeathercoinMain.java @@ -20,9 +20,9 @@ private FeathercoinMain() { uriScheme = "feathercoin"; bip44Index = 8; unitExponent = 8; - feePerKb = Coin.valueOf(2000000); - minNonDust = Coin.valueOf(1000); // 0.00001 FTC mininput - softDustLimit = Coin.valueOf(100000); // 0.001 FTC + feePerKb = value(2000000); + minNonDust = value(1000); // 0.00001 FTC mininput + softDustLimit = value(100000); // 0.001 FTC softDustPolicy = SoftDustPolicy.BASE_FEE_FOR_EACH_SOFT_DUST_TXO; } diff --git a/core/src/main/java/com/coinomi/core/coins/FiatType.java b/core/src/main/java/com/coinomi/core/coins/FiatType.java index 961de6c..94ead42 100644 --- a/core/src/main/java/com/coinomi/core/coins/FiatType.java +++ b/core/src/main/java/com/coinomi/core/coins/FiatType.java @@ -3,6 +3,8 @@ import com.coinomi.core.util.Currencies; import com.coinomi.core.util.MonetaryFormat; +import org.bitcoinj.core.Coin; + import java.math.BigInteger; import java.util.HashMap; @@ -20,12 +22,12 @@ public class FiatType implements ValueType { public static final MonetaryFormat FRIENDLY_FORMAT = new MonetaryFormat().noCode() .minDecimals(2).optionalDecimals(2, 2, 2).postfixCode(); - private static final HashMap types = new HashMap(); + private static final HashMap types = new HashMap<>(); private final String name; private final String currencyCode; - private Value oneCoin; - private MonetaryFormat friendlyFormat; + private transient Value oneCoin; + private transient MonetaryFormat friendlyFormat; public FiatType(final String currencyCode, @Nullable final String name) { this.name = name != null ? name : ""; @@ -63,6 +65,26 @@ public Value oneCoin() { return oneCoin; } + @Override + public Value minNonDust() { + return value(1); + } + + @Override + public Value value(Coin coin) { + return Value.valueOf(this, coin); + } + + @Override + public Value value(long units) { + return Value.valueOf(this, units); + } + + @Override + public Value value(String string) { + return Value.parse(this, string); + } + @Override public MonetaryFormat getMonetaryFormat() { if (friendlyFormat == null) { diff --git a/core/src/main/java/com/coinomi/core/coins/FiatValue.java b/core/src/main/java/com/coinomi/core/coins/FiatValue.java index 1282f46..d4ef7f1 100644 --- a/core/src/main/java/com/coinomi/core/coins/FiatValue.java +++ b/core/src/main/java/com/coinomi/core/coins/FiatValue.java @@ -1,6 +1,5 @@ package com.coinomi.core.coins; -import org.bitcoinj.core.Coin; import org.bitcoinj.utils.Fiat; import static com.google.common.base.Preconditions.checkArgument; @@ -33,6 +32,6 @@ public static Value valueOf(final Fiat fiat) { * @throws IllegalArgumentException if you try to specify fractional units, or a value out of range. */ public static Value parse(final String currencyCode, final String str) { - return Value.parseValue(FiatType.get(currencyCode), str); + return Value.parse(FiatType.get(currencyCode), str); } } diff --git a/core/src/main/java/com/coinomi/core/coins/LitecoinMain.java b/core/src/main/java/com/coinomi/core/coins/LitecoinMain.java index 0dcdfe7..c108b57 100644 --- a/core/src/main/java/com/coinomi/core/coins/LitecoinMain.java +++ b/core/src/main/java/com/coinomi/core/coins/LitecoinMain.java @@ -20,9 +20,9 @@ private LitecoinMain() { uriScheme = "litecoin"; bip44Index = 2; unitExponent = 8; - feePerKb = Coin.valueOf(100000); - minNonDust = Coin.valueOf(1000); // 0.00001 LTC mininput - softDustLimit = Coin.valueOf(100000); // 0.001 LTC + feePerKb = value(100000); + minNonDust = value(1000); // 0.00001 LTC mininput + softDustLimit = value(100000); // 0.001 LTC softDustPolicy = SoftDustPolicy.BASE_FEE_FOR_EACH_SOFT_DUST_TXO; } diff --git a/core/src/main/java/com/coinomi/core/coins/LitecoinTest.java b/core/src/main/java/com/coinomi/core/coins/LitecoinTest.java index ed71f8f..edd0daf 100644 --- a/core/src/main/java/com/coinomi/core/coins/LitecoinTest.java +++ b/core/src/main/java/com/coinomi/core/coins/LitecoinTest.java @@ -15,13 +15,13 @@ private LitecoinTest() { spendableCoinbaseDepth = 100; name = "Litecoin Test"; - symbol = "LTCTEST"; + symbol = "LTCt"; uriScheme = "litecoin"; bip44Index = 1; unitExponent = 8; - feePerKb = Coin.valueOf(100000); - minNonDust = Coin.valueOf(1000); // 0.00001 LTC mininput - softDustLimit = Coin.valueOf(100000); // 0.001 LTC + feePerKb = value(100000); + minNonDust = value(1000); // 0.00001 LTC mininput + softDustLimit = value(100000); // 0.001 LTC softDustPolicy = SoftDustPolicy.BASE_FEE_FOR_EACH_SOFT_DUST_TXO; } diff --git a/core/src/main/java/com/coinomi/core/coins/MonacoinMain.java b/core/src/main/java/com/coinomi/core/coins/MonacoinMain.java index b865b83..09f6da8 100644 --- a/core/src/main/java/com/coinomi/core/coins/MonacoinMain.java +++ b/core/src/main/java/com/coinomi/core/coins/MonacoinMain.java @@ -20,8 +20,8 @@ private MonacoinMain() { bip44Index = 12; unitExponent = 8; // TODO set correct values - feePerKb = Coin.valueOf(1); - minNonDust = Coin.valueOf(1000); + feePerKb = value(1); + minNonDust = value(1000); throw new RuntimeException(name+" bip44Index " + bip44Index + "is not standardized"); } diff --git a/core/src/main/java/com/coinomi/core/coins/NamecoinMain.java b/core/src/main/java/com/coinomi/core/coins/NamecoinMain.java index 444dfdc..c1fec72 100644 --- a/core/src/main/java/com/coinomi/core/coins/NamecoinMain.java +++ b/core/src/main/java/com/coinomi/core/coins/NamecoinMain.java @@ -19,9 +19,9 @@ private NamecoinMain() { uriScheme = "namecoin"; bip44Index = 7; unitExponent = 8; - feePerKb = Coin.valueOf(500000); - minNonDust = Coin.valueOf(10000); // 0.0001 NMC mininput - softDustLimit = Coin.valueOf(1000000); // 0.01 NMC + feePerKb = value(500000); + minNonDust = value(10000); // 0.0001 NMC mininput + softDustLimit = value(1000000); // 0.01 NMC softDustPolicy = SoftDustPolicy.BASE_FEE_FOR_EACH_SOFT_DUST_TXO; } diff --git a/core/src/main/java/com/coinomi/core/coins/NuBitsMain.java b/core/src/main/java/com/coinomi/core/coins/NuBitsMain.java index 982a469..ad0e638 100644 --- a/core/src/main/java/com/coinomi/core/coins/NuBitsMain.java +++ b/core/src/main/java/com/coinomi/core/coins/NuBitsMain.java @@ -19,9 +19,9 @@ private NuBitsMain() { uriScheme = "nubits"; bip44Index = 12; unitExponent = 4; - feePerKb = Coin.valueOf(100); // 0.01NBT, careful NuBits has 10000 units per coin - minNonDust = Coin.valueOf(100); - softDustLimit = Coin.valueOf(100); // 0.01NBT, careful NuBits has 10000 units per coin + feePerKb = value(100); // 0.01NBT, careful NuBits has 10000 units per coin + minNonDust = value(100); + softDustLimit = value(100); // 0.01NBT, careful NuBits has 10000 units per coin softDustPolicy = SoftDustPolicy.AT_LEAST_BASE_FEE_IF_SOFT_DUST_TXO_PRESENT; } diff --git a/core/src/main/java/com/coinomi/core/coins/NuSharesMain.java b/core/src/main/java/com/coinomi/core/coins/NuSharesMain.java index 8f8b720..2952e22 100644 --- a/core/src/main/java/com/coinomi/core/coins/NuSharesMain.java +++ b/core/src/main/java/com/coinomi/core/coins/NuSharesMain.java @@ -19,9 +19,9 @@ private NuSharesMain() { uriScheme = "nushares"; bip44Index = 11; unitExponent = 4; - feePerKb = Coin.valueOf(10000); // 1NSR, careful NuBits has 10000 units per coin - minNonDust = Coin.valueOf(1); - softDustLimit = Coin.valueOf(10000); // 1NSR, careful NuBits has 10000 units per coin + feePerKb = value(10000); // 1NSR, careful NuBits has 10000 units per coin + minNonDust = value(10000); + softDustLimit = value(10000); // 1NSR, careful NuBits has 10000 units per coin softDustPolicy = SoftDustPolicy.AT_LEAST_BASE_FEE_IF_SOFT_DUST_TXO_PRESENT; } diff --git a/core/src/main/java/com/coinomi/core/coins/PeercoinMain.java b/core/src/main/java/com/coinomi/core/coins/PeercoinMain.java index bacb85f..42c29ea 100644 --- a/core/src/main/java/com/coinomi/core/coins/PeercoinMain.java +++ b/core/src/main/java/com/coinomi/core/coins/PeercoinMain.java @@ -19,9 +19,9 @@ private PeercoinMain() { uriScheme = "peercoin"; // TODO verify, could be ppcoin? bip44Index = 6; unitExponent = 6; - feePerKb = Coin.valueOf(10000); // 0.01PPC, careful Peercoin has 1000000 units per coin - minNonDust = Coin.valueOf(1); - softDustLimit = Coin.valueOf(10000); // 0.01PPC, careful Peercoin has 1000000 units per coin + feePerKb = value(10000); // 0.01PPC, careful Peercoin has 1000000 units per coin + minNonDust = value(1); + softDustLimit = value(10000); // 0.01PPC, careful Peercoin has 1000000 units per coin softDustPolicy = SoftDustPolicy.AT_LEAST_BASE_FEE_IF_SOFT_DUST_TXO_PRESENT; } diff --git a/core/src/main/java/com/coinomi/core/coins/PeercoinTest.java b/core/src/main/java/com/coinomi/core/coins/PeercoinTest.java index c9e95c1..af46a4d 100644 --- a/core/src/main/java/com/coinomi/core/coins/PeercoinTest.java +++ b/core/src/main/java/com/coinomi/core/coins/PeercoinTest.java @@ -19,9 +19,9 @@ private PeercoinTest() { uriScheme = "peercoin"; // TODO verify, could be ppcoin? bip44Index = 1; unitExponent = 6; - feePerKb = Coin.valueOf(10000); // 0.01PPC, careful Peercoin has 1000000 units per coin - minNonDust = Coin.valueOf(1); - softDustLimit = Coin.valueOf(10000); // 0.01PPC, careful Peercoin has 1000000 units per coin + feePerKb = value(10000); // 0.01PPC, careful Peercoin has 1000000 units per coin + minNonDust = value(1); + softDustLimit = value(10000); // 0.01PPC, careful Peercoin has 1000000 units per coin softDustPolicy = SoftDustPolicy.AT_LEAST_BASE_FEE_IF_SOFT_DUST_TXO_PRESENT; } diff --git a/core/src/main/java/com/coinomi/core/coins/ReddcoinMain.java b/core/src/main/java/com/coinomi/core/coins/ReddcoinMain.java index e34cd11..c132d94 100644 --- a/core/src/main/java/com/coinomi/core/coins/ReddcoinMain.java +++ b/core/src/main/java/com/coinomi/core/coins/ReddcoinMain.java @@ -21,9 +21,9 @@ private ReddcoinMain() { uriScheme = "reddcoin"; bip44Index = 4; unitExponent = 8; - feePerKb = Coin.valueOf(100000); - minNonDust = Coin.valueOf(1000000); // 0.01 RDD mininput - softDustLimit = Coin.valueOf(100000000); // 1 RDD + feePerKb = value(100000); + minNonDust = value(1000000); // 0.01 RDD mininput + softDustLimit = value(100000000); // 1 RDD softDustPolicy = SoftDustPolicy.BASE_FEE_FOR_EACH_SOFT_DUST_TXO; } diff --git a/core/src/main/java/com/coinomi/core/coins/RubycoinMain.java b/core/src/main/java/com/coinomi/core/coins/RubycoinMain.java index 185175b..2e804f5 100644 --- a/core/src/main/java/com/coinomi/core/coins/RubycoinMain.java +++ b/core/src/main/java/com/coinomi/core/coins/RubycoinMain.java @@ -19,9 +19,9 @@ private RubycoinMain() { uriScheme = "rubycoin"; bip44Index = 16; unitExponent = 8; - feePerKb = Coin.valueOf(10000); // 0.0001 RBY - minNonDust = Coin.valueOf(1); - softDustLimit = Coin.valueOf(1000000); // 0.01 RBY + feePerKb = value(10000); // 0.0001 RBY + minNonDust = value(1); + softDustLimit = value(1000000); // 0.01 RBY softDustPolicy = SoftDustPolicy.AT_LEAST_BASE_FEE_IF_SOFT_DUST_TXO_PRESENT; } diff --git a/core/src/main/java/com/coinomi/core/coins/UroMain.java b/core/src/main/java/com/coinomi/core/coins/UroMain.java index 4f312f0..806a080 100644 --- a/core/src/main/java/com/coinomi/core/coins/UroMain.java +++ b/core/src/main/java/com/coinomi/core/coins/UroMain.java @@ -19,9 +19,9 @@ private UroMain() { uriScheme = "uro"; bip44Index = 17; unitExponent = 8; - feePerKb = Coin.valueOf(100000); - minNonDust = Coin.valueOf(1000); // 0.00001 URO mininput - softDustLimit = Coin.valueOf(100000); // 0.001 URO + feePerKb = value(100000); + minNonDust = value(1000); // 0.00001 URO mininput + softDustLimit = value(100000); // 0.001 URO softDustPolicy = SoftDustPolicy.BASE_FEE_FOR_EACH_SOFT_DUST_TXO; } diff --git a/core/src/main/java/com/coinomi/core/coins/Value.java b/core/src/main/java/com/coinomi/core/coins/Value.java index c1c4f10..4f71906 100644 --- a/core/src/main/java/com/coinomi/core/coins/Value.java +++ b/core/src/main/java/com/coinomi/core/coins/Value.java @@ -28,6 +28,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; /** * Represents a monetary value. This class is immutable. @@ -84,22 +85,40 @@ public static Value valueOf(final ValueType type, final int coins, final int cen } /** - * Parses an amount expressed in the way humans are used to.

- *

+ * Parses an amount expressed in the way humans are used to. + * * This takes string in a format understood by {@link BigDecimal#BigDecimal(String)}, * for example "0", "1", "0.10", "1.23E3", "1234.5E-5". * - * @throws IllegalArgumentException if you try to specify fractional units, or a value out of range. + * @throws IllegalArgumentException if you try to specify fractional units, or a value out of + * range. */ - public static Value parseValue(final ValueType type, final String str) { - return Value.valueOf(type, new BigDecimal(str).movePointRight(type.getUnitExponent()).toBigIntegerExact().longValue()); + public static Value parse(final ValueType type, final String str) { + return parse(type, new BigDecimal(str)); + } + + /** + * Parses a {@link BigDecimal} amount expressed in the way humans are used to. + * + * @throws IllegalArgumentException if you try to specify fractional units, or a value out of + * range. + */ + public static Value parse(final ValueType type, final BigDecimal decimal) { + return Value.valueOf(type, decimal.movePointRight(type.getUnitExponent()) + .toBigIntegerExact().longValue()); } public Value add(final Value value) { + checkArgument(type.equals(value.type), "Cannot add a different type"); + return new Value(this.type, LongMath.checkedAdd(this.value, value.value)); + } + + public Value add(final Coin value) { return new Value(this.type, LongMath.checkedAdd(this.value, value.value)); } public Value subtract(final Value value) { + checkArgument(type.equals(value.type), "Cannot subtract a different type"); return new Value(this.type, LongMath.checkedSubtract(this.value, value.value)); } @@ -112,10 +131,12 @@ public Value divide(final long divisor) { } public Value[] divideAndRemainder(final long divisor) { - return new Value[] { new Value(this.type, this.value / divisor), new Value(this.type, this.value % divisor) }; + return new Value[] { new Value(this.type, this.value / divisor), + new Value(this.type, this.value % divisor) }; } public long divide(final Value divisor) { + checkArgument(type.equals(divisor.type), "Cannot divide with a different type"); return this.value / divisor.value; } @@ -178,14 +199,6 @@ public Value negate() { return new Value(this.type, -this.value); } - /** - * Returns the number of units of this monetary value. It's deprecated in favour of accessing {@link #value} - * directly. - */ - public long longValue() { - return this.value; - } - /** * Returns the value as a 0.12 type string. More digits after the decimal place will be used * if necessary, but two will always be present. @@ -207,7 +220,7 @@ public String toPlainString() { @Override public String toString() { - return Long.toString(value); + return toPlainString() + type.getSymbol(); } @Override @@ -217,7 +230,7 @@ public boolean equals(final Object o) { if (o == null || o.getClass() != getClass()) return false; final Value other = (Value) o; - if (this.value != other.value) + if (this.value != other.value || !this.type.equals(other.type)) return false; return true; } @@ -229,8 +242,36 @@ public int hashCode() { @Override public int compareTo(@Nonnull final Value other) { + checkArgument(type.equals(other.type), "Cannot compare different types"); if (this.value == other.value) return 0; return this.value > other.value ? 1 : -1; } + + public boolean isDust() { + return compareTo(type.minNonDust()) < 0; + } + + public boolean isOfType(ValueType otherType) { + return type.equals(otherType); + } + + public boolean isOfType(Value otherValue) { + return type.equals(otherValue.type); + } + + /** + * Check if the value is within the [min, max] range + */ + public boolean within(Value min, Value max) { + return compareTo(min) >=0 && compareTo(max) <= 0; + } + + public static Value max(Value value1, Value value2) { + return value1.compareTo(value2) >= 0 ? value1 : value2; + } + + public static Value min(Value value1, Value value2) { + return value1.compareTo(value2) <= 0 ? value1 : value2; + } } diff --git a/core/src/main/java/com/coinomi/core/coins/ValueType.java b/core/src/main/java/com/coinomi/core/coins/ValueType.java index 1ad3d62..71799b6 100644 --- a/core/src/main/java/com/coinomi/core/coins/ValueType.java +++ b/core/src/main/java/com/coinomi/core/coins/ValueType.java @@ -2,6 +2,8 @@ import com.coinomi.core.util.MonetaryFormat; +import org.bitcoinj.core.Coin; + /** * @author John L. Jegutanis */ @@ -11,12 +13,23 @@ public interface ValueType { public int getUnitExponent(); /** - * Typical coin precision, like 1 Bitcoin or 1 Dollar + * Typical 1 coin value, like 1 Bitcoin, 1 Peercoin or 1 Dollar */ public Value oneCoin(); + /** + * Get the minimum valid amount that can be sent a.k.a. dust amount or minimum input + */ + Value minNonDust(); + + Value value(Coin coin); + + Value value(long units); + public MonetaryFormat getMonetaryFormat(); public MonetaryFormat getPlainFormat(); public boolean equals(ValueType obj); + + Value value(String string); } diff --git a/core/src/main/java/com/coinomi/core/exchange/shapeshift/Connection.java b/core/src/main/java/com/coinomi/core/exchange/shapeshift/Connection.java new file mode 100644 index 0000000..3ccd233 --- /dev/null +++ b/core/src/main/java/com/coinomi/core/exchange/shapeshift/Connection.java @@ -0,0 +1,48 @@ +package com.coinomi.core.exchange.shapeshift; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Lists; +import com.squareup.okhttp.Cache; +import com.squareup.okhttp.ConnectionSpec; +import com.squareup.okhttp.OkHttpClient; + +import java.io.File; +import java.net.URL; +import java.util.Collections; + +/** + * @author John L. Jegutanis + */ +abstract public class Connection { + private static final String DEFAULT_BASE_URL = "https://shapeshift.io/"; + + OkHttpClient client; + String baseUrl = DEFAULT_BASE_URL; + + protected Connection(OkHttpClient client) { + this.client = client; + } + + protected Connection() { + client = new OkHttpClient(); + client.setConnectionSpecs(Collections.singletonList(ConnectionSpec.MODERN_TLS)); + } + + /** + * Setup caching. The cache directory should be private, and untrusted applications should not + * be able to read its contents! + */ + public void setCache(File cacheDirectory) { + int cacheSize = 256 * 1024; // 256 KiB + Cache cache = new Cache(cacheDirectory, cacheSize); + client.setCache(cache); + } + + public boolean isCacheSet() { + return client.getCache() != null; + } + + protected String getApiUrl(String path) { + return baseUrl + path; + } +} diff --git a/core/src/main/java/com/coinomi/core/exchange/shapeshift/ShapeShift.java b/core/src/main/java/com/coinomi/core/exchange/shapeshift/ShapeShift.java new file mode 100644 index 0000000..2164dbf --- /dev/null +++ b/core/src/main/java/com/coinomi/core/exchange/shapeshift/ShapeShift.java @@ -0,0 +1,321 @@ +package com.coinomi.core.exchange.shapeshift; + +import com.coinomi.core.coins.CoinID; +import com.coinomi.core.coins.CoinType; +import com.coinomi.core.coins.Value; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftAmountTx; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftCoins; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftEmail; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftException; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftLimit; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftMarketInfo; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftNormalTx; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftRate; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftTime; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftTxStatus; +import com.squareup.okhttp.MediaType; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.RequestBody; +import com.squareup.okhttp.Response; + +import org.bitcoinj.core.Address; +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +import static com.coinomi.core.Preconditions.checkNotNull; +import static com.coinomi.core.Preconditions.checkState; + +/** + * @author John L. Jegutanis + */ +public class ShapeShift extends Connection { + private static final Logger log = LoggerFactory.getLogger(ShapeShift.class); + + private static final MediaType MEDIA_TYPE_JSON + = MediaType.parse("application/json"); + + private static final String GET_COINS_API = "getcoins"; + private static final String MARKET_INFO_API = "marketinfo/%s"; + private static final String RATE_API = "rate/%s"; + private static final String LIMIT_API = "limit/%s"; + private static final String TIME_REMAINING_API = "timeremaining/%s"; + private static final String TX_STATUS_API = "txStat/%s"; + private static final String NORMAL_TX_API = "shift"; + private static final String FIXED_AMOUNT_TX_API = "sendamount"; + private static final String EMAIL_RECEIPT_API = "mail"; + + private String apiPublicKey; + + public ShapeShift(OkHttpClient client) { + super(client); + } + + public ShapeShift() {} + + public void setApiPublicKey(String apiPublicKey) { + this.apiPublicKey = apiPublicKey; + } + + /** + * Get List of Supported Coins + * + * List of all the currencies that Shapeshift currently supports at any given time. Sometimes + * coins become temporarily unavailable during updates or unexpected service issues. + */ + public ShapeShiftCoins getCoins() throws ShapeShiftException, IOException { + Request request = new Request.Builder().url(getApiUrl(GET_COINS_API)).build(); + return new ShapeShiftCoins(getMakeApiCall(request)); + } + + /** + * Get Market Info + * + * This is a combined call for {@link #getRate(CoinType, CoinType) getRate()} and + * {@link #getLimit(CoinType, CoinType) getLimit()} API calls. + */ + public ShapeShiftMarketInfo getMarketInfo(CoinType typeFrom, CoinType typeTo) + throws ShapeShiftException, IOException { + return getMarketInfo(getPair(typeFrom, typeTo)); + } + + /** + * Get Market Info + * + * This is a combined call for {@link #getRate(CoinType, CoinType) getRate()} and + * {@link #getLimit(CoinType, CoinType) getLimit()} API calls. + */ + public ShapeShiftMarketInfo getMarketInfo(String pair) + throws ShapeShiftException, IOException { + log.info("Market info for pair {}", pair); + String apiUrl = getApiUrl(String.format(MARKET_INFO_API, pair)); + Request request = new Request.Builder().url(apiUrl).build(); + ShapeShiftMarketInfo reply = new ShapeShiftMarketInfo(getMakeApiCall(request)); + if (!reply.isError) checkPair(pair, reply.pair); + return reply; + } + + /** + * Rate + * + * Gets the current rate offered by Shapeshift. This is an estimate because the rate can + * occasionally change rapidly depending on the markets. The rate is also a 'use-able' rate not + * a direct market rate. Meaning multiplying your input coin amount times the rate should give + * you a close approximation of what will be sent out. This rate does not include the + * transaction (miner) fee taken off every transaction. + */ + public ShapeShiftRate getRate(CoinType typeFrom, CoinType typeTo) + throws ShapeShiftException, IOException { + String pair = getPair(typeFrom, typeTo); + String apiUrl = getApiUrl(String.format(RATE_API, pair)); + Request request = new Request.Builder().url(apiUrl).build(); + ShapeShiftRate reply = new ShapeShiftRate(getMakeApiCall(request)); + if (!reply.isError) checkPair(pair, reply.pair); + return reply; + } + + /** + * Deposit Limit + * + * Gets the current deposit limit set by Shapeshift. Amounts deposited over this limit will be + * sent to the return address if one was entered, otherwise the user will need to contact + * ShapeShift support to retrieve their coins. This is an estimate because a sudden market swing + * could move the limit. + */ + public ShapeShiftLimit getLimit(CoinType typeFrom, CoinType typeTo) + throws ShapeShiftException, IOException { + String pair = getPair(typeFrom, typeTo); + String apiUrl = getApiUrl(String.format(LIMIT_API, pair)); + Request request = new Request.Builder().url(apiUrl).build(); + ShapeShiftLimit reply = new ShapeShiftLimit(getMakeApiCall(request)); + if (!reply.isError) checkPair(pair, reply.pair); + return reply; + } + + /** + * Time Remaining on Fixed Amount Transaction + * + * When a transaction is created with a fixed amount requested there is a 10 minute window for + * the deposit. After the 10 minute window if the deposit has not been received the transaction + * expires and a new one must be created. This api call returns how many seconds are left before + * the transaction expires. + */ + public ShapeShiftTime getTime(Address address) throws ShapeShiftException, IOException { + String apiUrl = getApiUrl(String.format(TIME_REMAINING_API, address.toString())); + Request request = new Request.Builder().url(apiUrl).build(); + return new ShapeShiftTime(getMakeApiCall(request)); + } + + /** + * Status of deposit to address + * + * This returns the status of the most recent deposit transaction to the address. + */ + public ShapeShiftTxStatus getTxStatus(Address address) throws ShapeShiftException, IOException { + String apiUrl = getApiUrl(String.format(TX_STATUS_API, address.toString())); + Request request = new Request.Builder().url(apiUrl).build(); + ShapeShiftTxStatus reply = new ShapeShiftTxStatus(getMakeApiCall(request)); + if (!reply.isError && reply.address != null) checkAddress(address, reply.address); + return reply; + } + + /** + * Normal Transaction + * + * Make a normal exchange and receive with {@code withdrawal} address. The exchange pair is + * determined from the {@link CoinType}s of {@code refund} and {@code withdrawal}. + */ + public ShapeShiftNormalTx exchange(Address withdrawal, Address refund) + throws ShapeShiftException, IOException { + + JSONObject requestJson = new JSONObject(); + try { + requestJson.put("withdrawal", withdrawal.toString()); + requestJson.put("pair", getPair( + (CoinType) refund.getParameters(), (CoinType) withdrawal.getParameters())); + requestJson.put("returnAddress", refund.toString()); + if (apiPublicKey != null) requestJson.put("apiKey", apiPublicKey); + } catch (JSONException e) { + throw new ShapeShiftException("Could not create a JSON request", e); + } + + String apiUrl = getApiUrl(NORMAL_TX_API); + RequestBody body = RequestBody.create(MEDIA_TYPE_JSON, requestJson.toString()); + Request request = new Request.Builder().url(apiUrl).post(body).build(); + ShapeShiftNormalTx reply = new ShapeShiftNormalTx(getMakeApiCall(request)); + if (!reply.isError) checkAddress(withdrawal, reply.withdrawal); + return reply; + } + + + /** + * Fixed Amount Transaction + * + * This call allows you to request a fixed amount to be sent to the {@code withdrawal} address. + * You provide a withdrawal address and the amount you want sent to it. We return the amount + * to deposit and the address to deposit to. This allows you to use shapeshift as a payment + * mechanism. + * + * The exchange pair is determined from the {@link CoinType}s of {@code refund} and + * {@code withdrawal}. + */ + public ShapeShiftAmountTx exchangeForAmount(Value amount, Address withdrawal, Address refund) + throws ShapeShiftException, IOException { + String pair = getPair((CoinType) refund.getParameters(), + (CoinType) withdrawal.getParameters()); + JSONObject requestJson = new JSONObject(); + try { + requestJson.put("withdrawal", withdrawal.toString()); + requestJson.put("pair", pair); + requestJson.put("returnAddress", refund.toString()); + requestJson.put("amount", amount.toPlainString()); + if (apiPublicKey != null) requestJson.put("apiKey", apiPublicKey); + } catch (JSONException e) { + throw new ShapeShiftException("Could not create a JSON request", e); + } + + String apiUrl = getApiUrl(FIXED_AMOUNT_TX_API); + RequestBody body = RequestBody.create(MEDIA_TYPE_JSON, requestJson.toString()); + Request request = new Request.Builder().url(apiUrl).post(body).build(); + ShapeShiftAmountTx reply = new ShapeShiftAmountTx(getMakeApiCall(request)); + if (!reply.isError) { + checkPair(pair, reply.pair); + checkValue(amount, reply.withdrawalAmount); + checkAddress(withdrawal, reply.withdrawal); + } + return reply; + } + + /** + * Request email receipt + * + * This call allows you to request a fixed amount to be sent to the {@code withdrawal} address. + * You provide a withdrawal address and the amount you want sent to it. We return the amount + * to deposit and the address to deposit to. This allows you to use shapeshift as a payment + * mechanism. + * + * The exchange pair is determined from the {@link CoinType}s of {@code refund} and + * {@code withdrawal}. + */ + public ShapeShiftEmail requestEmailReceipt(String email, ShapeShiftTxStatus txStatus) + throws ShapeShiftException, IOException { + + JSONObject requestJson = new JSONObject(); + try { + requestJson.put("email", email); + checkState(txStatus.status == ShapeShiftTxStatus.Status.COMPLETE, + "Transaction not complete"); + requestJson.put("txid", checkNotNull(txStatus.transactionId, "Null transaction id")); + } catch (Exception e) { + throw new ShapeShiftException("Could not create a JSON request", e); + } + + String apiUrl = getApiUrl(EMAIL_RECEIPT_API); + RequestBody body = RequestBody.create(MEDIA_TYPE_JSON, requestJson.toString()); + Request request = new Request.Builder().url(apiUrl).post(body).build(); + return new ShapeShiftEmail(getMakeApiCall(request)); + } + + + /** + * Convert types to the ShapeShift format. For example Bitcoin to Litecoin will become btc_ltc. + */ + public static String getPair(CoinType typeFrom, CoinType typeTo) { + return typeFrom.getSymbol().toLowerCase() + "_" + typeTo.getSymbol().toLowerCase(); + } + + private void checkPair(String expectedPair, String pair) + throws ShapeShiftException { + if (!expectedPair.equals(pair)) { + String errorMsg = String.format("Pair mismatch, expected %s but got %s.", + expectedPair, pair); + throw new ShapeShiftException(errorMsg); + } + } + + private void checkValue(Value expected, Value value) throws ShapeShiftException { + if (!expected.equals(value)) { + String errorMsg = String.format("Value mismatch, expected %s but got %s.", + expected, value); + throw new ShapeShiftException(errorMsg); + } + } + + private void checkAddress(Address expected, Address address) throws ShapeShiftException { + if (!expected.getParameters().equals(address.getParameters()) || + !expected.toString().equals(address.toString())) { + String errorMsg = String.format("Address mismatch, expected %s but got %s.", + expected, address); + throw new ShapeShiftException(errorMsg); + } + } + + private JSONObject getMakeApiCall(Request request) throws ShapeShiftException, IOException { + try { + Response response = client.newCall(request).execute(); + if (!response.isSuccessful()) { + JSONObject reply = parseReply(response); + String genericMessage = "Error code " + response.code(); + throw new IOException( + reply != null ? reply.optString("error", genericMessage) : genericMessage); + } + return parseReply(response); + } catch (JSONException e) { + throw new ShapeShiftException("Could not parse JSON", e); + } + } + + private static JSONObject parseReply(Response response) throws IOException, JSONException { + return new JSONObject(response.body().string()); + } + + public static CoinType[] parsePair(String pair) { + String[] pairs = pair.split("_"); + checkState(pairs.length == 2); + return new CoinType[]{CoinID.typeFromSymbol(pairs[0]), CoinID.typeFromSymbol(pairs[1])}; + } +} diff --git a/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftAmountTx.java b/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftAmountTx.java new file mode 100644 index 0000000..d512549 --- /dev/null +++ b/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftAmountTx.java @@ -0,0 +1,58 @@ +package com.coinomi.core.exchange.shapeshift.data; + +import com.coinomi.core.coins.CoinType; +import com.coinomi.core.coins.Value; +import com.coinomi.core.exchange.shapeshift.ShapeShift; +import com.coinomi.core.util.ExchangeRate; +import com.coinomi.core.util.ExchangeRateBase; + +import org.bitcoinj.core.Address; +import org.json.JSONObject; + +import java.util.Date; + +import static com.coinomi.core.Preconditions.checkState; + +/** + * @author John L. Jegutanis + */ +public class ShapeShiftAmountTx extends ShapeShiftBase { + public final String pair; + public final Address deposit; + public final Value depositAmount; + public final Address withdrawal; + public final Value withdrawalAmount; + public final Date expiration; + public final ExchangeRate rate; + + public ShapeShiftAmountTx(JSONObject data) throws ShapeShiftException { + super(data); + if (!isError) { + try { + JSONObject innerData = data.getJSONObject("success"); + pair = innerData.getString("pair"); + CoinType[] coinTypePair = ShapeShift.parsePair(pair); + CoinType typeDeposit = coinTypePair[0]; + CoinType typeWithdrawal = coinTypePair[1]; + deposit = new Address(typeDeposit, innerData.getString("deposit")); + depositAmount = Value.parse(typeDeposit, innerData.getString("depositAmount")); + withdrawal = new Address(typeWithdrawal, innerData.getString("withdrawal")); + withdrawalAmount = Value.parse(typeWithdrawal, + innerData.getString("withdrawalAmount")); + expiration = new Date(innerData.getLong("expiration")); + rate = new ShapeShiftExchangeRate(typeDeposit, typeWithdrawal, + innerData.getString("quotedRate"), innerData.optString("minerFee", null)); + } catch (Exception e) { + throw new ShapeShiftException("Could not parse object", e); + } + } else { + pair = null; + deposit = null; + depositAmount = null; + withdrawal = null; + withdrawalAmount = null; + expiration = null; + rate = null; + } + } +} diff --git a/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftBase.java b/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftBase.java new file mode 100644 index 0000000..afed803 --- /dev/null +++ b/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftBase.java @@ -0,0 +1,16 @@ +package com.coinomi.core.exchange.shapeshift.data; + +import org.json.JSONObject; + +/** + * @author John L. Jegutanis + */ +abstract public class ShapeShiftBase { + final public String errorMessage; + final public boolean isError; + + protected ShapeShiftBase(JSONObject data) { + this.errorMessage = data.optString("error", null); + isError = errorMessage != null; + } +} diff --git a/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftCoin.java b/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftCoin.java new file mode 100644 index 0000000..d931d79 --- /dev/null +++ b/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftCoin.java @@ -0,0 +1,36 @@ +package com.coinomi.core.exchange.shapeshift.data; + +import com.coinomi.core.exchange.shapeshift.ShapeShift; + +import org.json.JSONObject; + +import java.net.URL; + +/** + * @author John L. Jegutanis + */ +final public class ShapeShiftCoin extends ShapeShiftBase { + final public String name; + final public String symbol; + final public URL image; + final public boolean isAvailable; + + public ShapeShiftCoin(JSONObject data) throws ShapeShiftException { + super(data); + if (!isError) { + try { + name = data.getString("name"); + symbol = data.getString("symbol"); + image = new URL(data.getString("image")); + isAvailable = data.getString("status").equals("available"); + } catch (Exception e) { + throw new ShapeShiftException("Could not parse object", e); + } + } else { + name = null; + symbol = null; + image = null; + isAvailable = false; + } + } +} diff --git a/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftCoins.java b/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftCoins.java new file mode 100644 index 0000000..1a9a0b9 --- /dev/null +++ b/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftCoins.java @@ -0,0 +1,46 @@ +package com.coinomi.core.exchange.shapeshift.data; + +import com.coinomi.core.coins.CoinID; +import com.coinomi.core.coins.CoinType; +import com.google.common.collect.ImmutableList; + +import org.json.JSONObject; + +import java.util.Iterator; +import java.util.List; + +/** + * @author John L. Jegutanis + */ +public class ShapeShiftCoins extends ShapeShiftBase { + public final List coins; + public final List availableCoinTypes; + + public ShapeShiftCoins(JSONObject data) throws ShapeShiftException { + super(data); + if (!isError) { + try { + ImmutableList.Builder listBuilder = ImmutableList.builder(); + Iterator iter = data.keys(); + while (iter.hasNext()) { + String k = (String) iter.next(); + listBuilder.add(new ShapeShiftCoin(data.getJSONObject(k))); + } + coins = listBuilder.build(); + + ImmutableList.Builder typesBuilder = ImmutableList.builder(); + for (ShapeShiftCoin coin : coins) { + if (coin.isAvailable && CoinID.isSymbolSupported(coin.symbol)) { + typesBuilder.add(CoinID.typeFromSymbol(coin.symbol)); + } + } + availableCoinTypes = typesBuilder.build(); + } catch (Exception e) { + throw new ShapeShiftException("Could not parse object", e); + } + } else { + coins = null; + availableCoinTypes = null; + } + } +} diff --git a/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftEmail.java b/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftEmail.java new file mode 100644 index 0000000..b2f7418 --- /dev/null +++ b/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftEmail.java @@ -0,0 +1,39 @@ +package com.coinomi.core.exchange.shapeshift.data; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * @author John L. Jegutanis + */ +public class ShapeShiftEmail extends ShapeShiftBase { + public final Status status; + public final String message; + + public static enum Status { + SUCCESS, UNKNOWN + } + + public ShapeShiftEmail(JSONObject data) throws ShapeShiftException { + super(data); + if (!isError) { + try { + JSONObject innerData = data.getJSONObject("email"); + message = innerData.getString("message"); + String statusStr = innerData.getString("status"); + switch (statusStr) { + case "success": + status = Status.SUCCESS; + break; + default: + status = Status.UNKNOWN; + } + } catch (JSONException e) { + throw new ShapeShiftException("Could not parse object", e); + } + } else { + status = null; + message = null; + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftException.java b/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftException.java new file mode 100644 index 0000000..4f03dbd --- /dev/null +++ b/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftException.java @@ -0,0 +1,20 @@ +package com.coinomi.core.exchange.shapeshift.data; + +import java.io.IOException; + +/** + * @author John L. Jegutanis + */ +public class ShapeShiftException extends Exception { + public ShapeShiftException(String message, Throwable cause) { + super(message, cause); + } + + public ShapeShiftException(Throwable cause) { + super(cause); + } + + public ShapeShiftException(String message) { + super(message); + } +} diff --git a/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftExchangeRate.java b/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftExchangeRate.java new file mode 100644 index 0000000..5572b5c --- /dev/null +++ b/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftExchangeRate.java @@ -0,0 +1,80 @@ +package com.coinomi.core.exchange.shapeshift.data; + +/** + * Copyright 2014 Andreas Schildbach + * Copyright 2015 John L. Jegutanis + * + * 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. + */ + + +import com.coinomi.core.coins.CoinType; +import com.coinomi.core.coins.Value; +import com.coinomi.core.coins.ValueType; +import com.coinomi.core.util.ExchangeRateBase; + +import org.bitcoinj.core.Coin; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; + +/** + * An exchange rate is expressed as a ratio of a pair of {@link com.coinomi.core.coins.Value} amounts. + */ +public class ShapeShiftExchangeRate extends ExchangeRateBase { + public final Value minerFee; + + public ShapeShiftExchangeRate(Value deposit, Value withdraw, Value minerFee) { + super(deposit, withdraw); + if (minerFee != null) checkArgument(withdraw.type.equals(minerFee.type)); + this.minerFee = minerFee; + } + + public ShapeShiftExchangeRate(ValueType depositType, ValueType withdrawType, String rateString, + String minerFeeString) { + super(depositType, withdrawType, rateString); + + if (minerFeeString != null) { + minerFee = withdrawType.value(minerFeeString); + } else { + minerFee = null; + } + } + + @Override + public Value convert(CoinType type, Coin coin) { + return convert(Value.valueOf(type, coin)); + } + + @Override + public Value convert(Value convertingValue) { + Value converted = convertValue(convertingValue); + if (!converted.isZero() && minerFee != null) { + Value fee; + // Deposit -> withdrawal + if (converted.type.equals(minerFee.type)) { + fee = minerFee.negate(); // Miner fee is removed from withdraw value + } else { // Withdrawal -> deposit + fee = convertValue(minerFee); // Miner fee is added to the deposit value + } + converted = converted.add(fee); + + // If the miner fee is higher than the value we are converting we get 0 + if (converted.isNegative()) converted = converted.multiply(0); + } + return converted; + } + +} diff --git a/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftLimit.java b/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftLimit.java new file mode 100644 index 0000000..4285f5b --- /dev/null +++ b/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftLimit.java @@ -0,0 +1,43 @@ +package com.coinomi.core.exchange.shapeshift.data; + +import com.coinomi.core.coins.CoinID; +import com.coinomi.core.coins.CoinType; +import com.coinomi.core.coins.Value; + +import org.json.JSONObject; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +import static com.coinomi.core.Preconditions.checkState; + +/** + * @author John L. Jegutanis + */ +public class ShapeShiftLimit extends ShapeShiftPairBase { + public final Value limit; + public final Value minimum; + + public ShapeShiftLimit(JSONObject data) throws ShapeShiftException { + super(data); + if (!isError) { + try { + String[] pairs = pair.split("_"); + checkState(pairs.length == 2); + CoinType typeFrom = CoinID.typeFromSymbol(pairs[0]); + limit = parseValue(typeFrom, data.getString("limit"), RoundingMode.DOWN); + minimum = parseValue(typeFrom, data.getString("min"), RoundingMode.UP); + } catch (Exception e) { + throw new ShapeShiftException("Could not parse object", e); + } + } else { + limit = null; + minimum = null; + } + } + + private static Value parseValue(CoinType type, String string, RoundingMode roundingMode) { + BigDecimal value = new BigDecimal(string).setScale(type.getUnitExponent(), roundingMode); + return Value.parse(type, value); + } +} diff --git a/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftMarketInfo.java b/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftMarketInfo.java new file mode 100644 index 0000000..6fd5580 --- /dev/null +++ b/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftMarketInfo.java @@ -0,0 +1,52 @@ +package com.coinomi.core.exchange.shapeshift.data; + +import com.coinomi.core.coins.CoinID; +import com.coinomi.core.coins.CoinType; +import com.coinomi.core.coins.Value; +import com.coinomi.core.exchange.shapeshift.ShapeShift; +import com.coinomi.core.util.ExchangeRate; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +import static com.coinomi.core.Preconditions.checkState; + +/** + * @author John L. Jegutanis + */ +public class ShapeShiftMarketInfo extends ShapeShiftPairBase { + public final ExchangeRate rate; + public final Value limit; + public final Value minimum; + + public ShapeShiftMarketInfo(JSONObject data) throws ShapeShiftException { + super(data); + if (!isError) { + // In market info minerFee is mandatory + if (!data.has("minerFee")) { + throw new ShapeShiftException("Missing value minerfee"); + } + try { + CoinType[] types = ShapeShift.parsePair(pair); + rate = new ShapeShiftExchangeRate(types[0], types[1], + data.getString("rate"), data.getString("minerFee")); + limit = parseValue(types[0], data.getString("limit"), RoundingMode.DOWN); + minimum = parseValue(types[0], data.getString("minimum"), RoundingMode.UP); + } catch (JSONException e) { + throw new ShapeShiftException("Could not parse object", e); + } + } else { + rate = null; + limit = null; + minimum = null; + } + } + + private static Value parseValue(CoinType type, String string, RoundingMode roundingMode) { + BigDecimal value = new BigDecimal(string).setScale(type.getUnitExponent(), roundingMode); + return Value.parse(type, value); + } +} diff --git a/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftNormalTx.java b/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftNormalTx.java new file mode 100644 index 0000000..9075d74 --- /dev/null +++ b/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftNormalTx.java @@ -0,0 +1,37 @@ +package com.coinomi.core.exchange.shapeshift.data; + +import com.coinomi.core.coins.CoinID; +import com.coinomi.core.coins.CoinType; +import com.coinomi.core.exchange.shapeshift.ShapeShift; + +import org.bitcoinj.core.Address; +import org.json.JSONObject; + +/** + * @author John L. Jegutanis + */ +public class ShapeShiftNormalTx extends ShapeShiftBase { + public final String pair; + public final Address deposit; + public final Address withdrawal; + + public ShapeShiftNormalTx(JSONObject data) throws ShapeShiftException { + super(data); + if (!isError) { + try { + deposit = new Address(CoinID.typeFromSymbol(data.getString("depositType")), + data.getString("deposit")); + withdrawal = new Address(CoinID.typeFromSymbol(data.getString("withdrawalType")), + data.getString("withdrawal")); + pair = ShapeShift.getPair((CoinType)deposit.getParameters(), + (CoinType)withdrawal.getParameters()); + } catch (Exception e) { + throw new ShapeShiftException("Could not parse object", e); + } + } else { + deposit = null; + withdrawal = null; + pair = null; + } + } +} diff --git a/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftPairBase.java b/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftPairBase.java new file mode 100644 index 0000000..ad75bd5 --- /dev/null +++ b/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftPairBase.java @@ -0,0 +1,40 @@ +package com.coinomi.core.exchange.shapeshift.data; + +import com.coinomi.core.coins.CoinType; +import com.coinomi.core.coins.Value; +import com.coinomi.core.exchange.shapeshift.ShapeShift; +import com.coinomi.core.util.ExchangeRate; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +/** + * @author John L. Jegutanis + */ +public class ShapeShiftPairBase extends ShapeShiftBase { + public final String pair; + + public ShapeShiftPairBase(JSONObject data) throws ShapeShiftException { + super(data); + if (!isError) { + try { + pair = data.getString("pair").toLowerCase(); + } catch (JSONException e) { + throw new ShapeShiftException("Could not parse object", e); + } + } else { + pair = null; + } + } + + public boolean isPair(CoinType sourceType, CoinType destinationType) { + return isPair(ShapeShift.getPair(sourceType, destinationType)); + } + + public boolean isPair(String otherPair) { + return pair != null && pair.equalsIgnoreCase(otherPair); + } +} diff --git a/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftRate.java b/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftRate.java new file mode 100644 index 0000000..e09c068 --- /dev/null +++ b/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftRate.java @@ -0,0 +1,35 @@ +package com.coinomi.core.exchange.shapeshift.data; + +import com.coinomi.core.coins.CoinID; +import com.coinomi.core.coins.CoinType; +import com.coinomi.core.util.ExchangeRate; +import com.coinomi.core.util.ExchangeRateBase; + +import org.json.JSONObject; + +import static com.coinomi.core.Preconditions.checkState; + +/** + * @author John L. Jegutanis + */ +public class ShapeShiftRate extends ShapeShiftPairBase { + public final ExchangeRate rate; + + public ShapeShiftRate(JSONObject data) throws ShapeShiftException { + super(data); + if (!isError) { + try { + String[] pairs = pair.split("_"); + checkState(pairs.length == 2); + CoinType typeFrom = CoinID.typeFromSymbol(pairs[0]); + CoinType typeTo = CoinID.typeFromSymbol(pairs[1]); + rate = new ShapeShiftExchangeRate(typeFrom, typeTo, + data.getString("rate"), data.optString("minerFee", null)); + } catch (Exception e) { + throw new ShapeShiftException("Could not parse object", e); + } + } else { + rate = null; + } + } +} diff --git a/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftTime.java b/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftTime.java new file mode 100644 index 0000000..408a86f --- /dev/null +++ b/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftTime.java @@ -0,0 +1,46 @@ +package com.coinomi.core.exchange.shapeshift.data; + +import com.coinomi.core.exchange.shapeshift.ShapeShift; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Stack; + +/** + * @author John L. Jegutanis + */ +public class ShapeShiftTime extends ShapeShiftBase { + public final Status status; + public final int secondsRemaining; + + public static enum Status { + PENDING, EXPIRED, UNKNOWN + } + + public ShapeShiftTime(JSONObject data) throws ShapeShiftException { + super(data); + if (!isError) { + try { + secondsRemaining = data.getInt("seconds_remaining"); + String statusStr = data.getString("status"); + switch (statusStr) { + case "pending": + status = Status.PENDING; + break; + case "expired": + status = Status.EXPIRED; + break; + default: + status = Status.UNKNOWN; + } + } catch (JSONException e) { + throw new ShapeShiftException("Could not parse object", e); + } + } else { + status = null; + secondsRemaining = -1; + } + } +} + diff --git a/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftTxStatus.java b/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftTxStatus.java new file mode 100644 index 0000000..0c80e9a --- /dev/null +++ b/core/src/main/java/com/coinomi/core/exchange/shapeshift/data/ShapeShiftTxStatus.java @@ -0,0 +1,97 @@ +package com.coinomi.core.exchange.shapeshift.data; + +import com.coinomi.core.coins.CoinID; +import com.coinomi.core.coins.CoinType; +import com.coinomi.core.coins.Value; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.AddressFormatException; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * @author John L. Jegutanis + */ +public class ShapeShiftTxStatus extends ShapeShiftBase { + public final Status status; + public final Address address; + public final Address withdraw; + public final Value incomingValue; + public final Value outgoingValue; + public final String transactionId; + + public static enum Status { + NO_DEPOSITS, RECEIVED, COMPLETE, FAILED, UNKNOWN + } + + public ShapeShiftTxStatus(JSONObject data) throws ShapeShiftException { + super(data); + + String statusStr = data.optString("status", null); + + if (statusStr != null) { + try { + CoinType inType; + CoinType outType; + switch (statusStr) { + case "no_deposits": + status = Status.NO_DEPOSITS; + address = null; // FIXME, we don't know the type here + withdraw = null; + incomingValue = null; + outgoingValue = null; + transactionId = null; + break; + case "received": + status = Status.RECEIVED; + inType = CoinID.typeFromSymbol(data.getString("incomingType")); + address = new Address(inType, data.getString("address")); + withdraw = null; + incomingValue = Value.parse(inType, data.getString("incomingCoin")); + outgoingValue = null; + transactionId = null; + break; + case "complete": + status = Status.COMPLETE; + inType = CoinID.typeFromSymbol(data.getString("incomingType")); + outType = CoinID.typeFromSymbol(data.getString("outgoingType")); + address = new Address(inType, data.getString("address")); + withdraw = new Address(outType, data.getString("withdraw")); + incomingValue = Value.parse(inType, data.getString("incomingCoin")); + outgoingValue = Value.parse(outType, data.getString("outgoingCoin")); + transactionId = data.getString("transaction"); + break; + case "failed": + status = Status.FAILED; + address = null; + withdraw = null; + incomingValue = null; + outgoingValue = null; + transactionId = null; + break; + default: + status = Status.UNKNOWN; + address = null; + withdraw = null; + incomingValue = null; + outgoingValue = null; + transactionId = null; + } + } catch (JSONException e) { + throw new ShapeShiftException("Could not parse object", e); + } catch (AddressFormatException e) { + throw new ShapeShiftException("Could not parse address", e); + } + } else { + // There should be an error, otherwise we don't know what happened + if (!isError) throw new ShapeShiftException("Unexpected state: no status and no error"); + status = null; + address = null; + withdraw = null; + incomingValue = null; + outgoingValue = null; + transactionId = null; + } + } +} + diff --git a/core/src/main/java/com/coinomi/core/network/ServerClient.java b/core/src/main/java/com/coinomi/core/network/ServerClient.java index 5948bf4..3a08542 100644 --- a/core/src/main/java/com/coinomi/core/network/ServerClient.java +++ b/core/src/main/java/com/coinomi/core/network/ServerClient.java @@ -34,6 +34,7 @@ import java.util.List; import java.util.Random; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; @@ -517,6 +518,27 @@ public void onFailure(Throwable t) { }, Threading.USER_THREAD); } + @Override + public boolean broadcastTxSync(final Transaction tx) { + checkNotNull(stratumClient); + + CallMessage message = new CallMessage("blockchain.transaction.broadcast", + Arrays.asList(Utils.HEX.encode(tx.bitcoinSerialize()))); + + try { + ResultMessage result = stratumClient.call(message).get(); + String txId = result.getResult().getString(0); + + // FIXME could return {u'message': u'', u'code': -25} + log.info("got tx {} =?= {}", txId, tx.getHash()); + checkState(tx.getHash().toString().equals(txId)); + return true; + } catch (Exception e) { + log.error("Could not get reply for blockchain.transaction.broadcast", e); + } + return false; + } + @Override public void ping() { checkNotNull(stratumClient); diff --git a/core/src/main/java/com/coinomi/core/network/interfaces/BlockchainConnection.java b/core/src/main/java/com/coinomi/core/network/interfaces/BlockchainConnection.java index 5b5af1b..19eb39e 100644 --- a/core/src/main/java/com/coinomi/core/network/interfaces/BlockchainConnection.java +++ b/core/src/main/java/com/coinomi/core/network/interfaces/BlockchainConnection.java @@ -25,6 +25,8 @@ void subscribeToAddresses(List

addresses, void broadcastTx(final Transaction tx, final TransactionEventListener listener); + boolean broadcastTxSync(final Transaction tx); + void ping(); } diff --git a/core/src/main/java/com/coinomi/core/util/ExchangeRate.java b/core/src/main/java/com/coinomi/core/util/ExchangeRate.java index 96d03d0..355f03e 100644 --- a/core/src/main/java/com/coinomi/core/util/ExchangeRate.java +++ b/core/src/main/java/com/coinomi/core/util/ExchangeRate.java @@ -1,105 +1,28 @@ package com.coinomi.core.util; -/** - * Copyright 2014 Andreas Schildbach - * Copyright 2015 John L. Jegutanis - * - * 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. - */ - - import com.coinomi.core.coins.CoinType; import com.coinomi.core.coins.Value; import com.coinomi.core.coins.ValueType; -import static com.google.common.base.Preconditions.checkArgument; +import org.bitcoinj.core.Coin; import java.io.Serializable; -import java.math.BigInteger; - -import org.bitcoinj.core.Coin; -import org.bitcoinj.utils.Fiat; /** - * An exchange rate is expressed as a ratio of a {@link Coin} and a {@link Fiat} amount. + * @author John L. Jegutanis */ -public class ExchangeRate implements Serializable { - - public final Value value1; - public final Value value2; - - /** Construct exchange rate. This amount of coin is worth that amount of fiat. */ - public ExchangeRate(Value value1, Value value2) { - this.value1 = value1; - this.value2 = value2; - } - - public Value convert(CoinType type, Coin coin) { - return convert(Value.valueOf(type, coin)); - } +public interface ExchangeRate extends Serializable { + Value convert(CoinType type, Coin coin); /** * Convert from one value to another */ - public Value convert(Value convertingValue) { - checkIfValueTypeAvailable(convertingValue.type); - - Value rateFrom = getFromRateValue(convertingValue.type); - Value rateTo = getToRateValue(convertingValue.type); - - // Use BigInteger because it's much easier to maintain full precision without overflowing. - final BigInteger converted = BigInteger.valueOf(convertingValue.value) - .multiply(BigInteger.valueOf(rateTo.value)) - .divide(BigInteger.valueOf(rateFrom.value)); - if (converted.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0 - || converted.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0) - throw new ArithmeticException("Overflow"); - return Value.valueOf(rateTo.type, converted.longValue()); - } - - public ValueType getOtherType(ValueType type) { - checkIfValueTypeAvailable(type); - if (value1.type.equals(type)) { - return value2.type; - } else { - return value1.type; - } - } + Value convert(Value convertingValue); - private Value getFromRateValue(ValueType fromType) { - if (value1.type.equals(fromType)) { - return value1; - } else if (value2.type.equals(fromType)) { - return value2; - } else { - // Should not happen - throw new IllegalStateException("Could not get 'from' rate"); - } - } + ValueType getOtherType(ValueType type); - private Value getToRateValue(ValueType fromType) { - if (value1.type.equals(fromType)) { - return value2; - } else if (value2.type.equals(fromType)) { - return value1; - } else { - // Should not happen - throw new IllegalStateException("Could not get 'to' rate"); - } - } + ValueType getSourceType(); + ValueType getDestinationType(); - private void checkIfValueTypeAvailable(ValueType type) { - checkArgument(value1.type.equals(type) || value2.type.equals(type), - "This exchange rate does not apply to: %s", type.getSymbol()); - } + boolean canConvert(ValueType type1, ValueType type2); } diff --git a/core/src/main/java/com/coinomi/core/util/ExchangeRateBase.java b/core/src/main/java/com/coinomi/core/util/ExchangeRateBase.java new file mode 100644 index 0000000..8e479e1 --- /dev/null +++ b/core/src/main/java/com/coinomi/core/util/ExchangeRateBase.java @@ -0,0 +1,151 @@ +package com.coinomi.core.util; + +/** + * Copyright 2014 Andreas Schildbach + * Copyright 2015 John L. Jegutanis + * + * 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. + */ + + +import com.coinomi.core.coins.CoinType; +import com.coinomi.core.coins.Value; +import com.coinomi.core.coins.ValueType; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.math.RoundingMode; + +import org.bitcoinj.core.Coin; + +/** + * An exchange rate is expressed as a ratio of a pair of {@link Value} amounts. + */ +public class ExchangeRateBase implements ExchangeRate { + private static final int RATE_SCALE = 10; + + public final Value value1; + public final Value value2; + + /** Construct exchange rate. This amount of coin is worth that amount of fiat. */ + public ExchangeRateBase(Value value1, Value value2) { + this.value1 = value1; + this.value2 = value2; + } + + public ExchangeRateBase(ValueType type1, ValueType type2, String rateString) { + // Make the rate having maximum of RATE_SCALE decimal places + BigDecimal rate = new BigDecimal(rateString) + .setScale(RATE_SCALE, RoundingMode.HALF_UP).stripTrailingZeros(); + checkState(rate.signum() >= 0); // rate cannot be negative + // Check if the rate has too many decimal places or scale() for the type2 to handle. + if (rate.scale() > type2.getUnitExponent()) { + // If we have too many decimal places, multiply everything by a factor so that the rate + // can fit in a type2 value. For example if the rate is 0.123456789 but a type2 can only + // handle 4 places, then final result will be value1 = 100000 and value2 = 12345.6789 + BigDecimal rateFactor = BigDecimal.TEN.pow(rate.scale() - type2.getUnitExponent()); + value1 = type1.oneCoin().multiply(rateFactor.longValue()); + value2 = Value.parse(type2, rate.multiply(rateFactor)); + } else { + value1 = type1.oneCoin(); + value2 = Value.parse(type2, rate); + } + } + + @Override + public Value convert(CoinType type, Coin coin) { + return convertValue(type.value(coin)); + } + + @Override + public Value convert(Value convertingValue) { + return convertValue(convertingValue); + } + + @Override + public ValueType getOtherType(ValueType type) { + checkIfValueTypeAvailable(type); + if (value1.type.equals(type)) { + return value2.type; + } else { + return value1.type; + } + } + + @Override + public ValueType getSourceType() { + return value1.type; + } + + @Override + public ValueType getDestinationType() { + return value2.type; + } + + @Override + public boolean canConvert(ValueType type1, ValueType type2) { + try { + checkIfValueTypeAvailable(type1); + checkIfValueTypeAvailable(type2); + return true; + } catch (IllegalArgumentException ignored) { + return false; + } + } + + protected Value convertValue(Value convertingValue) { + checkIfValueTypeAvailable(convertingValue.type); + + Value rateFrom = getFromRateValue(convertingValue.type); + Value rateTo = getToRateValue(convertingValue.type); + + // Use BigDecimal because it's much easier to maintain full precision without overflowing. + final BigDecimal converted = BigDecimal.valueOf(convertingValue.value) + .multiply(BigDecimal.valueOf(rateTo.value)) + .divide(BigDecimal.valueOf(rateFrom.value), RoundingMode.HALF_UP); + if (converted.compareTo(BigDecimal.valueOf(Long.MAX_VALUE)) > 0 + || converted.compareTo(BigDecimal.valueOf(Long.MIN_VALUE)) < 0) + throw new ArithmeticException("Overflow"); + return Value.valueOf(rateTo.type, converted.longValue()); + } + + protected Value getFromRateValue(ValueType fromType) { + if (value1.type.equals(fromType)) { + return value1; + } else if (value2.type.equals(fromType)) { + return value2; + } else { + // Should not happen + throw new IllegalStateException("Could not get 'from' rate"); + } + } + + protected Value getToRateValue(ValueType fromType) { + if (value1.type.equals(fromType)) { + return value2; + } else if (value2.type.equals(fromType)) { + return value1; + } else { + // Should not happen + throw new IllegalStateException("Could not get 'to' rate"); + } + } + + protected void checkIfValueTypeAvailable(ValueType type) { + checkArgument(value1.type.equals(type) || value2.type.equals(type), + "This exchange rate does not apply to: %s", type.getSymbol()); + } +} diff --git a/core/src/main/java/com/coinomi/core/util/GenericUtils.java b/core/src/main/java/com/coinomi/core/util/GenericUtils.java index b778fdf..27370a0 100644 --- a/core/src/main/java/com/coinomi/core/util/GenericUtils.java +++ b/core/src/main/java/com/coinomi/core/util/GenericUtils.java @@ -3,12 +3,11 @@ import com.coinomi.core.coins.CoinType; import com.coinomi.core.coins.Value; +import com.coinomi.core.coins.ValueType; import org.bitcoinj.core.Coin; import org.bitcoinj.core.Monetary; -import org.bitcoinj.utils.Fiat; -import java.math.BigDecimal; import java.util.Locale; import java.util.regex.Pattern; @@ -80,25 +79,29 @@ public static Coin parseCoin(final CoinType type, final String str) // .toBigIntegerExact() // .longValue(); // return Coin.valueOf(units); - return Value.parseValue(type, str).toCoin(); + return Value.parse(type, str).toCoin(); } - public static String formatCoinValue(@Nonnull final CoinType type, @Nonnull final Coin value) { + public static String formatValue(@Nonnull final Value value) { + return formatCoinValue(value.type, value.toCoin(), "", "-", 8, 0); + } + + public static String formatCoinValue(@Nonnull final ValueType type, @Nonnull final Coin value) { return formatCoinValue(type, value, "", "-", 8, 0); } - public static String formatCoinValue(@Nonnull final CoinType type, @Nonnull final Coin value, + public static String formatCoinValue(@Nonnull final ValueType type, @Nonnull final Coin value, final int precision, final int shift) { return formatCoinValue(type, value, "", "-", precision, shift); } - public static String formatCoinValue(@Nonnull final CoinType type, @Nonnull final Coin value, + public static String formatCoinValue(@Nonnull final ValueType type, @Nonnull final Coin value, @Nonnull final String plusSign, @Nonnull final String minusSign, final int precision, final int shift) { return formatValue(type.getUnitExponent(), value, plusSign, minusSign, precision, shift, false); } - public static String formatCoinValue(@Nonnull final CoinType type, @Nonnull final Coin value, + public static String formatCoinValue(@Nonnull final ValueType type, @Nonnull final Coin value, boolean removeFinalZeroes) { return formatValue(type.getUnitExponent(), value, "", "-", 8, 0, removeFinalZeroes); } diff --git a/core/src/main/java/com/coinomi/core/util/MonetaryFormat.java b/core/src/main/java/com/coinomi/core/util/MonetaryFormat.java index a20e55b..d650377 100644 --- a/core/src/main/java/com/coinomi/core/util/MonetaryFormat.java +++ b/core/src/main/java/com/coinomi/core/util/MonetaryFormat.java @@ -418,7 +418,7 @@ else if (positiveSign != 0) // * if the string cannot be parsed for some reason // */ // public Coin parse(String str, int smallestUnitExponent) throws NumberFormatException { -// return Coin.valueOf(parseValue(str, smallestUnitExponent)); +// return Coin.valueOf(parse(str, smallestUnitExponent)); // } /** diff --git a/core/src/main/java/com/coinomi/core/wallet/TransactionWatcherWallet.java b/core/src/main/java/com/coinomi/core/wallet/TransactionWatcherWallet.java index 684719f..2e25696 100644 --- a/core/src/main/java/com/coinomi/core/wallet/TransactionWatcherWallet.java +++ b/core/src/main/java/com/coinomi/core/wallet/TransactionWatcherWallet.java @@ -1,11 +1,11 @@ package com.coinomi.core.wallet; import com.coinomi.core.coins.CoinType; +import com.coinomi.core.coins.Value; import com.coinomi.core.network.AddressStatus; import com.coinomi.core.network.BlockHeader; import com.coinomi.core.network.ServerClient; import com.coinomi.core.network.interfaces.BlockchainConnection; -import com.coinomi.core.network.interfaces.ConnectionEventListener; import com.coinomi.core.network.interfaces.TransactionEventListener; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; @@ -13,16 +13,13 @@ import com.google.common.collect.Lists; import org.bitcoinj.core.Address; -import org.bitcoinj.core.Coin; import org.bitcoinj.core.ScriptException; import org.bitcoinj.core.Sha256Hash; import org.bitcoinj.core.Transaction; -import org.bitcoinj.core.TransactionBag; import org.bitcoinj.core.TransactionConfidence; import org.bitcoinj.core.TransactionInput; import org.bitcoinj.core.TransactionOutput; import org.bitcoinj.core.Utils; -import org.bitcoinj.script.Script; import org.bitcoinj.utils.ListenerRegistration; import org.bitcoinj.utils.Threading; import org.bitcoinj.wallet.WalletTransaction; @@ -58,7 +55,6 @@ abstract public class TransactionWatcherWallet implements WalletAccount { final ReentrantLock lock = Threading.lock("TransactionWatcherWallet"); protected final CoinType coinType; - protected final String id; @Nullable private Sha256Hash lastBlockSeenHash; private int lastBlockSeenHeight = -1; @@ -102,7 +98,7 @@ abstract public class TransactionWatcherWallet implements WalletAccount { // All transactions together. protected final Map transactions; private BlockchainConnection blockchainConnection; - private List> listeners; + private List> listeners; // Wallet that this account belongs @Nullable private transient Wallet wallet = null; @@ -122,8 +118,7 @@ public void run() { }; // Constructor - public TransactionWatcherWallet(String id, CoinType coinType) { - this.id = id; + public TransactionWatcherWallet(CoinType coinType) { this.coinType = coinType; addressesStatus = new HashMap(); addressesSubscribed = new ArrayList
(); @@ -135,12 +130,7 @@ public TransactionWatcherWallet(String id, CoinType coinType) { pending = new HashMap(); dead = new HashMap(); transactions = new HashMap(); - listeners = new CopyOnWriteArrayList>(); - } - - @Override - public String getId() { - return id; + listeners = new CopyOnWriteArrayList>(); } @Override @@ -457,11 +447,11 @@ public int getLastBlockSeenHeight() { } } - public Coin getBalance() { + public Value getBalance() { return getBalance(false); } - public Coin getBalance(boolean includeUnconfirmed) { + public Value getBalance(boolean includeUnconfirmed) { lock.lock(); try { // log.info("Get balance includeUnconfirmed = {}", includeUnconfirmed); @@ -474,7 +464,7 @@ public Coin getBalance(boolean includeUnconfirmed) { } } - public Coin getPendingBalance() { + public Value getPendingBalance() { lock.lock(); try { // log.info("Get pending balance"); @@ -484,10 +474,10 @@ public Coin getPendingBalance() { } } - Coin getTxBalance(Iterable txs, boolean toMe) { + Value getTxBalance(Iterable txs, boolean toMe) { lock.lock(); try { - Coin value = Coin.ZERO; + Value value = coinType.value(0); for (Transaction tx : txs) { // log.info("tx {}", tx.getHash()); // log.info("tx.getValue {}", tx.getValue(this)); @@ -1121,9 +1111,9 @@ public Map getTransactionPool(WalletTransaction.Pool po void queueOnNewBalance() { checkState(lock.isHeldByCurrentThread(), "Lock is held by another thread"); - final Coin balance = getBalance(); - final Coin pendingBalance = getPendingBalance(); - for (final ListenerRegistration registration : listeners) { + final Value balance = getBalance(); + final Value pendingBalance = getPendingBalance(); + for (final ListenerRegistration registration : listeners) { registration.executor.execute(new Runnable() { @Override public void run() { @@ -1136,7 +1126,7 @@ public void run() { void queueOnNewBlock() { checkState(lock.isHeldByCurrentThread(), "Lock is held by another thread"); - for (final ListenerRegistration registration : listeners) { + for (final ListenerRegistration registration : listeners) { registration.executor.execute(new Runnable() { @Override public void run() { @@ -1149,7 +1139,7 @@ public void run() { void queueOnConnectivity() { final WalletPocketConnectivity connectivity = getConnectivityStatus(); - for (final ListenerRegistration registration : listeners) { + for (final ListenerRegistration registration : listeners) { registration.executor.execute(new Runnable() { @Override public void run() { @@ -1160,7 +1150,7 @@ public void run() { } void queueOnTransactionBroadcastSuccess(final Transaction tx) { - for (final ListenerRegistration registration : listeners) { + for (final ListenerRegistration registration : listeners) { registration.executor.execute(new Runnable() { @Override public void run() { @@ -1171,7 +1161,7 @@ public void run() { } void queueOnTransactionBroadcastFailure(final Transaction tx) { - for (final ListenerRegistration registration : listeners) { + for (final ListenerRegistration registration : listeners) { registration.executor.execute(new Runnable() { @Override public void run() { @@ -1182,15 +1172,15 @@ public void run() { } - public void addEventListener(WalletPocketEventListener listener) { + public void addEventListener(WalletAccountEventListener listener) { addEventListener(listener, Threading.USER_THREAD); } - public void addEventListener(WalletPocketEventListener listener, Executor executor) { - listeners.add(new ListenerRegistration(listener, executor)); + public void addEventListener(WalletAccountEventListener listener, Executor executor) { + listeners.add(new ListenerRegistration<>(listener, executor)); } - public boolean removeEventListener(WalletPocketEventListener listener) { + public boolean removeEventListener(WalletAccountEventListener listener) { return ListenerRegistration.removeFromList(listener, listeners); } @@ -1199,6 +1189,23 @@ public boolean isPending() { } + public boolean broadcastTxSync(Transaction tx) throws IOException { + if (isConnected()) { + if (log.isInfoEnabled()) { + log.info("Broadcasting tx {}", Utils.HEX.encode(tx.bitcoinSerialize())); + } + boolean success = blockchainConnection.broadcastTxSync(tx); + if (success) { + onTransactionBroadcast(tx); + } else { + onTransactionBroadcastError(tx); + } + return success; + } else { + throw new IOException("No connection available"); + } + } + public void broadcastTx(Transaction tx) throws IOException { broadcastTx(tx, this); } diff --git a/core/src/main/java/com/coinomi/core/wallet/WalletAccount.java b/core/src/main/java/com/coinomi/core/wallet/WalletAccount.java index f03c0c9..42de0a7 100644 --- a/core/src/main/java/com/coinomi/core/wallet/WalletAccount.java +++ b/core/src/main/java/com/coinomi/core/wallet/WalletAccount.java @@ -1,6 +1,7 @@ package com.coinomi.core.wallet; import com.coinomi.core.coins.CoinType; +import com.coinomi.core.coins.Value; import com.coinomi.core.network.interfaces.ConnectionEventListener; import com.coinomi.core.network.interfaces.TransactionEventListener; @@ -15,9 +16,7 @@ import java.io.Serializable; import java.util.List; import java.util.Map; - -import static org.bitcoinj.wallet.KeyChain.KeyPurpose.CHANGE; -import static org.bitcoinj.wallet.KeyChain.KeyPurpose.RECEIVE_FUNDS; +import java.util.concurrent.Executor; /** * @author John L. Jegutanis @@ -32,6 +31,9 @@ public interface WalletAccount extends TransactionBag, KeyBag, TransactionEventL boolean isNew(); + Value getBalance(boolean includeUnconfirmed); +// Value getSpendableBalance(); + void refresh(); boolean isConnected(); @@ -46,6 +48,13 @@ public interface WalletAccount extends TransactionBag, KeyBag, TransactionEventL */ Address getReceiveAddress(); + /** + * Get current refund address, does not mark it as used. + * + * Notice: This address could be the same as the current receive address + */ + Address getRefundAddress(); + Map getUnspentTransactions(); Map getPendingTransactions(); @@ -63,4 +72,10 @@ public interface WalletAccount extends TransactionBag, KeyBag, TransactionEventL KeyCrypter getKeyCrypter(); void encrypt(KeyCrypter keyCrypter, KeyParameter aesKey); void decrypt(KeyParameter aesKey); + + boolean equals(WalletAccount otherAccount); + + void addEventListener(WalletAccountEventListener listener); + void addEventListener(WalletAccountEventListener listener, Executor executor); + boolean removeEventListener(WalletAccountEventListener listener); } diff --git a/core/src/main/java/com/coinomi/core/wallet/WalletPocketEventListener.java b/core/src/main/java/com/coinomi/core/wallet/WalletAccountEventListener.java similarity index 72% rename from core/src/main/java/com/coinomi/core/wallet/WalletPocketEventListener.java rename to core/src/main/java/com/coinomi/core/wallet/WalletAccountEventListener.java index d4471f9..f128332 100644 --- a/core/src/main/java/com/coinomi/core/wallet/WalletPocketEventListener.java +++ b/core/src/main/java/com/coinomi/core/wallet/WalletAccountEventListener.java @@ -1,13 +1,15 @@ package com.coinomi.core.wallet; -import org.bitcoinj.core.*; +import com.coinomi.core.coins.Value; + +import org.bitcoinj.core.Transaction; /** * @author John L. Jegutanis */ -public interface WalletPocketEventListener { +public interface WalletAccountEventListener { - void onNewBalance(Coin newBalance, Coin pendingAmount); + void onNewBalance(Value newBalance, Value pendingAmount); void onNewBlock(WalletAccount pocket); diff --git a/core/src/main/java/com/coinomi/core/wallet/WalletPocketHD.java b/core/src/main/java/com/coinomi/core/wallet/WalletPocketHD.java index c4b0807..f3747e0 100644 --- a/core/src/main/java/com/coinomi/core/wallet/WalletPocketHD.java +++ b/core/src/main/java/com/coinomi/core/wallet/WalletPocketHD.java @@ -61,6 +61,7 @@ import static com.coinomi.core.Preconditions.checkArgument; import static com.coinomi.core.Preconditions.checkNotNull; import static com.coinomi.core.Preconditions.checkState; +import static org.bitcoinj.wallet.KeyChain.KeyPurpose.REFUND; /** * @author John L. Jegutanis @@ -72,6 +73,7 @@ public class WalletPocketHD extends TransactionWatcherWallet { private final TransactionCreator transactionCreator; + private final String id; private String description; @VisibleForTesting SimpleHDKeyChain keys; @@ -86,7 +88,8 @@ public WalletPocketHD(DeterministicKey rootKey, CoinType coinType, } WalletPocketHD(String id, SimpleHDKeyChain keys, CoinType coinType) { - super(id, checkNotNull(coinType)); + super(checkNotNull(coinType)); + this.id = id; this.keys = checkNotNull(keys); transactionCreator = new TransactionCreator(this); } @@ -107,6 +110,12 @@ public int getAccountIndex() { } } + + @Override + public String getId() { + return id; + } + /** * Set the description of the wallet. * This is a Unicode encoding string typically entered by the user as descriptive text for the wallet. @@ -127,6 +136,13 @@ public String getDescription() { return description; } + @Override + public boolean equals(WalletAccount other) { + return other != null && + getId().equals(other.getId()) && + getCoinType().equals(other.getCoinType()); + } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Serialization support @@ -377,14 +393,28 @@ public Address getReceiveAddress() { return currentAddress(RECEIVE_FUNDS); } + /** {@inheritDoc} */ + @Override + public Address getRefundAddress() { + return currentAddress(REFUND); + } + + public Address getReceiveAddress(boolean isManualAddressManagement) { + return getAddress(RECEIVE_FUNDS, isManualAddressManagement); + } + + public Address getRefundAddress(boolean isManualAddressManagement) { + return getAddress(REFUND, isManualAddressManagement); + } + /** * Get the last used receiving address */ @Nullable - public Address getLastUsedReceiveAddress() { + public Address getLastUsedAddress(SimpleHDKeyChain.KeyPurpose purpose) { lock.lock(); try { - DeterministicKey lastUsedKey = keys.getLastIssuedKey(RECEIVE_FUNDS); + DeterministicKey lastUsedKey = keys.getLastIssuedKey(purpose); if (lastUsedKey != null) { return lastUsedKey.toAddress(coinType); } else { @@ -446,6 +476,24 @@ public Address getFreshReceiveAddress() throws Bip44KeyLookAheadExceededExceptio } } + public Address getFreshReceiveAddress(boolean isManualAddressManagement) throws Bip44KeyLookAheadExceededException { + lock.lock(); + try { + Address newAddress = null; + Address freshAddress = getFreshReceiveAddress(); + if (isManualAddressManagement) { + newAddress = getLastUsedAddress(RECEIVE_FUNDS); + } + if (newAddress == null) { + newAddress = freshAddress; + } + return newAddress; + } finally { + lock.unlock(); + walletSaveNow(); + } + } + private static final Comparator HD_KEY_COMPARATOR = new Comparator() { @Override @@ -510,6 +558,19 @@ public Set
getUsedAddresses() { } } + public Address getAddress(SimpleHDKeyChain.KeyPurpose purpose, + boolean isManualAddressManagement) { + Address receiveAddress = null; + if (isManualAddressManagement) { + receiveAddress = getLastUsedAddress(purpose); + } + + if (receiveAddress == null) { + receiveAddress = currentAddress(purpose); + } + return receiveAddress; + } + /** * Get the currently latest unused address by purpose. */ @@ -547,4 +608,9 @@ public List
getActiveAddresses() { public void markAddressAsUsed(Address address) { keys.markPubHashAsUsed(address.getHash160()); } + + @Override + public String toString() { + return WalletPocketHD.class.getSimpleName() + " " + id.substring(0, 4)+ " " + coinType; + } } diff --git a/core/src/test/java/com/coinomi/core/coins/ValueTest.java b/core/src/test/java/com/coinomi/core/coins/ValueTest.java index e10188e..16cba65 100644 --- a/core/src/test/java/com/coinomi/core/coins/ValueTest.java +++ b/core/src/test/java/com/coinomi/core/coins/ValueTest.java @@ -20,37 +20,66 @@ import static com.coinomi.core.coins.Value.*; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertFalse; import static org.junit.Assert.fail; +import org.bitcoinj.core.Coin; import org.junit.Test; +import java.math.BigDecimal; + public class ValueTest { - ValueType[] types = {BitcoinMain.get(), LitecoinMain.get(), NuBitsMain.get(), FiatType.get("USD")}; + final CoinType BTC = BitcoinMain.get(); + final CoinType LTC = LitecoinMain.get(); + final CoinType NBT = NuBitsMain.get(); + final FiatType USD = FiatType.get("USD"); + + ValueType[] types = {BTC, LTC, NBT, USD}; @Test - public void testParseCoin() { + public void testParseValue() { for (ValueType type : types) { - runTestParseCoin(type); + runTestParseValue(type); } } - public void runTestParseCoin(ValueType type) { + public void runTestParseValue(ValueType type) { // String version Value cent = type.oneCoin().divide(100); - assertEquals(cent, parseValue(type, "0.01")); - assertEquals(cent, parseValue(type, "1E-2")); - assertEquals(type.oneCoin().add(cent), parseValue(type, "1.01")); - assertEquals(type.oneCoin().negate(), parseValue(type, "-1")); + assertEquals(cent, parse(type, "0.01")); + assertEquals(cent, parse(type, "1E-2")); + assertEquals(type.oneCoin().add(cent), parse(type, "1.01")); + assertEquals(type.oneCoin().negate(), parse(type, "-1")); try { - parseValue(type, "2E-20"); + parse(type, "2E-20"); org.junit.Assert.fail("should not have accepted fractional satoshis"); - } catch (ArithmeticException e) { + } catch (ArithmeticException e) {} + } + + + @Test + public void testParseValue2() { + for (ValueType type : types) { + runTestParseValue2(type); } } + public void runTestParseValue2(ValueType type) { + // BigDecimal version + Value cent = type.oneCoin().divide(100); + BigDecimal bigDecimalCent = BigDecimal.ONE.divide(new BigDecimal(100)); + assertEquals(cent, parse(type, bigDecimalCent)); + assertEquals(type.oneCoin().add(cent), parse(type, BigDecimal.ONE.add(bigDecimalCent))); + assertEquals(type.oneCoin().negate(), parse(type, BigDecimal.ONE.negate())); + try { + parse(type, new BigDecimal("2E-20")); + org.junit.Assert.fail("should not have accepted fractional satoshis"); + } catch (ArithmeticException e) {} + } + @Test public void testValueOf() { for (ValueType type : types) { @@ -94,4 +123,74 @@ public void runTestOperators(ValueType type) { assertFalse(valueOf(type, 2).isLessThan(valueOf(type, 2))); assertFalse(valueOf(type, 2).isLessThan(valueOf(type, 1))); } + + @Test + public void testEquals() { + Value btcSatoshi = Value.valueOf(BitcoinMain.get(), 1); + Value btcSatoshi2 = Value.valueOf(BitcoinMain.get(), 1); + Value btcValue = Value.parse(BitcoinMain.get(), "3.14159"); + Value ltcSatoshi = Value.valueOf(LitecoinMain.get(), 1); + Value ppcValue = Value.parse(PeercoinMain.get(), "3.14159"); + + assertTrue(btcSatoshi.equals(btcSatoshi2)); + assertFalse(btcSatoshi.equals(ltcSatoshi)); + assertFalse(btcSatoshi.equals(btcValue)); + assertFalse(btcSatoshi.equals(ppcValue)); + assertFalse(btcValue.equals(ppcValue)); + } + + @Test + public void testIsOfType() { + assertTrue(BTC.oneCoin().isOfType(BTC)); + assertTrue(BTC.oneCoin().isOfType(BTC.oneCoin())); + assertFalse(BTC.oneCoin().isOfType(LTC)); + assertFalse(BTC.oneCoin().isOfType(LTC.oneCoin())); + } + + @Test + public void testWithin() { + assertTrue(BTC.value("5").within(BTC.value("1"), BTC.value("10"))); + assertTrue(BTC.value("1").within(BTC.value("1"), BTC.value("10"))); + assertTrue(BTC.value("10").within(BTC.value("1"), BTC.value("10"))); + assertFalse(BTC.value("0.1").within(BTC.value("1"), BTC.value("10"))); + assertFalse(BTC.value("11").within(BTC.value("1"), BTC.value("10"))); + } + + @Test + public void testMathOperators() { + assertEquals(BTC.value("3.14159"), BTC.value("3").add(BTC.value(".14159"))); + assertEquals(BTC.value("2"), BTC.oneCoin().add(Coin.COIN)); + assertEquals(LTC.value("1"), LTC.value("100").subtract(LTC.value("99"))); + assertEquals(100L, USD.value("100").divide(USD.value("1"))); + assertArrayEquals(new Value[]{NBT.value("0.0001"), NBT.value("0.0002")}, + NBT.value("0.0012").divideAndRemainder(10)); + // max + assertEquals(BTC.value("10"), Value.max(BTC.value("1"), BTC.value("10"))); + assertEquals(BTC.value("0.5"), Value.max(BTC.value("0.5"), BTC.value("-0.5"))); + assertEquals(BTC.value("1"), Value.max(BTC.value("1"), BTC.value("0"))); + // min + assertEquals(BTC.value("1"), Value.min(BTC.value("1"), BTC.value("10"))); + assertEquals(BTC.value("-0.5"), Value.min(BTC.value("0.5"), BTC.value("-0.5"))); + assertEquals(BTC.value("0"), Value.min(BTC.value("1"), BTC.value("0"))); + } + + @Test(expected = IllegalArgumentException.class) + public void testAddFail() { + BTC.oneCoin().add(LTC.oneCoin()); + } + + @Test(expected = IllegalArgumentException.class) + public void testSubtractFail() { + BTC.oneCoin().subtract(LTC.oneCoin()); + } + + @Test(expected = IllegalArgumentException.class) + public void testDivideFail() { + BTC.oneCoin().divide(LTC.oneCoin()); + } + + @Test(expected = IllegalArgumentException.class) + public void testCompareFail() { + BTC.oneCoin().divide(LTC.oneCoin()); + } } diff --git a/core/src/test/java/com/coinomi/core/exchange/shapeshift/ExchangeRateTest.java b/core/src/test/java/com/coinomi/core/exchange/shapeshift/ExchangeRateTest.java new file mode 100644 index 0000000..2121e7d --- /dev/null +++ b/core/src/test/java/com/coinomi/core/exchange/shapeshift/ExchangeRateTest.java @@ -0,0 +1,62 @@ +package com.coinomi.core.exchange.shapeshift; + +import com.coinomi.core.coins.BitcoinMain; +import com.coinomi.core.coins.CoinType; +import com.coinomi.core.coins.DogecoinMain; +import com.coinomi.core.coins.FiatValue; +import com.coinomi.core.coins.NuBitsMain; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftExchangeRate; +import com.coinomi.core.util.ExchangeRateBase; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author John L. Jegutanis + */ +public class ExchangeRateTest { + final CoinType BTC = BitcoinMain.get(); + final CoinType DOGE = DogecoinMain.get(); + final CoinType NBT = NuBitsMain.get(); + + + @Test + public void baseFee() { + ShapeShiftExchangeRate rate = new ShapeShiftExchangeRate(BTC, NBT, "100", "0.01"); + + assertEquals(BTC.value("1"), rate.value1); + assertEquals(NBT.value("100"), rate.value2); + assertEquals(NBT.value("0.01"), rate.minerFee); + + assertEquals(NBT.value("99.99"), rate.convert(BTC.oneCoin())); + assertEquals(BTC.value("1"), rate.convert(NBT.value("99.99"))); + + rate = new ShapeShiftExchangeRate(BTC.oneCoin(), + DOGE.value("1911057.69230769"), DOGE.value("1")); + assertEquals(BTC.value("1"), rate.value1); + assertEquals(DOGE.value("1911057.69230769"), rate.value2); + assertEquals(DOGE.value("1"), rate.minerFee); + assertEquals(BTC.value("0.00052379"), rate.convert(DOGE.value("1000"))); + + rate = new ShapeShiftExchangeRate(BTC.oneCoin(), + DOGE.value("1878207.54716981"), DOGE.value("1")); + assertEquals(BTC.value("1"), rate.value1); + assertEquals(DOGE.value("1878207.54716981"), rate.value2); + assertEquals(DOGE.value("1"), rate.minerFee); + assertEquals(BTC.value("0.00532476"), rate.convert(DOGE.value("10000"))); + } + + @Test + public void zeroValues() { + ShapeShiftExchangeRate rate = new ShapeShiftExchangeRate(BTC, NBT, "100", "0.01"); + assertEquals(BTC.value("0"), rate.convert(NBT.value("0"))); + assertEquals(NBT.value("0"), rate.convert(BTC.value("0"))); + } + + @Test + public void smallValues() { + ShapeShiftExchangeRate rate = new ShapeShiftExchangeRate(DOGE, BTC, "5.1e-7", "0.0001"); + assertEquals(BTC.value("0"), rate.convert(DOGE.value("1"))); + } +} \ No newline at end of file diff --git a/core/src/test/java/com/coinomi/core/exchange/shapeshift/MessagesTest.java b/core/src/test/java/com/coinomi/core/exchange/shapeshift/MessagesTest.java new file mode 100644 index 0000000..f4e50f0 --- /dev/null +++ b/core/src/test/java/com/coinomi/core/exchange/shapeshift/MessagesTest.java @@ -0,0 +1,569 @@ +package com.coinomi.core.exchange.shapeshift; + +import com.coinomi.core.coins.BitcoinMain; +import com.coinomi.core.coins.CoinType; +import com.coinomi.core.coins.DogecoinMain; +import com.coinomi.core.coins.LitecoinMain; +import com.coinomi.core.coins.NuBitsMain; +import com.coinomi.core.coins.Value; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftAmountTx; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftCoin; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftCoins; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftEmail; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftException; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftLimit; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftMarketInfo; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftNormalTx; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftRate; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftTime; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftTxStatus; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Test; + +import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; + +/** + * @author John L. Jegutanis + */ +public class MessagesTest { + final CoinType BTC = BitcoinMain.get(); + final CoinType LTC = LitecoinMain.get(); + final CoinType DOGE = DogecoinMain.get(); + final CoinType NBT = NuBitsMain.get(); + final Value ONE_BTC = BTC.oneCoin(); + final Value ONE_LTC = LTC.oneCoin(); + + @Test + public void testCoins() throws JSONException, ShapeShiftException { + JSONObject json = new JSONObject( + "{" + + "BTC: {" + + "name: \"Bitcoin\"," + + "symbol: \"BTC\"," + + "image: \"https://shapeshift.io/images/coins/bitcoin.png\"," + + "status: \"available\"" + + "}," + + "LTC: {" + + "name: \"Litecoin\"," + + "symbol: \"LTC\"," + + "image: \"https://shapeshift.io/images/coins/litecoin.png\"," + + "status: \"unavailable\"" + + "}," + + "UNSUPPORTED: {" + + "name: \"UnsupportedCoin\"," + + "symbol: \"UNSUPPORTED\"," + + "image: \"https://shapeshift.io/images/coins/UnsupportedCoin.png\"," + + "status: \"available\"" + + "}" + + "}"); + ShapeShiftCoins coins = new ShapeShiftCoins(json); + assertNotNull(coins); + assertFalse(coins.isError); + assertNotNull(coins.coins); + assertEquals(3, coins.coins.size()); + // LTC is unavailable and UNSUPPORTED is unsupported + assertEquals(1, coins.availableCoinTypes.size()); + assertEquals(BTC, coins.availableCoinTypes.get(0)); + + for (ShapeShiftCoin coin : coins.coins) { + JSONObject expected = json.getJSONObject(coin.symbol); + assertEquals(expected.getString("name"), coin.name); + assertEquals(expected.getString("symbol"), coin.symbol); + assertEquals(expected.getString("image"), coin.image.toString()); + assertEquals(expected.getString("status").equals("available"), coin.isAvailable); + } + } + + @Test + public void testCoin() throws JSONException, ShapeShiftException { + JSONObject json = new JSONObject( + "{" + + "name: \"Bitcoin\"," + + "symbol: \"BTC\"," + + "image: \"https://shapeshift.io/images/coins/bitcoin.png\"," + + "status: \"available\"" + + "}"); + ShapeShiftCoin coin = new ShapeShiftCoin(json); + assertNotNull(coin); + assertFalse(coin.isError); + assertEquals("Bitcoin", coin.name); + assertEquals("BTC", coin.symbol); + assertEquals("https://shapeshift.io/images/coins/bitcoin.png", coin.image.toString()); + assertTrue(coin.isAvailable); + } + + + @Test + public void testMarketInfo() throws JSONException, ShapeShiftException { + JSONObject json = new JSONObject( + "{" + + "\"pair\" : \"btc_nbt\"," + + "\"rate\" : \"100\"," + + "\"minerFee\" : \"0.01\"," + + "\"limit\" : \"4\"," + + "\"minimum\" : 0.00000104" + + "}"); + ShapeShiftMarketInfo marketInfo = new ShapeShiftMarketInfo(json); + assertNotNull(marketInfo); + assertFalse(marketInfo.isError); + assertEquals("btc_nbt", marketInfo.pair); + assertTrue(marketInfo.isPair("BTC_NBT")); + assertTrue(marketInfo.isPair("btc_nbt")); + assertTrue(marketInfo.isPair(BTC, NBT)); + assertFalse(marketInfo.isPair("doge_ltc")); + assertFalse(marketInfo.isPair(DOGE, LTC)); + assertNotNull(marketInfo.rate); + assertNotNull(marketInfo.limit); + assertNotNull(marketInfo.minimum); + + assertEquals(NBT.value("99.99"), marketInfo.rate.convert(BTC.value("1"))); + assertEquals(BTC.value("4"), marketInfo.limit); + assertEquals(BTC.value("0.00000104"), marketInfo.minimum); + } + + @Test + public void testRateWithoutMinerFee() throws JSONException, ShapeShiftException { + JSONObject json = new JSONObject( + "{" + + "\"pair\" : \"btc_ltc\"," + + "\"rate\" : \"100\"" + + "}"); + ShapeShiftRate rate = new ShapeShiftRate(json); + assertNotNull(rate); + assertFalse(rate.isError); + assertEquals("btc_ltc", rate.pair); + assertNotNull(rate.rate); + + assertEquals(LTC.value("100"), rate.rate.convert(BTC.value("1"))); + assertEquals(LTC.value("1"), rate.rate.convert(BTC.value("0.01"))); + assertEquals(BTC.value("0.01"), rate.rate.convert(LTC.value("1"))); + assertEquals(BTC.value("1"), rate.rate.convert(LTC.value("100"))); + } + + @Test + public void testRateWithoutMinerFee2() throws JSONException, ShapeShiftException { + JSONObject json = new JSONObject( + "{" + + "\"pair\" : \"btc_nbt\"," + + "\"rate\" : \"123.456789\"" + + "}"); + ShapeShiftRate rate = new ShapeShiftRate(json); + assertNotNull(rate); + assertFalse(rate.isError); + assertEquals("btc_nbt", rate.pair); + assertNotNull(rate.rate); + + assertEquals(NBT, rate.rate.convert(ONE_BTC).type); + assertEquals(BTC, rate.rate.convert(NBT.oneCoin()).type); + } + + @Test + public void testRateWithMinerFee() throws JSONException, ShapeShiftException { + JSONObject json = new JSONObject( + "{" + + "\"pair\" : \"btc_nbt\"," + + "\"rate\" : \"100\"," + + "\"minerFee\" : \"0.01\"" + + "}"); + ShapeShiftRate rate = new ShapeShiftRate(json); + assertNotNull(rate); + assertFalse(rate.isError); + assertEquals("btc_nbt", rate.pair); + assertNotNull(rate.rate); + + assertEquals(NBT.value("99.99"), rate.rate.convert(BTC.value("1"))); + assertEquals(BTC.value("1"), rate.rate.convert(NBT.value("99.99"))); + } + + @Test + public void testLimit() throws JSONException, ShapeShiftException { + JSONObject json = new JSONObject( + "{" + + "\"pair\" : \"ltc_doge\"," + + "\"limit\" : \"200\"," + + "\"min\" : 0.00014772" + + "}"); + ShapeShiftLimit limit = new ShapeShiftLimit(json); + assertNotNull(limit); + assertFalse(limit.isError); + assertEquals("ltc_doge", limit.pair); + assertNotNull(limit.limit); + assertNotNull(limit.minimum); + + assertEquals(LTC.value("200"), limit.limit); + assertEquals(LTC.value("0.00014772"), limit.minimum); + } + + @Test + public void testLimit2() throws JSONException, ShapeShiftException { + JSONObject json = new JSONObject( + "{" + + "pair: \"nbt_btc\"," + + "limit: \"1015.15359146\"," + + "min: 0.053118518219312046" + + "}"); + ShapeShiftLimit limit = new ShapeShiftLimit(json); + assertNotNull(limit); + assertFalse(limit.isError); + assertEquals("nbt_btc", limit.pair); + assertNotNull(limit.limit); + assertNotNull(limit.minimum); + + assertEquals(NBT.value("1015.1535"), limit.limit); + assertEquals(NBT.value("0.0532"), limit.minimum); + } + + @Test + public void testTime() throws JSONException, ShapeShiftException { + JSONObject json = new JSONObject( + "{" + + "status: \"pending\"," + + "seconds_remaining: \"100\"" + + "}"); + ShapeShiftTime time = new ShapeShiftTime(json); + assertNotNull(time); + assertFalse(time.isError); + assertEquals(ShapeShiftTime.Status.PENDING, time.status); + assertEquals(100, time.secondsRemaining); + } + + @Test + public void testTime2() throws JSONException, ShapeShiftException { + JSONObject json = new JSONObject( + "{" + + "status: \"expired\"," + + "seconds_remaining: \"0\"" + + "}"); + ShapeShiftTime time = new ShapeShiftTime(json); + assertNotNull(time); + assertFalse(time.isError); + assertEquals(ShapeShiftTime.Status.EXPIRED, time.status); + assertEquals(0, time.secondsRemaining); + } + + @Test + public void testTxStatus() throws JSONException, ShapeShiftException { + JSONObject json = new JSONObject( + "{" + + "status: \"no_deposits\"," + + "address: \"1NDQPAGamGePkSZXW2CYBzXJEefB7N4bTN\"" + + "}"); + ShapeShiftTxStatus txStatus = new ShapeShiftTxStatus(json); + assertNotNull(txStatus); + assertFalse(txStatus.isError); + assertEquals(ShapeShiftTxStatus.Status.NO_DEPOSITS, txStatus.status); +// assertEquals("1NDQPAGamGePkSZXW2CYBzXJEefB7N4bTN", txStatus.address.toString()); + assertNull(txStatus.withdraw); + assertNull(txStatus.incomingValue); + assertNull(txStatus.outgoingValue); + assertNull(txStatus.transactionId); + } + + @Test + public void testTxStatus2() throws JSONException, ShapeShiftException { + JSONObject json = new JSONObject( + "{" + + "status: \"received\"," + + "address: \"1NDQPAGamGePkSZXW2CYBzXJEefB7N4bTN\"," + + "incomingCoin: 0.00297537," + + "incomingType: \"BTC\"" + + "}"); + ShapeShiftTxStatus txStatus = new ShapeShiftTxStatus(json); + assertNotNull(txStatus); + assertFalse(txStatus.isError); + assertEquals(ShapeShiftTxStatus.Status.RECEIVED, txStatus.status); + assertEquals("1NDQPAGamGePkSZXW2CYBzXJEefB7N4bTN", txStatus.address.toString()); + assertEquals(BTC, txStatus.address.getParameters()); + assertNull(txStatus.withdraw); + assertEquals(BTC.value("0.00297537"), txStatus.incomingValue); + assertNull(txStatus.outgoingValue); + assertNull(txStatus.transactionId); + } + + @Test + public void testTxStatus3() throws JSONException, ShapeShiftException { + JSONObject json = new JSONObject( + "{" + + "status: \"complete\"," + + "address: \"1NDQPAGamGePkSZXW2CYBzXJEefB7N4bTN\"," + + "withdraw: \"LMmeBWH17TWkQKvK7YFio2oiimPAzrHG6f\"," + + "incomingCoin: 0.00297537," + + "incomingType: \"BTC\"," + + "outgoingCoin: \"0.42000000\"," + + "outgoingType: \"LTC\"," + + "transaction: " + + "\"66fa0b4c11227f9f05efa13d23e58c65b50acbd6395a126b5cd751064e6e79df\"" + + "}"); + ShapeShiftTxStatus txStatus = new ShapeShiftTxStatus(json); + assertNotNull(txStatus); + assertFalse(txStatus.isError); + assertEquals(ShapeShiftTxStatus.Status.COMPLETE, txStatus.status); + assertEquals("1NDQPAGamGePkSZXW2CYBzXJEefB7N4bTN", txStatus.address.toString()); + assertEquals(BTC, txStatus.address.getParameters()); + assertEquals("LMmeBWH17TWkQKvK7YFio2oiimPAzrHG6f", txStatus.withdraw.toString()); + assertEquals(LTC, txStatus.withdraw.getParameters()); + assertEquals(BTC.value("0.00297537"), txStatus.incomingValue); + assertEquals(LTC.value("0.42"), txStatus.outgoingValue); + assertEquals("66fa0b4c11227f9f05efa13d23e58c65b50acbd6395a126b5cd751064e6e79df", + txStatus.transactionId); + } + + @Test + public void testTxStatus4() throws JSONException, ShapeShiftException { + JSONObject json = new JSONObject( + "{" + + "status: \"failed\"," + + "error: \"error\"" + + "}"); + ShapeShiftTxStatus txStatus = new ShapeShiftTxStatus(json); + assertNotNull(txStatus); + assertTrue(txStatus.isError); + assertEquals(ShapeShiftTxStatus.Status.FAILED, txStatus.status); + assertEquals("error", txStatus.errorMessage); + assertNull(txStatus.address); + assertNull(txStatus.withdraw); + assertNull(txStatus.incomingValue); + assertNull(txStatus.outgoingValue); + assertNull(txStatus.transactionId); + } + + @Test + public void testTxStatus5() throws JSONException, ShapeShiftException { + JSONObject json = new JSONObject( + "{" + + "status: \"new_fancy_optional_status\"," + + "address: \"1NDQPAGamGePkSZXW2CYBzXJEefB7N4bTN\"," + + "withdraw: \"LMmeBWH17TWkQKvK7YFio2oiimPAzrHG6f\"," + + "incomingCoin: 0.00297537," + + "incomingType: \"BTC\"," + + "outgoingCoin: \"0.42000000\"," + + "outgoingType: \"LTC\"," + + "transaction: " + + "\"66fa0b4c11227f9f05efa13d23e58c65b50acbd6395a126b5cd751064e6e79df\"" + + "}"); + ShapeShiftTxStatus txStatus = new ShapeShiftTxStatus(json); + assertNotNull(txStatus); + assertFalse(txStatus.isError); + assertEquals(ShapeShiftTxStatus.Status.UNKNOWN, txStatus.status); + assertNull(txStatus.address); + assertNull(txStatus.withdraw); + assertNull(txStatus.incomingValue); + assertNull(txStatus.outgoingValue); + assertNull(txStatus.transactionId); + } + + @Test + public void testNormalTx() throws JSONException, ShapeShiftException { + JSONObject json = new JSONObject( + "{" + + "\"deposit\":\"18ETaXCYhJ8sxurh41vpKC3E6Tu7oJ94q8\"," + + "\"depositType\":\"BTC\"," + + "\"withdrawal\":\"DMHLQYG4j96V8cZX9WSuXxLs5RnZn6ibrV\"," + + "\"withdrawalType\":\"DOGE\"" + + "}"); + ShapeShiftNormalTx normalTx = new ShapeShiftNormalTx(json); + assertNotNull(normalTx); + assertFalse(normalTx.isError); + assertEquals("btc_doge", normalTx.pair); + assertEquals("18ETaXCYhJ8sxurh41vpKC3E6Tu7oJ94q8", normalTx.deposit.toString()); + assertEquals(BTC, normalTx.deposit.getParameters()); + assertEquals("DMHLQYG4j96V8cZX9WSuXxLs5RnZn6ibrV", normalTx.withdrawal.toString()); + assertEquals(DOGE, normalTx.withdrawal.getParameters()); + } + + @Test + public void testAmountTxWithoutMinerFee() throws JSONException, ShapeShiftException { + JSONObject json = new JSONObject( + "{" + + "\"success\":{" + + "\"pair\":\"btc_doge\"," + + "\"withdrawal\":\"DMHLQYG4j96V8cZX9WSuXxLs5RnZn6ibrV\"," + + "\"withdrawalAmount\":\"1000\"," + + "\"deposit\":\"14gQ3xywKEUA6CfH61F8t2c6oB5nLnUjL5\"," + + "\"depositAmount\":\"0.00052327\"," + + "\"expiration\":1427149038191," + + "\"quotedRate\":\"1911057.69230769\"" + + "}" + + "}"); + ShapeShiftAmountTx amountTx = new ShapeShiftAmountTx(json); + assertNotNull(amountTx); + assertFalse(amountTx.isError); + assertEquals("btc_doge", amountTx.pair); + assertEquals("14gQ3xywKEUA6CfH61F8t2c6oB5nLnUjL5", amountTx.deposit.toString()); + assertEquals(BTC, amountTx.deposit.getParameters()); + assertEquals(BTC.value("0.00052327"), amountTx.depositAmount); + assertEquals("DMHLQYG4j96V8cZX9WSuXxLs5RnZn6ibrV", amountTx.withdrawal.toString()); + assertEquals(DOGE, amountTx.withdrawal.getParameters()); + assertEquals(DOGE.value("1000"), amountTx.withdrawalAmount); + assertEquals(1427149038191L, amountTx.expiration.getTime()); + assertEquals(BTC.value("0.00052327"), amountTx.rate.convert(Value.parse(DOGE, "1000"))); + } + + @Test + public void testAmountTxWithMinerFee() throws JSONException, ShapeShiftException { + JSONObject json = new JSONObject( + "{" + + "\"success\":{" + + "\"pair\":\"btc_doge\"," + + "\"withdrawal\":\"DMHLQYG4j96V8cZX9WSuXxLs5RnZn6ibrV\"," + + "\"withdrawalAmount\":\"1000\"," + + "\"minerFee\":\"1\"," + + "\"deposit\":\"14gQ3xywKEUA6CfH61F8t2c6oB5nLnUjL5\"," + + "\"depositAmount\":\"0.00052379\"," + + "\"expiration\":1427149038191," + + "\"quotedRate\":\"1911057.69230769\"" + + "}" + + "}"); + ShapeShiftAmountTx amountTx = new ShapeShiftAmountTx(json); + assertNotNull(amountTx); + assertFalse(amountTx.isError); + assertEquals("btc_doge", amountTx.pair); + assertEquals("14gQ3xywKEUA6CfH61F8t2c6oB5nLnUjL5", amountTx.deposit.toString()); + assertEquals(BTC, amountTx.deposit.getParameters()); + assertEquals(BTC.value("0.00052379"), amountTx.depositAmount); + assertEquals("DMHLQYG4j96V8cZX9WSuXxLs5RnZn6ibrV", amountTx.withdrawal.toString()); + assertEquals(DOGE, amountTx.withdrawal.getParameters()); + assertEquals(DOGE.value("1000"), amountTx.withdrawalAmount); + assertEquals(1427149038191L, amountTx.expiration.getTime()); + assertEquals(BTC.value("0.00052379"), amountTx.rate.convert(Value.parse(DOGE, "1000"))); + } + + @Test + public void testEmail() throws JSONException, ShapeShiftException { + JSONObject json = new JSONObject( + "{" + + "\"email\":{" + + "\"status\":\"success\"," + + "\"message\":\"Email receipt sent\"" + + "}" + + "}"); + ShapeShiftEmail email = new ShapeShiftEmail(json); + assertNotNull(email); + assertFalse(email.isError); + assertEquals(ShapeShiftEmail.Status.SUCCESS, email.status); + assertEquals("Email receipt sent", email.message); + } + + @Test + public void testCoinsError() throws JSONException, ShapeShiftException { + JSONObject json = new JSONObject("{ error: \"error\" }"); + ShapeShiftCoins coins = new ShapeShiftCoins(json); + assertNotNull(coins); + assertTrue(coins.isError); + assertEquals("error", coins.errorMessage); + assertNull(coins.coins); + } + + @Test + public void testCoinError() throws JSONException, ShapeShiftException { + JSONObject json = new JSONObject("{ error: \"error\" }"); + ShapeShiftCoin coin = new ShapeShiftCoin(json); + assertNotNull(coin); + assertTrue(coin.isError); + assertEquals("error", coin.errorMessage); + assertNull(coin.name); + assertNull(coin.symbol); + assertNull(coin.image); + assertFalse(coin.isAvailable); + } + + @Test + public void testRateError() throws JSONException, ShapeShiftException { + JSONObject json = new JSONObject("{ error: \"error\" }"); + ShapeShiftRate rate = new ShapeShiftRate(json); + assertNotNull(rate); + assertTrue(rate.isError); + assertEquals("error", rate.errorMessage); + assertNull(rate.pair); + assertNull(rate.rate); + } + + @Test + public void testLimitError() throws JSONException, ShapeShiftException { + JSONObject json = new JSONObject("{ error: \"error\" }"); + ShapeShiftLimit limit = new ShapeShiftLimit(json); + assertNotNull(limit); + assertTrue(limit.isError); + assertEquals("error", limit.errorMessage); + assertNull(limit.pair); + assertNull(limit.limit); + assertNull(limit.minimum); + } + + @Test + public void testTimeError() throws JSONException, ShapeShiftException { + JSONObject json = new JSONObject("{ error: \"error\" }"); + ShapeShiftTime time = new ShapeShiftTime(json); + assertNotNull(time); + assertTrue(time.isError); + assertEquals("error", time.errorMessage); + assertNull(time.status); + assertEquals(-1, time.secondsRemaining); + } + + @Test + public void testTxStatusError() throws JSONException, ShapeShiftException { + JSONObject json = new JSONObject("{ error: \"error\" }"); + ShapeShiftTxStatus txStatus = new ShapeShiftTxStatus(json); + assertNotNull(txStatus); + assertTrue(txStatus.isError); + assertEquals("error", txStatus.errorMessage); + assertNull(txStatus.status); + assertNull(txStatus.address); + assertNull(txStatus.withdraw); + assertNull(txStatus.incomingValue); + assertNull(txStatus.outgoingValue); + assertNull(txStatus.transactionId); + } + + @Test(expected = ShapeShiftException.class) + public void testInvalidCoins() throws ShapeShiftException, JSONException { + JSONObject json = new JSONObject( + "{" + + "BTC: {" + + "name: \"Bitcoin\"," + + "symbol: \"BTC\"," + + "image: \"https://shapeshift.io/images/coins/bitcoin.png\"," + + "status: \"available\"" + + "}," + + "LTC: {" + + "bad: \"\"" + + "}" + + "}"); + new ShapeShiftCoins(json); + } + + @Test(expected = ShapeShiftException.class) + public void testInvalidCoin() throws ShapeShiftException, JSONException { + JSONObject json = new JSONObject( + "{" + + "LTC: {" + + "bad: \"\"" + + "}" + + "}"); + new ShapeShiftCoin(json); + } + + @Test(expected = ShapeShiftException.class) + public void testInvalidRate() throws ShapeShiftException { + new ShapeShiftRate(new JSONObject()); + } + + @Test(expected = ShapeShiftException.class) + public void testInvalidLimit() throws ShapeShiftException { + new ShapeShiftLimit(new JSONObject()); + } + + @Test(expected = ShapeShiftException.class) + public void testInvalidTime() throws ShapeShiftException { + new ShapeShiftTime(new JSONObject()); + } + + @Test(expected = ShapeShiftException.class) + public void testInvalidTxStatus() throws ShapeShiftException { + new ShapeShiftTime(new JSONObject()); + } +} \ No newline at end of file diff --git a/core/src/test/java/com/coinomi/core/exchange/shapeshift/ServerTest.java b/core/src/test/java/com/coinomi/core/exchange/shapeshift/ServerTest.java new file mode 100644 index 0000000..f2ef3b3 --- /dev/null +++ b/core/src/test/java/com/coinomi/core/exchange/shapeshift/ServerTest.java @@ -0,0 +1,492 @@ +package com.coinomi.core.exchange.shapeshift; + +import com.coinomi.core.coins.BitcoinMain; +import com.coinomi.core.coins.CoinType; +import com.coinomi.core.coins.DogecoinMain; +import com.coinomi.core.coins.LitecoinMain; +import com.coinomi.core.coins.NuBitsMain; +import com.coinomi.core.coins.Value; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftAmountTx; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftCoin; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftCoins; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftEmail; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftException; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftLimit; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftMarketInfo; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftNormalTx; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftRate; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftTime; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftTxStatus; +import com.squareup.okhttp.ConnectionSpec; +import com.squareup.okhttp.mockwebserver.MockResponse; +import com.squareup.okhttp.mockwebserver.MockWebServer; +import com.squareup.okhttp.mockwebserver.RecordedRequest; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.AddressFormatException; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.util.Collections; + +import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; + +/** + * @author John L. Jegutanis + */ +public class ServerTest { + + final CoinType BTC = BitcoinMain.get(); + final CoinType LTC = LitecoinMain.get(); + final CoinType DOGE = DogecoinMain.get(); + final CoinType NBT = NuBitsMain.get(); + + private MockWebServer server; + private ShapeShift shapeShift; + + @Before + public void setup() throws IOException { + server = new MockWebServer(); + server.start(); + + shapeShift = new ShapeShift(); + shapeShift.baseUrl = server.getUrl("/").toString(); + shapeShift.client.setConnectionSpecs(Collections.singletonList(ConnectionSpec.CLEARTEXT)); + } + + @After + public void tearDown() throws IOException { + server.shutdown(); + } + + @Test + public void testGetCoins() throws ShapeShiftException, IOException, InterruptedException, JSONException { + // Schedule some responses. + server.enqueue(new MockResponse().setBody(GET_COINS_JSON)); + + ShapeShiftCoins coinsReply = shapeShift.getCoins(); + assertFalse(coinsReply.isError); + assertEquals(3, coinsReply.coins.size()); + assertEquals(1, coinsReply.availableCoinTypes.size()); + assertEquals(BTC, coinsReply.availableCoinTypes.get(0)); + JSONObject coinsJson = new JSONObject(GET_COINS_JSON); + for (ShapeShiftCoin coin : coinsReply.coins) { + JSONObject json = coinsJson.getJSONObject(coin.symbol); + assertEquals(json.getString("name"), coin.name); + assertEquals(json.getString("symbol"), coin.symbol); + assertEquals(json.getString("image"), coin.image.toString()); + assertEquals(json.getString("status").equals("available"), coin.isAvailable); + } + + // Optional: confirm that your app made the HTTP requests you were expecting. + RecordedRequest request = server.takeRequest(); + assertEquals("/getcoins", request.getPath()); + } + + @Test + public void testGetMarketInfo() throws ShapeShiftException, IOException, InterruptedException, JSONException { + // Schedule some responses. + server.enqueue(new MockResponse().setBody(MARKET_INFO_BTC_NBT_JSON)); + + ShapeShiftMarketInfo marketInfoReply = shapeShift.getMarketInfo(BTC, NBT); + assertFalse(marketInfoReply.isError); + assertEquals("btc_nbt", marketInfoReply.pair); + assertNotNull(marketInfoReply.rate); + + assertNotNull(marketInfoReply.rate); + assertNotNull(marketInfoReply.limit); + assertNotNull(marketInfoReply.minimum); + + assertEquals(NBT.value("99.99"), marketInfoReply.rate.convert(BTC.value("1"))); + assertEquals(BTC.value("4"), marketInfoReply.limit); + assertEquals(BTC.value("0.00000104"), marketInfoReply.minimum); + + // Optional: confirm that your app made the HTTP requests you were expecting. + RecordedRequest request = server.takeRequest(); + assertEquals("/marketinfo/btc_nbt", request.getPath()); + } + + @Test + public void testGetRate() throws ShapeShiftException, IOException, InterruptedException, JSONException { + // Schedule some responses. + server.enqueue(new MockResponse().setBody(GET_RATE_BTC_LTC_JSON)); + + ShapeShiftRate rateReply = shapeShift.getRate(BTC, LTC); + assertFalse(rateReply.isError); + assertEquals("btc_ltc", rateReply.pair); + assertNotNull(rateReply.rate); + + assertEquals(LTC, rateReply.rate.convert(BTC.oneCoin()).type); + assertEquals(BTC, rateReply.rate.convert(LTC.oneCoin()).type); + + // Optional: confirm that your app made the HTTP requests you were expecting. + RecordedRequest request = server.takeRequest(); + assertEquals("/rate/btc_ltc", request.getPath()); + } + + @Test + public void testGetLimit() throws ShapeShiftException, IOException, InterruptedException, JSONException { + // Schedule some responses. + server.enqueue(new MockResponse().setBody(GET_LIMIT_BTC_LTC_JSON)); + + ShapeShiftLimit limitReply = shapeShift.getLimit(BTC, LTC); + assertFalse(limitReply.isError); + assertEquals("btc_ltc", limitReply.pair); + assertNotNull(limitReply.limit); + + assertEquals(BTC, limitReply.limit.type); + assertEquals("5", limitReply.limit.toPlainString()); + + // Optional: confirm that your app made the HTTP requests you were expecting. + RecordedRequest request = server.takeRequest(); + assertEquals("/limit/btc_ltc", request.getPath()); + } + + @Test + public void testGetTime() throws ShapeShiftException, IOException, InterruptedException, JSONException, AddressFormatException { + // Schedule some responses. + server.enqueue(new MockResponse().setBody(GET_TIME_PENDING_JSON)); + server.enqueue(new MockResponse().setBody(GET_TIME_EXPIRED_JSON)); + + Address address = new Address(NuBitsMain.get(), "BPjxHqswNZB5vznbrAAxi5zGVq3ruhtBU8"); + + ShapeShiftTime timeReply = shapeShift.getTime(address); + assertFalse(timeReply.isError); + assertEquals(ShapeShiftTime.Status.PENDING, timeReply.status); + assertEquals(100, timeReply.secondsRemaining); + + timeReply = shapeShift.getTime(address); + assertFalse(timeReply.isError); + assertEquals(ShapeShiftTime.Status.EXPIRED, timeReply.status); + assertEquals(0, timeReply.secondsRemaining); + + // Optional: confirm that your app made the HTTP requests you were expecting. + RecordedRequest request = server.takeRequest(); + assertEquals("/timeremaining/BPjxHqswNZB5vznbrAAxi5zGVq3ruhtBU8", request.getPath()); + request = server.takeRequest(); + assertEquals("/timeremaining/BPjxHqswNZB5vznbrAAxi5zGVq3ruhtBU8", request.getPath()); + } + + @Test + public void testGetTxStatus() throws ShapeShiftException, IOException, InterruptedException, JSONException, AddressFormatException { + // Schedule some responses. + server.enqueue(new MockResponse().setBody(TX_STATUS_NO_DEPOSIT_JSON)); + server.enqueue(new MockResponse().setBody(TX_STATUS_RECEIVED_JSON)); + server.enqueue(new MockResponse().setBody(TX_STATUS_NEW_STATUS_JSON)); + server.enqueue(new MockResponse().setBody(TX_STATUS_COMPLETE_JSON)); + server.enqueue(new MockResponse().setBody(TX_STATUS_FAILED_JSON)); + + Address address = new Address(BTC, "1NDQPAGamGePkSZXW2CYBzXJEefB7N4bTN"); + + ShapeShiftTxStatus txStatusReply = shapeShift.getTxStatus(address); + assertFalse(txStatusReply.isError); + assertEquals(ShapeShiftTxStatus.Status.NO_DEPOSITS, txStatusReply.status); + + txStatusReply = shapeShift.getTxStatus(address); + assertFalse(txStatusReply.isError); + assertEquals(ShapeShiftTxStatus.Status.RECEIVED, txStatusReply.status); + assertEquals(address, txStatusReply.address); + assertEquals("0.00297537", txStatusReply.incomingValue.toPlainString()); + assertEquals(BTC, txStatusReply.incomingValue.type); + + txStatusReply = shapeShift.getTxStatus(address); + assertFalse(txStatusReply.isError); + assertEquals(ShapeShiftTxStatus.Status.UNKNOWN, txStatusReply.status); + + txStatusReply = shapeShift.getTxStatus(address); + assertFalse(txStatusReply.isError); + assertEquals(ShapeShiftTxStatus.Status.COMPLETE, txStatusReply.status); + assertEquals(address, txStatusReply.address); + assertEquals("0.00297537", txStatusReply.incomingValue.toPlainString()); + assertEquals(BTC, txStatusReply.incomingValue.type); + assertEquals("LMmeBWH17TWkQKvK7YFio2oiimPAzrHG6f", txStatusReply.withdraw.toString()); + assertEquals(LTC, txStatusReply.withdraw.getParameters()); + assertEquals("0.42", txStatusReply.outgoingValue.toPlainString()); + assertEquals(LTC, txStatusReply.outgoingValue.type); + assertEquals("66fa0b4c11227f9f05efa13d23e58c65b50acbd6395a126b5cd751064e6e79df", + txStatusReply.transactionId); + + txStatusReply = shapeShift.getTxStatus(address); + assertTrue(txStatusReply.isError); + assertEquals(ShapeShiftTxStatus.Status.FAILED, txStatusReply.status); + assertEquals("error", txStatusReply.errorMessage); + + // Optional: confirm that your app made the HTTP requests you were expecting. + RecordedRequest request = server.takeRequest(); + assertEquals("/txStat/1NDQPAGamGePkSZXW2CYBzXJEefB7N4bTN", request.getPath()); + request = server.takeRequest(); + assertEquals("/txStat/1NDQPAGamGePkSZXW2CYBzXJEefB7N4bTN", request.getPath()); + request = server.takeRequest(); + assertEquals("/txStat/1NDQPAGamGePkSZXW2CYBzXJEefB7N4bTN", request.getPath()); + request = server.takeRequest(); + assertEquals("/txStat/1NDQPAGamGePkSZXW2CYBzXJEefB7N4bTN", request.getPath()); + } + + @Test + public void testNormalTransaction() throws ShapeShiftException, IOException, InterruptedException, JSONException, AddressFormatException { + // Schedule some responses. + server.enqueue(new MockResponse().setBody(NORMAL_TRANSACTION_JSON)); + + Address withdrawal = new Address(DOGE, "DMHLQYG4j96V8cZX9WSuXxLs5RnZn6ibrV"); + Address refund = new Address(BTC, "1Nz4xHJjNCnZFPjRUq8CN4BZEXTgLZfeUW"); + ShapeShiftNormalTx normalTxReply = shapeShift.exchange(withdrawal, refund); + assertFalse(normalTxReply.isError); + assertEquals("btc_doge", normalTxReply.pair); + assertEquals("18ETaXCYhJ8sxurh41vpKC3E6Tu7oJ94q8", normalTxReply.deposit.toString()); + assertEquals(BTC, normalTxReply.deposit.getParameters()); + assertEquals(withdrawal.toString(), normalTxReply.withdrawal.toString()); + assertEquals(DOGE, normalTxReply.withdrawal.getParameters()); + + // Optional: confirm that your app made the HTTP requests you were expecting. + RecordedRequest request = server.takeRequest(); + assertEquals("/shift", request.getPath()); + JSONObject reqJson = new JSONObject(request.getBody().readUtf8()); + assertEquals(withdrawal.toString(), reqJson.getString("withdrawal")); + assertEquals(refund.toString(), reqJson.getString("returnAddress")); + assertEquals("btc_doge", reqJson.getString("pair")); + } + + + @Test + public void testFixedAmountTransaction() throws ShapeShiftException, IOException, + InterruptedException, JSONException, AddressFormatException { + // Schedule some responses. + server.enqueue(new MockResponse().setBody(FIXED_AMOUNT_TRANSACTION_JSON)); + + Address withdrawal = new Address(DOGE, "DMHLQYG4j96V8cZX9WSuXxLs5RnZn6ibrV"); + Address refund = new Address(BTC, "1Nz4xHJjNCnZFPjRUq8CN4BZEXTgLZfeUW"); + Value amount = DOGE.value("1000"); + ShapeShiftAmountTx amountTxReply = shapeShift.exchangeForAmount(amount, withdrawal, refund); + assertFalse(amountTxReply.isError); + assertEquals("btc_doge", amountTxReply.pair); + + assertEquals("14gQ3xywKEUA6CfH61F8t2c6oB5nLnUjL5", amountTxReply.deposit.toString()); + assertEquals(BTC, amountTxReply.deposit.getParameters()); + assertEquals("0.00052379", amountTxReply.depositAmount.toPlainString()); + assertEquals(BTC, amountTxReply.depositAmount.type); + + assertEquals(withdrawal.toString(), amountTxReply.withdrawal.toString()); + assertEquals(DOGE, amountTxReply.withdrawal.getParameters()); + assertEquals(amount.toPlainString(), amountTxReply.withdrawalAmount.toPlainString()); + assertEquals(DOGE, amountTxReply.withdrawalAmount.type); + + assertEquals(1427149038191L, amountTxReply.expiration.getTime()); + assertEquals(BTC.value(".00052379"), amountTxReply.rate.convert(Value.parse(DOGE, "1000"))); + + // Optional: confirm that your app made the HTTP requests you were expecting. + RecordedRequest request = server.takeRequest(); + assertEquals("/sendamount", request.getPath()); + JSONObject reqJson = new JSONObject(request.getBody().readUtf8()); + assertEquals(withdrawal.toString(), reqJson.getString("withdrawal")); + assertEquals(refund.toString(), reqJson.getString("returnAddress")); + assertEquals("btc_doge", reqJson.getString("pair")); + assertEquals(amount.toPlainString(), reqJson.getString("amount")); + } + + @Test + public void testEmail() throws ShapeShiftException, IOException, InterruptedException, + JSONException, AddressFormatException { + // Schedule some responses. + server.enqueue(new MockResponse().setBody(TX_STATUS_COMPLETE_JSON)); + server.enqueue(new MockResponse().setBody(EMAIL_JSON)); + + ShapeShiftTxStatus txStatusReply = + shapeShift.getTxStatus(BTC.address("1NDQPAGamGePkSZXW2CYBzXJEefB7N4bTN")); + ShapeShiftEmail emailReply = + shapeShift.requestEmailReceipt("mail@example.com", txStatusReply); + assertFalse(emailReply.isError); + assertEquals(ShapeShiftEmail.Status.SUCCESS, emailReply.status); + assertEquals("Email receipt sent", emailReply.message); + + // Optional: confirm that your app made the HTTP requests you were expecting. + RecordedRequest request = server.takeRequest(); + assertEquals("/txStat/1NDQPAGamGePkSZXW2CYBzXJEefB7N4bTN", request.getPath()); + request = server.takeRequest(); + assertEquals("/mail", request.getPath()); + JSONObject reqJson = new JSONObject(request.getBody().readUtf8()); + assertEquals("mail@example.com", reqJson.getString("email")); + assertEquals("66fa0b4c11227f9f05efa13d23e58c65b50acbd6395a126b5cd751064e6e79df", + reqJson.getString("txid")); + } + + @Test(expected = ShapeShiftException.class) + public void testEmailFail() throws ShapeShiftException, IOException, InterruptedException, + JSONException, AddressFormatException { + server.enqueue(new MockResponse().setBody(TX_STATUS_NO_DEPOSIT_JSON)); + ShapeShiftTxStatus txStatusReply = shapeShift + .getTxStatus(BTC.address("1NDQPAGamGePkSZXW2CYBzXJEefB7N4bTN")); + // Bad status + shapeShift.requestEmailReceipt("mail@example.com", txStatusReply); + } + + @Test(expected = ShapeShiftException.class) + public void testGetMarketInfoFail() throws ShapeShiftException, IOException { + server.enqueue(new MockResponse().setBody(MARKET_INFO_BTC_NBT_JSON)); + // Incorrect pair + shapeShift.getMarketInfo(BTC, LTC); + } + + @Test(expected = ShapeShiftException.class) + public void testGetRateFail() throws ShapeShiftException, IOException { + server.enqueue(new MockResponse().setBody(GET_RATE_BTC_LTC_JSON)); + // Incorrect pair + shapeShift.getRate(NBT, LTC); + } + + @Test(expected = ShapeShiftException.class) + public void testGetLimitFail() throws ShapeShiftException, IOException { + server.enqueue(new MockResponse().setBody(GET_LIMIT_BTC_LTC_JSON)); + // Incorrect pair + shapeShift.getLimit(LTC, DOGE); + } + + @Test(expected = ShapeShiftException.class) + public void testGetTxStatusFail() throws ShapeShiftException, AddressFormatException, IOException { + server.enqueue(new MockResponse().setBody(TX_STATUS_COMPLETE_JSON)); + // Used an incorrect address, correct is 1NDQPAGamGePkSZXW2CYBzXJEefB7N4bTN + shapeShift.getTxStatus(BTC.address("18ETaXCYhJ8sxurh41vpKC3E6Tu7oJ94q8")); + } + + @Test(expected = ShapeShiftException.class) + public void testNormalTransactionFail() throws ShapeShiftException, AddressFormatException, IOException { + server.enqueue(new MockResponse().setBody(NORMAL_TRANSACTION_JSON)); + // Incorrect Dogecoin address, correct is DMHLQYG4j96V8cZX9WSuXxLs5RnZn6ibrV + shapeShift.exchange(DOGE.address("DSntbp199h851m3Y1g3ruYCQHzWYCZQmmA"), + BTC.address("1Nz4xHJjNCnZFPjRUq8CN4BZEXTgLZfeUW")); + } + + @Test(expected = ShapeShiftException.class) + public void testFixedAmountTransactionFail() + throws ShapeShiftException, AddressFormatException, IOException { + server.enqueue(new MockResponse().setBody(FIXED_AMOUNT_TRANSACTION_JSON)); + // We withdraw Dogecoins to a Bitcoin address + shapeShift.exchangeForAmount(DOGE.value("1000"), + BTC.address("18ETaXCYhJ8sxurh41vpKC3E6Tu7oJ94q8"), + BTC.address("1Nz4xHJjNCnZFPjRUq8CN4BZEXTgLZfeUW")); + } + + @Test(expected = ShapeShiftException.class) + public void testFixedAmountTransactionFail2() + throws ShapeShiftException, AddressFormatException, IOException { + server.enqueue(new MockResponse().setBody(FIXED_AMOUNT_TRANSACTION_JSON)); + // Incorrect Dogecoin address, correct is DMHLQYG4j96V8cZX9WSuXxLs5RnZn6ibrV + shapeShift.exchangeForAmount(DOGE.value("1000"), + DOGE.address("DSntbp199h851m3Y1g3ruYCQHzWYCZQmmA"), + BTC.address("1Nz4xHJjNCnZFPjRUq8CN4BZEXTgLZfeUW")); + } + + @Test(expected = ShapeShiftException.class) + public void testFixedAmountTransactionFail3() + throws ShapeShiftException, AddressFormatException, IOException { + server.enqueue(new MockResponse().setBody(FIXED_AMOUNT_TRANSACTION_JSON)); + // Incorrect amount, correct is 1000 + shapeShift.exchangeForAmount(DOGE.value("1"), + DOGE.address("DMHLQYG4j96V8cZX9WSuXxLs5RnZn6ibrV"), + BTC.address("1Nz4xHJjNCnZFPjRUq8CN4BZEXTgLZfeUW")); + } + + public static final String GET_COINS_JSON = + "{" + + "BTC: {" + + "name: \"Bitcoin\"," + + "symbol: \"BTC\"," + + "image: \"https://shapeshift.io/images/coins/bitcoin.png\"," + + "status: \"available\"" + + "}," + + "LTC: {" + + "name: \"Litecoin\"," + + "symbol: \"LTC\"," + + "image: \"https://shapeshift.io/images/coins/litecoin.png\"," + + "status: \"unavailable\"" + + "}," + + "UNSUPPORTED: {" + + "name: \"UnsupportedCoin\"," + + "symbol: \"UNSUPPORTED\"," + + "image: \"https://shapeshift.io/images/coins/UnsupportedCoin.png\"," + + "status: \"available\"" + + "}" + + "}"; + public static final String MARKET_INFO_BTC_NBT_JSON = "{" + + "\"pair\" : \"btc_nbt\"," + + "\"rate\" : \"100\"," + + "\"minerFee\" : \"0.01\"," + + "\"limit\" : \"4\"," + + "\"minimum\" : 0.00000104" + + "}"; + public static final String GET_RATE_BTC_LTC_JSON = "{" + + "\"pair\" : \"btc_ltc\"," + + "\"rate\" : \"100\"" + + "}"; + public static final String GET_LIMIT_BTC_LTC_JSON = "{" + + "\"pair\" : \"btc_ltc\"," + + "\"limit\" : \"5\"," + + "\"min\" : 0.00004008" + + "}"; + public static final String GET_TIME_PENDING_JSON = "{" + + "status: \"pending\"," + + "seconds_remaining: \"100\"" + + "}"; + public static final String GET_TIME_EXPIRED_JSON = "{" + + "status: \"expired\"," + + "seconds_remaining: \"0\"" + + "}"; + public static final String TX_STATUS_NO_DEPOSIT_JSON = "{" + + "status: \"no_deposits\"," + + "address: \"1NDQPAGamGePkSZXW2CYBzXJEefB7N4bTN\"" + + "}"; + public static final String TX_STATUS_RECEIVED_JSON = "{" + + "status: \"received\"," + + "address: \"1NDQPAGamGePkSZXW2CYBzXJEefB7N4bTN\"," + + "incomingCoin: 0.00297537," + + "incomingType: \"BTC\"" + + "}"; + public static final String TX_STATUS_NEW_STATUS_JSON = "{" + + "status: \"some_new_optional_status\"," + + "address: \"1NDQPAGamGePkSZXW2CYBzXJEefB7N4bTN\"," + + "incomingCoin: 0.00297537," + + "incomingType: \"BTC\"" + + "}"; + public static final String TX_STATUS_COMPLETE_JSON = "{" + + "status: \"complete\"," + + "address: \"1NDQPAGamGePkSZXW2CYBzXJEefB7N4bTN\"," + + "withdraw: \"LMmeBWH17TWkQKvK7YFio2oiimPAzrHG6f\"," + + "incomingCoin: 0.00297537," + + "incomingType: \"BTC\"," + + "outgoingCoin: \"0.42000000\"," + + "outgoingType: \"LTC\"," + + "transaction: \"66fa0b4c11227f9f05efa13d23e58c65b50acbd6395a126b5cd751064e6e79df\"" + + "}"; + public static final String TX_STATUS_FAILED_JSON = "{" + + "status: \"failed\"," + + "error: \"error\"" + + "}"; + public static final String NORMAL_TRANSACTION_JSON = "{" + + "\"deposit\":\"18ETaXCYhJ8sxurh41vpKC3E6Tu7oJ94q8\"," + + "\"depositType\":\"BTC\"," + + "\"withdrawal\":\"DMHLQYG4j96V8cZX9WSuXxLs5RnZn6ibrV\"," + + "\"withdrawalType\":\"DOGE\"" + + "}"; + public static final String FIXED_AMOUNT_TRANSACTION_JSON = "{" + + "\"success\":{" + + "\"pair\":\"btc_doge\"," + + "\"withdrawal\":\"DMHLQYG4j96V8cZX9WSuXxLs5RnZn6ibrV\"," + + "\"withdrawalAmount\":\"1000\"," + + "\"minerFee\":\"1\"," + + "\"deposit\":\"14gQ3xywKEUA6CfH61F8t2c6oB5nLnUjL5\"," + + "\"depositAmount\":\"0.00052379\"," + + "\"expiration\":1427149038191," + + "\"quotedRate\":\"1911057.69230769\"," + + "}" + + "}"; + public static final String EMAIL_JSON = "{" + + "\"email\":{" + + "\"status\":\"success\"," + + "\"message\":\"Email receipt sent\"" + + "}" + + "}"; +} diff --git a/core/src/test/java/com/coinomi/core/util/ExchangeRateTest.java b/core/src/test/java/com/coinomi/core/util/ExchangeRateTest.java index 0de2d9b..57770e5 100644 --- a/core/src/test/java/com/coinomi/core/util/ExchangeRateTest.java +++ b/core/src/test/java/com/coinomi/core/util/ExchangeRateTest.java @@ -18,97 +18,145 @@ */ import com.coinomi.core.coins.BitcoinMain; +import com.coinomi.core.coins.CoinType; import com.coinomi.core.coins.FiatType; import com.coinomi.core.coins.FiatValue; import com.coinomi.core.coins.LitecoinMain; import com.coinomi.core.coins.NuBitsMain; import com.coinomi.core.coins.Value; -import static org.junit.Assert.assertEquals; - -import org.bitcoinj.core.NetworkParameters; import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + public class ExchangeRateTest { + final CoinType BTC = BitcoinMain.get(); + final CoinType LTC = LitecoinMain.get(); + final CoinType NBT = NuBitsMain.get(); + final Value oneBtc = BTC.oneCoin(); + final Value oneNbt = NBT.oneCoin(); @Test public void getOtherType() throws Exception { - ExchangeRate rate = new ExchangeRate(BitcoinMain.get().oneCoin(), LitecoinMain.get().oneCoin().multiply(100)); + ExchangeRateBase rate = new ExchangeRateBase(oneBtc, LTC.oneCoin().multiply(100)); - assertEquals(BitcoinMain.get(), rate.getOtherType(LitecoinMain.get())); - assertEquals(LitecoinMain.get(), rate.getOtherType(BitcoinMain.get())); + assertEquals(BTC, rate.getOtherType(LTC)); + assertEquals(LTC, rate.getOtherType(BTC)); + } + + @Test + public void canConvert() throws Exception { + ExchangeRateBase rate = new ExchangeRateBase(oneBtc, oneNbt); + + assertTrue(rate.canConvert(BTC, NBT)); + assertTrue(rate.canConvert(NBT, BTC)); + assertFalse(rate.canConvert(LTC, BTC)); + assertFalse(rate.canConvert(LTC, FiatType.get("USD"))); } @Test public void cryptoToFiatRate() throws Exception { - ExchangeRate rate = new ExchangeRate(BitcoinMain.get().oneCoin(), FiatValue.parse("EUR", "500")); + ExchangeRateBase rate = new ExchangeRateBase(oneBtc, FiatValue.parse("EUR", "500")); - assertEquals(FiatType.get("EUR"), rate.convert(BitcoinMain.get().oneCoin()).type); - assertEquals(BitcoinMain.get(), rate.convert(FiatValue.parse("EUR", "1")).type); + assertEquals(FiatType.get("EUR"), rate.convert(oneBtc).type); + assertEquals(BTC, rate.convert(FiatValue.parse("EUR", "1")).type); - assertEquals("0.5", rate.convert(BitcoinMain.get().oneCoin().divide(1000)).toPlainString()); + assertEquals("0.5", rate.convert(oneBtc.divide(1000)).toPlainString()); assertEquals("0.002", rate.convert(FiatValue.parse("EUR", "1")).toPlainString()); } @Test public void cryptoToCryptoRate() throws Exception { // 1BTC = 100LTC - ExchangeRate rate = new ExchangeRate(BitcoinMain.get().oneCoin(), LitecoinMain.get().oneCoin().multiply(100)); + ExchangeRateBase rate = new ExchangeRateBase(oneBtc, LTC.oneCoin().multiply(100)); - assertEquals(LitecoinMain.get(), rate.convert(BitcoinMain.get().oneCoin()).type); - assertEquals(BitcoinMain.get(), rate.convert(LitecoinMain.get().oneCoin()).type); + assertEquals(LTC, rate.convert(oneBtc).type); + assertEquals(BTC, rate.convert(LTC.oneCoin()).type); - assertEquals("1", rate.convert(BitcoinMain.get().oneCoin().divide(100)).toPlainString()); - assertEquals("0.01", rate.convert(LitecoinMain.get().oneCoin()).toPlainString()); + assertEquals("1", rate.convert(oneBtc.divide(100)).toPlainString()); + assertEquals("0.01", rate.convert(LTC.oneCoin()).toPlainString()); // 250NBT = 1BTC - rate = new ExchangeRate(NuBitsMain.get().oneCoin().multiply(250), BitcoinMain.get().oneCoin()); - assertEquals("0.004", rate.convert(NuBitsMain.get().oneCoin()).toPlainString()); - assertEquals("2500", rate.convert(BitcoinMain.get().oneCoin().multiply(10)).toPlainString()); + rate = new ExchangeRateBase(oneNbt.multiply(250), oneBtc); + assertEquals("0.004", rate.convert(oneNbt).toPlainString()); + assertEquals("2500", rate.convert(oneBtc.multiply(10)).toPlainString()); + } + + @Test + public void cryptoToCryptoRateParseConstructor() throws Exception { + // 1BTC = 100LTC + ExchangeRateBase rate = new ExchangeRateBase(BTC, LTC, "100"); + + assertEquals(LTC, rate.convert(oneBtc).type); + assertEquals(BTC, rate.convert(LTC.oneCoin()).type); + assertEquals("1", rate.convert(oneBtc.divide(100)).toPlainString()); + assertEquals("0.01", rate.convert(LTC.oneCoin()).toPlainString()); + } + + @Test + public void scalingAndRounding() throws Exception { + // 1BTC = 100.1234567890NBT + // This rate causes the BTC & NBT to overflow so it sets the correct scale and rounding + ExchangeRateBase rate = new ExchangeRateBase(BTC, NBT, "100.1234567890"); + + // Check the rate's internal state + assertEquals("100000", rate.value1.toPlainString()); + assertEquals("10012345.6789", rate.value2.toPlainString()); + + // Make some conversions + assertEquals("0.00998767", rate.convert(oneNbt).toPlainString()); + assertEquals("0.000001", rate.convert(Value.parse(NBT, "0.0001")).toPlainString()); + assertEquals("0.0001", rate.convert(Value.parse(BTC, "0.00000099")).toPlainString()); + assertEquals("0.0099", rate.convert(Value.parse(BTC, "0.000099")).toPlainString()); + assertEquals("10012345.6789", rate.convert(oneBtc.multiply(100000)).toPlainString()); + assertEquals("1001.2346", rate.convert(oneBtc.multiply(10)).toPlainString()); + assertEquals("998766.95438852", rate.convert(oneNbt.multiply(100000000)).toPlainString()); + + // Check too precise rates + rate = new ExchangeRateBase(BTC, NBT, "100.12345678901999"); + assertEquals("100000", rate.value1.toPlainString()); + assertEquals("10012345.6789", rate.value2.toPlainString()); } @Test public void bigRate() throws Exception { - ExchangeRate rate = new ExchangeRate(Value.parseValue(BitcoinMain.get(), "0.0001"), FiatValue.parse("BYR", "5320387.3")); - assertEquals("53203873000", rate.convert(BitcoinMain.get().oneCoin()).toPlainString()); + ExchangeRateBase rate = new ExchangeRateBase(Value.parse(BTC, "0.0001"), FiatValue.parse("BYR", "5320387.3")); + assertEquals("53203873000", rate.convert(oneBtc).toPlainString()); assertEquals("0", rate.convert(FiatValue.parse("BYR", "1")).toPlainString()); // Tiny value! } @Test public void smallRate() throws Exception { - ExchangeRate rate = new ExchangeRate(Value.parseValue(BitcoinMain.get(), "1000"), FiatValue.parse("XXX", "0.0001")); - assertEquals("0", rate.convert(BitcoinMain.get().oneCoin()).toPlainString()); // Tiny value! - assertEquals("10000000", rate.convert(FiatValue.parse("XXX", "1")).toPlainString()); + ExchangeRateBase rate = new ExchangeRateBase(Value.parse(BTC, "10000"), FiatValue.parse("XXX", "0.00001")); + assertEquals("0", rate.convert(oneBtc).toPlainString()); // Tiny value! + assertEquals("1000000000", rate.convert(FiatValue.parse("XXX", "1")).toPlainString()); + } + + @Test + public void zeroValues() { + ExchangeRateBase rate = new ExchangeRateBase(BTC.value("1"), FiatValue.parse("XXX", "100")); + assertEquals(BTC.value("0"), rate.convert(FiatValue.parse("XXX", "0"))); + assertEquals(FiatValue.parse("XXX", "0"), rate.convert(BTC.value("0"))); } @Test(expected = IllegalArgumentException.class) public void currencyCodeMismatch() throws Exception { - ExchangeRate rate = new ExchangeRate(BitcoinMain.get().oneCoin(), FiatValue.parse("EUR", "500")); + ExchangeRateBase rate = new ExchangeRateBase(oneBtc, FiatValue.parse("EUR", "500")); rate.convert(FiatValue.parse("USD", "1")); } - @Test(expected = ArithmeticException.class) - public void fiatToCoinTooLarge() throws Exception { - ExchangeRate rate = new ExchangeRate(BitcoinMain.get().oneCoin(), FiatValue.parse("XXX", "1")); - rate.convert(FiatValue.parse("XXX", String.valueOf(NetworkParameters.MAX_COINS + 1))); - } - - @Test(expected = ArithmeticException.class) - public void fiatToCoinTooSmall() throws Exception { - ExchangeRate rate = new ExchangeRate(BitcoinMain.get().oneCoin(), FiatValue.parse("XXX", "1")); - rate.convert(FiatValue.parse("XXX", String.valueOf(-1 * (NetworkParameters.MAX_COINS + 1)))); - } - @Test(expected = ArithmeticException.class) public void coinToFiatTooLarge() throws Exception { - ExchangeRate rate = new ExchangeRate(BitcoinMain.get().oneCoin(), FiatValue.parse("XXX", "1000000000")); - rate.convert(Value.parseValue(BitcoinMain.get(), "1000000")); + ExchangeRateBase rate = new ExchangeRateBase(oneBtc, FiatValue.parse("XXX", "1000000000")); + rate.convert(Value.parse(BTC, "1000000")); } @Test(expected = ArithmeticException.class) public void coinToFiatTooSmall() throws Exception { - ExchangeRate rate = new ExchangeRate(BitcoinMain.get().oneCoin(), FiatValue.parse("XXX", "1000000000")); - rate.convert(Value.parseValue(BitcoinMain.get(), "-1000000")); + ExchangeRateBase rate = new ExchangeRateBase(oneBtc, FiatValue.parse("XXX", "1000000000")); + rate.convert(Value.parse(BTC, "-1000000")); } } diff --git a/core/src/test/java/com/coinomi/core/wallet/WalletPocketHDTest.java b/core/src/test/java/com/coinomi/core/wallet/WalletPocketHDTest.java index fde9342..c5f7ede 100644 --- a/core/src/test/java/com/coinomi/core/wallet/WalletPocketHDTest.java +++ b/core/src/test/java/com/coinomi/core/wallet/WalletPocketHDTest.java @@ -509,6 +509,11 @@ public void broadcastTx(Transaction tx, TransactionEventListener listener) { } } + @Override + public boolean broadcastTxSync(Transaction tx) { + return false; + } + @Override public void ping() {} } diff --git a/wallet/build.gradle b/wallet/build.gradle index 93b532f..cbb8383 100644 --- a/wallet/build.gradle +++ b/wallet/build.gradle @@ -15,7 +15,7 @@ android { applicationId "com.coinomi.wallet.dev" minSdkVersion 10 targetSdkVersion 21 - versionCode 33 + versionCode 35 versionName "v1.5.13" } buildTypes { diff --git a/wallet/src/main/AndroidManifest.xml b/wallet/src/main/AndroidManifest.xml index 2c02a1c..5665d5f 100644 --- a/wallet/src/main/AndroidManifest.xml +++ b/wallet/src/main/AndroidManifest.xml @@ -1,7 +1,7 @@ + android:installLocation="internalOnly" > diff --git a/wallet/src/main/java/com/coinomi/wallet/Configuration.java b/wallet/src/main/java/com/coinomi/wallet/Configuration.java index b46c736..9ecaa2b 100644 --- a/wallet/src/main/java/com/coinomi/wallet/Configuration.java +++ b/wallet/src/main/java/com/coinomi/wallet/Configuration.java @@ -174,7 +174,7 @@ public void setLastExchangeDirection(final boolean exchangeDirection) { prefs.edit().putBoolean(PREFS_KEY_LAST_EXCHANGE_DIRECTION, exchangeDirection).apply(); } - public boolean isManualReceivingAddressManagement() { + public boolean isManualAddressManagement() { return prefs.getBoolean(PREFS_KEY_MANUAL_RECEIVING_ADDRESSES, false); } diff --git a/wallet/src/main/java/com/coinomi/wallet/Constants.java b/wallet/src/main/java/com/coinomi/wallet/Constants.java index 5625bdf..402cdc0 100644 --- a/wallet/src/main/java/com/coinomi/wallet/Constants.java +++ b/wallet/src/main/java/com/coinomi/wallet/Constants.java @@ -47,10 +47,13 @@ public class Constants { public static final String ARG_SEED = "seed"; public static final String ARG_SEED_PROTECT = "seed_protect"; public static final String ARG_PASSWORD = "password"; + public static final String ARG_EMPTY_WALLET = "empty_wallet"; public static final String ARG_SEND_TO_ADDRESS = "send_to_address"; - public static final String ARG_SEND_AMOUNT = "send_amount"; + public static final String ARG_SEND_TO_COIN_TYPE = "send_to_coin_type"; + public static final String ARG_SEND_TO_ACCOUNT_ID = "send_to_account_id"; + public static final String ARG_SEND_VALUE = "send_value"; public static final String ARG_COIN_ID = "coin_id"; - public static final String ARG_ACCOUNT_ID = "coin_id"; + public static final String ARG_ACCOUNT_ID = "account_id"; public static final String ARG_MULTIPLE_COIN_IDS = "multiple_coin_ids"; public static final String ARG_MULTIPLE_CHOICE = "multiple_choice"; public static final String ARG_TRANSACTION_ID = "transaction_id"; @@ -65,6 +68,8 @@ public class Constants { public static final long STOP_SERVICE_AFTER_IDLE_SECS = 30 * 60; // 30 mins + public static final String HTTP_CACHE_DIR = "http_cache"; + public static final int HTTP_CACHE_SIZE = 256 * 1024; // 256 KiB public static final int HTTP_TIMEOUT_MS = 15 * (int) DateUtils.SECOND_IN_MILLIS; public static final long RATE_UPDATE_FREQ_MS = 1 * DateUtils.MINUTE_IN_MILLIS; diff --git a/wallet/src/main/java/com/coinomi/wallet/ExchangeRatesProvider.java b/wallet/src/main/java/com/coinomi/wallet/ExchangeRatesProvider.java index 5d3026b..455072c 100644 --- a/wallet/src/main/java/com/coinomi/wallet/ExchangeRatesProvider.java +++ b/wallet/src/main/java/com/coinomi/wallet/ExchangeRatesProvider.java @@ -31,14 +31,12 @@ import com.coinomi.core.coins.CoinID; import com.coinomi.core.coins.CoinType; -import com.coinomi.core.coins.FiatType; import com.coinomi.core.coins.FiatValue; import com.coinomi.core.coins.Value; +import com.coinomi.core.util.ExchangeRateBase; import com.coinomi.wallet.util.Io; import com.google.common.base.Charsets; -import org.bitcoinj.core.Coin; -import org.bitcoinj.utils.Fiat; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -48,7 +46,6 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; -import java.math.BigDecimal; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; @@ -69,11 +66,11 @@ public class ExchangeRatesProvider extends ContentProvider { public static class ExchangeRate { - @Nonnull public final com.coinomi.core.util.ExchangeRate rate; + @Nonnull public final ExchangeRateBase rate; public final String currencyCodeId; @Nullable public final String source; - public ExchangeRate(@Nonnull final com.coinomi.core.util.ExchangeRate rate, + public ExchangeRate(@Nonnull final ExchangeRateBase rate, final String currencyCodeId, @Nullable final String source) { this.rate = rate; this.currencyCodeId = currencyCodeId; @@ -238,7 +235,7 @@ public Cursor query(final Uri uri, final String[] projection, final String selec } private void addRow(MatrixCursor cursor, ExchangeRate exchangeRate) { - final com.coinomi.core.util.ExchangeRate rate = exchangeRate.rate; + final ExchangeRateBase rate = exchangeRate.rate; final String codeId = exchangeRate.currencyCodeId; cursor.newRow().add(codeId.hashCode()).add(codeId) .add(rate.value1.value).add(rate.value1.type.getSymbol()) @@ -258,7 +255,7 @@ public static ExchangeRate getExchangeRate(@Nonnull final Cursor cursor) { final Value rateFiat = FiatValue.valueOf(fiatCode, cursor.getLong(cursor.getColumnIndexOrThrow(ExchangeRatesProvider.KEY_RATE_FIAT))); final String source = cursor.getString(cursor.getColumnIndexOrThrow(ExchangeRatesProvider.KEY_SOURCE)); - com.coinomi.core.util.ExchangeRate rate = new com.coinomi.core.util.ExchangeRate(rateCoin, rateFiat); + ExchangeRateBase rate = new ExchangeRateBase(rateCoin, rateFiat); return new ExchangeRate(rate, codeId, source); } @@ -358,7 +355,7 @@ private Map parseExchangeRates(JSONObject json, String fro final Value rateCoin = type.oneCoin(); final Value rateLocal = FiatValue.parse(localSymbol, rateStr); - com.coinomi.core.util.ExchangeRate rate = new com.coinomi.core.util.ExchangeRate(rateCoin, rateLocal); + ExchangeRateBase rate = new ExchangeRateBase(rateCoin, rateLocal); rates.put(toSymbol, new ExchangeRate(rate, toSymbol, COINOMI_SOURCE)); } catch (final Exception x) { log.debug("ignoring {}/{}: {}", toSymbol, fromSymbol, x.getMessage()); diff --git a/wallet/src/main/java/com/coinomi/wallet/WalletApplication.java b/wallet/src/main/java/com/coinomi/wallet/WalletApplication.java index 18c4c86..4e60a8b 100644 --- a/wallet/src/main/java/com/coinomi/wallet/WalletApplication.java +++ b/wallet/src/main/java/com/coinomi/wallet/WalletApplication.java @@ -7,22 +7,31 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.graphics.Typeface; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; import android.os.StrictMode; import android.os.SystemClock; import android.preference.PreferenceManager; import android.widget.Toast; import com.coinomi.core.coins.CoinType; +import com.coinomi.core.exchange.shapeshift.ShapeShift; +import com.coinomi.core.network.ConnectivityHelper; import com.coinomi.core.util.HardwareSoftwareCompliance; import com.coinomi.core.wallet.Wallet; import com.coinomi.core.wallet.WalletAccount; +import com.coinomi.core.wallet.WalletPocketHD; import com.coinomi.core.wallet.WalletProtobufSerializer; import com.coinomi.wallet.service.CoinService; import com.coinomi.wallet.service.CoinServiceImpl; import com.coinomi.wallet.util.Fonts; import com.coinomi.wallet.util.LinuxSecureRandom; import com.google.common.collect.ImmutableList; +import com.squareup.okhttp.Cache; +import com.squareup.okhttp.ConnectionSpec; +import com.squareup.okhttp.OkHttpClient; +import org.acra.ACRA; import org.acra.annotation.ReportsCrashes; import org.acra.sender.HttpSender; import org.bitcoinj.crypto.MnemonicCode; @@ -34,8 +43,10 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; +import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; @@ -44,17 +55,21 @@ * @author Andreas Schildbach */ @ReportsCrashes( + // Also uncomment ACRA.init(this) in onCreate httpMethod = HttpSender.Method.PUT, reportType = HttpSender.Type.JSON, formKey = "" ) public class WalletApplication extends Application { + private static final Logger log = LoggerFactory.getLogger(WalletApplication.class); + private static HashMap typefaces; private static String httpUserAgent; private Configuration config; private ActivityManager activityManager; private Intent coinServiceIntent; + private Intent coinServiceConnectIntent; private Intent coinServiceCancelCoinsReceivedIntent; private Intent coinServiceResetWalletIntent; @@ -65,15 +80,21 @@ public class WalletApplication extends Application { private long lastStop; - private static final Logger log = LoggerFactory.getLogger(WalletApplication.class); + private ConnectivityManager connManager; + private OkHttpClient client; + private ShapeShift shapeShift; @Override public void onCreate() { +// ACRA.init(this); + new LinuxSecureRandom(); // init proper random number generator initLogging(); - StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectAll().permitDiskReads().permitDiskWrites().penaltyLog().build()); + // TODO review this + StrictMode.setThreadPolicy( + new StrictMode.ThreadPolicy.Builder().detectAll().permitDiskReads().permitDiskWrites().penaltyLog().build()); super.onCreate(); @@ -81,12 +102,12 @@ public void onCreate() { httpUserAgent = "Coinomi/" + packageInfo.versionName + " (Android)"; -// ACRA.init(this); - config = new Configuration(PreferenceManager.getDefaultSharedPreferences(this)); activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); coinServiceIntent = new Intent(this, CoinServiceImpl.class); + coinServiceConnectIntent = new Intent(CoinService.ACTION_CONNECT_COIN, + null, this, CoinServiceImpl.class); coinServiceCancelCoinsReceivedIntent = new Intent(CoinService.ACTION_CANCEL_COINS_RECEIVED, null, this, CoinServiceImpl.class); coinServiceResetWalletIntent = new Intent(CoinService.ACTION_RESET_WALLET, @@ -105,8 +126,9 @@ public void onCreate() { performComplianceTests(); - walletFile = getFileStreamPath(Constants.WALLET_FILENAME_PROTOBUF); + connManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + walletFile = getFileStreamPath(Constants.WALLET_FILENAME_PROTOBUF); loadWallet(); afterLoadWallet(); @@ -114,6 +136,31 @@ public void onCreate() { Fonts.initFonts(this.getAssets()); } + public boolean isConnected() { + NetworkInfo activeInfo = connManager.getActiveNetworkInfo(); + return activeInfo != null && activeInfo.isConnected(); + } + + public OkHttpClient getHttpClient() { + if (client == null) { + client = new OkHttpClient(); + client.setConnectionSpecs(Collections.singletonList(ConnectionSpec.MODERN_TLS)); + client.setConnectTimeout(Constants.HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS); + // Setup cache + File cacheDir = new File(getCacheDir(), Constants.HTTP_CACHE_DIR); + Cache cache = new Cache(cacheDir, Constants.HTTP_CACHE_SIZE); + client.setCache(cache); + } + return client; + } + + public ShapeShift getShapeShift() { + if (shapeShift == null) { + shapeShift = new ShapeShift(getHttpClient()); + } + return shapeShift; + } + /** * Some devices have software bugs that causes the EC crypto to malfunction. */ @@ -342,4 +389,11 @@ public void touchLastStop() { public long getLastStop() { return lastStop; } + + public void maybeConnectAccount(WalletAccount account) { + if (!account.isConnected()) { + coinServiceConnectIntent.putExtra(Constants.ARG_ACCOUNT_ID, account.getId()); + startService(coinServiceConnectIntent); + } + } } \ No newline at end of file diff --git a/wallet/src/main/java/com/coinomi/wallet/tasks/AddCoinTask.java b/wallet/src/main/java/com/coinomi/wallet/tasks/AddCoinTask.java new file mode 100644 index 0000000..84de3aa --- /dev/null +++ b/wallet/src/main/java/com/coinomi/wallet/tasks/AddCoinTask.java @@ -0,0 +1,56 @@ +package com.coinomi.wallet.tasks; + +import android.os.AsyncTask; + +import com.coinomi.core.coins.CoinType; +import com.coinomi.core.wallet.Wallet; +import com.coinomi.core.wallet.WalletAccount; +import com.coinomi.wallet.R; +import com.coinomi.wallet.ui.Dialogs; + +import org.spongycastle.crypto.params.KeyParameter; + +import javax.annotation.Nullable; + +/** + * @author John L. Jegutanis + */ +public abstract class AddCoinTask extends AsyncTask { + protected final CoinType type; + private final Wallet wallet; + @Nullable private final String password; + private WalletAccount newAccount; + private Exception exception; + + public AddCoinTask(CoinType type, Wallet wallet, @Nullable String password) { + this.type = type; + this.wallet = wallet; + this.password = password; + } + + @Override abstract protected void onPreExecute(); + + @Override + protected Void doInBackground(Void... params) { + KeyParameter key = null; + exception = null; + try { + if (wallet.isEncrypted() && wallet.getKeyCrypter() != null) { + key = wallet.getKeyCrypter().deriveKey(password); + } + newAccount = wallet.createAccount(type, true, key); + wallet.saveNow(); + } catch (Exception e) { + exception = e; + } + + return null; + } + + @Override + final protected void onPostExecute(Void aVoid) { + onPostExecute(exception, newAccount); + } + + abstract protected void onPostExecute(Exception exception, WalletAccount newAccount); +} \ No newline at end of file diff --git a/wallet/src/main/java/com/coinomi/wallet/ui/AddCoinsActivity.java b/wallet/src/main/java/com/coinomi/wallet/ui/AddCoinsActivity.java index 09c03cc..db0bc6f 100644 --- a/wallet/src/main/java/com/coinomi/wallet/ui/AddCoinsActivity.java +++ b/wallet/src/main/java/com/coinomi/wallet/ui/AddCoinsActivity.java @@ -4,10 +4,7 @@ import android.app.Dialog; import android.content.DialogInterface; import android.content.Intent; -import android.os.AsyncTask; import android.support.v4.app.DialogFragment; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentTransaction; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -20,8 +17,7 @@ import com.coinomi.core.wallet.WalletAccount; import com.coinomi.wallet.Constants; import com.coinomi.wallet.R; - -import org.spongycastle.crypto.params.KeyParameter; +import com.coinomi.wallet.tasks.AddCoinTask; import java.util.ArrayList; @@ -32,7 +28,7 @@ public class AddCoinsActivity extends BaseWalletActivity implements SelectCoinsFragment.Listener { @CheckForNull private Wallet wallet; - private WalletFromSeedTask addCoinTask; + private MyAddCoinTask addCoinTask; private CoinType selectedCoin; @Override @@ -50,18 +46,6 @@ protected void onCreate(Bundle savedInstanceState) { wallet = getWalletApplication().getWallet(); } - private void replaceFragment(Fragment fragment) { - FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); - - // Replace whatever is in the fragment_container view with this fragment, - // and add the transaction to the back stack so the user can navigate back - transaction.replace(R.id.container, fragment); - transaction.addToBackStack(null); - - // Commit the transaction - transaction.commit(); - } - @Override public void onCoinSelection(Bundle args) { ArrayList ids = args.getStringArrayList(Constants.ARG_MULTIPLE_COIN_IDS); @@ -96,48 +80,27 @@ public void onClick(DialogInterface dialog, int which) { private void addCoin(@Nullable String password) { if (selectedCoin != null && addCoinTask == null) { - addCoinTask = new WalletFromSeedTask(selectedCoin, password); + addCoinTask = new MyAddCoinTask(selectedCoin, wallet, password); addCoinTask.execute(); } } - private class WalletFromSeedTask extends AsyncTask { - private final CoinType type; - @Nullable private final String password; + private class MyAddCoinTask extends AddCoinTask { private Dialogs.ProgressDialogFragment verifyDialog; - private WalletAccount newAccount; - private WalletFromSeedTask(CoinType type, @Nullable String password) { - this.type = type; - this.password = password; + public MyAddCoinTask(CoinType type, Wallet wallet, @Nullable String password) { + super(type, wallet, password); } @Override protected void onPreExecute() { - super.onPreExecute(); verifyDialog = Dialogs.ProgressDialogFragment.newInstance( getResources().getString(R.string.adding_coin_working, type.getName())); verifyDialog.show(getSupportFragmentManager(), null); } @Override - protected Exception doInBackground(Void... params) { - KeyParameter key = null; - Exception exception = null; - try { - if (wallet.isEncrypted() && wallet.getKeyCrypter() != null) { - key = wallet.getKeyCrypter().deriveKey(password); - } - newAccount = wallet.createAccount(type, true, key); - wallet.saveNow(); - } catch (RuntimeException e) { - exception = e; - } - - return exception; - } - - protected void onPostExecute(Exception e) { + protected void onPostExecute(Exception e, WalletAccount newAccount) { verifyDialog.dismiss(); result(newAccount, e == null ? null : e.getMessage()); } diff --git a/wallet/src/main/java/com/coinomi/wallet/ui/AddressRequestFragment.java b/wallet/src/main/java/com/coinomi/wallet/ui/AddressRequestFragment.java index 7235789..69846ac 100644 --- a/wallet/src/main/java/com/coinomi/wallet/ui/AddressRequestFragment.java +++ b/wallet/src/main/java/com/coinomi/wallet/ui/AddressRequestFragment.java @@ -32,6 +32,7 @@ import com.coinomi.core.coins.CoinType; import com.coinomi.core.coins.FiatType; import com.coinomi.core.uri.CoinURI; +import com.coinomi.core.util.ExchangeRate; import com.coinomi.core.util.GenericUtils; import com.coinomi.core.wallet.WalletPocketHD; import com.coinomi.core.wallet.exceptions.Bip44KeyLookAheadExceededException; @@ -49,7 +50,6 @@ import com.coinomi.wallet.util.WeakHandler; import org.bitcoinj.core.Address; -import org.bitcoinj.core.AddressFormatException; import org.bitcoinj.core.Coin; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -104,7 +104,7 @@ protected void weakHandleMessage(AddressRequestFragment ref, Message msg) { ref.updateView(); break; case UPDATE_EXCHANGE_RATE: - ref.amountCalculatorLink.setExchangeRate((com.coinomi.core.util.ExchangeRate) msg.obj); + ref.amountCalculatorLink.setExchangeRate((ExchangeRate) msg.obj); } } } @@ -130,7 +130,7 @@ public static AddressRequestFragment newInstance(String accountId, @Nullable Add Bundle args = new Bundle(); args.putString(Constants.ARG_ACCOUNT_ID, accountId); if (showAddress != null) { - args.putString(Constants.ARG_ADDRESS, showAddress.toString()); + args.putSerializable(Constants.ARG_ADDRESS, showAddress); } return newInstance(args); } @@ -146,11 +146,7 @@ public void onCreate(Bundle savedInstanceState) { if (args != null) { accountId = args.getString(Constants.ARG_ACCOUNT_ID); if (args.containsKey(Constants.ARG_ADDRESS)) { - try { - showAddress = new Address(type, args.getString(Constants.ARG_ADDRESS)); - } catch (AddressFormatException e) { - throw new RuntimeException(e); - } + showAddress = (Address) args.getSerializable(Constants.ARG_ADDRESS); } } // TODO @@ -195,7 +191,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, AmountEditView sendLocalAmountView = (AmountEditView) view.findViewById(R.id.send_local_amount); sendLocalAmountView.setFormat(FiatType.FRIENDLY_FORMAT); - amountCalculatorLink = new CurrencyCalculatorLink(type, sendCoinAmountView, sendLocalAmountView); + amountCalculatorLink = new CurrencyCalculatorLink(sendCoinAmountView, sendLocalAmountView); previousAddressesLink = view.findViewById(R.id.view_previous_addresses); previousAddressesLink.setOnClickListener(new View.OnClickListener() { @@ -319,14 +315,8 @@ public void onClick(DialogInterface dialog, int which) { @Override public void onClick(DialogInterface dialog, int which) { try { - Address newAddress = null; - Address freshAddress = pocket.getFreshReceiveAddress(); - if (config.isManualReceivingAddressManagement()) { - newAddress = pocket.getLastUsedReceiveAddress(); - } - if (newAddress == null) { - newAddress = freshAddress; - } + Address newAddress = pocket.getFreshReceiveAddress( + config.isManualAddressManagement()); final String newLabel = viewLabel.getText().toString().trim(); if (!newLabel.isEmpty()) { @@ -362,13 +352,7 @@ private void updateView() { if (showAddress != null) { receiveAddress = showAddress; } else { - if (config.isManualReceivingAddressManagement()) { - receiveAddress = pocket.getLastUsedReceiveAddress(); - } - - if (receiveAddress == null) { - receiveAddress = pocket.getReceiveAddress(); - } + receiveAddress = pocket.getReceiveAddress(config.isManualAddressManagement()); } // Don't show previous addresses link if we are showing a specific address diff --git a/wallet/src/main/java/com/coinomi/wallet/ui/BalanceFragment.java b/wallet/src/main/java/com/coinomi/wallet/ui/BalanceFragment.java index 3245e8c..9be6964 100644 --- a/wallet/src/main/java/com/coinomi/wallet/ui/BalanceFragment.java +++ b/wallet/src/main/java/com/coinomi/wallet/ui/BalanceFragment.java @@ -28,8 +28,8 @@ import com.coinomi.core.coins.Value; import com.coinomi.core.util.GenericUtils; import com.coinomi.core.wallet.WalletAccount; +import com.coinomi.core.wallet.WalletAccountEventListener; import com.coinomi.core.wallet.WalletPocketConnectivity; -import com.coinomi.core.wallet.WalletPocketEventListener; import com.coinomi.core.wallet.WalletPocketHD; import com.coinomi.wallet.AddressBookProvider; import com.coinomi.wallet.Configuration; @@ -61,7 +61,7 @@ * Use the {@link BalanceFragment#newInstance} factory method to * create an instance of this fragment. */ -public class BalanceFragment extends Fragment implements WalletPocketEventListener, LoaderCallbacks> { +public class BalanceFragment extends Fragment implements WalletAccountEventListener, LoaderCallbacks> { private static final Logger log = LoggerFactory.getLogger(BalanceFragment.class); private static final int NEW_BALANCE = 0; @@ -85,10 +85,10 @@ private static class MyHandler extends WeakHandler { protected void weakHandleMessage(BalanceFragment ref, Message msg) { switch (msg.what) { case NEW_BALANCE: - ref.updateBalance((Coin) msg.obj); + ref.updateBalance((Value) msg.obj); break; case PENDING: - ref.setPending((Coin) msg.obj); + ref.setPending((Value) msg.obj); break; case CONNECTIVITY: ref.setConnectivityStatus((WalletPocketConnectivity) msg.obj); @@ -277,7 +277,7 @@ public void onDestroyView() { } @Override - public void onNewBalance(Coin newBalance, Coin pendingAmount) { + public void onNewBalance(Value newBalance, Value pendingAmount) { handler.sendMessage(handler.obtainMessage(NEW_BALANCE, newBalance)); handler.sendMessage(handler.obtainMessage(PENDING, pendingAmount)); } @@ -324,18 +324,18 @@ public void onConnectivityStatus(WalletPocketConnectivity pocketConnectivity) { handler.sendMessage(handler.obtainMessage(CONNECTIVITY, pocketConnectivity)); } - private void updateBalance(Coin newBalance) { - currentBalance = newBalance; + private void updateBalance(Value newBalance) { + currentBalance = newBalance.toCoin(); updateView(); } - private void setPending(Coin pendingAmount) { + private void setPending(Value pendingAmount) { if (pendingAmount.isZero()) { mainAmount.setAmountPending(null); } else { - String pendingAmountStr = GenericUtils.formatCoinValue(type, pendingAmount, - AMOUNT_FULL_PRECISION, AMOUNT_SHIFT); + String pendingAmountStr = GenericUtils.formatCoinValue(pendingAmount.type, + pendingAmount.toCoin(),AMOUNT_FULL_PRECISION, AMOUNT_SHIFT); mainAmount.setAmountPending(pendingAmountStr); } } diff --git a/wallet/src/main/java/com/coinomi/wallet/ui/CurrencyCalculatorLink.java b/wallet/src/main/java/com/coinomi/wallet/ui/CurrencyCalculatorLink.java index d9f8b13..8b832ea 100644 --- a/wallet/src/main/java/com/coinomi/wallet/ui/CurrencyCalculatorLink.java +++ b/wallet/src/main/java/com/coinomi/wallet/ui/CurrencyCalculatorLink.java @@ -17,36 +17,33 @@ * along with this program. If not, see . */ -import javax.annotation.CheckForNull; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - import android.view.View; import com.coinomi.core.coins.CoinType; -import com.coinomi.core.coins.FiatValue; import com.coinomi.core.coins.Value; -import com.coinomi.core.coins.ValueType; import com.coinomi.core.util.ExchangeRate; import com.coinomi.wallet.ui.widget.AmountEditView; import com.coinomi.wallet.ui.widget.AmountEditView.Listener; import org.bitcoinj.core.Coin; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + /** * @author Andreas Schildbach * @author John L. Jegutanis */ public final class CurrencyCalculatorLink { - private final ValueType primaryType; private final AmountEditView coinAmountView; private final AmountEditView localAmountView; private Listener listener = null; private boolean enabled = true; private ExchangeRate exchangeRate = null; - private boolean exchangeDirection = true; + @Nullable private boolean exchangeDirection = true; private final AmountEditView.Listener coinAmountViewListener = new AmountEditView.Listener() { @Override @@ -86,10 +83,8 @@ public void focusChanged(final boolean hasFocus) { } }; - public CurrencyCalculatorLink(ValueType primaryType, - @Nonnull final AmountEditView coinAmountView, + public CurrencyCalculatorLink(@Nonnull final AmountEditView coinAmountView, @Nonnull final AmountEditView localAmountView) { - this.primaryType = primaryType; this.coinAmountView = coinAmountView; this.coinAmountView.setListener(coinAmountViewListener); @@ -109,19 +104,19 @@ public void setEnabled(final boolean enabled) { update(); } - public void setExchangeRate(@Nonnull final ExchangeRate exchangeRate) { + public void setExchangeRate(@Nullable final ExchangeRate exchangeRate) { this.exchangeRate = exchangeRate; update(); } - @CheckForNull + @Nullable public Coin getPrimaryAmountCoin() { Value value = getPrimaryAmount(); - return value != null ? value.toCoin() : null; + return value != null ? value.toCoin() : null; } - @CheckForNull + @Nullable public Value getPrimaryAmount() { if (exchangeDirection) { return coinAmountView.getAmount(); @@ -137,6 +132,33 @@ public Value getPrimaryAmount() { } } + @Nullable + public Value getSecondaryAmount() { + if (exchangeRate == null) return null; + + if (exchangeDirection) { + final Value coinAmount = coinAmountView.getAmount(); + try { + return coinAmount != null ? exchangeRate.convert(coinAmount) : null; + } catch (ArithmeticException x) { + return null; + } + } else { + return localAmountView.getAmount(); + } + } + + /** + * Get the amount that the user entered, could be either type + */ + public Value getRequestedAmount() { + if (exchangeDirection) { + return coinAmountView.getAmount(); + } else { + return localAmountView.getAmount(); + } + } + public boolean hasAmount() { return getPrimaryAmount() != null; } @@ -146,9 +168,11 @@ private void update() { if (exchangeRate != null) { localAmountView.setEnabled(enabled); - localAmountView.setType(exchangeRate.getOtherType(primaryType)); + localAmountView.setType(exchangeRate.getDestinationType()); localAmountView.setVisibility(View.VISIBLE); + coinAmountView.setType(exchangeRate.getSourceType()); + if (exchangeDirection) { final Value coinAmount = coinAmountView.getAmount(); if (coinAmount != null) { @@ -197,6 +221,13 @@ public void requestFocus() { activeTextView().requestFocus(); } + public void setExchangeRateHints(@Nullable final Value primaryAmount) { + if (exchangeRate != null) { + coinAmountView.setHint(primaryAmount); + localAmountView.setHint(exchangeRate.convert(primaryAmount)); + } + } + public void setPrimaryAmount(@Nullable final Value amount) { final Listener listener = this.listener; this.listener = null; diff --git a/wallet/src/main/java/com/coinomi/wallet/ui/MakeTransactionFragment.java b/wallet/src/main/java/com/coinomi/wallet/ui/MakeTransactionFragment.java new file mode 100644 index 0000000..801c82c --- /dev/null +++ b/wallet/src/main/java/com/coinomi/wallet/ui/MakeTransactionFragment.java @@ -0,0 +1,621 @@ +package com.coinomi.wallet.ui; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Intent; +import android.content.res.Resources; +import android.database.Cursor; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.CountDownTimer; +import android.os.Handler; +import android.os.Message; +import android.support.v4.app.Fragment; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.Loader; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.TextView; + +import com.coinomi.core.coins.CoinType; +import com.coinomi.core.coins.Value; +import com.coinomi.core.exchange.shapeshift.ShapeShift; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftAmountTx; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftMarketInfo; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftNormalTx; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftTime; +import com.coinomi.core.util.ExchangeRate; +import com.coinomi.core.util.GenericUtils; +import com.coinomi.core.wallet.SendRequest; +import com.coinomi.core.wallet.Wallet; +import com.coinomi.core.wallet.WalletAccount; +import com.coinomi.core.wallet.WalletPocketHD; +import com.coinomi.core.wallet.exceptions.NoSuchPocketException; +import com.coinomi.wallet.AddressBookProvider; +import com.coinomi.wallet.Configuration; +import com.coinomi.wallet.Constants; +import com.coinomi.wallet.ExchangeRatesProvider; +import com.coinomi.wallet.R; +import com.coinomi.wallet.WalletApplication; +import com.coinomi.wallet.service.CoinService; +import com.coinomi.wallet.service.CoinServiceImpl; +import com.coinomi.wallet.ui.widget.SendOutput; +import com.coinomi.wallet.ui.widget.TransactionAmountVisualizer; +import com.coinomi.wallet.util.Keyboard; +import com.coinomi.wallet.util.WeakHandler; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.crypto.KeyCrypter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.HashMap; + +import javax.annotation.Nullable; + +import static com.coinomi.core.Preconditions.checkNotNull; +import static com.coinomi.wallet.Constants.ARG_ACCOUNT_ID; +import static com.coinomi.wallet.Constants.ARG_EMPTY_WALLET; +import static com.coinomi.wallet.Constants.ARG_SEND_TO_ACCOUNT_ID; +import static com.coinomi.wallet.Constants.ARG_SEND_TO_ADDRESS; +import static com.coinomi.wallet.Constants.ARG_SEND_VALUE; + +/** + * This fragment displays a busy message and makes the transaction in the background + * + */ +public class MakeTransactionFragment extends Fragment { + private static final Logger log = LoggerFactory.getLogger(MakeTransactionFragment.class); + + private static final int START_TRADE_TIMEOUT = 0; + private static final int UPDATE_TRADE_TIMEOUT = 1; + private static final int TRADE_EXPIRED = 2; + private static final int STOP_TRADE_TIMEOUT = 3; + + private static final int SAFE_TIMEOUT_MARGIN = 30; + + // Loader IDs + private static final int ID_RATE_LOADER = 0; + + private static final String DEPOSIT_ADDRESS = "deposit_address"; + private static final String DEPOSIT_AMOUNT = "deposit_amount"; + private static final String WITHDRAW_ADDRESS = "withdraw_address"; + private static final String WITHDRAW_AMOUNT = "withdraw_amount"; + + private Handler handler = new MyHandler(this); + @Nullable private String password; + private Listener mListener; + private SignAndBroadcastTask signAndBroadcastTask; + private CreateTransactionTask createTransactionTask; + private WalletApplication application; + private Configuration config; + private TextView transactionInfo; + private TransactionAmountVisualizer txVisualizer; + private SendOutput tradeWithdrawSendOutput; + private Address sendToAddress; + @Nullable private Value sendAmount; + private boolean emptyWallet; + private CoinType sourceType; + private SendRequest request; + private LoaderManager loaderManager; + private WalletPocketHD sourceAccount; + @Nullable private Address tradeDepositAddress; + @Nullable private Value tradeDepositAmount; + @Nullable private Address tradeWithdrawAddress; + @Nullable private Value tradeWithdrawAmount; + private boolean transactionBroadcast = false; + @Nullable private Exception error; + + private CountDownTimer countDownTimer; + + public static MakeTransactionFragment newInstance(Bundle args) { + MakeTransactionFragment fragment = new MakeTransactionFragment(); + fragment.setArguments(args); + return fragment; + } + public MakeTransactionFragment() { + // Required empty public constructor + } + + @Override + public void onCreate(Bundle savedState) { + super.onCreate(savedState); + signAndBroadcastTask = null; + + Bundle args = getArguments(); + checkNotNull(args, "Must provide arguments"); + + try { + String fromAccountId = args.getString(ARG_ACCOUNT_ID); + sourceAccount = (WalletPocketHD) checkNotNull(application.getAccount(fromAccountId)); + application.maybeConnectAccount(sourceAccount); + sourceType = sourceAccount.getCoinType(); + emptyWallet = args.getBoolean(ARG_EMPTY_WALLET, false); + sendAmount = (Value) args.getSerializable(ARG_SEND_VALUE); + if (emptyWallet && sendAmount != null) { + throw new IllegalArgumentException( + "Cannot set 'empty wallet' and 'send amount' at the same time"); + } + if (args.containsKey(ARG_SEND_TO_ACCOUNT_ID)) { + String toAccountId = args.getString(ARG_SEND_TO_ACCOUNT_ID); + WalletPocketHD toAccount = (WalletPocketHD) checkNotNull(application.getAccount(toAccountId)); + sendToAddress = toAccount.getReceiveAddress(config.isManualAddressManagement()); + } else { + sendToAddress = (Address) checkNotNull(args.getSerializable(ARG_SEND_TO_ADDRESS)); + } + + if (savedState != null) { + tradeDepositAddress = (Address) savedState.getSerializable(DEPOSIT_ADDRESS); + tradeDepositAmount = (Value) savedState.getSerializable(DEPOSIT_AMOUNT); + tradeWithdrawAddress = (Address) savedState.getSerializable(WITHDRAW_ADDRESS); + tradeWithdrawAmount = (Value) savedState.getSerializable(WITHDRAW_AMOUNT); + } + + maybeStartCreateTransaction(); + } catch (Exception e) { + error = e; + if (mListener != null) { + mListener.onSignResult(e); + } + } + + loaderManager.initLoader(ID_RATE_LOADER, null, rateLoaderCallbacks); + } + + @Override + public void onDestroy() { + loaderManager.destroyLoader(ID_RATE_LOADER); + super.onDestroy(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_make_transaction, container, false); + + if (error != null) return view; + + transactionInfo = (TextView) view.findViewById(R.id.transaction_info); + transactionInfo.setVisibility(View.GONE); + + final EditText passwordView = (EditText) view.findViewById(R.id.password); + final TextView passwordLabelView = (TextView) view.findViewById(R.id.enter_password_label); + if (sourceAccount.isEncrypted()) { + passwordView.requestFocus(); + passwordView.setVisibility(View.VISIBLE); + passwordLabelView.setVisibility(View.VISIBLE); + } else { + passwordView.setVisibility(View.GONE); + passwordLabelView.setVisibility(View.GONE); + } + + txVisualizer = (TransactionAmountVisualizer) view.findViewById(R.id.transaction_amount_visualizer); + tradeWithdrawSendOutput = (SendOutput) view.findViewById(R.id.transaction_trade_withdraw); + tradeWithdrawSendOutput.setVisibility(View.GONE); + showTransaction(); + + view.findViewById(R.id.button_confirm).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (passwordView.isShown()) { + Keyboard.hideKeyboard(getActivity()); + password = passwordView.getText().toString(); + } + maybeStartSignAndBroadcast(); + } + }); + + TextView poweredByShapeShift = (TextView) view.findViewById(R.id.powered_by_shapeshift); + poweredByShapeShift.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new AlertDialog.Builder(getActivity()) + .setTitle(R.string.about_shapeshift_title) + .setMessage(R.string.about_shapeshift_message) + .setPositiveButton(R.string.button_ok, null) + .create().show(); + } + }); + poweredByShapeShift.setVisibility((isExchangeNeeded() ? View.VISIBLE : View.GONE)); + + return view; + } + + private void showTransaction() { + if (request != null && txVisualizer != null) { + txVisualizer.setTransaction(sourceAccount, request.tx); + if (tradeWithdrawAmount != null && tradeWithdrawAddress != null) { +// String address = tradeWithdrawAddress.toString(); +// CoinType type = (CoinType) tradeWithdrawAddress.getParameters(); +// String label = AddressBookProvider.resolveLabel(getActivity(), type, address); + tradeWithdrawSendOutput.setVisibility(View.VISIBLE); + tradeWithdrawSendOutput.setSending(false); +// tradeWithdrawSendOutput.setLabelAndAddress(label, address); + tradeWithdrawSendOutput.setAmount(GenericUtils.formatValue(tradeWithdrawAmount)); + tradeWithdrawSendOutput.setSymbol(tradeWithdrawAmount.type.getSymbol()); + txVisualizer.getOutputs().get(0).setSendLabel(getString(R.string.trade)); + txVisualizer.hideAddresses(); + } + } + } + + boolean isExchangeNeeded() { + return !sourceAccount.getCoinType().equals(sendToAddress.getParameters()); + } + + private void maybeStartCreateTransaction() { + if (createTransactionTask == null && error == null) { + createTransactionTask = new CreateTransactionTask(); + createTransactionTask.execute(); + } + } + + private SendRequest generateSendRequest(Address sendTo, + boolean emptyWallet, @Nullable Value amount) + throws InsufficientMoneyException { + + SendRequest sendRequest; + if (emptyWallet) { + sendRequest = SendRequest.emptyWallet(sendTo); + } else { + sendRequest = SendRequest.to(sendTo, checkNotNull(amount).toCoin()); + } + sendRequest.signInputs = false; + sourceAccount.completeTx(sendRequest); + + return sendRequest; + } + + private boolean isSendingFromSourceAccount() { + return isEmptyWallet() || (sendAmount != null && sourceType.equals(sendAmount.type)); + } + + private boolean isEmptyWallet() { + return emptyWallet && sendAmount == null; + } + + private void maybeStartSignAndBroadcast() { + if (signAndBroadcastTask == null && request != null && error == null) { + signAndBroadcastTask = new SignAndBroadcastTask(); + signAndBroadcastTask.execute(); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + if (isExchangeNeeded()) { + outState.putSerializable(DEPOSIT_ADDRESS, tradeDepositAddress); + outState.putSerializable(DEPOSIT_AMOUNT, tradeDepositAmount); + outState.putSerializable(WITHDRAW_ADDRESS, tradeWithdrawAddress); + outState.putSerializable(WITHDRAW_AMOUNT, tradeWithdrawAmount); + } + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + try { + mListener = (Listener) activity; + application = (WalletApplication) activity.getApplication(); + config = application.getConfiguration(); + loaderManager = getLoaderManager(); + } catch (ClassCastException e) { + throw new ClassCastException(activity.toString() + + " must implement " + MakeTransactionFragment.Listener.class); + } + } + + @Override + public void onDetach() { + super.onDetach(); + mListener = null; + onStopTradeCountDown(); + } + + + void onStartTradeCountDown(int secondsLeft) { + if (countDownTimer != null) return; + + countDownTimer = new CountDownTimer(secondsLeft * 1000, 1000) { + public void onTick(long millisUntilFinished) { + handler.sendMessage(handler.obtainMessage( + UPDATE_TRADE_TIMEOUT, (int) (millisUntilFinished / 1000))); + } + + public void onFinish() { + handler.sendEmptyMessage(TRADE_EXPIRED); + } + }; + + countDownTimer.start(); + } + + void onStopTradeCountDown() { + if (countDownTimer != null) { + countDownTimer.cancel(); + countDownTimer = null; + } + } + + private void onTradeExpired() { + if (transactionBroadcast) { // Transaction already sent, so the trade is not expired + return; + } + if (transactionInfo.getVisibility() != View.VISIBLE) { + transactionInfo.setVisibility(View.VISIBLE); + } + String errorString = getString(R.string.trade_expired); + transactionInfo.setText(errorString); + + if (mListener != null) { + error = new Exception(errorString); + mListener.onSignResult(error); + } + } + + + private void onUpdateTradeCountDown(int secondsRemaining) { + if (transactionInfo.getVisibility() != View.VISIBLE) { + transactionInfo.setVisibility(View.VISIBLE); + } + + int minutes = secondsRemaining / 60; + int seconds = secondsRemaining % 60; + + Resources res = getResources(); + String timeLeft; + + if (minutes > 0) { + timeLeft = res.getQuantityString(R.plurals.tx_confirm_timer_minute, + minutes, String.format("%d:%02d", minutes, seconds)); + } else { + timeLeft = res.getQuantityString(R.plurals.tx_confirm_timer_second, + seconds, seconds); + } + + String message = getString(R.string.tx_confirm_timer_message, timeLeft); + transactionInfo.setText(message); + } + + + + /** + * Makes a call to ShapeShift about the time left for the trade + * + * Note: do not call this from the main thread! + */ + @Nullable + private static ShapeShiftTime getTimeLeftSync(ShapeShift shapeShift, Address address) { + // Try 3 times + for (int tries = 1; tries <= 3; tries++) { + try { + log.info("Getting time left for: {}", address); + return shapeShift.getTime(address); + } catch (Exception e) { + log.info("Will retry: {}", e.getMessage()); + /* ignore and retry, with linear backoff */ + try { + Thread.sleep(1000 * tries); + } catch (InterruptedException ie) { /*ignored*/ } + } + } + return null; + } + + + public interface Listener { + void onSignResult(@Nullable Exception error); + + /** + * This method is called when a trade is started and no error occurred + */ + void onTradeDeposit(Address deposit); + } + + private final LoaderManager.LoaderCallbacks rateLoaderCallbacks = new LoaderManager.LoaderCallbacks() { + String coinSymbol; + + @Override + public Loader onCreateLoader(final int id, final Bundle args) { + String localSymbol = config.getExchangeCurrencyCode(); + coinSymbol = sourceType.getSymbol(); + return new ExchangeRateLoader(getActivity(), config, localSymbol); + } + + @Override + public void onLoadFinished(final Loader loader, final Cursor data) { + if (data != null && data.getCount() > 0) { + HashMap rates = new HashMap<>(data.getCount()); + data.moveToFirst(); + do { + ExchangeRatesProvider.ExchangeRate rate = ExchangeRatesProvider.getExchangeRate(data); + rates.put(rate.currencyCodeId, rate.rate); + } while (data.moveToNext()); + + if (txVisualizer != null && rates.containsKey(coinSymbol)) { + txVisualizer.setExchangeRate(rates.get(coinSymbol)); + } + + if (tradeWithdrawAmount != null && rates.containsKey(tradeWithdrawAmount.type.getSymbol())) { + ExchangeRate rate = rates.get(tradeWithdrawAmount.type.getSymbol()); + Value fiatAmount = rate.convert(tradeWithdrawAmount); + tradeWithdrawSendOutput.setAmountLocal(GenericUtils.formatFiatValue(fiatAmount)); + tradeWithdrawSendOutput.setSymbolLocal(fiatAmount.type.getSymbol()); + } + } + } + + @Override + public void onLoaderReset(final Loader loader) { + } + }; + + /** + * The fragment handler + */ + private static class MyHandler extends WeakHandler { + public MyHandler(MakeTransactionFragment referencingObject) { super(referencingObject); } + + @Override + protected void weakHandleMessage(MakeTransactionFragment ref, Message msg) { + switch (msg.what) { + case START_TRADE_TIMEOUT: + ref.onStartTradeCountDown((int) msg.obj); + break; + case UPDATE_TRADE_TIMEOUT: + ref.onUpdateTradeCountDown((int) msg.obj); + break; + case TRADE_EXPIRED: + ref.onTradeExpired(); + break; + case STOP_TRADE_TIMEOUT: + ref.onStopTradeCountDown(); + break; + } + } + } + + private class CreateTransactionTask extends AsyncTask { + private Dialogs.ProgressDialogFragment busyDialog; + + @Override + protected void onPreExecute() { + // Show dialog as we need to make network connections + if (isExchangeNeeded()) { + busyDialog = Dialogs.ProgressDialogFragment.newInstance( + getString(R.string.contacting_exchange)); + busyDialog.show(getFragmentManager(), null); + } + } + + @Override + protected Void doInBackground(Void... params) { + try { + if (isExchangeNeeded()) { + + ShapeShift shapeShift = application.getShapeShift(); + Address refundAddress = + sourceAccount.getRefundAddress(config.isManualAddressManagement()); + + // If emptying wallet or the amount is the same type as the source account + if (isSendingFromSourceAccount()) { + ShapeShiftMarketInfo marketInfo = shapeShift.getMarketInfo( + sourceType, (CoinType) sendToAddress.getParameters()); + + // If no values set, make the call + if (tradeDepositAddress == null || tradeDepositAmount == null || + tradeWithdrawAddress == null || tradeWithdrawAmount == null) { + ShapeShiftNormalTx normalTx = + shapeShift.exchange(sendToAddress, refundAddress); + // TODO, show a retry message + if (normalTx.isError) throw new Exception(normalTx.errorMessage); + tradeDepositAddress = normalTx.deposit; + tradeDepositAmount = sendAmount; + tradeWithdrawAddress = sendToAddress; + // set tradeWithdrawAmount after we generate the send tx + } + + request = generateSendRequest(tradeDepositAddress, isEmptyWallet(), tradeDepositAmount); + + // The amountSending could be equal to sendAmount or the actual amount if + // emptying the wallet + Value amountSending = Value.valueOf(sourceType, request.tx + .getValue(sourceAccount).negate() .subtract(request.tx.getFee())); + tradeWithdrawAmount = marketInfo.rate.convert(amountSending); + } else { + // If no values set, make the call + if (tradeDepositAddress == null || tradeDepositAmount == null || + tradeWithdrawAddress == null || tradeWithdrawAmount == null) { + ShapeShiftAmountTx fixedAmountTx = + shapeShift.exchangeForAmount(sendAmount, sendToAddress, refundAddress); + // TODO, show a retry message + if (fixedAmountTx.isError) throw new Exception(fixedAmountTx.errorMessage); + tradeDepositAddress = fixedAmountTx.deposit; + tradeDepositAmount = fixedAmountTx.depositAmount; + tradeWithdrawAddress = fixedAmountTx.withdrawal; + tradeWithdrawAmount = fixedAmountTx.withdrawalAmount; + } + + ShapeShiftTime time = getTimeLeftSync(shapeShift, tradeDepositAddress); + if (time != null && !time.isError) { + int secondsLeft = time.secondsRemaining - SAFE_TIMEOUT_MARGIN; + handler.sendMessage(handler.obtainMessage( + START_TRADE_TIMEOUT, secondsLeft)); + } else { + throw new Exception(time == null ? "Error getting trade expiration time" : time.errorMessage); + } + request = generateSendRequest(tradeDepositAddress, false, tradeDepositAmount); + } + } else { + request = generateSendRequest(sendToAddress, isEmptyWallet(), sendAmount); + } + } catch (Exception e) { + error = e; + } + return null; + } + + @Override + protected void onPostExecute(Void aVoid) { + if (busyDialog != null) busyDialog.dismissAllowingStateLoss(); + if (error != null && mListener != null) { + mListener.onSignResult(error); + } else if (error == null) { + showTransaction(); + } else { + log.warn("Error occurred while creating transaction", error); + } + } + } + + private class SignAndBroadcastTask extends AsyncTask { + private Dialogs.ProgressDialogFragment busyDialog; + + @Override + protected void onPreExecute() { + super.onPreExecute(); + busyDialog = Dialogs.ProgressDialogFragment.newInstance( + getResources().getString(R.string.preparing_transaction)); + busyDialog.show(getFragmentManager(), null); + } + + @Override + protected Exception doInBackground(Void... params) { + Wallet wallet = application.getWallet(); + if (wallet == null) return new NoSuchPocketException("No wallet found."); + try { + if (wallet.isEncrypted()) { + KeyCrypter crypter = checkNotNull(wallet.getKeyCrypter()); + request.aesKey = crypter.deriveKey(password); + } + request.signInputs = true; + sourceAccount.completeAndSignTx(request); + // Before broadcasting, check if there is an error, like the trade expiration + if (error != null) throw error; + if (!sourceAccount.broadcastTxSync(request.tx)) { + throw new Exception("Error broadcasting transaction: " + request.tx.getHashAsString()); + } + transactionBroadcast = true; + handler.sendEmptyMessage(STOP_TRADE_TIMEOUT); + } + catch (Exception e) { error = e; } + + return error; + } + + protected void onPostExecute(Exception error) { + busyDialog.dismissAllowingStateLoss(); + if (mListener != null) { + mListener.onSignResult(error); + if (error == null && tradeDepositAddress != null) { + mListener.onTradeDeposit(tradeDepositAddress); + } + } + } + } +} diff --git a/wallet/src/main/java/com/coinomi/wallet/ui/PreviousAddressesFragment.java b/wallet/src/main/java/com/coinomi/wallet/ui/PreviousAddressesFragment.java index 386c9a4..7b0428d 100644 --- a/wallet/src/main/java/com/coinomi/wallet/ui/PreviousAddressesFragment.java +++ b/wallet/src/main/java/com/coinomi/wallet/ui/PreviousAddressesFragment.java @@ -120,7 +120,7 @@ public void onItemClick(AdapterView parent, View view, int position, long id) if (obj != null && obj instanceof Address) { Bundle args = new Bundle(); args.putString(Constants.ARG_ACCOUNT_ID, accountId); - args.putString(Constants.ARG_ADDRESS, obj.toString()); + args.putSerializable(Constants.ARG_ADDRESS, (Address)obj); listener.onAddressSelected(args); } else { Toast.makeText(getActivity(), R.string.error_generic, Toast.LENGTH_LONG).show(); diff --git a/wallet/src/main/java/com/coinomi/wallet/ui/SendFragment.java b/wallet/src/main/java/com/coinomi/wallet/ui/SendFragment.java index 4c2b890..b48a273 100644 --- a/wallet/src/main/java/com/coinomi/wallet/ui/SendFragment.java +++ b/wallet/src/main/java/com/coinomi/wallet/ui/SendFragment.java @@ -31,6 +31,7 @@ import com.coinomi.core.coins.CoinType; import com.coinomi.core.coins.FiatType; +import com.coinomi.core.coins.Value; import com.coinomi.core.uri.CoinURI; import com.coinomi.core.uri.CoinURIParseException; import com.coinomi.core.util.GenericUtils; @@ -168,7 +169,7 @@ public void onCreate(Bundle savedInstanceState) { private void updateBalance() { if (pocket != null) { - lastBalance = pocket.getBalance(false); + lastBalance = pocket.getBalance(false).toCoin(); } } @@ -191,7 +192,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, AmountEditView sendLocalAmountView = (AmountEditView) view.findViewById(R.id.send_local_amount); sendLocalAmountView.setFormat(FiatType.FRIENDLY_FORMAT); - amountCalculatorLink = new CurrencyCalculatorLink(type, sendCoinAmountView, sendLocalAmountView); + amountCalculatorLink = new CurrencyCalculatorLink(sendCoinAmountView, sendLocalAmountView); amountCalculatorLink.setExchangeDirection(config.getLastExchangeDirection()); addressError = (TextView) view.findViewById(R.id.address_error_message); @@ -288,8 +289,8 @@ public void onMakeTransaction(Address toAddress, Coin amount) { throw new NoSuchPocketException("No pocket found for " + type.getName()); } intent.putExtra(Constants.ARG_ACCOUNT_ID, pocket.getId()); - intent.putExtra(Constants.ARG_SEND_TO_ADDRESS, toAddress.toString()); - intent.putExtra(Constants.ARG_SEND_AMOUNT, amount.getValue()); + intent.putExtra(Constants.ARG_SEND_TO_ADDRESS, toAddress); + intent.putExtra(Constants.ARG_SEND_VALUE, Value.valueOf(type, amount)); startActivityForResult(intent, SIGN_TRANSACTION); } catch (NoSuchPocketException e) { Toast.makeText(getActivity(), R.string.no_such_pocket_error, Toast.LENGTH_LONG).show(); @@ -335,7 +336,7 @@ public void onActivityResult(final int requestCode, final int resultCode, final Toast.makeText(getActivity(), R.string.sending_msg, Toast.LENGTH_SHORT).show(); } else { if (error instanceof InsufficientMoneyException) { - Toast.makeText(getActivity(), R.string.amount_error_not_enough_money, Toast.LENGTH_LONG).show(); + Toast.makeText(getActivity(), R.string.amount_error_not_enough_money_plain, Toast.LENGTH_LONG).show(); } else if (error instanceof NoSuchPocketException) { Toast.makeText(getActivity(), R.string.no_such_pocket_error, Toast.LENGTH_LONG).show(); } else if (error instanceof KeyCrypterException) { @@ -456,7 +457,10 @@ private void validateAmount(boolean isTyping) { minAmount, type.getSymbol()); amountError.setText(message); } else if (lastBalance != null && amountParsed.compareTo(lastBalance) > 0) { - amountError.setText(R.string.amount_error_not_enough_money); + String balance = GenericUtils.formatCoinValue(type, lastBalance); + String message = getResources().getString(R.string.amount_error_not_enough_money, + balance, type.getSymbol()); + amountError.setText(message); } else { // Should not happen, but show a generic error amountError.setText(R.string.amount_error); } @@ -469,8 +473,7 @@ private void validateAmount(boolean isTyping) { } /** - * Show errors if the user is not typing and the input is not empty and the amount is zero. - * Exception is when the amount is lower than the available balance + * Decide if should show errors in the UI. */ private boolean shouldShowErrors(boolean isTyping, Coin amountParsed) { if (amountParsed != null && lastBalance != null && amountParsed.compareTo(lastBalance) >= 0) @@ -524,7 +527,10 @@ private void setAmountForEmptyWallet() { if (state != State.INPUT || pocket == null || lastBalance == null) return; if (lastBalance.isZero()) { - Toast.makeText(getActivity(), R.string.amount_error_not_enough_money, + String balance = GenericUtils.formatCoinValue(type, lastBalance); + String message = getResources().getString(R.string.amount_error_not_enough_money, + balance, type.getSymbol()); + Toast.makeText(getActivity(), balance, Toast.LENGTH_LONG).show(); } else { amountCalculatorLink.setPrimaryAmount(type, lastBalance); diff --git a/wallet/src/main/java/com/coinomi/wallet/ui/ShowSeedFragment.java b/wallet/src/main/java/com/coinomi/wallet/ui/ShowSeedFragment.java index 7da018b..4b2d3be 100644 --- a/wallet/src/main/java/com/coinomi/wallet/ui/ShowSeedFragment.java +++ b/wallet/src/main/java/com/coinomi/wallet/ui/ShowSeedFragment.java @@ -204,8 +204,6 @@ protected Void doInBackground(Void... params) { return null; } - - protected void onPostExecute(Void aVoid) { decryptSeedTask = null; password = null; diff --git a/wallet/src/main/java/com/coinomi/wallet/ui/SignTransactionActivity.java b/wallet/src/main/java/com/coinomi/wallet/ui/SignTransactionActivity.java index 06baf43..7baaf01 100644 --- a/wallet/src/main/java/com/coinomi/wallet/ui/SignTransactionActivity.java +++ b/wallet/src/main/java/com/coinomi/wallet/ui/SignTransactionActivity.java @@ -7,17 +7,19 @@ import com.coinomi.wallet.Constants; import com.coinomi.wallet.R; +import org.bitcoinj.core.Address; + import javax.annotation.Nullable; public class SignTransactionActivity extends AbstractWalletFragmentActivity - implements SignTransactionFragment.Listener { + implements MakeTransactionFragment.Listener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_sign_transaction); getSupportFragmentManager().beginTransaction() - .add(R.id.container, SignTransactionFragment.newInstance(getIntent().getExtras())) + .add(R.id.container, MakeTransactionFragment.newInstance(getIntent().getExtras())) .commit(); } @@ -37,4 +39,7 @@ public void run() } }); } + + @Override + public void onTradeDeposit(Address deposit) { } } diff --git a/wallet/src/main/java/com/coinomi/wallet/ui/SignTransactionFragment.java b/wallet/src/main/java/com/coinomi/wallet/ui/SignTransactionFragment.java deleted file mode 100644 index 757a40b..0000000 --- a/wallet/src/main/java/com/coinomi/wallet/ui/SignTransactionFragment.java +++ /dev/null @@ -1,246 +0,0 @@ -package com.coinomi.wallet.ui; - -import android.app.Activity; -import android.database.Cursor; -import android.os.AsyncTask; -import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.Loader; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.EditText; -import android.widget.TextView; -import android.widget.Toast; - -import com.coinomi.core.coins.CoinID; -import com.coinomi.core.coins.CoinType; -import com.coinomi.core.wallet.SendRequest; -import com.coinomi.core.wallet.Wallet; -import com.coinomi.core.wallet.WalletAccount; -import com.coinomi.core.wallet.WalletPocketHD; -import com.coinomi.core.wallet.exceptions.NoSuchPocketException; -import com.coinomi.wallet.Configuration; -import com.coinomi.wallet.Constants; -import com.coinomi.wallet.ExchangeRatesProvider; -import com.coinomi.wallet.R; -import com.coinomi.wallet.WalletApplication; -import com.coinomi.wallet.ui.widget.TransactionAmountVisualizer; -import com.coinomi.wallet.util.Keyboard; - -import org.bitcoinj.core.Address; -import org.bitcoinj.core.Coin; -import org.bitcoinj.crypto.KeyCrypter; - -import javax.annotation.Nullable; - -import static com.coinomi.core.Preconditions.checkNotNull; -import static com.coinomi.core.Preconditions.checkState; - -/** - * This fragment displays a busy message and makes the transaction in the background - * - */ -public class SignTransactionFragment extends Fragment { - private static final int PASSWORD_CONFIRMATION = 1; - - // Loader IDs - private static final int ID_RATE_LOADER = 0; - - @Nullable private String password; - private SignTransactionActivity mListener; - private MakeTransactionTask makeTransactionTask; - private WalletApplication application; - private Configuration config; - private TransactionAmountVisualizer txVisualizer; - private Address sendToAddress; - private Coin sentAmount; - private CoinType type; - private SendRequest request; - private LoaderManager loaderManager; - private String accountId; - private WalletPocketHD pocket; - - public static SignTransactionFragment newInstance(Bundle args) { - SignTransactionFragment fragment = new SignTransactionFragment(); - fragment.setArguments(args); - return fragment; - } - public SignTransactionFragment() { - // Required empty public constructor - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - application = mListener.getWalletApplication(); - config = application.getConfiguration(); - makeTransactionTask = null; - - Bundle args = getArguments(); - checkNotNull(args, "Must provide arguments"); - checkState(args.containsKey(Constants.ARG_ACCOUNT_ID), "Must provide a coin id"); - checkState(args.containsKey(Constants.ARG_SEND_TO_ADDRESS), "Must provide an address string"); - checkState(args.containsKey(Constants.ARG_SEND_AMOUNT), "Must provide an amount to send"); - - try { - accountId = getArguments().getString(Constants.ARG_ACCOUNT_ID); - // TODO - pocket = (WalletPocketHD) application.getAccount(accountId); - type = pocket.getCoinType(); - sendToAddress = new Address(type, args.getString(Constants.ARG_SEND_TO_ADDRESS)); - sentAmount = Coin.valueOf(args.getLong(Constants.ARG_SEND_AMOUNT)); - } catch (Exception e) { - throw new RuntimeException(e); - } - - loaderManager.initLoader(ID_RATE_LOADER, null, rateLoaderCallbacks); - } - - @Override - public void onDestroy() { - loaderManager.destroyLoader(ID_RATE_LOADER); - super.onDestroy(); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_make_transaction, container, false); - - final EditText passwordView = (EditText) view.findViewById(R.id.password); - final TextView passwordLabelView = (TextView) view.findViewById(R.id.enter_password_label); - if (application.getWallet() != null && application.getWallet().isEncrypted()) { - passwordView.requestFocus(); - passwordView.setVisibility(View.VISIBLE); - passwordLabelView.setVisibility(View.VISIBLE); - } else { - passwordView.setVisibility(View.GONE); - passwordLabelView.setVisibility(View.GONE); - } - - boolean emptyWallet = sentAmount.equals(pocket.getBalance(false)); - - // TODO handle in a task onCreate - try { - if (emptyWallet) { - request = SendRequest.emptyWallet(sendToAddress); - } else { - request = SendRequest.to(sendToAddress, sentAmount); - } - request.signInputs = false; - pocket.completeTx(request); - } catch (Exception e) { - if (mListener != null) { - mListener.onSignResult(e); - } - return view; - } - - txVisualizer = (TransactionAmountVisualizer) view.findViewById(R.id.transaction_amount_visualizer); - txVisualizer.setTransaction(pocket, request.tx); - - view.findViewById(R.id.button_confirm).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (passwordView.isShown()) { - Keyboard.hideKeyboard(getActivity()); - password = passwordView.getText().toString(); - } - maybeStartTask(); - } - }); - - return view; - } - - private void maybeStartTask() { - if (makeTransactionTask == null) { - makeTransactionTask = new MakeTransactionTask(); - makeTransactionTask.execute(); - } - } - - private class MakeTransactionTask extends AsyncTask { - private Dialogs.ProgressDialogFragment busyDialog; - - @Override - protected void onPreExecute() { - super.onPreExecute(); - busyDialog = Dialogs.ProgressDialogFragment.newInstance( - getResources().getString(R.string.preparing_transaction)); - busyDialog.show(getFragmentManager(), null); - } - - @Override - protected Exception doInBackground(Void... params) { - Wallet wallet = application.getWallet(); - if (wallet == null) return new NoSuchPocketException("No wallet found."); - Exception error = null; - try { - if (wallet.isEncrypted()) { - KeyCrypter crypter = checkNotNull(wallet.getKeyCrypter()); - request.aesKey = crypter.deriveKey(password); - } - request.signInputs = true; - pocket.completeAndSignTx(request); - pocket.broadcastTx(request.tx); - } - catch (Exception e) { error = e; } - - return error; - } - - protected void onPostExecute(Exception error) { - busyDialog.dismissAllowingStateLoss(); - if (mListener != null) { - mListener.onSignResult(error); - } - } - } - - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - try { - mListener = (SignTransactionActivity) activity; - loaderManager = getLoaderManager(); - } catch (ClassCastException e) { - throw new ClassCastException(activity.toString() - + " must implement " + SignTransactionActivity.class); - } - } - - @Override - public void onDetach() { - super.onDetach(); - mListener = null; - } - - public interface Listener { - public void onSignResult(@Nullable Exception error); - } - - private final LoaderManager.LoaderCallbacks rateLoaderCallbacks = new LoaderManager.LoaderCallbacks() { - @Override - public Loader onCreateLoader(final int id, final Bundle args) { - String localSymbol = config.getExchangeCurrencyCode(); - String coinSymbol = type.getSymbol(); - return new ExchangeRateLoader(getActivity(), config, localSymbol, coinSymbol); - } - - @Override - public void onLoadFinished(final Loader loader, final Cursor data) { - if (data != null && data.getCount() > 0) { - data.moveToFirst(); - final ExchangeRatesProvider.ExchangeRate exchangeRate = ExchangeRatesProvider.getExchangeRate(data); - if (txVisualizer != null) txVisualizer.setExchangeRate(exchangeRate.rate); - } - } - - @Override - public void onLoaderReset(final Loader loader) { - } - }; -} diff --git a/wallet/src/main/java/com/coinomi/wallet/ui/TradeActivity.java b/wallet/src/main/java/com/coinomi/wallet/ui/TradeActivity.java index eaec098..6470199 100644 --- a/wallet/src/main/java/com/coinomi/wallet/ui/TradeActivity.java +++ b/wallet/src/main/java/com/coinomi/wallet/ui/TradeActivity.java @@ -1,17 +1,38 @@ package com.coinomi.wallet.ui; import android.app.AlertDialog; +import android.app.Dialog; import android.content.DialogInterface; import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.support.v4.app.DialogFragment; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; +import com.coinomi.core.coins.CoinID; +import com.coinomi.core.coins.CoinType; +import com.coinomi.core.coins.Value; +import com.coinomi.core.wallet.Wallet; +import com.coinomi.core.wallet.WalletAccount; import com.coinomi.wallet.Constants; import com.coinomi.wallet.R; +import com.coinomi.wallet.tasks.AddCoinTask; +import com.coinomi.wallet.util.WeakHandler; +import org.bitcoinj.core.Address; -public class TradeActivity extends BaseWalletActivity { +import java.util.ArrayList; - private int containerRes; +import javax.annotation.Nullable; + + +public class TradeActivity extends BaseWalletActivity implements + TradeSelectFragment.Listener, MakeTransactionFragment.Listener, + TradeStatusFragment.Listener, TradeSelectFragmentOld.Listener { + private int containerRes; private enum State { INPUT, PREPARATION, SENDING, SENT, FAILED @@ -24,6 +45,13 @@ protected void onCreate(Bundle savedInstanceState) { containerRes = R.id.container; +// new AlertDialog.Builder(this) +// .setTitle(R.string.terms_of_use_title) +// .setMessage(R.string.terms_of_use_message) +// .setNegativeButton(R.string.button_disagree, null) +// .setPositiveButton(R.string.button_agree, null) +// .create().show(); + if (savedInstanceState == null) { getSupportFragmentManager().beginTransaction() .add(containerRes, new TradeSelectFragment()) @@ -31,78 +59,46 @@ protected void onCreate(Bundle savedInstanceState) { } } -// @Override -// public void onCreateNewWallet() { -// replaceFragment(new SeedFragment(), containerRes); -// } -// -// @Override -// public void onRestoreWallet() { -// replaceFragment(RestoreFragment.newInstance(), containerRes); -// } -// -// @Override -// public void onTestWallet() { -// if (getWalletApplication().getWallet() == null) { -// makeTestWallet(); -// } else { -// new AlertDialog.Builder(this) -// .setTitle(R.string.test_wallet_warning_title) -// .setMessage(R.string.test_wallet_warning_message) -// .setNegativeButton(R.string.button_cancel, null) -// .setPositiveButton(R.string.button_confirm, new DialogInterface.OnClickListener() { -// @Override -// public void onClick(DialogInterface dialog, int which) { -// makeTestWallet(); -// } -// }) -// .create().show(); -// } -// -// } -// -// -// @Override -// public void onSeedCreated(String seed) { -// replaceFragment(RestoreFragment.newInstance(seed)); -// } -// -// @Override -// public void onNewSeedVerified(String seed) { -// replaceFragment(SetPasswordFragment.newInstance(seed)); -// } -// -// @Override -// public void onExistingSeedVerified(String seed, boolean isSeedProtected) { -// Bundle args = new Bundle(); -// args.putString(Constants.ARG_SEED, seed); -// args.putBoolean(Constants.ARG_SEED_PROTECT, isSeedProtected); -// if (isSeedProtected) { -// replaceFragment(PasswordConfirmationFragment.newInstance( -// getResources().getString(R.string.password_wallet_recovery), args)); -// } else { -// replaceFragment(PasswordConfirmationFragment.newInstance( -// getResources().getString(R.string.set_password_info), args)); -// } -// } -// -// @Override -// public void onPasswordConfirmed(Bundle args) { -// selectCoins(args); -// } -// -// @Override -// public void onPasswordSet(Bundle args) { -// selectCoins(args); -// } -// -// private void selectCoins(Bundle args) { -// String message = getResources().getString(R.string.select_coins); -// replaceFragment(SelectCoinsFragment.newInstance(message, true, args)); -// } -// -// @Override -// public void onCoinSelection(Bundle args) { -// replaceFragment(FinalizeWalletRestorationFragment.newInstance(args)); -// } + @Override + public void onMakeTrade(WalletAccount fromAccount, WalletAccount toAccount, Value amount) { + Bundle args = new Bundle(); + args.putString(Constants.ARG_ACCOUNT_ID, fromAccount.getId()); + args.putString(Constants.ARG_SEND_TO_ACCOUNT_ID, toAccount.getId()); + if (amount.type.equals(fromAccount.getCoinType())) { + args.putSerializable(Constants.ARG_SEND_VALUE, amount); + } else if (amount.type.equals(toAccount.getCoinType())) { + args.putSerializable(Constants.ARG_SEND_VALUE, amount); + } else { + throw new IllegalStateException("Amount does not have the expected type: " + amount.type); + } + + replaceFragment(MakeTransactionFragment.newInstance(args), containerRes); + } + + @Override + public void onAbort() { + finish(); + } + + @Override + public void onSignResult(@Nullable Exception error) { + if (error != null) { + getSupportFragmentManager().popBackStack(); + DialogBuilder builder = DialogBuilder.warn(this, R.string.trade_error); + builder.setMessage(getString(R.string.trade_error_sign_tx_message, error.getMessage())); + builder.setPositiveButton(R.string.button_ok, null) + .create().show(); + } + } + + @Override + public void onTradeDeposit(Address deposit) { + getSupportFragmentManager().popBackStack(); + replaceFragment(TradeStatusFragment.newInstance(deposit), containerRes); + } + + @Override + public void onFinish() { + finish(); + } } diff --git a/wallet/src/main/java/com/coinomi/wallet/ui/TradeSelectFragment.java b/wallet/src/main/java/com/coinomi/wallet/ui/TradeSelectFragment.java index 4fb99ea..100067d 100644 --- a/wallet/src/main/java/com/coinomi/wallet/ui/TradeSelectFragment.java +++ b/wallet/src/main/java/com/coinomi/wallet/ui/TradeSelectFragment.java @@ -1,111 +1,115 @@ package com.coinomi.wallet.ui; import android.app.Activity; -import android.content.Intent; +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.Message; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; import android.support.v4.app.Fragment; -import android.text.Editable; -import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.AdapterView; import android.widget.Button; import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; -import com.coinomi.core.coins.BitcoinMain; import com.coinomi.core.coins.CoinType; -import com.coinomi.core.coins.LitecoinMain; -import com.coinomi.core.uri.CoinURI; -import com.coinomi.core.uri.CoinURIParseException; +import com.coinomi.core.coins.Value; +import com.coinomi.core.exchange.shapeshift.ShapeShift; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftCoins; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftException; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftMarketInfo; import com.coinomi.core.util.ExchangeRate; +import com.coinomi.core.util.GenericUtils; import com.coinomi.core.wallet.Wallet; -import com.coinomi.core.wallet.WalletPocketHD; -import com.coinomi.core.wallet.exceptions.NoSuchPocketException; -import com.coinomi.wallet.Configuration; +import com.coinomi.core.wallet.WalletAccount; import com.coinomi.wallet.Constants; import com.coinomi.wallet.R; +import com.coinomi.wallet.WalletApplication; +import com.coinomi.wallet.tasks.AddCoinTask; import com.coinomi.wallet.ui.adaptors.AvailableAccountsAdaptor; import com.coinomi.wallet.ui.widget.AmountEditView; +import com.coinomi.wallet.util.Keyboard; import com.coinomi.wallet.util.ThrottlingWalletChangeListener; import com.coinomi.wallet.util.WeakHandler; +import com.google.common.collect.ImmutableList; -import org.bitcoinj.core.Address; -import org.bitcoinj.core.Coin; -import org.bitcoinj.core.InsufficientMoneyException; -import org.bitcoinj.crypto.KeyCrypterException; -import org.bitcoinj.utils.Fiat; import org.bitcoinj.utils.Threading; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; import javax.annotation.Nullable; -import static com.coinomi.core.Preconditions.checkNotNull; - /** - * Fragment that prepares a transaction - * - * @author Andreas Schildbach * @author John L. Jegutanis */ public class TradeSelectFragment extends Fragment { private static final Logger log = LoggerFactory.getLogger(TradeSelectFragment.class); - // the fragment initialization parameters - private static final int REQUEST_CODE_SCAN = 0; - private static final int SIGN_TRANSACTION = 1; - - private static final int UPDATE_WALLET_CHANGE = 1; - - // Loader IDs - private static final int ID_RECEIVING_ADDRESS_LOADER = 0; - - private CoinType fromCoinType; - private CoinType toCoinType; - @Nullable private Coin lastBalance; // TODO setup wallet watcher for the latest balance -// private AutoCompleteTextView sendToAddressView; -// private TextView addressError; + private static final int UPDATE_MARKET = 0; + private static final int UPDATE_MARKET_ERROR = 1; + private static final int UPDATE_WALLET = 2; + private static final int VALIDATE_AMOUNT = 3; + + private static final long POLLING_MS = 30000; + + // UI & misc + private WalletApplication application; + private Wallet wallet; + private final Handler handler = new MyHandler(this); + private final AmountListener amountsListener = new AmountListener(handler); + private final AccountListener sourceAccountListener = new AccountListener(handler); + @Nullable private Listener listener; + @Nullable private MenuItem actionSwapMenu; + private Spinner sourceSpinner; + private Spinner destinationSpinner; + private AvailableAccountsAdaptor sourceAdapter; + private AvailableAccountsAdaptor destinationAdapter; + private AmountEditView sourceAmountView; + private AmountEditView destinationAmountView; private CurrencyCalculatorLink amountCalculatorLink; - private TextView receiveCoinWarning; private TextView amountError; private TextView amountWarning; -// private ImageButton scanQrCodeButton; private Button nextButton; -// private Address address; - private Coin tradeAmount; - private Coin receiveAmount; - @Nullable private BaseWalletActivity activity; - @Nullable private WalletPocketHD pocket; - @Nullable private Wallet wallet; - private Configuration config; -// private ReceivingAddressViewAdapter sendToAddressViewAdapter; + // Tasks + private MarketInfoTask marketTask; + private InitialCheckTask initialTask; + private Timer timer; + private MarketInfoPollTask pollTask; + private AddCoinAndProceedTask addCoinAndProceedTask; - Handler handler = new MyHandler(this); - private static class MyHandler extends WeakHandler { - public MyHandler(TradeSelectFragment referencingObject) { super(referencingObject); } + // State + private WalletAccount sourceAccount; + @Nullable private WalletAccount destinationAccount; + private CoinType destinationType; + @Nullable private Value sendAmount; + @Nullable private Value maximumDeposit; + @Nullable private Value minimumDeposit; + @Nullable private Value lastBalance; + @Nullable private ExchangeRate lastRate; - @Override - protected void weakHandleMessage(TradeSelectFragment ref, Message msg) { - switch (msg.what) { - case UPDATE_WALLET_CHANGE: - ref.onWalletUpdate(); - } - } - } - public TradeSelectFragment() { - // Required empty public constructor - } + /** Required empty public constructor */ + public TradeSelectFragment() {} + + + //////////////////////////////////////////////////////////////////////////////////////////////// + // Android callback methods @Override public void onCreate(Bundle savedInstanceState) { @@ -113,15 +117,22 @@ public void onCreate(Bundle savedInstanceState) { setHasOptionsMenu(true); -// loaderManager.initLoader(ID_RECEIVING_ADDRESS_LOADER, null, receivingAddressLoaderCallbacks); - - wallet = activity.getWallet(); - } - - private void updateBalance() { - if (pocket != null) { - lastBalance = pocket.getBalance(false); + // Select some default coins + List accounts = application.getAllAccounts(); + sourceAccount = accounts.get(0); + if (accounts.size() > 1) { + destinationAccount = accounts.get(1); + destinationType = destinationAccount.getCoinType(); + } else { + // Find a destination coin that is different than the source coin + for (CoinType type : Constants.SUPPORTED_COINS) { + if (type.equals(sourceAccount.getCoinType())) continue; + destinationType = type; + break; + } } + + updateBalance(); } @Override @@ -130,39 +141,21 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, // Inflate the layout for this fragment View view = inflater.inflate(R.layout.fragment_trade_select, container, false); -// sendToAddressView = (AutoCompleteTextView) view.findViewById(R.id.custom_receive_address); -// sendToAddressViewAdapter = new ReceivingAddressViewAdapter(activity); -// sendToAddressView.setAdapter(sendToAddressViewAdapter); -// sendToAddressView.setOnFocusChangeListener(receivingAddressListener); -// sendToAddressView.addTextChangedListener(receivingAddressListener); + sourceSpinner = (Spinner) view.findViewById(R.id.from_coin); + sourceSpinner.setAdapter(getSourceSpinnerAdapter()); + sourceSpinner.setOnItemSelectedListener(getSourceSpinnerListener()); - // TODO make dynamic - fromCoinType = BitcoinMain.get(); - toCoinType = LitecoinMain.get(); - pocket = (WalletPocketHD) activity.getWalletApplication().getAccounts(fromCoinType).get(0); + destinationSpinner = (Spinner) view.findViewById(R.id.to_coin); + destinationSpinner.setAdapter(getDestinationSpinnerAdapter()); + destinationSpinner.setOnItemSelectedListener(getDestinationSpinnerListener()); + sourceAmountView = (AmountEditView) view.findViewById(R.id.trade_coin_amount); + destinationAmountView = (AmountEditView) view.findViewById(R.id.receive_coin_amount); - Spinner fromCoinSpinner = (Spinner) view.findViewById(R.id.from_coin); - fromCoinSpinner.setAdapter(new AvailableAccountsAdaptor(activity, wallet.getAllAccounts())); - Spinner toCoinSpinner = (Spinner) view.findViewById(R.id.to_coin); - toCoinSpinner.setAdapter(new AvailableAccountsAdaptor(activity, Constants.SUPPORTED_COINS)); + amountCalculatorLink = new CurrencyCalculatorLink(sourceAmountView, destinationAmountView); - AmountEditView tradeCoinAmountView = (AmountEditView) view.findViewById(R.id.trade_coin_amount); - tradeCoinAmountView.setType(fromCoinType); - tradeCoinAmountView.setFormat(fromCoinType.getMonetaryFormat()); - - AmountEditView receiveCoinAmountView = (AmountEditView) view.findViewById(R.id.receive_coin_amount); - receiveCoinAmountView.setType(toCoinType); - receiveCoinAmountView.setFormat(toCoinType.getMonetaryFormat()); - - amountCalculatorLink = new CurrencyCalculatorLink(fromCoinType, tradeCoinAmountView, receiveCoinAmountView); - // TODO get rate from shapeshift and don't use Fiat - ExchangeRate rate = new ExchangeRate(BitcoinMain.get().oneCoin(), LitecoinMain.get().oneCoin().multiply(140)); - amountCalculatorLink.setExchangeRate(rate); -// amountCalculatorLink.setExchangeDirection(config.getLastExchangeDirection()); - - receiveCoinWarning = (TextView) view.findViewById(R.id.warn_no_account_found); - receiveCoinWarning.setVisibility(View.GONE); +// receiveCoinWarning = (TextView) view.findViewById(R.id.warn_no_account_found); +// receiveCoinWarning.setVisibility(View.GONE); // addressError = (TextView) view.findViewById(R.id.address_error_message); // addressError.setVisibility(View.GONE); amountError = (TextView) view.findViewById(R.id.amount_error_message); @@ -178,274 +171,726 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, // } // }); + view.findViewById(R.id.powered_by_shapeshift).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new AlertDialog.Builder(getActivity()) + .setTitle(R.string.about_shapeshift_title) + .setMessage(R.string.about_shapeshift_message) + .setPositiveButton(R.string.button_ok, null) + .create().show(); + } + }); + nextButton = (Button) view.findViewById(R.id.button_next); nextButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - System.out.println("TODO next"); // validateAddress(); -// validateAmount(); -// if (everythingValid()) -// handleSendConfirm(); -// else -// requestFocusFirst(); + validateAmount(); + if (everythingValid()) { + onHandleNext(); + } else if (amountCalculatorLink.isEmpty()) { + amountError.setText(R.string.amount_error_empty); + amountError.setVisibility(View.VISIBLE); + } } }); + // Setup the default source & destination views + setSource(sourceAccount, false); + if (destinationAccount != null) { + setDestination(destinationAccount, false); + } else { + setDestination(destinationType, false); + } + + if (!application.isConnected()) { + showInitialTaskErrorDialog(null); + } else { + maybeStartInitialTask(); + } + return view; } + private AvailableAccountsAdaptor getDestinationSpinnerAdapter() { + if (destinationAdapter == null) { + destinationAdapter = new AvailableAccountsAdaptor(getActivity()); + } + return destinationAdapter; + } + + private AvailableAccountsAdaptor getSourceSpinnerAdapter() { + if (sourceAdapter == null) { + sourceAdapter = new AvailableAccountsAdaptor(getActivity()); + } + return sourceAdapter; + } + + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + try { + this.listener = (Listener) activity; + this.application = (WalletApplication) activity.getApplication(); + this.wallet = application.getWallet(); + } catch (ClassCastException e) { + throw new ClassCastException(activity.toString() + + " must implement " + TradeSelectFragment.Listener.class); + } + } + + @Override + public void onDetach() { + super.onDetach(); + listener = null; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.trade, menu); + actionSwapMenu = menu.findItem(R.id.action_swap_coins); + } + @Override - public void onDestroy() { -// loaderManager.destroyLoader(ID_RECEIVING_ADDRESS_LOADER); + public void onPause() { + stopPolling(); + + removeSourceListener(); + + amountCalculatorLink.setListener(null); - super.onDestroy(); + super.onPause(); } @Override public void onResume() { super.onResume(); + startPolling(); amountCalculatorLink.setListener(amountsListener); - if (pocket != null) - pocket.addEventListener(transactionChangeListener, Threading.SAME_THREAD); - updateBalance(); + addSourceListener(); - updateView(); + updateNextButtonState(); } + @Override - public void onPause() { - if (pocket != null) pocket.removeEventListener(transactionChangeListener); - transactionChangeListener.removeCallbacks(); + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { +// case R.id.action_empty_wallet: +// setAmountForEmptyWallet(); +// return true; + case R.id.action_refresh: + refreshStartInitialTask(); + return true; + case R.id.action_swap_coins: + swapAccounts(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } - amountCalculatorLink.setListener(null); - super.onPause(); + //////////////////////////////////////////////////////////////////////////////////////////////// + // Methods + + private void onHandleNext() { + if (listener != null) { + if (destinationAccount == null) { + createToAccountAndProceed(); + } else { + if (everythingValid()) { + Keyboard.hideKeyboard(getActivity()); + listener.onMakeTrade(sourceAccount, destinationAccount, sendAmount); + } else { + Toast.makeText(getActivity(), R.string.amount_error, Toast.LENGTH_LONG).show(); + } + } + } } - private void handleScan() { - startActivityForResult(new Intent(getActivity(), ScanActivity.class), REQUEST_CODE_SCAN); - } - - private void handleSendConfirm() { - // TODO -// if (!everythingValid()) { // Sanity check -// log.error("Unexpected validity failure."); -// validateAmount(); -// validateAddress(); -// return; -// } -//// state = State.PREPARATION; -// updateView(); -// if (activity != null && activity.getWalletApplication().getWallet() != null) { -// onMakeTransaction(address, sendAmount); -// } -// reset(); - } - - public void onMakeTransaction(Address toAddress, Coin amount) { - // TODO -// Intent intent = new Intent(getActivity(), SignTransactionActivity.class); -// try { -// if (pocket == null) { -// throw new NoSuchPocketException("No pocket found for " + type.getName()); -// } -// intent.putExtra(Constants.ARG_ACCOUNT_ID, pocket.getId()); -// intent.putExtra(Constants.ARG_SEND_TO_ADDRESS, toAddress.toString()); -// intent.putExtra(Constants.ARG_SEND_AMOUNT, amount.getValue()); -// startActivityForResult(intent, SIGN_TRANSACTION); -// } catch (NoSuchPocketException e) { -// Toast.makeText(getActivity(), R.string.no_such_pocket_error, Toast.LENGTH_LONG).show(); -// } - } - - private void reset() { -// sendToAddressView.setText(null); - amountCalculatorLink.setPrimaryAmount(null); - tradeAmount = null; - receiveAmount = null; -// state = State.INPUT; -// addressError.setVisibility(View.GONE); - amountError.setVisibility(View.GONE); - amountWarning.setVisibility(View.GONE); - updateView(); + private void createToAccountAndProceed() { + if (destinationType == null) { + Toast.makeText(getActivity(), R.string.error_generic, Toast.LENGTH_SHORT).show(); + return; + } + + createToAccountAndProceedDialog.show(getFragmentManager(), null); } - @Override - public void onActivityResult(final int requestCode, final int resultCode, final Intent intent) { - if (requestCode == REQUEST_CODE_SCAN) { - if (resultCode == Activity.RESULT_OK) { - final String input = intent.getStringExtra(ScanActivity.INTENT_EXTRA_RESULT); + /** + * Start account creation task and proceed + */ + private void maybeStartAddCoinAndProceedTask(@Nullable String password) { + if (addCoinAndProceedTask == null) { + addCoinAndProceedTask = new AddCoinAndProceedTask(destinationType, wallet, password); + addCoinAndProceedTask.execute(); + } + } - try { - final CoinURI coinUri = new CoinURI(toCoinType, input); + private void addSourceListener() { + sourceAccount.addEventListener(sourceAccountListener, Threading.SAME_THREAD); + onWalletUpdate(); + } + + private void removeSourceListener() { + sourceAccount.removeEventListener(sourceAccountListener); + sourceAccountListener.removeCallbacks(); + } + + /** + * Start polling for the market information of the current pair, if it is already stated this + * call does nothing + */ + private void startPolling() { + if (timer == null) { + ShapeShift shapeShift = application.getShapeShift(); + pollTask = new MarketInfoPollTask(handler, shapeShift, getPair()); + timer = new Timer(); + timer.schedule(pollTask, 0, POLLING_MS); + } + } + + /** + * Stop the polling for the market info, if it is already stop this call does nothing + */ + private void stopPolling() { + if (timer != null) { + timer.cancel(); + timer.purge(); + timer = null; + pollTask.cancel(); + pollTask = null; + } + } + + /** + * Updates the spinners to include only available and supported coins + */ + private void updateAvailableCoins(ShapeShiftCoins availableCoins) { + List supportedTypes = getSupportedTypes(availableCoins.availableCoinTypes); + List allAccounts = application.getAllAccounts(); + + sourceAdapter.update(allAccounts, supportedTypes, false); + destinationAdapter.update(allAccounts, supportedTypes, true); + + if (sourceSpinner.getSelectedItemPosition() == -1) { + sourceSpinner.setSelection(0); + } + + if (destinationSpinner.getSelectedItemPosition() == -1) { + destinationSpinner.setSelection(1); + } + } + + /** + * Show a no connectivity error + */ + private void showInitialTaskErrorDialog(String error) { + DialogBuilder builder; - Address address = coinUri.getAddress(); - Coin amount = coinUri.getAmount(); - String label = coinUri.getLabel(); + if (error == null) { + builder = DialogBuilder.warn(getActivity(), R.string.trade_warn_no_connection_title); + builder.setMessage(R.string.trade_warn_no_connection_message); + } else { + builder = DialogBuilder.warn(getActivity(), R.string.trade_error); + builder.setMessage( + getString(R.string.trade_error_message, error)); + } - updateStateFrom(address, amount, label); - } catch (final CoinURIParseException x) { - String error = getResources().getString(R.string.uri_error, x.getMessage()); - Toast.makeText(getActivity(), error, Toast.LENGTH_LONG).show(); + builder.setNegativeButton(R.string.button_dismiss, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (listener != null) { + listener.onAbort(); } } - } else if (requestCode == SIGN_TRANSACTION) { - if (resultCode == Activity.RESULT_OK) { - Exception error = (Exception) intent.getSerializableExtra(Constants.ARG_ERROR); + }); + builder.setPositiveButton(R.string.button_retry, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + initialTask = null; + maybeStartInitialTask(); + } + }); + builder.create().show(); + } + + /** + * Returns a list of the supported coins from the list of the available coins + */ + private List getSupportedTypes(List availableCoins) { + ImmutableList.Builder builder = ImmutableList.builder(); + for (CoinType supportedType : Constants.SUPPORTED_COINS) { + if (availableCoins.contains(supportedType)) { + builder.add(supportedType); + } + } + return builder.build(); + } + + private void refreshStartInitialTask() { + if (initialTask != null) { + initialTask.cancel(true); + initialTask = null; + } + maybeStartInitialTask(); + } + + private void maybeStartInitialTask() { + if (initialTask == null) { + initialTask = new InitialCheckTask(); + initialTask.execute(); + } + } + + /** + * Starts a new task to query about the market of the currently selected pair. + * Notes: + * - If a task is already running, this call will cancel it. + * - If the fragment is detached, it will not run. + */ + private void startMarketInfoTask() { + if (marketTask != null) { + marketTask.cancel(true); + marketTask = null; + } + if (getActivity() != null) { + marketTask = new MarketInfoTask(handler, application.getShapeShift(), getPair()); + marketTask.execute(); + } + } + + /** + * Get the current source and destination pair + */ + private String getPair() { + return ShapeShift.getPair(sourceAccount.getCoinType(), destinationType); + } + + /** + * Updates the exchange rate and limits for the specific market. + * Note: if the current pair is different that the marketInfo pair, do nothing + */ + private void onMarketUpdate(ShapeShiftMarketInfo marketInfo) { + // If not current pair, do nothing + if (!marketInfo.isPair(sourceAccount.getCoinType(), destinationType)) return; + + maximumDeposit = marketInfo.limit; + minimumDeposit = marketInfo.minimum; + + lastRate = marketInfo.rate; + amountCalculatorLink.setExchangeRate(lastRate); + if (amountCalculatorLink.isEmpty() && lastRate != null) { + Value hintValue = sourceAccount.getCoinType().oneCoin(); + Value exchangedValue = lastRate.convert(hintValue); + // If hint value is too small, make it higher to get a no zero exchanged value + for (int tries = 8; tries > 0 && exchangedValue.isZero(); tries--) { + hintValue = hintValue.multiply(10); + exchangedValue = lastRate.convert(hintValue); + } + amountCalculatorLink.setExchangeRateHints(hintValue); + } + } - if (error == null) { - Toast.makeText(getActivity(), R.string.sending_msg, Toast.LENGTH_SHORT).show(); + /** + * Get the item selected listener for the source spinner. It will swap the accounts if the + * destination account is the same as the new source account. + */ + private AdapterView.OnItemSelectedListener getSourceSpinnerListener() { + return new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + AvailableAccountsAdaptor.Entry entry = + (AvailableAccountsAdaptor.Entry) parent.getSelectedItem(); + if (entry.accountOrCoinType instanceof WalletAccount) { + WalletAccount newSource = (WalletAccount) entry.accountOrCoinType; + // If same account selected, do nothing + if (newSource.equals(sourceAccount)) return; + // If new source and destination are the same, swap accounts + if (destinationAccount != null && destinationAccount.equals(newSource)) { + // Swap accounts + setDestinationSpinner(sourceAccount); + setDestination(sourceAccount, false); + } + setSource(newSource, true); } else { - if (error instanceof InsufficientMoneyException) { - Toast.makeText(getActivity(), R.string.amount_error_not_enough_money, Toast.LENGTH_LONG).show(); - } else if (error instanceof NoSuchPocketException) { - Toast.makeText(getActivity(), R.string.no_such_pocket_error, Toast.LENGTH_LONG).show(); - } else if (error instanceof KeyCrypterException) { - Toast.makeText(getActivity(), R.string.password_failed, Toast.LENGTH_LONG).show(); - } else if (error instanceof IOException) { - Toast.makeText(getActivity(), R.string.send_coins_error_network, Toast.LENGTH_LONG).show(); - } else if (error instanceof org.bitcoinj.core.Wallet.DustySendRequested) { - Toast.makeText(getActivity(), R.string.send_coins_error_dust, Toast.LENGTH_LONG).show(); - } else { - log.error("An unknown error occurred while sending coins", error); - String errorMessage = getString(R.string.send_coins_error, error.getMessage()); - Toast.makeText(getActivity(), errorMessage, Toast.LENGTH_LONG).show(); + // Should not happen as "source" is always an account + throw new IllegalStateException("Unexpected class: " + + entry.accountOrCoinType.getClass()); + } + } + + @Override public void onNothingSelected(AdapterView parent) {} + }; + } + + /** + * Get the item selected listener for the destination spinner. It will swap the accounts if the + * source account is the same as the new destination account. + */ + private AdapterView.OnItemSelectedListener getDestinationSpinnerListener() { + return new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + AvailableAccountsAdaptor.Entry entry = + (AvailableAccountsAdaptor.Entry) parent.getSelectedItem(); + if (entry.accountOrCoinType instanceof WalletAccount) { + WalletAccount newDestination = (WalletAccount) entry.accountOrCoinType; + // If same account selected, do nothing + if (newDestination.equals(destinationAccount)) return; + // If new destination and source are the same, swap accounts + if (destinationAccount != null && sourceAccount.equals(newDestination)) { + // Swap accounts + setSourceSpinner(destinationAccount); + setSource(destinationAccount, false); } + setDestination(newDestination, true); + } else if (entry.accountOrCoinType instanceof CoinType) { + setDestination((CoinType) entry.accountOrCoinType, true); + } else { + // Should not happen + throw new IllegalStateException("Unexpected class: " + + entry.accountOrCoinType.getClass()); } } + + @Override public void onNothingSelected(AdapterView parent) {} + }; + } + + /** + * Selects an account on the sourceSpinner without calling the callback. If no account found in + * the adaptor, does not do anything + */ + private void setSourceSpinner(WalletAccount account) { + int newPosition = sourceAdapter.getAccountOrTypePosition(account); + + if (newPosition >= 0) { + AdapterView.OnItemSelectedListener cb = sourceSpinner.getOnItemSelectedListener(); + sourceSpinner.setOnItemSelectedListener(null); + sourceSpinner.setSelection(newPosition); + sourceSpinner.setOnItemSelectedListener(cb); } } - void updateStateFrom(final Address address, final @Nullable Coin amount, - final @Nullable String label) throws CoinURIParseException { - // TODO -// log.info("got {}", address); -// if (address == null) { -// throw new CoinURIParseException("missing address"); -// } -// -// // delay these actions until fragment is resumed -// handler.post(new Runnable() { -// @Override -// public void run() { -// sendToAddressView.setText(address.toString()); -// if (amount != null) amountCalculatorLink.setPrimaryAmount(amount); -// validateEverything(); -// requestFocusFirst(); -// } -// }); + /** + * Selects an account on the destinationSpinner without calling the callback. If no account + * found in the adaptor, does not do anything + */ + private void setDestinationSpinner(Object accountOrType) { + int newPosition = destinationAdapter.getAccountOrTypePosition(accountOrType); + + if (newPosition >= 0) { + AdapterView.OnItemSelectedListener cb = destinationSpinner.getOnItemSelectedListener(); + destinationSpinner.setOnItemSelectedListener(null); + destinationSpinner.setSelection(newPosition); + destinationSpinner.setOnItemSelectedListener(cb); + } } - private void updateView() { - nextButton.setEnabled(everythingValid()); + /** + * Sets the source account and makes a network call to ask about the new pair. + * Note: this does not update the source spinner, use {@link #setSourceSpinner(WalletAccount)} + */ + private void setSource(WalletAccount account, boolean startNetworkTask) { + removeSourceListener(); + sourceAccount = account; + addSourceListener(); - // enable actions - // TODO -// if (scanQrCodeButton != null) { -// scanQrCodeButton.setEnabled(state == State.INPUT); -// } - } + sourceAmountView.reset(); + sourceAmountView.setType(sourceAccount.getCoinType()); + sourceAmountView.setFormat(sourceAccount.getCoinType().getMonetaryFormat()); - private boolean isOutputsValid() { - // TODO -// return address != null; - return true; + amountCalculatorLink.setExchangeRate(null); + + minimumDeposit = null; + maximumDeposit = null; + + updateOptionsMenu(); + + if (startNetworkTask) { + startMarketInfoTask(); + if (pollTask != null) pollTask.updatePair(getPair()); + application.maybeConnectAccount(sourceAccount); + } } - private boolean isAmountValid() { - return isAmountValid(fromCoinType, tradeAmount, true) && - isAmountValid(toCoinType, receiveAmount, false); + /** + * Sets the destination account and makes a network call to ask about the new pair. + * Note: this does not update the destination spinner, use + * {@link #setDestinationSpinner(Object)} + */ + private void setDestination(WalletAccount account, boolean startNetworkTask) { + setDestination(account.getCoinType(), false); + destinationAccount = account; + updateOptionsMenu(); + + if (startNetworkTask) { + startMarketInfoTask(); + if (pollTask != null) pollTask.updatePair(getPair()); + } } - private boolean isAmountValid(CoinType type, Coin amount, boolean isFromMe) { - boolean isValid = amount != null - && amount.isPositive() - && amount.compareTo(type.getMinNonDust()) >= 0; - if (isFromMe && isValid && lastBalance != null) { - // Check if we have the amount - isValid = amount.compareTo(lastBalance) <= 0; + /** + * Sets the destination coin type and makes a network call to ask about the new pair. + * Note: this does not update the destination spinner, use + * {@link #setDestinationSpinner(Object)} + */ + private void setDestination(CoinType type, boolean startNetworkTask) { + destinationAccount = null; + destinationType = type; + + destinationAmountView.reset(); + destinationAmountView.setType(destinationType); + destinationAmountView.setFormat(destinationType.getMonetaryFormat()); + + amountCalculatorLink.setExchangeRate(null); + + minimumDeposit = null; + maximumDeposit = null; + + updateOptionsMenu(); + + if (startNetworkTask) { + startMarketInfoTask(); + if (pollTask != null) pollTask.updatePair(getPair()); } - return isValid; } - private boolean everythingValid() { -// return state == State.INPUT && isOutputsValid() && isAmountValid(); - return isOutputsValid() && isAmountValid(); - } - - private void requestFocusFirst() { - if (!isOutputsValid()) { - // TODO -// sendToAddressView.requestFocus(); - } else if (!isAmountValid()) { - amountCalculatorLink.requestFocus(); - } else if (everythingValid()) { - nextButton.requestFocus(); + /** + * Swap the source & destination accounts. + * Note: this works if the destination is an account, not a CoinType. + */ + private void swapAccounts() { + if (isSwapAccountPossible()) { + WalletAccount newSource = destinationAccount; + WalletAccount newDestination = sourceAccount; + + setSourceSpinner(newSource); + setDestinationSpinner(newDestination); + + setSource(newSource, false); + setDestination(newDestination, true); } else { - log.warn("unclear focus"); + // Should not happen as we need to first check if isSwapAccountPossible() before showing + // a swap action to the user + Toast.makeText(getActivity(), R.string.error_generic, + Toast.LENGTH_SHORT).show(); + } + } + + /** + * Check is if possible to perform the {@link #swapAccounts()} action + */ + private boolean isSwapAccountPossible() { + return destinationAccount != null; + } + + /** + * Updates the options menu to take in to account the new selected accounts types, i.e. disable + * the swap action + */ + private void updateOptionsMenu() { + if (actionSwapMenu != null) { + actionSwapMenu.setEnabled(isSwapAccountPossible()); + } + } + + /** + * Makes a call to ShapeShift about the market info of a pair. If case of a problem, it will + * retry 3 times and return null if there was an error. + * + * Note: do not call this from the main thread! + */ + @Nullable + private static ShapeShiftMarketInfo getMarketInfoSync(ShapeShift shapeShift, String pair) { + // Try 3 times + for (int tries = 1; tries <= 3; tries++) { + try { + log.info("Polling market info for pair: {}", pair); + return shapeShift.getMarketInfo(pair); + } catch (Exception e) { + log.info("Will retry: {}", e.getMessage()); + /* ignore and retry, with linear backoff */ + try { + Thread.sleep(1000 * tries); + } catch (InterruptedException ie) { /*ignored*/ } + } } + return null; + } + + private void updateBalance() { + lastBalance = sourceAccount.getBalance(false); } - private void validateEverything() { - validateAddress(); + private void onWalletUpdate() { + updateBalance(); validateAmount(); } + /** + * Check if amount is within the minimum and maximum deposit limits and if is dust or if is more + * money than currently in the wallet + */ + private boolean isAmountWithinLimits(Value amount) { + boolean isWithinLimits = !amount.isDust(); + + // Check if within min & max deposit limits + if (isWithinLimits && minimumDeposit != null && maximumDeposit != null && + minimumDeposit.isOfType(amount) && maximumDeposit.isOfType(amount)) { + isWithinLimits = amount.within(minimumDeposit, maximumDeposit); + } + + // Check if we have the amount + if (isWithinLimits && lastBalance != null && lastBalance.isOfType(amount)) { + isWithinLimits = amount.compareTo(lastBalance) <= 0; + } + + return isWithinLimits; + } + + /** + * Check if amount is smaller than the dust limit or if applicable, the minimum deposit. + */ + private boolean isAmountTooSmall(Value amount) { + return amount.compareTo(getLowestAmount(amount)) < 0; + } + + /** + * Get the lowest deposit or withdraw for the provided amount type + */ + private Value getLowestAmount(Value amount) { + Value min = amount.type.minNonDust(); + if (minimumDeposit != null) { + if (minimumDeposit.isOfType(min)) { + min = Value.max(minimumDeposit, min); + } else if (lastRate != null && lastRate.canConvert(amount.type, minimumDeposit.type)) { + min = Value.max(lastRate.convert(minimumDeposit), min); + } + } + return min; + } + + /** + * Check if the amount is valid + */ + private boolean isAmountValid(Value amount) { + boolean isValid = amount != null && !amount.isDust(); + + if (isValid && amount.isOfType(sourceAccount.getCoinType())) { + isValid = isAmountWithinLimits(amount); + } + + return isValid; + } + + /** + * {@inheritDoc #validateAmount(boolean)} + */ private void validateAmount() { validateAmount(false); } + /** + * Validate amount and show errors if needed + */ private void validateAmount(boolean isTyping) { - // TODO -// Coin amountParsed = amountCalculatorLink.getPrimaryAmount(); -// -// if (isAmountValid(amountParsed)) { -// sendAmount = amountParsed; -// amountError.setVisibility(View.GONE); -// // Show warning that fees apply when entered the full amount inside the pocket -// if (sendAmount != null && lastBalance != null && sendAmount.compareTo(lastBalance) == 0) { -// amountWarning.setText(R.string.amount_warn_fees_apply); -// amountWarning.setVisibility(View.VISIBLE); -// } else { -// amountWarning.setVisibility(View.GONE); -// } -// } else { -// amountWarning.setVisibility(View.GONE); -// // ignore printing errors for null and zero amounts -// if (shouldShowErrors(isTyping, amountParsed)) { -// sendAmount = null; -// if (amountParsed == null) { -// amountError.setText(R.string.amount_error); -// } else if (amountParsed.isNegative()) { -// amountError.setText(R.string.amount_error_negative); -// } else if (amountParsed.compareTo(type.getMinNonDust()) < 0) { -// String minAmount = GenericUtils.formatCoinValue(type, type.getMinNonDust()); -// String message = getResources().getString(R.string.amount_error_too_small, -// minAmount, type.getSymbol()); -// amountError.setText(message); -// } else if (lastBalance != null && amountParsed.compareTo(lastBalance) > 0) { -// amountError.setText(R.string.amount_error_not_enough_money); -// } else { // Should not happen, but show a generic error -// amountError.setText(R.string.amount_error); -// } -// amountError.setVisibility(View.VISIBLE); -// } else { -// amountError.setVisibility(View.GONE); -// } -// } -// updateView(); + Value depositAmount = amountCalculatorLink.getPrimaryAmount(); + Value withdrawAmount = amountCalculatorLink.getSecondaryAmount(); + Value requestedAmount = amountCalculatorLink.getRequestedAmount(); + + if (isAmountValid(depositAmount) && isAmountValid(withdrawAmount)) { + sendAmount = requestedAmount; + amountError.setVisibility(View.GONE); + // Show warning that fees apply when entered the full amount inside the pocket + if (lastBalance != null && lastBalance.isOfType(depositAmount) && + lastBalance.compareTo(depositAmount) == 0) { + amountWarning.setText(R.string.amount_warn_fees_apply); + amountWarning.setVisibility(View.VISIBLE); + } else { + amountWarning.setVisibility(View.GONE); + } + } else { + amountWarning.setVisibility(View.GONE); + sendAmount = null; + boolean showErrors = shouldShowErrors(isTyping, depositAmount) || + shouldShowErrors(isTyping, withdrawAmount); + // ignore printing errors for null and zero amounts + if (showErrors) { + if (depositAmount == null || withdrawAmount == null) { + amountError.setText(R.string.amount_error); + } else if (depositAmount.isNegative() || withdrawAmount.isNegative()) { + amountError.setText(R.string.amount_error_negative); + } else if (!isAmountWithinLimits(depositAmount) || !isAmountWithinLimits(withdrawAmount)) { + String message = getString(R.string.error_generic); + // If the amount is dust or lower than the deposit limit + if (isAmountTooSmall(depositAmount) || isAmountTooSmall(withdrawAmount)) { + Value minimumDeposit = getLowestAmount(depositAmount); + Value minimumWithdraw = getLowestAmount(withdrawAmount); + message = getString(R.string.trade_error_min_limit, + minimumDeposit.toFriendlyString(), + minimumWithdraw.toFriendlyString()); + } else { + // If we have the amount + if (lastBalance != null && lastBalance.isOfType(depositAmount) && + depositAmount.compareTo(lastBalance) > 0) { + message = getString(R.string.amount_error_not_enough_money, + GenericUtils.formatValue(lastBalance), + lastBalance.type.getSymbol()); + } + + if (maximumDeposit != null && maximumDeposit.isOfType(depositAmount) && + depositAmount.compareTo(maximumDeposit) > 0) { + message = getString(R.string.trade_error_max_limit, + GenericUtils.formatValue(maximumDeposit), + maximumDeposit.type.getSymbol()); + } + } + amountError.setText(message); + } else { // Should not happen, but show a generic error + amountError.setText(R.string.amount_error); + } + amountError.setVisibility(View.VISIBLE); + } else { + amountError.setVisibility(View.GONE); + } + } + updateNextButtonState(); + } + + // TODO implement + private boolean isOutputsValid() { + return true; + } + + + private boolean everythingValid() { + return isOutputsValid() && isAmountValid(sendAmount); } + private void updateNextButtonState() { +// nextButton.setEnabled(everythingValid()); + } + + /** + * Decide if should show errors in the UI. + */ + /** - * Show errors if the user is not typing and the input is not empty and the amount is zero. - * Exception is when the amount is lower than the available balance + * Decide if should show errors in the UI. */ - private boolean shouldShowErrors(boolean isTyping, Coin amountParsed) { - if (amountParsed != null && lastBalance != null && amountParsed.compareTo(lastBalance) >= 0) + private boolean shouldShowErrors(boolean isTyping, Value amountParsed) { + if (amountParsed != null && lastBalance != null && + amountParsed.isOfType(lastBalance) && amountParsed.compareTo(lastBalance) >= 0) { return true; + } if (isTyping) return false; if (amountCalculatorLink.isEmpty()) return false; @@ -454,198 +899,231 @@ private boolean shouldShowErrors(boolean isTyping, Coin amountParsed) { return true; } - private void validateAddress() { - validateAddress(false); - } - - private void validateAddress(boolean isTyping) { - // TODO -// String addressStr = sendToAddressView.getText().toString().trim(); -// -// // If not typing, try to fix address if needed -// if (!isTyping) { -// addressStr = GenericUtils.fixAddress(addressStr); -// // Remove listener before changing input, then add it again. Hack to avoid stack overflow -// sendToAddressView.removeTextChangedListener(receivingAddressListener); -// sendToAddressView.setText(addressStr); -// sendToAddressView.addTextChangedListener(receivingAddressListener); -// } -// -// try { -// if (!addressStr.isEmpty()) { -// address = new Address(type, addressStr); -// } else { -// // empty field should not raise error message -// address = null; -// } -// addressError.setVisibility(View.GONE); -// } catch (final AddressFormatException x) { -// // could not decode address at all -// if (!isTyping) { -// address = null; -// addressError.setText(R.string.address_error); -// addressError.setVisibility(View.VISIBLE); -// } -// } -// -// updateView(); + + //////////////////////////////////////////////////////////////////////////////////////////////// + // Public classes and interfaces + + public interface Listener { + void onMakeTrade(WalletAccount fromAccount, WalletAccount toAccount, Value amount); + void onAbort(); } - private void setAmountForEmptyWallet() { - updateBalance(); -// if (state != State.INPUT || pocket == null || lastBalance == null) return; - if (pocket == null || lastBalance == null) return; - if (lastBalance.isZero()) { - Toast.makeText(getActivity(), R.string.amount_error_not_enough_money, - Toast.LENGTH_LONG).show(); - } else { - amountCalculatorLink.setPrimaryAmount(fromCoinType, lastBalance); - validateAmount(); + //////////////////////////////////////////////////////////////////////////////////////////////// + // Private classes + + + private static class AmountListener implements AmountEditView.Listener { + private final Handler handler; + + private AmountListener(Handler handler) { + this.handler = handler; } - } - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - inflater.inflate(R.menu.trade, menu); - } + @Override + public void changed() { + handler.sendMessage(handler.obtainMessage(VALIDATE_AMOUNT, true)); + } - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.action_swap_coins: - setAmountForEmptyWallet(); - return true; - default: - return super.onOptionsItemSelected(item); + @Override + public void focusChanged(final boolean hasFocus) { + if (!hasFocus) { + handler.sendMessage(handler.obtainMessage(VALIDATE_AMOUNT, false)); + } } } - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - try { - this.activity = (BaseWalletActivity) activity; - this.config = this.activity.getConfiguration(); -// this.loaderManager = getLoaderManager(); - } catch (ClassCastException e) { - throw new ClassCastException(activity.toString() - + " must implement " + BaseWalletActivity.class); + private static class AccountListener extends ThrottlingWalletChangeListener { + private final Handler handler; + + private AccountListener(Handler handler) { + this.handler = handler; } - } - @Override - public void onDetach() { - super.onDetach(); - activity = null; + @Override + public void onThrottledWalletChanged() { + handler.sendEmptyMessage(UPDATE_WALLET); + } } - private abstract class EditViewListener implements View.OnFocusChangeListener, TextWatcher { + /** + * The fragment handler + */ + private static class MyHandler extends WeakHandler { + public MyHandler(TradeSelectFragment referencingObject) { super(referencingObject); } + @Override - public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) { + protected void weakHandleMessage(TradeSelectFragment ref, Message msg) { + switch (msg.what) { + case UPDATE_MARKET: + ref.onMarketUpdate((ShapeShiftMarketInfo) msg.obj); + break; + case UPDATE_MARKET_ERROR: + String errorMessage = ref.getString(R.string.trade_market_info_error, + ref.sourceAccount.getCoinType().getName(), + ref.destinationType.getName()); + Toast.makeText(ref.getActivity(), errorMessage, Toast.LENGTH_LONG).show(); + break; + case UPDATE_WALLET: + ref.onWalletUpdate(); + break; + case VALIDATE_AMOUNT: + ref.validateAmount((Boolean) msg.obj); + break; + } } + } + + private class InitialCheckTask extends AsyncTask { + private Dialogs.ProgressDialogFragment busyDialog; + private ShapeShiftCoins shapeShiftCoins; @Override - public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { + protected void onPreExecute() { + busyDialog = Dialogs.ProgressDialogFragment.newInstance( + getString(R.string.contacting_exchange)); + busyDialog.setCancelable(false); + busyDialog.show(getFragmentManager(), null); } - } - EditViewListener receivingAddressListener = new EditViewListener() { @Override - public void onFocusChange(final View v, final boolean hasFocus) { - if (!hasFocus) { - validateAddress(); + protected Exception doInBackground(Void... params) { + if (!application.isConnected()) { + return new ShapeShiftException("No connection"); + } + try { + shapeShiftCoins = application.getShapeShift().getCoins(); + return null; + } catch (Exception e) { + return e; } } @Override - public void afterTextChanged(final Editable s) { - validateAddress(true); + protected void onPostExecute(Exception error) { + busyDialog.dismissAllowingStateLoss(); + if (error != null) { + log.warn("Could not get ShapeShift coins", error); + showInitialTaskErrorDialog(error.getMessage()); + } else { + if (shapeShiftCoins.isError) { + log.warn("Could not get ShapeShift coins: {}", shapeShiftCoins.errorMessage); + showInitialTaskErrorDialog(shapeShiftCoins.errorMessage); + } else { + updateAvailableCoins(shapeShiftCoins); + startMarketInfoTask(); + } + } + } + } + + /** + * Task to query about the market of a particular pair + */ + private static class MarketInfoTask extends AsyncTask { + final ShapeShift shapeShift; + final String pair; + final Handler handler; + + private MarketInfoTask(Handler handler, ShapeShift shift, String pair) { + this.shapeShift = shift; + this.handler = handler; + this.pair = pair; } - }; - private final AmountEditView.Listener amountsListener = new AmountEditView.Listener() { @Override - public void changed() { - validateAmount(true); + protected ShapeShiftMarketInfo doInBackground(Void... params) { + return getMarketInfoSync(shapeShift, pair); } @Override - public void focusChanged(final boolean hasFocus) { - if (!hasFocus) { - validateAmount(); + protected void onPostExecute(ShapeShiftMarketInfo marketInfo) { + if (marketInfo != null) { + handler.sendMessage(handler.obtainMessage(UPDATE_MARKET, marketInfo)); + } else { + handler.sendEmptyMessage(UPDATE_MARKET_ERROR); } } - }; + } - private void onWalletUpdate() { - updateBalance(); - validateAmount(); + private static class MarketInfoPollTask extends TimerTask { + private final Handler handler; + private final ShapeShift shapeShift; + private String pair; + + MarketInfoPollTask(Handler handler, ShapeShift shapeShift, String pair) { + this.shapeShift = shapeShift; + this.pair = pair; + this.handler = handler; + } + + void updatePair(String newPair) { + this.pair = newPair; + } + + @Override + public void run() { + ShapeShiftMarketInfo marketInfo = getMarketInfoSync(shapeShift, pair); + if (marketInfo != null) { + handler.sendMessage(handler.obtainMessage(UPDATE_MARKET, marketInfo)); + } + } } - private final ThrottlingWalletChangeListener transactionChangeListener = new ThrottlingWalletChangeListener() { + private class AddCoinAndProceedTask extends AddCoinTask { + private Dialogs.ProgressDialogFragment verifyDialog; + + public AddCoinAndProceedTask(CoinType type, Wallet wallet, @Nullable String password) { + super(type, wallet, password); + } + @Override - public void onThrottledWalletChanged() { - handler.sendMessage(handler.obtainMessage(UPDATE_WALLET_CHANGE)); + protected void onPreExecute() { + verifyDialog = Dialogs.ProgressDialogFragment.newInstance( + getResources().getString(R.string.adding_coin_working, type.getName())); + verifyDialog.show(getFragmentManager(), null); } - }; -// private final LoaderCallbacks receivingAddressLoaderCallbacks = new LoaderCallbacks() { -// @Override -// public Loader onCreateLoader(final int id, final Bundle args) { -// final String constraint = args != null ? args.getString("constraint") : null; -// Uri uri = AddressBookProvider.contentUri(activity.getPackageName(), type); -// return new CursorLoader(activity, uri, null, AddressBookProvider.SELECTION_QUERY, -// new String[]{constraint != null ? constraint : ""}, null); -// } -// -// @Override -// public void onLoadFinished(final Loader cursor, final Cursor data) { -// sendToAddressViewAdapter.swapCursor(data); -// } -// -// @Override -// public void onLoaderReset(final Loader cursor) { -// sendToAddressViewAdapter.swapCursor(null); -// } -// }; -// -// private final class ReceivingAddressViewAdapter extends CursorAdapter implements FilterQueryProvider { -// public ReceivingAddressViewAdapter(final Context context) { -// super(context, null, false); -// setFilterQueryProvider(this); -// } -// -// @Override -// public View newView(final Context context, final Cursor cursor, final ViewGroup parent) { -// final LayoutInflater inflater = LayoutInflater.from(context); -// return inflater.inflate(R.layout.address_book_row, parent, false); -// } -// -// @Override -// public void bindView(final View view, final Context context, final Cursor cursor) { -// final String label = cursor.getString(cursor.getColumnIndexOrThrow(AddressBookProvider.KEY_LABEL)); -// final String address = cursor.getString(cursor.getColumnIndexOrThrow(AddressBookProvider.KEY_ADDRESS)); -// -// final ViewGroup viewGroup = (ViewGroup) view; -// final TextView labelView = (TextView) viewGroup.findViewById(R.id.address_book_row_label); -// labelView.setText(label); -// final TextView addressView = (TextView) viewGroup.findViewById(R.id.address_book_row_address); -// addressView.setText(GenericUtils.addressSplitToGroupsMultiline(address)); -// } -// -// @Override -// public CharSequence convertToString(final Cursor cursor) { -// return cursor.getString(cursor.getColumnIndexOrThrow(AddressBookProvider.KEY_ADDRESS)); -// } -// -// @Override -// public Cursor runQuery(final CharSequence constraint) { -// final Bundle args = new Bundle(); -// if (constraint != null) -// args.putString("constraint", constraint.toString()); -// loaderManager.restartLoader(ID_RECEIVING_ADDRESS_LOADER, args, receivingAddressLoaderCallbacks); -// return getCursor(); -// } -// } -} + @Override + protected void onPostExecute(Exception e, WalletAccount newAccount) { + verifyDialog.dismiss(); + if (e != null) { + Toast.makeText(getActivity(), R.string.error_generic, Toast.LENGTH_LONG).show(); + } + destinationAccount = newAccount; + destinationType = newAccount.getCoinType(); + onHandleNext(); + addCoinAndProceedTask = null; + } + } + + private DialogFragment createToAccountAndProceedDialog = new DialogFragment() { + @Override @NonNull + public Dialog onCreateDialog(Bundle savedInstanceState) { + final LayoutInflater inflater = LayoutInflater.from(getActivity()); + final View view = inflater.inflate(R.layout.get_password_dialog, null); + final TextView passwordView = (TextView) view.findViewById(R.id.password); + // If not encrypted, don't ask the password + if (!wallet.isEncrypted()) { + view.findViewById(R.id.password_message).setVisibility(View.GONE); + passwordView.setVisibility(View.GONE); + } + + String title = getString(R.string.adding_coin_confirmation_title, + destinationType.getName()); + + return new DialogBuilder(getActivity()).setTitle(title).setView(view) + .setNegativeButton(R.string.button_cancel, null) + .setPositiveButton(R.string.button_add, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (wallet.isEncrypted()) { + maybeStartAddCoinAndProceedTask(passwordView.getText().toString()); + } else { + maybeStartAddCoinAndProceedTask(null); + } + } + }).create(); + } + }; +} \ No newline at end of file diff --git a/wallet/src/main/java/com/coinomi/wallet/ui/TradeStatusFragment.java b/wallet/src/main/java/com/coinomi/wallet/ui/TradeStatusFragment.java new file mode 100644 index 0000000..82f63b2 --- /dev/null +++ b/wallet/src/main/java/com/coinomi/wallet/ui/TradeStatusFragment.java @@ -0,0 +1,306 @@ +package com.coinomi.wallet.ui; + +import android.app.Activity; +import android.app.AlertDialog; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ProgressBar; +import android.widget.TextView; + +import com.coinomi.core.exchange.shapeshift.ShapeShift; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftException; +import com.coinomi.core.exchange.shapeshift.data.ShapeShiftTxStatus; +import com.coinomi.core.wallet.WalletPocketConnectivity; +import com.coinomi.wallet.Constants; +import com.coinomi.wallet.R; +import com.coinomi.wallet.WalletApplication; +import com.coinomi.wallet.util.Fonts; +import com.coinomi.wallet.util.WeakHandler; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Coin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.Timer; +import java.util.TimerTask; + +import static com.coinomi.core.Preconditions.checkNotNull; + +/** + * @author John L. Jegutanis + */ +public class TradeStatusFragment extends Fragment { + private static final Logger log = LoggerFactory.getLogger(TradeStatusFragment.class); + + private static final int UPDATE_STATUS = 0; + private static final int ERROR_MESSAGE = 1; + private static final long POLLING_MS = 3000; + + private Listener mListener; + private TextView depositIcon; + private ProgressBar depositProgress; + private TextView depositText; + private TextView exchangeIcon; + private ProgressBar exchangeProgress; + private TextView exchangeText; + private TextView errorIcon; + private TextView errorText; + private Button viewTransaction; + private Button emailReceipt; + + private Address deposit; + private ShapeShiftTxStatus status; + private StatusPollTask pollTask; + private final Handler handler = new MyHandler(this); + private Timer timer; + private WalletApplication application; + + private static class StatusPollTask extends TimerTask { + private final ShapeShift shapeShift; + private final Address depositAddress; + private final Handler handler; + + private StatusPollTask(ShapeShift shapeShift, Address depositAddress, Handler handler) { + this.shapeShift = shapeShift; + this.depositAddress = depositAddress; + this.handler = handler; + } + + @Override + public void run() { + for (int tries = 3; tries > 0; tries--) { + try { + log.info("Polling status for deposit: {}", depositAddress); + ShapeShiftTxStatus newStatus = shapeShift.getTxStatus(depositAddress); + handler.sendMessage(handler.obtainMessage(UPDATE_STATUS, newStatus)); + break; + } catch (ShapeShiftException e) { + log.warn("Error occurred while polling", e); + handler.sendMessage(handler.obtainMessage(ERROR_MESSAGE, e.getMessage())); + break; + } catch (IOException e) { + /* ignore and retry */ + } + } + } + } + + private static class MyHandler extends WeakHandler { + public MyHandler(TradeStatusFragment ref) { super(ref); } + + @Override + protected void weakHandleMessage(TradeStatusFragment ref, Message msg) { + switch (msg.what) { + case UPDATE_STATUS: + ref.status = (ShapeShiftTxStatus) msg.obj; + ref.updateView(); + break; + case ERROR_MESSAGE: + ref.errorIcon.setVisibility(View.VISIBLE); + ref.errorText.setVisibility(View.VISIBLE); + ref.errorText.setText(ref.getString(R.string.trade_status_failed, msg.obj)); + ref.stopPolling(); + break; + } + } + } + + public static TradeStatusFragment newInstance(Address deposit) { + TradeStatusFragment fragment = new TradeStatusFragment(); + Bundle args = new Bundle(); + args.putSerializable(Constants.ARG_ADDRESS, deposit); + fragment.setArguments(args); + return fragment; + } + + public TradeStatusFragment() {} + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (getArguments() != null) { + deposit = (Address) getArguments().getSerializable(Constants.ARG_ADDRESS); + } + checkNotNull(deposit); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_trade_status, container, false); + + depositIcon = (TextView) view.findViewById(R.id.trade_deposit_status_icon); + depositProgress = (ProgressBar) view.findViewById(R.id.trade_deposit_status_progress); + depositText = (TextView) view.findViewById(R.id.trade_deposit_status_text); + exchangeIcon = (TextView) view.findViewById(R.id.trade_exchange_status_icon); + exchangeProgress = (ProgressBar) view.findViewById(R.id.trade_exchange_status_progress); + exchangeText = (TextView) view.findViewById(R.id.trade_exchange_status_text); + errorIcon = (TextView) view.findViewById(R.id.trade_error_status_icon); + errorText = (TextView) view.findViewById(R.id.trade_error_status_text); + Fonts.setTypeface(depositIcon, Fonts.Font.COINOMI_FONT_ICONS); + Fonts.setTypeface(exchangeIcon, Fonts.Font.COINOMI_FONT_ICONS); + Fonts.setTypeface(errorIcon, Fonts.Font.COINOMI_FONT_ICONS); + + viewTransaction = (Button) view.findViewById(R.id.trade_view_transaction); + emailReceipt = (Button) view.findViewById(R.id.trade_email_receipt); + + view.findViewById(R.id.button_finish).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onFinishPressed(); + } + }); + + depositIcon.setVisibility(View.GONE); + depositProgress.setVisibility(View.VISIBLE); + depositText.setVisibility(View.VISIBLE); + depositText.setText(R.string.trade_status_waiting_deposit); + exchangeIcon.setVisibility(View.GONE); + exchangeProgress.setVisibility(View.GONE); + exchangeText.setVisibility(View.GONE); + errorIcon.setVisibility(View.GONE); + errorText.setVisibility(View.GONE); + viewTransaction.setVisibility(View.GONE); + emailReceipt.setVisibility(View.GONE); + + view.findViewById(R.id.powered_by_shapeshift).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new AlertDialog.Builder(getActivity()) + .setTitle(R.string.about_shapeshift_title) + .setMessage(R.string.about_shapeshift_message) + .setPositiveButton(R.string.button_ok, null) + .create().show(); + } + }); + + return view; + } + + @Override + public void onPause() { + super.onPause(); + stopPolling(); + } + + @Override + public void onResume() { + super.onResume(); + startPolling(); + } + + private void startPolling() { + if (timer == null) { + ShapeShift shapeShift = application.getShapeShift(); + pollTask = new StatusPollTask(shapeShift, deposit, handler); + timer = new Timer(); + timer.schedule(pollTask, 0, POLLING_MS); + } + } + + private void stopPolling() { + if (timer != null) { + timer.cancel(); + timer.purge(); + timer = null; + pollTask.cancel(); + pollTask = null; + } + } + + public void onFinishPressed() { + stopPolling(); + if (mListener != null) { + mListener.onFinish(); + } + } + + private void updateView() { + if (status != null && status.status != null) { + switch (status.status) { + case NO_DEPOSITS: + depositIcon.setVisibility(View.GONE); + depositProgress.setVisibility(View.VISIBLE); + depositText.setVisibility(View.VISIBLE); + depositText.setText(R.string.trade_status_waiting_deposit); + exchangeIcon.setVisibility(View.GONE); + exchangeProgress.setVisibility(View.GONE); + exchangeText.setVisibility(View.GONE); + errorIcon.setVisibility(View.GONE); + errorText.setVisibility(View.GONE); + viewTransaction.setVisibility(View.GONE); + emailReceipt.setVisibility(View.GONE); + break; + case RECEIVED: + depositIcon.setVisibility(View.VISIBLE); + depositProgress.setVisibility(View.GONE); + depositText.setVisibility(View.VISIBLE); + depositText.setText(getString(R.string.trade_status_received_deposit, + status.incomingValue)); + exchangeIcon.setVisibility(View.GONE); + exchangeProgress.setVisibility(View.VISIBLE); + exchangeText.setVisibility(View.VISIBLE); + exchangeText.setText(R.string.trade_status_waiting_trade); + errorIcon.setVisibility(View.GONE); + errorText.setVisibility(View.GONE); + viewTransaction.setVisibility(View.GONE); + emailReceipt.setVisibility(View.GONE); + break; + case COMPLETE: + depositIcon.setVisibility(View.VISIBLE); + depositProgress.setVisibility(View.GONE); + depositText.setVisibility(View.VISIBLE); + depositText.setText(getString(R.string.trade_status_received_deposit, + status.incomingValue)); + exchangeIcon.setVisibility(View.VISIBLE); + exchangeProgress.setVisibility(View.GONE); + exchangeText.setVisibility(View.VISIBLE); + exchangeText.setText(getString(R.string.trade_status_complete, + status.outgoingValue)); + errorIcon.setVisibility(View.GONE); + errorText.setVisibility(View.GONE); + viewTransaction.setVisibility(View.GONE); // TODO enable + emailReceipt.setVisibility(View.GONE); + stopPolling(); + break; + case FAILED: + errorIcon.setVisibility(View.VISIBLE); + errorText.setVisibility(View.VISIBLE); + errorText.setText(getString(R.string.trade_status_failed, status.errorMessage)); + stopPolling(); + } + } + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + try { + mListener = (Listener) activity; + application = (WalletApplication) activity.getApplication(); + } catch (ClassCastException e) { + throw new ClassCastException(activity.toString() + + " must implement " + TradeStatusFragment.Listener.class); + } + } + + @Override + public void onDetach() { + super.onDetach(); + mListener = null; + } + + public interface Listener { + public void onFinish(); + } + +} diff --git a/wallet/src/main/java/com/coinomi/wallet/ui/TransactionAmountVisualizerAdapter.java b/wallet/src/main/java/com/coinomi/wallet/ui/TransactionAmountVisualizerAdapter.java index 7b1e54a..d4de5ac 100644 --- a/wallet/src/main/java/com/coinomi/wallet/ui/TransactionAmountVisualizerAdapter.java +++ b/wallet/src/main/java/com/coinomi/wallet/ui/TransactionAmountVisualizerAdapter.java @@ -94,6 +94,9 @@ public long getItemId(int position) { public View getView(int position, View row, ViewGroup parent) { if (row == null) { row = inflater.inflate(R.layout.transaction_details_output_row, null); + + ((SendOutput) row).setSendLabel(context.getString(R.string.sent)); + ((SendOutput) row).setReceiveLabel(context.getString(R.string.received)); } final SendOutput output = (SendOutput) row; diff --git a/wallet/src/main/java/com/coinomi/wallet/ui/TransactionDetailsFragment.java b/wallet/src/main/java/com/coinomi/wallet/ui/TransactionDetailsFragment.java index 29e28e9..bde754f 100644 --- a/wallet/src/main/java/com/coinomi/wallet/ui/TransactionDetailsFragment.java +++ b/wallet/src/main/java/com/coinomi/wallet/ui/TransactionDetailsFragment.java @@ -7,12 +7,7 @@ import android.os.Handler; import android.os.Message; import android.support.v4.app.Fragment; -import android.support.v7.app.ActionBarActivity; -import android.support.v7.view.ActionMode; import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; @@ -21,9 +16,7 @@ import android.widget.Toast; import com.coinomi.core.coins.CoinType; -import com.coinomi.core.util.GenericUtils; import com.coinomi.core.wallet.WalletPocketHD; -import com.coinomi.wallet.AddressBookProvider; import com.coinomi.wallet.Constants; import com.coinomi.wallet.R; import com.coinomi.wallet.WalletApplication; @@ -57,7 +50,6 @@ public class TransactionDetailsFragment extends Fragment { private TextView txStatusView; private TextView txIdView; private TextView blockExplorerLink; - private TextView sendDirectionView; private final Handler handler = new MyHandler(this); private static class MyHandler extends WeakHandler { @@ -103,7 +95,6 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, View header = inflater.inflate(R.layout.fragment_transaction_details_header, null); outputRows.addHeaderView(header, null, false); txStatusView = (TextView) header.findViewById(R.id.tx_status); - sendDirectionView = (TextView) header.findViewById(R.id.send_direction); // Footer View footer = inflater.inflate(R.layout.fragment_transaction_details_footer, null); @@ -182,8 +173,6 @@ private void showTxDetails(WalletPocketHD pocket, Transaction tx) { txStatusText = getString(R.string.status_unknown); } txStatusView.setText(txStatusText); - boolean isSending = tx.getValue(pocket).signum() < 0; - sendDirectionView.setText(isSending ? R.string.sent : R.string.received); adapter.setTransaction(tx); txIdView.setText(tx.getHashAsString()); setupBlockExplorerLink(pocket.getCoinType(), tx.getHashAsString()); diff --git a/wallet/src/main/java/com/coinomi/wallet/ui/WalletActivity.java b/wallet/src/main/java/com/coinomi/wallet/ui/WalletActivity.java index 3fa8daf..aa1914b 100644 --- a/wallet/src/main/java/com/coinomi/wallet/ui/WalletActivity.java +++ b/wallet/src/main/java/com/coinomi/wallet/ui/WalletActivity.java @@ -120,7 +120,7 @@ protected void onCreate(Bundle savedInstanceState) { new AlertDialog.Builder(this) .setTitle(R.string.test_wallet) .setMessage(R.string.test_wallet_message) - .setNeutralButton(R.string.button_ok, null) + .setPositiveButton(R.string.button_ok, null) .create().show(); } diff --git a/wallet/src/main/java/com/coinomi/wallet/ui/adaptors/AvailableAccountsAdaptor.java b/wallet/src/main/java/com/coinomi/wallet/ui/adaptors/AvailableAccountsAdaptor.java index e19194f..e499ee6 100644 --- a/wallet/src/main/java/com/coinomi/wallet/ui/adaptors/AvailableAccountsAdaptor.java +++ b/wallet/src/main/java/com/coinomi/wallet/ui/adaptors/AvailableAccountsAdaptor.java @@ -7,10 +7,13 @@ import com.coinomi.core.coins.CoinType; import com.coinomi.core.wallet.WalletAccount; +import com.coinomi.core.wallet.WalletPocketHD; import com.coinomi.wallet.ui.widget.NavDrawerItemView; import com.coinomi.wallet.util.WalletUtils; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import java.util.ArrayList; import java.util.List; /** @@ -19,40 +22,96 @@ public class AvailableAccountsAdaptor extends BaseAdapter { private final Context context; - private final List entries; + private List entries; - private static class Entry { - public int iconRes; - public String title; + public static class Entry { + final public int iconRes; + final public String title; + final public Object accountOrCoinType; public Entry(WalletAccount account) { iconRes = WalletUtils.getIconRes(account); title = WalletUtils.getDescriptionOrCoinName(account); + accountOrCoinType = account; } public Entry(CoinType type) { iconRes = WalletUtils.getIconRes(type); title = type.getName(); + accountOrCoinType = type; + } + + // Used for search + private Entry(Object accountOrCoinType) { + iconRes = -1; + title = null; + this.accountOrCoinType = accountOrCoinType; + } + + @Override + public boolean equals(Object o) { +// return accountOrCoinType.getClass().isInstance(o) && accountOrCoinType.equals(o); + boolean result = accountOrCoinType.getClass().isInstance(o); + result = result && accountOrCoinType.equals(o); + return result; } } - public AvailableAccountsAdaptor(final Context context, final List accountsOrCoinTypes) { + public AvailableAccountsAdaptor(final Context context) { this.context = context; - this.entries = createEntries(accountsOrCoinTypes); + entries = ImmutableList.of(); + } + + /** + * Create an adaptor that contains all accounts that are in the validTypes list. + * + * If includeTypes is true, it will also include any coin type that is in not in accounts but is + * in the validTypes. + */ + public AvailableAccountsAdaptor(final Context context, final List accounts, + final List validTypes, final boolean includeTypes) { + this.context = context; + entries = createEntries(accounts, validTypes, includeTypes); + } + + public int getAccountOrTypePosition(Object accountOrCoinType) { + return entries.indexOf(accountOrCoinType); + } + + /** + * Update the adaptor to include all accounts that are in the validTypes list. + * + * If includeTypes is true, it will also include any coin type that is in not in accounts but is + * in the validTypes. + */ + public void update(final List accounts, final List validTypes, + final boolean includeTypes) { + entries = createEntries(accounts, validTypes, includeTypes); + notifyDataSetChanged(); } - private static List createEntries(List list) { - ImmutableList.Builder builder = ImmutableList.builder(); - for (Object o : list) { - if (o instanceof CoinType) { - builder.add(new Entry((CoinType) o)); - } else if (o instanceof WalletAccount) { - builder.add(new Entry((WalletAccount) o)); + private static ImmutableList createEntries(final List accounts, + final List validTypes, + final boolean includeTypes) { + final ArrayList typesToAdd = Lists.newArrayList(validTypes); + + final ImmutableList.Builder listBuilder = ImmutableList.builder(); + for (WalletAccount account : accounts) { + if (validTypes.contains(account.getCoinType())) { + listBuilder.add(new Entry(account)); + // Don't add this type as we just added the account for this type + typesToAdd.remove(account.getCoinType()); } } - return builder.build(); - } + if (includeTypes) { + for (CoinType type : typesToAdd) { + listBuilder.add(new Entry(type)); + } + } + + return listBuilder.build(); + } @Override public int getCount() { @@ -60,7 +119,7 @@ public int getCount() { } @Override - public Object getItem(int position) { + public Entry getItem(int position) { return entries.get(position); } @@ -75,7 +134,7 @@ public View getView(int position, View convertView, ViewGroup parent) { convertView = new NavDrawerItemView(context); } - Entry entry = (Entry) getItem(position); + final Entry entry = getItem(position); ((NavDrawerItemView) convertView).setData(entry.title, entry.iconRes); return convertView; diff --git a/wallet/src/main/java/com/coinomi/wallet/ui/widget/AmountEditView.java b/wallet/src/main/java/com/coinomi/wallet/ui/widget/AmountEditView.java index d7bfd3e..90c0a3d 100644 --- a/wallet/src/main/java/com/coinomi/wallet/ui/widget/AmountEditView.java +++ b/wallet/src/main/java/com/coinomi/wallet/ui/widget/AmountEditView.java @@ -34,7 +34,7 @@ public class AmountEditView extends RelativeLayout { @Nullable private ValueType type; private MonetaryFormat inputFormat; private boolean amountSigned = false; - private Value hint; + @Nullable private Value hint; private MonetaryFormat hintFormat = new MonetaryFormat().noCode(); public static interface Listener { @@ -55,6 +55,13 @@ public AmountEditView(Context context, AttributeSet attrs) { textView.setOnFocusChangeListener(textViewListener); } + public void reset() { + textView.setText(null); + symbol.setText(null); + type = null; + hint = null; + } + public void setFormat(final MonetaryFormat inputFormat) { this.inputFormat = inputFormat.noCode(); hintFormat = inputFormat.noCode(); diff --git a/wallet/src/main/java/com/coinomi/wallet/ui/widget/SendOutput.java b/wallet/src/main/java/com/coinomi/wallet/ui/widget/SendOutput.java index 81a964d..f00b199 100644 --- a/wallet/src/main/java/com/coinomi/wallet/ui/widget/SendOutput.java +++ b/wallet/src/main/java/com/coinomi/wallet/ui/widget/SendOutput.java @@ -11,13 +11,14 @@ import com.coinomi.core.util.GenericUtils; import com.coinomi.wallet.R; -import com.coinomi.wallet.util.Fonts; + +import javax.annotation.Nullable; /** * @author John L. Jegutanis */ public class SendOutput extends LinearLayout { - private TextView sendType; + private TextView sendTypeText; private TextView amount; private TextView symbol; private TextView amountLocal; @@ -28,6 +29,9 @@ public class SendOutput extends LinearLayout { private String address; private String label; private boolean isSending; + private String sendLabel; + private String receiveLabel; + private String feeLabel; public SendOutput(Context context) { super(context); @@ -52,7 +56,7 @@ public SendOutput(Context context, AttributeSet attrs) { private void inflateView(Context context) { LayoutInflater.from(context).inflate(R.layout.transaction_output, this, true); - sendType = (TextView) findViewById(R.id.send_output_type); + sendTypeText = (TextView) findViewById(R.id.send_output_type_text); amount = (TextView) findViewById(R.id.amount); symbol = (TextView) findViewById(R.id.symbol); amountLocal = (TextView) findViewById(R.id.local_amount); @@ -62,6 +66,8 @@ private void inflateView(Context context) { amountLocal.setVisibility(GONE); symbolLocal.setVisibility(GONE); + addressLabelView.setVisibility(View.GONE); + addressView.setVisibility(View.GONE); } public void setAmount(String amount) { @@ -98,11 +104,14 @@ private void updateView() { } else { addressView.setVisibility(View.GONE); } - } else { + } else if (address != null) { addressLabelView.setText(GenericUtils.addressSplitToGroups(address)); addressLabelView.setTypeface(Typeface.MONOSPACE); addressLabelView.setVisibility(View.VISIBLE); addressView.setVisibility(View.GONE); + } else { + addressLabelView.setVisibility(View.GONE); + addressView.setVisibility(View.GONE); } } @@ -113,28 +122,73 @@ public void setLabel(String label) { public void setIsFee(boolean isFee) { if (isFee) { - sendType.setText(R.string.fee); + setTypeLabel(getFeeLabel()); addressLabelView.setVisibility(GONE); addressView.setVisibility(GONE); } else { - if (!sendType.isInEditMode()) { // If not displayed within a developer tool - updateIcon(); - } + updateDirectionLabels(); } } - private void updateIcon() { - Fonts.setTypeface(sendType, Fonts.Font.COINOMI_FONT_ICONS); + private void updateDirectionLabels() { if (isSending) { - sendType.setText(getResources().getString(R.string.font_icon_send_coins)); + setTypeLabel(getSendLabel()); + } else { + setTypeLabel(getReceiveLabel()); + } + } + + private void setTypeLabel(String typeLabel) { + if (typeLabel.isEmpty()) { + sendTypeText.setVisibility(GONE); + } else { + sendTypeText.setVisibility(VISIBLE); + sendTypeText.setText(typeLabel); + } + } + + private String getSendLabel() { + if (sendLabel == null) { + return getResources().getString(R.string.send); + } else { + return sendLabel; + } + } + + private String getReceiveLabel() { + if (receiveLabel == null) { + return getResources().getString(R.string.receive); + } else { + return receiveLabel; + } + } + + private String getFeeLabel() { + if (feeLabel == null) { + return getResources().getString(R.string.fee); } else { - sendType.setText(getResources().getString(R.string.font_icon_receive_coins)); + return feeLabel; } } + public void setSendLabel(String sendLabel) { + this.sendLabel = sendLabel; + updateDirectionLabels(); + } + + public void setReceiveLabel(String receiveLabel) { + this.receiveLabel = receiveLabel; + updateDirectionLabels(); + } + + public void setFeeLabel(String feeLabel) { + this.feeLabel = feeLabel; + updateDirectionLabels(); + } + public void setSending(boolean isSending) { this.isSending = isSending; - updateIcon(); + updateDirectionLabels(); } public void setLabelAndAddress(String label, String address) { @@ -142,4 +196,10 @@ public void setLabelAndAddress(String label, String address) { this.address = address; updateView(); } + + public void hideLabelAndAddress() { + this.label = null; + this.address = null; + updateView(); + } } diff --git a/wallet/src/main/java/com/coinomi/wallet/ui/widget/TransactionAmountVisualizer.java b/wallet/src/main/java/com/coinomi/wallet/ui/widget/TransactionAmountVisualizer.java index 44a2be2..689cdb1 100644 --- a/wallet/src/main/java/com/coinomi/wallet/ui/widget/TransactionAmountVisualizer.java +++ b/wallet/src/main/java/com/coinomi/wallet/ui/widget/TransactionAmountVisualizer.java @@ -13,11 +13,14 @@ import com.coinomi.core.wallet.WalletPocketHD; import com.coinomi.wallet.AddressBookProvider; import com.coinomi.wallet.R; +import com.google.common.collect.ImmutableList; import org.bitcoinj.core.Coin; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionOutput; +import java.util.List; + import static com.coinomi.core.Preconditions.checkState; /** @@ -43,6 +46,11 @@ public TransactionAmountVisualizer(Context context, AttributeSet attrs) { output.setVisibility(View.GONE); fee = (SendOutput) findViewById(R.id.transaction_fee); fee.setVisibility(View.GONE); + + if (isInEditMode()) { + output.setVisibility(View.VISIBLE); + fee.setVisibility(View.VISIBLE); + } } public void setTransaction(WalletPocketHD pocket, Transaction tx) { @@ -100,8 +108,20 @@ public void setExchangeRate(ExchangeRate rate) { } } + /** + * Hide the output address and label. Useful when we are exchanging, where the send address is + * not important to the user. + */ + public void hideAddresses() { + output.hideLabelAndAddress(); + } + public void resetLabels() { output.setLabelAndAddress( AddressBookProvider.resolveLabel(getContext(), type, address), address); } + + public List getOutputs() { + return ImmutableList.of(output); + } } diff --git a/wallet/src/main/java/com/coinomi/wallet/util/ThrottlingWalletChangeListener.java b/wallet/src/main/java/com/coinomi/wallet/util/ThrottlingWalletChangeListener.java index f1afdf7..1ce4942 100644 --- a/wallet/src/main/java/com/coinomi/wallet/util/ThrottlingWalletChangeListener.java +++ b/wallet/src/main/java/com/coinomi/wallet/util/ThrottlingWalletChangeListener.java @@ -23,18 +23,17 @@ import android.os.Handler; +import com.coinomi.core.coins.Value; import com.coinomi.core.wallet.WalletAccount; -import com.coinomi.core.wallet.WalletPocketHD; +import com.coinomi.core.wallet.WalletAccountEventListener; import com.coinomi.core.wallet.WalletPocketConnectivity; -import com.coinomi.core.wallet.WalletPocketEventListener; -import org.bitcoinj.core.Coin; import org.bitcoinj.core.Transaction; /** * @author Andreas Schildbach */ -public abstract class ThrottlingWalletChangeListener implements WalletPocketEventListener +public abstract class ThrottlingWalletChangeListener implements WalletAccountEventListener { private final long throttleMs; private final boolean coinsRelevant; @@ -105,7 +104,7 @@ public void removeCallbacks() public abstract void onThrottledWalletChanged(); @Override - public void onNewBalance(Coin newBalance, Coin pendingAmount) { + public void onNewBalance(Value newBalance, Value pendingAmount) { if (coinsRelevant) relevant.set(true); } diff --git a/wallet/src/main/java/com/coinomi/wallet/util/WalletUtils.java b/wallet/src/main/java/com/coinomi/wallet/util/WalletUtils.java index 5112be7..86baf26 100644 --- a/wallet/src/main/java/com/coinomi/wallet/util/WalletUtils.java +++ b/wallet/src/main/java/com/coinomi/wallet/util/WalletUtils.java @@ -40,7 +40,6 @@ import java.util.ArrayList; import java.util.Currency; -import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.regex.Matcher; @@ -56,7 +55,6 @@ * @author Andreas Schildbach */ public class WalletUtils { - public static String getDescriptionOrCoinName(WalletAccount account) { return account.getDescription() != null ? account.getDescription() : account.getCoinType().getName(); } diff --git a/wallet/src/main/java/com/coinomi/wallet/util/WeakHandler.java b/wallet/src/main/java/com/coinomi/wallet/util/WeakHandler.java index bce94af..46909e7 100644 --- a/wallet/src/main/java/com/coinomi/wallet/util/WeakHandler.java +++ b/wallet/src/main/java/com/coinomi/wallet/util/WeakHandler.java @@ -2,6 +2,7 @@ import android.os.Handler; import android.os.Message; +import android.support.v4.app.Fragment; import java.lang.ref.WeakReference; @@ -18,6 +19,12 @@ public WeakHandler(T ref) { public void handleMessage(Message msg) { T ref = reference.get(); if (ref != null) { + // Do not call if is a detached fragment + if (ref instanceof Fragment) { + Fragment f = (Fragment) ref; + if (f.isRemoving() || f.isDetached() || f.getActivity() == null) return; + } + weakHandleMessage(ref, msg); } } diff --git a/wallet/src/main/res/drawable-hdpi/refresh_menu.png b/wallet/src/main/res/drawable-hdpi/refresh_menu.png new file mode 100644 index 0000000..6ca5930 Binary files /dev/null and b/wallet/src/main/res/drawable-hdpi/refresh_menu.png differ diff --git a/wallet/src/main/res/drawable-hdpi/shapeshift.png b/wallet/src/main/res/drawable-hdpi/shapeshift.png new file mode 100644 index 0000000..8b9c084 Binary files /dev/null and b/wallet/src/main/res/drawable-hdpi/shapeshift.png differ diff --git a/wallet/src/main/res/drawable-ldpi/refresh_menu.png b/wallet/src/main/res/drawable-ldpi/refresh_menu.png new file mode 100644 index 0000000..7ed384a Binary files /dev/null and b/wallet/src/main/res/drawable-ldpi/refresh_menu.png differ diff --git a/wallet/src/main/res/drawable-ldpi/shapeshift.png b/wallet/src/main/res/drawable-ldpi/shapeshift.png new file mode 100644 index 0000000..6412768 Binary files /dev/null and b/wallet/src/main/res/drawable-ldpi/shapeshift.png differ diff --git a/wallet/src/main/res/drawable-mdpi/refresh_menu.png b/wallet/src/main/res/drawable-mdpi/refresh_menu.png new file mode 100644 index 0000000..911feb7 Binary files /dev/null and b/wallet/src/main/res/drawable-mdpi/refresh_menu.png differ diff --git a/wallet/src/main/res/drawable-mdpi/shapeshift.png b/wallet/src/main/res/drawable-mdpi/shapeshift.png new file mode 100644 index 0000000..11093a1 Binary files /dev/null and b/wallet/src/main/res/drawable-mdpi/shapeshift.png differ diff --git a/wallet/src/main/res/drawable-xhdpi/refresh_menu.png b/wallet/src/main/res/drawable-xhdpi/refresh_menu.png new file mode 100644 index 0000000..f94c635 Binary files /dev/null and b/wallet/src/main/res/drawable-xhdpi/refresh_menu.png differ diff --git a/wallet/src/main/res/drawable-xhdpi/shapeshift.png b/wallet/src/main/res/drawable-xhdpi/shapeshift.png new file mode 100644 index 0000000..b778563 Binary files /dev/null and b/wallet/src/main/res/drawable-xhdpi/shapeshift.png differ diff --git a/wallet/src/main/res/drawable-xxhdpi/refresh_menu.png b/wallet/src/main/res/drawable-xxhdpi/refresh_menu.png new file mode 100644 index 0000000..e657d56 Binary files /dev/null and b/wallet/src/main/res/drawable-xxhdpi/refresh_menu.png differ diff --git a/wallet/src/main/res/drawable-xxhdpi/shapeshift.png b/wallet/src/main/res/drawable-xxhdpi/shapeshift.png new file mode 100644 index 0000000..6e3aa1e Binary files /dev/null and b/wallet/src/main/res/drawable-xxhdpi/shapeshift.png differ diff --git a/wallet/src/main/res/drawable/generic_green_circle.xml b/wallet/src/main/res/drawable/generic_green_circle.xml new file mode 100644 index 0000000..adff707 --- /dev/null +++ b/wallet/src/main/res/drawable/generic_green_circle.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/wallet/src/main/res/drawable/generic_red_circle.xml b/wallet/src/main/res/drawable/generic_red_circle.xml new file mode 100644 index 0000000..0d7f37d --- /dev/null +++ b/wallet/src/main/res/drawable/generic_red_circle.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/wallet/src/main/res/layout/fragment_make_transaction.xml b/wallet/src/main/res/layout/fragment_make_transaction.xml index df949bd..6073ac7 100644 --- a/wallet/src/main/res/layout/fragment_make_transaction.xml +++ b/wallet/src/main/res/layout/fragment_make_transaction.xml @@ -3,7 +3,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context="com.coinomi.wallet.ui.SignTransactionFragment"> + tools:context="com.coinomi.wallet.ui.MakeTransactionFragment"> - + + + android:layout_weight="0.8" + android:paddingTop="@dimen/activity_vertical_margin" + android:paddingBottom="@dimen/activity_vertical_margin" + android:gravity="center_vertical" + android:orientation="vertical"> + + + + + + -