ContractBaseTemplate.java

/*
 * @copyright defined in LICENSE.txt
 */

package hera.client.internal;

import static hera.util.TransportUtils.copyFrom;
import static org.slf4j.LoggerFactory.getLogger;
import static types.AergoRPCServiceGrpc.newFutureStub;
import static types.AergoRPCServiceGrpc.newStub;

import com.google.protobuf.ByteString;
import hera.ContextProvider;
import hera.ContextProviderInjectable;
import hera.annotation.ApiAudience;
import hera.annotation.ApiStability;
import hera.api.function.Function1;
import hera.api.function.Function2;
import hera.api.function.Function4;
import hera.api.function.Function5;
import hera.api.model.AccountAddress;
import hera.api.model.BytesValue;
import hera.api.model.ContractAddress;
import hera.api.model.ContractDefinition;
import hera.api.model.ContractInterface;
import hera.api.model.ContractInvocation;
import hera.api.model.ContractResult;
import hera.api.model.ContractTxHash;
import hera.api.model.ContractTxReceipt;
import hera.api.model.Event;
import hera.api.model.EventFilter;
import hera.api.model.Fee;
import hera.api.model.RawTransaction;
import hera.api.model.Subscription;
import hera.api.model.Transaction;
import hera.api.model.TxHash;
import hera.client.ChannelInjectable;
import hera.client.stream.GrpcStreamObserverAdaptor;
import hera.client.stream.GrpcStreamSubscription;
import hera.key.Signer;
import hera.spec.resolver.PayloadResolver;
import hera.spec.resolver.PayloadSpec.Type;
import hera.transport.AccountAddressConverterFactory;
import hera.transport.ContractInterfaceConverterFactory;
import hera.transport.ContractResultConverterFactory;
import hera.transport.EventConverterFactory;
import hera.transport.EventFilterConverterFactory;
import hera.transport.ModelConverter;
import hera.transport.TxReceiptConverterFactory;
import io.grpc.Context;
import io.grpc.ManagedChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future;
import lombok.Getter;
import org.slf4j.Logger;
import types.AergoRPCServiceGrpc.AergoRPCServiceFutureStub;
import types.AergoRPCServiceGrpc.AergoRPCServiceStub;
import types.Blockchain;
import types.Rpc;

@ApiAudience.Private
@ApiStability.Unstable
public class ContractBaseTemplate implements ChannelInjectable, ContextProviderInjectable {

  protected final transient Logger logger = getLogger(getClass());

  protected final ModelConverter<AccountAddress, ByteString> accountAddressConverter =
      new AccountAddressConverterFactory().create();

  protected final ModelConverter<ContractTxReceipt, Blockchain.Receipt> receiptConverter =
      new TxReceiptConverterFactory().create();

  protected final ModelConverter<ContractInterface, Blockchain.ABI> contractInterfaceConverter =
      new ContractInterfaceConverterFactory().create();

  protected final ModelConverter<ContractResult, Rpc.SingleBytes> contractResultConverter =
      new ContractResultConverterFactory().create();

  protected final ModelConverter<EventFilter, Blockchain.FilterInfo> eventFilterConverter =
      new EventFilterConverterFactory().create();

  protected final ModelConverter<Event, Blockchain.Event> eventConverter =
      new EventConverterFactory().create();

  protected AergoRPCServiceFutureStub futureService;
  protected AergoRPCServiceStub streamService;

  protected ContextProvider contextProvider;

  protected AccountBaseTemplate accountBaseTemplate = new AccountBaseTemplate();

  protected TransactionBaseTemplate transactionBaseTemplate = new TransactionBaseTemplate();

  @Override
  public void setChannel(final ManagedChannel channel) {
    this.futureService = newFutureStub(channel);
    this.streamService = newStub(channel);
    accountBaseTemplate.setChannel(channel);
    transactionBaseTemplate.setChannel(channel);
  }

  @Override
  public void setContextProvider(final ContextProvider contextProvider) {
    this.contextProvider = contextProvider;
    accountBaseTemplate.setContextProvider(contextProvider);
    transactionBaseTemplate.setContextProvider(contextProvider);
  }

