DeFiCh/wallet

View on GitHub
mobile-app/app/screens/AppNavigator/screens/Portfolio/screens/OCG/OCGConfirmScreen.tsx

Summary

Maintainability
C
1 day
Test Coverage
import { StackScreenProps } from "@react-navigation/stack";
import {
  ConversionParam,
  PortfolioParamList,
} from "@screens/AppNavigator/screens/Portfolio/PortfolioNavigator";
import {
  ThemedScrollViewV2,
  ThemedTextV2,
  ThemedViewV2,
} from "@components/themed";
import { getColor, tailwind } from "@tailwind";
import { translate } from "@translations";
import { TextRowV2 } from "@components/TextRowV2";
import { TouchableOpacity, View } from "react-native";
import { NumberRowV2 } from "@components/NumberRowV2";
import BigNumber from "bignumber.js";
import Checkbox from "expo-checkbox";
import { Dispatch, useEffect, useState } from "react";
import { SubmitButtonGroup } from "@components/SubmitButtonGroup";
import { useSelector } from "react-redux";
import { RootState } from "@store";
import {
  hasOceanTXQueued,
  hasTxQueued,
  transactionQueue,
} from "@waveshq/walletkit-ui/dist/store";
import { WalletAlert } from "@components/WalletAlert";
import { ScreenName } from "@screens/enum";
import { NavigationProp, useNavigation } from "@react-navigation/native";
import { OCGProposalType } from "@screens/AppNavigator/screens/Portfolio/screens/OCG/OCGProposalsScreen";
import {
  NativeLoggingProps,
  useLogger,
} from "@shared-contexts/NativeLoggingProvider";
import { WhaleWalletAccount } from "@defichain/whale-api-wallet";
import { CTransactionSegWit } from "@defichain/jellyfish-transaction";
import { DeFiAddress } from "@defichain/jellyfish-address";
import { NetworkName } from "@defichain/jellyfish-network";
import {
  CreateCfp,
  CreateVoc,
} from "@defichain/jellyfish-transaction/dist/script/dftx/dftx_governance";
import { useNetworkContext } from "@waveshq/walletkit-ui";
import { useAppDispatch } from "@hooks/useAppDispatch";
import { onTransactionBroadcast } from "@api/transaction/transaction_commands";

type Props = StackScreenProps<PortfolioParamList, "OCGConfirmScreen">;

