├── .gitignore ├── README.md ├── docs ├── apple-touch-icon.png ├── assets │ ├── index-73bf9a20.js │ ├── index-8d2dd6a7.css │ └── worker-f8577a9e.js ├── index.html ├── mockServiceWorker.js ├── privacy-policy.txt ├── tc-verify.json ├── terms-of-use.txt └── tonconnect-manifest.json ├── index.html ├── package-lock.json ├── package.json ├── public ├── apple-touch-icon.png ├── mockServiceWorker.js ├── privacy-policy.txt ├── tc-verify.json ├── terms-of-use.txt └── tonconnect-manifest.json ├── src ├── App.scss ├── App.tsx ├── TonProofDemoApi.ts ├── assets │ └── react.svg ├── components │ ├── CreateJettonDemo │ │ ├── CreateJettonDemo.tsx │ │ └── style.scss │ ├── Footer │ │ ├── ColorsModal │ │ │ ├── ColorsModal.tsx │ │ │ └── style.scss │ │ ├── ColorsSelect │ │ │ ├── ColorsSelect.tsx │ │ │ └── style.scss │ │ ├── Footer.tsx │ │ └── footer.scss │ ├── Header │ │ ├── Header.tsx │ │ └── header.scss │ ├── SignDataTester │ │ ├── SignDataTester.tsx │ │ └── style.scss │ ├── TonProofDemo │ │ ├── TonProofDemo.tsx │ │ └── style.scss │ ├── TxForm │ │ ├── TxForm.tsx │ │ └── style.scss │ └── WalletBatchLimitsTester │ │ ├── WalletBatchLimitsTester.tsx │ │ └── style.scss ├── hooks │ └── useInterval.ts ├── index.scss ├── main.tsx ├── patch-local-storage-for-github-pages.ts ├── polyfills.ts ├── server │ ├── api │ │ ├── check-proof.ts │ │ ├── check-sign-data.ts │ │ ├── create-jetton.ts │ │ ├── generate-payload.ts │ │ ├── get-account-info.ts │ │ └── healthz.ts │ ├── dto │ │ ├── check-proof-request-dto.ts │ │ ├── check-sign-data-request-dto.ts │ │ └── create-jetton-request-dto.ts │ ├── services │ │ ├── sign-data-service.ts │ │ ├── ton-api-service.ts │ │ └── ton-proof-service.ts │ ├── utils │ │ ├── http-utils.ts │ │ └── jwt.ts │ ├── worker.ts │ └── wrappers │ │ ├── wallet-contract-v4-r1.ts │ │ └── wallets-data.ts ├── utils │ └── run-signle-instance.ts └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.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 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | .ssh 26 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ton-connect/demo-dapp-with-react-ui/cc8b669970506ba8d61c01dc76cfc22c770d18fe/docs/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/assets/index-8d2dd6a7.css: -------------------------------------------------------------------------------- 1 | .app{min-height:100%;display:flex;flex-direction:column}.app>header{margin-bottom:10px}header{display:flex;align-items:center;justify-content:space-between;padding:10px 25px}header>span{font-size:30px;line-height:34px;color:#66aaeee8;font-weight:700}@media (max-width: 525px){header{flex-direction:column;gap:10px}header>*:nth-child(2){align-self:flex-end}}.send-tx-form{flex:1;display:flex;width:100%;flex-direction:column;gap:20px;padding:20px;align-items:center}.send-tx-form h3{color:#fff;opacity:.8;font-size:28px}.send-tx-form>div:nth-child(2){width:100%}.send-tx-form>div:nth-child(2) span{word-break:break-word}.send-tx-form>button{border:none;padding:7px 15px;border-radius:15px;cursor:pointer;background-color:#66aaeee8;color:#fff;font-size:16px;line-height:20px;transition:transform .1s ease-in-out}.send-tx-form>button:hover{transform:scale(1.03)}.send-tx-form>button:active{transform:scale(.97)}.footer{padding:20px;display:flex;gap:20px;justify-content:flex-end;align-items:center;flex-wrap:wrap}.footer>div>label{color:#fff;margin-right:5px}.footer-checkbox-container{display:flex;flex-direction:column}.footer-checkbox-container>span{color:#fff;font-weight:700;margin-bottom:4px}.footer-checkbox-container input{margin-left:3px}.colors-container>div{margin-bottom:20px}.colors-container>div>span{margin-right:14px;font-weight:700}.colors-container>div>label{margin-right:10px}.modal{position:fixed;left:0;top:0;bottom:0;right:0;z-index:10000000;background-color:#10161f;padding:20px;color:#fff}.modal>button{float:right}.modal__toggle{display:flex;justify-content:center;gap:20px}.modal__toggle>a{color:#fff}.ton-proof-demo{display:flex;width:100%;flex-direction:column;gap:20px;align-items:center;margin-top:60px;padding:20px}.ton-proof-demo h3{color:#fff;opacity:.8}.ton-proof-demo>div:nth-child(3){width:100%}.ton-proof-demo>div:nth-child(3) span{word-break:break-word}.ton-proof-demo__error{color:#66aaeee8;font-size:18px;line-height:20px}.ton-proof-demo button{border:none;padding:7px 15px;border-radius:15px;cursor:pointer;background-color:#66aaeee8;color:#fff;font-size:16px;line-height:20px;transition:transform .1s ease-in-out}.ton-proof-demo button:hover{transform:scale(1.03)}.ton-proof-demo button:active{transform:scale(.97)}.create-jetton-demo{display:flex;width:100%;flex-direction:column;gap:20px;align-items:center;margin-top:60px;padding:20px}.create-jetton-demo h3{color:#fff;opacity:.8}.create-jetton-demo>div:nth-child(3){width:100%}.create-jetton-demo>div:nth-child(3) span{word-break:break-word}.create-jetton-demo__error{color:#66aaeee8;font-size:18px;line-height:20px}.create-jetton-demo button{border:none;padding:7px 15px;border-radius:15px;cursor:pointer;background-color:#66aaeee8;color:#fff;font-size:16px;line-height:20px;transition:transform .1s ease-in-out}.create-jetton-demo button:hover{transform:scale(1.03)}.create-jetton-demo button:active{transform:scale(.97)}.wallet-batch-limits-tester{display:flex;width:100%;flex-direction:column;gap:20px;align-items:center;margin-top:60px;padding:20px}.wallet-batch-limits-tester h3{color:#fff;opacity:.8}.wallet-batch-limits-tester__info{color:#fff;font-size:18px;opacity:.8}.wallet-batch-limits-tester__error{color:#66aaeee8;font-size:18px;line-height:20px}.wallet-batch-limits-tester__buttons{display:flex;gap:20px;flex-wrap:wrap;justify-content:center}.wallet-batch-limits-tester button{border:none;padding:7px 15px;border-radius:15px;cursor:pointer;background-color:#66aaeee8;color:#fff;font-size:16px;line-height:20px;transition:transform .1s ease-in-out}.wallet-batch-limits-tester button:hover{transform:scale(1.03)}.wallet-batch-limits-tester button:active{transform:scale(.97)}.sign-data-tester{display:flex;width:100%;flex-direction:column;gap:20px;align-items:center;margin-top:60px;padding:20px}.sign-data-tester h3{color:#fff;opacity:.8}.sign-data-tester__info{color:#fff;font-size:18px;opacity:.8}.sign-data-tester__error{color:#66aaeee8;font-size:18px;line-height:20px}.sign-data-tester__buttons{display:flex;gap:20px;flex-wrap:wrap;justify-content:center}.sign-data-tester button{border:none;padding:7px 15px;border-radius:15px;cursor:pointer;background-color:#66aaeee8;color:#fff;font-size:16px;line-height:20px;transition:transform .1s ease-in-out}.sign-data-tester button:hover{transform:scale(1.03)}.sign-data-tester button:active{transform:scale(.97)}.sign-data-tester__debug{width:100%;max-width:800px;margin-top:20px;text-align:left}.sign-data-tester__debug h4{color:#fff;opacity:.9;margin-bottom:10px;font-size:16px}.sign-data-tester__debug .react-json-view{border-radius:8px;padding:10px;font-size:12px}html,body,#root{height:100%}body{margin:0;background-color:#10161feb;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}*{box-sizing:border-box} 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/mockServiceWorker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | 4 | /** 5 | * Mock Service Worker. 6 | * @see https://github.com/mswjs/msw 7 | * - Please do NOT modify this file. 8 | * - Please do NOT serve this file on production. 9 | */ 10 | 11 | const PACKAGE_VERSION = '2.3.0' 12 | const INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423' 13 | const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') 14 | const activeClientIds = new Set() 15 | 16 | self.addEventListener('install', function () { 17 | self.skipWaiting() 18 | }) 19 | 20 | self.addEventListener('activate', function (event) { 21 | event.waitUntil(self.clients.claim()) 22 | }) 23 | 24 | self.addEventListener('message', async function (event) { 25 | const clientId = event.source.id 26 | 27 | if (!clientId || !self.clients) { 28 | return 29 | } 30 | 31 | const client = await self.clients.get(clientId) 32 | 33 | if (!client) { 34 | return 35 | } 36 | 37 | const allClients = await self.clients.matchAll({ 38 | type: 'window', 39 | }) 40 | 41 | switch (event.data) { 42 | case 'KEEPALIVE_REQUEST': { 43 | sendToClient(client, { 44 | type: 'KEEPALIVE_RESPONSE', 45 | }) 46 | break 47 | } 48 | 49 | case 'INTEGRITY_CHECK_REQUEST': { 50 | sendToClient(client, { 51 | type: 'INTEGRITY_CHECK_RESPONSE', 52 | payload: { 53 | packageVersion: PACKAGE_VERSION, 54 | checksum: INTEGRITY_CHECKSUM, 55 | }, 56 | }) 57 | break 58 | } 59 | 60 | case 'MOCK_ACTIVATE': { 61 | activeClientIds.add(clientId) 62 | 63 | sendToClient(client, { 64 | type: 'MOCKING_ENABLED', 65 | payload: true, 66 | }) 67 | break 68 | } 69 | 70 | case 'MOCK_DEACTIVATE': { 71 | activeClientIds.delete(clientId) 72 | break 73 | } 74 | 75 | case 'CLIENT_CLOSED': { 76 | activeClientIds.delete(clientId) 77 | 78 | const remainingClients = allClients.filter((client) => { 79 | return client.id !== clientId 80 | }) 81 | 82 | // Unregister itself when there are no more clients 83 | if (remainingClients.length === 0) { 84 | self.registration.unregister() 85 | } 86 | 87 | break 88 | } 89 | 90 | } 91 | }) 92 | 93 | self.addEventListener('fetch', function (event) { 94 | const {request} = event 95 | 96 | // Bypass navigation requests. 97 | if (request.mode === 'navigate') { 98 | return 99 | } 100 | 101 | // Opening the DevTools triggers the "only-if-cached" request 102 | // that cannot be handled by the worker. Bypass such requests. 103 | if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { 104 | return 105 | } 106 | 107 | // Bypass all requests when there are no active clients. 108 | // Prevents the self-unregistered worked from handling requests 109 | // after it's been deleted (still remains active until the next reload). 110 | if (activeClientIds.size === 0) { 111 | return 112 | } 113 | 114 | // Bypass event source requests. 115 | const url = new URL(request.url) 116 | if (url.pathname.endsWith('/events')) { 117 | return 118 | } 119 | 120 | // Bypass event with not the same origin and not start with /api 121 | if (url.origin !== location.origin && !url.pathname.startsWith('/api')) { 122 | return 123 | } 124 | 125 | // Generate unique request ID. 126 | const requestId = crypto.randomUUID() 127 | event.respondWith(handleRequest(event, requestId)) 128 | }) 129 | 130 | async function handleRequest(event, requestId) { 131 | const client = await resolveMainClient(event) 132 | const response = await getResponse(event, client, requestId) 133 | 134 | // Send back the response clone for the "response:*" life-cycle events. 135 | // Ensure MSW is active and ready to handle the message, otherwise 136 | // this message will pend indefinitely. 137 | if (client && activeClientIds.has(client.id)) { 138 | ;(async function () { 139 | const responseClone = response.clone() 140 | 141 | sendToClient( 142 | client, 143 | { 144 | type: 'RESPONSE', 145 | payload: { 146 | requestId, 147 | isMockedResponse: IS_MOCKED_RESPONSE in response, 148 | type: responseClone.type, 149 | status: responseClone.status, 150 | statusText: responseClone.statusText, 151 | body: responseClone.body, 152 | headers: Object.fromEntries(responseClone.headers.entries()), 153 | }, 154 | }, 155 | [responseClone.body], 156 | ) 157 | })() 158 | } 159 | 160 | return response 161 | } 162 | 163 | // Resolve the main client for the given event. 164 | // Client that issues a request doesn't necessarily equal the client 165 | // that registered the worker. It's with the latter the worker should 166 | // communicate with during the response resolving phase. 167 | async function resolveMainClient(event) { 168 | const client = await self.clients.get(event.clientId) 169 | 170 | if (client?.frameType === 'top-level') { 171 | return client 172 | } 173 | 174 | const allClients = await self.clients.matchAll({ 175 | type: 'window', 176 | }) 177 | 178 | return allClients 179 | .filter((client) => { 180 | // Get only those clients that are currently visible. 181 | return client.visibilityState === 'visible' 182 | }) 183 | .find((client) => { 184 | // Find the client ID that's recorded in the 185 | // set of clients that have registered the worker. 186 | return activeClientIds.has(client.id) 187 | }) 188 | } 189 | 190 | async function getResponse(event, client, requestId) { 191 | const {request} = event 192 | 193 | // Clone the request because it might've been already used 194 | // (i.e. its body has been read and sent to the client). 195 | const requestClone = request.clone() 196 | 197 | function passthrough() { 198 | const headers = Object.fromEntries(requestClone.headers.entries()) 199 | 200 | // Remove internal MSW request header so the passthrough request 201 | // complies with any potential CORS preflight checks on the server. 202 | // Some servers forbid unknown request headers. 203 | delete headers['x-msw-intention'] 204 | 205 | return fetch(requestClone, {headers}) 206 | } 207 | 208 | // Bypass mocking when the client is not active. 209 | if (!client) { 210 | return passthrough() 211 | } 212 | 213 | // Bypass initial page load requests (i.e. static assets). 214 | // The absence of the immediate/parent client in the map of the active clients 215 | // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet 216 | // and is not ready to handle requests. 217 | if (!activeClientIds.has(client.id)) { 218 | return passthrough() 219 | } 220 | 221 | // Notify the client that a request has been intercepted. 222 | const requestBuffer = await request.arrayBuffer() 223 | const clientMessage = await sendToClient( 224 | client, 225 | { 226 | type: 'REQUEST', 227 | payload: { 228 | id: requestId, 229 | url: request.url, 230 | mode: request.mode, 231 | method: request.method, 232 | headers: Object.fromEntries(request.headers.entries()), 233 | cache: request.cache, 234 | credentials: request.credentials, 235 | destination: request.destination, 236 | integrity: request.integrity, 237 | redirect: request.redirect, 238 | referrer: request.referrer, 239 | referrerPolicy: request.referrerPolicy, 240 | body: requestBuffer, 241 | keepalive: request.keepalive, 242 | }, 243 | }, 244 | [requestBuffer], 245 | ) 246 | 247 | switch (clientMessage.type) { 248 | case 'MOCK_RESPONSE': { 249 | return respondWithMock(clientMessage.data) 250 | } 251 | 252 | case 'PASSTHROUGH': { 253 | return passthrough() 254 | } 255 | } 256 | 257 | return passthrough() 258 | } 259 | 260 | function sendToClient(client, message, transferrables = []) { 261 | return new Promise((resolve, reject) => { 262 | const channel = new MessageChannel() 263 | 264 | channel.port1.onmessage = (event) => { 265 | if (event.data && event.data.error) { 266 | return reject(event.data.error) 267 | } 268 | 269 | resolve(event.data) 270 | } 271 | 272 | client.postMessage( 273 | message, 274 | [channel.port2].concat(transferrables.filter(Boolean)), 275 | ) 276 | }) 277 | } 278 | 279 | async function respondWithMock(response) { 280 | // Setting response status code to 0 is a no-op. 281 | // However, when responding with a "Response.error()", the produced Response 282 | // instance will have status code set to 0. Since it's not possible to create 283 | // a Response instance with status code 0, handle that use-case separately. 284 | if (response.status === 0) { 285 | return Response.error() 286 | } 287 | 288 | const mockedResponse = new Response(response.body, response) 289 | 290 | Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { 291 | value: true, 292 | enumerable: true, 293 | }) 294 | 295 | return mockedResponse 296 | } 297 | -------------------------------------------------------------------------------- /docs/privacy-policy.txt: -------------------------------------------------------------------------------- 1 | Privacy Policy example 2 | ... 3 | -------------------------------------------------------------------------------- /docs/tc-verify.json: -------------------------------------------------------------------------------- 1 | { 2 | "payload": "th8tVbI2m9oAAAAAZJ7D1RzJ1JIYtpDgaiMk8pd0hlu6HSmRvQjwbk6fGE7ozlAV" 3 | } -------------------------------------------------------------------------------- /docs/terms-of-use.txt: -------------------------------------------------------------------------------- 1 | Terms of use example 2 | ... 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 --host --force", 8 | "build": "tsc && GH_PAGES=true VITE_GH_PAGES=true vite build", 9 | "build:dev": "tsc && vite build", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@ton-community/assets-sdk": "0.0.5", 14 | "@ton/core": "^0.56.3", 15 | "@ton/crypto": "^3.2.0", 16 | "@ton/ton": "^14.0.0", 17 | "@tonconnect/ui-react": "^2.2.0-beta.0", 18 | "buffer": "^6.0.3", 19 | "crc-32": "^1.2.2", 20 | "eruda": "^2.11.2", 21 | "jose": "^5.2.4", 22 | "msw": "2.3.5", 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 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ton-connect/demo-dapp-with-react-ui/cc8b669970506ba8d61c01dc76cfc22c770d18fe/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/mockServiceWorker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | 4 | /** 5 | * Mock Service Worker. 6 | * @see https://github.com/mswjs/msw 7 | * - Please do NOT modify this file. 8 | * - Please do NOT serve this file on production. 9 | */ 10 | 11 | const PACKAGE_VERSION = '2.3.0' 12 | const INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423' 13 | const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') 14 | const activeClientIds = new Set() 15 | 16 | self.addEventListener('install', function () { 17 | self.skipWaiting() 18 | }) 19 | 20 | self.addEventListener('activate', function (event) { 21 | event.waitUntil(self.clients.claim()) 22 | }) 23 | 24 | self.addEventListener('message', async function (event) { 25 | const clientId = event.source.id 26 | 27 | if (!clientId || !self.clients) { 28 | return 29 | } 30 | 31 | const client = await self.clients.get(clientId) 32 | 33 | if (!client) { 34 | return 35 | } 36 | 37 | const allClients = await self.clients.matchAll({ 38 | type: 'window', 39 | }) 40 | 41 | switch (event.data) { 42 | case 'KEEPALIVE_REQUEST': { 43 | sendToClient(client, { 44 | type: 'KEEPALIVE_RESPONSE', 45 | }) 46 | break 47 | } 48 | 49 | case 'INTEGRITY_CHECK_REQUEST': { 50 | sendToClient(client, { 51 | type: 'INTEGRITY_CHECK_RESPONSE', 52 | payload: { 53 | packageVersion: PACKAGE_VERSION, 54 | checksum: INTEGRITY_CHECKSUM, 55 | }, 56 | }) 57 | break 58 | } 59 | 60 | case 'MOCK_ACTIVATE': { 61 | activeClientIds.add(clientId) 62 | 63 | sendToClient(client, { 64 | type: 'MOCKING_ENABLED', 65 | payload: true, 66 | }) 67 | break 68 | } 69 | 70 | case 'MOCK_DEACTIVATE': { 71 | activeClientIds.delete(clientId) 72 | break 73 | } 74 | 75 | case 'CLIENT_CLOSED': { 76 | activeClientIds.delete(clientId) 77 | 78 | const remainingClients = allClients.filter((client) => { 79 | return client.id !== clientId 80 | }) 81 | 82 | // Unregister itself when there are no more clients 83 | if (remainingClients.length === 0) { 84 | self.registration.unregister() 85 | } 86 | 87 | break 88 | } 89 | 90 | } 91 | }) 92 | 93 | self.addEventListener('fetch', function (event) { 94 | const {request} = event 95 | 96 | // Bypass navigation requests. 97 | if (request.mode === 'navigate') { 98 | return 99 | } 100 | 101 | // Opening the DevTools triggers the "only-if-cached" request 102 | // that cannot be handled by the worker. Bypass such requests. 103 | if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { 104 | return 105 | } 106 | 107 | // Bypass all requests when there are no active clients. 108 | // Prevents the self-unregistered worked from handling requests 109 | // after it's been deleted (still remains active until the next reload). 110 | if (activeClientIds.size === 0) { 111 | return 112 | } 113 | 114 | // Bypass event source requests. 115 | const url = new URL(request.url) 116 | if (url.pathname.endsWith('/events')) { 117 | return 118 | } 119 | 120 | // Bypass event with not the same origin and not start with /api 121 | if (url.origin !== location.origin && !url.pathname.startsWith('/api')) { 122 | return 123 | } 124 | 125 | // Generate unique request ID. 126 | const requestId = crypto.randomUUID() 127 | event.respondWith(handleRequest(event, requestId)) 128 | }) 129 | 130 | async function handleRequest(event, requestId) { 131 | const client = await resolveMainClient(event) 132 | const response = await getResponse(event, client, requestId) 133 | 134 | // Send back the response clone for the "response:*" life-cycle events. 135 | // Ensure MSW is active and ready to handle the message, otherwise 136 | // this message will pend indefinitely. 137 | if (client && activeClientIds.has(client.id)) { 138 | ;(async function () { 139 | const responseClone = response.clone() 140 | 141 | sendToClient( 142 | client, 143 | { 144 | type: 'RESPONSE', 145 | payload: { 146 | requestId, 147 | isMockedResponse: IS_MOCKED_RESPONSE in response, 148 | type: responseClone.type, 149 | status: responseClone.status, 150 | statusText: responseClone.statusText, 151 | body: responseClone.body, 152 | headers: Object.fromEntries(responseClone.headers.entries()), 153 | }, 154 | }, 155 | [responseClone.body], 156 | ) 157 | })() 158 | } 159 | 160 | return response 161 | } 162 | 163 | // Resolve the main client for the given event. 164 | // Client that issues a request doesn't necessarily equal the client 165 | // that registered the worker. It's with the latter the worker should 166 | // communicate with during the response resolving phase. 167 | async function resolveMainClient(event) { 168 | const client = await self.clients.get(event.clientId) 169 | 170 | if (client?.frameType === 'top-level') { 171 | return client 172 | } 173 | 174 | const allClients = await self.clients.matchAll({ 175 | type: 'window', 176 | }) 177 | 178 | return allClients 179 | .filter((client) => { 180 | // Get only those clients that are currently visible. 181 | return client.visibilityState === 'visible' 182 | }) 183 | .find((client) => { 184 | // Find the client ID that's recorded in the 185 | // set of clients that have registered the worker. 186 | return activeClientIds.has(client.id) 187 | }) 188 | } 189 | 190 | async function getResponse(event, client, requestId) { 191 | const {request} = event 192 | 193 | // Clone the request because it might've been already used 194 | // (i.e. its body has been read and sent to the client). 195 | const requestClone = request.clone() 196 | 197 | function passthrough() { 198 | const headers = Object.fromEntries(requestClone.headers.entries()) 199 | 200 | // Remove internal MSW request header so the passthrough request 201 | // complies with any potential CORS preflight checks on the server. 202 | // Some servers forbid unknown request headers. 203 | delete headers['x-msw-intention'] 204 | 205 | return fetch(requestClone, {headers}) 206 | } 207 | 208 | // Bypass mocking when the client is not active. 209 | if (!client) { 210 | return passthrough() 211 | } 212 | 213 | // Bypass initial page load requests (i.e. static assets). 214 | // The absence of the immediate/parent client in the map of the active clients 215 | // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet 216 | // and is not ready to handle requests. 217 | if (!activeClientIds.has(client.id)) { 218 | return passthrough() 219 | } 220 | 221 | // Notify the client that a request has been intercepted. 222 | const requestBuffer = await request.arrayBuffer() 223 | const clientMessage = await sendToClient( 224 | client, 225 | { 226 | type: 'REQUEST', 227 | payload: { 228 | id: requestId, 229 | url: request.url, 230 | mode: request.mode, 231 | method: request.method, 232 | headers: Object.fromEntries(request.headers.entries()), 233 | cache: request.cache, 234 | credentials: request.credentials, 235 | destination: request.destination, 236 | integrity: request.integrity, 237 | redirect: request.redirect, 238 | referrer: request.referrer, 239 | referrerPolicy: request.referrerPolicy, 240 | body: requestBuffer, 241 | keepalive: request.keepalive, 242 | }, 243 | }, 244 | [requestBuffer], 245 | ) 246 | 247 | switch (clientMessage.type) { 248 | case 'MOCK_RESPONSE': { 249 | return respondWithMock(clientMessage.data) 250 | } 251 | 252 | case 'PASSTHROUGH': { 253 | return passthrough() 254 | } 255 | } 256 | 257 | return passthrough() 258 | } 259 | 260 | function sendToClient(client, message, transferrables = []) { 261 | return new Promise((resolve, reject) => { 262 | const channel = new MessageChannel() 263 | 264 | channel.port1.onmessage = (event) => { 265 | if (event.data && event.data.error) { 266 | return reject(event.data.error) 267 | } 268 | 269 | resolve(event.data) 270 | } 271 | 272 | client.postMessage( 273 | message, 274 | [channel.port2].concat(transferrables.filter(Boolean)), 275 | ) 276 | }) 277 | } 278 | 279 | async function respondWithMock(response) { 280 | // Setting response status code to 0 is a no-op. 281 | // However, when responding with a "Response.error()", the produced Response 282 | // instance will have status code set to 0. Since it's not possible to create 283 | // a Response instance with status code 0, handle that use-case separately. 284 | if (response.status === 0) { 285 | return Response.error() 286 | } 287 | 288 | const mockedResponse = new Response(response.body, response) 289 | 290 | Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { 291 | value: true, 292 | enumerable: true, 293 | }) 294 | 295 | return mockedResponse 296 | } 297 | -------------------------------------------------------------------------------- /public/privacy-policy.txt: -------------------------------------------------------------------------------- 1 | Privacy Policy example 2 | ... 3 | -------------------------------------------------------------------------------- /public/tc-verify.json: -------------------------------------------------------------------------------- 1 | { 2 | "payload": "th8tVbI2m9oAAAAAZJ7D1RzJ1JIYtpDgaiMk8pd0hlu6HSmRvQjwbk6fGE7ozlAV" 3 | } -------------------------------------------------------------------------------- /public/terms-of-use.txt: -------------------------------------------------------------------------------- 1 | Terms of use example 2 | ... 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 11 | function App() { 12 | return ( 13 | 321 |
322 |
323 | 324 | 325 | 326 | 327 | 328 |
330 |
331 | ) 332 | } 333 | 334 | export default App 335 | -------------------------------------------------------------------------------- /src/TonProofDemoApi.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Account, 3 | ConnectAdditionalRequest, 4 | SendTransactionRequest, 5 | TonProofItemReplySuccess, 6 | } from "@tonconnect/ui-react"; 7 | import "./patch-local-storage-for-github-pages"; 8 | import { CreateJettonRequestDto } from "./server/dto/create-jetton-request-dto"; 9 | 10 | class TonProofDemoApiService { 11 | private localStorageKey = "demo-api-access-token"; 12 | 13 | private host = document.baseURI.replace(/\/$/, ""); 14 | 15 | public accessToken: string | null = null; 16 | 17 | public readonly refreshIntervalMs = 9 * 60 * 1000; 18 | 19 | constructor() { 20 | this.accessToken = localStorage.getItem(this.localStorageKey); 21 | 22 | if (!this.accessToken) { 23 | this.generatePayload(); 24 | } 25 | } 26 | 27 | async generatePayload(): Promise { 28 | try { 29 | const response = await ( 30 | await fetch(`${this.host}/api/generate_payload`, { 31 | method: "POST", 32 | }) 33 | ).json(); 34 | return { tonProof: response.payload as string }; 35 | } catch { 36 | return null; 37 | } 38 | } 39 | 40 | async checkProof( 41 | proof: TonProofItemReplySuccess["proof"], 42 | account: Account 43 | ): Promise { 44 | try { 45 | const reqBody = { 46 | address: account.address, 47 | network: account.chain, 48 | public_key: account.publicKey, 49 | proof: { 50 | ...proof, 51 | state_init: account.walletStateInit, 52 | }, 53 | }; 54 | 55 | const response = await ( 56 | await fetch(`${this.host}/api/check_proof`, { 57 | method: "POST", 58 | body: JSON.stringify(reqBody), 59 | }) 60 | ).json(); 61 | 62 | if (response?.token) { 63 | localStorage.setItem(this.localStorageKey, response.token); 64 | this.accessToken = response.token; 65 | } 66 | } catch (e) { 67 | console.log("checkProof error:", e); 68 | } 69 | } 70 | 71 | async getAccountInfo(account: Account) { 72 | const response = await ( 73 | await fetch(`${this.host}/api/get_account_info`, { 74 | headers: { 75 | Authorization: `Bearer ${this.accessToken}`, 76 | "Content-Type": "application/json", 77 | }, 78 | }) 79 | ).json(); 80 | 81 | return response as {}; 82 | } 83 | 84 | async createJetton( 85 | jetton: CreateJettonRequestDto 86 | ): Promise { 87 | return await ( 88 | await fetch(`${this.host}/api/create_jetton`, { 89 | body: JSON.stringify(jetton), 90 | headers: { 91 | Authorization: `Bearer ${this.accessToken}`, 92 | "Content-Type": "application/json", 93 | }, 94 | method: "POST", 95 | }) 96 | ).json(); 97 | } 98 | 99 | async checkSignData(signDataResult: any, account: Account) { 100 | try { 101 | const reqBody = { 102 | address: account.address, 103 | network: account.chain, 104 | public_key: account.publicKey, 105 | signature: signDataResult.signature, 106 | timestamp: signDataResult.timestamp, 107 | domain: signDataResult.domain, 108 | payload: signDataResult.payload, 109 | walletStateInit: account.walletStateInit, 110 | }; 111 | 112 | const response = await ( 113 | await fetch(`${this.host}/api/check_sign_data`, { 114 | method: "POST", 115 | headers: { 116 | "Content-Type": "application/json", 117 | }, 118 | body: JSON.stringify(reqBody), 119 | }) 120 | ).json(); 121 | 122 | return response; 123 | } catch (e) { 124 | console.log("checkSignData error:", e); 125 | return { error: "Failed to verify signature" }; 126 | } 127 | } 128 | 129 | reset() { 130 | this.accessToken = null; 131 | localStorage.removeItem(this.localStorageKey); 132 | this.generatePayload(); 133 | } 134 | } 135 | 136 | export const TonProofDemoApi = new TonProofDemoApiService(); 137 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/CreateJettonDemo/CreateJettonDemo.tsx: -------------------------------------------------------------------------------- 1 | import {useTonConnectUI, useTonWallet} from "@tonconnect/ui-react"; 2 | import React, {useState} from 'react'; 3 | import ReactJson from 'react-json-view'; 4 | import './style.scss'; 5 | import {CreateJettonRequestDto} from "../../server/dto/create-jetton-request-dto"; 6 | import {TonProofDemoApi} from "../../TonProofDemoApi"; 7 | 8 | const jetton: CreateJettonRequestDto = { 9 | name: 'Joint Photographic Experts Group', 10 | description: 'JPEG is a commonly used method of lossy compression for digital images, particularly for those images produced by digital photography. The degree of compression can be adjusted, allowing a selectable tradeoff between storage size and image quality.', 11 | image_data: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOEAAADhCAMAAAAJbSJIAAAAdVBMVEX///8rKytSUlITExPT09Pm5uYcHBzKysqXl5cZGRmnp6cnJydISEjr6+tOTk5LS0swMDAhISHa2tq3t7fg4OBra2sKCgrw8PB3d3dCQkJeXl49PT29vb2goKCvr68AAADOzs5jY2ODg4ONjY1wcHA2NjaSkpJ0yRZUAAAFXklEQVR4nO3de3eaMBgGcCEIxkgVCiiIVG3t9/+IS8LFS+1ERy665/ljZwW3k99yeV9x5zgaIQiCIAiCIAiCIAiCIAhiLGFQTf85iWnF7wkKQui/Z2Yt8TuLnCEyn1tKnGSD+BznzbVzFuOPgYBc6Fo5i1/DLNFa+PZmIZEMBRRC152NTYMuMx5YaN9e9IYWurYtVAVCy46b4YW27UUVQrv2ohKhVUVDidCqhapIaNFCVSW0p4FTJrSmaCgTWrMXFQot2YsqhXY0cGqFNuxFpUIrFqpioQVFQ7XQfAOnWmi+aKgXmt6LGoSG96IOodmioUNodi/qEZrci5qEBhs4bUJjs6hLaG6h6hOaKhoahYYaOI1CQ0VDq9DIXtQrNFE0dAv1HzeahQb2onah9r2oX6i7aOgX8gZO60I1INS8UI0ItTZwZoQ696IZoc6iYUqoby8aE2pr4AwKNR03Awrn9wl1LdQBhbN7hXqIAwrXdwu1FI0BhYu7hVqKxoDCByZRx0IdUsjyB4jKT9QhhY/Nouq9OKzQcQ757N4cwqcS3h+WBS8udAiEEEJoGgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQWCJmM3cIFz/F34gdCaMROb5/llEejfO6689whfb+Uz4CQlHEczyQoiD2ZIKj2efNSWjUXm8QTv/ORYrpNxstwnHhVyvoZTQi34TicS2EcjuuEYTiu6rVH3tuLza1NKyRFEHb3wtBbZX0WqwlhwAfYCGtcKAcexmvWCsNjlo2Q0c2yvtP8Ml4Ghx7TaF64X61WmyoR4962cxhWk2NS+VLmlJIWTNKiSPel/AOxe5toXpj7UeSTw7cY8affCHfE71IvRSqnNiiIHzEWRWQ9FT8nhycQ1mcOIxM+4rgVppdzQ8XtcEO76/zU4dPY42tObRE6bCF2Vs6uC9laAs++edefe4Xv3Iw1QifzuCH9RSgvBvSC3avw2yaMrgpZLia4eOibd+0RMoGYX5/DaCWm8LEO0Boh+eQ7LaHtSUOjLk5zbdJj01kpzIXCJ6u2fak1qy7iuCRbPr+7O9ptq4SrNE33E08W8AP70dMsY3GAij0664R+1uX2yjUvPOnaZu2KPG1LvYzXikRMdiv0N8su3s3Tx7ywa73LtRytFB7fXsT8gLkUTo7/BsGTCJO43BTNO8T6pHHO3x6erdJ2DsMnEYqBM+bTdvzXqoU4aY7X2KyQSZPnEZ7dviIUbTdvStsf6wcZ0fqFhKLij7cXfxfLX0jI1qLh+dHKvZDQIRWfRM85e+lrCdlBtAHv9PS17JX2Ia8Q8hlAuTj2pkw2ek8r3GW0S42ilezrVhnvyZloZNfywrMKx+W06rKpr0fvdWs33afu12ojHyyG2/xmO26p8ORpYui1NyZ1+8q78bB5pvje412+YSEf6FXhWevd3qFFuTx5Irws+zymMSwsgyC4XGf+Zhuc5b27xWix8Zq3IsG3S29PoBlhtO3WJuH5MUyfnOf0+ROj2drdpUXuE7/nG2ITnz3xNwrJ7RPit4iO1O5P1+Rzs3hxZSxqolkY+X72yetdqe+zYb3CaL/5rMRBuH/o0ecTCOl0KQuZ98toXkPIK1n9QeFLCtmuCoLyM9II1H3SMMoLnL49aEBoIBBC+N8LEwuEnlLhKLs9BMXJlmqFO72l4WfYTC1wtP0wLPwoFQtHe7M7kaaqgaPRV6//X6cmjOzUA0ej6pBR30RotpjqAPJ404mJTLeKj1EEQRAEQRAEQRAEQRAEQf6SP4jPv7WHGoGuAAAAAElFTkSuQmCC', 12 | symbol: 'JPEG', 13 | decimals: 9, 14 | amount: '1000000000000000', 15 | } 16 | 17 | export const CreateJettonDemo = () => { 18 | const [data, setData] = useState({}); 19 | const [tonConnectUI] = useTonConnectUI(); 20 | const wallet = useTonWallet(); 21 | 22 | const handleClick = async () => { 23 | const response = await TonProofDemoApi.createJetton(jetton); 24 | 25 | setData(response); 26 | 27 | if (!('error' in response)) { 28 | await tonConnectUI.sendTransaction(response); 29 | } 30 | }; 31 | 32 | return ( 33 |
34 |

Create Jetton

35 | {wallet ? ( 36 | 39 | ) : ( 40 |
Connect wallet to send transaction
41 | )} 42 | 43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /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/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 |
16 | setTheme(THEME.LIGHT)}>LIGHT 17 | setTheme(THEME.DARK)}>DARK 18 |
19 | 20 | 21 |
22 | } 23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /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/Footer/ColorsSelect/ColorsSelect.tsx: -------------------------------------------------------------------------------- 1 | import {ColorsSet, THEME, useTonConnectUI} from "@tonconnect/ui-react"; 2 | import {FunctionComponent, useEffect, useState} from "react"; 3 | import './style.scss'; 4 | 5 | const defaultColors = { 6 | [THEME.LIGHT]: { 7 | constant: { 8 | black: '#000000', 9 | white: '#FFFFFF' 10 | }, 11 | connectButton: { 12 | background: '#0098EA', 13 | foreground: '#FFFFFF' 14 | }, 15 | accent: '#0098EA', 16 | telegramButton: '#0098EA', 17 | icon: { 18 | primary: '#0F0F0F', 19 | secondary: '#7A8999', 20 | tertiary: '#C1CAD2', 21 | success: '#29CC6A', 22 | error: '#F5A73B' 23 | }, 24 | background: { 25 | primary: '#FFFFFF', 26 | secondary: '#F1F3F5', 27 | segment: '#FFFFFF', 28 | tint: '#F1F3F5', 29 | qr: '#F1F3F5' 30 | }, 31 | text: { 32 | primary: '#0F0F0F', 33 | secondary: '#6A7785' 34 | } 35 | }, 36 | [THEME.DARK]: { 37 | constant: { 38 | black: '#000000', 39 | white: '#FFFFFF' 40 | }, 41 | connectButton: { 42 | background: '#0098EA', 43 | foreground: '#FFFFFF' 44 | }, 45 | accent: '#E5E5EA', 46 | telegramButton: '#31A6F5', 47 | icon: { 48 | primary: '#E5E5EA', 49 | secondary: '#909099', 50 | tertiary: '#434347', 51 | success: '#29CC6A', 52 | error: '#F5A73B' 53 | }, 54 | background: { 55 | primary: '#121214', 56 | secondary: '#18181A', 57 | segment: '#262629', 58 | tint: '#222224', 59 | qr: '#F1F3F5' 60 | }, 61 | text: { 62 | primary: '#E5E5EA', 63 | secondary: '#7D7D85' 64 | } 65 | } 66 | } 67 | 68 | export interface ColorsSelectProps { 69 | theme: THEME; 70 | } 71 | export const ColorsSelect: FunctionComponent = ({ theme }) => { 72 | const [_, setOptions] = useTonConnectUI(); 73 | const [colors, setColors] = useState(defaultColors[theme]); 74 | 75 | useEffect(() => { 76 | setColors(defaultColors[theme]); 77 | }, [theme]); 78 | 79 | const onChange = (value: string, property1: string, property2?: string) => { 80 | setOptions({ 81 | uiPreferences: { 82 | colorsSet: { 83 | [theme]: { 84 | [property1]: property2 ? { 85 | ...(colors as any)[property1], 86 | [property2]: value 87 | } : value 88 | } 89 | } 90 | } 91 | }) 92 | 93 | 94 | setColors(colors => ({ 95 | ...colors, 96 | [property1]: property2 ? { 97 | ...(colors as any)[property1], 98 | [property2]: value 99 | } : value 100 | })); 101 | 102 | defaultColors[theme] = { 103 | ...defaultColors[theme], 104 | [property1]: property2 ? { 105 | ...(colors as any)[property1], 106 | [property2]: value 107 | } : value 108 | } 109 | } 110 | 111 | return
112 | { Object.entries(colors).map(([key1, value1]) => { 113 | if (typeof value1 === 'object') { 114 | return
115 | {key1}: 116 | { 117 | Object.entries(value1).map(([key2, value2]) => 118 | 126 | ) 127 | } 128 | 129 |
130 | } 131 | 132 | return
133 | {key1}: 134 | 141 |
142 | 143 | }) 144 | } 145 |
146 | } 147 | -------------------------------------------------------------------------------- /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/Footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | import {BorderRadius, Locales, ReturnStrategy, Theme, THEME, useTonConnectUI} from "@tonconnect/ui-react"; 2 | import './footer.scss'; 3 | import {useEffect, useState} from "react"; 4 | import {ColorsModal} from "./ColorsModal/ColorsModal"; 5 | 6 | const defaultWalletsSelectValue = '["Tonkeeper", "OpenMask"]'; 7 | 8 | export const Footer = () => { 9 | const [checkboxes, setCheckboxes] = useState( 10 | [true, false, false, true, true, true] 11 | ); 12 | 13 | const [returnStrategy, setReturnStrategy] = useState('back'); 14 | const [skipRedirect, setSkipRedirect] = useState('ios'); 15 | const [enableAndroidBackHandler, setEnableAndroidBackHandler] = useState(true); 16 | 17 | const [_, setOptions] = useTonConnectUI(); 18 | 19 | const onLangChange = (lang: string) => { 20 | setOptions({language: lang as Locales}) 21 | } 22 | 23 | const onThemeChange = (theme: string) => { 24 | setOptions({uiPreferences: {theme: theme as Theme}}) 25 | } 26 | 27 | const onBordersChange = (borders: string) => { 28 | setOptions({uiPreferences: {borderRadius: borders as BorderRadius}}) 29 | } 30 | 31 | const onCheckboxChange = (position: number) => { 32 | setCheckboxes(state => state.map((item, index) => index === position ? !item : item )); 33 | } 34 | 35 | const onEnableAndroidBackHandlerChange = (value: boolean) => { 36 | setEnableAndroidBackHandler(value); 37 | } 38 | 39 | const onReturnStrategyInputBlur = () => { 40 | if (!returnStrategy) { 41 | setReturnStrategy('back'); 42 | return; 43 | } 44 | 45 | setOptions({ actionsConfiguration: { returnStrategy: returnStrategy as ReturnStrategy } }) 46 | 47 | } 48 | 49 | const onSkipRedirectInputBlur = () => { 50 | if (!skipRedirect) { 51 | setSkipRedirect('ios'); 52 | return; 53 | } 54 | 55 | setOptions({ actionsConfiguration: { skipRedirectToWallet: skipRedirect as 'ios' | 'never' | 'always' } }) 56 | 57 | } 58 | 59 | useEffect(() => { 60 | const actionValues = ['before', 'success', 'error']; 61 | const modals = actionValues.map((item, index) => checkboxes[index] ? item : undefined).filter(i => i) as ("before" | "success" | "error")[]; 62 | const notifications = actionValues.map((item, index) => checkboxes[index + 3] ? item : undefined).filter(i => i) as ("before" | "success" | "error")[]; 63 | 64 | setOptions({ actionsConfiguration: { modals, notifications } }) 65 | }, [checkboxes]) 66 | 67 | useEffect(() => { 68 | setOptions({ enableAndroidBackHandler }); 69 | }, [enableAndroidBackHandler]); 70 | 71 | return
72 |
73 | 74 | 78 |
79 | 80 |
81 | 82 | 87 |
88 | 89 |
90 | 91 | 96 |
97 | 98 |
99 | 100 | 107 |
108 | 109 |
110 | modals 111 | 115 | 119 | 123 |
124 | 125 |
126 | notifications 127 | 131 | 135 | 139 |
140 | 141 |
142 | 143 |
144 | 145 |
146 | 154 |
155 | 156 |
157 | 170 |
171 |
172 | } 173 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/components/SignDataTester/SignDataTester.tsx: -------------------------------------------------------------------------------- 1 | import './style.scss'; 2 | import { useTonConnectUI, useTonWallet } from "@tonconnect/ui-react"; 3 | import { beginCell, Cell } from '@ton/ton'; 4 | import { useState } from 'react'; 5 | import ReactJson from 'react-json-view'; 6 | import { TonProofDemoApi } from '../../TonProofDemoApi'; 7 | 8 | // Component to test SignData functionality 9 | export function SignDataTester() { 10 | const wallet = useTonWallet(); 11 | const [tonConnectUi] = useTonConnectUI(); 12 | const [signDataRequest, setSignDataRequest] = useState(null); 13 | const [signDataResponse, setSignDataResponse] = useState(null); 14 | const [verificationResult, setVerificationResult] = useState(null); 15 | 16 | // Handle text signing 17 | const handleTextSign = async () => { 18 | // Clear previous state 19 | setSignDataRequest(null); 20 | setSignDataResponse(null); 21 | setVerificationResult(null); 22 | 23 | try { 24 | const requestPayload = { 25 | type: 'text' as const, 26 | text: 'I confirm this test signature request.', 27 | }; 28 | 29 | setSignDataRequest(requestPayload); 30 | console.log('📤 Sign Data Request (Text):', requestPayload); 31 | 32 | const result = await tonConnectUi.signData(requestPayload); 33 | 34 | setSignDataResponse(result); 35 | console.log('📥 Sign Data Response (Text):', result); 36 | 37 | // Verify the signature 38 | if (wallet) { 39 | const verification = await TonProofDemoApi.checkSignData(result, wallet.account); 40 | setVerificationResult(verification); 41 | console.log('✅ Verification Result (Text):', verification); 42 | } 43 | } catch (e) { 44 | console.error('Error signing text:', e); 45 | if (e instanceof Error) { 46 | setSignDataResponse({ error: e.message }); 47 | } else { 48 | setSignDataResponse({ error: 'Unknown error' }); 49 | } 50 | } 51 | }; 52 | 53 | // Handle binary signing 54 | const handleBinarySign = async () => { 55 | // Clear previous state 56 | setSignDataRequest(null); 57 | setSignDataResponse(null); 58 | setVerificationResult(null); 59 | 60 | try { 61 | // Example binary data (random bytes) 62 | const binaryData = Buffer.from('I confirm this test signature request.', 'ascii'); 63 | const requestPayload = { 64 | type: 'binary' as const, 65 | bytes: binaryData.toString('base64'), 66 | }; 67 | 68 | setSignDataRequest(requestPayload); 69 | console.log('📤 Sign Data Request (Binary):', requestPayload); 70 | 71 | const result = await tonConnectUi.signData(requestPayload); 72 | 73 | setSignDataResponse(result); 74 | console.log('📥 Sign Data Response (Binary):', result); 75 | 76 | // Verify the signature 77 | if (wallet) { 78 | const verification = await TonProofDemoApi.checkSignData(result, wallet.account); 79 | setVerificationResult(verification); 80 | console.log('✅ Verification Result (Binary):', verification); 81 | } 82 | } catch (e) { 83 | console.error('Error signing binary:', e); 84 | if (e instanceof Error) { 85 | setSignDataResponse({ error: e.message }); 86 | } else { 87 | setSignDataResponse({ error: 'Unknown error' }); 88 | } 89 | } 90 | }; 91 | 92 | // Handle cell signing 93 | const handleCellSign = async () => { 94 | // Clear previous state 95 | setSignDataRequest(null); 96 | setSignDataResponse(null); 97 | setVerificationResult(null); 98 | 99 | try { 100 | // Create a simple cell with a message 101 | const text = "Test message in cell"; 102 | const cell = beginCell() 103 | .storeUint(text.length, 7) // length 104 | .storeStringTail(text) 105 | .endCell(); 106 | 107 | const requestPayload = { 108 | type: 'cell' as const, 109 | schema: 'message#_ len:uint7 {len <= 127} text:(bits len * 8) = Message;', 110 | cell: cell.toBoc().toString('base64'), 111 | }; 112 | 113 | setSignDataRequest(requestPayload); 114 | console.log('📤 Sign Data Request (Cell):', requestPayload); 115 | 116 | const result = await tonConnectUi.signData(requestPayload); 117 | 118 | setSignDataResponse(result); 119 | console.log('📥 Sign Data Response (Cell):', result); 120 | 121 | // Verify the signature 122 | if (wallet) { 123 | const verification = await TonProofDemoApi.checkSignData(result, wallet.account); 124 | setVerificationResult(verification); 125 | console.log('✅ Verification Result (Cell):', verification); 126 | } 127 | } catch (e) { 128 | console.error('Error signing cell:', e); 129 | if (e instanceof Error) { 130 | setSignDataResponse({ error: e.message }); 131 | } else { 132 | setSignDataResponse({ error: 'Unknown error' }); 133 | } 134 | } 135 | }; 136 | 137 | return ( 138 |
139 |

