From 77bab7b0772071682711042506ddeb89be7451cb Mon Sep 17 00:00:00 2001 From: fbwoolf Date: Thu, 14 Sep 2023 13:57:29 -0500 Subject: [PATCH] feat: implement alex sponsored txs --- src/app/common/date-utils.ts | 5 +++ src/app/common/money/calculate-money.ts | 16 ++++++- .../generic-error/generic-error.layout.tsx | 42 +++++++------------ .../generic-error/generic-error.tsx | 14 +++++-- src/app/components/icons/dot-icon.tsx | 1 + .../pages/home/components/account-actions.tsx | 18 ++++---- .../send-form-confirmation.utils.tsx | 16 ------- .../hooks/use-stacks-transaction-summary.ts | 7 ++-- .../swap/components/swap-amount-field.tsx | 2 +- .../components/swap-details/swap-details.tsx | 2 +- .../components/swap-status/swap-status.tsx | 19 +++++++-- src/app/pages/swap/hooks/use-alex-swap.tsx | 2 +- .../swap/hooks/use-stacks-broadcast-swap.tsx | 2 + .../swap/swap-choose-fee/swap-choose-fee.tsx | 0 src/app/pages/swap/swap-container.tsx | 32 ++++++++++---- src/app/pages/swap/swap-error/swap-error.tsx | 31 ++++++++++++++ .../pages/swap/swap-review/swap-review.tsx | 4 +- src/app/pages/swap/swap.context.ts | 1 + src/app/pages/swap/swap.routes.tsx | 2 + .../stacks/nonce/account-nonces.query.ts | 3 +- src/shared/route-urls.ts | 1 + yarn.lock | 2 +- 22 files changed, 143 insertions(+), 79 deletions(-) delete mode 100644 src/app/pages/send/send-crypto-asset-form/components/confirmation/send-form-confirmation.utils.tsx delete mode 100644 src/app/pages/swap/swap-choose-fee/swap-choose-fee.tsx create mode 100644 src/app/pages/swap/swap-error/swap-error.tsx diff --git a/src/app/common/date-utils.ts b/src/app/common/date-utils.ts index 952f4ad50f0..4d9db9df0bb 100644 --- a/src/app/common/date-utils.ts +++ b/src/app/common/date-utils.ts @@ -31,6 +31,11 @@ export function displayDate(txDate: string): string { return date.format('MMM Do, YYYY'); } +export function displayTime(txDate: string) { + const date = dayjs(txDate); + return date.format('h:mm A'); +} + export function isoDateToLocalDateSafe(isoDate: string) { try { return isoDateToLocalDate(isoDate); diff --git a/src/app/common/money/calculate-money.ts b/src/app/common/money/calculate-money.ts index 6a4a9d6943b..8086de577bb 100644 --- a/src/app/common/money/calculate-money.ts +++ b/src/app/common/money/calculate-money.ts @@ -1,8 +1,8 @@ import { BigNumber } from 'bignumber.js'; import { MarketData, formatMarketPair } from '@shared/models/market.model'; -import { Money, createMoney } from '@shared/models/money.model'; -import { isNumber } from '@shared/utils'; +import { Money, NumType, createMoney } from '@shared/models/money.model'; +import { isBigInt, isNumber } from '@shared/utils'; import { sumNumbers } from '../math/helpers'; import { formatMoney } from './format-money'; @@ -31,6 +31,18 @@ export function convertAmountToFractionalUnit(num: Money | BigNumber, decimals?: return num.shiftedBy(decimals); } +export function convertToMoneyTypeWithDefaultOfZero( + symbol: string, + num?: NumType, + decimals?: number +) { + return createMoney( + isBigInt(num) ? new BigNumber(num.toString()) : new BigNumber(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/components/generic-error/generic-error.layout.tsx b/src/app/components/generic-error/generic-error.layout.tsx index 61b67bf816c..608c2c355eb 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,31 @@ 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 a9f37caac50..e6f854cbc24 100644 --- a/src/app/components/generic-error/generic-error.tsx +++ b/src/app/components/generic-error/generic-error.tsx @@ -1,22 +1,30 @@ import { ReactNode } from 'react'; +import { FlexProps } from 'leather-styles/jsx'; + import { useRouteHeader } from '@app/common/hooks/use-route-header'; 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 = () => window.close(), title } = props; + const { body, helpTextList, onClose = () => window.close(), title, ...rest } = props; useRouteHeader(
); return ( - + ); } diff --git a/src/app/components/icons/dot-icon.tsx b/src/app/components/icons/dot-icon.tsx index 5d15cfb556c..49b8aa170ad 100644 --- a/src/app/components/icons/dot-icon.tsx +++ b/src/app/components/icons/dot-icon.tsx @@ -1,3 +1,4 @@ +// ts-unused-exports:disable-next-line export function DotIcon(props: React.SVGProps) { return ( navigate(RouteUrls.Fund)} /> - {SWAP_ENABLED ? ( - } - label="Swap" - onClick={() => navigate(RouteUrls.Swap)} - /> - ) : null} + {/* !!!IMPORTANT!!! */} + {/* TODO: Hide swap button before merging, use SWAP_ENABLED flag */} + } + label="Swap" + onClick={() => navigate(RouteUrls.Swap)} + /> ); } 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-transaction-summary.ts b/src/app/pages/send/send-crypto-asset-form/family/stacks/hooks/use-stacks-transaction-summary.ts index aeabbb4397b..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 @@ -13,15 +13,16 @@ 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(); diff --git a/src/app/pages/swap/components/swap-amount-field.tsx b/src/app/pages/swap/components/swap-amount-field.tsx index ab32df744d0..808f5e56016 100644 --- a/src/app/pages/swap/components/swap-amount-field.tsx +++ b/src/app/pages/swap/components/swap-amount-field.tsx @@ -32,7 +32,7 @@ export function SwapAmountField({ amountAsFiat, isDisabled, name }: SwapAmountFi } return ( - + 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 d64555b1e67..20be51c0b9c 100644 --- a/src/app/pages/swap/components/swap-details/swap-details.tsx +++ b/src/app/pages/swap/components/swap-details/swap-details.tsx @@ -1,8 +1,8 @@ import { isUndefined } from '@shared/utils'; +import { convertToMoneyTypeWithDefaultOfZero } from '@app/common/money/calculate-money'; import { formatMoney } from '@app/common/money/format-money'; import { getEstimatedConfirmationTime } from '@app/common/transactions/stacks/transaction.utils'; -import { convertToMoneyTypeWithDefaultOfZero } from '@app/pages/send/send-crypto-asset-form/components/confirmation/send-form-confirmation.utils'; import { useSwapContext } from '@app/pages/swap/swap.context'; import { useStacksBlockTime } from '@app/query/stacks/info/info.hooks'; import { useCurrentNetworkState } from '@app/store/networks/networks.hooks'; diff --git a/src/app/pages/swap/components/swap-status/swap-status.tsx b/src/app/pages/swap/components/swap-status/swap-status.tsx index af47184b6ea..4dc40d2aeef 100644 --- a/src/app/pages/swap/components/swap-status/swap-status.tsx +++ b/src/app/pages/swap/components/swap-status/swap-status.tsx @@ -1,15 +1,26 @@ -import { DashedHr } from '@app/components/hr'; +import { displayDate, displayTime } from '@app/common/date-utils'; import { CheckmarkIcon } from '@app/components/icons/checkmark-icon'; -import { DotIcon } from '@app/components/icons/dot-icon'; +import { useSwapContext } from '../../swap.context'; import { SwapStatusItemLayout } from './swap-status-item.layout'; import { SwapStatusLayout } from './swap-status.layout'; -// TODO: Replace with live data export function SwapStatus() { + const { swapSubmissionData } = useSwapContext(); + + if (!swapSubmissionData) return null; + return ( } + text="You submitted your swap" + timestamp={`${displayDate(swapSubmissionData.timestamp)} at ${displayTime( + swapSubmissionData.timestamp + )}`} + /> + {/* TODO: Use status updates with future protocols - leaving as examples from designs */} + {/* } text="You set up your swap" timestamp="Today at 10:14 PM" @@ -23,7 +34,7 @@ export function SwapStatus() { } text="We escrow your transaction" /> - } text="We add your xBTC to your balance" /> + } text="We add your xBTC to your balance" /> */} ); } diff --git a/src/app/pages/swap/hooks/use-alex-swap.tsx b/src/app/pages/swap/hooks/use-alex-swap.tsx index 00d7ce77ef9..a71ecdc6380 100644 --- a/src/app/pages/swap/hooks/use-alex-swap.tsx +++ b/src/app/pages/swap/hooks/use-alex-swap.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { AlexSDK, Currency, TokenInfo } from 'alex-sdk'; diff --git a/src/app/pages/swap/hooks/use-stacks-broadcast-swap.tsx b/src/app/pages/swap/hooks/use-stacks-broadcast-swap.tsx index b400363cbe8..4f56076559a 100644 --- a/src/app/pages/swap/hooks/use-stacks-broadcast-swap.tsx +++ b/src/app/pages/swap/hooks/use-stacks-broadcast-swap.tsx @@ -12,6 +12,8 @@ import { LoadingKeys } from '@app/common/hooks/use-loading'; import { useSubmitTransactionCallback } from '@app/common/hooks/use-submit-stx-transaction'; import { useSignTransactionSoftwareWallet } from '@app/store/transactions/transaction.hooks'; +// TODO: Remove if end up not needing +// ts-unused-exports:disable-next-line export function useStacksBroadcastSwap() { const signSoftwareWalletTx = useSignTransactionSoftwareWallet(); const [isBroadcasting, setIsBroadcasting] = useState(false); diff --git a/src/app/pages/swap/swap-choose-fee/swap-choose-fee.tsx b/src/app/pages/swap/swap-choose-fee/swap-choose-fee.tsx deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/app/pages/swap/swap-container.tsx b/src/app/pages/swap/swap-container.tsx index da4269bba30..435e03132a5 100644 --- a/src/app/pages/swap/swap-container.tsx +++ b/src/app/pages/swap/swap-container.tsx @@ -9,30 +9,31 @@ import { serializeCV, serializePostCondition, } from '@stacks/transactions'; +import { SponsoredTxError } from 'alex-sdk'; import BigNumber from 'bignumber.js'; -import get from 'lodash.get'; import { logger } from '@shared/logger'; import { RouteUrls } from '@shared/route-urls'; import { isDefined, isUndefined } from '@shared/utils'; +import { LoadingKeys, useLoading } from '@app/common/hooks/use-loading'; import { stxToMicroStx } from '@app/common/money/unit-conversion'; 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 { oneHundredMillion, useAlexSwap } from './hooks/use-alex-swap'; -import { useStacksBroadcastSwap } from './hooks/use-stacks-broadcast-swap'; import { SwapAsset, SwapFormValues } from './hooks/use-swap'; import { SwapContext, SwapProvider } from './swap.context'; export function SwapContainer() { const navigate = useNavigate(); + const { setIsLoading, setIsIdle } = useLoading(LoadingKeys.SUBMIT_SWAP_TRANSACTION); const currentAccount = useCurrentStacksAccount(); - // TODO: Refactor to review the unsigned tx? const generateUnsignedTx = useGenerateStacksContractCallUnsignedTx(); - const signAndBroadcastSwap = useStacksBroadcastSwap(); + const signSoftwareWalletTx = useSignTransactionSoftwareWallet(); const { alexSDK, @@ -75,6 +76,7 @@ export function SwapContainer() { swapAmountTo: values.swapAmountTo, swapAssetFrom: values.swapAssetFrom, swapAssetTo: values.swapAssetTo, + timestamp: new Date().toISOString(), }); navigate(RouteUrls.SwapReview); @@ -94,6 +96,8 @@ export function SwapContainer() { return; } + setIsLoading(); + const fromAmount = BigInt( new BigNumber(swapSubmissionData.swapAmountFrom) .multipliedBy(oneHundredMillion) @@ -134,19 +138,29 @@ export function SwapContainer() { postConditionMode: PostConditionMode.Deny, postConditions: tx.postConditions.map(pc => bytesToHex(serializePostCondition(pc))), publicKey: currentAccount?.stxPublicKey, + sponsored: true, txType: TransactionTypes.ContractCall, }; const unsignedTx = await generateUnsignedTx(payload, tempFormValues); if (!unsignedTx) return logger.error('Attempted to generate unsigned tx, but tx is undefined'); - console.log(unsignedTx); - const { stacksBroadcastTransaction } = signAndBroadcastSwap(unsignedTx); + + const signedTx = signSoftwareWalletTx(unsignedTx); + if (!signedTx) return logger.error('Attempted to generate raw tx, but signed tx is undefined'); + const txRaw = bytesToHex(signedTx.serialize()); try { - await stacksBroadcastTransaction(); + const txId = await alexSDK.broadcastSponsoredTx(txRaw); + setIsIdle(); + navigate(RouteUrls.SwapSummary, { state: { txId } }); } catch (e) { - navigate(RouteUrls.TransactionBroadcastError, { state: { message: get(e, 'message') } }); - return; + setIsIdle(); + navigate(RouteUrls.SwapError, { + state: { + message: e instanceof (Error || SponsoredTxError) ? e.message : 'Unknown error', + title: 'Failed to broadcast', + }, + }); } } 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.context.ts b/src/app/pages/swap/swap.context.ts index 3ce856ee372..046eb0bc331 100644 --- a/src/app/pages/swap/swap.context.ts +++ b/src/app/pages/swap/swap.context.ts @@ -7,6 +7,7 @@ export interface SwapSubmissionData extends SwapFormValues { protocol: string; router: SwapAsset[]; slippage: number; + timestamp: string; } export interface SwapContext { diff --git a/src/app/pages/swap/swap.routes.tsx b/src/app/pages/swap/swap.routes.tsx index 8414b47ef3d..d8967828fdf 100644 --- a/src/app/pages/swap/swap.routes.tsx +++ b/src/app/pages/swap/swap.routes.tsx @@ -9,6 +9,7 @@ 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'; @@ -23,6 +24,7 @@ export const swapRoutes = ( }> } /> + } /> } /> }> } /> 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/shared/route-urls.ts b/src/shared/route-urls.ts index 266fe6478c7..fd7cdb225be 100644 --- a/src/shared/route-urls.ts +++ b/src/shared/route-urls.ts @@ -86,6 +86,7 @@ export enum RouteUrls { // Swap routes Swap = '/swap', SwapChooseAsset = '/swap/choose-asset', + SwapError = '/swap/error', SwapReview = '/swap/review', SwapSummary = '/swap/summary', SwapSummaryDetails = '/swap/summary/details', diff --git a/yarn.lock b/yarn.lock index 4b9b69c5575..f787c37b76c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13706,7 +13706,7 @@ is-typedarray@^1.0.0, is-typedarray@~1.0.0: resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== -is-unicode-supported@^1.1.0, is-unicode-supported@^1.2.0, is-unicode-supported@^1.3.0: +is-unicode-supported@^1.1.0, is-unicode-supported@^1.2.0: version "1.3.0" resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz#d824984b616c292a2e198207d4a609983842f714" integrity sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==