export function OCGConfirmScreen({ route }: Props): JSX.Element {
  const {
    type,
    fee,
    proposalFee,
    url,
    title,
    amountRequest,
    cycle,
    receivingAddress,
    conversion,
  } = route.params;
  const isCFPType = type === OCGProposalType.CFP;

  const navigation = useNavigation<NavigationProp<PortfolioParamList>>();
  const logger = useLogger();
  const network = useNetworkContext();
  const dispatch = useAppDispatch();

  const hasPendingJob = useSelector((state: RootState) =>
    hasTxQueued(state.transactionQueue)
  );
  const hasPendingBroadcastJob = useSelector((state: RootState) =>
    hasOceanTXQueued(state.ocean)
  );

  const [isOnPage, setIsOnPage] = useState<boolean>(true);
  const [isAcknowledge, setIsAcknowledge] = useState<boolean>(false);
  const [isSubmitting, setIsSubmitting] = useState(false);

  useEffect(() => {
    setIsOnPage(true);
    return () => {
      setIsOnPage(false);
    };
  }, []);

  async function onSubmit(): Promise<void> {
    if (!isAcknowledge || hasPendingJob || hasPendingBroadcastJob) {
      return;
    }

    const form = {
      networkName: network.networkName,
      isCFPType,
      url,
      title,
      amountRequest,
      cycle,
      receivingAddress,
    };

    setIsSubmitting(true);
    await constructSignedProposalAndSend(
      form,
      dispatch,
      () => {
        onTransactionBroadcast(isOnPage, navigation.dispatch);
      },
      logger
    );
    setIsSubmitting(false);
  }

  function onCancel() {
    if (isSubmitting) {
      return;
    }

    WalletAlert({
      title: translate("screens/Settings", "Cancel transaction"),
      message: translate(
        "screens/Settings",
        "By cancelling, you will lose any changes you made for your transaction."
      ),
      buttons: [
        {
          text: translate("screens/Settings", "Go back"),
          style: "cancel",
        },
        {
          text: translate("screens/Settings", "Cancel"),
          style: "destructive",
          onPress: async () => {
            navigation.navigate(ScreenName.PORTFOLIO_screen);
          },
        },
      ],
    });
  }

  return (
    <ThemedScrollViewV2 contentContainerStyle={tailwind("py-8 px-5")}>
      <HeaderSection title={title} />
      {conversion !== undefined && conversion.isConversionRequired && (
        <ConversionSection conversion={conversion} />
      )}
      <ThemedViewV2
        style={tailwind("pt-5 border-t-0.5 ")}
        light={tailwind("text-mono-light-v2-900 border-mono-light-v2-300")}
        dark={tailwind("text-mono-dark-v2-900 border-mono-dark-v2-300")}
      >
        <TextRowV2
          lhs={{
            value: translate("screens/OCGConfirmScreen", "Proposal type"),
            themedProps: lhsTheme,
            testID: "proposal_type_label",
          }}
          rhs={{
            value: type,
            themedProps: rhsTheme,
            testID: "proposal_type_value",
          }}
        />
      </ThemedViewV2>
      <ThemedViewV2
        style={tailwind("py-5 border-b-0.5")}
        light={tailwind("border-mono-light-v2-300")}
        dark={tailwind("border-mono-dark-v2-300")}
      >
        <TextRowV2
          lhs={{
            value: translate("screens/OCGConfirmScreen", "Discussion"),
            themedProps: lhsTheme,
            testID: "discussion_label",
          }}
          rhs={{
            value: url,
            themedProps: rhsTheme,
            testID: "discussion_value",
          }}
        />
      </ThemedViewV2>
      {isCFPType &&
        amountRequest !== undefined &&
        cycle !== undefined &&
        receivingAddress !== undefined && (
          <CFPSection
            amountRequest={amountRequest}
            cycle={cycle}
            receivingAddress={receivingAddress}
          />
        )}
      <FeeSection proposalFee={proposalFee} transactionFee={fee} />
      <AcknowledgeSwitch
        isAcknowledge={isAcknowledge}
        onSwitch={setIsAcknowledge}
      />
      <SubmitButtonGroup
        isDisabled={!isAcknowledge || hasPendingJob || hasPendingBroadcastJob}
        title="submit"
        label={translate("screens/OCGConfirmScreen", "Submit")}
        displayCancelBtn
        onSubmit={onSubmit}
        onCancel={onCancel}
        buttonStyle="mx-7 mt-5 mb-2"
        isCancelDisabled={false}
      />
    </ThemedScrollViewV2>
  );
}

function HeaderSection({ title }: { title: string }): JSX.Element {
  return (
    <View>
      <ThemedTextV2
        style={tailwind("text-xs font-normal-v2")}
        light={tailwind("text-mono-light-v2-500")}
        dark={tailwind("text-mono-dark-v2-500")}
      >
        {translate("screens/OCGConfirmScreen", "You are proposing")}
      </ThemedTextV2>
      <ThemedTextV2
        style={tailwind("text-xl font-semibold-v2 mt-2 pb-8")}
        light={tailwind("text-mono-light-v2-900")}
        dark={tailwind("text-mono-dark-v2-900")}
        testID="proposal_title"
      >
        {title}
      </ThemedTextV2>
    </View>
  );
}