  @Getter
  private final Function1<ContractTxHash, Future<ContractTxReceipt>> receiptFunction =
      new Function1<ContractTxHash, Future<ContractTxReceipt>>() {

        @Override
        public Future<ContractTxReceipt> apply(
            final ContractTxHash deployTxHash) {
          logger.debug("Get receipt with txHash: {}", deployTxHash);

          final Rpc.SingleBytes rpcDeployTxHash = Rpc.SingleBytes.newBuilder()
              .setValue(copyFrom(deployTxHash.getBytesValue()))
              .build();
          logger.trace("AergoService getReceipt arg: {}", rpcDeployTxHash);

          final Future<Blockchain.Receipt> rawFuture = futureService.getReceipt(rpcDeployTxHash);
          final Future<ContractTxReceipt> convertedFuture = HerajFutures.transform(rawFuture,
              new Function1<Blockchain.Receipt, ContractTxReceipt>() {

                @Override
                public ContractTxReceipt apply(final Blockchain.Receipt receipt) {
                  return receiptConverter.convertToDomainModel(receipt);
                }
              });
          return convertedFuture;
        }
      };

  @Getter
  private final Function4<Signer, ContractDefinition, Long, Fee,
      Future<ContractTxHash>> deployFunction = new Function4<Signer, ContractDefinition, Long, Fee,
          Future<ContractTxHash>>() {

        @Override
        public Future<ContractTxHash> apply(final Signer signer,
            final ContractDefinition contractDefinition, final Long nonce, final Fee fee) {
          logger.debug("Deploy contract with creator: {}, definition: {}, nonce: {}, fee: {}",
              signer, contractDefinition, nonce, fee);

          final RawTransaction rawTransaction = RawTransaction.newDeployContractBuilder()
              .chainIdHash(contextProvider.get().getChainIdHash())
              .from(signer.getPrincipal())
              .nonce(nonce)
              .definition(contractDefinition)
              .fee(fee)
              .build();
          return signAndCommit(signer, rawTransaction);
        }
      };

  @Getter
  private final Function5<Signer, ContractAddress, ContractDefinition, Long, Fee,
      Future<ContractTxHash>> reDeployFunction = new Function5<Signer, ContractAddress,
          ContractDefinition, Long, Fee, Future<ContractTxHash>>() {

        @Override
        public Future<ContractTxHash> apply(final Signer signer,
            final ContractAddress existingContract, final ContractDefinition contractDefinition,
            final Long nonce, final Fee fee) {
          logger.debug("Re-deploy contract with creator: {}, existing one: {}, "
              + "definition: {}, nonce: {}, fee: {}",
              signer, existingContract, contractDefinition, nonce, fee);
          final RawTransaction rawTransaction = RawTransaction.newReDeployContractBuilder()
              .chainIdHash(contextProvider.get().getChainIdHash())
              .creator(signer.getPrincipal())
              .nonce(nonce)
              .contractAddress(existingContract)
              .definition(contractDefinition)
              .fee(fee)
              .build();
          return signAndCommit(signer, rawTransaction);
        }
      };

  @Getter
  private final Function1<ContractAddress,
      Future<ContractInterface>> contractInterfaceFunction = new Function1<
          ContractAddress, Future<ContractInterface>>() {

        @Override
        public Future<ContractInterface> apply(
            final ContractAddress contractAddress) {
          logger.debug("Get contract interface with contract address: {}", contractAddress);

          final Rpc.SingleBytes rpcContractAddress = Rpc.SingleBytes.newBuilder()
              .setValue(accountAddressConverter.convertToRpcModel(contractAddress))
              .build();
          logger.trace("AergoService getABI arg: {}", rpcContractAddress);

          final Future<Blockchain.ABI> rawFuture = futureService.getABI(rpcContractAddress);
          final Future<ContractInterface> convertedFuture = HerajFutures.transform(rawFuture,
              new Function1<Blockchain.ABI, ContractInterface>() {

                @Override
                public ContractInterface apply(final Blockchain.ABI abi) {
                  final ContractInterface withoutAddress =
                      contractInterfaceConverter.convertToDomainModel(abi);
                  return new ContractInterface(contractAddress, withoutAddress.getVersion(),
                      withoutAddress.getLanguage(), withoutAddress.getFunctions(),
                      withoutAddress.getStateVariables());
                }
              });
          return convertedFuture;
        }
      };

  @Getter
  private final Function4<Signer, ContractInvocation, Long, Fee,
      Future<ContractTxHash>> executeFunction = new Function4<Signer, ContractInvocation,
          Long, Fee, Future<ContractTxHash>>() {

        @Override
        public Future<ContractTxHash> apply(final Signer signer,
            final ContractInvocation contractInvocation, final Long nonce,
            final Fee fee) {
          logger.debug("Execute contract with executor: {}, invocation: {}, nonce: {}, fee: {}",
              signer.getPrincipal(), contractInvocation, nonce, fee);
          final RawTransaction rawTransaction = RawTransaction.newInvokeContractBuilder()
              .chainIdHash(contextProvider.get().getChainIdHash())
              .from(signer.getPrincipal())
              .nonce(nonce)
              .invocation(contractInvocation)
              .fee(fee)
              .build();
          return signAndCommit(signer, rawTransaction);
        }
      };

