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 |
--------------------------------------------------------------------------------