├── .devcontainer └── devcontainer.json ├── .editorconfig ├── .erb ├── configs │ ├── .eslintrc │ ├── webpack.config.base.ts │ ├── webpack.config.eslint.ts │ ├── webpack.config.main.prod.ts │ ├── webpack.config.renderer.dev.dll.ts │ ├── webpack.config.renderer.dev.ts │ ├── webpack.config.renderer.prod.ts │ └── webpack.paths.ts ├── img │ ├── erb-banner.svg │ └── erb-logo.png ├── mocks │ └── fileMock.js └── scripts │ ├── .eslintrc │ ├── check-build-exists.ts │ ├── check-native-dep.js │ ├── check-node-env.js │ ├── check-port-in-use.js │ ├── clean.js │ ├── delete-source-maps.js │ ├── download-swap-binaries.ts │ ├── download-tor-binaries.ts │ ├── electron-rebuild.js │ ├── link-modules.ts │ └── notarize.js ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── 1-Bug_report.md │ ├── 2-Question.md │ ├── 3-Feature_request.md │ └── 4-Problem_With_Swap.md ├── config.yml ├── stale.yml └── workflows │ ├── publish.yml │ └── test.yml ├── .gitignore ├── .husky └── pre-commit ├── .vscode ├── extensions.json ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── assets ├── assets.d.ts ├── entitlements.mac.plist ├── icon.icns ├── icon.ico ├── icon.png ├── icon.svg └── icons │ ├── 100x100.png │ ├── 1024x1024.png │ ├── 114x114.png │ ├── 120x120.png │ ├── 128x128.png │ ├── 144x144.png │ ├── 152x152.png │ ├── 167x167.png │ ├── 16x16.png │ ├── 172x172.png │ ├── 180x180.png │ ├── 196x196.png │ ├── 20x20.png │ ├── 216x216.png │ ├── 24x24.png │ ├── 256x256.png │ ├── 29x29.png │ ├── 32x32.png │ ├── 40x40.png │ ├── 48x48.png │ ├── 50x50.png │ ├── 512x512.png │ ├── 55x55.png │ ├── 57x57.png │ ├── 58x58.png │ ├── 60x60.png │ ├── 64x64.png │ ├── 72x72.png │ ├── 76x76.png │ ├── 80x80.png │ ├── 87x87.png │ ├── 88x88.png │ └── 96x96.png ├── package-lock.json ├── package.json ├── release └── app │ ├── package-lock.json │ └── package.json ├── src ├── __tests__ │ ├── mock_cli_logs │ │ ├── cli_log_advancing_state_btc_redeemed.json │ │ ├── cli_log_advancing_state_xmr_is_locked.json │ │ ├── cli_log_alice_locked_monero.json │ │ ├── cli_log_bitcoin_transaction_status_changed.json │ │ ├── cli_log_checked_bitcoin_balance.json │ │ ├── cli_log_manually_cancelling_swap.json │ │ ├── cli_log_published_btc_cancel_tx.json │ │ ├── cli_log_published_btc_lock_tx.json │ │ ├── cli_log_published_btc_refund_tx.json │ │ ├── cli_log_published_btc_withdraw_tx.json │ │ ├── cli_log_received_bitcoin.json │ │ ├── cli_log_received_new_conf_for_monero_lock_tx.json │ │ ├── cli_log_received_quote.json │ │ ├── cli_log_redeemed_xmr.json │ │ ├── cli_log_starting_new_swap.json │ │ ├── cli_log_waiting_for_bitcoin_deposit.json │ │ ├── cli_log_waiting_for_btc_refund_tx_finality_init.json │ │ └── cli_log_waiting_for_btc_refund_tx_finality_update.json │ ├── store │ │ ├── config.spec.ts │ │ ├── listSellersSlice.spec.ts │ │ └── swapSlice.spec.ts │ └── utils │ │ ├── conversionUtils.spec.ts │ │ └── parseUtils.spec.ts ├── main │ ├── cli │ │ ├── cli.ts │ │ ├── dirs.ts │ │ └── rpc.ts │ ├── dev-app-update.yml │ ├── main.ts │ ├── socket.ts │ ├── stats.ts │ ├── store │ │ ├── mainStore.ts │ │ └── mainStoreListeners.ts │ ├── tor.ts │ ├── updater.ts │ └── util.ts ├── models │ ├── apiModel.ts │ ├── cliModel.ts │ ├── downloaderModel.ts │ ├── rpcModel.ts │ └── storeModel.ts ├── renderer │ ├── api.ts │ ├── components │ │ ├── App.tsx │ │ ├── IpcInvokeButton.tsx │ │ ├── alert │ │ │ ├── FundsLeftInWalletAlert.tsx │ │ │ ├── MoneroWalletRpcUpdatingAlert.tsx │ │ │ ├── RemainingFundsWillBeUsedAlert.tsx │ │ │ ├── RpcStatusAlert.tsx │ │ │ ├── SwapMightBeCancelledAlert.tsx │ │ │ ├── SwapStatusAlert.tsx │ │ │ ├── SwapTxLockAlertsBox.tsx │ │ │ └── UnfinishedSwapsAlert.tsx │ │ ├── icons │ │ │ ├── BitcoinIcon.tsx │ │ │ ├── DiscordIcon.tsx │ │ │ ├── LinkIconButton.tsx │ │ │ ├── MoneroIcon.tsx │ │ │ └── TorIcon.tsx │ │ ├── inputs │ │ │ ├── BitcoinAddressTextField.tsx │ │ │ └── MoneroAddressTextField.tsx │ │ ├── modal │ │ │ ├── DialogHeader.tsx │ │ │ ├── PaperTextBox.tsx │ │ │ ├── SwapSuspendAlert.tsx │ │ │ ├── feedback │ │ │ │ └── FeedbackDialog.tsx │ │ │ ├── listSellers │ │ │ │ └── ListSellersDialog.tsx │ │ │ ├── provider │ │ │ │ ├── ProviderInfo.tsx │ │ │ │ ├── ProviderListDialog.tsx │ │ │ │ ├── ProviderSelect.tsx │ │ │ │ └── ProviderSubmitDialog.tsx │ │ │ ├── swap │ │ │ │ ├── BitcoinQrCode.tsx │ │ │ │ ├── BitcoinTransactionInfoBox.tsx │ │ │ │ ├── CircularProgressWithSubtitle.tsx │ │ │ │ ├── ClipbiardIconButton.tsx │ │ │ │ ├── DepositAddressInfoBox.tsx │ │ │ │ ├── InfoBox.tsx │ │ │ │ ├── MoneroTransactionInfoBox.tsx │ │ │ │ ├── SwapDialog.tsx │ │ │ │ ├── SwapDialogTitle.tsx │ │ │ │ ├── SwapStateStepper.tsx │ │ │ │ ├── TransactionInfoBox.tsx │ │ │ │ └── pages │ │ │ │ │ ├── DebugPage.tsx │ │ │ │ │ ├── DebugPageSwitchBadge.tsx │ │ │ │ │ ├── FeedbackSubmitBadge.tsx │ │ │ │ │ ├── SwapStatePage.tsx │ │ │ │ │ ├── TorStatusBadge.tsx │ │ │ │ │ ├── done │ │ │ │ │ ├── BitcoinPunishedPage.tsx │ │ │ │ │ ├── BitcoinRefundedPage.tsx │ │ │ │ │ └── XmrRedeemInMempoolPage.tsx │ │ │ │ │ ├── exited │ │ │ │ │ ├── ProcessExitedAndNotDonePage.tsx │ │ │ │ │ └── ProcessExitedPage.tsx │ │ │ │ │ ├── in_progress │ │ │ │ │ ├── BitcoinCancelledPage.tsx │ │ │ │ │ ├── BitcoinLockTxInMempoolPage.tsx │ │ │ │ │ ├── BitcoinRedeemedPage.tsx │ │ │ │ │ ├── ReceivedQuotePage.tsx │ │ │ │ │ ├── StartedPage.tsx │ │ │ │ │ ├── SyncingMoneroWalletPage.tsx │ │ │ │ │ ├── XmrLockInMempoolPage.tsx │ │ │ │ │ └── XmrLockedPage.tsx │ │ │ │ │ └── init │ │ │ │ │ ├── DepositAmountHelper.tsx │ │ │ │ │ ├── DownloadingMoneroWalletRpcPage.tsx │ │ │ │ │ ├── InitPage.tsx │ │ │ │ │ ├── InitiatedPage.tsx │ │ │ │ │ └── WaitingForBitcoinDepositPage.tsx │ │ │ ├── updater │ │ │ │ └── UpdaterDialog.tsx │ │ │ └── wallet │ │ │ │ ├── WithdrawDialog.tsx │ │ │ │ ├── WithdrawDialogContent.tsx │ │ │ │ ├── WithdrawStatePage.tsx │ │ │ │ ├── WithdrawStepper.tsx │ │ │ │ └── pages │ │ │ │ ├── AddressInputPage.tsx │ │ │ │ ├── BitcoinWithdrawTxInMempoolPage.tsx │ │ │ │ └── InitiatedPage.tsx │ │ ├── navigation │ │ │ ├── Navigation.tsx │ │ │ ├── NavigationFooter.tsx │ │ │ ├── NavigationHeader.tsx │ │ │ ├── RouteListItemIconButton.tsx │ │ │ └── UnfinishedSwapsCountBadge.tsx │ │ ├── other │ │ │ ├── ExpandableSearchBox.tsx │ │ │ ├── HumanizedBitcoinBlockDuration.tsx │ │ │ ├── JSONViewTree.tsx │ │ │ ├── LoadingButton.tsx │ │ │ ├── RenderedCliLog.tsx │ │ │ ├── ScrollablePaperTextBox.tsx │ │ │ └── Units.tsx │ │ ├── pages │ │ │ ├── help │ │ │ │ ├── ContactInfoBox.tsx │ │ │ │ ├── DonateInfoBox.tsx │ │ │ │ ├── FeedbackInfoBox.tsx │ │ │ │ ├── HelpPage.tsx │ │ │ │ ├── RpcControlBox.tsx │ │ │ │ └── TorInfoBox.tsx │ │ │ ├── history │ │ │ │ ├── HistoryPage.tsx │ │ │ │ └── table │ │ │ │ │ ├── HistoryRow.tsx │ │ │ │ │ ├── HistoryRowActions.tsx │ │ │ │ │ ├── HistoryRowExpanded.tsx │ │ │ │ │ ├── HistoryTable.tsx │ │ │ │ │ ├── SwapLogFileOpenButton.tsx │ │ │ │ │ └── SwapMoneroRecoveryButton.tsx │ │ │ ├── swap │ │ │ │ ├── ApiAlertsBox.tsx │ │ │ │ ├── SwapPage.tsx │ │ │ │ └── SwapWidget.tsx │ │ │ └── wallet │ │ │ │ ├── WalletPage.tsx │ │ │ │ ├── WalletRefreshButton.tsx │ │ │ │ └── WithdrawWidget.tsx │ │ └── snackbar │ │ │ ├── GlobalSnackbarProvider.tsx │ │ │ └── IpcSnackbar.tsx │ ├── index.ejs │ ├── index.tsx │ └── store │ │ └── storeRenderer.ts ├── store │ ├── combinedReducer.ts │ ├── config.ts │ ├── features │ │ ├── alertsSlice.ts │ │ ├── providersSlice.ts │ │ ├── ratesSlice.ts │ │ ├── rpcSlice.ts │ │ ├── swapSlice.ts │ │ ├── torSlice.ts │ │ └── updateSlice.ts │ └── hooks.ts └── utils │ ├── conversionUtils.ts │ ├── cryptoUtils.ts │ ├── event.ts │ ├── logger.ts │ ├── multiAddrUtils.ts │ ├── parseUtils.ts │ ├── sortUtils.ts │ └── typescriptUtils.tsx └── tsconfig.json /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node 3 | { 4 | "name": "Node.js", 5 | "image": "mcr.microsoft.com/devcontainers/javascript-node:0-16-bullseye" 6 | 7 | // Features to add to the dev container. More info: https://containers.dev/features. 8 | // "features": {}, 9 | 10 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 11 | // "forwardPorts": [], 12 | 13 | // Use 'postCreateCommand' to run commands after the container is created. 14 | // "postCreateCommand": "yarn install", 15 | 16 | // Configure tool-specific properties. 17 | // "customizations": {}, 18 | 19 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 20 | // "remoteUser": "root" 21 | } 22 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.erb/configs/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": "off", 4 | "global-require": "off", 5 | "import/no-dynamic-require": "off" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.base.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Base webpack config used across other specific configs 3 | */ 4 | 5 | import webpack from 'webpack'; 6 | import webpackPaths from './webpack.paths'; 7 | import { dependencies as externals } from '../../release/app/package.json'; 8 | 9 | const configuration: webpack.Configuration = { 10 | externals: [...Object.keys(externals || {})], 11 | 12 | stats: 'errors-only', 13 | 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.[jt]sx?$/, 18 | exclude: /node_modules/, 19 | use: { 20 | loader: 'ts-loader', 21 | options: { 22 | // Remove this line to enable type checking in webpack builds 23 | transpileOnly: true, 24 | }, 25 | }, 26 | }, 27 | ], 28 | }, 29 | 30 | output: { 31 | path: webpackPaths.srcPath, 32 | // https://github.com/webpack/webpack/issues/1114 33 | library: { 34 | type: 'commonjs2', 35 | }, 36 | }, 37 | 38 | /** 39 | * Determine the array of extensions that should be used to resolve modules. 40 | */ 41 | resolve: { 42 | extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'], 43 | modules: [webpackPaths.srcPath, 'node_modules'], 44 | fallback: { 45 | path: require.resolve('path-browserify'), 46 | }, 47 | alias: { 48 | 'react/jsx-dev-runtime': 'react/jsx-dev-runtime.js', 49 | 'react/jsx-runtime': 'react/jsx-runtime.js', 50 | }, 51 | }, 52 | 53 | plugins: [ 54 | new webpack.EnvironmentPlugin({ 55 | NODE_ENV: 'production', 56 | }), 57 | ], 58 | }; 59 | 60 | export default configuration; 61 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.eslint.ts: -------------------------------------------------------------------------------- 1 | /* eslint import/no-unresolved: off, import/no-self-import: off */ 2 | 3 | module.exports = require('./webpack.config.renderer.dev').default; 4 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.main.prod.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack config for production electron main process 3 | */ 4 | 5 | import path from 'path'; 6 | import webpack from 'webpack'; 7 | import { merge } from 'webpack-merge'; 8 | import TerserPlugin from 'terser-webpack-plugin'; 9 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 10 | import baseConfig from './webpack.config.base'; 11 | import webpackPaths from './webpack.paths'; 12 | import checkNodeEnv from '../scripts/check-node-env'; 13 | import deleteSourceMaps from '../scripts/delete-source-maps'; 14 | 15 | checkNodeEnv('production'); 16 | deleteSourceMaps(); 17 | 18 | const devtoolsConfig = 19 | process.env.DEBUG_PROD === 'true' 20 | ? { 21 | devtool: 'source-map', 22 | } 23 | : {}; 24 | 25 | const configuration: webpack.Configuration = { 26 | ...devtoolsConfig, 27 | 28 | mode: 'production', 29 | 30 | target: 'electron-main', 31 | 32 | entry: { 33 | main: path.join(webpackPaths.srcMainPath, 'main.ts'), 34 | }, 35 | 36 | output: { 37 | path: webpackPaths.distMainPath, 38 | filename: '[name].js', 39 | }, 40 | 41 | optimization: { 42 | minimizer: [ 43 | new TerserPlugin({ 44 | parallel: true, 45 | }), 46 | ], 47 | }, 48 | 49 | plugins: [ 50 | new BundleAnalyzerPlugin({ 51 | analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled', 52 | }), 53 | 54 | /** 55 | * Create global constants which can be configured at compile time. 56 | * 57 | * Useful for allowing different behaviour between development builds and 58 | * release builds 59 | * 60 | * NODE_ENV should be production so that modules do not perform certain 61 | * development checks 62 | */ 63 | new webpack.EnvironmentPlugin({ 64 | NODE_ENV: 'production', 65 | DEBUG_PROD: false, 66 | START_MINIMIZED: false, 67 | }), 68 | ], 69 | 70 | /** 71 | * Disables webpack processing of __dirname and __filename. 72 | * If you run the bundle in node.js it falls back to these values of node.js. 73 | * https://github.com/webpack/webpack/issues/2010 74 | */ 75 | node: { 76 | __dirname: false, 77 | __filename: false, 78 | }, 79 | }; 80 | 81 | export default merge(baseConfig, configuration); 82 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.renderer.dev.dll.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Builds the DLL for development electron renderer process 3 | */ 4 | 5 | import webpack from 'webpack'; 6 | import path from 'path'; 7 | import { merge } from 'webpack-merge'; 8 | import baseConfig from './webpack.config.base'; 9 | import webpackPaths from './webpack.paths'; 10 | import { dependencies } from '../../package.json'; 11 | import checkNodeEnv from '../scripts/check-node-env'; 12 | 13 | checkNodeEnv('development'); 14 | 15 | const dist = webpackPaths.dllPath; 16 | 17 | const configuration: webpack.Configuration = { 18 | context: webpackPaths.rootPath, 19 | 20 | devtool: 'eval', 21 | 22 | mode: 'development', 23 | 24 | target: 'electron-renderer', 25 | 26 | externals: ['fsevents', 'crypto-browserify'], 27 | 28 | /** 29 | * Use `module` from `webpack.config.renderer.dev.js` 30 | */ 31 | module: require('./webpack.config.renderer.dev').default.module, 32 | 33 | entry: { 34 | renderer: Object.keys(dependencies || {}), 35 | }, 36 | 37 | output: { 38 | path: dist, 39 | filename: '[name].dev.dll.js', 40 | library: { 41 | name: 'renderer', 42 | type: 'var', 43 | }, 44 | }, 45 | 46 | plugins: [ 47 | new webpack.DllPlugin({ 48 | path: path.join(dist, '[name].json'), 49 | name: '[name]', 50 | }), 51 | 52 | /** 53 | * Create global constants which can be configured at compile time. 54 | * 55 | * Useful for allowing different behaviour between development builds and 56 | * release builds 57 | * 58 | * NODE_ENV should be production so that modules do not perform certain 59 | * development checks 60 | */ 61 | new webpack.EnvironmentPlugin({ 62 | NODE_ENV: 'development', 63 | }), 64 | 65 | new webpack.LoaderOptionsPlugin({ 66 | debug: true, 67 | options: { 68 | context: webpackPaths.srcPath, 69 | output: { 70 | path: webpackPaths.dllPath, 71 | }, 72 | }, 73 | }), 74 | ], 75 | }; 76 | 77 | export default merge(baseConfig, configuration); 78 | -------------------------------------------------------------------------------- /.erb/configs/webpack.paths.ts: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const rootPath = path.join(__dirname, '../..'); 4 | 5 | const dllPath = path.join(__dirname, '../dll'); 6 | 7 | const srcPath = path.join(rootPath, 'src'); 8 | const srcMainPath = path.join(srcPath, 'main'); 9 | const srcRendererPath = path.join(srcPath, 'renderer'); 10 | 11 | const releasePath = path.join(rootPath, 'release'); 12 | const appPath = path.join(releasePath, 'app'); 13 | const appPackagePath = path.join(appPath, 'package.json'); 14 | const appNodeModulesPath = path.join(appPath, 'node_modules'); 15 | const srcNodeModulesPath = path.join(srcPath, 'node_modules'); 16 | 17 | const distPath = path.join(appPath, 'dist'); 18 | const distMainPath = path.join(distPath, 'main'); 19 | const distRendererPath = path.join(distPath, 'renderer'); 20 | 21 | const buildPath = path.join(releasePath, 'build'); 22 | 23 | export default { 24 | rootPath, 25 | dllPath, 26 | srcPath, 27 | srcMainPath, 28 | srcRendererPath, 29 | releasePath, 30 | appPath, 31 | appPackagePath, 32 | appNodeModulesPath, 33 | srcNodeModulesPath, 34 | distPath, 35 | distMainPath, 36 | distRendererPath, 37 | buildPath, 38 | }; 39 | -------------------------------------------------------------------------------- /.erb/img/erb-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/.erb/img/erb-logo.png -------------------------------------------------------------------------------- /.erb/mocks/fileMock.js: -------------------------------------------------------------------------------- 1 | export default 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /.erb/scripts/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": "off", 4 | "global-require": "off", 5 | "import/no-dynamic-require": "off", 6 | "import/no-extraneous-dependencies": "off" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.erb/scripts/check-build-exists.ts: -------------------------------------------------------------------------------- 1 | // Check if the renderer and main bundles are built 2 | import path from 'path'; 3 | import chalk from 'chalk'; 4 | import fs from 'fs'; 5 | import webpackPaths from '../configs/webpack.paths'; 6 | 7 | const mainPath = path.join(webpackPaths.distMainPath, 'main.js'); 8 | const rendererPath = path.join(webpackPaths.distRendererPath, 'renderer.js'); 9 | 10 | if (!fs.existsSync(mainPath)) { 11 | throw new Error( 12 | chalk.whiteBright.bgRed.bold( 13 | 'The main process is not built yet. Build it by running "npm run build:main"', 14 | ), 15 | ); 16 | } 17 | 18 | if (!fs.existsSync(rendererPath)) { 19 | throw new Error( 20 | chalk.whiteBright.bgRed.bold( 21 | 'The renderer process is not built yet. Build it by running "npm run build:renderer"', 22 | ), 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /.erb/scripts/check-native-dep.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import chalk from 'chalk'; 3 | import { execSync } from 'child_process'; 4 | import { dependencies } from '../../package.json'; 5 | 6 | if (dependencies) { 7 | const dependenciesKeys = Object.keys(dependencies); 8 | const nativeDeps = fs 9 | .readdirSync('node_modules') 10 | .filter((folder) => fs.existsSync(`node_modules/${folder}/binding.gyp`)); 11 | if (nativeDeps.length === 0) { 12 | process.exit(0); 13 | } 14 | try { 15 | // Find the reason for why the dependency is installed. If it is installed 16 | // because of a devDependency then that is okay. Warn when it is installed 17 | // because of a dependency 18 | const { dependencies: dependenciesObject } = JSON.parse( 19 | execSync(`npm ls ${nativeDeps.join(' ')} --json`).toString(), 20 | ); 21 | const rootDependencies = Object.keys(dependenciesObject); 22 | const filteredRootDependencies = rootDependencies.filter((rootDependency) => 23 | dependenciesKeys.includes(rootDependency), 24 | ); 25 | if (filteredRootDependencies.length > 0) { 26 | const plural = filteredRootDependencies.length > 1; 27 | console.log(` 28 | ${chalk.whiteBright.bgYellow.bold( 29 | 'Webpack does not work with native dependencies.', 30 | )} 31 | ${chalk.bold(filteredRootDependencies.join(', '))} ${ 32 | plural ? 'are native dependencies' : 'is a native dependency' 33 | } and should be installed inside of the "./release/app" folder. 34 | First, uninstall the packages from "./package.json": 35 | ${chalk.whiteBright.bgGreen.bold('npm uninstall your-package')} 36 | ${chalk.bold( 37 | 'Then, instead of installing the package to the root "./package.json":', 38 | )} 39 | ${chalk.whiteBright.bgRed.bold('npm install your-package')} 40 | ${chalk.bold('Install the package to "./release/app/package.json"')} 41 | ${chalk.whiteBright.bgGreen.bold( 42 | 'cd ./release/app && npm install your-package', 43 | )} 44 | Read more about native dependencies at: 45 | ${chalk.bold( 46 | 'https://electron-react-boilerplate.js.org/docs/adding-dependencies/#module-structure', 47 | )} 48 | `); 49 | process.exit(1); 50 | } 51 | } catch (e) { 52 | console.log('Native dependencies could not be checked'); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.erb/scripts/check-node-env.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | export default function checkNodeEnv(expectedEnv) { 4 | if (!expectedEnv) { 5 | throw new Error('"expectedEnv" not set'); 6 | } 7 | 8 | if (process.env.NODE_ENV !== expectedEnv) { 9 | console.log( 10 | chalk.whiteBright.bgRed.bold( 11 | `"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config`, 12 | ), 13 | ); 14 | process.exit(2); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.erb/scripts/check-port-in-use.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import detectPort from 'detect-port'; 3 | 4 | const port = process.env.PORT || '1212'; 5 | 6 | detectPort(port, (err, availablePort) => { 7 | if (port !== String(availablePort)) { 8 | throw new Error( 9 | chalk.whiteBright.bgRed.bold( 10 | `Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4343 npm start`, 11 | ), 12 | ); 13 | } else { 14 | process.exit(0); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /.erb/scripts/clean.js: -------------------------------------------------------------------------------- 1 | import rimraf from 'rimraf'; 2 | import webpackPaths from '../configs/webpack.paths.ts'; 3 | import process from 'process'; 4 | 5 | const args = process.argv.slice(2); 6 | const commandMap = { 7 | dist: webpackPaths.distPath, 8 | release: webpackPaths.releasePath, 9 | dll: webpackPaths.dllPath, 10 | }; 11 | 12 | args.forEach((x) => { 13 | const pathToRemove = commandMap[x]; 14 | if (pathToRemove !== undefined) { 15 | rimraf.sync(pathToRemove); 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /.erb/scripts/delete-source-maps.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import rimraf from 'rimraf'; 3 | import webpackPaths from '../configs/webpack.paths'; 4 | 5 | export default function deleteSourceMaps() { 6 | rimraf.sync(path.join(webpackPaths.distMainPath, '*.js.map')); 7 | rimraf.sync(path.join(webpackPaths.distRendererPath, '*.js.map')); 8 | } 9 | -------------------------------------------------------------------------------- /.erb/scripts/download-swap-binaries.ts: -------------------------------------------------------------------------------- 1 | import path, { join } from 'path'; 2 | import download from 'download'; 3 | import { chmod, emptyDir, ensureDir, stat, move } from 'fs-extra'; 4 | import { constants } from 'fs'; 5 | import { Binary } from '../../src/models/downloaderModel'; 6 | 7 | const swapBinDir = path.join(__dirname, '../../build/bin/swap'); 8 | 9 | async function makeFileExecutable(binary: Binary) { 10 | const fullPath = path.join(binary.dirPath, binary.fileName); 11 | const { mode } = await stat(fullPath); 12 | await chmod( 13 | fullPath, 14 | // eslint-disable-next-line no-bitwise 15 | mode | constants.S_IXUSR | constants.S_IXGRP | constants.S_IXOTH, 16 | ); 17 | } 18 | 19 | const CLI_VERSION = '0.13.4'; 20 | // Ensure the value here matches with the one in src/main/cli/dirs.ts 21 | const CLI_FILE_NAME_VERSION_PREFIX = '0_13_4_'; 22 | 23 | const binaries = [ 24 | { 25 | dest: path.join(swapBinDir, 'linux'), 26 | url: `https://github.com/comit-network/xmr-btc-swap/releases/download/${CLI_VERSION}/swap_${CLI_VERSION}_Linux_x86_64.tar`, 27 | filename: 'swap', 28 | }, 29 | { 30 | dest: path.join(swapBinDir, 'mac'), 31 | url: `https://github.com/comit-network/xmr-btc-swap/releases/download/${CLI_VERSION}/swap_${CLI_VERSION}_Darwin_x86_64.tar`, 32 | filename: 'swap', 33 | }, 34 | { 35 | dest: path.join(swapBinDir, 'win'), 36 | url: `https://github.com/comit-network/xmr-btc-swap/releases/download/${CLI_VERSION}/swap_${CLI_VERSION}_Windows_x86_64.zip`, 37 | filename: 'swap.exe', 38 | }, 39 | ]; 40 | 41 | console.log(`Downloading ${binaries.length} swap binaries...`); 42 | Promise.all( 43 | binaries.map(async (binary) => { 44 | console.log(`Downloading and extracting ${binary.url} to ${binary.dest}`); 45 | await ensureDir(binary.dest); 46 | await emptyDir(binary.dest); 47 | await download(binary.url, binary.dest, { 48 | extract: true, 49 | }); 50 | 51 | // Append the prefix to the binary filename 52 | const newFilename = `${CLI_FILE_NAME_VERSION_PREFIX}${binary.filename}`; 53 | await move( 54 | join(binary.dest, binary.filename), 55 | join(binary.dest, newFilename), 56 | ); 57 | binary.filename = newFilename; 58 | 59 | // Chmod binary in the directory to make them executable 60 | await makeFileExecutable({ 61 | dirPath: binary.dest, 62 | fileName: binary.filename, 63 | }); 64 | 65 | console.log( 66 | `Downloaded and extracted ${binary.url} to ${binary.dest} as ${binary.filename}`, 67 | ); 68 | }), 69 | ) 70 | .then(() => { 71 | console.log(`Successfully downloaded ${binaries.length} swap binaries!`); 72 | process.exit(0); 73 | return 0; 74 | }) 75 | .catch((error) => { 76 | console.error(`Failed to download swap binaries! Error: ${error}`); 77 | process.exit(1); 78 | }); 79 | -------------------------------------------------------------------------------- /.erb/scripts/download-tor-binaries.ts: -------------------------------------------------------------------------------- 1 | import path, { join } from 'path'; 2 | import { chmod, emptyDir, stat } from 'fs-extra'; 3 | import { spawn } from 'child_process'; 4 | import { Binary } from '../../src/models/downloaderModel'; 5 | import { constants } from 'fs'; 6 | 7 | const TOR_BINARIES_GIT_URL = 8 | 'https://github.com/UnstoppableSwap/static-tor-binaries.git'; 9 | const torBuildDir = join(__dirname, '../../build/bin/tor'); 10 | 11 | async function makeFileExecutable(binary: Binary) { 12 | const fullPath = path.join(binary.dirPath, binary.fileName); 13 | const { mode } = await stat(fullPath); 14 | await chmod( 15 | fullPath, 16 | // eslint-disable-next-line no-bitwise 17 | mode | constants.S_IXUSR | constants.S_IXGRP | constants.S_IXOTH, 18 | ); 19 | } 20 | 21 | const extractedTorBinaryPaths = [ 22 | [join(torBuildDir, 'linux'), 'tor'], 23 | [join(torBuildDir, 'mac'), 'tor'], 24 | [join(torBuildDir, 'win'), 'tor.exe'], 25 | ]; 26 | 27 | // Delete tor binaries, use Node fs API instead of spawn 28 | // to avoid spawning a process that we need to kill. 29 | emptyDir(torBuildDir, (error) => { 30 | if (error) { 31 | console.error(`Failed to delete tor binaries! Error: ${error}`); 32 | process.exit(1); 33 | } 34 | 35 | const ls = spawn('git', ['clone', '-v', TOR_BINARIES_GIT_URL, torBuildDir]); 36 | 37 | ls.stdout.on('data', (data: unknown) => { 38 | console.log(`Tor Downloader: ${data}`); 39 | }); 40 | 41 | ls.stderr.on('data', (data: unknown) => { 42 | console.log(`Tor Downloader: ${data}`); 43 | }); 44 | 45 | ls.on('error', (e: unknown) => { 46 | console.log(`Tor Downloader Error: ${e}`); 47 | }); 48 | 49 | ls.on('close', async (code: number) => { 50 | console.log(`Tor Downloader process exited with code ${code}`); 51 | if (code === 0) { 52 | console.log('Tor binaries downloaded successfully!'); 53 | await Promise.all( 54 | extractedTorBinaryPaths.map(async (binary) => { 55 | await makeFileExecutable({ 56 | dirPath: binary[0], 57 | fileName: binary[1], 58 | }); 59 | }), 60 | ); 61 | } else { 62 | console.log('Tor binaries download failed!'); 63 | process.exit(1); 64 | } 65 | }); 66 | 67 | ls.on('spawn', () => { 68 | console.log( 69 | `Downloading precompiled tor binaries from ${TOR_BINARIES_GIT_URL}`, 70 | ); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /.erb/scripts/electron-rebuild.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { execSync } from 'child_process'; 3 | import fs from 'fs'; 4 | import { dependencies } from '../../release/app/package.json'; 5 | import webpackPaths from '../configs/webpack.paths'; 6 | 7 | if ( 8 | Object.keys(dependencies || {}).length > 0 && 9 | fs.existsSync(webpackPaths.appNodeModulesPath) 10 | ) { 11 | const electronRebuildCmd = 12 | '../../node_modules/.bin/electron-rebuild --parallel --force --types prod,dev,optional --module-dir .'; 13 | const cmd = 14 | process.platform === 'win32' 15 | ? electronRebuildCmd.replace(/\//g, '\\') 16 | : electronRebuildCmd; 17 | execSync(cmd, { 18 | cwd: webpackPaths.appPath, 19 | stdio: 'inherit', 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /.erb/scripts/link-modules.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import webpackPaths from '../configs/webpack.paths'; 3 | 4 | const srcNodeModulesPath = webpackPaths.srcNodeModulesPath; 5 | const appNodeModulesPath = webpackPaths.appNodeModulesPath; 6 | 7 | if (!fs.existsSync(srcNodeModulesPath) && fs.existsSync(appNodeModulesPath)) { 8 | fs.symlinkSync(appNodeModulesPath, srcNodeModulesPath, 'junction'); 9 | } 10 | -------------------------------------------------------------------------------- /.erb/scripts/notarize.js: -------------------------------------------------------------------------------- 1 | const { notarize } = require('@electron/notarize'); 2 | const { build } = require('../../package.json'); 3 | 4 | exports.default = async function notarizeMacos(context) { 5 | const { electronPlatformName, appOutDir } = context; 6 | if (electronPlatformName !== 'darwin') { 7 | return; 8 | } 9 | 10 | if (process.env.CI !== 'true') { 11 | console.warn('Skipping notarizing step. Packaging is not running in CI'); 12 | return; 13 | } 14 | 15 | if ( 16 | !('APPLE_ID' in process.env && 'APPLE_APP_SPECIFIC_PASSWORD' in process.env) 17 | ) { 18 | console.warn( 19 | 'Skipping notarizing step. APPLE_ID and APPLE_APP_SPECIFIC_PASSWORD env variables must be set', 20 | ); 21 | return; 22 | } 23 | 24 | const appName = context.packager.appInfo.productFilename; 25 | 26 | await notarize({ 27 | appBundleId: build.appId, 28 | appPath: `${appOutDir}/${appName}.app`, 29 | appleId: process.env.APPLE_ID, 30 | appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD, 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Coverage directory used by tools like istanbul 11 | coverage 12 | .eslintcache 13 | 14 | # Dependency directory 15 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 16 | node_modules 17 | 18 | # OSX 19 | .DS_Store 20 | 21 | release/app/dist 22 | release/build 23 | .erb/dll 24 | 25 | .idea 26 | npm-debug.log.* 27 | *.css.d.ts 28 | *.sass.d.ts 29 | *.scss.d.ts 30 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'erb', 3 | rules: { 4 | 'import/no-extraneous-dependencies': 'off', 5 | 'react/react-in-jsx-scope': 'off', 6 | 'import/no-named-as-default': 'off', 7 | 'react/jsx-props-no-spreading': 'off', 8 | 'react/jsx-no-bind': 'off', 9 | 'promise/param-names': 'off', 10 | 'no-async-promise-executor': 'off', 11 | 'no-await-in-loop': 'off', 12 | 'import/prefer-default-export': 'off', 13 | 'import/no-restricted-paths': [ 14 | 'error', 15 | { 16 | basePath: './src', 17 | zones: [ 18 | { target: './renderer', from: './main' }, 19 | { target: './main', from: './renderer' }, 20 | ], 21 | }, 22 | ], 23 | '@typescript-eslint/switch-exhaustiveness-check': 'error', 24 | }, 25 | parserOptions: { 26 | ecmaVersion: 2020, 27 | sourceType: 'module', 28 | project: './tsconfig.json', 29 | tsconfigRootDir: __dirname, 30 | createDefaultProgram: true, 31 | }, 32 | settings: { 33 | 'import/resolver': { 34 | // See https://github.com/benmosher/eslint-plugin-import/issues/1396#issuecomment-575727774 for line below 35 | node: {}, 36 | webpack: { 37 | config: require.resolve('./.erb/configs/webpack.config.eslint.ts'), 38 | }, 39 | typescript: {}, 40 | }, 41 | 'import/parsers': { 42 | '@typescript-eslint/parser': ['.ts', '.tsx'], 43 | }, 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | *.exe binary 3 | *.png binary 4 | *.jpg binary 5 | *.jpeg binary 6 | *.ico binary 7 | *.icns binary 8 | *.eot binary 9 | *.otf binary 10 | *.ttf binary 11 | *.woff binary 12 | *.woff2 binary 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: You're having technical issues. 🐞 4 | labels: 'bug' 5 | --- 6 | 7 | ## Expected Behavior 8 | 9 | 10 | 11 | ## Current Behavior 12 | 13 | 14 | 15 | ## Steps to Reproduce 16 | 17 | 18 | 19 | 20 | 1. 21 | 22 | 2. 23 | 24 | 3. 25 | 26 | 4. 27 | 28 | ## Possible Solution (Not obligatory) 29 | 30 | 31 | 32 | ## Your Environment 33 | 34 | 35 | 36 | - Node version : 37 | - Operating System and version : 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-Question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question.❓ 4 | labels: 'question' 5 | --- 6 | 7 | ## Summary 8 | 9 | 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3-Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: You want something added 🎉 4 | labels: 'enhancement' 5 | --- 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/4-Problem_With_Swap.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue with swap 3 | about: You've started a swap and are now facing an issue 4 | labels: 'Ongoing swap' 5 | --- 6 | 7 | ## Explain what you did 8 | 9 | 10 | 11 | ## Which state is your swap in? 12 | 13 | 14 | 15 | ## What is the problem? 16 | 17 | 18 | 19 | ## Logs 20 | 21 | 22 | 23 | ## Your Environment 24 | 25 | - Operating System: 26 | - GUI Version: 27 | -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | requiredHeaders: 2 | - Prerequisites 3 | - Expected Behavior 4 | - Current Behavior 5 | - Possible Solution 6 | - Your Environment 7 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - discussion 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | publish: 10 | runs-on: ${{ matrix.os }} 11 | environment: production-notarize-environment 12 | strategy: 13 | matrix: 14 | os: [macos-12] 15 | 16 | steps: 17 | - name: Checkout git repo 18 | uses: actions/checkout@v1 19 | 20 | - name: Install wine 21 | run: | 22 | brew install --cask wine-stable 23 | 24 | - name: Install Node and NPM 25 | uses: actions/setup-node@v1 26 | with: 27 | node-version: 18 28 | cache: 'npm' 29 | 30 | - name: Install python setup tools 31 | run: | 32 | pip install setuptools 33 | 34 | - name: Install dependencies 35 | run: | 36 | npm install 37 | 38 | - name: Publish releases 39 | env: 40 | # These values are used for auto updates signing 41 | APPLE_ID: ${{ secrets.APPLE_ID }} 42 | APPLE_ID_PASS: ${{ secrets.APPLE_ID_PASS }} 43 | APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} 44 | CSC_LINK: ${{ secrets.CSC_LINK }} 45 | CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} 46 | 47 | # This is used for uploading release assets to github 48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | EP_GH_IGNORE_TIME: true 50 | run: | 51 | npm run build 52 | npm exec electron-builder -- --publish always --win --mac --linux 53 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | release: 7 | runs-on: ${{ matrix.os }} 8 | 9 | strategy: 10 | matrix: 11 | os: [macos-11, ubuntu-latest] 12 | 13 | steps: 14 | - name: Check out Git repository 15 | uses: actions/checkout@v1 16 | 17 | - name: Install Node.js and NPM 18 | uses: actions/setup-node@v2 19 | with: 20 | node-version: 16 21 | cache: npm 22 | 23 | - name: Install python setup tools 24 | run: | 25 | pip install setuptools 26 | 27 | - name: npm install 28 | run: | 29 | npm install 30 | 31 | - name: npm test 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | run: | 35 | npm run package 36 | npm run lint 37 | npm exec tsc 38 | npm test 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # swap binary directory 2 | build/bin/** 3 | 4 | # Logs 5 | logs 6 | *.log 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | .eslintcache 16 | 17 | # Dependency directory 18 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 19 | node_modules 20 | 21 | # OSX 22 | .DS_Store 23 | 24 | release/app/dist 25 | release/build 26 | .erb/dll 27 | 28 | .idea 29 | npm-debug.log.* 30 | *.css.d.ts 31 | *.sass.d.ts 32 | *.scss.d.ts 33 | .vscode/launch.json 34 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint", "EditorConfig.EditorConfig"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | ".eslintrc": "jsonc", 4 | ".prettierrc": "jsonc", 5 | ".eslintignore": "ignore" 6 | }, 7 | 8 | "javascript.validate.enable": false, 9 | "javascript.format.enable": false, 10 | "typescript.format.enable": false, 11 | 12 | "search.exclude": { 13 | ".git": true, 14 | ".eslintcache": true, 15 | ".erb/dll": true, 16 | "release/{build,app/dist}": true, 17 | "node_modules": true, 18 | "npm-debug.log.*": true, 19 | "test/**/__snapshots__": true, 20 | "package-lock.json": true, 21 | "*.{css,sass,scss}.d.ts": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "label": "Start Webpack Dev", 7 | "script": "start:renderer", 8 | "options": { 9 | "cwd": "${workspaceFolder}" 10 | }, 11 | "isBackground": true, 12 | "problemMatcher": { 13 | "owner": "custom", 14 | "pattern": { 15 | "regexp": "____________" 16 | }, 17 | "background": { 18 | "activeOnStart": true, 19 | "beginsPattern": "Compiling\\.\\.\\.$", 20 | "endsPattern": "(Compiled successfully|Failed to compile)\\.$" 21 | } 22 | } 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /assets/assets.d.ts: -------------------------------------------------------------------------------- 1 | type Styles = Record; 2 | 3 | declare module '*.svg' { 4 | const content: string; 5 | export default content; 6 | } 7 | 8 | declare module '*.png' { 9 | const content: string; 10 | export default content; 11 | } 12 | 13 | declare module '*.jpg' { 14 | const content: string; 15 | export default content; 16 | } 17 | 18 | declare module '*.scss' { 19 | const content: Styles; 20 | export default content; 21 | } 22 | 23 | declare module '*.sass' { 24 | const content: Styles; 25 | export default content; 26 | } 27 | 28 | declare module '*.css' { 29 | const content: Styles; 30 | export default content; 31 | } 32 | -------------------------------------------------------------------------------- /assets/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-unsigned-executable-memory 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /assets/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icon.icns -------------------------------------------------------------------------------- /assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icon.ico -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icon.png -------------------------------------------------------------------------------- /assets/icons/100x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icons/100x100.png -------------------------------------------------------------------------------- /assets/icons/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icons/1024x1024.png -------------------------------------------------------------------------------- /assets/icons/114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icons/114x114.png -------------------------------------------------------------------------------- /assets/icons/120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icons/120x120.png -------------------------------------------------------------------------------- /assets/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icons/128x128.png -------------------------------------------------------------------------------- /assets/icons/144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icons/144x144.png -------------------------------------------------------------------------------- /assets/icons/152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icons/152x152.png -------------------------------------------------------------------------------- /assets/icons/167x167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icons/167x167.png -------------------------------------------------------------------------------- /assets/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icons/16x16.png -------------------------------------------------------------------------------- /assets/icons/172x172.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icons/172x172.png -------------------------------------------------------------------------------- /assets/icons/180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icons/180x180.png -------------------------------------------------------------------------------- /assets/icons/196x196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icons/196x196.png -------------------------------------------------------------------------------- /assets/icons/20x20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icons/20x20.png -------------------------------------------------------------------------------- /assets/icons/216x216.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icons/216x216.png -------------------------------------------------------------------------------- /assets/icons/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icons/24x24.png -------------------------------------------------------------------------------- /assets/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icons/256x256.png -------------------------------------------------------------------------------- /assets/icons/29x29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icons/29x29.png -------------------------------------------------------------------------------- /assets/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icons/32x32.png -------------------------------------------------------------------------------- /assets/icons/40x40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icons/40x40.png -------------------------------------------------------------------------------- /assets/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icons/48x48.png -------------------------------------------------------------------------------- /assets/icons/50x50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icons/50x50.png -------------------------------------------------------------------------------- /assets/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icons/512x512.png -------------------------------------------------------------------------------- /assets/icons/55x55.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icons/55x55.png -------------------------------------------------------------------------------- /assets/icons/57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icons/57x57.png -------------------------------------------------------------------------------- /assets/icons/58x58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icons/58x58.png -------------------------------------------------------------------------------- /assets/icons/60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icons/60x60.png -------------------------------------------------------------------------------- /assets/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icons/64x64.png -------------------------------------------------------------------------------- /assets/icons/72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icons/72x72.png -------------------------------------------------------------------------------- /assets/icons/76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icons/76x76.png -------------------------------------------------------------------------------- /assets/icons/80x80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icons/80x80.png -------------------------------------------------------------------------------- /assets/icons/87x87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icons/87x87.png -------------------------------------------------------------------------------- /assets/icons/88x88.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icons/88x88.png -------------------------------------------------------------------------------- /assets/icons/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnstoppableSwap/unstoppableswap-gui/9a20659f51cc4ab063628d2fc0aca2e05e577156/assets/icons/96x96.png -------------------------------------------------------------------------------- /release/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unstoppableswap-gui", 3 | "version": "0.6.4", 4 | "description": "Graphical User Interface for XMR<>BTC Atomic Swaps", 5 | "main": "./dist/main/main.js", 6 | "scripts": { 7 | "electron-rebuild": "node -r ts-node/register ../../.erb/scripts/electron-rebuild.js", 8 | "link-modules": "node -r ts-node/register ../../.erb/scripts/link-modules.ts", 9 | "postinstall": "npm run electron-rebuild && npm run link-modules" 10 | }, 11 | "author": { 12 | "name": "binarybaron", 13 | "url": "https://unstoppableswap.net", 14 | "email": "binarybaron@protonmail.com" 15 | }, 16 | "dependencies": { 17 | "jayson": "^4.0.0" 18 | }, 19 | "license": "MIT" 20 | } 21 | -------------------------------------------------------------------------------- /src/__tests__/mock_cli_logs/cli_log_advancing_state_btc_redeemed.json: -------------------------------------------------------------------------------- 1 | { 2 | "timestamp": "Jan 09 14:21:59.387", 3 | "level": "DEBUG", 4 | "fields": { 5 | "message": "Advancing state", 6 | "state": "btc is redeemed" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/__tests__/mock_cli_logs/cli_log_advancing_state_xmr_is_locked.json: -------------------------------------------------------------------------------- 1 | { 2 | "timestamp": "Jan 09 14:20:53.479", 3 | "level": "DEBUG", 4 | "fields": { 5 | "message": "Advancing state", 6 | "state": "xmr is locked" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/__tests__/mock_cli_logs/cli_log_alice_locked_monero.json: -------------------------------------------------------------------------------- 1 | { 2 | "timestamp": "2021-09-05 03:56:52", 3 | "level": "INFO", 4 | "fields": { 5 | "message": "Alice locked Monero", 6 | "txid": "cb46ad562ffc868a7c2d8c72cecd9090cca7b6f102199db6a6cbef65afeb09d1" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/__tests__/mock_cli_logs/cli_log_bitcoin_transaction_status_changed.json: -------------------------------------------------------------------------------- 1 | { 2 | "timestamp": "2021-09-05 03:56:02", 3 | "level": "DEBUG", 4 | "fields": { 5 | "message": "Bitcoin transaction status changed", 6 | "txid": "6297106e3fb91cfb94e5b069af03248ebfdc63087db4a19c833f76df1b9aff51", 7 | "new_status": "confirmed with 3 blocks" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/__tests__/mock_cli_logs/cli_log_checked_bitcoin_balance.json: -------------------------------------------------------------------------------- 1 | { 2 | "timestamp": "2021-12-23 15:50:39", 3 | "level": "INFO", 4 | "fields": { 5 | "message": "Checked Bitcoin balance", 6 | "balance": "0.10000000 BTC" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/__tests__/mock_cli_logs/cli_log_manually_cancelling_swap.json: -------------------------------------------------------------------------------- 1 | { 2 | "timestamp": "2021-09-05 03:41:07", 3 | "level": "INFO", 4 | "fields": { 5 | "message": "Manually cancelling swap", 6 | "swap_id": "2a034c59-72bc-4b7b-839f-d32522099bcc" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/__tests__/mock_cli_logs/cli_log_published_btc_cancel_tx.json: -------------------------------------------------------------------------------- 1 | { 2 | "timestamp": "2021-09-05 03:41:07", 3 | "level": "INFO", 4 | "fields": { 5 | "message": "Published Bitcoin transaction", 6 | "txid": "4b4f379f34e88084d0443886942d4f059a1ae1cc91102adae5654f4b3ea980f7", 7 | "kind": "cancel" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/__tests__/mock_cli_logs/cli_log_published_btc_lock_tx.json: -------------------------------------------------------------------------------- 1 | { 2 | "timestamp": "2021-09-05 03:41:07", 3 | "level": "INFO", 4 | "fields": { 5 | "message": "Published Bitcoin transaction", 6 | "txid": "6297106e3fb91cfb94e5b069af03248ebfdc63087db4a19c833f76df1b9aff51", 7 | "kind": "lock" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/__tests__/mock_cli_logs/cli_log_published_btc_refund_tx.json: -------------------------------------------------------------------------------- 1 | { 2 | "timestamp": "2021-09-05 03:41:07", 3 | "level": "INFO", 4 | "fields": { 5 | "message": "Published Bitcoin transaction", 6 | "txid": "4dfb63a139d5f00d31b55beeabcf229647f18d6f68c44e09d7750ee185a6b1f2", 7 | "kind": "refund" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/__tests__/mock_cli_logs/cli_log_published_btc_withdraw_tx.json: -------------------------------------------------------------------------------- 1 | { 2 | "timestamp": "2021-11-05 21:06:35", 3 | "level": "INFO", 4 | "fields": { 5 | "message": "Published Bitcoin transaction", 6 | "txid": "3462e1179c6035120608921bf1177c65456bd35fd31ed37545a19fd58818f796", 7 | "kind": "withdraw" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/__tests__/mock_cli_logs/cli_log_received_bitcoin.json: -------------------------------------------------------------------------------- 1 | { 2 | "timestamp": "2021-09-05 03:41:03", 3 | "level": "INFO", 4 | "fields": { 5 | "message": "Received Bitcoin", 6 | "new_balance": "0.00100000 BTC", 7 | "max_giveable": "0.00099878 BTC" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/__tests__/mock_cli_logs/cli_log_received_new_conf_for_monero_lock_tx.json: -------------------------------------------------------------------------------- 1 | { 2 | "timestamp": "2021-09-05 03:57:16", 3 | "level": "INFO", 4 | "fields": { 5 | "message": "Received new confirmation for Monero lock tx", 6 | "txid": "cb46ad562ffc868a7c2d8c72cecd9090cca7b6f102199db6a6cbef65afeb09d1", 7 | "seen_confirmations": "1", 8 | "needed_confirmations": "10" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/__tests__/mock_cli_logs/cli_log_received_quote.json: -------------------------------------------------------------------------------- 1 | { 2 | "timestamp": "2021-09-05 03:40:36", 3 | "level": "INFO", 4 | "fields": { 5 | "message": "Received quote", 6 | "price": "0.00610233 BTC", 7 | "minimum_amount": "0.00010000 BTC", 8 | "maximum_amount": "0.10000000 BTC" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/__tests__/mock_cli_logs/cli_log_redeemed_xmr.json: -------------------------------------------------------------------------------- 1 | { 2 | "timestamp": "2021-09-05 04:07:37", 3 | "level": "INFO", 4 | "fields": { 5 | "message": "Successfully transferred XMR to wallet", 6 | "monero_receive_address": "59McWTPGc745SRWrSMoh8oTjoXoQq6sPUgKZ66dQWXuKFQ2q19h9gvhJNZcFTizcnT12r63NFgHiGd6gBCjabzmzHAMoyD6", 7 | "txid": "eadda576b5929c55bcc58f55c24bb52ac1853edb7d3b068ab67a3f66b0a1c546" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/__tests__/mock_cli_logs/cli_log_starting_new_swap.json: -------------------------------------------------------------------------------- 1 | { 2 | "timestamp": "2021-09-05 03:41:03", 3 | "level": "INFO", 4 | "fields": { 5 | "message": "Starting new swap", 6 | "swap_id": "2a034c59-72bc-4b7b-839f-d32522099bcc" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/__tests__/mock_cli_logs/cli_log_waiting_for_bitcoin_deposit.json: -------------------------------------------------------------------------------- 1 | { 2 | "timestamp": "2021-09-05 03:40:36", 3 | "level": "INFO", 4 | "fields": { 5 | "message": "Waiting for Bitcoin deposit", 6 | "deposit_address": "tb1qajq94d72k9hhcmtrlwhfuhc5yz0w298uym980g", 7 | "max_giveable": "0.00000000 BTC", 8 | "minimum_amount": "0.00010000 BTC", 9 | "maximum_amount": "0.10000000 BTC", 10 | "min_deposit": "0.00011000 BTC" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/__tests__/mock_cli_logs/cli_log_waiting_for_btc_refund_tx_finality_init.json: -------------------------------------------------------------------------------- 1 | { 2 | "timestamp": "2021-09-05 03:41:07", 3 | "level": "INFO", 4 | "fields": { 5 | "message": "Waiting for Bitcoin transaction finality", 6 | "txid": "4dfb63a139d5f00d31b55beeabcf229647f18d6f68c44e09d7750ee185a6b1f2", 7 | "required_confirmation": "2" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/__tests__/mock_cli_logs/cli_log_waiting_for_btc_refund_tx_finality_update.json: -------------------------------------------------------------------------------- 1 | { 2 | "timestamp": "2021-09-05 03:41:07", 3 | "level": "INFO", 4 | "fields": { 5 | "message": "Waiting for Bitcoin transaction finality", 6 | "txid": "4dfb63a139d5f00d31b55beeabcf229647f18d6f68c44e09d7750ee185a6b1f2", 7 | "needed_confirmations": "2", 8 | "seen_confirmations": "1" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/__tests__/store/config.spec.ts: -------------------------------------------------------------------------------- 1 | import { isTestnet } from 'store/config'; 2 | 3 | test('should detect testnet environment', () => { 4 | process.env.TESTNET = 'true'; 5 | expect(isTestnet()).toBe(true); 6 | 7 | process.env.TESTNET = 'TRUE'; 8 | expect(isTestnet()).toBe(true); 9 | 10 | process.env.TESTNET = 'false'; 11 | expect(isTestnet()).toBe(false); 12 | 13 | process.env.TESTNET = 'FALSE'; 14 | expect(isTestnet()).toBe(false); 15 | 16 | process.env.TESTNET = undefined; 17 | expect(isTestnet()).toBe(false); 18 | }); 19 | -------------------------------------------------------------------------------- /src/__tests__/store/listSellersSlice.spec.ts: -------------------------------------------------------------------------------- 1 | import { TextEncoder, TextDecoder } from 'util'; 2 | 3 | import { AnyAction } from '@reduxjs/toolkit'; 4 | import { ExtendedProviderStatus } from 'models/apiModel'; 5 | import reducer, { 6 | setRegistryProviders, 7 | } from '../../store/features/providersSlice'; 8 | 9 | Object.assign(global, { TextDecoder, TextEncoder }); 10 | 11 | const exampleTestnetProvider: ExtendedProviderStatus = { 12 | multiAddr: '/dnsaddr/t.xmr.example', 13 | peerId: '12394294389438924', 14 | testnet: true, 15 | age: 5, 16 | uptime: 0.99, 17 | maxSwapAmount: 1, 18 | minSwapAmount: 0.1, 19 | price: 0.1, 20 | relevancy: 1, 21 | }; 22 | 23 | const exampleMainnetProvider: ExtendedProviderStatus = { 24 | multiAddr: '/dnsaddr/xmr.example', 25 | peerId: '32394294389438924', 26 | testnet: false, 27 | age: 5, 28 | uptime: 0.99, 29 | maxSwapAmount: 1, 30 | minSwapAmount: 0.1, 31 | price: 0.1, 32 | relevancy: 1, 33 | }; 34 | 35 | const initialState = { 36 | rendezvous: { 37 | providers: [], 38 | processRunning: false, 39 | exitCode: null, 40 | stdOut: '', 41 | logs: [], 42 | }, 43 | registry: { 44 | providers: null, 45 | failedReconnectAttemptsSinceLastSuccess: 0, 46 | }, 47 | selectedProvider: null, 48 | }; 49 | 50 | test('should return the initial state', () => { 51 | expect(reducer(undefined, {} as AnyAction)).toEqual(initialState); 52 | }); 53 | 54 | describe('testnet', () => { 55 | beforeAll(() => { 56 | process.env.TESTNET = 'true'; 57 | }); 58 | 59 | test('should set and filter the provider list', () => { 60 | expect( 61 | reducer( 62 | initialState, 63 | setRegistryProviders([exampleMainnetProvider, exampleTestnetProvider]), 64 | ), 65 | ).toMatchObject({ 66 | registry: { 67 | providers: [exampleTestnetProvider], 68 | failedReconnectAttemptsSinceLastSuccess: 0, 69 | }, 70 | selectedProvider: exampleTestnetProvider, 71 | }); 72 | }); 73 | }); 74 | 75 | describe('mainnet', () => { 76 | beforeAll(() => { 77 | process.env.TESTNET = 'false'; 78 | }); 79 | 80 | test('should set and filter the provider list', () => { 81 | expect( 82 | reducer( 83 | initialState, 84 | setRegistryProviders([exampleMainnetProvider, exampleTestnetProvider]), 85 | ), 86 | ).toMatchObject({ 87 | registry: { 88 | providers: [exampleMainnetProvider], 89 | failedReconnectAttemptsSinceLastSuccess: 0, 90 | }, 91 | rendezvous: { 92 | providers: [], 93 | processRunning: false, 94 | exitCode: null, 95 | stdOut: '', 96 | logs: [], 97 | }, 98 | selectedProvider: exampleMainnetProvider, 99 | }); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /src/__tests__/utils/parseUtils.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | extractAmountFromUnitString, 3 | getLinesOfString, 4 | parseDateString, 5 | } from '../../utils/parseUtils'; 6 | 7 | test('should parse btc amount string correctly', () => { 8 | expect(extractAmountFromUnitString('0.1 BTC')).toBe(0.1); 9 | expect(extractAmountFromUnitString('0.0045 BTC')).toBe(0.0045); 10 | }); 11 | 12 | test('should parse xmr amount string correctly', () => { 13 | expect(extractAmountFromUnitString('0.1 XMR')).toBe(0.1); 14 | expect(extractAmountFromUnitString('0.0045 XMR')).toBe(0.0045); 15 | }); 16 | 17 | test('should return null when parsing btc amount with invalid string', () => { 18 | expect(extractAmountFromUnitString('0.1')).toBeNull(); 19 | expect(extractAmountFromUnitString('BTC')).toBeNull(); 20 | expect(extractAmountFromUnitString('')).toBeNull(); 21 | expect(extractAmountFromUnitString(null as unknown as string)).toBeNull(); 22 | expect( 23 | extractAmountFromUnitString(undefined as unknown as string), 24 | ).toBeNull(); 25 | }); 26 | 27 | test('should parse UTC date string with offset correctly', () => { 28 | // TODO: Handle timezones properly 29 | expect( 30 | parseDateString('2021-12-29 14:25:59.64082 +00:00:00'), 31 | ).toBeGreaterThan(1640000000000); 32 | }); 33 | 34 | test('should throw error when parsing invalid date', () => { 35 | expect(() => parseDateString('20fdf21-12-29 14:25')).toThrow(); 36 | expect(() => parseDateString('20fdf21-12-29 14:25 Ms Ol23')).toThrow(); 37 | }); 38 | 39 | test('should extract lines from string and ignore empty oness', () => { 40 | expect(getLinesOfString(`hello\nworld`)).toStrictEqual(['hello', 'world']); 41 | expect(getLinesOfString(`hello\r\nworld`)).toStrictEqual(['hello', 'world']); 42 | expect( 43 | getLinesOfString(`hello 44 | world`), 45 | ).toStrictEqual(['hello', 'world']); 46 | expect( 47 | getLinesOfString(`hello 48 | 49 | world`), 50 | ).toStrictEqual(['hello', 'world']); 51 | }); 52 | -------------------------------------------------------------------------------- /src/main/dev-app-update.yml: -------------------------------------------------------------------------------- 1 | owner: UnstoppableSwap 2 | repo: unstoppableswap-gui 3 | provider: github 4 | releaseType: prerelease 5 | updaterCacheDirName: unstoppableswap-gui-updater 6 | -------------------------------------------------------------------------------- /src/main/stats.ts: -------------------------------------------------------------------------------- 1 | import { SwapStateName } from 'models/rpcModel'; 2 | import { sha256 } from 'utils/cryptoUtils'; 3 | import { Provider } from 'models/apiModel'; 4 | import { isTestnet } from 'store/config'; 5 | import { store } from 'main/store/mainStore'; 6 | import { isCliLogReceivedQuote } from 'models/cliModel'; 7 | import { 8 | transmitReceivedQuoteFromProvider, 9 | transmitSwapDetailsUpdated, 10 | } from './socket'; 11 | import { 12 | extractAmountFromUnitString, 13 | parseDateString, 14 | } from '../utils/parseUtils'; 15 | 16 | export default function initStats() { 17 | const timestampsOfTransmittedReceivedQuotes: string[] = []; 18 | const transmittedSwapDetails = new Map(); 19 | 20 | setInterval(() => { 21 | const state = store.getState(); 22 | 23 | const receivedQuoteLog = state.swap.logs.find(isCliLogReceivedQuote); 24 | const receivedQuoteProvider = state.swap.provider; 25 | if ( 26 | receivedQuoteLog && 27 | receivedQuoteProvider && 28 | !timestampsOfTransmittedReceivedQuotes.includes( 29 | receivedQuoteLog.timestamp, 30 | ) 31 | ) { 32 | const priceBtc = extractAmountFromUnitString( 33 | receivedQuoteLog.fields.price, 34 | ); 35 | const minimumAmountBtc = extractAmountFromUnitString( 36 | receivedQuoteLog.fields.minimum_amount, 37 | ); 38 | const maximumAmountBtc = extractAmountFromUnitString( 39 | receivedQuoteLog.fields.maximum_amount, 40 | ); 41 | 42 | if (priceBtc && minimumAmountBtc && maximumAmountBtc) { 43 | if ( 44 | transmitReceivedQuoteFromProvider( 45 | { 46 | multiAddr: receivedQuoteProvider.multiAddr, 47 | peerId: receivedQuoteProvider.peerId, 48 | testnet: receivedQuoteProvider.testnet, 49 | }, 50 | priceBtc, 51 | minimumAmountBtc, 52 | maximumAmountBtc, 53 | ) 54 | ) { 55 | timestampsOfTransmittedReceivedQuotes.push( 56 | receivedQuoteLog.timestamp, 57 | ); 58 | } 59 | } 60 | } 61 | 62 | Object.values(state.rpc.state.swapInfos).forEach((swap) => { 63 | const swapIdHash = sha256(swap.swapId); 64 | const { stateName } = swap; 65 | const provider: Provider = { 66 | multiAddr: swap.seller.addresses[0], 67 | peerId: swap.seller.peerId, 68 | testnet: isTestnet(), 69 | }; 70 | const firstEnteredDate = parseDateString(swap.startDate); 71 | 72 | if (transmittedSwapDetails.get(swapIdHash) !== stateName) { 73 | if ( 74 | transmitSwapDetailsUpdated( 75 | provider, 76 | swapIdHash, 77 | swap.xmrAmount, 78 | stateName, 79 | firstEnteredDate, 80 | ) 81 | ) { 82 | transmittedSwapDetails.set(swapIdHash, stateName); 83 | } 84 | } 85 | }); 86 | }, 1000 * 60); 87 | } 88 | -------------------------------------------------------------------------------- /src/main/store/mainStore.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import { stateSyncEnhancer } from 'electron-redux'; 3 | import { reducers } from 'store/combinedReducer'; 4 | import { createMainListeners } from './mainStoreListeners'; 5 | 6 | export const store = configureStore({ 7 | reducer: reducers, 8 | enhancers: [stateSyncEnhancer()], 9 | middleware: (getDefaultMiddleware) => 10 | getDefaultMiddleware().prepend(createMainListeners().middleware), 11 | }); 12 | 13 | export type AppDispatch = typeof store.dispatch; 14 | export type RootState = ReturnType; 15 | -------------------------------------------------------------------------------- /src/main/tor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChildProcessWithoutNullStreams, 3 | spawn as spawnProc, 4 | } from 'child_process'; 5 | import { store } from 'main/store/mainStore'; 6 | import { 7 | torProcessExited, 8 | torInitiate, 9 | torAppendStdOut, 10 | } from '../store/features/torSlice'; 11 | import logger from '../utils/logger'; 12 | import { getTorBinary, makeFileExecutable } from './cli/dirs'; 13 | 14 | let torProc: ChildProcessWithoutNullStreams | null = null; 15 | 16 | export function isTorRunning(): boolean { 17 | return torProc != null; 18 | } 19 | 20 | export function stopTor() { 21 | torProc?.kill(); 22 | torProc = null; 23 | } 24 | 25 | export async function spawnTor(): Promise { 26 | return new Promise(async (resolve, reject) => { 27 | if (torProc) { 28 | stopTor(); 29 | } 30 | 31 | const torBinary = getTorBinary(); 32 | 33 | try { 34 | await makeFileExecutable(torBinary); 35 | } catch (err) { 36 | logger.error({ err, torBinary }, 'Failed to make tor binary executable'); 37 | } 38 | 39 | torProc = spawnProc(`./${torBinary.fileName}`, { 40 | cwd: torBinary.dirPath, 41 | detached: false, 42 | }); 43 | 44 | torProc.on('error', (err) => { 45 | logger.error({ err, torBinary }, `Failed to spawn tor`); 46 | reject(err); 47 | }); 48 | 49 | // Added in: Node v15.1.0, v14.17.0 50 | torProc.on('spawn', () => { 51 | store.dispatch(torInitiate()); 52 | logger.info({ torBinary }, 'Tor spawned'); 53 | resolve(); 54 | }); 55 | 56 | torProc.on('exit', (exitCode, exitSignal) => { 57 | store.dispatch(torProcessExited({ exitCode, exitSignal })); 58 | logger.info({ exitCode, exitSignal, torBinary }, `Tor exited`); 59 | }); 60 | 61 | [torProc.stdout, torProc.stderr].forEach((stream) => 62 | stream.on('data', (data) => { 63 | const text = Buffer.from(data).toString(); 64 | store.dispatch(torAppendStdOut(text)); 65 | logger.debug({ text }, `Tor stdout/stderr`); 66 | }), 67 | ); 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /src/main/updater.ts: -------------------------------------------------------------------------------- 1 | import { UpdateInfo, autoUpdater } from 'electron-updater'; 2 | import logger from '../utils/logger'; 3 | import { isDevelopment } from 'store/config'; 4 | import { updateReceived } from 'store/features/updateSlice'; 5 | import { store } from './store/mainStore'; 6 | 7 | export default async function initAutoUpdater() { 8 | autoUpdater.on('update-available', (info: UpdateInfo) => { 9 | store.dispatch(updateReceived(info)); 10 | logger.info({ info }, 'Update available'); 11 | }); 12 | autoUpdater.on('update-not-available', (info: any) => { 13 | logger.info({ info }, 'Update not available'); 14 | }); 15 | autoUpdater.on('error', (err: any) => { 16 | logger.error({ err }, 'Update error'); 17 | }); 18 | autoUpdater.on('checking-for-update', () => { 19 | logger.info('Checking for update'); 20 | }); 21 | 22 | autoUpdater.autoDownload = false; 23 | autoUpdater.allowPrerelease = false; 24 | 25 | // This is for development purposes only. It will force the auto updater to use the dev-app-update.yml file for updates. 26 | if (isDevelopment) { 27 | autoUpdater.forceDevUpdateConfig = true; 28 | autoUpdater.allowDowngrade = true; 29 | } 30 | 31 | logger.info('Starting auto updater'); 32 | await autoUpdater.checkForUpdates(); 33 | } 34 | -------------------------------------------------------------------------------- /src/main/util.ts: -------------------------------------------------------------------------------- 1 | /* eslint import/no-mutable-exports: off */ 2 | import { URL } from 'url'; 3 | import path from 'path'; 4 | 5 | export let resolveHtmlPath: (htmlFileName: string) => string; 6 | 7 | if (process.env.NODE_ENV === 'development') { 8 | const port = process.env.PORT || 1212; 9 | resolveHtmlPath = (htmlFileName: string) => { 10 | const url = new URL(`http://localhost:${port}`); 11 | url.pathname = htmlFileName; 12 | return url.href; 13 | }; 14 | } else { 15 | resolveHtmlPath = (htmlFileName: string) => { 16 | return `file://${path.resolve(__dirname, '../renderer/', htmlFileName)}`; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/models/apiModel.ts: -------------------------------------------------------------------------------- 1 | export interface ExtendedProviderStatus extends ProviderStatus { 2 | uptime?: number; 3 | age?: number; 4 | relevancy?: number; 5 | version?: string; 6 | recommended?: boolean; 7 | } 8 | 9 | export interface ProviderStatus extends ProviderQuote, Provider {} 10 | 11 | export interface ProviderQuote { 12 | price: number; 13 | minSwapAmount: number; 14 | maxSwapAmount: number; 15 | } 16 | 17 | export interface Provider { 18 | multiAddr: string; 19 | testnet: boolean; 20 | peerId: string; 21 | } 22 | 23 | export interface Alert { 24 | id: number; 25 | title: string; 26 | body: string; 27 | severity: 'info' | 'warning' | 'error'; 28 | } 29 | -------------------------------------------------------------------------------- /src/models/downloaderModel.ts: -------------------------------------------------------------------------------- 1 | export interface Binary { 2 | dirPath: string; // Path without filename appended 3 | fileName: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/renderer/api.ts: -------------------------------------------------------------------------------- 1 | import { Alert, ExtendedProviderStatus } from 'models/apiModel'; 2 | 3 | const API_BASE_URL = 4 | process.env.OVERWRITE_API_ADDRESS || 'https://api.unstoppableswap.net'; 5 | 6 | export async function fetchProvidersViaHttp(): Promise< 7 | ExtendedProviderStatus[] 8 | > { 9 | const response = await fetch(`${API_BASE_URL}/api/list`); 10 | return (await response.json()) as ExtendedProviderStatus[]; 11 | } 12 | 13 | export async function fetchAlertsViaHttp(): Promise { 14 | const response = await fetch(`${API_BASE_URL}/api/alerts`); 15 | return (await response.json()) as Alert[]; 16 | } 17 | 18 | export async function submitFeedbackViaHttp( 19 | body: string, 20 | attachedData: string, 21 | ): Promise { 22 | type Response = { 23 | feedbackId: string; 24 | }; 25 | 26 | const response = await fetch(`${API_BASE_URL}/api/submit-feedback`, { 27 | method: 'POST', 28 | headers: { 29 | 'Content-Type': 'application/json', 30 | }, 31 | body: JSON.stringify({ body, attachedData }), 32 | }); 33 | 34 | if (!response.ok) { 35 | throw new Error(`Status: ${response.status}`); 36 | } 37 | 38 | const responseBody = (await response.json()) as Response; 39 | 40 | return responseBody.feedbackId; 41 | } 42 | 43 | async function fetchCurrencyUsdPrice(currency: string): Promise { 44 | try { 45 | const response = await fetch( 46 | `https://api.coingecko.com/api/v3/simple/price?ids=${currency}&vs_currencies=usd`, 47 | ); 48 | const data = await response.json(); 49 | return data[currency].usd; 50 | } catch (error) { 51 | console.error(`Error fetching ${currency} price:`, error); 52 | throw error; 53 | } 54 | } 55 | 56 | export async function fetchBtcPrice(): Promise { 57 | return fetchCurrencyUsdPrice('bitcoin'); 58 | } 59 | 60 | export async function fetchXmrPrice(): Promise { 61 | return fetchCurrencyUsdPrice('monero'); 62 | } 63 | -------------------------------------------------------------------------------- /src/renderer/components/App.tsx: -------------------------------------------------------------------------------- 1 | import { Box, makeStyles, CssBaseline } from '@material-ui/core'; 2 | import { createTheme, ThemeProvider } from '@material-ui/core/styles'; 3 | import { indigo } from '@material-ui/core/colors'; 4 | import { MemoryRouter as Router, Routes, Route } from 'react-router-dom'; 5 | import Navigation, { drawerWidth } from './navigation/Navigation'; 6 | import HistoryPage from './pages/history/HistoryPage'; 7 | import SwapPage from './pages/swap/SwapPage'; 8 | import WalletPage from './pages/wallet/WalletPage'; 9 | import HelpPage from './pages/help/HelpPage'; 10 | import GlobalSnackbarProvider from './snackbar/GlobalSnackbarProvider'; 11 | import UpdaterDialog from './modal/updater/UpdaterDialog'; 12 | 13 | const useStyles = makeStyles((theme) => ({ 14 | innerContent: { 15 | padding: theme.spacing(4), 16 | marginLeft: drawerWidth, 17 | maxHeight: `100vh`, 18 | flex: 1, 19 | }, 20 | })); 21 | 22 | const theme = createTheme({ 23 | palette: { 24 | type: 'dark', 25 | primary: { 26 | main: '#f4511e', 27 | }, 28 | secondary: indigo, 29 | }, 30 | transitions: { 31 | create: () => 'none', 32 | }, 33 | props: { 34 | MuiButtonBase: { 35 | disableRipple: true, 36 | }, 37 | }, 38 | }); 39 | 40 | function InnerContent() { 41 | const classes = useStyles(); 42 | 43 | return ( 44 | 45 | 46 | } /> 47 | } /> 48 | } /> 49 | } /> 50 | } /> 51 | 52 | 53 | ); 54 | } 55 | 56 | export default function App() { 57 | return ( 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /src/renderer/components/alert/FundsLeftInWalletAlert.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@material-ui/core'; 2 | import Alert from '@material-ui/lab/Alert'; 3 | import { useNavigate } from 'react-router-dom'; 4 | import { useAppSelector } from 'store/hooks'; 5 | 6 | export default function FundsLeftInWalletAlert() { 7 | const fundsLeft = useAppSelector((state) => state.rpc.state.balance); 8 | const navigate = useNavigate(); 9 | 10 | if (fundsLeft != null && fundsLeft > 0) { 11 | return ( 12 | navigate('/wallet')} 20 | > 21 | View 22 | 23 | } 24 | > 25 | There are some Bitcoin left in your wallet 26 | 27 | ); 28 | } 29 | return null; 30 | } 31 | -------------------------------------------------------------------------------- /src/renderer/components/alert/MoneroWalletRpcUpdatingAlert.tsx: -------------------------------------------------------------------------------- 1 | import { Alert } from '@material-ui/lab'; 2 | import { Box, LinearProgress } from '@material-ui/core'; 3 | import { useAppSelector } from 'store/hooks'; 4 | 5 | export default function MoneroWalletRpcUpdatingAlert() { 6 | const updateState = useAppSelector( 7 | (s) => s.rpc.state.moneroWalletRpc.updateState, 8 | ); 9 | 10 | if (updateState === false) { 11 | return null; 12 | } 13 | 14 | const progress = Number.parseFloat( 15 | updateState.progress.substring(0, updateState.progress.length - 1), 16 | ); 17 | 18 | return ( 19 | 20 | 21 | The Monero wallet is updating. This may take a few moments 22 | 27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/renderer/components/alert/RemainingFundsWillBeUsedAlert.tsx: -------------------------------------------------------------------------------- 1 | import { Alert } from '@material-ui/lab'; 2 | import { Box, makeStyles } from '@material-ui/core'; 3 | import { useAppSelector } from 'store/hooks'; 4 | import { satsToBtc } from 'utils/conversionUtils'; 5 | import WalletRefreshButton from '../pages/wallet/WalletRefreshButton'; 6 | import { SatsAmount } from '../other/Units'; 7 | 8 | const useStyles = makeStyles((theme) => ({ 9 | outer: { 10 | paddingBottom: theme.spacing(1), 11 | }, 12 | })); 13 | 14 | export default function RemainingFundsWillBeUsedAlert() { 15 | const classes = useStyles(); 16 | const balance = useAppSelector((s) => s.rpc.state.balance); 17 | 18 | if (balance == null || balance <= 0) { 19 | return <>; 20 | } 21 | 22 | return ( 23 | 24 | } 27 | variant="filled" 28 | > 29 | The remaining funds of in the wallet 30 | will be used for the next swap. If the remaining funds exceed the 31 | minimum swap amount of the provider, a swap will be initiated 32 | instantaneously. 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/renderer/components/alert/RpcStatusAlert.tsx: -------------------------------------------------------------------------------- 1 | import { Alert } from '@material-ui/lab'; 2 | import { CircularProgress } from '@material-ui/core'; 3 | import { useAppSelector } from 'store/hooks'; 4 | import { RpcProcessStateType } from 'models/rpcModel'; 5 | 6 | export default function RpcStatusAlert() { 7 | const rpcProcess = useAppSelector((s) => s.rpc.process); 8 | if (rpcProcess.type === RpcProcessStateType.STARTED) { 9 | return ( 10 | }> 11 | The swap daemon is starting 12 | 13 | ); 14 | } 15 | if (rpcProcess.type === RpcProcessStateType.LISTENING_FOR_CONNECTIONS) { 16 | return The swap daemon is running; 17 | } 18 | if (rpcProcess.type === RpcProcessStateType.NOT_STARTED) { 19 | return The swap daemon is being started; 20 | } 21 | if (rpcProcess.type === RpcProcessStateType.EXITED) { 22 | return ( 23 | The swap daemon has stopped unexpectedly 24 | ); 25 | } 26 | return <>; 27 | } 28 | -------------------------------------------------------------------------------- /src/renderer/components/alert/SwapMightBeCancelledAlert.tsx: -------------------------------------------------------------------------------- 1 | import { makeStyles } from '@material-ui/core'; 2 | import { Alert, AlertTitle } from '@material-ui/lab'; 3 | import { useActiveSwapInfo } from 'store/hooks'; 4 | import { 5 | isSwapTimelockInfoCancelled, 6 | isSwapTimelockInfoNone, 7 | } from 'models/rpcModel'; 8 | import HumanizedBitcoinBlockDuration from '../other/HumanizedBitcoinBlockDuration'; 9 | 10 | const useStyles = makeStyles((theme) => ({ 11 | outer: { 12 | marginBottom: theme.spacing(1), 13 | }, 14 | list: { 15 | margin: theme.spacing(0.25), 16 | }, 17 | })); 18 | 19 | export default function SwapMightBeCancelledAlert({ 20 | bobBtcLockTxConfirmations, 21 | }: { 22 | bobBtcLockTxConfirmations: number; 23 | }) { 24 | const classes = useStyles(); 25 | const swap = useActiveSwapInfo(); 26 | 27 | if ( 28 | bobBtcLockTxConfirmations < 5 || 29 | swap === null || 30 | swap.timelock === null 31 | ) { 32 | return <>; 33 | } 34 | 35 | const { timelock } = swap; 36 | const punishTimelockOffset = swap.punishTimelock; 37 | 38 | return ( 39 | 40 | Be careful! 41 | The swap provider has taken a long time to lock their Monero. This might 42 | mean that: 43 |
    44 |
  • 45 | There is a technical issue that prevents them from locking their funds 46 |
  • 47 |
  • They are a malicious actor (unlikely)
  • 48 |
49 |
50 | There is still hope for the swap to be successful but you have to be extra 51 | careful. Regardless of why it has taken them so long, it is important that 52 | you refund the swap within the required time period if the swap is not 53 | completed. If you fail to to do so, you will be punished and lose your 54 | money. 55 |
    56 | {isSwapTimelockInfoNone(timelock) && ( 57 | <> 58 |
  • 59 | 60 | You will be able to refund in about{' '} 61 | 64 | 65 |
  • 66 | 67 |
  • 68 | 69 | If you have not refunded or completed the swap in about{' '} 70 | 73 | , you will lose your funds. 74 | 75 |
  • 76 | 77 | )} 78 | {isSwapTimelockInfoCancelled(timelock) && ( 79 |
  • 80 | 81 | If you have not refunded or completed the swap in about{' '} 82 | 85 | , you will lose your funds. 86 | 87 |
  • 88 | )} 89 |
  • 90 | As long as you see this screen, the swap will be refunded 91 | automatically when the time comes. If this fails, you have to manually 92 | refund by navigating to the History page. 93 |
  • 94 |
95 |
96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /src/renderer/components/alert/SwapTxLockAlertsBox.tsx: -------------------------------------------------------------------------------- 1 | import { Box, makeStyles } from '@material-ui/core'; 2 | import { useAppSelector, useSwapInfosSortedByDate } from 'store/hooks'; 3 | import SwapStatusAlert from './SwapStatusAlert'; 4 | 5 | const useStyles = makeStyles((theme) => ({ 6 | outer: { 7 | display: 'flex', 8 | flexDirection: 'column', 9 | gap: theme.spacing(1), 10 | }, 11 | })); 12 | 13 | export default function SwapTxLockAlertsBox() { 14 | const classes = useStyles(); 15 | 16 | // We specifically choose ALL swaps here 17 | // If a swap is in a state where an Alert is not needed (becaue no Bitcoin have been locked or because the swap has been completed) 18 | // the SwapStatusAlert component will not render an Alert 19 | const swaps = useSwapInfosSortedByDate(); 20 | 21 | return ( 22 | 23 | {swaps.map((swap) => ( 24 | 25 | ))} 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/renderer/components/alert/UnfinishedSwapsAlert.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@material-ui/core'; 2 | import Alert from '@material-ui/lab/Alert'; 3 | import { useNavigate } from 'react-router-dom'; 4 | import { useResumeableSwapsCountExcludingPunished } from 'store/hooks'; 5 | 6 | export default function UnfinishedSwapsAlert() { 7 | const resumableSwapsCount = useResumeableSwapsCountExcludingPunished(); 8 | const navigate = useNavigate(); 9 | 10 | if (resumableSwapsCount > 0) { 11 | return ( 12 | navigate('/history')} 20 | > 21 | VIEW 22 | 23 | } 24 | > 25 | You have{' '} 26 | {resumableSwapsCount > 1 27 | ? `${resumableSwapsCount} unfinished swaps` 28 | : 'one unfinished swap'} 29 | 30 | ); 31 | } 32 | return null; 33 | } 34 | -------------------------------------------------------------------------------- /src/renderer/components/icons/BitcoinIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgIcon } from '@material-ui/core'; 2 | import { SvgIconProps } from '@material-ui/core/SvgIcon/SvgIcon'; 3 | 4 | export default function BitcoinIcon(props: SvgIconProps) { 5 | return ( 6 | // eslint-disable-next-line react/jsx-props-no-spreading 7 | 8 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/renderer/components/icons/DiscordIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgIconProps } from '@material-ui/core/SvgIcon/SvgIcon'; 2 | import { SvgIcon } from '@material-ui/core'; 3 | 4 | export default function DiscordIcon(props: SvgIconProps) { 5 | return ( 6 | 7 | 15 | 16 | 20 | 21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/renderer/components/icons/LinkIconButton.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { IconButton } from '@material-ui/core'; 3 | 4 | export default function LinkIconButton({ 5 | url, 6 | children, 7 | }: { 8 | url: string; 9 | children: ReactNode; 10 | }) { 11 | return ( 12 | window.open(url, '_blank')}> 13 | {children} 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/renderer/components/icons/MoneroIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgIcon } from '@material-ui/core'; 2 | import { SvgIconProps } from '@material-ui/core/SvgIcon/SvgIcon'; 3 | 4 | export default function MoneroIcon(props: SvgIconProps) { 5 | return ( 6 | // eslint-disable-next-line react/jsx-props-no-spreading 7 | 8 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/renderer/components/icons/TorIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SvgIcon } from '@material-ui/core'; 2 | import { SvgIconProps } from '@material-ui/core/SvgIcon/SvgIcon'; 3 | 4 | export default function TorIcon(props: SvgIconProps) { 5 | return ( 6 | // eslint-disable-next-line react/jsx-props-no-spreading 7 | 8 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/renderer/components/inputs/BitcoinAddressTextField.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { InputAdornment, TextField } from '@material-ui/core'; 3 | import { TextFieldProps } from '@material-ui/core/TextField/TextField'; 4 | import { isBtcAddressValid } from 'utils/conversionUtils'; 5 | import { isTestnet } from 'store/config'; 6 | import BitcoinIcon from '../icons/BitcoinIcon'; 7 | 8 | export default function BitcoinAddressTextField({ 9 | address, 10 | onAddressChange, 11 | onAddressValidityChange, 12 | helperText, 13 | ...props 14 | }: { 15 | address: string; 16 | onAddressChange: (address: string) => void; 17 | onAddressValidityChange: (valid: boolean) => void; 18 | helperText: string; 19 | } & TextFieldProps) { 20 | const placeholder = isTestnet() ? 'tb1q4aelwalu...' : 'bc18ociqZ9mZ...'; 21 | const errorText = isBtcAddressValid(address, isTestnet()) 22 | ? null 23 | : `Only bech32 addresses are supported. They begin with "${ 24 | isTestnet() ? 'tb1' : 'bc1' 25 | }"`; 26 | 27 | useEffect(() => { 28 | onAddressValidityChange(!errorText); 29 | }, [address, errorText, onAddressValidityChange]); 30 | 31 | return ( 32 | onAddressChange(e.target.value)} 35 | error={!!errorText && address.length > 0} 36 | helperText={address.length > 0 ? errorText || helperText : helperText} 37 | placeholder={placeholder} 38 | variant="outlined" 39 | InputProps={{ 40 | startAdornment: ( 41 | 42 | 43 | 44 | ), 45 | }} 46 | {...props} 47 | /> 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/renderer/components/inputs/MoneroAddressTextField.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { InputAdornment, TextField } from '@material-ui/core'; 3 | import { TextFieldProps } from '@material-ui/core/TextField/TextField'; 4 | import { isXmrAddressValid } from 'utils/conversionUtils'; 5 | import { isTestnet } from 'store/config'; 6 | import MoneroIcon from '../icons/MoneroIcon'; 7 | 8 | export default function MoneroAddressTextField({ 9 | address, 10 | onAddressChange, 11 | onAddressValidityChange, 12 | helperText, 13 | ...props 14 | }: { 15 | address: string; 16 | onAddressChange: (address: string) => void; 17 | onAddressValidityChange: (valid: boolean) => void; 18 | helperText: string; 19 | } & TextFieldProps) { 20 | const placeholder = isTestnet() ? '59McWTPGc745...' : '888tNkZrPN6J...'; 21 | const errorText = isXmrAddressValid(address, isTestnet()) 22 | ? null 23 | : 'Not a valid Monero address'; 24 | 25 | useEffect(() => { 26 | onAddressValidityChange(!errorText); 27 | }, [address, onAddressValidityChange, errorText]); 28 | 29 | return ( 30 | onAddressChange(e.target.value)} 33 | error={!!errorText && address.length > 0} 34 | helperText={address.length > 0 ? errorText || helperText : helperText} 35 | placeholder={placeholder} 36 | variant="outlined" 37 | InputProps={{ 38 | startAdornment: ( 39 | 40 | 41 | 42 | ), 43 | }} 44 | {...props} 45 | /> 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/renderer/components/modal/DialogHeader.tsx: -------------------------------------------------------------------------------- 1 | import { DialogTitle, makeStyles, Typography } from '@material-ui/core'; 2 | 3 | const useStyles = makeStyles({ 4 | root: { 5 | display: 'flex', 6 | justifyContent: 'space-between', 7 | }, 8 | }); 9 | 10 | type DialogTitleProps = { 11 | title: string; 12 | }; 13 | 14 | export default function DialogHeader({ title }: DialogTitleProps) { 15 | const classes = useStyles(); 16 | 17 | return ( 18 | 19 | {title} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/components/modal/PaperTextBox.tsx: -------------------------------------------------------------------------------- 1 | import { Button, makeStyles, Paper, Typography } from '@material-ui/core'; 2 | import { clipboard } from 'electron'; 3 | 4 | const useStyles = makeStyles((theme) => ({ 5 | logsOuter: { 6 | overflow: 'auto', 7 | padding: theme.spacing(1), 8 | marginTop: theme.spacing(1), 9 | marginBottom: theme.spacing(1), 10 | maxHeight: '10rem', 11 | }, 12 | copyButton: { 13 | marginTop: theme.spacing(1), 14 | }, 15 | })); 16 | 17 | export default function PaperTextBox({ stdOut }: { stdOut: string }) { 18 | const classes = useStyles(); 19 | 20 | function handleCopyLogs() { 21 | clipboard.writeText(stdOut); 22 | } 23 | 24 | return ( 25 | 26 | 27 | {stdOut} 28 | 29 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/renderer/components/modal/SwapSuspendAlert.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Dialog, 4 | DialogActions, 5 | DialogContent, 6 | DialogContentText, 7 | DialogTitle, 8 | } from '@material-ui/core'; 9 | import IpcInvokeButton from '../IpcInvokeButton'; 10 | 11 | type SwapCancelAlertProps = { 12 | open: boolean; 13 | onClose: () => void; 14 | }; 15 | 16 | export default function SwapSuspendAlert({ 17 | open, 18 | onClose, 19 | }: SwapCancelAlertProps) { 20 | return ( 21 | 22 | Force stop running operation? 23 | 24 | 25 | Are you sure you want to force stop the running swap? 26 | 27 | 28 | 29 | 32 | 39 | Force stop 40 | 41 | 42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/renderer/components/modal/provider/ProviderInfo.tsx: -------------------------------------------------------------------------------- 1 | import { makeStyles, Box, Typography, Chip, Tooltip } from '@material-ui/core'; 2 | import { VerifiedUser } from '@material-ui/icons'; 3 | import { satsToBtc, secondsToDays } from 'utils/conversionUtils'; 4 | import { ExtendedProviderStatus } from 'models/apiModel'; 5 | import { 6 | MoneroBitcoinExchangeRate, 7 | SatsAmount, 8 | } from 'renderer/components/other/Units'; 9 | import { isProviderOutdated } from 'utils/multiAddrUtils'; 10 | import WarningIcon from '@material-ui/icons/Warning'; 11 | 12 | const useStyles = makeStyles((theme) => ({ 13 | content: { 14 | flex: 1, 15 | '& *': { 16 | lineBreak: 'anywhere', 17 | }, 18 | }, 19 | chipsOuter: { 20 | display: 'flex', 21 | marginTop: theme.spacing(1), 22 | gap: theme.spacing(0.5), 23 | flexWrap: 'wrap', 24 | }, 25 | })); 26 | 27 | export default function ProviderInfo({ 28 | provider, 29 | }: { 30 | provider: ExtendedProviderStatus; 31 | }) { 32 | const classes = useStyles(); 33 | const isOutdated = isProviderOutdated(provider); 34 | 35 | return ( 36 | 37 | 38 | Swap Provider 39 | 40 | 41 | {provider.multiAddr} 42 | 43 | 44 | {provider.peerId.substring(0, 8)}...{provider.peerId.slice(-8)} 45 | 46 | 47 | Exchange rate:{' '} 48 | 49 |
50 | Minimum swap amount: 51 |
52 | Maximum swap amount: 53 |
54 | 55 | 56 | {provider.uptime && ( 57 | 58 | 59 | 60 | )} 61 | {provider.age ? ( 62 | 67 | ) : ( 68 | 69 | )} 70 | {provider.recommended === true && ( 71 | 72 | } color="primary" /> 73 | 74 | )} 75 | {isOutdated && ( 76 | 77 | } color="primary" /> 78 | 79 | )} 80 | 81 |
82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /src/renderer/components/modal/provider/ProviderSelect.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | makeStyles, 3 | Card, 4 | CardContent, 5 | Box, 6 | IconButton, 7 | } from '@material-ui/core'; 8 | import ArrowForwardIosIcon from '@material-ui/icons/ArrowForwardIos'; 9 | import { useState } from 'react'; 10 | import { useAppSelector } from 'store/hooks'; 11 | import ProviderInfo from './ProviderInfo'; 12 | import ProviderListDialog from './ProviderListDialog'; 13 | 14 | const useStyles = makeStyles({ 15 | inner: { 16 | textAlign: 'left', 17 | width: '100%', 18 | height: '100%', 19 | }, 20 | providerCard: { 21 | width: '100%', 22 | }, 23 | providerCardContent: { 24 | display: 'flex', 25 | alignItems: 'center', 26 | }, 27 | }); 28 | 29 | export default function ProviderSelect() { 30 | const classes = useStyles(); 31 | const [selectDialogOpen, setSelectDialogOpen] = useState(false); 32 | const selectedProvider = useAppSelector( 33 | (state) => state.providers.selectedProvider, 34 | ); 35 | 36 | if (!selectedProvider) return <>No provider selected; 37 | 38 | function handleSelectDialogClose() { 39 | setSelectDialogOpen(false); 40 | } 41 | 42 | function handleSelectDialogOpen() { 43 | setSelectDialogOpen(true); 44 | } 45 | 46 | return ( 47 | 48 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /src/renderer/components/modal/swap/BitcoinQrCode.tsx: -------------------------------------------------------------------------------- 1 | import QRCode from 'react-qr-code'; 2 | import { Box } from '@material-ui/core'; 3 | 4 | export default function BitcoinQrCode({ address }: { address: string }) { 5 | return ( 6 | 12 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/components/modal/swap/BitcoinTransactionInfoBox.tsx: -------------------------------------------------------------------------------- 1 | import { isTestnet } from 'store/config'; 2 | import { getBitcoinTxExplorerUrl } from 'utils/conversionUtils'; 3 | import BitcoinIcon from 'renderer/components/icons/BitcoinIcon'; 4 | import { ReactNode } from 'react'; 5 | import TransactionInfoBox from './TransactionInfoBox'; 6 | 7 | type Props = { 8 | title: string; 9 | txId: string; 10 | additionalContent: ReactNode; 11 | loading: boolean; 12 | }; 13 | 14 | export default function BitcoinTransactionInfoBox({ txId, ...props }: Props) { 15 | const explorerUrl = getBitcoinTxExplorerUrl(txId, isTestnet()); 16 | 17 | return ( 18 | } 22 | {...props} 23 | /> 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/renderer/components/modal/swap/CircularProgressWithSubtitle.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | CircularProgress, 4 | makeStyles, 5 | Typography, 6 | } from '@material-ui/core'; 7 | import { ReactNode } from 'react'; 8 | 9 | const useStyles = makeStyles((theme) => ({ 10 | subtitle: { 11 | paddingTop: theme.spacing(1), 12 | }, 13 | })); 14 | 15 | export default function CircularProgressWithSubtitle({ 16 | description, 17 | }: { 18 | description: string | ReactNode; 19 | }) { 20 | const classes = useStyles(); 21 | 22 | return ( 23 | 29 | 30 | 31 | {description} 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/renderer/components/modal/swap/ClipbiardIconButton.tsx: -------------------------------------------------------------------------------- 1 | import { clipboard } from 'electron'; 2 | import { Button } from '@material-ui/core'; 3 | import { ButtonProps } from '@material-ui/core/Button/Button'; 4 | 5 | export default function ClipboardIconButton({ 6 | text, 7 | ...props 8 | }: { text: string } & ButtonProps) { 9 | function writeToClipboard() { 10 | clipboard.writeText(text); 11 | } 12 | 13 | return ( 14 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/renderer/components/modal/swap/DepositAddressInfoBox.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { Box, Typography } from '@material-ui/core'; 3 | import FileCopyOutlinedIcon from '@material-ui/icons/FileCopyOutlined'; 4 | import InfoBox from './InfoBox'; 5 | import ClipboardIconButton from './ClipbiardIconButton'; 6 | import BitcoinQrCode from './BitcoinQrCode'; 7 | 8 | type Props = { 9 | title: string; 10 | address: string; 11 | additionalContent: ReactNode; 12 | icon: ReactNode; 13 | }; 14 | 15 | export default function DepositAddressInfoBox({ 16 | title, 17 | address, 18 | additionalContent, 19 | icon, 20 | }: Props) { 21 | return ( 22 | {address}} 25 | additionalContent={ 26 | 27 | 28 | } 31 | color="primary" 32 | variant="contained" 33 | size="medium" 34 | /> 35 | 43 | {additionalContent} 44 | 45 | 46 | 47 | 48 | } 49 | icon={icon} 50 | loading={false} 51 | /> 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/renderer/components/modal/swap/InfoBox.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | LinearProgress, 4 | makeStyles, 5 | Paper, 6 | Typography, 7 | } from '@material-ui/core'; 8 | import { ReactNode } from 'react'; 9 | 10 | type Props = { 11 | title: ReactNode; 12 | mainContent: ReactNode; 13 | additionalContent: ReactNode; 14 | loading: boolean; 15 | icon: ReactNode; 16 | }; 17 | 18 | const useStyles = makeStyles((theme) => ({ 19 | outer: { 20 | padding: theme.spacing(1.5), 21 | overflow: 'hidden', 22 | display: 'flex', 23 | flexDirection: 'column', 24 | gap: theme.spacing(1), 25 | }, 26 | upperContent: { 27 | display: 'flex', 28 | alignItems: 'center', 29 | gap: theme.spacing(0.5), 30 | }, 31 | })); 32 | 33 | export default function InfoBox({ 34 | title, 35 | mainContent, 36 | additionalContent, 37 | icon, 38 | loading, 39 | }: Props) { 40 | const classes = useStyles(); 41 | 42 | return ( 43 | 44 | {title} 45 | 46 | {icon} 47 | {mainContent} 48 | 49 | {loading ? : null} 50 | {additionalContent} 51 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/renderer/components/modal/swap/MoneroTransactionInfoBox.tsx: -------------------------------------------------------------------------------- 1 | import { isTestnet } from 'store/config'; 2 | import { getMoneroTxExplorerUrl } from 'utils/conversionUtils'; 3 | import MoneroIcon from 'renderer/components/icons/MoneroIcon'; 4 | import { ReactNode } from 'react'; 5 | import TransactionInfoBox from './TransactionInfoBox'; 6 | 7 | type Props = { 8 | title: string; 9 | txId: string; 10 | additionalContent: ReactNode; 11 | loading: boolean; 12 | }; 13 | 14 | export default function MoneroTransactionInfoBox({ txId, ...props }: Props) { 15 | const explorerUrl = getMoneroTxExplorerUrl(txId, isTestnet()); 16 | 17 | return ( 18 | } 22 | {...props} 23 | /> 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/renderer/components/modal/swap/SwapDialog.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { 3 | Button, 4 | Dialog, 5 | DialogActions, 6 | DialogContent, 7 | makeStyles, 8 | } from '@material-ui/core'; 9 | import { useAppDispatch, useAppSelector } from 'store/hooks'; 10 | import { swapReset } from 'store/features/swapSlice'; 11 | import SwapStatePage from './pages/SwapStatePage'; 12 | import SwapStateStepper from './SwapStateStepper'; 13 | import SwapSuspendAlert from '../SwapSuspendAlert'; 14 | import SwapDialogTitle from './SwapDialogTitle'; 15 | import DebugPage from './pages/DebugPage'; 16 | 17 | const useStyles = makeStyles({ 18 | content: { 19 | minHeight: '25rem', 20 | display: 'flex', 21 | flexDirection: 'column', 22 | justifyContent: 'space-between', 23 | }, 24 | }); 25 | 26 | export default function SwapDialog({ 27 | open, 28 | onClose, 29 | }: { 30 | open: boolean; 31 | onClose: () => void; 32 | }) { 33 | const classes = useStyles(); 34 | const swap = useAppSelector((state) => state.swap); 35 | const [debug, setDebug] = useState(false); 36 | const [openSuspendAlert, setOpenSuspendAlert] = useState(false); 37 | const dispatch = useAppDispatch(); 38 | 39 | function onCancel() { 40 | if (swap.processRunning) { 41 | setOpenSuspendAlert(true); 42 | } else { 43 | onClose(); 44 | setImmediate(() => dispatch(swapReset())); 45 | } 46 | } 47 | 48 | // This prevents an issue where the Dialog is shown for a split second without a present swap state 49 | if (!open) return null; 50 | 51 | return ( 52 | 53 | 58 | 59 | 60 | {debug ? ( 61 | 62 | ) : ( 63 | <> 64 | 65 | 66 | 67 | )} 68 | 69 | 70 | 71 | 74 | 82 | 83 | 84 | setOpenSuspendAlert(false)} 87 | /> 88 | 89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /src/renderer/components/modal/swap/SwapDialogTitle.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | DialogTitle, 4 | FormControlLabel, 5 | makeStyles, 6 | Switch, 7 | Typography, 8 | } from '@material-ui/core'; 9 | import TorStatusBadge from './pages/TorStatusBadge'; 10 | import FeedbackSubmitBadge from './pages/FeedbackSubmitBadge'; 11 | import DebugPageSwitchBadge from './pages/DebugPageSwitchBadge'; 12 | 13 | const useStyles = makeStyles((theme) => ({ 14 | root: { 15 | display: 'flex', 16 | justifyContent: 'space-between', 17 | alignItems: 'center', 18 | }, 19 | rightSide: { 20 | display: 'flex', 21 | alignItems: 'center', 22 | gridGap: theme.spacing(1), 23 | }, 24 | })); 25 | 26 | export default function SwapDialogTitle({ 27 | title, 28 | debug, 29 | setDebug, 30 | }: { 31 | title: string; 32 | debug: boolean; 33 | setDebug: (d: boolean) => void; 34 | }) { 35 | const classes = useStyles(); 36 | 37 | return ( 38 | 39 | {title} 40 | 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/renderer/components/modal/swap/TransactionInfoBox.tsx: -------------------------------------------------------------------------------- 1 | import { Link, Typography } from '@material-ui/core'; 2 | import { ReactNode } from 'react'; 3 | import InfoBox from './InfoBox'; 4 | 5 | type TransactionInfoBoxProps = { 6 | title: string; 7 | txId: string; 8 | explorerUrl: string; 9 | additionalContent: ReactNode; 10 | loading: boolean; 11 | icon: JSX.Element; 12 | }; 13 | 14 | export default function TransactionInfoBox({ 15 | title, 16 | txId, 17 | explorerUrl, 18 | additionalContent, 19 | icon, 20 | loading, 21 | }: TransactionInfoBoxProps) { 22 | return ( 23 | {txId}} 26 | loading={loading} 27 | additionalContent={ 28 | <> 29 | {additionalContent} 30 | 31 | 32 | View on explorer 33 | 34 | 35 | 36 | } 37 | icon={icon} 38 | /> 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/renderer/components/modal/swap/pages/DebugPage.tsx: -------------------------------------------------------------------------------- 1 | import { Box, DialogContentText } from '@material-ui/core'; 2 | import { useActiveSwapInfo, useAppSelector } from 'store/hooks'; 3 | import CliLogsBox from '../../../other/RenderedCliLog'; 4 | import JsonTreeView from '../../../other/JSONViewTree'; 5 | 6 | export default function DebugPage() { 7 | const torStdOut = useAppSelector((s) => s.tor.stdOut); 8 | const logs = useAppSelector((s) => s.swap.logs); 9 | const guiState = useAppSelector((s) => s.swap); 10 | const cliState = useActiveSwapInfo(); 11 | 12 | return ( 13 | 14 | 15 | 22 | 23 | 27 | 31 | 32 | 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/renderer/components/modal/swap/pages/DebugPageSwitchBadge.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip } from '@material-ui/core'; 2 | import IconButton from '@material-ui/core/IconButton'; 3 | import DeveloperBoardIcon from '@material-ui/icons/DeveloperBoard'; 4 | 5 | export default function DebugPageSwitchBadge({ 6 | enabled, 7 | setEnabled, 8 | }: { 9 | enabled: boolean; 10 | setEnabled: (enabled: boolean) => void; 11 | }) { 12 | const handleToggle = () => { 13 | setEnabled(!enabled); 14 | }; 15 | 16 | return ( 17 | 18 | 22 | 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/renderer/components/modal/swap/pages/FeedbackSubmitBadge.tsx: -------------------------------------------------------------------------------- 1 | import { IconButton } from '@material-ui/core'; 2 | import FeedbackIcon from '@material-ui/icons/Feedback'; 3 | import FeedbackDialog from '../../feedback/FeedbackDialog'; 4 | import { useState } from 'react'; 5 | 6 | export default function FeedbackSubmitBadge() { 7 | const [showFeedbackDialog, setShowFeedbackDialog] = useState(false); 8 | 9 | return ( 10 | <> 11 | {showFeedbackDialog && ( 12 | setShowFeedbackDialog(false)} 15 | /> 16 | )} 17 | setShowFeedbackDialog(true)}> 18 | 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/components/modal/swap/pages/TorStatusBadge.tsx: -------------------------------------------------------------------------------- 1 | import { IconButton, Tooltip } from '@material-ui/core'; 2 | import { useAppSelector } from 'store/hooks'; 3 | import TorIcon from '../../../icons/TorIcon'; 4 | 5 | export default function TorStatusBadge() { 6 | const tor = useAppSelector((s) => s.tor); 7 | 8 | if (tor.processRunning) { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | } 17 | 18 | return <>; 19 | } 20 | -------------------------------------------------------------------------------- /src/renderer/components/modal/swap/pages/done/BitcoinPunishedPage.tsx: -------------------------------------------------------------------------------- 1 | import { Box, DialogContentText } from '@material-ui/core'; 2 | import FeedbackInfoBox from '../../../../pages/help/FeedbackInfoBox'; 3 | import { 4 | SwapStateCooperativeRedeemRejected, 5 | isSwapStateCooperativeRedeemRejected, 6 | } from 'models/storeModel'; 7 | 8 | export default function BitcoinPunishedPage({ 9 | state, 10 | }: { 11 | state: null | SwapStateCooperativeRedeemRejected; 12 | }) { 13 | return ( 14 | 15 | 16 | Unfortunately, the swap was unsuccessful. Since you did not refund in 17 | time, the Bitcoin has been lost. However, with the cooperation of the 18 | other party, you might still be able to redeem the Monero, although this 19 | is not guaranteed.{' '} 20 | {isSwapStateCooperativeRedeemRejected(state) && ( 21 | <> 22 |
23 | We tried to redeem the Monero with the other party's help, but it 24 | was unsuccessful (reason: {state.reason}). Attempting again at a 25 | later time might yield success.
26 | 27 | )} 28 |
29 | 30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/renderer/components/modal/swap/pages/done/BitcoinRefundedPage.tsx: -------------------------------------------------------------------------------- 1 | import { Box, DialogContentText } from '@material-ui/core'; 2 | import { SwapStateBtcRefunded } from 'models/storeModel'; 3 | import { useActiveSwapInfo } from 'store/hooks'; 4 | import BitcoinTransactionInfoBox from '../../BitcoinTransactionInfoBox'; 5 | import FeedbackInfoBox from '../../../../pages/help/FeedbackInfoBox'; 6 | 7 | export default function BitcoinRefundedPage({ 8 | state, 9 | }: { 10 | state: SwapStateBtcRefunded | null; 11 | }) { 12 | const swap = useActiveSwapInfo(); 13 | const additionalContent = swap 14 | ? `Refund address: ${swap.btcRefundAddress}` 15 | : null; 16 | 17 | return ( 18 | 19 | 20 | Unfortunately, the swap was not successful. However, rest assured that 21 | all your Bitcoin has been refunded to the specified address. The swap 22 | process is now complete, and you are free to exit the application. 23 | 24 | 31 | {state && ( 32 | 38 | )} 39 | 40 | 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/renderer/components/modal/swap/pages/done/XmrRedeemInMempoolPage.tsx: -------------------------------------------------------------------------------- 1 | import { Box, DialogContentText } from '@material-ui/core'; 2 | import { SwapStateXmrRedeemInMempool } from 'models/storeModel'; 3 | import { useActiveSwapInfo } from 'store/hooks'; 4 | import { getSwapXmrAmount } from 'models/rpcModel'; 5 | import MoneroTransactionInfoBox from '../../MoneroTransactionInfoBox'; 6 | import FeedbackInfoBox from '../../../../pages/help/FeedbackInfoBox'; 7 | import { MoneroAmount } from 'renderer/components/other/Units'; 8 | 9 | type XmrRedeemInMempoolPageProps = { 10 | state: SwapStateXmrRedeemInMempool | null; 11 | }; 12 | 13 | export default function XmrRedeemInMempoolPage({ 14 | state, 15 | }: XmrRedeemInMempoolPageProps) { 16 | const swap = useActiveSwapInfo(); 17 | const additionalContent = swap ? ( 18 | <> 19 | This transaction transfers{' '} 20 | to the{' '} 21 | {state?.bobXmrRedeemAddress} 22 | 23 | ) : null; 24 | 25 | return ( 26 | 27 | 28 | The swap was successful and the Monero has been sent to the address you 29 | specified. The swap is completed and you may exit the application now. 30 | 31 | 38 | {state && ( 39 | <> 40 | 46 | 47 | )} 48 | 49 | 50 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /src/renderer/components/modal/swap/pages/exited/ProcessExitedAndNotDonePage.tsx: -------------------------------------------------------------------------------- 1 | import { Box, DialogContentText } from '@material-ui/core'; 2 | import { useActiveSwapInfo, useAppSelector } from 'store/hooks'; 3 | import { SwapStateProcessExited } from 'models/storeModel'; 4 | import CliLogsBox from '../../../../other/RenderedCliLog'; 5 | import { SwapSpawnType } from 'models/cliModel'; 6 | 7 | export default function ProcessExitedAndNotDonePage({ 8 | state, 9 | }: { 10 | state: SwapStateProcessExited; 11 | }) { 12 | const swap = useActiveSwapInfo(); 13 | const logs = useAppSelector((s) => s.swap.logs); 14 | const spawnType = useAppSelector((s) => s.swap.spawnType); 15 | 16 | function getText() { 17 | const isCancelRefund = spawnType === SwapSpawnType.CANCEL_REFUND; 18 | const hasRpcError = state.rpcError != null; 19 | const hasSwap = swap != null; 20 | 21 | let messages = []; 22 | 23 | messages.push( 24 | isCancelRefund 25 | ? 'The manual cancel and refund was unsuccessful.' 26 | : 'The swap exited unexpectedly without completing.', 27 | ); 28 | 29 | if (!hasSwap && !isCancelRefund) { 30 | messages.push('No funds were locked.'); 31 | } 32 | 33 | messages.push( 34 | hasRpcError 35 | ? 'Check the error and the logs below for more information.' 36 | : 'Check the logs below for more information.', 37 | ); 38 | 39 | if (hasSwap) { 40 | messages.push(`The swap is in the "${swap.stateName}" state.`); 41 | if (!isCancelRefund) { 42 | messages.push( 43 | 'Try resuming the swap or attempt to initiate a manual cancel and refund.', 44 | ); 45 | } 46 | } 47 | 48 | return messages.join(' '); 49 | } 50 | 51 | return ( 52 | 53 | {getText()} 54 | 61 | {state.rpcError && ( 62 | 66 | )} 67 | 68 | 69 | 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /src/renderer/components/modal/swap/pages/exited/ProcessExitedPage.tsx: -------------------------------------------------------------------------------- 1 | import { useActiveSwapInfo } from 'store/hooks'; 2 | import { SwapStateName } from 'models/rpcModel'; 3 | import { 4 | isSwapStateBtcPunished, 5 | isSwapStateBtcRefunded, 6 | isSwapStateCooperativeRedeemRejected, 7 | isSwapStateXmrRedeemInMempool, 8 | SwapStateProcessExited, 9 | } from '../../../../../../models/storeModel'; 10 | import XmrRedeemInMempoolPage from '../done/XmrRedeemInMempoolPage'; 11 | import BitcoinPunishedPage from '../done/BitcoinPunishedPage'; 12 | // eslint-disable-next-line import/no-cycle 13 | import SwapStatePage from '../SwapStatePage'; 14 | import BitcoinRefundedPage from '../done/BitcoinRefundedPage'; 15 | import ProcessExitedAndNotDonePage from './ProcessExitedAndNotDonePage'; 16 | 17 | type ProcessExitedPageProps = { 18 | state: SwapStateProcessExited; 19 | }; 20 | 21 | export default function ProcessExitedPage({ state }: ProcessExitedPageProps) { 22 | const swap = useActiveSwapInfo(); 23 | 24 | // If we have a swap state, for a "done" state we should use it to display additional information that can't be extracted from the database 25 | if ( 26 | isSwapStateXmrRedeemInMempool(state.prevState) || 27 | isSwapStateBtcRefunded(state.prevState) || 28 | isSwapStateBtcPunished(state.prevState) || 29 | isSwapStateCooperativeRedeemRejected(state.prevState) 30 | ) { 31 | return ; 32 | } 33 | 34 | // If we don't have a swap state for a "done" state, we should fall back to using the database to display as much information as we can 35 | if (swap) { 36 | if (swap.stateName === SwapStateName.XmrRedeemed) { 37 | return ; 38 | } 39 | if (swap.stateName === SwapStateName.BtcRefunded) { 40 | return ; 41 | } 42 | if (swap.stateName === SwapStateName.BtcPunished) { 43 | return ; 44 | } 45 | } 46 | 47 | // If the swap is not a "done" state (or we don't have a db state because the swap did complete the SwapSetup yet) we should tell the user and show logs 48 | return ; 49 | } 50 | -------------------------------------------------------------------------------- /src/renderer/components/modal/swap/pages/in_progress/BitcoinCancelledPage.tsx: -------------------------------------------------------------------------------- 1 | import CircularProgressWithSubtitle from '../../CircularProgressWithSubtitle'; 2 | 3 | export default function BitcoinCancelledPage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/renderer/components/modal/swap/pages/in_progress/BitcoinLockTxInMempoolPage.tsx: -------------------------------------------------------------------------------- 1 | import { Box, DialogContentText } from '@material-ui/core'; 2 | import { SwapStateBtcLockInMempool } from 'models/storeModel'; 3 | import BitcoinTransactionInfoBox from '../../BitcoinTransactionInfoBox'; 4 | import SwapMightBeCancelledAlert from '../../../../alert/SwapMightBeCancelledAlert'; 5 | 6 | type BitcoinLockTxInMempoolPageProps = { 7 | state: SwapStateBtcLockInMempool; 8 | }; 9 | 10 | export default function BitcoinLockTxInMempoolPage({ 11 | state, 12 | }: BitcoinLockTxInMempoolPageProps) { 13 | return ( 14 | 15 | 18 | 19 | The Bitcoin lock transaction has been published. The swap will proceed 20 | once the transaction is confirmed and the swap provider locks their 21 | Monero. 22 | 23 | 29 | Most swap providers require one confirmation before locking their 30 | Monero 31 |
32 | Confirmations: {state.bobBtcLockTxConfirmations} 33 | 34 | } 35 | /> 36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/renderer/components/modal/swap/pages/in_progress/BitcoinRedeemedPage.tsx: -------------------------------------------------------------------------------- 1 | import CircularProgressWithSubtitle from '../../CircularProgressWithSubtitle'; 2 | 3 | export default function BitcoinRedeemedPage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/renderer/components/modal/swap/pages/in_progress/ReceivedQuotePage.tsx: -------------------------------------------------------------------------------- 1 | import CircularProgressWithSubtitle from '../../CircularProgressWithSubtitle'; 2 | 3 | export default function ReceivedQuotePage() { 4 | return ( 5 | 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /src/renderer/components/modal/swap/pages/in_progress/StartedPage.tsx: -------------------------------------------------------------------------------- 1 | import { SwapStateStarted } from 'models/storeModel'; 2 | import { BitcoinAmount } from 'renderer/components/other/Units'; 3 | import CircularProgressWithSubtitle from '../../CircularProgressWithSubtitle'; 4 | 5 | export default function StartedPage({ state }: { state: SwapStateStarted }) { 6 | const description = state.txLockDetails ? ( 7 | <> 8 | Locking with a 9 | network fee of 10 | 11 | ) : ( 12 | 'Locking Bitcoin' 13 | ); 14 | 15 | return ; 16 | } 17 | -------------------------------------------------------------------------------- /src/renderer/components/modal/swap/pages/in_progress/SyncingMoneroWalletPage.tsx: -------------------------------------------------------------------------------- 1 | import CircularProgressWithSubtitle from '../../CircularProgressWithSubtitle'; 2 | 3 | export function SyncingMoneroWalletPage() { 4 | return ( 5 | 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /src/renderer/components/modal/swap/pages/in_progress/XmrLockInMempoolPage.tsx: -------------------------------------------------------------------------------- 1 | import { Box, DialogContentText } from '@material-ui/core'; 2 | import { SwapStateXmrLockInMempool } from 'models/storeModel'; 3 | import MoneroTransactionInfoBox from '../../MoneroTransactionInfoBox'; 4 | 5 | type XmrLockTxInMempoolPageProps = { 6 | state: SwapStateXmrLockInMempool; 7 | }; 8 | 9 | export default function XmrLockTxInMempoolPage({ 10 | state, 11 | }: XmrLockTxInMempoolPageProps) { 12 | const additionalContent = `Confirmations: ${state.aliceXmrLockTxConfirmations}/10`; 13 | 14 | return ( 15 | 16 | 17 | They have published their Monero lock transaction. The swap will proceed 18 | once the transaction has been confirmed. 19 | 20 | 21 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/renderer/components/modal/swap/pages/in_progress/XmrLockedPage.tsx: -------------------------------------------------------------------------------- 1 | import CircularProgressWithSubtitle from '../../CircularProgressWithSubtitle'; 2 | 3 | export default function XmrLockedPage() { 4 | return ( 5 | 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /src/renderer/components/modal/swap/pages/init/DepositAmountHelper.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Box, makeStyles, TextField, Typography } from '@material-ui/core'; 3 | import { SwapStateWaitingForBtcDeposit } from 'models/storeModel'; 4 | import { useAppSelector } from 'store/hooks'; 5 | import { satsToBtc } from 'utils/conversionUtils'; 6 | import { MoneroAmount } from '../../../../other/Units'; 7 | 8 | const MONERO_FEE = 0.000016; 9 | 10 | const useStyles = makeStyles((theme) => ({ 11 | outer: { 12 | display: 'flex', 13 | alignItems: 'center', 14 | gap: theme.spacing(1), 15 | }, 16 | textField: { 17 | '& input::-webkit-outer-spin-button, & input::-webkit-inner-spin-button': { 18 | display: 'none', 19 | }, 20 | '& input[type=number]': { 21 | MozAppearance: 'textfield', 22 | }, 23 | maxWidth: theme.spacing(16), 24 | }, 25 | })); 26 | 27 | function calcBtcAmountWithoutFees(amount: number, fees: number) { 28 | return amount - fees; 29 | } 30 | 31 | export default function DepositAmountHelper({ 32 | state, 33 | }: { 34 | state: SwapStateWaitingForBtcDeposit; 35 | }) { 36 | const classes = useStyles(); 37 | const [amount, setAmount] = useState(state.minDeposit); 38 | const bitcoinBalance = useAppSelector((s) => s.rpc.state.balance) || 0; 39 | 40 | function getTotalAmountAfterDeposit() { 41 | return amount + satsToBtc(bitcoinBalance); 42 | } 43 | 44 | function hasError() { 45 | return ( 46 | amount < state.minDeposit || 47 | getTotalAmountAfterDeposit() > state.maximumAmount 48 | ); 49 | } 50 | 51 | function calcXMRAmount(): number | null { 52 | if (Number.isNaN(amount)) return null; 53 | if (hasError()) return null; 54 | if (state.price == null) return null; 55 | 56 | console.log( 57 | `Calculating calcBtcAmountWithoutFees(${getTotalAmountAfterDeposit()}, ${ 58 | state.minBitcoinLockTxFee 59 | }) / ${state.price} - ${MONERO_FEE}`, 60 | ); 61 | 62 | return ( 63 | calcBtcAmountWithoutFees( 64 | getTotalAmountAfterDeposit(), 65 | state.minBitcoinLockTxFee, 66 | ) / 67 | state.price - 68 | MONERO_FEE 69 | ); 70 | } 71 | 72 | return ( 73 | 74 | 75 | Depositing {bitcoinBalance > 0 && <>another} 76 | 77 | setAmount(parseFloat(e.target.value))} 81 | size="small" 82 | type="number" 83 | className={classes.textField} 84 | /> 85 | 86 | BTC will give you approximately{' '} 87 | . 88 | 89 | 90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /src/renderer/components/modal/swap/pages/init/DownloadingMoneroWalletRpcPage.tsx: -------------------------------------------------------------------------------- 1 | import CircularProgressWithSubtitle from '../../CircularProgressWithSubtitle'; 2 | import { MoneroWalletRpcUpdateState } from '../../../../../../models/storeModel'; 3 | 4 | export default function DownloadingMoneroWalletRpcPage({ 5 | updateState, 6 | }: { 7 | updateState: MoneroWalletRpcUpdateState; 8 | }) { 9 | return ( 10 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/renderer/components/modal/swap/pages/init/InitiatedPage.tsx: -------------------------------------------------------------------------------- 1 | import { useAppSelector } from 'store/hooks'; 2 | import { SwapSpawnType } from 'models/cliModel'; 3 | import CircularProgressWithSubtitle from '../../CircularProgressWithSubtitle'; 4 | 5 | export default function InitiatedPage() { 6 | const description = useAppSelector((s) => { 7 | switch (s.swap.spawnType) { 8 | case SwapSpawnType.INIT: 9 | return 'Requesting quote from provider...'; 10 | case SwapSpawnType.RESUME: 11 | return 'Resuming swap...'; 12 | case SwapSpawnType.CANCEL_REFUND: 13 | return 'Attempting to cancel & refund swap...'; 14 | default: 15 | // Should never be hit 16 | return 'Initiating swap...'; 17 | } 18 | }); 19 | 20 | return ; 21 | } 22 | -------------------------------------------------------------------------------- /src/renderer/components/modal/swap/pages/init/WaitingForBitcoinDepositPage.tsx: -------------------------------------------------------------------------------- 1 | import { Box, makeStyles, Typography } from '@material-ui/core'; 2 | import { SwapStateWaitingForBtcDeposit } from 'models/storeModel'; 3 | import { btcToSats, satsToBtc } from 'utils/conversionUtils'; 4 | import { useAppSelector } from 'store/hooks'; 5 | import DepositAddressInfoBox from '../../DepositAddressInfoBox'; 6 | import BitcoinIcon from '../../../../icons/BitcoinIcon'; 7 | import DepositAmountHelper from './DepositAmountHelper'; 8 | import { 9 | BitcoinAmount, 10 | MoneroBitcoinExchangeRate, 11 | SatsAmount, 12 | } from '../../../../other/Units'; 13 | 14 | const useStyles = makeStyles((theme) => ({ 15 | amountHelper: { 16 | display: 'flex', 17 | alignItems: 'center', 18 | }, 19 | additionalContent: { 20 | paddingTop: theme.spacing(1), 21 | gap: theme.spacing(0.5), 22 | display: 'flex', 23 | flexDirection: 'column', 24 | }, 25 | })); 26 | 27 | type WaitingForBtcDepositPageProps = { 28 | state: SwapStateWaitingForBtcDeposit; 29 | }; 30 | 31 | export default function WaitingForBtcDepositPage({ 32 | state, 33 | }: WaitingForBtcDepositPageProps) { 34 | const classes = useStyles(); 35 | const bitcoinBalance = useAppSelector((s) => s.rpc.state.balance) || 0; 36 | 37 | // TODO: Account for BTC lock tx fees 38 | return ( 39 | 40 | 45 | 46 |
    47 | {bitcoinBalance > 0 ? ( 48 |
  • 49 | You have already deposited{' '} 50 | 51 |
  • 52 | ) : null} 53 |
  • 54 | Send any amount between{' '} 55 | and{' '} 56 | to the address 57 | above 58 | {bitcoinBalance > 0 && ( 59 | <> (on top of the already deposited funds) 60 | )} 61 |
  • 62 |
  • 63 | All Bitcoin sent to this this address will converted into 64 | Monero at an exchance rate of{' '} 65 | 66 |
  • 67 |
  • 68 | The network fee of{' '} 69 | will 70 | automatically be deducted from the deposited coins 71 |
  • 72 |
  • 73 | The swap will start automatically as soon as the minimum 74 | amount is deposited 75 |
  • 76 |
77 |
78 | 82 |
83 | } 84 | icon={} 85 | /> 86 | 87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /src/renderer/components/modal/updater/UpdaterDialog.tsx: -------------------------------------------------------------------------------- 1 | import SystemUpdateIcon from '@material-ui/icons/SystemUpdate'; 2 | import { 3 | Dialog, 4 | DialogTitle, 5 | DialogContent, 6 | DialogContentText, 7 | DialogActions, 8 | Button, 9 | Typography, 10 | makeStyles, 11 | } from '@material-ui/core'; 12 | import { UpdateFileInfo, UpdateInfo } from 'electron-updater'; 13 | import { useAppDispatch, useAppSelector } from 'store/hooks'; 14 | import { updateShownToUser } from 'store/features/updateSlice'; 15 | 16 | const useStyles = makeStyles((theme) => ({ 17 | fileLink: { 18 | '&:hover': { 19 | textDecoration: 'underline', 20 | cursor: 'pointer', 21 | }, 22 | }, 23 | })); 24 | 25 | export default function UpdaterDialog() { 26 | const classes = useStyles(); 27 | const dispatch = useAppDispatch(); 28 | const updateNotification: UpdateInfo | null = useAppSelector( 29 | (state) => state.update.updateNotification, 30 | ); 31 | 32 | if (updateNotification == null) return null; 33 | 34 | function hideNotification() { 35 | dispatch(updateShownToUser()); 36 | } 37 | 38 | const releasePageUrl = `https://github.com/UnstoppableSwap/unstoppableswap-gui/releases/tag/v${ 39 | updateNotification!.version 40 | }/`; 41 | const releasePageDownloadBaseUrl = `https://github.com/UnstoppableSwap/unstoppableswap-gui/releases/download/v${updateNotification!.version}`; 42 | 43 | function openDownloadUrl() { 44 | window.open(releasePageUrl, '_blank'); 45 | hideNotification(); 46 | } 47 | 48 | function openDownloadFile(file: UpdateFileInfo) { 49 | window.open(releasePageDownloadBaseUrl + '/' + file.url, '_blank'); 50 | } 51 | 52 | return ( 53 | hideNotification()} 58 | > 59 | Update Available 60 | 61 | 62 | A new version (v{updateNotification.version}) of the software was 63 | released on{' '} 64 | {new Date(updateNotification.releaseDate).toLocaleDateString()}. 65 |
66 | Updating ensures you have the latest improvements and security fixes. 67 | Please visit the release page, download one of the files listed below, 68 | and install it manually. 69 | 70 |
    71 | {updateNotification.files.map((file) => ( 72 |
  • openDownloadFile(file)} 76 | > 77 | {file.url} 78 |
  • 79 | ))} 80 |
81 |
82 |
83 |
84 | 85 | 92 | 100 | 101 |
102 | ); 103 | } 104 | -------------------------------------------------------------------------------- /src/renderer/components/modal/wallet/WithdrawDialog.tsx: -------------------------------------------------------------------------------- 1 | import { Dialog } from '@material-ui/core'; 2 | import { useAppDispatch, useIsRpcEndpointBusy } from 'store/hooks'; 3 | import { RpcMethod } from 'models/rpcModel'; 4 | import { rpcResetWithdrawTxId } from 'store/features/rpcSlice'; 5 | import WithdrawStatePage from './WithdrawStatePage'; 6 | import DialogHeader from '../DialogHeader'; 7 | 8 | export default function WithdrawDialog({ 9 | open, 10 | onClose, 11 | }: { 12 | open: boolean; 13 | onClose: () => void; 14 | }) { 15 | const isRpcEndpointBusy = useIsRpcEndpointBusy(RpcMethod.WITHDRAW_BTC); 16 | const dispatch = useAppDispatch(); 17 | 18 | function onCancel() { 19 | if (!isRpcEndpointBusy) { 20 | onClose(); 21 | dispatch(rpcResetWithdrawTxId()); 22 | } 23 | } 24 | 25 | // This prevents an issue where the Dialog is shown for a split second without a present withdraw state 26 | if (!open && !isRpcEndpointBusy) return null; 27 | 28 | return ( 29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/renderer/components/modal/wallet/WithdrawDialogContent.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { Box, DialogContent, makeStyles } from '@material-ui/core'; 3 | import WithdrawStepper from './WithdrawStepper'; 4 | 5 | const useStyles = makeStyles({ 6 | outer: { 7 | minHeight: '15rem', 8 | display: 'flex', 9 | flexDirection: 'column', 10 | justifyContent: 'space-between', 11 | }, 12 | }); 13 | 14 | export default function WithdrawDialogContent({ 15 | children, 16 | }: { 17 | children: ReactNode; 18 | }) { 19 | const classes = useStyles(); 20 | 21 | return ( 22 | 23 | {children} 24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/renderer/components/modal/wallet/WithdrawStatePage.tsx: -------------------------------------------------------------------------------- 1 | import { useAppSelector, useIsRpcEndpointBusy } from 'store/hooks'; 2 | import { RpcMethod } from 'models/rpcModel'; 3 | import AddressInputPage from './pages/AddressInputPage'; 4 | import InitiatedPage from './pages/InitiatedPage'; 5 | import BtcTxInMempoolPageContent from './pages/BitcoinWithdrawTxInMempoolPage'; 6 | 7 | export default function WithdrawStatePage({ 8 | onCancel, 9 | }: { 10 | onCancel: () => void; 11 | }) { 12 | const isRpcEndpointBusy = useIsRpcEndpointBusy(RpcMethod.WITHDRAW_BTC); 13 | const withdrawTxId = useAppSelector((state) => state.rpc.state.withdrawTxId); 14 | 15 | if (withdrawTxId !== null) { 16 | return ( 17 | 21 | ); 22 | } 23 | if (isRpcEndpointBusy) { 24 | return ; 25 | } 26 | return ; 27 | } 28 | -------------------------------------------------------------------------------- /src/renderer/components/modal/wallet/WithdrawStepper.tsx: -------------------------------------------------------------------------------- 1 | import { Step, StepLabel, Stepper } from '@material-ui/core'; 2 | import { useAppSelector, useIsRpcEndpointBusy } from 'store/hooks'; 3 | import { RpcMethod } from 'models/rpcModel'; 4 | 5 | function getActiveStep( 6 | isWithdrawInProgress: boolean, 7 | withdrawTxId: string | null, 8 | ) { 9 | if (isWithdrawInProgress) { 10 | return 1; 11 | } 12 | if (withdrawTxId !== null) { 13 | return 2; 14 | } 15 | return 0; 16 | } 17 | 18 | export default function WithdrawStepper() { 19 | const isWithdrawInProgress = useIsRpcEndpointBusy(RpcMethod.WITHDRAW_BTC); 20 | const withdrawTxId = useAppSelector((s) => s.rpc.state.withdrawTxId); 21 | 22 | return ( 23 | 24 | 25 | Enter withdraw address 26 | 27 | 28 | Transfer funds to wallet 29 | 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/renderer/components/modal/wallet/pages/AddressInputPage.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Button, DialogActions, DialogContentText } from '@material-ui/core'; 3 | import BitcoinAddressTextField from '../../../inputs/BitcoinAddressTextField'; 4 | import WithdrawDialogContent from '../WithdrawDialogContent'; 5 | import IpcInvokeButton from '../../../IpcInvokeButton'; 6 | 7 | export default function AddressInputPage({ 8 | onCancel, 9 | }: { 10 | onCancel: () => void; 11 | }) { 12 | const [withdrawAddressValid, setWithdrawAddressValid] = useState(false); 13 | const [withdrawAddress, setWithdrawAddress] = useState(''); 14 | 15 | return ( 16 | <> 17 | 18 | 19 | To withdraw the BTC of the internal wallet, please enter an address. 20 | All funds will be sent to that address. 21 | 22 | 23 | 30 | 31 | 32 | 33 | 36 | 44 | Withdraw 45 | 46 | 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/renderer/components/modal/wallet/pages/BitcoinWithdrawTxInMempoolPage.tsx: -------------------------------------------------------------------------------- 1 | import { Button, DialogActions, DialogContentText } from '@material-ui/core'; 2 | import BitcoinTransactionInfoBox from '../../swap/BitcoinTransactionInfoBox'; 3 | import WithdrawDialogContent from '../WithdrawDialogContent'; 4 | 5 | export default function BtcTxInMempoolPageContent({ 6 | withdrawTxId, 7 | onCancel, 8 | }: { 9 | withdrawTxId: string; 10 | onCancel: () => void; 11 | }) { 12 | return ( 13 | <> 14 | 15 | 16 | All funds of the internal Bitcoin wallet have been transferred to your 17 | withdraw address. 18 | 19 | 25 | 26 | 27 | 30 | 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/renderer/components/modal/wallet/pages/InitiatedPage.tsx: -------------------------------------------------------------------------------- 1 | import { Button, DialogActions } from '@material-ui/core'; 2 | import CircularProgressWithSubtitle from '../../swap/CircularProgressWithSubtitle'; 3 | import WithdrawDialogContent from '../WithdrawDialogContent'; 4 | 5 | export default function InitiatedPage({ onCancel }: { onCancel: () => void }) { 6 | return ( 7 | <> 8 | 9 | 10 | 11 | 12 | 15 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/renderer/components/navigation/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import { Drawer, makeStyles, Box } from '@material-ui/core'; 2 | import NavigationHeader from './NavigationHeader'; 3 | import NavigationFooter from './NavigationFooter'; 4 | 5 | export const drawerWidth = 240; 6 | 7 | const useStyles = makeStyles({ 8 | drawer: { 9 | width: drawerWidth, 10 | flexShrink: 0, 11 | }, 12 | drawerPaper: { 13 | width: drawerWidth, 14 | }, 15 | drawerContainer: { 16 | overflow: 'auto', 17 | display: 'flex', 18 | flexDirection: 'column', 19 | justifyContent: 'space-between', 20 | height: '100%', 21 | }, 22 | }); 23 | 24 | export default function Navigation() { 25 | const classes = useStyles(); 26 | 27 | return ( 28 | 35 | 36 | 37 | 38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/renderer/components/navigation/NavigationFooter.tsx: -------------------------------------------------------------------------------- 1 | import RedditIcon from '@material-ui/icons/Reddit'; 2 | import GitHubIcon from '@material-ui/icons/GitHub'; 3 | import { Box, makeStyles } from '@material-ui/core'; 4 | import LinkIconButton from '../icons/LinkIconButton'; 5 | import UnfinishedSwapsAlert from '../alert/UnfinishedSwapsAlert'; 6 | import FundsLeftInWalletAlert from '../alert/FundsLeftInWalletAlert'; 7 | import RpcStatusAlert from '../alert/RpcStatusAlert'; 8 | import DiscordIcon from '../icons/DiscordIcon'; 9 | import { DISCORD_URL } from '../pages/help/ContactInfoBox'; 10 | import MoneroWalletRpcUpdatingAlert from '../alert/MoneroWalletRpcUpdatingAlert'; 11 | 12 | const useStyles = makeStyles((theme) => ({ 13 | outer: { 14 | display: 'flex', 15 | flexDirection: 'column', 16 | padding: theme.spacing(1), 17 | gap: theme.spacing(1), 18 | }, 19 | linksOuter: { 20 | display: 'flex', 21 | justifyContent: 'space-evenly', 22 | }, 23 | })); 24 | 25 | export default function NavigationFooter() { 26 | const classes = useStyles(); 27 | 28 | return ( 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/renderer/components/navigation/NavigationHeader.tsx: -------------------------------------------------------------------------------- 1 | import { Box, List } from '@material-ui/core'; 2 | import SwapHorizOutlinedIcon from '@material-ui/icons/SwapHorizOutlined'; 3 | import HistoryOutlinedIcon from '@material-ui/icons/HistoryOutlined'; 4 | import AccountBalanceWalletIcon from '@material-ui/icons/AccountBalanceWallet'; 5 | import HelpOutlineIcon from '@material-ui/icons/HelpOutline'; 6 | import RouteListItemIconButton from './RouteListItemIconButton'; 7 | import UnfinishedSwapsBadge from './UnfinishedSwapsCountBadge'; 8 | 9 | export default function NavigationHeader() { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/renderer/components/navigation/RouteListItemIconButton.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { ListItem, ListItemIcon, ListItemText } from '@material-ui/core'; 4 | 5 | export default function RouteListItemIconButton({ 6 | name, 7 | route, 8 | children, 9 | }: { 10 | name: string; 11 | route: string; 12 | children: ReactNode; 13 | }) { 14 | const navigate = useNavigate(); 15 | 16 | return ( 17 | navigate(route)} key={name}> 18 | {children} 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/components/navigation/UnfinishedSwapsCountBadge.tsx: -------------------------------------------------------------------------------- 1 | import { Badge } from '@material-ui/core'; 2 | import { useResumeableSwapsCountExcludingPunished } from 'store/hooks'; 3 | 4 | export default function UnfinishedSwapsBadge({ 5 | children, 6 | }: { 7 | children: JSX.Element; 8 | }) { 9 | const resumableSwapsCount = useResumeableSwapsCountExcludingPunished(); 10 | 11 | if (resumableSwapsCount > 0) { 12 | return ( 13 | 14 | {children} 15 | 16 | ); 17 | } 18 | return children; 19 | } 20 | -------------------------------------------------------------------------------- /src/renderer/components/other/ExpandableSearchBox.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Box, IconButton, TextField } from '@material-ui/core'; 3 | import SearchIcon from '@material-ui/icons/Search'; 4 | import CloseIcon from '@material-ui/icons/Close'; 5 | 6 | export function ExpandableSearchBox({ 7 | query, 8 | setQuery, 9 | }: { 10 | query: string; 11 | setQuery: (query: string) => void; 12 | }) { 13 | const [expanded, setExpanded] = useState(false); 14 | 15 | return ( 16 | 17 | 18 | {expanded ? ( 19 | <> 20 | setQuery(e.target.value)} 23 | autoFocus 24 | size="small" 25 | /> 26 | { 28 | setExpanded(false); 29 | setQuery(''); 30 | }} 31 | size="small" 32 | > 33 | 34 | 35 | 36 | ) : ( 37 | setExpanded(true)} size="small"> 38 | 39 | 40 | )} 41 | 42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/renderer/components/other/HumanizedBitcoinBlockDuration.tsx: -------------------------------------------------------------------------------- 1 | import humanizeDuration from 'humanize-duration'; 2 | 3 | const AVG_BLOCK_TIME_MS = 10 * 60 * 1000; 4 | 5 | export default function HumanizedBitcoinBlockDuration({ 6 | blocks, 7 | }: { 8 | blocks: number; 9 | }) { 10 | return ( 11 | <> 12 | {`${humanizeDuration(blocks * AVG_BLOCK_TIME_MS, { 13 | conjunction: ' and ', 14 | })} (${blocks} blocks)`} 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/renderer/components/other/JSONViewTree.tsx: -------------------------------------------------------------------------------- 1 | import TreeView from '@material-ui/lab/TreeView'; 2 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; 3 | import ChevronRightIcon from '@material-ui/icons/ChevronRight'; 4 | import TreeItem from '@material-ui/lab/TreeItem'; 5 | import ScrollablePaperTextBox from './ScrollablePaperTextBox'; 6 | 7 | interface JsonTreeViewProps { 8 | data: any; 9 | label: string; 10 | } 11 | 12 | export default function JsonTreeView({ data, label }: JsonTreeViewProps) { 13 | const renderTree = (nodes: any, parentId: string) => { 14 | return Object.keys(nodes).map((key, _) => { 15 | const nodeId = `${parentId}.${key}`; 16 | if (typeof nodes[key] === 'object' && nodes[key] !== null) { 17 | return ( 18 | 19 | {renderTree(nodes[key], nodeId)} 20 | 21 | ); 22 | } 23 | return ( 24 | 29 | ); 30 | }); 31 | }; 32 | 33 | return ( 34 | } 40 | defaultExpandIcon={} 41 | defaultExpanded={['root']} 42 | > 43 | 44 | {renderTree(data ?? {}, 'root')} 45 | 46 | , 47 | ]} 48 | /> 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/renderer/components/other/LoadingButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button, { ButtonProps } from '@material-ui/core/Button'; 3 | import CircularProgress from '@material-ui/core/CircularProgress'; 4 | 5 | interface LoadingButtonProps extends ButtonProps { 6 | loading: boolean; 7 | } 8 | 9 | const LoadingButton: React.FC = ({ 10 | loading, 11 | disabled, 12 | children, 13 | ...props 14 | }) => { 15 | return ( 16 | 23 | ); 24 | }; 25 | 26 | export default LoadingButton; 27 | -------------------------------------------------------------------------------- /src/renderer/components/other/RenderedCliLog.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Chip, Typography } from '@material-ui/core'; 2 | import { useMemo, useState } from 'react'; 3 | import { CliLog } from 'models/cliModel'; 4 | import { logsToRawString } from 'utils/parseUtils'; 5 | import ScrollablePaperTextBox from './ScrollablePaperTextBox'; 6 | 7 | function RenderedCliLog({ log }: { log: CliLog }) { 8 | const { timestamp, level, fields } = log; 9 | 10 | const levelColorMap = { 11 | DEBUG: '#1976d2', // Blue 12 | INFO: '#388e3c', // Green 13 | WARN: '#fbc02d', // Yellow 14 | ERROR: '#d32f2f', // Red 15 | TRACE: '#8e24aa', // Purple 16 | }; 17 | 18 | return ( 19 | 20 | 27 | 32 | 33 | {fields.message} 34 | 35 | 43 | {Object.entries(fields).map(([key, value]) => { 44 | if (key !== 'message') { 45 | return ( 46 | 47 | {key}: {JSON.stringify(value)} 48 | 49 | ); 50 | } 51 | return null; 52 | })} 53 | 54 | 55 | ); 56 | } 57 | 58 | export default function CliLogsBox({ 59 | label, 60 | logs, 61 | }: { 62 | label: string; 63 | logs: (CliLog | string)[]; 64 | }) { 65 | const [searchQuery, setSearchQuery] = useState(''); 66 | 67 | const memoizedLogs = useMemo(() => { 68 | if (searchQuery.length === 0) { 69 | return logs; 70 | } 71 | return logs.filter((log) => 72 | JSON.stringify(log).toLowerCase().includes(searchQuery.toLowerCase()), 73 | ); 74 | }, [logs, searchQuery]); 75 | 76 | return ( 77 | 83 | typeof log === 'string' ? ( 84 | {log} 85 | ) : ( 86 | 87 | ), 88 | )} 89 | /> 90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /src/renderer/components/other/ScrollablePaperTextBox.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Divider, IconButton, Paper, Typography } from '@material-ui/core'; 2 | import { ReactNode, useRef } from 'react'; 3 | import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown'; 4 | import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp'; 5 | import { VList, VListHandle } from 'virtua'; 6 | import FileCopyOutlinedIcon from '@material-ui/icons/FileCopyOutlined'; 7 | import { ExpandableSearchBox } from './ExpandableSearchBox'; 8 | 9 | const MIN_HEIGHT = '10rem'; 10 | 11 | export default function ScrollablePaperTextBox({ 12 | rows, 13 | title, 14 | copyValue, 15 | searchQuery, 16 | setSearchQuery, 17 | minHeight, 18 | }: { 19 | rows: ReactNode[]; 20 | title: string; 21 | copyValue: string; 22 | searchQuery?: string; 23 | setSearchQuery?: (query: string) => void; 24 | minHeight?: string; 25 | }) { 26 | const virtuaEl = useRef(null); 27 | 28 | function onCopy() { 29 | navigator.clipboard.writeText(copyValue); 30 | } 31 | 32 | function scrollToBottom() { 33 | virtuaEl.current?.scrollToIndex(rows.length - 1); 34 | } 35 | 36 | function scrollToTop() { 37 | virtuaEl.current?.scrollToIndex(0); 38 | } 39 | 40 | return ( 41 | 51 | {title} 52 | 53 | 64 | 65 | {rows} 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | {searchQuery !== undefined && setSearchQuery !== undefined && ( 79 | 80 | )} 81 | 82 | 83 | ); 84 | } 85 | 86 | ScrollablePaperTextBox.defaultProps = { 87 | searchQuery: undefined, 88 | setSearchQuery: undefined, 89 | minHeight: MIN_HEIGHT, 90 | }; 91 | -------------------------------------------------------------------------------- /src/renderer/components/other/Units.tsx: -------------------------------------------------------------------------------- 1 | import { piconerosToXmr, satsToBtc } from 'utils/conversionUtils'; 2 | import { Tooltip } from '@material-ui/core'; 3 | import { useAppSelector } from 'store/hooks'; 4 | 5 | type Amount = number | null | undefined; 6 | 7 | export function AmountWithUnit({ 8 | amount, 9 | unit, 10 | fixedPrecision, 11 | dollarRate, 12 | }: { 13 | amount: Amount; 14 | unit: string; 15 | fixedPrecision: number; 16 | dollarRate?: Amount; 17 | }) { 18 | return ( 19 | 27 | 28 | {amount != null 29 | ? Number.parseFloat(amount.toFixed(fixedPrecision)) 30 | : '?'}{' '} 31 | {unit} 32 | 33 | 34 | ); 35 | } 36 | 37 | AmountWithUnit.defaultProps = { 38 | dollarRate: null, 39 | }; 40 | 41 | export function BitcoinAmount({ amount }: { amount: Amount }) { 42 | const btcUsdRate = useAppSelector((state) => state.rates.btcPrice); 43 | 44 | return ( 45 | 51 | ); 52 | } 53 | 54 | export function MoneroAmount({ amount }: { amount: Amount }) { 55 | const xmrUsdRate = useAppSelector((state) => state.rates.xmrPrice); 56 | 57 | return ( 58 | 64 | ); 65 | } 66 | 67 | export function MoneroBitcoinExchangeRate({ rate }: { rate: Amount }) { 68 | return ; 69 | } 70 | 71 | export function SatsAmount({ amount }: { amount: Amount }) { 72 | const btcAmount = amount == null ? null : satsToBtc(amount); 73 | return ; 74 | } 75 | 76 | export function PiconeroAmount({ amount }: { amount: Amount }) { 77 | return ( 78 | 79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /src/renderer/components/pages/help/ContactInfoBox.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Button, makeStyles, Typography } from '@material-ui/core'; 2 | import InfoBox from '../../modal/swap/InfoBox'; 3 | 4 | const useStyles = makeStyles((theme) => ({ 5 | spacedBox: { 6 | display: 'flex', 7 | gap: theme.spacing(1), 8 | }, 9 | })); 10 | 11 | const GITHUB_ISSUE_URL = 12 | 'https://github.com/UnstoppableSwap/unstoppableswap-gui/issues/new/choose'; 13 | const MATRIX_ROOM_URL = 'https://matrix.to/#/#unstoppableswap:matrix.org'; 14 | export const DISCORD_URL = 'https://discord.gg/APJ6rJmq'; 15 | 16 | export default function ContactInfoBox() { 17 | const classes = useStyles(); 18 | 19 | return ( 20 | 24 | If you need help or just want to reach out to the contributors of this 25 | project you can open a GitHub issue, join our Matrix room or Discord 26 | 27 | } 28 | additionalContent={ 29 | 30 | 36 | 42 | 45 | 46 | } 47 | icon={null} 48 | loading={false} 49 | /> 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /src/renderer/components/pages/help/DonateInfoBox.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from '@material-ui/core'; 2 | import DepositAddressInfoBox from '../../modal/swap/DepositAddressInfoBox'; 3 | import MoneroIcon from '../../icons/MoneroIcon'; 4 | 5 | const XMR_DONATE_ADDRESS = 6 | '87jS4C7ngk9EHdqFFuxGFgg8AyH63dRUoULshWDybFJaP75UA89qsutG5B1L1QTc4w228nsqsv8EjhL7bz8fB3611Mh98mg'; 7 | 8 | export default function DonateInfoBox() { 9 | return ( 10 | } 14 | additionalContent={ 15 | 16 | We rely on generous donors like you to keep development moving 17 | forward. To bring Atomic Swaps to life, we need resources. If you have 18 | the possibility, please consider making a donation to the project. All 19 | funds will be used to support contributors and critical 20 | infrastructure. 21 | 22 | } 23 | /> 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/renderer/components/pages/help/FeedbackInfoBox.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Typography } from '@material-ui/core'; 2 | import { useState } from 'react'; 3 | import InfoBox from '../../modal/swap/InfoBox'; 4 | import FeedbackDialog from '../../modal/feedback/FeedbackDialog'; 5 | 6 | export default function FeedbackInfoBox() { 7 | const [showDialog, setShowDialog] = useState(false); 8 | 9 | return ( 10 | 14 | The main goal of this project is to make Atomic Swaps easier to use, 15 | and for that we need genuine users' input. Please leave some 16 | feedback, it takes just two minutes. I'll read each and every 17 | survey response and take your feedback into consideration. 18 | 19 | } 20 | additionalContent={ 21 | <> 22 | 25 | setShowDialog(false)} 28 | /> 29 | 30 | } 31 | icon={null} 32 | loading={false} 33 | /> 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/renderer/components/pages/help/HelpPage.tsx: -------------------------------------------------------------------------------- 1 | import { Box, makeStyles } from '@material-ui/core'; 2 | import ContactInfoBox from './ContactInfoBox'; 3 | import FeedbackInfoBox from './FeedbackInfoBox'; 4 | import DonateInfoBox from './DonateInfoBox'; 5 | import TorInfoBox from './TorInfoBox'; 6 | import RpcControlBox from './RpcControlBox'; 7 | 8 | const useStyles = makeStyles((theme) => ({ 9 | outer: { 10 | display: 'flex', 11 | gap: theme.spacing(2), 12 | flexDirection: 'column', 13 | }, 14 | })); 15 | 16 | export default function HelpPage() { 17 | const classes = useStyles(); 18 | 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/renderer/components/pages/help/RpcControlBox.tsx: -------------------------------------------------------------------------------- 1 | import { Box, makeStyles } from '@material-ui/core'; 2 | import IpcInvokeButton from 'renderer/components/IpcInvokeButton'; 3 | import { useAppSelector } from 'store/hooks'; 4 | import StopIcon from '@material-ui/icons/Stop'; 5 | import PlayArrowIcon from '@material-ui/icons/PlayArrow'; 6 | import { RpcProcessStateType } from 'models/rpcModel'; 7 | import { getLogsAndStringsFromRawFileString } from 'utils/parseUtils'; 8 | import InfoBox from '../../modal/swap/InfoBox'; 9 | import CliLogsBox from '../../other/RenderedCliLog'; 10 | import FolderOpenIcon from '@material-ui/icons/FolderOpen'; 11 | 12 | const useStyles = makeStyles((theme) => ({ 13 | actionsOuter: { 14 | display: 'flex', 15 | gap: theme.spacing(1), 16 | alignItems: 'center', 17 | }, 18 | })); 19 | 20 | export default function RpcControlBox() { 21 | const rpcProcess = useAppSelector((state) => state.rpc.process); 22 | const isRunning = 23 | rpcProcess.type === RpcProcessStateType.STARTED || 24 | rpcProcess.type === RpcProcessStateType.LISTENING_FOR_CONNECTIONS; 25 | const classes = useStyles(); 26 | 27 | return ( 28 | 36 | ) : null 37 | } 38 | additionalContent={ 39 | 40 | } 45 | disabled={isRunning} 46 | requiresRpc={false} 47 | > 48 | Start Daemon 49 | 50 | } 55 | disabled={!isRunning} 56 | requiresRpc={false} 57 | > 58 | Stop Daemon 59 | 60 | } 64 | requiresRpc={false} 65 | isIconButton 66 | size="small" 67 | tooltipTitle="Open the data directory of the Swap Daemon in your file explorer" 68 | /> 69 | 70 | } 71 | icon={null} 72 | loading={false} 73 | /> 74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /src/renderer/components/pages/help/TorInfoBox.tsx: -------------------------------------------------------------------------------- 1 | import { Box, makeStyles, Typography } from '@material-ui/core'; 2 | import IpcInvokeButton from 'renderer/components/IpcInvokeButton'; 3 | import { useAppSelector } from 'store/hooks'; 4 | import StopIcon from '@material-ui/icons/Stop'; 5 | import PlayArrowIcon from '@material-ui/icons/PlayArrow'; 6 | import InfoBox from '../../modal/swap/InfoBox'; 7 | import CliLogsBox from '../../other/RenderedCliLog'; 8 | 9 | const useStyles = makeStyles((theme) => ({ 10 | actionsOuter: { 11 | display: 'flex', 12 | gap: theme.spacing(1), 13 | }, 14 | })); 15 | 16 | export default function TorInfoBox() { 17 | const isTorRunning = useAppSelector((state) => state.tor.processRunning); 18 | const torStdOut = useAppSelector((s) => s.tor.stdOut); 19 | const classes = useStyles(); 20 | 21 | return ( 22 | 33 | 34 | Tor is a network that allows you to anonymously connect to the 35 | internet. It is a free and open network that is operated by 36 | volunteers. You can start and stop Tor by clicking the buttons 37 | below. If Tor is running, all traffic will be routed through it and 38 | the swap provider will not be able to see your IP address. 39 | 40 | 41 | 42 | } 43 | additionalContent={ 44 | 45 | } 51 | requiresRpc={false} 52 | > 53 | Start Tor 54 | 55 | } 61 | requiresRpc={false} 62 | > 63 | Stop Tor 64 | 65 | 66 | } 67 | icon={null} 68 | loading={false} 69 | /> 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /src/renderer/components/pages/history/HistoryPage.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from '@material-ui/core'; 2 | import { useIsSwapRunning } from 'store/hooks'; 3 | import HistoryTable from './table/HistoryTable'; 4 | import SwapDialog from '../../modal/swap/SwapDialog'; 5 | import SwapTxLockAlertsBox from '../../alert/SwapTxLockAlertsBox'; 6 | 7 | export default function HistoryPage() { 8 | const showDialog = useIsSwapRunning(); 9 | 10 | return ( 11 | <> 12 | History 13 | 14 | 15 | {}} /> 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/renderer/components/pages/history/table/HistoryRow.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Collapse, 4 | IconButton, 5 | makeStyles, 6 | TableCell, 7 | TableRow, 8 | } from '@material-ui/core'; 9 | import { useState } from 'react'; 10 | import ArrowForwardIcon from '@material-ui/icons/ArrowForward'; 11 | import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown'; 12 | import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp'; 13 | import { 14 | getHumanReadableDbStateType, 15 | getSwapBtcAmount, 16 | getSwapXmrAmount, 17 | GetSwapInfoResponse, 18 | } from '../../../../../models/rpcModel'; 19 | import HistoryRowActions from './HistoryRowActions'; 20 | import HistoryRowExpanded from './HistoryRowExpanded'; 21 | import { BitcoinAmount, MoneroAmount } from '../../../other/Units'; 22 | 23 | type HistoryRowProps = { 24 | swap: GetSwapInfoResponse; 25 | }; 26 | 27 | const useStyles = makeStyles((theme) => ({ 28 | amountTransferContainer: { 29 | display: 'flex', 30 | alignItems: 'center', 31 | gap: theme.spacing(1), 32 | }, 33 | })); 34 | 35 | function AmountTransfer({ 36 | btcAmount, 37 | xmrAmount, 38 | }: { 39 | xmrAmount: number; 40 | btcAmount: number; 41 | }) { 42 | const classes = useStyles(); 43 | 44 | return ( 45 | 46 | 47 | 48 | 49 | 50 | ); 51 | } 52 | 53 | export default function HistoryRow({ swap }: HistoryRowProps) { 54 | const btcAmount = getSwapBtcAmount(swap); 55 | const xmrAmount = getSwapXmrAmount(swap); 56 | 57 | const [expanded, setExpanded] = useState(false); 58 | 59 | return ( 60 | <> 61 | 62 | 63 | setExpanded(!expanded)}> 64 | {expanded ? : } 65 | 66 | 67 | {swap.swapId.substring(0, 5)}... 68 | 69 | 70 | 71 | {getHumanReadableDbStateType(swap.stateName)} 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | {expanded && } 81 | 82 | 83 | 84 | 85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /src/renderer/components/pages/history/table/HistoryRowActions.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip } from '@material-ui/core'; 2 | import Button, { ButtonProps } from '@material-ui/core/Button/Button'; 3 | import DoneIcon from '@material-ui/icons/Done'; 4 | import ErrorIcon from '@material-ui/icons/Error'; 5 | import { green, red } from '@material-ui/core/colors'; 6 | import PlayArrowIcon from '@material-ui/icons/PlayArrow'; 7 | import IpcInvokeButton from '../../../IpcInvokeButton'; 8 | import { 9 | GetSwapInfoResponse, 10 | SwapStateName, 11 | isSwapStateNamePossiblyCancellableSwap, 12 | isSwapStateNamePossiblyRefundableSwap, 13 | } from '../../../../../models/rpcModel'; 14 | 15 | export function SwapResumeButton({ 16 | swap, 17 | children, 18 | ...props 19 | }: { swap: GetSwapInfoResponse } & ButtonProps) { 20 | return ( 21 | } 28 | requiresRpc 29 | {...props} 30 | > 31 | {children} 32 | 33 | ); 34 | } 35 | 36 | export function SwapCancelRefundButton({ 37 | swap, 38 | ...props 39 | }: { swap: GetSwapInfoResponse } & ButtonProps) { 40 | const cancelOrRefundable = 41 | isSwapStateNamePossiblyCancellableSwap(swap.stateName) || 42 | isSwapStateNamePossiblyRefundableSwap(swap.stateName); 43 | 44 | if (!cancelOrRefundable) { 45 | return <>; 46 | } 47 | 48 | return ( 49 | 56 | Attempt manual Cancel & Refund 57 | 58 | ); 59 | } 60 | 61 | export default function HistoryRowActions({ 62 | swap, 63 | }: { 64 | swap: GetSwapInfoResponse; 65 | }) { 66 | if (swap.stateName === SwapStateName.XmrRedeemed) { 67 | return ( 68 | 69 | 70 | 71 | ); 72 | } 73 | 74 | if (swap.stateName === SwapStateName.BtcRefunded) { 75 | return ( 76 | 77 | 78 | 79 | ); 80 | } 81 | 82 | if (swap.stateName === SwapStateName.BtcPunished) { 83 | return ( 84 | 85 | Attempt recovery 86 | 87 | ); 88 | } 89 | 90 | return Resume; 91 | } 92 | -------------------------------------------------------------------------------- /src/renderer/components/pages/history/table/HistoryTable.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | makeStyles, 4 | Paper, 5 | Table, 6 | TableBody, 7 | TableCell, 8 | TableContainer, 9 | TableHead, 10 | TableRow, 11 | } from '@material-ui/core'; 12 | import { sortBy } from 'lodash'; 13 | import { parseDateString } from 'utils/parseUtils'; 14 | import { 15 | useAppSelector, 16 | useSwapInfosSortedByDate, 17 | } from '../../../../../store/hooks'; 18 | import HistoryRow from './HistoryRow'; 19 | 20 | const useStyles = makeStyles((theme) => ({ 21 | outer: { 22 | paddingTop: theme.spacing(1), 23 | paddingBottom: theme.spacing(1), 24 | }, 25 | })); 26 | 27 | export default function HistoryTable() { 28 | const classes = useStyles(); 29 | const swapSortedByDate = useSwapInfosSortedByDate(); 30 | 31 | return ( 32 | 33 | 34 | 35 | 36 | 37 | 38 | ID 39 | Amount 40 | State 41 | 42 | 43 | 44 | 45 | {swapSortedByDate.map((swap) => ( 46 | 47 | ))} 48 | 49 |
50 |
51 |
52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/renderer/components/pages/history/table/SwapLogFileOpenButton.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonProps } from '@material-ui/core/Button/Button'; 2 | import { 3 | Button, 4 | Dialog, 5 | DialogActions, 6 | DialogContent, 7 | DialogTitle, 8 | } from '@material-ui/core'; 9 | import { useState } from 'react'; 10 | import { CliLog } from 'models/cliModel'; 11 | import IpcInvokeButton from '../../../IpcInvokeButton'; 12 | import CliLogsBox from '../../../other/RenderedCliLog'; 13 | 14 | export default function SwapLogFileOpenButton({ 15 | swapId, 16 | ...props 17 | }: { swapId: string } & ButtonProps) { 18 | const [logs, setLogs] = useState(null); 19 | 20 | return ( 21 | <> 22 | { 26 | setLogs(data as CliLog[]); 27 | }} 28 | {...props} 29 | > 30 | view log 31 | 32 | {logs && ( 33 | setLogs(null)} fullWidth maxWidth="lg"> 34 | Logs of swap {swapId} 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | )} 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/renderer/components/pages/swap/ApiAlertsBox.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from '@material-ui/core'; 2 | import { Alert, AlertTitle } from '@material-ui/lab'; 3 | import { removeAlert } from 'store/features/alertsSlice'; 4 | import { useAppDispatch, useAppSelector } from 'store/hooks'; 5 | 6 | export default function ApiAlertsBox() { 7 | const alerts = useAppSelector((state) => state.alerts.alerts); 8 | const dispatch = useAppDispatch(); 9 | 10 | function onRemoveAlert(id: number) { 11 | dispatch(removeAlert(id)); 12 | } 13 | 14 | if (alerts.length === 0) return null; 15 | 16 | return ( 17 | 18 | {alerts.map((alert) => ( 19 | onRemoveAlert(alert.id)} 24 | > 25 | {alert.title} 26 | {alert.body} 27 | 28 | ))} 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/renderer/components/pages/swap/SwapPage.tsx: -------------------------------------------------------------------------------- 1 | import { Box, makeStyles } from '@material-ui/core'; 2 | import SwapWidget from './SwapWidget'; 3 | import ApiAlertsBox from './ApiAlertsBox'; 4 | 5 | const useStyles = makeStyles((theme) => ({ 6 | outer: { 7 | display: 'flex', 8 | width: '100%', 9 | flexDirection: 'column', 10 | alignItems: 'center', 11 | paddingBottom: theme.spacing(1), 12 | gap: theme.spacing(1), 13 | }, 14 | })); 15 | 16 | export default function SwapPage() { 17 | const classes = useStyles(); 18 | 19 | return ( 20 | 21 | 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/renderer/components/pages/wallet/WalletPage.tsx: -------------------------------------------------------------------------------- 1 | import { Box, makeStyles, Typography } from '@material-ui/core'; 2 | import { Alert } from '@material-ui/lab'; 3 | import WithdrawWidget from './WithdrawWidget'; 4 | 5 | const useStyles = makeStyles((theme) => ({ 6 | outer: { 7 | display: 'flex', 8 | flexDirection: 'column', 9 | gridGap: theme.spacing(0.5), 10 | }, 11 | })); 12 | 13 | export default function WalletPage() { 14 | const classes = useStyles(); 15 | 16 | return ( 17 | 18 | Wallet 19 | 20 | You do not have to deposit money before starting a swap. Instead, you 21 | will be greeted with a deposit address after you initiate one. 22 | 23 | 24 | If funds are left in your wallet after a swap, you can withdraw them to 25 | your wallet. If you decide to leave them inside the internal wallet, the 26 | funds will automatically be used when starting a new swap. 27 | 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/renderer/components/pages/wallet/WalletRefreshButton.tsx: -------------------------------------------------------------------------------- 1 | import { CircularProgress } from '@material-ui/core'; 2 | import RefreshIcon from '@material-ui/icons/Refresh'; 3 | import IpcInvokeButton from '../../IpcInvokeButton'; 4 | 5 | export default function WalletRefreshButton() { 6 | return ( 7 | } 9 | size="small" 10 | isIconButton 11 | endIcon={} 12 | ipcArgs={[]} 13 | ipcChannel="spawn-balance-check" 14 | /> 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/renderer/components/pages/wallet/WithdrawWidget.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Button, makeStyles, Typography } from '@material-ui/core'; 2 | import { useState } from 'react'; 3 | import SendIcon from '@material-ui/icons/Send'; 4 | import { useAppSelector, useIsRpcEndpointBusy } from 'store/hooks'; 5 | import { RpcMethod } from 'models/rpcModel'; 6 | import BitcoinIcon from '../../icons/BitcoinIcon'; 7 | import WithdrawDialog from '../../modal/wallet/WithdrawDialog'; 8 | import WalletRefreshButton from './WalletRefreshButton'; 9 | import InfoBox from '../../modal/swap/InfoBox'; 10 | import { SatsAmount } from 'renderer/components/other/Units'; 11 | 12 | const useStyles = makeStyles((theme) => ({ 13 | title: { 14 | alignItems: 'center', 15 | display: 'flex', 16 | gap: theme.spacing(0.5), 17 | }, 18 | })); 19 | 20 | export default function WithdrawWidget() { 21 | const classes = useStyles(); 22 | const walletBalance = useAppSelector((state) => state.rpc.state.balance); 23 | const checkingBalance = useIsRpcEndpointBusy(RpcMethod.GET_BTC_BALANCE); 24 | const [showDialog, setShowDialog] = useState(false); 25 | 26 | function onShowDialog() { 27 | setShowDialog(true); 28 | } 29 | 30 | return ( 31 | <> 32 | 35 | Wallet Balance 36 | 37 | 38 | } 39 | mainContent={ 40 | 41 | 42 | 43 | } 44 | icon={} 45 | additionalContent={ 46 | 58 | } 59 | loading={false} 60 | /> 61 | setShowDialog(false)} /> 62 | 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /src/renderer/components/snackbar/GlobalSnackbarProvider.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | MaterialDesignContent, 3 | SnackbarKey, 4 | SnackbarProvider, 5 | useSnackbar, 6 | } from 'notistack'; 7 | import { IconButton, styled } from '@material-ui/core'; 8 | import { Close } from '@material-ui/icons'; 9 | import { ReactNode } from 'react'; 10 | import IpcSnackbar from './IpcSnackbar'; 11 | 12 | const StyledMaterialDesignContent = styled(MaterialDesignContent)(() => ({ 13 | '&.notistack-MuiContent': { 14 | maxWidth: '50vw', 15 | }, 16 | })); 17 | 18 | function CloseSnackbarButton({ snackbarId }: { snackbarId: SnackbarKey }) { 19 | const { closeSnackbar } = useSnackbar(); 20 | 21 | return ( 22 | closeSnackbar(snackbarId)}> 23 | 24 | 25 | ); 26 | } 27 | 28 | export default function GlobalSnackbarManager({ 29 | children, 30 | }: { 31 | children: ReactNode; 32 | }) { 33 | return ( 34 | } 36 | Components={{ 37 | success: StyledMaterialDesignContent, 38 | error: StyledMaterialDesignContent, 39 | default: StyledMaterialDesignContent, 40 | info: StyledMaterialDesignContent, 41 | warning: StyledMaterialDesignContent, 42 | }} 43 | > 44 | 45 | {children} 46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/renderer/components/snackbar/IpcSnackbar.tsx: -------------------------------------------------------------------------------- 1 | import { useSnackbar } from 'notistack'; 2 | import { useEffect } from 'react'; 3 | import { ipcRenderer } from 'electron'; 4 | 5 | export default function IpcSnackbar() { 6 | const { enqueueSnackbar } = useSnackbar(); 7 | 8 | useEffect(() => { 9 | function onIpcMessage( 10 | _: any, 11 | message: string, 12 | variant: any, 13 | autoHideDuration: number | null, 14 | key: string | null, 15 | ) { 16 | enqueueSnackbar(message, { 17 | variant, 18 | autoHideDuration, 19 | key, 20 | preventDuplicate: true, 21 | }); 22 | } 23 | 24 | ipcRenderer.on('display-snackbar-alert', onIpcMessage); 25 | 26 | return () => { 27 | ipcRenderer.removeListener('display-snackbar-alert', onIpcMessage); 28 | }; 29 | }, [enqueueSnackbar]); 30 | 31 | return <>; 32 | } 33 | -------------------------------------------------------------------------------- /src/renderer/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 19 | 20 | 21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /src/renderer/index.tsx: -------------------------------------------------------------------------------- 1 | import { render } from 'react-dom'; 2 | import { Provider } from 'react-redux'; 3 | import { store } from 'renderer/store/storeRenderer'; 4 | import { setRegistryProviders } from 'store/features/providersSlice'; 5 | import { setAlerts } from 'store/features/alertsSlice'; 6 | import { setXmrPrice, setBtcPrice } from 'store/features/ratesSlice'; 7 | import { 8 | fetchAlertsViaHttp, 9 | fetchBtcPrice, 10 | fetchProvidersViaHttp, 11 | fetchXmrPrice, 12 | } from './api'; 13 | import logger from '../utils/logger'; 14 | import App from './components/App'; 15 | import { webFrame } from 'electron'; 16 | 17 | render( 18 | 19 | 20 | , 21 | document.getElementById('root'), 22 | ); 23 | 24 | async function fetchInitialData() { 25 | try { 26 | const providerList = await fetchProvidersViaHttp(); 27 | store.dispatch(setRegistryProviders(providerList)); 28 | 29 | logger.info( 30 | { providerList }, 31 | 'Fetched providers via UnstoppableSwap HTTP API', 32 | ); 33 | } catch (e) { 34 | logger.error(e, 'Failed to fetch providers via UnstoppableSwap HTTP API'); 35 | } 36 | 37 | try { 38 | const alerts = await fetchAlertsViaHttp(); 39 | store.dispatch(setAlerts(alerts)); 40 | logger.info({ alerts }, 'Fetched alerts via UnstoppableSwap HTTP API'); 41 | } catch (e) { 42 | logger.error(e, 'Failed to fetch alerts via UnstoppableSwap HTTP API'); 43 | } 44 | 45 | try { 46 | const xmrPrice = await fetchXmrPrice(); 47 | store.dispatch(setXmrPrice(xmrPrice)); 48 | logger.info({ xmrPrice }, 'Fetched XMR price'); 49 | 50 | const btcPrice = await fetchBtcPrice(); 51 | store.dispatch(setBtcPrice(btcPrice)); 52 | logger.info({ btcPrice }, 'Fetched BTC price'); 53 | } catch (e) { 54 | logger.error(e, 'Error retrieving fiat prices'); 55 | } 56 | } 57 | 58 | fetchInitialData(); 59 | webFrame.setZoomFactor(1.2); 60 | -------------------------------------------------------------------------------- /src/renderer/store/storeRenderer.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import { stateSyncEnhancer } from 'electron-redux'; 3 | import { reducers } from 'store/combinedReducer'; 4 | 5 | export const store = configureStore({ 6 | reducer: reducers, 7 | enhancers: [stateSyncEnhancer()], 8 | }); 9 | 10 | export type AppDispatch = typeof store.dispatch; 11 | export type RootState = ReturnType; 12 | -------------------------------------------------------------------------------- /src/store/combinedReducer.ts: -------------------------------------------------------------------------------- 1 | import swapReducer from './features/swapSlice'; 2 | import providersSlice from './features/providersSlice'; 3 | import torSlice from './features/torSlice'; 4 | import rpcSlice from './features/rpcSlice'; 5 | import alertsSlice from './features/alertsSlice'; 6 | import ratesSlice from './features/ratesSlice'; 7 | import updateSlice from './features/updateSlice'; 8 | 9 | export const reducers = { 10 | swap: swapReducer, 11 | providers: providersSlice, 12 | tor: torSlice, 13 | rpc: rpcSlice, 14 | alerts: alertsSlice, 15 | rates: ratesSlice, 16 | update: updateSlice, 17 | }; 18 | -------------------------------------------------------------------------------- /src/store/config.ts: -------------------------------------------------------------------------------- 1 | import { ExtendedProviderStatus } from 'models/apiModel'; 2 | 3 | const DEFAULT_MAINNET_ELECTRUM_RPC_URL = 'tcp://blockstream.info:110'; 4 | const DEFAULT_TESTNET_ELECTRUM_RPC_URL = 'ssl://testnet.foundation.xyz:50002'; 5 | 6 | export const isTestnet = () => 7 | process.env.TESTNET?.toString().toLowerCase() === 'true'; 8 | 9 | export const isExternalRpc = () => 10 | process.env.USE_EXTERNAL_RPC?.toString().toLowerCase() === 'true'; 11 | 12 | export const isDevelopment = 13 | process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true'; 14 | 15 | export function getStubTestnetProvider(): ExtendedProviderStatus | null { 16 | if ( 17 | !isTestnet() || 18 | !process.env.STUB_TESTNET_PROVIDER_MULTIADDR || 19 | !process.env.STUB_TESTNET_PROVIDER_PEER_ID 20 | ) { 21 | return null; 22 | } 23 | 24 | return { 25 | multiAddr: process.env.STUB_TESTNET_PROVIDER_MULTIADDR, 26 | peerId: process.env.STUB_TESTNET_PROVIDER_PEER_ID, 27 | testnet: true, 28 | age: 0, 29 | maxSwapAmount: 10000000, 30 | minSwapAmount: 100000, 31 | price: 700000, 32 | relevancy: 1, 33 | uptime: 1, 34 | recommended: true, 35 | }; 36 | } 37 | 38 | export const getPlatform = () => { 39 | switch (process.platform) { 40 | case 'darwin': 41 | case 'sunos': 42 | return 'mac'; 43 | case 'win32': 44 | return 'win'; 45 | case 'aix': 46 | case 'freebsd': 47 | case 'linux': 48 | case 'openbsd': 49 | case 'android': 50 | return 'linux'; 51 | default: 52 | return 'linux'; 53 | } 54 | }; 55 | 56 | export function getElectrumRpcUrl(): string { 57 | if (isTestnet()) { 58 | // If running on testnet, return the testnet Electrum RPC URL from environment variable or use the default 59 | return ( 60 | process.env.OVERRIDE_TESTNET_ELECTRUM_RPC_URL ?? 61 | DEFAULT_TESTNET_ELECTRUM_RPC_URL 62 | ); 63 | } 64 | // If running on mainnet, return the mainnet Electrum RPC URL from environment variable or use the default 65 | return ( 66 | process.env.OVERRIDE_ELECTRUM_RPC_URL ?? DEFAULT_MAINNET_ELECTRUM_RPC_URL 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /src/store/features/alertsSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import { Alert } from 'models/apiModel'; 3 | 4 | export interface AlertsSlice { 5 | alerts: Alert[]; 6 | } 7 | 8 | const initialState: AlertsSlice = { 9 | alerts: [], 10 | }; 11 | 12 | const alertsSlice = createSlice({ 13 | name: 'alerts', 14 | initialState, 15 | reducers: { 16 | setAlerts(slice, action: PayloadAction) { 17 | slice.alerts = action.payload; 18 | }, 19 | removeAlert(slice, action: PayloadAction) { 20 | slice.alerts = slice.alerts.filter( 21 | (alert) => alert.id !== action.payload, 22 | ); 23 | }, 24 | }, 25 | }); 26 | 27 | export const { setAlerts, removeAlert } = alertsSlice.actions; 28 | export default alertsSlice.reducer; 29 | -------------------------------------------------------------------------------- /src/store/features/ratesSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | 3 | export interface RatesState { 4 | btcPrice: number | null; 5 | xmrPrice: number | null; 6 | } 7 | 8 | const initialState: RatesState = { 9 | btcPrice: null, 10 | xmrPrice: null, 11 | }; 12 | 13 | const ratesSlice = createSlice({ 14 | name: 'rates', 15 | initialState, 16 | reducers: { 17 | setBtcPrice: (state, action: PayloadAction) => { 18 | state.btcPrice = action.payload; 19 | }, 20 | setXmrPrice: (state, action: PayloadAction) => { 21 | state.xmrPrice = action.payload; 22 | }, 23 | }, 24 | }); 25 | 26 | export const { setBtcPrice, setXmrPrice } = ratesSlice.actions; 27 | 28 | export default ratesSlice.reducer; 29 | -------------------------------------------------------------------------------- /src/store/features/torSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | 3 | export interface TorSlice { 4 | exitCode: number | null; 5 | processRunning: boolean; 6 | stdOut: string; 7 | proxyStatus: 8 | | false 9 | | { 10 | proxyHostname: string; 11 | proxyPort: number; 12 | bootstrapped: boolean; 13 | }; 14 | } 15 | 16 | const initialState: TorSlice = { 17 | processRunning: false, 18 | exitCode: null, 19 | stdOut: '', 20 | proxyStatus: false, 21 | }; 22 | 23 | const socksListenerRegex = 24 | /Opened Socks listener connection.*on (\d+\.\d+\.\d+\.\d+):(\d+)/; 25 | const bootstrapDoneRegex = /Bootstrapped 100% \(done\)/; 26 | 27 | export const torSlice = createSlice({ 28 | name: 'tor', 29 | initialState, 30 | reducers: { 31 | torAppendStdOut(slice, action: PayloadAction) { 32 | slice.stdOut += action.payload; 33 | 34 | const logs = slice.stdOut.split('\n'); 35 | logs.forEach((log) => { 36 | if (socksListenerRegex.test(log)) { 37 | const match = socksListenerRegex.exec(log); 38 | if (match) { 39 | slice.proxyStatus = { 40 | proxyHostname: match[1], 41 | proxyPort: Number.parseInt(match[2], 10), 42 | bootstrapped: slice.proxyStatus 43 | ? slice.proxyStatus.bootstrapped 44 | : false, 45 | }; 46 | } 47 | } else if (bootstrapDoneRegex.test(log)) { 48 | if (slice.proxyStatus) { 49 | slice.proxyStatus.bootstrapped = true; 50 | } 51 | } 52 | }); 53 | }, 54 | torInitiate(slice) { 55 | slice.processRunning = true; 56 | }, 57 | torProcessExited( 58 | slice, 59 | action: PayloadAction<{ 60 | exitCode: number | null; 61 | exitSignal: NodeJS.Signals | null; 62 | }>, 63 | ) { 64 | slice.processRunning = false; 65 | slice.exitCode = action.payload.exitCode; 66 | slice.proxyStatus = false; 67 | }, 68 | }, 69 | }); 70 | 71 | export const { torAppendStdOut, torInitiate, torProcessExited } = 72 | torSlice.actions; 73 | 74 | export default torSlice.reducer; 75 | -------------------------------------------------------------------------------- /src/store/features/updateSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import { UpdateInfo } from 'electron-updater'; 3 | 4 | export interface UpdateState { 5 | updateNotification: UpdateInfo | null; 6 | } 7 | 8 | const initialState: UpdateState = { 9 | updateNotification: null, 10 | }; 11 | 12 | const updateSlice = createSlice({ 13 | name: 'update', 14 | initialState, 15 | reducers: { 16 | updateReceived: (state, action: PayloadAction) => { 17 | state.updateNotification = action.payload; 18 | }, 19 | updateShownToUser: (state) => { 20 | state.updateNotification = null; 21 | }, 22 | }, 23 | }); 24 | 25 | export const { updateReceived, updateShownToUser } = updateSlice.actions; 26 | 27 | export default updateSlice.reducer; 28 | -------------------------------------------------------------------------------- /src/store/hooks.ts: -------------------------------------------------------------------------------- 1 | import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; 2 | import type { AppDispatch, RootState } from 'renderer/store/storeRenderer'; 3 | import { sortBy } from 'lodash'; 4 | import { parseDateString } from 'utils/parseUtils'; 5 | import { GetSwapInfoResponse, SwapStateName } from 'models/rpcModel'; 6 | 7 | // Use throughout your app instead of plain `useDispatch` and `useSelector` 8 | export const useAppDispatch = () => useDispatch(); 9 | export const useAppSelector: TypedUseSelectorHook = useSelector; 10 | 11 | export function useResumeableSwapsCount( 12 | filter?: (s: GetSwapInfoResponse) => boolean, 13 | ) { 14 | return useAppSelector( 15 | (state) => 16 | Object.values(state.rpc.state.swapInfos).filter( 17 | (swapInfo) => 18 | !swapInfo.completed && (filter == null || filter(swapInfo)), 19 | ).length, 20 | ); 21 | } 22 | 23 | export function useResumeableSwapsCountExcludingPunished() { 24 | return useResumeableSwapsCount( 25 | (s) => s.stateName !== SwapStateName.BtcPunished, 26 | ); 27 | } 28 | 29 | export function useIsSwapRunning() { 30 | return useAppSelector((state) => state.swap.state !== null); 31 | } 32 | 33 | export function useSwapInfo(swapId: string | null) { 34 | return useAppSelector((state) => 35 | swapId ? state.rpc.state.swapInfos[swapId] ?? null : null, 36 | ); 37 | } 38 | 39 | export function useActiveSwapId() { 40 | return useAppSelector((s) => s.swap.swapId); 41 | } 42 | 43 | export function useActiveSwapInfo() { 44 | const swapId = useActiveSwapId(); 45 | return useSwapInfo(swapId); 46 | } 47 | 48 | export function useIsRpcEndpointBusy(method: string) { 49 | return useAppSelector((state) => state.rpc.busyEndpoints.includes(method)); 50 | } 51 | 52 | export function useAllProviders() { 53 | return useAppSelector((state) => { 54 | const registryProviders = state.providers.registry.providers || []; 55 | const listSellersProviders = state.providers.rendezvous.providers || []; 56 | return [...registryProviders, ...listSellersProviders]; 57 | }); 58 | } 59 | 60 | export function useSwapInfosSortedByDate() { 61 | const swapInfos = useAppSelector((state) => state.rpc.state.swapInfos); 62 | return sortBy( 63 | Object.values(swapInfos), 64 | (swap) => -parseDateString(swap.startDate), 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /src/utils/conversionUtils.ts: -------------------------------------------------------------------------------- 1 | export function satsToBtc(sats: number): number { 2 | return sats / 100000000; 3 | } 4 | 5 | export function btcToSats(btc: number): number { 6 | return btc * 100000000; 7 | } 8 | 9 | export function piconerosToXmr(piconeros: number): number { 10 | return piconeros / 1000000000000; 11 | } 12 | 13 | export function isXmrAddressValid(address: string, stagenet: boolean) { 14 | const re = stagenet 15 | ? '[57][0-9AB][1-9A-HJ-NP-Za-km-z]{93}' 16 | : '[48][0-9AB][1-9A-HJ-NP-Za-km-z]{93}'; 17 | return new RegExp(`(?:^${re}$)`).test(address); 18 | } 19 | 20 | export function isBtcAddressValid(address: string, testnet: boolean) { 21 | const re = testnet 22 | ? '(tb1)[a-zA-HJ-NP-Z0-9]{25,49}' 23 | : '(bc1)[a-zA-HJ-NP-Z0-9]{25,49}'; 24 | return new RegExp(`(?:^${re}$)`).test(address); 25 | } 26 | 27 | export function getBitcoinTxExplorerUrl(txid: string, testnet: boolean) { 28 | return `https://blockchair.com/bitcoin${ 29 | testnet ? '/testnet' : '' 30 | }/transaction/${txid}`; 31 | } 32 | 33 | export function getMoneroTxExplorerUrl(txid: string, stagenet: boolean) { 34 | if (stagenet) { 35 | return `https://stagenet.xmrchain.net/tx/${txid}`; 36 | } 37 | return `https://xmrchain.net/tx/${txid}`; 38 | } 39 | 40 | export function secondsToDays(seconds: number): number { 41 | return seconds / 86400; 42 | } 43 | -------------------------------------------------------------------------------- /src/utils/cryptoUtils.ts: -------------------------------------------------------------------------------- 1 | import { createHash } from 'crypto'; 2 | 3 | export function sha256(data: string): string { 4 | return createHash('md5').update(data).digest('hex'); 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/event.ts: -------------------------------------------------------------------------------- 1 | export class SingleTypeEventEmitter { 2 | private listeners: Array<(data: T) => void> = []; 3 | 4 | // Method to add a listener for the event 5 | on(listener: (data: T) => void) { 6 | this.listeners.push(listener); 7 | } 8 | 9 | // Method to remove a listener 10 | off(listener: (data: T) => void) { 11 | const index = this.listeners.indexOf(listener); 12 | if (index > -1) { 13 | this.listeners.splice(index, 1); 14 | } 15 | } 16 | 17 | // Method to emit the event 18 | emit(data: T) { 19 | this.listeners.forEach((listener) => listener(data)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import pino from 'pino'; 2 | import pretty from 'pino-pretty'; 3 | 4 | export default pino( 5 | { 6 | level: 'trace', 7 | }, 8 | pretty({ 9 | colorize: true, 10 | }), 11 | ); 12 | -------------------------------------------------------------------------------- /src/utils/multiAddrUtils.ts: -------------------------------------------------------------------------------- 1 | import { Multiaddr } from 'multiaddr'; 2 | import semver from 'semver'; 3 | import { ExtendedProviderStatus, Provider } from 'models/apiModel'; 4 | import { isTestnet } from 'store/config'; 5 | 6 | const MIN_ASB_VERSION = '0.13.4'; 7 | 8 | export function providerToConcatenatedMultiAddr(provider: Provider) { 9 | return new Multiaddr(provider.multiAddr) 10 | .encapsulate(`/p2p/${provider.peerId}`) 11 | .toString(); 12 | } 13 | 14 | export function isProviderOutdated(provider: ExtendedProviderStatus): boolean { 15 | if (provider.version) { 16 | if (semver.satisfies(provider.version, `>=${MIN_ASB_VERSION}`)) 17 | return false; 18 | } else { 19 | return false; 20 | } 21 | 22 | return true; 23 | } 24 | 25 | export function isProviderCompatible( 26 | provider: ExtendedProviderStatus, 27 | ): boolean { 28 | return provider.testnet === isTestnet(); 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/parseUtils.ts: -------------------------------------------------------------------------------- 1 | import { CliLog, isCliLog } from 'models/cliModel'; 2 | 3 | /* 4 | Extract btc amount from string 5 | 6 | E.g: "0.00100000 BTC" 7 | Output: 0.001 8 | */ 9 | export function extractAmountFromUnitString(text: string): number | null { 10 | if (text != null) { 11 | const parts = text.split(' '); 12 | if (parts.length === 2) { 13 | const amount = Number.parseFloat(parts[0]); 14 | return amount; 15 | } 16 | } 17 | return null; 18 | } 19 | 20 | // E.g 2021-12-29 14:25:59.64082 +00:00:00 21 | export function parseDateString(str: string): number { 22 | const parts = str.split(' ').slice(0, -1); 23 | if (parts.length !== 2) { 24 | throw new Error( 25 | `Date string does not consist solely of date and time Str: ${str} Parts: ${parts}`, 26 | ); 27 | } 28 | const wholeString = parts.join(' '); 29 | const date = Date.parse(wholeString); 30 | if (Number.isNaN(date)) { 31 | throw new Error( 32 | `Date string could not be parsed Str: ${str} Parts: ${parts}`, 33 | ); 34 | } 35 | return date; 36 | } 37 | 38 | export function getLinesOfString(data: string): string[] { 39 | return data 40 | .toString() 41 | .replace('\r\n', '\n') 42 | .replace('\r', '\n') 43 | .split('\n') 44 | .filter((l) => l.length > 0); 45 | } 46 | 47 | export function getLogsAndStringsFromRawFileString( 48 | rawFileData: string, 49 | ): (CliLog | string)[] { 50 | return getLinesOfString(rawFileData).map((line) => { 51 | try { 52 | return JSON.parse(line); 53 | } catch (e) { 54 | return line; 55 | } 56 | }); 57 | } 58 | 59 | export function getLogsFromRawFileString(rawFileData: string): CliLog[] { 60 | return getLogsAndStringsFromRawFileString(rawFileData).filter(isCliLog); 61 | } 62 | 63 | export function logsToRawString(logs: (CliLog | string)[]): string { 64 | return logs.map((l) => JSON.stringify(l)).join('\n'); 65 | } 66 | -------------------------------------------------------------------------------- /src/utils/sortUtils.ts: -------------------------------------------------------------------------------- 1 | import { ExtendedProviderStatus } from 'models/apiModel'; 2 | import { isProviderCompatible } from './multiAddrUtils'; 3 | 4 | export function sortProviderList(list: ExtendedProviderStatus[]) { 5 | return list 6 | .filter(isProviderCompatible) 7 | .concat() 8 | .sort((firstEl, secondEl) => { 9 | // If neither of them have a relevancy score, sort by max swap amount 10 | if (firstEl.relevancy === undefined && secondEl.relevancy === undefined) { 11 | if (firstEl.maxSwapAmount > secondEl.maxSwapAmount) { 12 | return -1; 13 | } 14 | } 15 | // If only on of the two don't have a relevancy score, prioritize the one that does 16 | if (firstEl.relevancy === undefined) return 1; 17 | if (secondEl.relevancy === undefined) return -1; 18 | if (firstEl.relevancy > secondEl.relevancy) { 19 | return -1; 20 | } 21 | return 1; 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/typescriptUtils.tsx: -------------------------------------------------------------------------------- 1 | export function exhaustiveGuard(_value: never): never { 2 | throw new Error( 3 | `ERROR! Reached forbidden guard function with unexpected value: ${JSON.stringify( 4 | _value, 5 | )}`, 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2021", 4 | "module": "commonjs", 5 | "lib": ["dom", "esnext"], 6 | "declaration": true, 7 | "declarationMap": true, 8 | "jsx": "react-jsx", 9 | "strict": true, 10 | "pretty": true, 11 | "sourceMap": true, 12 | "baseUrl": "./src", 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "moduleResolution": "node", 18 | "esModuleInterop": true, 19 | "allowSyntheticDefaultImports": true, 20 | "resolveJsonModule": true, 21 | "allowJs": true, 22 | "outDir": "release/app/dist" 23 | }, 24 | "exclude": ["test", "release/build", "release/app/dist", ".erb/dll"] 25 | } 26 | --------------------------------------------------------------------------------