From 8fcc5993ea2d818d0ff0429b95fc9ea9c1ba8ce1 Mon Sep 17 00:00:00 2001 From: pseudozach Date: Wed, 14 Feb 2024 00:57:31 -0800 Subject: [PATCH 1/5] enable swap on testnet --- src/app/pages/home/components/account-actions.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/app/pages/home/components/account-actions.tsx b/src/app/pages/home/components/account-actions.tsx index c9cf6df91f7..873449aa7de 100644 --- a/src/app/pages/home/components/account-actions.tsx +++ b/src/app/pages/home/components/account-actions.tsx @@ -58,7 +58,14 @@ export function AccountActions(props: FlexProps) { onClick={() => navigate(RouteUrls.Swap)} /> ), - [ChainID.Testnet]: null, + [ChainID.Testnet]: ( + } + label="Swap" + onClick={() => navigate(RouteUrls.Swap)} + /> + ), })} ); From 0f2dae0d395abe2193223e6e2aec9a5f153c1e7c Mon Sep 17 00:00:00 2001 From: pseudozach Date: Wed, 14 Feb 2024 00:58:27 -0800 Subject: [PATCH 2/5] add lnswap-swap-container --- src/app/pages/swap/lnswap-swap-container.tsx | 214 +++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 src/app/pages/swap/lnswap-swap-container.tsx diff --git a/src/app/pages/swap/lnswap-swap-container.tsx b/src/app/pages/swap/lnswap-swap-container.tsx new file mode 100644 index 00000000000..bc64d9b6637 --- /dev/null +++ b/src/app/pages/swap/lnswap-swap-container.tsx @@ -0,0 +1,214 @@ +import { useMemo, useState } from 'react'; +import { Outlet, useNavigate } from 'react-router-dom'; + +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 { 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 { useWalletType } from '@app/common/use-wallet-type'; +import { NonceSetter } from '@app/components/nonce-setter'; +import { defaultFeesMinValuesAsMoney } from '@app/query/stacks/fees/fees.utils'; +import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; +import { useGenerateStacksContractCallUnsignedTx } from '@app/store/transactions/contract-call.hooks'; +import { useSignStacksTransaction } from '@app/store/transactions/transaction.hooks'; + +import { SwapContainerLayout } from './components/swap-container.layout'; +import { SwapForm } from './components/swap-form'; +import { generateSwapRoutes } from './generate-swap-routes'; +import { useAlexBroadcastSwap } from './hooks/use-alex-broadcast-swap'; +import { oneHundredMillion, useLNSwapSwap } from './hooks/use-lnswap-swap'; +import { useStacksBroadcastSwap } from './hooks/use-stacks-broadcast-swap'; +import { SwapAsset, SwapFormValues } from './hooks/use-swap-form'; +import { SwapContext, SwapProvider } from './swap.context'; +import { migratePositiveBalancesToTop, sortSwappableAssetsBySymbol } from './swap.utils'; + +export const lnswapSwapRoutes = generateSwapRoutes(); + +function LNSwapContainer() { + const [isSendingMax, setIsSendingMax] = useState(false); + const navigate = useNavigate(); + const { setIsLoading } = useLoading(LoadingKeys.SUBMIT_SWAP_TRANSACTION); + const currentAccount = useCurrentStacksAccount(); + const generateUnsignedTx = useGenerateStacksContractCallUnsignedTx(); + const signTx = useSignStacksTransaction(); + const { whenWallet } = useWalletType(); + + // Setting software to false until we revisit: + // https://github.com/leather-wallet/extension/issues/4750 + const isSponsoredByAlex = whenWallet({ + ledger: false, + software: false, + }); + + const { + alexSDK, + fetchToAmount, + createSwapAssetFromAlexCurrency, // TODO: change this + isFetchingExchangeRate, + onSetIsFetchingExchangeRate, + onSetSwapSubmissionData, + slippage, + supportedCurrencies, + swapSubmissionData, + } = useLNSwapSwap(); + + const broadcastAlexSwap = useAlexBroadcastSwap(alexSDK); + const broadcastStacksSwap = useStacksBroadcastSwap(); + + // this is where we set the swappable assets + 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' : defaultFeesMinValuesAsMoney[1].amount.toString(), + feeCurrency: values.feeCurrency, + feeType: values.feeType, + liquidityFee: new BigNumber(Number(lpFee)).dividedBy(oneHundredMillion).toNumber(), + nonce: values.nonce, + protocol: 'LNSWAP', + 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); + } + + 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(new BigNumber(1).minus(slippage)) + .dp(0) + .toString() + ); + + console.log('143 swapSubmissionData', swapSubmissionData); + 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'); + + try { + const signedTx = await signTx(unsignedTx); + if (!signedTx) + return logger.error('Attempted to generate raw tx, but signed tx is undefined'); + const txRaw = bytesToHex(signedTx.serialize()); + + return whenWallet({ + ledger: await broadcastStacksSwap(signedTx), + software: isSponsoredByAlex + ? await broadcastAlexSwap(txRaw) + : await broadcastStacksSwap(signedTx), + }); + } catch (error) {} + } + + const swapContextValue: SwapContext = { + fetchToAmount, + isFetchingExchangeRate, + isSendingMax, + onSetIsFetchingExchangeRate, + onSetIsSendingMax: value => setIsSendingMax(value), + onSubmitSwapForReview, + onSubmitSwap, + swappableAssetsFrom: migratePositiveBalancesToTop(swappableAssets), + swappableAssetsTo: swappableAssets, + swapSubmissionData, + }; + + return ( + + + + + + + + + ); +} From 662c9e36638b05fd4db04adc9244b64632d4285f Mon Sep 17 00:00:00 2001 From: pseudozach Date: Wed, 14 Feb 2024 01:02:18 -0800 Subject: [PATCH 3/5] add lnswap components use lnswap-sdk --- src/app/pages/swap/hooks/use-lnswap-swap.tsx | 110 ++++++++++++++++++ .../lnswap-swaps/swappable-currency.query.ts | 16 +++ 2 files changed, 126 insertions(+) create mode 100644 src/app/pages/swap/hooks/use-lnswap-swap.tsx create mode 100644 src/app/query/common/lnswap-swaps/swappable-currency.query.ts diff --git a/src/app/pages/swap/hooks/use-lnswap-swap.tsx b/src/app/pages/swap/hooks/use-lnswap-swap.tsx new file mode 100644 index 00000000000..1581bd918da --- /dev/null +++ b/src/app/pages/swap/hooks/use-lnswap-swap.tsx @@ -0,0 +1,110 @@ +import { useCallback, useState } from 'react'; +import { useAsync } from 'react-async-hook'; + +import { AlexSDK, Currency, TokenInfo } from 'lnswap-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 { pullContractIdFromIdentity } from '@app/common/utils'; +// TODO: change this +import { useSwappableCurrencyQuery } from '@app/query/common/lnswap-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 useLNSwapSwap() { + 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); + console.log('30.hooks/use-lnswap-swap.tsx', supportedCurrencies); + 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; + console.log('49.hooks/use-lnswap-swap.tsx', tokenInfo.id, currency); + const price = convertAmountToFractionalUnit(new BigNumber(prices[currency] ?? 0), 2); + const swapAsset = { + currency, + icon: tokenInfo.icon, + name: tokenInfo.name, + price: createMoney(price, 'USD'), + principal: 'pullContractIdFromIdentity(tokenInfo.contractAddress)', + }; + console.log('58.hooks/use-lnswap-swap.tsx', swapAsset); + if (currency === Currency.STX) { + return { + ...swapAsset, + balance: availableStxBalance, + displayName: 'Stacks', + }; + } + + const fungibleTokenBalance = + stacksFtAssetBalances.find(x => tokenInfo.contractAddress === x.asset.contractId) + ?.balance ?? createMoney(0, tokenInfo.name, tokenInfo.decimals); + console.log('70.hooks/use-lnswap-swap.tsx', fungibleTokenBalance); + return { + ...swapAsset, + balance: fungibleTokenBalance, + }; + }, + [availableStxBalance, prices, stacksFtAssetBalances] + ); + + async function fetchToAmount( + from: SwapAsset, + to: SwapAsset, + fromAmount: string + ): Promise { + console.log('84.fetchToAmount', from, to, fromAmount); + fromAmount = Math.floor(Number(fromAmount)).toString(); + const amount = new BigNumber(fromAmount).multipliedBy(oneHundredMillion).dp(0).toString(); + const amountAsBigInt = isNaN(Number(amount)) ? BigInt(0) : BigInt(amount); + console.log('86.fetchToAmount', from, to, amountAsBigInt); + try { + setIsFetchingExchangeRate(true); + const result = await alexSDK.getAmountTo(from.currency, amountAsBigInt, to.currency); + setIsFetchingExchangeRate(false); + console.log('91.setIsFetchingExchangeRate', result); + return new BigNumber(Number(result)).dividedBy(oneHundredMillion).toString(); + } catch (e) { + logger.error('Error fetching exchange rate from LNSwap', 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/query/common/lnswap-swaps/swappable-currency.query.ts b/src/app/query/common/lnswap-swaps/swappable-currency.query.ts new file mode 100644 index 00000000000..034d1178b98 --- /dev/null +++ b/src/app/query/common/lnswap-swaps/swappable-currency.query.ts @@ -0,0 +1,16 @@ +import { useQuery } from '@tanstack/react-query'; +import { AlexSDK } from 'lnswap-sdk'; + +export function useSwappableCurrencyQuery(alexSDK: AlexSDK) { + return useQuery( + ['lnswap-supported-swap-currencies'], + async () => alexSDK.fetchSwappableCurrency(), + { + refetchOnMount: false, + refetchOnReconnect: false, + refetchOnWindowFocus: false, + retryDelay: 1000 * 60, + staleTime: 1000 * 60 * 10, + } + ); +} From 1beb9a48613de47102de226e7642fded8d34c93f Mon Sep 17 00:00:00 2001 From: pseudozach Date: Wed, 14 Feb 2024 01:03:16 -0800 Subject: [PATCH 4/5] lnswap-sdk@0.0.1 --- package.json | 1 + yarn.lock | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/package.json b/package.json index 70ceedce586..4a20be37dec 100644 --- a/package.json +++ b/package.json @@ -200,6 +200,7 @@ "jsontokens": "4.0.1", "ledger-bitcoin": "0.2.3", "limiter": "2.1.0", + "lnswap-sdk": "^0.0.1", "lodash.get": "4.4.2", "lodash.uniqby": "4.7.0", "micro-packed": "0.3.2", diff --git a/yarn.lock b/yarn.lock index f80eb53952f..77365597ead 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9684,6 +9684,17 @@ clarity-codegen@^0.2.6: yargs "^17.7.2" yqueue "^1.0.1" +clarity-codegen@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/clarity-codegen/-/clarity-codegen-0.3.5.tgz#4c4a411478500164b597fa36f422078e0ef6cacc" + integrity sha512-tMYXP0lyZ/WViR2vRHCdv/vD+VLhzguawa+GyGRhwMTaYzBRzgyvaZf1qipEf8tIhd8wQGI+3e9KGaWC8TRcaA== + 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.5.1" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" @@ -14734,6 +14745,13 @@ linked-list@^0.1.0: resolved "https://registry.yarnpkg.com/linked-list/-/linked-list-0.1.0.tgz#798b0ff97d1b92a4fd08480f55aea4e9d49d37bf" integrity sha512-Zr4ovrd0ODzF3ut2TWZMdHIxb8iFdJc/P3QM4iCJdlxxGHXo69c9hGIHzLo8/FtuR9E6WUZc5irKhtPUgOKMAg== +lnswap-sdk@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/lnswap-sdk/-/lnswap-sdk-0.0.1.tgz#c7fa94e5c4a007e8da4e001fd8e8a806c84d4c93" + integrity sha512-9Qsy/1oBFJaCzu4WjH1pc6WNNWrer7i/Bg8SpAB8Lbb9py8dHt7X+djDAhAwpm0Pz4UhDNDua2UfnBor8ptf7w== + dependencies: + clarity-codegen "^0.3.5" + load-yaml-file@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/load-yaml-file/-/load-yaml-file-0.2.0.tgz#af854edaf2bea89346c07549122753c07372f64d" From 3d579749f2edd6dc2afbed743f4f930c8ed1a5fe Mon Sep 17 00:00:00 2001 From: pseudozach Date: Wed, 14 Feb 2024 01:03:50 -0800 Subject: [PATCH 5/5] activate lnswapSwapRoutes --- src/app/routes/app-routes.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/routes/app-routes.tsx b/src/app/routes/app-routes.tsx index d53359792b8..13b482df4d3 100644 --- a/src/app/routes/app-routes.tsx +++ b/src/app/routes/app-routes.tsx @@ -54,6 +54,7 @@ import { rpcRequestRoutes } from '@app/routes/rpc-routes'; import { settingsRoutes } from '@app/routes/settings-routes'; import { OnboardingGate } from './onboarding-gate'; +import { lnswapSwapRoutes } from '@app/pages/swap/lnswap-swap-container'; export function SuspenseLoadingSpinner() { return ; @@ -252,7 +253,8 @@ function useAppRoutes() { {ledgerBitcoinTxSigningRoutes} - {alexSwapRoutes} + {/* {alexSwapRoutes} */} + {lnswapSwapRoutes} {/* Catch-all route redirects to onboarding */} } />