package com.alphawallet.app.entity; import android.content.Context; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; import com.alphawallet.app.C; import com.alphawallet.app.R; import com.alphawallet.app.repository.EthereumNetworkRepository; import com.alphawallet.token.tools.ParseMagicLink; import com.google.gson.annotations.SerializedName; import com.alphawallet.app.entity.tokens.Token; import org.web3j.crypto.Keys; import org.web3j.crypto.Sign; import java.math.BigDecimal; import java.math.BigInteger; import java.util.Arrays; import java.util.Collections; import java.util.List; /** * * This is supposed to be a generic transaction class which can * contain all of 3 stages of a transaction: * * 1. being compiled, in progress, or ready to be signed; * 2. compiled and signed, or ready to be broadcasted; * 2. already broadcasted, obtained in its raw format from a node, * including the signatures in it; * 4. already included in a blockchain. */ public class Transaction implements Parcelable { @SerializedName("id") public final String hash; public final String blockNumber; public final long timeStamp; public final int nonce; public final String from; public final String to; public final String value; public final String gas; public final String gasPrice; public final String gasUsed; public final String input; public final TransactionOperation[] operations; public final String error; public final int chainId; public boolean isConstructor = false; private static TransactionDecoder decoder = null; private static ParseMagicLink parser = null; public Transaction( String hash, String error, String blockNumber, long timeStamp, int nonce, String from, String to, String value, String gas, String gasPrice, String input, String gasUsed, int chainId, TransactionOperation[] operations) { this.hash = hash; this.error = error; this.blockNumber = blockNumber; this.timeStamp = timeStamp; this.nonce = nonce; this.from = from; this.to = to; this.value = value; this.gas = gas; this.gasPrice = gasPrice; this.input = input; this.gasUsed = gasUsed; this.chainId = chainId; this.operations = operations; } public Transaction(String hash, String isError, String blockNumber, long timeStamp, int nonce, String from, String to, String value, String gas, String gasPrice, String input, String gasUsed, int chainId, String contractAddress) { //build transaction using input TransactionInput f; if (!TextUtils.isEmpty(contractAddress)) //must be a constructor { if (decoder == null) decoder = new TransactionDecoder(); //initialise decoder on demand to = contractAddress; //add a constructor here operations = generateERC875Op(); TransactionContract ct = operations[0].contract; ct.setOperation(TransactionType.CONSTRUCTOR); ct.address = contractAddress; ct.setType(-3);// indicate that we need to load the contract operations[0].value = ""; isConstructor = true; ContractType type = decoder.getContractType(input); ct.decimals = type.ordinal(); input = "Constructor"; //Placeholder - don't consume storage for the constructor } else { //Now perform as complete processing as we are able to here. This saves re-allocating and makes code far less brittle. TransactionOperation[] o = new TransactionOperation[0]; //TODO: Handle transaction with multiple operations if (input != null && input.length() >= 10) { TransactionOperation op = null; TransactionContract ct; if (decoder == null) decoder = new TransactionDecoder(); //initialise decoder on demand f = decoder.decodeInput(input); //is this a trade? if (f.functionData != null) { //recover recipient switch (f.functionData.functionFullName) { case "trade(uint256,uint16[],uint8,bytes32,bytes32)": case "trade(uint256,uint256[],uint8,bytes32,bytes32)": o = processTrade(f, contractAddress); op = o[0]; setName(o, TransactionType.MAGICLINK_TRANSFER); op.contract.address = to; op.value = String.valueOf(f.paramValues.size()); break; case "transferFrom(address,address,uint16[])": case "transferFrom(address,address,uint256[])": o = generateERC875Op(); op = o[0]; op.contract.setIndicies(f.paramValues); if (f.containsAddress(C.BURN_ADDRESS)) { setName(o, TransactionType.REDEEM); } else { setName(o, TransactionType.TRANSFER_FROM); } op.contract.setType(-1); op.contract.address = to; op.contract.setOtherParty(f.getFirstAddress()); op.value = String.valueOf(f.paramValues.size()); op.to = f.getAddress(1); break; case "transfer(address,uint16[])": case "transfer(address,uint256[])": o = generateERC875Op(); op = o[0]; op.contract.setOtherParty(f.getFirstAddress()); op.contract.setIndicies(f.paramValues); setName(o, TransactionType.TRANSFER_TO); op.value = String.valueOf(f.paramValues.size()); op.contract.address = to; break; case "transfer(address,uint256)": o = generateERC20Op(); op = o[0]; op.from = from; op.to = f.getFirstAddress(); op.value = String.valueOf(f.getFirstValue()); op.contract.address = to; setName(o, TransactionType.TRANSFER_TO); break; case "transferFrom(address,address,uint256)": o = generateERC20Op(); op = o[0]; op.from = f.getFirstAddress(); op.to = f.getAddress(1); op.value = String.valueOf(f.getFirstValue()); op.contract.address = to; setName(o, TransactionType.TRANSFER_FROM); op.contract.setType(1); break; case "allocateTo(address,uint256)": o = generateERC20Op(); op = o[0]; op.from = from; op.to = f.getFirstAddress(); op.value = String.valueOf(f.getFirstValue()); op.contract.address = to; setName(o, TransactionType.ALLOCATE_TO); break; case "approve(address,uint256)": o = generateERC20Op(); op = o[0]; op.from = from; op.to = f.getFirstAddress(); op.value = String.valueOf(f.getFirstValue()); op.contract.address = to; setName(o, TransactionType.APPROVE); break; case "loadNewTickets(bytes32[])": case "loadNewTickets(uint256[])": o = generateERC875Op(); op = o[0]; op.from = from; op.value = String.valueOf(f.paramValues.size()); op.contract.address = to; setName(o, TransactionType.LOAD_NEW_TOKENS); op.contract.setType(1); break; case "passTo(uint256,uint16[],uint8,bytes32,bytes32,address)": case "passTo(uint256,uint256[],uint8,bytes32,bytes32,address)": o = processPassTo(f, contractAddress); op = o[0]; op.from = from; op.to = f.getFirstAddress(); op.value = String.valueOf(f.paramValues.size()); op.contract.address = to; setName(o, TransactionType.PASS_TO); op.contract.setType(-1); break; case "endContract()": case "selfdestruct()": case "kill()": o = generateERC875Op(); op = o[0]; ct = op.contract; ct.setOperation(TransactionType.TERMINATE_CONTRACT); ct.name = to; ct.setType(-2); setName(o, TransactionType.TERMINATE_CONTRACT); op.value = ""; ct.address = to; break; default: break; } if (op != null) { op.transactionId = hash; } } } operations = o; } this.to = to; this.hash = hash; this.error = isError; this.blockNumber = blockNumber; this.timeStamp = timeStamp; this.nonce = nonce; this.from = from; this.value = value; this.gas = gas; this.gasPrice = gasPrice; this.input = input; this.gasUsed = gasUsed; this.chainId = chainId; } public String getTokenAddress(String walletAddress) { if (operations == null || operations.length == 0) { return walletAddress; } else return to; } protected Transaction(Parcel in) { hash = in.readString(); error = in.readString(); blockNumber = in.readString(); timeStamp = in.readLong(); nonce = in.readInt(); from = in.readString(); to = in.readString(); value = in.readString(); gas = in.readString(); gasPrice = in.readString(); input = in.readString(); gasUsed = in.readString(); chainId = in.readInt(); Parcelable[] parcelableArray = in.readParcelableArray(TransactionOperation.class.getClassLoader()); TransactionOperation[] operations = null; if (parcelableArray != null) { operations = Arrays.copyOf(parcelableArray, parcelableArray.length, TransactionOperation[].class); } this.operations = operations; } public static final Creator<Transaction> CREATOR = new Creator<Transaction>() { @Override public Transaction createFromParcel(Parcel in) { return new Transaction(in); } @Override public Transaction[] newArray(int size) { return new Transaction[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(hash); dest.writeString(error); dest.writeString(blockNumber); dest.writeLong(timeStamp); dest.writeInt(nonce); dest.writeString(from); dest.writeString(to); dest.writeString(value); dest.writeString(gas); dest.writeString(gasPrice); dest.writeString(input); dest.writeString(gasUsed); dest.writeInt(chainId); dest.writeParcelableArray(operations, flags); } public static void sortTransactions(List<Transaction> txList) { Collections.sort(txList, (e1, e2) -> { long w1 = e1.timeStamp; long w2 = e2.timeStamp; if (w1 > w2) return -1; if (w1 < w2) return 1; return 0; }); } public boolean isRelated(String contractAddress, String walletAddress) { TransactionOperation operation = operations == null || operations.length == 0 ? null : operations[0]; if (walletAddress.equals(contractAddress)) //transactions sent from or sent to the main currency account { return from.equals(walletAddress) || to.equals(walletAddress); } else { if (to.equals(contractAddress)) return true; if (operation != null && (operations[0].contract.address.equals(contractAddress))) return true; } return false; } public TransactionContract getOperation() { return operations == null || operations.length == 0 ? null : operations[0].contract; } private TransactionOperation[] generateERC20Op() { TransactionOperation[] o = new TransactionOperation[1]; TransactionOperation op = new TransactionOperation(); TransactionContract ct = new TransactionContract(); o[0] = op; op.contract = ct; return o; } private TransactionOperation[] generateERC875Op() { TransactionOperation[] o = new TransactionOperation[1]; TransactionOperation op = new TransactionOperation(); ERC875ContractTransaction ct = new ERC875ContractTransaction(); o[0] = op; op.contract = ct; return o; } private TransactionOperation[] processPassTo(TransactionInput f, String contractAddress) { TransactionOperation[] o = processTrade(f, contractAddress); if (o.length > 0) { o[0].contract.totalSupply = f.getFirstAddress(); //store destination address for this passTo. We don't use totalSupply for anything else in this case } return o; } private TransactionOperation[] processTrade(TransactionInput f, String contractAddress) { TransactionOperation[] o; try { Sign.SignatureData sig = decoder.getSignatureData(f); //ecrecover the recipient of the ether int[] ticketIndexArray = decoder.getIndices(f); String expiryStr = f.miscData.get(0); long expiry = Long.valueOf(expiryStr, 16); BigInteger priceWei = new BigInteger(value); contractAddress = to; o = generateERC875Op(); TransactionOperation op = o[0]; TransactionContract ct = op.contract; if (error.equals("0")) //don't bother checking signature unless the transaction succeeded { if (parser == null) parser = new ParseMagicLink(new CryptoFunctions(), EthereumNetworkRepository.extraChains()); //parser on demand byte[] tradeBytes = parser.getTradeBytes(ticketIndexArray, contractAddress, priceWei, expiry); //attempt ecrecover BigInteger key = Sign.signedMessageToKey(tradeBytes, sig); ct.setOtherParty("0x" + Keys.getAddress(key)); } ct.address = contractAddress; ct.setIndicies(f.paramValues); ct.name = contractAddress; } catch (Exception e) { o = generateERC875Op(); e.printStackTrace(); } return o; } private void setName(TransactionOperation[] o, TransactionType name) { if (o.length > 0 && o[0] != null) { TransactionOperation op = o[0]; TransactionContract ct = op.contract; if (ct instanceof ERC875ContractTransaction) { ((ERC875ContractTransaction) ct).operation = name; } else { op.contract.name = "*" + String.valueOf(name.ordinal()); } } } /** * Fetch result of transaction operation. * This is very much a WIP * @param token * @return */ public String getOperationResult(Token token, int precision) { if (operations == null || operations.length == 0) return token.getTransactionValue(this, precision); if (error.equals("1")) return ""; //TODO: Handle multiple operation transactions TransactionOperation operation = operations[0]; return operation.getOperationResult(token, this); } public String getOperationTokenAddress() { TransactionOperation operation = operations == null || operations.length == 0 ? null : operations[0]; if (operation == null || operation.contract == null) { return ""; } else { return operation.contract.address; } } public String getOperationName(Context ctx) { String txName = null; try { if (blockNumber != null && blockNumber.equals("0")) { txName = ctx.getString(R.string.status_pending); } else if (operations != null && operations.length > 0) { TransactionOperation operation = operations[0]; txName = operation.getOperationName(ctx); } } catch (NumberFormatException e) { //Silent fail, number was invalid just display default } return txName; } public String getContract(Token token) { TransactionOperation operation = operations == null || operations.length == 0 ? null : operations[0]; if (operation == null || operation.contract == null) { return token.getAddress(); } else { return token.getFullName(); } } public int getOperationImage(Token token) { TransactionOperation operation = operations == null || operations.length == 0 ? null : operations[0]; if (operation == null || operation.contract == null) { return from.equalsIgnoreCase(token.getWallet()) ? R.drawable.ic_arrow_downward_black_24dp : R.drawable.ic_arrow_upward_black_24dp; } else { return operation.contract.getOperationImage(token, this); } } /** * Supplimental info in this case is the intrinsic root value attached to a contract call * EG: Calling cryptokitties ERC721 'breedWithAuto' function requires you to call the function and also attach a small amount of ETH * for the 'breeding fee'. That fee is later released to the caller of the 'birth' function. * Supplimental info for these transaction would appear as -0.031 for the 'breedWithAuto' and +0.031 on the 'birth' call * However it's not that simple - the 'breeding fee' will be in the value attached to the transaction, however the 'midwife' reward appears * as an internal transaction, so won't be in the 'value' property. * * @return */ public String getSupplementalInfo(String walletAddress, String networkName) { TransactionOperation operation = operations == null || operations.length == 0 ? null : operations[0]; if (operation == null || operation.contract == null) { return ""; } else { return operation.contract.getSupplimentalInfo(this, walletAddress, networkName); } } public String getPrefix(Token token) { boolean isSent = token.getIsSent(this); boolean isSelf = from.equalsIgnoreCase(to); if (isSelf) return ""; else if (isSent) return "-"; else return "+"; } public BigDecimal getRawValue() throws Exception { if (operations == null || operations.length == 0) { return new BigDecimal(value); } else { TransactionOperation operation = operations[0]; return operation.getRawValue(); } } }