function ConversionSection({
  conversion,
}: {
  conversion: ConversionParam;
}): JSX.Element {
  return (
    <ThemedViewV2
      style={tailwind("py-5 border-t-0.5")}
      light={tailwind("border-mono-light-v2-300")}
      dark={tailwind("border-mono-dark-v2-300")}
      testID="convert_section"
    >
      <NumberRowV2
        containerStyle={{
          style: tailwind("flex-row items-start w-full bg-transparent"),
        }}
        lhs={{
          value: translate("screens/OCGConfirmScreen", "Amount to convert"),
          testID: "amount_to_convert",
          themedProps: lhsTheme,
        }}
        rhs={{
          value: conversion.conversionAmount.toFixed(8),
          suffix: " DFI",
          testID: "amount_to_convert_value",
          themedProps: rhsTheme,
          isConverting: !conversion.isConverted,
        }}
      />
    </ThemedViewV2>
  );
}

function CFPSection({
  amountRequest,
  cycle,
  receivingAddress,
}: {
  amountRequest: BigNumber;
  cycle: number;
  receivingAddress: string;
}) {
  return (
    <ThemedViewV2
      style={tailwind("py-4 border-b-0.5")}
      light={tailwind("border-mono-light-v2-300")}
      dark={tailwind("border-mono-dark-v2-300")}
      testID="cfp_section"
    >
      <NumberRowV2
        lhs={{
          testID: "amount_requested",
          value: translate("screens/OCGConfirmScreen", "Amount requested"),
          themedProps: lhsTheme,
        }}
        rhs={{
          testID: "amount_requested_value",
          value: amountRequest?.toFixed(8),
          suffix: " DFI",
          themedProps: rhsTheme,
        }}
      />
      <View style={tailwind("py-5")}>
        <NumberRowV2
          lhs={{
            testID: "cycle",
            value: translate("screens/OCGConfirmScreen", "Cycle(s)"),
            themedProps: lhsTheme,
          }}
          rhs={{
            testID: "cycle_value",
            value: cycle,
            suffix: ` ${translate("screens/OCGConfirmScreen", "Cycle(s)")}`,
            themedProps: rhsTheme,
          }}
        />
      </View>
      <TextRowV2
        lhs={{
          value: translate("screens/OCGConfirmScreen", "Receiving address"),
          themedProps: lhsTheme,
          testID: "receiving_address_label",
        }}
        rhs={{
          value: receivingAddress,
          themedProps: rhsTheme,
          testID: "receiving_address_value",
        }}
      />
    </ThemedViewV2>
  );
}

function FeeSection({
  proposalFee,
  transactionFee,
}: {
  proposalFee: BigNumber;
  transactionFee: BigNumber;
}): JSX.Element {
  return (
    <ThemedViewV2
      style={tailwind("py-4 border-b-0.5")}
      light={tailwind("border-mono-light-v2-300")}
      dark={tailwind("border-mono-dark-v2-300")}
    >
      <NumberRowV2
        lhs={{
          testID: "proposal_fee",
          value: translate("screens/OCGConfirmScreen", "Proposal fee"),
          themedProps: lhsTheme,
        }}
        rhs={{
          testID: "proposal_fee_value",
          value: proposalFee?.toFixed(8),
          suffix: " DFI",
          themedProps: rhsTheme,
        }}
        info={{
          title: translate("screens/OCGConfirmScreen", "Proposal Fees"),
          message: translate(
            "screens/OCGConfirmScreen",
            "Proposals fees are implemented to encourage more responsible submissions.\nProposal fees are calculated based on the type of proposal selected, and the requested amount (only for CFP).\nRefer to our FAQ section for more detailed breakdown."
          ),
          iconStyle: {
            light: tailwind("text-mono-light-v2-500"),
            dark: tailwind("text-mono-dark-v2-500"),
          },
        }}
        customSnapPoints={["45%"]}
      />
      <View style={tailwind("pt-5")}>
        <NumberRowV2
          lhs={{
            testID: "transaction_fee",
            value: translate("screens/OCGConfirmScreen", "Transaction fee"),
            themedProps: lhsTheme,
          }}
          rhs={{
            testID: "transaction_fee_value",
            value: transactionFee?.toFixed(8),
            suffix: " DFI",
            themedProps: rhsTheme,
          }}
        />
      </View>
    </ThemedViewV2>
  );
}

