AccountBaseTemplate.java

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

package hera.client.internal;

import static hera.util.ValidationUtils.assertTrue;
import static org.slf4j.LoggerFactory.getLogger;
import static types.AergoRPCServiceGrpc.newFutureStub;

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.Function3;
import hera.api.function.Function4;
import hera.api.model.Account;
import hera.api.model.AccountAddress;
import hera.api.model.AccountState;
import hera.api.model.AccountTotalVote;
import hera.api.model.Aer;
import hera.api.model.BytesValue;
import hera.api.model.ElectedCandidate;
import hera.api.model.RawTransaction;
import hera.api.model.StakeInfo;
import hera.api.model.Transaction;
import hera.api.model.TxHash;
import hera.client.ChannelInjectable;
import hera.exception.TransactionVerificationException;
import hera.key.Signer;
import hera.key.TxSigner;
import hera.transport.AccountAddressConverterFactory;
import hera.transport.AccountStateConverterFactory;
import hera.transport.AccountTotalVoteConverterFactory;
import hera.transport.ElectedCandidateConverterFactory;
import hera.transport.ModelConverter;
import hera.transport.StakeInfoConverterFactory;
import hera.transport.TransactionConverterFactory;
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.Blockchain;
import types.Rpc;

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

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

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

  protected final ModelConverter<AccountState, Blockchain.State> accountStateConverter =
      new AccountStateConverterFactory().create();

  protected final ModelConverter<StakeInfo, Rpc.Staking> stakingInfoConverter =
      new StakeInfoConverterFactory().create();

  protected final ModelConverter<Transaction, Blockchain.Tx> transactionConverter =
      new TransactionConverterFactory().create();

  protected final ModelConverter<ElectedCandidate, Rpc.Vote> electedCandidateConverter =
      new ElectedCandidateConverterFactory().create();

  protected final ModelConverter<AccountTotalVote, Rpc.AccountVoteInfo> accountTotalVoteConverter =
      new AccountTotalVoteConverterFactory().create();

  @Getter
  protected AergoRPCServiceFutureStub aergoService;

  protected ContextProvider contextProvider;

  protected TransactionBaseTemplate transactionBaseTemplate = new TransactionBaseTemplate();

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

  @Override
  public void setChannel(final ManagedChannel channel) {
    this.aergoService = newFutureStub(channel);
    this.transactionBaseTemplate.setChannel(channel);
  }

  @Getter
  private final Function1<AccountAddress, Future<AccountState>> stateFunction =
      new Function1<AccountAddress, Future<AccountState>>() {

        @Override
        public Future<AccountState> apply(final AccountAddress address) {
          logger.debug("GetState with {}", address);

          final Rpc.SingleBytes rpcAddress = Rpc.SingleBytes.newBuilder()
              .setValue(accountAddressConverter.convertToRpcModel(address))
              .build();
          logger.trace("AergoService getstate arg: {}", rpcAddress);

          final Future<Blockchain.State> rawFuture = aergoService.getState(rpcAddress);
          final Future<AccountState> convertedFuture =
              HerajFutures.transform(rawFuture, new Function1<Blockchain.State, AccountState>() {

                @Override
                public AccountState apply(final Blockchain.State state) {
                  final AccountState withoutAddress =
                      accountStateConverter.convertToDomainModel(state);
                  return AccountState.newBuilder().address(address)
                      .nonce(withoutAddress.getNonce())
                      .balance(withoutAddress.getBalance())
                      .build();
                }
              });
          return convertedFuture;
        }
      };

  @Getter
  private final Function3<Signer, String, Long, Future<TxHash>> createNameFunction =
      new Function3<Signer, String, Long, Future<TxHash>>() {

        @Override
        public Future<TxHash> apply(final Signer signer, final String name,
            final Long nonce) {
          logger.debug("Create account name with signer: {}, name: {}, nonce: {}",
              signer.getPrincipal(), name, nonce);

          final RawTransaction rawTransaction = RawTransaction.newCreateNameTxBuilder()
              .chainIdHash(contextProvider.get().getChainIdHash())
              .from(signer.getPrincipal())
              .nonce(nonce)
              .name(name)
              .build();
          final Transaction signed = signer.sign(rawTransaction);
          return transactionBaseTemplate.getCommitFunction().apply(signed);
        }
      };

  @Getter
  private final Function4<Signer, String, AccountAddress, Long,
      Future<TxHash>> updateNameFunction = new Function4<Signer, String,
          AccountAddress, Long, Future<TxHash>>() {

        @Override
        public Future<TxHash> apply(final Signer signer, final String name,
            final AccountAddress newOwner, final Long nonce) {
          logger.debug("Update account name with signer: {}, name: {}, to account: {}, nonce: {}",
              signer, name, newOwner, nonce);

          final RawTransaction rawTransaction = RawTransaction.newUpdateNameTxBuilder()
              .chainIdHash(contextProvider.get().getChainIdHash())
              .from(signer.getPrincipal())
              .nonce(nonce)
              .name(name)
              .newOwner(newOwner)
              .build();
          final Transaction signed = signer.sign(rawTransaction);
          return transactionBaseTemplate.getCommitFunction().apply(signed);
        }
      };

  @Getter
  private final Function2<String, Long, Future<AccountAddress>> getNameOwnerFunction =
      new Function2<String, Long, Future<AccountAddress>>() {

        @Override
        public Future<AccountAddress> apply(final String name, final Long blockNumber) {
          logger.debug("Get name owner of name: {}, blockNumber: {}", name, blockNumber);
          assertTrue(blockNumber >= 0, "Block number must >= 0");

          final Rpc.Name rpcName = Rpc.Name.newBuilder()
              .setName(name)
              .setBlockNo(blockNumber)
              .build();
          logger.trace("AergoService getNameInfo arg: {}", rpcName);

          final Future<Rpc.NameInfo> rawFuture = aergoService.getNameInfo(rpcName);
          final Future<AccountAddress> convertedFuture =
              HerajFutures.transform(rawFuture, new Function1<Rpc.NameInfo, AccountAddress>() {

                @Override
                public AccountAddress apply(final Rpc.NameInfo nameInfo) {
                  final AccountAddress converted =
                      accountAddressConverter.convertToDomainModel(nameInfo.getOwner());
                  return BytesValue.EMPTY.equals(converted.getBytesValue()) ? null : converted;
                }
              });
          return convertedFuture;
        }
      };

  @Getter
  private final Function3<Signer, Aer, Long, Future<TxHash>> stakingFunction =
      new Function3<Signer, Aer, Long, Future<TxHash>>() {

        @Override
        public Future<TxHash> apply(final Signer signer, final Aer amount,
            final Long nonce) {
          logger.debug("Staking account with signer: {}, amount: {}, nonce: {}",
              signer.getPrincipal(), amount, nonce);

          final RawTransaction rawTransaction = RawTransaction.newStakeTxBuilder()
              .chainIdHash(contextProvider.get().getChainIdHash())
              .from(signer.getPrincipal())
              .amount(amount)
              .nonce(nonce)
              .build();
          final Transaction signed = signer.sign(rawTransaction);
          return transactionBaseTemplate.getCommitFunction().apply(signed);
        }
      };

  @Getter
  private final Function3<Signer, Aer, Long, Future<TxHash>> unstakingFunction =
      new Function3<Signer, Aer, Long, Future<TxHash>>() {

        @Override
        public Future<TxHash> apply(final Signer signer, final Aer amount,
            final Long nonce) {
          logger.debug("Unstaking account with signer: {}, amount: {}, nonce: {}",
              signer.getPrincipal(), amount, nonce);

          final RawTransaction rawTransaction = RawTransaction.newUnstakeTxBuilder()
              .chainIdHash(contextProvider.get().getChainIdHash())
              .from(signer.getPrincipal())
              .amount(amount)
              .nonce(nonce)
              .build();
          final Transaction signed = signer.sign(rawTransaction);
          return transactionBaseTemplate.getCommitFunction().apply(signed);
        }
      };

  @Getter
  private final Function1<AccountAddress, Future<StakeInfo>> stakingInfoFunction =
      new Function1<AccountAddress, Future<StakeInfo>>() {

        @Override
        public Future<StakeInfo> apply(final AccountAddress accountAddress) {
          logger.debug("Get staking information with address: {}", accountAddress);

          final Rpc.AccountAddress rpcAddress = Rpc.AccountAddress.newBuilder()
              .setValue(accountAddressConverter.convertToRpcModel(accountAddress))
              .build();
          logger.trace("AergoService getStaking arg: {}", rpcAddress);

          final Future<Rpc.Staking> rawFuture = aergoService.getStaking(rpcAddress);
          final Future<StakeInfo> convertedFuture =
              HerajFutures.transform(rawFuture, new Function1<Rpc.Staking, StakeInfo>() {

                @Override
                public StakeInfo apply(final Rpc.Staking rpcStaking) {
                  final StakeInfo withoutAddress =
                      stakingInfoConverter.convertToDomainModel(rpcStaking);
                  return StakeInfo.newBuilder().address(accountAddress)
                      .amount(withoutAddress.getAmount())
                      .blockNumber(withoutAddress.getBlockNumber())
                      .build();
                }
              });
          return convertedFuture;
        }
      };

  @Getter
  private final Function2<Account, RawTransaction, Future<Transaction>> signFunction =
      new Function2<Account, RawTransaction, Future<Transaction>>() {

        @Override
        public Future<Transaction> apply(final Account account,
            final RawTransaction rawTransaction) {
          logger.debug("Sign request with account: {}, rawTx: {}", account.getAddress(),
              rawTransaction);

          if (account instanceof TxSigner) {
            final TxSigner signer = (TxSigner) account;
            final Transaction signed = signer.sign(rawTransaction);
            return HerajFutures.success(signed);
          } else {
            final Transaction domainTransaction = Transaction.newBuilder()
                .rawTransaction(rawTransaction)
                .build();
            final Blockchain.Tx rpcTx = transactionConverter.convertToRpcModel(domainTransaction);
            logger.trace("AergoService signTX arg: {}", rpcTx);

            final Future<Blockchain.Tx> rawFuture = aergoService.signTX(rpcTx);
            final Future<Transaction> convertedFuture =
                HerajFutures.transform(rawFuture, new Function1<Blockchain.Tx, Transaction>() {

                  @Override
                  public Transaction apply(final Blockchain.Tx rpcTx) {
                    return transactionConverter.convertToDomainModel(rpcTx);
                  }
                });
            return convertedFuture;
          }
        }
      };

  @Getter
  private final Function2<Account, Transaction, Future<Boolean>> verifyFunction =
      new Function2<Account, Transaction, Future<Boolean>>() {

        @Override
        public Future<Boolean> apply(final Account account,
            final Transaction transaction) {
          logger.debug("Sign verify with account: {}, signedTx: {}", account, transaction);

          final Blockchain.Tx rpcTx = transactionConverter.convertToRpcModel(transaction);
          logger.trace("AergoService verifyTX arg: {}", rpcTx);

          final Future<Rpc.VerifyResult> rawFuture = aergoService.verifyTX(rpcTx);
          final Future<Boolean> convertedFuture =
              HerajFutures.transform(rawFuture, new Function1<Rpc.VerifyResult, Boolean>() {

                @Override
                public Boolean apply(final Rpc.VerifyResult result) {
                  if (Rpc.VerifyStatus.VERIFY_STATUS_OK != result.getError()) {
                    new TransactionVerificationException(result.getError());
                  }
                  return true;
                }
              });
          return convertedFuture;
        }
      };

  @Getter
  private final Function4<Signer, String, List<String>, Long,
      Future<TxHash>> voteFunction = new Function4<
          Signer, String, List<String>, Long, Future<TxHash>>() {

        @Override
        public Future<TxHash> apply(final Signer signer, final String voteId,
            final List<String> candidates, final Long nonce) {
          logger.debug("Voting with signer: }, voteId: {}, candidates: {}, nonce: {}",
              signer.getPrincipal(), voteId, candidates, nonce);

          final RawTransaction rawTransaction = RawTransaction.newVoteTxBuilder()
              .chainIdHash(contextProvider.get().getChainIdHash())
              .from(signer.getPrincipal())
              .nonce(nonce)
              .voteId(voteId)
              .candidates(candidates)
              .build();
          final Transaction signed = signer.sign(rawTransaction);
          return transactionBaseTemplate.getCommitFunction().apply(signed);
        }
      };

  @Getter
  private final Function2<String, Integer,
      Future<List<ElectedCandidate>>> listElectedFunction = new Function2<
          String, Integer, Future<List<ElectedCandidate>>>() {

        @Override
        public Future<List<ElectedCandidate>> apply(final String voteId,
            final Integer showCount) {
          logger.debug("Get votes status with voteId: {}, showCount: {}", voteId, showCount);

          final Rpc.VoteParams rpcVoteParams = Rpc.VoteParams.newBuilder()
              .setId(voteId)
              .setCount(showCount)
              .build();
          logger.trace("AergoService getVotes arg: {}", rpcVoteParams);

          final Future<Rpc.VoteList> rawFuture = aergoService.getVotes(rpcVoteParams);
          final Future<List<ElectedCandidate>> convertedFuture =
              HerajFutures.transform(rawFuture,
                  new Function1<Rpc.VoteList, List<ElectedCandidate>>() {

                    @Override
                    public List<ElectedCandidate> apply(final Rpc.VoteList rpcVoteList) {
                      final List<ElectedCandidate> electedCandidates = new ArrayList<>();
                      for (final Rpc.Vote rpcCandidate : rpcVoteList.getVotesList()) {
                        final ElectedCandidate domainElectedCandidate =
                            electedCandidateConverter.convertToDomainModel(rpcCandidate);
                        electedCandidates.add(domainElectedCandidate);
                      }
                      return electedCandidates;
                    }
                  });
          return convertedFuture;
        }
      };

  @Getter
  private final Function1<AccountAddress, Future<AccountTotalVote>> votesOfFunction =
      new Function1<AccountAddress, Future<AccountTotalVote>>() {

        @Override
        public Future<AccountTotalVote> apply(final AccountAddress accountAddress) {
          logger.debug("Get votes with address: {}", accountAddress);

          final Rpc.AccountAddress rpcAddress = Rpc.AccountAddress.newBuilder()
              .setValue(accountAddressConverter.convertToRpcModel(accountAddress))
              .build();
          logger.trace("AergoService getAccountVotes arg: {}", rpcAddress);

          final Future<Rpc.AccountVoteInfo> rawFuture =
              aergoService.getAccountVotes(rpcAddress);
          final Future<AccountTotalVote> convertedFuture =
              HerajFutures.transform(rawFuture,
                  new Function1<Rpc.AccountVoteInfo, AccountTotalVote>() {

                    @Override
                    public AccountTotalVote apply(final Rpc.AccountVoteInfo rpcAccountVoteTotal) {
                      return accountTotalVoteConverter.convertToDomainModel(rpcAccountVoteTotal);
                    }
                  });
          return convertedFuture;
        }
      };

}