diff --git a/package.json b/package.json
index e580783339e..245d6720e5c 100644
--- a/package.json
+++ b/package.json
@@ -169,6 +169,7 @@
"@typescript-eslint/eslint-plugin": "6.7.4",
"@vkontakte/vk-qr": "2.0.13",
"@zondax/ledger-stacks": "1.0.4",
+ "alex-sdk": "0.1.22",
"are-passive-events-supported": "1.1.1",
"argon2-browser": "1.18.0",
"assert": "2.0.0",
diff --git a/src/app/common/error-messages.ts b/src/app/common/error-messages.ts
index 0e89903387f..e5a25ce7348 100644
--- a/src/app/common/error-messages.ts
+++ b/src/app/common/error-messages.ts
@@ -13,7 +13,7 @@ export enum FormErrorMessages {
InsufficientFunds = 'Insufficient funds',
MemoExceedsLimit = 'Memo must be less than 34-bytes',
MustBeNumber = 'Amount must be a number',
- MustBePositive = 'Amount must be positive',
+ MustBePositive = 'Amount must be greater than zero',
MustSelectAsset = 'Select a valid token to transfer',
SameAddress = 'Cannot send to yourself',
TooMuchPrecision = 'Token can only have {decimals} decimals',
diff --git a/src/app/common/hooks/use-bitcoin-contracts.ts b/src/app/common/hooks/use-bitcoin-contracts.ts
index aec2a6700a7..ed785a13f70 100644
--- a/src/app/common/hooks/use-bitcoin-contracts.ts
+++ b/src/app/common/hooks/use-bitcoin-contracts.ts
@@ -252,7 +252,7 @@ export function useBitcoinContracts() {
const txMoney = createMoneyFromDecimal(bitcoinValue, 'BTC');
const txFiatValue = i18nFormatCurrency(calculateFiatValue(txMoney)).toString();
const txFiatValueSymbol = bitcoinMarketData.price.symbol;
- const txLink = { blockchain: 'bitcoin', txid: txId };
+ const txLink = { blockchain: 'bitcoin', txId };
return {
txId,
diff --git a/src/app/common/hooks/use-convert-to-fiat-amount.ts b/src/app/common/hooks/use-convert-to-fiat-amount.ts
index ac64254c446..f38c9dae623 100644
--- a/src/app/common/hooks/use-convert-to-fiat-amount.ts
+++ b/src/app/common/hooks/use-convert-to-fiat-amount.ts
@@ -1,6 +1,7 @@
-import { useCallback } from 'react';
+import { useCallback, useMemo } from 'react';
import { CryptoCurrencies } from '@shared/models/currencies.model';
+import { createMarketData, createMarketPair } from '@shared/models/market.model';
import type { Money } from '@shared/models/money.model';
import { useCryptoCurrencyMarketData } from '@app/query/common/market-data/market-data.hooks';
@@ -15,3 +16,15 @@ export function useConvertCryptoCurrencyToFiatAmount(currency: CryptoCurrencies)
[cryptoCurrencyMarketData]
);
}
+
+export function useConvertAlexSdkCurrencyToFiatAmount(currency: CryptoCurrencies, price: Money) {
+ const alexCurrencyMarketData = useMemo(
+ () => createMarketData(createMarketPair(currency, 'USD'), price),
+ [currency, price]
+ );
+
+ return useCallback(
+ (value: Money) => baseCurrencyAmountInQuote(value, alexCurrencyMarketData),
+ [alexCurrencyMarketData]
+ );
+}
diff --git a/src/app/common/hooks/use-explorer-link.ts b/src/app/common/hooks/use-explorer-link.ts
index cd446dc1c7b..f118b1adb6d 100644
--- a/src/app/common/hooks/use-explorer-link.ts
+++ b/src/app/common/hooks/use-explorer-link.ts
@@ -10,13 +10,13 @@ import { openInNewTab } from '../utils/open-in-new-tab';
export interface HandleOpenTxLinkArgs {
blockchain: Blockchains;
suffix?: string;
- txid: string;
+ txId: string;
}
export function useExplorerLink() {
const { mode } = useCurrentNetworkState();
const handleOpenTxLink = useCallback(
- ({ blockchain, suffix, txid }: HandleOpenTxLinkArgs) =>
- openInNewTab(makeTxExplorerLink({ blockchain, mode, suffix, txid })),
+ ({ blockchain, suffix, txId }: HandleOpenTxLinkArgs) =>
+ openInNewTab(makeTxExplorerLink({ blockchain, mode, suffix, txId })),
[mode]
);
diff --git a/src/app/common/hooks/use-loading.ts b/src/app/common/hooks/use-loading.ts
index 991b232bb4c..3d48ef662f6 100644
--- a/src/app/common/hooks/use-loading.ts
+++ b/src/app/common/hooks/use-loading.ts
@@ -1,10 +1,10 @@
import { useLoadingState } from '@app/store/ui/ui.hooks';
export enum LoadingKeys {
- CONFIRM_DRAWER = 'loading/CONFIRM_DRAWER',
INCREASE_FEE_DRAWER = 'loading/INCREASE_FEE_DRAWER',
- SUBMIT_TRANSACTION = 'loading/SUBMIT_TRANSACTION',
- SUBMIT_SIGNATURE = 'loading/SUBMIT_SIGNATURE',
+ SUBMIT_SEND_FORM_TRANSACTION = 'loading/SUBMIT_SEND_FORM_TRANSACTION',
+ SUBMIT_SWAP_TRANSACTION = 'loading/SUBMIT_SWAP_TRANSACTION',
+ SUBMIT_TRANSACTION_REQUEST = 'loading/SUBMIT_TRANSACTION_REQUEST',
}
export function useLoading(key: string) {
diff --git a/src/app/common/math/helpers.ts b/src/app/common/math/helpers.ts
index b38a33a9319..84746b97f80 100644
--- a/src/app/common/math/helpers.ts
+++ b/src/app/common/math/helpers.ts
@@ -1,7 +1,10 @@
import BigNumber from 'bignumber.js';
-export function initBigNumber(num: string | number | BigNumber) {
- return BigNumber.isBigNumber(num) ? num : new BigNumber(num);
+import { isBigInt } from '@shared/utils';
+
+export function initBigNumber(num: string | number | BigNumber | bigint) {
+ if (BigNumber.isBigNumber(num)) return num;
+ return isBigInt(num) ? new BigNumber(num.toString()) : new BigNumber(num);
}
export function sumNumbers(nums: number[]) {
diff --git a/src/app/common/money/calculate-money.ts b/src/app/common/money/calculate-money.ts
index 6a4a9d6943b..8063e2d0ce1 100644
--- a/src/app/common/money/calculate-money.ts
+++ b/src/app/common/money/calculate-money.ts
@@ -1,10 +1,10 @@
import { BigNumber } from 'bignumber.js';
import { MarketData, formatMarketPair } from '@shared/models/market.model';
-import { Money, createMoney } from '@shared/models/money.model';
+import { Money, NumType, createMoney } from '@shared/models/money.model';
import { isNumber } from '@shared/utils';
-import { sumNumbers } from '../math/helpers';
+import { initBigNumber, sumNumbers } from '../math/helpers';
import { formatMoney } from './format-money';
import { isMoney } from './is-money';
@@ -31,6 +31,14 @@ export function convertAmountToFractionalUnit(num: Money | BigNumber, decimals?:
return num.shiftedBy(decimals);
}
+export function convertToMoneyTypeWithDefaultOfZero(
+ symbol: string,
+ num?: NumType,
+ decimals?: number
+) {
+ return createMoney(initBigNumber(num ?? 0), symbol.toUpperCase(), decimals);
+}
+
// ts-unused-exports:disable-next-line
export function convertAmountToBaseUnit(num: Money | BigNumber, decimals?: number) {
if (isMoney(num)) return num.amount.shiftedBy(-num.decimals);
diff --git a/src/app/common/transactions/stacks/transaction.utils.ts b/src/app/common/transactions/stacks/transaction.utils.ts
index e27989125e7..d74dac8d624 100644
--- a/src/app/common/transactions/stacks/transaction.utils.ts
+++ b/src/app/common/transactions/stacks/transaction.utils.ts
@@ -2,6 +2,7 @@ import { bytesToHex } from '@stacks/common';
import { TransactionTypes } from '@stacks/connect';
import {
CoinbaseTransaction,
+ NetworkBlockTimesResponse,
TransactionEventFungibleAsset,
} from '@stacks/stacks-blockchain-api-types';
import {
@@ -126,3 +127,16 @@ export function getTxSenderAddress(tx: StacksTransaction): string | undefined {
);
return txSender;
}
+
+export function getEstimatedConfirmationTime(
+ isTestnet: boolean,
+ blockTime?: NetworkBlockTimesResponse
+) {
+ const arrivesIn = isTestnet
+ ? blockTime?.testnet.target_block_time
+ : blockTime?.mainnet.target_block_time;
+
+ if (!arrivesIn) return '~10 – 20 min';
+
+ return `~${arrivesIn / 60} min`;
+}
diff --git a/src/app/common/utils.ts b/src/app/common/utils.ts
index 2f8314ea828..a715451fe58 100644
--- a/src/app/common/utils.ts
+++ b/src/app/common/utils.ts
@@ -44,19 +44,19 @@ interface MakeTxExplorerLinkArgs {
blockchain: Blockchains;
mode: BitcoinNetworkModes;
suffix?: string;
- txid: string;
+ txId: string;
}
export function makeTxExplorerLink({
blockchain,
mode,
suffix = '',
- txid,
+ txId,
}: MakeTxExplorerLinkArgs) {
switch (blockchain) {
case 'bitcoin':
- return `https://mempool.space/${mode !== 'mainnet' ? mode + '/' : ''}tx/${txid}`;
+ return `https://mempool.space/${mode !== 'mainnet' ? mode + '/' : ''}tx/${txId}`;
case 'stacks':
- return `https://explorer.hiro.so/txid/${txid}?chain=${mode}${suffix}`;
+ return `https://explorer.hiro.so/txid/${txId}?chain=${mode}${suffix}`;
default:
return '';
}
diff --git a/src/app/components/bitcoin-contract-entry-point/bitcoin-contract-entry-point-layout.tsx b/src/app/components/bitcoin-contract-entry-point/bitcoin-contract-entry-point-layout.tsx
index e45c3a21295..3a9dd85f6ab 100644
--- a/src/app/components/bitcoin-contract-entry-point/bitcoin-contract-entry-point-layout.tsx
+++ b/src/app/components/bitcoin-contract-entry-point/bitcoin-contract-entry-point-layout.tsx
@@ -11,7 +11,7 @@ import { Flag } from '@app/components/layout/flag';
import { Tooltip } from '@app/components/tooltip';
import { Caption, Text } from '@app/components/typography';
-import { SmallLoadingSpinner } from '../loading-spinner';
+import { LoadingSpinner } from '../loading-spinner';
interface BitcoinContractEntryPointLayoutProps extends StackProps {
balance: Money;
@@ -48,7 +48,7 @@ export const BitcoinContractEntryPointLayout = forwardRefWithAs(
fontVariantNumeric="tabular-nums"
textAlign="right"
>
- {isLoading ? : formattedBalance.value}
+ {isLoading ? : formattedBalance.value}
diff --git a/src/app/components/bitcoin-transaction-item/bitcoin-transaction-item.tsx b/src/app/components/bitcoin-transaction-item/bitcoin-transaction-item.tsx
index 33ca25356e3..4bc58d0c060 100644
--- a/src/app/components/bitcoin-transaction-item/bitcoin-transaction-item.tsx
+++ b/src/app/components/bitcoin-transaction-item/bitcoin-transaction-item.tsx
@@ -79,7 +79,7 @@ export function BitcoinTransactionItem({ transaction, ...rest }: BitcoinTransact
}
handleOpenTxLink({
blockchain: 'bitcoin',
- txid: transaction?.txid || '',
+ txId: transaction?.txid || '',
});
};
diff --git a/src/app/components/generic-error/generic-error.layout.tsx b/src/app/components/generic-error/generic-error.layout.tsx
index 61b67bf816c..c588c78bda6 100644
--- a/src/app/components/generic-error/generic-error.layout.tsx
+++ b/src/app/components/generic-error/generic-error.layout.tsx
@@ -1,7 +1,6 @@
import { ReactNode } from 'react';
import GenericError from '@assets/images/generic-error.png';
-import { Box, Text, color } from '@stacks/ui';
import { Flex, FlexProps, HStack, styled } from 'leather-styles/jsx';
import { openInNewTab } from '@app/common/utils/open-in-new-tab';
@@ -22,16 +21,8 @@ export function GenericErrorLayout(props: GenericErrorProps) {
const { body, helpTextList, onClose, title, ...rest } = props;
return (
-
-
-
-
+
+
{title}
@@ -44,32 +35,30 @@ export function GenericErrorLayout(props: GenericErrorProps) {
>
{body}
-
{helpTextList}
-
+
- Reach out to our support team
- openInNewTab(supportUrl)}>
+ Reach out to our support team
+ openInNewTab(supportUrl)}>
-
+
-
-
+
+
Close window
diff --git a/src/app/components/generic-error/generic-error.tsx b/src/app/components/generic-error/generic-error.tsx
index c514d3c163f..6928ff7283b 100644
--- a/src/app/components/generic-error/generic-error.tsx
+++ b/src/app/components/generic-error/generic-error.tsx
@@ -1,5 +1,7 @@
import { ReactNode } from 'react';
+import { FlexProps } from 'leather-styles/jsx';
+
import { closeWindow } from '@shared/utils';
import { useRouteHeader } from '@app/common/hooks/use-route-header';
@@ -7,18 +9,24 @@ import { Header } from '@app/components/header';
import { GenericErrorLayout } from './generic-error.layout';
-interface GenericErrorProps {
+interface GenericErrorProps extends FlexProps {
body: string;
helpTextList: ReactNode[];
onClose?(): void;
title: string;
}
export function GenericError(props: GenericErrorProps) {
- const { body, helpTextList, onClose = () => closeWindow(), title } = props;
+ const { body, helpTextList, onClose = () => closeWindow(), title, ...rest } = props;
useRouteHeader();
return (
-
+
);
}
diff --git a/src/app/components/icons/dot-icon.tsx b/src/app/components/icons/dot-icon.tsx
deleted file mode 100644
index 5d15cfb556c..00000000000
--- a/src/app/components/icons/dot-icon.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-export function DotIcon(props: React.SVGProps) {
- return (
-
- );
-}
diff --git a/src/app/components/icons/swap-icon.tsx b/src/app/components/icons/swap-icon.tsx
index 2e7413ca217..c7da54524fd 100644
--- a/src/app/components/icons/swap-icon.tsx
+++ b/src/app/components/icons/swap-icon.tsx
@@ -11,7 +11,7 @@ export function SwapIcon(props: React.SVGProps) {
diff --git a/src/app/components/loading-spinner.tsx b/src/app/components/loading-spinner.tsx
index 1e195f10006..7eeb2633e1b 100644
--- a/src/app/components/loading-spinner.tsx
+++ b/src/app/components/loading-spinner.tsx
@@ -1,17 +1,12 @@
-import { Flex, FlexProps, Spinner, color } from '@stacks/ui';
+import { Spinner, SpinnerSize } from '@stacks/ui';
+import { Flex, FlexProps } from 'leather-styles/jsx';
+import { token } from 'leather-styles/tokens';
-export function LoadingSpinner(props: FlexProps) {
+export function LoadingSpinner(props: { size?: SpinnerSize } & FlexProps) {
+ const { size = 'lg' } = props;
return (
-
-
- );
-}
-
-export function SmallLoadingSpinner(props: FlexProps) {
- return (
-
-
+
);
}
diff --git a/src/app/components/nonce-setter.tsx b/src/app/components/nonce-setter.tsx
index 6fd0e5acb41..e528ed5c4f0 100644
--- a/src/app/components/nonce-setter.tsx
+++ b/src/app/components/nonce-setter.tsx
@@ -1,4 +1,4 @@
-import { useEffect } from 'react';
+import { useAsync } from 'react-async-hook';
import { useFormikContext } from 'formik';
@@ -12,10 +12,10 @@ export function NonceSetter() {
>();
const { data: nextNonce } = useNextNonce();
- useEffect(() => {
- if (nextNonce && !touched.nonce && values.nonce !== nextNonce.nonce)
- setFieldValue('nonce', nextNonce.nonce);
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ useAsync(async () => {
+ if (nextNonce?.nonce && !touched.nonce && values.nonce !== nextNonce.nonce)
+ return await setFieldValue('nonce', nextNonce?.nonce);
+ return;
}, [nextNonce?.nonce]);
return <>>;
diff --git a/src/app/components/stacks-transaction-item/stacks-transaction-item.tsx b/src/app/components/stacks-transaction-item/stacks-transaction-item.tsx
index 8a491299dc3..5235f4bfe84 100644
--- a/src/app/components/stacks-transaction-item/stacks-transaction-item.tsx
+++ b/src/app/components/stacks-transaction-item/stacks-transaction-item.tsx
@@ -52,7 +52,7 @@ export function StacksTransactionItem({
void analytics.track('view_transaction');
handleOpenTxLink({
blockchain: 'stacks',
- txid: transaction?.tx_id || transferDetails?.link || '',
+ txId: transaction?.tx_id || transferDetails?.link || '',
});
};
diff --git a/src/app/features/activity-list/components/submitted-transaction-list/submitted-transaction-item.tsx b/src/app/features/activity-list/components/submitted-transaction-list/submitted-transaction-item.tsx
index 188c4fdfc15..2c458e762de 100644
--- a/src/app/features/activity-list/components/submitted-transaction-list/submitted-transaction-item.tsx
+++ b/src/app/features/activity-list/components/submitted-transaction-list/submitted-transaction-item.tsx
@@ -42,7 +42,7 @@ export function SubmittedTransactionItem(props: SubmittedTransactionItemProps) {
handleOpenTxLink({
blockchain: 'stacks',
suffix: `&submitted=true`,
- txid: txId,
+ txId,
})
}
position="relative"
diff --git a/src/app/features/edit-nonce-drawer/edit-nonce-drawer.tsx b/src/app/features/edit-nonce-drawer/edit-nonce-drawer.tsx
index 938c2f98910..14fb48d4827 100644
--- a/src/app/features/edit-nonce-drawer/edit-nonce-drawer.tsx
+++ b/src/app/features/edit-nonce-drawer/edit-nonce-drawer.tsx
@@ -37,12 +37,15 @@ export function EditNonceDrawer() {
useOnMount(() => setLoadedNextNonce(values.nonce));
- const onGoBack = useCallback(() => navigate('..' + search), [navigate, search]);
+ const onGoBack = useCallback(
+ () => navigate('..' + search, { replace: true }),
+ [navigate, search]
+ );
const onBlur = useCallback(() => validateField('nonce'), [validateField]);
const onSubmit = useCallback(async () => {
- validateField('nonce');
+ await validateField('nonce');
if (!errors.nonce) onGoBack();
}, [errors.nonce, onGoBack, validateField]);
diff --git a/src/app/features/psbt-signer/components/psbt-inputs-and-outputs/components/psbt-input-output-item.layout.tsx b/src/app/features/psbt-signer/components/psbt-inputs-and-outputs/components/psbt-input-output-item.layout.tsx
index 62476e21690..8e74243b475 100644
--- a/src/app/features/psbt-signer/components/psbt-inputs-and-outputs/components/psbt-input-output-item.layout.tsx
+++ b/src/app/features/psbt-signer/components/psbt-inputs-and-outputs/components/psbt-input-output-item.layout.tsx
@@ -57,7 +57,7 @@ export function PsbtInputOutputItemLayout({
onClick={() =>
handleOpenTxLink({
blockchain: 'bitcoin',
- txid: txIdHoverLabel ?? '',
+ txId: txIdHoverLabel ?? '',
})
}
variant="text"
diff --git a/src/app/features/psbt-signer/components/psbt-inputs-outputs-totals/psbt-inputs-outputs-totals.tsx b/src/app/features/psbt-signer/components/psbt-inputs-outputs-totals/psbt-inputs-outputs-totals.tsx
index 7ff20f7a228..de0b0cbc2f6 100644
--- a/src/app/features/psbt-signer/components/psbt-inputs-outputs-totals/psbt-inputs-outputs-totals.tsx
+++ b/src/app/features/psbt-signer/components/psbt-inputs-outputs-totals/psbt-inputs-outputs-totals.tsx
@@ -32,9 +32,7 @@ export function PsbtInputsOutputsTotals() {
) : null}
- {showDivider ? (
-
- ) : null}
+ {showDivider ? : null}
{isReceiving ? (
diff --git a/src/app/features/psbt-signer/components/psbt-request-details-section.layout.tsx b/src/app/features/psbt-signer/components/psbt-request-details-section.layout.tsx
index 4d8d63e29e9..e9e5a7299a6 100644
--- a/src/app/features/psbt-signer/components/psbt-request-details-section.layout.tsx
+++ b/src/app/features/psbt-signer/components/psbt-request-details-section.layout.tsx
@@ -4,15 +4,7 @@ import { HasChildren } from '@app/common/has-children';
export function PsbtRequestDetailsSectionLayout({ children, ...props }: HasChildren & StackProps) {
return (
-
+
{children}
);
diff --git a/src/app/features/stacks-transaction-request/contract-call-details/contract-call-details.tsx b/src/app/features/stacks-transaction-request/contract-call-details/contract-call-details.tsx
index b71e4d8b44b..4e834fe4595 100644
--- a/src/app/features/stacks-transaction-request/contract-call-details/contract-call-details.tsx
+++ b/src/app/features/stacks-transaction-request/contract-call-details/contract-call-details.tsx
@@ -38,7 +38,7 @@ function ContractCallDetailsSuspense() {
onClick={() =>
handleOpenTxLink({
blockchain: 'stacks',
- txid: formatContractId(contractAddress, contractName),
+ txId: formatContractId(contractAddress, contractName),
})
}
contractAddress={contractAddress}
diff --git a/src/app/features/stacks-transaction-request/submit-action.tsx b/src/app/features/stacks-transaction-request/submit-action.tsx
index 98040c53568..54a8e0ad59e 100644
--- a/src/app/features/stacks-transaction-request/submit-action.tsx
+++ b/src/app/features/stacks-transaction-request/submit-action.tsx
@@ -21,7 +21,7 @@ function BaseConfirmButton(props: ButtonProps): React.JSX.Element {
export function SubmitAction() {
const { handleSubmit, values, validateForm } = useFormikContext();
const { isShowingHighFeeConfirmation, setIsShowingHighFeeConfirmation } = useDrawers();
- const { isLoading } = useLoading(LoadingKeys.SUBMIT_TRANSACTION);
+ const { isLoading } = useLoading(LoadingKeys.SUBMIT_TRANSACTION_REQUEST);
const error = useTransactionError();
const isDisabled = !!error || Number(values.fee) < 0;
diff --git a/src/app/pages/bitcoin-contract-list/components/bitcoin-contract-list-item-layout.tsx b/src/app/pages/bitcoin-contract-list/components/bitcoin-contract-list-item-layout.tsx
index ed41f2ec49f..5f94bf2acf8 100644
--- a/src/app/pages/bitcoin-contract-list/components/bitcoin-contract-list-item-layout.tsx
+++ b/src/app/pages/bitcoin-contract-list/components/bitcoin-contract-list-item-layout.tsx
@@ -45,7 +45,7 @@ export function BitcoinContractListItemLayout({
handleOpenTxLink({
blockchain: 'bitcoin',
suffix: `&submitted=true`,
- txid: txId,
+ txId,
})
}
>
diff --git a/src/app/pages/home/components/account-actions.tsx b/src/app/pages/home/components/account-actions.tsx
index 04ab3120e9f..eca16e3585d 100644
--- a/src/app/pages/home/components/account-actions.tsx
+++ b/src/app/pages/home/components/account-actions.tsx
@@ -3,9 +3,9 @@ import { useLocation, useNavigate } from 'react-router-dom';
import { HomePageSelectors } from '@tests/selectors/home.selectors';
import { Flex, FlexProps } from 'leather-styles/jsx';
-import { SWAP_ENABLED } from '@shared/environment';
import { RouteUrls } from '@shared/route-urls';
+import { useWalletType } from '@app/common/use-wallet-type';
import { ArrowDown } from '@app/components/icons/arrow-down';
import { Plus2 } from '@app/components/icons/plus2';
import { SwapIcon } from '@app/components/icons/swap-icon';
@@ -18,6 +18,8 @@ export function AccountActions(props: FlexProps) {
const navigate = useNavigate();
const location = useLocation();
const isBitcoinEnabled = useConfigBitcoinEnabled();
+ const { whenWallet } = useWalletType();
+
const receivePath = isBitcoinEnabled
? RouteUrls.Receive
: `${RouteUrls.Home}${RouteUrls.ReceiveStx}`;
@@ -44,14 +46,17 @@ export function AccountActions(props: FlexProps) {
label="Buy"
onClick={() => navigate(RouteUrls.Fund)}
/>
- {SWAP_ENABLED ? (
- }
- label="Swap"
- onClick={() => navigate(RouteUrls.Swap)}
- />
- ) : null}
+ {whenWallet({
+ software: (
+ }
+ label="Swap"
+ onClick={() => navigate(RouteUrls.Swap)}
+ />
+ ),
+ ledger: null,
+ })}
);
}
diff --git a/src/app/pages/rpc-sign-psbt/use-rpc-sign-psbt.tsx b/src/app/pages/rpc-sign-psbt/use-rpc-sign-psbt.tsx
index e21974bf969..f955fd83d51 100644
--- a/src/app/pages/rpc-sign-psbt/use-rpc-sign-psbt.tsx
+++ b/src/app/pages/rpc-sign-psbt/use-rpc-sign-psbt.tsx
@@ -65,7 +65,7 @@ export function useRpcSignPsbt() {
txId: txid,
txLink: {
blockchain: 'bitcoin',
- txid: txid || '',
+ txId: txid || '',
},
txValue: formatMoney(transferTotalAsMoney),
};
diff --git a/src/app/pages/send/ordinal-inscription/sent-inscription-summary.tsx b/src/app/pages/send/ordinal-inscription/sent-inscription-summary.tsx
index 09ac0b56bec..ad6111d1b37 100644
--- a/src/app/pages/send/ordinal-inscription/sent-inscription-summary.tsx
+++ b/src/app/pages/send/ordinal-inscription/sent-inscription-summary.tsx
@@ -41,7 +41,7 @@ export function SendInscriptionSummary() {
const navigate = useNavigate();
const txLink = {
blockchain: 'bitcoin' as Blockchains,
- txid: txId || '',
+ txId,
};
const { onCopy } = useClipboard(txId || '');
diff --git a/src/app/pages/send/send-crypto-asset-form/components/confirmation/send-form-confirmation.utils.tsx b/src/app/pages/send/send-crypto-asset-form/components/confirmation/send-form-confirmation.utils.tsx
deleted file mode 100644
index 7f7efccf5a3..00000000000
--- a/src/app/pages/send/send-crypto-asset-form/components/confirmation/send-form-confirmation.utils.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import BigNumber from 'bignumber.js';
-
-import { NumType, createMoney } from '@shared/models/money.model';
-import { isBigInt } from '@shared/utils';
-
-export function convertToMoneyTypeWithDefaultOfZero(
- symbol: string,
- num?: NumType,
- decimals?: number
-) {
- return createMoney(
- isBigInt(num) ? new BigNumber(num.toString()) : new BigNumber(num ?? 0),
- symbol.toUpperCase(),
- decimals
- );
-}
diff --git a/src/app/pages/send/send-crypto-asset-form/family/stacks/hooks/use-stacks-broadcast-transaction.tsx b/src/app/pages/send/send-crypto-asset-form/family/stacks/hooks/use-stacks-broadcast-transaction.tsx
index 6f735bfb0b4..0f48f98487d 100644
--- a/src/app/pages/send/send-crypto-asset-form/family/stacks/hooks/use-stacks-broadcast-transaction.tsx
+++ b/src/app/pages/send/send-crypto-asset-form/family/stacks/hooks/use-stacks-broadcast-transaction.tsx
@@ -26,7 +26,7 @@ export function useStacksBroadcastTransaction(
const navigate = useNavigate();
const broadcastTransactionFn = useSubmitTransactionCallback({
- loadingKey: LoadingKeys.CONFIRM_DRAWER,
+ loadingKey: LoadingKeys.SUBMIT_SEND_FORM_TRANSACTION,
});
return useMemo(() => {
diff --git a/src/app/pages/send/send-crypto-asset-form/family/stacks/hooks/use-stacks-transaction-summary.ts b/src/app/pages/send/send-crypto-asset-form/family/stacks/hooks/use-stacks-transaction-summary.ts
index 7d99d1c2205..baed2213cf2 100644
--- a/src/app/pages/send/send-crypto-asset-form/family/stacks/hooks/use-stacks-transaction-summary.ts
+++ b/src/app/pages/send/send-crypto-asset-form/family/stacks/hooks/use-stacks-transaction-summary.ts
@@ -11,15 +11,18 @@ import BigNumber from 'bignumber.js';
import { CryptoCurrencies } from '@shared/models/currencies.model';
import { createMoney } from '@shared/models/money.model';
+import { removeTrailingNullCharacters } from '@shared/utils';
-import { baseCurrencyAmountInQuote } from '@app/common/money/calculate-money';
+import {
+ baseCurrencyAmountInQuote,
+ convertToMoneyTypeWithDefaultOfZero,
+} from '@app/common/money/calculate-money';
import { formatMoney, i18nFormatCurrency } from '@app/common/money/format-money';
+import { getEstimatedConfirmationTime } from '@app/common/transactions/stacks/transaction.utils';
import { useCryptoCurrencyMarketData } from '@app/query/common/market-data/market-data.hooks';
import { useStacksBlockTime } from '@app/query/stacks/info/info.hooks';
import { useCurrentNetworkState } from '@app/store/networks/networks.hooks';
-import { convertToMoneyTypeWithDefaultOfZero } from '../../../components/confirmation/send-form-confirmation.utils';
-
export function useStacksTransactionSummary(token: CryptoCurrencies) {
const tokenMarketData = useCryptoCurrencyMarketData(token);
const { isTestnet } = useCurrentNetworkState();
@@ -54,7 +57,7 @@ export function useStacksTransactionSummary(token: CryptoCurrencies) {
recipient: addressToString(payload.recipient.address),
fee: formatMoney(convertToMoneyTypeWithDefaultOfZero('STX', Number(fee))),
totalSpend: formatMoney(convertToMoneyTypeWithDefaultOfZero('STX', Number(txValue + fee))),
- arrivesIn: getArrivesInTime(),
+ arrivesIn: getEstimatedConfirmationTime(isTestnet, blockTime),
symbol: 'STX',
txValue: microStxToStx(Number(txValue)),
sendingValue: formatMoney(convertToMoneyTypeWithDefaultOfZero('STX', Number(txValue))),
@@ -91,7 +94,7 @@ export function useStacksTransactionSummary(token: CryptoCurrencies) {
return {
recipient: cvToString(payload.functionArgs[2]),
- arrivesIn: getArrivesInTime(),
+ arrivesIn: getEstimatedConfirmationTime(isTestnet, blockTime),
txValue: new BigNumber(txValue).shiftedBy(-decimals).toString(),
nonce: String(tx.auth.spendingCondition.nonce),
fee: feeValue,
@@ -104,22 +107,6 @@ export function useStacksTransactionSummary(token: CryptoCurrencies) {
};
}
- function getArrivesInTime() {
- let arrivesIn = isTestnet
- ? blockTime?.testnet.target_block_time
- : blockTime?.mainnet.target_block_time;
- if (!arrivesIn) {
- return '~10 – 20 min';
- }
-
- arrivesIn = arrivesIn / 60;
- return `~${arrivesIn} min`;
- }
-
- function removeTrailingNullCharacters(s: string) {
- return s.replace(/\0*$/g, '');
- }
-
return {
formSentSummaryTxState,
formReviewTxSummary,
diff --git a/src/app/pages/swap/components/selected-asset-field.tsx b/src/app/pages/swap/components/selected-asset-field.tsx
index b5eef81350c..aa47dbe4784 100644
--- a/src/app/pages/swap/components/selected-asset-field.tsx
+++ b/src/app/pages/swap/components/selected-asset-field.tsx
@@ -1,48 +1,40 @@
import { Field } from 'formik';
-import { Box, Flex, HStack, styled } from 'leather-styles/jsx';
-
-import { Flag } from '@app/components/layout/flag';
+import { Box, HStack, styled } from 'leather-styles/jsx';
interface SelectedAssetFieldProps {
contentLeft: React.JSX.Element;
contentRight: React.JSX.Element;
- icon?: string;
name: string;
+ showError?: boolean;
}
export function SelectedAssetField({
contentLeft,
contentRight,
- icon,
name,
+ showError,
}: SelectedAssetFieldProps) {
return (
-
- : null
- }
- spacing="tight"
- >
-
- {contentLeft}
- {contentRight}
-
-
+
+ {contentLeft}
+ {contentRight}
+
-
+
);
}
diff --git a/src/app/pages/swap/components/swap-amount-field.tsx b/src/app/pages/swap/components/swap-amount-field.tsx
index afd3fbb19cd..50a2e44dd60 100644
--- a/src/app/pages/swap/components/swap-amount-field.tsx
+++ b/src/app/pages/swap/components/swap-amount-field.tsx
@@ -1,58 +1,83 @@
import { ChangeEvent } from 'react';
-import { Input, Stack, color } from '@stacks/ui';
+import BigNumber from 'bignumber.js';
import { useField, useFormikContext } from 'formik';
+import { Stack, styled } from 'leather-styles/jsx';
+
+import { createMoney } from '@shared/models/money.model';
+import { isDefined, isUndefined } from '@shared/utils';
import { useShowFieldError } from '@app/common/form-utils';
-import { Caption } from '@app/components/typography';
+import { convertAmountToFractionalUnit } from '@app/common/money/calculate-money';
+import { formatMoneyWithoutSymbol } from '@app/common/money/format-money';
-import { SwapFormValues } from '../hooks/use-swap';
+import { SwapFormValues } from '../hooks/use-swap-form';
import { useSwapContext } from '../swap.context';
+function getPlaceholderValue(name: string, values: SwapFormValues) {
+ if (name === 'swapAmountFrom' && isDefined(values.swapAssetFrom)) return '0';
+ if (name === 'swapAmountTo' && isDefined(values.swapAssetTo)) return '0';
+ return '-';
+}
+
interface SwapAmountFieldProps {
amountAsFiat: string;
isDisabled?: boolean;
name: string;
}
export function SwapAmountField({ amountAsFiat, isDisabled, name }: SwapAmountFieldProps) {
- const { exchangeRate, onSetIsSendingMax } = useSwapContext();
- const { setFieldValue } = useFormikContext();
+ const { fetchToAmount, isFetchingExchangeRate, onSetIsSendingMax } = useSwapContext();
+ const { setFieldError, setFieldValue, values } = useFormikContext();
const [field] = useField(name);
- const showError = useShowFieldError(name);
+ const showError = useShowFieldError(name) && name === 'swapAmountFrom' && values.swapAssetTo;
- async function onChange(event: ChangeEvent) {
+ async function onBlur(event: ChangeEvent) {
+ const { swapAssetFrom, swapAssetTo } = values;
+ if (isUndefined(swapAssetFrom) || isUndefined(swapAssetTo)) return;
onSetIsSendingMax(false);
const value = event.currentTarget.value;
- await setFieldValue('swapAmountTo', Number(value) * exchangeRate);
- field.onChange(event);
+ const toAmount = await fetchToAmount(swapAssetFrom, swapAssetTo, value);
+ if (isUndefined(toAmount)) {
+ await setFieldValue('swapAmountTo', '');
+ return;
+ }
+ const toAmountAsMoney = createMoney(
+ convertAmountToFractionalUnit(new BigNumber(toAmount), values.swapAssetTo?.balance.decimals),
+ values.swapAssetTo?.balance.symbol ?? '',
+ values.swapAssetTo?.balance.decimals
+ );
+ await setFieldValue('swapAmountTo', formatMoneyWithoutSymbol(toAmountAsMoney));
+ setFieldError('swapAmountTo', undefined);
}
return (
-
-
- {name}
-
-
+
-
- {amountAsFiat}
-
+ {amountAsFiat ? (
+
+ {amountAsFiat}
+
+ ) : null}
);
}
diff --git a/src/app/pages/swap/components/swap-assets-pair/swap-asset-item.layout.tsx b/src/app/pages/swap/components/swap-assets-pair/swap-asset-item.layout.tsx
index 2cdd5fbe80d..19fe903475b 100644
--- a/src/app/pages/swap/components/swap-assets-pair/swap-asset-item.layout.tsx
+++ b/src/app/pages/swap/components/swap-assets-pair/swap-asset-item.layout.tsx
@@ -3,18 +3,22 @@ import { HStack, styled } from 'leather-styles/jsx';
import { Flag } from '@app/components/layout/flag';
interface SwapAssetItemLayoutProps {
+ caption: string;
icon: string;
symbol: string;
value: string;
}
-export function SwapAssetItemLayout({ icon, symbol, value }: SwapAssetItemLayoutProps) {
+export function SwapAssetItemLayout({ caption, icon, symbol, value }: SwapAssetItemLayoutProps) {
return (
}
- spacing="tight"
+ img={}
+ spacing="space.03"
width="100%"
>
+
+ {caption}
+
{symbol}
{value}
diff --git a/src/app/pages/swap/components/swap-assets-pair/swap-assets-pair.layout.tsx b/src/app/pages/swap/components/swap-assets-pair/swap-assets-pair.layout.tsx
index 9e678a4a785..4ef675deb52 100644
--- a/src/app/pages/swap/components/swap-assets-pair/swap-assets-pair.layout.tsx
+++ b/src/app/pages/swap/components/swap-assets-pair/swap-assets-pair.layout.tsx
@@ -9,17 +9,16 @@ interface SwapAssetsPairLayoutProps {
export function SwapAssetsPairLayout({ swapAssetFrom, swapAssetTo }: SwapAssetsPairLayoutProps) {
return (
{swapAssetFrom}
-
+
{swapAssetTo}
diff --git a/src/app/pages/swap/components/swap-assets-pair/swap-assets-pair.tsx b/src/app/pages/swap/components/swap-assets-pair/swap-assets-pair.tsx
index b6377926500..cda96afe328 100644
--- a/src/app/pages/swap/components/swap-assets-pair/swap-assets-pair.tsx
+++ b/src/app/pages/swap/components/swap-assets-pair/swap-assets-pair.tsx
@@ -1,18 +1,21 @@
+import { useNavigate } from 'react-router-dom';
+
import { useFormikContext } from 'formik';
-import { logger } from '@shared/logger';
+import { RouteUrls } from '@shared/route-urls';
import { isUndefined } from '@shared/utils';
-import { SwapFormValues } from '../../hooks/use-swap';
+import { SwapFormValues } from '../../hooks/use-swap-form';
import { SwapAssetItemLayout } from './swap-asset-item.layout';
import { SwapAssetsPairLayout } from './swap-assets-pair.layout';
export function SwapAssetsPair() {
const { values } = useFormikContext();
const { swapAmountFrom, swapAmountTo, swapAssetFrom, swapAssetTo } = values;
+ const navigate = useNavigate();
if (isUndefined(swapAssetFrom) || isUndefined(swapAssetTo)) {
- logger.error('No asset selected to swap');
+ navigate(RouteUrls.Swap, { replace: true });
return null;
}
@@ -20,6 +23,7 @@ export function SwapAssetsPair() {
}
- >
+ />
);
}
diff --git a/src/app/pages/swap/components/swap-content.layout.tsx b/src/app/pages/swap/components/swap-content.layout.tsx
index dc535460247..d6e94e7d75d 100644
--- a/src/app/pages/swap/components/swap-content.layout.tsx
+++ b/src/app/pages/swap/components/swap-content.layout.tsx
@@ -11,8 +11,8 @@ export function SwapContentLayout({ children }: HasChildren) {
flexDirection="column"
maxHeight={['calc(100vh - 116px)', 'calc(85vh - 116px)']}
overflowY="auto"
- pb={['120px', '48px']}
- pt={['space.04', '48px']}
+ pb={['60px', 'unset']}
+ pt="space.02"
px="space.05"
width="100%"
>
diff --git a/src/app/pages/swap/components/swap-details/swap-detail.layout.tsx b/src/app/pages/swap/components/swap-details/swap-detail.layout.tsx
index 1e44ce2dcb4..c2cf222318e 100644
--- a/src/app/pages/swap/components/swap-details/swap-detail.layout.tsx
+++ b/src/app/pages/swap/components/swap-details/swap-detail.layout.tsx
@@ -1,3 +1,5 @@
+import { ReactNode } from 'react';
+
import { Box, HStack, styled } from 'leather-styles/jsx';
import { InfoIcon } from '@app/components/icons/info-icon';
@@ -6,22 +8,26 @@ import { Tooltip } from '@app/components/tooltip';
interface SwapDetailLayoutProps {
title: string;
tooltipLabel?: string;
- value: string;
+ value: ReactNode;
}
export function SwapDetailLayout({ title, tooltipLabel, value }: SwapDetailLayoutProps) {
return (
-
+
- {title}
+
+ {title}
+
{tooltipLabel ? (
-
+
) : null}
- {value}
+
+ {value}
+
);
}
diff --git a/src/app/pages/swap/components/swap-details/swap-details.layout.tsx b/src/app/pages/swap/components/swap-details/swap-details.layout.tsx
index 3fd7a3514ff..337e8d47e47 100644
--- a/src/app/pages/swap/components/swap-details/swap-details.layout.tsx
+++ b/src/app/pages/swap/components/swap-details/swap-details.layout.tsx
@@ -1,18 +1,11 @@
-import { Box, Stack, styled } from 'leather-styles/jsx';
+import { Stack } from 'leather-styles/jsx';
import { HasChildren } from '@app/common/has-children';
export function SwapDetailsLayout({ children }: HasChildren) {
return (
-
-
- Swap details
-
-
-
- {children}
-
-
+
+ {children}
);
}
diff --git a/src/app/pages/swap/components/swap-details/swap-details.tsx b/src/app/pages/swap/components/swap-details/swap-details.tsx
index 27c5fe64eab..eee3d91f041 100644
--- a/src/app/pages/swap/components/swap-details/swap-details.tsx
+++ b/src/app/pages/swap/components/swap-details/swap-details.tsx
@@ -1,14 +1,87 @@
+import BigNumber from 'bignumber.js';
+import { HStack, styled } from 'leather-styles/jsx';
+
+import { createMoney } from '@shared/models/money.model';
+import { isDefined, isUndefined } from '@shared/utils';
+
+import { formatMoneyPadded } from '@app/common/money/format-money';
+import { microStxToStx } from '@app/common/money/unit-conversion';
+import { getEstimatedConfirmationTime } from '@app/common/transactions/stacks/transaction.utils';
+import { ChevronUpIcon } from '@app/components/icons/chevron-up-icon';
+import { SwapSubmissionData, useSwapContext } from '@app/pages/swap/swap.context';
+import { useStacksBlockTime } from '@app/query/stacks/info/info.hooks';
+import { useCurrentNetworkState } from '@app/store/networks/networks.hooks';
+
import { SwapDetailLayout } from './swap-detail.layout';
import { SwapDetailsLayout } from './swap-details.layout';
-// TODO: Replace with live data
+function RouteNames(props: { swapSubmissionData: SwapSubmissionData }) {
+ return props.swapSubmissionData.router.map((route, i) => {
+ const insertIcon = isDefined(props.swapSubmissionData.router[i + 1]);
+ return (
+ <>
+ {route.name}
+ {insertIcon && }
+ >
+ );
+ });
+}
+
export function SwapDetails() {
+ const { swapSubmissionData } = useSwapContext();
+ const { isTestnet } = useCurrentNetworkState();
+ const { data: blockTime } = useStacksBlockTime();
+
+ if (
+ isUndefined(swapSubmissionData) ||
+ isUndefined(swapSubmissionData.swapAssetFrom) ||
+ isUndefined(swapSubmissionData.swapAssetTo)
+ )
+ return null;
+
+ const formattedMinToReceive = formatMoneyPadded(
+ createMoney(
+ new BigNumber(swapSubmissionData.swapAmountTo).times(1 - swapSubmissionData.slippage),
+ swapSubmissionData.swapAssetTo.balance.symbol,
+ swapSubmissionData.swapAssetTo.balance.decimals
+ )
+ );
+
return (
-
-
-
-
+
+
+
+
+ }
+ />
+
+
+
+
+
+
);
}
diff --git a/src/app/pages/swap/components/swap-form.tsx b/src/app/pages/swap/components/swap-form.tsx
index 50f631a5cda..90915b4bdfd 100644
--- a/src/app/pages/swap/components/swap-form.tsx
+++ b/src/app/pages/swap/components/swap-form.tsx
@@ -1,21 +1,21 @@
import { Form, Formik } from 'formik';
import { Box } from 'leather-styles/jsx';
-import { noop } from '@shared/utils';
-
import { HasChildren } from '@app/common/has-children';
-import { useSwap } from '../hooks/use-swap';
+import { useSwapForm } from '../hooks/use-swap-form';
+import { useSwapContext } from '../swap.context';
export function SwapForm({ children }: HasChildren) {
- const { initialValues, validationSchema } = useSwap();
+ const { initialValues, validationSchema } = useSwapForm();
+ const { onSubmitSwapForReview } = useSwapContext();
return (
diff --git a/src/app/pages/swap/components/swap-selected-asset-from.tsx b/src/app/pages/swap/components/swap-selected-asset-from.tsx
index 71a781ea15d..3a493dadef4 100644
--- a/src/app/pages/swap/components/swap-selected-asset-from.tsx
+++ b/src/app/pages/swap/components/swap-selected-asset-from.tsx
@@ -1,69 +1,85 @@
+import BigNumber from 'bignumber.js';
import { useField, useFormikContext } from 'formik';
+import { createMoney } from '@shared/models/money.model';
import { isUndefined } from '@shared/utils';
import { useShowFieldError } from '@app/common/form-utils';
+import { convertAmountToFractionalUnit } from '@app/common/money/calculate-money';
import { formatMoneyWithoutSymbol } from '@app/common/money/format-money';
-import { useAmountAsFiat } from '../hooks/use-amount-as-fiat';
-import { SwapFormValues } from '../hooks/use-swap';
+import { useAlexSdkAmountAsFiat } from '../hooks/use-alex-sdk-fiat-price';
+import { SwapFormValues } from '../hooks/use-swap-form';
import { useSwapContext } from '../swap.context';
import { SwapAmountField } from './swap-amount-field';
import { SwapSelectedAssetLayout } from './swap-selected-asset.layout';
-const sendingMaxCaption = 'Using max available';
-const sendingMaxTooltip = 'When sending max, this amount is affected by the fee you choose.';
-
-const maxAvailableCaption = 'Max available in your balance';
+const availableBalanceCaption = 'Available balance';
const maxAvailableTooltip =
- 'Amount of funds that is immediately available for use, after taking into account any pending transactions or holds placed on your account by the protocol.';
-
-const sendAnyValue = 'Send any value';
-
+ 'Amount of funds that are immediately available for use, after taking into account any pending transactions or holds placed on your account by the protocol.';
+const sendingMaxTooltip = 'When sending max, this amount is affected by the fee you choose.';
interface SwapSelectedAssetFromProps {
onChooseAsset(): void;
title: string;
}
export function SwapSelectedAssetFrom({ onChooseAsset, title }: SwapSelectedAssetFromProps) {
- const { exchangeRate, isSendingMax, onSetIsSendingMax } = useSwapContext();
- const { setFieldValue, validateForm, values } = useFormikContext();
+ const { fetchToAmount, isFetchingExchangeRate, isSendingMax, onSetIsSendingMax } =
+ useSwapContext();
+ const { setFieldValue, setFieldError, values } = useFormikContext();
const [amountField, amountFieldMeta, amountFieldHelpers] = useField('swapAmountFrom');
const showError = useShowFieldError('swapAmountFrom');
const [assetField] = useField('swapAssetFrom');
- const amountAsFiat = useAmountAsFiat(amountField.value, assetField.value.balance);
-
+ const amountAsFiat = useAlexSdkAmountAsFiat(
+ assetField.value.balance,
+ assetField.value.price,
+ amountField.value
+ );
const formattedBalance = formatMoneyWithoutSymbol(assetField.value.balance);
+ const isSwapAssetFromBalanceGreaterThanZero =
+ values.swapAssetFrom?.balance.amount.isGreaterThan(0);
async function onSetMaxBalanceAsAmountToSwap() {
- if (isUndefined(values.swapAssetTo)) return;
+ const { swapAssetFrom, swapAssetTo } = values;
+ if (isFetchingExchangeRate || isUndefined(swapAssetFrom)) return;
onSetIsSendingMax(!isSendingMax);
- const value = isSendingMax ? '' : formattedBalance;
- await amountFieldHelpers.setValue(value);
- await setFieldValue('swapAmountTo', Number(value) * exchangeRate);
- await validateForm();
+ await amountFieldHelpers.setValue(Number(formattedBalance));
+ await amountFieldHelpers.setTouched(true);
+ if (isUndefined(swapAssetTo)) return;
+ const toAmount = await fetchToAmount(swapAssetFrom, swapAssetTo, formattedBalance);
+ if (isUndefined(toAmount)) {
+ await setFieldValue('swapAmountTo', '');
+ return;
+ }
+ const toAmountAsMoney = createMoney(
+ convertAmountToFractionalUnit(new BigNumber(toAmount), values.swapAssetTo?.balance.decimals),
+ values.swapAssetTo?.balance.symbol ?? '',
+ values.swapAssetTo?.balance.decimals
+ );
+ await setFieldValue('swapAmountTo', formatMoneyWithoutSymbol(toAmountAsMoney));
+ setFieldError('swapAmountTo', undefined);
}
return (
}
- symbol={assetField.value.balance.symbol}
+ symbol={assetField.value.name}
title={title}
tooltipLabel={isSendingMax ? sendingMaxTooltip : maxAvailableTooltip}
- value={isSendingMax ? sendAnyValue : formattedBalance}
+ value={formattedBalance}
/>
);
}
diff --git a/src/app/pages/swap/components/swap-selected-asset-placeholder.tsx b/src/app/pages/swap/components/swap-selected-asset-placeholder.tsx
deleted file mode 100644
index 651ccdec8a9..00000000000
--- a/src/app/pages/swap/components/swap-selected-asset-placeholder.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { SwapAmountField } from './swap-amount-field';
-import { SwapSelectedAssetLayout } from './swap-selected-asset.layout';
-
-interface SwapSelectedAssetPlaceholderProps {
- onChooseAsset(): void;
- showToggle?: boolean;
- title: string;
-}
-export function SwapSelectedAssetPlaceholder({
- onChooseAsset,
- showToggle,
- title,
-}: SwapSelectedAssetPlaceholderProps) {
- return (
- }
- symbol="Select asset"
- title={title}
- value="0"
- />
- );
-}
diff --git a/src/app/pages/swap/components/swap-selected-asset-to.tsx b/src/app/pages/swap/components/swap-selected-asset-to.tsx
index 19472289e7b..7e2c94f7049 100644
--- a/src/app/pages/swap/components/swap-selected-asset-to.tsx
+++ b/src/app/pages/swap/components/swap-selected-asset-to.tsx
@@ -1,8 +1,10 @@
import { useField } from 'formik';
import { formatMoneyWithoutSymbol } from '@app/common/money/format-money';
+import { LoadingSpinner } from '@app/components/loading-spinner';
-import { useAmountAsFiat } from '../hooks/use-amount-as-fiat';
+import { useAlexSdkAmountAsFiat } from '../hooks/use-alex-sdk-fiat-price';
+import { useSwapContext } from '../swap.context';
import { SwapAmountField } from './swap-amount-field';
import { SwapSelectedAssetLayout } from './swap-selected-asset.layout';
@@ -11,24 +13,33 @@ interface SwapSelectedAssetToProps {
title: string;
}
export function SwapSelectedAssetTo({ onChooseAsset, title }: SwapSelectedAssetToProps) {
+ const { isFetchingExchangeRate } = useSwapContext();
const [amountField] = useField('swapAmountTo');
const [assetField] = useField('swapAssetTo');
- const amountAsFiat = useAmountAsFiat(amountField.value, assetField.value.balance);
+ const amountAsFiat = useAlexSdkAmountAsFiat(
+ assetField.value?.balance,
+ assetField.value?.price,
+ amountField.value
+ );
return (
+ isFetchingExchangeRate ? (
+
+ ) : (
+
+ )
}
- symbol={assetField.value.balance.symbol}
+ symbol={assetField.value?.name ?? 'Select asset'}
title={title}
- value={formatMoneyWithoutSymbol(assetField.value.balance)}
+ value={assetField.value?.balance ? formatMoneyWithoutSymbol(assetField.value?.balance) : '0'}
/>
);
}
diff --git a/src/app/pages/swap/components/swap-selected-asset.layout.tsx b/src/app/pages/swap/components/swap-selected-asset.layout.tsx
index 4ce083fd6f0..7def032844c 100644
--- a/src/app/pages/swap/components/swap-selected-asset.layout.tsx
+++ b/src/app/pages/swap/components/swap-selected-asset.layout.tsx
@@ -4,7 +4,6 @@ import { noop } from '@shared/utils';
import { LeatherButton } from '@app/components/button/button';
import { ChevronDownIcon } from '@app/components/icons/chevron-down-icon';
-import { InfoIcon } from '@app/components/icons/info-icon';
import { Tooltip } from '@app/components/tooltip';
import { SelectedAssetField } from './selected-asset-field';
@@ -50,44 +49,48 @@ export function SwapSelectedAssetLayout({
return (
-
+
{title}
{showToggle && }
+
+ {icon && }
{symbol}
-
+
}
contentRight={swapAmountInput}
- icon={icon}
name={name}
+ showError={showError}
/>
{caption ? (
-
-
- {error ?? caption}
+
+
+ {showError ? error : caption}
- {tooltipLabel ? (
-
-
-
-
-
- ) : null}
-
+
-
- {value}
-
+ {value}
) : null}
diff --git a/src/app/pages/swap/components/swap-selected-assets.tsx b/src/app/pages/swap/components/swap-selected-assets.tsx
index 2fbefce8720..3c685bc4597 100644
--- a/src/app/pages/swap/components/swap-selected-assets.tsx
+++ b/src/app/pages/swap/components/swap-selected-assets.tsx
@@ -1,20 +1,14 @@
import { useNavigate } from 'react-router-dom';
-import { useFormikContext } from 'formik';
-
import { RouteUrls } from '@shared/route-urls';
-import { isUndefined } from '@shared/utils';
-import { SwapFormValues } from '../hooks/use-swap';
import { SwapSelectedAssetFrom } from './swap-selected-asset-from';
-import { SwapSelectedAssetPlaceholder } from './swap-selected-asset-placeholder';
import { SwapSelectedAssetTo } from './swap-selected-asset-to';
-const titleFrom = 'Convert';
-const titleTo = 'To';
+const titleFrom = 'You pay';
+const titleTo = 'You receive';
export function SwapSelectedAssets() {
- const { values } = useFormikContext();
const navigate = useNavigate();
function onChooseAssetFrom() {
@@ -27,16 +21,8 @@ export function SwapSelectedAssets() {
return (
<>
- {isUndefined(values.swapAssetFrom) ? (
-
- ) : (
-
- )}
- {isUndefined(values.swapAssetTo) ? (
-
- ) : (
-
- )}
+
+
>
);
}
diff --git a/src/app/pages/swap/components/swap-status/swap-status-item.layout.tsx b/src/app/pages/swap/components/swap-status/swap-status-item.layout.tsx
deleted file mode 100644
index a4b74e58f44..00000000000
--- a/src/app/pages/swap/components/swap-status/swap-status-item.layout.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { HStack, Stack, styled } from 'leather-styles/jsx';
-
-import { ArrowUpIcon } from '@app/components/icons/arrow-up-icon';
-import { Flag } from '@app/components/layout/flag';
-
-interface SwapStatusItemLayoutProps {
- icon: React.JSX.Element;
- text: string;
- timestamp?: string;
-}
-export function SwapStatusItemLayout({ icon, text, timestamp }: SwapStatusItemLayoutProps) {
- return (
-
-
-
- {timestamp ? {timestamp} : null}
- {text}
-
-
-
-
- );
-}
diff --git a/src/app/pages/swap/components/swap-status/swap-status.layout.tsx b/src/app/pages/swap/components/swap-status/swap-status.layout.tsx
deleted file mode 100644
index 01ba1c941ac..00000000000
--- a/src/app/pages/swap/components/swap-status/swap-status.layout.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import { Stack } from 'leather-styles/jsx';
-
-import { HasChildren } from '@app/common/has-children';
-
-export function SwapStatusLayout({ children }: HasChildren) {
- return {children};
-}
diff --git a/src/app/pages/swap/components/swap-status/swap-status.tsx b/src/app/pages/swap/components/swap-status/swap-status.tsx
deleted file mode 100644
index af47184b6ea..00000000000
--- a/src/app/pages/swap/components/swap-status/swap-status.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { DashedHr } from '@app/components/hr';
-import { CheckmarkIcon } from '@app/components/icons/checkmark-icon';
-import { DotIcon } from '@app/components/icons/dot-icon';
-
-import { SwapStatusItemLayout } from './swap-status-item.layout';
-import { SwapStatusLayout } from './swap-status.layout';
-
-// TODO: Replace with live data
-export function SwapStatus() {
- return (
-
- }
- text="You set up your swap"
- timestamp="Today at 10:14 PM"
- />
-
- }
- text="We received your BTC"
- timestamp="Today at 10:14 PM"
- />
-
- } text="We escrow your transaction" />
-
- } text="We add your xBTC to your balance" />
-
- );
-}
diff --git a/src/app/pages/swap/components/swap-toggle-button.tsx b/src/app/pages/swap/components/swap-toggle-button.tsx
index 150d7fca3e5..6ffafcff1e4 100644
--- a/src/app/pages/swap/components/swap-toggle-button.tsx
+++ b/src/app/pages/swap/components/swap-toggle-button.tsx
@@ -1,30 +1,48 @@
import { useFormikContext } from 'formik';
import { styled } from 'leather-styles/jsx';
+import { isDefined, isUndefined } from '@shared/utils';
+
import { SwapIcon } from '@app/components/icons/swap-icon';
-import { SwapFormValues } from '../hooks/use-swap';
+import { SwapFormValues } from '../hooks/use-swap-form';
import { useSwapContext } from '../swap.context';
export function SwapToggleButton() {
- const { onSetIsSendingMax } = useSwapContext();
- const { setFieldValue, values } = useFormikContext();
+ const { fetchToAmount, isFetchingExchangeRate, onSetIsSendingMax } = useSwapContext();
+ const { setFieldValue, validateForm, values } = useFormikContext();
async function onToggleSwapAssets() {
onSetIsSendingMax(false);
+
const prevAmountFrom = values.swapAmountFrom;
const prevAmountTo = values.swapAmountTo;
const prevAssetFrom = values.swapAssetFrom;
const prevAssetTo = values.swapAssetTo;
- await setFieldValue('swapAmountFrom', prevAmountTo);
- await setFieldValue('swapAmountTo', prevAmountFrom);
await setFieldValue('swapAssetFrom', prevAssetTo);
await setFieldValue('swapAssetTo', prevAssetFrom);
+ await setFieldValue('swapAmountFrom', prevAmountTo);
+
+ if (isDefined(prevAssetFrom) && isDefined(prevAssetTo)) {
+ const toAmount = await fetchToAmount(prevAssetTo, prevAssetFrom, prevAmountTo);
+ if (isUndefined(toAmount)) {
+ await setFieldValue('swapAmountTo', '');
+ return;
+ }
+ await setFieldValue('swapAmountTo', Number(toAmount));
+ } else {
+ await setFieldValue('swapAmountTo', Number(prevAmountFrom));
+ }
+ await validateForm();
}
return (
-
+
);
diff --git a/src/app/pages/swap/hooks/use-alex-broadcast-swap.ts b/src/app/pages/swap/hooks/use-alex-broadcast-swap.ts
new file mode 100644
index 00000000000..b3fa5bde8ed
--- /dev/null
+++ b/src/app/pages/swap/hooks/use-alex-broadcast-swap.ts
@@ -0,0 +1,36 @@
+import { useCallback } from 'react';
+import { useNavigate } from 'react-router-dom';
+
+import { AlexSDK, SponsoredTxError } from 'alex-sdk';
+
+import { logger } from '@shared/logger';
+import { RouteUrls } from '@shared/route-urls';
+
+import { LoadingKeys, useLoading } from '@app/common/hooks/use-loading';
+import { delay } from '@app/common/utils';
+
+export function useAlexBroadcastSwap(alexSDK: AlexSDK) {
+ const { setIsIdle } = useLoading(LoadingKeys.SUBMIT_SWAP_TRANSACTION);
+ const navigate = useNavigate();
+
+ return useCallback(
+ async (txRaw: string) => {
+ try {
+ const txId = await alexSDK.broadcastSponsoredTx(txRaw);
+ logger.info('transaction:', txId);
+ await delay(1000);
+ setIsIdle();
+ navigate(RouteUrls.Activity);
+ } catch (e) {
+ setIsIdle();
+ navigate(RouteUrls.SwapError, {
+ state: {
+ message: e instanceof (Error || SponsoredTxError) ? e.message : 'Unknown error',
+ title: 'Failed to broadcast',
+ },
+ });
+ }
+ },
+ [alexSDK, navigate, setIsIdle]
+ );
+}
diff --git a/src/app/pages/swap/hooks/use-alex-sdk-fiat-price.tsx b/src/app/pages/swap/hooks/use-alex-sdk-fiat-price.tsx
new file mode 100644
index 00000000000..8869d579507
--- /dev/null
+++ b/src/app/pages/swap/hooks/use-alex-sdk-fiat-price.tsx
@@ -0,0 +1,36 @@
+import { Money, createMoney } from '@shared/models/money.model';
+import { isUndefined } from '@shared/utils';
+
+import { useConvertAlexSdkCurrencyToFiatAmount } from '@app/common/hooks/use-convert-to-fiat-amount';
+import { i18nFormatCurrency } from '@app/common/money/format-money';
+import { unitToFractionalUnit } from '@app/common/money/unit-conversion';
+
+export function useAlexSdkAmountAsFiat(balance?: Money, price?: Money, value?: string) {
+ const convertAlexSdkCurrencyToUsd = useConvertAlexSdkCurrencyToFiatAmount(
+ balance?.symbol ?? '',
+ price ?? createMoney(0, 'USD')
+ );
+
+ if (isUndefined(balance) || isUndefined(price) || isUndefined(value)) return '';
+
+ const convertedAmountAsMoney = convertAlexSdkCurrencyToUsd(
+ createMoney(unitToFractionalUnit(balance.decimals)(value), balance.symbol, balance.decimals)
+ );
+
+ return convertedAmountAsMoney.amount.isNaN() ? '' : i18nFormatCurrency(convertedAmountAsMoney);
+}
+
+export function useAlexSdkBalanceAsFiat(balance?: Money, price?: Money) {
+ const convertAlexSdkCurrencyToUsd = useConvertAlexSdkCurrencyToFiatAmount(
+ balance?.symbol ?? '',
+ price ?? createMoney(0, 'USD')
+ );
+
+ if (isUndefined(balance) || isUndefined(price)) return '';
+
+ const convertedBalanceAsMoney = convertAlexSdkCurrencyToUsd(
+ createMoney(balance.amount, balance.symbol, balance.decimals)
+ );
+
+ return convertedBalanceAsMoney.amount.isNaN() ? '' : i18nFormatCurrency(convertedBalanceAsMoney);
+}
diff --git a/src/app/pages/swap/hooks/use-alex-swap.tsx b/src/app/pages/swap/hooks/use-alex-swap.tsx
new file mode 100644
index 00000000000..eee4fdb2a2a
--- /dev/null
+++ b/src/app/pages/swap/hooks/use-alex-swap.tsx
@@ -0,0 +1,100 @@
+import { useCallback, useState } from 'react';
+import { useAsync } from 'react-async-hook';
+
+import { AlexSDK, Currency, TokenInfo } from 'alex-sdk';
+import BigNumber from 'bignumber.js';
+
+import { logger } from '@shared/logger';
+import { createMoney } from '@shared/models/money.model';
+
+import { useStxBalance } from '@app/common/hooks/balance/stx/use-stx-balance';
+import { convertAmountToFractionalUnit } from '@app/common/money/calculate-money';
+import { useSwappableCurrencyQuery } from '@app/query/common/alex-swaps/swappable-currency.query';
+import { useTransferableStacksFungibleTokenAssetBalances } from '@app/query/stacks/balance/stacks-ft-balances.hooks';
+import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks';
+
+import { SwapSubmissionData } from '../swap.context';
+import { SwapAsset } from './use-swap-form';
+
+export const oneHundredMillion = 100_000_000;
+
+export function useAlexSwap() {
+ const alexSDK = useState(() => new AlexSDK())[0];
+ const [swapSubmissionData, setSwapSubmissionData] = useState();
+ const [slippage, _setSlippage] = useState(0.04);
+ const [isFetchingExchangeRate, setIsFetchingExchangeRate] = useState(false);
+ const { data: supportedCurrencies = [] } = useSwappableCurrencyQuery(alexSDK);
+ const { result: prices } = useAsync(async () => await alexSDK.getLatestPrices(), [alexSDK]);
+ const { availableBalance: availableStxBalance } = useStxBalance();
+ const account = useCurrentStacksAccount();
+ const stacksFtAssetBalances = useTransferableStacksFungibleTokenAssetBalances(
+ account?.address ?? ''
+ );
+
+ const createSwapAssetFromAlexCurrency = useCallback(
+ (tokenInfo?: TokenInfo) => {
+ if (!prices) return;
+ if (!tokenInfo) {
+ logger.error('No token data found to swap');
+ return;
+ }
+
+ const currency = tokenInfo.id as Currency;
+ const price = convertAmountToFractionalUnit(new BigNumber(prices[currency] ?? 0), 2);
+ const swapAsset = {
+ currency,
+ icon: tokenInfo.icon,
+ name: tokenInfo.name,
+ price: createMoney(price, 'USD'),
+ };
+
+ if (currency === Currency.STX) {
+ return {
+ ...swapAsset,
+ balance: availableStxBalance,
+ };
+ }
+
+ const fungibleTokenBalance =
+ stacksFtAssetBalances.find(x => alexSDK.getAddressFrom(currency) === x.asset.contractId)
+ ?.balance ?? createMoney(0, tokenInfo.name, tokenInfo.decimals);
+
+ return {
+ ...swapAsset,
+ balance: fungibleTokenBalance,
+ };
+ },
+ [alexSDK, availableStxBalance, prices, stacksFtAssetBalances]
+ );
+
+ async function fetchToAmount(
+ from: SwapAsset,
+ to: SwapAsset,
+ fromAmount: string
+ ): Promise {
+ const amount = new BigNumber(fromAmount).multipliedBy(oneHundredMillion).dp(0).toString();
+ const amountAsBigInt = isNaN(Number(amount)) ? BigInt(0) : BigInt(amount);
+ try {
+ setIsFetchingExchangeRate(true);
+ const result = await alexSDK.getAmountTo(from.currency, amountAsBigInt, to.currency);
+ setIsFetchingExchangeRate(false);
+ return new BigNumber(Number(result)).dividedBy(oneHundredMillion).toString();
+ } catch (e) {
+ logger.error('Error fetching exchange rate from ALEX', e);
+ setIsFetchingExchangeRate(false);
+ return;
+ }
+ }
+
+ return {
+ alexSDK,
+ fetchToAmount,
+ createSwapAssetFromAlexCurrency,
+ isFetchingExchangeRate,
+ onSetIsFetchingExchangeRate: (value: boolean) => setIsFetchingExchangeRate(value),
+ onSetSwapSubmissionData: (value: SwapSubmissionData) => setSwapSubmissionData(value),
+ slippage,
+ supportedCurrencies,
+ swapSubmissionData,
+ };
+}
diff --git a/src/app/pages/swap/hooks/use-amount-as-fiat.tsx b/src/app/pages/swap/hooks/use-amount-as-fiat.tsx
deleted file mode 100644
index efd8614a748..00000000000
--- a/src/app/pages/swap/hooks/use-amount-as-fiat.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { Money, createMoney } from '@shared/models/money.model';
-import { isUndefined } from '@shared/utils';
-
-import { useConvertCryptoCurrencyToFiatAmount } from '@app/common/hooks/use-convert-to-fiat-amount';
-import { i18nFormatCurrency } from '@app/common/money/format-money';
-import { unitToFractionalUnit } from '@app/common/money/unit-conversion';
-
-export function useAmountAsFiat(value: string, balance?: Money) {
- const convertCryptoCurrencyToUsd = useConvertCryptoCurrencyToFiatAmount(balance?.symbol ?? '');
-
- if (isUndefined(balance)) return '';
-
- const convertedAmountAsMoney = convertCryptoCurrencyToUsd(
- createMoney(unitToFractionalUnit(balance.decimals)(value), balance.symbol, balance.decimals)
- );
- // TODO: Remove this when using live data bc amounts won't be null?
- return convertedAmountAsMoney.amount.isNaN() ? '' : i18nFormatCurrency(convertedAmountAsMoney);
-}
diff --git a/src/app/pages/swap/hooks/use-stacks-broadcast-swap.tsx b/src/app/pages/swap/hooks/use-stacks-broadcast-swap.tsx
new file mode 100644
index 00000000000..551eb4598b3
--- /dev/null
+++ b/src/app/pages/swap/hooks/use-stacks-broadcast-swap.tsx
@@ -0,0 +1,53 @@
+import { useCallback } from 'react';
+import toast from 'react-hot-toast';
+import { useNavigate } from 'react-router-dom';
+
+import { StacksTransaction } from '@stacks/transactions';
+
+import { logger } from '@shared/logger';
+import { RouteUrls } from '@shared/route-urls';
+import { isString } from '@shared/utils';
+
+import { LoadingKeys, useLoading } from '@app/common/hooks/use-loading';
+import { useSubmitTransactionCallback } from '@app/common/hooks/use-submit-stx-transaction';
+
+export function useStacksBroadcastSwap() {
+ const { setIsIdle } = useLoading(LoadingKeys.SUBMIT_SWAP_TRANSACTION);
+ const navigate = useNavigate();
+
+ const broadcastTransactionFn = useSubmitTransactionCallback({
+ loadingKey: LoadingKeys.SUBMIT_SWAP_TRANSACTION,
+ });
+
+ return useCallback(
+ async (signedTx: StacksTransaction) => {
+ if (!signedTx) {
+ logger.error('Cannot broadcast transaction, no tx in state');
+ toast.error('Unable to broadcast transaction');
+ return;
+ }
+ try {
+ await broadcastTransactionFn({
+ onError(e: Error | string) {
+ setIsIdle();
+ const message = isString(e) ? e : e.message;
+ navigate(RouteUrls.TransactionBroadcastError, { state: { message } });
+ },
+ onSuccess() {
+ setIsIdle();
+ navigate(RouteUrls.Activity);
+ },
+ replaceByFee: false,
+ })(signedTx);
+ } catch (e) {
+ setIsIdle();
+ navigate(RouteUrls.TransactionBroadcastError, {
+ state: { message: e instanceof Error ? e.message : 'Unknown error' },
+ });
+ } finally {
+ setIsIdle();
+ }
+ },
+ [broadcastTransactionFn, setIsIdle, navigate]
+ );
+}
diff --git a/src/app/pages/swap/hooks/use-swap-form.tsx b/src/app/pages/swap/hooks/use-swap-form.tsx
new file mode 100644
index 00000000000..b506650391b
--- /dev/null
+++ b/src/app/pages/swap/hooks/use-swap-form.tsx
@@ -0,0 +1,76 @@
+import { Currency } from 'alex-sdk';
+import BigNumber from 'bignumber.js';
+import * as yup from 'yup';
+
+import { FeeTypes } from '@shared/models/fees/fees.model';
+import { StacksTransactionFormValues } from '@shared/models/form.model';
+import { Money, createMoney } from '@shared/models/money.model';
+
+import { FormErrorMessages } from '@app/common/error-messages';
+import { convertAmountToFractionalUnit } from '@app/common/money/calculate-money';
+import { useNextNonce } from '@app/query/stacks/nonce/account-nonces.hooks';
+
+export interface SwapAsset {
+ balance: Money;
+ currency: Currency;
+ icon: string;
+ name: string;
+ price: Money;
+}
+
+export interface SwapFormValues extends StacksTransactionFormValues {
+ swapAmountFrom: string;
+ swapAmountTo: string;
+ swapAssetFrom?: SwapAsset;
+ swapAssetTo?: SwapAsset;
+}
+
+export function useSwapForm() {
+ const { data: nextNonce } = useNextNonce();
+
+ const initialValues: SwapFormValues = {
+ fee: '0',
+ feeCurrency: 'STX',
+ feeType: FeeTypes[FeeTypes.Middle],
+ nonce: nextNonce?.nonce,
+ swapAmountFrom: '',
+ swapAmountTo: '',
+ swapAssetFrom: undefined,
+ swapAssetTo: undefined,
+ };
+
+ const validationSchema = yup.object({
+ swapAssetFrom: yup.object().required(),
+ swapAssetTo: yup.object().required(),
+ swapAmountFrom: yup
+ .number()
+ .test({
+ message: 'Insufficient balance',
+ test(value) {
+ const { swapAssetFrom } = this.parent;
+ const valueInFractionalUnit = convertAmountToFractionalUnit(
+ createMoney(
+ new BigNumber(Number(value)),
+ swapAssetFrom.balance.symbol,
+ swapAssetFrom.balance.decimals
+ )
+ );
+ if (swapAssetFrom.balance.amount.isLessThan(valueInFractionalUnit)) return false;
+ return true;
+ },
+ })
+ .required(FormErrorMessages.AmountRequired)
+ .typeError(FormErrorMessages.MustBeNumber)
+ .positive(FormErrorMessages.MustBePositive),
+ swapAmountTo: yup
+ .number()
+ .required(FormErrorMessages.AmountRequired)
+ .typeError(FormErrorMessages.MustBeNumber)
+ .positive(FormErrorMessages.MustBePositive),
+ });
+
+ return {
+ initialValues,
+ validationSchema,
+ };
+}
diff --git a/src/app/pages/swap/hooks/use-swap.tsx b/src/app/pages/swap/hooks/use-swap.tsx
deleted file mode 100644
index 26cb0e3d84d..00000000000
--- a/src/app/pages/swap/hooks/use-swap.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import * as yup from 'yup';
-
-import { Money } from '@shared/models/money.model';
-
-import { FormErrorMessages } from '@app/common/error-messages';
-// import { tokenAmountValidator } from '@app/common/validation/forms/amount-validators';
-import { currencyAmountValidator } from '@app/common/validation/forms/currency-validators';
-
-export interface SwapAsset {
- balance: Money;
- icon: string;
- name: string;
-}
-
-export interface SwapFormValues {
- swapAmountFrom: string;
- swapAmountTo: string;
- swapAssetFrom?: SwapAsset;
- swapAssetTo?: SwapAsset;
-}
-
-export function useSwap() {
- const initialValues: SwapFormValues = {
- swapAmountFrom: '',
- swapAmountTo: '',
- swapAssetFrom: undefined,
- swapAssetTo: undefined,
- };
-
- // TODO: Need to add insufficient balance validation
- // Validate directly on Field once asset is selected?
- const validationSchema = yup.object({
- swapAmountFrom: yup
- .number()
- .required(FormErrorMessages.AmountRequired)
- .concat(currencyAmountValidator()),
- // .concat(tokenAmountValidator(balance)),
- swapAmountTo: yup
- .number()
- .required(FormErrorMessages.AmountRequired)
- .concat(currencyAmountValidator()),
- // .concat(tokenAmountValidator(balance)),
- swapAssetFrom: yup.object().required(),
- swapAssetTo: yup.object().required(),
- });
-
- return {
- initialValues,
- validationSchema,
- };
-}
diff --git a/src/app/pages/swap/swap-choose-asset/components/swap-asset-item.layout.tsx b/src/app/pages/swap/swap-choose-asset/components/swap-asset-item.layout.tsx
index f858e2b8e12..6f85aeafa2c 100644
--- a/src/app/pages/swap/swap-choose-asset/components/swap-asset-item.layout.tsx
+++ b/src/app/pages/swap/swap-choose-asset/components/swap-asset-item.layout.tsx
@@ -3,9 +3,13 @@ import { Stack } from 'leather-styles/jsx';
import { HasChildren } from '@app/common/has-children';
import { Flag } from '@app/components/layout/flag';
-export function SwapAssetItemLayout({ children, icon }: HasChildren & { icon: React.JSX.Element }) {
+export function SwapAssetItemLayout({
+ children,
+ icon,
+ ...rest
+}: HasChildren & { icon: React.JSX.Element }) {
return (
-
+
{children}
);
diff --git a/src/app/pages/swap/swap-choose-asset/components/swap-asset-item.tsx b/src/app/pages/swap/swap-choose-asset/components/swap-asset-item.tsx
index c0af94974f8..42e4d9baea0 100644
--- a/src/app/pages/swap/swap-choose-asset/components/swap-asset-item.tsx
+++ b/src/app/pages/swap/swap-choose-asset/components/swap-asset-item.tsx
@@ -1,23 +1,35 @@
import { HStack, styled } from 'leather-styles/jsx';
import { formatMoneyWithoutSymbol } from '@app/common/money/format-money';
+import { usePressable } from '@app/components/item-hover';
-import { SwapAsset } from '../../hooks/use-swap';
+import { useAlexSdkBalanceAsFiat } from '../../hooks/use-alex-sdk-fiat-price';
+import { SwapAsset } from '../../hooks/use-swap-form';
import { SwapAssetItemLayout } from './swap-asset-item.layout';
interface SwapAssetItemProps {
asset: SwapAsset;
}
export function SwapAssetItem({ asset }: SwapAssetItemProps) {
+ const [component, bind] = usePressable(true);
+ const balanceAsFiat = useAlexSdkBalanceAsFiat(asset.balance, asset.price);
+
return (
}
+ {...bind}
>
{asset.name}
{formatMoneyWithoutSymbol(asset.balance)}
- {asset.balance.symbol}
+
+ {asset.name}
+
+ {balanceAsFiat}
+
+
+ {component}
);
}
diff --git a/src/app/pages/swap/swap-choose-asset/components/swap-asset-list.layout.tsx b/src/app/pages/swap/swap-choose-asset/components/swap-asset-list.layout.tsx
index 55c3712a1f4..e7feee61700 100644
--- a/src/app/pages/swap/swap-choose-asset/components/swap-asset-list.layout.tsx
+++ b/src/app/pages/swap/swap-choose-asset/components/swap-asset-list.layout.tsx
@@ -2,7 +2,7 @@ import { Stack, StackProps } from 'leather-styles/jsx';
export function SwapAssetListLayout({ children }: StackProps) {
return (
-
+
{children}
);
diff --git a/src/app/pages/swap/swap-choose-asset/components/swap-asset-list.tsx b/src/app/pages/swap/swap-choose-asset/components/swap-asset-list.tsx
index 286ec4d5385..eee860036c8 100644
--- a/src/app/pages/swap/swap-choose-asset/components/swap-asset-list.tsx
+++ b/src/app/pages/swap/swap-choose-asset/components/swap-asset-list.tsx
@@ -1,10 +1,18 @@
-import { useLocation, useNavigate } from 'react-router-dom';
+import { useNavigate } from 'react-router-dom';
+import BigNumber from 'bignumber.js';
import { useFormikContext } from 'formik';
import { styled } from 'leather-styles/jsx';
-import get from 'lodash.get';
-import { SwapAsset, SwapFormValues } from '../../hooks/use-swap';
+import { createMoney } from '@shared/models/money.model';
+import { isUndefined } from '@shared/utils';
+
+import { convertAmountToFractionalUnit } from '@app/common/money/calculate-money';
+import { formatMoneyWithoutSymbol } from '@app/common/money/format-money';
+import { useSwapContext } from '@app/pages/swap/swap.context';
+
+import { SwapAsset, SwapFormValues } from '../../hooks/use-swap-form';
+import { useSwapChooseAssetState } from '../swap-choose-asset';
import { SwapAssetItem } from './swap-asset-item';
import { SwapAssetListLayout } from './swap-asset-list.layout';
@@ -12,23 +20,58 @@ interface SwapAssetList {
assets: SwapAsset[];
}
export function SwapAssetList({ assets }: SwapAssetList) {
- const { setFieldValue } = useFormikContext();
- const location = useLocation();
+ const { fetchToAmount } = useSwapContext();
+ const { swapListType } = useSwapChooseAssetState();
+ const { setFieldError, setFieldValue, values } = useFormikContext();
const navigate = useNavigate();
+ const isFromList = swapListType === 'from';
+ const isToList = swapListType === 'to';
+
+ const selectableAssets = assets.filter(
+ asset =>
+ (isFromList && asset.name !== values.swapAssetTo?.name) ||
+ (isToList && asset.name !== values.swapAssetFrom?.name)
+ );
+
async function onChooseAsset(asset: SwapAsset) {
- if (get(location.state, 'swap') === 'from') await setFieldValue('swapAssetFrom', asset);
- if (get(location.state, 'swap') === 'to') await setFieldValue('swapAssetTo', asset);
+ let from: SwapAsset | undefined;
+ let to: SwapAsset | undefined;
+ if (isFromList) {
+ from = asset;
+ to = values.swapAssetTo;
+ await setFieldValue('swapAssetFrom', asset);
+ } else if (isToList) {
+ from = values.swapAssetFrom;
+ to = asset;
+ await setFieldValue('swapAssetTo', asset);
+ setFieldError('swapAssetTo', undefined);
+ }
navigate(-1);
+ if (from && to && values.swapAmountFrom) {
+ const toAmount = await fetchToAmount(from, to, values.swapAmountFrom);
+ if (isUndefined(toAmount)) {
+ await setFieldValue('swapAmountTo', '');
+ return;
+ }
+ const toAmountAsMoney = createMoney(
+ convertAmountToFractionalUnit(new BigNumber(toAmount), to?.balance.decimals),
+ to?.balance.symbol ?? '',
+ to?.balance.decimals
+ );
+ await setFieldValue('swapAmountTo', formatMoneyWithoutSymbol(toAmountAsMoney));
+ setFieldError('swapAmountTo', undefined);
+ }
}
return (
- {assets.map(asset => (
+ {selectableAssets.map(asset => (
onChooseAsset(asset)}
textAlign="left"
+ type="button"
>
diff --git a/src/app/pages/swap/swap-choose-asset/swap-choose-asset.tsx b/src/app/pages/swap/swap-choose-asset/swap-choose-asset.tsx
index 52c79073b3e..b997b5c4df5 100644
--- a/src/app/pages/swap/swap-choose-asset/swap-choose-asset.tsx
+++ b/src/app/pages/swap/swap-choose-asset/swap-choose-asset.tsx
@@ -1,17 +1,48 @@
-import { useNavigate } from 'react-router-dom';
+import { useLocation, useNavigate } from 'react-router-dom';
+
+import { Box, styled } from 'leather-styles/jsx';
+import get from 'lodash.get';
import { BaseDrawer } from '@app/components/drawer/base-drawer';
import { useSwapContext } from '../swap.context';
import { SwapAssetList } from './components/swap-asset-list';
+export function useSwapChooseAssetState() {
+ const location = useLocation();
+ const swapListType = get(location.state, 'swap') as string;
+ return { swapListType };
+}
+
export function SwapChooseAsset() {
- const { swappableAssets } = useSwapContext();
+ const { swappableAssetsFrom, swappableAssetsTo } = useSwapContext();
+ const { swapListType } = useSwapChooseAssetState();
const navigate = useNavigate();
+ const isFromList = swapListType === 'from';
+
+ const title = isFromList ? (
+ <>
+ Choose asset
+
+ to swap
+ >
+ ) : (
+ <>
+ Choose asset
+
+ to receive
+ >
+ );
+
return (
- navigate(-1)}>
-
+ navigate(-1)}>
+
+
+ {title}
+
+
+
);
}
diff --git a/src/app/pages/swap/swap-container.tsx b/src/app/pages/swap/swap-container.tsx
index 32f07ec3e89..d3596acef43 100644
--- a/src/app/pages/swap/swap-container.tsx
+++ b/src/app/pages/swap/swap-container.tsx
@@ -1,71 +1,196 @@
-import { useState } from 'react';
+import { useMemo, useState } from 'react';
import { Outlet, useNavigate } from 'react-router-dom';
-import BtcIcon from '@assets/images/btc-icon.png';
-import XBtcIcon from '@assets/images/xbtc-icon.png';
+import { bytesToHex } from '@stacks/common';
+import { ContractCallPayload, TransactionTypes } from '@stacks/connect';
+import {
+ AnchorMode,
+ PostConditionMode,
+ serializeCV,
+ serializePostCondition,
+} from '@stacks/transactions';
import BigNumber from 'bignumber.js';
-import { createMoney } from '@shared/models/money.model';
+import { logger } from '@shared/logger';
import { RouteUrls } from '@shared/route-urls';
+import { isDefined, isUndefined } from '@shared/utils';
-import { useNativeSegwitBalance } from '@app/query/bitcoin/balance/btc-native-segwit-balance.hooks';
-import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
+import { LoadingKeys, useLoading } from '@app/common/hooks/use-loading';
+import { NonceSetter } from '@app/components/nonce-setter';
+import { defaultFeesMinValues } from '@app/query/stacks/fees/fees.hooks';
+import { useStacksPendingTransactions } from '@app/query/stacks/mempool/mempool.hooks';
+import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks';
+import { useGenerateStacksContractCallUnsignedTx } from '@app/store/transactions/contract-call.hooks';
+import { useSignTransactionSoftwareWallet } from '@app/store/transactions/transaction.hooks';
import { SwapContainerLayout } from './components/swap-container.layout';
import { SwapForm } from './components/swap-form';
-import { SwapAsset, SwapFormValues } from './hooks/use-swap';
+import { useAlexBroadcastSwap } from './hooks/use-alex-broadcast-swap';
+import { oneHundredMillion, useAlexSwap } from './hooks/use-alex-swap';
+import { useStacksBroadcastSwap } from './hooks/use-stacks-broadcast-swap';
+import { SwapAsset, SwapFormValues } from './hooks/use-swap-form';
import { SwapContext, SwapProvider } from './swap.context';
-
-// TODO: Remove and set to initial state to 0 with live data
-const tempExchangeRate = 0.5;
+import { migratePositiveBalancesToTop, sortSwappableAssetsBySymbol } from './swap.utils';
export function SwapContainer() {
- const [exchangeRate, setExchangeRate] = useState(tempExchangeRate);
const [isSendingMax, setIsSendingMax] = useState(false);
const navigate = useNavigate();
- const { address } = useCurrentAccountNativeSegwitIndexZeroSigner();
- const { balance: btcBalance } = useNativeSegwitBalance(address);
- // TODO: Filter these assets for list to swap, not sure if need?
- // const allTransferableCryptoAssetBalances = useAllTransferableCryptoAssetBalances();
-
- // TODO: Replace with live asset list
- const tempSwapAssetFrom: SwapAsset = {
- balance: btcBalance,
- icon: BtcIcon,
- name: 'Bitcoin',
- };
+ const { setIsLoading } = useLoading(LoadingKeys.SUBMIT_SWAP_TRANSACTION);
+ const currentAccount = useCurrentStacksAccount();
+ const generateUnsignedTx = useGenerateStacksContractCallUnsignedTx();
+ const signSoftwareWalletTx = useSignTransactionSoftwareWallet();
+ const { transactions: pendingTransactions } = useStacksPendingTransactions();
- const tempSwapAssetTo: SwapAsset = {
- balance: createMoney(new BigNumber(0), 'xBTC', 0),
- icon: XBtcIcon,
- name: 'Wrapped Bitcoin',
- };
+ const isSponsoredByAlex = !pendingTransactions.length;
+
+ const {
+ alexSDK,
+ fetchToAmount,
+ createSwapAssetFromAlexCurrency,
+ isFetchingExchangeRate,
+ onSetIsFetchingExchangeRate,
+ onSetSwapSubmissionData,
+ slippage,
+ supportedCurrencies,
+ swapSubmissionData,
+ } = useAlexSwap();
+
+ const broadcastAlexSwap = useAlexBroadcastSwap(alexSDK);
+ const broadcastStacksSwap = useStacksBroadcastSwap();
- function onSubmitSwapForReview(values: SwapFormValues) {
- navigate(RouteUrls.SwapReview, {
- state: { ...values },
+ const swappableAssets: SwapAsset[] = useMemo(
+ () =>
+ sortSwappableAssetsBySymbol(
+ supportedCurrencies.map(createSwapAssetFromAlexCurrency).filter(isDefined)
+ ),
+ [createSwapAssetFromAlexCurrency, supportedCurrencies]
+ );
+
+ async function onSubmitSwapForReview(values: SwapFormValues) {
+ if (isUndefined(values.swapAssetFrom) || isUndefined(values.swapAssetTo)) {
+ logger.error('Error submitting swap for review');
+ return;
+ }
+
+ const [router, lpFee] = await Promise.all([
+ alexSDK.getRouter(values.swapAssetFrom.currency, values.swapAssetTo.currency),
+ alexSDK.getFeeRate(values.swapAssetFrom.currency, values.swapAssetTo.currency),
+ ]);
+
+ onSetSwapSubmissionData({
+ fee: isSponsoredByAlex ? '0' : defaultFeesMinValues[1].amount.toString(),
+ feeCurrency: values.feeCurrency,
+ feeType: values.feeType,
+ liquidityFee: new BigNumber(Number(lpFee)).dividedBy(oneHundredMillion).toNumber(),
+ nonce: values.nonce,
+ protocol: 'ALEX',
+ router: router
+ .map(x => createSwapAssetFromAlexCurrency(supportedCurrencies.find(y => y.id === x)))
+ .filter(isDefined),
+ slippage,
+ sponsored: isSponsoredByAlex,
+ swapAmountFrom: values.swapAmountFrom,
+ swapAmountTo: values.swapAmountTo,
+ swapAssetFrom: values.swapAssetFrom,
+ swapAssetTo: values.swapAssetTo,
+ timestamp: new Date().toISOString(),
});
+
+ navigate(RouteUrls.SwapReview);
}
- // TODO: Generate/broadcast transaction > pass real tx data
- function onSubmitSwap() {
- navigate(RouteUrls.SwapSummary);
+ async function onSubmitSwap() {
+ if (isUndefined(currentAccount) || isUndefined(swapSubmissionData)) {
+ logger.error('Error submitting swap data to sign');
+ return;
+ }
+
+ if (
+ isUndefined(swapSubmissionData.swapAssetFrom) ||
+ isUndefined(swapSubmissionData.swapAssetTo)
+ ) {
+ logger.error('No assets selected to perform swap');
+ return;
+ }
+
+ setIsLoading();
+
+ const fromAmount = BigInt(
+ new BigNumber(swapSubmissionData.swapAmountFrom)
+ .multipliedBy(oneHundredMillion)
+ .dp(0)
+ .toString()
+ );
+
+ const minToAmount = BigInt(
+ new BigNumber(swapSubmissionData.swapAmountTo)
+ .multipliedBy(oneHundredMillion)
+ .multipliedBy(1 - slippage)
+ .dp(0)
+ .toString()
+ );
+
+ const tx = alexSDK.runSwap(
+ currentAccount?.address,
+ swapSubmissionData.swapAssetFrom.currency,
+ swapSubmissionData.swapAssetTo.currency,
+ fromAmount,
+ minToAmount,
+ swapSubmissionData.router.map(x => x.currency)
+ );
+
+ // TODO: Add choose fee step
+ const tempFormValues = {
+ fee: swapSubmissionData.fee,
+ feeCurrency: swapSubmissionData.feeCurrency,
+ feeType: swapSubmissionData.feeType,
+ nonce: swapSubmissionData.nonce,
+ };
+
+ const payload: ContractCallPayload = {
+ anchorMode: AnchorMode.Any,
+ contractAddress: tx.contractAddress,
+ contractName: tx.contractName,
+ functionName: tx.functionName,
+ functionArgs: tx.functionArgs.map(x => bytesToHex(serializeCV(x))),
+ postConditionMode: PostConditionMode.Deny,
+ postConditions: tx.postConditions.map(pc => bytesToHex(serializePostCondition(pc))),
+ publicKey: currentAccount?.stxPublicKey,
+ sponsored: swapSubmissionData.sponsored,
+ txType: TransactionTypes.ContractCall,
+ };
+
+ const unsignedTx = await generateUnsignedTx(payload, tempFormValues);
+ if (!unsignedTx) return logger.error('Attempted to generate unsigned tx, but tx is undefined');
+
+ const signedTx = signSoftwareWalletTx(unsignedTx);
+ if (!signedTx) return logger.error('Attempted to generate raw tx, but signed tx is undefined');
+ const txRaw = bytesToHex(signedTx.serialize());
+
+ if (isSponsoredByAlex) {
+ return await broadcastAlexSwap(txRaw);
+ }
+ return await broadcastStacksSwap(unsignedTx);
}
const swapContextValue: SwapContext = {
- exchangeRate,
+ fetchToAmount,
+ isFetchingExchangeRate,
isSendingMax,
- onSetExchangeRate: value => setExchangeRate(value),
+ onSetIsFetchingExchangeRate,
onSetIsSendingMax: value => setIsSendingMax(value),
onSubmitSwapForReview,
onSubmitSwap,
- swappableAssets: [tempSwapAssetFrom, tempSwapAssetTo],
+ swappableAssetsFrom: migratePositiveBalancesToTop(swappableAssets),
+ swappableAssetsTo: swappableAssets,
+ swapSubmissionData,
};
return (
+
diff --git a/src/app/pages/swap/swap-error/swap-error.tsx b/src/app/pages/swap/swap-error/swap-error.tsx
new file mode 100644
index 00000000000..bc38890803c
--- /dev/null
+++ b/src/app/pages/swap/swap-error/swap-error.tsx
@@ -0,0 +1,31 @@
+import { useLocation, useNavigate } from 'react-router-dom';
+
+import { styled } from 'leather-styles/jsx';
+import get from 'lodash.get';
+
+import { RouteUrls } from '@shared/route-urls';
+
+import { GenericError } from '@app/components/generic-error/generic-error';
+
+const helpTextList = [
+
+ Please report issue to swap protocol
+ ,
+];
+
+export function SwapError() {
+ const location = useLocation();
+ const navigate = useNavigate();
+ const message = get(location.state, 'message') as string;
+ const title = get(location.state, 'title') as string;
+
+ return (
+ navigate(RouteUrls.Home)}
+ title={title}
+ />
+ );
+}
diff --git a/src/app/pages/swap/swap-review/swap-review.tsx b/src/app/pages/swap/swap-review/swap-review.tsx
index ddd0d61d493..4c756543d68 100644
--- a/src/app/pages/swap/swap-review/swap-review.tsx
+++ b/src/app/pages/swap/swap-review/swap-review.tsx
@@ -1,3 +1,4 @@
+import { LoadingKeys, useLoading } from '@app/common/hooks/use-loading';
import { useRouteHeader } from '@app/common/hooks/use-route-header';
import { LeatherButton } from '@app/components/button/button';
import { ModalHeader } from '@app/components/modal-header';
@@ -11,6 +12,7 @@ import { SwapReviewLayout } from './swap-review.layout';
export function SwapReview() {
const { onSubmitSwap } = useSwapContext();
+ const { isLoading } = useLoading(LoadingKeys.SUBMIT_SWAP_TRANSACTION);
useRouteHeader(, true);
@@ -21,7 +23,7 @@ export function SwapReview() {
-
+
Swap
diff --git a/src/app/pages/swap/swap-summary/swap-summary-action-button.tsx b/src/app/pages/swap/swap-summary/swap-summary-action-button.tsx
deleted file mode 100644
index 684e3d5c9fd..00000000000
--- a/src/app/pages/swap/swap-summary/swap-summary-action-button.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { Box, Flex, styled } from 'leather-styles/jsx';
-
-import { LeatherButton } from '@app/components/button/button';
-
-interface SwapSummaryActionButtonProps {
- icon: React.JSX.Element;
- label: string;
- onClick: () => void;
-}
-export function SwapSummaryActionButton({ icon, label, onClick }: SwapSummaryActionButtonProps) {
- return (
-
-
-
- {label}
-
- {icon}
-
-
- );
-}
diff --git a/src/app/pages/swap/swap-summary/swap-summary-tabs.tsx b/src/app/pages/swap/swap-summary/swap-summary-tabs.tsx
deleted file mode 100644
index ef652acaf71..00000000000
--- a/src/app/pages/swap/swap-summary/swap-summary-tabs.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import { Suspense, useCallback, useMemo } from 'react';
-import { useLocation, useNavigate } from 'react-router-dom';
-
-import { Box, Stack } from 'leather-styles/jsx';
-
-import { RouteUrls } from '@shared/route-urls';
-
-import { HasChildren } from '@app/common/has-children';
-import { LoadingSpinner } from '@app/components/loading-spinner';
-import { Tabs } from '@app/components/tabs';
-
-export function SwapSummaryTabs({ children }: HasChildren) {
- const navigate = useNavigate();
- const { pathname } = useLocation();
-
- const tabs = useMemo(
- () => [
- { slug: RouteUrls.SwapSummary, label: 'Status' },
- { slug: RouteUrls.SwapSummaryDetails, label: 'Swap details' },
- ],
- []
- );
-
- const getActiveTab = useCallback(
- () => tabs.findIndex(tab => tab.slug === pathname),
- [tabs, pathname]
- );
-
- const setActiveTab = useCallback(
- (index: number) => navigate(tabs[index]?.slug),
- [navigate, tabs]
- );
-
- return (
-
-
- }>
- {children}
-
-
- );
-}
diff --git a/src/app/pages/swap/swap-summary/swap-summary.layout.tsx b/src/app/pages/swap/swap-summary/swap-summary.layout.tsx
deleted file mode 100644
index fcadd615109..00000000000
--- a/src/app/pages/swap/swap-summary/swap-summary.layout.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import { Flex } from 'leather-styles/jsx';
-
-import { HasChildren } from '@app/common/has-children';
-
-export function SwapSummaryLayout({ children }: HasChildren) {
- return (
-
- {children}
-
- );
-}
diff --git a/src/app/pages/swap/swap-summary/swap-summary.tsx b/src/app/pages/swap/swap-summary/swap-summary.tsx
deleted file mode 100644
index acc48533c0e..00000000000
--- a/src/app/pages/swap/swap-summary/swap-summary.tsx
+++ /dev/null
@@ -1,88 +0,0 @@
-import toast from 'react-hot-toast';
-import { Outlet } from 'react-router-dom';
-
-import WaxSeal from '@assets/illustrations/wax-seal.png';
-import { useFormikContext } from 'formik';
-import { HStack, styled } from 'leather-styles/jsx';
-
-import { logger } from '@shared/logger';
-import { isUndefined } from '@shared/utils';
-
-import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
-import { useClipboard } from '@app/common/hooks/use-copy-to-clipboard';
-// import { useExplorerLink } from '@app/common/hooks/use-explorer-link';
-import { useRouteHeader } from '@app/common/hooks/use-route-header';
-import { CopyIcon } from '@app/components/icons/copy-icon';
-import { ExternalLinkIcon } from '@app/components/icons/external-link-icon';
-import { ModalHeader } from '@app/components/modal-header';
-
-import { SwapAssetsPair } from '../components/swap-assets-pair/swap-assets-pair';
-import { SwapContentLayout } from '../components/swap-content.layout';
-import { SwapFooterLayout } from '../components/swap-footer.layout';
-import { useAmountAsFiat } from '../hooks/use-amount-as-fiat';
-import { SwapFormValues } from '../hooks/use-swap';
-import { SwapSummaryActionButton } from './swap-summary-action-button';
-import { SwapSummaryTabs } from './swap-summary-tabs';
-import { SwapSummaryLayout } from './swap-summary.layout';
-
-// TODO: Pass/replace state with tx data where needed and handle click events
-// Commented code left here to use with tx data
-export function SwapSummary() {
- const { values } = useFormikContext();
- const analytics = useAnalytics();
- const { onCopy } = useClipboard('');
- // const { handleOpenTxLink } = useExplorerLink();
-
- useRouteHeader(, true);
-
- const amountAsFiat = useAmountAsFiat(values.swapAmountTo, values.swapAssetTo?.balance);
-
- function onClickCopy() {
- onCopy();
- toast.success('ID copied!');
- }
-
- function onClickLink() {
- void analytics.track('view_swap_transaction_confirmation', {
- swapSymbolFrom: values.swapAssetFrom?.balance.symbol,
- swapSymbolTo: values.swapAssetTo?.balance.symbol,
- });
- // handleOpenTxLink(txLink);
- }
-
- if (isUndefined(values.swapAssetTo)) {
- logger.error('No asset selected for swap');
- return null;
- }
-
- return (
-
-
-
-
- All done
-
-
- {values.swapAmountTo} {values.swapAssetTo.balance.symbol}
-
-
- {amountAsFiat ? `~ ${amountAsFiat}` : '~ 0'}
-
-
-
-
-
-
-
-
- }
- label="View details"
- onClick={onClickLink}
- />
- } label="Copy ID" onClick={onClickCopy} />
-
-
-
- );
-}
diff --git a/src/app/pages/swap/swap.context.ts b/src/app/pages/swap/swap.context.ts
index 4b757f6964d..0634881a577 100644
--- a/src/app/pages/swap/swap.context.ts
+++ b/src/app/pages/swap/swap.context.ts
@@ -1,15 +1,27 @@
import { createContext, useContext } from 'react';
-import { SwapAsset, SwapFormValues } from './hooks/use-swap';
+import { SwapAsset, SwapFormValues } from './hooks/use-swap-form';
+
+export interface SwapSubmissionData extends SwapFormValues {
+ liquidityFee: number;
+ protocol: string;
+ router: SwapAsset[];
+ slippage: number;
+ sponsored: boolean;
+ timestamp: string;
+}
export interface SwapContext {
- exchangeRate: number;
+ fetchToAmount(from: SwapAsset, to: SwapAsset, fromAmount: string): Promise;
+ isFetchingExchangeRate: boolean;
isSendingMax: boolean;
- onSetExchangeRate(value: number): void;
+ onSetIsFetchingExchangeRate(value: boolean): void;
onSetIsSendingMax(value: boolean): void;
onSubmitSwapForReview(values: SwapFormValues): Promise | void;
onSubmitSwap(): Promise | void;
- swappableAssets: SwapAsset[];
+ swappableAssetsFrom: SwapAsset[];
+ swappableAssetsTo: SwapAsset[];
+ swapSubmissionData?: SwapSubmissionData;
}
const swapContext = createContext(null);
diff --git a/src/app/pages/swap/swap.routes.tsx b/src/app/pages/swap/swap.routes.tsx
index a2aeeaa54c4..110d488276f 100644
--- a/src/app/pages/swap/swap.routes.tsx
+++ b/src/app/pages/swap/swap.routes.tsx
@@ -4,13 +4,11 @@ import { RouteUrls } from '@shared/route-urls';
import { AccountGate } from '@app/routes/account-gate';
-import { SwapDetails } from './components/swap-details/swap-details';
-import { SwapStatus } from './components/swap-status/swap-status';
+import { Swap } from './swap';
import { SwapChooseAsset } from './swap-choose-asset/swap-choose-asset';
import { SwapContainer } from './swap-container';
+import { SwapError } from './swap-error/swap-error';
import { SwapReview } from './swap-review/swap-review';
-import { SwapSummary } from './swap-summary/swap-summary';
-import { Swap } from './swap/swap';
export const swapRoutes = (
}>
} />
+ } />
} />
- }>
- } />
- } />
-
);
diff --git a/src/app/pages/swap/swap.tsx b/src/app/pages/swap/swap.tsx
new file mode 100644
index 00000000000..1f270291d39
--- /dev/null
+++ b/src/app/pages/swap/swap.tsx
@@ -0,0 +1,46 @@
+import { useAsync } from 'react-async-hook';
+import { Outlet } from 'react-router-dom';
+
+import { useFormikContext } from 'formik';
+
+import { isUndefined } from '@shared/utils';
+
+import { useRouteHeader } from '@app/common/hooks/use-route-header';
+import { LeatherButton } from '@app/components/button/button';
+import { LoadingSpinner } from '@app/components/loading-spinner';
+import { ModalHeader } from '@app/components/modal-header';
+
+import { SwapContentLayout } from './components/swap-content.layout';
+import { SwapFooterLayout } from './components/swap-footer.layout';
+import { SwapSelectedAssets } from './components/swap-selected-assets';
+import { SwapFormValues } from './hooks/use-swap-form';
+import { useSwapContext } from './swap.context';
+
+export function Swap() {
+ const { isFetchingExchangeRate, swappableAssetsFrom } = useSwapContext();
+ const { dirty, isValid, setFieldValue, values } = useFormikContext();
+
+ useRouteHeader(, true);
+
+ useAsync(async () => {
+ if (isUndefined(values.swapAssetFrom))
+ return await setFieldValue('swapAssetFrom', swappableAssetsFrom[0]);
+ return;
+ }, [swappableAssetsFrom, values.swapAssetFrom]);
+
+ if (isUndefined(values.swapAssetFrom)) return ;
+
+ return (
+ <>
+
+
+
+
+
+ Review and swap
+
+
+
+ >
+ );
+}
diff --git a/src/app/pages/swap/swap.utils.ts b/src/app/pages/swap/swap.utils.ts
new file mode 100644
index 00000000000..181458ac6d7
--- /dev/null
+++ b/src/app/pages/swap/swap.utils.ts
@@ -0,0 +1,28 @@
+import { SwapAsset } from './hooks/use-swap-form';
+
+export function sortSwappableAssetsBySymbol(swappableAssets: SwapAsset[]) {
+ return swappableAssets
+ .sort((a, b) => {
+ if (a.name < b.name) return -1;
+ if (a.name > b.name) return 1;
+ return 0;
+ })
+ .sort((a, b) => {
+ if (a.name === 'STX') return -1;
+ if (b.name !== 'STX') return 1;
+ return 0;
+ })
+ .sort((a, b) => {
+ if (a.name === 'BTC') return -1;
+ if (b.name !== 'BTC') return 1;
+ return 0;
+ });
+}
+
+export function migratePositiveBalancesToTop(swappableAssets: SwapAsset[]) {
+ const assetsWithPositiveBalance = swappableAssets.filter(asset =>
+ asset.balance.amount.isGreaterThan(0)
+ );
+ const assetsWithZeroBalance = swappableAssets.filter(asset => asset.balance.amount.isEqualTo(0));
+ return [...assetsWithPositiveBalance, ...assetsWithZeroBalance];
+}
diff --git a/src/app/pages/swap/swap/swap.tsx b/src/app/pages/swap/swap/swap.tsx
deleted file mode 100644
index ac2c6608d6e..00000000000
--- a/src/app/pages/swap/swap/swap.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import { Outlet } from 'react-router-dom';
-
-import { useFormikContext } from 'formik';
-
-import { useRouteHeader } from '@app/common/hooks/use-route-header';
-import { LeatherButton } from '@app/components/button/button';
-import { ModalHeader } from '@app/components/modal-header';
-
-import { SwapContentLayout } from '../components/swap-content.layout';
-import { SwapFooterLayout } from '../components/swap-footer.layout';
-import { SwapSelectedAssets } from '../components/swap-selected-assets';
-import { SwapFormValues } from '../hooks/use-swap';
-import { useSwapContext } from '../swap.context';
-
-export function Swap() {
- const { onSubmitSwapForReview } = useSwapContext();
- const { dirty, handleSubmit, isValid, values } = useFormikContext();
-
- useRouteHeader(, true);
-
- return (
- <>
-
-
-
-
- {
- handleSubmit(e);
- await onSubmitSwapForReview(values);
- }}
- width="100%"
- >
- Review and swap
-
-
-
- >
- );
-}
diff --git a/src/app/pages/transaction-request/transaction-request.tsx b/src/app/pages/transaction-request/transaction-request.tsx
index c86651f30ab..330016ef3d7 100644
--- a/src/app/pages/transaction-request/transaction-request.tsx
+++ b/src/app/pages/transaction-request/transaction-request.tsx
@@ -46,7 +46,7 @@ import {
function TransactionRequestBase() {
const transactionRequest = useTransactionRequestState();
- const { setIsLoading, setIsIdle } = useLoading(LoadingKeys.SUBMIT_TRANSACTION);
+ const { setIsLoading, setIsIdle } = useLoading(LoadingKeys.SUBMIT_TRANSACTION_REQUEST);
const handleBroadcastTransaction = useSoftwareWalletTransactionRequestBroadcast();
const unsignedTx = useUnsignedStacksTransactionBaseState();
const { data: stxFees } = useCalculateStacksTxFees(unsignedTx.transaction);
diff --git a/src/app/query/common/alex-swaps/swappable-currency.query.ts b/src/app/query/common/alex-swaps/swappable-currency.query.ts
new file mode 100644
index 00000000000..415bbcc09df
--- /dev/null
+++ b/src/app/query/common/alex-swaps/swappable-currency.query.ts
@@ -0,0 +1,16 @@
+import { useQuery } from '@tanstack/react-query';
+import { AlexSDK } from 'alex-sdk';
+
+export function useSwappableCurrencyQuery(alexSDK: AlexSDK) {
+ return useQuery(
+ ['alex-supported-swap-currencies'],
+ async () => alexSDK.fetchSwappableCurrency(),
+ {
+ refetchOnMount: false,
+ refetchOnReconnect: false,
+ refetchOnWindowFocus: false,
+ retryDelay: 1000 * 60,
+ staleTime: 1000 * 60 * 10,
+ }
+ );
+}
diff --git a/src/app/query/stacks/fees/fees.hooks.ts b/src/app/query/stacks/fees/fees.hooks.ts
index 75478e0dc23..766de8ab51c 100644
--- a/src/app/query/stacks/fees/fees.hooks.ts
+++ b/src/app/query/stacks/fees/fees.hooks.ts
@@ -27,7 +27,7 @@ const defaultFeesMaxValues = [
createMoney(750000, 'STX'),
createMoney(2000000, 'STX'),
];
-const defaultFeesMinValues = [
+export const defaultFeesMinValues = [
createMoney(2500, 'STX'),
createMoney(3000, 'STX'),
createMoney(3500, 'STX'),
diff --git a/src/app/query/stacks/nonce/account-nonces.query.ts b/src/app/query/stacks/nonce/account-nonces.query.ts
index e45dbf109f4..e4d29d913c2 100644
--- a/src/app/query/stacks/nonce/account-nonces.query.ts
+++ b/src/app/query/stacks/nonce/account-nonces.query.ts
@@ -1,4 +1,3 @@
-import type { AddressNonces } from '@stacks/blockchain-api-client/lib/generated';
import { useQuery } from '@tanstack/react-query';
import { AppUseQueryConfig } from '@app/query/query-config';
@@ -21,7 +20,7 @@ function fetchAccountNonces(client: StacksClient, limiter: RateLimiter) {
await limiter.removeTokens(1);
return client.accountsApi.getAccountNonces({
principal,
- }) as Promise;
+ });
};
}
diff --git a/src/app/query/stacks/nonce/account-nonces.utils.ts b/src/app/query/stacks/nonce/account-nonces.utils.ts
index b646fa04b47..bd98a53fe74 100644
--- a/src/app/query/stacks/nonce/account-nonces.utils.ts
+++ b/src/app/query/stacks/nonce/account-nonces.utils.ts
@@ -97,7 +97,10 @@ export function parseAccountNoncesResponse({
const lastConfirmedTxNonceIncremented = confirmedTxsNonces.length && confirmedTxsNonces[0] + 1;
const lastPendingTxNonceIncremented = lastPendingTxNonce + 1;
const pendingTxsNoncesIncludesApiPossibleNextNonce = pendingTxsNonces.includes(possibleNextNonce);
- const pendingTxsMissingNonces = findAnyMissingPendingTxsNonces(pendingTxsNonces);
+ // Make sure any pending tx nonces are not already confirmed
+ const pendingTxsMissingNonces = findAnyMissingPendingTxsNonces(pendingTxsNonces).filter(
+ nonce => !confirmedTxsNonces.includes(nonce)
+ );
const firstPendingMissingNonce = pendingTxsMissingNonces.sort()[0];
const hasApiMissingNonces = detectedMissingNonces?.length > 0;
diff --git a/src/app/store/transactions/contract-call.hooks.ts b/src/app/store/transactions/contract-call.hooks.ts
new file mode 100644
index 00000000000..c29e3974ab3
--- /dev/null
+++ b/src/app/store/transactions/contract-call.hooks.ts
@@ -0,0 +1,35 @@
+import { useCallback } from 'react';
+
+import { ContractCallPayload } from '@stacks/connect';
+
+import { StacksTransactionFormValues } from '@shared/models/form.model';
+
+import {
+ GenerateUnsignedTransactionOptions,
+ generateUnsignedTransaction,
+} from '@app/common/transactions/stacks/generate-unsigned-txs';
+import { useNextNonce } from '@app/query/stacks/nonce/account-nonces.hooks';
+
+import { useCurrentStacksAccount } from '../accounts/blockchain/stacks/stacks-account.hooks';
+import { useCurrentStacksNetworkState } from '../networks/networks.hooks';
+
+export function useGenerateStacksContractCallUnsignedTx() {
+ const { data: nextNonce } = useNextNonce();
+ const network = useCurrentStacksNetworkState();
+ const account = useCurrentStacksAccount();
+
+ return useCallback(
+ async (payload: ContractCallPayload, values: StacksTransactionFormValues) => {
+ if (!account) return;
+
+ const options: GenerateUnsignedTransactionOptions = {
+ publicKey: account.stxPublicKey,
+ nonce: Number(values?.nonce) ?? nextNonce?.nonce,
+ fee: values.fee ?? 0,
+ txData: { ...payload, network },
+ };
+ return generateUnsignedTransaction(options);
+ },
+ [account, network, nextNonce?.nonce]
+ );
+}
diff --git a/src/app/store/transactions/requests.hooks.ts b/src/app/store/transactions/requests.hooks.ts
index 1adf80e6b16..44254f8d534 100644
--- a/src/app/store/transactions/requests.hooks.ts
+++ b/src/app/store/transactions/requests.hooks.ts
@@ -12,9 +12,7 @@ export function useTransactionRequestState() {
const requestToken = useTransactionRequest();
return useMemo(() => {
- if (!requestToken) {
- return null;
- }
+ if (!requestToken) return null;
return getPayloadFromToken(requestToken);
}, [requestToken]);
}
diff --git a/src/shared/logger-storage.ts b/src/shared/logger-storage.ts
index 5d0ca41a671..5609acf2c76 100644
--- a/src/shared/logger-storage.ts
+++ b/src/shared/logger-storage.ts
@@ -6,7 +6,9 @@ const maxLogLength = 2_000;
const logStorageKey = 'logs';
-const storageAdapter = chrome.storage.local;
+function getStorageAdapter() {
+ return chrome.storage.local;
+}
function truncateLogToMaxSize(logs: LogItem[]) {
if (logs.length <= maxLogLength) return logs;
@@ -15,17 +17,17 @@ function truncateLogToMaxSize(logs: LogItem[]) {
export async function getLogSizeInBytes(): Promise {
return new Promise(resolve =>
- storageAdapter.getBytesInUse([logStorageKey], bytes => resolve(bytes))
+ getStorageAdapter().getBytesInUse([logStorageKey], bytes => resolve(bytes))
);
}
export async function clearBrowserStorageLogs(): Promise {
- return new Promise(resolve => storageAdapter.set({ [logStorageKey]: [] }, () => resolve()));
+ return new Promise(resolve => getStorageAdapter().set({ [logStorageKey]: [] }, () => resolve()));
}
export async function getLogsFromBrowserStorage(): Promise {
return new Promise(resolve =>
- storageAdapter.get([logStorageKey], ({ logs }) => resolve(Array.isArray(logs) ? logs : []))
+ getStorageAdapter().get([logStorageKey], ({ logs }) => resolve(Array.isArray(logs) ? logs : []))
);
}
@@ -39,8 +41,9 @@ export async function appendLogToBrowserStorage(logEvent: pino.LogEvent): Promis
const { ts, level, messages } = logEvent;
const formattedLogItem = [new Date(ts).toISOString(), level.label, ...messages] as LogItem;
return new Promise(resolve =>
- storageAdapter.set({ [logStorageKey]: truncateLogToMaxSize([formattedLogItem, ...logs]) }, () =>
- resolve(formattedLogItem)
+ getStorageAdapter().set(
+ { [logStorageKey]: truncateLogToMaxSize([formattedLogItem, ...logs]) },
+ () => resolve(formattedLogItem)
)
);
}
diff --git a/src/shared/models/blockchain.model.ts b/src/shared/models/blockchain.model.ts
index b5110ad7c20..345284314d3 100644
--- a/src/shared/models/blockchain.model.ts
+++ b/src/shared/models/blockchain.model.ts
@@ -1 +1,3 @@
-export type Blockchains = 'bitcoin' | 'stacks';
+import { LiteralUnion } from '@shared/utils/type-utils';
+
+export type Blockchains = LiteralUnion<'bitcoin' | 'stacks', string>;
diff --git a/src/shared/route-urls.ts b/src/shared/route-urls.ts
index 36e50fc005d..1d4fd3e4311 100644
--- a/src/shared/route-urls.ts
+++ b/src/shared/route-urls.ts
@@ -86,9 +86,8 @@ export enum RouteUrls {
// Swap routes
Swap = '/swap',
SwapChooseAsset = '/swap/choose-asset',
+ SwapError = '/swap/error',
SwapReview = '/swap/review',
- SwapSummary = '/swap/summary',
- SwapSummaryDetails = '/swap/summary/details',
// Legacy request routes
ProfileUpdateRequest = '/update-profile',
diff --git a/src/shared/utils.ts b/src/shared/utils.ts
index 9df2aceaa8f..37ee9fc2ad1 100644
--- a/src/shared/utils.ts
+++ b/src/shared/utils.ts
@@ -48,7 +48,9 @@ export function ensureArray(value: T | T[]): T[] {
export function undefinedIfLengthZero(arr: T) {
return arr.length ? arr : undefined;
}
+
type NetworkMap = Record;
+
export function whenNetwork(mode: NetworkModes) {
return >(networkMap: T) => networkMap[mode] as T[NetworkModes];
}
@@ -68,3 +70,7 @@ export function closeWindow() {
// eslint-disable-next-line no-restricted-properties
window.close();
}
+
+export function removeTrailingNullCharacters(s: string) {
+ return s.replace(/\0*$/g, '');
+}
diff --git a/theme/recipes/button.ts b/theme/recipes/button.ts
index a0469bcc9b9..710c76dc6e4 100644
--- a/theme/recipes/button.ts
+++ b/theme/recipes/button.ts
@@ -64,6 +64,13 @@ export const buttonRecipe = defineRecipe({
color: 'brown.1',
_hover: { bg: 'brown.10' },
_active: { bg: 'brown.12' },
+ _disabled: {
+ _hover: {
+ bg: 'brown.6',
+ },
+ bg: 'brown.6',
+ color: 'white',
+ },
...focusStyles,
...loadingStyles('brown.2'),
},
@@ -84,7 +91,7 @@ export const buttonRecipe = defineRecipe({
// Ghost button
ghost: {
- _hover: { bg: 'brown.3' },
+ _hover: { bg: 'brown.2' },
_focus: { _before: { border: '2px solid', borderColor: 'blue.500' } },
...loadingStyles('brown.12'),
},
diff --git a/theme/semantic-tokens.ts b/theme/semantic-tokens.ts
index 2636852533e..498d032efca 100644
--- a/theme/semantic-tokens.ts
+++ b/theme/semantic-tokens.ts
@@ -37,7 +37,7 @@ export const semanticTokens = defineSemanticTokens({
value: { base: '{colors.lightModeBrown.12}', _dark: '{colors.darkModeBrown.12}' },
},
'text-subdued': {
- value: { base: '{colors.lightModeBrown.11}', _dark: '{colors.darkModeBrown.11}' },
+ value: { base: '{colors.lightModeBrown.8}', _dark: '{colors.darkModeBrown.8}' },
},
'action-primary-hover': {
value: { base: '{colors.lightModeBrown.10}', _dark: '{colors.darkModeBrown.10}' },
@@ -72,6 +72,9 @@ export const semanticTokens = defineSemanticTokens({
disabled: {
value: { base: '{colors.blue.100}', _dark: '{colors.blue.100}' },
},
+ focused: {
+ value: { base: '{colors.blue.500}', _dark: '{colors.blue.500}' },
+ },
warning: {
value: { base: '{colors.yellow.100}', _dark: '{colors.yellow.100}' },
},
diff --git a/theme/tokens.ts b/theme/tokens.ts
index d06014c41e7..252ffee29ff 100644
--- a/theme/tokens.ts
+++ b/theme/tokens.ts
@@ -40,4 +40,9 @@ export const tokens = defineTokens({
'extra-loose': { value: '32px' },
},
colors,
+ borders: {
+ default: { value: '1px solid {colors.accent.border-default}' },
+ error: { value: '1px solid {colors.error}' },
+ 'action-primary-default': { value: '1px solid {colors.accent.action-primary-default}' },
+ },
});
diff --git a/yarn.lock b/yarn.lock
index 34f11ceb63f..85eaf897be7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4793,7 +4793,7 @@
"@blockstack/stacks-transactions" "0.7.0"
cross-fetch "^3.0.4"
-"@stacks/stacks-blockchain-api-types@*":
+"@stacks/stacks-blockchain-api-types@*", "@stacks/stacks-blockchain-api-types@^7.1.10":
version "7.3.2"
resolved "https://registry.yarnpkg.com/@stacks/stacks-blockchain-api-types/-/stacks-blockchain-api-types-7.3.2.tgz#33838e96312c2be1df93dce1c76e6d584b966a39"
integrity sha512-1r0+eqEWOOo7UYrFq9HGbc02DVME3NVCW/45sNKPN31PkOMMaK59DHragPJ2QbxPFiutVDUCS924+48+o3+0Tw==
@@ -8259,6 +8259,13 @@ ajv@^6.12.2, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.7.0:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
+alex-sdk@0.1.22:
+ version "0.1.22"
+ resolved "https://registry.yarnpkg.com/alex-sdk/-/alex-sdk-0.1.22.tgz#ea94f2ebbb962c402ee485e10c5de5b5b66240af"
+ integrity sha512-g8sQN5Cs8mbkbOb0sHFN//lYVlJq6jG452LGOtNSOeoP7I5WNWgkwn0OrW8jqxjanbQCgoouP4xutXky1JDIGQ==
+ dependencies:
+ clarity-codegen "^0.2.6"
+
anser@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/anser/-/anser-2.1.1.tgz#8afae28d345424c82de89cc0e4d1348eb0c5af7c"
@@ -8712,7 +8719,7 @@ aws4@^1.8.0:
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.12.0.tgz#ce1c9d143389679e253b314241ea9aa5cec980d3"
integrity sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==
-axios@1.5.1:
+axios@1.5.1, axios@^1.5.0:
version "1.5.1"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.5.1.tgz#11fbaa11fc35f431193a9564109c88c1f27b585f"
integrity sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==
@@ -9574,6 +9581,17 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
inherits "^2.0.1"
safe-buffer "^5.0.1"
+clarity-codegen@^0.2.6:
+ version "0.2.6"
+ resolved "https://registry.yarnpkg.com/clarity-codegen/-/clarity-codegen-0.2.6.tgz#897bfdc0374279c3a6dceebbe83bb6b553d6184b"
+ integrity sha512-1ZZoPO4VcqPkOaOPaj0OxgVeAJAjpga2nbbMTVynrYBEwN77hrWIwYfnICR0K3XFoyeW+mzxnYw9CpOvEA9eWQ==
+ dependencies:
+ "@stacks/stacks-blockchain-api-types" "^7.1.10"
+ axios "^1.5.0"
+ lodash "^4.17.21"
+ yargs "^17.7.2"
+ yqueue "^1.0.1"
+
classnames@^2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924"
@@ -20812,7 +20830,7 @@ yargs@17.7.1:
y18n "^5.0.5"
yargs-parser "^21.1.1"
-yargs@17.7.2, yargs@^17.0.0, yargs@^17.7.1:
+yargs@17.7.2, yargs@^17.0.0, yargs@^17.7.1, yargs@^17.7.2:
version "17.7.2"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"
integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==
@@ -20860,6 +20878,11 @@ yocto-queue@^1.0.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251"
integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==
+yqueue@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/yqueue/-/yqueue-1.0.1.tgz#3b4f17344f2481350577f0fd29146556439f542b"
+ integrity sha512-DBxJZBRafFLA/tCc5uO8ZTGFr+sQgn1FRJkZ4cVrIQIk6bv2bInraE3mbpLAJw9z93JGrLkqDoyTLrrZaCNq5w==
+
yup@1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/yup/-/yup-1.3.2.tgz#afffc458f1513ed386e6aaf4bcaa4e67a9e270dc"