Skip to content

Commit

Permalink
Multi coins (#6)
Browse files Browse the repository at this point in the history
* add multi coins support

* add tests for ltc

* add default method

* update version
  • Loading branch information
vladmelnyk authored Oct 3, 2019
1 parent 0dd0982 commit 7402e24
Show file tree
Hide file tree
Showing 15 changed files with 386 additions and 207 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
group 'sd.fomin'
version '1.6.0'
version '1.7.0'

apply plugin: 'java'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import sd.fomin.gerbera.boot.processor.annotation.BuildingProcessor;
import sd.fomin.gerbera.boot.processor.annotation.CommandAliases;
import sd.fomin.gerbera.transaction.TransactionBuilder;
import sd.fomin.gerbera.types.Coin;

import java.util.Arrays;
import java.util.List;
Expand All @@ -17,10 +18,11 @@ public class InitProcessor extends Processor {
public TransactionBuilder processBuilder(TransactionBuilder builder, List<String> arguments) {

String network = arguments.get(0);
Coin coin = Coin.valueOf(arguments.get(1));
if ("mainnet".equalsIgnoreCase(network)) {
return TransactionBuilder.create(true);
return TransactionBuilder.create(true, coin);
} else if ("testnet".equalsIgnoreCase(network)) {
return TransactionBuilder.create(false);
return TransactionBuilder.create(false, coin);
} else {
throw new RuntimeException("First argument must be 'mainnet' or 'testnet'");
}
Expand Down
43 changes: 34 additions & 9 deletions src/main/java/sd/fomin/gerbera/crypto/PrivateKey.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
package sd.fomin.gerbera.crypto;

import sd.fomin.gerbera.util.ByteBuffer;
import sd.fomin.gerbera.types.Coin;
import sd.fomin.gerbera.util.ApplicationRandom;
import sd.fomin.gerbera.util.Base58CheckUtils;
import sd.fomin.gerbera.util.ByteBuffer;
import sd.fomin.gerbera.util.HexUtils;
import sd.fomin.gerbera.util.ApplicationRandom;

import java.math.BigInteger;
import java.util.Arrays;
import java.util.List;

import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static sd.fomin.gerbera.crypto.Numbers.*;
import static sd.fomin.gerbera.crypto.Numbers.N;
import static sd.fomin.gerbera.crypto.Numbers.S_MAX;

public class PrivateKey {

Expand All @@ -23,12 +25,22 @@ private PrivateKey(BigInteger key, boolean compressed) {
this.compressed = compressed;
}

public static PrivateKey ofWif(boolean mainNet, String wif) {
validateWifFormat(mainNet, wif);
public static PrivateKey ofWif(boolean mainNet, Coin coin, String wif) {
validateWifFormat(mainNet, coin, wif);

boolean compressed = false;

byte prefix = mainNet ? (byte) 0x80 : (byte) 0xEF;
byte prefix;
switch (coin) {
case BTC:
prefix = mainNet ? (byte) 0x80 : (byte) 0xEF;
break;
case LTC:
prefix = mainNet ? (byte) 0xB0 : (byte) 0xEF;
break;
default:
throw new IllegalStateException("Unexpected value: " + coin);
}
byte[] decoded = Base58CheckUtils.decode(wif);
if (decoded[0] != prefix) {
throw new IllegalArgumentException("Decoded WIF must start with 0x" + HexUtils.asString(prefix) + " byte");
Expand All @@ -50,9 +62,22 @@ public static PrivateKey ofWif(boolean mainNet, String wif) {
return new PrivateKey(new BigInteger(HexUtils.asString(pkBytes), 16), compressed);
}

private static void validateWifFormat(boolean mainNet, String wif) {
List<Character> prefixWif = singletonList(mainNet ? '5' : '9');
List<Character> prefixWifComp = mainNet ? asList('K', 'L') : singletonList('c');
private static void validateWifFormat(boolean mainNet, Coin coin, String wif) {
List<Character> prefixWif;
List<Character> prefixWifComp;
switch (coin) {
case BTC:
prefixWif = singletonList(mainNet ? '5' : '9');
prefixWifComp = mainNet ? asList('K', 'L') : singletonList('c');
break;
case LTC:
prefixWif = singletonList(mainNet ? '5' : '9');
prefixWifComp = mainNet ? singletonList('T') : singletonList('c');
break;
default:
throw new IllegalStateException("Unexpected value: " + coin);
}

char prefix = wif.charAt(0);

if (!prefixWif.contains(prefix) && !prefixWifComp.contains(prefix)) {
Expand Down
9 changes: 5 additions & 4 deletions src/main/java/sd/fomin/gerbera/transaction/Input.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import sd.fomin.gerbera.constant.ErrorMessages;
import sd.fomin.gerbera.crypto.PrivateKey;
import sd.fomin.gerbera.types.Coin;
import sd.fomin.gerbera.types.OpSize;
import sd.fomin.gerbera.types.UInt;
import sd.fomin.gerbera.types.VarInt;
Expand All @@ -23,27 +24,27 @@ class Input {
private final PrivateKey privateKey;
private final UInt sequence;

Input(boolean mainNet, String transaction, int index, String lock, long satoshi, String wif) {
Input(boolean mainNet, Coin coin, String transaction, int index, String lock, long satoshi, String wif) {
validateInputData(transaction, index, lock, satoshi, wif);

this.transaction = transaction;
this.index = index;
this.lock = lock;
this.satoshi = satoshi;
this.wif = wif;
this.privateKey = PrivateKey.ofWif(mainNet, wif);
this.privateKey = PrivateKey.ofWif(mainNet, coin, wif);
this.sequence = SEQUENCE_IRREPLACEABLE;
}

Input(boolean mainNet, String transaction, int index, String lock, long satoshi, String wif, Boolean replaceable) {
Input(boolean mainNet, Coin coin, String transaction, int index, String lock, long satoshi, String wif, Boolean replaceable) {
validateInputData(transaction, index, lock, satoshi, wif);

this.transaction = transaction;
this.index = index;
this.lock = lock;
this.satoshi = satoshi;
this.wif = wif;
this.privateKey = PrivateKey.ofWif(mainNet, wif);
this.privateKey = PrivateKey.ofWif(mainNet, coin, wif);
if (replaceable) {
sequence = SEQUENCE_REPLACEABLE;
} else {
Expand Down
41 changes: 28 additions & 13 deletions src/main/java/sd/fomin/gerbera/transaction/RegularOutput.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package sd.fomin.gerbera.transaction;

import sd.fomin.gerbera.constant.ErrorMessages;
import sd.fomin.gerbera.types.Coin;
import sd.fomin.gerbera.util.Base58CheckUtils;
import sd.fomin.gerbera.util.Bech32CheckUtils;

Expand All @@ -9,26 +10,26 @@

import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static sd.fomin.gerbera.util.ValidationUtils.isBase58;
import static sd.fomin.gerbera.util.ValidationUtils.isBech32;
import static sd.fomin.gerbera.util.ValidationUtils.isEmpty;
import static sd.fomin.gerbera.util.ValidationUtils.*;

class RegularOutput extends Output {

private final boolean mainNet;
private final Coin coin;
private final String destination;
private final byte[] decodedAddress;
private final boolean isBech32;

RegularOutput(boolean mainNet, long satoshi, String destination, OutputType type) {
RegularOutput(boolean mainNet, Coin coin, long satoshi, String destination, OutputType type) {
super(type, satoshi);

validateOutputData(mainNet, destination);
validateOutputData(mainNet, coin, destination);

this.mainNet = mainNet;
this.destination = destination;
this.isBech32 = !isBase58(destination) && isBech32(destination);
this.isBech32 = !isBase58(destination) && isBech32(destination, coin);
this.decodedAddress = isBech32 ? decodeBech32Address(destination) : Base58CheckUtils.decode(destination);
this.coin = coin;
}

@Override
Expand All @@ -44,22 +45,36 @@ public String toString() {
return destination + " " + satoshi;
}

private void validateOutputData(boolean mainNet, String destination) {
validateDestinationAddress(mainNet, destination);
private void validateOutputData(boolean mainNet, Coin coin, String destination) {
validateDestinationAddress(mainNet, coin, destination);
}

private void validateDestinationAddress(boolean mainNet, String destination) {
private void validateDestinationAddress(boolean mainNet, Coin coin, String destination) {
if (isEmpty(destination)) {
throw new IllegalArgumentException(ErrorMessages.OUTPUT_ADDRESS_EMPTY);
}

if (!isBase58(destination) && !isBech32(destination)) {
if (!isBase58(destination) && !isBech32(destination, coin)) {
throw new IllegalArgumentException(ErrorMessages.OUTPUT_ADDRESS_NOT_BASE_58);
}

List<Character> prefixP2PKH = mainNet ? singletonList('1') : asList('m', 'n');
List<Character> prefixP2SH = singletonList(mainNet ? '3' : '2');
List<Character> prefixBech32 = mainNet ? asList('b', 'c') : asList('t', 'b');
List<Character> prefixP2PKH;
List<Character> prefixP2SH;
List<Character> prefixBech32;
switch (coin) {
case BTC:
prefixP2PKH = mainNet ? singletonList('1') : asList('m', 'n');
prefixP2SH = singletonList(mainNet ? '3' : '2');
prefixBech32 = mainNet ? asList('b', 'c') : asList('t', 'b');
break;
case LTC:
prefixP2PKH = mainNet ? singletonList('L') : asList('m', 'n');
prefixP2SH = singletonList(mainNet ? 'M' : '2');
prefixBech32 = mainNet ? asList('l', '1', 'c') : asList('t', 'b');
break;
default:
throw new IllegalStateException("Unexpected value: " + coin);
}

char prefix = destination.charAt(0);
if (!prefixP2PKH.contains(prefix) && !prefixP2SH.contains(prefix) && !prefixBech32.contains(prefix)) {
Expand Down
29 changes: 14 additions & 15 deletions src/main/java/sd/fomin/gerbera/transaction/TransactionBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import sd.fomin.gerbera.constant.ErrorMessages;
import sd.fomin.gerbera.constant.SigHashType;
import sd.fomin.gerbera.types.Coin;
import sd.fomin.gerbera.util.ByteBuffer;
import sd.fomin.gerbera.types.UInt;
import sd.fomin.gerbera.types.VarInt;
Expand All @@ -13,16 +14,14 @@

public class TransactionBuilder {

private static final String DONATE_ADDRESS_MAINNET = "3Jyiwca9Gz8fD9LCCw5JnPaUciEtM6Fi7Z";
private static final String DONATE_ADDRESS_TESTNET = "mwC7PAhQWSHmjeVXuCwXaP28kjMmsr2LZk";

private static final UInt VERSION = UInt.of(1);
private static final byte SEGWIT_MARKER = (byte) 0x00;
private static final byte SEGWIT_FLAG = (byte) 0x01;
private static final UInt LOCK_TIME = UInt.of(0);
private static final Integer BTC_DUST_AMOUNT = 546;

private final boolean mainNet;
private final Coin coin;

private final List<Input> inputs = new LinkedList<>();
private final List<Output> outputs = new LinkedList<>();
Expand All @@ -31,25 +30,30 @@ public class TransactionBuilder {
private long fee;
private long donate;

private TransactionBuilder(boolean mainNet) {
private TransactionBuilder(boolean mainNet, Coin coin) {
this.mainNet = mainNet;
this.coin = coin;
}

public static TransactionBuilder create() {
return new TransactionBuilder(true);
return new TransactionBuilder(true, Coin.BTC);
}

public static TransactionBuilder create(boolean mainNet) {
return new TransactionBuilder(mainNet);
return new TransactionBuilder(mainNet, Coin.BTC);
}

public static TransactionBuilder create(boolean mainNet, Coin coin) {
return new TransactionBuilder(mainNet, coin);
}

public TransactionBuilder from(String fromTransactionBigEnd, int fromToutNumber, String closingScript, long satoshi, String wif) {
inputs.add(new Input(mainNet, fromTransactionBigEnd, fromToutNumber, closingScript, satoshi, wif));
inputs.add(new Input(mainNet, coin, fromTransactionBigEnd, fromToutNumber, closingScript, satoshi, wif));
return this;
}

public TransactionBuilder from(String fromTransactionBigEnd, int fromToutNumber, String closingScript, long satoshi, String wif, Boolean replaceable) {
inputs.add(new Input(mainNet, fromTransactionBigEnd, fromToutNumber, closingScript, satoshi, wif, replaceable));
inputs.add(new Input(mainNet, coin, fromTransactionBigEnd, fromToutNumber, closingScript, satoshi, wif, replaceable));
return this;
}

Expand All @@ -63,7 +67,7 @@ public TransactionBuilder rmInputAt(int i) {
}

public TransactionBuilder to(String address, long value) {
outputs.add(new RegularOutput(mainNet, value, address, OutputType.CUSTOM));
outputs.add(new RegularOutput(mainNet, coin, value, address, OutputType.CUSTOM));
return this;
}

Expand Down Expand Up @@ -114,14 +118,9 @@ public Transaction build() {

List<Output> buildOutputs = new LinkedList<>(outputs);

if (donate > 0) {
String donateAddress = mainNet ? DONATE_ADDRESS_MAINNET : DONATE_ADDRESS_TESTNET;
buildOutputs.add(new RegularOutput(mainNet, donate, donateAddress, OutputType.DONATE));
}

long change = getChange();
if (change >= BTC_DUST_AMOUNT) {
buildOutputs.add(new RegularOutput(mainNet, change, changeAddress, OutputType.CHANGE));
buildOutputs.add(new RegularOutput(mainNet, coin, change, changeAddress, OutputType.CHANGE));
}

if (buildOutputs.isEmpty()) {
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/sd/fomin/gerbera/types/Coin.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package sd.fomin.gerbera.types;

public enum Coin {
BTC,
LTC
}
28 changes: 25 additions & 3 deletions src/main/java/sd/fomin/gerbera/util/ValidationUtils.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
package sd.fomin.gerbera.util;

import sd.fomin.gerbera.types.Coin;

import java.util.List;

import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;

public class ValidationUtils {

private ValidationUtils() { }
private ValidationUtils() {
}

public static boolean isTransactionId(String stirng) {
return stirng.matches("\\p{XDigit}{64}");
Expand All @@ -16,8 +24,22 @@ public static boolean isBase58(String string) {
return string.matches("[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+");
}

public static boolean isBech32(String string) {
return string.matches("^(bc1|tb1)[a-zA-HJ-NP-Z0-9]{25,39}$");
public static boolean isBech32(String string, Coin coin) {
List<String> prefixList;
switch (coin) {

case BTC:
prefixList = asList("bc1", "tb1");
break;
case LTC:
prefixList = singletonList("ltc1");
break;
default:
throw new IllegalStateException("Unexpected value: " + coin);
}
String prefix = String.join("|", prefixList);
String regexp = String.format("^(%s)[a-zA-HJ-NP-Z0-9]{25,39}$", prefix);
return string.matches(regexp);
}

public static boolean isEmpty(String string) {
Expand Down
Loading

0 comments on commit 7402e24

Please sign in to comment.