├── .env ├── .env.example ├── .eslintignore ├── .eslintrc ├── .github └── workflows │ ├── coverage-comment.yaml │ └── release.yaml ├── .gitignore ├── .husky └── commit-msg ├── .parcelrc ├── .postcssrc ├── .prettierignore ├── .prettierrc ├── .releaserc ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── badges ├── coverage-branches.svg ├── coverage-functions.svg ├── coverage-jest coverage.svg ├── coverage-lines.svg └── coverage-statements.svg ├── codegen.ts ├── commitlint.config.js ├── dist ├── binance-pay-button.636f07b8.png ├── binance-pay-logo.b07f161a.png ├── coinbase.4181cfb1.png ├── fa-brands-400.7446506e.ttf ├── fa-brands-400.a10a0ab7.woff2 ├── fa-regular-400.59f2f550.ttf ├── fa-regular-400.65902181.woff2 ├── fa-solid-900.1396cdae.ttf ├── fa-solid-900.7f7ca931.woff2 ├── fa-v4compatibility.36d56e56.woff2 ├── fa-v4compatibility.958fdfe4.ttf ├── global │ ├── binance-pay-button.636f07b8.png │ ├── binance-pay-logo.b07f161a.png │ ├── coinbase.4181cfb1.png │ ├── google-play-badge.66215e40.png │ ├── index.js │ ├── index.js.map │ └── metamask.03f4e8fd.png ├── google-play-badge.66215e40.png ├── index.css ├── index.css.map ├── index.js ├── index.js.map ├── metamask.03f4e8fd.png ├── types.d.ts └── types.d.ts.map ├── i18n.ts ├── jest.config.js ├── jest ├── __mocks__ │ ├── add-watched-address │ │ └── index.ts │ ├── asset │ │ └── index.ts │ ├── assets │ │ └── index.ts │ ├── binance-order │ │ └── index.ts │ ├── fileMock.js │ ├── gqlMocks.ts │ ├── jsonMock.js │ ├── mockConfig.ts │ ├── networks │ │ └── index.ts │ ├── payment-methods │ │ └── index.ts │ ├── styleMock.js │ ├── svgMock.js │ └── web3Mock.ts ├── setupTests.js └── test-utils.tsx ├── package.json ├── src ├── App.test.tsx ├── App.tsx ├── App.withAddressAndNetwork.tsx ├── App.withAssetId.tsx ├── App.withNetwork.tsx ├── assets │ ├── app-store-badge.svg │ ├── binance-pay-button.png │ ├── binance-pay-logo.png │ ├── coinbase.png │ ├── google-play-badge.png │ ├── logo.svg │ ├── lottie │ │ └── tada.json │ ├── map3-logo-wordmark--dark.png │ ├── map3-logo-wordmark--light.png │ └── metamask.png ├── components │ ├── BgOffsetWrapper │ │ └── index.tsx │ ├── CountdownTimer │ │ └── index.tsx │ ├── ErrorWrapper │ │ └── index.tsx │ ├── InnerWrapper │ │ └── index.tsx │ ├── ListItem │ │ └── index.tsx │ ├── LoadingWrapper │ │ └── index.tsx │ ├── MethodIcon │ │ └── index.tsx │ ├── ProgressBar │ │ └── index.tsx │ ├── StateDescriptionHeader │ │ └── index.tsx │ ├── StepTitle │ │ └── index.tsx │ ├── confirmations │ │ └── BridgeQuoteConfirmation │ │ │ └── index.tsx │ └── methods │ │ ├── BinancePay │ │ └── index.tsx │ │ ├── WalletConnect │ │ └── index.tsx │ │ ├── WindowEthereum │ │ └── index.tsx │ │ └── types.ts ├── constants │ ├── index.ts │ └── iso4217.ts ├── custom.d.ts ├── generated │ └── apollo-gql.tsx ├── graphql │ ├── fragments │ │ ├── Asset.gql │ │ ├── BridgeQuote.gql │ │ └── Network.gql │ ├── mutations │ │ ├── AddWatchedAddress.gql │ │ ├── CreateBinanceOrder.gql │ │ ├── CreateBridgeQuote.gql │ │ ├── RemoveWatchedAddress.gql │ │ └── SubscribeToBridgeTransaction.gql │ └── queries │ │ ├── GetAssetByAddressAndNetworkCode.gql │ │ ├── GetAssetByMappedAssetIdAndNetworkCode.gql │ │ ├── GetAssetPrice.gql │ │ ├── GetAssets.gql │ │ ├── GetBridgeTransactionByIds.gql │ │ ├── GetMappedNetworksForAsset.gql │ │ ├── GetNetworkByChainId.gql │ │ ├── GetNetworks.gql │ │ ├── GetOrganizationById.gql │ │ ├── GetPaymentMethods.gql │ │ ├── QueryBinanceOrder.gql │ │ └── SearchAssets.gql ├── hooks │ ├── tests │ │ └── useWeb3.test.tsx │ ├── useBridgeTransactionProgress.tsx │ ├── useChainWatcher.tsx │ ├── useDepositAddress.tsx │ ├── useModalSize.tsx │ ├── useOnClickOutside.tsx │ ├── usePrebuildTx.tsx │ ├── useProviderTransactionProgress.tsx │ ├── useWatchedAddressProgress.tsx │ ├── useWeb3.tsx │ └── useWindowSize.tsx ├── index.css ├── index.html ├── index.test.tsx ├── index.tsx ├── locales │ ├── en │ │ └── translation.json │ ├── es │ │ └── translation.json │ └── index.ts ├── preview.tsx ├── providers │ └── Store │ │ └── index.tsx ├── steps │ ├── AssetSelection │ │ ├── AssetSelection.test.tsx │ │ └── index.tsx │ ├── BinancePay │ │ ├── BinancePay.test.tsx │ │ └── index.tsx │ ├── ConfirmRequiredAmount │ │ ├── ConfirmRequiredAmount.test.tsx │ │ └── index.tsx │ ├── EnterAmount │ │ ├── EnterAmount.test.tsx │ │ └── index.tsx │ ├── History │ │ ├── HistoryContactUs │ │ │ └── index.tsx │ │ └── index.tsx │ ├── NetworkSelection │ │ ├── NetworkSelection.test.tsx │ │ └── index.tsx │ ├── PaymentMethod │ │ ├── PaymentMethod.test.tsx │ │ └── index.tsx │ ├── Result │ │ ├── BridgeQuoteTransactionDetails │ │ │ └── index.tsx │ │ ├── Result.test.tsx │ │ ├── TransactionDetails │ │ │ └── index.tsx │ │ ├── index.tsx │ │ └── utils.ts │ ├── ShowAddress │ │ ├── ShowAddress.test.tsx │ │ └── index.tsx │ ├── SwitchChain │ │ ├── SwitchChain.test.tsx │ │ └── index.tsx │ ├── WalletConnect │ │ ├── WalletConnect.test.tsx │ │ └── index.tsx │ └── index.tsx ├── styles │ ├── globals.scss │ ├── scrollbars.scss │ └── supabase.scss ├── types │ ├── index.d.ts │ ├── supabase.ts │ └── walletConnect.ts └── utils │ ├── abis │ └── erc20.ts │ ├── debounce.ts │ ├── iso8601.ts │ ├── parseJwt.ts │ ├── supabase.ts │ ├── toHex.ts │ ├── transactions │ ├── evm │ │ ├── index.test.ts │ │ └── index.ts │ └── index.ts │ ├── utils.test.ts │ └── wait.tsx ├── tailwind.config.js ├── tsconfig.json └── yarn.lock /.env: -------------------------------------------------------------------------------- 1 | CONSOLE_API_URL=https://console.map3.xyz/api 2 | CONSOLE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJjb25zb2xlIiwib3JnX2lkIjoiMDFkNTNmNzEtZTI5OS00NTIxLWE0NWItNmE4OTA5ZDNjMGQ1Iiwicm9sZXMiOlsiYW5vbnltb3VzIl0sImlhdCI6MTY2ODk4NjIwMywiZXhwIjoxNzAwNTIyMjAzfQ.xvLZT4ZbJyGkt6t2ga2hf-0ZwpG3ag07Gp9pCPL96J8 3 | SUPABASE_URL=https://sb.map3.xyz 4 | SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJxdHdjdmN0dXJsc2R6ZWl2eWhvIiwicm9sZSI6ImFub24iLCJpYXQiOjE2NjI1NDkwMjEsImV4cCI6MTk3ODEyNTAyMX0.aNrCs3-cekJ8u3AMRIMqUfOympIHHDRVE1w5INKO7qc 5 | REACT_APP_PUBLIC_POSTHOG_KEY=phc_6oFckUCr32QQY1ARg6YFv26bBHtQFdc55DBRcIKLH0p 6 | REACT_APP_PUBLIC_POSTHOG_HOST=https://app.posthog.com -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | CONSOLE_API_URL= 2 | CONSOLE_ANON_KEY= 3 | SUPABASE_URL= 4 | SUPABASE_ANON_KEY= -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | generated/ 3 | dist/ 4 | coverage/ 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": ["plugin:prettier/recommended", "plugin:tailwindcss/recommended"], 4 | "root": true, 5 | 6 | "plugins": [ 7 | "hooks", 8 | "jest-dom", 9 | "prettier", 10 | "react", 11 | "simple-import-sort", 12 | "sort-destructure-keys", 13 | "sort-keys-fix", 14 | "tailwindcss", 15 | "testing-library", 16 | "typescript-sort-keys" 17 | ], 18 | 19 | "rules": { 20 | "hooks/sort": [ 21 | 2, 22 | { 23 | "groups": [ 24 | "useReducer", 25 | "useContext", 26 | "useState", 27 | "useRef", 28 | "useDispatch", 29 | "useCallback", 30 | "useEffect" 31 | ] 32 | } 33 | ], 34 | "no-console": [ 35 | "error", 36 | { 37 | "allow": ["warn", "error"] 38 | } 39 | ], 40 | "prettier/prettier": "error", 41 | "react/jsx-sort-props": "error", 42 | "tailwindcss/no-custom-classname": [ 43 | 1, 44 | { 45 | "whitelist": ["map3", "sbui-badge--.*"] 46 | } 47 | ], 48 | "simple-import-sort/exports": "error", 49 | "simple-import-sort/imports": "error", 50 | "sort-destructure-keys/sort-destructure-keys": "error", 51 | "sort-keys-fix/sort-keys-fix": "error", 52 | "typescript-sort-keys/interface": "error", 53 | "typescript-sort-keys/string-enum": "error" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.github/workflows/coverage-comment.yaml: -------------------------------------------------------------------------------- 1 | # This workflow will install dependencies, create coverage tests and run Jest Coverage Comment 2 | # For more information see: https://github.com/MishaKav/jest-coverage-comment/ 3 | name: Jest coverage comment 4 | on: 5 | pull_request: 6 | jobs: 7 | comment: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@master 11 | with: 12 | token: ${{ secrets.BOT_ACCESS_TOKEN }} 13 | fetch-depth: 0 # otherwise, you will fail to push refs to dest repo 14 | - name: Install dependencies 15 | run: yarn install 16 | - name: Run tests 17 | run: | 18 | yarn test 19 | - name: Jest coverage comment 20 | uses: MishaKav/jest-coverage-comment@main 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | release: 8 | if: "!contains(github.event.head_commit.message, 'chore: release')" 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@master 12 | with: 13 | branch: master 14 | persist-credentials: false 15 | fetch-depth: 0 # otherwise, you will fail to push refs to dest repo 16 | - name: Setup Git 17 | run: | 18 | git config --global user.email "accounts+release@map3.xyz" 19 | git config --global user.name "map3-release-bot" 20 | - name: Build 21 | run: | 22 | yarn install 23 | yarn build 24 | - name: Check for uncommitted changes 25 | id: get_changes 26 | run: echo "::set-output name=changed::$(git status --porcelain | wc -l)" 27 | - name: Commit and push files 28 | if: steps.get_changes.outputs.changed != 0 29 | run: | 30 | git commit -a -m "chore: release 🚀" 31 | git push https://${{ secrets.BOT_ACCESS_TOKEN }}@github.com/map3xyz/supercharge HEAD:master --follow-tags 32 | - name: Release 33 | if: steps.get_changes.outputs.changed != 0 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.BOT_ACCESS_TOKEN }} 36 | run: npx semantic-release 37 | - name: Purge CDN jsDelivr cache 38 | if: steps.get_changes.outputs.changed != 0 39 | uses: gacts/purge-jsdelivr-cache@v1 40 | with: 41 | url: | 42 | https://cdn.jsdelivr.net/gh/map3xyz/supercharge@1/dist/global/index.js 43 | https://cdn.jsdelivr.net/gh/map3xyz/supercharge@1/dist/index.css 44 | 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # MacOS https://github.com/github/gitignore/blob/main/Global/macOS.gitignore 2 | 3 | # General 4 | .DS_Store 5 | 6 | 7 | # Node https://github.com/github/gitignore/blob/main/Node.gitignore 8 | 9 | # Logs 10 | logs 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | lerna-debug.log* 16 | .pnpm-debug.log* 17 | 18 | # Diagnostic reports (https://nodejs.org/api/report.html) 19 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 20 | 21 | # Runtime data 22 | pids 23 | *.pid 24 | *.seed 25 | *.pid.lock 26 | 27 | # Directory for instrumented libs generated by jscoverage/JSCover 28 | lib-cov 29 | 30 | # Coverage directory used by tools like istanbul 31 | coverage 32 | *.lcov 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript cache 42 | *.tsbuildinfo 43 | 44 | # Optional eslint cache 45 | .eslintcache 46 | 47 | # Optional stylelint cache 48 | .stylelintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variable files 60 | .env.development.local 61 | .env.test.local 62 | .env.production.local 63 | .env.production 64 | .env.local 65 | .env.test 66 | 67 | # parcel-bundler cache (https://parceljs.org/) 68 | .cache 69 | .parcel-cache 70 | 71 | # parcel output 72 | output 73 | 74 | # Stores VSCode versions used for testing VSCode extensions 75 | .vscode-test 76 | 77 | # yarn v2 78 | .yarn/cache 79 | .yarn/unplugged 80 | .yarn/build-state.yml 81 | .yarn/install-state.gz 82 | .pnp.* 83 | 84 | 85 | # VSCode https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore 86 | 87 | .vscode/* 88 | !.vscode/settings.json 89 | !.vscode/tasks.json 90 | !.vscode/launch.json 91 | !.vscode/extensions.json 92 | !.vscode/*.code-snippets 93 | 94 | # Local History for Visual Studio Code 95 | .history/ 96 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit 5 | -------------------------------------------------------------------------------- /.parcelrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@parcel/config-default", 3 | "transformers": { 4 | "jsx:*.svg": ["@parcel/transformer-svg-react"], 5 | "jsx:*": ["..."] 6 | } 7 | } -------------------------------------------------------------------------------- /.postcssrc: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "plugins": { 4 | "tailwindcss": {}, 5 | "tailwindcss/nesting": {} 6 | } 7 | } -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | styles/ 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "useTabs": false 6 | } 7 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@semantic-release/commit-analyzer", 4 | "@semantic-release/release-notes-generator", 5 | "@semantic-release/github", 6 | [ 7 | "@semantic-release/npm", 8 | { 9 | "npmPublish": false 10 | } 11 | ] 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:1234", 12 | "webRoot": "${workspaceFolder}", 13 | "breakOnLoad": true, 14 | "sourceMapPathOverrides": { 15 | "/__parcel_source_root/*": "${webRoot}/*" 16 | } 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Map3 Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Supercharge Header Image](https://user-images.githubusercontent.com/5640772/226018165-954caf3e-bf91-4ac4-a3eb-462a011d0a61.png) 2 | 3 |