Sign Data Test & Verification

140 | 141 |
142 | Test different types of data signing: text, binary, and cell formats with signature verification 143 |
144 | 145 | {wallet ? ( 146 |
147 | 150 | 153 | 156 |
157 | ) : ( 158 |
159 | Connect wallet to test signing 160 |
161 | )} 162 | 163 | {signDataRequest && ( 164 |
165 |

📤 Sign Data Request

166 | 167 |
168 | )} 169 | 170 | {signDataResponse && ( 171 |
172 |

📥 Sign Data Response

173 | 174 |
175 | )} 176 | 177 | {verificationResult && ( 178 |
179 |

✅ Verification Result

180 | 181 |
182 | )} 183 |
184 | ); 185 | } 186 | -------------------------------------------------------------------------------- /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/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 | 84 |
85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /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/TxForm/TxForm.tsx: -------------------------------------------------------------------------------- 1 | import React, {useCallback, useState} from 'react'; 2 | import ReactJson, {InteractionProps} from 'react-json-view'; 3 | import './style.scss'; 4 | import {SendTransactionRequest, useTonConnectUI, useTonWallet} from "@tonconnect/ui-react"; 5 | 6 | // In this example, we are using a predefined smart contract state initialization (`stateInit`) 7 | // to interact with an "EchoContract". This contract is designed to send the value back to the sender, 8 | // serving as a testing tool to prevent users from accidentally spending money. 9 | const defaultTx: SendTransactionRequest = { 10 | // The transaction is valid for 10 minutes from now, in unix epoch seconds. 11 | validUntil: Math.floor(Date.now() / 1000) + 600, 12 | messages: [ 13 | 14 | { 15 | // The receiver's address. 16 | address: 'EQCKWpx7cNMpvmcN5ObM5lLUZHZRFKqYA4xmw9jOry0ZsF9M', 17 | // Amount to send in nanoTON. For example, 0.005 TON is 5000000 nanoTON. 18 | amount: '5000000', 19 | // (optional) State initialization in boc base64 format. 20 | stateInit: 'te6cckEBBAEAOgACATQCAQAAART/APSkE/S88sgLAwBI0wHQ0wMBcbCRW+D6QDBwgBDIywVYzxYh+gLLagHPFsmAQPsAlxCarA==', 21 | // (optional) Payload in boc base64 format. 22 | payload: 'te6ccsEBAQEADAAMABQAAAAASGVsbG8hCaTc/g==', 23 | }, 24 | 25 | // Uncomment the following message to send two messages in one transaction. 26 | /* 27 | { 28 | // Note: Funds sent to this address will not be returned back to the sender. 29 | address: 'UQAuz15H1ZHrZ_psVrAra7HealMIVeFq0wguqlmFno1f3B-m', 30 | amount: toNano('0.01').toString(), 31 | } 32 | */ 33 | 34 | ], 35 | }; 36 | 37 | export function TxForm() { 38 | 39 | const [tx, setTx] = useState(defaultTx); 40 | 41 | const wallet = useTonWallet(); 42 | 43 | const [tonConnectUi] = useTonConnectUI(); 44 | 45 | const onChange = useCallback((value: InteractionProps) => { 46 | setTx(value.updated_src as SendTransactionRequest) 47 | }, []); 48 | 49 | return ( 50 |
51 |

