Skip to content

Commit

Permalink
feat: implement alex sponsored txs
Browse files Browse the repository at this point in the history
  • Loading branch information
fbwoolf committed Oct 12, 2023
1 parent a35669c commit 77bab7b
Show file tree
Hide file tree
Showing 22 changed files with 143 additions and 79 deletions.
5 changes: 5 additions & 0 deletions src/app/common/date-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
16 changes: 14 additions & 2 deletions src/app/common/money/calculate-money.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
Expand Down
42 changes: 16 additions & 26 deletions src/app/components/generic-error/generic-error.layout.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -22,16 +21,8 @@ export function GenericErrorLayout(props: GenericErrorProps) {
const { body, helpTextList, onClose, title, ...rest } = props;

return (
<Flex
alignItems="center"
flexDirection="column"
px={['space.05', 'space.05', 'unset']}
width="100%"
{...rest}
>
<Box mt="loose">
<img src={GenericError} width="106px" />
</Box>
<Flex alignItems="center" flexDirection="column" px="space.05" width="100%" {...rest}>
<styled.img src={GenericError} width="106px" height="72px" alt="Error" mt="space.06" />
<styled.h1 mt="space.05" textStyle="heading.04">
{title}
</styled.h1>
Expand All @@ -44,32 +35,31 @@ export function GenericErrorLayout(props: GenericErrorProps) {
>
{body}
</styled.h2>
<Box
as="ul"
border="2px solid #EFEFF2"
borderRadius="12px"
color={color('text-caption')}
<styled.ul
border="1px solid"
borderColor="accent.border-default !important"
borderRadius="10px"
fontSize="14px"
lineHeight="1.6"
listStyleType="circle"
mt="extra-loose"
pb="loose"
mt="space.06"
pb="space.05"
pl="40px"
pr="loose"
pt="tight"
pr="space.05"
pt="space.02"
textAlign="left"
width="100%"
>
{helpTextList}
<Box as="li" mt="base" textAlign="left">
<styled.li mt="space.04" textAlign="left">
<HStack alignItems="center">
<Text>Reach out to our support team</Text>
<Box as="button" onClick={() => openInNewTab(supportUrl)}>
<styled.span textStyle="label.02">Reach out to our support team</styled.span>
<styled.button onClick={() => openInNewTab(supportUrl)}>
<ExternalLinkIcon />
</Box>
</styled.button>
</HStack>
</Box>
</Box>
</styled.li>
</styled.ul>
<LeatherButton fontSize="14px" mt="space.05" onClick={onClose} variant="link">
Close window
</LeatherButton>
Expand Down
14 changes: 11 additions & 3 deletions src/app/components/generic-error/generic-error.tsx
Original file line number Diff line number Diff line change
@@ -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(<Header hideActions />);

return (
<GenericErrorLayout body={body} helpTextList={helpTextList} onClose={onClose} title={title} />
<GenericErrorLayout
body={body}
helpTextList={helpTextList}
onClose={onClose}
title={title}
{...rest}
/>
);
}
1 change: 1 addition & 0 deletions src/app/components/icons/dot-icon.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// ts-unused-exports:disable-next-line
export function DotIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
Expand Down
18 changes: 9 additions & 9 deletions src/app/pages/home/components/account-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { 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 { SWAP_ENABLED } from '@shared/environment';
import { RouteUrls } from '@shared/route-urls';

import { ArrowDown } from '@app/components/icons/arrow-down';
Expand Down Expand Up @@ -34,14 +34,14 @@ export function AccountActions(props: FlexProps) {
label="Buy"
onClick={() => navigate(RouteUrls.Fund)}
/>
{SWAP_ENABLED ? (
<ActionButton
data-testid={''}
icon={<SwapIcon />}
label="Swap"
onClick={() => navigate(RouteUrls.Swap)}
/>
) : null}
{/* !!!IMPORTANT!!! */}
{/* TODO: Hide swap button before merging, use SWAP_ENABLED flag */}
<ActionButton
data-testid={''}
icon={<SwapIcon />}
label="Swap"
onClick={() => navigate(RouteUrls.Swap)}
/>
</Flex>
);
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion src/app/pages/swap/components/swap-amount-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function SwapAmountField({ amountAsFiat, isDisabled, name }: SwapAmountFi
}

return (
<Stack alignItems="flex-end" spacing="extra-tight">
<Stack alignItems="flex-end" spacing="extra-tight" width="50%">
<Caption as="label" hidden htmlFor={name}>
{name}
</Caption>
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
19 changes: 15 additions & 4 deletions src/app/pages/swap/components/swap-status/swap-status.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<SwapStatusLayout>
<SwapStatusItemLayout
icon={<CheckmarkIcon />}
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 */}
{/* <SwapStatusItemLayout
icon={<CheckmarkIcon />}
text="You set up your swap"
timestamp="Today at 10:14 PM"
Expand All @@ -23,7 +34,7 @@ export function SwapStatus() {
<DashedHr />
<SwapStatusItemLayout icon={<DotIcon />} text="We escrow your transaction" />
<DashedHr />
<SwapStatusItemLayout icon={<DotIcon />} text="We add your xBTC to your balance" />
<SwapStatusItemLayout icon={<DotIcon />} text="We add your xBTC to your balance" /> */}
</SwapStatusLayout>
);
}
2 changes: 1 addition & 1 deletion src/app/pages/swap/hooks/use-alex-swap.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
2 changes: 2 additions & 0 deletions src/app/pages/swap/hooks/use-stacks-broadcast-swap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Empty file.
32 changes: 23 additions & 9 deletions src/app/pages/swap/swap-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -75,6 +76,7 @@ export function SwapContainer() {
swapAmountTo: values.swapAmountTo,
swapAssetFrom: values.swapAssetFrom,
swapAssetTo: values.swapAssetTo,
timestamp: new Date().toISOString(),
});

navigate(RouteUrls.SwapReview);
Expand All @@ -94,6 +96,8 @@ export function SwapContainer() {
return;
}

setIsLoading();

const fromAmount = BigInt(
new BigNumber(swapSubmissionData.swapAmountFrom)
.multipliedBy(oneHundredMillion)
Expand Down Expand Up @@ -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',
},
});
}
}

Expand Down
Loading

0 comments on commit 77bab7b

Please sign in to comment.