package com.alphawallet.app.repository; import android.util.Log; import com.alphawallet.app.C; import com.alphawallet.app.entity.ContractType; import com.alphawallet.app.entity.NetworkInfo; import com.alphawallet.app.entity.Transaction; import com.alphawallet.app.entity.TransactionData; import com.alphawallet.app.entity.Wallet; import com.alphawallet.app.entity.cryptokeys.SignatureFromKey; import com.alphawallet.app.entity.cryptokeys.SignatureReturnType; import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.entity.tokens.TokenInfo; import com.alphawallet.app.service.AccountKeystoreService; import com.alphawallet.app.service.TokensService; import com.alphawallet.app.service.TransactionsNetworkClientType; import org.web3j.crypto.RawTransaction; import org.web3j.crypto.Sign; import org.web3j.crypto.TransactionEncoder; import org.web3j.protocol.Web3j; import org.web3j.protocol.core.methods.response.EthSendTransaction; import org.web3j.rlp.RlpEncoder; import org.web3j.rlp.RlpList; import org.web3j.rlp.RlpType; import org.web3j.utils.Numeric; import java.math.BigInteger; import java.util.List; import io.reactivex.Observable; import io.reactivex.Single; import io.reactivex.schedulers.Schedulers; import static com.alphawallet.app.entity.CryptoFunctions.sigFromByteArray; import static com.alphawallet.app.repository.TokenRepository.getWeb3jService; import static com.alphawallet.app.service.KeyService.FAILED_SIGNATURE; public class TransactionRepository implements TransactionRepositoryType { private final String TAG = "TREPO"; private final EthereumNetworkRepositoryType networkRepository; private final AccountKeystoreService accountKeystoreService; private final TransactionLocalSource inDiskCache; private final TransactionsNetworkClientType blockExplorerClient; public TransactionRepository( EthereumNetworkRepositoryType networkRepository, AccountKeystoreService accountKeystoreService, TransactionLocalSource inDiskCache, TransactionsNetworkClientType blockExplorerClient) { this.networkRepository = networkRepository; this.accountKeystoreService = accountKeystoreService; this.blockExplorerClient = blockExplorerClient; this.inDiskCache = inDiskCache; } @Override public Observable<Transaction[]> fetchCachedTransactions(Wallet wallet, int maxTransactions, List<Integer> networkFilters) { Log.d(TAG, "Fetching Cached TX: " + wallet.address); return fetchFromCache(wallet, maxTransactions, networkFilters) .observeOn(Schedulers.newThread()) .toObservable(); } @Override public Transaction fetchCachedTransaction(String walletAddr, String hash) { Wallet wallet = new Wallet(walletAddr); return inDiskCache.fetchTransaction(wallet, hash); } @Override public Observable<Transaction[]> fetchNetworkTransaction(NetworkInfo network, String tokenAddress, long lastBlock, String userAddress) { return fetchFromNetwork(network, tokenAddress, lastBlock, userAddress) .observeOn(Schedulers.newThread()) .toObservable(); } @Override public Single<String> createTransaction(Wallet from, String toAddress, BigInteger subunitAmount, BigInteger gasPrice, BigInteger gasLimit, byte[] data, int chainId) { final Web3j web3j = getWeb3jService(chainId); final BigInteger useGasPrice = gasPriceForNode(chainId, gasPrice); return networkRepository.getLastTransactionNonce(web3j, from.address) .flatMap(nonce -> accountKeystoreService.signTransaction(from, toAddress, subunitAmount, useGasPrice, gasLimit, nonce.longValue(), data, chainId)) .flatMap(signedMessage -> Single.fromCallable( () -> { if (signedMessage.sigType != SignatureReturnType.SIGNATURE_GENERATED) { throw new Exception(signedMessage.failMessage); } EthSendTransaction raw = web3j .ethSendRawTransaction(Numeric.toHexString(signedMessage.signature)) .send(); if (raw.hasError()) { throw new Exception(raw.getError().getMessage()); } return raw.getTransactionHash(); })) .flatMap(txHash -> storeUnconfirmedTransaction(from, txHash, toAddress, subunitAmount, useGasPrice, chainId, data != null ? Numeric.toHexString(data) : "0x")) .subscribeOn(Schedulers.io()); } @Override public Single<TransactionData> createTransactionWithSig(Wallet from, String toAddress, BigInteger subunitAmount, BigInteger gasPrice, BigInteger gasLimit, byte[] data, int chainId) { final Web3j web3j = getWeb3jService(chainId); final BigInteger useGasPrice = gasPriceForNode(chainId, gasPrice); TransactionData txData = new TransactionData(); return networkRepository.getLastTransactionNonce(web3j, from.address) .flatMap(nonce -> accountKeystoreService.signTransaction(from, toAddress, subunitAmount, useGasPrice, gasLimit, nonce.longValue(), data, chainId)) .flatMap(signedMessage -> Single.fromCallable( () -> { if (signedMessage.sigType != SignatureReturnType.SIGNATURE_GENERATED) { throw new Exception(signedMessage.failMessage); } txData.signature = Numeric.toHexString(signedMessage.signature); EthSendTransaction raw = web3j .ethSendRawTransaction(Numeric.toHexString(signedMessage.signature)) .send(); if (raw.hasError()) { throw new Exception(raw.getError().getMessage()); } txData.txHash = raw.getTransactionHash(); return txData; })) .flatMap(tx -> storeUnconfirmedTransaction(from, tx, toAddress, subunitAmount, useGasPrice, chainId, data != null ? Numeric.toHexString(data) : "0x", "")) .subscribeOn(Schedulers.io()); } // Called for constructors from web3 Dapp transaction @Override public Single<TransactionData> createTransactionWithSig(Wallet from, BigInteger gasPrice, BigInteger gasLimit, String data, int chainId) { final Web3j web3j = getWeb3jService(chainId); final BigInteger useGasPrice = gasPriceForNode(chainId, gasPrice); TransactionData txData = new TransactionData(); return networkRepository.getLastTransactionNonce(web3j, from.address) .flatMap(nonce -> getRawTransaction(nonce, useGasPrice, gasLimit, BigInteger.ZERO, data)) .flatMap(rawTx -> signEncodeRawTransaction(rawTx, from, chainId)) .flatMap(signedMessage -> Single.fromCallable( () -> { txData.signature = Numeric.toHexString(signedMessage); EthSendTransaction raw = web3j .ethSendRawTransaction(Numeric.toHexString(signedMessage)) .send(); if (raw.hasError()) { throw new Exception(raw.getError().getMessage()); } txData.txHash = raw.getTransactionHash(); return txData; })) .flatMap(tx -> storeUnconfirmedTransaction(from, tx, "", BigInteger.ZERO, useGasPrice, chainId, data, C.BURN_ADDRESS)) .subscribeOn(Schedulers.io()); } private BigInteger gasPriceForNode(int chainId, BigInteger gasPrice) { if (EthereumNetworkRepository.hasGasOverride(chainId)) return EthereumNetworkRepository.gasOverrideValue(chainId); else return gasPrice; } private Single<TransactionData> storeUnconfirmedTransaction(Wallet from, TransactionData txData, String toAddress, BigInteger value, BigInteger gasPrice, int chainId, String data, String contractAddr) { return Single.fromCallable(() -> { Transaction newTx = new Transaction(txData.txHash, "0", "0", System.currentTimeMillis()/1000, 0, from.address, toAddress, value.toString(10), "0", gasPrice.toString(10), data, "0", chainId, contractAddr); inDiskCache.putTransaction(from, newTx); return txData; }); } private Single<String> storeUnconfirmedTransaction(Wallet from, String txHash, String toAddress, BigInteger value, BigInteger gasPrice, int chainId, String data) { return Single.fromCallable(() -> { Transaction newTx = new Transaction(txHash, "0", "0", System.currentTimeMillis()/1000, 0, from.address, toAddress, value.toString(10), "0", gasPrice.toString(10), data, "0", chainId, ""); inDiskCache.putTransaction(from, newTx); return txHash; }); } private Single<RawTransaction> getRawTransaction(BigInteger nonce, BigInteger gasPrice, BigInteger gasLimit, BigInteger value, String data) { return Single.fromCallable(() -> RawTransaction.createContractTransaction( nonce, gasPrice, gasLimit, value, data)); } private Single<byte[]> signEncodeRawTransaction(RawTransaction rtx, Wallet wallet, int chainId) { return Single.fromCallable(() -> TransactionEncoder.encode(rtx)) .flatMap(encoded -> accountKeystoreService.signTransaction(wallet, encoded, chainId)) .flatMap(signatureReturn -> { if (signatureReturn.sigType != SignatureReturnType.SIGNATURE_GENERATED) { throw new Exception(signatureReturn.failMessage); } return encodeTransaction(signatureReturn.signature, rtx); }); } private Single<byte[]> encodeTransaction(byte[] signatureBytes, RawTransaction rtx) { return Single.fromCallable(() -> { Sign.SignatureData sigData = sigFromByteArray(signatureBytes); if (sigData == null) return FAILED_SIGNATURE.getBytes(); return encode(rtx, sigData); }); } @Override public Single<SignatureFromKey> getSignature(Wallet wallet, byte[] message, int chainId) { return accountKeystoreService.signTransaction(wallet, message, chainId); } @Override public Single<byte[]> getSignatureFast(Wallet wallet, String password, byte[] message, int chainId) { return accountKeystoreService.signTransactionFast(wallet, password, message, chainId); } private Single<Transaction[]> fetchFromCache(Wallet wallet, int maxTransactions, List<Integer> networkFilters) { return inDiskCache.fetchTransaction(wallet, maxTransactions, networkFilters); } private Single<Transaction[]> fetchFromNetwork(NetworkInfo networkInfo, String tokenAddress, long lastBlock, String userAddress) { return blockExplorerClient.fetchLastTransactions(networkInfo, tokenAddress, lastBlock, userAddress); } @Override public Single<Transaction[]> fetchTransactionsFromStorage(Wallet wallet, Token token, int count) { return inDiskCache.fetchTransactions(wallet, token, count); } @Override public Single<Transaction[]> storeTransactions(Wallet wallet, Transaction[] txList) { if (txList.length == 0) { return noTransactions(); } else { return inDiskCache.putAndReturnTransactions(wallet, txList); } } private Single<Transaction[]> noTransactions() { return Single.fromCallable(() -> new Transaction[0]); } /** * From Web3j to encode a constructor * @param rawTransaction * @param signatureData * @return */ private static byte[] encode(RawTransaction rawTransaction, Sign.SignatureData signatureData) { List<RlpType> values = TransactionEncoder.asRlpValues(rawTransaction, signatureData); RlpList rlpList = new RlpList(values); return RlpEncoder.encode(rlpList); } @Override public Single<ContractType> queryInterfaceSpec(String address, TokenInfo tokenInfo) { NetworkInfo networkInfo = networkRepository.getNetworkByChain(tokenInfo.chainId); ContractType checked = TokensService.checkInterfaceSpec(tokenInfo.chainId, tokenInfo.address); if (tokenInfo.name == null && tokenInfo.symbol == null) { return Single.fromCallable(() -> ContractType.NOT_SET); } else if (checked != null && checked != ContractType.NOT_SET && checked != ContractType.OTHER) { return Single.fromCallable(() -> checked); } else return blockExplorerClient.checkConstructorArgs(networkInfo, address); } }