├── public ├── privacy-policy.txt ├── terms-of-use.txt ├── favicon.ico ├── tc-verify.json ├── apple-touch-icon.png ├── vercel.svg ├── tonconnect-manifest.json ├── window.svg ├── file.svg ├── globe.svg ├── next.svg └── mockServiceWorker.js ├── src ├── components │ ├── Header │ │ ├── Header.tsx │ │ └── header.scss │ ├── Footer │ │ ├── ColorsSelect │ │ │ ├── style.scss │ │ │ └── ColorsSelect.tsx │ │ ├── ColorsModal │ │ │ ├── style.scss │ │ │ └── ColorsModal.tsx │ │ ├── footer.scss │ │ └── Footer.tsx │ ├── TonProofDemo │ │ ├── style.scss │ │ └── TonProofDemo.tsx │ ├── CreateJettonDemo │ │ ├── style.scss │ │ └── CreateJettonDemo.tsx │ ├── WalletBatchLimitsTester │ │ ├── style.scss │ │ └── WalletBatchLimitsTester.tsx │ ├── MerkleExample │ │ ├── style.scss │ │ └── MerkleExample.tsx │ ├── TxForm │ │ ├── style.scss │ │ └── TxForm.tsx │ ├── SignDataTester │ │ ├── style.scss │ │ └── SignDataTester.tsx │ └── FindTransactionDemo │ │ ├── FindTransactionDemo.tsx │ │ └── style.scss ├── pages │ ├── _document.tsx │ ├── api │ │ ├── hello.ts │ │ ├── generate_payload.ts │ │ ├── get_account_info.ts │ │ ├── merkle_proof.ts │ │ ├── create_jetton.ts │ │ ├── check_sign_data.ts │ │ ├── check_proof.ts │ │ ├── wait_for_transaction.ts │ │ └── find_transaction_by_external_message.ts │ ├── index.tsx │ └── _app.tsx ├── server │ ├── dto │ │ ├── create-jetton-request-dto.ts │ │ ├── check-proof-request-dto.ts │ │ └── check-sign-data-request-dto.ts │ ├── utils │ │ ├── http-utils.ts │ │ ├── transactions-utils.ts │ │ ├── jwt.ts │ │ └── exotic.ts │ ├── services │ │ ├── sign-data-service.ts │ │ ├── ton-api-service.ts │ │ └── ton-proof-service.ts │ └── wrappers │ │ ├── wallet-contract-v4-r1.ts │ │ └── wallets-data.ts ├── hooks │ └── useInterval.ts ├── styles │ ├── globals.css │ └── Home.module.css └── TonProofDemoApi.ts ├── next.config.ts ├── eslint.config.mjs ├── tsconfig.json ├── .gitignore ├── package.json └── README.md /public/privacy-policy.txt: -------------------------------------------------------------------------------- 1 | Privacy Policy example -------------------------------------------------------------------------------- /public/terms-of-use.txt: -------------------------------------------------------------------------------- 1 | Terms of use example 2 | ... -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ton-connect/demo-dapp-with-next/main/public/favicon.ico -------------------------------------------------------------------------------- /public/tc-verify.json: -------------------------------------------------------------------------------- 1 | { 2 | "payload": "th8tVbI2m9oAAAAAZJ7D1RzJ1JIYtpDgaiMk8pd0hlu6HSmRvQjwbk6fGE7ozlAV" 3 | } -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ton-connect/demo-dapp-with-next/main/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 Next.js 7 | 8 |
9 | } 10 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | import path from "path"; 3 | 4 | const nextConfig: NextConfig = { 5 | /* config options here */ 6 | reactStrictMode: true, 7 | outputFileTracingRoot: path.join(__dirname, "../") 8 | }; 9 | 10 | export default nextConfig; 11 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from "next/document"; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /public/tonconnect-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "http://localhost:3002/", 3 | "name": "Demo Dapp with Next.js", 4 | "iconUrl": "http://localhost:3002/apple-touch-icon.png", 5 | "termsOfUseUrl": "http://localhost:3002/terms-of-use.txt", 6 | "privacyPolicyUrl": "http://localhost:3002/privacy-policy.txt" 7 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | 4 | type Data = { 5 | name: string; 6 | }; 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse, 11 | ) { 12 | res.status(200).json({ name: "John Doe" }); 13 | } 14 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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; -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | ]; 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "paths": { 17 | "@/*": ["./src/*"] 18 | } 19 | }, 20 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /src/server/dto/check-proof-request-dto.ts: -------------------------------------------------------------------------------- 1 | import { CHAIN } from "@tonconnect/ui-react"; 2 | import zod from "zod"; 3 | 4 | export const CheckProofRequest = zod.object({ 5 | address: zod.string(), 6 | network: zod.enum([CHAIN.MAINNET, CHAIN.TESTNET]), 7 | public_key: zod.string(), 8 | proof: zod.object({ 9 | timestamp: zod.number(), 10 | domain: zod.object({ 11 | lengthBytes: zod.number(), 12 | value: zod.string(), 13 | }), 14 | payload: zod.string(), 15 | signature: zod.string(), 16 | state_init: zod.string(), 17 | }), 18 | payloadToken: zod.string(), 19 | }); 20 | 21 | export type CheckProofRequestDto = zod.infer; -------------------------------------------------------------------------------- /src/server/utils/http-utils.ts: -------------------------------------------------------------------------------- 1 | import { Address, Cell } from '@ton/core'; 2 | 3 | export function jsonReplacer(_key: string, value: unknown): unknown { 4 | if (typeof value === 'bigint') { 5 | return value.toString(); 6 | } else if (value instanceof Address) { 7 | return value.toString(); 8 | } else if (value instanceof Cell) { 9 | return value.toBoc().toString('base64'); 10 | } else if (value instanceof Buffer) { 11 | return value.toString('base64'); 12 | } else if ( 13 | value && 14 | typeof value === 'object' && 15 | (value as any).type === 'Buffer' && 16 | Array.isArray((value as any).data) 17 | ) { 18 | return Buffer.from((value as any).data).toString('base64'); 19 | } 20 | 21 | return value; 22 | } -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Footer/ColorsModal/ColorsModal.tsx: -------------------------------------------------------------------------------- 1 | import { THEME } from "@tonconnect/ui-react"; 2 | import { useState } from "react"; 3 | import { ColorsSelect } from "../ColorsSelect/ColorsSelect"; 4 | import './style.scss'; 5 | 6 | export const ColorsModal = () => { 7 | const [opened, setOpened] = useState(false); 8 | const [theme, setTheme] = useState(THEME.LIGHT); 9 | 10 | return (<> 11 | 12 | {opened && 13 |
14 | 15 | 19 | 20 | 21 |
22 | } 23 | 24 | ) 25 | } -------------------------------------------------------------------------------- /src/pages/api/generate_payload.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import { sha256 } from "@ton/crypto"; 3 | import { createPayloadToken } from '../../server/utils/jwt'; 4 | import { TonProofService } from '../../server/services/ton-proof-service'; 5 | 6 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 7 | if (req.method !== 'POST') { 8 | return res.status(405).json({ error: 'Method not allowed' }); 9 | } 10 | 11 | try { 12 | const service = new TonProofService(); 13 | 14 | const randomBytes = await service.generateRandomBytes(); 15 | const payloadToken = await createPayloadToken({ 16 | randomBytes: randomBytes.toString('hex') 17 | }); 18 | const payloadTokenHash = (await sha256(payloadToken)).toString('hex'); 19 | 20 | return res.status(200).json({ 21 | payloadToken: payloadToken, 22 | payloadTokenHash: payloadTokenHash, 23 | }); 24 | } catch (e) { 25 | return res.status(400).json({ error: 'Invalid request', trace: e }); 26 | } 27 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-dapp-with-next", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@ton-community/assets-sdk": "0.0.5", 13 | "@ton/core": "^0.56.3", 14 | "@ton/crypto": "^3.2.0", 15 | "@ton/ton": "^14.0.0", 16 | "@tonconnect/ui-react": "^2.2.0", 17 | "buffer": "^6.0.3", 18 | "crc-32": "^1.2.2", 19 | "eruda": "^2.11.2", 20 | "jose": "^5.2.4", 21 | "msw": "2.3.5", 22 | "next": "15.3.5", 23 | "react": "^18.2.0", 24 | "react-dom": "^18.2.0", 25 | "react-json-view": "^1.21.3", 26 | "tweetnacl": "^1.0.3", 27 | "zod": "^3.23.8" 28 | }, 29 | "devDependencies": { 30 | "@eslint/eslintrc": "^3", 31 | "@types/node": "^20", 32 | "@types/react": "^19", 33 | "@types/react-dom": "^19", 34 | "eslint": "^9", 35 | "eslint-config-next": "15.3.5", 36 | "sass": "^1.57.1", 37 | "typescript": "^5" 38 | }, 39 | "msw": { 40 | "workerDirectory": [ 41 | "public" 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/components/WalletBatchLimitsTester/style.scss: -------------------------------------------------------------------------------- 1 | .wallet-batch-limits-tester { 2 | display: flex; 3 | width: 100%; 4 | flex-direction: column; 5 | gap: 20px; 6 | align-items: center; 7 | margin-top: 60px; 8 | padding: 20px; 9 | 10 | h3 { 11 | color: white; 12 | opacity: 0.8; 13 | } 14 | 15 | &__info { 16 | color: white; 17 | font-size: 18px; 18 | opacity: 0.8; 19 | } 20 | 21 | &__error { 22 | color: rgba(102, 170, 238, 0.91); 23 | font-size: 18px; 24 | line-height: 20px; 25 | } 26 | 27 | &__buttons { 28 | display: flex; 29 | gap: 20px; 30 | flex-wrap: wrap; 31 | justify-content: center; 32 | } 33 | 34 | button { 35 | border: none; 36 | padding: 7px 15px; 37 | border-radius: 15px; 38 | cursor: pointer; 39 | 40 | background-color: rgba(102, 170, 238, 0.91); 41 | color: white; 42 | font-size: 16px; 43 | line-height: 20px; 44 | 45 | transition: transform 0.1s ease-in-out; 46 | 47 | &:hover { 48 | transform: scale(1.03); 49 | } 50 | 51 | &:active { 52 | transform: scale(0.97); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/components/MerkleExample/style.scss: -------------------------------------------------------------------------------- 1 | .merkle-proof-demo { 2 | display: flex; 3 | width: 100%; 4 | flex-direction: column; 5 | gap: 20px; 6 | align-items: center; 7 | margin-top: 60px; 8 | padding: 20px; 9 | 10 | h3 { 11 | color: white; 12 | opacity: 0.8; 13 | } 14 | 15 | >div:nth-child(3) { 16 | width: 100%; 17 | 18 | span { 19 | word-break: break-word; 20 | } 21 | } 22 | 23 | &__buttons { 24 | display: flex; 25 | gap: 20px; 26 | flex-wrap: wrap; 27 | justify-content: center; 28 | } 29 | 30 | &__error { 31 | color: rgba(102, 170, 238, 0.91); 32 | font-size: 18px; 33 | line-height: 20px; 34 | } 35 | 36 | button { 37 | border: none; 38 | padding: 7px 15px; 39 | border-radius: 15px; 40 | cursor: pointer; 41 | 42 | background-color: rgba(102, 170, 238, 0.91); 43 | color: white; 44 | font-size: 16px; 45 | line-height: 20px; 46 | 47 | transition: transform 0.1s ease-in-out; 48 | 49 | &:hover { 50 | transform: scale(1.03); 51 | } 52 | 53 | &:active { 54 | transform: scale(0.97); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /src/server/utils/transactions-utils.ts: -------------------------------------------------------------------------------- 1 | import { beginCell, storeMessage } from "@ton/core"; 2 | 3 | /** 4 | * Generates a normalized hash of an "external-in" message for comparison. 5 | * Follows TEP-467. 6 | */ 7 | export function getNormalizedExtMessageHash(message: any) { 8 | if (message.info.type !== 'external-in') { 9 | throw new Error(`Message must be "external-in", got ${message.info.type}`); 10 | } 11 | const info = { ...message.info, src: undefined, importFee: 0n }; 12 | const normalizedMessage = { 13 | ...message, 14 | init: null, 15 | info: info, 16 | }; 17 | return beginCell().store(storeMessage(normalizedMessage, { forceRef: true })).endCell().hash(); 18 | } 19 | 20 | /** 21 | * Retries async fn with delay and count. 22 | */ 23 | export async function retry(fn: () => Promise, options: { retries: number; delay: number }) { 24 | let lastError: Error | undefined; 25 | for (let i = 0; i < options.retries; i++) { 26 | try { 27 | return await fn(); 28 | } catch (e) { 29 | if (e instanceof Error) { 30 | lastError = e; 31 | } 32 | await new Promise((resolve) => setTimeout(resolve, options.delay)); 33 | } 34 | } 35 | throw lastError; 36 | } -------------------------------------------------------------------------------- /src/pages/api/get_account_info.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | 3 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 4 | if (req.method !== 'GET') { 5 | return res.status(405).json({ error: 'Method not allowed' }); 6 | } 7 | 8 | try { 9 | const authHeader = req.headers.authorization; 10 | if (!authHeader || !authHeader.startsWith('Bearer ')) { 11 | return res.status(401).json({ error: 'Unauthorized' }); 12 | } 13 | 14 | // In a real app, you would verify the token here 15 | const token = authHeader.substring(7); 16 | 17 | // Return mock account info 18 | return res.status(200).json({ 19 | address: "EQCKWpx7cNMpvmcN5ObM5lLUZHZRFKqYA4xmw9jOry0ZsF9M", 20 | balance: "1000000000", 21 | network: "mainnet", 22 | lastActivity: new Date().toISOString(), 23 | transactions: [ 24 | { 25 | hash: "abc123", 26 | amount: "1000000", 27 | timestamp: new Date().toISOString() 28 | } 29 | ] 30 | }); 31 | } catch (e) { 32 | return res.status(400).json({ error: 'Invalid request', trace: e }); 33 | } 34 | } -------------------------------------------------------------------------------- /src/pages/api/merkle_proof.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import { beginCell } from '@ton/ton'; 3 | 4 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 5 | if (req.method !== 'POST') { 6 | return res.status(405).json({ error: 'Method not allowed' }); 7 | } 8 | 9 | try { 10 | const authHeader = req.headers.authorization; 11 | if (!authHeader || !authHeader.startsWith('Bearer ')) { 12 | return res.status(401).json({ error: 'Unauthorized' }); 13 | } 14 | 15 | // Create a mock merkle proof transaction 16 | const defaultBody = beginCell().storeUint(0, 32).storeStringTail("Merkle Proof").endCell(); 17 | 18 | const transaction = { 19 | validUntil: Math.floor(Date.now() / 1000) + 600, 20 | messages: [ 21 | { 22 | address: 'EQD_5KMZVIqzYY91-t5CdRD_V71wRrVzxDXu9n2XEwz2wwdv', 23 | amount: '5000000', 24 | payload: defaultBody.toBoc().toString('base64'), 25 | }, 26 | ], 27 | }; 28 | 29 | return res.status(200).json(transaction); 30 | } catch (e) { 31 | return res.status(400).json({ error: 'Invalid request', trace: e }); 32 | } 33 | } -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/TxForm/style.scss: -------------------------------------------------------------------------------- 1 | .send-tx-form { 2 | flex: 1; 3 | display: flex; 4 | width: 100%; 5 | flex-direction: column; 6 | gap: 20px; 7 | padding: 20px; 8 | align-items: center; 9 | 10 | h3 { 11 | color: white; 12 | opacity: 0.8; 13 | font-size: 28px; 14 | } 15 | 16 | >div:nth-child(2) { 17 | width: 100%; 18 | 19 | span { 20 | word-break: break-word; 21 | } 22 | } 23 | 24 | >button { 25 | border: none; 26 | padding: 7px 15px; 27 | border-radius: 15px; 28 | cursor: pointer; 29 | 30 | background-color: rgba(102, 170, 238, 0.91); 31 | color: white; 32 | font-size: 16px; 33 | line-height: 20px; 34 | 35 | transition: transform 0.1s ease-in-out; 36 | 37 | &:hover { 38 | transform: scale(1.03); 39 | } 40 | 41 | &:active { 42 | transform: scale(0.97); 43 | } 44 | } 45 | 46 | &__json-view { 47 | background: rgba(24, 32, 48, 0.98); 48 | border-radius: 14px; 49 | box-shadow: 0 2px 12px 0 rgba(16, 22, 31, 0.13); 50 | padding: 18px 18px 14px 18px; 51 | margin-top: 0; 52 | margin-bottom: 0; 53 | width: 100%; 54 | overflow-x: auto; 55 | font-size: 15px; 56 | word-break: break-all; 57 | white-space: pre-wrap; 58 | } 59 | } -------------------------------------------------------------------------------- /src/pages/api/create_jetton.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import { beginCell } from '@ton/ton'; 3 | 4 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 5 | if (req.method !== 'POST') { 6 | return res.status(405).json({ error: 'Method not allowed' }); 7 | } 8 | 9 | try { 10 | const authHeader = req.headers.authorization; 11 | if (!authHeader || !authHeader.startsWith('Bearer ')) { 12 | return res.status(401).json({ error: 'Unauthorized' }); 13 | } 14 | 15 | const body = req.body; 16 | 17 | // Create a mock jetton transaction 18 | const defaultBody = beginCell().storeUint(0, 32).storeStringTail("Create Jetton").endCell(); 19 | 20 | const transaction = { 21 | validUntil: Math.floor(Date.now() / 1000) + 600, 22 | messages: [ 23 | { 24 | address: 'EQCKWpx7cNMpvmcN5ObM5lLUZHZRFKqYA4xmw9jOry0ZsF9M', 25 | amount: '5000000', 26 | stateInit: 'te6cckEBBAEAOgACATQCAQAAART/APSkE/S88sgLAwBI0wHQ0wMBcbCRW+D6QDBwgBDIywVYzxYh+gLLagHPFsmAQPsAlxCarA==', 27 | payload: defaultBody.toBoc().toString('base64'), 28 | }, 29 | ], 30 | }; 31 | 32 | return res.status(200).json(transaction); 33 | } catch (e) { 34 | return res.status(400).json({ error: 'Invalid request', trace: e }); 35 | } 36 | } -------------------------------------------------------------------------------- /src/pages/api/check_sign_data.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import { CheckSignDataRequest } from '../../server/dto/check-sign-data-request-dto'; 3 | import { SignDataService } from '../../server/services/sign-data-service'; 4 | import { TonApiService } from '../../server/services/ton-api-service'; 5 | 6 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 7 | if (req.method !== 'POST') { 8 | return res.status(405).json({ error: 'Method not allowed' }); 9 | } 10 | 11 | try { 12 | const body = req.body; 13 | 14 | // Validate request body 15 | const validatedBody = CheckSignDataRequest.parse(body); 16 | 17 | const service = new SignDataService(); 18 | const client = TonApiService.create(validatedBody.network); 19 | 20 | const isValid = await service.checkSignData(validatedBody, (address) => 21 | client.getWalletPublicKey(address) 22 | ); 23 | 24 | if (!isValid) { 25 | return res.status(400).json({ error: 'Invalid signature' }); 26 | } 27 | 28 | return res.status(200).json({ 29 | valid: true, 30 | message: "Signature verified successfully", 31 | payload: validatedBody.payload, 32 | address: validatedBody.address, 33 | timestamp: validatedBody.timestamp, 34 | domain: validatedBody.domain, 35 | }); 36 | } catch (e) { 37 | return res.status(400).json({ error: "Invalid request", trace: e }); 38 | } 39 | } -------------------------------------------------------------------------------- /src/server/services/sign-data-service.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from "buffer"; 2 | import { 3 | CheckSignDataRequestDto, 4 | } from "../dto/check-sign-data-request-dto"; 5 | 6 | const allowedDomains = ["ton-connect.github.io", "localhost:3000", "localhost:3002", "localhost:5173"]; 7 | const validAuthTime = 15 * 60; // 15 minutes 8 | 9 | export class SignDataService { 10 | /** 11 | * Verifies sign-data signature. 12 | * Simplified implementation for demo purposes. 13 | */ 14 | public async checkSignData( 15 | payload: CheckSignDataRequestDto, 16 | getWalletPublicKey: (address: string) => Promise 17 | ): Promise { 18 | try { 19 | const { 20 | address, 21 | timestamp, 22 | domain, 23 | } = payload; 24 | 25 | // Check domain 26 | if (!allowedDomains.includes(domain)) { 27 | console.log("Domain not allowed:", domain); 28 | return false; 29 | } 30 | 31 | // Check timestamp 32 | const now = Math.floor(Date.now() / 1000); 33 | if (now - validAuthTime > timestamp) { 34 | console.log("Timestamp expired"); 35 | return false; 36 | } 37 | 38 | // For demo purposes, return true if basic checks pass 39 | // In a real implementation, you would verify the signature here 40 | return true; 41 | } catch (e) { 42 | console.error("Sign data verification error:", e); 43 | return false; 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/styles/globals.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | padding: 0; 4 | margin: 0; 5 | } 6 | 7 | html, 8 | body { 9 | height: 100%; 10 | max-width: 100vw; 11 | overflow-x: hidden; 12 | } 13 | 14 | body { 15 | margin: 0; 16 | background-color: rgba(16, 22, 31, 0.92); 17 | color: white; 18 | min-height: 100vh; 19 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 20 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 21 | sans-serif; 22 | -webkit-font-smoothing: antialiased; 23 | -moz-osx-font-smoothing: grayscale; 24 | } 25 | 26 | a { 27 | color: inherit; 28 | text-decoration: none; 29 | } 30 | 31 | code { 32 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 33 | monospace; 34 | } 35 | 36 | @media (prefers-color-scheme: dark) { 37 | html { 38 | color-scheme: dark; 39 | } 40 | } 41 | 42 | .app { 43 | min-height: 100%; 44 | display: flex; 45 | flex-direction: column; 46 | 47 | >header { 48 | margin-bottom: 10px; 49 | } 50 | } 51 | 52 | .react-json-view { 53 | background: rgba(24, 32, 48, 0.98) !important; 54 | border-radius: 14px; 55 | box-shadow: 0 2px 12px 0 rgba(16, 22, 31, 0.13); 56 | padding: 18px 18px 14px 18px; 57 | margin-top: 0; 58 | margin-bottom: 0; 59 | width: 100%; 60 | overflow-x: auto; 61 | font-size: 15px; 62 | word-break: break-all; 63 | white-space: pre-wrap; 64 | } 65 | 66 | .find-transaction-demo__json-view { 67 | background: rgba(24, 32, 48, 0.98); 68 | border-radius: 14px; 69 | box-shadow: 0 2px 12px 0 rgba(16, 22, 31, 0.13); 70 | padding: 18px 18px 14px 18px; 71 | margin-top: 0; 72 | margin-bottom: 0; 73 | width: 100%; 74 | overflow-x: auto; 75 | font-size: 15px; 76 | word-break: break-all; 77 | white-space: pre-wrap; 78 | } -------------------------------------------------------------------------------- /src/pages/api/check_proof.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import { sha256 } from '@ton/crypto'; 3 | import { CheckProofRequest } from '../../server/dto/check-proof-request-dto'; 4 | import { TonProofService } from '../../server/services/ton-proof-service'; 5 | import { TonApiService } from '../../server/services/ton-api-service'; 6 | import { createAuthToken, verifyToken } from '../../server/utils/jwt'; 7 | 8 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 9 | if (req.method !== 'POST') { 10 | return res.status(405).json({ error: 'Method not allowed' }); 11 | } 12 | 13 | try { 14 | const body = CheckProofRequest.parse(JSON.parse(req.body)); 15 | 16 | const client = TonApiService.create(body.network); 17 | const service = new TonProofService(); 18 | 19 | const isValid = await service.checkProof(body, (address) => client.getWalletPublicKey(address)); 20 | 21 | if (!isValid) { 22 | return res.status(400).json({ error: 'Invalid proof' }); 23 | } 24 | 25 | const payloadTokenHash = body.proof.payload; 26 | const payloadToken = body.payloadToken; 27 | 28 | if (!await verifyToken(payloadToken)) { 29 | return res.status(400).json({ error: 'Invalid token' }); 30 | } 31 | 32 | if ((await sha256(payloadToken)).toString('hex') !== payloadTokenHash) { 33 | return res.status(400).json({ error: 'Invalid payload token hash' }); 34 | } 35 | 36 | const token = await createAuthToken({ address: body.address, network: body.network }); 37 | 38 | return res.status(200).json({ token }); 39 | } catch (e) { 40 | console.log('ОШИБКА',e); 41 | return res.status(400).json({ error: 'Invalid request', trace: e }); 42 | } 43 | } -------------------------------------------------------------------------------- /src/server/dto/check-sign-data-request-dto.ts: -------------------------------------------------------------------------------- 1 | import { CHAIN } from "@tonconnect/ui-react"; 2 | import zod from "zod"; 3 | 4 | const SignDataPayloadText = zod.object({ 5 | type: zod.literal("text"), 6 | text: zod.string(), 7 | network: zod.string().optional(), 8 | from: zod.string().optional(), 9 | }); 10 | 11 | const SignDataPayloadBinary = zod.object({ 12 | type: zod.literal("binary"), 13 | bytes: zod.string(), // base64 (not url safe) encoded bytes array 14 | network: zod.string().optional(), 15 | from: zod.string().optional(), 16 | }); 17 | 18 | const SignDataPayloadCell = zod.object({ 19 | type: zod.literal("cell"), 20 | schema: zod.string(), // TL-B scheme of the cell payload 21 | cell: zod.string(), // base64 (not url safe) encoded cell 22 | network: zod.string().optional(), 23 | from: zod.string().optional(), 24 | }); 25 | 26 | const SignDataPayload = zod.union([ 27 | SignDataPayloadText, 28 | SignDataPayloadBinary, 29 | SignDataPayloadCell, 30 | ]); 31 | 32 | export const CheckSignDataRequest = zod.object({ 33 | address: zod.string(), 34 | network: zod.string().refine((val) => val === CHAIN.MAINNET || val === CHAIN.TESTNET, { 35 | message: `Network must be either ${CHAIN.MAINNET} or ${CHAIN.TESTNET}` 36 | }), 37 | public_key: zod.string(), 38 | signature: zod.string(), // base64 39 | timestamp: zod.number(), 40 | domain: zod.string(), 41 | payload: SignDataPayload, 42 | walletStateInit: zod.string(), // base64 encoded state init 43 | }); 44 | 45 | export type CheckSignDataRequestDto = zod.infer; 46 | export type SignDataPayloadText = zod.infer; 47 | export type SignDataPayloadBinary = zod.infer; 48 | export type SignDataPayloadCell = zod.infer; 49 | export type SignDataPayload = zod.infer; -------------------------------------------------------------------------------- /src/server/utils/jwt.ts: -------------------------------------------------------------------------------- 1 | import { CHAIN } from "@tonconnect/ui-react"; 2 | import { decodeJwt, JWTPayload, jwtVerify, SignJWT } from 'jose'; 3 | 4 | /** 5 | * Secret key for the token. 6 | */ 7 | const JWT_SECRET_KEY = 'your_secret_key'; 8 | 9 | /** 10 | * Payload of the token. 11 | */ 12 | export type AuthToken = { 13 | address: string; 14 | network: CHAIN; 15 | }; 16 | 17 | export type PayloadToken = { 18 | randomBytes: string; 19 | }; 20 | 21 | /** 22 | * Create a token with the given payload. 23 | */ 24 | function buildCreateToken(expirationTime: string): (payload: T) => Promise { 25 | return async (payload: T) => { 26 | const encoder = new TextEncoder(); 27 | const key = encoder.encode(JWT_SECRET_KEY); 28 | return new SignJWT(payload) 29 | .setProtectedHeader({ alg: 'HS256' }) 30 | .setIssuedAt() 31 | .setExpirationTime(expirationTime) 32 | .sign(key); 33 | }; 34 | } 35 | 36 | export const createAuthToken = buildCreateToken('1Y'); 37 | export const createPayloadToken = buildCreateToken('15m'); 38 | 39 | /** 40 | * Verify the given token. 41 | */ 42 | export async function verifyToken(token: string): Promise { 43 | const encoder = new TextEncoder(); 44 | const key = encoder.encode(JWT_SECRET_KEY); 45 | try { 46 | const { payload } = await jwtVerify(token, key); 47 | return payload; 48 | } catch (e) { 49 | return null; 50 | } 51 | } 52 | 53 | /** 54 | * Decode the given token. 55 | */ 56 | function buildDecodeToken(): (token: string) => T | null { 57 | return (token: string) => { 58 | try { 59 | return decodeJwt(token) as T; 60 | } catch (e) { 61 | return null; 62 | } 63 | }; 64 | } 65 | 66 | export const decodeAuthToken = buildDecodeToken(); 67 | export const decodePayloadToken = buildDecodeToken(); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/pages/api-reference/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 20 | 21 | [API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 22 | 23 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) instead of React pages. 24 | 25 | This project uses [`next/font`](https://nextjs.org/docs/pages/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 26 | 27 | ## Learn More 28 | 29 | To learn more about Next.js, take a look at the following resources: 30 | 31 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 32 | - [Learn Next.js](https://nextjs.org/learn-pages-router) - an interactive Next.js tutorial. 33 | 34 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 35 | 36 | ## Deploy on Vercel 37 | 38 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 39 | 40 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/pages/building-your-application/deploying) for more details. 41 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import dynamic from "next/dynamic"; 3 | import { Header } from "../components/Header/Header"; 4 | import { Footer } from "../components/Footer/Footer"; 5 | import { WalletBatchLimitsTester } from "../components/WalletBatchLimitsTester/WalletBatchLimitsTester"; 6 | 7 | // Динамические импорты для компонентов с react-json-view или localStorage 8 | const TxForm = dynamic(() => import("../components/TxForm/TxForm").then(mod => ({ default: mod.TxForm })), { ssr: false, loading: () =>
Loading transaction form...
}); 9 | const TonProofDemo = dynamic(() => import("../components/TonProofDemo/TonProofDemo").then(mod => ({ default: mod.TonProofDemo })), { ssr: false, loading: () =>
Loading TON proof demo...
}); 10 | const CreateJettonDemo = dynamic(() => import("../components/CreateJettonDemo/CreateJettonDemo").then(mod => ({ default: mod.CreateJettonDemo })), { ssr: false, loading: () =>
Loading jetton demo...
}); 11 | const SignDataTester = dynamic(() => import("../components/SignDataTester/SignDataTester").then(mod => ({ default: mod.SignDataTester })), { ssr: false, loading: () =>
Loading sign data tester...
}); 12 | const MerkleExample = dynamic(() => import("../components/MerkleExample/MerkleExample").then(mod => ({ default: mod.MerkleExample })), { ssr: false, loading: () =>
Loading merkle example...
}); 13 | const FindTransactionDemo = dynamic(() => import("../components/FindTransactionDemo/FindTransactionDemo").then(mod => ({ default: mod.FindTransactionDemo })), { ssr: false, loading: () =>
Loading find transaction demo...
}); 14 | 15 | export default function Home() { 16 | return ( 17 | <> 18 | 19 | Demo Dapp with Next.js 20 | 21 | 22 | 23 | 24 |
25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 |
35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/components/MerkleExample/MerkleExample.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | import { useTonConnectUI, useTonWallet } from "@tonconnect/ui-react"; 4 | import ReactJson from "react-json-view"; 5 | import { Cell, toNano } from "@ton/core"; 6 | import { 7 | buildSuccessMerkleProof, 8 | buildSuccessMerkleUpdate, 9 | buildVerifyMerkleProof, 10 | buildVerifyMerkleUpdate, 11 | } from "../../server/utils/exotic"; 12 | 13 | import './style.scss'; 14 | import { TonProofDemoApi } from "../../TonProofDemoApi"; 15 | 16 | const merkleExampleAddress = 'EQD_5KMZVIqzYY91-t5CdRD_V71wRrVzxDXu9n2XEwz2wwdv'; 17 | const merkleProofBody = buildVerifyMerkleProof(buildSuccessMerkleProof()); 18 | const merkleUpdateBody = buildVerifyMerkleUpdate(buildSuccessMerkleUpdate()); 19 | 20 | export const MerkleExample = () => { 21 | const [tonConnectUI] = useTonConnectUI(); 22 | const wallet = useTonWallet(); 23 | 24 | const handleMerkleProofClick = async () => { 25 | const response = await TonProofDemoApi.merkleProof(); 26 | 27 | if (!('error' in response)) { 28 | await tonConnectUI.sendTransaction(response); 29 | } 30 | }; 31 | 32 | const handleMerkleUpdateClick = async () => { 33 | const myTransaction = { 34 | validUntil: Math.floor(Date.now() / 1000) + 360, 35 | messages: [ 36 | { 37 | address: merkleExampleAddress, 38 | amount: toNano("0.05").toString(), 39 | payload: merkleUpdateBody.toBoc().toString("base64") 40 | } 41 | ] 42 | } 43 | 44 | await tonConnectUI.sendTransaction(myTransaction); 45 | } 46 | 47 | return ( 48 |
49 |

Merkle proof/update

50 | {wallet ? ( 51 |
52 | 55 | 58 |
59 | ) : ( 60 |
Connect wallet to send transaction
61 | )} 62 |
63 | ); 64 | } -------------------------------------------------------------------------------- /src/pages/api/wait_for_transaction.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import { Cell, loadMessage, TonClient, Transaction } from "@ton/ton"; 3 | import { getNormalizedExtMessageHash, retry } from '@/server/utils/transactions-utils'; 4 | import { jsonReplacer } from '@/server/utils/http-utils'; 5 | 6 | async function waitForTransaction( 7 | inMessageBoc: string, 8 | client: TonClient, 9 | retries: number = 10, 10 | timeout: number = 1000, 11 | ): Promise { 12 | const inMessage = loadMessage(Cell.fromBase64(inMessageBoc).beginParse()); 13 | 14 | if (inMessage.info.type !== 'external-in') { 15 | throw new Error(`Message must be "external-in", got ${inMessage.info.type}`); 16 | } 17 | const account = inMessage.info.dest; 18 | 19 | const targetInMessageHash = getNormalizedExtMessageHash(inMessage); 20 | 21 | let attempt = 0; 22 | while (attempt < retries) { 23 | console.log(`Waiting for transaction to appear in network. Attempt: ${attempt}`); 24 | 25 | const transactions = await retry( 26 | () => 27 | client.getTransactions(account, { 28 | limit: 10, 29 | archival: true, 30 | }), 31 | { delay: 1000, retries: 3 }, 32 | ); 33 | 34 | for (const transaction of transactions) { 35 | if (transaction.inMessage?.info.type !== 'external-in') { 36 | continue; 37 | } 38 | 39 | const inMessageHash = getNormalizedExtMessageHash(transaction.inMessage); 40 | if (inMessageHash.equals(targetInMessageHash)) { 41 | return transaction; 42 | } 43 | } 44 | 45 | await new Promise((resolve) => setTimeout(resolve, timeout)); 46 | } 47 | 48 | return undefined; 49 | } 50 | 51 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 52 | if (req.method !== 'POST') { 53 | return res.status(405).json({ error: 'Method not allowed' }); 54 | } 55 | 56 | try { 57 | const body = req.body; 58 | const network = body.network; 59 | const inMessageBoc = body.inMessageBoc; 60 | 61 | const client = new TonClient({ 62 | endpoint: `https://${network === 'testnet' ? 'testnet.' : ''}toncenter.com/api/v2/jsonRPC`, 63 | }); 64 | 65 | const transaction = await waitForTransaction(inMessageBoc, client); 66 | const transactionJson = JSON.stringify(transaction, jsonReplacer); 67 | return res.status(200).json({ transaction: JSON.parse(transactionJson) }); 68 | } catch (e) { 69 | return res.status(400).json({ 70 | error: 'Invalid request', 71 | trace: e instanceof Error ? e.message : e 72 | }); 73 | } 74 | } -------------------------------------------------------------------------------- /src/components/FindTransactionDemo/FindTransactionDemo.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import ReactJson from 'react-json-view'; 3 | 4 | import './style.scss'; 5 | import { TonProofDemoApi } from '../../TonProofDemoApi'; 6 | 7 | export const FindTransactionDemo = () => { 8 | const [boc, setBoc] = useState(''); 9 | const [network, setNetwork] = useState<'mainnet' | 'testnet'>('mainnet'); 10 | const [txLoading, setTxLoading] = useState(false); 11 | const [txError, setTxError] = useState(null); 12 | const [txResult, setTxResult] = useState(null); 13 | 14 | const handleFindTx = async () => { 15 | setTxLoading(true); 16 | setTxError(null); 17 | setTxResult(null); 18 | try { 19 | const transaction = await TonProofDemoApi.findTransactionByExternalMessage(boc, network); 20 | if (!transaction) { 21 | setTxError('Transaction not found'); 22 | } else { 23 | setTxResult(transaction); 24 | } 25 | } catch (err: any) { 26 | setTxError(err?.message || 'Unknown error'); 27 | } finally { 28 | setTxLoading(false); 29 | } 30 | }; 31 | 32 | return ( 33 |
34 |

Find Transaction by External-in Message BOC

35 |
36 |