├── docs ├── privacy-policy.txt ├── terms-of-use.txt ├── tc-verify.json ├── apple-touch-icon.png ├── tonconnect-manifest.json ├── tonconnect-manifest.vercel.json ├── index.html ├── assets │ └── index-6de983ab.css ├── mockServiceWorker.js └── mockServiceWorkerDemo.js ├── public ├── terms-of-use.txt ├── privacy-policy.txt ├── tc-verify.json ├── apple-touch-icon.png ├── tonconnect-manifest.json ├── tonconnect-manifest.vercel.json ├── mockServiceWorker.js └── mockServiceWorkerDemo.js ├── src ├── vite-env.d.ts ├── server │ ├── api │ │ ├── healthz.ts │ │ ├── generate-payload.ts │ │ ├── get-account-info.ts │ │ ├── check-sign-data.ts │ │ ├── check-proof.ts │ │ ├── wait-for-transaction.ts │ │ ├── find-transaction-by-external-message.ts │ │ ├── merkle_proof.ts │ │ └── create-jetton.ts │ ├── dto │ │ ├── create-jetton-request-dto.ts │ │ ├── check-proof-request-dto.ts │ │ └── check-sign-data-request-dto.ts │ ├── utils │ │ ├── transactions-utils.ts │ │ ├── jwt.ts │ │ ├── http-utils.ts │ │ └── exotic.ts │ ├── worker.ts │ ├── services │ │ ├── ton-api-service.ts │ │ ├── ton-proof-service.ts │ │ └── sign-data-service.ts │ └── wrappers │ │ ├── wallet-contract-v4-r1.ts │ │ └── wallets-data.ts ├── polyfills.ts ├── components │ ├── Footer │ │ ├── ColorsSelect │ │ │ ├── style.scss │ │ │ └── ColorsSelect.tsx │ │ ├── ColorsModal │ │ │ ├── style.scss │ │ │ └── ColorsModal.tsx │ │ ├── footer.scss │ │ └── Footer.tsx │ ├── Header │ │ ├── Header.tsx │ │ └── header.scss │ ├── TonProofDemo │ │ ├── style.scss │ │ └── TonProofDemo.tsx │ ├── CreateJettonDemo │ │ ├── style.scss │ │ └── CreateJettonDemo.tsx │ ├── WalletBatchLimitsTester │ │ ├── style.scss │ │ └── WalletBatchLimitsTester.tsx │ ├── MerkleExample │ │ ├── style.scss │ │ └── MerkleExample.tsx │ ├── TransferUsdt │ │ ├── style.scss │ │ └── TransferUsdt.tsx │ ├── TxForm │ │ ├── style.scss │ │ └── TxForm.tsx │ ├── SignDataTester │ │ ├── style.scss │ │ └── SignDataTester.tsx │ └── FindTransactionDemo │ │ ├── FindTransactionDemo.tsx │ │ └── style.scss ├── App.scss ├── index.scss ├── hooks │ └── useInterval.ts ├── utils │ ├── run-single-instance.ts │ └── units.ts ├── patch-local-storage-for-github-pages.ts ├── App.tsx ├── main.tsx ├── assets │ └── react.svg └── TonProofDemoApi.ts ├── tsconfig.node.json ├── README.md ├── .gitignore ├── vite.config.ts ├── tsconfig.json ├── index.html └── package.json /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 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /docs/tc-verify.json: -------------------------------------------------------------------------------- 1 | { 2 | "payload": "th8tVbI2m9oAAAAAZJ7D1RzJ1JIYtpDgaiMk8pd0hlu6HSmRvQjwbk6fGE7ozlAV" 3 | } -------------------------------------------------------------------------------- /public/tc-verify.json: -------------------------------------------------------------------------------- 1 | { 2 | "payload": "th8tVbI2m9oAAAAAZJ7D1RzJ1JIYtpDgaiMk8pd0hlu6HSmRvQjwbk6fGE7ozlAV" 3 | } -------------------------------------------------------------------------------- /docs/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ton-connect/demo-dapp-with-react-ui/master/docs/apple-touch-icon.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ton-connect/demo-dapp-with-react-ui/master/public/apple-touch-icon.png -------------------------------------------------------------------------------- /src/server/api/healthz.ts: -------------------------------------------------------------------------------- 1 | import {HttpResponseResolver} from "msw"; 2 | import {ok} from "../utils/http-utils"; 3 | 4 | export const healthz: HttpResponseResolver = async () => { 5 | return ok({ok: true}); 6 | }; 7 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | import {Buffer} from 'buffer'; 2 | 3 | declare global { 4 | interface Window { 5 | Buffer: typeof Buffer; 6 | } 7 | } 8 | 9 | if (window && !window.Buffer) { 10 | window.Buffer = Buffer; 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Demo dapp with @tonconnect/ui-react 2 | 3 | Try it out https://ton-connect.github.io/demo-dapp-with-react-ui/ 4 | 5 | ## Learn more about Ton Connect 6 | - https://docs.ton.org/develop/dapps/ton-connect/ 7 | - https://github.com/ton-connect/sdk/tree/main/packages/ui 8 | -------------------------------------------------------------------------------- /src/components/Footer/ColorsSelect/style.scss: -------------------------------------------------------------------------------- 1 | .colors-container { 2 | > div { 3 | margin-bottom: 20px; 4 | 5 | > span { 6 | margin-right: 14px; 7 | font-weight: bold; 8 | } 9 | 10 | > label { 11 | margin-right: 10px; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/components/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import {TonConnectButton} from "@tonconnect/ui-react"; 2 | import './header.scss'; 3 | 4 | export const Header = () => { 5 | return
6 | My App with React UI 7 | 8 |
9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | .env 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | .ssh 27 | -------------------------------------------------------------------------------- /src/server/dto/create-jetton-request-dto.ts: -------------------------------------------------------------------------------- 1 | import zod from "zod"; 2 | 3 | export const CreateJettonRequest = zod.object({ 4 | name: zod.string(), 5 | description: zod.string(), 6 | image_data: zod.string(), 7 | symbol: zod.string(), 8 | decimals: zod.number(), 9 | amount: zod.string(), 10 | }); 11 | 12 | export type CreateJettonRequestDto = zod.infer; 13 | -------------------------------------------------------------------------------- /docs/tonconnect-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://ton-connect.github.io/demo-dapp-with-react-ui/", 3 | "name": "Demo Dapp with React UI", 4 | "iconUrl": "https://ton-connect.github.io/demo-dapp-with-react-ui/apple-touch-icon.png", 5 | "termsOfUseUrl": "https://ton-connect.github.io/demo-dapp-with-react-ui/terms-of-use.txt", 6 | "privacyPolicyUrl": "https://ton-connect.github.io/demo-dapp-with-react-ui/privacy-policy.txt" 7 | } 8 | -------------------------------------------------------------------------------- /public/tonconnect-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://ton-connect.github.io/demo-dapp-with-react-ui/", 3 | "name": "Demo Dapp with React UI", 4 | "iconUrl": "https://ton-connect.github.io/demo-dapp-with-react-ui/apple-touch-icon.png", 5 | "termsOfUseUrl": "https://ton-connect.github.io/demo-dapp-with-react-ui/terms-of-use.txt", 6 | "privacyPolicyUrl": "https://ton-connect.github.io/demo-dapp-with-react-ui/privacy-policy.txt" 7 | } 8 | -------------------------------------------------------------------------------- /docs/tonconnect-manifest.vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://tonconnect-demo-dapp-with-react-ui.vercel.app/", 3 | "name": "Demo Dapp with React UI", 4 | "iconUrl": "https://tonconnect-demo-dapp-with-react-ui.vercel.app/apple-touch-icon.png", 5 | "termsOfUseUrl": "https://tonconnect-demo-dapp-with-react-ui.vercel.app/terms-of-use.txt", 6 | "privacyPolicyUrl": "https://tonconnect-demo-dapp-with-react-ui.vercel.app/privacy-policy.txt" 7 | } 8 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | 6 | export default defineConfig({ 7 | plugins: [react()], 8 | build: { 9 | outDir: 'docs' 10 | }, 11 | // @ts-ignore 12 | base: process.env.GH_PAGES ? '/demo-dapp-with-react-ui/' : './', 13 | server: { 14 | fs: { 15 | allow: ['../sdk', './'], 16 | }, 17 | }, 18 | }) 19 | -------------------------------------------------------------------------------- /public/tonconnect-manifest.vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://tonconnect-demo-dapp-with-react-ui.vercel.app/", 3 | "name": "Demo Dapp with React UI", 4 | "iconUrl": "https://tonconnect-demo-dapp-with-react-ui.vercel.app/apple-touch-icon.png", 5 | "termsOfUseUrl": "https://tonconnect-demo-dapp-with-react-ui.vercel.app/terms-of-use.txt", 6 | "privacyPolicyUrl": "https://tonconnect-demo-dapp-with-react-ui.vercel.app/privacy-policy.txt" 7 | } 8 | -------------------------------------------------------------------------------- /src/components/Footer/ColorsModal/style.scss: -------------------------------------------------------------------------------- 1 | .modal { 2 | position: fixed; 3 | left: 0; 4 | top: 0; 5 | bottom: 0; 6 | right: 0; 7 | z-index: 10000000; 8 | 9 | background-color: rgb(16, 22, 31); 10 | 11 | padding: 20px; 12 | 13 | color: white; 14 | 15 | > button { 16 | float: right; 17 | } 18 | 19 | &__toggle { 20 | display: flex; 21 | justify-content: center; 22 | gap: 20px; 23 | 24 | > a { 25 | color: white; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/components/Header/header.scss: -------------------------------------------------------------------------------- 1 | header { 2 | display: flex; 3 | align-items: center; 4 | justify-content: space-between; 5 | padding: 10px 25px; 6 | 7 | > span { 8 | font-size: 30px; 9 | line-height: 34px; 10 | color: rgba(102,170,238,0.91); 11 | font-weight: bold; 12 | } 13 | } 14 | 15 | @media (max-width: 525px) { 16 | header { 17 | flex-direction: column; 18 | gap: 10px; 19 | 20 | > *:nth-child(2) { 21 | align-self: flex-end; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/App.scss: -------------------------------------------------------------------------------- 1 | .app { 2 | min-height: 100%; 3 | display: flex; 4 | flex-direction: column; 5 | 6 | > header { 7 | margin-bottom: 10px; 8 | } 9 | } 10 | 11 | .react-json-view { 12 | background: rgba(24, 32, 48, 0.98) !important; 13 | border-radius: 14px; 14 | box-shadow: 0 2px 12px 0 rgba(16, 22, 31, 0.13); 15 | padding: 18px 18px 14px 18px; 16 | margin-top: 0; 17 | margin-bottom: 0; 18 | width: 100%; 19 | overflow-x: auto; 20 | font-size: 15px; 21 | word-break: break-all; 22 | white-space: pre-wrap; 23 | } -------------------------------------------------------------------------------- /src/components/Footer/footer.scss: -------------------------------------------------------------------------------- 1 | .footer { 2 | padding: 20px; 3 | display: flex; 4 | gap: 20px; 5 | justify-content: flex-end; 6 | align-items: center; 7 | flex-wrap: wrap; 8 | 9 | > div > label { 10 | color: white; 11 | margin-right: 5px; 12 | } 13 | } 14 | 15 | .footer-checkbox-container { 16 | display: flex; 17 | flex-direction: column; 18 | 19 | > span { 20 | color: white; 21 | font-weight: bold; 22 | margin-bottom: 4px; 23 | } 24 | 25 | input { 26 | margin-left: 3px; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | html, body, #root { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | background-color: rgba(16, 22, 31, 0.92);; 8 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 9 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 10 | sans-serif; 11 | -webkit-font-smoothing: antialiased; 12 | -moz-osx-font-smoothing: grayscale; 13 | } 14 | 15 | code { 16 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 17 | monospace; 18 | } 19 | 20 | * { 21 | box-sizing: border-box; 22 | } 23 | -------------------------------------------------------------------------------- /src/hooks/useInterval.ts: -------------------------------------------------------------------------------- 1 | import {useEffect, useLayoutEffect, useRef} from 'react' 2 | 3 | function useInterval(callback: () => void, delay: number | null) { 4 | const savedCallback = useRef(callback) 5 | 6 | useLayoutEffect(() => { 7 | savedCallback.current = callback 8 | }, [callback]) 9 | 10 | useEffect(() => { 11 | if (!delay && delay !== 0) { 12 | return 13 | } 14 | 15 | const id = setInterval(() => savedCallback.current(), delay) 16 | 17 | return () => clearInterval(id) 18 | }, [delay]) 19 | } 20 | 21 | export default useInterval 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /src/server/dto/check-proof-request-dto.ts: -------------------------------------------------------------------------------- 1 | import {CHAIN} from "@tonconnect/ui-react"; 2 | import zod from "zod"; 3 | 4 | export const CheckProofRequest = zod.object({ 5 | address: zod.string(), 6 | network: zod.enum([CHAIN.MAINNET, CHAIN.TESTNET]), 7 | public_key: zod.string(), 8 | proof: zod.object({ 9 | timestamp: zod.number(), 10 | domain: zod.object({ 11 | lengthBytes: zod.number(), 12 | value: zod.string(), 13 | }), 14 | payload: zod.string(), 15 | signature: zod.string(), 16 | state_init: zod.string(), 17 | }), 18 | payloadToken: zod.string(), 19 | }); 20 | 21 | export type CheckProofRequestDto = zod.infer; 22 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Demo Dapp with @tonconnect/ui-react 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Demo Dapp with @tonconnect/ui-react 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/utils/run-single-instance.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * An asynchronous task. 3 | */ 4 | type AsyncTask = () => Promise; 5 | 6 | /** 7 | * A function that runs an asynchronous task. 8 | */ 9 | type RunAsyncTask = () => Promise; 10 | 11 | /** 12 | * Runs a single instance of an asynchronous task without overlapping. 13 | * 14 | * @param asyncTask - The asynchronous task to be executed. 15 | * @return - A function that, when called, runs the asyncTask if it is not already running. 16 | */ 17 | export function runSingleInstance(asyncTask: AsyncTask): RunAsyncTask { 18 | let isTaskRunning = false; 19 | return async () => { 20 | if (isTaskRunning) { 21 | return; 22 | } 23 | 24 | isTaskRunning = true; 25 | try { 26 | await asyncTask(); 27 | } catch (e) { 28 | console.error(e); 29 | } finally { 30 | isTaskRunning = false; 31 | } 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /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) => setItem.apply(localStorage, [separator + key, value]); 5 | localStorage.setItem = (key: unknown, value: string) => 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 | localStorage.getItem = (key: unknown) => getItem.apply(localStorage, [separator + key]); 10 | 11 | const removeItem = localStorage.removeItem; 12 | localStorage.constructor.prototype.removeItem = (key: unknown) => removeItem.apply(localStorage, [separator + key]); 13 | localStorage.removeItem = (key: unknown) => removeItem.apply(localStorage, [separator + key]); 14 | 15 | export {}; 16 | -------------------------------------------------------------------------------- /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 | padding: 20px; 9 | 10 | h3 { 11 | color: white; 12 | opacity: 0.8; 13 | } 14 | 15 | > div:nth-child(3) { 16 | width: 100%; 17 | 18 | span { 19 | word-break: break-word; 20 | } 21 | } 22 | 23 | &__error { 24 | color: rgba(102,170,238,0.91); 25 | font-size: 18px; 26 | line-height: 20px; 27 | } 28 | 29 | button { 30 | border: none; 31 | padding: 7px 15px; 32 | border-radius: 15px; 33 | cursor: pointer; 34 | 35 | background-color: rgba(102,170,238,0.91); 36 | color: white; 37 | font-size: 16px; 38 | line-height: 20px; 39 | 40 | transition: transform 0.1s ease-in-out; 41 | 42 | &:hover { 43 | transform: scale(1.03); 44 | } 45 | 46 | &:active { 47 | transform: scale(0.97); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/components/CreateJettonDemo/style.scss: -------------------------------------------------------------------------------- 1 | .create-jetton-demo { 2 | display: flex; 3 | width: 100%; 4 | flex-direction: column; 5 | gap: 20px; 6 | align-items: center; 7 | margin-top: 60px; 8 | padding: 20px; 9 | 10 | h3 { 11 | color: white; 12 | opacity: 0.8; 13 | } 14 | 15 | > div:nth-child(3) { 16 | width: 100%; 17 | 18 | span { 19 | word-break: break-word; 20 | } 21 | } 22 | 23 | &__error { 24 | color: rgba(102,170,238,0.91); 25 | font-size: 18px; 26 | line-height: 20px; 27 | } 28 | 29 | button { 30 | border: none; 31 | padding: 7px 15px; 32 | border-radius: 15px; 33 | cursor: pointer; 34 | 35 | background-color: rgba(102,170,238,0.91); 36 | color: white; 37 | font-size: 16px; 38 | line-height: 20px; 39 | 40 | transition: transform 0.1s ease-in-out; 41 | 42 | &:hover { 43 | transform: scale(1.03); 44 | } 45 | 46 | &:active { 47 | transform: scale(0.97); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/server/api/generate-payload.ts: -------------------------------------------------------------------------------- 1 | import {sha256} from "@ton/crypto"; 2 | import {HttpResponseResolver} from "msw"; 3 | import {TonProofService} from "../services/ton-proof-service"; 4 | import {badRequest, ok} from "../utils/http-utils"; 5 | import {createPayloadToken} from "../utils/jwt"; 6 | 7 | /** 8 | * Generates a payload for ton proof. 9 | * 10 | * POST /api/generate_payload 11 | */ 12 | export const generatePayload: HttpResponseResolver = async () => { 13 | try { 14 | const service = new TonProofService(); 15 | 16 | const randomBytes = await service.generateRandomBytes(); 17 | const payloadToken = await createPayloadToken({ 18 | randomBytes: randomBytes.toString('hex') 19 | }); 20 | const payloadTokenHash = (await sha256(payloadToken)).toString('hex'); 21 | 22 | return ok({ 23 | payloadToken: payloadToken, 24 | payloadTokenHash: payloadTokenHash, 25 | }); 26 | } catch (e) { 27 | return badRequest({error: 'Invalid request', trace: e}); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /src/server/api/get-account-info.ts: -------------------------------------------------------------------------------- 1 | import {HttpResponseResolver} from "msw"; 2 | import {TonApiService} from "../services/ton-api-service"; 3 | import {badRequest, ok, unauthorized} from "../utils/http-utils"; 4 | import {decodeAuthToken, verifyToken} from "../utils/jwt"; 5 | 6 | /** 7 | * Returns account info. 8 | * 9 | * GET /api/get_account_info 10 | */ 11 | export const getAccountInfo: HttpResponseResolver = async ({request}) => { 12 | try { 13 | const token = request.headers.get('Authorization')?.replace('Bearer ', ''); 14 | 15 | if (!token || !await verifyToken(token)) { 16 | return unauthorized({error: 'Unauthorized'}); 17 | } 18 | 19 | const payload = decodeAuthToken(token); 20 | if (!payload?.address || !payload?.network) { 21 | return unauthorized({error: 'Invalid token'}); 22 | } 23 | 24 | const client = TonApiService.create(payload.network); 25 | 26 | return ok(await client.getAccountInfo(payload.address)); 27 | } catch (e) { 28 | return badRequest({error: 'Invalid request', trace: e}); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /src/components/WalletBatchLimitsTester/style.scss: -------------------------------------------------------------------------------- 1 | .wallet-batch-limits-tester { 2 | display: flex; 3 | width: 100%; 4 | flex-direction: column; 5 | gap: 20px; 6 | align-items: center; 7 | margin-top: 60px; 8 | padding: 20px; 9 | 10 | h3 { 11 | color: white; 12 | opacity: 0.8; 13 | } 14 | 15 | &__info { 16 | color: white; 17 | font-size: 18px; 18 | opacity: 0.8; 19 | } 20 | 21 | &__error { 22 | color: rgba(102,170,238,0.91); 23 | font-size: 18px; 24 | line-height: 20px; 25 | } 26 | 27 | &__buttons { 28 | display: flex; 29 | gap: 20px; 30 | flex-wrap: wrap; 31 | justify-content: center; 32 | } 33 | 34 | button { 35 | border: none; 36 | padding: 7px 15px; 37 | border-radius: 15px; 38 | cursor: pointer; 39 | 40 | background-color: rgba(102,170,238,0.91); 41 | color: white; 42 | font-size: 16px; 43 | line-height: 20px; 44 | 45 | transition: transform 0.1s ease-in-out; 46 | 47 | &:hover { 48 | transform: scale(1.03); 49 | } 50 | 51 | &:active { 52 | transform: scale(0.97); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/components/Footer/ColorsModal/ColorsModal.tsx: -------------------------------------------------------------------------------- 1 | import { THEME } from "@tonconnect/ui-react"; 2 | import {useState} from "react"; 3 | import {ColorsSelect} from "../ColorsSelect/ColorsSelect"; 4 | import './style.scss'; 5 | 6 | export const ColorsModal = () => { 7 | const [opened, setOpened] = useState(false); 8 | const [theme, setTheme] = useState(THEME.LIGHT); 9 | 10 | return(<> 11 | 12 | {opened && 13 |
14 | 15 | 19 | 20 | 21 |
22 | } 23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/components/MerkleExample/style.scss: -------------------------------------------------------------------------------- 1 | .merkle-proof-demo { 2 | display: flex; 3 | width: 100%; 4 | flex-direction: column; 5 | gap: 20px; 6 | align-items: center; 7 | margin-top: 60px; 8 | padding: 20px; 9 | 10 | h3 { 11 | color: white; 12 | opacity: 0.8; 13 | } 14 | 15 | > div:nth-child(3) { 16 | width: 100%; 17 | 18 | span { 19 | word-break: break-word; 20 | } 21 | } 22 | 23 | &__buttons { 24 | display: flex; 25 | gap: 20px; 26 | flex-wrap: wrap; 27 | justify-content: center; 28 | } 29 | 30 | &__error { 31 | color: rgba(102,170,238,0.91); 32 | font-size: 18px; 33 | line-height: 20px; 34 | } 35 | 36 | button { 37 | border: none; 38 | padding: 7px 15px; 39 | border-radius: 15px; 40 | cursor: pointer; 41 | 42 | background-color: rgba(102,170,238,0.91); 43 | color: white; 44 | font-size: 16px; 45 | line-height: 20px; 46 | 47 | transition: transform 0.1s ease-in-out; 48 | 49 | &:hover { 50 | transform: scale(1.03); 51 | } 52 | 53 | &:active { 54 | transform: scale(0.97); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/components/TransferUsdt/style.scss: -------------------------------------------------------------------------------- 1 | .transfer-usdt { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 10px; 5 | padding: 20px; 6 | align-items: center; 7 | margin-top: 32px; 8 | 9 | h3, 10 | h4 { 11 | margin: 0; 12 | color: white; 13 | opacity: 0.8; 14 | } 15 | 16 | .input-group { 17 | display: flex; 18 | flex-direction: column; 19 | gap: 10px; 20 | width: 500px; 21 | 22 | label { 23 | display: flex; 24 | flex-direction: column; 25 | color: white; 26 | } 27 | 28 | input { 29 | width: 500px; 30 | padding: 5px 10px; 31 | border-radius: 10px; 32 | border: 1px solid #ccc; 33 | } 34 | } 35 | 36 | a { 37 | color:rgba(102, 170, 238, 0.91) 38 | } 39 | 40 | button { 41 | margin-top: 16px; 42 | border: none; 43 | padding: 7px 15px; 44 | border-radius: 15px; 45 | cursor: pointer; 46 | background-color: rgba(102, 170, 238, 0.91); 47 | color: white; 48 | font-size: 16px; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/components/TxForm/style.scss: -------------------------------------------------------------------------------- 1 | .send-tx-form { 2 | flex: 1; 3 | display: flex; 4 | width: 100%; 5 | flex-direction: column; 6 | gap: 20px; 7 | padding: 20px; 8 | align-items: center; 9 | 10 | h3 { 11 | color: white; 12 | opacity: 0.8; 13 | font-size: 28px; 14 | } 15 | 16 | > div:nth-child(2) { 17 | width: 100%; 18 | 19 | span { 20 | word-break: break-word; 21 | } 22 | } 23 | 24 | > button { 25 | border: none; 26 | padding: 7px 15px; 27 | border-radius: 15px; 28 | cursor: pointer; 29 | 30 | background-color: rgba(102,170,238,0.91); 31 | color: white; 32 | font-size: 16px; 33 | line-height: 20px; 34 | 35 | transition: transform 0.1s ease-in-out; 36 | 37 | &:hover { 38 | transform: scale(1.03); 39 | } 40 | 41 | &:active { 42 | transform: scale(0.97); 43 | } 44 | } 45 | 46 | &__json-view { 47 | background: rgba(24, 32, 48, 0.98); 48 | border-radius: 14px; 49 | box-shadow: 0 2px 12px 0 rgba(16, 22, 31, 0.13); 50 | padding: 18px 18px 14px 18px; 51 | margin-top: 0; 52 | margin-bottom: 0; 53 | width: 100%; 54 | overflow-x: auto; 55 | font-size: 15px; 56 | word-break: break-all; 57 | white-space: pre-wrap; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/server/utils/transactions-utils.ts: -------------------------------------------------------------------------------- 1 | import { beginCell, storeMessage } from "@ton/core"; 2 | /** 3 | * Generates a normalized hash of an "external-in" message for comparison. 4 | * Follows TEP-467. 5 | */ 6 | export function getNormalizedExtMessageHash(message: any) { 7 | if (message.info.type !== 'external-in') { 8 | throw new Error(`Message must be "external-in", got ${message.info.type}`); 9 | } 10 | const info = { ...message.info, src: undefined, importFee: 0n }; 11 | const normalizedMessage = { 12 | ...message, 13 | init: null, 14 | info: info, 15 | }; 16 | return beginCell().store(storeMessage(normalizedMessage, { forceRef: true })).endCell().hash(); 17 | } 18 | 19 | /** 20 | * Retries async fn with delay and count. 21 | */ 22 | export async function retry(fn: () => Promise, options: { retries: number; delay: number }): Promise { 23 | let lastError: Error | undefined; 24 | for (let i = 0; i < options.retries; i++) { 25 | try { 26 | return await fn(); 27 | } catch (e) { 28 | if (e instanceof Error) { 29 | lastError = e; 30 | } 31 | await new Promise((resolve) => setTimeout(resolve, options.delay)); 32 | } 33 | } 34 | throw lastError; 35 | } -------------------------------------------------------------------------------- /src/server/api/check-sign-data.ts: -------------------------------------------------------------------------------- 1 | import { HttpResponseResolver } from "msw"; 2 | import { CheckSignDataRequest } from "../dto/check-sign-data-request-dto"; 3 | import { TonApiService } from "../services/ton-api-service"; 4 | import { SignDataService } from "../services/sign-data-service"; 5 | import { badRequest, ok } from "../utils/http-utils"; 6 | 7 | /** 8 | * Checks the sign data signature and returns verification result. 9 | * 10 | * POST /api/check_sign_data 11 | */ 12 | export const checkSignData: HttpResponseResolver = async ({ request }) => { 13 | try { 14 | const body = CheckSignDataRequest.parse(await request.json()); 15 | 16 | const client = TonApiService.create(body.network); 17 | const service = new SignDataService(); 18 | 19 | const isValid = await service.checkSignData(body, (address) => 20 | client.getWalletPublicKey(address) 21 | ); 22 | 23 | if (!isValid) { 24 | return badRequest({ error: "Invalid signature" }); 25 | } 26 | 27 | return ok({ 28 | valid: true, 29 | message: "Signature verified successfully", 30 | payload: body.payload, 31 | address: body.address, 32 | timestamp: body.timestamp, 33 | domain: body.domain, 34 | }); 35 | } catch (e) { 36 | return badRequest({ error: "Invalid request", trace: e }); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /src/server/worker.ts: -------------------------------------------------------------------------------- 1 | import { http } from "msw"; 2 | import { setupWorker } from "msw/browser"; 3 | import { checkProof } from "./api/check-proof"; 4 | import { checkSignData } from "./api/check-sign-data"; 5 | import { createJetton } from "./api/create-jetton"; 6 | import { generatePayload } from "./api/generate-payload"; 7 | import { getAccountInfo } from "./api/get-account-info"; 8 | import { healthz } from "./api/healthz"; 9 | import { merkleProof } from "./api/merkle_proof"; 10 | import { findTransactionByExternalMessage } from "./api/find-transaction-by-external-message"; 11 | import { waitForTransactionResolver } from "./api/wait-for-transaction"; 12 | 13 | const baseUrl = document.baseURI.replace(/\/$/, ""); 14 | 15 | export const worker = setupWorker( 16 | http.get(`${baseUrl}/api/healthz`, healthz), 17 | http.post(`${baseUrl}/api/generate_payload`, generatePayload), 18 | http.post(`${baseUrl}/api/check_proof`, checkProof), 19 | http.post(`${baseUrl}/api/check_sign_data`, checkSignData), 20 | http.get(`${baseUrl}/api/get_account_info`, getAccountInfo), 21 | http.post(`${baseUrl}/api/create_jetton`, createJetton), 22 | http.post(`${baseUrl}/api/merkle_proof`, merkleProof), 23 | http.post(`${baseUrl}/api/find_transaction_by_external_message`, findTransactionByExternalMessage), 24 | http.post(`${baseUrl}/api/wait_for_transaction`, waitForTransactionResolver), 25 | ); 26 | -------------------------------------------------------------------------------- /src/components/SignDataTester/style.scss: -------------------------------------------------------------------------------- 1 | .sign-data-tester { 2 | display: flex; 3 | width: 100%; 4 | flex-direction: column; 5 | gap: 20px; 6 | align-items: center; 7 | margin-top: 60px; 8 | padding: 20px; 9 | 10 | h3 { 11 | color: white; 12 | opacity: 0.8; 13 | } 14 | 15 | &__info { 16 | color: white; 17 | font-size: 18px; 18 | opacity: 0.8; 19 | } 20 | 21 | &__error { 22 | color: rgba(102,170,238,0.91); 23 | font-size: 18px; 24 | line-height: 20px; 25 | } 26 | 27 | &__buttons { 28 | display: flex; 29 | gap: 20px; 30 | flex-wrap: wrap; 31 | justify-content: center; 32 | } 33 | 34 | button { 35 | border: none; 36 | padding: 7px 15px; 37 | border-radius: 15px; 38 | cursor: pointer; 39 | 40 | background-color: rgba(102,170,238,0.91); 41 | color: white; 42 | font-size: 16px; 43 | line-height: 20px; 44 | 45 | transition: transform 0.1s ease-in-out; 46 | 47 | &:hover { 48 | transform: scale(1.03); 49 | } 50 | 51 | &:active { 52 | transform: scale(0.97); 53 | } 54 | } 55 | 56 | &__debug { 57 | width: 100%; 58 | max-width: 800px; 59 | margin-top: 20px; 60 | text-align: left; 61 | 62 | h4 { 63 | color: white; 64 | opacity: 0.9; 65 | margin-bottom: 10px; 66 | font-size: 16px; 67 | } 68 | 69 | .react-json-view { 70 | border-radius: 8px; 71 | padding: 10px; 72 | font-size: 12px; 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /src/server/services/ton-api-service.ts: -------------------------------------------------------------------------------- 1 | import {Address, TonClient4} from "@ton/ton"; 2 | import {CHAIN} from "@tonconnect/ui-react"; 3 | import {Buffer} from "buffer"; 4 | 5 | export class TonApiService { 6 | 7 | public static create(client: TonClient4 | CHAIN): TonApiService { 8 | if (client === CHAIN.MAINNET) { 9 | client = new TonClient4({ 10 | endpoint: 'https://mainnet-v4.tonhubapi.com' 11 | }); 12 | } 13 | if (client === CHAIN.TESTNET) { 14 | client = new TonClient4({ 15 | endpoint: 'https://testnet-v4.tonhubapi.com' 16 | }); 17 | } 18 | return new TonApiService(client); 19 | } 20 | 21 | private readonly client: TonClient4; 22 | 23 | private constructor(client: TonClient4) { 24 | this.client = client; 25 | } 26 | 27 | /** 28 | * Get wallet public key by address. 29 | */ 30 | public async getWalletPublicKey(address: string): Promise { 31 | const masterAt = await this.client.getLastBlock(); 32 | const result = await this.client.runMethod( 33 | masterAt.last.seqno, Address.parse(address), 'get_public_key', []); 34 | return Buffer.from(result.reader.readBigNumber().toString(16).padStart(64, '0'), 'hex'); 35 | } 36 | 37 | /** 38 | * Get account info by address. 39 | */ 40 | public async getAccountInfo(address: string): Promise> { 41 | const masterAt = await this.client.getLastBlock(); 42 | return await this.client.getAccount(masterAt.last.seqno, Address.parse(address)); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/server/api/check-proof.ts: -------------------------------------------------------------------------------- 1 | import {sha256} from "@ton/crypto"; 2 | import {HttpResponseResolver} from "msw"; 3 | import {CheckProofRequest} from "../dto/check-proof-request-dto"; 4 | import {TonApiService} from "../services/ton-api-service"; 5 | import {TonProofService} from "../services/ton-proof-service"; 6 | import {badRequest, ok} from "../utils/http-utils"; 7 | import {createAuthToken, verifyToken} from "../utils/jwt"; 8 | 9 | /** 10 | * Checks the proof and returns an access token. 11 | * 12 | * POST /api/check_proof 13 | */ 14 | export const checkProof: HttpResponseResolver = async ({request}) => { 15 | try { 16 | const body = CheckProofRequest.parse(await request.json()); 17 | 18 | const client = TonApiService.create(body.network); 19 | const service = new TonProofService(); 20 | 21 | const isValid = await service.checkProof(body, (address) => client.getWalletPublicKey(address)); 22 | if (!isValid) { 23 | return badRequest({error: 'Invalid proof'}); 24 | } 25 | 26 | const payloadTokenHash = body.proof.payload; 27 | const payloadToken = body.payloadToken; 28 | if (!await verifyToken(payloadToken)) { 29 | return badRequest({error: 'Invalid token'}); 30 | } 31 | if ((await sha256(payloadToken)).toString('hex') !== payloadTokenHash) { 32 | return badRequest({error: 'Invalid payload token hash'}) 33 | } 34 | 35 | const token = await createAuthToken({address: body.address, network: body.network}); 36 | 37 | return ok({token: token}); 38 | } catch (e) { 39 | return badRequest({error: 'Invalid request', trace: e}); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import './App.scss' 2 | import {THEME, TonConnectUIProvider} from "@tonconnect/ui-react"; 3 | import {Header} from "./components/Header/Header"; 4 | import {TxForm} from "./components/TxForm/TxForm"; 5 | import {Footer} from "./components/Footer/Footer"; 6 | import {TonProofDemo} from "./components/TonProofDemo/TonProofDemo"; 7 | import {CreateJettonDemo} from "./components/CreateJettonDemo/CreateJettonDemo"; 8 | import {WalletBatchLimitsTester} from "./components/WalletBatchLimitsTester/WalletBatchLimitsTester"; 9 | import {SignDataTester} from "./components/SignDataTester/SignDataTester"; 10 | import { MerkleExample } from "./components/MerkleExample/MerkleExample"; 11 | import { FindTransactionDemo } from './components/FindTransactionDemo/FindTransactionDemo'; 12 | import { TransferUsdt } from './components/TransferUsdt/TransferUsdt'; 13 | 14 | function App() { 15 | return ( 16 | 23 |
24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 |
35 |
36 | ) 37 | } 38 | 39 | export default App 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-dapp-react-ui", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "VITE_MANIFEST_URL=https://ton-connect.github.io/demo-dapp-with-react-ui/tonconnect-manifest.json vite --host --force", 8 | "build": "tsc && GH_PAGES=true VITE_GH_PAGES=true VITE_MANIFEST_URL=https://ton-connect.github.io/demo-dapp-with-react-ui/tonconnect-manifest.json vite build && cp ./public/mockServiceWorkerDemo.js ./docs/mockServiceWorker.js", 9 | "build:dev": "tsc && VITE_MANIFEST_URL=https://tonconnect-demo-dapp-with-react-ui.vercel.app/tonconnect-manifest.vercel.json vite build && cp ./public/mockServiceWorkerDemo.js ./docs/mockServiceWorker.js", 10 | "preview": "VITE_MANIFEST_URL=https://ton-connect.github.io/demo-dapp-with-react-ui/tonconnect-manifest.json vite preview" 11 | }, 12 | "dependencies": { 13 | "@ton-community/assets-sdk": "0.0.5", 14 | "@ton/core": "0.61.0", 15 | "@ton/crypto": "3.3.0", 16 | "@ton/ton": "15.3.1", 17 | "@tonconnect/ui-react": "2.3.0-beta.4", 18 | "buffer": "^6.0.3", 19 | "crc-32": "^1.2.2", 20 | "eruda": "^2.11.2", 21 | "jose": "^5.2.4", 22 | "msw": "2.10.4", 23 | "react": "^17.0.0", 24 | "react-dom": "^17.0.0", 25 | "react-json-view": "^1.21.3", 26 | "zod": "^3.23.8" 27 | }, 28 | "devDependencies": { 29 | "@types/node": "^22.13.4", 30 | "@types/react": "^18.0.26", 31 | "@types/react-dom": "^18.0.9", 32 | "@vitejs/plugin-react": "^3.0.0", 33 | "sass": "^1.57.1", 34 | "typescript": "^4.9.3", 35 | "vite": "^4.0.0" 36 | }, 37 | "msw": { 38 | "workerDirectory": [ 39 | "public" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/server/dto/check-sign-data-request-dto.ts: -------------------------------------------------------------------------------- 1 | import { CHAIN } from "@tonconnect/ui-react"; 2 | import zod from "zod"; 3 | 4 | const SignDataPayloadText = zod.object({ 5 | type: zod.literal("text"), 6 | text: zod.string(), 7 | network: zod.string().optional(), 8 | from: zod.string().optional(), 9 | }); 10 | 11 | const SignDataPayloadBinary = zod.object({ 12 | type: zod.literal("binary"), 13 | bytes: zod.string(), // base64 (not url safe) encoded bytes array 14 | network: zod.string().optional(), 15 | from: zod.string().optional(), 16 | }); 17 | 18 | const SignDataPayloadCell = zod.object({ 19 | type: zod.literal("cell"), 20 | schema: zod.string(), // TL-B scheme of the cell payload 21 | cell: zod.string(), // base64 (not url safe) encoded cell 22 | network: zod.string().optional(), 23 | from: zod.string().optional(), 24 | }); 25 | 26 | const SignDataPayload = zod.union([ 27 | SignDataPayloadText, 28 | SignDataPayloadBinary, 29 | SignDataPayloadCell, 30 | ]); 31 | 32 | export const CheckSignDataRequest = zod.object({ 33 | address: zod.string(), 34 | network: zod.enum([CHAIN.MAINNET, CHAIN.TESTNET]), 35 | public_key: zod.string(), 36 | signature: zod.string(), // base64 37 | timestamp: zod.number(), 38 | domain: zod.string(), 39 | payload: SignDataPayload, 40 | walletStateInit: zod.string(), // base64 encoded state init 41 | }); 42 | 43 | export type CheckSignDataRequestDto = zod.infer; 44 | export type SignDataPayloadText = zod.infer; 45 | export type SignDataPayloadBinary = zod.infer; 46 | export type SignDataPayloadCell = zod.infer; 47 | export type SignDataPayload = zod.infer; 48 | -------------------------------------------------------------------------------- /src/server/utils/jwt.ts: -------------------------------------------------------------------------------- 1 | import {CHAIN} from "@tonconnect/ui-react"; 2 | import {decodeJwt, JWTPayload, jwtVerify, SignJWT} from 'jose'; 3 | 4 | /** 5 | * Secret key for the token. 6 | */ 7 | const JWT_SECRET_KEY = 'your_secret_key'; 8 | 9 | /** 10 | * Payload of the token. 11 | */ 12 | export type AuthToken = { 13 | address: string; 14 | network: CHAIN; 15 | }; 16 | 17 | export type PayloadToken = { 18 | randomBytes: string; 19 | }; 20 | 21 | /** 22 | * Create a token with the given payload. 23 | */ 24 | function buildCreateToken(expirationTime: string): (payload: T) => Promise { 25 | return async (payload: T) => { 26 | const encoder = new TextEncoder(); 27 | const key = encoder.encode(JWT_SECRET_KEY); 28 | return new SignJWT(payload) 29 | .setProtectedHeader({alg: 'HS256'}) 30 | .setIssuedAt() 31 | .setExpirationTime(expirationTime) 32 | .sign(key); 33 | }; 34 | } 35 | 36 | export const createAuthToken = buildCreateToken('1Y'); 37 | export const createPayloadToken = buildCreateToken('15m'); 38 | 39 | /** 40 | * Verify the given token. 41 | */ 42 | export async function verifyToken(token: string): Promise { 43 | const encoder = new TextEncoder(); 44 | const key = encoder.encode(JWT_SECRET_KEY); 45 | try { 46 | const {payload} = await jwtVerify(token, key); 47 | return payload; 48 | } catch (e) { 49 | return null; 50 | } 51 | } 52 | 53 | 54 | /** 55 | * Decode the given token. 56 | */ 57 | function buildDecodeToken(): (token: string) => T | null { 58 | return (token: string) => { 59 | try { 60 | return decodeJwt(token) as T; 61 | } catch (e) { 62 | return null; 63 | } 64 | }; 65 | } 66 | 67 | export const decodeAuthToken = buildDecodeToken(); 68 | export const decodePayloadToken = buildDecodeToken(); 69 | -------------------------------------------------------------------------------- /src/server/utils/http-utils.ts: -------------------------------------------------------------------------------- 1 | import { HttpResponse, JsonBodyType, StrictResponse } from "msw"; 2 | import { Address, Cell } from '@ton/core'; 3 | 4 | 5 | /** 6 | * Receives a body and returns an HTTP response with the given body and status code 200. 7 | */ 8 | export function ok(body: T): StrictResponse { 9 | return HttpResponse.text(JSON.stringify(body, jsonReplacer, 2), { status: 200, statusText: 'OK', headers: { 'Content-Type': 'application/json' } }); 10 | } 11 | 12 | /** 13 | * Receives a body and returns an HTTP response with the given body and status code 400. 14 | */ 15 | export function badRequest(body: T): StrictResponse { 16 | return HttpResponse.json(body, { 17 | status: 400, 18 | statusText: 'Bad Request' 19 | }); 20 | } 21 | 22 | /** 23 | * Receives a body and returns an HTTP response with the given body and status code 401. 24 | */ 25 | export function unauthorized(body: T): StrictResponse { 26 | return HttpResponse.json(body, { 27 | status: 401, 28 | statusText: 'Unauthorized' 29 | }); 30 | } 31 | 32 | 33 | export function notFound(body: T): StrictResponse { 34 | return HttpResponse.json(body, { 35 | status: 404, 36 | statusText: 'Not Found' 37 | }); 38 | } 39 | 40 | 41 | export function jsonReplacer(_key: string, value: unknown): unknown { 42 | if (typeof value === 'bigint') { 43 | return value.toString(); 44 | } else if (value instanceof Address) { 45 | return value.toString(); 46 | } else if (value instanceof Cell) { 47 | return value.toBoc().toString('base64'); 48 | } else if (value instanceof Buffer) { 49 | return value.toString('base64'); 50 | } else if ( 51 | value && 52 | typeof value === 'object' && 53 | (value as any).type === 'Buffer' && 54 | Array.isArray((value as any).data) 55 | ) { 56 | return Buffer.from((value as any).data).toString('base64'); 57 | } 58 | 59 | return value; 60 | } 61 | 62 | -------------------------------------------------------------------------------- /src/server/wrappers/wallet-contract-v4-r1.ts: -------------------------------------------------------------------------------- 1 | import {Cell, contractAddress, WalletContractV4 as WalletContractV4R2} from "@ton/ton"; 2 | import {Buffer} from "buffer"; 3 | 4 | export class WalletContractV4R1 { 5 | static create(args: { workchain: number, publicKey: Buffer, walletId?: number | null }) { 6 | const wallet = WalletContractV4R2.create(args); 7 | const {data} = wallet.init; 8 | const code = Cell.fromBoc(Buffer.from('B5EE9C72410215010002F5000114FF00F4A413F4BCF2C80B010201200203020148040504F8F28308D71820D31FD31FD31F02F823BBF263ED44D0D31FD31FD3FFF404D15143BAF2A15151BAF2A205F901541064F910F2A3F80024A4C8CB1F5240CB1F5230CBFF5210F400C9ED54F80F01D30721C0009F6C519320D74A96D307D402FB00E830E021C001E30021C002E30001C0039130E30D03A4C8CB1F12CB1FCBFF1112131403EED001D0D3030171B0915BE021D749C120915BE001D31F218210706C7567BD228210626C6E63BDB022821064737472BDB0925F03E002FA403020FA4401C8CA07CBFFC9D0ED44D0810140D721F404305C810108F40A6FA131B3925F05E004D33FC8258210706C7567BA9131E30D248210626C6E63BAE30004060708020120090A005001FA00F404308210706C7567831EB17080185005CB0527CF165003FA02F40012CB69CB1F5210CB3F0052F8276F228210626C6E63831EB17080185005CB0527CF1624FA0214CB6A13CB1F5230CB3F01FA02F4000092821064737472BA8E3504810108F45930ED44D0810140D720C801CF16F400C9ED54821064737472831EB17080185004CB0558CF1622FA0212CB6ACB1FCB3F9410345F04E2C98040FB000201200B0C0059BD242B6F6A2684080A06B90FA0218470D4080847A4937D29910CE6903E9FF9837812801B7810148987159F31840201580D0E0011B8C97ED44D0D70B1F8003DB29DFB513420405035C87D010C00B23281F2FFF274006040423D029BE84C600201200F100019ADCE76A26840206B90EB85FFC00019AF1DF6A26840106B90EB858FC0006ED207FA00D4D422F90005C8CA0715CBFFC9D077748018C8CB05CB0222CF165005FA0214CB6B12CCCCC971FB00C84014810108F451F2A702006C810108D718C8542025810108F451F2A782106E6F746570748018C8CB05CB025004CF16821005F5E100FA0213CB6A12CB1FC971FB00020072810108D718305202810108F459F2A7F82582106473747270748018C8CB05CB025005CF16821005F5E100FA0214CB6A13CB1F12CB3FC973FB00000AF400C9ED5446A9F34F', 'hex'))[0]!; 9 | (wallet as any).init = {data, code}; 10 | (wallet as any).address = contractAddress(args.workchain, wallet.init); 11 | return wallet; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/MerkleExample/MerkleExample.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | import { useTonConnectUI, useTonWallet } from "@tonconnect/ui-react"; 4 | import ReactJson from "react-json-view"; 5 | import { Cell, toNano } from "@ton/core"; 6 | import { 7 | buildSuccessMerkleProof, 8 | buildSuccessMerkleUpdate, 9 | buildVerifyMerkleProof, 10 | buildVerifyMerkleUpdate, 11 | } from "../../server/utils/exotic"; 12 | 13 | import './style.scss'; 14 | import { TonProofDemoApi } from "../../TonProofDemoApi"; 15 | 16 | const merkleExampleAddress = 'EQD_5KMZVIqzYY91-t5CdRD_V71wRrVzxDXu9n2XEwz2wwdv'; 17 | const merkleProofBody = buildVerifyMerkleProof(buildSuccessMerkleProof()); 18 | const merkleUpdateBody = buildVerifyMerkleUpdate(buildSuccessMerkleUpdate()); 19 | 20 | export const MerkleExample = () => { 21 | const [tonConnectUI] = useTonConnectUI(); 22 | const wallet = useTonWallet(); 23 | 24 | const handleMerkleProofClick = async () => { 25 | const response = await TonProofDemoApi.merkleProof(); 26 | 27 | if (!('error' in response)) { 28 | await tonConnectUI.sendTransaction(response); 29 | } 30 | }; 31 | 32 | const handleMerkleUpdateClick = async () => { 33 | const myTransaction = { 34 | validUntil: Math.floor(Date.now() / 1000) + 360, 35 | messages: [ 36 | { 37 | address: merkleExampleAddress, 38 | amount: toNano("0.05").toString(), 39 | payload: merkleUpdateBody.toBoc().toString("base64") 40 | } 41 | ] 42 | } 43 | 44 | await tonConnectUI.sendTransaction(myTransaction); 45 | } 46 | 47 | return ( 48 |
49 |

Merkle proof/update

50 | {wallet ? ( 51 |
52 | 55 | 58 |
59 | ) : ( 60 |
Connect wallet to send transaction
61 | )} 62 |
63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import './patch-local-storage-for-github-pages'; 2 | import './polyfills'; 3 | import eruda from "eruda"; 4 | 5 | import React, {StrictMode} from 'react' 6 | import {render} from 'react-dom'; 7 | import App from './App' 8 | import './index.scss' 9 | import {runSingleInstance} from "./utils/run-single-instance"; 10 | import { enableQaMode } from '@tonconnect/ui-react'; 11 | 12 | if (import.meta.env.VITE_QA_MODE === 'enable') { 13 | enableQaMode(); 14 | } 15 | 16 | eruda.init(); 17 | 18 | async function enableMocking() { 19 | const host = document.baseURI.replace(/\/$/, ''); 20 | 21 | return new Promise(async (resolve) => { 22 | const {worker} = await import('./server/worker'); 23 | 24 | const startMockWorker = () => worker.start({ 25 | onUnhandledRequest: 'bypass', 26 | quiet: false, 27 | serviceWorker: { 28 | url: `${import.meta.env.VITE_GH_PAGES ? '/demo-dapp-with-react-ui' : ''}/mockServiceWorker.js` 29 | } 30 | }); 31 | let serviceWorkerRegistration: ServiceWorkerRegistration | null | void = await startMockWorker(); 32 | resolve(serviceWorkerRegistration); 33 | 34 | const verifyAndRestartWorker = runSingleInstance(async () => { 35 | try { 36 | const serviceWorkerRegistrations = await navigator.serviceWorker?.getRegistrations() || []; 37 | 38 | const isServiceWorkerOk = serviceWorkerRegistrations.length > 0; 39 | const isApiOk = await fetch(`${host}/api/healthz`) 40 | .then(r => r.status === 200 ? r.json().then(p => p.ok).catch(() => false) : false) 41 | .catch(() => false); 42 | 43 | if (!isServiceWorkerOk || !isApiOk) { 44 | await serviceWorkerRegistration?.unregister().catch(() => {}); 45 | serviceWorkerRegistration = await startMockWorker().catch(() => null); 46 | } 47 | } catch (error) { 48 | console.error('Error in verifyAndRestartWorker:', error); 49 | serviceWorkerRegistration = await startMockWorker().catch(() => null); 50 | } 51 | }); 52 | 53 | setInterval(verifyAndRestartWorker, 1_000); 54 | }); 55 | } 56 | 57 | enableMocking().then(() => render( 58 | 59 | 60 | , 61 | document.getElementById('root') as HTMLElement 62 | )); 63 | -------------------------------------------------------------------------------- /src/server/api/wait-for-transaction.ts: -------------------------------------------------------------------------------- 1 | import { Cell, loadMessage, TonClient, Transaction } from "@ton/ton"; 2 | import { getNormalizedExtMessageHash, retry } from "../utils/transactions-utils"; 3 | import { HttpResponseResolver } from "msw"; 4 | import { badRequest, notFound, ok } from "../utils/http-utils"; 5 | 6 | async function waitForTransaction( 7 | inMessageBoc: string, 8 | client: TonClient, 9 | retries: number = 10, 10 | timeout: number = 1000, 11 | ): Promise { 12 | const inMessage = loadMessage(Cell.fromBase64(inMessageBoc).beginParse()); 13 | 14 | if (inMessage.info.type !== 'external-in') { 15 | throw new Error(`Message must be "external-in", got ${inMessage.info.type}`); 16 | } 17 | const account = inMessage.info.dest; 18 | 19 | const targetInMessageHash = getNormalizedExtMessageHash(inMessage); 20 | 21 | let attempt = 0; 22 | while (attempt < retries) { 23 | console.log(`Waiting for transaction to appear in network. Attempt: ${attempt}`); 24 | 25 | const transactions = await retry( 26 | () => 27 | client.getTransactions(account, { 28 | limit: 10, 29 | archival: true, 30 | }), 31 | { delay: 1000, retries: 3 }, 32 | ); 33 | 34 | for (const transaction of transactions) { 35 | if (transaction.inMessage?.info.type !== 'external-in') { 36 | continue; 37 | } 38 | 39 | const inMessageHash = getNormalizedExtMessageHash(transaction.inMessage); 40 | if (inMessageHash.equals(targetInMessageHash)) { 41 | return transaction; 42 | } 43 | } 44 | 45 | await new Promise((resolve) => setTimeout(resolve, timeout)); 46 | } 47 | 48 | return undefined; 49 | } 50 | 51 | export const waitForTransactionResolver: HttpResponseResolver = async ({ request }) => { 52 | try { 53 | const body = (await request.json()) as any; 54 | const network = body.network; 55 | const inMessageBoc = body.inMessageBoc; 56 | const client = new TonClient({ 57 | endpoint: `https://${network === 'testnet' ? 'tesnet.' : ''}toncenter.com/api/v2/jsonRPC`, 58 | }); 59 | const transaction = await waitForTransaction(inMessageBoc, client); 60 | if (!transaction) { 61 | return notFound({ error: 'Transaction not found' }); 62 | } 63 | 64 | return ok({ transaction: { ...transaction, hash: transaction.hash().toString('base64') } }); 65 | } catch (e) { 66 | return badRequest({ error: 'Invalid request', trace: e instanceof Error ? e.message : e }); 67 | } 68 | }; -------------------------------------------------------------------------------- /src/components/WalletBatchLimitsTester/WalletBatchLimitsTester.tsx: -------------------------------------------------------------------------------- 1 | import './style.scss'; 2 | import { SendTransactionRequest, useTonConnectUI, useTonWallet } from "@tonconnect/ui-react"; 3 | import { Address } from '@ton/ton'; 4 | 5 | // Component to test wallet batch message limits 6 | export function WalletBatchLimitsTester() { 7 | const wallet = useTonWallet(); 8 | const [tonConnectUi] = useTonConnectUI(); 9 | 10 | // Generate transaction with specified number of messages 11 | const generateMultipleMessages = (count: number): SendTransactionRequest => { 12 | // The transaction is valid for 10 minutes 13 | const validUntil = Math.floor(Date.now() / 1000) + 600; 14 | 15 | // Get user's wallet address and convert to non-bounceable format 16 | let userAddress = ''; 17 | if (wallet && wallet.account) { 18 | try { 19 | // Convert to Address object then to non-bounceable format 20 | const address = Address.parse(wallet.account.address); 21 | userAddress = address.toString({ 22 | urlSafe: true, 23 | bounceable: false 24 | }); 25 | } catch (e) { 26 | console.error('Error converting address:', e); 27 | userAddress = wallet.account.address; 28 | } 29 | } 30 | 31 | // Create array with 'count' messages 32 | const messages = Array(count).fill(null).map(() => ({ 33 | // Send to user's own wallet address in non-bounceable format 34 | address: userAddress, 35 | // Small amount to send in nanoTON (0.00001 TON = 10000 nanoTON) 36 | amount: '10000', 37 | })); 38 | 39 | return { 40 | validUntil, 41 | messages, 42 | }; 43 | }; 44 | 45 | // Send transaction with specified number of messages 46 | const handleSendTransaction = (count: number) => { 47 | const tx = generateMultipleMessages(count); 48 | tonConnectUi.sendTransaction(tx); 49 | }; 50 | 51 | return ( 52 |
53 |

Batch Message Limits Test

54 | 55 |
56 | Send multiple messages to the wallet to test message batching capabilities 57 |
58 | 59 | {wallet ? ( 60 |
61 | 66 | 71 |
72 | ) : ( 73 |
74 | Connect wallet to test batch limits 75 |
76 | )} 77 |
78 | ); 79 | } -------------------------------------------------------------------------------- /src/components/TonProofDemo/TonProofDemo.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useRef, useState } from 'react'; 2 | import ReactJson from 'react-json-view'; 3 | import './style.scss'; 4 | import { TonProofDemoApi } from "../../TonProofDemoApi"; 5 | import { useTonConnectUI, useTonWallet } from "@tonconnect/ui-react"; 6 | import useInterval from "../../hooks/useInterval"; 7 | 8 | 9 | export const TonProofDemo = () => { 10 | const firstProofLoading = useRef(true); 11 | 12 | const [data, setData] = useState({}); 13 | const wallet = useTonWallet(); 14 | const [authorized, setAuthorized] = useState(false); 15 | const [tonConnectUI] = useTonConnectUI(); 16 | 17 | const recreateProofPayload = useCallback(async () => { 18 | if (firstProofLoading.current) { 19 | tonConnectUI.setConnectRequestParameters({ state: 'loading' }); 20 | firstProofLoading.current = false; 21 | } 22 | 23 | const payload = await TonProofDemoApi.generatePayload(); 24 | 25 | if (payload) { 26 | tonConnectUI.setConnectRequestParameters({ state: 'ready', value: payload }); 27 | } else { 28 | tonConnectUI.setConnectRequestParameters(null); 29 | } 30 | }, [tonConnectUI, firstProofLoading]) 31 | 32 | if (firstProofLoading.current) { 33 | recreateProofPayload(); 34 | } 35 | 36 | useInterval(recreateProofPayload, TonProofDemoApi.refreshIntervalMs); 37 | 38 | useEffect(() => 39 | tonConnectUI.onStatusChange(async w => { 40 | if (!w) { 41 | TonProofDemoApi.reset(); 42 | setAuthorized(false); 43 | return; 44 | } 45 | 46 | if (w.connectItems?.tonProof && 'proof' in w.connectItems.tonProof) { 47 | await TonProofDemoApi.checkProof(w.connectItems.tonProof.proof, w.account); 48 | } 49 | 50 | if (!TonProofDemoApi.accessToken) { 51 | tonConnectUI.disconnect(); 52 | setAuthorized(false); 53 | return; 54 | } 55 | 56 | setAuthorized(true); 57 | }), [tonConnectUI]); 58 | 59 | 60 | const handleClick = useCallback(async () => { 61 | if (!wallet) { 62 | return; 63 | } 64 | const response = await TonProofDemoApi.getAccountInfo(wallet.account); 65 | 66 | setData(response); 67 | }, [wallet]); 68 | 69 | if (!authorized) { 70 | return null; 71 | } 72 | 73 | return ( 74 |
75 |

Demo backend API with ton_proof verification

76 | {authorized ? ( 77 | 80 | ) : ( 81 |
Connect wallet to call API
82 | )} 83 | {data && Object.keys(data).length > 0 && ( 84 | <> 85 |
Response
86 |
87 | 88 |
89 | 90 | )} 91 |
92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /src/utils/units.ts: -------------------------------------------------------------------------------- 1 | // github.com/wevm/viem/blob/f361a7a0465ded8303ea439b0dc2568459230ee6/src/utils/unit/formatUnits.ts 2 | 3 | /** 4 | * Multiplies a string representation of a number by a given exponent of base 10 (10exponent). 5 | * 6 | * - Docs: https://viem.sh/docs/utilities/parseUnits 7 | * 8 | * @example 9 | * import { parseUnits } from 'viem' 10 | * 11 | * parseUnits('420', 9) 12 | * // 420000000000n 13 | */ 14 | export function parseUnits(value: string, decimals: number) { 15 | let [integer, fraction = '0'] = value.split('.') 16 | 17 | const negative = integer.startsWith('-') 18 | if (negative) integer = integer.slice(1) 19 | 20 | // trim leading zeros. 21 | fraction = fraction.replace(/(0+)$/, '') 22 | 23 | // round off if the fraction is larger than the number of decimals. 24 | if (decimals === 0) { 25 | if (Math.round(Number(`.${fraction}`)) === 1) integer = `${BigInt(integer) + 1n}` 26 | fraction = '' 27 | } else if (fraction.length > decimals) { 28 | const [left, unit, right] = [ 29 | fraction.slice(0, decimals - 1), 30 | fraction.slice(decimals - 1, decimals), 31 | fraction.slice(decimals), 32 | ] 33 | 34 | const rounded = Math.round(Number(`${unit}.${right}`)) 35 | if (rounded > 9) fraction = `${BigInt(left) + BigInt(1)}0`.padStart(left.length + 1, '0') 36 | else fraction = `${left}${rounded}` 37 | 38 | if (fraction.length > decimals) { 39 | fraction = fraction.slice(1) 40 | integer = `${BigInt(integer) + 1n}` 41 | } 42 | 43 | fraction = fraction.slice(0, decimals) 44 | } else { 45 | fraction = fraction.padEnd(decimals, '0') 46 | } 47 | 48 | return BigInt(`${negative ? '-' : ''}${integer}${fraction}`) 49 | } 50 | 51 | /** 52 | * Divides a number by a given exponent of base 10 (10exponent), and formats it into a string representation of the number. 53 | * 54 | * - Docs: https://viem.sh/docs/utilities/formatUnits 55 | * 56 | * @example 57 | * import { formatUnits } from 'viem' 58 | * 59 | * formatUnits(420000000000n, 9) 60 | * // '420' 61 | */ 62 | export function formatUnits(value: bigint | string, decimals: number) { 63 | let display = value.toString() 64 | 65 | const negative = display.startsWith('-') 66 | if (negative) display = display.slice(1) 67 | 68 | display = display.padStart(decimals, '0') 69 | 70 | // eslint-disable-next-line prefer-const 71 | let [integer, fraction] = [ 72 | display.slice(0, display.length - decimals), 73 | display.slice(display.length - decimals), 74 | ] 75 | fraction = fraction.replace(/(0+)$/, '') 76 | return `${negative ? '-' : ''}${integer || '0'}${fraction ? `.${fraction}` : ''}` 77 | } 78 | 79 | export function parseTon(value: string) { 80 | return parseUnits(value, 9) 81 | } 82 | export function formatTon(value: bigint | string) { 83 | return formatUnits(value, 9) 84 | } 85 | -------------------------------------------------------------------------------- /src/components/FindTransactionDemo/FindTransactionDemo.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React, { useState } from 'react'; 4 | import ReactJson from 'react-json-view'; 5 | 6 | import './style.scss'; 7 | import { TonProofDemoApi } from '../../TonProofDemoApi'; 8 | 9 | export const FindTransactionDemo = () => { 10 | const [boc, setBoc] = useState('te6cckEBBQEA6wAB4YgB76ksIXpmobiUHDUtWosNdLgI+loKYwC+3DgXeRr2DJ4F4G+ja0rbyhi5yzD+xbfXI1owr5X3/uucREXZXZP4dqxPXukwqPGVrKzUL0g80tYaTgh95b0myTcmVFMS8cTIOU1NGLtDx7h4AAAQ8AAcAQJ7YgBFLU49uGmU3zOG8nNmcylqMjsoilVMAcYzYexnV5aM2BpiWgAAAAAAAAAAAAAAAAACMAAAAAEhlbGxvIYCBAEU/wD0pBP0vPLICwMASNMB0NMDAXGwkVvg+kAwcIAQyMsFWM8WIfoCy2oBzxbJgED7AAAAGE8sBQ=='); 11 | const [network, setNetwork] = useState<'mainnet' | 'testnet'>('mainnet'); 12 | const [txLoading, setTxLoading] = useState(false); 13 | const [txError, setTxError] = useState(null); 14 | const [txResult, setTxResult] = useState(null); 15 | 16 | const handleFindTx = async () => { 17 | setTxLoading(true); 18 | setTxError(null); 19 | setTxResult(null); 20 | try { 21 | const transaction = await TonProofDemoApi.findTransactionByExternalMessage(boc, network); 22 | if (!transaction) { 23 | setTxError('Transaction not found'); 24 | } else { 25 | setTxResult(transaction); 26 | } 27 | } catch (err: any) { 28 | setTxError(err?.message || 'Unknown error'); 29 | } finally { 30 | setTxLoading(false); 31 | } 32 | }; 33 | 34 | return ( 35 |
36 |

Find Transaction by External-in Message BOC

37 |
38 |