├── .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 | 
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 |
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 | [](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 |
--------------------------------------------------------------------------------
/badges/coverage-functions.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/badges/coverage-jest coverage.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/badges/coverage-lines.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/badges/coverage-statements.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
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 |
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 |
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 |
126 |
127 |
128 | );
129 | };
130 |
131 | type Props = {
132 | setShowContactUs: React.Dispatch>;
133 | };
134 |
135 | export default HistoryContactUs;
136 |
--------------------------------------------------------------------------------
/src/steps/NetworkSelection/NetworkSelection.test.tsx:
--------------------------------------------------------------------------------
1 | import { MockedProvider } from '@apollo/client/testing';
2 | import { render as RTLRender } from '@testing-library/react';
3 |
4 | import { assetsForOrganizationMockResult } from '~/jest/__mocks__/assets';
5 | import { mockConfig } from '~/jest/__mocks__/mockConfig';
6 | import { fireEvent, render, screen } from '~/jest/test-utils';
7 |
8 | import App from '../../App';
9 | import { GetAssetsForOrgDocument } from '../../generated/apollo-gql';
10 | import NetworkSelection from '.';
11 |
12 | describe('Network Selection', () => {
13 | it('renders', async () => {
14 | render( {}} />);
15 | expect(await screen.findByText('Loading...')).toBeInTheDocument();
16 | const elonCoin = await screen.findByText('ElonCoin');
17 | fireEvent.click(elonCoin);
18 | const networkSelection = await screen.findByText('Select Network');
19 | expect(networkSelection).toBeInTheDocument();
20 | });
21 | it('handles errors', async () => {
22 | RTLRender(
23 |
43 | {
50 | return {
51 | address: '0x0000000000000000000000000000000000000000',
52 | };
53 | },
54 | },
55 | },
56 | }}
57 | onClose={() => {}}
58 | />
59 |
60 | );
61 | const bitcoin = await screen.findByText('Bitcoin');
62 | fireEvent.click(bitcoin);
63 | const error = await screen.findByText('Error Fetching Networks');
64 | expect(error).toBeInTheDocument();
65 | });
66 | });
67 |
68 | describe('Bridged Networks', () => {
69 | beforeEach(async () => {
70 | render( {}} />);
71 | const elonCoin = await screen.findByText('ElonCoin');
72 | fireEvent.click(elonCoin);
73 | });
74 | it('displays bridged networks', async () => {
75 | const bridged = await screen.findAllByText('Bridged');
76 | expect(bridged[0]).toBeInTheDocument();
77 | });
78 | });
79 |
80 | describe('Network Selection Skip', () => {
81 | beforeEach(async () => {
82 | render( {}} />);
83 | const bitcoin = await screen.findByText('Bitcoin');
84 | fireEvent.click(bitcoin);
85 | });
86 |
87 | it('skips network selection if asset is a network', async () => {
88 | await screen.findByText('Fetching Networks...');
89 | const paymentMethod = await screen.findByText('Payment Method');
90 | expect(paymentMethod).toBeInTheDocument();
91 | });
92 | });
93 |
94 | describe('Network Selection Errors', () => {
95 | it('renders', async () => {
96 | render();
97 | expect(true).toBe(true);
98 | });
99 | });
100 |
--------------------------------------------------------------------------------
/src/steps/NetworkSelection/index.tsx:
--------------------------------------------------------------------------------
1 | import { Badge, CoinLogo } from '@map3xyz/components';
2 | import React, { useContext, useEffect } from 'react';
3 | import { useTranslation } from 'react-i18next';
4 |
5 | import ErrorWrapper from '../../components/ErrorWrapper';
6 | import InnerWrapper from '../../components/InnerWrapper';
7 | import ListItem from '../../components/ListItem';
8 | import LoadingWrapper from '../../components/LoadingWrapper';
9 | import StateDescriptionHeader from '../../components/StateDescriptionHeader';
10 | import StepTitle from '../../components/StepTitle';
11 | import { useGetMappedNetworksForAssetQuery } from '../../generated/apollo-gql';
12 | import { Context, Steps } from '../../providers/Store';
13 |
14 | const NetworkSelection: React.FC = () => {
15 | const [state, dispatch] = useContext(Context);
16 | const { t } = useTranslation();
17 |
18 | const { data, error, loading, refetch } = useGetMappedNetworksForAssetQuery({
19 | variables: {
20 | assetId: state.asset?.config?.mappedAssetId,
21 | },
22 | });
23 |
24 | useEffect(() => {
25 | if (
26 | data?.mappedNetworksForAssetByOrg?.length === 1 &&
27 | data.mappedNetworksForAssetByOrg[0]
28 | ) {
29 | if (state.prevStep >= Steps.NetworkSelection) {
30 | dispatch({ payload: Steps.AssetSelection, type: 'SET_STEP' });
31 | } else {
32 | dispatch({
33 | payload: data.mappedNetworksForAssetByOrg[0],
34 | type: 'SET_NETWORK',
35 | });
36 | dispatch({ payload: Steps.PaymentMethod, type: 'SET_STEP' });
37 | }
38 | }
39 | }, [data?.mappedNetworksForAssetByOrg?.length]);
40 |
41 | if (data?.mappedNetworksForAssetByOrg?.length === 1) return null;
42 |
43 | if (!state.asset) {
44 | dispatch({ payload: Steps.AssetSelection, type: 'SET_STEP' });
45 | return null;
46 | }
47 |
48 | if (loading)
49 | return (
50 |
53 | );
54 |
55 | if (error)
56 | return (
57 |
62 | );
63 |
64 | // TODO: find the best network to bridge into
65 | const enabledNetworks = data?.mappedNetworksForAssetByOrg?.filter(
66 | (network) => !network?.bridged
67 | );
68 | const destinationNetwork = enabledNetworks?.[0];
69 | const bridgedNetworks = data?.mappedNetworksForAssetByOrg?.filter(
70 | (network) => network?.bridged
71 | );
72 |
73 | if (!enabledNetworks?.length && !bridgedNetworks?.length) return null;
74 |
75 | return (
76 |
77 |
78 |
79 |
83 |
84 |
85 |
86 |
87 |
88 | {data?.mappedNetworksForAssetByOrg?.map((network) =>
89 | network ? (
90 |
{
93 | if (network.bridged && destinationNetwork) {
94 | dispatch({
95 | payload: destinationNetwork,
96 | type: 'SET_DESTINATION_NETWORK',
97 | });
98 | }
99 | dispatch({ payload: network, type: 'SET_NETWORK' });
100 | dispatch({ payload: Steps.PaymentMethod, type: 'SET_STEP' });
101 | }}
102 | role="button"
103 | >
104 |
105 |
106 |
113 |
114 |
{network.networkName}
115 |
116 |
117 | {network.bridged ? (
118 | // @ts-ignore
119 | {t('copy.bridged')}
120 | ) : null}
121 | {state.network?.networkName === network.networkName ? (
122 |
123 | ) : (
124 |
125 | )}
126 |
127 |
128 | ) : null
129 | )}
130 |
131 |
132 |
133 | );
134 | };
135 |
136 | type Props = {};
137 |
138 | export default NetworkSelection;
139 |
--------------------------------------------------------------------------------
/src/steps/PaymentMethod/PaymentMethod.test.tsx:
--------------------------------------------------------------------------------
1 | import { generateTestingUtils } from 'eth-testing';
2 | import * as reactDeviceDetect from 'react-device-detect';
3 | import { act } from 'react-dom/test-utils';
4 |
5 | import { mockConfig } from '~/jest/__mocks__/mockConfig';
6 | import { fireEvent, render, screen } from '~/jest/test-utils';
7 |
8 | import App from '../../App';
9 | import PaymentMethod from '.';
10 |
11 | describe('Payment Selection', () => {
12 | beforeEach(() => {
13 | render(
14 | {}}
19 | />
20 | );
21 | });
22 | const testingUtils = generateTestingUtils({ providerType: 'MetaMask' });
23 | beforeAll(() => {
24 | global.window.ethereum = testingUtils.getProvider();
25 | global.window.ethereum.providers = [testingUtils.getProvider()];
26 | });
27 | afterEach(() => {
28 | testingUtils.clearAllMocks();
29 | });
30 | beforeEach(async () => {
31 | expect(await screen.findByText('Loading...')).toBeInTheDocument();
32 | const ethereum = await screen.findByText('Ether');
33 | fireEvent.click(ethereum);
34 | });
35 | it('renders', async () => {
36 | const paymentSelection = await screen.findByText('Payment Method');
37 | expect(paymentSelection).toBeInTheDocument();
38 | });
39 | describe('Search', () => {
40 | it('searches for a payment method', async () => {
41 | const rainbow = await screen.findByText('Rainbow');
42 | const searchInput = await screen.findByTestId('method-search');
43 | fireEvent.change(searchInput, { target: { value: 'metamask' } });
44 | const metamask = await screen.findByText('MetaMask');
45 | expect(metamask).toBeInTheDocument();
46 | expect(rainbow).not.toBeInTheDocument();
47 | await act(async () => {
48 | fireEvent.change(searchInput, {
49 | target: { value: '' },
50 | });
51 | });
52 | expect(await screen.findByText('Rainbow')).toBeInTheDocument();
53 | await act(async () => {
54 | fireEvent.change(searchInput, {
55 | target: { value: 'superduperwallet' },
56 | });
57 | });
58 | await screen.findByText('Payment Method Not Found');
59 | const retry = await screen.findByText('click here');
60 | await act(() => {
61 | fireEvent.click(retry);
62 | });
63 | // @ts-expect-error
64 | expect(searchInput.value).toBe('');
65 | });
66 | });
67 | describe('Method filtering', () => {
68 | it('filters out methods that dont support the eip:155 chain', async () => {
69 | const paymentSelection = await screen.findByText('Payment Method');
70 | expect(paymentSelection).toBeInTheDocument();
71 | const mtGoxWallet = await screen.queryByText('Mt Gox');
72 | expect(mtGoxWallet).toBeNull();
73 | });
74 | it('handles iOS and Android devices', async () => {
75 | Object.defineProperties(reactDeviceDetect, {
76 | isAndroid: { get: () => true },
77 | isIOS: { get: () => true },
78 | });
79 | const paymentSelection = await screen.findByText('Payment Method');
80 | expect(paymentSelection).toBeInTheDocument();
81 | });
82 | });
83 | });
84 |
85 | describe('Payment Selection', () => {
86 | beforeEach(() => {
87 | render(
88 | {}}
98 | />
99 | );
100 | });
101 | const testingUtils = generateTestingUtils({ providerType: 'MetaMask' });
102 | beforeAll(() => {
103 | global.window.ethereum = testingUtils.getProvider();
104 | global.window.ethereum.providers = [testingUtils.getProvider()];
105 | });
106 | afterEach(() => {
107 | testingUtils.clearAllMocks();
108 | });
109 | it('doesnt allow the user to go back beyond min step', async () => {
110 | const back = await screen.findByLabelText('Back');
111 | expect(back).toHaveClass('invisible');
112 | });
113 | });
114 |
115 | describe('Payment Selection', () => {
116 | beforeEach(() => {
117 | render(
118 | {}}
123 | />
124 | );
125 | });
126 |
127 | it('skips the payment selection step if there is only one payment method', async () => {
128 | const bitcoin = await screen.findByText('Bitcoin');
129 | fireEvent.click(bitcoin);
130 | const payToAddress = await screen.findByText('Pay to Address');
131 | expect(payToAddress).toBeInTheDocument();
132 | });
133 | it('skips the payment selection step if there is only one payment method due to mobile', async () => {
134 | Object.defineProperties(reactDeviceDetect, {
135 | isMobile: { get: () => true },
136 | });
137 | const polygon = await screen.findByText('Polygon');
138 | fireEvent.click(polygon);
139 | const showAddr = await screen.findByText('Pay to Address');
140 | expect(showAddr).toBeInTheDocument();
141 | });
142 | });
143 |
144 | describe('Payment Selection', () => {
145 | beforeEach(() => {
146 | render(
147 | {}}
159 | />
160 | );
161 | });
162 |
163 | it('skips the payment selection step if method is required and present', async () => {
164 | const ethereum = await screen.findByText('Ether');
165 | fireEvent.click(ethereum);
166 | const enterAmount = await screen.findByText('Enter Amount');
167 | expect(enterAmount).toBeInTheDocument();
168 | });
169 | });
170 |
171 | describe('Payment Method Errors', () => {
172 | it('renders', () => {
173 | render();
174 | expect(true).toBe(true);
175 | });
176 | });
177 |
--------------------------------------------------------------------------------
/src/steps/Result/BridgeQuoteTransactionDetails/index.tsx:
--------------------------------------------------------------------------------
1 | import { ReadOnlyText } from '@map3xyz/components';
2 | import { ethers } from 'ethers';
3 | import React, { useContext } from 'react';
4 | import { useTranslation } from 'react-i18next';
5 |
6 | import { ISO_4217_TO_SYMBOL } from '../../../constants/iso4217';
7 | import { Context } from '../../../providers/Store';
8 | import { DECIMAL_FALLBACK } from '../../EnterAmount';
9 |
10 | const BridgeQuoteTransactionDetails: React.FC = () => {
11 | const { t } = useTranslation();
12 | const [state] = useContext(Context);
13 |
14 | if (!state.asset) {
15 | return null;
16 | }
17 |
18 | return (
19 | <>
20 | {state.bridgeQuote?.approval?.amount ? (
21 |
22 |
23 | {t('copy.amount_to_pay')}
24 |
25 |
26 |
34 |
35 |
36 | ) : null}
37 | {state.bridgeQuote?.estimate?.gasCosts ? (
38 |
39 |
40 | {t('copy.gas_cost')}
41 |
42 |
43 |
50 |
51 |
52 | ) : null}
53 | {state.bridgeQuote?.estimate?.amountToReceive ? (
54 |
55 |
56 | {t('copy.receive_amount')}
57 |
58 |
59 |
66 |
67 |
68 | ) : null}
69 | {state.bridgeQuote?.id ? (
70 |
71 |
72 | Order ID
73 |
74 |
75 |
76 |
77 |
78 | ) : null}
79 | {state.bridgeTransaction?.destinationChainTxId ? (
80 |
81 |
82 | Destination Chain Tx ID
83 |
84 |
85 |
89 |
90 |
91 | ) : null}
92 | >
93 | );
94 | };
95 |
96 | type Props = {};
97 |
98 | export default BridgeQuoteTransactionDetails;
99 |
--------------------------------------------------------------------------------
/src/steps/Result/Result.test.tsx:
--------------------------------------------------------------------------------
1 | import { generateTestingUtils } from 'eth-testing';
2 | import { ethers } from 'ethers';
3 |
4 | import { mockConfig } from '~/jest/__mocks__/mockConfig';
5 | import { web3Mock } from '~/jest/__mocks__/web3Mock';
6 | import { act, fireEvent, render, screen } from '~/jest/test-utils';
7 |
8 | import App from '../../App';
9 | import * as useWeb3Mock from '../../hooks/useWeb3';
10 | import Result from '.';
11 |
12 | jest.mock('ethers', () => {
13 | const originalModule = jest.requireActual('ethers');
14 | return {
15 | ...originalModule,
16 | ethers: {
17 | ...originalModule.ethers,
18 | Contract: jest.fn(() => {
19 | return {
20 | balanceOf: jest.fn(),
21 | estimateGas: {
22 | transfer: jest.fn(() => {
23 | return ethers.BigNumber.from(21_000);
24 | }),
25 | },
26 | };
27 | }),
28 | },
29 | };
30 | });
31 |
32 | describe('Result', () => {
33 | const web3MockSpy = jest.spyOn(useWeb3Mock, 'useWeb3');
34 |
35 | const getBalanceMock = jest.fn().mockImplementation(() => ({
36 | assetBalance: ethers.BigNumber.from('100000000'),
37 | chainBalance: ethers.BigNumber.from('20000000000000000000'),
38 | }));
39 |
40 | const onSuccessMock = jest.fn();
41 | const onFailureMock = jest.fn();
42 | beforeEach(() => {
43 | render(
44 | {}}
57 | />
58 | );
59 | });
60 | describe('success', () => {
61 | const testingUtils = generateTestingUtils({
62 | providerType: 'MetaMask',
63 | });
64 | beforeAll(() => {
65 | const waitForTransactionMock = jest.fn().mockImplementation(() => ({
66 | blockNumber: 1,
67 | }));
68 | const mockPrepareFinalTransaction = jest
69 | .fn()
70 | .mockImplementation(
71 | () =>
72 | '0x0766849abf0e1d3288512c3a3580193b28036e6e7765362868a679435f275f1e'
73 | );
74 | web3MockSpy.mockImplementation(() => ({
75 | ...web3Mock,
76 | getBalance: getBalanceMock,
77 | prepareFinalTransaction: mockPrepareFinalTransaction,
78 | waitForTransaction: waitForTransactionMock,
79 | }));
80 | global.window.ethereum = testingUtils.getProvider();
81 | global.window.ethereum.providers = [testingUtils.getProvider()];
82 | testingUtils.mockConnectedWallet([
83 | '0xf61B443A155b07D2b2cAeA2d99715dC84E839EEf',
84 | ]);
85 | testingUtils.lowLevel.mockRequest('eth_gasPrice', () => '0x10cd96a16e');
86 | testingUtils.lowLevel.mockRequest(
87 | 'eth_sendTransaction',
88 | '0x0766849abf0e1d3288512c3a3580193b28036e6e7765362868a679435f275f1e'
89 | );
90 | });
91 | afterEach(() => {
92 | testingUtils.clearAllMocks();
93 | });
94 | beforeEach(async () => {
95 | await screen.findByText('Loading...');
96 | const elonCoin = await screen.findByText('ElonCoin');
97 | fireEvent.click(elonCoin);
98 | const ethNetwork = await screen.findByText('Ethereum');
99 | fireEvent.click(ethNetwork);
100 | const metamaskExtension = await screen.findByText('MetaMask');
101 | fireEvent.click(metamaskExtension);
102 | const input = await screen.findByTestId('input');
103 | await act(() => {
104 | fireEvent.change(input, { target: { value: '1' } });
105 | });
106 | await screen.findByText(/Max: 100 ELON/);
107 | await screen.findByText('Confirm Payment');
108 | const form = await screen.findByTestId('enter-amount-form');
109 | await act(async () => {
110 | fireEvent.submit(form);
111 | });
112 | });
113 | it('renders', async () => {
114 | expect(await screen.findByText('Submitted')).toBeInTheDocument();
115 | expect(await screen.findByText('Confirming')).toBeInTheDocument();
116 | expect(await screen.findByText('Confirmed')).toBeInTheDocument();
117 | const toggle = await screen.findByText('Transaction Details');
118 | await act(async () => {
119 | fireEvent.click(toggle);
120 | });
121 | const details = await screen.findByTestId('transaction-details');
122 | expect(details).toHaveClass(
123 | 'relative w-full border-t border-primary-200 bg-primary-100 transition-all dark:border-primary-700 dark:bg-primary-800 hidden'
124 | );
125 | });
126 | });
127 | });
128 |
129 | describe('Result Error', () => {
130 | render();
131 | expect(true).toBe(true);
132 | });
133 |
--------------------------------------------------------------------------------
/src/steps/Result/TransactionDetails/index.tsx:
--------------------------------------------------------------------------------
1 | import { ReadOnlyText } from '@map3xyz/components';
2 | import React, { useContext } from 'react';
3 |
4 | import { Context } from '../../../providers/Store';
5 |
6 | const TransactionDetails: React.FC = () => {
7 | const [state] = useContext(Context);
8 |
9 | return (
10 | <>
11 |
12 | Amount
13 |
14 |
15 |
16 |
17 | {state.tx.response?.from ? (
18 | <>
19 |
20 | From
21 |
22 |
23 | >
24 | ) : null}
25 | {state.tx.response?.to ? (
26 | <>
27 |
28 | To
29 |
30 |
31 | >
32 | ) : null}
33 | {state.tx.hash ? (
34 | <>
35 |
36 | Hash
37 |
38 |
39 | >
40 | ) : null}
41 | >
42 | );
43 | };
44 |
45 | type Props = {};
46 |
47 | export default TransactionDetails;
48 |
--------------------------------------------------------------------------------
/src/steps/Result/utils.ts:
--------------------------------------------------------------------------------
1 | export const timeToExpiration = (
2 | originalTime: number,
3 | durationSeconds: number
4 | ) => {
5 | const time = new Date().getTime();
6 | const durationMilliseconds = durationSeconds * 1000;
7 | const expiresAt = new Date(originalTime + durationMilliseconds).getTime();
8 | const millisecondsRemaining = expiresAt - time;
9 |
10 | const seconds = Math.floor((millisecondsRemaining / 1000) % 60);
11 | const minutes = Math.floor((millisecondsRemaining / (1000 * 60)) % 60);
12 | const hours = Math.floor((millisecondsRemaining / (1000 * 60 * 60)) % 24);
13 | const timeString = `${
14 | hours ? hours.toString().padStart(2, '0') + ':' : ''
15 | }${minutes.toString().padStart(2, '0')}:${seconds
16 | .toString()
17 | .padStart(2, '0')}`;
18 |
19 | if (millisecondsRemaining < 0) {
20 | return '00:00';
21 | }
22 |
23 | return timeString;
24 | };
25 |
--------------------------------------------------------------------------------
/src/steps/ShowAddress/ShowAddress.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 | import ShowAddress from '.';
6 |
7 | describe('Show Address', () => {
8 | beforeEach(async () => {
9 | render( {}} />);
10 | await screen.findByText('Loading...');
11 | const elonCoin = await screen.findByText('ElonCoin');
12 | fireEvent.click(elonCoin);
13 | const eth = await screen.findByText('Ethereum');
14 | fireEvent.click(eth);
15 | const showAddress = await screen.findByText('Show Address');
16 | fireEvent.click(showAddress);
17 | });
18 | it('renders', async () => {
19 | const showAddressMethod = await screen.findByTestId('show-address-method');
20 | expect(showAddressMethod).toBeInTheDocument();
21 | const qrValue = await screen.findByTestId('qr-value');
22 | expect(qrValue.textContent).toBe(
23 | '0x0000000000000000000000000000000000000000'
24 | );
25 | });
26 | it('goes back to the correct step', async () => {
27 | const back = await screen.findByLabelText('Back');
28 | await act(async () => {
29 | await fireEvent.click(back);
30 | });
31 | const paymentSelection = await screen.findByTestId('payment-method');
32 | expect(paymentSelection).toBeInTheDocument();
33 | });
34 | });
35 |
36 | describe('Show Address > bip21', () => {
37 | beforeEach(async () => {
38 | render(
39 | {
47 | return {
48 | address: '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa',
49 | };
50 | },
51 | },
52 | selection: {
53 | amount: '100000',
54 | assetId: 'satoshi123',
55 | },
56 | },
57 | }}
58 | onClose={() => {}}
59 | />
60 | );
61 | });
62 | it('generates a bip21 url scheme', async () => {
63 | const ackCheckbox = await screen.findByTestId('acknowledge-checkbox');
64 | fireEvent.click(ackCheckbox);
65 | const ackButton = await screen.findByText('Acknowledge Amount');
66 | fireEvent.click(ackButton);
67 | const qrValue = await screen.findByTestId('qr-value');
68 | expect(qrValue.textContent).toBe(
69 | 'bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?amount=0.001'
70 | );
71 | expect(qrValue).toHaveClass('hidden');
72 | });
73 | });
74 |
75 | describe('Show Address > eip681', () => {
76 | beforeEach(async () => {
77 | render(
78 | {
86 | return {
87 | address: '0x0000000000000000000000000000000000000000',
88 | };
89 | },
90 | },
91 | selection: {
92 | amount: '100000000',
93 | assetId: 'ethereum123',
94 | },
95 | },
96 | }}
97 | onClose={() => {}}
98 | />
99 | );
100 | });
101 | it('generates a eip681 url scheme', async () => {
102 | const showAddress = await screen.findByText('Show Address');
103 | fireEvent.click(showAddress);
104 | const ackCheckbox = await screen.findByTestId('acknowledge-checkbox');
105 | fireEvent.click(ackCheckbox);
106 | const ackButton = await screen.findByText('Acknowledge Amount');
107 | fireEvent.click(ackButton);
108 | const qrValue = await screen.findByTestId('qr-value');
109 | expect(qrValue.textContent).toBe(
110 | 'ethereum:0x0000000000000000000000000000000000000000@1?value=1e8'
111 | );
112 | expect(qrValue).toHaveClass('hidden');
113 | });
114 | });
115 |
116 | describe('Show Address Errors', () => {
117 | it('renders', async () => {
118 | render();
119 | expect(true).toBe(true);
120 | });
121 | it('handles errors generating address', async () => {
122 | render(
123 | {
131 | throw new Error('Test Error');
132 | },
133 | },
134 | },
135 | }}
136 | onClose={() => {}}
137 | />
138 | );
139 | await screen.findByText('Loading...');
140 | const bitcoin = await screen.findByText('Bitcoin');
141 | fireEvent.click(bitcoin);
142 | const showAddress = await screen.findByText('Show Address');
143 | fireEvent.click(showAddress);
144 | const error = await screen.findByText('Error Generating Address');
145 | expect(error).toBeInTheDocument();
146 | });
147 | });
148 |
--------------------------------------------------------------------------------
/src/steps/SwitchChain/index.tsx:
--------------------------------------------------------------------------------
1 | import { Badge, Button, CoinLogo } from '@map3xyz/components';
2 | import React, { useContext, useState } from 'react';
3 |
4 | import InnerWrapper from '../../components/InnerWrapper';
5 | import LoadingWrapper from '../../components/LoadingWrapper';
6 | import StateDescriptionHeader from '../../components/StateDescriptionHeader';
7 | import StepTitle from '../../components/StepTitle';
8 | import { useGetNetworkByChainIdQuery } from '../../generated/apollo-gql';
9 | import { useWeb3 } from '../../hooks/useWeb3';
10 | import { Context, Steps } from '../../providers/Store';
11 |
12 | const CHAIN_MISSING = 'Unrecognized chain ID';
13 |
14 | const SwitchChain: React.FC = () => {
15 | const [state, dispatch] = useContext(Context);
16 | const { addChain, switchChain } = useWeb3();
17 | const [loading, setLoading] = useState(false);
18 | const [formError, setFormError] = useState(null);
19 |
20 | const {
21 | data: currentChain,
22 | loading: loadingCurrentChain,
23 | } = useGetNetworkByChainIdQuery({
24 | variables: { chainId: Number(state.providerChainId) },
25 | });
26 |
27 | if (!state.method) {
28 | dispatch({ payload: Steps.PaymentMethod, type: 'SET_STEP' });
29 | return null;
30 | }
31 |
32 | const handleSwitch = async () => {
33 | try {
34 | setLoading(true);
35 | await switchChain(state.network?.identifiers?.chainId!);
36 | } catch (e: any) {
37 | if (e.message?.includes(CHAIN_MISSING)) {
38 | try {
39 | await addChain();
40 | return;
41 | } catch (addChainError) {
42 | e = addChainError as Error;
43 | }
44 | }
45 | setFormError(e.message);
46 | } finally {
47 | setLoading(false);
48 | }
49 | };
50 |
51 | return (
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | {loadingCurrentChain ? (
61 |
62 | ) : currentChain ? (
63 |
64 |
65 |
66 | Current Network
67 |
68 |
75 |
76 | {currentChain.networkByChainId?.name}
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | Switch to
85 |
86 |
93 |
94 | {state.network?.networkName}
95 |
96 |
97 |
98 | ) : null}
99 |
100 |
101 |
102 |
103 | {formError ? (
104 |
105 | {formError}
106 |
107 | ) : null}
108 |
109 |
110 |
120 |
121 |
122 | );
123 | };
124 |
125 | type Props = {};
126 |
127 | export default SwitchChain;
128 |
--------------------------------------------------------------------------------
/src/styles/globals.scss:
--------------------------------------------------------------------------------
1 | .map3 {
2 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
3 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
4 | sans-serif;
5 | -webkit-font-smoothing: antialiased;
6 | -moz-osx-font-smoothing: grayscale;
7 |
8 | input::-webkit-outer-spin-button,
9 | input::-webkit-inner-spin-button {
10 | -webkit-appearance: none;
11 | margin: 0;
12 | }
13 |
14 | @media (min-width: 640px) {
15 | .sm\:text-xs {
16 | font-size: .75rem !important;
17 | line-height: 1rem !important;
18 | }
19 |
20 | .sm\:text-sm {
21 | font-size: .875rem !important;
22 | line-height: 1.25rem !important;
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/styles/scrollbars.scss:
--------------------------------------------------------------------------------
1 | /* Variables */
2 | :root {
3 | --scrollbar-size: .375rem;
4 | --scrollbar-minlength: 1.5rem;
5 | /* Minimum length of scrollbar thumb (width of horizontal, height of vertical) */
6 | --scrollbar-ff-width: thin;
7 | /* FF-only accepts auto, thin, none */
8 | --scrollbar-track-color: transparent;
9 | --scrollbar-color: rgba(0, 0, 0, .2);
10 | --scrollbar-color-hover: rgba(0, 0, 0, .3);
11 | --scrollbar-color-active: rgb(0, 0, 0);
12 | }
13 |
14 | /* Use .map3-layout-scrollbar-obtrusive to only use overflow if scrollbars don’t overlay */
15 | .scrollbar-test,
16 | .layout-cell {
17 | overscroll-behavior: contain;
18 | overflow-y: auto;
19 | -webkit-overflow-scrolling: touch;
20 | -ms-overflow-style: -ms-autohiding-scrollbar;
21 | scrollbar-width: var(--scrollbar-ff-width);
22 | }
23 |
24 | /* This class controls what elements have the new fancy scrollbar CSS */
25 | .layout-scrollbar {
26 | overflow-y: overlay;
27 | scrollbar-color: var(--scrollbar-color) var(--scrollbar-track-color);
28 | }
29 |
30 | /* Only apply height/width to ::-webkit-scrollbar if is obtrusive */
31 | .map3-layout-scrollbar-obtrusive .layout-scrollbar::-webkit-scrollbar {
32 | height: var(--scrollbar-size);
33 | width: var(--scrollbar-size);
34 | }
35 |
36 | .layout-scrollbar::-webkit-scrollbar-track {
37 | background: var(--scrollbar-track-color);
38 | }
39 |
40 | .layout-scrollbar::-webkit-scrollbar-thumb {
41 | background: var(--scrollbar-color);
42 | border-radius: 3px;
43 | }
44 |
45 | .layout-scrollbar::-webkit-scrollbar-thumb:hover {
46 | background: var(--scrollbar-color-hover);
47 | }
48 |
49 | .layout-scrollbar::-webkit-scrollbar-thumb:active {
50 | background: var(--scrollbar-color-active);
51 | }
52 |
53 | .scrollbar-test::-webkit-scrollbar-thumb:vertical,
54 | .layout-scrollbar::-webkit-scrollbar-thumb:vertical {
55 | min-height: var(--scrollbar-minlength);
56 | }
57 |
58 | .scrollbar-test::-webkit-scrollbar-thumb:horizontal,
59 | .layout-scrollbar::-webkit-scrollbar-thumb:horizontal {
60 | min-width: var(--scrollbar-minlength);
61 | }
62 |
63 | .dark {
64 | --scrollbar-color: rgba(255, 255, 255, .2);
65 | --scrollbar-color-hover: rgba(255, 255, 255, .3);
66 | --scrollbar-color-active: rgb(255, 255, 255, .5);
67 | }
68 |
69 | .rainbow-road {
70 | --scrollbar-track-color: linear-gradient(
71 | 0deg,
72 | rgba(255, 0, 0, 1) 0%,
73 | rgba(255, 154, 0, 1) 10%,
74 | rgba(208, 222, 33, 1) 20%,
75 | rgba(79, 220, 74, 1) 30%,
76 | rgba(63, 218, 216, 1) 40%,
77 | rgba(47, 201, 226, 1) 50%,
78 | rgba(28, 127, 238, 1) 60%,
79 | rgba(95, 21, 242, 1) 70%,
80 | rgba(186, 12, 248, 1) 80%,
81 | rgba(251, 7, 217, 1) 90%,
82 | rgba(255, 0, 0, 1) 100%
83 | );
84 | .rainbow-background {
85 | background: linear-gradient(
86 | 90deg,
87 | rgba(255, 0, 0, 1) 0%,
88 | rgba(255, 154, 0, 1) 10%,
89 | rgba(208, 222, 33, 1) 20%,
90 | rgba(79, 220, 74, 1) 30%,
91 | rgba(63, 218, 216, 1) 40%,
92 | rgba(47, 201, 226, 1) 50%,
93 | rgba(28, 127, 238, 1) 60%,
94 | rgba(95, 21, 242, 1) 70%,
95 | rgba(186, 12, 248, 1) 80%,
96 | rgba(251, 7, 217, 1) 90%,
97 | rgba(255, 0, 0, 1) 100%
98 | );
99 | }
100 | --scrollbar-color: transparent;
101 | .layout-scrollbar {
102 | overflow-y: overlay;
103 | scrollbar-color: initial;
104 | }
105 |
106 | .layout-scrollbar::-webkit-scrollbar-thumb {
107 | background: transparent; /* opacity: 0; should do the thing either */
108 | box-shadow: 0px 0px 0px 100000vh rgb(0 0 0 / var(--tw-bg-opacity));
109 | }
110 |
111 | &.dark {
112 | .layout-scrollbar::-webkit-scrollbar-thumb {
113 | box-shadow: 0px 0px 0px 100000vh var(--bg-primary-900);
114 | }
115 | }
116 | }
--------------------------------------------------------------------------------
/src/styles/supabase.scss:
--------------------------------------------------------------------------------
1 | .map3-accent {
2 | .sbui-btn-default {
3 | background: var(--accent-color) !important;
4 | color: var(--primary-color-900) !important;
5 | text-shadow: none !important;
6 | }
7 |
8 | }
9 |
10 | .dark {
11 | .map3 {
12 | .sbui-modal {
13 | background: var(--primary-color-900) !important;
14 | }
15 | }
16 | }
17 |
18 | .map3 {
19 | .sbui-modal {
20 | background: white !important;
21 | }
22 |
23 | .sbui-modal-content {
24 | padding: 0 !important;
25 | height: 100%;
26 | width: 100%;
27 | }
28 |
29 | .sbui-modal-content>.sbui-space-row {
30 | height: 100%;
31 | }
32 |
33 | .sbui-modal-content>.sbui-space-row>.sbui-space-col {
34 | height: 100%;
35 | }
36 |
37 | .w-full.sbui-modal--tiny {
38 | max-width: 100% !important;
39 | }
40 |
41 | .h-full .sbui-modal-content > .sbui-space-row {
42 | height: inherit !important;
43 | }
44 |
45 | .h-full .sbui-modal-content > .sbui-space-row > .sbui-space-col {
46 | height: inherit !important;
47 | }
48 |
49 | @media screen and (min-width: 640px) {
50 | .sbui-modal-content>.sbui-space-row {
51 | height: initial;
52 | }
53 |
54 | .sbui-modal-content>.sbui-space-row>.sbui-space-col {
55 | height: initial;
56 | }
57 | }
58 |
59 | .sbui-space-y-4> :not([hidden])~ :not([hidden]) {
60 | margin: 0 !important;
61 | }
62 |
63 | .sbui-modal-footer {
64 | display: none !important;
65 | }
66 | }
--------------------------------------------------------------------------------
/src/types/index.d.ts:
--------------------------------------------------------------------------------
1 | export {};
2 |
3 | declare global {
4 | interface Window {
5 | ethereum: any;
6 | isMap3Hosted: boolean;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/types/supabase.ts:
--------------------------------------------------------------------------------
1 | type WatchAddressPayload = {
2 | commit_timestamp: string; // '2023-01-13T15:36:49Z';
3 | errors: null;
4 | eventType: 'UPDATE';
5 | new: {
6 | address: string; // '0x165CD37b4C644C2921454429E7F9358d18A45e14';
7 | asset_id: string; // '38975bff-987f-4a06-b488-c75177e06914';
8 | block_height: number;
9 | confirmations_to_watch: number;
10 | created: string; // '2023-01-13T15:36:11.086';
11 | id: string; // '650b5833-1a98-4d30-bbfd-922c077bb400';
12 | memo: null;
13 | network_code: string; // 'goerli';
14 | organization_id: string; // '01d53f71-e299-4521-a45b-6a8909d3c0d5';
15 | state: 'confirming' | 'pending' | 'confirmed';
16 | subscribed: true;
17 | tx_amount: string; // '5000000000000';
18 | tx_block_hash: string; // '0x1705f2930e2de09d0394f76b946b067115f14e8144bb3e3f077be618fd11436e';
19 | tx_block_height: number;
20 | tx_confirmations: number;
21 | tx_data: string; // '0x';
22 | tx_detected_at: string; // '2023-01-13T15:36:27.722';
23 | tx_formatted_amount: string; // '0.005';
24 | tx_id: string; // '0x1fc77a32a90a7f885c5141692ecf7d89a963ea8db0abd5a9debb00fb473b948f';
25 | updated: string; // '2023-01-13T15:36:49.281';
26 | user_id: string; // 'preview-user-id';
27 | };
28 | old: {
29 | id: '650b5833-1a98-4d30-bbfd-922c077bb400';
30 | };
31 | schema: 'public';
32 | table: 'watched_address';
33 | };
34 |
35 | type WatchBridgeTransactionPayload = {
36 | commit_timestamp: string; // '2023-01-13T15:36:49Z';
37 | errors: null;
38 | eventType: 'UPDATE';
39 | new: {
40 | state: 'quoted' | 'subscribed' | 'failed' | 'completed';
41 | };
42 | old: {
43 | id: '650b5833-1a98-4d30-bbfd-922c077bb400';
44 | };
45 | schema: 'public';
46 | table: 'watched_address';
47 | };
48 |
--------------------------------------------------------------------------------
/src/types/walletConnect.ts:
--------------------------------------------------------------------------------
1 | type WalletConnectAppType = {
2 | android: string;
3 | browser: string;
4 | ios: string;
5 | linux: string;
6 | mac: string;
7 | windows: string;
8 | };
9 |
10 | type WalletConnectImageURLType = {
11 | lg: string;
12 | md: string;
13 | sm: string;
14 | };
15 |
16 | type WalletConnectPlatformType = {
17 | native: string;
18 | universal: string;
19 | };
20 |
21 | type WalletConnectMetadataColorsType = {
22 | primary: string;
23 | secondary: string;
24 | };
25 |
26 | type WalletConnectMetadataType = {
27 | colors: WalletConnectMetadataColorsType;
28 | shortName: string;
29 | };
30 |
31 | export type WalletConnectWallet = {
32 | app: WalletConnectAppType;
33 | app_type: string;
34 | chains: string[];
35 | description: string;
36 | desktop: WalletConnectPlatformType;
37 | homepage: string;
38 | id: string;
39 | image_id: string;
40 | image_url: WalletConnectImageURLType;
41 | metadata: WalletConnectMetadataType;
42 | mobile: WalletConnectPlatformType;
43 | name: string;
44 | sdks: string[];
45 | versions: string[];
46 | };
47 |
--------------------------------------------------------------------------------
/src/utils/abis/erc20.ts:
--------------------------------------------------------------------------------
1 | export const erc20Abi = [
2 | // Read-Only Functions
3 | 'function balanceOf(address owner) view returns (uint256)',
4 | 'function allowance(address owner, address spender) view returns (uint256)',
5 |
6 | // Authenticated Functions
7 | 'function transfer(address to, uint256 amount) returns (bool)',
8 | 'function approve(address spender, uint256 amount) returns (bool)',
9 |
10 | // Events
11 | 'event Transfer(address indexed from, address indexed to, uint amount)',
12 | ];
13 |
--------------------------------------------------------------------------------
/src/utils/debounce.ts:
--------------------------------------------------------------------------------
1 | export const debounce = (func: any, duration: number) => {
2 | let timeout: any;
3 | // @ts-ignore
4 | return function (...args) {
5 | const effect = () => {
6 | timeout = null;
7 | // @ts-ignore
8 | return func.apply(this, args);
9 | };
10 | clearTimeout(timeout);
11 | timeout = setTimeout(effect, duration);
12 | };
13 | };
14 |
--------------------------------------------------------------------------------
/src/utils/iso8601.ts:
--------------------------------------------------------------------------------
1 | export const iso8601ToDate = (iso8601: string, offset?: number) => {
2 | const tzoffset = (offset || new Date(iso8601).getTimezoneOffset()) * 60000;
3 | const localISODate = new Date(new Date(iso8601).getTime() - tzoffset);
4 |
5 | return localISODate;
6 | };
7 |
8 | export const iso8601ToString = (iso8601: string, offset?: number) => {
9 | const date = iso8601ToDate(iso8601, offset);
10 | return date.toLocaleString();
11 | };
12 |
--------------------------------------------------------------------------------
/src/utils/parseJwt.ts:
--------------------------------------------------------------------------------
1 | export const parseJwt = (token: string) =>
2 | JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
3 |
--------------------------------------------------------------------------------
/src/utils/supabase.ts:
--------------------------------------------------------------------------------
1 | import { createClient, RealtimeChannel } from '@supabase/supabase-js';
2 |
3 | const supabase = createClient(
4 | process.env.SUPABASE_URL || '',
5 | process.env.SUPABASE_ANON_KEY || '',
6 | {
7 | realtime: {
8 | params: {
9 | eventsPerSecond: 10,
10 | },
11 | },
12 | }
13 | );
14 |
15 | interface WatchChannelCallbackFn {
16 | (payload: any): void;
17 | }
18 |
19 | export function listenToWatchedAddress(
20 | watchedAddressId: string,
21 | callback: WatchChannelCallbackFn
22 | ): RealtimeChannel {
23 | return supabase
24 | .channel('watched_address_changes')
25 | .on(
26 | 'postgres_changes',
27 | {
28 | event: 'UPDATE',
29 | filter: `id=eq.${watchedAddressId}`,
30 | schema: 'public',
31 | table: 'watched_address',
32 | },
33 | callback
34 | )
35 | .subscribe();
36 | }
37 |
38 | export function listenToBridgeTransaction(
39 | bridgeTransactionId: string,
40 | callback: WatchChannelCallbackFn
41 | ): RealtimeChannel {
42 | return supabase
43 | .channel('bridge_transaction_changes')
44 | .on(
45 | 'postgres_changes',
46 | {
47 | event: 'UPDATE',
48 | filter: `id=eq.${bridgeTransactionId}`,
49 | schema: 'public',
50 | table: 'bridge_transaction',
51 | },
52 | callback
53 | )
54 | .subscribe();
55 | }
56 |
--------------------------------------------------------------------------------
/src/utils/toHex.ts:
--------------------------------------------------------------------------------
1 | export const toHex = (value: number) => {
2 | if (
3 | typeof value === 'string' &&
4 | ((value as unknown) as string).startsWith('0x')
5 | ) {
6 | return value;
7 | }
8 |
9 | return '0x' + value.toString(16);
10 | };
11 |
--------------------------------------------------------------------------------
/src/utils/transactions/evm/index.test.ts:
--------------------------------------------------------------------------------
1 | import { buildTx } from './index';
2 |
3 | describe('EVM transactions', () => {
4 | describe('buildTx', () => {
5 | it('builds a transaction', () => {
6 | const result = buildTx({
7 | address: '0x123',
8 | amount: '1',
9 | assetContract: null,
10 | decimals: 18,
11 | from: '0x456',
12 | memo: '0x678',
13 | });
14 | expect(result).toEqual({
15 | data: '0x678',
16 | from: '0x456',
17 | to: '0x123',
18 | value: '0x0de0b6b3a7640000',
19 | });
20 | });
21 | it('builds erc20 transaction', () => {
22 | const result = buildTx({
23 | address: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174',
24 | amount: '1',
25 | assetContract: '0xabc',
26 | decimals: 18,
27 | from: '0x456',
28 | memo: '0x678',
29 | });
30 | expect(result).toEqual({
31 | data: '0xa9059cbb0000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa841740000000000000000000000000000000000000000000000000de0b6b3a7640000678',
32 | from: '0x456',
33 | to: '0xabc',
34 | value: '0x0',
35 | });
36 | });
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/src/utils/transactions/evm/index.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from 'ethers';
2 |
3 | import { erc20Abi } from '../../abis/erc20';
4 |
5 | export type PrebuiltTx = {
6 | data: string;
7 | from: string;
8 | to: string;
9 | value: string;
10 | };
11 |
12 | export type FinalTx = PrebuiltTx & {
13 | gas: string;
14 | gasPrice?: string;
15 | maxFeePerGas?: string;
16 | maxPriorityFeePerGas?: string;
17 | };
18 |
19 | export const buildTx = (params: {
20 | address: string;
21 | amount: string;
22 | assetContract?: string | null;
23 | decimals: number;
24 | from: string;
25 | memo?: string | null;
26 | }): PrebuiltTx => {
27 | const { address, amount, assetContract, decimals, from, memo } = params;
28 | const txParams = {
29 | data: memo || '0x',
30 | from,
31 | to: address,
32 | value: ethers.utils.parseEther(amount).toHexString(),
33 | };
34 |
35 | if (assetContract) {
36 | txParams.data =
37 | new ethers.utils.Interface(erc20Abi).encodeFunctionData('transfer', [
38 | address,
39 | ethers.utils.parseUnits(amount, decimals).toString(),
40 | ]) + (typeof memo === 'string' ? (memo as string).replace('0x', '') : '');
41 |
42 | txParams.to = assetContract;
43 | txParams.value = '0x0';
44 | }
45 |
46 | return txParams;
47 | };
48 |
--------------------------------------------------------------------------------
/src/utils/transactions/index.ts:
--------------------------------------------------------------------------------
1 | import * as evm from './evm';
2 |
3 | export { evm };
4 |
--------------------------------------------------------------------------------
/src/utils/utils.test.ts:
--------------------------------------------------------------------------------
1 | import { iso8601ToString } from './iso8601';
2 | import { toHex } from './toHex';
3 |
4 | describe('toHex', () => {
5 | it('should convert a number to hex', () => {
6 | expect(toHex(0)).toEqual('0x0');
7 | expect(toHex(1)).toEqual('0x1');
8 | expect(toHex(10)).toEqual('0xa');
9 | expect(toHex(15)).toEqual('0xf');
10 | expect(toHex(16)).toEqual('0x10');
11 | expect(toHex(255)).toEqual('0xff');
12 | expect(toHex(256)).toEqual('0x100');
13 | expect(toHex(21_000)).toEqual('0x5208');
14 | expect(toHex(1_000_000)).toEqual('0xf4240');
15 | // @ts-expect-error
16 | expect(toHex('0x199563048')).toEqual('0x199563048');
17 | });
18 | });
19 |
20 | describe('iso8601', () => {
21 | expect(iso8601ToString('2023-03-07T22:33:50.6', 300)).toBe(
22 | '3/7/2023, 5:33:50 PM'
23 | );
24 | });
25 |
--------------------------------------------------------------------------------
/src/utils/wait.tsx:
--------------------------------------------------------------------------------
1 | export const wait = (n: number) =>
2 | new Promise((resolve) => setTimeout(resolve, n));
3 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | './src/**/*.{html,tsx}',
5 | 'node_modules/@map3xyz/components/dist/esm/components/**/*.{html,js}',
6 | ],
7 | darkMode: 'class',
8 | plugins: [],
9 | theme: {
10 | extend: {
11 | colors: {
12 | accent: 'var(--accent-color)',
13 | 'accent-light': 'var(--accent-color-light)',
14 | primary: {
15 | 100: 'var(--primary-color-100)',
16 | 200: 'var(--primary-color-200)',
17 | 400: 'var(--primary-color-400)',
18 | 500: 'var(--primary-color-500)',
19 | 700: 'var(--primary-color-700)',
20 | 800: 'var(--primary-color-800)',
21 | 900: 'var(--primary-color-900)',
22 | },
23 | },
24 | fontSize: {
25 | xxs: '.625rem',
26 | },
27 | },
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["src", "src/custom.d.ts"],
3 | "exclude": ["dist", "node_modules"],
4 | "compilerOptions": {
5 | "esModuleInterop": true,
6 | "declaration": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "jsx": "react-jsx",
9 | "importHelpers": true,
10 | "lib": ["dom", "esnext"],
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "noFallthroughCasesInSwitch": true,
14 | "noImplicitReturns": true,
15 | "noUnusedLocals": true,
16 | "noUnusedParameters": true,
17 | "outDir": "./dist/esm",
18 | "resolveJsonModule": true,
19 | "rootDir": ".",
20 | "skipLibCheck": true,
21 | "sourceMap": true,
22 | "strict": true,
23 | "paths": {
24 | "~/jest/*": ["./jest/*"]
25 | },
26 | }
27 | }
28 |
--------------------------------------------------------------------------------