Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add stacks multisig support, closes #3889 #4276

Merged
merged 1 commit into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/app/components/fees-row/components/custom-fee-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ interface CustomFeeFieldProps extends StackProps {
feeCurrencySymbol: CryptoCurrencies;
lowFeeEstimate: StacksFeeEstimate;
setFieldWarning(value: string): void;
disableFeeSelection?: boolean;
}
export function CustomFeeField(props: CustomFeeFieldProps) {
const { feeCurrencySymbol, lowFeeEstimate, setFieldWarning, ...rest } = props;
const { feeCurrencySymbol, lowFeeEstimate, setFieldWarning, disableFeeSelection, ...rest } =
props;
const [field, meta, helpers] = useField('fee');

const checkFieldWarning = useCallback(
Expand Down Expand Up @@ -52,6 +54,7 @@ export function CustomFeeField(props: CustomFeeFieldProps) {
display="block"
height="32px"
name="fee"
isDisabled={disableFeeSelection}
onChange={(evt: FormEvent<HTMLInputElement>) => {
helpers.setValue(evt.currentTarget.value);
// Separating warning check from field validations
Expand Down
7 changes: 4 additions & 3 deletions src/app/components/fees-row/components/fee-estimate-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ interface FeeEstimateItemProps {
isVisible?: boolean;
onSelectItem(index: number): void;
selectedItem: number;
disableFeeSelection?: boolean;
}
export function FeeEstimateItem(props: FeeEstimateItemProps) {
const { index, isVisible, onSelectItem, selectedItem } = props;
const { index, isVisible, onSelectItem, selectedItem, disableFeeSelection } = props;

const selectedIcon = useMemo(() => {
const isSelected = index === selectedItem;
Expand All @@ -29,13 +30,13 @@ export function FeeEstimateItem(props: FeeEstimateItemProps) {
isInline
mb="0px !important"
minWidth="100px"
onClick={() => onSelectItem(index)}
onClick={() => !disableFeeSelection && onSelectItem(index)}
p="tight"
>
<Text fontSize={1} fontWeight={500} ml="2px">
{labels[index]}
</Text>
{isVisible ? selectedIcon : <FiChevronDown />}
{!disableFeeSelection && (isVisible ? selectedIcon : <FiChevronDown />)}
</Stack>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,19 @@ interface FeeEstimateSelectLayoutProps {
isVisible: boolean;
onSetIsSelectVisible(value: boolean): void;
selectedItem: number;
disableFeeSelection?: boolean;
}
export function FeeEstimateSelectLayout(props: FeeEstimateSelectLayoutProps) {
const { children, isVisible, onSetIsSelectVisible, selectedItem } = props;
const { children, isVisible, onSetIsSelectVisible, selectedItem, disableFeeSelection } = props;
const ref = useRef<HTMLDivElement | null>(null);

useOnClickOutside(ref, () => onSetIsSelectVisible(false));

return (
<>
<Stack _hover={{ cursor: 'pointer' }}>
<Stack _hover={{ cursor: disableFeeSelection ? 'default' : 'pointer' }}>
<FeeEstimateItem
disableFeeSelection={disableFeeSelection}
index={selectedItem}
onSelectItem={() => onSetIsSelectVisible(true)}
selectedItem={FeeTypes.Middle}
Expand Down
13 changes: 11 additions & 2 deletions src/app/components/fees-row/components/fee-estimate-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,22 @@ interface FeeEstimateSelectProps {
onSetIsSelectVisible(value: boolean): void;
selectedItem: number;
allowCustom: boolean;
disableFeeSelection?: boolean;
}
export function FeeEstimateSelect(props: FeeEstimateSelectProps) {
const { isVisible, estimate, onSelectItem, onSetIsSelectVisible, selectedItem, allowCustom } =
props;
const {
isVisible,
estimate,
onSelectItem,
onSetIsSelectVisible,
selectedItem,
allowCustom,
disableFeeSelection,
} = props;

return (
<FeeEstimateSelectLayout
disableFeeSelection={disableFeeSelection}
isVisible={isVisible}
onSetIsSelectVisible={onSetIsSelectVisible}
selectedItem={selectedItem}
Expand Down
29 changes: 26 additions & 3 deletions src/app/components/fees-row/fees-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { SharedComponentsSelectors } from '@tests/selectors/shared-component.sel
import BigNumber from 'bignumber.js';
import { useField } from 'formik';

import { STX_DECIMALS } from '@shared/constants';
import { FeeTypes, Fees } from '@shared/models/fees/fees.model';
import { createMoney } from '@shared/models/money.model';
import { isNumber, isString } from '@shared/utils';
Expand All @@ -22,6 +23,8 @@ interface FeeRowProps extends StackProps {
fees?: Fees;
allowCustom?: boolean;
isSponsored: boolean;
defaultFeeValue?: number;
disableFeeSelection?: boolean;
}
export function FeesRow(props: FeeRowProps): React.JSX.Element {
const { fees, isSponsored, allowCustom = true, ...rest } = props;
Expand All @@ -46,14 +49,27 @@ export function FeesRow(props: FeeRowProps): React.JSX.Element {
}, [convertCryptoCurrencyToUsd, feeCurrencySymbol, feeField.value]);

useEffect(() => {
if (hasFeeEstimates && !feeField.value && !isCustom) {
if (props.defaultFeeValue) {
feeHelper.setValue(
convertAmountToBaseUnit(
new BigNumber(Number(props.defaultFeeValue)),
STX_DECIMALS
).toString()
);
feeTypeHelper.setValue(FeeTypes[FeeTypes.Custom]);
}
}, [feeHelper, props.defaultFeeValue, feeTypeHelper]);

useEffect(() => {
if (!props.defaultFeeValue && hasFeeEstimates && !feeField.value && !isCustom) {
feeHelper.setValue(convertAmountToBaseUnit(fees.estimates[FeeTypes.Middle].fee).toString());
feeTypeHelper.setValue(FeeTypes[FeeTypes.Middle]);
}
if (isSponsored) {
feeHelper.setValue(0);
}
}, [
props.defaultFeeValue,
feeField.value,
feeHelper,
feeTypeHelper,
Expand All @@ -66,13 +82,18 @@ export function FeesRow(props: FeeRowProps): React.JSX.Element {
const handleSelectFeeEstimateOrCustomField = useCallback(
(index: number) => {
feeTypeHelper.setValue(FeeTypes[index]);
if (index === FeeTypes.Custom) feeHelper.setValue('');
if (index === FeeTypes.Custom)
feeHelper.setValue(
props.defaultFeeValue
? convertAmountToBaseUnit(new BigNumber(Number(props.defaultFeeValue)), STX_DECIMALS)
: ''
);
else
fees && feeHelper.setValue(convertAmountToBaseUnit(fees.estimates[index].fee).toString());
setFieldWarning('');
setIsSelectVisible(false);
},
[feeTypeHelper, feeHelper, fees]
[feeTypeHelper, feeHelper, fees, props.defaultFeeValue]
);

if (!hasFeeEstimates) return <LoadingRectangle height="32px" width="100%" {...rest} />;
Expand All @@ -83,6 +104,7 @@ export function FeesRow(props: FeeRowProps): React.JSX.Element {
feeField={
isCustom ? (
<CustomFeeField
disableFeeSelection={props.disableFeeSelection}
feeCurrencySymbol={feeCurrencySymbol}
lowFeeEstimate={fees.estimates[FeeTypes.Low]}
setFieldWarning={(value: string) => setFieldWarning(value)}
Expand All @@ -101,6 +123,7 @@ export function FeesRow(props: FeeRowProps): React.JSX.Element {
isSponsored={isSponsored}
selectInput={
<FeeEstimateSelect
disableFeeSelection={props.disableFeeSelection}
allowCustom={allowCustom}
isVisible={isSelectVisible}
estimate={fees.estimates}
Expand Down
9 changes: 3 additions & 6 deletions src/app/components/nonce-setter.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import { ReactNode, useEffect } from 'react';
import { useEffect } from 'react';

import { useFormikContext } from 'formik';

import { StacksSendFormValues, StacksTransactionFormValues } from '@shared/models/form.model';

import { useNextNonce } from '@app/query/stacks/nonce/account-nonces.hooks';

interface NonceSetterProps {
children: ReactNode;
}
export function NonceSetter({ children }: NonceSetterProps) {
export function NonceSetter() {
const { setFieldValue, touched, values } = useFormikContext<
StacksSendFormValues | StacksTransactionFormValues
>();
Expand All @@ -21,5 +18,5 @@ export function NonceSetter({ children }: NonceSetterProps) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [nextNonce?.nonce]);

return <>{children}</>;
return <></>;
}
5 changes: 3 additions & 2 deletions src/app/features/edit-nonce-drawer/edit-nonce-drawer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useCallback, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useLocation, useNavigate } from 'react-router-dom';

import { useFormikContext } from 'formik';
import { Stack, styled } from 'leather-styles/jsx';
Expand Down Expand Up @@ -33,10 +33,11 @@ export function EditNonceDrawer() {
const [loadedNextNonce, setLoadedNextNonce] = useState<number | string>();

const navigate = useNavigate();
const { search } = useLocation();

useOnMount(() => setLoadedNextNonce(values.nonce));

const onGoBack = useCallback(() => navigate('..'), [navigate]);
const onGoBack = useCallback(() => navigate('..' + search), [navigate, search]);

const onBlur = useCallback(() => validateField('nonce'), [validateField]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { useExplorerLink } from '@app/common/hooks/use-explorer-link';
import { formatContractId } from '@app/common/utils';
import { Divider } from '@app/components/layout/divider';
import { Title } from '@app/components/typography';
import { AttachmentRow } from '@app/pages/transaction-request/components/attachment-row';
import { ContractPreviewLayout } from '@app/pages/transaction-request/components/contract-preview';
import { AttachmentRow } from '@app/features/stacks-transaction-request/attachment-row';
import { ContractPreviewLayout } from '@app/features/stacks-transaction-request/contract-preview';
import { useTransactionRequestState } from '@app/store/transactions/requests.hooks';

import { FunctionArgumentsList } from './function-arguments-list';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { cvToString, deserializeCV, getCVTypeString } from '@stacks/transactions';

import { Row } from '@app/pages/transaction-request/components/row';
import { Row } from '@app/features/stacks-transaction-request/row';
import { useContractFunction } from '@app/query/stacks/contract/contract.hooks';

interface FunctionArgumentProps {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { BoxProps, CodeBlock, Stack, color } from '@stacks/ui';
import { Prism } from '@app/common/clarity-prism';
import { Divider } from '@app/components/layout/divider';
import { Caption, Title } from '@app/components/typography';
import { AttachmentRow } from '@app/pages/transaction-request/components/attachment-row';
import { ContractPreviewLayout } from '@app/pages/transaction-request/components/contract-preview';
import { Row } from '@app/pages/transaction-request/components/row';
import { AttachmentRow } from '@app/features/stacks-transaction-request/attachment-row';
import { ContractPreviewLayout } from '@app/features/stacks-transaction-request/contract-preview';
import { Row } from '@app/features/stacks-transaction-request/row';
import {
useCurrentAccountStxAddressState,
useCurrentStacksAccount,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import { useUnsignedPrepareTransactionDetails } from '@app/store/transactions/tr

interface FeeFormProps {
fees?: Fees;
disableFeeSelection?: boolean;
defaultFeeValue?: number;
}

export function FeeForm({ fees }: FeeFormProps) {
export function FeeForm({ fees, disableFeeSelection, defaultFeeValue }: FeeFormProps) {
const { values } = useFormikContext<StacksTransactionFormValues>();
const transaction = useUnsignedPrepareTransactionDetails(values);

Expand All @@ -21,7 +23,12 @@ export function FeeForm({ fees }: FeeFormProps) {
return (
<>
{fees?.estimates.length ? (
<FeesRow allowCustom fees={fees} isSponsored={isSponsored} />
<FeesRow
disableFeeSelection={disableFeeSelection}
defaultFeeValue={defaultFeeValue}
fees={fees}
isSponsored={isSponsored}
/>
) : (
<LoadingRectangle height="32px" width="100%" />
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@ import { ContractCallPayload, TransactionTypes } from '@stacks/connect';
import BigNumber from 'bignumber.js';

import { useDefaultRequestParams } from '@app/common/hooks/use-default-request-search-params';
import { initialSearchParams } from '@app/common/initial-search-params';
import { microStxToStx } from '@app/common/money/unit-conversion';
import { validateStacksAddress } from '@app/common/stacks-utils';
import { TransactionErrorReason } from '@app/pages/transaction-request/components/transaction-error/transaction-error';
import { TransactionErrorReason } from '@app/features/stacks-transaction-request/transaction-error/transaction-error';
import { useCurrentStacksAccountAnchoredBalances } from '@app/query/stacks/balance/stx-balance.hooks';
import { useContractInterface } from '@app/query/stacks/contract/contract.hooks';
import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks';
import { useTransactionRequestState } from '@app/store/transactions/requests.hooks';

function getIsMultisig() {
return initialSearchParams.get('isMultisig') === 'true';
}

export function useTransactionError() {
const transactionRequest = useTransactionRequestState();
const contractInterface = useContractInterface(transactionRequest as ContractCallPayload);
Expand All @@ -33,7 +38,7 @@ export function useTransactionError() {
if ((contractInterface as any)?.isError) return TransactionErrorReason.NoContract;
}

if (balances) {
if (balances && !getIsMultisig()) {
const zeroBalance = balances?.stx.unlockedStx.amount.toNumber() === 0;

if (transactionRequest.txType === TransactionTypes.STXTransfer) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { TransactionRequestSelectors } from '@tests/selectors/requests.selectors
import { styled } from 'leather-styles/jsx';

import { ErrorIcon } from '@app/components/icons/error-icon';
import { TransactionErrorReason } from '@app/pages/transaction-request/components/transaction-error/transaction-error';
import { useTransactionError } from '@app/pages/transaction-request/hooks/use-transaction-error';
import { useTransactionError } from '@app/features/stacks-transaction-request/hooks/use-transaction-error';
import { TransactionErrorReason } from '@app/features/stacks-transaction-request/transaction-error/transaction-error';

function MinimalErrorMessageSuspense(props: StackProps) {
const error = useTransactionError();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useDefaultRequestParams } from '@app/common/hooks/use-default-request-s
import { addPortSuffix, getUrlHostname } from '@app/common/utils';
import { Favicon } from '@app/components/favicon';
import { Flag } from '@app/components/layout/flag';
import { usePageTitle } from '@app/pages/transaction-request/hooks/use-page-title';
import { usePageTitle } from '@app/features/stacks-transaction-request/hooks/use-page-title';
import { useCurrentNetworkState } from '@app/store/networks/networks.hooks';
import { useTransactionRequestState } from '@app/store/transactions/requests.hooks';

Expand Down
Loading