package io.emax.cosigner.ethereum.token;
import io.emax.cosigner.api.core.ServerStatus;
import io.emax.cosigner.api.currency.CurrencyAdmin;
import io.emax.cosigner.api.currency.OfflineWallet;
import io.emax.cosigner.api.currency.Wallet;
import io.emax.cosigner.common.ByteUtilities;
import io.emax.cosigner.common.Json;
import io.emax.cosigner.common.crypto.Secp256k1;
import io.emax.cosigner.ethereum.core.EthereumConfiguration;
import io.emax.cosigner.ethereum.core.EthereumResource;
import io.emax.cosigner.ethereum.core.common.EthereumTools;
import io.emax.cosigner.ethereum.core.gethrpc.Block;
import io.emax.cosigner.ethereum.core.gethrpc.CallData;
import io.emax.cosigner.ethereum.core.gethrpc.DefaultBlock;
import io.emax.cosigner.ethereum.core.gethrpc.EthereumRpc;
import io.emax.cosigner.ethereum.core.gethrpc.RawTransaction;
import io.emax.cosigner.ethereum.token.gethrpc.tokencontract.TokenContract;
import io.emax.cosigner.ethereum.token.gethrpc.tokencontract.TokenContractInterface;
import io.emax.cosigner.ethereum.token.gethrpc.tokencontract.TokenContractParametersInterface;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import java.util.Set;
public class TokenWallet implements Wallet, OfflineWallet, CurrencyAdmin {
private static final Logger LOGGER = LoggerFactory.getLogger(TokenWallet.class);
private static final String TESTNET_VERSION = "2";
private static final long TESTNET_BASE_ROUNDS = (long) Math.pow(2, 20);
// RPC and configuration
private final EthereumRpc ethereumRpc = EthereumResource.getResource().getGethRpc();
TokenConfiguration config;
private String storageContractAddress = "";
private String tokenContractAddress = "";
private String adminContractAddress = "";
private TokenContractInterface contractInterface = new TokenContract();
private HashSet<String> knownAddresses = new HashSet<>();
private HashMap<String, HashSet<String>> ownedAddresses = new HashMap<>();
public TokenWallet(TokenConfiguration conf) {
config = conf;
setupTokenContract();
}
private void findExistingContract(String contractAccount) {
try {
String txCount = ethereumRpc
.eth_getTransactionCount("0x" + contractAccount, DefaultBlock.LATEST.toString());
int rounds = new BigInteger(1, ByteUtilities.toByteArray(txCount)).intValue();
int baseRounds = 0;
if (ethereumRpc.net_version().equals(TESTNET_VERSION)) {
baseRounds = (int) TESTNET_BASE_ROUNDS;
}
LOGGER.info(
"[" + config.getCurrencySymbol() + "] Token Rounds: " + (rounds - baseRounds) + "("
+ txCount + " - " + baseRounds + ") for " + contractAccount);
for (int i = baseRounds; i < rounds; i++) {
if (i % 10000 == 0) {
LOGGER.info(
"[" + config.getCurrencySymbol() + "] Token Round progress: " + i + "/" + rounds
+ "...");
}
String contract = EthereumTools.calculateContractAddress(contractAccount, (long) i);
TokenContractInterface contractClass = getContractVersion(contract);
if (storageContractAddress.isEmpty() && contractClass != null) {
this.contractInterface = contractClass;
}
String contractType = getContractType(contract);
if (adminContractAddress.isEmpty() && contractType != null && contractType
.equalsIgnoreCase(ADMIN)) {
this.adminContractAddress = contract;
} else if (tokenContractAddress.isEmpty() && contractType != null && contractType
.equalsIgnoreCase(TOKEN)) {
this.tokenContractAddress = contract;
} else if (storageContractAddress.isEmpty() && contractType != null && contractType
.equalsIgnoreCase(STORAGE)) {
this.storageContractAddress = contract;
} else if (!adminContractAddress.isEmpty() && !tokenContractAddress.isEmpty()
&& !storageContractAddress.isEmpty()) {
break;
}
}
} catch (Exception e) {
LOGGER.debug(null, e);
}
}
private String waitForReceipt(String txId) {
String minedContractAddress = null;
int confirmations = 0;
Map<String, Object> receiptData = ethereumRpc.eth_getTransactionReceipt(txId);
try {
while (receiptData == null || config.getMinConfirmations() > confirmations) {
LOGGER.info("Waiting for transaction receipt...");
Thread.sleep(5000);
receiptData = ethereumRpc.eth_getTransactionReceipt(txId);
if (receiptData != null) {
minedContractAddress = (String) receiptData.get("contractAddress");
minedContractAddress =
ByteUtilities.toHexString(ByteUtilities.toByteArray(minedContractAddress));
BigInteger latestBlockNumber =
new BigInteger(1, ByteUtilities.toByteArray(ethereumRpc.eth_blockNumber()));
BigInteger txBlockNumber =
new BigInteger(1, ByteUtilities.toByteArray((String) receiptData.get("blockNumber")));
confirmations = latestBlockNumber.subtract(txBlockNumber).intValue();
LOGGER.info("[TX Receipt] Got " + config.getCurrencySymbol() + " contract address: "
+ minedContractAddress);
LOGGER.info("[TX Receipt] Confirmations: " + confirmations);
}
}
return minedContractAddress;
} catch (Exception e) {
LOGGER.debug("Interrupted while waiting for tx receipt", e);
return null;
}
}
public void setupTokenContract() {
LOGGER.info("[" + config.getCurrencySymbol() + "] Attempting to setup token contract");
if (config.getStorageContractAddress() != null && !config.getStorageContractAddress()
.isEmpty()) {
// Contract info specified in configuration, using that.
LOGGER
.info("[" + config.getCurrencySymbol() + "] Using " + config.getStorageContractAddress());
storageContractAddress = config.getStorageContractAddress();
tokenContractAddress = config.getTokenContractAddress();
adminContractAddress = config.getAdminContractAddress();
contractInterface = getContractVersion(storageContractAddress);
if (contractInterface == null) {
contractInterface = new TokenContract();
}
} else {
// Attempting to find existing contract on the blockchain given our configuration.
String contractKey = config.getContractKey();
String contractAccount = config.getContractAccount();
if (!contractKey.isEmpty()) {
contractAccount = EthereumTools.getPublicAddress(contractKey, true);
LOGGER.debug(
"[" + config.getCurrencySymbol() + "] ContractAccount from key: " + contractAccount);
} else {
contractKey = null;
LOGGER.debug(
"[" + config.getCurrencySymbol() + "] ContractAccount from config: " + contractAccount);
}
findExistingContract(contractAccount);
try {
if ((storageContractAddress == null || storageContractAddress.isEmpty()) && config
.generateNewContract()) {
// Could not find an existing contract, attempting to register a new instance of it.
LOGGER.info("[" + config.getCurrencySymbol() + "] Generating new contract...");
if (config.generateTokenContract()) {
// Admin contract
// Gather owner addresses
LinkedList<String> decodedAddresses = new LinkedList<>();
decodedAddresses.addAll(Arrays.asList(config.getMultiSigAccounts()));
Arrays.asList(config.getMultiSigKeys()).forEach(key -> {
if (key.isEmpty()) {
return;
}
String address = EthereumTools.getPublicAddress(key, true);
decodedAddresses.add(address);
});
decodedAddresses.removeIf(String::isEmpty);
// Generating tx structure
RawTransaction tx = RawTransaction.createContract(config,
contractInterface.getContractParameters()
.createAdminContract(config.getAdminAccount(), decodedAddresses,
config.getMinSignatures()));
String rawTx = ByteUtilities.toHexString(tx.encode());
LOGGER.debug("[" + config.getCurrencySymbol() + "] Creating contract: " + rawTx);
// Signing it
Iterable<Iterable<String>> sigData = getSigString(rawTx, contractAccount, true);
sigData = signWithPrivateKey(sigData, contractKey,
contractKey == null ? contractAccount : null);
rawTx = applySignature(rawTx, contractAccount, sigData);
LOGGER.debug("[" + config.getCurrencySymbol() + "] Signed contract: " + rawTx);
// Wait for receipt
String txId = sendTransaction(rawTx);
adminContractAddress = waitForReceipt(txId);
// Token Contract
// Generate tx structure
tx = RawTransaction.createContract(config, contractInterface.getContractParameters()
.createTokenContract(this.adminContractAddress, config.getCurrencySymbol(),
config.getCurrencySymbol(), (int) config.getDecimalPlaces()));
rawTx = ByteUtilities.toHexString(tx.encode());
LOGGER.debug("[" + config.getCurrencySymbol() + "] Creating contract: " + rawTx);
// Sign it
sigData = getSigString(rawTx, contractAccount, true);
sigData = signWithPrivateKey(sigData, contractKey,
contractKey == null ? contractAccount : null);
rawTx = applySignature(rawTx, contractAccount, sigData);
LOGGER.debug("[" + config.getCurrencySymbol() + "] Signed contract: " + rawTx);
// Wait for receipt
txId = sendTransaction(rawTx);
tokenContractAddress = waitForReceipt(txId);
// Token Contract Assignment
// Generate tx structure
Long nonce = contractInterface.getContractParameters()
.getNonce(ethereumRpc, this.adminContractAddress);
tx = RawTransaction.createTransaction(config, adminContractAddress, null,
contractInterface.getContractParameters()
.setTokenChild(nonce, tokenContractAddress, new LinkedList<>(),
new LinkedList<>(), new LinkedList<>()));
rawTx = ByteUtilities.toHexString(tx.encode());
LOGGER.debug("[" + config.getCurrencySymbol() + "] Issuing transaction: " + rawTx);
// Sign it
// Admin key first, because it has to be there.
sigData = getSigString(rawTx, contractAccount, false);
sigData = signWithPrivateKey(sigData, config.getAdminKey(),
config.getAdminKey().isEmpty() ? config.getAdminContractAddress() : null);
rawTx = applySignature(rawTx, contractAccount, sigData);
// Sign with any multi-sig keys configured to meet minimum req's
rawTx = signTransaction(rawTx, contractAccount);
// Sign with contract account because it's the one that should have funds in it
sigData = getSigString(rawTx, contractAccount, false);
sigData = signWithPrivateKey(sigData, contractKey, null);
rawTx = applySignature(rawTx, contractAccount, sigData);
LOGGER.debug("[" + config.getCurrencySymbol() + "] Signed transaction: " + rawTx);
// Wait for receipt
txId = sendTransaction(rawTx);
waitForReceipt(txId);
}
// Work around for invalid token configuration when running on the ether version
if (config.useAlternateEtherContract()) {
tokenContractAddress = "0x12345678901234567890";
}
if (!tokenContractAddress.isEmpty()) {
LinkedList<String> decodedAddresses = new LinkedList<>();
decodedAddresses.addAll(Arrays.asList(config.getMultiSigAccounts()));
Arrays.asList(config.getMultiSigKeys()).forEach(key -> {
if (key.isEmpty()) {
return;
}
String address = EthereumTools.getPublicAddress(key, true);
decodedAddresses.add(address);
});
decodedAddresses.removeIf(String::isEmpty);
// Generating tx structure
RawTransaction tx = RawTransaction.createContract(config,
contractInterface.getContractParameters()
.createStorageContract(config, tokenContractAddress, config.getAdminAccount(),
decodedAddresses, config.getMinSignatures(), new Random().nextLong(),
config.getCurrencySymbol(), config.getCurrencySymbol(),
(int) config.getDecimalPlaces()));
String rawTx = ByteUtilities.toHexString(tx.encode());
LOGGER.debug("[" + config.getCurrencySymbol() + "] Creating contract: " + rawTx);
// Signing it
Iterable<Iterable<String>> sigData = getSigString(rawTx, contractAccount, true);
sigData = signWithPrivateKey(sigData, contractKey,
contractKey == null ? contractAccount : null);
rawTx = applySignature(rawTx, contractAccount, sigData);
LOGGER.debug("[" + config.getCurrencySymbol() + "] Signed contract: " + rawTx);
// Wait for receipt
String txId = sendTransaction(rawTx);
storageContractAddress = waitForReceipt(txId);
} else {
throw new Exception(
"Could not create storage contract, no token contract address available!");
}
}
LOGGER.info("[" + config.getCurrencySymbol() + "] Got contract address of: "
+ storageContractAddress);
} catch (Exception e) {
LOGGER.error("[" + config.getCurrencySymbol()
+ "] Unable to create contract, Token module is not usable!");
LOGGER.debug("[" + config.getCurrencySymbol() + "] Contract setup", e);
}
LOGGER.debug("[" + config.getCurrencySymbol() + "] Admin Contract: " + adminContractAddress);
LOGGER.debug("[" + config.getCurrencySymbol() + "] Token Contract: " + tokenContractAddress);
LOGGER.debug(
"[" + config.getCurrencySymbol() + "] Storage Contract: " + storageContractAddress);
}
}
public TokenContractInterface getContractVersion(String contract) {
try {
String contractCode = ethereumRpc
.eth_getCode("0x" + contract.toLowerCase(Locale.US), DefaultBlock.LATEST.toString());
contractCode = contractCode.substring(2);
Class<?> contractType = TokenContract.class;
while (TokenContractInterface.class.isAssignableFrom(contractType)) {
TokenContractInterface contractParams = (TokenContractInterface) contractType.newInstance();
if (contractParams.getStorageRuntime().equalsIgnoreCase(contractCode) || contractParams
.getAdminRuntime().equalsIgnoreCase(contractCode)) {
return contractParams;
} else if (config.useAlternateEtherContract() && contractParams.getAlternateStorageRunTime()
.equalsIgnoreCase(contractCode)) {
return contractParams;
}
contractType = contractType.getSuperclass();
}
} catch (Exception e) {
LOGGER.debug(null, e);
return null;
}
return null;
}
private static final String ADMIN = "admin";
private static final String TOKEN = "token";
private static final String STORAGE = "storage";
public String getContractType(String contract) {
try {
String contractCode = ethereumRpc
.eth_getCode("0x" + contract.toLowerCase(Locale.US), DefaultBlock.LATEST.toString());
contractCode = contractCode.substring(2);
Class<?> contractType = TokenContract.class;
while (TokenContractInterface.class.isAssignableFrom(contractType)) {
TokenContractInterface contractParams = (TokenContractInterface) contractType.newInstance();
if (contractParams.getAdminRuntime().equalsIgnoreCase(contractCode)) {
return ADMIN;
} else if (contractParams.getTokenRuntime().equalsIgnoreCase(contractCode)) {
return TOKEN;
} else if (contractParams.getStorageRuntime().equalsIgnoreCase(contractCode)) {
return STORAGE;
} else if (config.useAlternateEtherContract() && contractParams.getAlternateStorageRunTime()
.equalsIgnoreCase(contractCode)) {
return STORAGE;
}
contractType = contractType.getSuperclass();
}
} catch (Exception e) {
LOGGER.debug(null, e);
return null;
}
return null;
}
@Override
public String createAddress(String name) {
return createAddress(name, 0);
}
@Override
public String createAddress(String name, int skipNumber) {
// Generate the next private key
LOGGER.debug("Creating a new normal address...");
String user = EthereumTools.encodeUserKey(name);
if (!ownedAddresses.containsKey(user)) {
getAddresses(name);
}
int rounds = 1 + skipNumber;
String privateKey =
EthereumTools.getDeterministicPrivateKey(name, config.getServerPrivateKey(), rounds);
// Convert to an Ethereum address
String publicAddress = EthereumTools.getPublicAddress(privateKey);
while (knownAddresses.contains(publicAddress.toLowerCase(Locale.US))) {
LOGGER.debug("Address " + publicAddress + " already known");
LOGGER.debug("KnownAddress: " + Json.stringifyObject(Set.class, knownAddresses));
rounds++;
privateKey =
EthereumTools.getDeterministicPrivateKey(name, config.getServerPrivateKey(), rounds);
publicAddress = EthereumTools.getPublicAddress(privateKey);
}
knownAddresses.add(publicAddress);
ownedAddresses.get(user).add(publicAddress);
LOGGER.debug("New address " + publicAddress + " generated after " + rounds + " rounds");
return publicAddress;
}
@Override
public boolean registerAddress(String address) {
return true;
}
@Override
public String createAddressFromKey(String key, boolean isPrivateKey) {
return EthereumTools.getPublicAddress(key, isPrivateKey);
}
@Override
public Iterable<String> getAddresses(String name) {
String user = EthereumTools.encodeUserKey(name);
if (!ownedAddresses.containsKey(user)) {
ownedAddresses.put(user, new HashSet<>());
}
HashSet<String> userAddresses = ownedAddresses.get(user);
if (userAddresses.isEmpty()) {
String balanceCheck = getBalance(createAddress(name));
while (balanceCheck != null
&& (new BigDecimal(balanceCheck).compareTo(BigDecimal.ZERO)) != 0) {
LOGGER.debug(
"BalanceCheck was: " + balanceCheck + " compared to " + BigInteger.ZERO.toString(10));
userAddresses = ownedAddresses.get(user);
balanceCheck = getBalance(createAddress(name));
}
}
return userAddresses;
}
@Override
public String getMultiSigAddress(Iterable<String> addresses, String name) {
return addresses.iterator().next();
}
@Override
public String getBalance(String address) {
CallData callData = EthereumTools
.generateCall(contractInterface.getContractParameters().getBalance(address),
storageContractAddress);
LOGGER.debug("Balance request: " + Json.stringifyObject(CallData.class, callData));
String response = ethereumRpc.eth_call(callData, DefaultBlock.LATEST.toString());
BigInteger intBalance = new BigInteger(1, ByteUtilities.toByteArray(response));
BigDecimal balance = new BigDecimal(intBalance);
balance = balance.setScale(20, BigDecimal.ROUND_UNNECESSARY);
balance = balance.divide(BigDecimal.valueOf(10).pow((int) config.getDecimalPlaces()),
BigDecimal.ROUND_UNNECESSARY);
// Subtract any pending txs from the available balance
TransactionDetails[] txDetails = getTransactions(address, 100, 0);
for (TransactionDetails txDetail : txDetails) {
if (!txDetail.isConfirmed() && txDetail.getToAddress()[0].equalsIgnoreCase(address)) {
balance = balance.subtract(txDetail.getAmount());
}
}
return balance.toPlainString();
}
@Override
public String getPendingBalance(String address) {
BigDecimal balance = BigDecimal.ZERO;
TransactionDetails[] txDetails = getTransactions(address, 100, 0);
for (TransactionDetails txDetail : txDetails) {
if (!txDetail.isConfirmed() && txDetail.getToAddress()[0].equalsIgnoreCase(address)) {
balance = balance.add(txDetail.getAmount());
} else if (!txDetail.isConfirmed() && txDetail.getFromAddress()[0]
.equalsIgnoreCase(address)) {
balance = balance.subtract(txDetail.getAmount());
}
}
balance = balance.max(BigDecimal.ZERO);
return balance.toPlainString();
}
public String getTotalBalances() {
CallData callData = EthereumTools
.generateCall(contractInterface.getContractParameters().getTotalBalance(),
storageContractAddress);
LOGGER.debug("Total balance request: " + Json.stringifyObject(CallData.class, callData));
String response = ethereumRpc.eth_call(callData, DefaultBlock.LATEST.toString());
BigInteger intBalance = new BigInteger(1, ByteUtilities.toByteArray(response));
BigDecimal balance = new BigDecimal(intBalance);
balance = balance.setScale(20, BigDecimal.ROUND_UNNECESSARY);
return balance.divide(BigDecimal.valueOf(10).pow((int) config.getDecimalPlaces()),
BigDecimal.ROUND_UNNECESSARY).toPlainString();
}
@Override
public String createTransaction(Iterable<String> fromAddresses, Iterable<Recipient> toAddresses) {
return createTransaction(fromAddresses, toAddresses, null);
}
@Override
public String createTransaction(Iterable<String> fromAddresses, Iterable<Recipient> toAddresses,
String options) {
String firstSender = fromAddresses.iterator().next();
String contract = storageContractAddress;
TokenContractInterface txInterface = getContractVersion(firstSender);
if (txInterface != null && config.useTokenTransferFunction()) {
contract = firstSender;
}
if (txInterface == null && config.useTokenTransferFunction()) {
Recipient recipient = toAddresses.iterator().next();
String rcpt = recipient.getRecipientAddress();
BigInteger amount =
recipient.getAmount().multiply(BigDecimal.TEN.pow((int) config.getDecimalPlaces()))
.toBigInteger();
RawTransaction tx = RawTransaction.createTransaction(config, tokenContractAddress, null,
contractInterface.getContractParameters().tokenTransfer(rcpt, amount));
return ByteUtilities.toHexString(tx.encode());
} else {
if (txInterface == null) {
txInterface = contractInterface;
}
// Format tx data
List<String> recipients = new LinkedList<>();
List<BigInteger> amounts = new LinkedList<>();
toAddresses.forEach(recipient -> {
amounts.add(
recipient.getAmount().multiply(BigDecimal.TEN.pow((int) config.getDecimalPlaces()))
.toBigInteger());
recipients.add(recipient.getRecipientAddress());
});
String txCount = ethereumRpc.eth_getStorageAt("0x" + contract.toLowerCase(Locale.US), "0x1",
DefaultBlock.LATEST.toString());
BigInteger nonce = new BigInteger(1, ByteUtilities.toByteArray(txCount)).add(BigInteger.ONE);
// Create the TX data structure
RawTransaction tx = RawTransaction.createTransaction(config, contract, null,
txInterface.getContractParameters()
.transfer(nonce.longValue(), firstSender, recipients, amounts, new LinkedList<>(),
new LinkedList<>(), new LinkedList<>()));
return ByteUtilities.toHexString(tx.encode());
}
}
@Override
public Iterable<String> getSignersForTransaction(String transaction) {
RawTransaction rawTx = RawTransaction.parseBytes(ByteUtilities.toByteArray(transaction));
if (rawTx == null) {
return new LinkedList<>();
}
String contractData = ByteUtilities.toHexString(rawTx.getData().getDecodedContents());
Map<String, List<String>> contractDataParams =
contractInterface.getContractParameters().parseTransfer(contractData);
return contractDataParams.get(TokenContractParametersInterface.SENDER);
}
@Override
public String signTransaction(String transaction, String address) {
return signTransaction(transaction, address, null);
}
@Override
public String signTransaction(String transaction, String address, String name) {
return signTransaction(transaction, address, name, null);
}
@Override
public String signTransaction(String transaction, String address, String name, String options) {
// Convert transaction to data, and to parsed input.
RawTransaction tx = RawTransaction.parseBytes(ByteUtilities.toByteArray(transaction));
if (tx == null) {
return transaction;
}
Iterable<Iterable<String>> sigData;
if (name == null) {
for (int i = 0; i < config.getMultiSigAccounts().length; i++) {
if (config.getMultiSigAccounts()[i].isEmpty()) {
continue;
}
sigData = getSigString(transaction, config.getMultiSigAccounts()[i]);
sigData = signWithPrivateKey(sigData, null, config.getMultiSigAccounts()[i]);
transaction = applySignature(transaction, address, sigData);
}
for (int i = 0; i < config.getMultiSigKeys().length; i++) {
if (config.getMultiSigKeys()[i].isEmpty()) {
continue;
}
String msigAddress = EthereumTools.getPublicAddress(config.getMultiSigKeys()[i], true);
sigData = getSigString(transaction, msigAddress);
sigData = signWithPrivateKey(sigData, config.getMultiSigKeys()[i], null);
transaction = applySignature(transaction, address, sigData);
}
}
sigData = getSigString(transaction, address);
sigData = signWithPrivateKey(sigData, name, address);
return applySignature(transaction, address, sigData);
}
@Override
public Iterable<Iterable<String>> getSigString(String transaction, String address) {
return getSigString(transaction, address, false);
}
private Iterable<Iterable<String>> getSigString(String transaction, String address,
boolean ignoreContractCode) {
RawTransaction tx = RawTransaction.parseBytes(ByteUtilities.toByteArray(transaction));
LinkedList<Iterable<String>> sigStrings = new LinkedList<>();
if (tx == null) {
LOGGER.warn("Not able to parse tx.");
LinkedList<String> txData = new LinkedList<>();
txData.add(transaction);
LinkedList<Iterable<String>> wrappedTxData = new LinkedList<>();
wrappedTxData.add(txData);
return wrappedTxData;
}
if (!ignoreContractCode) {
LOGGER.debug("Attempting to parse contract code...");
String hashBytes;
// Get the transaction data
TokenContractInterface txContractInterface =
getContractVersion(ByteUtilities.toHexString(tx.getTo().getDecodedContents()));
LOGGER.debug("Found contract interface: " + (txContractInterface == null ? "null" :
txContractInterface.getClass().getCanonicalName()) + " at " + ByteUtilities
.toHexString(tx.getTo().getDecodedContents()));
if (txContractInterface != null) {
Map<String, List<String>> contractParams = txContractInterface.getContractParameters()
.parseTransfer(ByteUtilities.toHexString(tx.getData().getDecodedContents()));
if (contractParams != null) {
LOGGER.debug(Json.stringifyObject(Map.class, contractParams));
BigInteger nonce =
new BigInteger(contractParams.get(TokenContractParametersInterface.NONCE).get(0));
List<String> recipients = contractParams.get(TokenContractParametersInterface.RECIPIENTS);
List<String> amounts = contractParams.get(TokenContractParametersInterface.AMOUNT);
// Hash to sign is hash(previous hash + recipient + amount + nonce)
hashBytes = txContractInterface.getContractParameters()
.calculateTxHash(nonce.longValue(), recipients, amounts);
LinkedList<String> msigString = new LinkedList<>();
msigString.add(txContractInterface.getClass().getCanonicalName());
msigString.add(hashBytes);
sigStrings.add(msigString);
} else {
contractParams = txContractInterface.getContractParameters()
.parseAdminFunction(ByteUtilities.toHexString(tx.getData().getDecodedContents()));
if (contractParams != null) {
// Sign it as an admin function
BigInteger nonce =
new BigInteger(contractParams.get(TokenContractParametersInterface.NONCE).get(0));
hashBytes = txContractInterface.getContractParameters().calculateAdminHash(ethereumRpc,
ByteUtilities.toHexString(tx.getTo().getDecodedContents()), nonce.longValue());
LOGGER.debug("Result: " + hashBytes);
LinkedList<String> msigString = new LinkedList<>();
msigString.add(txContractInterface.getClass().getCanonicalName());
msigString.add(hashBytes);
sigStrings.add(msigString);
}
}
}
}
// Calculate the transaction's signature data.
String txCount =
ethereumRpc.eth_getTransactionCount("0x" + address, DefaultBlock.LATEST.toString());
LinkedList<String> txString = new LinkedList<>();
txString.add(transaction);
txString.add(txCount);
sigStrings.add(txString);
LOGGER.debug(sigStrings.toString());
return sigStrings;
}
@Override
public String applySignature(String transaction, String address,
Iterable<Iterable<String>> signatureData) {
// This is taken care of in the signing process for Ethereum, so we can just return the data.
try {
return signatureData.iterator().next().iterator().next();
} catch (Exception e) {
return "";
}
}
@Override
public String sendTransaction(String transaction) {
LOGGER.debug("Asked to send: " + transaction);
RawTransaction rawTx = RawTransaction.parseBytes(ByteUtilities.toByteArray(transaction));
if (rawTx == null) {
return "Bad Transaction";
}
if (ByteUtilities.toHexString(rawTx.getTo().getDecodedContents())
.equalsIgnoreCase(storageContractAddress)) {
Map<String, List<String>> contractParams = contractInterface.getContractParameters()
.parseTransfer(ByteUtilities.toHexString(rawTx.getData().getDecodedContents()));
Map<String, List<String>> adminParams = contractInterface.getContractParameters()
.parseAdminFunction(ByteUtilities.toHexString(rawTx.getData().getDecodedContents()));
if (contractParams != null || adminParams != null) {
String contractKey = config.getContractKey();
String contractAddress = config.getContractAccount();
if (!contractKey.isEmpty()) {
contractAddress = EthereumTools.getPublicAddress(contractKey, true);
} else {
contractKey = null;
}
Iterable<Iterable<String>> sigData = getSigString(transaction, contractAddress, true);
sigData =
signWithPrivateKey(sigData, contractKey, contractKey == null ? contractAddress : null);
LOGGER.debug("Re-signing transfer transaction");
transaction = applySignature(transaction, contractAddress, sigData);
}
}
LOGGER.debug("Sending: " + transaction);
if (transactionsEnabled) {
return ethereumRpc.eth_sendRawTransaction("0x" + transaction);
} else {
return "Transactions Temporarily Disabled";
}
}
@Override
public Map<String, String> getConfiguration() {
HashMap<String, String> configSummary = new HashMap<>();
configSummary.put("Currency Symbol", config.getCurrencySymbol());
configSummary.put("Geth Connection", new EthereumConfiguration().getDaemonConnectionString());
configSummary.put("Minimum Signatures", ((Integer) config.getMinSignatures()).toString());
configSummary.put("Minimum Confirmations", ((Integer) config.getMinConfirmations()).toString());
configSummary
.put("Maximum Transaction Value", config.getMaxAmountPerTransaction().toPlainString());
configSummary
.put("Maximum Transaction Value Per Hour", config.getMaxAmountPerHour().toPlainString());
configSummary
.put("Maximum Transaction Value Per Day", config.getMaxAmountPerDay().toPlainString());
configSummary.put("Contract", this.storageContractAddress);
if (config.getContractKey() != null && !config.getContractKey().isEmpty()) {
configSummary
.put("Contract Manager", EthereumTools.getPublicAddress(config.getContractKey(), true));
} else {
configSummary.put("Contract Manager", config.getContractAccount());
}
return configSummary;
}
private boolean transactionsEnabled = true;
@Override
public void enableTransactions() {
transactionsEnabled = true;
}
@Override
public void disableTransactions() {
transactionsEnabled = false;
}
@Override
public boolean transactionsEnabled() {
return transactionsEnabled;
}
@Override
public long getBlockchainHeight() {
BigInteger latestBlockNumber =
new BigInteger(1, ByteUtilities.toByteArray(ethereumRpc.eth_blockNumber()));
return latestBlockNumber.longValue();
}
@Override
public long getLastBlockTime() {
BigInteger latestBlockNumber =
new BigInteger(1, ByteUtilities.toByteArray(ethereumRpc.eth_blockNumber()));
Block block = ethereumRpc.eth_getBlockByNumber(latestBlockNumber.toString(), true);
BigInteger dateConverter = new BigInteger(1, ByteUtilities.toByteArray(block.getTimestamp()));
return dateConverter.longValue();
}
private class TxDateComparator implements Comparator<TransactionDetails> {
@Override
public int compare(TransactionDetails o1, TransactionDetails o2) {
return o1.getTxDate().compareTo(o2.getTxDate());
}
}
@Override
public TransactionDetails[] getTransactions(String address, int numberToReturn, int skipNumber) {
LinkedList<TransactionDetails> txDetails = new LinkedList<>();
Arrays.asList(getReconciliations(address)).forEach(txDetails::add);
Arrays.asList(getTransfers(address)).forEach(txDetails::add);
LOGGER.debug(Json.stringifyObject(LinkedList.class, txDetails));
Collections.sort(txDetails, new TxDateComparator());
for (int i = 0; i < skipNumber; i++) {
txDetails.removeLast();
}
while (txDetails.size() > numberToReturn) {
txDetails.removeFirst();
}
return txDetails.toArray(new TransactionDetails[txDetails.size()]);
}
private TransactionDetails[] getTransfers(String address) {
// Get latest block
BigInteger latestBlockNumber =
new BigInteger(1, ByteUtilities.toByteArray(ethereumRpc.eth_blockNumber()));
LinkedList<TransactionDetails> txDetails = new LinkedList<>();
Map<String, Object> filterParams = new HashMap<>();
filterParams.put("fromBlock", "0x00");
filterParams.put("toBlock", "latest");
filterParams.put("address", "0x" + storageContractAddress);
LinkedList<String> functionTopics = new LinkedList<>();
functionTopics.add("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef");
functionTopics.add("0x5548c837ab068cf56a2c2479df0882a4922fd203edb7517321831d95078c5f62");
for (String functionTopic : functionTopics) {
Object[] topicArray = new Object[1];
String[] senderTopic = {functionTopic};
topicArray[0] = senderTopic;
filterParams.put("topics", topicArray);
LOGGER.debug("Requesting filter for: " + Json.stringifyObject(Map.class, filterParams));
String txFilter = ethereumRpc.eth_newFilter(filterParams);
LOGGER.debug("Setup filter: " + txFilter);
Map<String, Object>[] filterResults;
try {
LOGGER.debug("Getting filter results...");
filterResults = ethereumRpc.eth_getFilterLogs(txFilter);
} catch (Exception e) {
LOGGER.debug("Something went wrong", e);
filterResults = new Map[0];
} finally {
ethereumRpc.eth_uninstallFilter(txFilter);
}
for (Map<String, Object> result : filterResults) {
LOGGER.debug(result.toString());
TransactionDetails txDetail = new TransactionDetails();
txDetail.setTxHash((String) result.get("transactionHash"));
try {
Block block = ethereumRpc.eth_getBlockByNumber((String) result.get("blockNumber"), true);
BigInteger dateConverter =
new BigInteger(1, ByteUtilities.toByteArray(block.getTimestamp()));
dateConverter = dateConverter.multiply(BigInteger.valueOf(1000));
txDetail.setTxDate(new Date(dateConverter.longValue()));
BigInteger txBlockNumber =
new BigInteger(1, ByteUtilities.toByteArray((String) result.get("blockNumber")));
txDetail.setConfirmed(
config.getMinConfirmations() <= latestBlockNumber.subtract(txBlockNumber).intValue());
txDetail.setConfirmations(latestBlockNumber.subtract(txBlockNumber).intValue());
txDetail.setMinConfirmations(config.getMinConfirmations());
ArrayList<String> topics = (ArrayList<String>) result.get("topics");
if (!topics.get(0).equalsIgnoreCase(functionTopic)) {
continue;
}
String from = ByteUtilities.toHexString(
ByteUtilities.stripLeadingNullBytes(ByteUtilities.toByteArray(topics.get(1))));
txDetail.setFromAddress(new String[]{from});
String to = ByteUtilities.toHexString(
ByteUtilities.stripLeadingNullBytes(ByteUtilities.toByteArray(topics.get(2))));
txDetail.setToAddress(new String[]{to});
String amount = ByteUtilities.toHexString(ByteUtilities.stripLeadingNullBytes(
ByteUtilities
.readBytes(ByteUtilities.toByteArray((String) result.get("data")), 0, 32)));
txDetail.setAmount(new BigDecimal(new BigInteger(1, ByteUtilities.toByteArray(amount)))
.setScale(20, BigDecimal.ROUND_UNNECESSARY)
.divide(BigDecimal.valueOf(10).pow((int) config.getDecimalPlaces()),
BigDecimal.ROUND_UNNECESSARY));
if (address == null || ByteUtilities.toHexString(
ByteUtilities.stripLeadingNullBytes(ByteUtilities.toByteArray(topics.get(1))))
.equalsIgnoreCase(address) || ByteUtilities.toHexString(
ByteUtilities.stripLeadingNullBytes(ByteUtilities.toByteArray(topics.get(2))))
.equalsIgnoreCase(address)) {
txDetails.add(txDetail);
}
} catch (Exception e) {
// Pending TX
LOGGER.debug("Pending Tx Found or wrong event returned by geth.", e);
}
}
}
LOGGER.debug(Json.stringifyObject(LinkedList.class, txDetails));
Collections.sort(txDetails, new TxDateComparator());
return txDetails.toArray(new TransactionDetails[txDetails.size()]);
}
private TransactionDetails[] getReconciliations(String address) {
// Get latest block
BigInteger latestBlockNumber =
new BigInteger(1, ByteUtilities.toByteArray(ethereumRpc.eth_blockNumber()));
LinkedList<TransactionDetails> txDetails = new LinkedList<>();
Map<String, Object> filterParams = new HashMap<>();
filterParams.put("fromBlock", "0x00");
filterParams.put("toBlock", "latest");
filterParams.put("address", "0x" + storageContractAddress);
LinkedList<String> functionTopics = new LinkedList<>();
functionTopics.add("0x73bb00f3ad09ef6bc524e5cf56563dff2bc6663caa0b4054aa5946811083ed2e");
for (String functionTopic : functionTopics) {
Object[] topicArray = new Object[1];
String[] senderTopic = {functionTopic};
topicArray[0] = senderTopic;
filterParams.put("topics", topicArray);
LOGGER.debug(
"Requesting reconciliation filter for: " + Json.stringifyObject(Map.class, filterParams));
String txFilter = ethereumRpc.eth_newFilter(filterParams);
LOGGER.debug("Setup filter: " + txFilter);
Map<String, Object>[] filterResults;
try {
LOGGER.debug("Getting filter results...");
filterResults = ethereumRpc.eth_getFilterLogs(txFilter);
} catch (Exception e) {
LOGGER.debug("Something went wrong", e);
filterResults = new Map[0];
}
for (Map<String, Object> result : filterResults) {
LOGGER.debug(result.toString());
TransactionDetails txDetail = new TransactionDetails();
txDetail.setTxHash((String) result.get("transactionHash"));
try {
Block block = ethereumRpc.eth_getBlockByNumber((String) result.get("blockNumber"), true);
BigInteger dateConverter =
new BigInteger(1, ByteUtilities.toByteArray(block.getTimestamp()));
dateConverter = dateConverter.multiply(BigInteger.valueOf(1000));
txDetail.setTxDate(new Date(dateConverter.longValue()));
BigInteger txBlockNumber =
new BigInteger(1, ByteUtilities.toByteArray((String) result.get("blockNumber")));
txDetail.setConfirmed(
config.getMinConfirmations() <= latestBlockNumber.subtract(txBlockNumber).intValue());
txDetail.setConfirmations(latestBlockNumber.subtract(txBlockNumber).intValue());
txDetail.setMinConfirmations(config.getMinConfirmations());
ArrayList<String> topics = (ArrayList<String>) result.get("topics");
if (!topics.get(0).equalsIgnoreCase(functionTopic)) {
continue;
}
String amount = ByteUtilities.toHexString(ByteUtilities.stripLeadingNullBytes(
ByteUtilities
.readBytes(ByteUtilities.toByteArray((String) result.get("data")), 0, 32)));
txDetail.setAmount(new BigDecimal(new BigInteger(ByteUtilities.toByteArray(amount)))
.setScale(20, BigDecimal.ROUND_UNNECESSARY)
.divide(BigDecimal.valueOf(10).pow((int) config.getDecimalPlaces()),
BigDecimal.ROUND_UNNECESSARY));
if (BigInteger.ZERO.compareTo(new BigInteger(ByteUtilities.toByteArray(amount))) > 0) {
String from = ByteUtilities.toHexString(
ByteUtilities.stripLeadingNullBytes(ByteUtilities.toByteArray(topics.get(1))));
txDetail.setFromAddress(new String[]{from});
} else {
String to = ByteUtilities.toHexString(
ByteUtilities.stripLeadingNullBytes(ByteUtilities.toByteArray(topics.get(1))));
txDetail.setToAddress(new String[]{to});
}
if (address == null || ByteUtilities.toHexString(
ByteUtilities.stripLeadingNullBytes(ByteUtilities.toByteArray(topics.get(1))))
.equalsIgnoreCase(address)) {
txDetails.add(txDetail);
}
} catch (Exception e) {
// Pending TX
LOGGER.debug("Pending Tx Found or wrong event returned by geth.", e);
}
}
}
LOGGER.debug(Json.stringifyObject(LinkedList.class, txDetails));
Collections.sort(txDetails, new TxDateComparator());
return txDetails.toArray(new TransactionDetails[txDetails.size()]);
}
@Override
public TransactionDetails getTransaction(String transactionId) {
Map txMap = ethereumRpc.eth_getTransactionByHash(transactionId);
Block txBlock = ethereumRpc.eth_getBlockByNumber(txMap.get("blockNumber").toString(), true);
TransactionDetails txDetail = new TransactionDetails();
txDetail.setTxHash(txMap.get("hash").toString());
txDetail.setTxDate(new Date(
new BigInteger(1, ByteUtilities.toByteArray(txBlock.getTimestamp())).longValue() * 1000L));
BigInteger latestBlockNumber =
new BigInteger(1, ByteUtilities.toByteArray(ethereumRpc.eth_blockNumber()));
BigInteger txBlockNumber =
new BigInteger(1, ByteUtilities.toByteArray(txMap.get("blockNumber").toString()));
txDetail.setConfirmed(
config.getMinConfirmations() <= latestBlockNumber.subtract(txBlockNumber).intValue());
txDetail.setConfirmations(latestBlockNumber.subtract(txBlockNumber).intValue());
txDetail.setMinConfirmations(config.getMinConfirmations());
txDetail.setToAddress(new String[]{txMap.get("to").toString()});
txDetail.setFromAddress(new String[]{txMap.get("from").toString()});
LinkedList<TransactionDetails> txDetails = new LinkedList<>();
Arrays.asList(getTransfers(null)).forEach(tx -> {
if (tx.getTxHash().equalsIgnoreCase(transactionId)) {
txDetails.add(tx);
}
});
Arrays.asList(getReconciliations(null)).forEach(tx -> {
if (tx.getTxHash().equalsIgnoreCase(transactionId)) {
txDetails.add(tx);
}
});
txDetail.setData(Json.stringifyObject(LinkedList.class, txDetails));
if (txDetails.size() == 0) {
return null;
}
return txDetail;
}
@Override
public ServerStatus getWalletStatus() {
try {
ethereumRpc.eth_blockNumber();
return ServerStatus.CONNECTED;
} catch (Exception e) {
return ServerStatus.DISCONNECTED;
}
}
@Override
public String generatePrivateKey() {
return ByteUtilities.toHexString(Secp256k1.generatePrivateKey());
}
@Override
public String generatePublicKey(String privateKey) {
return EthereumTools.getPublicKey(privateKey);
}
@Override
public Iterable<Iterable<String>> signWithPrivateKey(Iterable<Iterable<String>> data,
String privateKey) {
return signWithPrivateKey(data, privateKey, null);
}
private Iterable<Iterable<String>> signWithPrivateKey(Iterable<Iterable<String>> data,
String privateKey, String address) {
LOGGER.debug("Attempting to sign: " + address + data.toString());
LinkedList<Iterable<String>> signedData = new LinkedList<>();
LinkedList<Iterable<String>> listedData = new LinkedList<>();
data.forEach(listedData::add);
LinkedList<String> contractData = new LinkedList<>();
LinkedList<String> txData = new LinkedList<>();
// Check if there are two entries, if there are, the first one should be mSig data.
int txDataLocation = 0;
if (listedData.size() == 2) {
txDataLocation++;
listedData.get(0).forEach(contractData::add);
}
listedData.get(txDataLocation).forEach(txData::add);
try {
// Sign mSig if there is any
if (contractData.size() > 0) {
LOGGER.debug("Reading mSig data");
String sigBytes = contractData.getLast();
byte[][] sigData = signData(sigBytes, address, privateKey);
// Return the original TX on failure
if (sigData.length < 3) {
LinkedList<String> signature = new LinkedList<>();
signature.add(txData.getFirst());
LinkedList<Iterable<String>> result = new LinkedList<>();
result.add(signature);
return result;
}
LinkedList<String> msigSig = new LinkedList<>();
msigSig.add(ByteUtilities.toHexString(sigData[0]));
msigSig.add(ByteUtilities.toHexString(sigData[1]));
msigSig.add(ByteUtilities.toHexString(sigData[2]));
signedData.add(msigSig);
} else {
LOGGER.debug("No mSig data to process.");
}
// Rebuild the TX if there is any mSig data
RawTransaction rawTx =
RawTransaction.parseBytes(ByteUtilities.toByteArray(txData.getFirst()));
if (rawTx == null) {
LinkedList<String> signature = new LinkedList<>();
signature.add(txData.getFirst());
LinkedList<Iterable<String>> result = new LinkedList<>();
result.add(signature);
return result;
}
// If we've added mSig data then update the TX.
// Get the contract that corresponds to the recipients code if possible.
TokenContractInterface txContractInterface =
getContractVersion(ByteUtilities.toHexString(rawTx.getTo().getDecodedContents()));
if (txContractInterface == null) {
txContractInterface = contractInterface;
}
if (signedData.size() > 0 && txContractInterface.getContractParameters()
.parseTransfer(ByteUtilities.toHexString(rawTx.getData().getDecodedContents())) != null) {
// There are new signatures and the TX appears to be a transfer
// Load the right contract version so we put the data in the right places.
String contractVersion = contractData.getFirst();
TokenContractInterface contract =
(TokenContractInterface) TokenContractInterface.class.getClassLoader()
.loadClass(contractVersion).newInstance();
TokenContractParametersInterface contractParms = contract.getContractParameters();
Map<String, List<String>> contractParamData = contractParms
.parseTransfer(ByteUtilities.toHexString(rawTx.getData().getDecodedContents()));
// Append the signature data to data structures
Iterator<String> msigSig = signedData.getFirst().iterator();
contractParamData.get(TokenContractParametersInterface.SIGR).add(msigSig.next());
contractParamData.get(TokenContractParametersInterface.SIGS).add(msigSig.next());
contractParamData.get(TokenContractParametersInterface.SIGV).add(msigSig.next());
// Convert all the components into values we can pass into the transfer call
Long nonce =
new BigInteger(contractParamData.get(TokenContractParametersInterface.NONCE).get(0))
.longValue();
String sender = contractParamData.get(TokenContractParametersInterface.SENDER).get(0);
List<String> recipients =
contractParamData.get(TokenContractParametersInterface.RECIPIENTS);
List<BigInteger> amounts = new LinkedList<>();
for (String amount : contractParamData.get(TokenContractParametersInterface.AMOUNT)) {
amounts.add(new BigInteger(amount));
}
List<String> sigV = contractParamData.get(TokenContractParametersInterface.SIGV);
List<String> sigR = contractParamData.get(TokenContractParametersInterface.SIGR);
List<String> sigS = contractParamData.get(TokenContractParametersInterface.SIGS);
// Rebuild the function data
rawTx.getData().setDecodedContents(ByteUtilities.toByteArray(
contractParms.transfer(nonce, sender, recipients, amounts, sigV, sigR, sigS)));
} else if (signedData.size() > 0 && txContractInterface.getContractParameters()
.parseAdminFunction(ByteUtilities.toHexString(rawTx.getData().getDecodedContents()))
!= null) {
// There are new signatures and the TX appears to be an admin function
// Get the right contract version.
String contractVersion = contractData.getFirst();
TokenContractInterface contract =
(TokenContractInterface) TokenContractInterface.class.getClassLoader()
.loadClass(contractVersion).newInstance();
TokenContractParametersInterface contractParms = contract.getContractParameters();
Map<String, List<String>> contractParamData = contractParms
.parseAdminFunction(ByteUtilities.toHexString(rawTx.getData().getDecodedContents()));
// Update the contract data with the new signatures.
Iterator<String> msigSig = signedData.getFirst().iterator();
contractParamData.get(TokenContractParametersInterface.SIGR).add(msigSig.next());
contractParamData.get(TokenContractParametersInterface.SIGS).add(msigSig.next());
contractParamData.get(TokenContractParametersInterface.SIGV).add(msigSig.next());
// Rebuild the function data
rawTx.getData().setDecodedContents(
ByteUtilities.toByteArray(contractParms.rebuildAdminFunction(contractParamData)));
}
// Sign the TX itself so it can be broadcast.
String txCount = txData.getLast();
BigInteger nonce = new BigInteger(1, ByteUtilities.toByteArray(txCount));
// RLP formatting quirks
if (nonce.equals(BigInteger.ZERO)) {
rawTx.getNonce().setDecodedContents(new byte[]{});
} else {
rawTx.getNonce()
.setDecodedContents(ByteUtilities.stripLeadingNullBytes(nonce.toByteArray()));
}
String sigString = ByteUtilities.toHexString(rawTx.getSigBytes());
LOGGER.debug("Tx: " + ByteUtilities.toHexString(rawTx.encode()));
LOGGER.debug("SigBytes: " + sigString);
sigString = EthereumTools.hashKeccak(sigString);
LOGGER.debug("Hashed: " + sigString);
byte[][] sigData = signData(sigString, address, privateKey);
if (sigData.length < 3) {
// Signature data is bad, return the original transaction.
LinkedList<String> signature = new LinkedList<>();
signature.add(txData.getFirst());
LinkedList<Iterable<String>> result = new LinkedList<>();
result.add(signature);
return result;
}
// Apply the signature to the tx structure.
rawTx.getSigR().setDecodedContents(sigData[0]);
rawTx.getSigS().setDecodedContents(sigData[1]);
rawTx.getSigV().setDecodedContents(sigData[2]);
// Return the signed TX as-is, we don't need network information to apply it.
LinkedList<String> signature = new LinkedList<>();
signature.add(ByteUtilities.toHexString(rawTx.encode()));
LinkedList<Iterable<String>> result = new LinkedList<>();
result.add(signature);
result.add(new LinkedList<>(Collections.singletonList(sigString)));
return result;
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
// Something went very wrong, return the original transaction.
LOGGER.warn(null, e);
LinkedList<String> signature = new LinkedList<>();
signature.add(txData.getFirst());
LinkedList<Iterable<String>> result = new LinkedList<>();
result.add(signature);
return result;
}
}
private byte[][] signData(String data, String address, String name) {
if (name == null) {
// We have nothing to go on but address, we have to ask a 3rd party to sign for us.
// At this point, that's geth/whatever node software we're using.
String sig;
try {
LOGGER.debug("Asking geth to sign 0x" + data + " for 0x" + address);
sig = ethereumRpc.eth_sign("0x" + address, "0x" + data);
} catch (Exception e) {
LOGGER.warn(null, e);
return new byte[0][0];
}
try {
LOGGER.debug("Decoding sig result: " + sig);
byte[] sigBytes = ByteUtilities.toByteArray(sig);
byte[] sigR = Arrays.copyOfRange(sigBytes, 0, 32);
byte[] sigS = Arrays.copyOfRange(sigBytes, 32, 64);
byte[] sigV = Arrays.copyOfRange(sigBytes, 64, 65);
String signingAddress = null;
try {
signingAddress = ByteUtilities.toHexString(
Secp256k1.recoverPublicKey(sigR, sigS, sigV, ByteUtilities.toByteArray(data)))
.substring(2);
} catch (Exception e) {
LOGGER.debug("Couldn't recover public key from signature", e);
}
signingAddress = EthereumTools.getPublicAddress(signingAddress, false);
LOGGER.debug("Appears to be signed by: " + signingAddress);
// Adjust for expected format.
sigV[0] += 27;
return new byte[][]{sigR, sigS, sigV};
} catch (Exception e) {
LOGGER.error(null, e);
return new byte[0][0];
}
} else {
int maxRounds = 100;
// Attempt to recover the address as if it's a user key.
String privateKey = "";
if (address != null) {
for (int i = 1; i <= maxRounds; i++) {
String privateKeyCheck =
EthereumTools.getDeterministicPrivateKey(name, config.getServerPrivateKey(), i);
if (EthereumTools.getPublicAddress(privateKeyCheck).equalsIgnoreCase(address)) {
privateKey = privateKeyCheck;
break;
}
}
// If we couldn't match the address, assume the name is actually a private key
if (privateKey.isEmpty()) {
privateKey = name;
address = EthereumTools.getPublicAddress(privateKey);
}
} else {
// If the address is empty assume the name is a private key and generate it from that.
privateKey = name;
address = EthereumTools.getPublicAddress(privateKey);
}
// Sign and return it
byte[] privateBytes = ByteUtilities.toByteArray(privateKey);
byte[] sigBytes = ByteUtilities.toByteArray(data);
String signingAddress = "";
// The odd signature can't be resolved to a recoveryId, in those cases, just sign it again.
byte[] sigV;
byte[] sigR;
byte[] sigS;
do {
byte[][] signedBytes = Secp256k1.signTransaction(sigBytes, privateBytes);
// EIP-2
BigInteger lowSlimit =
new BigInteger("007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0",
16);
BigInteger ourSvalue = new BigInteger(1, signedBytes[1]);
while (ourSvalue.compareTo(lowSlimit) > 0) {
signedBytes = Secp256k1.signTransaction(sigBytes, privateBytes);
ourSvalue = new BigInteger(1, signedBytes[1]);
}
sigR = ByteUtilities.stripLeadingNullBytes(signedBytes[0]);
sigS = ByteUtilities.stripLeadingNullBytes(signedBytes[1]);
sigV = signedBytes[2];
if (sigV[0] != 0 && sigV[0] != 1) {
continue;
}
try {
signingAddress =
ByteUtilities.toHexString(Secp256k1.recoverPublicKey(sigR, sigS, sigV, sigBytes))
.substring(2);
} catch (Exception e) {
LOGGER.debug("Couldn't recover the public key", e);
}
signingAddress = EthereumTools.getPublicAddress(signingAddress, false);
} while (!address.equalsIgnoreCase(signingAddress));
// Adjust for ethereum's encoding
sigV[0] += 27;
return new byte[][]{sigR, sigS, sigV};
}
}
public String generateTokens(String recipient, long amount) {
Long nonce =
contractInterface.getContractParameters().getNonce(ethereumRpc, adminContractAddress);
RawTransaction tx = RawTransaction.createTransaction(config, adminContractAddress, null,
contractInterface.getContractParameters()
.createTokens(nonce, recipient, amount, new LinkedList<>(), new LinkedList<>(),
new LinkedList<>()));
return ByteUtilities.toHexString(tx.encode());
}
public String destroyTokens(String sender, long amount) {
Long nonce =
contractInterface.getContractParameters().getNonce(ethereumRpc, adminContractAddress);
RawTransaction tx = RawTransaction.createTransaction(config, adminContractAddress, null,
contractInterface.getContractParameters()
.destroyTokens(nonce, sender, amount, new LinkedList<>(), new LinkedList<>(),
new LinkedList<>()));
return ByteUtilities.toHexString(tx.encode());
}
public String reconcile(Map<String, BigInteger> addressChanges) {
Long nonce =
contractInterface.getContractParameters().getNonce(ethereumRpc, storageContractAddress);
RawTransaction tx = RawTransaction.createTransaction(config, storageContractAddress, null,
contractInterface.getContractParameters()
.reconcile(nonce, addressChanges, new LinkedList<>(), new LinkedList<>(),
new LinkedList<>()));
return ByteUtilities.toHexString(tx.encode());
}
public String allowance(String owner, String grantee) {
CallData callData = EthereumTools
.generateCall(contractInterface.getContractParameters().allowance(owner, grantee),
tokenContractAddress);
LOGGER.debug("Balance request: " + Json.stringifyObject(CallData.class, callData));
return ethereumRpc.eth_call(callData, DefaultBlock.LATEST.toString());
}
public String approve(String grantee, BigDecimal amount) {
RawTransaction tx = RawTransaction.createTransaction(config, tokenContractAddress, null,
contractInterface.getContractParameters()
.approve(grantee, amount.multiply(BigDecimal.TEN.pow((int)config.getDecimalPlaces())).toBigInteger()));
return ByteUtilities.toHexString(tx.encode());
}
}