package im.xiaoyao.presto.ethereum; import com.facebook.presto.spi.RecordCursor; import com.facebook.presto.spi.block.Block; import com.facebook.presto.spi.block.BlockBuilder; import com.facebook.presto.spi.block.BlockBuilderStatus; import com.facebook.presto.spi.type.StandardTypes; import com.facebook.presto.spi.type.Type; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import io.airlift.log.Logger; import io.airlift.slice.Slice; import io.airlift.slice.Slices; import org.joda.time.DateTimeZone; import org.web3j.protocol.Web3j; import org.web3j.protocol.core.methods.response.EthBlock; import org.web3j.protocol.core.methods.response.Log; import java.sql.Date; import java.sql.Timestamp; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; import java.util.stream.Collectors; import static com.facebook.presto.spi.type.BigintType.BIGINT; import static com.facebook.presto.spi.type.BooleanType.BOOLEAN; import static com.facebook.presto.spi.type.Chars.isCharType; import static com.facebook.presto.spi.type.Chars.truncateToLengthAndTrimSpaces; import static com.facebook.presto.spi.type.DateType.DATE; import static com.facebook.presto.spi.type.DoubleType.DOUBLE; import static com.facebook.presto.spi.type.IntegerType.INTEGER; import static com.facebook.presto.spi.type.RealType.REAL; import static com.facebook.presto.spi.type.SmallintType.SMALLINT; import static com.facebook.presto.spi.type.TimestampType.TIMESTAMP; import static com.facebook.presto.spi.type.TinyintType.TINYINT; import static com.facebook.presto.spi.type.VarbinaryType.VARBINARY; import static com.facebook.presto.spi.type.Varchars.isVarcharType; import static com.facebook.presto.spi.type.Varchars.truncateToLength; import static com.google.common.base.Preconditions.checkArgument; import static java.lang.Float.floatToRawIntBits; import static java.util.Objects.requireNonNull; public class EthereumRecordCursor implements RecordCursor { private static final Logger log = Logger.get(EthereumRecordCursor.class); private final EthBlock block; private final Iterator<EthBlock> blockIter; private final Iterator<EthBlock.TransactionResult> txIter; private final Iterator<Log> logIter; private final EthereumTable table; private final Web3j web3j; private final List<EthereumColumnHandle> columnHandles; private final int[] fieldToColumnIndex; private List<Supplier> suppliers; public EthereumRecordCursor(List<EthereumColumnHandle> columnHandles, EthBlock block, EthereumTable table, Web3j web3j) { this.columnHandles = columnHandles; this.table = table; this.web3j = web3j; this.suppliers = Collections.emptyList(); fieldToColumnIndex = new int[columnHandles.size()]; for (int i = 0; i < columnHandles.size(); i++) { EthereumColumnHandle columnHandle = columnHandles.get(i); fieldToColumnIndex[i] = columnHandle.getOrdinalPosition(); } // TODO: handle failure upstream this.block = requireNonNull(block, "block is null"); this.blockIter = ImmutableList.of(block).iterator(); this.txIter = block.getBlock().getTransactions().iterator(); this.logIter = new EthereumLogLazyIterator(block, web3j); } @Override public long getCompletedBytes() { return block.getBlock().getSize().longValue(); } @Override public long getReadTimeNanos() { return 0; } @Override public Type getType(int field) { checkArgument(field < columnHandles.size(), "Invalid field index"); return columnHandles.get(field).getType(); } @Override public boolean advanceNextPosition() { if (table == EthereumTable.BLOCK && !blockIter.hasNext() || table == EthereumTable.TRANSACTION && !txIter.hasNext() || table == EthereumTable.ERC20 && !logIter.hasNext()) { return false; } ImmutableList.Builder<Supplier> builder = ImmutableList.builder(); if (table == EthereumTable.BLOCK) { blockIter.next(); EthBlock.Block blockBlock = this.block.getBlock(); builder.add(blockBlock::getNumber); builder.add(blockBlock::getHash); builder.add(blockBlock::getParentHash); builder.add(blockBlock::getNonceRaw); builder.add(blockBlock::getSha3Uncles); builder.add(blockBlock::getLogsBloom); builder.add(blockBlock::getTransactionsRoot); builder.add(blockBlock::getStateRoot); builder.add(blockBlock::getMiner); builder.add(blockBlock::getDifficulty); builder.add(blockBlock::getTotalDifficulty); builder.add(blockBlock::getSize); builder.add(blockBlock::getExtraData); builder.add(blockBlock::getGasLimit); builder.add(blockBlock::getGasUsed); builder.add(blockBlock::getTimestamp); builder.add(() -> { return blockBlock.getTransactions() .stream() .map(tr -> ((EthBlock.TransactionObject) tr.get()).getHash()) .collect(Collectors.toList()); }); builder.add(blockBlock::getUncles); } else if (table == EthereumTable.TRANSACTION) { EthBlock.TransactionResult tr = txIter.next(); EthBlock.TransactionObject tx = (EthBlock.TransactionObject) tr.get(); builder.add(tx::getHash); builder.add(tx::getNonce); builder.add(tx::getBlockHash); builder.add(tx::getBlockNumber); builder.add(tx::getTransactionIndex); builder.add(tx::getFrom); builder.add(tx::getTo); builder.add(tx::getValue); builder.add(tx::getGas); builder.add(tx::getGasPrice); builder.add(tx::getInput); } else if (table == EthereumTable.ERC20) { while (logIter.hasNext()) { Log l = logIter.next(); List<String> topics = l.getTopics(); String data = l.getData(); if (topics.get(0).equalsIgnoreCase(EthereumERC20Utils.TRANSFER_EVENT_TOPIC)) { // Handle unindexed event fields: // if the number of topics and fields in data part != 4, then it's a weird event if (topics.size() < 3 && topics.size() + (data.length() - 2) / 64 != 4) { continue; } if (topics.size() < 3) { Iterator<String> dataFields = Splitter.fixedLength(64).split(data.substring(2)).iterator(); while (topics.size() < 3) { topics.add("0x" + dataFields.next()); } data = "0x" + dataFields.next(); } // Token contract address builder.add(() -> Optional.ofNullable(EthereumERC20Token.lookup.get(l.getAddress().toLowerCase())) .map(Enum::name).orElse(String.format("ERC20(%s)", l.getAddress()))); // from address builder.add(() -> h32ToH20(topics.get(1))); // to address builder.add(() -> h32ToH20(topics.get(2))); // amount value String finalData = data; builder.add(() -> EthereumERC20Utils.hexToDouble(finalData)); builder.add(l::getTransactionHash); builder.add(l::getBlockNumber); this.suppliers = builder.build(); return true; } } return false; } else { return false; } this.suppliers = builder.build(); return true; } @Override public boolean getBoolean(int field) { return (boolean) suppliers.get(fieldToColumnIndex[field]).get(); } @Override public long getLong(int field) { return ((Number) suppliers.get(fieldToColumnIndex[field]).get()).longValue(); } @Override public double getDouble(int field) { return ((Number) suppliers.get(fieldToColumnIndex[field]).get()).doubleValue(); } @Override public Slice getSlice(int field) { return Slices.utf8Slice((String) suppliers.get(fieldToColumnIndex[field]).get()); } @Override public Object getObject(int field) { return serializeObject(columnHandles.get(field).getType(), null, suppliers.get(fieldToColumnIndex[field]).get()); } @Override public boolean isNull(int field) { return suppliers.get(fieldToColumnIndex[field]).get() == null; } @Override public void close() { } private static long getLongExpressedValue(Object value) { if (value instanceof Date) { long storageTime = ((Date) value).getTime(); // convert date from VM current time zone to UTC long utcMillis = storageTime + DateTimeZone.getDefault().getOffset(storageTime); return TimeUnit.MILLISECONDS.toDays(utcMillis); } if (value instanceof Timestamp) { long parsedJvmMillis = ((Timestamp) value).getTime(); DateTimeZone jvmTimeZone = DateTimeZone.getDefault(); long convertedMillis = jvmTimeZone.convertUTCToLocal(parsedJvmMillis); return convertedMillis; } if (value instanceof Float) { return floatToRawIntBits(((Float) value)); } return ((Number) value).longValue(); } private static Slice getSliceExpressedValue(Object value, Type type) { Slice sliceValue; if (value instanceof String) { sliceValue = Slices.utf8Slice((String) value); } else if (value instanceof byte[]) { sliceValue = Slices.wrappedBuffer((byte[]) value); } else if (value instanceof Integer) { sliceValue = Slices.utf8Slice(value.toString()); } else { throw new IllegalStateException("unsupported string field type: " + value.getClass().getName()); } if (isVarcharType(type)) { sliceValue = truncateToLength(sliceValue, type); } if (isCharType(type)) { sliceValue = truncateToLengthAndTrimSpaces(sliceValue, type); } return sliceValue; } private static Block serializeObject(Type type, BlockBuilder builder, Object object) { if (!isStructuralType(type)) { serializePrimitive(type, builder, object); return null; } else if (isArrayType(type)) { return serializeList(type, builder, object); } else if (isMapType(type)) { return serializeMap(type, builder, object); } else if (isRowType(type)) { return serializeStruct(type, builder, object); } throw new RuntimeException("Unknown object type: " + type); } private static Block serializeList(Type type, BlockBuilder builder, Object object) { List<?> list = (List) object; if (list == null) { requireNonNull(builder, "parent builder is null").appendNull(); return null; } List<Type> typeParameters = type.getTypeParameters(); checkArgument(typeParameters.size() == 1, "list must have exactly 1 type parameter"); Type elementType = typeParameters.get(0); BlockBuilder currentBuilder; if (builder != null) { currentBuilder = builder.beginBlockEntry(); } else { currentBuilder = elementType.createBlockBuilder(new BlockBuilderStatus(), list.size()); } for (Object element : list) { serializeObject(elementType, currentBuilder, element); } if (builder != null) { builder.closeEntry(); return null; } else { Block resultBlock = currentBuilder.build(); return resultBlock; } } private static Block serializeMap(Type type, BlockBuilder builder, Object object) { Map<?, ?> map = (Map) object; if (map == null) { requireNonNull(builder, "parent builder is null").appendNull(); return null; } List<Type> typeParameters = type.getTypeParameters(); checkArgument(typeParameters.size() == 2, "map must have exactly 2 type parameter"); Type keyType = typeParameters.get(0); Type valueType = typeParameters.get(1); boolean builderSynthesized = false; if (builder == null) { builderSynthesized = true; builder = type.createBlockBuilder(new BlockBuilderStatus(), 1); } BlockBuilder currentBuilder = builder.beginBlockEntry(); for (Map.Entry<?, ?> entry : map.entrySet()) { // Hive skips map entries with null keys if (entry.getKey() != null) { serializeObject(keyType, currentBuilder, entry.getKey()); serializeObject(valueType, currentBuilder, entry.getValue()); } } builder.closeEntry(); if (builderSynthesized) { return (Block) type.getObject(builder, 0); } else { return null; } } private static Block serializeStruct(Type type, BlockBuilder builder, Object object) { if (object == null) { requireNonNull(builder, "parent builder is null").appendNull(); return null; } List<Type> typeParameters = type.getTypeParameters(); EthBlock.TransactionObject structData = (EthBlock.TransactionObject) object; boolean builderSynthesized = false; if (builder == null) { builderSynthesized = true; builder = type.createBlockBuilder(new BlockBuilderStatus(), 1); } BlockBuilder currentBuilder = builder.beginBlockEntry(); ImmutableList.Builder<Supplier> lstBuilder = ImmutableList.builder(); lstBuilder.add(structData::getHash); lstBuilder.add(structData::getNonce); lstBuilder.add(structData::getBlockHash); lstBuilder.add(structData::getBlockNumber); lstBuilder.add(structData::getTransactionIndex); lstBuilder.add(structData::getFrom); lstBuilder.add(structData::getTo); lstBuilder.add(structData::getValue); lstBuilder.add(structData::getGas); lstBuilder.add(structData::getGasPrice); lstBuilder.add(structData::getInput); ImmutableList<Supplier> txColumns = lstBuilder.build(); for (int i = 0; i < typeParameters.size(); i++) { serializeObject(typeParameters.get(i), currentBuilder, txColumns.get(i).get()); } builder.closeEntry(); if (builderSynthesized) { return (Block) type.getObject(builder, 0); } else { return null; } } private static void serializePrimitive(Type type, BlockBuilder builder, Object object) { requireNonNull(builder, "parent builder is null"); if (object == null) { builder.appendNull(); return; } if (BOOLEAN.equals(type)) { BOOLEAN.writeBoolean(builder, (Boolean) object); } else if (BIGINT.equals(type) || INTEGER.equals(type) || SMALLINT.equals(type) || TINYINT.equals(type) || REAL.equals(type) || DATE.equals(type) || TIMESTAMP.equals(type)) { type.writeLong(builder, getLongExpressedValue(object)); } else if (DOUBLE.equals(type)) { DOUBLE.writeDouble(builder, ((Number) object).doubleValue()); } else if (isVarcharType(type) || VARBINARY.equals(type) || isCharType(type)) { type.writeSlice(builder, getSliceExpressedValue(object, type)); } else { throw new UnsupportedOperationException("Unsupported primitive type: " + type); } } public static boolean isArrayType(Type type) { return type.getTypeSignature().getBase().equals(StandardTypes.ARRAY); } public static boolean isMapType(Type type) { return type.getTypeSignature().getBase().equals(StandardTypes.MAP); } public static boolean isRowType(Type type) { return type.getTypeSignature().getBase().equals(StandardTypes.ROW); } public static boolean isStructuralType(Type type) { String baseName = type.getTypeSignature().getBase(); return baseName.equals(StandardTypes.MAP) || baseName.equals(StandardTypes.ARRAY) || baseName.equals(StandardTypes.ROW); } private static String h32ToH20(String h32) { return "0x" + h32.substring(EthereumMetadata.H32_BYTE_HASH_STRING_LENGTH - EthereumMetadata.H20_BYTE_HASH_STRING_LENGTH + 2); } }