From ce05d1e384ea8bc5f149b34aa8a491e056b522d7 Mon Sep 17 00:00:00 2001 From: Piyal Basu Date: Mon, 1 Jul 2024 15:19:15 -0400 Subject: [PATCH] Release/5.21.0 (#1349) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feature/soroswap (#1347) * Feature/hash signing 2 (#1303) * flips the session store flag, updates listeners to not be intialized … (#1169) * capture amplitude errors to better understand why they're triggering sentry (#1167) * flips the session store flag, updates listeners to not be intialized async for v3 * updates e2e tests for manifest v3 --------- Co-authored-by: Piyal Basu * add scripts tag for Firefox (#1294) * uses chrome storage in migrations instead of local storage * checks for migrated account in migration logic to set migrated network * don't babel-polyfill contentScript (#1297) * don't clear all of localStore on recoverAccount (#1301) * add hash signing option (ui and data storage) * fix redux state update; show custom errors from hardware wallet * Added translations * when experimental mode is enabled, switch network * PR comments * rm logs --------- Co-authored-by: aristides * Feature/trustline sac (#1289) * Bump axios, @docusaurus/core and @docusaurus/preset-classic in /docs (#1244) Removes [axios](https://github.com/axios/axios). It's no longer used after updating ancestor dependencies [axios](https://github.com/axios/axios), [@docusaurus/core](https://github.com/facebook/docusaurus/tree/HEAD/packages/docusaurus) and [@docusaurus/preset-classic](https://github.com/facebook/docusaurus/tree/HEAD/packages/docusaurus-preset-classic). These dependencies need to be updated together. Removes `axios` Updates `@docusaurus/core` from 2.4.1 to 3.2.1 - [Release notes](https://github.com/facebook/docusaurus/releases) - [Changelog](https://github.com/facebook/docusaurus/blob/main/CHANGELOG.md) - [Commits](https://github.com/facebook/docusaurus/commits/v3.2.1/packages/docusaurus) Updates `@docusaurus/preset-classic` from 2.4.1 to 3.2.1 - [Release notes](https://github.com/facebook/docusaurus/releases) - [Changelog](https://github.com/facebook/docusaurus/blob/main/CHANGELOG.md) - [Commits](https://github.com/facebook/docusaurus/commits/v3.2.1/packages/docusaurus-preset-classic) --- updated-dependencies: - dependency-name: axios dependency-type: indirect - dependency-name: "@docusaurus/core" dependency-type: direct:production - dependency-name: "@docusaurus/preset-classic" dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump express from 4.18.2 to 4.19.2 in /docs (#1216) Bumps [express](https://github.com/expressjs/express) from 4.18.2 to 4.19.2. - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/master/History.md) - [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2) --- updated-dependencies: - dependency-name: express dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Piyal Basu * Bump tar from 6.1.15 to 6.2.1 (#1231) Bumps [tar](https://github.com/isaacs/node-tar) from 6.1.15 to 6.2.1. - [Release notes](https://github.com/isaacs/node-tar/releases) - [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md) - [Commits](https://github.com/isaacs/node-tar/compare/v6.1.15...v6.2.1) --- updated-dependencies: - dependency-name: tar dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Piyal Basu * Bump express from 4.18.2 to 4.19.2 (#1214) Bumps [express](https://github.com/expressjs/express) from 4.18.2 to 4.19.2. - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/master/History.md) - [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2) --- updated-dependencies: - dependency-name: express dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Piyal Basu * Bump webpack-dev-middleware from 5.3.3 to 5.3.4 in /docs (#1208) Bumps [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware) from 5.3.3 to 5.3.4. - [Release notes](https://github.com/webpack/webpack-dev-middleware/releases) - [Changelog](https://github.com/webpack/webpack-dev-middleware/blob/v5.3.4/CHANGELOG.md) - [Commits](https://github.com/webpack/webpack-dev-middleware/compare/v5.3.3...v5.3.4) --- updated-dependencies: - dependency-name: webpack-dev-middleware dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Piyal Basu * Bump webpack-dev-middleware from 5.3.3 to 5.3.4 (#1207) Bumps [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware) from 5.3.3 to 5.3.4. - [Release notes](https://github.com/webpack/webpack-dev-middleware/releases) - [Changelog](https://github.com/webpack/webpack-dev-middleware/blob/v5.3.4/CHANGELOG.md) - [Commits](https://github.com/webpack/webpack-dev-middleware/compare/v5.3.3...v5.3.4) --- updated-dependencies: - dependency-name: webpack-dev-middleware dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump follow-redirects from 1.15.4 to 1.15.6 in /docs (#1198) Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.4 to 1.15.6. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.4...v1.15.6) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Piyal Basu * Bump follow-redirects from 1.15.2 to 1.15.6 (#1197) Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.2 to 1.15.6. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.2...v1.15.6) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Piyal Basu * Bump ip from 2.0.0 to 2.0.1 (#1128) Bumps [ip](https://github.com/indutny/node-ip) from 2.0.0 to 2.0.1. - [Commits](https://github.com/indutny/node-ip/compare/v2.0.0...v2.0.1) --- updated-dependencies: - dependency-name: ip dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Piyal Basu * Bump axios, @docusaurus/core and @docusaurus/preset-classic in /docs (#1055) Removes [axios](https://github.com/axios/axios). It's no longer used after updating ancestor dependencies [axios](https://github.com/axios/axios), [@docusaurus/core](https://github.com/facebook/docusaurus/tree/HEAD/packages/docusaurus) and [@docusaurus/preset-classic](https://github.com/facebook/docusaurus/tree/HEAD/packages/docusaurus-preset-classic). These dependencies need to be updated together. Removes `axios` Updates `@docusaurus/core` from 2.4.1 to 3.0.1 - [Release notes](https://github.com/facebook/docusaurus/releases) - [Changelog](https://github.com/facebook/docusaurus/blob/main/CHANGELOG.md) - [Commits](https://github.com/facebook/docusaurus/commits/v3.0.1/packages/docusaurus) Updates `@docusaurus/preset-classic` from 2.4.1 to 3.0.1 - [Release notes](https://github.com/facebook/docusaurus/releases) - [Changelog](https://github.com/facebook/docusaurus/blob/main/CHANGELOG.md) - [Commits](https://github.com/facebook/docusaurus/commits/v3.0.1/packages/docusaurus-preset-classic) --- updated-dependencies: - dependency-name: axios dependency-type: indirect - dependency-name: "@docusaurus/core" dependency-type: direct:production - dependency-name: "@docusaurus/preset-classic" dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Piyal Basu * first pass at adding remove dropdown * replaces all remaining instances of wallet-sdk * tweaks tx detail row layouts to avoid overflows * adds getContractSpec, uses it to display parameter names in tx sign detail view * migrates KeyManager to ts-wallet-sdk * upgrades react and react dom types to 18 * adds react types to extension workspace, fixes type errors * set styles for dropdown * Added translations * adds standalone version of getContractSpec and related helpers * Added translations * Added translations * undo husky pre push comments * Feature/p21 futurenet release (#1278) * switch between stellar-sdk and stellar-sdk-next based on network * increase max diff pixel ratio for playwright * rm console * adds tx timeout in send and swap settings * Added translations * tweaks tooltip text * Added translations * happy path for new add asset flow * fix trustline error warning and fix naming * Added translations * rollback dep upgrades * rollback changes * rollback package-lock upgrade * add snapshots for manage assets and send payment (#1309) --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Aristides Staffieri * guard against dispatching incorrect parameter (#1305) * adds Hardware sign usage in ReviewAuth for use during invokeHostFn signing, tweaks soroban icon to not overflow in signing details (#1282) * adds loader for hardware signing during signing (#1284) Co-authored-by: Piyal Basu * adds sign blob error for encoding mismatch (#1300) * fix bad merge * [BUG] replaces all instances of pickTransfer with getTokenSpec workflow (#1312) * replaces all instances of pickTransfer with getTokenSpec workflow * check for transfer once token spec is confirmed * rm unused dep * fixes bad merge from upstream in ManageAssets * Added translations * remove accidental mobile4 dir addition --------- Co-authored-by: Piyal Basu * Test/data storage (#1304) * adds test for data storage get item * adds tests for network migrations * adds test for remaining migrations * reverts bad merge from upstream --------- Co-authored-by: Piyal Basu * [CHORE] upgrades all webpack related deps (#1307) * upgrades all webpack related deps * updates lock file * updates migrations test for recent changes * [CHORE] upgrade to docusaurus v3 (#1306) * upgrade to docusaurus v3 * edit manifest for chrome before uploading (#1315) * use single quotes (#1316) --------- Co-authored-by: Piyal Basu * Fix manage assets tests (#1317) * fix tests for new manage assets flow * fix broken manage-assets tests * rm unused dep * fix e2e tests * fix query for visible loader * fix assets test * selector for individual asset balance * add better error messaging for liabilities when removing an asset (#1319) * add better error messaging for liabilities when removing an asset * add tests and rm sellingLiabilities * rm more selling liabilities * fix diff * fix diff again * verify params before fetching issuer info (#1320) * add better error messaging for liabilities when removing an asset * add tests and rm sellingLiabilities * rm more selling liabilities * fix diff * fix diff again * verify params before fetching issuer info * test soroswap api * pin docusaurus core to match versions * adding soroswap tokens to swap dropdown * split dataStorageAccess into a different file to allow for jest mocks with circular deps * updates all references for new dataStorage path * fix add asset padding; fix ledger trustline error; fix token-spec check (#1324) * fix add asset padding; fix ledger trustline error; fix token-spec check * use dynamic verified token in e2e test * undo the hw wallet trustline error fix while I figure out how to do it properly * revert isTokenSpec change * better trustline error handling; fix remove token (#1326) * starting work on bestpath * upgrade redux toolkit (#1333) * upgrade redux toolkit * allow a longer timeout to account for network congestion * styling and copy fixes * Added translations * upgrade soroswap sdk and break out simulation component * Bugfix/5.20.0 qa (#1334) * styling and copy fixes * Added translations * Bugfix/5.20.0 legal copy (#1337) * legal copy changes * Added translations * fix go back button * use history.goBack for go back button * show icons for soroswap tokens and final cleanup * remove axios and auto add the token after successful token swap * adding comments * enable linting * simplify asset dropdown logic --------- Signed-off-by: dependabot[bot] Co-authored-by: aristides Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * uses key val for public keys for inflation dest and trustor fields * adding documentation to the Soroswap methods (#1350) * removes sentry exceptions from expected failures to fetch contract specs --------- Signed-off-by: dependabot[bot] Co-authored-by: aristides Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- @shared/api/types.ts | 8 + extension/INTEGRATING_SOROSWAP.MD | 22 ++ extension/package.json | 3 +- extension/public/static/manifest/v3.json | 19 +- .../account/AccountAssets/index.tsx | 25 +- .../manageAssets/ChooseAsset/index.tsx | 48 ++- .../manageAssets/ManageAssetRows/index.tsx | 1 + .../manageAssets/SelectAssetRows/index.tsx | 100 +++--- .../SendAmount/AssetSelect/index.tsx | 3 + .../sendPayment/SendAmount/index.tsx | 61 +++- .../SendConfirm/TransactionDetails/index.tsx | 21 +- .../SendSettings/Settings/index.tsx | 35 ++- .../Operations/KeyVal/index.tsx | 4 - .../signTransaction/Operations/index.tsx | 4 +- .../src/popup/ducks/transactionSubmission.ts | 104 +++++++ extension/src/popup/helpers/getAssetDomain.ts | 9 +- extension/src/popup/helpers/sorobanSwap.ts | 294 ++++++++++++++++++ extension/src/popup/helpers/useIsSwap.ts | 10 + extension/src/popup/views/Account/index.tsx | 9 +- .../src/popup/views/ReviewAuth/index.tsx | 4 - yarn.lock | 202 +++++++++++- 21 files changed, 874 insertions(+), 112 deletions(-) create mode 100644 extension/INTEGRATING_SOROSWAP.MD create mode 100644 extension/src/popup/helpers/sorobanSwap.ts 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 4bae0a59b0..a8b6e8cad2 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 f82b0c3c14..5d679d6af0 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/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 ( 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..21416706e7 100644 --- a/extension/src/popup/components/manageAssets/SelectAssetRows/index.tsx +++ b/extension/src/popup/components/manageAssets/SelectAssetRows/index.tsx @@ -7,8 +7,10 @@ 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"; @@ -29,6 +31,7 @@ export const SelectAssetRows = ({ assetRows }: SelectAssetRowsProps) => { accountBalances: { balances = {} }, assetSelect, blockedDomains, + soroswapTokens, } = useSelector(transactionSubmissionSelector); const dispatch: AppDispatch = useDispatch(); const history = useHistory(); @@ -52,7 +55,7 @@ export const SelectAssetRows = ({ assetRows }: SelectAssetRowsProps) => { if (bal) { return getTokenBalance(bal); } - return ""; + return "0"; }; // hide balances for path pay dest asset @@ -63,55 +66,62 @@ 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); - 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)); + dispatch( + saveIsSoroswap( + !!soroswapTokens.find( + ({ contract }) => contract === issuer, + ), + ), + ); + history.goBack(); } - history.goBack(); - } else { - dispatch(saveDestinationAsset(canonical)); - history.goBack(); - } - }} - > - -
-
- {code} - -
-
- {formatDomain(domain)} + }} + > + +
+
+ {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) { @@ -524,6 +561,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/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/yarn.lock b/yarn.lock index 89a49e31eb..8d7f550da5 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" @@ -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" @@ -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==