💸 Supercharge

4 | 5 |
The Map3 Supercharge SDK connects
crypto apps to Wallets, Exchanges & Bridges, 6 | enabling
cross-chain deposits and increasing volumes.
7 |
8 |
9 | Website 10 | · 11 | GitHub 12 | · 13 | Contact 14 |
15 |
16 | 17 | ## Getting Started 18 | 19 | ```html 20 | // index.html 21 | 22 | 23 | ... 24 | 25 | 29 | 30 | 31 | 32 | 33 | 34 | 55 | 56 | ``` 57 | 58 | ### Examples 59 | 60 | [![Edit map3-supercharge-cdn-demo-l9t2x5](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/map3-supercharge-cdn-demo-l9t2x5) 61 | 62 | ### Generating Anon Keys 63 | 64 | Visit https://console.map3.xyz/ to generate your `ANON_KEY`. 65 | -------------------------------------------------------------------------------- /badges/coverage-branches.svg: -------------------------------------------------------------------------------- 1 | branches: 72.63%branches72.63% -------------------------------------------------------------------------------- /badges/coverage-functions.svg: -------------------------------------------------------------------------------- 1 | functions: 92.72%functions92.72% -------------------------------------------------------------------------------- /badges/coverage-jest coverage.svg: -------------------------------------------------------------------------------- 1 | jest coverage: 87.74%jest coverage87.74% -------------------------------------------------------------------------------- /badges/coverage-lines.svg: -------------------------------------------------------------------------------- 1 | lines: 92.9%lines92.9% -------------------------------------------------------------------------------- /badges/coverage-statements.svg: -------------------------------------------------------------------------------- 1 | statements: 92.7%statements92.7% -------------------------------------------------------------------------------- /codegen.ts: -------------------------------------------------------------------------------- 1 | require('dotenv/config'); 2 | 3 | const config = { 4 | documents: 'src/graphql/**/*.gql', 5 | generates: { 6 | 'src/generated/apollo-gql.tsx': { 7 | plugins: [ 8 | 'typescript', 9 | 'typescript-operations', 10 | 'typescript-react-apollo', 11 | ], 12 | }, 13 | }, 14 | overwrite: true, 15 | schema: { 16 | 'http://localhost:3001/api/graphql': { 17 | headers: { 18 | Authorization: 'Bearer ' + process.env.CONSOLE_ANON_KEY, 19 | }, 20 | }, 21 | }, 22 | }; 23 | 24 | export default config; 25 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /dist/binance-pay-button.636f07b8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map3xyz/supercharge/b183f57cf10e2cd0c0f54767b5e8bc023c54dc59/dist/binance-pay-button.636f07b8.png -------------------------------------------------------------------------------- /dist/binance-pay-logo.b07f161a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map3xyz/supercharge/b183f57cf10e2cd0c0f54767b5e8bc023c54dc59/dist/binance-pay-logo.b07f161a.png -------------------------------------------------------------------------------- /dist/coinbase.4181cfb1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map3xyz/supercharge/b183f57cf10e2cd0c0f54767b5e8bc023c54dc59/dist/coinbase.4181cfb1.png -------------------------------------------------------------------------------- /dist/fa-brands-400.7446506e.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map3xyz/supercharge/b183f57cf10e2cd0c0f54767b5e8bc023c54dc59/dist/fa-brands-400.7446506e.ttf -------------------------------------------------------------------------------- /dist/fa-brands-400.a10a0ab7.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map3xyz/supercharge/b183f57cf10e2cd0c0f54767b5e8bc023c54dc59/dist/fa-brands-400.a10a0ab7.woff2 -------------------------------------------------------------------------------- /dist/fa-regular-400.59f2f550.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map3xyz/supercharge/b183f57cf10e2cd0c0f54767b5e8bc023c54dc59/dist/fa-regular-400.59f2f550.ttf -------------------------------------------------------------------------------- /dist/fa-regular-400.65902181.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map3xyz/supercharge/b183f57cf10e2cd0c0f54767b5e8bc023c54dc59/dist/fa-regular-400.65902181.woff2 -------------------------------------------------------------------------------- /dist/fa-solid-900.1396cdae.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map3xyz/supercharge/b183f57cf10e2cd0c0f54767b5e8bc023c54dc59/dist/fa-solid-900.1396cdae.ttf -------------------------------------------------------------------------------- /dist/fa-solid-900.7f7ca931.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map3xyz/supercharge/b183f57cf10e2cd0c0f54767b5e8bc023c54dc59/dist/fa-solid-900.7f7ca931.woff2 -------------------------------------------------------------------------------- /dist/fa-v4compatibility.36d56e56.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map3xyz/supercharge/b183f57cf10e2cd0c0f54767b5e8bc023c54dc59/dist/fa-v4compatibility.36d56e56.woff2 -------------------------------------------------------------------------------- /dist/fa-v4compatibility.958fdfe4.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map3xyz/supercharge/b183f57cf10e2cd0c0f54767b5e8bc023c54dc59/dist/fa-v4compatibility.958fdfe4.ttf -------------------------------------------------------------------------------- /dist/global/binance-pay-button.636f07b8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map3xyz/supercharge/b183f57cf10e2cd0c0f54767b5e8bc023c54dc59/dist/global/binance-pay-button.636f07b8.png -------------------------------------------------------------------------------- /dist/global/binance-pay-logo.b07f161a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map3xyz/supercharge/b183f57cf10e2cd0c0f54767b5e8bc023c54dc59/dist/global/binance-pay-logo.b07f161a.png -------------------------------------------------------------------------------- /dist/global/coinbase.4181cfb1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map3xyz/supercharge/b183f57cf10e2cd0c0f54767b5e8bc023c54dc59/dist/global/coinbase.4181cfb1.png -------------------------------------------------------------------------------- /dist/global/google-play-badge.66215e40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map3xyz/supercharge/b183f57cf10e2cd0c0f54767b5e8bc023c54dc59/dist/global/google-play-badge.66215e40.png -------------------------------------------------------------------------------- /dist/global/metamask.03f4e8fd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map3xyz/supercharge/b183f57cf10e2cd0c0f54767b5e8bc023c54dc59/dist/global/metamask.03f4e8fd.png -------------------------------------------------------------------------------- /dist/google-play-badge.66215e40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map3xyz/supercharge/b183f57cf10e2cd0c0f54767b5e8bc023c54dc59/dist/google-play-badge.66215e40.png -------------------------------------------------------------------------------- /dist/metamask.03f4e8fd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map3xyz/supercharge/b183f57cf10e2cd0c0f54767b5e8bc023c54dc59/dist/metamask.03f4e8fd.png -------------------------------------------------------------------------------- /dist/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface Map3InitConfig { 2 | anonKey: string; 3 | options?: { 4 | callbacks?: { 5 | handleAuthorizeTransaction?: (fromAddress: string, networkCode: string, amount: string) => Promise; 6 | handleOrderFeeCalculation?: (asset: string, networkCode: string, amount: string) => Promise<{ 7 | fixedFee?: number; 8 | message?: string; 9 | variableFee?: number; 10 | }>; 11 | onAddressRequested?: (asset: string, networkCode: string) => Promise<{ 12 | address: string; 13 | memo?: string; 14 | }> | { 15 | address: string; 16 | memo?: string; 17 | }; 18 | onClose?: () => void; 19 | onExpire?: () => void; 20 | onFailure?: (error: string, networkCode: string, address?: string) => void; 21 | onOrderCreated?: (orderId: string, type: string) => void; 22 | onSuccess?: (txHash: string, networkCode: string, address?: string) => void; 23 | }; 24 | selection?: { 25 | address?: string; 26 | amount?: string; 27 | assetId?: string; 28 | canBridge?: boolean; 29 | expiration?: string | number; 30 | fiat?: string; 31 | networkCode?: string; 32 | paymentMethod?: 'binance-pay'; 33 | rate?: number; 34 | shortcutAmounts?: number[]; 35 | }; 36 | style?: { 37 | appName?: string; 38 | colors?: { 39 | accent?: string; 40 | primary?: string; 41 | }; 42 | embed?: { 43 | height?: string; 44 | id?: string; 45 | width?: string; 46 | }; 47 | locale?: 'en' | 'es'; 48 | theme?: 'dark' | 'light'; 49 | }; 50 | }; 51 | userId: string; 52 | } 53 | export class Map3 { 54 | constructor(config: Map3InitConfig); 55 | open(): void; 56 | close(): void; 57 | } 58 | export const initMap3Supercharge: (args: Map3InitConfig) => Map3; 59 | 60 | //# sourceMappingURL=types.d.ts.map 61 | -------------------------------------------------------------------------------- /i18n.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import { initReactI18next } from 'react-i18next'; 3 | 4 | import locales from './src/locales'; 5 | 6 | i18n.use(initReactI18next).init({ 7 | fallbackLng: 'en', 8 | interpolation: { 9 | escapeValue: false, 10 | }, 11 | resources: locales, 12 | }); 13 | 14 | export default i18n; 15 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverage: true, 3 | collectCoverageFrom: [ 4 | 'src/components/**/**/*.{ts,tsx}', 5 | 'src/components/**/*.{ts,tsx}', 6 | 'src/hooks/*.{ts,tsx}', 7 | 'src/utils/{!(supabase),}.ts', 8 | 'src/steps/**/*.{ts,tsx}', 9 | 'src/steps/index.tsx', 10 | 'src/providers/Store/index.tsx', 11 | 'src/index.tsx', 12 | 'src/App.tsx', 13 | ], 14 | coverageDirectory: 'coverage', 15 | coverageReporters: ['lcov', 'json-summary', 'text', 'text-summary'], 16 | maxWorkers: 1, 17 | moduleNameMapper: { 18 | '../../assets/lottie/tada.json': '/jest/__mocks__/jsonMock.js', 19 | '@walletconnect/qrcode-modal': '/jest/__mocks__/fileMock.js', 20 | '\\.(css|less)$': '/jest/__mocks__/styleMock.js', 21 | '^.+\\.png$': '/jest/__mocks__/fileMock.js', 22 | '^.+\\.svg$': '/jest/__mocks__/svgMock.js', 23 | '^~/jest/(.*)$': '/jest/$1', 24 | }, 25 | setupFiles: ['/jest/setupTests.js'], 26 | testEnvironment: 'jsdom', 27 | transform: { 28 | '^.+\\.(ts|tsx)$': 'ts-jest', 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /jest/__mocks__/add-watched-address/index.ts: -------------------------------------------------------------------------------- 1 | import { AddWatchedAddressDocument } from '../../../src/generated/apollo-gql'; 2 | 3 | export const addWatchedAddressMock = (variables: { 4 | address: string; 5 | assetId: string; 6 | confirmationsToWatch: number; 7 | expectedAmount?: number; 8 | }) => ({ 9 | request: { 10 | query: AddWatchedAddressDocument, 11 | variables, 12 | }, 13 | result: { 14 | __typename: 'Query', 15 | data: { 16 | addWatchedAddress: '0x0000000000000000000000000000000000000000', 17 | }, 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /jest/__mocks__/asset/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GetAssetByMappedAssetIdAndNetworkCodeDocument, 3 | GetAssetByMappedAssetIdAndNetworkCodeQueryVariables, 4 | GetAssetPriceDocument, 5 | GetAssetPriceQueryVariables, 6 | } from '../../../src/generated/apollo-gql'; 7 | 8 | export const getAssetByMappedAssetIdAndNetworkCodeMock = ( 9 | variables: GetAssetByMappedAssetIdAndNetworkCodeQueryVariables 10 | ) => { 11 | return { 12 | request: { 13 | query: GetAssetByMappedAssetIdAndNetworkCodeDocument, 14 | variables, 15 | }, 16 | result: { 17 | data: { 18 | __typename: 'Query', 19 | assetByMappedAssetIdAndNetworkCode: { 20 | __typename: 'Asset', 21 | address: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174', 22 | id: 'asset', 23 | }, 24 | }, 25 | }, 26 | }; 27 | }; 28 | 29 | export const getAssetPrice = (variables: GetAssetPriceQueryVariables) => { 30 | return { 31 | request: { 32 | query: GetAssetPriceDocument, 33 | variables, 34 | }, 35 | result: { 36 | data: { 37 | __typename: 'Query', 38 | assetPrice: { 39 | __typename: 'AssetPrice', 40 | price: variables.assetId === 'ethereum123' ? 1000 : 20_000.11111111, 41 | }, 42 | }, 43 | }, 44 | }; 45 | }; 46 | -------------------------------------------------------------------------------- /jest/__mocks__/binance-order/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CreateBinanceOrderDocument, 3 | CreateBinanceOrderMutationVariables, 4 | } from '../../../src/generated/apollo-gql'; 5 | 6 | export const createBinanceOrderMock = ( 7 | variables: CreateBinanceOrderMutationVariables 8 | ) => ({ 9 | request: { 10 | query: CreateBinanceOrderDocument, 11 | variables, 12 | }, 13 | result: { 14 | data: { 15 | __typename: 'Query', 16 | createBinanceOrder: { 17 | __typename: 'BinanceOrder', 18 | checkoutUrl: 'https://www.binance.com/en/checkout/1', 19 | id: '1', 20 | qrContent: 'https://www.binance.com/en/checkout/1', 21 | universalUrl: 'https://www.binance.com/en/checkout/1', 22 | }, 23 | }, 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /jest/__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = ''; 2 | -------------------------------------------------------------------------------- /jest/__mocks__/gqlMocks.ts: -------------------------------------------------------------------------------- 1 | import { MockedResponse } from '@apollo/client/testing'; 2 | 3 | import { addWatchedAddressMock } from './add-watched-address'; 4 | import { 5 | getAssetByMappedAssetIdAndNetworkCodeMock, 6 | getAssetPrice, 7 | } from './asset'; 8 | import { 9 | getAssetByAddressAndNetworkCodeMock, 10 | getAssetsForOrgMock, 11 | searchAssetsMock, 12 | } from './assets'; 13 | import { createBinanceOrderMock } from './binance-order'; 14 | import { 15 | getMappedNetworksForOrgMock, 16 | getNetworkByChainIdMock, 17 | getNetworksMock, 18 | } from './networks'; 19 | import { getMethodsMock } from './payment-methods'; 20 | 21 | export const mocks: MockedResponse[] = [ 22 | getAssetsForOrgMock({ currency: 'USD', limit: 10, offset: 0 }), 23 | getAssetsForOrgMock({ currency: 'USD', limit: 10, offset: 0 }), 24 | getAssetsForOrgMock({ assetId: 'satoshi123' }), 25 | getAssetsForOrgMock({ assetId: 'elon123' }), 26 | getAssetsForOrgMock({ assetId: 'ethereum123' }), 27 | getAssetsForOrgMock({ 28 | address: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174', 29 | }), 30 | getAssetsForOrgMock({}), 31 | getAssetByMappedAssetIdAndNetworkCodeMock({ 32 | mappedAssetId: 'elon123', 33 | networkCode: 'ethereum', 34 | }), 35 | getAssetByMappedAssetIdAndNetworkCodeMock({ 36 | mappedAssetId: 'elon123', 37 | networkCode: 'polygon', 38 | }), 39 | getAssetByAddressAndNetworkCodeMock({ 40 | address: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174', 41 | networkCode: 'ethereum', 42 | }), 43 | searchAssetsMock(), 44 | addWatchedAddressMock({ 45 | address: '0x0000000000000000000000000000000000000000', 46 | assetId: 'polygon123', 47 | confirmationsToWatch: 3, 48 | expectedAmount: undefined, 49 | }), 50 | addWatchedAddressMock({ 51 | address: '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa', 52 | assetId: 'satoshi123', 53 | confirmationsToWatch: 3, 54 | expectedAmount: undefined, 55 | }), 56 | addWatchedAddressMock({ 57 | address: '0x0000000000000000000000000000000000000000', 58 | assetId: 'satoshi123', 59 | confirmationsToWatch: 3, 60 | expectedAmount: undefined, 61 | }), 62 | addWatchedAddressMock({ 63 | address: '0x0000000000000000000000000000000000000000', 64 | assetId: 'satoshi123', 65 | confirmationsToWatch: 3, 66 | expectedAmount: undefined, 67 | }), 68 | addWatchedAddressMock({ 69 | address: '0x0000000000000000000000000000000000000000', 70 | assetId: 'ethereum123', 71 | confirmationsToWatch: 3, 72 | expectedAmount: undefined, 73 | }), 74 | getAssetPrice({ assetId: 'satoshi123', currency: 'USD' }), 75 | getAssetPrice({ assetId: 'ethereum123', currency: 'USD' }), 76 | getMappedNetworksForOrgMock({ assetId: 'elon123' }), 77 | getMappedNetworksForOrgMock({ assetId: 'satoshi123' }), 78 | getMappedNetworksForOrgMock({ assetId: 'ethereum123' }), 79 | getMappedNetworksForOrgMock({ assetId: 'polygon123' }), 80 | getNetworkByChainIdMock(1), 81 | getNetworkByChainIdMock(137), 82 | getNetworksMock(), 83 | getMethodsMock({ chainId: null }), 84 | getMethodsMock({ chainId: 1 }), 85 | getMethodsMock({ chainId: 137 }), 86 | createBinanceOrderMock({ 87 | assetId: 'elon123', 88 | orderAmount: '100.0', 89 | userId: 'test', 90 | }), 91 | ]; 92 | -------------------------------------------------------------------------------- /jest/__mocks__/jsonMock.js: -------------------------------------------------------------------------------- 1 | module.exports = []; 2 | -------------------------------------------------------------------------------- /jest/__mocks__/mockConfig.ts: -------------------------------------------------------------------------------- 1 | import { Map3InitConfig } from '../../src'; 2 | 3 | export const mockConfig: Map3InitConfig = { 4 | anonKey: 5 | process.env.CONSOLE_ANON_KEY || 6 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJjb25zb2xlIiwib3JnX2lkIjoiYzljNDczMzYtNWM5MS00MDM0LWIyYTgtMGI1NzA5ZTAwMGI1Iiwicm9sZXMiOlsiYW5vbnltb3VzIl0sImlhdCI6MTY3NTg4ODUwOCwiZXhwIjoxNzA3NDI0NTA4fQ.GzuXjFzSVkE3L-LlhtvpXa3aIi48rvHgMY3hw6lS8KU', 7 | options: { 8 | callbacks: { 9 | onAddressRequested: () => { 10 | return { address: '0x0000000000000000000000000000000000000000' }; 11 | }, 12 | }, 13 | selection: { 14 | fiat: 'USD', 15 | }, 16 | style: { 17 | theme: 'dark' as const, 18 | }, 19 | }, 20 | userId: 'test', 21 | }; 22 | -------------------------------------------------------------------------------- /jest/__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /jest/__mocks__/svgMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'icon-mock'; 2 | -------------------------------------------------------------------------------- /jest/__mocks__/web3Mock.ts: -------------------------------------------------------------------------------- 1 | export const web3Mock = { 2 | addChain: jest.fn(), 3 | approveTokenAllowance: jest.fn(), 4 | estimateGas: jest.fn(), 5 | getBalance: jest.fn(), 6 | getChainId: jest.fn(), 7 | getFeeData: jest.fn(), 8 | getTokenAllowance: jest.fn(), 9 | getTransaction: jest.fn().mockImplementation(() => true), 10 | handleAuthorizeTransactionProxy: jest.fn(), 11 | prepareFinalTransaction: jest.fn(), 12 | providers: {}, 13 | sendFinalTransaction: jest.fn(), 14 | switchChain: jest.fn(), 15 | waitForTransaction: jest.fn(), 16 | }; 17 | -------------------------------------------------------------------------------- /jest/setupTests.js: -------------------------------------------------------------------------------- 1 | require('jest-fetch-mock').enableMocks(); 2 | 3 | jest.mock('posthog-js', () => ({ 4 | __esModule: true, 5 | default: { 6 | capture: jest.fn(), 7 | identify: jest.fn(), 8 | }, 9 | })); 10 | 11 | jest.mock('../src/utils/supabase', () => ({ 12 | __esModule: true, 13 | listenToWatchedAddress: jest.fn(), 14 | supabase: { 15 | createClient: jest.fn(), 16 | }, 17 | })); 18 | 19 | jest.mock('lottie-web', () => ({ 20 | __esModule: true, 21 | loadAnimation: jest.fn().mockImplementation(() => ({ 22 | play: jest.fn(), 23 | })), 24 | })); 25 | 26 | global.CSS = { 27 | supports: (k, v) => { 28 | return v === 'supported'; 29 | }, 30 | }; 31 | 32 | global.IntersectionObserver = class IntersectionObserver { 33 | constructor() {} 34 | 35 | disconnect() { 36 | return null; 37 | } 38 | 39 | observe() { 40 | return null; 41 | } 42 | 43 | takeRecords() { 44 | return null; 45 | } 46 | 47 | unobserve() { 48 | return null; 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /jest/test-utils.tsx: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | 3 | import { MockedProvider } from '@apollo/client/testing'; 4 | import { render, RenderOptions } from '@testing-library/react'; 5 | import React, { ReactElement } from 'react'; 6 | import { I18nextProvider } from 'react-i18next'; 7 | 8 | import i18n from '../i18n'; 9 | import { mocks } from './__mocks__/gqlMocks'; 10 | 11 | const AllTheProviders: React.FC<{ children: React.ReactNode }> = ({ 12 | children, 13 | }) => { 14 | return ( 15 | 16 | {children} 17 | 18 | ); 19 | }; 20 | 21 | const customRender = ( 22 | ui: ReactElement, 23 | options?: Omit 24 | ) => render(ui, { wrapper: AllTheProviders, ...options }); 25 | 26 | export * from '@testing-library/react'; 27 | export { customRender as render }; 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@map3xyz/supercharge", 3 | "version": "1.3.2", 4 | "private": false, 5 | "types": "dist/types.d.ts", 6 | "main": "dist/index.js", 7 | "css": "dist/index.css", 8 | "files": [ 9 | "dist" 10 | ], 11 | "targets": { 12 | "main": { 13 | "source": "./src/index.tsx", 14 | "optimize": true, 15 | "includeNodeModules": true 16 | }, 17 | "css": { 18 | "source": "./src/index.css", 19 | "optimize": true 20 | }, 21 | "global": { 22 | "source": "./src/index.tsx", 23 | "outputFormat": "global" 24 | }, 25 | "types": { 26 | "source": "src/index.tsx" 27 | } 28 | }, 29 | "@parcel/transformer-css": { 30 | "cssModules": { 31 | "dashedIdents": true 32 | } 33 | }, 34 | "scripts": { 35 | "build": "rm -rf dist && parcel build", 36 | "dev": "parcel src/index.html --dist-dir output", 37 | "lint": "eslint \"{**/*,*}.{ts,tsx}\"", 38 | "lint:fix": "eslint \"{**/*,*}.{ts,tsx}\" --fix", 39 | "codegen": "graphql-codegen --config codegen.ts", 40 | "test": "jest" 41 | }, 42 | "resolutions": { 43 | "typescript": "~4.7", 44 | "typescript:comment": "see https://github.com/parcel-bundler/parcel/issues/8419" 45 | }, 46 | "devDependencies": { 47 | "@commitlint/config-conventional": "17.3.0", 48 | "@graphql-codegen/cli": "2.13.7", 49 | "@graphql-codegen/client-preset": "1.1.0", 50 | "@graphql-codegen/typescript-react-apollo": "3.3.5", 51 | "@parcel/packager-ts": "2.7.0", 52 | "@parcel/transformer-inline-string": "2.7.0", 53 | "@parcel/transformer-sass": "2.7.0", 54 | "@parcel/transformer-svg-react": "2.7.0", 55 | "@parcel/transformer-typescript-types": "2.7.0", 56 | "@testing-library/jest-dom": "5.16.5", 57 | "@testing-library/react": "13.4.0", 58 | "@types/eth-url-parser": "1.0.0", 59 | "@types/react": "18.0.18", 60 | "@types/react-dom": "18.0.6", 61 | "@typescript-eslint/parser": "5.38.1", 62 | "assert": "2.0.0", 63 | "eslint": "8.24.0", 64 | "eslint-config-prettier": "8.5.0", 65 | "eslint-plugin-hooks": "0.4.3", 66 | "eslint-plugin-jest-dom": "4.0.2", 67 | "eslint-plugin-prettier": "4.2.1", 68 | "eslint-plugin-react": "7.31.7", 69 | "eslint-plugin-react-hooks": "4.6.0", 70 | "eslint-plugin-simple-import-sort": "8.0.0", 71 | "eslint-plugin-sort-destructure-keys": "1.4.0", 72 | "eslint-plugin-sort-keys-fix": "1.1.2", 73 | "eslint-plugin-tailwindcss": "3.6.1", 74 | "eslint-plugin-testing-library": "5.9.1", 75 | "eslint-plugin-typescript-sort-keys": "2.1.0", 76 | "https-browserify": "1.0.0", 77 | "husky": "8.0.2", 78 | "jest": "29.5.0", 79 | "jest-environment-jsdom": "29.5.0", 80 | "jest-fetch-mock": "3.0.3", 81 | "jest-svg-transformer": "1.0.0", 82 | "os-browserify": "0.3.0", 83 | "parcel": "2.7.0", 84 | "postcss": "8.4.16", 85 | "postcss-modules": "4.3.0", 86 | "prettier": "2.2.1", 87 | "prettier-plugin-tailwindcss": "0.1.13", 88 | "process": "0.11.10", 89 | "querystring-es3": "0.2.1", 90 | "semantic-release": "19.0.5", 91 | "stream-browserify": "3.0.0", 92 | "stream-http": "3.2.0", 93 | "ts-jest": "29.0.5", 94 | "typescript": "4.8.4", 95 | "url": "0.11.0", 96 | "util": "0.12.5" 97 | }, 98 | "dependencies": { 99 | "@apollo/client": "3.7.0", 100 | "@fortawesome/fontawesome-free": "6.1.2", 101 | "@map3xyz/components": "0.1.60", 102 | "@supabase/supabase-js": "2.2.2", 103 | "@web3modal/standalone": "2.4.1", 104 | "@walletconnect/ethereum-provider": "2.7.7", 105 | "bip21": "2.0.3", 106 | "colord": "^2.9.3", 107 | "eth-testing": "1.10.0", 108 | "eth-url-parser": "1.0.4", 109 | "ethers": "5.7.2", 110 | "framer-motion": "7.5.3", 111 | "graphql": "16.6.0", 112 | "i18next": "^22.4.9", 113 | "i18next-http-backend": "^2.1.1", 114 | "lottie-web": "^5.10.2", 115 | "posthog-js": "1.51.5", 116 | "qrcode.react": "3.1.0", 117 | "react": "18.2.0", 118 | "react-device-detect": "2.2.2", 119 | "react-dom": "18.2.0", 120 | "react-i18next": "^12.1.5", 121 | "react-intersection-observer": "9.4.0", 122 | "tailwindcss": "3.1.8" 123 | } 124 | } -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import { mockConfig } from '~/jest/__mocks__/mockConfig'; 2 | import { act, fireEvent, render, screen } from '~/jest/test-utils'; 3 | 4 | import App from './App'; 5 | 6 | describe('App', () => { 7 | it('renders', async () => { 8 | render( {}} />); 9 | 10 | expect(await screen.findByText('Loading...')).toBeInTheDocument(); 11 | const assetSelection = await screen.findByText('Select Asset'); 12 | expect(assetSelection).toBeInTheDocument(); 13 | }); 14 | 15 | it('renders with an assetId', async () => { 16 | render( 17 | {}} 29 | /> 30 | ); 31 | 32 | expect(await screen.findByText('Loading...')).toBeInTheDocument(); 33 | expect( 34 | await screen.findByText('Fetching Payment Methods...') 35 | ).toBeInTheDocument(); 36 | const paymentMethod = await screen.findByText('Payment Method'); 37 | expect(paymentMethod).toBeInTheDocument(); 38 | }); 39 | it('renders with an address and network', async () => { 40 | render( 41 | {}} 54 | /> 55 | ); 56 | 57 | expect(await screen.findByText('Loading...')).toBeInTheDocument(); 58 | expect( 59 | await screen.findByText('Fetching Payment Methods...') 60 | ).toBeInTheDocument(); 61 | const paymentSelection = await screen.findByText('Payment Method'); 62 | expect(paymentSelection).toBeInTheDocument(); 63 | }); 64 | it('renders with a network', async () => { 65 | render( 66 | {}} 78 | /> 79 | ); 80 | 81 | expect(await screen.findByText('Loading...')).toBeInTheDocument(); 82 | expect( 83 | await screen.findByText('Fetching Payment Methods...') 84 | ).toBeInTheDocument(); 85 | const paymentSelection = await screen.findByText('Payment Method'); 86 | expect(paymentSelection).toBeInTheDocument(); 87 | }); 88 | it('accepts an optional callback `handleAuthorizeTransaction`', async () => { 89 | render( 90 | { 98 | return true; 99 | }, 100 | }, 101 | }, 102 | }} 103 | onClose={() => {}} 104 | /> 105 | ); 106 | 107 | expect(await screen.findByText('Loading...')).toBeInTheDocument(); 108 | const assetSelection = await screen.findByText('Select Asset'); 109 | expect(assetSelection).toBeInTheDocument(); 110 | }); 111 | it('handles close', async () => { 112 | jest.useFakeTimers(); 113 | jest.spyOn(window, 'confirm').mockImplementationOnce(() => true); 114 | const closeMock = jest.fn(); 115 | 116 | render(); 117 | 118 | const closeButton = await screen.findByLabelText('Close'); 119 | expect(closeButton).toBeInTheDocument(); 120 | await act(async () => { 121 | fireEvent.click(closeButton); 122 | jest.advanceTimersByTime(1000); 123 | }); 124 | expect(closeMock).toHaveBeenCalled(); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import '../i18n'; 2 | 3 | import { Modal } from '@map3xyz/components'; 4 | import { motion } from 'framer-motion'; 5 | import React, { Suspense, useEffect, useState } from 'react'; 6 | import { isMobile, isTablet } from 'react-device-detect'; 7 | import { useTranslation } from 'react-i18next'; 8 | 9 | import { Map3InitConfig } from '.'; 10 | import AppWithAddressAndNetwork from './App.withAddressAndNetwork'; 11 | import AppWithAssetId from './App.withAssetId'; 12 | import AppWithNetwork from './App.withNetwork'; 13 | import LoadingWrapper from './components/LoadingWrapper'; 14 | import { useGetOrganizationByIdLazyQuery } from './generated/apollo-gql'; 15 | import { useWindowSize } from './hooks/useWindowSize'; 16 | import { Store } from './providers/Store'; 17 | import Map3SdkSteps from './steps'; 18 | import { parseJwt } from './utils/parseJwt'; 19 | 20 | const Layout = ({ 21 | children, 22 | config, 23 | handleClose, 24 | visible, 25 | }: { 26 | children: React.ReactNode; 27 | config: Map3InitConfig; 28 | handleClose: () => void; 29 | visible: boolean; 30 | }) => { 31 | const { minWidth } = useWindowSize(); 32 | 33 | if (!config.options?.style?.embed?.id) { 34 | return ( 35 | 46 | {children} 47 | 48 | ); 49 | } 50 | 51 | const height = config.options.style.embed.height || '500px'; 52 | const width = config.options.style.embed.width || '320px'; 53 | const offsetLeft = parseFloat(width) / 2; 54 | const offsetTop = parseFloat(height) / 2; 55 | 56 | return ( 57 | 74 | {children} 75 | 76 | ); 77 | }; 78 | 79 | const App: React.FC = ({ config, onClose }) => { 80 | const { options } = config; 81 | const [getOrganizationById, { data }] = useGetOrganizationByIdLazyQuery(); 82 | const { selection, style } = options || {}; 83 | const { address, assetId, networkCode } = selection || {}; 84 | const { locale } = style || {}; 85 | const [visible, setVisible] = useState(false); 86 | const { i18n } = useTranslation(); 87 | 88 | useEffect(() => { 89 | if (locale !== 'en') { 90 | i18n.changeLanguage(locale); 91 | } 92 | }, [locale]); 93 | 94 | useEffect(() => { 95 | try { 96 | const { org_id } = parseJwt(config.anonKey); 97 | getOrganizationById({ variables: { id: org_id } }); 98 | } catch (e) { 99 | console.error(e); 100 | } 101 | }, []); 102 | 103 | useEffect(() => { 104 | setVisible(true); 105 | 106 | return () => { 107 | setVisible(false); 108 | }; 109 | }, []); 110 | 111 | const handleClose = () => { 112 | const yes = window.confirm('Are you sure you want to exit?'); 113 | if (!yes) return; 114 | 115 | setVisible(false); 116 | setTimeout(() => { 117 | onClose(); 118 | }, 150); 119 | }; 120 | 121 | return ( 122 |
123 | 124 | }> 125 | {assetId ? ( 126 | 131 | ) : address && networkCode ? ( 132 | 137 | ) : networkCode ? ( 138 | 143 | ) : ( 144 | 145 | 149 | 150 | )} 151 | 152 | 153 |
154 | ); 155 | }; 156 | 157 | export type AppProps = { 158 | config: Map3InitConfig; 159 | onClose: () => void; 160 | }; 161 | 162 | export default App; 163 | -------------------------------------------------------------------------------- /src/App.withAddressAndNetwork.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { AppProps } from './App'; 4 | import ErrorWrapper from './components/ErrorWrapper'; 5 | import LoadingWrapper from './components/LoadingWrapper'; 6 | import { 7 | Organization, 8 | useGetAssetByAddressAndNetworkCodeQuery, 9 | useGetNetworksQuery, 10 | } from './generated/apollo-gql'; 11 | import { Store } from './providers/Store'; 12 | import Map3SdkSteps from './steps'; 13 | 14 | const AppWithAddressAndNetwork: React.FC< 15 | AppProps & { plan: Organization['plan'] } 16 | > = ({ config, onClose, plan }) => { 17 | const { data, error, loading, refetch } = useGetNetworksQuery(); 18 | const { selection } = config.options || {}; 19 | const { address, networkCode } = selection || {}; 20 | const { 21 | data: assetData, 22 | error: assetError, 23 | loading: assetLoading, 24 | refetch: assetRefetch, 25 | } = useGetAssetByAddressAndNetworkCodeQuery({ 26 | fetchPolicy: 'no-cache', 27 | variables: { address, networkCode }, 28 | }); 29 | 30 | if (loading || assetLoading) return ; 31 | 32 | const network = data?.networks?.find( 33 | (network) => network?.networkCode === networkCode 34 | ); 35 | const asset = assetData?.assetByAddressAndNetworkCodeForOrganization; 36 | 37 | if (error || assetError || !network || !asset) 38 | return ( 39 | { 43 | refetch(); 44 | assetRefetch(); 45 | }} 46 | /> 47 | ); 48 | return ( 49 | 50 | 51 | 52 | ); 53 | }; 54 | export default AppWithAddressAndNetwork; 55 | -------------------------------------------------------------------------------- /src/App.withAssetId.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { AppProps } from './App'; 4 | import ErrorWrapper from './components/ErrorWrapper'; 5 | import LoadingWrapper from './components/LoadingWrapper'; 6 | import { 7 | Organization, 8 | useGetAssetsForOrgQuery, 9 | useGetNetworksQuery, 10 | } from './generated/apollo-gql'; 11 | import { Store } from './providers/Store'; 12 | import Map3SdkSteps from './steps'; 13 | 14 | const AppWithAssetId: React.FC = ({ 15 | config, 16 | onClose, 17 | plan, 18 | }) => { 19 | const { selection } = config.options || {}; 20 | const { assetId } = selection || {}; 21 | const { data, error, loading, refetch } = useGetAssetsForOrgQuery({ 22 | fetchPolicy: 'no-cache', 23 | variables: { assetId: assetId }, 24 | }); 25 | 26 | const { 27 | data: networkData, 28 | error: networkError, 29 | loading: networkLoading, 30 | refetch: networkRefetch, 31 | } = useGetNetworksQuery(); 32 | 33 | const retry = async () => { 34 | await refetch(); 35 | await networkRefetch(); 36 | }; 37 | 38 | if (loading || networkLoading) return ; 39 | 40 | const asset = data?.assetsForOrganization?.find( 41 | (asset) => asset?.id === assetId 42 | ); 43 | const network = networkData?.networks?.find( 44 | (n) => n?.networkCode === asset?.networkCode 45 | ); 46 | 47 | if (error || networkError || !asset || !network) 48 | return ( 49 | 54 | ); 55 | 56 | return ( 57 | 58 | 59 | 60 | ); 61 | }; 62 | 63 | export default AppWithAssetId; 64 | -------------------------------------------------------------------------------- /src/App.withNetwork.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { AppProps } from './App'; 4 | import ErrorWrapper from './components/ErrorWrapper'; 5 | import LoadingWrapper from './components/LoadingWrapper'; 6 | import { 7 | Organization, 8 | useGetAssetsForOrgQuery, 9 | useGetNetworksQuery, 10 | } from './generated/apollo-gql'; 11 | import { Store } from './providers/Store'; 12 | import Map3SdkSteps from './steps'; 13 | 14 | const AppWithNetwork: React.FC = ({ 15 | config, 16 | onClose, 17 | plan, 18 | }) => { 19 | const { selection } = config.options || {}; 20 | const { networkCode } = selection || {}; 21 | 22 | const { data, error, loading, refetch } = useGetNetworksQuery(); 23 | const { 24 | data: assetsData, 25 | error: assetsError, 26 | loading: assetsLoading, 27 | refetch: assetsRefetch, 28 | } = useGetAssetsForOrgQuery(); 29 | 30 | if (loading || assetsLoading) return ; 31 | 32 | const network = data?.networks?.find( 33 | (network) => network?.networkCode === networkCode 34 | ); 35 | const asset = assetsData?.assetsForOrganization?.find( 36 | (asset) => asset?.networkCode === networkCode && asset?.type === 'network' 37 | ); 38 | 39 | if (error || assetsError || !network || !asset) 40 | return ( 41 | { 45 | refetch(); 46 | assetsRefetch(); 47 | }} 48 | /> 49 | ); 50 | return ( 51 | 52 | 53 | 54 | ); 55 | }; 56 | export default AppWithNetwork; 57 | -------------------------------------------------------------------------------- /src/assets/binance-pay-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map3xyz/supercharge/b183f57cf10e2cd0c0f54767b5e8bc023c54dc59/src/assets/binance-pay-button.png -------------------------------------------------------------------------------- /src/assets/binance-pay-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map3xyz/supercharge/b183f57cf10e2cd0c0f54767b5e8bc023c54dc59/src/assets/binance-pay-logo.png -------------------------------------------------------------------------------- /src/assets/coinbase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map3xyz/supercharge/b183f57cf10e2cd0c0f54767b5e8bc023c54dc59/src/assets/coinbase.png -------------------------------------------------------------------------------- /src/assets/google-play-badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map3xyz/supercharge/b183f57cf10e2cd0c0f54767b5e8bc023c54dc59/src/assets/google-play-badge.png -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/assets/map3-logo-wordmark--dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map3xyz/supercharge/b183f57cf10e2cd0c0f54767b5e8bc023c54dc59/src/assets/map3-logo-wordmark--dark.png -------------------------------------------------------------------------------- /src/assets/map3-logo-wordmark--light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map3xyz/supercharge/b183f57cf10e2cd0c0f54767b5e8bc023c54dc59/src/assets/map3-logo-wordmark--light.png -------------------------------------------------------------------------------- /src/assets/metamask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/map3xyz/supercharge/b183f57cf10e2cd0c0f54767b5e8bc023c54dc59/src/assets/metamask.png -------------------------------------------------------------------------------- /src/components/BgOffsetWrapper/index.tsx: -------------------------------------------------------------------------------- 1 | export const BgOffsetWrapper = ({ 2 | border, 3 | children, 4 | className = '', 5 | ...rest 6 | }: JSX.IntrinsicElements['div'] & { 7 | border: 't' | 'y'; 8 | children: React.ReactNode; 9 | className?: string; 10 | }) => ( 11 |
17 | {children} 18 |
19 | ); 20 | -------------------------------------------------------------------------------- /src/components/CountdownTimer/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useState } from 'react'; 2 | 3 | import { Context } from '../../providers/Store'; 4 | 5 | const circumference = Math.PI * 20; 6 | 7 | const CountdownTimer: React.FC = () => { 8 | const [state, _dispatch, { onExpire }] = useContext(Context); 9 | 10 | if (!state.expiration) { 11 | return null; 12 | } 13 | 14 | const currentTime = new Date().getTime(); 15 | const progressedTime = currentTime - state.initTime.getTime(); 16 | const totalTime = state.expiration.getTime() - state.initTime.getTime(); 17 | const remainingTime = state.expiration.getTime() - currentTime; 18 | const remainingTimeSeconds = Math.floor(remainingTime / 1000); 19 | const completedPercentage = progressedTime / totalTime; 20 | const [seconds, setSeconds] = useState(remainingTimeSeconds); 21 | const [position, setPosition] = useState(0); 22 | const [timer, setTimer] = useState(null); 23 | 24 | const countdown = () => { 25 | if (seconds <= 0) { 26 | return; 27 | } 28 | setSeconds((prev) => prev - 1); 29 | }; 30 | 31 | useEffect(() => { 32 | countdown(); 33 | setTimer(setInterval(countdown, 1000)); 34 | 35 | return () => { 36 | if (timer) { 37 | clearInterval(timer); 38 | } 39 | }; 40 | }, []); 41 | 42 | useEffect(() => { 43 | const position = circumference - circumference * completedPercentage; 44 | setPosition(position); 45 | 46 | if (seconds <= 0) { 47 | onExpire?.(); 48 | clearInterval(timer!); 49 | } 50 | }, [seconds]); 51 | 52 | const hours = Math.floor(seconds / 3600) 53 | .toString() 54 | .padStart(2, '0'); 55 | const minutes = (Math.floor(seconds / 60) % 60).toString().padStart(2, '0'); 56 | const secondsLeft = (seconds % 60).toString().padStart(2, '0'); 57 | 58 | return seconds > 0 ? ( 59 | 65 | 69 | 82 | 83 | 84 | ) : ( 85 | Expired 86 | ); 87 | }; 88 | 89 | type Props = {}; 90 | 91 | export default CountdownTimer; 92 | -------------------------------------------------------------------------------- /src/components/ErrorWrapper/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { PropsWithChildren } from 'react'; 2 | 3 | import InnerWrapper from '../InnerWrapper'; 4 | 5 | const ErrorWrapper: React.FC> = ({ 6 | description, 7 | header, 8 | retry, 9 | stacktrace, 10 | }) => { 11 | return ( 12 | 13 |
14 |
15 | {' '} 16 |

{header}

17 |
18 |
19 | {description} Please{' '} 20 | 21 | click here 22 | {' '} 23 | to retry. 24 |
25 |
26 | {stacktrace ? ( 27 |
28 | View the raw error:{' '} 29 |
30 |             {stacktrace}
31 |           
32 |
33 | ) : null} 34 |
35 | ); 36 | }; 37 | 38 | type Props = { 39 | description: string; 40 | header: string; 41 | retry: () => void; 42 | stacktrace?: string; 43 | }; 44 | 45 | export default ErrorWrapper; 46 | -------------------------------------------------------------------------------- /src/components/InnerWrapper/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { AriaRole, PropsWithChildren } from 'react'; 2 | 3 | const InnerWrapper: React.FC> = ({ 4 | children, 5 | className, 6 | ...rest 7 | }) => { 8 | return ( 9 |
10 | {children} 11 |
12 | ); 13 | }; 14 | 15 | type Props = { 16 | className?: string; 17 | role?: AriaRole; 18 | }; 19 | 20 | export default InnerWrapper; 21 | -------------------------------------------------------------------------------- /src/components/ListItem/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { PropsWithChildren } from 'react'; 2 | 3 | import InnerWrapper from '../InnerWrapper'; 4 | 5 | const ListItem: React.FC< 6 | PropsWithChildren & React.HTMLAttributes 7 | > = ({ children, ...rest }) => { 8 | return ( 9 | 14 | {children} 15 | 16 | ); 17 | }; 18 | 19 | type Props = {}; 20 | 21 | export default ListItem; 22 | -------------------------------------------------------------------------------- /src/components/LoadingWrapper/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | 4 | const LoadingWrapper: React.FC = ({ message }) => { 5 | const { t } = useTranslation(); 6 | 7 | return ( 8 |
9 |
10 |
11 | 12 |
13 | {message || t('copy.loading')} 14 |
15 |
16 | ); 17 | }; 18 | 19 | type Props = { 20 | message?: string; 21 | }; 22 | 23 | export default LoadingWrapper; 24 | -------------------------------------------------------------------------------- /src/components/MethodIcon/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BinanceLogo from 'url:../../assets/binance-pay-logo.png'; 3 | import CoinbaseWalletLogo from 'url:../../assets/coinbase.png'; 4 | import MetaMaskLogo from 'url:../../assets/metamask.png'; 5 | 6 | import { PaymentMethod } from '../../generated/apollo-gql'; 7 | 8 | const MethodIcon: React.FC = ({ method }) => { 9 | return ( 10 | <> 11 | {method.value === 'isMetaMask' ? ( 12 | 17 | ) : method.value === 'isCoinbaseWallet' ? ( 18 | 23 | ) : method.value === 'binance-pay' ? ( 24 | 25 | ) : method.icon ? ( 26 | 27 | ) : method.logo ? ( 28 | 29 | ) : null} 30 | 31 | ); 32 | }; 33 | 34 | type Props = { 35 | method: PaymentMethod; 36 | }; 37 | 38 | export default MethodIcon; 39 | -------------------------------------------------------------------------------- /src/components/ProgressBar/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ProgressBar: React.FC = ({ progress }) => { 4 | return ( 5 |
6 |
12 |
13 | ); 14 | }; 15 | 16 | type Props = { 17 | progress: number; 18 | }; 19 | 20 | export default ProgressBar; 21 | -------------------------------------------------------------------------------- /src/components/StepTitle/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import CountdownTimer from '../CountdownTimer'; 4 | 5 | const StepTitle: React.FC = (props) => { 6 | return ( 7 |

11 | {props.value} 12 |

13 | ); 14 | }; 15 | 16 | type Props = { 17 | testId?: string; 18 | value: string; 19 | }; 20 | 21 | export default StepTitle; 22 | -------------------------------------------------------------------------------- /src/components/confirmations/BridgeQuoteConfirmation/index.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from 'framer-motion'; 2 | import React, { useContext } from 'react'; 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | import { ISO_4217_TO_SYMBOL } from '../../../constants/iso4217'; 6 | import { Context } from '../../../providers/Store'; 7 | import { DECIMAL_FALLBACK } from '../../../steps/EnterAmount'; 8 | 9 | const BridgeQuoteConfirmation: React.FC = ({ 10 | amount, 11 | setIsConfirming, 12 | }) => { 13 | const { t } = useTranslation(); 14 | const [state] = useContext(Context); 15 | 16 | const { bridgeQuote } = state; 17 | 18 | if (!bridgeQuote) return null; 19 | 20 | return ( 21 | 28 |
setIsConfirming(false)} 32 | onDragEnd={() => setIsConfirming(false)} 33 | > 34 | 35 |
36 |
37 |
{t('copy.amount_to_pay')}:
38 |
39 | {Number(amount).toFixed( 40 | Math.min(6, state.asset?.decimals || DECIMAL_FALLBACK) 41 | )}{' '} 42 | {state.asset?.symbol} ({ISO_4217_TO_SYMBOL['USD']} 43 | {bridgeQuote.estimate?.fromAmountUsd?.toFixed(2)}) 44 |
45 |
46 | {bridgeQuote.estimate?.gasCostsUsd && 47 | bridgeQuote.estimate.gasCostsUsd > 1 && ( 48 |
49 |
{t('copy.gas_cost')}:
50 |
51 | <> 52 | {Number(bridgeQuote.estimate?.gasCosts)?.toFixed(6)}{' '} 53 | {state.network?.symbol} ({ISO_4217_TO_SYMBOL['USD']} 54 | {bridgeQuote.estimate?.gasCostsUsd?.toFixed(2)}) 55 | 56 |
57 |
58 | )} 59 | {bridgeQuote.estimate?.amountToReceive && 60 | Number(bridgeQuote.estimate.fromAmountUsd) - 61 | Number(bridgeQuote.estimate.toAmountUsd) > 62 | 0 ? ( 63 |
64 |
Bridge Fee:
65 |
66 | {( 67 | Number(amount) - Number(bridgeQuote.estimate.amountToReceive) 68 | ).toFixed(6)}{' '} 69 | {state.asset?.symbol} ({ISO_4217_TO_SYMBOL['USD']} 70 | {( 71 | Number(bridgeQuote.estimate.fromAmountUsd) - 72 | Number(bridgeQuote.estimate.toAmountUsd) 73 | )?.toFixed(2)} 74 | ) 75 |
76 |
77 | ) : null} 78 | {bridgeQuote.estimate?.amountToReceive ? ( 79 |
80 |
{t('copy.receive_amount')}:
81 |
82 | {Number(bridgeQuote.estimate.amountToReceive).toFixed(6)}{' '} 83 | {state.asset?.symbol} ({ISO_4217_TO_SYMBOL['USD']} 84 | {bridgeQuote.estimate.toAmountUsd?.toFixed(2)}) 85 |
86 |
87 | ) : null} 88 |
89 | ); 90 | }; 91 | 92 | type Props = { 93 | amount: string; 94 | setIsConfirming: React.Dispatch>; 95 | }; 96 | 97 | export default BridgeQuoteConfirmation; 98 | -------------------------------------------------------------------------------- /src/components/methods/WalletConnect/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@map3xyz/components'; 2 | import { ethers } from 'ethers'; 3 | import { AnimatePresence } from 'framer-motion'; 4 | import React, { useContext, useEffect, useRef } from 'react'; 5 | 6 | import useOnClickOutside from '../../../hooks/useOnClickOutside'; 7 | import { Context } from '../../../providers/Store'; 8 | import { DECIMAL_FALLBACK } from '../../../steps/EnterAmount'; 9 | import BridgeQuoteConfirmation from '../../confirmations/BridgeQuoteConfirmation'; 10 | import MethodIcon from '../../MethodIcon'; 11 | import { EvmMethodProviderProps } from '../types'; 12 | 13 | const WalletConnect: React.FC = ({ 14 | amount, 15 | disabled, 16 | isConfirming, 17 | loading, 18 | setIsConfirming, 19 | }) => { 20 | const [state] = useContext(Context); 21 | const ref = useRef(null); 22 | useOnClickOutside(ref, () => setIsConfirming(false)); 23 | 24 | useEffect(() => { 25 | setIsConfirming(false); 26 | }, [amount]); 27 | 28 | if (!state.method || !state.method.value) return null; 29 | 30 | let cta = isConfirming ? 'Finalize Payment' : 'Confirm Payment'; 31 | 32 | return ( 33 |
34 | 35 | {isConfirming && state.bridgeQuote && ( 36 | 45 | )} 46 | 47 | 66 |
67 | ); 68 | }; 69 | 70 | type Props = EvmMethodProviderProps; 71 | 72 | export default WalletConnect; 73 | -------------------------------------------------------------------------------- /src/components/methods/WindowEthereum/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@map3xyz/components'; 2 | import { ethers } from 'ethers'; 3 | import { AnimatePresence } from 'framer-motion'; 4 | import { 5 | forwardRef, 6 | useContext, 7 | useEffect, 8 | useImperativeHandle, 9 | useRef, 10 | } from 'react'; 11 | 12 | import useOnClickOutside from '../../../hooks/useOnClickOutside'; 13 | import { Context } from '../../../providers/Store'; 14 | import { 15 | DECIMAL_FALLBACK, 16 | DOWNLOAD_EXTENSION, 17 | SubmitHandler, 18 | } from '../../../steps/EnterAmount'; 19 | import BridgeQuoteConfirmation from '../../confirmations/BridgeQuoteConfirmation'; 20 | import MethodIcon from '../../MethodIcon'; 21 | import { EvmMethodProviderProps } from '../types'; 22 | 23 | const WindowEthereum = forwardRef( 24 | ( 25 | { amount, disabled, isConfirming, loading, setFormError, setIsConfirming }, 26 | submitRef 27 | ) => { 28 | const [state, dispatch] = useContext(Context); 29 | const ref = useRef(null); 30 | useOnClickOutside(ref, () => setIsConfirming(false)); 31 | 32 | const submit = async () => { 33 | dispatch({ type: 'SET_PROVIDER_LOADING' }); 34 | const providers = window.ethereum?.providers; 35 | const selectedProvider = providers?.find( 36 | (x: any) => x[state.method?.value!] 37 | ); 38 | 39 | if ( 40 | (!window.ethereum || !window.ethereum[state.method?.value!]) && 41 | !selectedProvider 42 | ) { 43 | dispatch({ payload: 'No provider found.', type: 'SET_ACCOUNT_ERROR' }); 44 | dispatch({ payload: 'No provider found.', type: 'SET_PROVIDER_ERROR' }); 45 | setFormError(DOWNLOAD_EXTENSION); 46 | 47 | return; 48 | } 49 | 50 | const web3Provider = new ethers.providers.Web3Provider( 51 | selectedProvider || window.ethereum, 52 | 'any' 53 | ); 54 | dispatch({ payload: web3Provider, type: 'SET_PROVIDER_SUCCESS' }); 55 | 56 | if (web3Provider?.provider) { 57 | dispatch({ type: 'SET_ACCOUNT_LOADING' }); 58 | // @ts-ignore 59 | web3Provider.provider.on( 60 | 'accountsChanged', 61 | async (accounts: string[]) => { 62 | if (accounts && accounts[0]) { 63 | dispatch({ payload: accounts[0], type: 'SET_ACCOUNT_SUCCESS' }); 64 | } else { 65 | dispatch({ type: 'SET_ACCOUNT_IDLE' }); 66 | } 67 | } 68 | ); 69 | 70 | const accounts = await web3Provider.send('eth_accounts', []); 71 | 72 | if (accounts && accounts[0]) { 73 | dispatch({ payload: accounts[0], type: 'SET_ACCOUNT_SUCCESS' }); 74 | } else { 75 | try { 76 | const requestedAccounts = await web3Provider.send( 77 | 'eth_requestAccounts', 78 | [] 79 | ); 80 | if (requestedAccounts && requestedAccounts[0]) { 81 | dispatch({ 82 | payload: requestedAccounts[0], 83 | type: 'SET_ACCOUNT_SUCCESS', 84 | }); 85 | } 86 | } catch (e: any) { 87 | if ( 88 | e.message === 'User rejected the request.' || 89 | e.message === 'User denied account authorization' 90 | ) { 91 | dispatch({ payload: e.message, type: 'SET_ACCOUNT_ERROR' }); 92 | setFormError(e.message); 93 | } 94 | } 95 | } 96 | } 97 | }; 98 | 99 | useEffect(() => { 100 | setIsConfirming(false); 101 | }, [amount]); 102 | 103 | useImperativeHandle(submitRef, () => ({ 104 | submit: () => { 105 | submit(); 106 | }, 107 | })); 108 | 109 | useEffect(() => { 110 | submit(); 111 | }, []); 112 | 113 | if (!state.method || !state.method?.value) return null; 114 | 115 | let cta = isConfirming ? 'Finalize Payment' : 'Confirm Payment'; 116 | 117 | switch (state.account.status) { 118 | case 'loading': 119 | cta = 'Connecting...'; 120 | break; 121 | case 'error': 122 | case 'idle': 123 | cta = 'Connect Wallet'; 124 | break; 125 | } 126 | 127 | return ( 128 |
129 | 130 | {isConfirming && state.bridgeQuote && ( 131 | 140 | )} 141 | 142 | 161 |
162 | ); 163 | } 164 | ); 165 | 166 | type Props = EvmMethodProviderProps; 167 | 168 | export default WindowEthereum; 169 | -------------------------------------------------------------------------------- /src/components/methods/types.ts: -------------------------------------------------------------------------------- 1 | export type EvmMethodProviderProps = { 2 | amount: string; 3 | disabled: boolean; 4 | isConfirming: boolean; 5 | loading?: boolean; 6 | setFormError: React.Dispatch>; 7 | setIsConfirming: React.Dispatch>; 8 | }; 9 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const MIN_CONFIRMATIONS = 3; 2 | -------------------------------------------------------------------------------- /src/constants/iso4217.ts: -------------------------------------------------------------------------------- 1 | export const ISO_4217_TO_SYMBOL: { [key in string]: string } = { 2 | AUD: '$', 3 | BRL: 'R$', 4 | CAD: '$', 5 | CHF: 'CHF', 6 | CLP: '$', 7 | CNY: '¥', 8 | COP: '$', 9 | EUR: '€', 10 | GBP: '£', 11 | HKD: '$', 12 | HUF: 'Ft', 13 | IDR: 'Rp', 14 | ILS: '₪', 15 | INR: '₹', 16 | JPY: '¥', 17 | KRW: '₩', 18 | MXN: '$', 19 | NZD: '$', 20 | PHP: '₱', 21 | PLN: 'zł', 22 | RUB: '₽', 23 | SGD: '$', 24 | THB: '฿', 25 | TRY: '₺', 26 | TWD: 'NT$', 27 | USD: '$', 28 | ZAR: 'R', 29 | }; 30 | -------------------------------------------------------------------------------- /src/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | const content: any; 3 | export default content; 4 | } 5 | 6 | declare module '*.png' { 7 | const content: any; 8 | export default content; 9 | } 10 | -------------------------------------------------------------------------------- /src/graphql/fragments/Asset.gql: -------------------------------------------------------------------------------- 1 | fragment AssetFields on Asset { 2 | address 3 | decimals 4 | id 5 | name 6 | networkCode 7 | symbol 8 | type 9 | config { 10 | mappedAssetId 11 | } 12 | networks { 13 | name 14 | networkCode 15 | } 16 | logo { 17 | png 18 | svg 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/graphql/fragments/BridgeQuote.gql: -------------------------------------------------------------------------------- 1 | fragment BridgeQuoteFields on BridgeQuote { 2 | aggregator 3 | approval { 4 | address 5 | amount 6 | } 7 | estimate { 8 | amountToReceive 9 | executionDurationSeconds 10 | fromAmountUsd 11 | gasCosts 12 | gasCostsUsd 13 | slippage 14 | toAmountUsd 15 | } 16 | id 17 | transaction { 18 | to 19 | from 20 | gasLimit 21 | gasPrice 22 | data 23 | value 24 | chainId 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/graphql/fragments/Network.gql: -------------------------------------------------------------------------------- 1 | fragment NetworkFields on Network { 2 | decimals 3 | name 4 | networkCode 5 | networkName 6 | symbol 7 | bridged 8 | identifiers { 9 | chainId 10 | } 11 | links { 12 | explorer 13 | } 14 | logo { 15 | png 16 | svg 17 | } 18 | regex { 19 | address 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/graphql/mutations/AddWatchedAddress.gql: -------------------------------------------------------------------------------- 1 | mutation AddWatchedAddress( 2 | $address: String! 3 | $assetId: String! 4 | $confirmationsToWatch: Int! 5 | $expectedAmount: String 6 | $memo: String 7 | ) { 8 | addWatchedAddress( 9 | address: $address 10 | assetId: $assetId 11 | confirmationsToWatch: $confirmationsToWatch 12 | expectedAmount: $expectedAmount 13 | memo: $memo 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /src/graphql/mutations/CreateBinanceOrder.gql: -------------------------------------------------------------------------------- 1 | mutation CreateBinanceOrder( 2 | $assetId: String! 3 | $userId: String! 4 | $orderAmount: String! 5 | ) { 6 | createBinanceOrder( 7 | assetId: $assetId 8 | userId: $userId 9 | orderAmount: $orderAmount 10 | ) { 11 | checkoutUrl 12 | id 13 | qrContent 14 | universalUrl 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/graphql/mutations/CreateBridgeQuote.gql: -------------------------------------------------------------------------------- 1 | mutation CreateBridgeQuote( 2 | $amount: String! 3 | $fromAddress: String! 4 | $fromAssetId: String! 5 | $toAddress: String! 6 | $toAssetId: String! 7 | $userId: String! 8 | ) { 9 | prepareBridgeQuote( 10 | amount: $amount 11 | fromAddress: $fromAddress 12 | fromAssetId: $fromAssetId 13 | toAddress: $toAddress 14 | toAssetId: $toAssetId 15 | userId: $userId 16 | ) { 17 | ...BridgeQuoteFields 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/graphql/mutations/RemoveWatchedAddress.gql: -------------------------------------------------------------------------------- 1 | mutation RemoveWatchedAddress( 2 | $watchedAddressId: ID! 3 | ) { 4 | removeWatchedAddress( 5 | watchedAddressId: $watchedAddressId 6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /src/graphql/mutations/SubscribeToBridgeTransaction.gql: -------------------------------------------------------------------------------- 1 | mutation SubscribeToBridgeTransaction( 2 | $id: ID! 3 | $txHash: String! 4 | ) { 5 | subscribeToBridgeTransaction( 6 | id: $id 7 | txHash: $txHash 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /src/graphql/queries/GetAssetByAddressAndNetworkCode.gql: -------------------------------------------------------------------------------- 1 | query GetAssetByAddressAndNetworkCode($address: String, $networkCode: String) { 2 | assetByAddressAndNetworkCodeForOrganization(address: $address, networkCode: $networkCode) { 3 | ...AssetFields 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/graphql/queries/GetAssetByMappedAssetIdAndNetworkCode.gql: -------------------------------------------------------------------------------- 1 | query GetAssetByMappedAssetIdAndNetworkCode($mappedAssetId: String, $networkCode: String) { 2 | assetByMappedAssetIdAndNetworkCode(mappedAssetId: $mappedAssetId, networkCode: $networkCode) { 3 | id 4 | address 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/graphql/queries/GetAssetPrice.gql: -------------------------------------------------------------------------------- 1 | query GetAssetPrice( 2 | $assetId: String 3 | $currency: String 4 | ) { 5 | assetPrice( 6 | assetId: $assetId 7 | currency: $currency 8 | ) { 9 | price 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/graphql/queries/GetAssets.gql: -------------------------------------------------------------------------------- 1 | query GetAssetsForOrg( 2 | $limit: Int 3 | $offset: Int 4 | $currency: String 5 | $address: String 6 | $assetId: ID 7 | ) { 8 | assetsForOrganization( 9 | limit: $limit 10 | offset: $offset 11 | currency: $currency 12 | address: $address 13 | assetId: $assetId 14 | ) { 15 | ...AssetFields 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/graphql/queries/GetBridgeTransactionByIds.gql: -------------------------------------------------------------------------------- 1 | query getBridgeTransactionsByUserId($id: String!) { 2 | getBridgeTransactionsByUserId(id: $id) { 3 | id 4 | created 5 | organizationId 6 | sourceChainTxId 7 | destinationChainTxId 8 | state 9 | fromAsset { 10 | ...AssetFields 11 | } 12 | fromNetwork { 13 | ...NetworkFields 14 | } 15 | toAsset { 16 | ...AssetFields 17 | } 18 | toNetwork { 19 | ...NetworkFields 20 | } 21 | quote { 22 | ...BridgeQuoteFields 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/graphql/queries/GetMappedNetworksForAsset.gql: -------------------------------------------------------------------------------- 1 | query GetMappedNetworksForAsset($assetId: String) { 2 | mappedNetworksForAssetByOrg(assetId: $assetId) { 3 | ...NetworkFields 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/graphql/queries/GetNetworkByChainId.gql: -------------------------------------------------------------------------------- 1 | query GetNetworkByChainId($chainId: Int!) { 2 | networkByChainId(chainId: $chainId) { 3 | ...NetworkFields 4 | } 5 | } -------------------------------------------------------------------------------- /src/graphql/queries/GetNetworks.gql: -------------------------------------------------------------------------------- 1 | query GetNetworks { 2 | networks { 3 | ...NetworkFields 4 | } 5 | } -------------------------------------------------------------------------------- /src/graphql/queries/GetOrganizationById.gql: -------------------------------------------------------------------------------- 1 | query GetOrganizationById($id: ID!) { 2 | organizationById(id: $id) { 3 | plan 4 | } 5 | } -------------------------------------------------------------------------------- /src/graphql/queries/GetPaymentMethods.gql: -------------------------------------------------------------------------------- 1 | query GetPaymentMethods($chainId: Int) { 2 | methodsForNetwork(chainId: $chainId) { 3 | name 4 | icon 5 | logo 6 | value 7 | flags { 8 | enabled 9 | memo 10 | } 11 | links { 12 | brave 13 | chrome 14 | edge 15 | firefox 16 | opera 17 | } 18 | walletConnect { 19 | description 20 | chains 21 | app { 22 | ios 23 | android 24 | } 25 | mobile { 26 | native 27 | universal 28 | } 29 | desktop { 30 | native 31 | } 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/graphql/queries/QueryBinanceOrder.gql: -------------------------------------------------------------------------------- 1 | query queryBinanceOrder($id: String!) { 2 | queryBinanceOrder(id: $id) { 3 | status 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/graphql/queries/SearchAssets.gql: -------------------------------------------------------------------------------- 1 | query SearchAssets($query: String) { 2 | searchAssetsForOrganization(query: $query) { 3 | ...AssetFields 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/hooks/tests/useWeb3.test.tsx: -------------------------------------------------------------------------------- 1 | import { generateTestingUtils } from 'eth-testing'; 2 | 3 | import { mockConfig } from '~/jest/__mocks__/mockConfig'; 4 | import { act, fireEvent, render, screen } from '~/jest/test-utils'; 5 | 6 | import App from '../../App'; 7 | 8 | describe('useWeb3', () => { 9 | beforeEach(async () => { 10 | render( 11 | { 19 | throw 'Error generating deposit address.'; 20 | }, 21 | }, 22 | }, 23 | }} 24 | onClose={() => {}} 25 | /> 26 | ); 27 | 28 | await screen.findByText('Loading...'); 29 | const elonCoin = await screen.findByText('ElonCoin'); 30 | fireEvent.click(elonCoin); 31 | await screen.findByText('Fetching Networks...'); 32 | const ethereum = await screen.findByText('Ethereum'); 33 | fireEvent.click(ethereum); 34 | }); 35 | 36 | describe('MetaMask Provider', () => { 37 | const testingUtils = generateTestingUtils({ 38 | providerType: 'MetaMask', 39 | }); 40 | beforeAll(() => { 41 | global.window.ethereum = testingUtils.getProvider(); 42 | act(() => { 43 | testingUtils.lowLevel.mockRequest('eth_accounts', []); 44 | testingUtils.lowLevel.mockRequest('eth_requestAccounts', [ 45 | '0x123EthReqAccounts', 46 | ]); 47 | }); 48 | }); 49 | afterEach(() => { 50 | testingUtils.clearAllMocks(); 51 | }); 52 | 53 | it('should connect', async () => { 54 | const metaMask = await screen.findByText('MetaMask'); 55 | fireEvent.click(metaMask); 56 | 57 | const input = await screen.findByTestId('input'); 58 | act(() => { 59 | fireEvent.change(input, { target: { value: '1' } }); 60 | }); 61 | expect(await screen.findByText('Connecting...')).toBeInTheDocument(); 62 | expect(await screen.findByText('Confirm Payment')).toBeInTheDocument(); 63 | }); 64 | }); 65 | describe('CoinbaseWallet Provider', () => { 66 | const testingUtils = generateTestingUtils({ 67 | providerType: 'Coinbase', 68 | }); 69 | beforeAll(() => { 70 | global.window.ethereum = testingUtils.getProvider(); 71 | act(() => { 72 | testingUtils.lowLevel.mockRequest('eth_accounts', []); 73 | testingUtils.lowLevel.mockRequest('eth_requestAccounts', [ 74 | '0x123EthReqAccounts', 75 | ]); 76 | }); 77 | }); 78 | afterEach(() => { 79 | testingUtils.clearAllMocks(); 80 | }); 81 | 82 | it('should connect', async () => { 83 | const cb = (await screen.findAllByText('Coinbase Wallet'))[0]; 84 | fireEvent.click(cb); 85 | 86 | const input = await screen.findByTestId('input'); 87 | act(() => { 88 | fireEvent.change(input, { target: { value: '1' } }); 89 | }); 90 | expect(await screen.findByText('Connecting...')).toBeInTheDocument(); 91 | expect(await screen.findByText('Confirm Payment')).toBeInTheDocument(); 92 | }); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /src/hooks/useChainWatcher.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect } from 'react'; 2 | 3 | import { Context, Steps } from '../providers/Store'; 4 | 5 | export const useChainWatcher = () => { 6 | const [state, dispatch] = useContext(Context); 7 | 8 | const run = async () => { 9 | state.provider?.data?.on?.('network', ({ chainId }) => { 10 | dispatch({ payload: chainId, type: 'SET_PROVIDER_CHAIN_ID' }); 11 | }); 12 | }; 13 | useEffect(() => { 14 | if (state.provider?.status !== 'success') return; 15 | run(); 16 | }, [state.provider?.status]); 17 | 18 | useEffect(() => { 19 | updateSteps(); 20 | }, [state.providerChainId]); 21 | 22 | useEffect(() => { 23 | if (state.steps[state.step] === 'EnterAmount') { 24 | updateSteps(); 25 | } 26 | }, [state.step]); 27 | 28 | const updateSteps = () => { 29 | if (!state.providerChainId) return; 30 | if (state.providerChainId === state.network?.identifiers?.chainId) { 31 | dispatch({ 32 | payload: [ 33 | 'AssetSelection', 34 | 'NetworkSelection', 35 | 'PaymentMethod', 36 | 'EnterAmount', 37 | 'Result', 38 | ], 39 | type: 'SET_STEPS', 40 | }); 41 | dispatch({ payload: Steps.EnterAmount, type: 'SET_STEP' }); 42 | return; 43 | } 44 | 45 | dispatch({ 46 | payload: [ 47 | 'AssetSelection', 48 | 'NetworkSelection', 49 | 'PaymentMethod', 50 | 'SwitchChain', 51 | 'EnterAmount', 52 | 'Result', 53 | ], 54 | type: 'SET_STEPS', 55 | }); 56 | dispatch({ payload: Steps.SwitchChain, type: 'SET_STEP' }); 57 | }; 58 | }; 59 | -------------------------------------------------------------------------------- /src/hooks/useDepositAddress.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | 3 | import { Context } from '../providers/Store'; 4 | 5 | const NO_ADDRESS = 'No address generated. Please contact support.'; 6 | 7 | export const useDepositAddress = () => { 8 | const [state, dispatch, { onAddressRequested }] = useContext(Context); 9 | 10 | const getDepositAddress = async (): Promise<{ 11 | address: string; 12 | memo?: string; 13 | }> => { 14 | try { 15 | if ( 16 | state.depositAddress.status === 'success' && 17 | state.depositAddress.data 18 | ) { 19 | return state.depositAddress.data; 20 | } 21 | dispatch({ type: 'GENERATE_DEPOSIT_ADDRESS_LOADING' }); 22 | if (typeof onAddressRequested !== 'function') { 23 | throw new Error(NO_ADDRESS); 24 | } 25 | const { address, memo } = await onAddressRequested( 26 | state.asset?.symbol as string, 27 | state.network?.networkCode as string 28 | ); 29 | if (!address) { 30 | throw new Error(NO_ADDRESS); 31 | } 32 | if ( 33 | state.network?.regex?.address && 34 | !RegExp(state.network.regex.address).test(address) 35 | ) { 36 | throw new Error(`Address format is invalid.`); 37 | } 38 | dispatch({ 39 | payload: { address, memo }, 40 | type: 'GENERATE_DEPOSIT_ADDRESS_SUCCESS', 41 | }); 42 | 43 | return { address, memo }; 44 | } catch (e: any) { 45 | dispatch({ 46 | payload: e.message, 47 | type: 'GENERATE_DEPOSIT_ADDRESS_ERROR', 48 | }); 49 | throw e; 50 | } 51 | }; 52 | 53 | return { 54 | getDepositAddress, 55 | }; 56 | }; 57 | -------------------------------------------------------------------------------- /src/hooks/useModalSize.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export const useModalSize = ( 4 | ref: React.MutableRefObject 5 | ) => { 6 | const [modalSize, setModalSize] = useState<{ 7 | height?: number; 8 | width?: number; 9 | }>({ 10 | height: undefined, 11 | width: undefined, 12 | }); 13 | useEffect(() => { 14 | function handleResize() { 15 | if (!ref.current || !ref.current.getBoundingClientRect()) return; 16 | const { height, width } = ref.current.getBoundingClientRect(); 17 | 18 | setModalSize({ 19 | height: height, 20 | width: width, 21 | }); 22 | } 23 | 24 | window.addEventListener('resize', handleResize); 25 | 26 | handleResize(); 27 | 28 | return () => window.removeEventListener('resize', handleResize); 29 | }, []); 30 | 31 | return { ...modalSize }; 32 | }; 33 | -------------------------------------------------------------------------------- /src/hooks/useOnClickOutside.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | const useOnClickOutside = ( 4 | ref: React.RefObject, 5 | handler: (event: any) => void 6 | ) => { 7 | useEffect( 8 | () => { 9 | const listener = (event: any) => { 10 | // Do nothing if clicking ref's element or descendent elements 11 | if (!ref.current || ref.current.contains(event.target)) { 12 | return; 13 | } 14 | handler(event); 15 | }; 16 | document.addEventListener('mousedown', listener); 17 | document.addEventListener('touchstart', listener); 18 | return () => { 19 | document.removeEventListener('mousedown', listener); 20 | document.removeEventListener('touchstart', listener); 21 | }; 22 | }, 23 | // Add ref and handler to effect dependencies 24 | // It's worth noting that because passed in handler is a new ... 25 | // ... function on every render that will cause this effect ... 26 | // ... callback/cleanup to run every render. It's not a big deal ... 27 | // ... but to optimize you can wrap handler in useCallback before ... 28 | // ... passing it into this hook. 29 | [ref, handler] 30 | ); 31 | }; 32 | 33 | export default useOnClickOutside; 34 | -------------------------------------------------------------------------------- /src/hooks/usePrebuildTx.tsx: -------------------------------------------------------------------------------- 1 | import { BigNumber, ethers } from 'ethers'; 2 | import { useContext } from 'react'; 3 | 4 | import { Context } from '../providers/Store'; 5 | import { erc20Abi } from '../utils/abis/erc20'; 6 | import { buildTx } from '../utils/transactions/evm'; 7 | import { useDepositAddress } from './useDepositAddress'; 8 | import { useWeb3 } from './useWeb3'; 9 | 10 | const INSUFFICIENT_FUNDS_FOR_GAS = 'insufficient funds for gas * price + value'; 11 | 12 | export const usePrebuildTx = () => { 13 | const [state, dispatch] = useContext(Context); 14 | const { estimateGas, getBalance, getFeeData } = useWeb3(); 15 | const { getDepositAddress } = useDepositAddress(); 16 | 17 | const prebuildTx = async (amount: string, assetContract?: string | null) => { 18 | if (!state.provider?.data) return; 19 | if (!state.account.data) return; 20 | const decimals = state.asset?.decimals; 21 | 22 | try { 23 | if (!decimals) throw new Error('Unable to get decimals.'); 24 | 25 | dispatch({ type: 'SET_PREBUILT_TX_LOADING' }); 26 | const { assetBalance, chainBalance } = await getBalance(assetContract); 27 | const { address, memo } = await getDepositAddress(); 28 | const tx = buildTx({ 29 | address, 30 | amount, 31 | assetContract, 32 | decimals, 33 | from: state.account.data, 34 | memo, 35 | }); 36 | let estimatedGas = BigNumber.from(0); 37 | if (assetContract) { 38 | const contract = new ethers.Contract( 39 | assetContract, 40 | new ethers.utils.Interface(erc20Abi), 41 | state.provider?.data.getSigner() 42 | ); 43 | estimatedGas = await contract.estimateGas.transfer(address, 0); 44 | } else { 45 | estimatedGas = await estimateGas(tx); 46 | } 47 | const eth_gasPrice: string = await state.provider?.data?.send( 48 | 'eth_gasPrice', 49 | [] 50 | ); 51 | const feeData = (await getFeeData()) || {}; 52 | const gasPrice = feeData.maxFeePerGas || BigNumber.from(eth_gasPrice); 53 | 54 | let max: BigNumber | undefined; 55 | let maxLimitRaw: BigNumber | undefined; 56 | 57 | const extraGas = BigNumber.from(memo ? (memo.length / 2) * 16 : 0); 58 | const unpaddedGasLimit = estimatedGas.add(extraGas); 59 | const gasLimit = unpaddedGasLimit.mul(2); 60 | const fee = gasLimit.mul(gasPrice); 61 | 62 | if (state.asset?.type === 'asset') { 63 | max = assetBalance; 64 | } else { 65 | max = chainBalance.sub(fee); 66 | } 67 | maxLimitRaw = max?.gt(0) ? max : BigNumber.from(0); 68 | 69 | const feeError = chainBalance.sub(fee).lte(0); 70 | let maxLimitFormatted = ethers.utils.formatUnits( 71 | maxLimitRaw.toString(), 72 | state.asset?.decimals || 'ether' 73 | ); 74 | 75 | if (maxLimitFormatted.split('.')[1] === '0') { 76 | maxLimitFormatted = maxLimitFormatted.split('.')[0]; 77 | } 78 | 79 | dispatch({ 80 | payload: { 81 | ...feeData, 82 | feeError, 83 | gasLimit, 84 | gasPrice, 85 | maxLimitFormatted, 86 | maxLimitRaw, 87 | memo, 88 | tx, 89 | }, 90 | type: 'SET_PREBUILT_TX_SUCCESS', 91 | }); 92 | } catch (e: any) { 93 | let message = e?.message; 94 | if (message?.includes(INSUFFICIENT_FUNDS_FOR_GAS)) { 95 | message = 'Insufficient funds.'; 96 | } 97 | dispatch({ 98 | payload: 'Error calculating max limit.', 99 | type: 'SET_PREBUILT_TX_ERROR', 100 | }); 101 | } 102 | }; 103 | 104 | return { prebuildTx }; 105 | }; 106 | -------------------------------------------------------------------------------- /src/hooks/useProviderTransactionProgress.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | 3 | import { MIN_CONFIRMATIONS } from '../constants'; 4 | import { useGetAssetByMappedAssetIdAndNetworkCodeQuery } from '../generated/apollo-gql'; 5 | import { Context } from '../providers/Store'; 6 | import { useWeb3 } from './useWeb3'; 7 | 8 | export const useProviderTransactionProgress = () => { 9 | const [state, dispatch] = useContext(Context); 10 | 11 | const { 12 | getTransaction, 13 | prepareFinalTransaction, 14 | sendFinalTransaction, 15 | waitForTransaction, 16 | } = useWeb3(); 17 | 18 | const { data } = useGetAssetByMappedAssetIdAndNetworkCodeQuery({ 19 | skip: state.asset?.type !== 'asset', 20 | variables: { 21 | mappedAssetId: state.asset?.config?.mappedAssetId, 22 | networkCode: state.network?.networkCode, 23 | }, 24 | }); 25 | 26 | const run = async () => { 27 | if (!state.tx.amount) { 28 | dispatch({ 29 | payload: { 30 | data: `Unable to prepare final transaction.`, 31 | status: 'error', 32 | step: 'Submitted', 33 | title: 'Awaiting Submission', 34 | }, 35 | type: 'SET_TX', 36 | }); 37 | return; 38 | } 39 | 40 | dispatch({ 41 | payload: { 42 | data: `Please confirm the transaction on ${state.method?.name}.`, 43 | status: 'loading', 44 | step: 'Submitted', 45 | title: 'Awaiting Submission', 46 | }, 47 | type: 'SET_TX', 48 | }); 49 | 50 | const finalTx = await prepareFinalTransaction( 51 | state.tx.amount, 52 | data?.assetByMappedAssetIdAndNetworkCode?.address 53 | ); 54 | let hash; 55 | try { 56 | hash = await sendFinalTransaction(finalTx); 57 | } catch (e: any) { 58 | dispatch({ 59 | payload: { 60 | data: e?.message ? e.message : `Unable to submit transaction.`, 61 | status: 'error', 62 | step: 'Submitted', 63 | title: 'Awaiting Submission', 64 | }, 65 | type: 'SET_TX', 66 | }); 67 | return; 68 | } 69 | dispatch({ payload: hash, type: 'SET_TX_HASH' }); 70 | dispatch({ 71 | payload: { 72 | data: `Transaction submitted at ${new Date().toLocaleString()}.`, 73 | status: 'success', 74 | step: 'Submitted', 75 | title: 'Submitted', 76 | }, 77 | type: 'SET_TX', 78 | }); 79 | dispatch({ 80 | payload: { 81 | data: 'Waiting for transaction to be included in a block.', 82 | status: 'loading', 83 | step: 'Confirming', 84 | }, 85 | type: 'SET_TX', 86 | }); 87 | let response; 88 | while (!response) { 89 | response = await getTransaction(hash); 90 | } 91 | dispatch({ payload: response, type: 'SET_TX_RESPONSE' }); 92 | await waitForTransaction(hash, 1); 93 | dispatch({ 94 | payload: { 95 | data: 'Has reached a block confirmation.', 96 | status: 'success', 97 | step: 'Confirming', 98 | }, 99 | type: 'SET_TX', 100 | }); 101 | dispatch({ 102 | payload: { 103 | data: `Waiting for ${MIN_CONFIRMATIONS} confirmations.`, 104 | status: 'loading', 105 | step: 'Confirmed', 106 | }, 107 | type: 'SET_TX', 108 | }); 109 | await waitForTransaction(hash, MIN_CONFIRMATIONS); 110 | dispatch({ 111 | payload: { 112 | data: '🚀 Transaction confirmed!', 113 | status: 'success', 114 | step: 'Confirmed', 115 | }, 116 | type: 'SET_TX', 117 | }); 118 | }; 119 | 120 | return { run }; 121 | }; 122 | -------------------------------------------------------------------------------- /src/hooks/useWatchedAddressProgress.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | 3 | import { MIN_CONFIRMATIONS } from '../constants'; 4 | import { Context } from '../providers/Store'; 5 | 6 | export const useWatchedAddressProgress = () => { 7 | const [state, dispatch] = useContext(Context); 8 | 9 | const handleConfirmed = () => { 10 | dispatch({ 11 | payload: { 12 | data: '🚀 Transaction confirmed!', 13 | status: 'success', 14 | step: 'Confirmed', 15 | }, 16 | type: 'SET_TX', 17 | }); 18 | }; 19 | 20 | const run = async (payload: WatchAddressPayload, submmitedDate: string) => { 21 | switch (payload.new.state) { 22 | case 'pending': 23 | case 'confirming': 24 | dispatch({ 25 | payload: payload.new.tx_id, 26 | type: 'SET_TX_HASH', 27 | }); 28 | dispatch({ 29 | // @ts-expect-error 30 | payload: { 31 | to: payload.new.address, 32 | }, 33 | type: 'SET_TX_RESPONSE', 34 | }); 35 | dispatch({ 36 | payload: { 37 | data: submmitedDate || new Date().toLocaleString(), 38 | status: 'success', 39 | step: 'Submitted', 40 | }, 41 | type: 'SET_TX', 42 | }); 43 | dispatch({ 44 | payload: { 45 | data: 46 | state.tx.progress.Confirming.data || 47 | `Has reached a block confirmation.`, 48 | status: 'success', 49 | step: 'Confirming', 50 | }, 51 | type: 'SET_TX', 52 | }); 53 | const currentBlock = 54 | payload.new.tx_block_height + payload.new.tx_confirmations; 55 | const requiredBlock = payload.new.tx_block_height + MIN_CONFIRMATIONS; 56 | const remainingBlocks = Math.max(0, requiredBlock - currentBlock); 57 | const remainingBlocksText = remainingBlocks === 1 ? 'block' : 'blocks'; 58 | if (remainingBlocks === 0) { 59 | handleConfirmed(); 60 | return; 61 | } 62 | dispatch({ 63 | payload: { 64 | data: `${remainingBlocks} more ${remainingBlocksText} required for confirmation.`, 65 | status: 'loading', 66 | step: 'Confirmed', 67 | }, 68 | type: 'SET_TX', 69 | }); 70 | break; 71 | case 'confirmed': 72 | handleConfirmed(); 73 | break; 74 | } 75 | }; 76 | 77 | return { run }; 78 | }; 79 | -------------------------------------------------------------------------------- /src/hooks/useWindowSize.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | const breakpoints = { 4 | '2xl': 1536, 5 | lg: 1024, 6 | md: 768, 7 | sm: 640, 8 | xl: 1280, 9 | }; 10 | 11 | export const useWindowSize = () => { 12 | const [windowSize, setWindowSize] = useState<{ 13 | breakpoint?: '2xl' | 'lg' | 'md' | 'sm' | 'xl'; 14 | height?: number; 15 | width?: number; 16 | }>({ 17 | height: undefined, 18 | width: undefined, 19 | }); 20 | useEffect(() => { 21 | function handleResize() { 22 | const { innerHeight, innerWidth } = window; 23 | const breakpoint = Object.entries(breakpoints) 24 | .filter(([, value]) => value > innerWidth) 25 | ?.sort((a, b) => a[1] - b[1])[0]?.[0] as 26 | | '2xl' 27 | | 'lg' 28 | | 'md' 29 | | 'sm' 30 | | 'xl'; 31 | 32 | setWindowSize({ 33 | breakpoint, 34 | height: innerHeight, 35 | width: innerWidth, 36 | }); 37 | } 38 | 39 | window.addEventListener('resize', handleResize); 40 | 41 | handleResize(); 42 | 43 | return () => window.removeEventListener('resize', handleResize); 44 | }, []); 45 | 46 | const minWidth = (breakpoint: '2xl' | 'lg' | 'md' | 'sm' | 'xl') => { 47 | return windowSize.width && windowSize.width >= breakpoints[breakpoint]; 48 | }; 49 | 50 | return { ...windowSize, minWidth }; 51 | }; 52 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import '../node_modules/@fortawesome/fontawesome-free/css/all.min.css'; 2 | @import '../node_modules/@map3xyz/components/dist/index.css'; 3 | @import './styles/globals.scss'; 4 | @import './styles/supabase.scss'; 5 | @import './styles/scrollbars.scss'; 6 | 7 | .map3 { 8 | @tailwind components; 9 | @tailwind utilities; 10 | @tailwind base; 11 | } -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 17 | 31 | 32 | 33 | 34 |
35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/locales/en/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "translation": { 3 | "title": { 4 | "attention": "Attention!", 5 | "confirm_amount": "Confirm Amount", 6 | "enter_amount": "Enter Amount", 7 | "error_fetching_assets": "Error Fetching Assets", 8 | "error_fetching_networks": "Error Fetching Networks", 9 | "error_fetching_payment_methods": "Error Fetching Payment Methods", 10 | "no_assets_found": "No Assets Found", 11 | "pay_via_binance": "Pay via Binance", 12 | "select_network": "Select Network" 13 | }, 14 | "copy": { 15 | "acknowledge_amount": "Acknowledge Amount", 16 | "amount_to_pay": "Amount to Pay", 17 | "bridged": "Bridged", 18 | "creating_order": "Creating Order...", 19 | "deposit": "Deposit", 20 | "error_fetching_assets": "We couldn't get a list of assets to select.", 21 | "error_fetching_networks": "We couldn't get a list of networks to select.", 22 | "error_fetching_payment_methods": "We couldn't get a list of payment methods to select.", 23 | "fee_cost": "Fee Cost", 24 | "fetching_networks": "Fetching Networks...", 25 | "fetching_payment_methods": "Fetching Payment Methods...", 26 | "gas_cost": "Gas Cost", 27 | "monitoring_for_deposits": "Monitoring for deposits", 28 | "loading": "Loading...", 29 | "no_assets_found": "We couldn't find any assets that matched your search.", 30 | "no_pricing_available": "No pricing available for this asset.", 31 | "pay_on_binance_com": "Pay on Binance.com", 32 | "payment_fee": "Payment Fee", 33 | "receive_amount": "Receive Amount", 34 | "scan_binance_qr_code": "Scan the QR code with the Binance app to pay.", 35 | "search_for_an_asset": "Search for an asset...", 36 | "select_asset": "Select Asset", 37 | "total_receive_less_than_zero": "Please try again with a higher amount.", 38 | "via": "via" 39 | }, 40 | "button": { 41 | "confirm_payment": "Confirm Payment", 42 | "pay_via_binance": "Pay via Binance", 43 | "finalize_on_binance": "Finalize on Binance", 44 | "finalize_payment": "Finalize Payment" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/locales/es/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "translation": { 3 | "Deposit {{amount}} {{symbol}} on": "Enviar {{amount}} {{symbol}} en", 4 | "Deposit {{amount}} {{symbol}} on the {{network}}": "Enviar {{amount}} {{symbol}} en la red {{network}}", 5 | "You must send exactly {{amount}} {{symbol}} on the {{network}} or your payment may be delayed, returned or lost.": "Debes enviar exactamente {{amount}} {{symbol}} en la red {{network}} o tu pago podría ser dilatado, devuelto o extraviado.", 6 | "By clicking this checkbox I acknowledge I must send exactly {{amount}} {{symbol}} on the {{networkName}}.": "Acepto que debo enviar exactamente {{amount}} {{symbol}} en la red {{networkName}}.", 7 | "title": { 8 | "attention": "Atención!", 9 | "confirm_amount": "Confirmar Monto", 10 | "enter_amount": "Ingresar Monto", 11 | "error_fetching_assets": "Error Cargando Activos", 12 | "error_fetching_networks": "Error Cargando Redes", 13 | "error_fetching_payment_methods": "Error Cargando Métodos de Pago", 14 | "no_assets_found": "No Encontramos esa Crypto", 15 | "pay_via_binance": "Pagar con Binance", 16 | "select_network": "Selecciona la Red" 17 | }, 18 | "copy": { 19 | "acknowledge_amount": "Confirma Monto", 20 | "amount_to_pay": "Monto a Recargar", 21 | "creating_order": "Creando Orden...", 22 | "deposit": "Depositar", 23 | "error_fetching_assets": "No Pudimos Cargar la Lista de Cryptos.", 24 | "error_fetching_networks": "No Pudimos Cargar la Lista de Redes.", 25 | "error_fetching_payment_methods": "No Pudimos Cargar los Métodos de Pago.", 26 | "fetching_networks": "Cargando Redes...", 27 | "fetching_payment_methods": "Cargando Métodos de Pago...", 28 | "monitoring_for_deposits": "Verificando Depósitos", 29 | "loading": "Cargando...", 30 | "no_assets_found": "No Pudimos Encontrar la Crypto que Buscaste.", 31 | "no_pricing_available": "No tenemos precio indicativo para esta Crypto.", 32 | "pay_on_binance_com": "Pagar con Binance.com", 33 | "payment_fee": "Costo Pasarela", 34 | "receive_amount": "Vas a Recibir", 35 | "scan_binance_qr_code": "Escanea el Código QR con el App de Binance Para Pagar.", 36 | "search_for_an_asset": "Buscar una Crypto...", 37 | "select_asset": "Selecciona la Crypto", 38 | "send": "Enviar", 39 | "total_receive_less_than_zero": "Vuelva a intentarlo con un monto mayor.", 40 | "via": "vía" 41 | }, 42 | "button": { 43 | "pay_via_binance": "Pagar con Binance", 44 | "finalize_on_binance": "Finalizar en Binance" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/locales/index.ts: -------------------------------------------------------------------------------- 1 | import en from './en/translation.json'; 2 | import es from './es/translation.json'; 3 | 4 | // TODO: use https://github.com/i18next/i18next-http-backend to load translations from the backend dynamically 5 | export default { 6 | en, 7 | es, 8 | }; 9 | -------------------------------------------------------------------------------- /src/preview.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@map3xyz/components'; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | import { initMap3Supercharge } from '.'; 5 | 6 | const container = document.getElementById('app'); 7 | const root = createRoot(container!); 8 | root.render( 9 |
10 | 43 |
47 |
48 | ); 49 | -------------------------------------------------------------------------------- /src/steps/AssetSelection/AssetSelection.test.tsx: -------------------------------------------------------------------------------- 1 | import { MockedProvider } from '@apollo/client/testing'; 2 | import { fireEvent, render as RTLRender } from '@testing-library/react'; 3 | 4 | import { mockConfig } from '~/jest/__mocks__/mockConfig'; 5 | import { render, screen } from '~/jest/test-utils'; 6 | 7 | import App from '../../App'; 8 | 9 | describe('Asset Selection', () => { 10 | it('renders', async () => { 11 | render( {}} />); 12 | expect(await screen.findByText('Loading...')).toBeInTheDocument(); 13 | const assetSelection = await screen.findByText('Select Asset'); 14 | expect(assetSelection).toBeInTheDocument(); 15 | }); 16 | it('searches', async () => { 17 | render( {}} />); 18 | await screen.findByText('Loading...'); 19 | const search = await screen.findByPlaceholderText('Search for an asset...'); 20 | fireEvent.change(search, { target: { value: 'Bitcoin' } }); 21 | expect(await screen.findByText('Bitcoin')).toBeInTheDocument(); 22 | }); 23 | it('handles search error', async () => { 24 | render( {}} />); 25 | await screen.findByText('Loading...'); 26 | const search = await screen.findByPlaceholderText('Search for an asset...'); 27 | fireEvent.change(search, { target: { value: 'Bitcoin Bitcoin Bitcoin' } }); 28 | const retry = await screen.findByText('click here'); 29 | fireEvent.click(retry); 30 | expect(await screen.findByText('click here')).toBeInTheDocument(); 31 | }); 32 | it('handles errors', async () => { 33 | RTLRender( 34 | 35 | {}} /> 36 | 37 | ); 38 | expect(await screen.findByText('Loading...')).toBeInTheDocument(); 39 | const error = await screen.findByText('Error Fetching Assets'); 40 | expect(error).toBeInTheDocument(); 41 | const retry = await screen.findByText('click here'); 42 | fireEvent.click(retry); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/steps/BinancePay/BinancePay.test.tsx: -------------------------------------------------------------------------------- 1 | import * as reactDeviceDetect from 'react-device-detect'; 2 | 3 | import { mockConfig } from '~/jest/__mocks__/mockConfig'; 4 | import { act, fireEvent, render, screen } from '~/jest/test-utils'; 5 | 6 | import App from '../../App'; 7 | import BinancePay from '.'; 8 | 9 | describe.skip('BinancePay > Desktop', () => { 10 | beforeEach(async () => { 11 | render( 12 | { 18 | return { 19 | address: '0x0000000000000000000000000000000000000000', 20 | }; 21 | }, 22 | }, 23 | selection: { 24 | amount: '100000000', 25 | assetId: 'elon123', 26 | }, 27 | }, 28 | }} 29 | onClose={() => {}} 30 | /> 31 | ); 32 | 33 | await screen.findByText('Loading...'); 34 | const binancePay = await screen.findByText('Binance Pay'); 35 | fireEvent.click(binancePay); 36 | }); 37 | it('displays binance pay qr code on desktop', async () => { 38 | expect(await screen.findByText('Enter Amount')).toBeInTheDocument(); 39 | const form = await screen.findByTestId('enter-amount-form'); 40 | await act(async () => { 41 | fireEvent.submit(form); 42 | }); 43 | const payViaBinance = await screen.findByText('Pay via Binance'); 44 | expect(payViaBinance).toBeInTheDocument(); 45 | }); 46 | }); 47 | 48 | describe.skip('Binance Pay > Mobile', () => { 49 | beforeEach(async () => { 50 | render( 51 | ({ 57 | fixedFee: 0.1, 58 | variableFee: 0.01, 59 | }), 60 | onAddressRequested: async (_asset, _network) => { 61 | return { 62 | address: '0x0000000000000000000000000000000000000000', 63 | }; 64 | }, 65 | }, 66 | selection: { 67 | amount: '100000000', 68 | assetId: 'elon123', 69 | }, 70 | }, 71 | }} 72 | onClose={() => {}} 73 | /> 74 | ); 75 | 76 | await screen.findByText('Loading...'); 77 | const binancePay = await screen.findByText('Binance Pay'); 78 | fireEvent.click(binancePay); 79 | }); 80 | it('creates an order', async () => { 81 | Object.defineProperties(reactDeviceDetect, { 82 | isMobile: { get: () => true }, 83 | }); 84 | 85 | expect(await screen.findByText('Enter Amount')).toBeInTheDocument(); 86 | const form = await screen.findByTestId('enter-amount-form'); 87 | await act(async () => { 88 | fireEvent.submit(form); 89 | }); 90 | await screen.findByLabelText('(1% + 0.1 ELON)'); 91 | const button2 = await screen.findByTestId('binance-pay-button'); 92 | fireEvent.click(button2); 93 | }); 94 | it('sets min step to Enter Amount if binance-pay is required method', async () => { 95 | jest.clearAllMocks(); 96 | render( 97 | {}} 107 | /> 108 | ); 109 | const ethereum = await screen.findByText('Ether'); 110 | fireEvent.click(ethereum); 111 | const binancePay = await screen.findByText('Binance Pay'); 112 | expect(binancePay).toBeInTheDocument(); 113 | }); 114 | }); 115 | 116 | describe('BinancePay > Required', () => { 117 | beforeEach(() => { 118 | render( 119 | {}} 131 | /> 132 | ); 133 | }); 134 | 135 | it('sets min step to Enter Amount', async () => { 136 | const ethereum = await screen.findByText('Ether'); 137 | fireEvent.click(ethereum); 138 | const enterAmount = await screen.findByText('Enter Amount'); 139 | expect(enterAmount).toBeInTheDocument(); 140 | const back = await screen.findByLabelText('Back'); 141 | expect(back).toHaveClass('invisible'); 142 | }); 143 | }); 144 | 145 | describe('BinancePay Error', () => { 146 | render(); 147 | expect(true).toBe(true); 148 | }); 149 | -------------------------------------------------------------------------------- /src/steps/BinancePay/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Pill } from '@map3xyz/components'; 2 | import { motion } from 'framer-motion'; 3 | import { posthog } from 'posthog-js'; 4 | import { QRCodeSVG } from 'qrcode.react'; 5 | import React, { useContext, useEffect, useRef, useState } from 'react'; 6 | import { useTranslation } from 'react-i18next'; 7 | import BinancePayButton from 'url:../../assets/binance-pay-button.png'; 8 | import BinanceLogo from 'url:../../assets/binance-pay-logo.png'; 9 | 10 | import ErrorWrapper from '../../components/ErrorWrapper'; 11 | import InnerWrapper from '../../components/InnerWrapper'; 12 | import LoadingWrapper from '../../components/LoadingWrapper'; 13 | import { 14 | Map3PlatformOrderStatus, 15 | useCreateBinanceOrderMutation, 16 | useQueryBinanceOrderLazyQuery, 17 | } from '../../generated/apollo-gql'; 18 | import { useModalSize } from '../../hooks/useModalSize'; 19 | import { Context, Steps } from '../../providers/Store'; 20 | 21 | const BinancePayFinalStatuses = [ 22 | Map3PlatformOrderStatus.Paid, 23 | Map3PlatformOrderStatus.Canceled, 24 | Map3PlatformOrderStatus.Expired, 25 | Map3PlatformOrderStatus.Refunded, 26 | Map3PlatformOrderStatus.Refunding, 27 | Map3PlatformOrderStatus.Error, 28 | ]; 29 | 30 | const BinancePay: React.FC = () => { 31 | const [state, dispatch, { onOrderCreated }] = useContext(Context); 32 | const [ 33 | createBinanceOrder, 34 | { data, error, loading }, 35 | ] = useCreateBinanceOrderMutation(); 36 | const [isPolling, setIsPolling] = useState(false); 37 | const ref = useRef(null); 38 | const { width } = useModalSize(ref); 39 | const { t } = useTranslation(); 40 | 41 | const [ 42 | queryBinanceOrder, 43 | { data: queryData, stopPolling }, 44 | ] = useQueryBinanceOrderLazyQuery(); 45 | 46 | const stopPoll = () => { 47 | setIsPolling(false); 48 | stopPolling(); 49 | }; 50 | 51 | const run = async () => { 52 | if (!state.asset || !state.tx.amount || !state.userId) { 53 | return; 54 | } 55 | const { data } = await createBinanceOrder({ 56 | variables: { 57 | assetId: state.asset!.id!, 58 | orderAmount: state.tx.amount, 59 | userId: state.userId, 60 | }, 61 | }); 62 | 63 | posthog.capture('BINANCE_PAY_ORDER_CREATED', { 64 | amount: state.tx.amount, 65 | asset: state.asset!.symbol, 66 | }); 67 | 68 | if (data?.createBinanceOrder?.id) { 69 | onOrderCreated?.(data?.createBinanceOrder.id, 'binance-pay'); 70 | } 71 | }; 72 | 73 | useEffect(() => { 74 | run(); 75 | }, []); 76 | 77 | useEffect(() => { 78 | const poll = async () => { 79 | if (data?.createBinanceOrder?.id) { 80 | await queryBinanceOrder({ 81 | pollInterval: 1500, 82 | variables: { 83 | id: data.createBinanceOrder.id, 84 | }, 85 | }); 86 | setIsPolling(true); 87 | } 88 | }; 89 | 90 | poll(); 91 | }, [data?.createBinanceOrder?.id]); 92 | 93 | useEffect(() => { 94 | if ( 95 | queryData?.queryBinanceOrder?.status && 96 | BinancePayFinalStatuses.includes(queryData?.queryBinanceOrder?.status) 97 | ) { 98 | stopPoll(); 99 | } 100 | }, [queryData?.queryBinanceOrder]); 101 | 102 | if (!state.method) { 103 | dispatch({ payload: Steps.PaymentMethod, type: 'SET_STEP' }); 104 | return null; 105 | } 106 | 107 | if (error) { 108 | return ( 109 | 115 | ); 116 | } 117 | 118 | return ( 119 |
120 | 121 |

125 | {t('title.pay_via_binance')} 126 |

127 |
128 | 129 | {loading && } 130 | {data?.createBinanceOrder?.qrContent && ( 131 |
132 |
133 | {t('copy.scan_binance_qr_code')} 134 |
135 | {isPolling && ( 136 | 141 | } 144 | > 145 | {t('copy.monitoring_for_deposits')} 146 | 147 | 148 | )} 149 |
150 | 170 |
171 | 182 |
183 | )} 184 |
185 |
186 | ); 187 | }; 188 | 189 | type Props = {}; 190 | 191 | export default BinancePay; 192 | -------------------------------------------------------------------------------- /src/steps/ConfirmRequiredAmount/ConfirmRequiredAmount.test.tsx: -------------------------------------------------------------------------------- 1 | import { mockConfig } from '~/jest/__mocks__/mockConfig'; 2 | import { fireEvent, render, screen } from '~/jest/test-utils'; 3 | 4 | import App from '../../App'; 5 | import ConfirmRequiredAmount from '.'; 6 | 7 | describe('ConfirmRequiredAmount', () => { 8 | describe('render major amount if minor amount is required', () => { 9 | beforeEach(async () => { 10 | render( 11 | { 17 | return { 18 | address: '0x0000000000000000000000000000000000000000', 19 | }; 20 | }, 21 | }, 22 | selection: { 23 | amount: '1000000000000000', 24 | networkCode: 'ethereum', 25 | }, 26 | }, 27 | }} 28 | onClose={() => {}} 29 | /> 30 | ); 31 | 32 | await screen.findByText('Loading...'); 33 | const qrCode = await screen.findByText('Show Address'); 34 | fireEvent.click(qrCode); 35 | }); 36 | it('renders', async () => { 37 | expect(await screen.findByText('Acknowledge Amount')).toBeInTheDocument(); 38 | expect((await screen.findAllByText(/0.001 ETH/))[0]).toBeInTheDocument(); 39 | }); 40 | }); 41 | }); 42 | 43 | describe('ConfirmRequireAmount Error', () => { 44 | render(); 45 | expect(true).toBe(true); 46 | }); 47 | -------------------------------------------------------------------------------- /src/steps/ConfirmRequiredAmount/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@map3xyz/components'; 2 | import { ethers } from 'ethers'; 3 | import React, { useContext, useState } from 'react'; 4 | import { Trans, useTranslation } from 'react-i18next'; 5 | 6 | import InnerWrapper from '../../components/InnerWrapper'; 7 | import StateDescriptionHeader from '../../components/StateDescriptionHeader'; 8 | import StepTitle from '../../components/StepTitle'; 9 | import { Context, Steps } from '../../providers/Store'; 10 | 11 | const ConfirmRequiredAmount: React.FC = () => { 12 | const [state, dispatch] = useContext(Context); 13 | const [acknowledged, setAcknowledged] = useState(false); 14 | const { t } = useTranslation(); 15 | 16 | if (!state.method) { 17 | dispatch({ payload: Steps.PaymentMethod, type: 'SET_STEP' }); 18 | return null; 19 | } 20 | 21 | return ( 22 |
23 |
24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 |
32 | 🎯 33 |

{t('title.attention')}

34 |
35 | , 38 | italic: , 39 | }} 40 | defaults="You must send exactly {{amount}} {{symbol}} on the {{network}} or your payment may be 41 | delayed, returned or lost." 42 | values={{ 43 | amount: state.requiredAmountMajor, 44 | network: state.network?.networkName, 45 | symbol: state.asset?.symbol, 46 | }} 47 | /> 48 |
49 |
50 |
51 |
{ 53 | dispatch({ 54 | payload: [ 55 | 'AssetSelection', 56 | 'NetworkSelection', 57 | 'PaymentMethod', 58 | 'ShowAddress', 59 | 'Result', 60 | ], 61 | type: 'SET_STEPS', 62 | }); 63 | dispatch({ payload: Steps.ShowAddress, type: 'SET_STEP' }); 64 | }} 65 | > 66 | 67 |
68 | { 72 | setAcknowledged(e.target.checked); 73 | 74 | if (!e.target.checked) return; 75 | 76 | dispatch({ 77 | payload: { 78 | assetId: state.asset?.id as string, 79 | symbol: state.asset?.symbol as string, 80 | txAmount: ethers.utils 81 | .parseUnits( 82 | state.requiredAmountMajor as string, 83 | state.asset?.decimals as number 84 | ) 85 | .toString(), 86 | }, 87 | type: 'SET_COMMITTED_TX_AMOUNT', 88 | }); 89 | }} 90 | type="checkbox" 91 | /> 92 | 108 |
109 | 118 |
119 |
120 |
121 | ); 122 | }; 123 | 124 | type Props = {}; 125 | 126 | export default ConfirmRequiredAmount; 127 | -------------------------------------------------------------------------------- /src/steps/History/HistoryContactUs/index.tsx: -------------------------------------------------------------------------------- 1 | import { Badge, Button, Input, Radio, Textarea } from '@map3xyz/components'; 2 | import { motion } from 'framer-motion'; 3 | import React, { useContext, useState } from 'react'; 4 | 5 | import InnerWrapper from '../../../components/InnerWrapper'; 6 | import { Context } from '../../../providers/Store'; 7 | 8 | const HistoryContactUs: React.FC = ({ setShowContactUs }) => { 9 | const [state] = useContext(Context); 10 | const [formValue, setFormValue] = useState(); 11 | const [formError, setFormError] = useState(null); 12 | const [formSuccess, setFormSuccess] = useState(false); 13 | const [loading, setLoading] = useState(false); 14 | 15 | const handleSubmit = async (e: React.FormEvent) => { 16 | setFormError(null); 17 | e.preventDefault(); 18 | 19 | setLoading(true); 20 | 21 | try { 22 | const result = await fetch(`${process.env.CONSOLE_API_URL}/contact-us`, { 23 | body: JSON.stringify({ 24 | email: formValue?.get('email'), 25 | issue: formValue?.get('issue'), 26 | message: formValue?.get('message'), 27 | orderId: formValue?.get('order-id'), 28 | userId: state.userId, 29 | }), 30 | headers: { 31 | authorization: `Bearer ${state.anonKey}`, 32 | }, 33 | method: 'POST', 34 | }); 35 | 36 | const json = await result.json(); 37 | if (!result.ok) { 38 | throw new Error(json?.message || 'Something went wrong.'); 39 | } 40 | setFormSuccess(true); 41 | } catch (error: any) { 42 | console.error(error); 43 | setFormError(error?.message || 'Something went wrong.'); 44 | } finally { 45 | setLoading(false); 46 | 47 | setTimeout(() => { 48 | setFormSuccess(false); 49 | }, 2000); 50 | } 51 | }; 52 | 53 | return ( 54 | 61 | 62 |
{ 65 | setFormError(null); 66 | setFormValue(new FormData(e.currentTarget)); 67 | }} 68 | onSubmit={handleSubmit} 69 | > 70 |
71 |
72 | 75 | 80 | 85 | 86 |
87 | 88 | 89 |