  protected Future<ContractTxHash> signAndCommit(final Signer signer,
      final RawTransaction rawTransaction) {
    final Transaction signed = signer.sign(rawTransaction);
    final Future<TxHash> txHashFuture = transactionBaseTemplate.getCommitFunction().apply(signed);
    return HerajFutures.transform(txHashFuture, new Function1<TxHash, ContractTxHash>() {

      @Override
      public ContractTxHash apply(final TxHash txHash) {
        return txHash.adapt(ContractTxHash.class);
      }
    });
  }

  @Getter
  private final Function1<ContractInvocation, Future<
      ContractResult>> queryFunction = new Function1<ContractInvocation, Future<ContractResult>>() {

        @Override
        public Future<ContractResult> apply(final ContractInvocation contractInvocation) {
          logger.debug("Query contract with invocation: {}", contractInvocation);

          final ByteString rpcContractAddress =
              accountAddressConverter.convertToRpcModel(contractInvocation.getAddress());
          final BytesValue rpcContractInvocation =
              PayloadResolver.resolve(Type.ContractInvocation, contractInvocation);
          final Blockchain.Query rpcQuery = Blockchain.Query.newBuilder()
              .setContractAddress(rpcContractAddress)
              .setQueryinfo(copyFrom(rpcContractInvocation))
              .build();
          logger.trace("AergoService queryContract arg: {}", rpcQuery);

          final Future<Rpc.SingleBytes> rawFuture = futureService.queryContract(rpcQuery);
          final Future<ContractResult> convertedFuture = HerajFutures.transform(rawFuture,
              new Function1<Rpc.SingleBytes, ContractResult>() {

                @Override
                public ContractResult apply(final Rpc.SingleBytes rawQueryResult) {
                  return contractResultConverter.convertToDomainModel(rawQueryResult);
                }
              });
          return convertedFuture;
        }
      };

  @Getter
  private final Function1<EventFilter,
      Future<List<Event>>> listEventFunction = new Function1<EventFilter, Future<List<Event>>>() {

        @Override
        public Future<List<Event>> apply(final EventFilter eventFilter) {
          logger.debug("List event with filter: {}", eventFilter);

          final Blockchain.FilterInfo rpcEventFilter =
              eventFilterConverter.convertToRpcModel(eventFilter);
          logger.trace("AergoService listEvents arg: {}", rpcEventFilter);

          final Future<Rpc.EventList> rawFuture = futureService.listEvents(rpcEventFilter);
          final Future<List<Event>> convertedFuture =
              HerajFutures.transform(rawFuture, new Function1<Rpc.EventList, List<Event>>() {

                @Override
                public List<Event> apply(final Rpc.EventList rawEventList) {
                  final List<Event> domainEvents = new ArrayList<>();
                  for (final Blockchain.Event rpcEvent : rawEventList.getEventsList()) {
                    domainEvents.add(eventConverter.convertToDomainModel(rpcEvent));
                  }
                  return domainEvents;
                }
              });
          return convertedFuture;
        }
      };

  @Getter
  private final Function2<EventFilter, hera.api.model.StreamObserver<Event>,
      Future<Subscription<Event>>> subscribeEventFunction = new Function2<EventFilter,
          hera.api.model.StreamObserver<Event>, Future<Subscription<Event>>>() {

        @Override
        public Future<Subscription<Event>> apply(final EventFilter filter,
            final hera.api.model.StreamObserver<Event> observer) {
          logger.debug("Event subsribe with filter: {}, observer: {}", filter, observer);

          final Blockchain.FilterInfo filterInfo =
              eventFilterConverter.convertToRpcModel(filter);
          Context.CancellableContext cancellableContext =
              Context.current().withCancellation();
          final io.grpc.stub.StreamObserver<Blockchain.Event> adaptor =
              new GrpcStreamObserverAdaptor<Blockchain.Event, Event>(cancellableContext,
                  observer, eventConverter);
          cancellableContext.run(new Runnable() {

            @Override
            public void run() {
              streamService.listEventStream(filterInfo, adaptor);
            }
          });

          final Subscription<Event> subscription = new GrpcStreamSubscription<>(cancellableContext);
          return HerajFutures.success(subscription);
        }
      };

}