Configure and send transaction

52 | 53 | 54 | 55 | {wallet ? ( 56 | 59 | ) : ( 60 | 63 | )} 64 |
65 | ); 66 | } 67 | 68 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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-signle-instance"; 10 | 11 | eruda.init(); 12 | 13 | async function enableMocking() { 14 | const host = document.baseURI.replace(/\/$/, ''); 15 | 16 | return new Promise(async (resolve) => { 17 | const {worker} = await import('./server/worker'); 18 | 19 | const startMockWorker = () => worker.start({ 20 | onUnhandledRequest: 'bypass', 21 | quiet: false, 22 | serviceWorker: { 23 | url: `${import.meta.env.VITE_GH_PAGES ? '/demo-dapp-with-react-ui' : ''}/mockServiceWorker.js` 24 | } 25 | }); 26 | let serviceWorkerRegistration: ServiceWorkerRegistration | null | void = await startMockWorker(); 27 | resolve(serviceWorkerRegistration); 28 | 29 | const verifyAndRestartWorker = runSingleInstance(async () => { 30 | try { 31 | const serviceWorkerRegistrations = await navigator.serviceWorker?.getRegistrations() || []; 32 | 33 | const isServiceWorkerOk = serviceWorkerRegistrations.length > 0; 34 | const isApiOk = await fetch(`${host}/api/healthz`) 35 | .then(r => r.status === 200 ? r.json().then(p => p.ok).catch(() => false) : false) 36 | .catch(() => false); 37 | 38 | if (!isServiceWorkerOk || !isApiOk) { 39 | await serviceWorkerRegistration?.unregister().catch(() => {}); 40 | serviceWorkerRegistration = await startMockWorker().catch(() => null); 41 | } 42 | } catch (error) { 43 | console.error('Error in verifyAndRestartWorker:', error); 44 | serviceWorkerRegistration = await startMockWorker().catch(() => null); 45 | } 46 | }); 47 | 48 | setInterval(verifyAndRestartWorker, 1_000); 49 | }); 50 | } 51 | 52 | enableMocking().then(() => render( 53 | 54 | 55 | , 56 | document.getElementById('root') as HTMLElement 57 | )); 58 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /src/server/api/check-proof.ts: -------------------------------------------------------------------------------- 1 | import {HttpResponseResolver} from "msw"; 2 | import {CheckProofRequest} from "../dto/check-proof-request-dto"; 3 | import {TonApiService} from "../services/ton-api-service"; 4 | import {TonProofService} from "../services/ton-proof-service"; 5 | import {badRequest, ok} from "../utils/http-utils"; 6 | import {createAuthToken, verifyToken} from "../utils/jwt"; 7 | 8 | /** 9 | * Checks the proof and returns an access token. 10 | * 11 | * POST /api/check_proof 12 | */ 13 | export const checkProof: HttpResponseResolver = async ({request}) => { 14 | try { 15 | const body = CheckProofRequest.parse(await request.json()); 16 | 17 | const client = TonApiService.create(body.network); 18 | const service = new TonProofService(); 19 | 20 | const isValid = await service.checkProof(body, (address) => client.getWalletPublicKey(address)); 21 | if (!isValid) { 22 | return badRequest({error: 'Invalid proof'}); 23 | } 24 | 25 | const payloadToken = body.proof.payload; 26 | if (!await verifyToken(payloadToken)) { 27 | return badRequest({error: 'Invalid token'}); 28 | } 29 | 30 | const token = await createAuthToken({address: body.address, network: body.network}); 31 | 32 | return ok({token: token}); 33 | } catch (e) { 34 | return badRequest({error: 'Invalid request', trace: e}); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /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/api/create-jetton.ts: -------------------------------------------------------------------------------- 1 | import {JettonMinter, storeJettonMintMessage} from "@ton-community/assets-sdk"; 2 | import {internalOnchainContentToCell} from "@ton-community/assets-sdk/dist/utils"; 3 | import {beginCell, storeStateInit, toNano} from "@ton/core"; 4 | import {Address} from "@ton/ton"; 5 | import {CHAIN} from "@tonconnect/sdk"; 6 | import {HttpResponseResolver} from "msw"; 7 | import {CreateJettonRequest} from "../dto/create-jetton-request-dto"; 8 | import {badRequest, ok, unauthorized} from "../utils/http-utils"; 9 | import {decodeAuthToken, verifyToken} from "../utils/jwt"; 10 | 11 | const VALID_UNTIL = 1000 * 60 * 5; // 5 minutes 12 | 13 | /** 14 | * Checks the proof and returns an access token. 15 | * 16 | * POST /api/create_jetton 17 | */ 18 | export const createJetton: HttpResponseResolver = async ({request}) => { 19 | try { 20 | const token = request.headers.get('Authorization')?.replace('Bearer ', ''); 21 | 22 | if (!token || !await verifyToken(token)) { 23 | return unauthorized({error: 'Unauthorized'}); 24 | } 25 | 26 | const payload = decodeAuthToken(token); 27 | if (!payload?.address || !payload?.network) { 28 | return unauthorized({error: 'Invalid token'}); 29 | } 30 | 31 | const body = CreateJettonRequest.parse(await request.json()); 32 | 33 | // specify the time until the message is valid 34 | const validUntil = Math.round((Date.now() + VALID_UNTIL) / 1000); 35 | 36 | // amount of TON to send with the message 37 | const amount = toNano('0.06').toString(); 38 | // forward value for the message to the wallet 39 | const walletForwardValue = toNano('0.05'); 40 | 41 | // who send the jetton create message 42 | const senderAddress = Address.parse(payload.address); 43 | // who will be the owner of the jetton 44 | const ownerAddress = Address.parse(payload.address); 45 | // who will receive the jetton 46 | const receiverAddress = Address.parse(payload.address); 47 | 48 | // create a jetton master 49 | const jettonMaster = JettonMinter.createFromConfig({ 50 | admin: ownerAddress, 51 | content: internalOnchainContentToCell({ 52 | name: body.name, 53 | description: body.description, 54 | image_data: Buffer.from(body.image_data, 'ascii').toString('base64'), 55 | symbol: body.symbol, 56 | decimals: body.decimals, 57 | }), 58 | }); 59 | if (!jettonMaster.init) { 60 | return badRequest({error: 'Invalid jetton master'}); 61 | } 62 | 63 | // prepare jetton master address 64 | const jettonMasterAddress = jettonMaster.address.toString({ 65 | urlSafe: true, 66 | bounceable: true, 67 | testOnly: payload.network === CHAIN.TESTNET 68 | }); 69 | 70 | // prepare stateInit for the jetton deploy message 71 | const stateInitBase64 = beginCell() 72 | .store(storeStateInit(jettonMaster.init)) 73 | .endCell().toBoc().toString('base64'); 74 | 75 | // prepare payload for the jetton mint message 76 | const payloadBase64 = beginCell().store(storeJettonMintMessage({ 77 | queryId: 0n, 78 | amount: BigInt(body.amount), 79 | from: jettonMaster.address, 80 | to: receiverAddress, 81 | responseAddress: senderAddress, 82 | forwardPayload: null, 83 | forwardTonAmount: 1n, 84 | walletForwardValue: walletForwardValue, 85 | })).endCell().toBoc().toString('base64'); 86 | 87 | return ok({ 88 | validUntil: validUntil, 89 | from: senderAddress.toRawString(), 90 | messages: [ 91 | { 92 | address: jettonMasterAddress, 93 | amount: amount, 94 | stateInit: stateInitBase64, 95 | payload: payloadBase64 96 | } 97 | ] 98 | }); 99 | } catch (e) { 100 | if (e instanceof Error) { 101 | return badRequest({error: 'Invalid request', trace: e.message}); 102 | } 103 | return badRequest({error: 'Invalid request', trace: e}); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/server/api/generate-payload.ts: -------------------------------------------------------------------------------- 1 | import {HttpResponseResolver} from "msw"; 2 | import {TonProofService} from "../services/ton-proof-service"; 3 | import {badRequest, ok} from "../utils/http-utils"; 4 | import {createPayloadToken} from "../utils/jwt"; 5 | 6 | /** 7 | * Generates a payload for ton proof. 8 | * 9 | * POST /api/generate_payload 10 | */ 11 | export const generatePayload: HttpResponseResolver = async () => { 12 | try { 13 | const service = new TonProofService(); 14 | 15 | const payload = service.generatePayload(); 16 | const payloadToken = await createPayloadToken({payload: payload}); 17 | 18 | return ok({payload: payloadToken}); 19 | } catch (e) { 20 | return badRequest({error: 'Invalid request', trace: e}); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /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/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/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 | }); 19 | 20 | export type CheckProofRequestDto = zod.infer; 21 | -------------------------------------------------------------------------------- /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 | }); 8 | 9 | const SignDataPayloadBinary = zod.object({ 10 | type: zod.literal("binary"), 11 | bytes: zod.string(), // base64 (not url safe) encoded bytes array 12 | }); 13 | 14 | const SignDataPayloadCell = zod.object({ 15 | type: zod.literal("cell"), 16 | schema: zod.string(), // TL-B scheme of the cell payload 17 | cell: zod.string(), // base64 (not url safe) encoded cell 18 | }); 19 | 20 | const SignDataPayload = zod.union([ 21 | SignDataPayloadText, 22 | SignDataPayloadBinary, 23 | SignDataPayloadCell, 24 | ]); 25 | 26 | export const CheckSignDataRequest = zod.object({ 27 | address: zod.string(), 28 | network: zod.enum([CHAIN.MAINNET, CHAIN.TESTNET]), 29 | public_key: zod.string(), 30 | signature: zod.string(), // base64 31 | timestamp: zod.number(), 32 | domain: zod.string(), 33 | payload: SignDataPayload, 34 | walletStateInit: zod.string(), // base64 encoded state init 35 | }); 36 | 37 | export type CheckSignDataRequestDto = zod.infer; 38 | export type SignDataPayloadText = zod.infer; 39 | export type SignDataPayloadBinary = zod.infer; 40 | export type SignDataPayloadCell = zod.infer; 41 | export type SignDataPayload = zod.infer; 42 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/server/services/sign-data-service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Address, 3 | beginCell, 4 | Cell, 5 | contractAddress, 6 | loadStateInit, 7 | } from "@ton/core"; 8 | import { sha256 } from "@ton/crypto"; 9 | import { Buffer } from "buffer"; 10 | import nacl from "tweetnacl"; 11 | import crc32 from "crc-32"; 12 | import { 13 | CheckSignDataRequestDto, 14 | SignDataPayloadText, 15 | SignDataPayloadBinary, 16 | SignDataPayload, 17 | } from "../dto/check-sign-data-request-dto"; 18 | import { tryParsePublicKey } from "../wrappers/wallets-data"; 19 | 20 | const allowedDomains = ["ton-connect.github.io", "localhost:5173"]; 21 | const validAuthTime = 15 * 60; // 15 minutes 22 | 23 | export class SignDataService { 24 | /** 25 | * Verifies sign-data signature. 26 | * 27 | * Supports three payload types: 28 | * 1. text - for text messages 29 | * 2. binary - for arbitrary binary data 30 | * 3. cell - for TON Cell with TL-B schema 31 | */ 32 | public async checkSignData( 33 | payload: CheckSignDataRequestDto, 34 | getWalletPublicKey: (address: string) => Promise 35 | ): Promise { 36 | try { 37 | const { 38 | signature, 39 | address, 40 | timestamp, 41 | domain, 42 | payload: signDataPayload, 43 | public_key, 44 | walletStateInit, 45 | } = payload; 46 | 47 | console.log("=== Sign Data Verification Started ==="); 48 | console.log("Address:", address); 49 | console.log("Domain:", domain); 50 | console.log("Timestamp:", timestamp); 51 | console.log("Payload Type:", signDataPayload.type); 52 | console.log("Signature:", signature); 53 | console.log("WalletStateInit:", walletStateInit); 54 | 55 | // Check domain 56 | if (!allowedDomains.includes(domain)) { 57 | console.log("❌ Domain not allowed:", domain); 58 | return false; 59 | } 60 | console.log("✅ Domain check passed"); 61 | 62 | // Check timestamp 63 | const now = Math.floor(Date.now() / 1000); 64 | if (now - validAuthTime > timestamp) { 65 | console.log( 66 | "❌ Timestamp expired - Now:", 67 | now, 68 | "Timestamp:", 69 | timestamp, 70 | "Valid time:", 71 | validAuthTime 72 | ); 73 | return false; 74 | } 75 | console.log("✅ Timestamp check passed"); 76 | 77 | // Parse address and state init 78 | const parsedAddr = Address.parse(address); 79 | const stateInit = loadStateInit( 80 | Cell.fromBase64(walletStateInit).beginParse() 81 | ); 82 | 83 | // 1. First, try to obtain public key via get_public_key get-method on smart contract deployed at Address. 84 | // 2. If the smart contract is not deployed yet, or the get-method is missing, you need: 85 | // 2.1. Parse walletStateInit and get public key from stateInit. 86 | let publicKey = 87 | tryParsePublicKey(stateInit) ?? (await getWalletPublicKey(address)); 88 | if (!publicKey) { 89 | console.log("❌ Public key not found for address:", address); 90 | return false; 91 | } 92 | console.log("✅ Public key obtained"); 93 | 94 | // 2.2. Check that provided public key equals to obtained public key 95 | const wantedPublicKey = Buffer.from(public_key, "hex"); 96 | if (!publicKey.equals(wantedPublicKey)) { 97 | console.log("❌ Public key mismatch"); 98 | console.log("Expected:", wantedPublicKey.toString("hex")); 99 | console.log("Got:", publicKey.toString("hex")); 100 | return false; 101 | } 102 | console.log("✅ Public key verification passed"); 103 | 104 | // 2.3. Check that walletStateInit.hash() equals to address 105 | const wantedAddress = Address.parse(address); 106 | const contractAddr = contractAddress(wantedAddress.workChain, stateInit); 107 | if (!contractAddr.equals(wantedAddress)) { 108 | console.log("❌ Address mismatch with state init"); 109 | console.log("Expected:", wantedAddress.toString()); 110 | console.log("Got:", contractAddr.toString()); 111 | return false; 112 | } 113 | console.log("✅ Address verification passed"); 114 | 115 | // Create hash based on payload type 116 | const finalHash = 117 | signDataPayload.type === "cell" 118 | ? this.createCellHash(signDataPayload, parsedAddr, domain, timestamp) 119 | : await this.createTextBinaryHash( 120 | signDataPayload, 121 | parsedAddr, 122 | domain, 123 | timestamp 124 | ); 125 | 126 | console.log("=== Hash Creation ==="); 127 | console.log("Payload Type:", signDataPayload.type); 128 | console.log("Hash Length:", finalHash.length); 129 | console.log("Hash Hex:", finalHash.toString("hex")); 130 | 131 | // Verify Ed25519 signature 132 | console.log("=== Signature Verification ==="); 133 | const isValid = nacl.sign.detached.verify( 134 | new Uint8Array(finalHash), 135 | new Uint8Array(Buffer.from(signature, "base64")), 136 | new Uint8Array(publicKey) 137 | ); 138 | 139 | console.log("Verification Result:", isValid ? "✅ VALID" : "❌ INVALID"); 140 | return isValid; 141 | } catch (e) { 142 | console.error("Sign data verification error:", e); 143 | return false; 144 | } 145 | } 146 | 147 | /** 148 | * Creates hash for text or binary payload. 149 | * Message format: 150 | * message = 0xffff || "ton-connect/sign-data/" || workchain || address_hash || domain_len || domain || timestamp || payload 151 | */ 152 | private async createTextBinaryHash( 153 | payload: SignDataPayloadText | SignDataPayloadBinary, 154 | parsedAddr: Address, 155 | domain: string, 156 | timestamp: number 157 | ): Promise { 158 | console.log("=== Creating Text/Binary Hash ==="); 159 | console.log("Type:", payload.type); 160 | console.log( 161 | "Content:", 162 | payload.type === "text" ? payload.text : payload.bytes 163 | ); 164 | console.log("Domain:", domain); 165 | console.log("Timestamp:", timestamp); 166 | console.log("Address:", parsedAddr.toString()); 167 | 168 | // Create workchain buffer 169 | const wcBuffer = Buffer.alloc(4); 170 | wcBuffer.writeInt32BE(parsedAddr.workChain); 171 | 172 | // Create domain buffer 173 | const domainBuffer = Buffer.from(domain, "utf8"); 174 | const domainLenBuffer = Buffer.alloc(4); 175 | domainLenBuffer.writeUInt32BE(domainBuffer.length); 176 | 177 | // Create timestamp buffer 178 | const tsBuffer = Buffer.alloc(8); 179 | tsBuffer.writeBigUInt64BE(BigInt(timestamp)); 180 | 181 | // Create payload buffer 182 | const typePrefix = payload.type === "text" ? "txt" : "bin"; 183 | const content = payload.type === "text" ? payload.text : payload.bytes; 184 | const encoding = payload.type === "text" ? "utf8" : "base64"; 185 | 186 | const payloadPrefix = Buffer.from(typePrefix); 187 | const payloadBuffer = Buffer.from(content, encoding); 188 | const payloadLenBuffer = Buffer.alloc(4); 189 | payloadLenBuffer.writeUInt32BE(payloadBuffer.length); 190 | 191 | console.log("=== Hash Components ==="); 192 | console.log("Workchain Buffer:", wcBuffer.toString("hex")); 193 | console.log("Address Hash:", parsedAddr.hash.toString("hex")); 194 | console.log("Domain Length:", domainLenBuffer.toString("hex")); 195 | console.log("Domain:", domainBuffer.toString("hex")); 196 | console.log("Timestamp:", tsBuffer.toString("hex")); 197 | console.log("Type Prefix:", payloadPrefix.toString("hex")); 198 | console.log("Payload Length:", payloadLenBuffer.toString("hex")); 199 | console.log("Payload Buffer:", payloadBuffer.toString("hex")); 200 | 201 | // Build message 202 | const message = Buffer.concat([ 203 | Buffer.from([0xff, 0xff]), 204 | Buffer.from("ton-connect/sign-data/"), 205 | wcBuffer, 206 | parsedAddr.hash, 207 | domainLenBuffer, 208 | domainBuffer, 209 | tsBuffer, 210 | payloadPrefix, 211 | payloadLenBuffer, 212 | payloadBuffer, 213 | ]); 214 | 215 | console.log("=== Final Message ==="); 216 | console.log("Message Length:", message.length); 217 | console.log("Message Hex:", message.toString("hex")); 218 | 219 | // Hash message with sha256 220 | const hash = await sha256(message); 221 | console.log("=== SHA256 Result ==="); 222 | console.log("Hash:", Buffer.from(hash).toString("hex")); 223 | return Buffer.from(hash); 224 | } 225 | 226 | /** 227 | * Creates hash for Cell payload according to TON Connect specification. 228 | */ 229 | private createCellHash( 230 | payload: SignDataPayload & { type: "cell" }, 231 | parsedAddr: Address, 232 | domain: string, 233 | timestamp: number 234 | ): Buffer { 235 | const cell = Cell.fromBase64(payload.cell); 236 | const schemaHash = crc32.buf(Buffer.from(payload.schema, "utf8")) >>> 0; // unsigned crc32 hash 237 | 238 | // Encode domain in DNS-like format (e.g. "example.com" -> "com\0example\0") 239 | const encodedDomain = this.encodeDomainDnsLike(domain); 240 | 241 | const message = beginCell() 242 | .storeUint(0x75569022, 32) // prefix 243 | .storeUint(schemaHash, 32) // schema hash 244 | .storeUint(timestamp, 64) // timestamp 245 | .storeAddress(parsedAddr) // user wallet address 246 | .storeStringRefTail(encodedDomain.toString("utf8")) // app domain (DNS-like encoded, snake stored) 247 | .storeRef(cell) // payload cell 248 | .endCell(); 249 | 250 | return Buffer.from(message.hash()); 251 | } 252 | 253 | /** 254 | * Encodes domain name in DNS-like format. 255 | * Example: "example.com" -> "com\0example\0" 256 | */ 257 | private encodeDomainDnsLike(domain: string): Buffer { 258 | const parts = domain.split(".").reverse(); // reverse for DNS-like encoding 259 | const encoded: number[] = []; 260 | 261 | for (const part of parts) { 262 | // Add the part characters 263 | for (let i = 0; i < part.length; i++) { 264 | encoded.push(part.charCodeAt(i)); 265 | } 266 | encoded.push(0); // null byte after each part 267 | } 268 | 269 | return Buffer.from(encoded); 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /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/services/ton-proof-service.ts: -------------------------------------------------------------------------------- 1 | import {sha256} from "@ton/crypto"; 2 | import {Address, Cell, contractAddress, loadStateInit} from "@ton/ton"; 3 | import {Buffer} from "buffer"; 4 | import {randomBytes, sign} from "tweetnacl"; 5 | import {CheckProofRequestDto} from "../dto/check-proof-request-dto"; 6 | import {tryParsePublicKey} from "../wrappers/wallets-data"; 7 | 8 | const tonProofPrefix = 'ton-proof-item-v2/'; 9 | const tonConnectPrefix = 'ton-connect'; 10 | const allowedDomains = [ 11 | 'ton-connect.github.io', 12 | 'localhost:5173' 13 | ]; 14 | const validAuthTime = 15 * 60; // 15 minute 15 | 16 | export class TonProofService { 17 | 18 | /** 19 | * Generate a random payload. 20 | */ 21 | public generatePayload(): string { 22 | return Buffer.from(randomBytes(32)).toString('hex'); 23 | } 24 | 25 | /** 26 | * Reference implementation of the checkProof method: 27 | * https://github.com/ton-blockchain/ton-connect/blob/main/requests-responses.md#address-proof-signature-ton_proof 28 | */ 29 | public async checkProof(payload: CheckProofRequestDto, getWalletPublicKey: (address: string) => Promise): Promise { 30 | try { 31 | const stateInit = loadStateInit(Cell.fromBase64(payload.proof.state_init).beginParse()); 32 | 33 | // 1. First, try to obtain public key via get_public_key get-method on smart contract deployed at Address. 34 | // 2. If the smart contract is not deployed yet, or the get-method is missing, you need: 35 | // 2.1. Parse TonAddressItemReply.walletStateInit and get public key from stateInit. You can compare the walletStateInit.code 36 | // with the code of standard wallets contracts and parse the data according to the found wallet version. 37 | let publicKey = tryParsePublicKey(stateInit) ?? await getWalletPublicKey(payload.address); 38 | if (!publicKey) { 39 | return false; 40 | } 41 | 42 | // 2.2. Check that TonAddressItemReply.publicKey equals to obtained public key 43 | const wantedPublicKey = Buffer.from(payload.public_key, 'hex'); 44 | if (!publicKey.equals(wantedPublicKey)) { 45 | return false; 46 | } 47 | 48 | // 2.3. Check that TonAddressItemReply.walletStateInit.hash() equals to TonAddressItemReply.address. .hash() means BoC hash. 49 | const wantedAddress = Address.parse(payload.address); 50 | const address = contractAddress(wantedAddress.workChain, stateInit); 51 | if (!address.equals(wantedAddress)) { 52 | return false; 53 | } 54 | 55 | if (!allowedDomains.includes(payload.proof.domain.value)) { 56 | return false; 57 | } 58 | 59 | const now = Math.floor(Date.now() / 1000); 60 | if (now - validAuthTime > payload.proof.timestamp) { 61 | return false; 62 | } 63 | 64 | const message = { 65 | workchain: address.workChain, 66 | address: address.hash, 67 | domain: { 68 | lengthBytes: payload.proof.domain.lengthBytes, 69 | value: payload.proof.domain.value, 70 | }, 71 | signature: Buffer.from(payload.proof.signature, 'base64'), 72 | payload: payload.proof.payload, 73 | stateInit: payload.proof.state_init, 74 | timestamp: payload.proof.timestamp 75 | }; 76 | 77 | const wc = Buffer.alloc(4); 78 | wc.writeUInt32BE(message.workchain, 0); 79 | 80 | const ts = Buffer.alloc(8); 81 | ts.writeBigUInt64LE(BigInt(message.timestamp), 0); 82 | 83 | const dl = Buffer.alloc(4); 84 | dl.writeUInt32LE(message.domain.lengthBytes, 0); 85 | 86 | // message = utf8_encode("ton-proof-item-v2/") ++ 87 | // Address ++ 88 | // AppDomain ++ 89 | // Timestamp ++ 90 | // Payload 91 | const msg = Buffer.concat([ 92 | Buffer.from(tonProofPrefix), 93 | wc, 94 | message.address, 95 | dl, 96 | Buffer.from(message.domain.value), 97 | ts, 98 | Buffer.from(message.payload), 99 | ]); 100 | 101 | const msgHash = Buffer.from(await sha256(msg)); 102 | 103 | // signature = Ed25519Sign(privkey, sha256(0xffff ++ utf8_encode("ton-connect") ++ sha256(message))) 104 | const fullMsg = Buffer.concat([ 105 | Buffer.from([0xff, 0xff]), 106 | Buffer.from(tonConnectPrefix), 107 | msgHash, 108 | ]); 109 | 110 | const result = Buffer.from(await sha256(fullMsg)); 111 | 112 | return sign.detached.verify(result, message.signature, publicKey); 113 | } catch (e) { 114 | return false; 115 | } 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /src/server/utils/http-utils.ts: -------------------------------------------------------------------------------- 1 | import {HttpResponse, JsonBodyType, StrictResponse} from "msw"; 2 | 3 | /** 4 | * Receives a body and returns an HTTP response with the given body and status code 200. 5 | */ 6 | export function ok(body: T): StrictResponse { 7 | return HttpResponse.json(body, {status: 200, statusText: 'OK'}); 8 | } 9 | 10 | /** 11 | * Receives a body and returns an HTTP response with the given body and status code 400. 12 | */ 13 | export function badRequest(body: T): StrictResponse { 14 | return HttpResponse.json(body, { 15 | status: 400, 16 | statusText: 'Bad Request' 17 | }); 18 | } 19 | 20 | /** 21 | * Receives a body and returns an HTTP response with the given body and status code 401. 22 | */ 23 | export function unauthorized(body: T): StrictResponse { 24 | return HttpResponse.json(body, { 25 | status: 401, 26 | statusText: 'Unauthorized' 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /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 | payload: 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/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 | 10 | const baseUrl = document.baseURI.replace(/\/$/, ""); 11 | 12 | export const worker = setupWorker( 13 | http.get(`${baseUrl}/api/healthz`, healthz), 14 | http.post(`${baseUrl}/api/generate_payload`, generatePayload), 15 | http.post(`${baseUrl}/api/check_proof`, checkProof), 16 | http.post(`${baseUrl}/api/check_sign_data`, checkSignData), 17 | http.get(`${baseUrl}/api/get_account_info`, getAccountInfo), 18 | http.post(`${baseUrl}/api/create_jetton`, createJetton) 19 | ); 20 | -------------------------------------------------------------------------------- /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/server/wrappers/wallets-data.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Slice, 3 | StateInit, 4 | WalletContractV1R1, 5 | WalletContractV1R2, 6 | WalletContractV1R3, 7 | WalletContractV2R1, 8 | WalletContractV2R2, 9 | WalletContractV3R1, 10 | WalletContractV3R2, 11 | WalletContractV4 as WalletContractV4R2, 12 | WalletContractV5Beta, 13 | WalletContractV5R1 14 | } from "@ton/ton"; 15 | import {Buffer} from "buffer"; 16 | import {WalletContractV4R1} from "./wallet-contract-v4-r1"; 17 | 18 | const knownWallets = [ 19 | {contract: WalletContractV1R1, loadData: loadWalletV1Data}, 20 | {contract: WalletContractV1R2, loadData: loadWalletV1Data}, 21 | {contract: WalletContractV1R3, loadData: loadWalletV1Data}, 22 | {contract: WalletContractV2R1, loadData: loadWalletV2Data}, 23 | {contract: WalletContractV2R2, loadData: loadWalletV2Data}, 24 | {contract: WalletContractV3R1, loadData: loadWalletV3Data}, 25 | {contract: WalletContractV3R2, loadData: loadWalletV3Data}, 26 | {contract: WalletContractV4R1, loadData: loadWalletV4Data}, 27 | {contract: WalletContractV4R2, loadData: loadWalletV4Data}, 28 | {contract: WalletContractV5Beta, loadData: loadWalletV5BetaData}, 29 | {contract: WalletContractV5R1, loadData: loadWalletV5Data}, 30 | ].map(({contract, loadData}) => ({ 31 | contract: contract, 32 | loadData: loadData, 33 | wallet: contract.create({workchain: 0, publicKey: Buffer.alloc(32)}), 34 | })); 35 | 36 | function loadWalletV1Data(cs: Slice) { 37 | const seqno = cs.loadUint(32); 38 | const publicKey = cs.loadBuffer(32); 39 | return {seqno, publicKey}; 40 | } 41 | 42 | function loadWalletV2Data(cs: Slice) { 43 | const seqno = cs.loadUint(32); 44 | const publicKey = cs.loadBuffer(32); 45 | return {seqno, publicKey}; 46 | } 47 | 48 | function loadWalletV3Data(cs: Slice) { 49 | const seqno = cs.loadUint(32); 50 | const walletId = cs.loadUint(32); 51 | const publicKey = cs.loadBuffer(32); 52 | return {seqno, publicKey, walletId}; 53 | } 54 | 55 | function loadWalletV4Data(cs: Slice) { 56 | const seqno = cs.loadUint(32); 57 | const walletId = cs.loadUint(32); 58 | const publicKey = cs.loadBuffer(32); 59 | const plugins = cs.loadMaybeRef(); 60 | return {seqno, publicKey, walletId, plugins}; 61 | } 62 | 63 | function loadWalletV5BetaData(cs: Slice) { 64 | const isSignatureAuthAllowed = cs.loadBoolean(); 65 | const seqno = cs.loadUint(32); 66 | const walletId = cs.loadUintBig(80); 67 | const publicKey = cs.loadBuffer(32); 68 | const plugins = cs.loadMaybeRef(); 69 | return {isSignatureAuthAllowed, seqno, publicKey, walletId, plugins}; 70 | } 71 | 72 | function loadWalletV5Data(cs: Slice) { 73 | const isSignatureAuthAllowed = cs.loadBoolean(); 74 | const seqno = cs.loadUint(32); 75 | const walletId = cs.loadUint(32); 76 | const publicKey = cs.loadBuffer(32); 77 | const plugins = cs.loadMaybeRef(); 78 | return {isSignatureAuthAllowed, seqno, publicKey, walletId, plugins}; 79 | } 80 | 81 | export function tryParsePublicKey(stateInit: StateInit): Buffer | null { 82 | if (!stateInit.code || !stateInit.data) { 83 | return null; 84 | } 85 | 86 | for (const {wallet, loadData} of knownWallets) { 87 | try { 88 | if (wallet.init.code.equals(stateInit.code)) { 89 | return loadData(stateInit.data.beginParse()).publicKey; 90 | } 91 | } catch (e) { 92 | } 93 | } 94 | 95 | return null; 96 | } 97 | -------------------------------------------------------------------------------- /src/utils/run-signle-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/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------