├── .editorconfig ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .prettierrc ├── README.md ├── cypress.json ├── cypress ├── fixtures │ └── example.json ├── integration │ └── demo.spec.ts └── tsconfig.json ├── next-env.d.ts ├── next.config.js ├── package.json ├── pnpm-lock.yaml ├── public └── images │ ├── account.svg │ ├── bsc.svg │ ├── en.png │ ├── enter.svg │ ├── eth.svg │ ├── icon_history.svg │ ├── imtoken.svg │ ├── iopay.svg │ ├── iotex.svg │ ├── learn connect.svg │ ├── mathwallet.svg │ ├── metamask.svg │ ├── polygon.svg │ ├── ru.png │ ├── token.svg │ ├── tokenpocket.svg │ ├── trustwallet.svg │ ├── v2.png │ ├── walletConnect.svg │ └── zh_CN.png ├── src ├── components │ ├── Common │ │ ├── Copy │ │ │ └── index.tsx │ │ └── Table │ │ │ └── index.tsx │ ├── HistoryModal │ │ └── index.tsx │ ├── Jazzicon │ │ ├── hexstring-utils.ts │ │ ├── icon-factory.ts │ │ └── index.tsx │ ├── LanguageSwitch.tsx │ ├── Layout │ │ ├── HeaderLeft.tsx │ │ ├── HeaderTop.tsx │ │ ├── Links.tsx │ │ ├── SwitchTheme.tsx │ │ ├── User.tsx │ │ └── index.tsx │ ├── NetworkCheckProvider │ │ └── index.tsx │ ├── Template.tsx │ ├── WagmiProvider │ │ └── index.tsx │ ├── WalletInfo │ │ └── index.tsx │ └── WalletSelecter │ │ └── index.tsx ├── config │ ├── chain.ts │ └── public.ts ├── i18n │ ├── config.ts │ ├── de │ │ └── translation.json │ ├── en │ │ └── translation.json │ ├── es │ │ └── translation.json │ ├── ja │ │ └── translation.json │ ├── ko │ │ └── translation.json │ ├── ru │ │ └── translation.json │ ├── zh-cn │ │ └── translation.json │ └── zh-tw │ │ └── translation.json ├── lib │ ├── __generated │ │ ├── schema.graphql │ │ ├── sdk.ts │ │ └── typing.ts │ ├── axios.ts │ ├── event.ts │ ├── helper.ts │ ├── hooks.ts │ ├── ledger.ts │ ├── lodash.ts │ ├── metaskUtils.ts │ ├── smartgraph │ │ ├── antenna.plugin.ts │ │ ├── gql.ts │ │ ├── index.ts │ │ ├── schema.json │ │ └── uniswap.plugin.ts │ ├── trpc.ts │ ├── wagmi.ts │ └── web3-react.ts ├── pages │ ├── 404.tsx │ ├── 500.tsx │ ├── _app.tsx │ ├── _document.tsx │ ├── api │ │ ├── auth │ │ │ ├── jwt.ts │ │ │ └── nonce.ts │ │ ├── graphql.ts │ │ └── trpc │ │ │ └── [trpc].ts │ ├── index.tsx │ └── swap.tsx ├── server │ ├── context.ts │ ├── createRouter.ts │ ├── routers │ │ ├── _app.ts │ │ └── template.ts │ └── trpc.ts └── store │ ├── entity.ts │ ├── god.ts │ ├── history.ts │ ├── index.ts │ ├── lang.ts │ ├── ledger.ts │ ├── lib │ ├── ChainState.ts │ ├── CoinState.ts │ ├── EthNetworkState.ts │ └── NetworkState.ts │ ├── root.ts │ ├── standard │ ├── BigNumberInputState.ts │ ├── BigNumberState.ts │ ├── MappingState.ts │ ├── PromiseState.ts │ ├── StorageState.ts │ └── base.ts │ ├── template │ ├── State.ts │ └── Store.ts │ └── user.tsx ├── tsconfig.json └── type.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 2 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: tests 4 | 5 | # Controls when the action will run. 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the main branch 8 | push: 9 | branches: [ main ] 10 | pull_request: 11 | branches: [ main ] 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | # This workflow contains a single job called "build" 16 | build: 17 | # The type of runner that the job will run on 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: pnpm/action-setup@v2.2.2 23 | with: 24 | version: 7 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v2 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | cache: 'pnpm' 30 | - name: Install dependencies 31 | run: pnpm install 32 | - name: Run Test Build 33 | env: 34 | CI: false 35 | run: pnpm build 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | .vscode 7 | out 8 | .next 9 | yarn-error.log 10 | .env* 11 | package-lock.json 12 | yarn.lock 13 | .scannerwork 14 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 200, 4 | "trailingComma": "none" 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IoTeX dApp Sample V3 2 | 3 | 4 | 5 | 6 | 7 | ![8861650093939_ pic_hd](https://user-images.githubusercontent.com/448293/171796205-937711d1-e336-4770-9388-ec0b02de3b89.jpg) 8 | 9 | 10 | This is a boilerplate template for making your awesome dApp on IoTeX and ETH, BSC, and other possible chains ([request here](https://github.com/iotexproject/iotex-dapp-sample-v2/issues/new)) 11 | 12 | Technology used in this template are 13 | 14 | - [Next](https://github.com/vercel/next.js) 15 | - [React](https://reactjs.org/) 16 | - [TRPC](https://trpc.io/) 17 | - [Typescript](https://www.typescriptlang.org/) 18 | - [Mobx](https://mobx.js.org/README.html) 19 | - [Matine](https://mantine.dev/core/theme-icon/) 20 | - [Cypress](https://www.cypress.io/) 21 | 22 | ## Intro 23 | 24 | A starter for React with Typescript with the fast Vite and the beautiful Matine, tested with the powerful Cypress. 25 | 26 | ## Cheat Sheet 27 | 28 | Here's a cheat sheet list to help you get started quickly 29 | 30 | ```ts 31 | import { rootStore, useStore } from '@/store/index'; 32 | 33 | const { god } = useStore(); 34 | // or const god = rootStore.god 35 | 36 | god.isConnect; 37 | 38 | god.currentChain; 39 | god.currentChain.chainId; // for current connected chain id 40 | god.currentChain.Coin; // eth/bnb/iotx 41 | god.currentChain.Coin.balance; // current balance 42 | // ... see ChainState 43 | 44 | god.currentNetwork; 45 | god.currentNetwork.account; // for current connected account address 46 | // ... see NetworkState 47 | 48 | god.setShowConnecter(); // to show/close the Wallet Selector 49 | 50 | god.currentNetwork.loadBalance(); // to load chain coin balance 51 | 52 | await rpc('query')({ 53 | UniswapRouter: [ 54 | { calls: [{ address: '0x95cB18889B968AbABb9104f30aF5b310bD007Fd8', chainId: 4689 }] }, 55 | { 56 | swap: [ 57 | { 58 | args: { 59 | sellToken: 'BUSD_b', 60 | buyToken: '0xb8744ae4032be5e5ef9fab94ee9c3bf38d5d2ae0', 61 | buyAmount, 62 | recipient: '0x2AcB8663B18d8c8180783C18b88D60b86de26dF2', 63 | offlinePrice: true 64 | } 65 | }, 66 | { 67 | amount: true, 68 | data: true, 69 | router: true, 70 | path: { 71 | address: true, 72 | symbol: true, 73 | decimals: true, 74 | totalSupply: true 75 | } 76 | } 77 | ] 78 | } 79 | ] 80 | }); 81 | ``` 82 | 83 | ## Generate sdk 84 | 85 | ``` 86 | $ npm i @smartgraph/cli -g 87 | $ pnpm dev 88 | $ smartgraph codegen -l http://localhost:3000/api/graphql -o ./src/lib 89 | 90 | ``` 91 | 92 | ## Installation 93 | 94 | Clone the repo and run `pnpm install` 95 | 96 | ## Start 97 | 98 | After the successfull installation of the packages: `pnpm dev` 99 | 100 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "video": false, 3 | "pluginsFile": false, 4 | "supportFile": false 5 | } 6 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /cypress/integration/demo.spec.ts: -------------------------------------------------------------------------------- 1 | describe("Cypress", () => { 2 | it.only("is Cypress working?", () => { 3 | expect(true).to.equal(true); 4 | }); 5 | }); 6 | 7 | export {}; 8 | -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["es5", "dom"], 5 | "types": ["cypress", "node"] 6 | }, 7 | "include": ["**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const nextConfig = { 2 | webpack: (config, { isServer }) => { 3 | if (!isServer) { 4 | config.resolve.fallback.fs = false; 5 | } 6 | return config; 7 | } 8 | }; 9 | 10 | module.exports = nextConfig; 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iotex-dapp-sample", 3 | "version": "0.1.0", 4 | "license": "MIT", 5 | "description": "Starter pack using Nextjs, React, Typescript, tRPC, Mantine and Cypress", 6 | "keywords": [ 7 | "next", 8 | "react", 9 | "typescript" 10 | ], 11 | "scripts": { 12 | "dev": "next dev", 13 | "build": "next build", 14 | "export": "next build && next export", 15 | "start": "next start", 16 | "test": "concurrently \"yarn start \" \"cypress open\"", 17 | "test:ci": "cypress run --headless" 18 | }, 19 | "dependencies": { 20 | "@ethersproject/abstract-signer": "^5.6.0", 21 | "@ethersproject/contracts": "^5.6.0", 22 | "@ethersproject/providers": "^5.6.4", 23 | "@graphql-yoga/node": "^2.2.1", 24 | "@iotexproject/iotex-address-ts": "^1.0.2", 25 | "@ledgerhq/hw-transport": "^6.27.8", 26 | "@ledgerhq/hw-transport-webhid": "^6.27.8", 27 | "@ledgerhq/hw-transport-webusb": "^6.27.8", 28 | "@mantine/core": "^4.1.4", 29 | "@mantine/dates": "^4.1.4", 30 | "@mantine/dropzone": "^4.1.4", 31 | "@mantine/form": "^4.1.4", 32 | "@mantine/hooks": "^4.1.4", 33 | "@mantine/modals": "^4.1.4", 34 | "@mantine/next": "^4.1.4", 35 | "@mantine/notifications": "^4.1.4", 36 | "@mantine/prism": "^4.1.4", 37 | "@mantine/spotlight": "^4.1.4", 38 | "@metamask/jazzicon": "^2.0.0", 39 | "@smartgraph/core": "0.1.95", 40 | "@smartgraph/plugins": "0.2.2", 41 | "@tanstack/react-table": "^8.2.6", 42 | "@trpc/client": "^10.4.3", 43 | "@trpc/next": "^10.4.3", 44 | "@trpc/react": "^9.27.4", 45 | "@trpc/server": "^10.4.3", 46 | "@types/localforage": "^0.0.34", 47 | "@types/prop-types": "^15.7.5", 48 | "@types/reflect-metadata": "^0.1.0", 49 | "@types/uuid": "^8.3.4", 50 | "@web3-react/core": "^6.1.9", 51 | "@web3-react/injected-connector": "^6.0.7", 52 | "axios": "^0.21.1", 53 | "bignumber.js": "^9.0.1", 54 | "class-transformer": "^0.5.1", 55 | "clipboard-polyfill": "^3.0.3", 56 | "copy-to-clipboard": "^3.3.1", 57 | "dataloader": "^2.0.0", 58 | "dayjs": "^1.11.1", 59 | "ethcall": "^4.1.3", 60 | "ethereumjs-util": "^7.1.4", 61 | "ethers": "^5.1.3", 62 | "events": "^3.3.0", 63 | "graphql": "^16.3.0", 64 | "graphql-request": "^4.2.0", 65 | "i18next": "^21.8.4", 66 | "iotex-antenna": "^0.31.6", 67 | "js-sha3": "^0.8.0", 68 | "jsonwebtoken": "^8.5.1", 69 | "localforage": "^1.10.0", 70 | "localforage-driver-memory": "^1.0.5", 71 | "lodash": "^4.17.21", 72 | "mobx": "^6.2.0", 73 | "mobx-react-lite": "^3.2.0", 74 | "next": "^12.0.7", 75 | "nexus": "^1.3.0", 76 | "numeral": "^2.0.6", 77 | "promise-retry": "^2.0.1", 78 | "prop-types": "^15.8.1", 79 | "react": "^17.0.0", 80 | "react-dom": "^17.0.0", 81 | "react-hot-toast": "^1.0.2", 82 | "react-i18next": "^11.16.9", 83 | "react-query": "^3.34.19", 84 | "react-tiny-virtual-list": "^2.2.0", 85 | "reflect-metadata": "^0.1.13", 86 | "siwe": "^1.1.6", 87 | "sleep-promise": "^9.1.0", 88 | "superjson": "^1.8.1", 89 | "tabler-icons-react": "^1.45.0", 90 | "type-fest": "^1.2.0", 91 | "typed-emitter": "^1.3.1", 92 | "uuid": "^8.3.2", 93 | "wagmi": "^0.8.6", 94 | "zod": "^3.14.3" 95 | }, 96 | "devDependencies": { 97 | "@babel/core": ">=7.4.0 <8.0.0", 98 | "@babel/preset-env": ">=7.0.0 <8.0.0", 99 | "@cypress/react": "^4.16.4", 100 | "@tanstack/query-core": "4.19.0", 101 | "@tanstack/react-query": "^4.3.8", 102 | "@trpc/react-query": "^10.0.0-proxy-beta.21", 103 | "@types/node": "^14.14.31", 104 | "@types/react": "^17.0.0", 105 | "@types/react-dom": "^17.0.0", 106 | "@types/react-router-dom": "^5.1.7", 107 | "babel-loader": ">=8.0.2 <9.0.0", 108 | "concurrently": "^6.0.0", 109 | "csstype": "^3.0.10", 110 | "cypress": "^6.5.0", 111 | "ioredis": "^5.1.0", 112 | "react-native": "*", 113 | "tslib": ">=2.0.0 <3.0.0", 114 | "typescript": "^4.5.4" 115 | }, 116 | "resolutions": { 117 | "graphql": "^16.3.0" 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /public/images/account.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/images/bsc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /public/images/en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotexproject/iotex-dapp-sample/5191aaf9ccb93ca92819ee7398dd114a54f4bfdc/public/images/en.png -------------------------------------------------------------------------------- /public/images/enter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/eth.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/images/icon_history.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/images/imtoken.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/images/iopay.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /public/images/iotex.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/images/learn connect.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/images/mathwallet.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/images/metamask.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /public/images/polygon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/images/ru.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotexproject/iotex-dapp-sample/5191aaf9ccb93ca92819ee7398dd114a54f4bfdc/public/images/ru.png -------------------------------------------------------------------------------- /public/images/token.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/images/tokenpocket.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/images/trustwallet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/images/v2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotexproject/iotex-dapp-sample/5191aaf9ccb93ca92819ee7398dd114a54f4bfdc/public/images/v2.png -------------------------------------------------------------------------------- /public/images/walletConnect.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/zh_CN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iotexproject/iotex-dapp-sample/5191aaf9ccb93ca92819ee7398dd114a54f4bfdc/public/images/zh_CN.png -------------------------------------------------------------------------------- /src/components/Common/Copy/index.tsx: -------------------------------------------------------------------------------- 1 | import { helper, toast } from '@/lib/helper'; 2 | import { Tooltip, Text } from '@mantine/core'; 3 | import { observer, useLocalStore } from 'mobx-react-lite'; 4 | import { Copy as CopyIcon } from 'tabler-icons-react'; 5 | import * as clipboard from 'clipboard-polyfill/text'; 6 | import { from } from '@iotexproject/iotex-address-ts'; 7 | 8 | interface IProps { 9 | value: string; 10 | } 11 | export const Copy = observer(({ value }: IProps) => { 12 | const store = useLocalStore(() => ({ 13 | isIOTipOpen: false, 14 | toggleIOTipOpen(val: boolean) { 15 | this.isTipOpen = val; 16 | } 17 | })); 18 | 19 | return ( 20 | { 28 | let text: string; 29 | try { 30 | text = from(value)?.string(); 31 | } catch (error) { 32 | text = value; 33 | } 34 | const [error] = await helper.promise.runAsync(clipboard.writeText(text)); 35 | if (!error) { 36 | console.log('yes'); 37 | toast.success('Copied!'); 38 | } 39 | }} 40 | > 41 | 42 | 43 | ); 44 | }); 45 | -------------------------------------------------------------------------------- /src/components/Common/Table/index.tsx: -------------------------------------------------------------------------------- 1 | import { ColumnDef, flexRender, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, useReactTable } from '@tanstack/react-table'; 2 | import { observer } from 'mobx-react-lite'; 3 | import { Box, Center, Pagination, Table as MantineTable, TableProps } from '@mantine/core'; 4 | import { useEffect } from 'react'; 5 | //refer https://github.com/TanStack/table/blob/main/examples/react/basic/src/main.tsx 6 | 7 | interface IProps extends TableProps { 8 | data: any[]; 9 | columns: ColumnDef[]; 10 | showFooter?: boolean; 11 | showPagination?: boolean; 12 | } 13 | 14 | export const Table = observer(({ data, columns, showFooter, showPagination = true, ...TableProps }: IProps) => { 15 | const ReactTableProps = { 16 | data, 17 | columns, 18 | getCoreRowModel: getCoreRowModel(), 19 | getFilteredRowModel: getFilteredRowModel(), 20 | getPaginationRowModel: getPaginationRowModel(), 21 | debugTable: true 22 | }; 23 | !showPagination ? delete ReactTableProps.getPaginationRowModel : null; 24 | const table = useReactTable(ReactTableProps); 25 | 26 | useEffect(() => { 27 | showPagination ? table.setPageSize(5) : null; 28 | }, []); 29 | 30 | return ( 31 | <> 32 | 33 | 34 | 35 | {table.getHeaderGroups().map((headerGroup) => ( 36 | 37 | {headerGroup.headers.map((header) => ( 38 | {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} 39 | ))} 40 | 41 | ))} 42 | 43 | 44 | 45 | {table.getRowModel().rows.map((row) => ( 46 | 47 | {row.getVisibleCells().map((cell) => ( 48 | {flexRender(cell.column.columnDef.cell, cell.getContext())} 49 | ))} 50 | 51 | ))} 52 | 53 | 54 | {showFooter && ( 55 | 56 | {table.getFooterGroups().map((footerGroup) => ( 57 | 58 | {footerGroup.headers.map((header) => ( 59 | {header.isPlaceholder ? null : flexRender(header.column.columnDef.footer, header.getContext())} 60 | ))} 61 | 62 | ))} 63 | 64 | )} 65 | 66 | 67 |
68 | { 72 | table.setPageIndex(page - 1); 73 | }} 74 | /> 75 |
76 | 77 | ); 78 | }); 79 | -------------------------------------------------------------------------------- /src/components/HistoryModal/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer, useLocalObservable } from 'mobx-react-lite'; 3 | import { useStore } from '../../store/index'; 4 | import { useEffect } from 'react'; 5 | import dayjs from 'dayjs'; 6 | import { helper } from '@/lib/helper'; 7 | import { eventBus } from '@/lib/event'; 8 | import { metamaskUtils } from '@/lib/metaskUtils'; 9 | import { Box, Button, Group, Modal, Text, Tooltip, Image, Badge, Tabs, Stack, Center, Anchor } from '@mantine/core'; 10 | import Link from 'next/link'; 11 | import { useTranslation } from 'react-i18next'; 12 | import { ExternalLink } from 'tabler-icons-react'; 13 | import { ColumnDef } from '@tanstack/react-table'; 14 | import { TransactionItem } from '@/store/history'; 15 | import { Table } from '../Common/Table'; 16 | import { Copy } from '../Common/Copy'; 17 | 18 | export const TransactionResultModal = observer(() => { 19 | const { 20 | god, 21 | history, 22 | history: { curTransactionHistoryItem, transactionHistory } 23 | } = useStore(); 24 | 25 | const curNetwork = god.currentNetwork.chain.map[god.currentChain.chainId]; 26 | 27 | const registerToken = () => { 28 | metamaskUtils.registerToken( 29 | curTransactionHistoryItem.coin?.address, 30 | curTransactionHistoryItem.coin?.name, 31 | curTransactionHistoryItem.coin?.decimals ? curTransactionHistoryItem.coin?.decimals : 18, 32 | window.location.origin + curTransactionHistoryItem.coin?.logo 33 | ); 34 | }; 35 | return ( 36 | <> 37 | history.toggleTransactionDialog()} title="Transaction successfully"> 38 | {/* 39 | store.keyword.setValue(e.target.value)} /> 40 | */} 41 | 42 | Your {curNetwork?.name} TX hash 43 | {curTransactionHistoryItem?.hash} 44 | {/* */} 45 | 46 | 47 | 48 | {curTransactionHistoryItem?.coin && ( 49 | 57 | )} 58 | 61 | 62 | 63 | 64 | ); 65 | }); 66 | 67 | interface GlobalHistoryIconProps { 68 | [key: string]: any; 69 | } 70 | export const GlobalHistoryIcon = observer(({ ...restProps }: GlobalHistoryIconProps) => { 71 | const { 72 | history, 73 | history: { isOpen, transactionHistory } 74 | } = useStore(); 75 | 76 | const store = useLocalObservable(() => ({ 77 | unreadCount: 0, 78 | setCount() { 79 | store.unreadCount = history.transactionHistory.value.filter((item) => !item.isRead).length; 80 | } 81 | })); 82 | 83 | useEffect(() => { 84 | store.setCount(); 85 | }, [isOpen]); 86 | 87 | useEffect(() => { 88 | eventBus.on('history.insert', () => { 89 | store.setCount(); 90 | }); 91 | }, []); 92 | 93 | return ( 94 | <> 95 | 96 | { 99 | history.toggleOpen(); 100 | history.clearHitoryRead(); 101 | }} 102 | > 103 | 104 | {/* <> 105 | {store.unreadCount != 0 && ( 106 | // 120 | // {store.unreadCount} 121 | // 122 | )} 123 | */} 124 | 125 | 126 | 127 | ); 128 | }); 129 | 130 | export const HistoryModal = observer(() => { 131 | const { history, god } = useStore(); 132 | 133 | const columns: ColumnDef[] = [ 134 | { 135 | header: 'TITLE', 136 | accessorKey: 'title', 137 | cell: (v) => {v.getValue()} 138 | }, 139 | { 140 | header: 'TIMESTAMP', 141 | accessorKey: 'timestamp', 142 | cell: (v) => ( 143 | <> 144 | {dayjs(v.getValue() as any).format('DD/MM/YYYY')} 145 | {dayjs(v.getValue() as any).format('hh:mm:ss A')} 146 | 147 | ) 148 | }, 149 | { 150 | header: 'FROM', 151 | accessorKey: 'from', 152 | cell: (v) => {helper.address.formatAddress(v.getValue())} 153 | }, 154 | { 155 | header: 'TO', 156 | accessorKey: 'to', 157 | cell: (v) => {helper.address.formatAddress(v.getValue())} 158 | }, 159 | 160 | { 161 | header: 'HASH', 162 | accessorKey: 'hash', 163 | cell: ({ 164 | getValue, 165 | row: { 166 | original: { chainId } 167 | } 168 | }) => ( 169 | 170 | 171 | {helper.address.formatAddress(getValue())} 172 | 173 | 174 | 175 | 176 | ) 177 | // render: (value, record) => { 178 | // return ( 179 | // <> 180 | // {value ? ( 181 | // 182 | // {/* {record.status === 'loading' && } */} 183 | // {helper.address.formatAddress(value)} 184 | // {/* */} 185 | // 186 | // ) : ( 187 | // '-' 188 | // )} 189 | // 190 | // ); 191 | // } 192 | }, 193 | { 194 | header: 'AMOUNT', 195 | accessorKey: 'amount', 196 | cell: (v) => {v.getValue()} 197 | // render: (value, record) => { 198 | // return ( 199 | // <> 200 | // {value} 201 | // 202 | // ); 203 | // } 204 | }, 205 | { 206 | header: 'STATUS', 207 | accessorKey: 'status', 208 | cell: (v) => {v.getValue()} 209 | // render: (value, record) => { 210 | // return ( 211 | // <> 212 | // // 213 | // // {value == 'success' && } 214 | // // {value == 'loading' && } 215 | // // {value.toUpperCase()} 216 | // // 217 | // ); 218 | // } 219 | } 220 | ]; 221 | 222 | return ( 223 | { 227 | history.toggleOpen(); 228 | history.setFilterMoudle(history.moduleList[0]); 229 | }} 230 | title="History" 231 | > 232 | 233 | {/* 234 | 235 | {history.moduleList?.map((item, index) => { 236 | return ( 237 | { 242 | history.setFilterMoudle(item); 243 | history.setFilterFrom(''); 244 | history.setFilterTo(''); 245 | }} 246 | > 247 | {item.toUpperCase()} 248 | 249 | ); 250 | })} 251 | 252 | */} 253 |
254 | {/* 255 |
256 | history.setCurrentPage(page)} 261 | onSizeChange={(size) => history.setPageSize(size)} 262 | > 263 |
*/} 264 |
265 |
266 | ); 267 | }); 268 | 269 | export const TransactionSubmitDialog = observer(() => { 270 | const { t } = useTranslation(); 271 | const { god } = useStore(); 272 | 273 | return ( 274 | god.showTransactionSubmitDialog.setValue(false)} title={t('transaction-submitted')}> 275 |
276 | 277 | 278 | 279 | 280 | 281 |
282 | 283 |
({ 287 | color: '#42a2eb', 288 | cursor: 'pointer', 289 | ':hover': { 290 | textDecoration: 'underline' 291 | } 292 | })} 293 | > 294 | 295 | 296 | {t('view-on-explorername', { explorerName: god.currentChain.explorerName })} 297 | 298 | 299 | 300 |
301 |
302 | ); 303 | }); 304 | -------------------------------------------------------------------------------- /src/components/Jazzicon/hexstring-utils.ts: -------------------------------------------------------------------------------- 1 | import { isHexString, isValidAddress, isValidChecksumAddress, addHexPrefix, toChecksumAddress, zeroAddress } from "ethereumjs-util" 2 | 3 | export const BURN_ADDRESS = zeroAddress() 4 | 5 | export function isBurnAddress(address) { 6 | return address === BURN_ADDRESS 7 | } 8 | 9 | /** 10 | * Validates that the input is a hex address. This utility method is a thin 11 | * wrapper around ethereumjs-util.isValidAddress, with the exception that it 12 | * does not throw an error when provided values that are not hex strings. In 13 | * addition, and by default, this method will return true for hex strings that 14 | * meet the length requirement of a hex address, but are not prefixed with `0x` 15 | * Finally, if the mixedCaseUseChecksum flag is true and a mixed case string is 16 | * provided this method will validate it has the proper checksum formatting. 17 | * @param {string} possibleAddress - Input parameter to check against 18 | * @param {Object} [options] - options bag 19 | * @param {boolean} [options.allowNonPrefixed] - If true will first ensure '0x' 20 | * is prepended to the string 21 | * @param {boolean} [options.mixedCaseUseChecksum] - If true will treat mixed 22 | * case addresses as checksum addresses and validate that proper checksum 23 | * format is used 24 | * @returns {boolean} whether or not the input is a valid hex address 25 | */ 26 | export function isValidHexAddress(possibleAddress, { allowNonPrefixed = true, mixedCaseUseChecksum = false } = {}) { 27 | const addressToCheck = allowNonPrefixed ? addHexPrefix(possibleAddress) : possibleAddress 28 | if (!isHexString(addressToCheck)) { 29 | return false 30 | } 31 | 32 | if (mixedCaseUseChecksum) { 33 | const prefixRemoved = addressToCheck.slice(2) 34 | const lower = prefixRemoved.toLowerCase() 35 | const upper = prefixRemoved.toUpperCase() 36 | const allOneCase = prefixRemoved === lower || prefixRemoved === upper 37 | if (!allOneCase) { 38 | return isValidChecksumAddress(addressToCheck) 39 | } 40 | } 41 | 42 | return isValidAddress(addressToCheck) 43 | } 44 | 45 | export function toChecksumHexAddress(address) { 46 | if (!address) { 47 | // our internal checksumAddress function that this method replaces would 48 | // return an empty string for nullish input. If any direct usages of 49 | // ethereumjs-util.toChecksumAddress were called with nullish input it 50 | // would have resulted in an error on version 5.1. 51 | return "" 52 | } 53 | const hexPrefixed = addHexPrefix(address) 54 | if (!isHexString(hexPrefixed)) { 55 | // Version 5.1 of ethereumjs-utils would have returned '0xY' for input 'y' 56 | // but we shouldn't waste effort trying to change case on a clearly invalid 57 | // string. Instead just return the hex prefixed original string which most 58 | // closely mimics the original behavior. 59 | return hexPrefixed 60 | } 61 | return toChecksumAddress(addHexPrefix(address)) 62 | } 63 | -------------------------------------------------------------------------------- /src/components/Jazzicon/icon-factory.ts: -------------------------------------------------------------------------------- 1 | import { isValidHexAddress, toChecksumHexAddress } from "./hexstring-utils" 2 | 3 | let iconFactory 4 | 5 | export default function iconFactoryGenerator(jazzicon) { 6 | if (!iconFactory) { 7 | iconFactory = new IconFactory(jazzicon) 8 | } 9 | return iconFactory 10 | } 11 | 12 | function IconFactory(jazzicon) { 13 | this.jazzicon = jazzicon 14 | this.cache = {} 15 | } 16 | 17 | IconFactory.prototype.iconForAddress = function (address, diameter, useTokenDetection, tokenList) { 18 | // When useTokenDetection flag is true the tokenList contains tokens with non-checksum address from the dynamic token service api, 19 | // When useTokenDetection flag is false the tokenList contains tokens with checksum addresses from contract-metadata. 20 | // So the flag indicates whether the address of tokens currently on the tokenList is checksum or not. 21 | const addr = useTokenDetection ? address : toChecksumHexAddress(address) 22 | if (iconExistsFor(addr, tokenList)) { 23 | return imageElFor(addr, useTokenDetection, tokenList) 24 | } 25 | 26 | return this.generateIdenticonSvg(address, diameter) 27 | } 28 | 29 | // returns svg dom element 30 | IconFactory.prototype.generateIdenticonSvg = function (address, diameter) { 31 | const cacheId = `${address}:${diameter}` 32 | // check cache, lazily generate and populate cache 33 | const identicon = this.cache[cacheId] || (this.cache[cacheId] = this.generateNewIdenticon(address, diameter)) 34 | // create a clean copy so you can modify it 35 | const cleanCopy = identicon.cloneNode(true) 36 | return cleanCopy 37 | } 38 | 39 | // creates a new identicon 40 | IconFactory.prototype.generateNewIdenticon = function (address, diameter) { 41 | const numericRepresentation = jsNumberForAddress(address) 42 | const identicon = this.jazzicon(diameter, numericRepresentation) 43 | return identicon 44 | } 45 | 46 | // util 47 | 48 | function iconExistsFor(address, tokenList) { 49 | return tokenList[address] && isValidHexAddress(address, { allowNonPrefixed: false }) && tokenList[address].iconUrl 50 | } 51 | 52 | function imageElFor(address, useTokenDetection, tokenList) { 53 | const tokenMetadata = tokenList[address] 54 | const fileName = tokenMetadata?.iconUrl 55 | // token from dynamic api list is fetched when useTokenDetection is true 56 | // In the static list, the iconUrl will be holding only a filename for the image, 57 | // the corresponding images will be available in the `images/contract/` location when the contract-metadata package was added to the extension 58 | // so that it can be accessed using the filename in iconUrl. 59 | const path = useTokenDetection ? fileName : `images/contract/${fileName}` 60 | const img = document.createElement("img") 61 | img.src = path 62 | img.style.width = "100%" 63 | return img 64 | } 65 | 66 | function jsNumberForAddress(address) { 67 | const addr = address.slice(2, 10) 68 | const seed = parseInt(addr, 16) 69 | return seed 70 | } 71 | -------------------------------------------------------------------------------- /src/components/Jazzicon/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { createRef, PureComponent } from "react" 2 | import PropTypes from "prop-types" 3 | import jazzicon from "@metamask/jazzicon" 4 | import iconFactoryGenerator from "./icon-factory" 5 | 6 | const iconFactory = iconFactoryGenerator(jazzicon) 7 | 8 | /** 9 | * Wrapper around the jazzicon library to return a React component, as the library returns an 10 | * HTMLDivElement which needs to be appended. 11 | */ 12 | export default class Jazzicon extends PureComponent { 13 | static propTypes = { 14 | address: PropTypes.string.isRequired, 15 | className: PropTypes.string, 16 | diameter: PropTypes.number, 17 | style: PropTypes.object, 18 | useTokenDetection: PropTypes.bool, 19 | } 20 | 21 | static defaultProps = { 22 | diameter: 46, 23 | } 24 | 25 | container: React.RefObject = createRef() 26 | 27 | componentDidMount() { 28 | this.appendJazzicon() 29 | } 30 | 31 | componentDidUpdate(prevProps) { 32 | const { address: prevAddress, diameter: prevDiameter } = prevProps 33 | const { address, diameter } = this.props as any 34 | 35 | if (address !== prevAddress || diameter !== prevDiameter) { 36 | this.removeExistingChildren() 37 | this.appendJazzicon() 38 | } 39 | } 40 | 41 | removeExistingChildren() { 42 | const { children } = this.container.current 43 | for (let i = 0; i < children.length; i++) { 44 | this.container.current.removeChild(children[i]) 45 | } 46 | } 47 | 48 | appendJazzicon() { 49 | const { address, diameter, useTokenDetection } = this.props as any 50 | const image = iconFactory.iconForAddress(address, diameter, useTokenDetection, [address]) 51 | this.container.current.appendChild(image) 52 | } 53 | 54 | render() { 55 | const { className, style } = this.props as any 56 | return
57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/components/LanguageSwitch.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useLocalObservable, observer } from 'mobx-react-lite'; 3 | import { Box, Button, createStyles, Menu } from '@mantine/core'; 4 | import { LanguageHiragana } from 'tabler-icons-react'; 5 | import { useStore } from '../store/index'; 6 | import { useTranslation } from 'react-i18next'; 7 | 8 | const BREAKPOINT = '@media (max-width: 755px)'; 9 | const useStyles = createStyles((theme) => ({ 10 | control: { 11 | height: 54, 12 | paddingLeft: 38, 13 | paddingRight: 38, 14 | 15 | [BREAKPOINT]: { 16 | height: 54, 17 | paddingLeft: 18, 18 | paddingRight: 18, 19 | flex: 1 20 | } 21 | } 22 | })); 23 | 24 | export const LanguageSwitch = observer(() => { 25 | const { classes, cx } = useStyles(); 26 | const { lang } = useStore(); 27 | const { t } = useTranslation(); 28 | const langList = [ 29 | { 30 | label: '简体中文', 31 | value: 'zh_CN' 32 | }, 33 | { 34 | label: 'English', 35 | value: 'en' 36 | }, 37 | { 38 | label: '日本語', 39 | value: 'ja' 40 | }, 41 | { 42 | label: '繁體中文', 43 | value: 'zh_TW' 44 | }, 45 | { 46 | label: '한국어', 47 | value: 'ko' 48 | }, 49 | { 50 | label: 'Deutsch', 51 | value: 'de' 52 | }, 53 | { 54 | label: 'Español', 55 | value: 'es' 56 | }, 57 | { 58 | label: 'русск', 59 | value: 'ru' 60 | } 61 | ]; 62 | return ( 63 | 64 | }> 70 | {t('language')} 71 | 72 | } 73 | > 74 | {langList.map((v) => ( 75 | { 79 | // console.log(e.target.innerText) 80 | //@ts-ignore 81 | let index = langList.findIndex((item) => item.label === e.target.innerText); 82 | console.log(langList[index].value); 83 | lang.setLang(langList[index].value); 84 | }} 85 | > 86 | {v.label} 87 | 88 | ))} 89 | 90 | 91 | ); 92 | }); 93 | -------------------------------------------------------------------------------- /src/components/Layout/HeaderLeft.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { createStyles, Navbar, Group, Box, TextInput, Code, Space, ThemeIcon, Text, MediaQuery } from '@mantine/core'; 3 | import { Home, Code as CodeIcon, Search, Photo, LayersLinked } from 'tabler-icons-react'; 4 | import { useStore } from '../../store/index'; 5 | import { observer } from 'mobx-react-lite'; 6 | import Link from 'next/link'; 7 | import { SwitchThemeToggle } from './SwitchTheme'; 8 | import { openSpotlight } from '@mantine/spotlight'; 9 | import { User } from './User'; 10 | import { WalletInfo } from '../WalletInfo'; 11 | import { useRouter } from 'next/router'; 12 | import { useTranslation } from 'react-i18next'; 13 | import { Links } from './Links'; 14 | import HeaderTop from './HeaderTop'; 15 | import { UserStore } from '@/store/user'; 16 | import { useMediaQuery } from '@mantine/hooks'; 17 | const useStyles = createStyles((theme, _params, getRef) => { 18 | const icon = getRef('icon'); 19 | return { 20 | header: { 21 | // paddingBottom: theme.spacing.md, 22 | // marginBottom: theme.spacing.md 23 | // borderBottom: `1px solid ${theme.colorScheme === 'dark' ? theme.colors.dark[4] : theme.colors.gray[2]}` 24 | }, 25 | 26 | footer: { 27 | paddingTop: theme.spacing.md, 28 | marginTop: theme.spacing.md, 29 | borderTop: `1px solid ${theme.colorScheme === 'dark' ? theme.colors.dark[4] : theme.colors.gray[2]}` 30 | }, 31 | 32 | link: { 33 | ...theme.fn.focusStyles(), 34 | display: 'flex', 35 | alignItems: 'center', 36 | textDecoration: 'none', 37 | fontSize: theme.fontSizes.sm, 38 | color: theme.colorScheme === 'dark' ? theme.colors.dark[1] : theme.colors.gray[7], 39 | padding: `${theme.spacing.xs}px ${theme.spacing.sm}px`, 40 | borderRadius: theme.radius.sm, 41 | fontWeight: 500, 42 | 43 | '&:hover': { 44 | backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[0], 45 | color: theme.colorScheme === 'dark' ? theme.white : theme.black, 46 | 47 | [`& .${icon}`]: { 48 | color: theme.colorScheme === 'dark' ? theme.white : theme.black 49 | } 50 | } 51 | }, 52 | 53 | searchCode: { 54 | fontWeight: 700, 55 | fontSize: 10, 56 | backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[7] : theme.colors.gray[0], 57 | border: `1px solid ${theme.colorScheme === 'dark' ? theme.colors.dark[7] : theme.colors.gray[2]}` 58 | }, 59 | 60 | linkIcon: { 61 | ref: icon, 62 | color: theme.colorScheme === 'dark' ? theme.colors.dark[2] : theme.colors.gray[6], 63 | marginRight: theme.spacing.sm 64 | }, 65 | 66 | linkActive: { 67 | '&, &:hover': { 68 | backgroundColor: theme.colorScheme === 'dark' ? theme.fn.rgba(theme.colors[theme.primaryColor][8], 0.25) : theme.colors[theme.primaryColor][0], 69 | color: theme.colorScheme === 'dark' ? theme.white : theme.colors[theme.primaryColor][7], 70 | [`& .${icon}`]: { 71 | color: theme.colors[theme.primaryColor][theme.colorScheme === 'dark' ? 5 : 7] 72 | } 73 | } 74 | } 75 | }; 76 | }); 77 | 78 | export const Logo = () => { 79 | const { classes, cx } = useStyles(); 80 | return ( 81 | 82 | 83 | 84 | 85 | 86 | 87 | IoTeX Dapp V3 88 | 89 | 90 | 91 | ); 92 | }; 93 | 94 | export const HeaderLeft = observer(() => { 95 | const { classes, cx } = useStyles(); 96 | const { t } = useTranslation(); 97 | const { user, god } = useStore(); 98 | const isPC = useMediaQuery('(min-width: 768px)'); 99 | 100 | return ( 101 | 123 | ); 124 | }); 125 | -------------------------------------------------------------------------------- /src/components/Layout/HeaderTop.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer } from 'mobx-react-lite'; 3 | import { Burger, Container, createStyles, Group, Header, MediaQuery, Text } from '@mantine/core'; 4 | import { WalletInfo } from '../WalletInfo'; 5 | import { useStore } from '@/store/index'; 6 | import { HeaderLeft, Logo } from './HeaderLeft'; 7 | import { Links } from './Links'; 8 | import { User } from './User'; 9 | import { UserStore } from '@/store/user'; 10 | import { GlobalHistoryIcon, HistoryModal, TransactionResultModal } from '../HistoryModal'; 11 | 12 | const useStyles = createStyles((theme) => ({ 13 | inner: { 14 | height: 56, 15 | display: 'flex', 16 | justifyContent: 'space-between', 17 | alignItems: 'center' 18 | }, 19 | 20 | links: { 21 | [theme.fn.smallerThan('sm')]: { 22 | display: 'none' 23 | } 24 | }, 25 | 26 | burger: { 27 | [theme.fn.largerThan('sm')]: { 28 | display: 'none' 29 | } 30 | } 31 | })); 32 | 33 | export const HeaderTop = observer(() => { 34 | const { classes } = useStyles(); 35 | const { user } = useStore(); 36 | const isShow = (user as UserStore).layout.navbarMode === 'top' ? {} : { largerThan: 'sm' }; 37 | return ( 38 | //@ts-ignore 39 | 40 |
41 | {/* @ts-ignore */} 42 | 43 |
44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | user.layout.sidebarOpen.setValue(!user.layout.sidebarOpen.value)} className={classes.burger} size="sm" /> 53 |
54 |
55 | 56 |
57 |
58 | ); 59 | }); 60 | 61 | export default HeaderTop; 62 | -------------------------------------------------------------------------------- /src/components/Layout/Links.tsx: -------------------------------------------------------------------------------- 1 | import { observer } from 'mobx-react-lite'; 2 | //@ts-ignore 3 | import { Box, Center, createStyles, Menu, Link } from '@mantine/core'; 4 | import { useRouter } from 'next/router'; 5 | import { useStore } from '@/store/index'; 6 | import { useTranslation } from 'react-i18next'; 7 | import { ChevronDown } from 'tabler-icons-react'; 8 | 9 | const useStyles = createStyles((theme, _params, getRef) => { 10 | const icon = getRef('icon'); 11 | return { 12 | link: { 13 | ...theme.fn.focusStyles(), 14 | display: 'flex', 15 | alignItems: 'center', 16 | textDecoration: 'none', 17 | fontSize: theme.fontSizes.sm, 18 | color: theme.colorScheme === 'dark' ? theme.colors.dark[1] : theme.colors.gray[7], 19 | padding: `${theme.spacing.xs}px ${theme.spacing.sm}px`, 20 | borderRadius: theme.radius.sm, 21 | fontWeight: 500, 22 | 23 | '&:hover': { 24 | backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[0], 25 | color: theme.colorScheme === 'dark' ? theme.white : theme.black, 26 | 27 | [`& .${icon}`]: { 28 | color: theme.colorScheme === 'dark' ? theme.white : theme.black 29 | } 30 | } 31 | }, 32 | 33 | linkIcon: { 34 | ref: icon, 35 | color: theme.colorScheme === 'dark' ? theme.colors.dark[2] : theme.colors.gray[6], 36 | marginRight: theme.spacing.sm 37 | }, 38 | 39 | linkActive: { 40 | '&, &:hover': { 41 | backgroundColor: theme.colorScheme === 'dark' ? theme.fn.rgba(theme.colors[theme.primaryColor][8], 0.25) : theme.colors[theme.primaryColor][0], 42 | color: theme.colorScheme === 'dark' ? theme.white : theme.colors[theme.primaryColor][7], 43 | [`& .${icon}`]: { 44 | color: theme.colors[theme.primaryColor][theme.colorScheme === 'dark' ? 5 : 7] 45 | } 46 | } 47 | } 48 | }; 49 | }); 50 | 51 | export const Links = observer(() => { 52 | const { classes, cx } = useStyles(); 53 | const { t } = useTranslation(); 54 | const { user, god } = useStore(); 55 | const router = useRouter(); 56 | 57 | return ( 58 | <> 59 | {user.layout?.router?.map((item) => ( 60 | { 64 | if (item.link) { 65 | if (item.__blank) { 66 | window.open(item.link, '_blank'); 67 | } else { 68 | router.push(item.link); 69 | } 70 | } 71 | }} 72 | > 73 | 74 | {t(item.label)} 75 | 76 | ))} 77 | 78 | ); 79 | }); 80 | -------------------------------------------------------------------------------- /src/components/Layout/SwitchTheme.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createStyles, Switch, Group, useMantineColorScheme } from '@mantine/core'; 3 | import { Sun, MoonStars } from 'tabler-icons-react'; 4 | import { observe } from 'mobx'; 5 | import { observer } from 'mobx-react-lite'; 6 | import { useStore } from '../../store/index'; 7 | 8 | const useStyles = createStyles((theme) => ({ 9 | root: { 10 | position: 'relative', 11 | '& *': { 12 | cursor: 'pointer' 13 | } 14 | }, 15 | 16 | icon: { 17 | pointerEvents: 'none', 18 | position: 'absolute', 19 | zIndex: 1, 20 | top: 3 21 | }, 22 | 23 | iconLight: { 24 | left: 4, 25 | color: theme.white 26 | }, 27 | 28 | iconDark: { 29 | right: 4, 30 | color: theme.colors.gray[6] 31 | } 32 | })); 33 | 34 | export const SwitchThemeToggle = observer(() => { 35 | const { user } = useStore(); 36 | const { classes, cx } = useStyles(); 37 | 38 | return ( 39 | 40 |
41 | 42 | 43 | user.toggleTheme()} size="md" /> 44 |
45 |
46 | ); 47 | }); 48 | -------------------------------------------------------------------------------- /src/components/Layout/User.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { UnstyledButton, Group, Text, createStyles, Box, Image, ThemeIcon, Avatar, useMantineTheme } from '@mantine/core'; 3 | import { ChevronRight, Search } from 'tabler-icons-react'; 4 | import Jazzicon from '../Jazzicon'; 5 | import { observer, useLocalObservable } from 'mobx-react-lite'; 6 | import { useStore } from '../../store/index'; 7 | import { helper } from '../../lib/helper'; 8 | import { SwitchThemeToggle } from './SwitchTheme'; 9 | import { useRouter } from 'next/router'; 10 | 11 | const useStyles = createStyles((theme) => ({ 12 | user: { 13 | display: 'block', 14 | width: 'fit-content', 15 | margin: '0', 16 | padding: '0', 17 | borderRadius: '10px', 18 | color: theme.colorScheme === 'dark' ? theme.colors.dark[0] : theme.black, 19 | '&:hover': { 20 | backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[8] : theme.colors.gray[0] 21 | } 22 | } 23 | })); 24 | 25 | export const User = observer(({ type }: { type: 'HeaderLeft' | 'HeaderTop' }) => { 26 | const { god, user } = useStore(); 27 | const { classes } = useStyles(); 28 | const theme = useMantineTheme(); 29 | const { pathname } = useRouter(); 30 | const store = useLocalObservable(() => ({ 31 | showConnecter() { 32 | god.setShowConnecter(true); 33 | }, 34 | showWalletInfo() { 35 | god.currentNetwork.walletInfo.visible = true; 36 | }, 37 | currentAvatar: 1 38 | })); 39 | return ( 40 | 41 | {god.currentNetwork.account ? ( 42 | 43 | 44 | 45 |
46 | 47 | {helper.string.truncate(god.currentNetwork.account || '0x......', 12, '...')} 48 | 49 | 50 | {god.currentChain.Coin.balance.format} 51 | {god.currentChain.Coin.symbol} 52 | 53 |
54 |
55 |
56 | ) : ( 57 | 66 | 67 | 68 | Connect Wallet 69 | 70 | 71 | 72 | )} 73 | 74 | {user.wetherWrongnetwork(pathname) ? ( 75 | ({ 77 | background: theme.colorScheme == 'dark' ? '#552323' : '#ffe2e2', 78 | flexWrap: 'nowrap', 79 | borderRadius: '6px' 80 | })} 81 | spacing={10} 82 | mr={10} 83 | p="xs" 84 | onClick={() => user.networkChecker.isWrongNetworkDialogOpen.setValue(true)} 85 | > 86 | ({ 88 | color: theme.colorScheme == 'dark' ? 'white' : '#e53b3b', 89 | fontWeight: 'bold' 90 | })} 91 | size="sm" 92 | style={{ whiteSpace: 'nowrap' }} 93 | > 94 | Wrong network 95 | 96 | 97 | ) : ( 98 | 99 | 100 | {god.currentNetwork.currentChain.name} 101 | 102 | )} 103 | 104 | 105 | 106 |
107 | ); 108 | }); 109 | -------------------------------------------------------------------------------- /src/components/Layout/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { AppShell, useMantineTheme } from '@mantine/core'; 3 | import { useStore } from '../../store/index'; 4 | import { Sun, MoonStars, Search } from 'tabler-icons-react'; 5 | import { observer } from 'mobx-react-lite'; 6 | import { SpotlightProvider } from '@mantine/spotlight'; 7 | import HeaderTop from '@/components/Layout/HeaderTop'; 8 | import { HeaderLeft } from './HeaderLeft'; 9 | import { UserStore } from '@/store/user'; 10 | import { useMediaQuery } from '@mantine/hooks'; 11 | 12 | export const MainLayout = observer(({ children }: { children?: any }) => { 13 | const theme = useMantineTheme(); 14 | const { user } = useStore(); 15 | const navbarMode = (user as UserStore).layout.navbarMode; 16 | const isPC = useMediaQuery('(min-width: 768px)'); 17 | const navbar = isPC ? (navbarMode === 'left' ? { navbar: } : null) : { navbar: }; 18 | const header = isPC ? (navbarMode === 'top' ? { header: } : null) : { header: }; 19 | 20 | return ( 21 | } searchPlaceholder="Search..." shortcut="mod + k" nothingFoundMessage="Nothing found..." highlightQuery> 22 | 35 | {children} 36 | 37 | 38 | ); 39 | }); 40 | 41 | export default MainLayout; 42 | -------------------------------------------------------------------------------- /src/components/NetworkCheckProvider/index.tsx: -------------------------------------------------------------------------------- 1 | import { helper } from '@/lib/helper'; 2 | import { useStore } from '@/store/index'; 3 | import { ChainState } from '@/store/lib/ChainState'; 4 | import { Text, Modal, Group, Avatar, Box, Grid, Indicator, Center, Stack, Button } from '@mantine/core'; 5 | import _ from 'lodash'; 6 | import { observer, useLocalObservable } from 'mobx-react-lite'; 7 | import { useRouter } from 'next/router'; 8 | import React, { useEffect } from 'react'; 9 | import { AlertCircle } from 'tabler-icons-react'; 10 | 11 | interface NetworkCheckDialog { 12 | [key: string]: any; 13 | } 14 | // [4690, 1, 56, 97] 15 | export const WrongNetworkDialog = observer(({ ...restProps }: NetworkCheckDialog) => { 16 | const { 17 | user, 18 | user: { networkChecker }, 19 | god 20 | } = useStore(); 21 | const router = useRouter(); 22 | const supportChainId = networkChecker?.supportChainIds[router.pathname] || []; 23 | 24 | useEffect(() => { 25 | console.log(user?.wetherWrongnetwork(router.pathname), router.pathname); 26 | if (user?.wetherWrongnetwork(router.pathname)) { 27 | user.setWrongNetworkDialog(true); 28 | } 29 | }, [god.updateTicker.value]); 30 | 31 | const getSupportChainArray = (): ChainState[] => { 32 | return _.filter(god.network.chain.map, (v) => supportChainId.some((i) => i === v.chainId)); 33 | }; 34 | 35 | return ( 36 | <> 37 | user.setWrongNetworkDialog(false)} 41 | title={ 42 | 43 | 44 | WRONG NETWORK 45 | 46 | } 47 | > 48 | 49 | Your current network is 50 | 51 | {god.currentChain.name} 52 | {' '} 53 | . Please connect to a supported network below or in your wallet. 54 | 55 | 56 | 57 | {/* getSupportChainArray*/} 58 | {getSupportChainArray().map((i) => ( 59 | ({ 61 | padding: '10px', 62 | cursor: 'pointer', 63 | '&:hover': { 64 | borderRadius: '10px', 65 | transition: 'all .3s', 66 | backgroundColor: theme.colorScheme === 'dark' ? theme.colors.gray[9] : theme.colors.gray[1] 67 | } 68 | })} 69 | > 70 | 71 | 72 | { 76 | helper.setChain(god, i.chainId).then((res) => { 77 | user.setWrongNetworkDialog(false); 78 | }); 79 | }} 80 | > 81 | 82 | 83 | 84 | 85 | {i.name} 86 | 87 | 88 | ))} 89 | 90 | 91 | 92 | (Click the top button to switch the chain) 93 | 94 | 97 | 98 | 99 | ); 100 | }); 101 | -------------------------------------------------------------------------------- /src/components/Template.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useLocalObservable, observer } from 'mobx-react-lite'; 3 | import { Box, Button } from '@mantine/core'; 4 | 5 | interface Props {} 6 | 7 | export const Template = observer((props: Props) => { 8 | const store = useLocalObservable(() => ({ 9 | count: 0, 10 | setCount(count) { 11 | this.count = count; 12 | } 13 | })); 14 | return ( 15 | 16 | Template: {store.count} 17 | 18 | 19 | 20 | ); 21 | }); 22 | -------------------------------------------------------------------------------- /src/components/WagmiProvider/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | WagmiConfig, 3 | createClient, 4 | useProvider, 5 | configureChains, 6 | useSigner, 7 | chain, 8 | Chain, 9 | useAccount, 10 | useConnect, 11 | useDisconnect, 12 | useEnsAvatar, 13 | useEnsName, 14 | useNetwork, 15 | useSignMessage, 16 | } from 'wagmi'; 17 | 18 | import { alchemyProvider } from 'wagmi/providers/alchemy'; 19 | import { publicProvider } from 'wagmi/providers/public'; 20 | import { InjectedConnector } from 'wagmi/connectors/injected'; 21 | import { observer, useLocalObservable, useLocalStore } from 'mobx-react-lite'; 22 | import { useStore } from '@/store/index'; 23 | import { useEffect } from 'react'; 24 | import { showNotification, updateNotification } from '@mantine/notifications'; 25 | import { eventBus } from '../../lib/event'; 26 | import axios from 'axios'; 27 | import { SiweMessage } from 'siwe'; 28 | import { hooks } from '@/lib/hooks'; 29 | import { helper } from '@/lib/helper'; 30 | 31 | const createSiweMessage = async (address: string, chainId: number) => { 32 | const res = await axios.get(`/api/auth/nonce`); 33 | const message = new SiweMessage({ 34 | address, 35 | chainId, 36 | statement: 'Sign in with Ethereum to the app.', 37 | domain: window.location.host, 38 | uri: window.location.origin, 39 | version: '1', 40 | nonce: res.data.nonce, 41 | }); 42 | return message.prepareMessage(); 43 | }; 44 | 45 | export const WagmiProvider = observer(({ children }) => { 46 | const { god } = useStore(); 47 | 48 | let godChains: Chain[] = []; 49 | 50 | for (let key in god.network.chain.map) { 51 | let chain: Chain = { 52 | id: god.network.chain.map[key].chainId, 53 | name: god.network.chain.map[key].name, 54 | rpcUrls: { 55 | default: god.network.chain.map[key].rpcUrl, 56 | }, 57 | network: god.network.chain.map[key].name, 58 | }; 59 | godChains.push(chain); 60 | } 61 | 62 | const { chains, provider, webSocketProvider } = configureChains(godChains, [ 63 | publicProvider(), 64 | ]); 65 | console.log('chains', chains); 66 | 67 | const client = createClient({ 68 | autoConnect: true, 69 | connectors: [ 70 | new InjectedConnector({ 71 | chains, 72 | options: { 73 | name: 'Injected', 74 | //auto choose other account = false 75 | shimDisconnect: false, 76 | }, 77 | }), 78 | 79 | // new MetaMaskConnector({ chains }), 80 | // new CoinbaseWalletConnector({ 81 | // chains, 82 | // options: { 83 | // appName: 'wagmi' 84 | // } 85 | // }), 86 | // new WalletConnectConnector({ 87 | // chains, 88 | // options: { 89 | // qrcode: true 90 | // } 91 | // }), 92 | ], 93 | provider, 94 | webSocketProvider, 95 | }); 96 | 97 | return ( 98 | 99 | 100 | {children} 101 | 102 | ); 103 | }); 104 | 105 | const Wallet = observer(() => { 106 | const { god, user } = useStore(); 107 | const { chain } = useNetwork(); 108 | const { address, connector, isConnected } = useAccount(); 109 | // const provider = useProvider(); 110 | // const { data: signer } = useSigner(); 111 | // const { data: ensAvatar } = useEnsAvatar({ address }); 112 | // const { data: ensName } = useEnsName({ address }); 113 | const { connect, connectors, error, isLoading, pendingConnector } = 114 | useConnect(); 115 | const { disconnect } = useDisconnect(); 116 | const { signMessageAsync } = useSignMessage(); 117 | 118 | const store = useLocalStore(() => ({ 119 | logout() { 120 | console.log('logggout'); 121 | disconnect(); 122 | god.currentNetwork.set({ account: '' }); 123 | god.eth.connector.latestProvider.clear(); 124 | store.clearToken(); 125 | }, 126 | async login() { 127 | showNotification({ 128 | id: 'login', 129 | title: 'Login', 130 | loading: true, 131 | message: 'Please confirm the login request in your wallet.', 132 | color: 'yellow', 133 | autoClose: false, 134 | }); 135 | try { 136 | const address = god.currentNetwork.account; 137 | const chainId = god.currentNetwork.currentChain.chainId; 138 | const message = await createSiweMessage(address, chainId); 139 | const signature = await signMessageAsync({ 140 | message, 141 | }); 142 | updateNotification({ 143 | id: 'login', 144 | title: 'Login', 145 | loading: false, 146 | message: 'Login successful.', 147 | color: 'green', 148 | autoClose: 1000, 149 | }); 150 | if (!user.token.value) { 151 | const tokenRes = await axios.post(`/api/auth/jwt`, { 152 | message, 153 | signature, 154 | }); 155 | if (tokenRes.data) { 156 | user.token.save(tokenRes.data.token); 157 | user.tokenAddress.save(address); 158 | eventBus.emit('wallet.onToken'); 159 | } 160 | } 161 | } catch (error) { 162 | updateNotification({ 163 | id: 'login', 164 | title: 'Login', 165 | loading: false, 166 | message: 'Login failed', 167 | color: 'red', 168 | autoClose: 1000, 169 | }); 170 | console.error('[handleLogin]', error); 171 | } 172 | }, 173 | onAccount() { 174 | const account = god.currentNetwork.account.toLowerCase(); 175 | const tokenAddress = user.tokenAddress.value 176 | ? user.tokenAddress.value.toLowerCase() 177 | : ''; 178 | if (tokenAddress !== account) { 179 | store.clearToken(); 180 | eventBus.emit('wallet.login'); 181 | } 182 | }, 183 | clearToken() { 184 | user.token.clear(); 185 | user.token.value = undefined; 186 | user.tokenAddress.clear(); 187 | user.tokenAddress.value = undefined; 188 | }, 189 | })); 190 | 191 | useEffect(() => { 192 | if (error) { 193 | if (error.message.includes('Connector already connected')) return; 194 | showNotification({ 195 | title: 'Error', 196 | message: error.message, 197 | color: 'red', 198 | }); 199 | } 200 | if (connector) { 201 | connector.getChainId().then((chainId) => { 202 | // console.log('chain.switch', chainId); 203 | if (god.currentNetwork.allowChains.includes(chainId)) { 204 | god.setChainId(chainId); 205 | connector.getSigner().then((signer) => { 206 | if ( 207 | !god.currentNetwork.account || 208 | god.currentNetwork.account == address 209 | ) { 210 | god.currentNetwork.set({ 211 | account: address, 212 | signer, 213 | }); 214 | eventBus.emit('wallet.onAccount'); 215 | } 216 | god.currentNetwork.loadBalance(); 217 | }); 218 | } 219 | }); 220 | } 221 | if (isConnected) { 222 | god.setShowConnecter(false); 223 | } 224 | }, [isConnected, error, connector, chain]); 225 | 226 | useEffect(() => { 227 | //@ts-ignore 228 | const { ethereum } = window; 229 | if (ethereum && ethereum.on) { 230 | const handleChainChanged = () => { 231 | connect({ connector: connectors[0] }); 232 | god.currentNetwork.loadBalance(); 233 | }; 234 | const handleAccountsChanged = (accounts: string[]) => { 235 | if (accounts.length > 0) { 236 | god.currentNetwork.set({ 237 | account: helper.address.toUppercase(accounts[0]), 238 | }); 239 | god.currentNetwork.loadBalance(); 240 | } 241 | eventBus.emit('wallet.onAccount'); 242 | }; 243 | ethereum.on('networkChanged', handleChainChanged); 244 | ethereum.on('close', handleChainChanged); 245 | ethereum.on('chainChanged', handleChainChanged); 246 | ethereum.on('accountsChanged', handleAccountsChanged); 247 | return () => { 248 | if (ethereum.removeListener) { 249 | ethereum.removeListener('networkChanged', handleChainChanged); 250 | ethereum.removeListener('close', handleChainChanged); 251 | ethereum.removeListener('chainChanged', handleChainChanged); 252 | ethereum.removeListener('accountsChanged', handleAccountsChanged); 253 | } 254 | }; 255 | } 256 | }, []); 257 | 258 | //logout 259 | useEffect(() => { 260 | eventBus.addListener('wallet.logout', store.logout); 261 | eventBus.addListener('wallet.login', store.login); 262 | eventBus.addListener('wallet.onAccount', store.onAccount); 263 | return () => { 264 | eventBus.removeListener('wallet.logout', store.logout); 265 | eventBus.removeListener('wallet.login', store.login); 266 | eventBus.removeListener('wallet.onAccount', store.onAccount); 267 | }; 268 | }, []); 269 | 270 | return <>; 271 | }); 272 | -------------------------------------------------------------------------------- /src/components/WalletInfo/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer, useLocalStore } from 'mobx-react-lite'; 3 | import { useStore } from '../../store/index'; 4 | import { eventBus } from '../../lib/event'; 5 | import copy from 'copy-to-clipboard'; 6 | import toast from 'react-hot-toast'; 7 | import * as clipboard from 'clipboard-polyfill/text'; 8 | import { helper } from '@/lib/helper'; 9 | import { from } from '@iotexproject/iotex-address-ts'; 10 | import { NetworkState } from '@/store/lib/NetworkState'; 11 | import { Box, Button, Modal, Group, Tooltip, Image, Anchor, Text, Center } from '@mantine/core'; 12 | import { Copy, ExternalLink } from 'tabler-icons-react'; 13 | import Jazzicon from '../Jazzicon/index'; 14 | import { useTranslation } from 'react-i18next'; 15 | 16 | export const WalletInfo = observer(() => { 17 | const { god, lang } = useStore(); 18 | const { t } = useTranslation(); 19 | const store = useLocalStore(() => ({ 20 | isTipOpen: false, 21 | isIOTipOpen: false, 22 | get visible() { 23 | return god.currentNetwork.walletInfo.visible; 24 | }, 25 | close() { 26 | god.currentNetwork.walletInfo.visible = false; 27 | }, 28 | copy() { 29 | copy(god.currentNetwork.account); 30 | toast.success(lang.t('the-address-is-copied')); 31 | }, 32 | logout() { 33 | eventBus.emit('wallet.logout'); 34 | store.close(); 35 | }, 36 | toggleTipOpen(val: boolean) { 37 | this.isTipOpen = val; 38 | }, 39 | toggleIOTipOpen(val: boolean) { 40 | this.isTipOpen = val; 41 | } 42 | })); 43 | 44 | return ( 45 | 46 | 47 | 48 | 49 | 50 | { 58 | const [error] = await helper.promise.runAsync(clipboard.writeText(god.currentNetwork.account)); 59 | if (!error) { 60 | store.toggleTipOpen(true); 61 | setTimeout(() => { 62 | store.toggleTipOpen(false); 63 | }, 500); 64 | } 65 | }} 66 | > 67 | {helper.string.truncate(god.currentNetwork.account || '0x......', 12, '...')} 68 | 69 | 70 | 71 | 72 | {god.Coin.symbol === 'iotex' && ( 73 | <> 74 | 75 | 76 | 77 | { 85 | const [error] = await helper.promise.runAsync(clipboard.writeText(from(god.currentNetwork.account)?.string())); 86 | if (!error) { 87 | store.toggleIOTipOpen(true); 88 | setTimeout(() => { 89 | store.toggleIOTipOpen(false); 90 | }, 500); 91 | } 92 | }} 93 | > 94 | {god.currentNetwork?.account && from(god.currentNetwork.account)?.string()} 95 | {/* */} 96 | 97 | 98 | 99 | 100 | 101 | )} 102 | 103 | 111 | 112 | {t('view-on-0')} {`${god.currentChain.explorerName}`} 113 | 114 | 115 |
116 | 119 |
120 |
121 |
122 | ); 123 | }); 124 | -------------------------------------------------------------------------------- /src/components/WalletSelecter/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer, useLocalObservable, useLocalStore } from 'mobx-react-lite'; 3 | import { useStore } from '../../store/index'; 4 | // import { useWeb3React } from '@web3-react/core'; 5 | import { injected } from '../../lib/web3-react'; 6 | 7 | import { Box, Group, Modal, Tabs, TabsProps, Text, Avatar, AvatarsGroup, Badge, SegmentedControl } from '@mantine/core'; 8 | import { metamaskUtils } from '../../lib/metaskUtils'; 9 | import { useEffect } from 'react'; 10 | import { StringState } from '../../store/standard/base'; 11 | import { useConnect } from 'wagmi'; 12 | import { MetaMaskConnector } from 'wagmi/connectors/metaMask'; 13 | 14 | export const WalletSelecter = observer(() => { 15 | const { god, lang, ledger } = useStore(); 16 | // const { active, error, activate } = useWeb3React(); 17 | const { connect, connectors, error, isLoading, pendingConnector } = useConnect(); 18 | 19 | const store = useLocalObservable(() => ({ 20 | network: new StringState<'mainnet' | 'testnet'>({ value: 'mainnet' }), 21 | get visible() { 22 | return god.eth.connector.showConnector; 23 | }, 24 | get networks() { 25 | return god.currentNetwork.chain.set.filter((i) => i.type == store.network.value); 26 | }, 27 | 28 | close() { 29 | god.eth.connector.showConnector = false; 30 | }, 31 | async setChain(val) { 32 | god.switchChain(val); 33 | }, 34 | connectInejct() { 35 | // activate(injected); 36 | connect({ connector: connectors[0] }); 37 | // god.eth.connector.latestProvider.save('inject'); 38 | }, 39 | onWalletConnect() { 40 | // activate(walletconnect); 41 | // god.eth.connector.latestProvider.save('walletConnect'); 42 | }, 43 | connectIotexLedger() { 44 | ledger.ledger.call(); 45 | } 46 | })); 47 | 48 | const config = [ 49 | { 50 | title: 'Metamask', 51 | icon: '/images/metamask.svg' 52 | }, 53 | { 54 | title: 'ioPay', 55 | icon: '/images/iopay.svg' 56 | }, 57 | { 58 | title: 'Trust', 59 | icon: '/images/trustwallet.svg' 60 | }, 61 | { 62 | title: 'Math', 63 | icon: '/images/mathwallet.svg' 64 | }, 65 | { 66 | title: 'imToken', 67 | icon: '/images/imtoken.svg' 68 | } 69 | ]; 70 | const names = config.map((item) => item.title).join(', '); 71 | return ( 72 | 73 | store.network.setValue(v.toLowerCase() as any)} /> 74 | 75 | {/*
76 | {connectors.map((connector) => ( 77 | 82 | ))} 83 | 84 | {error &&
{error.message}
} 85 |
*/} 86 | 87 | 88 | {store.networks.map((i) => ( 89 | 90 | 91 | store.setChain(i.chainId)}> 92 | {god.currentChain.chainId == i.chainId && } 93 | 94 | 95 | {i.name} 96 | 97 | 98 | ))} 99 | 100 | {!god.currentNetwork.account && ( 101 | 102 | 103 | 104 | {lang.t('browser-wallet')} 105 | 106 | ({names}) 107 | 108 | 109 | 110 | 111 | {config.map((item, index) => { 112 | return ; 113 | })} 114 | 115 | 116 | 117 | 118 | )} 119 | 120 | 121 | 122 | {lang.t('ledger')} 123 | 124 | 125 | 126 |
127 |
128 | ); 129 | }); 130 | -------------------------------------------------------------------------------- /src/config/chain.ts: -------------------------------------------------------------------------------- 1 | import { publicConfig } from './public'; 2 | 3 | export type NetworkObject = { 4 | name: string; 5 | chainId: number; 6 | rpcUrl: string; 7 | logoUrl: string; 8 | explorerUrl: string; 9 | explorerName: string; 10 | nativeCoin: string; 11 | // blockPerSeconds: number; 12 | // multicallAddr: string; 13 | type: 'mainnet' | 'testnet'; 14 | }; 15 | 16 | export const defaultNetworks: NetworkObject[] = [ 17 | { 18 | name: 'ETH', 19 | chainId: 1, 20 | rpcUrl: `https://rpc.ankr.com/eth`, 21 | logoUrl: 'https://cdn.jsdelivr.net/gh/atomiclabs/cryptocurrency-icons@1a63530be6e374711a8554f31b17e4cb92c25fa5/svg/icon/eth.svg', 22 | explorerUrl: 'https://etherscan.io', 23 | explorerName: 'EtherScan', 24 | nativeCoin: 'ETH', 25 | type: 'mainnet' 26 | }, 27 | { 28 | name: 'Polygon', 29 | chainId: 137, 30 | logoUrl: 'https://cdn.jsdelivr.net/gh/atomiclabs/cryptocurrency-icons@1a63530be6e374711a8554f31b17e4cb92c25fa5/svg/icon/matic.svg', 31 | rpcUrl: 'https://polygon-rpc.com/', 32 | explorerUrl: 'https://explorer-mainnet.maticvigil.com/', 33 | explorerName: 'PolygonScan', 34 | nativeCoin: 'MATIC', 35 | type: 'mainnet' 36 | }, 37 | { 38 | name: 'BSC', 39 | chainId: 56, 40 | rpcUrl: 'https://rpc.ankr.com/bsc', 41 | logoUrl: 'https://cdn.jsdelivr.net/gh/atomiclabs/cryptocurrency-icons@1a63530be6e374711a8554f31b17e4cb92c25fa5/svg/icon/bnb.svg', 42 | explorerUrl: 'https://bscscan.com', 43 | explorerName: 'BscScan', 44 | nativeCoin: 'BNB', 45 | type: 'mainnet' 46 | }, 47 | { 48 | name: 'IoTeX', 49 | chainId: 4689, 50 | rpcUrl: 'https://babel-api.mainnet.iotex.io/', 51 | logoUrl: 'https://cdn.jsdelivr.net/gh/atomiclabs/cryptocurrency-icons@1a63530be6e374711a8554f31b17e4cb92c25fa5/svg/icon/iotx.svg', 52 | explorerUrl: 'https://iotexscan.io', 53 | explorerName: 'IotexScan', 54 | nativeCoin: 'IOTX', 55 | type: 'mainnet' 56 | }, 57 | { 58 | name: 'Avalanche', 59 | chainId: 43114, 60 | rpcUrl: 'https://rpc.ankr.com/avalanche', 61 | logoUrl: 'https://cdn.jsdelivr.net/gh/atomiclabs/cryptocurrency-icons@1a63530be6e374711a8554f31b17e4cb92c25fa5/svg/icon/avax.svg', 62 | explorerUrl: 'https://subnets.avax.network/', 63 | explorerName: 'AVAXScan', 64 | nativeCoin: 'AVAX', 65 | type: 'mainnet' 66 | }, 67 | { 68 | name: 'Fantom', 69 | chainId: 250, 70 | rpcUrl: 'https://rpc.ankr.com/fantom', 71 | logoUrl: 'https://cryptologos.cc/logos/fantom-ftm-logo.svg', 72 | explorerUrl: 'https://ftmscan.com/', 73 | explorerName: 'FTMScan', 74 | nativeCoin: 'FTM', 75 | type: 'mainnet' 76 | }, 77 | { 78 | name: 'BSC Testnet', 79 | chainId: 97, 80 | rpcUrl: 'https://data-seed-prebsc-1-s1.binance.org:8545', 81 | logoUrl: 'https://cdn.jsdelivr.net/gh/atomiclabs/cryptocurrency-icons@1a63530be6e374711a8554f31b17e4cb92c25fa5/svg/icon/bnb.svg', 82 | explorerUrl: 'https://testnet.bscscan.com', 83 | explorerName: 'BscScan', 84 | nativeCoin: 'BNB', 85 | type: 'testnet' 86 | }, 87 | { 88 | name: 'ETH Kovan', 89 | chainId: 42, 90 | rpcUrl: `https://kovan.infura.io/v3/${publicConfig.infuraId}`, 91 | logoUrl: 'https://cdn.jsdelivr.net/gh/atomiclabs/cryptocurrency-icons@1a63530be6e374711a8554f31b17e4cb92c25fa5/svg/icon/eth.svg', 92 | explorerUrl: 'https://kovan.etherscan.io', 93 | explorerName: 'EtherScan', 94 | nativeCoin: 'ETH', 95 | type: 'testnet' 96 | }, 97 | { 98 | name: 'ETH Rinkeby', 99 | chainId: 4, 100 | rpcUrl: `https://rinkeby.infura.io/v3/${publicConfig.infuraId}`, 101 | logoUrl: 'https://cdn.jsdelivr.net/gh/atomiclabs/cryptocurrency-icons@1a63530be6e374711a8554f31b17e4cb92c25fa5/svg/icon/eth.svg', 102 | explorerUrl: 'https://rinkeby.etherscan.io', 103 | explorerName: 'EtherScan', 104 | nativeCoin: 'ETH', 105 | type: 'testnet' 106 | }, 107 | { 108 | name: 'IoTeX Testnet', 109 | chainId: 4690, 110 | rpcUrl: `https://babel-api.testnet.iotex.io`, 111 | logoUrl: 'https://cdn.jsdelivr.net/gh/atomiclabs/cryptocurrency-icons@1a63530be6e374711a8554f31b17e4cb92c25fa5/svg/icon/iotx.svg', 112 | explorerUrl: 'https://testnet.iotexscan.io', 113 | explorerName: 'IotexScan', 114 | nativeCoin: 'IOTX', 115 | type: 'testnet' 116 | } 117 | ]; 118 | -------------------------------------------------------------------------------- /src/config/public.ts: -------------------------------------------------------------------------------- 1 | export const publicConfig = { 2 | isProd: process.env.PROD, 3 | infuraId: process.env.INFURA_ID || '9aa3d95b3bc440fa88ea12eaa4456161', 4 | APIURL: process.env.APIURL 5 | }; 6 | -------------------------------------------------------------------------------- /src/i18n/config.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import en from './en/translation.json'; 3 | import zh_CN from './zh-cn/translation.json'; 4 | import ru from './ru/translation.json'; 5 | import de from './de/translation.json'; 6 | import ja from './ja/translation.json'; 7 | import ko from './ko/translation.json'; 8 | import es from './es/translation.json'; 9 | import zh_TW from './zh-tw/translation.json'; 10 | 11 | import { initReactI18next } from 'react-i18next'; 12 | //refer https://react.i18next.com/ 13 | 14 | export const resources = { 15 | en: { 16 | translation: en 17 | }, 18 | zh_CN: { 19 | translation: zh_CN 20 | }, 21 | zh_TW: { 22 | translation: zh_TW 23 | }, 24 | ru: { 25 | translation: ru 26 | }, 27 | de: { 28 | translation: de 29 | }, 30 | ja: { 31 | translation: ja 32 | }, 33 | ko: { 34 | translation: ko 35 | }, 36 | es: { 37 | translation: es 38 | } 39 | } as const; 40 | 41 | i18n.use(initReactI18next).init({ 42 | lng: 'en', 43 | interpolation: { 44 | escapeValue: false 45 | }, 46 | resources 47 | }); 48 | 49 | export default i18n; 50 | -------------------------------------------------------------------------------- /src/i18n/de/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "a": "", 3 | "account": "Konto", 4 | "approve": "Genehmigen", 5 | "browser-wallet": "Browser Wallet", 6 | "connect-to-wallet": "Connect to a wallet", 7 | "connect-wallet": "Connect Wallet", 8 | "connector-failed": "Connect failed, Please try again later.", 9 | "connector-loading": "Connecting. Please unlock ioPay and don't use global proxy or VPN.", 10 | "connector-loading-mobile": "Connecting.", 11 | "connector-success": "Connected", 12 | "dapp-dev-framework": "Dapp-Entwicklungsframework", 13 | "erc20-toolbox": "ERC20-Toolbox", 14 | "error": "", 15 | "example": "Beispiel", 16 | "get-started": "Get started", 17 | "hello": "hello ${msg}", 18 | "home": "Heim", 19 | "invalid-input": "Invalid Input", 20 | "issues": "Issues", 21 | "language": "SPRACHE", 22 | "logout": "Ausloggen", 23 | "next-generation": "Next Generation", 24 | "notification": "", 25 | "playground": "Spielplatz", 26 | "receiver-address": "Receiver Address", 27 | "sample": "Sample", 28 | "search": "Suche", 29 | "select-token": "Select a token", 30 | "select-token-placeholder": "Search name or paste address", 31 | "submit": "Submit", 32 | "switch-network": "Switch Network", 33 | "the-address-is-copied": "Die Adresse wird kopiert.", 34 | "tip": "Erstellen Sie einfach und schnell eine voll funktionsfähige DAPP mit graphql und typsicherem SDK.", 35 | "token-amount": "Token Amount", 36 | "transaction-submitted": "", 37 | "transfer": "Transfer", 38 | "use-template": "Use template", 39 | "view-on": "View On ${explorerName}", 40 | "view-on-0": "Ansicht ein", 41 | "view-on-explorername": "", 42 | "wallet-connect": "Wallet Connect", 43 | "warning": "", 44 | "wrong-network": "Wrong Network" 45 | } 46 | -------------------------------------------------------------------------------- /src/i18n/en/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "a": "A", 3 | "account": "Account", 4 | "approve": "Approve", 5 | "browser-wallet": "Browser Wallet", 6 | "connect-to-wallet": "Connect to a wallet", 7 | "connect-wallet": "Connect Wallet", 8 | "connector-failed": "Connect failed, Please try again later.", 9 | "connector-loading": "Connecting. Please unlock ioPay and don't use global proxy or VPN.", 10 | "connector-loading-mobile": "Connecting.", 11 | "connector-success": "Connected", 12 | "dapp-dev-framework": "Dapp Dev Framework", 13 | "erc20-toolbox": "ERC20 Toolbox", 14 | "error": "error", 15 | "example": "Example", 16 | "get-started": "Get started", 17 | "hello": "hello ${msg}", 18 | "home": "Home", 19 | "invalid-input": "Invalid Input", 20 | "issues": "Issues", 21 | "language": "LANGUAGE", 22 | "logout": "Logout", 23 | "next-generation": "Next Generation", 24 | "notification": "notification", 25 | "playground": "Playground", 26 | "receiver-address": "Receiver Address", 27 | "sample": "Sample", 28 | "search": "Search", 29 | "select-token": "Select a token", 30 | "select-token-placeholder": "Search name or paste address", 31 | "submit": "Submit", 32 | "switch-network": "Switch Network", 33 | "the-address-is-copied": "The address is copied.", 34 | "tip": "Build fully functional dapp with graphql and typesafe sdk easy and fast.", 35 | "token-amount": "Token Amount", 36 | "transaction-submitted": "Transaction submitted", 37 | "transfer": "Transfer", 38 | "use-template": "Use template", 39 | "view-on": "View On ${explorerName}", 40 | "view-on-0": "View On", 41 | "view-on-explorername": "View on {{explorerName}}", 42 | "wallet-connect": "Wallet Connect", 43 | "warning": "warning", 44 | "wrong-network": "Wrong Network" 45 | } 46 | -------------------------------------------------------------------------------- /src/i18n/es/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "a": "", 3 | "account": "Cuenta", 4 | "approve": "Aprobar", 5 | "browser-wallet": "Monedero del navegador", 6 | "connect-to-wallet": "Conectarse a una billetera", 7 | "connect-wallet": "Conectar billetera", 8 | "connector-failed": "Error de conexión, inténtalo de nuevo más tarde.", 9 | "connector-loading": "Conectando. \nDesbloquee ioPay y no use proxy global o VPN.", 10 | "connector-loading-mobile": "Conectando.", 11 | "connector-success": "Conectado", 12 | "dapp-dev-framework": "Marco de desarrollo de Dapp", 13 | "erc20-toolbox": "Caja de herramientas ERC20", 14 | "error": "", 15 | "example": "Ejemplo", 16 | "get-started": "Empezar", 17 | "hello": "hola ${mensaje}", 18 | "home": "Casa", 19 | "invalid-input": "entrada inválida", 20 | "issues": "Cuestiones", 21 | "language": "IDIOMA", 22 | "logout": "Cerrar sesión", 23 | "next-generation": "Próxima generación", 24 | "notification": "", 25 | "playground": "Patio de recreo", 26 | "receiver-address": "Dirección del receptor", 27 | "sample": "Muestra", 28 | "search": "Búsqueda", 29 | "select-token": "Seleccione una ficha", 30 | "select-token-placeholder": "Buscar nombre o pegar dirección", 31 | "submit": "Entregar", 32 | "switch-network": "Cambiar red", 33 | "the-address-is-copied": "Se copia la dirección.", 34 | "tip": "Cree dapp completamente funcional con graphql y typesafe sdk fácil y rápido.", 35 | "token-amount": "Cantidad de fichas", 36 | "transaction-submitted": "", 37 | "transfer": "Transferir", 38 | "use-template": "Usar plantilla", 39 | "view-on": "Ver en ${explorerName}", 40 | "view-on-0": "Ver en", 41 | "view-on-explorername": "", 42 | "wallet-connect": "Monedero Conectar", 43 | "warning": "", 44 | "wrong-network": "Red incorrecta" 45 | } 46 | -------------------------------------------------------------------------------- /src/i18n/ja/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "a": "", 3 | "account": "アカウント", 4 | "approve": "承認", 5 | "browser-wallet": "ブラウザウォレット", 6 | "connect-to-wallet": "ウォレットに接続する", 7 | "connect-wallet": "ウォレットを接続する", 8 | "connector-failed": "接続に失敗しました。しばらくしてからもう一度お試しください。", 9 | "connector-loading": "接続しています。 \nioPayのロックを解除し、グローバルプロキシまたはVPNを使用しないでください。", 10 | "connector-loading-mobile": "接続しています。", 11 | "connector-success": "接続済み", 12 | "dapp-dev-framework": "Dapp開発フレームワーク", 13 | "erc20-toolbox": "ERC20ツールボックス", 14 | "error": "", 15 | "example": "例", 16 | "get-started": "始めましょう", 17 | "hello": "こんにちは${msg}", 18 | "home": "ホームページ", 19 | "invalid-input": "無効入力", 20 | "issues": "問題", 21 | "language": "言語", 22 | "logout": "ログアウト", 23 | "next-generation": "次世代", 24 | "notification": "", 25 | "playground": "遊び場", 26 | "receiver-address": "受信者アドレス", 27 | "sample": "サンプル", 28 | "search": "探す", 29 | "select-token": "トークンを選択", 30 | "select-token-placeholder": "名前を検索するか、アドレスを貼り付けます", 31 | "submit": "送信", 32 | "switch-network": "スイッチネットワーク", 33 | "the-address-is-copied": "アドレスがコピーされます。", 34 | "tip": "完全に機能するdappをgraphqlとtypesafesdkで簡単かつ迅速に構築します。", 35 | "token-amount": "トークン量", 36 | "transaction-submitted": "", 37 | "transfer": "移行", 38 | "use-template": "テンプレートを使用", 39 | "view-on": "${explorerName}で表示", 40 | "view-on-0": "チェック", 41 | "view-on-explorername": "", 42 | "wallet-connect": "ウォレットコネクト", 43 | "warning": "", 44 | "wrong-network": "間違ったネットワーク" 45 | } 46 | -------------------------------------------------------------------------------- /src/i18n/ko/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "a": "", 3 | "account": "계정", 4 | "approve": "승인하다", 5 | "browser-wallet": "브라우저 지갑", 6 | "connect-to-wallet": "지갑에 연결", 7 | "connect-wallet": "지갑 연결", 8 | "connector-failed": "연결에 실패했습니다. 나중에 다시 시도해 주세요.", 9 | "connector-loading": "연결 중. \nioPay를 잠금 해제하고 글로벌 프록시 또는 VPN을 사용하지 마십시오.", 10 | "connector-loading-mobile": "연결 중.", 11 | "connector-success": "연결됨", 12 | "dapp-dev-framework": "Dapp 개발 프레임워크", 13 | "erc20-toolbox": "ERC20 도구 상자", 14 | "error": "", 15 | "example": "예시", 16 | "get-started": "시작하다", 17 | "hello": "안녕하세요 ${msg}", 18 | "home": "집", 19 | "invalid-input": "잘못된 입력", 20 | "issues": "문제", 21 | "language": "언어", 22 | "logout": "로그 아웃", 23 | "next-generation": "다음 세대", 24 | "notification": "", 25 | "playground": "운동장", 26 | "receiver-address": "수신자 주소", 27 | "sample": "견본", 28 | "search": "검색", 29 | "select-token": "토큰 선택", 30 | "select-token-placeholder": "이름 검색 또는 주소 붙여넣기", 31 | "submit": "제출하다", 32 | "switch-network": "네트워크 전환", 33 | "the-address-is-copied": "주소가 복사됩니다.", 34 | "tip": "graphql 및 typesafe sdk로 쉽고 빠르게 완전한 기능을 갖춘 dapp을 빌드하십시오.", 35 | "token-amount": "토큰 금액", 36 | "transaction-submitted": "", 37 | "transfer": "옮기다", 38 | "use-template": "템플릿 사용", 39 | "view-on": "${explorerName}에서 보기", 40 | "view-on-0": "보기에", 41 | "view-on-explorername": "", 42 | "wallet-connect": "지갑 연결", 43 | "warning": "", 44 | "wrong-network": "잘못된 네트워크" 45 | } 46 | -------------------------------------------------------------------------------- /src/i18n/ru/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "a": "", 3 | "account": "Рахунак", 4 | "approve": "Утвердить", 5 | "browser-wallet": "Кашалёк браўзэра", 6 | "connect-to-wallet": "Падключыцеся да кашалька", 7 | "connect-wallet": "Падключыце кашалёк", 8 | "connector-failed": "Не атрымалася падключыцца. Паўтарыце спробу пазней.", 9 | "connector-loading": "Падключэнне. Разблакіруйце ioPay і не выкарыстоўвайце глабальны проксі-сервер або VPN.", 10 | "connector-loading-mobile": "Падключэнне.", 11 | "connector-success": "Падключаны", 12 | "dapp-dev-framework": "Платформа разработки децентрализованных приложений", 13 | "erc20-toolbox": "ERC20 набор інструментаў", 14 | "error": "", 15 | "example": "Пример", 16 | "get-started": "Начать", 17 | "hello": "добры дзень ${msg}", 18 | "home": "Дом", 19 | "invalid-input": "Няправільны ўвод", 20 | "issues": "пытанні", 21 | "language": "ЯЗЫК", 22 | "logout": "Выйти", 23 | "next-generation": "Следующее поколение", 24 | "notification": "", 25 | "playground": "Детская площадка", 26 | "receiver-address": "Адрас атрымальніка", 27 | "sample": "Узор", 28 | "search": "Поиск", 29 | "select-token": "Выберыце маркер", 30 | "select-token-placeholder": "Шукаць імя або ўставіць адрас", 31 | "submit": "Адправіць", 32 | "switch-network": "Пераключыць сетку", 33 | "the-address-is-copied": "Адрес скопирован.", 34 | "tip": "Создавайте полнофункциональные децентрализованные приложения с помощью graphql и typesafe sdk легко и быстро.", 35 | "token-amount": "Сума токена", 36 | "transaction-submitted": "", 37 | "transfer": "Перечислить", 38 | "use-template": "Выкарыстоўвайце шаблон", 39 | "view-on": "Прагляд укл ${explorerName}", 40 | "view-on-0": "Смотреть на", 41 | "view-on-explorername": "", 42 | "wallet-connect": "Кашалёк Connect", 43 | "warning": "", 44 | "wrong-network": "Няправільная сетка" 45 | } 46 | -------------------------------------------------------------------------------- /src/i18n/zh-cn/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "a": "", 3 | "account": "账户", 4 | "approve": "授权", 5 | "browser-wallet": "浏览器钱包", 6 | "connect-to-wallet": "连接到一个钱包", 7 | "connect-wallet": "连接钱包", 8 | "connector-failed": "连接失败,请稍后再试.", 9 | "connector-loading": "Connecting. Please unlock ioPay and don't use global proxy or VPN.", 10 | "connector-loading-mobile": "连接中.", 11 | "connector-success": "连接成功", 12 | "dapp-dev-framework": "Dapp 开发框架", 13 | "erc20-toolbox": "ERC20 工具箱", 14 | "error": "", 15 | "example": "例子", 16 | "get-started": "开始使用", 17 | "hello": "你好 ${msg}", 18 | "home": "主页", 19 | "invalid-input": "Invalid Input", 20 | "issues": "问题", 21 | "language": "语言", 22 | "logout": "登出", 23 | "next-generation": "下一代", 24 | "notification": "", 25 | "playground": "操场", 26 | "receiver-address": "接收地址", 27 | "sample": "示例", 28 | "search": "搜索", 29 | "select-token": "选择代币", 30 | "select-token-placeholder": "按名称搜索或粘贴地址", 31 | "submit": "提交", 32 | "switch-network": "切换网络", 33 | "the-address-is-copied": "地址被复制。", 34 | "tip": "使用 graphql 和 typesafe sdk 轻松快速地构建功能齐全的 dapp。", 35 | "token-amount": "代币数量", 36 | "transaction-submitted": "交易已提交", 37 | "transfer": "转账", 38 | "use-template": "使用模板", 39 | "view-on": "在${explorerName}查看", 40 | "view-on-0": "查看", 41 | "view-on-explorername": "在 {{explorerName}} 查看", 42 | "wallet-connect": "钱包连接", 43 | "warning": "", 44 | "wrong-network": "网络错误" 45 | } 46 | -------------------------------------------------------------------------------- /src/i18n/zh-tw/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "a": "", 3 | "account": "帳戶", 4 | "approve": "批准", 5 | "browser-wallet": "瀏覽器錢包", 6 | "connect-to-wallet": "連接到錢包", 7 | "connect-wallet": "連接錢包", 8 | "connector-failed": "連接失敗,請稍後再試。", 9 | "connector-loading": "連接。\n請解鎖ioPay,不要使用全球代理或VPN。", 10 | "connector-loading-mobile": "連接。", 11 | "connector-success": "連接的", 12 | "dapp-dev-framework": "Dapp 開發框架", 13 | "erc20-toolbox": "ERC20 工具箱", 14 | "error": "", 15 | "example": "例子", 16 | "get-started": "開始使用", 17 | "hello": "你好${味精}", 18 | "home": "主頁", 19 | "invalid-input": "輸入無效", 20 | "issues": "問題", 21 | "language": "語言", 22 | "logout": "登出", 23 | "next-generation": "下一代", 24 | "notification": "", 25 | "playground": "操場", 26 | "receiver-address": "收貨人地址", 27 | "sample": "樣本", 28 | "search": "搜索", 29 | "select-token": "選擇一個令牌", 30 | "select-token-placeholder": "搜索名稱或粘貼地址", 31 | "submit": "", 32 | "switch-network": "交換網絡", 33 | "the-address-is-copied": "地址被複製。", 34 | "tip": "使用 graphql 和 typesafe sdk 輕鬆快速地構建功能齊全的 dapp。", 35 | "token-amount": "代幣數量", 36 | "transaction-submitted": "交易已提交", 37 | "transfer": "轉移", 38 | "use-template": "使用模板", 39 | "view-on": "在 ${explorerName} 上查看", 40 | "view-on-0": "查看", 41 | "view-on-explorername": "", 42 | "wallet-connect": "錢包連接", 43 | "warning": "", 44 | "wrong-network": "錯誤的網絡" 45 | } 46 | -------------------------------------------------------------------------------- /src/lib/axios.ts: -------------------------------------------------------------------------------- 1 | import Axios from 'axios'; 2 | import { publicConfig } from '../config/public'; 3 | 4 | export const api = Axios.create({ 5 | baseURL: publicConfig.APIURL 6 | }); 7 | 8 | api.interceptors.request.use((req) => { 9 | // req.headers['Authorization'] = "" 10 | return req; 11 | }); 12 | 13 | api.interceptors.response.use( 14 | (res) => { 15 | return res; 16 | }, 17 | (err) => { 18 | return Promise.reject(err); 19 | } 20 | ); 21 | -------------------------------------------------------------------------------- /src/lib/event.ts: -------------------------------------------------------------------------------- 1 | import { TransactionItem } from '@/store/history'; 2 | import { EventEmitter } from 'events'; 3 | import TypedEmitter from 'typed-emitter'; 4 | import { publicConfig } from '../config/public'; 5 | 6 | class MyEmitter extends EventEmitter { 7 | emit(type: any, ...args: any[]) { 8 | super.emit('*', { type, args }); 9 | return super.emit(type, ...args) || super.emit('', ...args); 10 | } 11 | } 12 | 13 | interface MessageEvents { 14 | '*': (agrgs: { type: string; args: [] }) => void; 15 | 'wallet.onAccount': () => void; 16 | 'wallet.login': () => Promise; 17 | 'wallet.onToken': () => void; 18 | 'wallet.logout': () => void; 19 | 'chain.switch': () => void; 20 | 'global.cacheData': () => void; 21 | 'history.insert': (transactionItem: TransactionItem) => void; 22 | 'history.update': (transactionItem: TransactionItem) => void; 23 | 'history.delete': (transactionItem: Pick) => void; 24 | signer: (signer: any) => void; 25 | provider: (signer: any) => void; 26 | } 27 | 28 | export const eventBus = new MyEmitter() as TypedEmitter; 29 | 30 | if (!publicConfig.isProd) { 31 | eventBus.on('*', ({ type, args }) => { 32 | console.log(type, args); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /src/lib/hooks.ts: -------------------------------------------------------------------------------- 1 | import { rootStore } from '../store/index'; 2 | import { eventBus } from './event'; 3 | 4 | export const hooks = { 5 | async waitAccount(timeout?) { 6 | return new Promise((res, rej) => { 7 | if (rootStore.god.currentNetwork.account) { 8 | res(); 9 | } else { 10 | rootStore.god.setShowConnecter(true); 11 | eventBus.once('wallet.onAccount', () => { 12 | res(); 13 | }); 14 | if (timeout) { 15 | setTimeout(() => { 16 | rej(); 17 | }, timeout); 18 | } 19 | } 20 | }); 21 | }, 22 | async waitJWT(timeout?) { 23 | return new Promise((res, rej) => { 24 | if (rootStore.user.token.value) { 25 | res(); 26 | } else { 27 | eventBus.emit('wallet.login'); 28 | eventBus.once('wallet.onToken', () => { 29 | res(); 30 | }); 31 | if (timeout) { 32 | setTimeout(() => { 33 | rej(); 34 | }, timeout); 35 | } 36 | } 37 | }); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/lib/ledger.ts: -------------------------------------------------------------------------------- 1 | import { Account } from 'iotex-antenna/lib/account/account'; 2 | import { Envelop, SealedEnvelop } from 'iotex-antenna/lib/action/envelop'; 3 | import { SignerPlugin } from 'iotex-antenna/lib/action/method'; 4 | // @ts-ignore 5 | import type Transport from '@ledgerhq/hw-transport'; 6 | import TransportWebUSB from '@ledgerhq/hw-transport-webusb'; 7 | // @ts-ignore 8 | import TransportWebHID from '@ledgerhq/hw-transport-webhid'; 9 | 10 | // add ledger dependencies 11 | // pnpm i iotex-antenna @ledgerhq/hw-transport @ledgerhq/hw-transport-webusb @ledgerhq/hw-transport-webhid 12 | 13 | const isWebHIDSupported = async (): Promise => { 14 | return await TransportWebHID.isSupported(); 15 | }; 16 | 17 | export const createTransport = async (): Promise => { 18 | try { 19 | if (await isWebHIDSupported()) { 20 | return await TransportWebHID.create(); 21 | } else { 22 | return await TransportWebUSB.create(); 23 | } 24 | } catch (e) { 25 | try { 26 | return await TransportWebUSB.create(); 27 | } catch (eu) { 28 | console.log(`create WebUSB transport error ${eu}`); 29 | throw e; 30 | } 31 | } 32 | }; 33 | 34 | export class LedgerPlugin implements SignerPlugin { 35 | public ledger: IoTeXApp; 36 | public initialed: boolean; 37 | private address: { returnCode: number; publicKey: string; address: string } | undefined; 38 | 39 | constructor(ledger: IoTeXApp) { 40 | this.ledger = ledger; 41 | } 42 | 43 | public async init() { 44 | if (!this.initialed) { 45 | const address = await this.ledger.getAddress("44'/304'/0'/0/0"); 46 | if (address.returnCode != 0x9000) { 47 | throw new Error(`fetch address error ${address}`); 48 | } 49 | this.address = address; 50 | this.initialed = true; 51 | } 52 | } 53 | 54 | public async getAccounts(): Promise> { 55 | if (!this.initialed) { 56 | throw new Error('plugin not initial'); 57 | } 58 | // TODO how to process different path? 59 | const account = new Account(); 60 | account.address = this.address!.address; 61 | return [account]; 62 | } 63 | 64 | public async signOnly(envelop: Envelop): Promise { 65 | if (!this.initialed) { 66 | throw new Error('plugin not initial'); 67 | } 68 | const signed = await this.ledger.signTransaction("44'/304'/0'/0/0", Buffer.from(envelop.bytestream())); 69 | if (signed.code !== 36864) { 70 | throw new Error(signed.message || 'ledger error'); 71 | } 72 | return new SealedEnvelop(envelop, Buffer.from(this.address!.publicKey, 'hex'), signed.signature!); 73 | } 74 | } 75 | 76 | //@ts-ignore 77 | import type Transport from '@ledgerhq/hw-transport'; 78 | 79 | import { publicKeyToAddress } from 'iotex-antenna/lib/crypto/crypto'; 80 | import { v4 as uuid } from 'uuid'; 81 | 82 | export class ConcurrentState { 83 | value: T; 84 | pending = false; 85 | promiseHooks: { resolve: Function; reject: Function }[] = []; 86 | wait() { 87 | return new Promise((resolve, reject) => { 88 | this.promiseHooks.push({ 89 | resolve, 90 | reject 91 | }); 92 | }); 93 | } 94 | releaseLock(res?: T) { 95 | if (res) { 96 | this.value = res; 97 | } 98 | this.promiseHooks.forEach(({ resolve }) => { 99 | resolve(res); 100 | }); 101 | this.pending = false; 102 | } 103 | releaseErrorLock(error: Error) { 104 | this.promiseHooks.forEach(({ reject }) => { 105 | reject(error); 106 | }); 107 | } 108 | lock() { 109 | this.pending = true; 110 | } 111 | } 112 | 113 | export const splitPath = (path: string): number[] => { 114 | const result: number[] = []; 115 | const components = path.split('/'); 116 | components.forEach((element) => { 117 | let number = parseInt(element, 10); 118 | if (isNaN(number)) { 119 | return; 120 | } 121 | if (element.length > 1 && element[element.length - 1] === "'") { 122 | number |= 0x80000000; 123 | } 124 | result.push(number); 125 | }); 126 | return result; 127 | }; 128 | 129 | const CLA = 0x55; 130 | const CHUNK_SIZE = 250; 131 | const INS = { 132 | GET_VERSION: 0x00, 133 | PUBLIC_KEY_SECP256K1: 0x01, 134 | SIGN_SECP256K1: 0x02, 135 | SHOW_ADDR_SECP256K1: 0x03, 136 | GET_ADDR_SECP256K1: 0x04, 137 | SIGN_PERSONAL_MESSAGE: 0x05 138 | }; 139 | const ERROR_DESCRIPTION = { 140 | 1: 'U2F: Unknown', 141 | 2: 'U2F: Bad request', 142 | 3: 'U2F: Configuration unsupported', 143 | 4: 'U2F: Device Ineligible', 144 | 5: 'U2F: Timeout', 145 | 14: 'Timeout', 146 | 0x9000: 'No errors', 147 | 0x9001: 'Device is busy', 148 | 0x6400: 'Execution Error', 149 | 0x6700: 'Wrong Length', 150 | 0x6982: 'Empty Buffer', 151 | 0x6983: 'Output buffer too small', 152 | 0x6984: 'Data is invalid', 153 | 0x6985: 'Conditions not satisfied', 154 | 0x6986: 'Transaction rejected', 155 | 0x6a80: 'Bad key handle', 156 | 0x6b00: 'Invalid P1/P2', 157 | 0x6d00: 'Instruction not supported', 158 | 0x6e00: 'App does not seem to be open', 159 | 0x6f00: 'Unknown error', 160 | 0x6f01: 'Sign/verify error' 161 | }; 162 | 163 | const errorCodeToString = (statusCode: number): string => { 164 | if (statusCode in ERROR_DESCRIPTION) { 165 | // @ts-ignore 166 | return ERROR_DESCRIPTION[statusCode]; 167 | } 168 | return `Unknown Status Code: ${statusCode}`; 169 | }; 170 | 171 | const processErrorResponse = (response: { statusCode: number }) => { 172 | return { 173 | code: response.statusCode, 174 | message: errorCodeToString(response.statusCode) 175 | }; 176 | }; 177 | 178 | export class IoTeXApp { 179 | private transport: Transport; 180 | private account: ConcurrentState<{ 181 | returnCode: number; 182 | publicKey: string; 183 | address: string; 184 | }>; 185 | private pendingKey: string; 186 | private sendQueue: Array<() => any> = []; 187 | 188 | constructor(transport: Transport) { 189 | this.transport = transport; 190 | this.account = new ConcurrentState(); 191 | } 192 | 193 | public async getAddress(path: string): Promise<{ 194 | returnCode: number; 195 | publicKey: string; 196 | address: string; 197 | }> { 198 | console.log('direct getAccounts------------------'); 199 | if (this.account.value) { 200 | return this.account.value; 201 | } 202 | if (this.account.pending) { 203 | return this.account.wait(); 204 | } else { 205 | this.account.lock(); 206 | } 207 | console.log('direct requestAccounts------------------'); 208 | return this.transport.send(CLA, INS.PUBLIC_KEY_SECP256K1, 0x00, 0x00, this.serializePath(path)).then((response) => { 209 | const errorCodeData = response.slice(-2); 210 | const returnCode = errorCodeData[0] * 256 + errorCodeData[1]; 211 | const publicKey = Buffer.from(response.slice(0, 65)).toString('hex'); 212 | this.account.releaseLock({ 213 | returnCode, 214 | publicKey: publicKey, 215 | address: publicKeyToAddress(publicKey) 216 | }); 217 | return { 218 | returnCode, 219 | publicKey: publicKey, 220 | address: publicKeyToAddress(publicKey) 221 | }; 222 | }); 223 | } 224 | 225 | private serializePath(path: string): Buffer { 226 | const paths = splitPath(path); 227 | const buf = Buffer.alloc(1 + 4 * paths.length); 228 | buf.writeUInt8(paths.length, 0); 229 | for (let i = 0; i < paths.length; i++) { 230 | buf.writeInt32LE(paths[i], 1 + i * 4); 231 | } 232 | return buf; 233 | } 234 | 235 | private signGetChunks(path: string, message: Buffer): Buffer[] { 236 | const chunks = []; 237 | // @ts-ignore 238 | chunks.push(this.serializePath(path)); 239 | const buffer = Buffer.from(message); 240 | 241 | for (let i = 0; i < buffer.length; i += CHUNK_SIZE) { 242 | let end = i + CHUNK_SIZE; 243 | if (i > buffer.length) { 244 | end = buffer.length; 245 | } 246 | // @ts-ignore 247 | chunks.push(buffer.slice(i, end)); 248 | } 249 | 250 | return chunks; 251 | } 252 | 253 | private async signSendChunk( 254 | reqId: string, 255 | type: number, 256 | chunkIdx: number, 257 | chunkNum: number, 258 | chunk: Buffer 259 | ): Promise<{ 260 | signature?: Buffer; 261 | code: number; 262 | message: string; 263 | }> { 264 | const send = async (resolve) => { 265 | const res = await this.transport.send(CLA, type, chunkIdx, chunkNum, chunk, [0x9000, 0x6a80, 0x6986]).then((response) => { 266 | const errorCodeData = response.slice(-2); 267 | const returnCode = errorCodeData[0] * 256 + errorCodeData[1]; 268 | 269 | let errorMessage = errorCodeToString(returnCode); 270 | 271 | if (returnCode === 0x6a80) { 272 | errorMessage = response.slice(0, response.length - 2).toString('ascii'); 273 | } 274 | 275 | let signature = null; 276 | if (response.length > 2) { 277 | // @ts-ignore 278 | signature = response.slice(0, response.length - 2); 279 | } 280 | 281 | return { 282 | signature, 283 | code: returnCode, 284 | message: errorMessage 285 | }; 286 | }, processErrorResponse); 287 | resolve(res); 288 | if (this.sendQueue.length) { 289 | this.sendQueue.shift()?.call(this); 290 | } else { 291 | this.pendingKey = ''; 292 | } 293 | }; 294 | return new Promise(async (resolve, reject) => { 295 | if (!this.pendingKey || this.pendingKey === reqId) { 296 | this.pendingKey = reqId; 297 | send(resolve); 298 | } else { 299 | this.sendQueue.push(() => send(resolve)); 300 | } 301 | }); 302 | } 303 | 304 | public async signTransaction( 305 | path: string, 306 | transaction: Buffer 307 | ): Promise<{ 308 | signature?: Buffer; 309 | code: number; 310 | message: string; 311 | }> { 312 | console.log(`request sign transacton: ${transaction.toString('hex')}`); 313 | const chunks = this.signGetChunks(path, transaction); 314 | const reqId = uuid(); 315 | // @ts-ignore 316 | return await this.signSendChunk(reqId, INS.SIGN_SECP256K1, 1, chunks.length, chunks[0]).then(async (response) => { 317 | let result = { 318 | code: response.code, 319 | message: response.message, 320 | signature: null 321 | }; 322 | const reqId = uuid(); 323 | for (let i = 1; i < chunks.length; i += 1) { 324 | // @ts-ignore 325 | result = await this.signSendChunk(reqId, INS.SIGN_SECP256K1, 1 + i, chunks.length, chunks[i]); 326 | if (result.code !== 0x9000) { 327 | break; 328 | } 329 | } 330 | 331 | return { 332 | code: result.code, 333 | message: result.message, 334 | signature: result.signature 335 | }; 336 | }, processErrorResponse); 337 | } 338 | 339 | public async signMessage( 340 | path: string, 341 | message: Buffer 342 | ): Promise<{ 343 | signature?: Buffer; 344 | code: number; 345 | message: string; 346 | }> { 347 | console.log(`request sign message: ${message.toString('hex')}`); 348 | const chunks = this.signGetChunks(path, message); 349 | const reqId = uuid(); 350 | // @ts-ignore 351 | return await this.signSendChunk(reqId, INS.SIGN_PERSONAL_MESSAGE, 1, chunks.length, chunks[0]).then(async (response) => { 352 | let result = { 353 | code: response.code, 354 | message: response.message, 355 | signature: null 356 | }; 357 | const reqId = uuid(); 358 | for (let i = 1; i < chunks.length; i += 1) { 359 | // @ts-ignore 360 | result = await this.signSendChunk(reqId, INS.SIGN_PERSONAL_MESSAGE, 1 + i, chunks.length, chunks[i]); 361 | if (result.code !== 0x9000) { 362 | break; 363 | } 364 | } 365 | 366 | // return { 367 | // code: result.code, 368 | // message: result.message, 369 | // signature: result.signature, 370 | // }; 371 | 372 | return result.signature; 373 | }, processErrorResponse); 374 | } 375 | } 376 | -------------------------------------------------------------------------------- /src/lib/lodash.ts: -------------------------------------------------------------------------------- 1 | import debounce from 'lodash/debounce'; 2 | import throttle from 'lodash/throttle'; 3 | import each from 'lodash/each'; 4 | import flattenDeep from 'lodash/flattenDeep'; 5 | import omitBy from 'lodash/omitBy'; 6 | import isNil from 'lodash/isNil'; 7 | import keyBy from 'lodash/keyBy'; 8 | 9 | export const _ = { 10 | throttle, 11 | debounce, 12 | each, 13 | flattenDeep, 14 | omitBy, 15 | isNil, 16 | keyBy 17 | }; 18 | -------------------------------------------------------------------------------- /src/lib/metaskUtils.ts: -------------------------------------------------------------------------------- 1 | export const metamaskUtils = { 2 | registerToken: async (tokenAddress: string, tokenSymbol: string, tokenDecimals: number, tokenImage: string) => { 3 | //@ts-ignore 4 | 5 | const tokenAdded = await window.ethereum.request({ 6 | //@ts-ignore 7 | method: 'wallet_watchAsset', 8 | params: { 9 | //@ts-ignore 10 | type: 'ERC20', 11 | options: { 12 | address: tokenAddress, 13 | symbol: tokenSymbol, 14 | decimals: tokenDecimals, 15 | image: tokenImage 16 | } 17 | } 18 | }); 19 | 20 | return tokenAdded; 21 | }, 22 | setupNetwork: async ({ 23 | chainId, 24 | chainName, 25 | rpcUrls, 26 | blockExplorerUrls, 27 | nativeCurrency 28 | }: { 29 | chainId: number; 30 | chainName: string; 31 | rpcUrls: string[]; 32 | blockExplorerUrls: string[]; 33 | nativeCurrency: { 34 | name: string; 35 | symbol: string; 36 | decimals: number; 37 | }; 38 | }) => { 39 | return new Promise(async (resolve, reject) => { 40 | //@ts-ignore 41 | const provider = window.ethereum; 42 | if (provider) { 43 | try { 44 | await provider.request({ 45 | method: 'wallet_switchEthereumChain', 46 | params: [{ chainId: `0x${chainId.toString(16)}` }] 47 | }); 48 | resolve(true); 49 | } catch (switchError) { 50 | // This error code indicates that the chain has not been added to MetaMask. 51 | if (switchError.code === 4902) { 52 | try { 53 | await provider.request({ 54 | method: 'wallet_addEthereumChain', 55 | params: [ 56 | { 57 | chainId: `0x${chainId.toString(16)}`, 58 | chainName, 59 | nativeCurrency, 60 | rpcUrls, 61 | blockExplorerUrls 62 | } 63 | ] 64 | }); 65 | resolve(true); 66 | return true; 67 | } catch (error) { 68 | reject(error); 69 | return false; 70 | } 71 | } else { 72 | reject(switchError); 73 | } 74 | } 75 | } else { 76 | console.error("Can't setup the BSC network on metamask because window.ethereum is undefined"); 77 | reject(false); 78 | return false; 79 | } 80 | }); 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /src/lib/smartgraph/antenna.plugin.ts: -------------------------------------------------------------------------------- 1 | import { SmartGraph } from '@smartgraph/core'; 2 | import { extendInputType } from 'nexus'; 3 | 4 | export const AntennaPlugin = SmartGraph.Plugin(() => { 5 | return { 6 | name: 'AntennaPlugin', 7 | types: [ 8 | extendInputType({ 9 | type: 'CrossChainCalls', 10 | definition(t) { 11 | t.field('antenna', { 12 | type: 'Boolean' 13 | }); 14 | } 15 | }) 16 | ] 17 | }; 18 | }); 19 | 20 | export class TemplateService { 21 | static async getPrice({ ctx, address, root, ttl = 60 }: { ctx: SmartGraph['Context']; address: string; root: any; ttl?: number }) { 22 | return ctx.smartGraph.wrap(`${root.chainId}-Template.getPrice-${address}`, async () => {}, { ttl: 60 }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/lib/smartgraph/gql.ts: -------------------------------------------------------------------------------- 1 | import request from 'graphql-request'; 2 | import { execute, parse } from 'graphql'; 3 | import { Thunder } from '../__generated/sdk'; 4 | import { schema, smartGraph } from './index'; 5 | 6 | // http client 7 | export const gql = Thunder(async (query, variables) => { 8 | const res = await request('http://localhost:3000/api/graphql', query, variables); 9 | return res; 10 | }); 11 | 12 | // jsonrpc client 13 | export const rpc = Thunder(async (query, variables) => { 14 | const res = await execute({ 15 | schema, 16 | contextValue: { smartGraph }, 17 | //@ts-ignore 18 | variableValues: variables, 19 | //@ts-ignore 20 | document: parse(query) 21 | }); 22 | if (res.errors) { 23 | throw new Error(res.errors[0].message); 24 | } 25 | return res.data; 26 | }); 27 | 28 | export const use = (client: 'rpc' | 'graphql') => (client == 'rpc' ? rpc : gql); 29 | -------------------------------------------------------------------------------- /src/lib/smartgraph/index.ts: -------------------------------------------------------------------------------- 1 | import { makeSchema, declarativeWrappingPlugin } from 'nexus'; 2 | import { SmartGraph } from '@smartgraph/core'; 3 | import { plugins } from '@smartgraph/plugins'; 4 | import { encodeInputData } from 'iotex-antenna/lib/contract/abi-to-byte'; 5 | 6 | import schemaJson from './schema.json'; 7 | import { UniswapPlugin } from './uniswap.plugin'; 8 | import { AntennaPlugin } from './antenna.plugin'; 9 | import { _ } from '../lodash'; 10 | import { helper } from '@/lib/helper'; 11 | import { from } from '@iotexproject/iotex-address-ts'; 12 | 13 | export const smartGraph = new SmartGraph({ 14 | plugins: [plugins.NetworkPlugin(), plugins.ERC20Extension(), plugins.LpTokenExtension(), UniswapPlugin(), AntennaPlugin()], 15 | //@ts-ignore 16 | encodeFunction(args: { contract: string; method: string; params: any[]; root }) { 17 | const contract = smartGraph.getContract(args.contract); 18 | 19 | if (args.root.antenna) { 20 | const abi = _.keyBy(contract.abi, 'name'); 21 | 22 | let params = args.params.reduce((p, c, i) => { 23 | p[abi[args.method].inputs[i].name] = c; 24 | return p; 25 | }, {}); 26 | params = helper.object.crawlObject(params, { 27 | string(val: string) { 28 | if (val.startsWith('0x')) { 29 | return from(val).string(); 30 | } 31 | return val; 32 | } 33 | }); 34 | console.log(params); 35 | 36 | // console.log(params, _.keyBy(contract.abi, 'name')['swapETHForExactTokens']['inputs'], args.method); 37 | return encodeInputData(abi, args.method, params); 38 | } 39 | return contract.interface.encodeFunctionData(args.method, args.params); 40 | } 41 | }); 42 | 43 | export const schema: any = makeSchema({ 44 | types: smartGraph.parseAbi({ schema: schemaJson }), 45 | plugins: [declarativeWrappingPlugin()] 46 | }); 47 | -------------------------------------------------------------------------------- /src/lib/trpc.ts: -------------------------------------------------------------------------------- 1 | import { httpBatchLink } from '@trpc/client'; 2 | import { createTRPCNext } from '@trpc/next'; 3 | import type { AppRouter } from '../server/routers/_app'; 4 | 5 | function getBaseUrl() { 6 | if (typeof window !== 'undefined') 7 | // browser should use relative path 8 | return ''; 9 | 10 | if (process.env.VERCEL_URL) 11 | // reference for vercel.com 12 | return `https://${process.env.VERCEL_URL}`; 13 | 14 | if (process.env.RENDER_INTERNAL_HOSTNAME) 15 | // reference for render.com 16 | return `http://${process.env.RENDER_INTERNAL_HOSTNAME}:${process.env.PORT}`; 17 | 18 | // assume localhost 19 | return `http://localhost:${process.env.PORT ?? 3000}`; 20 | } 21 | 22 | export const trpc = createTRPCNext({ 23 | config({ ctx }) { 24 | return { 25 | links: [ 26 | httpBatchLink({ 27 | /** 28 | * If you want to use SSR, you need to use the server's full URL 29 | * @link https://trpc.io/docs/ssr 30 | **/ 31 | url: `${getBaseUrl()}/api/trpc`, 32 | }), 33 | ], 34 | /** 35 | * @link https://tanstack.com/query/v4/docs/reference/QueryClient 36 | **/ 37 | // queryClientConfig: { defaultOptions: { queries: { staleTime: 60 } } }, 38 | }; 39 | }, 40 | /** 41 | * @link https://trpc.io/docs/ssr 42 | **/ 43 | ssr: true, 44 | }); -------------------------------------------------------------------------------- /src/lib/wagmi.ts: -------------------------------------------------------------------------------- 1 | import { WagmiConfig, createClient } from 'wagmi'; 2 | import { getDefaultProvider } from 'ethers'; 3 | import { InjectedConnector } from 'wagmi/connectors/injected'; 4 | 5 | const client = createClient({ 6 | autoConnect: true, 7 | provider: getDefaultProvider() 8 | }); 9 | -------------------------------------------------------------------------------- /src/lib/web3-react.ts: -------------------------------------------------------------------------------- 1 | import { publicConfig } from './../config/public'; 2 | import { Web3Provider } from '@ethersproject/providers'; 3 | import { InjectedConnector, NoEthereumProviderError, UserRejectedRequestError } from '@web3-react/injected-connector'; 4 | // import { WalletConnectConnector, UserRejectedRequestError as UserRejectedRequestErrorWalletConnect } from '@iotexproject/walletconnect-connector'; 5 | import { UnsupportedChainIdError } from '@web3-react/core'; 6 | 7 | const POLLING_INTERVAL = 12000; 8 | export const RPC_URLS = { 9 | 1: `https://rpc.ankr.com/eth`, 10 | 42: `https://kovan.infura.io/v3/${publicConfig.infuraId}`, 11 | 56: 'https://rpc.ankr.com/bsc', 12 | 97: 'https://data-seed-prebsc-1-s1.binance.org:8545', 13 | 4689: 'https://babel-api.mainnet.iotex.io/', 14 | 4690: `https://babel-api.testnet.iotex.io`, 15 | 137: 'https://polygon-rpc.com/' 16 | }; 17 | 18 | export const allowChains = Object.keys(RPC_URLS).map((i) => Number(i)); 19 | 20 | export function getLibrary(provider: any): Web3Provider { 21 | const library = new Web3Provider(provider); 22 | library.pollingInterval = POLLING_INTERVAL; 23 | return library; 24 | } 25 | 26 | export const injected = new InjectedConnector({ supportedChainIds: allowChains }); 27 | 28 | // export const walletconnect = new WalletConnectConnector({ 29 | // rpc: { 4689: RPC_URLS[4689] }, 30 | // bridge: 'https://bridge.walletconnect.org', 31 | // qrcode: true, 32 | // pollingInterval: POLLING_INTERVAL 33 | // }); 34 | 35 | export function getErrorMessage(error: Error) { 36 | if (error instanceof NoEthereumProviderError) { 37 | return 'No Ethereum browser extension detected, install MetaMask on desktop or visit from a dApp browser on mobile.'; 38 | } else if (error instanceof UnsupportedChainIdError) { 39 | return "You're connected to an unsupported network."; 40 | // } else if (error instanceof UserRejectedRequestError || error instanceof UserRejectedRequestErrorWalletConnect) { 41 | // return 'Please authorize this website to access your Ethereum account.'; 42 | } else { 43 | console.error(error); 44 | return 'An unknown error occurred. Check the console for more details.'; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | export default function Custom404() { 4 | return

404 - Page Not Found

5 | } 6 | -------------------------------------------------------------------------------- /src/pages/500.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | export default function Custom500() { 4 | return

500 - Page Not Found

5 | } 6 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import React, { useEffect, useMemo, useState } from 'react'; 3 | import { Web3ReactProvider } from '@web3-react/core'; 4 | import type { AppProps } from 'next/app'; 5 | import superjson from 'superjson'; 6 | 7 | import { httpBatchLink } from '@trpc/client/links/httpBatchLink'; 8 | import { loggerLink } from '@trpc/client/links/loggerLink'; 9 | 10 | import { useStore } from '@/store/index'; 11 | import { getLibrary } from '../lib/web3-react'; 12 | import { WalletSelecter } from '../components/WalletSelecter/index'; 13 | import { AppRouter } from '@/server/routers/_app'; 14 | import { MantineProvider, ColorSchemeProvider, ColorScheme, Global } from '@mantine/core'; 15 | import { observer, useLocalObservable } from 'mobx-react-lite'; 16 | import { helper } from '../lib/helper'; 17 | import { NotificationsProvider } from '@mantine/notifications'; 18 | import '../i18n/config'; 19 | import { smartGraph } from '../lib/smartgraph/index'; 20 | import { WrongNetworkDialog } from '@/components/NetworkCheckProvider'; 21 | import { TransactionSubmitDialog } from '@/components/HistoryModal'; 22 | import { WagmiProvider } from '@/components/WagmiProvider'; 23 | import { withTRPC } from '@trpc/next'; 24 | import { trpc } from '@/lib/trpc'; 25 | 26 | function MyApp({ Component, pageProps }: AppProps) { 27 | const { lang, god, user } = useStore(); 28 | const store = useLocalObservable(() => ({ 29 | get colorScheme() { 30 | return user.theme.value || ('dark' as ColorScheme); 31 | } 32 | })); 33 | 34 | useEffect(() => { 35 | lang.init(); 36 | // setInterval(() => { 37 | // god.pollingData(); 38 | // }, 15000); 39 | smartGraph.event.on('provider.newBlock', (chainId, blockNumber) => { 40 | console.log('new block', chainId, blockNumber); 41 | if (chainId == god.currentChain.chainId) { 42 | god.pollingData(); 43 | } 44 | }); 45 | }, []); 46 | 47 | if (!helper.env.isBrower) { 48 | return
; 49 | } 50 | 51 | // use useMemo to fix issue https://github.com/vercel/next.js/issues/12010 52 | return ( 53 | <> 54 | {/* */} 55 | 56 | ({ 58 | body: {} 59 | })} 60 | /> 61 | 62 | 63 | 64 | 65 | {/* */} 66 | 67 | 68 | 69 | 70 | {/* */} 71 | {/*
*/} 72 | 73 | 74 | 75 | {/* */} 76 | 77 | ); 78 | } 79 | 80 | export default trpc.withTRPC(observer(MyApp)); 81 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { createGetInitialProps } from '@mantine/next'; 2 | import Document, { Head, Html, Main, NextScript } from 'next/document'; 3 | 4 | const getInitialProps = createGetInitialProps(); 5 | 6 | export default class _Document extends Document { 7 | static getInitialProps = getInitialProps; 8 | 9 | render() { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/pages/api/auth/jwt.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import jwt from 'jsonwebtoken'; 3 | import { SiweMessage } from 'siwe'; 4 | 5 | const encode = (jwtClaims: { sub: string; name: string; iat: number; exp: number }) => { 6 | return jwt.sign(jwtClaims, process.env.JWT_SECRET, { algorithm: 'HS256' }); 7 | }; 8 | 9 | const decode = (token: string) => { 10 | return jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'] }); 11 | }; 12 | 13 | const verify = async (message: string, signature: string) => { 14 | const siweMessage = new SiweMessage(message); 15 | try { 16 | await siweMessage.validate(signature); 17 | return siweMessage; 18 | } catch { 19 | return null; 20 | } 21 | }; 22 | 23 | const generateJWT = (address: string) => { 24 | const iat = Date.now() / 1000; 25 | const exp = Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60; 26 | const token = encode({ 27 | sub: address, 28 | name: address, 29 | iat, 30 | exp 31 | }); 32 | 33 | return { 34 | token, 35 | exp 36 | }; 37 | }; 38 | 39 | const handler = async (req: NextApiRequest, res: NextApiResponse) => { 40 | if (req.method === 'POST') { 41 | const { message, signature } = req.body; 42 | if (!message || !signature) { 43 | res.status(400).json({ message: 'invalid request' }); 44 | return; 45 | } 46 | const siweMessage = await verify(message, signature); 47 | if (!siweMessage) { 48 | res.status(400).json({ message: 'invalid signature' }); 49 | return; 50 | } 51 | const jwt = generateJWT(siweMessage.address); 52 | res.status(200).json(jwt); 53 | } else { 54 | res.status(405).json({ message: 'Method Not Allowed' }); 55 | } 56 | }; 57 | 58 | export default handler; 59 | -------------------------------------------------------------------------------- /src/pages/api/auth/nonce.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import { generateNonce } from 'siwe'; 3 | 4 | const handler = async (req: NextApiRequest, res: NextApiResponse) => { 5 | if (req.method === 'GET') { 6 | res.status(200).json({ 7 | nonce: generateNonce() 8 | }); 9 | } else { 10 | res.status(405).json({ message: 'Method Not Allowed' }); 11 | } 12 | }; 13 | 14 | export default handler; 15 | -------------------------------------------------------------------------------- /src/pages/api/graphql.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from '@graphql-yoga/node'; 2 | import { NextApiRequest, NextApiResponse } from 'next'; 3 | import { smartGraph, schema } from '../../lib/smartgraph'; 4 | 5 | const server = createServer<{ 6 | req: NextApiRequest; 7 | res: NextApiResponse; 8 | }>({ 9 | schema, 10 | context: ({ req, res }) => { 11 | return { smartGraph, req, res }; 12 | }, 13 | }); 14 | 15 | export default server; 16 | -------------------------------------------------------------------------------- /src/pages/api/trpc/[trpc].ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains tRPC's HTTP response handler 3 | */ 4 | import * as trpcNext from '@trpc/server/adapters/next'; 5 | import { createContext } from '@/server/context'; 6 | import { appRouter } from '@/server/routers/_app'; 7 | 8 | export default trpcNext.createNextApiHandler({ 9 | router: appRouter, 10 | /** 11 | * @link https://trpc.io/docs/context 12 | */ 13 | createContext, 14 | /** 15 | * @link https://trpc.io/docs/error-handling 16 | */ 17 | onError({ error }) { 18 | if (error.code === 'INTERNAL_SERVER_ERROR') { 19 | // send to bug reporting 20 | console.error('Something went wrong', error); 21 | } 22 | }, 23 | /** 24 | * Enable query batching 25 | */ 26 | batching: { 27 | enabled: true 28 | } 29 | /** 30 | * @link https://trpc.io/docs/caching#api-response-caching 31 | */ 32 | // responseMeta() { 33 | // // ... 34 | // }, 35 | }); 36 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { createStyles, Container, Text, Button, Group, useMantineTheme } from '@mantine/core'; 3 | import MainLayout from '@/components/Layout'; 4 | import { useTranslation } from 'react-i18next'; 5 | import { useStore } from '../store'; 6 | import { LanguageSwitch } from '@/components/LanguageSwitch'; 7 | import { helper } from '@/lib/helper'; 8 | import { WagmiProvider } from '@/components/WagmiProvider'; 9 | 10 | const BREAKPOINT = '@media (max-width: 755px)'; 11 | 12 | const useStyles = createStyles((theme) => ({ 13 | wrapper: { 14 | position: 'relative', 15 | boxSizing: 'border-box', 16 | backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[8] : theme.white 17 | }, 18 | 19 | inner: { 20 | position: 'relative', 21 | paddingTop: 200, 22 | paddingBottom: 120, 23 | 24 | [BREAKPOINT]: { 25 | paddingBottom: 80, 26 | paddingTop: 80 27 | } 28 | }, 29 | 30 | title: { 31 | fontFamily: `Greycliff CF, ${theme.fontFamily}`, 32 | fontSize: 62, 33 | fontWeight: 900, 34 | lineHeight: 1.1, 35 | margin: 0, 36 | padding: 0, 37 | color: theme.colorScheme === 'dark' ? theme.white : theme.black, 38 | 39 | [BREAKPOINT]: { 40 | fontSize: 42, 41 | lineHeight: 1.2 42 | } 43 | }, 44 | 45 | description: { 46 | marginTop: theme.spacing.xl, 47 | fontSize: 24, 48 | 49 | [BREAKPOINT]: { 50 | fontSize: 18 51 | } 52 | }, 53 | 54 | controls: { 55 | marginTop: theme.spacing.xl * 2, 56 | 57 | [BREAKPOINT]: { 58 | marginTop: theme.spacing.xl 59 | } 60 | }, 61 | 62 | control: { 63 | height: 54, 64 | paddingLeft: 38, 65 | paddingRight: 38, 66 | 67 | [BREAKPOINT]: { 68 | height: 54, 69 | paddingLeft: 18, 70 | paddingRight: 18, 71 | flex: 1 72 | } 73 | }, 74 | 75 | githubControl: { 76 | borderWidth: 2, 77 | borderColor: theme.colorScheme === 'dark' ? 'transparent' : theme.colors.dark[9], 78 | backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[5] : 'transparent', 79 | 80 | '&:hover': { 81 | backgroundColor: `${theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[0]} !important` 82 | } 83 | } 84 | })); 85 | 86 | export default function HeroTitle() { 87 | const { user, god } = useStore(); 88 | const { classes, cx } = useStyles(); 89 | const theme = useMantineTheme(); 90 | const { t } = useTranslation(); 91 | 92 | useEffect(() => { 93 | user.enableNetworkChecker(window?.location?.pathname, Object.keys(god.network.chain.map).map(i=>Number(i))); 94 | }, []); 95 | 96 | return ( 97 | 98 |
99 | 100 |

101 | {t('a')}{' '} 102 | 103 | {t('next-generation')} 104 | {' '} 105 | {t('dapp-dev-framework')} 106 |

107 | 108 | 109 | {t('tip')} 110 | 111 | 112 | 113 | 116 | 117 | 127 | 128 | 129 | 130 |
131 |
132 |
133 | ); 134 | } 135 | -------------------------------------------------------------------------------- /src/pages/swap.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer, useLocalObservable } from 'mobx-react-lite'; 3 | import { Container, LoadingOverlay, SegmentedControl, Button } from '@mantine/core'; 4 | import { useEffect } from 'react'; 5 | import { rpc, gql } from '../lib/smartgraph/gql'; 6 | import { plainToInstance, plainToClass, classToPlain, instanceToPlain, plainToClassFromExist } from 'class-transformer'; 7 | import { UniswapRouterEntity } from '../store/entity'; 8 | import MainLayout from '../components/Layout/index'; 9 | import { Prism } from '@mantine/prism'; 10 | import { PromiseState } from '@/store/standard/PromiseState'; 11 | import { useStore } from '../store/index'; 12 | 13 | const demoCode = ` 14 | 15 | // smartgraph.ts 16 | import { SmartGraph } from '@smartgraph/core'; 17 | export const smartGraph = new SmartGraph({ 18 | plugins: [plugins.NetworkPlugin(), plugins.ERC20Extension(), plugins.LpTokenExtension(), UniswapPlugin()] 19 | }); 20 | ... 21 | 22 | //page.tsx 23 | import {rpc} from "@/src/lib/smartgraph/gql" 24 | 25 | const swapQuery = await rpc('query')({ 26 | UniswapRouter: [ { calls: [{ address: '0x147CdAe2BF7e809b9789aD0765899c06B361C5cE', chainId: 4689 }] }, { 27 | address: true, 28 | chainId: true, 29 | swap: [{args: {sellToken: 'IOTX',buyToken: '0xb8744ae4032be5e5ef9fab94ee9c3bf38d5d2ae0', buyAmount: 1000000000000000000,recipient: '0x2AcB8663B18d8c8180783C18b88D60b86de26dF2'}}, { 30 | amount: true, 31 | data: true, 32 | router: true, 33 | value: true, 34 | path: { 35 | address: true, 36 | symbol: true, 37 | decimals: true, 38 | totalSupply: true 39 | } 40 | } 41 | ] 42 | } 43 | ] 44 | }); 45 | return plainToInstance(UniswapRouter, data.UniswapRouter[0]); 46 | 47 | 50 | 51 | `; 52 | 53 | export const Home = observer(() => { 54 | const { god, ledger } = useStore(); 55 | const store = useLocalObservable(() => ({ 56 | swapQuery: new PromiseState({ 57 | function: async ({ buyAmount = '1000000000000000000' }: { buyAmount?: string } = {}): Promise => { 58 | console.log(ledger.ledger.value); 59 | 60 | const data = await rpc('query')({ 61 | UniswapRouter: [ 62 | { calls: [{ address: '0x147CdAe2BF7e809b9789aD0765899c06B361C5cE', chainId: 4689, antenna: ledger.ledger.value }] }, 63 | { 64 | address: true, 65 | chainId: true, 66 | swap: [ 67 | { 68 | args: { 69 | sellToken: 'IOTX', 70 | buyToken: '0xb8744ae4032be5e5ef9fab94ee9c3bf38d5d2ae0', 71 | buyAmount, 72 | recipient: '0xd023cfc198718bf1133ed99beadc227e340fd6d3' 73 | } 74 | }, 75 | { 76 | amount: true, 77 | data: true, 78 | router: true, 79 | value: true, 80 | path: { 81 | address: true, 82 | symbol: [{}, true], 83 | decimals: [{}, true], 84 | totalSupply: [{}, true] 85 | } 86 | } 87 | ] 88 | } 89 | ] 90 | }); 91 | if (store.swapQuery.value) { 92 | return plainToClassFromExist(store.swapQuery.value, data.UniswapRouter[0]); 93 | } 94 | 95 | const instance = plainToInstance(UniswapRouterEntity, data.UniswapRouter[0]); 96 | return instance; 97 | } 98 | }), 99 | async handleSwap() { 100 | if (!store.swapQuery.value.swap) { 101 | await store.swapQuery.call(); 102 | } 103 | store.swapQuery.value._swap.call(); 104 | } 105 | })); 106 | useEffect(() => { 107 | store.swapQuery.call(); 108 | }, []); 109 | return ( 110 | 111 | {demoCode} 112 | 115 |
116 |         
117 |         {JSON.stringify(store.swapQuery.value?.swap || {}, null, 2)}
118 |       
119 |
120 | ); 121 | }); 122 | 123 | export default Home; 124 | -------------------------------------------------------------------------------- /src/server/context.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import * as trpc from '@trpc/server'; 3 | import * as trpcNext from '@trpc/server/adapters/next'; 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 6 | interface CreateContextOptions { 7 | // session: Session | null 8 | } 9 | 10 | /** 11 | * Inner function for `createContext` where we create the context. 12 | * This is useful for testing when we don't want to mock Next.js' request/response 13 | */ 14 | export async function createContextInner(_opts: CreateContextOptions) { 15 | return {}; 16 | } 17 | 18 | export type Context = trpc.inferAsyncReturnType; 19 | 20 | /** 21 | * Creates context for an incoming request 22 | * @link https://trpc.io/docs/context 23 | */ 24 | export async function createContext(opts: trpcNext.CreateNextContextOptions): Promise { 25 | // for API-response caching see https://trpc.io/docs/caching 26 | 27 | const ctx = await createContextInner({}); 28 | return ctx; 29 | } 30 | -------------------------------------------------------------------------------- /src/server/createRouter.ts: -------------------------------------------------------------------------------- 1 | import { Context } from './context'; 2 | import * as trpc from '@trpc/server'; 3 | 4 | /** 5 | * Helper function to create a router with context 6 | */ 7 | export function createRouter() { 8 | return trpc.router(); 9 | } 10 | -------------------------------------------------------------------------------- /src/server/routers/_app.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { procedure, router } from '../trpc'; 3 | 4 | export const appRouter = router({ 5 | healthz: procedure 6 | .query(async () => { 7 | return 'ok'; 8 | } 9 | ) 10 | }); 11 | 12 | // export type definition of API 13 | export type AppRouter = typeof appRouter; -------------------------------------------------------------------------------- /src/server/routers/template.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { procedure, router } from '../trpc'; 3 | export const templateRouter = router({ 4 | whoami: procedure 5 | .input( 6 | z.object({ 7 | name: z.string() 8 | }) 9 | ) 10 | .query(async ({ input }) => { 11 | const { name } = input; 12 | return name; 13 | }) 14 | }); 15 | -------------------------------------------------------------------------------- /src/server/trpc.ts: -------------------------------------------------------------------------------- 1 | import { TRPCError, initTRPC } from '@trpc/server'; 2 | 3 | // Avoid exporting the entire t-object 4 | // since it's not very descriptive. 5 | // For instance, the use of a t variable 6 | // is common in i18n libraries. 7 | const t = initTRPC.create(); 8 | 9 | // Base router and procedure helpers 10 | export const router = t.router; 11 | export const procedure = t.procedure; -------------------------------------------------------------------------------- /src/store/entity.ts: -------------------------------------------------------------------------------- 1 | import { Expose, Transform, Type } from 'class-transformer'; 2 | import { IUniswapRouter, IErc20, IAmount } from '../lib/__generated/typing'; 3 | import { PromiseState } from './standard/PromiseState'; 4 | import { helper } from '../lib/helper'; 5 | 6 | export class ERC20Entity extends IErc20 { 7 | @Expose() 8 | get logo() { 9 | return `https://iotexproject.iotex.io/iotex-token-metadata/master/images/${this.address}.png`; 10 | } 11 | test() { 12 | console.log('test'); 13 | } 14 | } 15 | 16 | export class SwapEntity extends IAmount { 17 | foo = 123; 18 | @Type(() => ERC20Entity) 19 | path: ERC20Entity[] = []; 20 | @Expose() 21 | get bar() { 22 | return 456; 23 | } 24 | } 25 | 26 | export class UniswapRouterEntity extends IUniswapRouter { 27 | @Type(() => SwapEntity) 28 | swap: SwapEntity = null; 29 | 30 | _swap = new PromiseState({ 31 | function: async () => { 32 | const { data, value } = this.swap; 33 | await helper.c 34 | .sendTx({ 35 | address: this.address, 36 | chainId: this.chainId, 37 | data, 38 | value 39 | }) 40 | .finally(() => { 41 | this.swap = null; 42 | console.log(this.swap); 43 | }); 44 | } 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /src/store/god.ts: -------------------------------------------------------------------------------- 1 | import { makeAutoObservable } from 'mobx'; 2 | import { MappingState } from './standard/MappingState'; 3 | import { ChainState } from './lib/ChainState'; 4 | import { EthNetworkState } from './lib/EthNetworkState'; 5 | import RootStore from './root'; 6 | import { BooleanState, NumberState } from './standard/base'; 7 | import { eventBus } from '../lib/event'; 8 | import { rpc } from '../lib/smartgraph/gql'; 9 | import { CoinState } from './lib/CoinState'; 10 | import { ethers } from 'ethers'; 11 | import { defaultNetworks } from '../config/chain'; 12 | import { metamaskUtils } from '../lib/metaskUtils'; 13 | 14 | export class GodStore { 15 | rootStore: RootStore; 16 | network: EthNetworkState; 17 | showTransactionSubmitDialog = new BooleanState(); 18 | curTransaction: ethers.providers.TransactionResponse = null; 19 | updateTicker = new NumberState(); 20 | 21 | constructor(rootStore: RootStore) { 22 | this.rootStore = rootStore; 23 | makeAutoObservable(this, { 24 | rootStore: false 25 | }); 26 | this.network = new EthNetworkState({ 27 | // allowChains: data.networks.map((i) => i.chainId), 28 | god: this, 29 | chain: new MappingState({ 30 | currentId: 4689, 31 | map: defaultNetworks 32 | .map( 33 | (i) => 34 | new ChainState({ 35 | name: i.name, 36 | chainId: i.chainId, 37 | explorerName: i.explorerName, 38 | explorerURL: i.explorerUrl, 39 | info: { theme: { bgGradient: '' } }, 40 | logoUrl: i.logoUrl, 41 | rpcUrl: i.rpcUrl, 42 | //@ts-ignore 43 | type: i.type, 44 | Coin: new CoinState({ 45 | symbol: i.nativeCoin, 46 | decimals: 18 47 | }) 48 | }) 49 | ) 50 | .reduce((p, c) => { 51 | p[c.chainId] = c; 52 | return p; 53 | }, {}) 54 | }) 55 | }); 56 | } 57 | 58 | get eth(): EthNetworkState { 59 | return this.network; 60 | } 61 | 62 | get isConnect() { 63 | return !!this.currentNetwork.account; 64 | } 65 | get currentNetwork() { 66 | return this.network; 67 | } 68 | get currentChain(): ChainState { 69 | return this.currentNetwork.currentChain; 70 | } 71 | get Coin() { 72 | return this.currentChain.Coin; 73 | } 74 | 75 | getNetworkByChainId(chainId: number) { 76 | return this.currentNetwork.chain.map[chainId]; 77 | } 78 | 79 | async switchChain(chianId: number) { 80 | const chain = this.currentNetwork.chain.map[chianId]; 81 | try { 82 | await metamaskUtils.setupNetwork({ 83 | chainId: chain.chainId, 84 | blockExplorerUrls: [chain.explorerURL], 85 | chainName: chain.name, 86 | nativeCurrency: { 87 | decimals: chain.Coin.decimals || 18, 88 | name: chain.Coin.symbol, 89 | symbol: chain.Coin.symbol 90 | }, 91 | rpcUrls: [chain.rpcUrl] 92 | }); 93 | this.setChainId(chianId); 94 | eventBus.emit('chain.switch'); 95 | } catch (error) { 96 | console.log(error); 97 | } 98 | } 99 | setChainId(chianId) { 100 | this.currentNetwork.chain.setCurrentId(chianId); 101 | } 102 | setShowConnecter(value: boolean) { 103 | this.eth.connector.showConnector = value; 104 | } 105 | 106 | pollingData() { 107 | this.updateTicker.setValue(this.updateTicker.value + 1); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/store/history.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { makeAutoObservable } from 'mobx'; 3 | import { StorageState } from './standard/StorageState'; 4 | import RootStore from './root'; 5 | import { eventBus } from '@/lib/event'; 6 | import { TransactionReceipt } from '@ethersproject/providers'; 7 | 8 | export interface ITransactionHistoryItem { 9 | text: string; 10 | hash: string; 11 | amount: string; 12 | explorerURL: string; 13 | status?: number; 14 | isRead?: boolean; 15 | } 16 | 17 | export type TransactionResultStatus = 'loading' | 'success' | 'fail'; 18 | export type TransactionModule = 'Farm' | 'veFarm' | 'Swap' | 'PICO Launch' | 'DAO' | 'Faucet'; 19 | export type TransctionCoin = { 20 | name?: string; 21 | address?: string; 22 | logo?: string; 23 | decimals?: number; 24 | }; 25 | 26 | export interface TransactionItem { 27 | uuid: string; 28 | chainId: number; 29 | timestamp: number; //s 30 | from: string; 31 | to: string; 32 | amount: string; 33 | coin?: TransctionCoin; 34 | 35 | module: TransactionModule; 36 | title: string; //like 'Withdraw LP Token' 'Deposit LP Token' 'Claim mimo' 37 | 38 | isRead: boolean; 39 | hash: string; 40 | 41 | status: TransactionResultStatus; 42 | } 43 | 44 | export class TransactionHistoryStore { 45 | transactionHistory = new StorageState({ key: 'Global.TransactionHistory', default: [] }); 46 | // curTransactionResult: TransactionItem; 47 | rootStore: RootStore; 48 | isOpen: boolean = false; 49 | isTransactionDialogOpen: boolean = false; 50 | curTransactionHistoryItem: TransactionItem; 51 | filterParams = { 52 | module: 'Farm', 53 | to: '', 54 | from: '', 55 | chainId: 0 56 | }; 57 | // moduleList = ['Farm', 'veFarm', 'Swap', 'PICO Launch', 'DAO']; 58 | moduleList = ['Swap']; 59 | 60 | constructor(rootStore: RootStore) { 61 | this.rootStore = rootStore; 62 | this.initEvent(); 63 | this.filterParams.module = this.moduleList[0]; 64 | makeAutoObservable(this); 65 | } 66 | 67 | toggleOpen() { 68 | this.isOpen = !this.isOpen; 69 | } 70 | 71 | toggleTransactionDialog() { 72 | this.isTransactionDialogOpen = !this.isTransactionDialogOpen; 73 | } 74 | 75 | get unreadCount() { 76 | return this.history.filter((item) => !item.isRead).length; 77 | } 78 | 79 | get god() { 80 | return this.rootStore.god; 81 | } 82 | 83 | get history() { 84 | let transactionHistory = this.transactionHistory.value 85 | .slice() 86 | .reverse() 87 | .filter((item: TransactionItem) => { 88 | if (!this.filterParams.module) return true; 89 | return this.filterParams.module.toLowerCase() == item?.module?.toLowerCase(); 90 | }) 91 | .filter((item: TransactionItem) => { 92 | if (!this.filterParams.from) return true; 93 | return this.filterParams.from.toLowerCase() == item?.from?.toLowerCase(); 94 | }) 95 | .filter((item: TransactionItem) => { 96 | if (!this.filterParams.to) return true; 97 | return this.filterParams.to.toLowerCase() == item?.to?.toLowerCase(); 98 | }) 99 | .filter((item: TransactionItem) => { 100 | if (!this.rootStore.god.currentChain.chainId) return true; 101 | return this.rootStore.god.currentChain.chainId == item?.chainId; 102 | }); 103 | return transactionHistory; 104 | } 105 | 106 | get lastestHistory() { 107 | return _.orderBy(this.history, ['timestamp'], ['desc']); 108 | } 109 | 110 | setValue(value: Partial) { 111 | Object.assign(this, value); 112 | } 113 | 114 | setFilterMoudle(module: string) { 115 | this.filterParams.module = module; 116 | } 117 | 118 | setFilterFrom(from: string) { 119 | this.filterParams.from = from; 120 | } 121 | 122 | setFilterTo(to: string) { 123 | this.filterParams.to = to; 124 | } 125 | 126 | insertHistoryFromParma({ uuid, chainId, amount, module, title }: { uuid: string; chainId: number; amount: string; module: TransactionModule; title: string }) { 127 | this.insertHistory({ 128 | chainId, 129 | amount, 130 | module, 131 | title, 132 | uuid, 133 | timestamp: Date.now(), 134 | from: null, 135 | to: null, 136 | isRead: false, 137 | hash: null, 138 | status: 'loading' 139 | }); 140 | } 141 | 142 | updateHistoryFromParma({ receipt, uuid, showDialog }: { receipt: TransactionReceipt; uuid: string; showDialog: () => TransctionCoin }) { 143 | let transctionItem: any = { 144 | uuid, 145 | timestamp: Date.now(), 146 | from: receipt.from, 147 | to: receipt.to, 148 | hash: receipt.transactionHash, 149 | status: 'success' 150 | }; 151 | 152 | if (showDialog) { 153 | const coin = showDialog(); 154 | transctionItem = { 155 | ...transctionItem, 156 | ...coin 157 | }; 158 | } 159 | 160 | this.updateHistory(transctionItem); 161 | } 162 | 163 | insertHistory(transactionItem: TransactionItem) { 164 | this.transactionHistory.value.push(transactionItem); 165 | this.transactionHistory.save(this.transactionHistory.value); 166 | this.curTransactionHistoryItem = transactionItem; 167 | if (this.transactionHistory.value.hasOwnProperty('coin')) { 168 | this.isTransactionDialogOpen = true; 169 | } 170 | } 171 | 172 | updateHistory(transactionItem: TransactionItem) { 173 | const index = this.transactionHistory.value.findIndex((item) => item.uuid === transactionItem.uuid); 174 | this.transactionHistory.value[index] = Object.assign(this.transactionHistory.value[index], transactionItem); 175 | this.transactionHistory.save(this.transactionHistory.value); 176 | this.curTransactionHistoryItem = this.transactionHistory.value[index]; 177 | } 178 | 179 | clearHitoryRead() { 180 | this.transactionHistory.value.forEach((item) => { 181 | item.isRead = true; 182 | }); 183 | this.transactionHistory.save(this.transactionHistory.value); 184 | } 185 | 186 | deleteHistory(transactionItem: Pick) { 187 | const index = this.transactionHistory.value.findIndex((item) => item.uuid === transactionItem.uuid); 188 | this.transactionHistory.value.splice(index, 1); 189 | this.transactionHistory.save(this.transactionHistory.value); 190 | } 191 | 192 | initEvent() { 193 | eventBus.on('history.insert', (transactionItem: TransactionItem) => { 194 | this.insertHistory(transactionItem); 195 | }); 196 | 197 | eventBus.on('history.update', (transactionItem: TransactionItem) => { 198 | this.updateHistory(transactionItem); 199 | }); 200 | 201 | eventBus.on('history.delete', (transactionItem: Pick) => { 202 | this.deleteHistory(transactionItem); 203 | }); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import RootStore from './root'; 3 | 4 | export const rootStore = new RootStore(); 5 | 6 | const StoresContext = React.createContext(rootStore); 7 | 8 | export const useStore = () => React.useContext(StoresContext); 9 | 10 | globalThis.store = rootStore; 11 | -------------------------------------------------------------------------------- /src/store/lang.ts: -------------------------------------------------------------------------------- 1 | import { makeAutoObservable } from 'mobx'; 2 | import i18n from 'i18n/config'; 3 | const fileNameMapping = { 4 | 'en-US': 'en', 5 | 'zh-CN': 'zh_CN', 6 | 'zh-TW': 'zh_TW', 7 | ru: 'ru', 8 | ja: 'ja', 9 | ko: 'ko', 10 | es: 'es', 11 | de: 'de', 12 | jp: 'jp', 13 | }; 14 | 15 | export class LangStore { 16 | constructor() { 17 | makeAutoObservable(this); 18 | } 19 | 20 | lang: string = 'en'; 21 | 22 | async init() { 23 | const urlParams = new URLSearchParams(window.location.search); 24 | const langFromQuery = urlParams.get('lang'); 25 | const browserLang = navigator.languages ? navigator.languages[0] : navigator.language; 26 | let lang = langFromQuery || localStorage.getItem('lang') || browserLang; 27 | if (fileNameMapping[lang]) { 28 | lang = fileNameMapping[lang]; 29 | } 30 | this.lang = lang; 31 | console.log('lang',this.lang) 32 | i18n.changeLanguage(lang); 33 | } 34 | 35 | async setLang(lang: string) { 36 | localStorage.setItem('lang', lang); 37 | this.lang = lang; 38 | i18n.changeLanguage(lang); 39 | } 40 | 41 | t(key: string) { 42 | return i18n.t(key); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/store/ledger.ts: -------------------------------------------------------------------------------- 1 | import { makeAutoObservable } from 'mobx'; 2 | import RootStore from '@/store/root'; 3 | import { rootStore } from './index'; 4 | import { createTransport, IoTeXApp, LedgerPlugin } from '@/lib/ledger'; 5 | import { Iotx } from 'iotex-antenna/lib/iotx'; 6 | import { ExecutionMethod } from 'iotex-antenna/lib/action/method'; 7 | import { PromiseState } from './standard/PromiseState'; 8 | import { from } from '@iotexproject/iotex-address-ts'; 9 | import { TransactionRequest } from '@ethersproject/providers'; 10 | import { Deferrable } from 'ethers/lib/utils'; 11 | 12 | export class Ledger { 13 | rootStore: RootStore; 14 | constructor(rootStore: RootStore) { 15 | this.rootStore = rootStore; 16 | makeAutoObservable(this); 17 | } 18 | 19 | _ledger: { iotx: Iotx; [key: string]: any } = null; 20 | async initLedger() { 21 | if (!this._ledger) { 22 | const transport = await createTransport(); 23 | const app = new IoTeXApp(transport); 24 | const plugin = new LedgerPlugin(app); 25 | await plugin.init(); 26 | const addresses = await plugin.getAccounts(); 27 | const address = addresses[0].address; 28 | 29 | const iotx = new Iotx('https://api.mainnet.iotex.one:443', 1, { 30 | signer: plugin 31 | }); 32 | await iotx.getAccount({ address }); 33 | 34 | this._ledger = { 35 | transport, 36 | app, 37 | plugin, 38 | iotx, 39 | address 40 | }; 41 | } 42 | return this._ledger; 43 | } 44 | 45 | ledger = new PromiseState({ 46 | function: async () => { 47 | const { plugin, iotx, address } = await this.initLedger(); 48 | 49 | this.rootStore.god.setChainId(4689); 50 | 51 | this.rootStore.god.currentNetwork.set({ 52 | account: from(address).stringEth(), 53 | //@ts-ignore 54 | signer: { 55 | //@ts-ignore 56 | async sendTransaction(tx: TransactionRequest) { 57 | // console.log({ 58 | // contract: from(tx.to).string(), 59 | // amount: tx.value.toString(), 60 | // data: Buffer.from(tx.data as string, 'hex'), 61 | // gasPrice: '1000000000000', 62 | // gasLimit: '1000000' 63 | // }); 64 | // return; 65 | const method = new ExecutionMethod( 66 | iotx, 67 | //@ts-ignore 68 | { address }, 69 | { 70 | contract: from(tx.to).string(), 71 | amount: tx.value.toString(), 72 | data: Buffer.from(tx.data as string, 'hex'), 73 | gasPrice: '1000000000000', 74 | gasLimit: '1000000' 75 | }, 76 | { 77 | signer: plugin 78 | } 79 | ); 80 | 81 | const actionHash = await method.execute(); 82 | return { 83 | hash: actionHash, 84 | async wait() { 85 | // @ts-ignore 86 | await new Promise((res) => setTimeout(res, 10000)); 87 | const res = await iotx.getReceiptByAction({ actionHash }); 88 | return { status: res.receiptInfo.receipt.status }; 89 | } 90 | }; 91 | } 92 | } 93 | }); 94 | this.rootStore.god.currentNetwork.loadBalance(); 95 | this.rootStore.god.setShowConnecter(false); 96 | return true; 97 | } 98 | }); 99 | } 100 | -------------------------------------------------------------------------------- /src/store/lib/ChainState.ts: -------------------------------------------------------------------------------- 1 | import { makeAutoObservable } from 'mobx'; 2 | import { NetworkState } from './NetworkState'; 3 | import { CoinState } from './CoinState'; 4 | 5 | export class ChainState { 6 | name: string; 7 | network: NetworkState; 8 | // networkKey: string; 9 | chainId: number; 10 | logoUrl: string; 11 | rpcUrl: string; 12 | explorerName: string; 13 | explorerURL: string; 14 | Coin: CoinState; 15 | type: 'mainnet' | 'testnet'; 16 | info: { 17 | // blockPerSeconds: number; 18 | // multicallAddr?: string; 19 | // multicall2Addr?: string; 20 | theme?: { 21 | bgGradient: string; 22 | }; 23 | }; 24 | // provider?: JsonRpcProvider; 25 | // multiCall?: MulticallProvider; 26 | constructor(args: Partial) { 27 | Object.assign(this, args); 28 | makeAutoObservable(this, { network: false }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/store/lib/CoinState.ts: -------------------------------------------------------------------------------- 1 | import { makeAutoObservable } from 'mobx'; 2 | import { BigNumberState } from '../standard/BigNumberState'; 3 | 4 | export class CoinState { 5 | address: string; 6 | symbol: string; 7 | decimals: number; 8 | balance = new BigNumberState({}); 9 | constructor(args: Partial) { 10 | Object.assign(this, args); 11 | makeAutoObservable(this); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/store/lib/EthNetworkState.ts: -------------------------------------------------------------------------------- 1 | import { makeAutoObservable } from 'mobx'; 2 | import { Contract } from '@ethersproject/contracts'; 3 | import { Signer } from '@ethersproject/abstract-signer'; 4 | import { JsonRpcProvider, TransactionResponse, BaseProvider } from '@ethersproject/providers'; 5 | import { utils } from 'ethers'; 6 | import { Contract as MuticallContract, Provider as MulticallProvider } from 'ethcall'; 7 | import { MappingState } from '../standard/MappingState'; 8 | import { ChainState } from './ChainState'; 9 | import { NetworkState } from './NetworkState'; 10 | import { StorageState } from '../standard/StorageState'; 11 | import BigNumber from 'bignumber.js'; 12 | import { GodStore } from '../god'; 13 | import { metamaskUtils } from '../../lib/metaskUtils'; 14 | import { smartGraph } from '../../lib/smartgraph/index'; 15 | 16 | export class EthNetworkState implements NetworkState { 17 | god: GodStore; 18 | 19 | // contract 20 | chain: MappingState = new MappingState({ currentId: '' }); 21 | signer: Signer; 22 | // provider: BaseProvider; 23 | account: string = ''; 24 | allowChains: number[]; 25 | 26 | info = {}; 27 | 28 | // ui 29 | connector = { 30 | latestProvider: new StorageState({ key: 'latestEthProvider' }), 31 | showConnector: false 32 | }; 33 | 34 | walletInfo = { 35 | visible: false 36 | }; 37 | 38 | constructor(args: Partial) { 39 | Object.assign(this, args); 40 | makeAutoObservable(this); 41 | // Object.values(this.chain.map).forEach((chain) => { 42 | // chain.provider = new JsonRpcProvider(chain.rpcUrl); 43 | // }); 44 | this.allowChains = Object.values(this.chain.map).map((i) => i.chainId); 45 | } 46 | 47 | get currentChain() { 48 | return this.chain.current; 49 | } 50 | 51 | get provider() { 52 | return smartGraph.chain[this.currentChain.chainId].caller.provider; 53 | } 54 | 55 | async loadBalance() { 56 | if (!this.signer || !this.account) { 57 | return this.currentChain.Coin.balance.setValue(new BigNumber(0)); 58 | } 59 | const balance = await this.provider.getBalance(this.account); 60 | this.currentChain.Coin.balance.setValue(new BigNumber(balance.toString())); 61 | } 62 | 63 | set(args: Partial) { 64 | Object.assign(this, args); 65 | } 66 | 67 | isAddress(address: string): boolean { 68 | return utils.isAddress(address); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/store/lib/NetworkState.ts: -------------------------------------------------------------------------------- 1 | import { ChainState } from './ChainState'; 2 | import { MappingState } from '../standard/MappingState'; 3 | import { StorageState } from '../standard/StorageState'; 4 | import { TransactionResponse } from '@ethersproject/providers'; 5 | import { GodStore } from '../god'; 6 | import { CallParams } from '../../../type'; 7 | 8 | export interface NetworkState { 9 | god: GodStore; 10 | chain: MappingState; 11 | allowChains: number[]; 12 | account: string; 13 | connector: { latestProvider: StorageState; showConnector: boolean }; 14 | walletInfo: { visible: boolean }; 15 | currentChain: ChainState; 16 | // multicall(calls: Partial[], args?: { crosschain?: boolean }): Promise; 17 | set: (args: Partial) => void; 18 | loadBalance: Function; 19 | // execContract(call: { address: string; abi: any; method: string; params?: any[]; options?: any }): Promise>; 20 | isAddress(address: string): boolean; 21 | } 22 | -------------------------------------------------------------------------------- /src/store/root.ts: -------------------------------------------------------------------------------- 1 | import { LangStore } from './lang'; 2 | import { GodStore } from './god'; 3 | import { UserStore } from './user'; 4 | import { TransactionHistoryStore } from './history'; 5 | import { Ledger } from './ledger'; 6 | 7 | export default class RootStore { 8 | lang = new LangStore(); 9 | god = new GodStore(this); 10 | user = new UserStore(this); 11 | ledger = new Ledger(this); 12 | history = new TransactionHistoryStore(this); 13 | } 14 | -------------------------------------------------------------------------------- /src/store/standard/BigNumberInputState.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | import { makeAutoObservable } from 'mobx'; 3 | import { helper } from '../../lib/helper'; 4 | export class BigNumberInputState { 5 | value = new BigNumber(0); 6 | format = ''; 7 | loading = false; 8 | decimals = 18; 9 | formatter?: Function; 10 | constructor(args: Partial) { 11 | Object.assign(this, args); 12 | makeAutoObservable(this); 13 | } 14 | 15 | setValue(value: BigNumber) { 16 | this.value = value; 17 | this.format = helper.number.toPrecisionFloor(new BigNumber(this.value).dividedBy(10 ** this.decimals).toFixed()); 18 | this.setLoading(false); 19 | } 20 | setFormat(val: any) { 21 | this.format = val; 22 | this.value = new BigNumber(val).multipliedBy(10 ** this.decimals); 23 | } 24 | setLoading(val: boolean) { 25 | this.loading = val; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/store/standard/BigNumberState.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | import { makeAutoObservable } from 'mobx'; 3 | import { helper } from '@/lib/helper'; 4 | 5 | export class BigNumberState { 6 | value = new BigNumber(0); 7 | loading = false; 8 | decimals = 18; 9 | fixed = 6; 10 | formatter?: Function; 11 | constructor(args: Partial) { 12 | Object.assign(this, args); 13 | makeAutoObservable(this); 14 | } 15 | get format() { 16 | if (this.loading) return '...'; 17 | return this.getFormat(); 18 | } 19 | 20 | getFormat({ decimals = this.decimals, fixed = this.fixed }: { decimals?: number; fixed?: number } = {}) { 21 | if (this.loading) return '...'; 22 | if (this.formatter) return this.formatter(this); 23 | return helper.number.toPrecisionFloor(new BigNumber(this.value).dividedBy(10 ** decimals).toFixed(), { 24 | decimals: fixed 25 | }); 26 | } 27 | setDecimals(decimals: number) { 28 | this.decimals = decimals; 29 | } 30 | setValue(value: BigNumber) { 31 | this.value = value; 32 | this.setLoading(false); 33 | } 34 | setLoading(val) { 35 | this.loading = val; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/store/standard/MappingState.ts: -------------------------------------------------------------------------------- 1 | import { makeAutoObservable, makeObservable, observable } from 'mobx'; 2 | import { StorageState } from './StorageState'; 3 | 4 | export class MappingState { 5 | currentId: any; 6 | map: { 7 | [key: string]: T; 8 | } = {}; 9 | set: T[] = []; 10 | constructor(args: Partial>) { 11 | Object.assign(this, args); 12 | this.set = Object.values(this.map); 13 | makeAutoObservable(this); 14 | } 15 | get current(): T { 16 | return this.map[this.currentId]; 17 | } 18 | setCurrentId(val: any) { 19 | this.currentId = val; 20 | } 21 | } 22 | 23 | export class MappingStorageState { 24 | currentId: StorageState; 25 | map: { 26 | [key: string]: T; 27 | }; 28 | constructor(args: Partial>) { 29 | Object.assign(this, args); 30 | makeAutoObservable(this); 31 | } 32 | get current(): T { 33 | return this.map[this.currentId.value]; 34 | } 35 | setCurrentId(val: any) { 36 | this.currentId.save(val); 37 | } 38 | } 39 | 40 | export class DynamicMappingState { 41 | map: { 42 | [key: string]: T; 43 | }; 44 | getId: Function; 45 | constructor(args: Partial>) { 46 | Object.assign(this, args); 47 | makeAutoObservable(this); 48 | } 49 | get current(): T { 50 | return this.map[this.getId()]; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/store/standard/PromiseState.ts: -------------------------------------------------------------------------------- 1 | import { makeAutoObservable } from 'mobx'; 2 | import { BooleanState } from './base'; 3 | import { helper } from '../../lib/helper'; 4 | import { showNotification } from '@mantine/notifications'; 5 | import toast from 'react-hot-toast'; 6 | 7 | export class PromiseState Promise, U = ReturnType> { 8 | loading = new BooleanState(); 9 | value?: Awaited = null; 10 | function: T; 11 | 12 | autoAlert = true; 13 | context: any = undefined; 14 | 15 | constructor(args: Partial> = {}) { 16 | Object.assign(this, args); 17 | makeAutoObservable(this); 18 | } 19 | 20 | async call(...args: Parameters): Promise> { 21 | try { 22 | this.loading.setValue(true); 23 | const res = await this.function.apply(this.context, args); 24 | this.value = res; 25 | return res; 26 | } catch (error) { 27 | console.log(error); 28 | if (this.autoAlert) { 29 | showNotification({ 30 | title: 'Error', 31 | message: error.data?.message || error.message, 32 | color: 'red' 33 | }); 34 | } else { 35 | throw error; 36 | } 37 | } finally { 38 | this.loading.setValue(false); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/store/standard/StorageState.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | import { makeAutoObservable } from 'mobx'; 3 | 4 | export class StorageState { 5 | key: string; 6 | value: T | any = null; 7 | default: T | any = null; 8 | constructor(args: Partial>) { 9 | Object.assign(this, args); 10 | makeAutoObservable(this); 11 | this.load(); 12 | } 13 | 14 | static safeParse(val: any) { 15 | try { 16 | return JSON.parse(val); 17 | } catch (error) { 18 | return val; 19 | } 20 | } 21 | 22 | load() { 23 | const value = global?.localStorage?.getItem(this.key); 24 | this.value = StorageState.safeParse(value); 25 | if (this.value == null) { 26 | this.value = this.default; 27 | } 28 | return this.value; 29 | } 30 | 31 | save(value?: any) { 32 | if (value) { 33 | this.value = value; 34 | } 35 | global?.localStorage.setItem(this.key, JSON.stringify(value)); 36 | } 37 | 38 | clear() { 39 | localStorage.removeItem(this.key); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/store/standard/base.ts: -------------------------------------------------------------------------------- 1 | import { makeAutoObservable } from 'mobx'; 2 | 3 | export class StringState { 4 | value: T = null; 5 | constructor(args: Partial> = {}) { 6 | Object.assign(this, args); 7 | makeAutoObservable(this); 8 | } 9 | setValue(value: T) { 10 | this.value = value; 11 | } 12 | } 13 | 14 | export class BooleanState { 15 | value: boolean = false; 16 | constructor(args: Partial = {}) { 17 | Object.assign(this, args); 18 | makeAutoObservable(this); 19 | } 20 | setValue(value: boolean) { 21 | this.value = value; 22 | } 23 | } 24 | 25 | export class NumberState { 26 | value: number = 0; 27 | constructor(args: Partial = {}) { 28 | Object.assign(this, args); 29 | makeAutoObservable(this); 30 | } 31 | setValue(value: number) { 32 | this.value = value; 33 | } 34 | } 35 | 36 | export class ValueState { 37 | _value: T = null; 38 | constructor(args: Partial> = {}) { 39 | Object.assign(this, args); 40 | makeAutoObservable(this); 41 | } 42 | 43 | get value() { 44 | return this.getValue ? this.getValue(this._value) : this._value; 45 | } 46 | set value(value) { 47 | this._value = value; 48 | } 49 | 50 | getValue: (value: T) => T; 51 | 52 | setValue(value: T) { 53 | this._value = value; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/store/template/State.ts: -------------------------------------------------------------------------------- 1 | import { makeAutoObservable } from 'mobx'; 2 | 3 | export class TemplateState { 4 | constructor(args: Partial) { 5 | Object.assign(this, args); 6 | makeAutoObservable(this); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/store/template/Store.ts: -------------------------------------------------------------------------------- 1 | import { makeAutoObservable } from 'mobx'; 2 | import RootStore from '@/store/root'; 3 | import { rootStore } from '../index'; 4 | 5 | export class TodoStore { 6 | rootStore: RootStore; 7 | constructor(rootStore: RootStore) { 8 | this.rootStore = rootStore; 9 | makeAutoObservable(this); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/store/user.tsx: -------------------------------------------------------------------------------- 1 | import { makeAutoObservable } from 'mobx'; 2 | import RootStore from '@/store/root'; 3 | import { StorageState } from './standard/StorageState'; 4 | import { Home, Dashboard, LayersLinked, Code as CodeIcon, Wallet, Paint } from 'tabler-icons-react'; 5 | import { rootStore } from '../store/index'; 6 | import type { SpotlightAction } from '@mantine/spotlight'; 7 | import { BooleanState } from './standard/base'; 8 | 9 | export class UserStore { 10 | rootStore: RootStore; 11 | token = new StorageState({ key: 'token' }); 12 | tokenAddress = new StorageState({ key: 'token-address' }); 13 | theme = new StorageState<'light' | 'dark'>({ 14 | key: 'theme', 15 | default: 'light' 16 | }); 17 | layout = { 18 | sidebarOpen: new BooleanState(), 19 | navbarMode: 'top' as 'left' | 'top', 20 | showHistoryButton: new BooleanState(), 21 | router: [ 22 | { link: '/', label: 'home', icon: Home }, 23 | { link: '/swap', label: 'example', icon: CodeIcon }, 24 | { link: '/api/graphql', label: 'playground', icon: LayersLinked, __blank: true } 25 | ] 26 | }; 27 | networkChecker = { 28 | supportChainIds: {}, 29 | isWrongNetworkDialogOpen: new BooleanState({ value: false }) 30 | }; 31 | get actions(): SpotlightAction[] { 32 | return [ 33 | { 34 | title: 'Home', 35 | description: 'Get to home page', 36 | onTrigger: () => console.log('Home'), 37 | icon: 38 | }, 39 | { 40 | title: 'Connect Wallet', 41 | description: 'Connect Wallet', 42 | onTrigger: () => { 43 | rootStore.god.setShowConnecter(true); 44 | }, 45 | icon: 46 | }, 47 | { 48 | title: 'Switch Theme', 49 | description: 'Switch Theme', 50 | onTrigger: () => { 51 | this.toggleTheme(); 52 | }, 53 | icon: 54 | } 55 | ]; 56 | } 57 | 58 | constructor(rootStore: RootStore) { 59 | makeAutoObservable(this); 60 | } 61 | 62 | get isDark() { 63 | return this.theme.value == 'dark'; 64 | } 65 | 66 | toggleTheme() { 67 | this.theme.save(this.isDark ? 'light' : 'dark'); 68 | } 69 | 70 | enableNetworkChecker(path: string, value: number[]) { 71 | this.networkChecker.supportChainIds[path] = value; 72 | } 73 | 74 | setWrongNetworkDialog(value: boolean) { 75 | this.networkChecker.isWrongNetworkDialogOpen.setValue(value); 76 | } 77 | 78 | wetherWrongnetwork(key: string) { 79 | const supportChainId = this.networkChecker.supportChainIds[key] || []; 80 | if (supportChainId.length === 0) return false; 81 | if (!rootStore.god.isConnect) return false; 82 | if (!supportChainId.some((i) => i === rootStore.god.currentChain.chainId)) { 83 | return true; 84 | } 85 | return false; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": [ 5 | "DOM", 6 | "DOM.Iterable", 7 | "ESNext" 8 | ], 9 | "types": [ 10 | "node" 11 | ], 12 | "allowJs": false, 13 | "skipLibCheck": true, 14 | "esModuleInterop": true, 15 | "allowSyntheticDefaultImports": true, 16 | "strict": false, 17 | "forceConsistentCasingInFileNames": true, 18 | "strictPropertyInitialization": false, 19 | "module": "ESNext", 20 | "moduleResolution": "Node", 21 | "resolveJsonModule": true, 22 | "isolatedModules": true, 23 | "noImplicitAny": false, 24 | "noEmit": true, 25 | "jsx": "preserve", 26 | "baseUrl": "src", 27 | "experimentalDecorators": true, 28 | "emitDecoratorMetadata": true, 29 | "paths": { 30 | "@/store/*": [ 31 | "store/*" 32 | ], 33 | "@/components/*": [ 34 | "components/*" 35 | ], 36 | "@/utils/*": [ 37 | "utils/*" 38 | ], 39 | "@/lib/*": [ 40 | "lib/*" 41 | ], 42 | "@/constants/*": [ 43 | "constants/*" 44 | ], 45 | "@/server/*":["server/*"] 46 | }, 47 | "incremental": true 48 | }, 49 | "include": [ 50 | "src", 51 | "type.ts" 52 | , "__generated" ], 53 | "exclude": [ 54 | "node_modules" 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /type.ts: -------------------------------------------------------------------------------- 1 | export interface CallParams

{ 2 | address: string; 3 | abi: any; 4 | method: string; 5 | params?: P; 6 | options?: Partial<{ 7 | from: string; 8 | value: string; 9 | gasLimit: string; 10 | gasPrice: string; 11 | }>; 12 | handler?: any; 13 | read?: boolean; 14 | chainId?: number; 15 | } 16 | --------------------------------------------------------------------------------