Skip to content

Commit

Permalink
feat: stacks multisig support, closes #3889
Browse files Browse the repository at this point in the history
  • Loading branch information
fess-v authored and kyranjamie committed Oct 10, 2023
1 parent eedbed5 commit 9dabfc2
Show file tree
Hide file tree
Showing 47 changed files with 791 additions and 66 deletions.
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);

Check warning on line 59 in src/app/components/fees-row/components/custom-fee-field.tsx

View workflow job for this annotation

GitHub Actions / lint-eslint

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator

Check warning on line 59 in src/app/components/fees-row/components/custom-fee-field.tsx

View workflow job for this annotation

GitHub Actions / lint-eslint

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
// 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(

Check warning on line 53 in src/app/components/fees-row/fees-row.tsx

View workflow job for this annotation

GitHub Actions / lint-eslint

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator

Check warning on line 53 in src/app/components/fees-row/fees-row.tsx

View workflow job for this annotation

GitHub Actions / lint-eslint

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
convertAmountToBaseUnit(
new BigNumber(Number(props.defaultFeeValue)),
STX_DECIMALS
).toString()
);
feeTypeHelper.setValue(FeeTypes[FeeTypes.Custom]);

Check warning on line 59 in src/app/components/fees-row/fees-row.tsx

View workflow job for this annotation

GitHub Actions / lint-eslint

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator

Check warning on line 59 in src/app/components/fees-row/fees-row.tsx

View workflow job for this annotation

GitHub Actions / lint-eslint

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
}
}, [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

0 comments on commit 9dabfc2

Please sign in to comment.