diff --git a/@shared/api/types.ts b/@shared/api/types.ts index d340d23bbc..38e030b911 100644 --- a/@shared/api/types.ts +++ b/@shared/api/types.ts @@ -188,6 +188,14 @@ export interface AssetDomains { [code: string]: string; } +export interface SoroswapToken { + code: string; + contract: string; + decimals: number; + icon: string; + name: string; +} + export interface NativeToken { type: SdkAssetType; code: string; diff --git a/extension/INTEGRATING_SOROSWAP.MD b/extension/INTEGRATING_SOROSWAP.MD new file mode 100644 index 0000000000..0cfc2f16c5 --- /dev/null +++ b/extension/INTEGRATING_SOROSWAP.MD @@ -0,0 +1,22 @@ +# How to integrate with Soroswap + +Integrating with [Soroswap](https://app.soroswap.finance/) allows wallets to +swap between Soroban tokens using a smart contract. + +In this repo, you will find all of our logic for constructing a swap between +Soroban tokens at the path: /`src/popup/helpers/sorobanSwap.ts`. + +In this file, you will find 3 key methods that utilize `soroswap-router-sdk` and +`stellar-sdk`. Please see this file for a detailed breakdown of how each of the +below functions works. + +1. `getSoroswapTokens()`: This will retrieve the tokens that Soroswap supports + on each network. + +2. `soroswapGetBestPath()`: This function builds the path between your starting + asset and your destination asset. This will also tell you the conversion rate + between the 2 assets. This function will be used in the 2nd function below. + +3. `buildAndSimulateSoroswapTx()`: Once we have the path, we can construct a + Stellar transaction to send your starting asset and receive your destination + asset. diff --git a/extension/package.json b/extension/package.json index 37194273a1..44e326bf2c 100644 --- a/extension/package.json +++ b/extension/package.json @@ -79,6 +79,7 @@ "sass-loader": "^14.2.1", "semver": "^7.5.4", "ses": "^0.18.5", + "soroswap-router-sdk": "^1.2.8", "stellar-hd-wallet": "^0.0.10", "stellar-identicon-js": "^1.0.0", "stellar-sdk": "yarn:stellar-sdk@^12.1.0", @@ -111,4 +112,4 @@ "stellar-sdk>stellar-base>sodium-native": false } } -} \ No newline at end of file +} diff --git a/extension/public/static/manifest/v3.json b/extension/public/static/manifest/v3.json index fb4b556181..351778e451 100644 --- a/extension/public/static/manifest/v3.json +++ b/extension/public/static/manifest/v3.json @@ -11,18 +11,12 @@ }, "background": { "service_worker": "background.min.js", - "scripts": [ - "background.min.js" - ] + "scripts": ["background.min.js"] }, "content_scripts": [ { - "matches": [ - "" - ], - "js": [ - "contentScript.min.js" - ], + "matches": [""], + "js": ["contentScript.min.js"], "run_at": "document_start" } ], @@ -41,9 +35,6 @@ "48": "images/icon48.png", "128": "images/icon128.png" }, - "permissions": [ - "storage", - "alarms" - ], + "permissions": ["storage", "alarms"], "manifest_version": 3 -} \ No newline at end of file +} diff --git a/extension/src/popup/components/SubviewHeader/index.tsx b/extension/src/popup/components/SubviewHeader/index.tsx index 13c6198f80..fd16cb9a7f 100644 --- a/extension/src/popup/components/SubviewHeader/index.tsx +++ b/extension/src/popup/components/SubviewHeader/index.tsx @@ -7,7 +7,7 @@ import "./styles.scss"; interface SubviewHeaderProps { customBackAction?: () => void; customBackIcon?: React.ReactNode; - title: string; + title: string | React.ReactNode; subtitle?: React.ReactNode; hasBackButton?: boolean; rightButton?: React.ReactNode; diff --git a/extension/src/popup/components/account/AccountAssets/index.tsx b/extension/src/popup/components/account/AccountAssets/index.tsx index ba8a9445d4..cf446e9550 100644 --- a/extension/src/popup/components/account/AccountAssets/index.tsx +++ b/extension/src/popup/components/account/AccountAssets/index.tsx @@ -39,6 +39,7 @@ export const AssetIcon = ({ retryAssetIconFetch, isLPShare = false, isSorobanToken = false, + icon, }: { assetIcons: AssetIcons; code: string; @@ -46,6 +47,7 @@ export const AssetIcon = ({ retryAssetIconFetch?: (arg: { key: string; code: string }) => void; isLPShare?: boolean; isSorobanToken?: boolean; + icon?: string; }) => { /* We load asset icons in 2 ways: @@ -64,8 +66,13 @@ export const AssetIcon = ({ // For all non-XLM assets (assets where we need to fetch the icon from elsewhere), start by showing a loading state as there is work to do const [isLoading, setIsLoading] = useState(!isXlm); + const { soroswapTokens } = useSelector(transactionSubmissionSelector); + const canonicalAsset = assetIcons[getCanonicalFromAsset(code, issuerKey)]; - const imgSrc = hasError ? ImageMissingIcon : canonicalAsset || ""; + let imgSrc = hasError ? ImageMissingIcon : canonicalAsset || ""; + if (icon) { + imgSrc = icon; + } const _isSorobanToken = !isSorobanToken ? issuerKey && isSorobanIssuer(issuerKey) @@ -80,9 +87,17 @@ export const AssetIcon = ({ ); } - // Placeholder for Soroban tokens - if (_isSorobanToken) { - return ; + // Get icons for Soroban tokens + if (_isSorobanToken && !icon) { + const soroswapTokenDetail = soroswapTokens.find( + (token) => token.contract === issuerKey, + ); + // check to see if we have an icon from an external service, like Soroswap + if (soroswapTokenDetail?.icon) { + imgSrc = soroswapTokenDetail?.icon; + } else { + return ; + } } // If we're waiting on the icon lookup (Method 1), just return the loader until this re-renders with `assetIcons`. We can't do anything until we have it. @@ -93,7 +108,7 @@ export const AssetIcon = ({ } // if we have an asset path, start loading the path in an `` - return canonicalAsset || isXlm ? ( + return canonicalAsset || isXlm || imgSrc ? (
{ const { t } = useTranslation(); - const { assetIcons, assetSelect } = useSelector( + const { assetIcons, assetSelect, soroswapTokens } = useSelector( transactionSubmissionSelector, ); const isSorobanSuported = useSelector(settingsSorobanSupportedSelector); - const { networkUrl, networkPassphrase } = useSelector( - settingsNetworkDetailsSelector, - ); + const networkDetails = useSelector(settingsNetworkDetailsSelector); const [assetRows, setAssetRows] = useState([] as ManageAssetCurrency[]); const ManageAssetRowsWrapperRef = useRef(null); const [isLoading, setIsLoading] = useState(false); const isSwap = useIsSwap(); + const isSoroswapEnabled = useIsSoroswapEnabled(); const managingAssets = assetSelect.type === AssetSelectType.MANAGE; @@ -68,7 +68,8 @@ export const ChooseAsset = ({ balances }: ChooseAssetProps) => { contractId, } = sortedBalances[i]; - if (isSwap && "decimals" in sortedBalances[i]) { + // If we are in the swap flow and the asset has decimals (is a token), we skip it if Soroswap is not enabled + if ("decimals" in sortedBalances[i] && isSwap && !isSoroswapEnabled) { // eslint-disable-next-line continue; } @@ -81,8 +82,8 @@ export const ChooseAsset = ({ balances }: ChooseAssetProps) => { // eslint-disable-next-line no-await-in-loop domain = await getAssetDomain( issuer.key as string, - networkUrl, - networkPassphrase, + networkDetails.networkUrl, + networkDetails.networkPassphrase, ); } catch (e) { console.error(e); @@ -110,6 +111,31 @@ export const ChooseAsset = ({ balances }: ChooseAssetProps) => { } } + if (isSoroswapEnabled && isSwap && !assetSelect.isSource) { + soroswapTokens.forEach((token) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const canonical = getCanonicalFromAsset(token.code, token.contract); + const nativeContractDetails = + getNativeContractDetails(networkDetails); + + // if we have a balance for a token, it will have been handled above. + // This is designed to populate tokens available from Soroswap that the user does not already have + if ( + balances && + !balances[canonical] && + token.contract !== nativeContractDetails.contract + ) { + collection.push({ + code: token.code, + issuer: token.contract, + image: token.icon, + domain: "", + icon: token.icon, + }); + } + }); + } + setAssetRows(collection); setIsLoading(false); }; @@ -118,11 +144,13 @@ export const ChooseAsset = ({ balances }: ChooseAssetProps) => { }, [ assetIcons, balances, - networkUrl, managingAssets, isSorobanSuported, isSwap, - networkPassphrase, + isSoroswapEnabled, + assetSelect.isSource, + soroswapTokens, + networkDetails, ]); return ( @@ -152,24 +180,22 @@ export const ChooseAsset = ({ balances }: ChooseAssetProps) => {
- - {managingAssets && ( - <> -
- - - -
- - )} -
+ {managingAssets && ( + +
+ + + +
+
+ )} ); }; diff --git a/extension/src/popup/components/manageAssets/ManageAssetRows/index.tsx b/extension/src/popup/components/manageAssets/ManageAssetRows/index.tsx index a21c5d07e6..0477e5a12d 100644 --- a/extension/src/popup/components/manageAssets/ManageAssetRows/index.tsx +++ b/extension/src/popup/components/manageAssets/ManageAssetRows/index.tsx @@ -38,6 +38,7 @@ import "./styles.scss"; export type ManageAssetCurrency = StellarToml.Api.Currency & { domain: string; contract?: string; + icon?: string; }; export interface NewAssetFlags { diff --git a/extension/src/popup/components/manageAssets/SelectAssetRows/index.tsx b/extension/src/popup/components/manageAssets/SelectAssetRows/index.tsx index 1fc615f60c..668b1adfd1 100644 --- a/extension/src/popup/components/manageAssets/SelectAssetRows/index.tsx +++ b/extension/src/popup/components/manageAssets/SelectAssetRows/index.tsx @@ -7,16 +7,23 @@ import { transactionSubmissionSelector, saveAsset, saveDestinationAsset, + saveDestinationIcon, saveIsToken, AssetSelectType, + saveIsSoroswap, } from "popup/ducks/transactionSubmission"; import { AssetIcon } from "popup/components/account/AccountAssets"; import { ManageAssetCurrency } from "popup/components/manageAssets/ManageAssetRows"; -import { getCanonicalFromAsset, formatDomain } from "helpers/stellar"; +import { + getCanonicalFromAsset, + formatDomain, + getAssetFromCanonical, +} from "helpers/stellar"; import { getTokenBalance, isContractId } from "popup/helpers/soroban"; import { ScamAssetIcon } from "popup/components/account/ScamAssetIcon"; import { Balance, Balances, SorobanBalance } from "@shared/api/types"; import { formatAmount } from "popup/helpers/formatters"; +import { useIsSoroswapEnabled } from "popup/helpers/useIsSwap"; import "./styles.scss"; @@ -29,9 +36,12 @@ export const SelectAssetRows = ({ assetRows }: SelectAssetRowsProps) => { accountBalances: { balances = {} }, assetSelect, blockedDomains, + soroswapTokens, + transactionData, } = useSelector(transactionSubmissionSelector); const dispatch: AppDispatch = useDispatch(); const history = useHistory(); + const isSoroswapEnabled = useIsSoroswapEnabled(); const getAccountBalance = (canonical: string) => { if (!balances) { @@ -52,7 +62,7 @@ export const SelectAssetRows = ({ assetRows }: SelectAssetRowsProps) => { if (bal) { return getTokenBalance(bal); } - return ""; + return "0"; }; // hide balances for path pay dest asset @@ -63,55 +73,71 @@ export const SelectAssetRows = ({ assetRows }: SelectAssetRowsProps) => { return (
- {assetRows.map(({ code = "", domain, image = "", issuer = "" }) => { - const isScamAsset = !!blockedDomains.domains[domain]; - const isContract = isContractId(issuer); - const canonical = getCanonicalFromAsset(code, issuer); + {assetRows.map( + ({ code = "", domain, image = "", issuer = "", icon }) => { + const isScamAsset = !!blockedDomains.domains[domain]; + const isContract = isContractId(issuer); + const canonical = getCanonicalFromAsset(code, issuer); + let isSoroswap = false; + + if (isSoroswapEnabled) { + // check if either asset is a Soroswap token + const otherAsset = getAssetFromCanonical( + assetSelect.isSource + ? transactionData.destinationAsset + : transactionData.asset, + ); + isSoroswap = + !!soroswapTokens.find(({ contract }) => contract === issuer) || + !!soroswapTokens.find( + ({ contract }) => contract === otherAsset.issuer, + ); + } - return ( -
{ - if (assetSelect.isSource) { - dispatch(saveAsset(canonical)); - if (isContract) { - dispatch(saveIsToken(true)); + return ( +
{ + if (assetSelect.isSource) { + dispatch(saveAsset(canonical)); + dispatch(saveIsToken(isContract)); + history.goBack(); } else { - dispatch(saveIsToken(false)); + dispatch(saveDestinationAsset(canonical)); + dispatch(saveDestinationIcon(icon)); + history.goBack(); } - history.goBack(); - } else { - dispatch(saveDestinationAsset(canonical)); - history.goBack(); - } - }} - > - -
-
- {code} - -
-
- {formatDomain(domain)} + dispatch(saveIsSoroswap(isSoroswap)); + }} + > + +
+
+ {code} + +
+
+ {formatDomain(domain)} +
+ {!hideBalances && ( +
+ {isContract + ? getTokenBalanceFromCanonical(canonical) + : formatAmount(getAccountBalance(canonical))}{" "} + {code} +
+ )}
- {!hideBalances && ( -
- {isContract - ? getTokenBalanceFromCanonical(canonical) - : formatAmount(getAccountBalance(canonical))}{" "} - {code} -
- )} -
- ); - })} + ); + }, + )}
); diff --git a/extension/src/popup/components/sendPayment/SendAmount/AssetSelect/index.tsx b/extension/src/popup/components/sendPayment/SendAmount/AssetSelect/index.tsx index 34cfc0b9bf..aa531c5e57 100644 --- a/extension/src/popup/components/sendPayment/SendAmount/AssetSelect/index.tsx +++ b/extension/src/popup/components/sendPayment/SendAmount/AssetSelect/index.tsx @@ -101,11 +101,13 @@ export const PathPayAssetSelect = ({ assetCode, issuerKey, balance, + icon, }: { source: boolean; assetCode: string; issuerKey: string; balance: string; + icon: string; }) => { const dispatch = useDispatch(); const { assetIcons } = useSelector(transactionSubmissionSelector); @@ -147,6 +149,7 @@ export const PathPayAssetSelect = ({ assetIcons={assetIcons} code={assetCode} issuerKey={issuerKey} + icon={icon} /> { - await dispatch( - getBestPath({ - amount: formikAm, - sourceAsset, - destAsset, - networkDetails, - }), - ); + if (isSoroswap) { + const getContract = (formAsset: string) => + formAsset === "native" + ? getNativeContractDetails(networkDetails).contract + : formAsset.split(":")[1]; + + await dispatch( + getBestSoroswapPath({ + amount: formikAm, + sourceContract: getContract(formik.values.asset), + destContract: getContract(formik.values.destinationAsset), + networkDetails, + publicKey, + }), + ); + } else { + await dispatch( + getBestPath({ + amount: formikAm, + sourceAsset, + destAsset, + networkDetails, + }), + ); + } + setLoadingRate(false); }, 2000), [], @@ -325,6 +356,12 @@ export const SendAmount = ({ asset, ]); + useEffect(() => { + if (!soroswapTokens.length) { + dispatch(getSoroswapTokens()); + } + }, [isSwap, useIsSoroswapEnabled]); + const getAmountFontSize = () => { const length = formik.values.amount.length; if (length <= 9) { @@ -398,12 +435,28 @@ export const SendAmount = ({ )} + {isSwap ? "Swap" : "Send"} {parsedSourceAsset.code}{" "} + {isSoroswap ? ( + + on{" "} + + Soroswap + + + ) : null} + + } subtitle={ - <> +
{formatAmount(availBalance)}{" "} {parsedSourceAsset.code} {t("available")} - +
} hasBackButton={!isSwap} customBackAction={() => navigateTo(previous)} @@ -524,6 +577,7 @@ export const SendAmount = ({ assetCode={parsedSourceAsset.code} issuerKey={parsedSourceAsset.issuer} balance={formik.values.amount} + icon="" /> )} diff --git a/extension/src/popup/components/sendPayment/SendConfirm/TransactionDetails/index.tsx b/extension/src/popup/components/sendPayment/SendConfirm/TransactionDetails/index.tsx index 497faac572..fa2eac4ce6 100644 --- a/extension/src/popup/components/sendPayment/SendConfirm/TransactionDetails/index.tsx +++ b/extension/src/popup/components/sendPayment/SendConfirm/TransactionDetails/index.tsx @@ -7,6 +7,7 @@ import { Memo, Operation, TransactionBuilder, + Networks, } from "stellar-sdk"; import { Card, Loader, Icon, Button } from "@stellar/design-system"; import { useTranslation } from "react-i18next"; @@ -47,6 +48,7 @@ import { import { publicKeySelector, hardwareWalletTypeSelector, + addTokenId, } from "popup/ducks/accountServices"; import { navigateTo, openTab } from "popup/helpers/navigate"; import { useIsSwap } from "popup/helpers/useIsSwap"; @@ -206,6 +208,7 @@ export const TransactionDetails = ({ goBack }: { goBack: () => void }) => { destinationAmount, path, isToken, + isSoroswap, }, assetIcons, hardwareWalletData: { status: hwStatus }, @@ -268,7 +271,7 @@ export const TransactionDetails = ({ goBack }: { goBack: () => void }) => { dispatch(getBlockedAccounts()); }, [dispatch]); - const handleXferTransaction = async () => { + const handleSorobanTransaction = async () => { try { const res = await dispatch( signFreighterSorobanTransaction({ @@ -289,10 +292,20 @@ export const TransactionDetails = ({ goBack }: { goBack: () => void }) => { }), ); - if (submitFreighterTransaction.fulfilled.match(submitResp)) { + if (submitFreighterSorobanTransaction.fulfilled.match(submitResp)) { emitMetric(METRIC_NAMES.sendPaymentSuccess, { sourceAsset: sourceAsset.code, }); + + if (isSoroswap) { + await dispatch( + addTokenId({ + publicKey, + tokenId: destAsset.issuer, + network: networkDetails.network as Networks, + }), + ); + } } } } catch (e) { @@ -385,8 +398,8 @@ export const TransactionDetails = ({ goBack }: { goBack: () => void }) => { // handles signing and submitting const handleSend = async () => { - if (isToken) { - await handleXferTransaction(); + if (isToken || isSoroswap) { + await handleSorobanTransaction(); } else { await handlePaymentTransaction(); } diff --git a/extension/src/popup/components/sendPayment/SendSettings/Settings/index.tsx b/extension/src/popup/components/sendPayment/SendSettings/Settings/index.tsx index 90b28eb5b3..23baa6d491 100644 --- a/extension/src/popup/components/sendPayment/SendSettings/Settings/index.tsx +++ b/extension/src/popup/components/sendPayment/SendSettings/Settings/index.tsx @@ -3,6 +3,7 @@ import { useSelector, useDispatch } from "react-redux"; import { Formik, Form, Field, FieldProps } from "formik"; import { Icon, Textarea, Link, Button } from "@stellar/design-system"; import { useTranslation } from "react-i18next"; +import { captureException } from "@sentry/browser"; import { navigateTo } from "popup/helpers/navigate"; import { useNetworkFees } from "popup/helpers/useNetworkFees"; @@ -21,6 +22,7 @@ import { transactionSubmissionSelector, } from "popup/ducks/transactionSubmission"; import { simulateTokenPayment } from "popup/ducks/token-payment"; +import { buildAndSimulateSoroswapTx } from "popup/helpers/sorobanSwap"; import { InfoTooltip } from "popup/basics/InfoTooltip"; import { publicKeySelector } from "popup/ducks/accountServices"; @@ -43,12 +45,17 @@ export const Settings = ({ const { asset, amount, + decimals, destination, + destinationAmount, + destinationDecimals, transactionFee, transactionTimeout, memo, allowedSlippage, isToken, + isSoroswap, + path, } = useSelector(transactionDataSelector); const networkDetails = useSelector(settingsNetworkDetailsSelector); const isPathPayment = useSelector(isPathPaymentSelector); @@ -79,7 +86,7 @@ export const Settings = ({ // dont show memo for regular sends to Muxed, or for swaps const showMemo = !isSwap && !isMuxedAccount(destination); - const showSlippage = isPathPayment || isSwap; + const showSlippage = (isPathPayment || isSwap) && !isSoroswap; async function goToReview() { if (isToken) { @@ -120,6 +127,32 @@ export const Settings = ({ } return; } + + if (isSoroswap) { + let simulatedTx; + try { + simulatedTx = await buildAndSimulateSoroswapTx({ + networkDetails, + publicKey, + amountIn: amount, + amountInDecimals: decimals, + amountOut: destinationAmount, + amountOutDecimals: destinationDecimals, + memo, + transactionFee, + path, + }); + } catch (e) { + console.error(e); + captureException( + `Unable to simulate soroswap tx ${JSON.stringify(path)}`, + ); + return; + } + + dispatch(saveSimulation(simulatedTx)); + navigateTo(next); + } navigateTo(next); } diff --git a/extension/src/popup/components/sendPayment/styles.scss b/extension/src/popup/components/sendPayment/styles.scss index 27e76c7d19..4115ad5695 100644 --- a/extension/src/popup/components/sendPayment/styles.scss +++ b/extension/src/popup/components/sendPayment/styles.scss @@ -9,6 +9,10 @@ flex-direction: column; height: 100%; + &__subtitle { + text-align: center; + } + &__content { display: flex; flex-direction: column; diff --git a/extension/src/popup/components/signTransaction/Operations/KeyVal/index.tsx b/extension/src/popup/components/signTransaction/Operations/KeyVal/index.tsx index fc9bd19630..cfe531b471 100644 --- a/extension/src/popup/components/signTransaction/Operations/KeyVal/index.tsx +++ b/extension/src/popup/components/signTransaction/Operations/KeyVal/index.tsx @@ -1,7 +1,6 @@ import React from "react"; import { useSelector } from "react-redux"; import { useTranslation } from "react-i18next"; -import * as Sentry from "@sentry/browser"; import { Asset, Claimant, @@ -412,9 +411,6 @@ export const KeyValueInvokeHostFnArgs = ({ setArgNames(argNamesPositional); setLoading(false); } catch (error) { - Sentry.captureException( - `Failed to get contract spec for ${contractId}`, - ); setLoading(false); } } diff --git a/extension/src/popup/components/signTransaction/Operations/index.tsx b/extension/src/popup/components/signTransaction/Operations/index.tsx index 7fe4dbcb56..f4dbd291e8 100644 --- a/extension/src/popup/components/signTransaction/Operations/index.tsx +++ b/extension/src/popup/components/signTransaction/Operations/index.tsx @@ -322,7 +322,7 @@ export const Operations = ({ <> {signer && } {inflationDest && ( - @@ -519,7 +519,7 @@ export const Operations = ({ const { trustor, asset, flags } = op; return ( <> - diff --git a/extension/src/popup/ducks/transactionSubmission.ts b/extension/src/popup/ducks/transactionSubmission.ts index 052c8b4ab3..121bf8d5d7 100644 --- a/extension/src/popup/ducks/transactionSubmission.ts +++ b/extension/src/popup/ducks/transactionSubmission.ts @@ -34,6 +34,7 @@ import { ActionStatus, BlockedAccount, BalanceToMigrate, + SoroswapToken, } from "@shared/api/types"; import { NETWORKS, NetworkDetails } from "@shared/constants/stellar"; @@ -46,6 +47,10 @@ import { MetricsData, emitMetric } from "helpers/metrics"; import { METRIC_NAMES } from "popup/constants/metricsNames"; import { INDEXER_URL } from "@shared/constants/mercury"; import { horizonGetBestPath } from "popup/helpers/horizonGetBestPath"; +import { + soroswapGetBestPath, + getSoroswapTokens as getSoroswapTokensService, +} from "popup/helpers/sorobanSwap"; import { hardwareSign } from "popup/helpers/hardwareConnect"; export const signFreighterTransaction = createAsyncThunk< @@ -424,6 +429,23 @@ export const getAssetDomains = createAsyncThunk< }) => getAssetDomainsService({ balances, networkDetails }), ); +export const getSoroswapTokens = createAsyncThunk< + SoroswapToken[], + undefined, + { rejectValue: ErrorMessage } +>("getSoroswapTokens", async (_, thunkApi) => { + let tokenData = { assets: [] as SoroswapToken[] }; + + try { + tokenData = await getSoroswapTokensService(); + } catch (e) { + const message = e instanceof Error ? e.message : JSON.stringify(e); + return thunkApi.rejectWithValue({ errorMessage: message }); + } + + return tokenData.assets; +}); + // returns the full record so can save the best path and its rate export const getBestPath = createAsyncThunk< Horizon.ServerApi.PaymentPathRecord, @@ -453,6 +475,45 @@ export const getBestPath = createAsyncThunk< }, ); +export const getBestSoroswapPath = createAsyncThunk< + { + amountIn?: string; + amountOutMin?: string; + amountInDecimals: number; + amountOutDecimals: number; + path: string[]; + } | null, + { + amount: string; + sourceContract: string; + destContract: string; + networkDetails: NetworkDetails; + publicKey: string; + }, + { rejectValue: ErrorMessage } +>( + "getBestSoroswapPath", + async ( + { amount, sourceContract, destContract, networkDetails, publicKey }, + thunkApi, + ) => { + try { + return await soroswapGetBestPath({ + amount, + sourceContract, + destContract, + networkDetails, + publicKey, + }); + } catch (e) { + const message = e instanceof Error ? e.message : JSON.stringify(e); + return thunkApi.rejectWithValue({ + errorMessage: message, + }); + } + }, +); + export const getBlockedDomains = createAsyncThunk< BlockedDomains, undefined, @@ -487,18 +548,22 @@ export enum ShowOverlayStatus { interface TransactionData { amount: string; asset: string; + decimals?: number; destination: string; federationAddress: string; transactionFee: string; transactionTimeout: number; memo: string; destinationAsset: string; + destinationDecimals?: number; destinationAmount: string; + destinationIcon: string; path: string[]; allowedSlippage: string; isToken: boolean; isMergeSelected: boolean; balancesToMigrate: BalanceToMigrate[]; + isSoroswap: boolean; } interface HardwareWalletData { @@ -532,6 +597,7 @@ interface InitialState { destinationBalances: AccountBalancesInterface; assetIcons: AssetIcons; assetDomains: AssetDomains; + soroswapTokens: SoroswapToken[]; assetSelect: { type: AssetSelectType; isSource: boolean; @@ -558,11 +624,13 @@ export const initialState: InitialState = { memo: "", destinationAsset: "", destinationAmount: "", + destinationIcon: "", path: [], allowedSlippage: "1", isToken: false, isMergeSelected: false, balancesToMigrate: [] as BalanceToMigrate[], + isSoroswap: false, }, transactionSimulation: { response: null, @@ -587,6 +655,7 @@ export const initialState: InitialState = { }, assetIcons: {}, assetDomains: {}, + soroswapTokens: [], assetSelect: { type: AssetSelectType.MANAGE, isSource: true, @@ -636,6 +705,12 @@ const transactionSubmissionSlice = createSlice({ saveDestinationAsset: (state, action) => { state.transactionData.destinationAsset = action.payload; }, + saveDestinationIcon: (state, action) => { + state.transactionData.destinationIcon = action.payload; + }, + saveIsSoroswap: (state, action) => { + state.transactionData.isSoroswap = action.payload; + }, saveAllowedSlippage: (state, action) => { state.transactionData.allowedSlippage = action.payload; }, @@ -723,6 +798,11 @@ const transactionSubmissionSlice = createSlice({ state.transactionData.destinationAmount = initialState.transactionData.destinationAmount; }); + builder.addCase(getBestSoroswapPath.rejected, (state) => { + state.transactionData.path = initialState.transactionData.path; + state.transactionData.destinationAmount = + initialState.transactionData.destinationAmount; + }); builder.addCase(getAccountBalances.pending, (state) => { state.accountBalanceStatus = ActionStatus.PENDING; state.accountBalances = initialState.accountBalances; @@ -755,6 +835,14 @@ const transactionSubmissionSlice = createSlice({ assetDomains, }; }); + builder.addCase(getSoroswapTokens.fulfilled, (state, action) => { + const soroswapTokens = action.payload || {}; + + return { + ...state, + soroswapTokens, + }; + }); builder.addCase(getBestPath.fulfilled, (state, action) => { if (!action.payload) { state.transactionData.path = []; @@ -776,6 +864,20 @@ const transactionSubmissionSlice = createSlice({ state.transactionData.destinationAmount = action.payload.destination_amount; }); + builder.addCase(getBestSoroswapPath.fulfilled, (state, action) => { + if (!action.payload) { + state.transactionData.path = []; + state.transactionData.destinationAmount = ""; + return; + } + + state.transactionData.path = action.payload.path; + state.transactionData.destinationAmount = + action.payload.amountOutMin || ""; + state.transactionData.decimals = action.payload.amountInDecimals; + state.transactionData.destinationDecimals = + action.payload.amountOutDecimals; + }); builder.addCase(getBlockedDomains.fulfilled, (state, action) => { state.blockedDomains.domains = action.payload; }); @@ -798,6 +900,8 @@ export const { saveTransactionTimeout, saveMemo, saveDestinationAsset, + saveDestinationIcon, + saveIsSoroswap, saveAllowedSlippage, saveIsToken, saveSimulation, diff --git a/extension/src/popup/helpers/getAssetDomain.ts b/extension/src/popup/helpers/getAssetDomain.ts index a74eeea0a5..20f22031e4 100644 --- a/extension/src/popup/helpers/getAssetDomain.ts +++ b/extension/src/popup/helpers/getAssetDomain.ts @@ -1,3 +1,4 @@ +import { StrKey } from "stellar-sdk"; import { stellarSdkServer } from "@shared/api/helpers/stellarSdkServer"; export const getAssetDomain = async ( @@ -6,7 +7,11 @@ export const getAssetDomain = async ( networkPassphrase: string, ) => { const server = stellarSdkServer(networkUrl, networkPassphrase); - const acct = await server.loadAccount(issuerKey); + if (StrKey.isValidEd25519PublicKey(issuerKey)) { + const acct = await server.loadAccount(issuerKey); - return acct.home_domain || ""; + return acct.home_domain || ""; + } + + return ""; }; diff --git a/extension/src/popup/helpers/sorobanSwap.ts b/extension/src/popup/helpers/sorobanSwap.ts new file mode 100644 index 0000000000..cc88804fa3 --- /dev/null +++ b/extension/src/popup/helpers/sorobanSwap.ts @@ -0,0 +1,294 @@ +import { xdr } from "stellar-sdk"; +import { + Router, + Token, + CurrencyAmount, + TradeType, + Networks, + Protocols, +} from "soroswap-router-sdk"; +import BigNumber from "bignumber.js"; + +import { NetworkDetails } from "@shared/constants/stellar"; +import { getSdk } from "@shared/helpers/stellar"; +import { stellarSdkServer } from "@shared/api/helpers/stellarSdkServer"; +import { getTokenDetails } from "@shared/api/internal"; +import { SoroswapToken } from "@shared/api/types"; +import { buildSorobanServer } from "@shared/helpers/soroban/server"; +import { isTestnet, xlmToStroop } from "helpers/stellar"; +import { parseTokenAmount, formatTokenAmount } from "popup/helpers/soroban"; + +/* + * getSoroswapTokens + * Get the list of tokens that Soroswap supports for swapping. + * This is useful to display in the UI to show the user what tokens they can swap between. + * Returns just the list you'll need to use for Testnet. + */ +export const getSoroswapTokens = async (): Promise<{ + assets: SoroswapToken[]; +}> => { + const res = await fetch(new URL("https://api.soroswap.finance/api/tokens")); + + const data = await res.json(); + + return data.find((d: { network: string }) => d.network === "testnet"); +}; + +interface SoroswapGetBestPathParams { + amount: string; + sourceContract: string; + destContract: string; + networkDetails: NetworkDetails; + publicKey: string; +} + +/* + * soroswapGetBestPath + * Given 2 assets, constructs a path to swap between them. + * Returns the path for a swap as well as the `amountIn` and `amountOutMin` values (the conversion rate). + */ +export const soroswapGetBestPath = async ({ + amount, + sourceContract, + destContract, + networkDetails, + publicKey, +}: SoroswapGetBestPathParams) => { + // For Freighter's purposes, we only support Testnet. Therefore, we error if this is called by another network + if (!isTestnet(networkDetails)) { + throw Error("Network not supported"); + } + + // We can default to Testnet as we only support Testnet, but for your purposes, you may configure this to any network you'd like. + const network = Networks.TESTNET; + + // Construct the details for the source and destination tokens + + // In Freighter, we have a helper method that fetches the token details for a given contract ID. + // Our helper simulates tx's on the given contract ID to get read-only information about each token. + // The important information we're trying to retrieve is each token's `decimals`. You can retrieve this information however you'd like. + const sourceTokenDetails = await getTokenDetails({ + contractId: sourceContract, + networkDetails, + publicKey, + }); + const destTokenDetails = await getTokenDetails({ + contractId: destContract, + networkDetails, + publicKey, + }); + + if (!sourceTokenDetails || !destTokenDetails) { + throw Error("Source token not found"); + } + + // Here we start interacting with `soroswap-router-sdk` + // For each token, we create a `Token` object with the token's network, contract ID, and the decimals we retrieved. + const sourceToken = new Token( + network, + sourceContract, + sourceTokenDetails.decimals, + ); + + const destToken = new Token(network, destContract, destTokenDetails.decimals); + + // The `Router` is used to find the best path for a swap between two tokens. + const router = new Router({ + getPairsFns: [ + { + protocol: Protocols.SOROSWAP, + fn: async () => { + const res = await fetch( + // this endpoint is used to get the pairs for Testnet which `Router` will used to determine conversion rate + new URL( + "https://info.soroswap.finance/api/pairs/plain?network=TESTNET", + ), + ); + + const data = await res.json(); + + return data; + }, + }, + ], + pairsCacheInSeconds: 60, + protocols: [Protocols.SOROSWAP], + network, + maxHops: 5, + }); + + // Now that we have `Router` setup, we can tell it to find the best path between our source and destination tokens. + + // When determining the best path, we need to format our source amount because in the UI, we use a more human-readable format. + // But here, we need to pass an argument using the token's decimals. (For ex: 1 XLM will become 10000000 because the `decimals` value is 7). + // Freighter has a helper method called `parseTokenAmount`, to do this + const parsedAmount = parseTokenAmount(amount, sourceTokenDetails.decimals); + + // We then create a `CurrencyAmount` object using `soroswap-router-sdk` with the source token and this parsed amount. + const currencyAmount = CurrencyAmount.fromRawAmount( + sourceToken, + parsedAmount.toNumber(), + ); + const quoteCurrency = destToken; + + // Now we can generate the path between the amount of source token and the destination token. + const route = await router.route( + currencyAmount, + quoteCurrency, + TradeType.EXACT_INPUT, + ); + + if (route?.trade) { + // The path is an array of strings that represent the contract IDs of the tokens in the path. This may not be useful for the user, but we'll need it in the next step. + // The amounIn and amountOutMin are useful for the UI: it will show the user what they're putting and what they expect to receive for it. + // We're removing the trailing decimals using `formatTokenAmount` before returning just to make it a little more readbale for the user. + return { + amountIn: formatTokenAmount( + new BigNumber(route.trade?.amountIn || ""), + sourceTokenDetails.decimals, + ).toString(), + amountInDecimals: sourceTokenDetails.decimals, + amountOutMin: formatTokenAmount( + new BigNumber(route.trade?.amountOutMin || ""), + destTokenDetails.decimals, + ).toString(), + amountOutDecimals: destTokenDetails.decimals, + path: route.trade?.path, + }; + } + + return null; +}; + +// After constructing the path and showing the user their conversion rate, we'll ask them to confirm the swap. +// From there, we can use the `buildAndSimulateSoroswapTx` method to simulate the swap and build the transaction. + +interface BuildAndSimulateSoroswapTxParams { + amountIn: string; + amountInDecimals?: number; + amountOut: string; + amountOutDecimals?: number; + path: string[]; + networkDetails: NetworkDetails; + publicKey: string; + memo: string; + transactionFee: string; +} + +/* + * buildAndSimulateSoroswapTx + * Given our 2 assets and the path between them, we can build and simulate our transaction to confirm it will behave as expected when submitted to the network. + * Returns the simulation response as well as the transaction that will be submitted to the network. + */ +export const buildAndSimulateSoroswapTx = async ({ + amountIn, + amountInDecimals = 7, + amountOut, + amountOutDecimals = 7, + path, + networkDetails, + publicKey, + memo, + transactionFee, +}: BuildAndSimulateSoroswapTxParams) => { + // This is some custom logic specific just to Freighter that we use to utilize the stellar-sdk. You can ignore this. + // Anytime you see `Sdk`, it's just a reference to the stellar-sdk. + // For example: import `Sdk` from "stellar-sdk"; + const Sdk = getSdk(networkDetails.networkPassphrase); + const server = stellarSdkServer( + networkDetails.networkUrl, + networkDetails.networkPassphrase, + ); + + /* + import Sdk from "soroban-sdk"; + const sorobanServer = new Sdk.SorobanRpc.Server(serverUrl); + + is equivalent to the code below: + */ + const sorobanServer = buildSorobanServer( + networkDetails.sorobanRpcUrl || "", + networkDetails.networkPassphrase, + ); + + // Load the user's account + const account = await server.loadAccount(publicKey); + + // This endpoint will return the contract address for the router on Testnet, which will be needed later + const routerRes = await fetch( + new URL("https://api.soroswap.finance/api/testnet/router"), + ); + const routerData = await routerRes.json(); + const routerAddress: string = routerData.address; + + // Just like in other stellar-sdk flows, we construct a Transaction using TransactionBuilder + // More info here: https://developers.stellar.org/docs/smart-contracts/guides/transactions + const tx = new Sdk.TransactionBuilder(account, { + fee: xlmToStroop(transactionFee).toFixed(), + timebounds: { minTime: 0, maxTime: 0 }, + networkPassphrase: networkDetails.networkPassphrase, + }); + + // Similar to the `soroswapGetBestPath` method, we again need to format our amounts using the token's decimals. + // .i.e, going from the human-readable 1 XLM to 10000000 + const parsedAmountIn = parseTokenAmount( + amountIn, + amountInDecimals, + ).toNumber(); + const parsedAmountOut = parseTokenAmount( + amountOut, + amountOutDecimals, + ).toNumber(); + + // Now we'll utilize the `path` we generated in `soroswapGetBestPath`. + // The path we generated before was just an array of strings. Here we'll build an array of Address objects + const mappedPath = path.map((address) => new Sdk.Address(address)); + + // We'll use a helper method from stellar-sdk to generate the `SCVal` for each of the parameters of the swap + const swapParams: xdr.ScVal[] = [ + // the amount the user is sending + Sdk.nativeToScVal(parsedAmountIn, { type: "i128" }), + // the amount the user expects to receive + Sdk.nativeToScVal(parsedAmountOut, { type: "i128" }), + // the path between the source and destination tokens + Sdk.nativeToScVal(mappedPath), + // the user's public key + new Sdk.Address(publicKey).toScVal(), + // the deadline for the swap + Sdk.nativeToScVal(Date.now() + 3600000, { type: "u64" }), + ]; + + // We then create a contract instance using the router address + const contractInstance = new Sdk.Contract(routerAddress); + // And create a contract operation to swap the tokens using the `swap_exact_tokens_for_tokens` method + const contractOperation = contractInstance.call( + "swap_exact_tokens_for_tokens", + ...swapParams, + ); + + // We add the contract operation to the transaction + tx.addOperation(contractOperation); + + // And, if applicable, add a memo to the transaction + if (memo) { + tx.addMemo(Sdk.Memo.text(memo)); + } + const builtTx = tx.build(); + + // Now we can simulate and see if we have any issues + const simulationResponse = await sorobanServer.simulateTransaction(builtTx); + + // If the simulation response is valid, we can prepare the transaction to be submitted to the network + // This is the transaction the user will sign and then submit to complete the swap + const preparedTransaction = Sdk.SorobanRpc.assembleTransaction( + builtTx, + simulationResponse, + ) + .build() + .toXDR(); + + return { + simulationResponse, + preparedTransaction, + }; +}; diff --git a/extension/src/popup/helpers/useIsSwap.ts b/extension/src/popup/helpers/useIsSwap.ts index 1ba6acd777..a128fd86e7 100644 --- a/extension/src/popup/helpers/useIsSwap.ts +++ b/extension/src/popup/helpers/useIsSwap.ts @@ -1,4 +1,8 @@ import { useLocation } from "react-router-dom"; +import { useSelector } from "react-redux"; + +import { isTestnet } from "helpers/stellar"; +import { settingsNetworkDetailsSelector } from "popup/ducks/settings"; export const useIsSwap = () => { const location = useLocation(); @@ -7,3 +11,9 @@ export const useIsSwap = () => { location.search.includes("swap=true") : false; }; + +export const useIsSoroswapEnabled = () => { + const networkDetails = useSelector(settingsNetworkDetailsSelector); + + return isTestnet(networkDetails); +}; diff --git a/extension/src/popup/views/Account/index.tsx b/extension/src/popup/views/Account/index.tsx index 8ba8b80591..7d929a8702 100644 --- a/extension/src/popup/views/Account/index.tsx +++ b/extension/src/popup/views/Account/index.tsx @@ -38,6 +38,7 @@ import { AssetSelectType, getBlockedDomains, getAccountBalances, + getSoroswapTokens, } from "popup/ducks/transactionSubmission"; import { ROUTES } from "popup/constants/routes"; import { @@ -47,6 +48,7 @@ import { } from "popup/helpers/account"; import { truncatedPublicKey } from "helpers/stellar"; import { navigateTo } from "popup/helpers/navigate"; +import { useIsSoroswapEnabled } from "popup/helpers/useIsSwap"; import { AccountAssets } from "popup/components/account/AccountAssets"; import { AccountHeader } from "popup/components/account/AccountHeader"; import { AssetDetail } from "popup/components/account/AssetDetail"; @@ -82,6 +84,7 @@ export const Account = () => { const [assetOperations, setAssetOperations] = useState({} as AssetOperations); const [selectedAsset, setSelectedAsset] = useState(""); const [isLoading, setLoading] = useState(true); + const isSoroswapEnabled = useIsSoroswapEnabled(); const { balances, isFunded, error } = accountBalances; @@ -108,10 +111,14 @@ export const Account = () => { return; } + if (isSoroswapEnabled) { + dispatch(getSoroswapTokens()); + } + setSortedBalances(sortBalances(balances)); dispatch(getAssetIcons({ balances, networkDetails })); dispatch(getAssetDomains({ balances, networkDetails })); - }, [balances, networkDetails, dispatch]); + }, [balances, networkDetails, dispatch, isSoroswapEnabled]); useEffect(() => { if (!balances) { diff --git a/extension/src/popup/views/ReviewAuth/index.tsx b/extension/src/popup/views/ReviewAuth/index.tsx index 6aadea80bf..be1a6a7e8a 100644 --- a/extension/src/popup/views/ReviewAuth/index.tsx +++ b/extension/src/popup/views/ReviewAuth/index.tsx @@ -2,7 +2,6 @@ import React, { useState } from "react"; import { useLocation } from "react-router-dom"; import { captureException } from "@sentry/browser"; import BigNumber from "bignumber.js"; -import * as Sentry from "@sentry/browser"; import { MemoType, Operation, @@ -393,9 +392,6 @@ const AuthDetail = ({ setCheckingTransfers(false); } catch (error) { console.error(error); - Sentry.captureException( - `Failed to check spec for invocation - ${rootJsonDepKey}`, - ); setCheckingTransfers(false); } } diff --git a/extension/src/popup/views/SignTransaction/index.tsx b/extension/src/popup/views/SignTransaction/index.tsx index aa65d3bd7e..8e0e68d670 100644 --- a/extension/src/popup/views/SignTransaction/index.tsx +++ b/extension/src/popup/views/SignTransaction/index.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useState } from "react"; import { useLocation } from "react-router-dom"; -import { useSelector } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { useTranslation, Trans } from "react-i18next"; import { Button, Icon, Notification } from "@stellar/design-system"; import { @@ -13,13 +13,19 @@ import { Operation, } from "stellar-sdk"; +import { ActionStatus } from "@shared/api/types"; import { signTransaction, rejectTransaction } from "popup/ducks/access"; import { settingsNetworkDetailsSelector, settingsExperimentalModeSelector, } from "popup/ducks/settings"; -import { ShowOverlayStatus } from "popup/ducks/transactionSubmission"; +import { + ShowOverlayStatus, + getAccountBalances, + resetAccountBalanceStatus, + transactionSubmissionSelector, +} from "popup/ducks/transactionSubmission"; import { OPERATION_TYPES, TRANSACTION_WARNING } from "constants/transaction"; @@ -29,6 +35,7 @@ import { getTransactionInfo, isFederationAddress, isMuxedAccount, + stroopToXlm, truncatedPublicKey, } from "helpers/stellar"; import { decodeMemo } from "popup/helpers/parseTransaction"; @@ -48,6 +55,7 @@ import { import { HardwareSign } from "popup/components/hardwareConnect/HardwareSign"; import { KeyIdenticon } from "popup/components/identicons/KeyIdenticon"; import { SlideupModal } from "popup/components/SlideupModal"; +import { Loading } from "popup/components/Loading"; import { VerifyAccount } from "popup/views/VerifyAccount"; import { Tabs } from "popup/components/Tabs"; @@ -60,15 +68,20 @@ import "./styles.scss"; export const SignTransaction = () => { const location = useLocation(); const { t } = useTranslation(); + const dispatch = useDispatch(); const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [hasAcceptedInsufficientFee, setHasAcceptedInsufficientFee] = + useState(false); + const { accountBalances, accountBalanceStatus } = useSelector( + transactionSubmissionSelector, + ); const isExperimentalModeEnabled = useSelector( settingsExperimentalModeSelector, ); - const { networkName, networkPassphrase } = useSelector( - settingsNetworkDetailsSelector, - ); + const networkDetails = useSelector(settingsNetworkDetailsSelector); + const { networkName, networkPassphrase } = networkDetails; const tx = getTransactionInfo(location.search); @@ -174,6 +187,20 @@ export const SignTransaction = () => { } }, [isMemoRequired, isMalicious, isUnsafe]); + useEffect(() => { + if (currentAccount.publicKey) { + dispatch( + getAccountBalances({ + publicKey: currentAccount.publicKey, + networkDetails, + }), + ); + } + return () => { + dispatch(resetAccountBalanceStatus()); + }; + }, [currentAccount.publicKey, dispatch, networkDetails]); + const isSubmitDisabled = isMemoRequired || isMalicious; if (_networkPassphrase !== networkPassphrase) { @@ -212,6 +239,42 @@ export const SignTransaction = () => { ); } + const hasLoadedBalances = + accountBalanceStatus !== ActionStatus.PENDING && + accountBalanceStatus !== ActionStatus.IDLE; + + if (!hasLoadedBalances) { + return ; + } + + const hasBalance = + hasLoadedBalances && accountBalanceStatus !== ActionStatus.ERROR; + const hasEnoughXlm = accountBalances.balances?.native.available.gt( + stroopToXlm(_fee as string), + ); + if ( + hasBalance && + currentAccount.publicKey && + !hasEnoughXlm && + !hasAcceptedInsufficientFee + ) { + return ( + setHasAcceptedInsufficientFee(true)} + isActive + variant={WarningMessageVariant.warning} + header={t("INSUFFICIENT FUNDS FOR FEE")} + > +

+ + Your available XLM balance is not enough to pay for the transaction + fee. + +

+
+ ); + } + function renderTab(tab: string) { function renderTabBody() { const _tx = transaction as Transaction, Operation[]>; diff --git a/extension/src/popup/views/__tests__/SignTransaction.test.tsx b/extension/src/popup/views/__tests__/SignTransaction.test.tsx index 4c40187c7c..5e29787ed7 100644 --- a/extension/src/popup/views/__tests__/SignTransaction.test.tsx +++ b/extension/src/popup/views/__tests__/SignTransaction.test.tsx @@ -13,12 +13,17 @@ import { import * as Stellar from "helpers/stellar"; import { getTokenInvocationArgs } from "popup/helpers/soroban"; +import * as ApiInternal from "@shared/api/internal"; import { SignTransaction } from "../SignTransaction"; -import { Wrapper } from "../../__testHelpers__"; +import { Wrapper, mockBalances, mockAccounts } from "../../__testHelpers__"; jest.mock("stellar-identicon-js"); +jest + .spyOn(ApiInternal, "getAccountIndexerBalances") + .mockImplementation(() => Promise.resolve(mockBalances)); + const defaultSettingsState = { networkDetails: { isTestnet: false, @@ -43,6 +48,7 @@ const mockTransactionInfo = { }, }, ], + _fee: 0.001, }, transactionXdr: "", domain: "", @@ -68,6 +74,7 @@ describe("SignTransactions", () => { transaction: { _networkPassphrase: Networks.FUTURENET, _operations: [op], + _fee: 0.001, }, transactionXdr: xdr, accountToSign: "", @@ -114,6 +121,7 @@ describe("SignTransactions", () => { ...mockTransactionInfo, transactionXdr: transactions.sorobanTransfer, transaction: { + ...mockTransactionInfo.transaction, _networkPassphrase: Networks.FUTURENET, _operations: [op], }, @@ -122,6 +130,10 @@ describe("SignTransactions", () => { render( { ...mockTransactionInfo, transactionXdr: transactions.classic, transaction: { + ...mockTransactionInfo.transaction, _networkPassphrase: Networks.TESTNET, _operations: [op], }, @@ -156,6 +169,10 @@ describe("SignTransactions", () => { render( { ...mockTransactionInfo, transactionXdr: transactions.sorobanTransfer, transaction: { + ...mockTransactionInfo.transaction, _networkPassphrase: Networks.FUTURENET, _operations: [op], }, @@ -193,6 +211,10 @@ describe("SignTransactions", () => { render( { ...mockTransactionInfo, transactionXdr: transactions.sorobanMint, transaction: { + ...mockTransactionInfo.transaction, _networkPassphrase: Networks.FUTURENET, _operations: [op], }, @@ -251,6 +274,10 @@ describe("SignTransactions", () => { render( { it("memo: render memo text", async () => { const transaction = TransactionBuilder.fromXDR( MEMO_TXN_TEXT, - Networks.TESTNET, + Networks.FUTURENET, ) as Transaction, Operation[]>; const op = transaction.operations[0]; jest.spyOn(Stellar, "getTransactionInfo").mockImplementation(() => ({ @@ -325,6 +352,10 @@ describe("SignTransactions", () => { render( { , ); + await waitFor(() => screen.getByTestId("SignTransaction")); expect(screen.getByTestId("MemoBlock")).toHaveTextContent( "text memo (MEMO_TEXT)", ); @@ -357,6 +389,10 @@ describe("SignTransactions", () => { render( { , ); - + await waitFor(() => screen.getByTestId("SignTransaction")); expect(screen.getByTestId("MemoBlock")).toHaveTextContent( "123456 (MEMO_ID)", ); @@ -389,6 +425,10 @@ describe("SignTransactions", () => { render( { , ); + await waitFor(() => screen.getByTestId("SignTransaction")); expect(screen.getByTestId("MemoBlock")).toHaveTextContent( "e98869bba8bce08c10b78406202127f3888c25454cd37b02600862452751f526 (MEMO_HASH)", ); @@ -421,6 +462,10 @@ describe("SignTransactions", () => { render( { , ); + await waitFor(() => screen.getByTestId("SignTransaction")); expect(screen.getByTestId("MemoBlock")).toHaveTextContent( "e98869bba8bce08c10b78406202127f3888c25454cd37b02600862452751f526 (MEMO_RETURN)", ); diff --git a/package.json b/package.json index 75894646e5..319d6e6ae3 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "eslint-plugin-react-hooks": "4.6.2", "eslint-webpack-plugin": "^4.2.0", "glob": "^9.3.2", - "got": "11.8.2", + "got": "11.8.5", "husky": "^8.0.0", "isomorphic-unfetch": "^3.0.0", "jest": "^28.1.3", diff --git a/yarn.lock b/yarn.lock index 89a49e31eb..fe153f87ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2369,6 +2369,11 @@ resolved "https://registry.yarnpkg.com/@jsonjoy.com/util/-/util-1.1.3.tgz#75b1c3cf21b70e665789d1ad3eabeff8b7fd1429" integrity sha512-g//kkF4kOwUjemValCtOc/xiYzmwMRmWq3Bn+YnzOzuZLHq2PpMOxxIayN3cKbo7Ko2Np65t6D9H81IvXbXhqg== +"@juanelas/base64@^1.1.2": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@juanelas/base64/-/base64-1.1.5.tgz#d43b3c78e32e609d9b17a15e4f1b6342c9ca1238" + integrity sha512-mjAF27LzwfYobdwqnxZgeucbKT5wRRNvILg3h5OvCWK+3F7mw/A1tnjHnNiTYtLmTvT/bM1jA5AX7eQawDGs1w== + "@lavamoat/aa@^3.1.5": version "3.1.5" resolved "https://registry.yarnpkg.com/@lavamoat/aa/-/aa-3.1.5.tgz#2cca45f6269184f39b170d46588df962c17f4083" @@ -3070,7 +3075,14 @@ dependencies: type-detect "4.0.8" -"@sinonjs/commons@^3.0.0": +"@sinonjs/commons@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-2.0.0.tgz#fd4ca5b063554307e8327b4564bd56d3b73924a3" + integrity sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg== + dependencies: + type-detect "4.0.8" + +"@sinonjs/commons@^3.0.0", "@sinonjs/commons@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.1.tgz#1029357e44ca901a615585f6d27738dbc89084cd" integrity sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ== @@ -3084,6 +3096,13 @@ dependencies: "@sinonjs/commons" "^3.0.0" +"@sinonjs/fake-timers@^11.2.2": + version "11.2.2" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz#50063cc3574f4a27bd8453180a04171c85cc9699" + integrity sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw== + dependencies: + "@sinonjs/commons" "^3.0.0" + "@sinonjs/fake-timers@^9.1.2": version "9.1.2" resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz#4eaab737fab77332ab132d396a3c0d364bd0ea8c" @@ -3091,6 +3110,20 @@ dependencies: "@sinonjs/commons" "^1.7.0" +"@sinonjs/samsam@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-8.0.0.tgz#0d488c91efb3fa1442e26abea81759dfc8b5ac60" + integrity sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew== + dependencies: + "@sinonjs/commons" "^2.0.0" + lodash.get "^4.4.2" + type-detect "^4.0.8" + +"@sinonjs/text-encoding@^0.7.2": + version "0.7.2" + resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz#5981a8db18b56ba38ef0efb7d995b12aa7b51918" + integrity sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ== + "@slorber/remark-comment@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@slorber/remark-comment/-/remark-comment-1.0.0.tgz#2a020b3f4579c89dec0361673206c28d67e08f5a" @@ -3159,7 +3192,7 @@ optionalDependencies: sodium-native "^4.1.1" -"@stellar/stellar-sdk@^11.1.0": +"@stellar/stellar-sdk@^11.1.0", "@stellar/stellar-sdk@^11.3.0": version "11.3.0" resolved "https://registry.yarnpkg.com/@stellar/stellar-sdk/-/stellar-sdk-11.3.0.tgz#7cb010651846a07e1853e0fe30e430ece4da340b" integrity sha512-i+heopibJNRA7iM8rEPz0AXphBPYvy2HDo8rxbDwWpozwCfw8kglP9cLkkhgJe8YicgLrdExz/iQZaLpqLC+6w== @@ -4788,7 +4821,7 @@ axe-core@=4.7.0: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.0.tgz#34ba5a48a8b564f67e103f0aa5768d76e15bbbbf" integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ== -axios@^1.6.8, axios@^1.7.2: +axios@^1.6.5, axios@^1.6.8, axios@^1.7.2: version "1.7.2" resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.2.tgz#b625db8a7051fbea61c35a3cbb3a1daa7b9c7621" integrity sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw== @@ -5057,6 +5090,18 @@ big.js@^5.2.2: resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== +big.js@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-6.2.1.tgz#7205ce763efb17c2e41f26f121c420c6a7c2744f" + integrity sha512-bCtHMwL9LeDIozFn+oNhhFoq+yQ3BNdnsLSASUxLciOb1vgvpHsIO1dsENiGMgbb4SkP5TrzWzRiLddn8ahVOQ== + +bigint-conversion@^2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/bigint-conversion/-/bigint-conversion-2.4.3.tgz#cca2ff59033960be8ff517b8e931db82b6bb24fa" + integrity sha512-eM76IXlhXQD6HAoE6A7QLQ3jdC04EJdjH3zrlU1Jtt4/jj+O/pMGjGR5FY8/55FOIBsK25kly0RoG4GA4iKdvg== + dependencies: + "@juanelas/base64" "^1.1.2" + bignumber.js@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-4.1.0.tgz#db6f14067c140bd46624815a7916c92d9b6c24b1" @@ -5282,6 +5327,16 @@ bundle-name@^4.1.0: dependencies: run-applescript "^7.0.0" +bunyan@^1.8.15: + version "1.8.15" + resolved "https://registry.yarnpkg.com/bunyan/-/bunyan-1.8.15.tgz#8ce34ca908a17d0776576ca1b2f6cbd916e93b46" + integrity sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig== + optionalDependencies: + dtrace-provider "~0.8" + moment "^2.19.3" + mv "~2" + safe-json-stringify "~1" + bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" @@ -5339,7 +5394,7 @@ cacheable-request@^10.2.8: normalize-url "^8.0.0" responselike "^3.0.0" -cacheable-request@^7.0.1: +cacheable-request@^7.0.2: version "7.0.4" resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.4.tgz#7a33ebf08613178b403635be7b899d3e69bbe817" integrity sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg== @@ -6499,6 +6554,11 @@ decamelize@^1.2.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== +decimal.js-light@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934" + integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg== + decimal.js@^10.2.1, decimal.js@^10.3.1: version "10.4.3" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" @@ -6729,6 +6789,11 @@ diff-sequences@^29.6.3: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== +diff@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" + integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -6867,11 +6932,23 @@ dotenv-webpack@^8.0.1: dependencies: dotenv-defaults "^2.0.2" +dotenv@^16.4.0: + version "16.4.5" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" + integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== + dotenv@^8.2.0: version "8.6.0" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b" integrity sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g== +dtrace-provider@~0.8: + version "0.8.8" + resolved "https://registry.yarnpkg.com/dtrace-provider/-/dtrace-provider-0.8.8.tgz#2996d5490c37e1347be263b423ed7b297fb0d97e" + integrity sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg== + dependencies: + nan "^2.14.0" + duplexer@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" @@ -8322,6 +8399,17 @@ glob@^10.3.7: minipass "^7.1.2" path-scurry "^1.11.1" +glob@^6.0.1: + version "6.0.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" + integrity sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A== + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + glob@^7.0.0, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.0: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -8440,17 +8528,17 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -got@11.8.2: - version "11.8.2" - resolved "https://registry.yarnpkg.com/got/-/got-11.8.2.tgz#7abb3959ea28c31f3576f1576c1effce23f33599" - integrity sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ== +got@11.8.5: + version "11.8.5" + resolved "https://registry.yarnpkg.com/got/-/got-11.8.5.tgz#ce77d045136de56e8f024bebb82ea349bc730046" + integrity sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ== dependencies: "@sindresorhus/is" "^4.0.0" "@szmarczak/http-timer" "^4.0.5" "@types/cacheable-request" "^6.0.1" "@types/responselike" "^1.0.0" cacheable-lookup "^5.0.3" - cacheable-request "^7.0.1" + cacheable-request "^7.0.2" decompress-response "^6.0.0" http2-wrapper "^1.0.0-beta.5.2" lowercase-keys "^2.0.0" @@ -10393,6 +10481,11 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" +jsbi@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/jsbi/-/jsbi-4.3.0.tgz#b54ee074fb6fcbc00619559305c8f7e912b04741" + integrity sha512-SnZNcinB4RIcnEyZqFPdGPVgrg2AcnykiBy0sHVJQKHYeaLUvi3Exj+iaPpLnFVkDPZIV4U0yvgC9/R4uEAZ9g== + jsbn@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" @@ -10602,6 +10695,11 @@ jsonschema@^1.4.1: object.assign "^4.1.4" object.values "^1.1.6" +just-extend@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-6.2.0.tgz#b816abfb3d67ee860482e7401564672558163947" + integrity sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw== + keyv@^4.0.0, keyv@^4.5.3: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" @@ -10804,6 +10902,11 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== + lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -11952,7 +12055,7 @@ minimalistic-assert@^1.0.0: resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== -minimatch@3.1.2, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: +"minimatch@2 || 3", minimatch@3.1.2, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -12054,7 +12157,7 @@ minizlib@^2.1.1, minizlib@^2.1.2: minipass "^3.0.0" yallist "^4.0.0" -mkdirp@^0.5.1: +mkdirp@^0.5.1, mkdirp@~0.5.1: version "0.5.6" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== @@ -12071,6 +12174,11 @@ mktemp@~0.4.0: resolved "https://registry.yarnpkg.com/mktemp/-/mktemp-0.4.0.tgz#6d0515611c8a8c84e484aa2000129b98e981ff0b" integrity sha512-IXnMcJ6ZyTuhRmJSjzvHSRhlVPiN9Jwc6e59V0bEJ0ba6OBeX2L0E+mRN1QseeOF4mM+F1Rit6Nh7o+rl2Yn/A== +moment@^2.19.3: + version "2.30.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" + integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== + moo-color@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/moo-color/-/moo-color-1.0.3.tgz#d56435f8359c8284d83ac58016df7427febece74" @@ -12122,6 +12230,15 @@ multimatch@^4.0.0: arrify "^2.0.1" minimatch "^3.0.4" +mv@~2: + version "2.1.1" + resolved "https://registry.yarnpkg.com/mv/-/mv-2.1.1.tgz#ae6ce0d6f6d5e0a4f7d893798d03c1ea9559b6a2" + integrity sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg== + dependencies: + mkdirp "~0.5.1" + ncp "~2.0.0" + rimraf "~2.4.0" + nan@^2.14.0: version "2.20.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.20.0.tgz#08c5ea813dd54ed16e5bd6505bf42af4f7838ca3" @@ -12142,6 +12259,11 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== +ncp@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3" + integrity sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA== + negotiator@0.6.3, negotiator@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" @@ -12157,6 +12279,17 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +nise@^5.1.9: + version "5.1.9" + resolved "https://registry.yarnpkg.com/nise/-/nise-5.1.9.tgz#0cb73b5e4499d738231a473cd89bd8afbb618139" + integrity sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww== + dependencies: + "@sinonjs/commons" "^3.0.0" + "@sinonjs/fake-timers" "^11.2.2" + "@sinonjs/text-encoding" "^0.7.2" + just-extend "^6.2.0" + path-to-regexp "^6.2.1" + no-case@^2.2.0: version "2.3.2" resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac" @@ -12853,6 +12986,11 @@ path-to-regexp@^1.7.0: dependencies: isarray "0.0.1" +path-to-regexp@^6.2.1: + version "6.2.2" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.2.tgz#324377a83e5049cbecadc5554d6a63a9a4866b36" + integrity sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw== + path-type@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" @@ -14361,6 +14499,13 @@ rimraf@^5.0.5: dependencies: glob "^10.3.7" +rimraf@~2.4.0: + version "2.4.5" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.4.5.tgz#ee710ce5d93a8fdb856fb5ea8ff0e2d75934b2da" + integrity sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ== + dependencies: + glob "^6.0.1" + rimraf@~2.6.2: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" @@ -14447,6 +14592,11 @@ safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-json-stringify@~1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz#356e44bc98f1f93ce45df14bcd7c01cda86e0afd" + integrity sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg== + safe-regex-test@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" @@ -14774,6 +14924,18 @@ signal-exit@^4.0.1: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== +sinon@^17.0.1: + version "17.0.2" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-17.0.2.tgz#470894bcc2d24b01bad539722ea46da949892405" + integrity sha512-uihLiaB9FhzesElPDFZA7hDcNABzsVHwr3YfmM9sBllVwab3l0ltGlRV1XhpNfIacNDLGD1QRZNLs5nU5+hTuA== + dependencies: + "@sinonjs/commons" "^3.0.1" + "@sinonjs/fake-timers" "^11.2.2" + "@sinonjs/samsam" "^8.0.0" + diff "^5.2.0" + nise "^5.1.9" + supports-color "^7" + sirv@^2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/sirv/-/sirv-2.0.4.tgz#5dd9a725c578e34e449f332703eb2a74e46a29b0" @@ -14909,6 +15071,25 @@ sonic-forest@^1.0.0: dependencies: tree-dump "^1.0.0" +soroswap-router-sdk@^1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/soroswap-router-sdk/-/soroswap-router-sdk-1.2.8.tgz#17d5a6e69b39ce61aa6ca312f4f0f6dd28d8ec73" + integrity sha512-m8kHw5EGStNlYyE4eTBP5o5475yCNTAE+zyGUWW3uScQsLWinm7yRxq6U2biWMocMmj5Eo2rDQUmhJyrUpDanw== + dependencies: + "@stellar/stellar-sdk" "^11.3.0" + axios "^1.6.5" + big.js "^6.2.1" + bigint-conversion "^2.4.3" + bignumber.js "^9.1.2" + bunyan "^1.8.15" + decimal.js-light "^2.5.1" + dotenv "^16.4.0" + jsbi "^4.3.0" + lodash "^4.17.21" + sinon "^17.0.1" + tiny-invariant "^1.3.1" + toformat "^2.0.0" + sort-css-media-queries@2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/sort-css-media-queries/-/sort-css-media-queries-2.2.0.tgz#aa33cf4a08e0225059448b6c40eddbf9f1c8334c" @@ -15459,7 +15640,7 @@ supports-color@^6.1.0: dependencies: has-flag "^3.0.0" -supports-color@^7.0.0, supports-color@^7.1.0: +supports-color@^7, supports-color@^7.0.0, supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== @@ -15645,7 +15826,7 @@ thunky@^1.0.2: resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== -tiny-invariant@^1.0.2: +tiny-invariant@^1.0.2, tiny-invariant@^1.3.1: version "1.3.3" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== @@ -15687,6 +15868,11 @@ to-through@^2.0.0: dependencies: through2 "^2.0.3" +toformat@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/toformat/-/toformat-2.0.0.tgz#7a043fd2dfbe9021a4e36e508835ba32056739d8" + integrity sha512-03SWBVop6nU8bpyZCx7SodpYznbZF5R4ljwNLBcTQzKOD9xuihRo/psX58llS1BMFhhAI08H3luot5GoXJz2pQ== + toggle-selection@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32" @@ -15858,7 +16044,7 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" -type-detect@4.0.8: +type-detect@4.0.8, type-detect@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== @@ -16983,21 +17169,21 @@ write-file-atomic@^5.0.0: signal-exit "^4.0.1" ws@^6.2.2: - version "6.2.2" - resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e" - integrity sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw== + version "6.2.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.3.tgz#ccc96e4add5fd6fedbc491903075c85c5a11d9ee" + integrity sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA== dependencies: async-limiter "~1.0.0" ws@^7, ws@^7.3.1, ws@^7.4.6, ws@^7.5.1: - version "7.5.9" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" - integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== + version "7.5.10" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" + integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== ws@^8.13.0, ws@^8.16.0, ws@^8.2.3: - version "8.17.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.0.tgz#d145d18eca2ed25aaf791a183903f7be5e295fea" - integrity sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow== + version "8.17.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" + integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== xdg-basedir@^5.0.1, xdg-basedir@^5.1.0: version "5.1.0"