function AcknowledgeSwitch({
  isAcknowledge,
  onSwitch,
}: {
  isAcknowledge: boolean;
  onSwitch: (val: boolean) => void;
}): JSX.Element {
  return (
    <View style={tailwind("px-7 pt-16 flex-1 flex-row justify-center")}>
      <Checkbox
        value={isAcknowledge}
        style={tailwind("h-6 w-6 mt-1 rounded")}
        onValueChange={onSwitch}
        color={isAcknowledge ? getColor("brand-v2-500") : undefined}
        testID="switch_ack_proposal"
      />
      <TouchableOpacity
        style={tailwind("flex-1")}
        activeOpacity={0.7}
        onPress={() => {
          onSwitch(!isAcknowledge);
        }}
        testID="button_ack_proposal"
      >
        <ThemedTextV2
          style={tailwind("ml-4 flex-1 text-xs font-normal-v2")}
          light={tailwind("text-mono-light-v2-700")}
          dark={tailwind("text-mono-dark-v2-700")}
        >
          {translate(
            "screens/OCGConfirmScreen",
            "I acknowledge all proposal details to be correct and final. Once submitted, the proposal is no longer editable. View your posted proposal in DeFiScan."
          )}
        </ThemedTextV2>
      </TouchableOpacity>
    </View>
  );
}

const lhsTheme = {
  light: tailwind("text-mono-light-v2-500"),
  dark: tailwind("text-mono-dark-v2-500"),
};

const rhsTheme = {
  light: tailwind("text-mono-light-v2-900"),
  dark: tailwind("text-mono-dark-v2-900"),
};

interface ProposalForm {
  networkName: NetworkName;
  isCFPType: boolean;
  url: string;
  title: string;
}

interface CFPProposalForm extends ProposalForm {
  amountRequest: BigNumber;
  cycle: number;
  receivingAddress: string;
}

async function constructSignedProposalAndSend(
  form: ProposalForm,
  dispatch: Dispatch<any>,
  onBroadcast: () => void,
  logger: NativeLoggingProps
) {
  try {
    const signer = async (
      account: WhaleWalletAccount
    ): Promise<CTransactionSegWit> => {
      const script = await account.getScript();
      const builder = account.withTransactionBuilder();

      if (form.isCFPType) {
        const cfpForm = form as CFPProposalForm;
        const to = DeFiAddress.from(
          cfpForm.networkName,
          cfpForm.receivingAddress
        ).getScript();
        const cfp: CreateCfp = {
          type: 0x01,
          address: to,
          nAmount: cfpForm.amountRequest,
          nCycles: cfpForm.cycle,
          title: cfpForm.title,
          context: cfpForm.url,
          contexthash: "",
          options: 0x00,
        };
        const signed = await builder.governance.createCfp(cfp, script);
        return new CTransactionSegWit(signed);
      } else {
        const dfip: CreateVoc = {
          type: 0x02,
          title: form.title,
          context: form.url,
          contexthash: "",
          nAmount: new BigNumber(0),
          address: {
            stack: [],
          },
          nCycles: 1,
          options: 0x00,
        };
        const signed = await builder.governance.createVoc(dfip, script);
        return new CTransactionSegWit(signed);
      }
    };

    dispatch(
      transactionQueue.actions.push({
        sign: signer,
        title: translate("screens/OCGConfirmScreen", "Submitting proposal"),
        drawerMessages: {
          preparing: translate(
            "screens/OCGConfirmScreen",
            "Submitting proposal"
          ),
          waiting: translate("screens/OCGConfirmScreen", "Submitting proposal"),
          complete: translate("screens/OCGConfirmScreen", "Proposal Submitted"),
        },
        onBroadcast,
      })
    );
  } catch (e) {
    logger.error(e);
  }
}