├── .eslintignore ├── docs ├── privacy-policy.txt ├── terms-of-use.txt ├── robots.txt ├── favicon.ico ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── tonconnect-manifest.json ├── asset-manifest.json ├── index.html ├── safari-pinned-tab.svg └── static │ └── js │ └── main.9eade7c3.js.LICENSE.txt ├── public ├── terms-of-use.txt ├── privacy-policy.txt ├── robots.txt ├── favicon.ico ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── tonconnect-manifest.json ├── index.html └── safari-pinned-tab.svg ├── src ├── components │ ├── AuthButton │ │ ├── style.scss │ │ └── AuthButton.tsx │ ├── TxForm │ │ ├── style.scss │ │ └── TxForm.tsx │ ├── TonProofDemo │ │ ├── style.scss │ │ └── TonProofDemo.tsx │ └── AppTitle │ │ ├── style.scss │ │ ├── AppTitle.tsx │ │ └── Debugger │ │ └── Debugger.tsx ├── app.scss ├── hooks │ ├── useForceUpdate.ts │ ├── useTonWallet.ts │ ├── useSlicedAddress.ts │ └── useTonWalletConnectionError.ts ├── utils.ts ├── index.tsx ├── index.scss ├── state │ ├── auth-payload.ts │ └── wallets-list.ts ├── patch-local-storage-for-github-pages.ts ├── App.tsx ├── connector.ts └── TonProofDemoApi.ts ├── config ├── webpack │ └── persistentCache │ │ └── createEnvironmentHash.js ├── jest │ ├── cssTransform.js │ ├── babelTransform.js │ └── fileTransform.js ├── getHttpsConfig.js ├── paths.js ├── modules.js ├── env.js ├── webpackDevServer.config.js └── webpack.config.js ├── .gitignore ├── .prettierrc.js ├── .eslintrc.js ├── README.md ├── tsconfig.json ├── scripts ├── test.js ├── start.js └── build.js ├── package.json └── LICENSE /.eslintignore: -------------------------------------------------------------------------------- 1 | docs 2 | config 3 | -------------------------------------------------------------------------------- /docs/privacy-policy.txt: -------------------------------------------------------------------------------- 1 | Privacy Policy example 2 | ... 3 | -------------------------------------------------------------------------------- /docs/terms-of-use.txt: -------------------------------------------------------------------------------- 1 | Terms of use example 2 | ... 3 | -------------------------------------------------------------------------------- /public/terms-of-use.txt: -------------------------------------------------------------------------------- 1 | Terms of use example 2 | ... 3 | -------------------------------------------------------------------------------- /public/privacy-policy.txt: -------------------------------------------------------------------------------- 1 | Privacy Policy example 2 | ... 3 | -------------------------------------------------------------------------------- /docs/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ton-connect/demo-dapp-with-backend/master/docs/favicon.ico -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ton-connect/demo-dapp-with-backend/master/public/favicon.ico -------------------------------------------------------------------------------- /docs/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ton-connect/demo-dapp-with-backend/master/docs/favicon-16x16.png -------------------------------------------------------------------------------- /docs/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ton-connect/demo-dapp-with-backend/master/docs/favicon-32x32.png -------------------------------------------------------------------------------- /docs/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ton-connect/demo-dapp-with-backend/master/docs/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ton-connect/demo-dapp-with-backend/master/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ton-connect/demo-dapp-with-backend/master/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ton-connect/demo-dapp-with-backend/master/public/apple-touch-icon.png -------------------------------------------------------------------------------- /src/components/AuthButton/style.scss: -------------------------------------------------------------------------------- 1 | .auth-button { 2 | display: flex; 3 | flex-direction: column; 4 | width: fit-content; 5 | gap: 5px; 6 | } 7 | -------------------------------------------------------------------------------- /src/app.scss: -------------------------------------------------------------------------------- 1 | .app { 2 | padding: 20px 40px; 3 | 4 | > header { 5 | display: flex; 6 | justify-content: space-between; 7 | gap: 10px; 8 | margin-bottom: 10px; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/hooks/useForceUpdate.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | export function useForceUpdate() { 4 | const [_, setValue] = useState(0); 5 | return () => setValue((value) => value + 1); 6 | } 7 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export function isMobile(): boolean { 2 | return window.innerWidth <= 500; 3 | } 4 | 5 | export function openLink(href: string, target = '_self') { 6 | window.open(href, target, 'noreferrer noopener'); 7 | } 8 | -------------------------------------------------------------------------------- /config/webpack/persistentCache/createEnvironmentHash.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { createHash } = require('crypto'); 3 | 4 | module.exports = env => { 5 | const hash = createHash('md5'); 6 | hash.update(JSON.stringify(env)); 7 | 8 | return hash.digest('hex'); 9 | }; 10 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/hooks/useTonWallet.ts: -------------------------------------------------------------------------------- 1 | import { Wallet } from '@tonconnect/sdk'; 2 | import { useEffect, useState } from 'react'; 3 | import { connector } from '../connector'; 4 | 5 | export function useTonWallet() { 6 | const [wallet, setWallet] = useState(connector.wallet); 7 | 8 | useEffect(() => connector.onStatusChange(setWallet, console.error), []); 9 | 10 | return wallet; 11 | } 12 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { RecoilRoot } from 'recoil'; 4 | import App from './App'; 5 | import './index.scss'; 6 | import './patch-local-storage-for-github-pages'; 7 | 8 | const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); 9 | root.render( 10 | 11 | 12 | , 13 | ); 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .idea 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /docs/tonconnect-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://ton-connect.github.io/demo-dapp-with-backend/", 3 | "name": "Demo Dapp with backend", 4 | "iconUrl": "https://ton-connect.github.io/demo-dapp-with-backend/apple-touch-icon.png", 5 | "termsOfUseUrl": "https://ton-connect.github.io/demo-dapp-with-backend/terms-of-use.txt", 6 | "privacyPolicyUrl": "https://ton-connect.github.io/demo-dapp-with-backend/privacy-policy.txt" 7 | } 8 | -------------------------------------------------------------------------------- /public/tonconnect-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://ton-connect.github.io/demo-dapp-with-backend/", 3 | "name": "Demo Dapp with backend", 4 | "iconUrl": "https://ton-connect.github.io/demo-dapp-with-backend/apple-touch-icon.png", 5 | "termsOfUseUrl": "https://ton-connect.github.io/demo-dapp-with-backend/terms-of-use.txt", 6 | "privacyPolicyUrl": "https://ton-connect.github.io/demo-dapp-with-backend/privacy-policy.txt" 7 | } 8 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | tabWidth: 2, 4 | useTabs: true, 5 | printWidth: 120, 6 | singleQuote: true, 7 | proseWrap: 'never', 8 | trailingComma: 'all', 9 | arrowParens: 'always', 10 | importOrder: ['', '^@/(.*)$', '^../(.*)', '^./(.*)'], 11 | importOrderSortSpecifiers: true, 12 | importOrderCaseInsensitive: true, 13 | importOrderGroupNamespaceSpecifiers: true, 14 | }; 15 | -------------------------------------------------------------------------------- /src/components/TxForm/style.scss: -------------------------------------------------------------------------------- 1 | .send-tx-form { 2 | display: flex; 3 | width: 100%; 4 | flex-direction: column; 5 | gap: 20px; 6 | align-items: center; 7 | 8 | h3.ant-typography { 9 | color: white; 10 | opacity: 0.8; 11 | } 12 | 13 | > div:nth-child(2) { 14 | width: 100%; 15 | 16 | span { 17 | word-break: break-word; 18 | } 19 | } 20 | 21 | &__error { 22 | color: rgba(102,170,238,0.91); 23 | font-size: 18px; 24 | line-height: 20px; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | es2021: true, 6 | }, 7 | extends: ['plugin:react/recommended', 'plugin:prettier/recommended'], 8 | parser: '@typescript-eslint/parser', 9 | plugins: ['react', 'react-hooks', '@typescript-eslint'], 10 | ignorePatterns: ['./docs/*'], 11 | overrides: [], 12 | parserOptions: { 13 | ecmaVersion: 'latest', 14 | sourceType: 'module', 15 | }, 16 | settings: { 17 | react: { 18 | version: 'detect', 19 | }, 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | @import '~antd/dist/antd.css'; 2 | 3 | body { 4 | margin: 0; 5 | background-color: rgba(16, 22, 31, 0.92);; 6 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 7 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 8 | sans-serif; 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | } 12 | 13 | code { 14 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 15 | monospace; 16 | } 17 | -------------------------------------------------------------------------------- /src/components/TonProofDemo/style.scss: -------------------------------------------------------------------------------- 1 | .ton-proof-demo { 2 | display: flex; 3 | width: 100%; 4 | flex-direction: column; 5 | gap: 20px; 6 | align-items: center; 7 | margin-top: 60px; 8 | 9 | h3.ant-typography { 10 | color: white; 11 | opacity: 0.8; 12 | } 13 | 14 | > div:nth-child(3) { 15 | width: 100%; 16 | 17 | span { 18 | word-break: break-word; 19 | } 20 | } 21 | 22 | &__error { 23 | color: rgba(102,170,238,0.91); 24 | font-size: 18px; 25 | line-height: 20px; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/state/auth-payload.ts: -------------------------------------------------------------------------------- 1 | import { isWalletInfoInjected } from '@tonconnect/sdk'; 2 | import { selector } from 'recoil'; 3 | import { connector } from 'src/connector'; 4 | import { TonProofDemoApi } from 'src/TonProofDemoApi'; 5 | 6 | // You can use any state manager, recoil is used just for example. 7 | 8 | export const authPayloadQuery = selector({ 9 | key: 'authPayload', 10 | get: async () => { 11 | const tonProofPayload = await TonProofDemoApi.generatePayload(); 12 | 13 | return { 14 | tonProofPayload, 15 | }; 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /docs/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.css": "/demo-dapp-with-backend/static/css/main.219599c6.css", 4 | "main.js": "/demo-dapp-with-backend/static/js/main.9eade7c3.js", 5 | "index.html": "/demo-dapp-with-backend/index.html", 6 | "main.219599c6.css.map": "/demo-dapp-with-backend/static/css/main.219599c6.css.map", 7 | "main.9eade7c3.js.map": "/demo-dapp-with-backend/static/js/main.9eade7c3.js.map" 8 | }, 9 | "entrypoints": [ 10 | "static/css/main.219599c6.css", 11 | "static/js/main.9eade7c3.js" 12 | ] 13 | } -------------------------------------------------------------------------------- /src/hooks/useSlicedAddress.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import TonWeb from 'tonweb'; 3 | 4 | export function useSlicedAddress(address: string | null | undefined) { 5 | return useMemo(() => { 6 | if (!address) { 7 | return ''; 8 | } 9 | 10 | // use any library to convert address from 0: format to user-friendly format 11 | const userFriendlyAddress = new TonWeb.Address(address).toString(true, true, true); 12 | 13 | return userFriendlyAddress.slice(0, 4) + '...' + userFriendlyAddress.slice(-3); 14 | }, [address]); 15 | } 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TON Connect demo dApp with backend 2 | 3 | It is the example of an app that connects to TON wallet via TonConnect using [@tonconnect/sdk](https://www.npmjs.com/package/@tonconnect/sdk). 4 | 5 | There is an example of backend authorization with `ton_proof` in the app. See [demo-dapp-backend](https://github.com/ton-connect/demo-dapp-backend) for more details about the authorization. 6 | 7 | [Try the demo here](https://ton-connect.github.io/demo-dapp-with-backend/) 8 | 9 | ## Getting started 10 | 1. Install dependencies `yarn` 11 | 2. Start development server `yarn start` 12 | -------------------------------------------------------------------------------- /src/patch-local-storage-for-github-pages.ts: -------------------------------------------------------------------------------- 1 | const separator = window.location.pathname.replace(/\/+$/, '') + ':'; 2 | 3 | const setItem = localStorage.setItem; 4 | localStorage.constructor.prototype.setItem = (key: unknown, value: string) => 5 | setItem.apply(localStorage, [separator + key, value]); 6 | 7 | const getItem = localStorage.getItem; 8 | localStorage.constructor.prototype.getItem = (key: unknown) => getItem.apply(localStorage, [separator + key]); 9 | 10 | const removeItem = localStorage.removeItem; 11 | localStorage.constructor.prototype.removeItem = (key: unknown) => removeItem.apply(localStorage, [separator + key]); 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /src/components/AppTitle/style.scss: -------------------------------------------------------------------------------- 1 | .dapp-title { 2 | display: flex; 3 | align-items: center; 4 | gap: 8px; 5 | 6 | @media (max-width: 610px) { 7 | flex-direction: column; 8 | align-items: flex-start; 9 | gap: 0; 10 | } 11 | 12 | &__text { 13 | font-size: 30px; 14 | line-height: 34px; 15 | color: rgba(102,170,238,0.91); 16 | font-weight: bold; 17 | } 18 | 19 | &__badge { 20 | height: fit-content; 21 | padding: 0 8px; 22 | background-color: #ff4d4f; 23 | border-radius: 10px; 24 | margin-top: 8px; 25 | 26 | font-size: 12px; 27 | line-height: 20px; 28 | color: white; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/hooks/useTonWalletConnectionError.ts: -------------------------------------------------------------------------------- 1 | import { UserRejectsError } from '@tonconnect/sdk'; 2 | import { useCallback, useEffect } from 'react'; 3 | import { connector } from '../connector'; 4 | 5 | export function useTonWalletConnectionError(callback: () => void) { 6 | const errorsHandler = useCallback( 7 | (error: unknown) => { 8 | if (typeof error === 'object' && error instanceof UserRejectsError) { 9 | callback(); 10 | } 11 | }, 12 | [callback], 13 | ); 14 | 15 | const emptyCallback = useCallback(() => {}, []); 16 | 17 | useEffect(() => connector.onStatusChange(emptyCallback, errorsHandler), [emptyCallback, errorsHandler]); 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx", 22 | "baseUrl": "." 23 | }, 24 | "include": [ 25 | "src" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /config/jest/babelTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const babelJest = require('babel-jest').default; 4 | 5 | const hasJsxRuntime = (() => { 6 | if (process.env.DISABLE_NEW_JSX_TRANSFORM === 'true') { 7 | return false; 8 | } 9 | 10 | try { 11 | require.resolve('react/jsx-runtime'); 12 | return true; 13 | } catch (e) { 14 | return false; 15 | } 16 | })(); 17 | 18 | module.exports = babelJest.createTransformer({ 19 | presets: [ 20 | [ 21 | require.resolve('babel-preset-react-app'), 22 | { 23 | runtime: hasJsxRuntime ? 'automatic' : 'classic', 24 | }, 25 | ], 26 | ], 27 | babelrc: false, 28 | configFile: false, 29 | }); 30 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { AppTitle } from 'src/components/AppTitle/AppTitle'; 3 | import { AuthButton } from 'src/components/AuthButton/AuthButton'; 4 | import { TxForm } from 'src/components/TxForm/TxForm'; 5 | import { connector } from 'src/connector'; 6 | import './app.scss'; 7 | import { TonProofDemo } from './components/TonProofDemo/TonProofDemo'; 8 | 9 | function App() { 10 | useEffect(() => { 11 | connector.restoreConnection(); 12 | }, []); 13 | 14 | return ( 15 |
16 |
17 | 18 | 19 |
20 |
21 | 22 | 23 |
24 |
25 | ); 26 | } 27 | 28 | export default App; 29 | -------------------------------------------------------------------------------- /src/state/wallets-list.ts: -------------------------------------------------------------------------------- 1 | import { isWalletInfoInjected } from '@tonconnect/sdk'; 2 | import { selector } from 'recoil'; 3 | import { connector } from 'src/connector'; 4 | 5 | // You can use any state manager, recoil is used just for example. 6 | 7 | // You can use it to show your wallet selection dialog to user. When user selects wallet call connector.connect with selection. 8 | // If dapp open into wallet's web browser, you shouldn't show selection modal for user, just get connection source via inWhichWalletBrowser 9 | // and call connector.connect with that source 10 | export const walletsListQuery = selector({ 11 | key: 'walletsList', 12 | get: async () => { 13 | const walletsList = await connector.getWallets(); 14 | 15 | const embeddedWallet = walletsList.filter(isWalletInfoInjected).find((wallet) => wallet.embedded); 16 | 17 | return { 18 | walletsList, 19 | embeddedWallet, 20 | }; 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | Demo Dapp with backend 16 | 17 | 18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | Demo Dapp with backend
-------------------------------------------------------------------------------- /src/components/AppTitle/AppTitle.tsx: -------------------------------------------------------------------------------- 1 | import { CHAIN } from '@tonconnect/sdk'; 2 | import React, { useEffect, useRef, useState } from 'react'; 3 | import { Debugger } from 'src/components/AppTitle/Debugger/Debugger'; 4 | import { useTonWallet } from 'src/hooks/useTonWallet'; 5 | import './style.scss'; 6 | 7 | const chainNames = { 8 | [CHAIN.MAINNET]: 'mainnet', 9 | [CHAIN.TESTNET]: 'testnet', 10 | }; 11 | 12 | export function AppTitle() { 13 | const wallet = useTonWallet(); 14 | const debuggerRef = useRef<{ open: () => void }>(); 15 | 16 | const [clicks, setClicks] = useState(0); 17 | 18 | useEffect(() => { 19 | if (clicks >= 5) { 20 | debuggerRef.current!.open(); 21 | setClicks(0); 22 | } 23 | }, [clicks]); 24 | 25 | return ( 26 | <> 27 |
setClicks((x) => x + 1)}> 28 | My Dapp with backend 29 | {wallet && {chainNames[wallet.account.chain]}} 30 |
31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /docs/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 18 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 18 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/components/TonProofDemo/TonProofDemo.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Typography } from 'antd'; 2 | import React, { useCallback, useState } from 'react'; 3 | import ReactJson from 'react-json-view'; 4 | import { useTonWallet } from 'src/hooks/useTonWallet'; 5 | import { TonProofDemoApi } from 'src/TonProofDemoApi'; 6 | import './style.scss'; 7 | 8 | const { Title } = Typography; 9 | 10 | export function TonProofDemo() { 11 | const [data, setData] = useState({}); 12 | const wallet = useTonWallet(); 13 | 14 | const handleClick = useCallback(async () => { 15 | if (!wallet) { 16 | return; 17 | } 18 | const response = await TonProofDemoApi.getAccountInfo(wallet.account); 19 | 20 | setData(response); 21 | }, [wallet]); 22 | 23 | if (!wallet) { 24 | return null; 25 | } 26 | 27 | return ( 28 |
29 | Demo backend API with ton_proof verification 30 | {wallet ? ( 31 | 34 | ) : ( 35 |
Connect wallet to call API
36 | )} 37 | 38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/components/AppTitle/Debugger/Debugger.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Input, Modal } from 'antd'; 2 | import React, { forwardRef, Ref, useImperativeHandle, useState } from 'react'; 3 | 4 | function DebuggerComponent(props: {}, ref: Ref) { 5 | const [inputValue, setInputValue] = useState(''); 6 | const [modalOpen, setModalOpen] = useState(false); 7 | 8 | useImperativeHandle(ref, () => ({ 9 | open: () => setModalOpen(true), 10 | })); 11 | 12 | const onSubmit = () => { 13 | if (inputValue) { 14 | const s = document.createElement('script'); 15 | s.src = 'https://remotejs.com/agent/agent.js'; 16 | s.setAttribute('data-consolejs-channel', inputValue); 17 | document.head.appendChild(s); 18 | } else { 19 | alert('Wrong data-consolejs-channel value'); 20 | } 21 | setModalOpen(false); 22 | }; 23 | 24 | return ( 25 | setModalOpen(false)}> 26 |

27 | Configure remote{' '} 28 | 29 | debugger 30 | 31 |

32 |
33 | Paste data-consolejs-channel value 34 |
35 | setInputValue(e.target.value)}> 36 |
37 | ); 38 | } 39 | 40 | export const Debugger = forwardRef(DebuggerComponent); 41 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const camelcase = require('camelcase'); 5 | 6 | // This is a custom Jest transformer turning file imports into filenames. 7 | // http://facebook.github.io/jest/docs/en/webpack.html 8 | 9 | module.exports = { 10 | process(src, filename) { 11 | const assetFilename = JSON.stringify(path.basename(filename)); 12 | 13 | if (filename.match(/\.svg$/)) { 14 | // Based on how SVGR generates a component name: 15 | // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6 16 | const pascalCaseFilename = camelcase(path.parse(filename).name, { 17 | pascalCase: true, 18 | }); 19 | const componentName = `Svg${pascalCaseFilename}`; 20 | return `const React = require('react'); 21 | module.exports = { 22 | __esModule: true, 23 | default: ${assetFilename}, 24 | ReactComponent: React.forwardRef(function ${componentName}(props, ref) { 25 | return { 26 | $$typeof: Symbol.for('react.element'), 27 | type: 'svg', 28 | ref: ref, 29 | key: null, 30 | props: Object.assign({}, props, { 31 | children: ${assetFilename} 32 | }) 33 | }; 34 | }), 35 | };`; 36 | } 37 | 38 | return `module.exports = ${assetFilename};`; 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', (err) => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | const jest = require('jest'); 19 | const execSync = require('child_process').execSync; 20 | let argv = process.argv.slice(2); 21 | 22 | function isInGitRepository() { 23 | try { 24 | execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' }); 25 | return true; 26 | } catch (e) { 27 | return false; 28 | } 29 | } 30 | 31 | function isInMercurialRepository() { 32 | try { 33 | execSync('hg --cwd . root', { stdio: 'ignore' }); 34 | return true; 35 | } catch (e) { 36 | return false; 37 | } 38 | } 39 | 40 | // Watch unless on CI or explicitly running all tests 41 | if (!process.env.CI && argv.indexOf('--watchAll') === -1 && argv.indexOf('--watchAll=false') === -1) { 42 | // https://github.com/facebook/create-react-app/issues/5210 43 | const hasSourceControl = isInGitRepository() || isInMercurialRepository(); 44 | argv.push(hasSourceControl ? '--watch' : '--watchAll'); 45 | } 46 | 47 | jest.run(argv); 48 | -------------------------------------------------------------------------------- /src/components/TxForm/TxForm.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Typography } from 'antd'; 2 | import React, { useCallback, useState } from 'react'; 3 | import ReactJson from 'react-json-view'; 4 | import { useRecoilValueLoadable } from 'recoil'; 5 | import { sendTransaction } from 'src/connector'; 6 | import { useTonWallet } from 'src/hooks/useTonWallet'; 7 | import { walletsListQuery } from 'src/state/wallets-list'; 8 | import './style.scss'; 9 | 10 | const { Title } = Typography; 11 | 12 | const defaultTx = { 13 | validUntil: Date.now() + 1000000, 14 | messages: [ 15 | { 16 | address: '0:412410771DA82CBA306A55FA9E0D43C9D245E38133CB58F1457DFB8D5CD8892F', 17 | amount: '20000000', 18 | }, 19 | { 20 | address: '0:E69F10CC84877ABF539F83F879291E5CA169451BA7BCE91A37A5CED3AB8080D3', 21 | amount: '60000000', 22 | }, 23 | ], 24 | }; 25 | 26 | export function TxForm() { 27 | const [tx, setTx] = useState(defaultTx); 28 | const wallet = useTonWallet(); 29 | const walletsList = useRecoilValueLoadable(walletsListQuery); 30 | 31 | const onChange = useCallback((value: object) => setTx((value as { updated_src: typeof defaultTx }).updated_src), []); 32 | 33 | return ( 34 |
35 | Configure and send transaction 36 | 37 | {wallet ? ( 38 | 41 | ) : ( 42 |
Connect wallet to send the transaction
43 | )} 44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/connector.ts: -------------------------------------------------------------------------------- 1 | import { SendTransactionRequest, TonConnect, UserRejectsError, WalletInfo, WalletInfoInjected } from '@tonconnect/sdk'; 2 | import { notification } from 'antd'; 3 | import { isMobile, openLink } from 'src/utils'; 4 | 5 | const dappMetadata = { manifestUrl: 'https://ton-connect.github.io/demo-dapp-with-backend/tonconnect-manifest.json' }; 6 | 7 | export const connector = new TonConnect(dappMetadata); 8 | export async function sendTransaction(tx: SendTransactionRequest, wallet: WalletInfo): Promise<{ boc: string }> { 9 | try { 10 | if ('universalLink' in wallet && !(wallet as WalletInfoInjected).embedded && isMobile()) { 11 | openLink(addReturnStrategy(wallet.universalLink, 'none'), '_blank'); 12 | } 13 | 14 | const result = await connector.sendTransaction(tx); 15 | notification.success({ 16 | message: 'Successful transaction', 17 | description: 18 | 'You transaction was successfully sent. Please wait until the transaction is included to the TON blockchain.', 19 | duration: 5, 20 | }); 21 | console.log(`Send tx result: ${JSON.stringify(result)}`); 22 | return result; 23 | } catch (e) { 24 | let message = 'Send transaction error'; 25 | let description = ''; 26 | 27 | if (typeof e === 'object' && e instanceof UserRejectsError) { 28 | message = 'You rejected the transaction'; 29 | description = 'Please try again and confirm transaction in your wallet.'; 30 | } 31 | 32 | notification.error({ 33 | message, 34 | description, 35 | }); 36 | console.log(e); 37 | throw e; 38 | } 39 | } 40 | 41 | export function addReturnStrategy(url: string, returnStrategy: 'back' | 'none'): string { 42 | const link = new URL(url); 43 | link.searchParams.append('ret', returnStrategy); 44 | return link.toString(); 45 | } 46 | -------------------------------------------------------------------------------- /config/getHttpsConfig.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const crypto = require('crypto'); 6 | const chalk = require('react-dev-utils/chalk'); 7 | const paths = require('./paths'); 8 | 9 | // Ensure the certificate and key provided are valid and if not 10 | // throw an easy to debug error 11 | function validateKeyAndCerts({ cert, key, keyFile, crtFile }) { 12 | let encrypted; 13 | try { 14 | // publicEncrypt will throw an error with an invalid cert 15 | encrypted = crypto.publicEncrypt(cert, Buffer.from('test')); 16 | } catch (err) { 17 | throw new Error( 18 | `The certificate "${chalk.yellow(crtFile)}" is invalid.\n${err.message}` 19 | ); 20 | } 21 | 22 | try { 23 | // privateDecrypt will throw an error with an invalid key 24 | crypto.privateDecrypt(key, encrypted); 25 | } catch (err) { 26 | throw new Error( 27 | `The certificate key "${chalk.yellow(keyFile)}" is invalid.\n${ 28 | err.message 29 | }` 30 | ); 31 | } 32 | } 33 | 34 | // Read file and throw an error if it doesn't exist 35 | function readEnvFile(file, type) { 36 | if (!fs.existsSync(file)) { 37 | throw new Error( 38 | `You specified ${chalk.cyan( 39 | type 40 | )} in your env, but the file "${chalk.yellow(file)}" can't be found.` 41 | ); 42 | } 43 | return fs.readFileSync(file); 44 | } 45 | 46 | // Get the https config 47 | // Return cert files if provided in env, otherwise just true or false 48 | function getHttpsConfig() { 49 | const { SSL_CRT_FILE, SSL_KEY_FILE, HTTPS } = process.env; 50 | const isHttps = HTTPS === 'true'; 51 | 52 | if (isHttps && SSL_CRT_FILE && SSL_KEY_FILE) { 53 | const crtFile = path.resolve(paths.appPath, SSL_CRT_FILE); 54 | const keyFile = path.resolve(paths.appPath, SSL_KEY_FILE); 55 | const config = { 56 | cert: readEnvFile(crtFile, 'SSL_CRT_FILE'), 57 | key: readEnvFile(keyFile, 'SSL_KEY_FILE'), 58 | }; 59 | 60 | validateKeyAndCerts({ ...config, keyFile, crtFile }); 61 | return config; 62 | } 63 | return isHttps; 64 | } 65 | 66 | module.exports = getHttpsConfig; 67 | -------------------------------------------------------------------------------- /src/TonProofDemoApi.ts: -------------------------------------------------------------------------------- 1 | import { TonProofItemReplySuccess } from '@tonconnect/protocol'; 2 | import { Account } from '@tonconnect/sdk'; 3 | import { connector } from './connector'; 4 | import './patch-local-storage-for-github-pages'; 5 | 6 | class TonProofDemoApiService { 7 | localStorageKey = 'demo-api-access-token'; 8 | 9 | host = 'https://demo.tonconnect.dev'; 10 | 11 | accessToken: string | null = null; 12 | 13 | constructor() { 14 | this.accessToken = localStorage.getItem(this.localStorageKey); 15 | 16 | connector.onStatusChange((wallet) => { 17 | if (!wallet) { 18 | this.reset(); 19 | return; 20 | } 21 | 22 | const tonProof = wallet.connectItems?.tonProof; 23 | 24 | if (tonProof) { 25 | if ('proof' in tonProof) { 26 | this.checkProof(tonProof.proof, wallet.account); 27 | return; 28 | } 29 | 30 | console.error(tonProof.error); 31 | } 32 | 33 | if (!this.accessToken) { 34 | connector.disconnect(); 35 | } 36 | }); 37 | } 38 | 39 | async generatePayload() { 40 | const response = await ( 41 | await fetch(`${this.host}/ton-proof/generatePayload`, { 42 | method: 'POST', 43 | }) 44 | ).json(); 45 | 46 | return response.payload as string; 47 | } 48 | 49 | async checkProof(proof: TonProofItemReplySuccess['proof'], account: Account) { 50 | try { 51 | const reqBody = { 52 | address: account.address, 53 | network: account.chain, 54 | proof: { 55 | ...proof, 56 | state_init: account.walletStateInit, 57 | }, 58 | }; 59 | 60 | const response = await ( 61 | await fetch(`${this.host}/ton-proof/checkProof`, { 62 | method: 'POST', 63 | body: JSON.stringify(reqBody), 64 | }) 65 | ).json(); 66 | 67 | if (response?.token) { 68 | localStorage.setItem(this.localStorageKey, response.token); 69 | this.accessToken = response.token; 70 | } 71 | } catch (e) { 72 | console.log('checkProof error:', e); 73 | } 74 | } 75 | 76 | async getAccountInfo(account: Account) { 77 | const response = await ( 78 | await fetch(`${this.host}/dapp/getAccountInfo?network=${account.chain}`, { 79 | headers: { 80 | Authorization: `Bearer ${this.accessToken}`, 81 | 'Content-Type': 'application/json', 82 | }, 83 | }) 84 | ).json(); 85 | 86 | return response as {}; 87 | } 88 | 89 | reset() { 90 | this.accessToken = null; 91 | localStorage.removeItem(this.localStorageKey); 92 | } 93 | } 94 | 95 | export const TonProofDemoApi = new TonProofDemoApiService(); 96 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebook/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 13 | // "public path" at which the app is served. 14 | // webpack needs to know it to put the right