diff --git a/package-lock.json b/package-lock.json
index e52262da..893d7817 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,9 +9,9 @@
"version": "0.0.0",
"dependencies": {
"@ton-community/assets-sdk": "0.0.5",
- "@ton/core": "^0.56.3",
- "@ton/crypto": "^3.2.0",
- "@ton/ton": "^14.0.0",
+ "@ton/core": "0.61.0",
+ "@ton/crypto": "3.3.0",
+ "@ton/ton": "15.3.1",
"@tonconnect/ui-react": "^2.2.0",
"buffer": "^6.0.3",
"crc-32": "^1.2.2",
@@ -2480,9 +2480,9 @@
}
},
"node_modules/@ton/core": {
- "version": "0.56.3",
- "resolved": "https://registry.npmjs.org/@ton/core/-/core-0.56.3.tgz",
- "integrity": "sha512-HVkalfqw8zqLLPehtq0CNhu5KjVzc7IrbDwDHPjGoOSXmnqSobiWj8a5F+YuWnZnEbQKtrnMGNOOjVw4LG37rg==",
+ "version": "0.61.0",
+ "resolved": "https://registry.npmjs.org/@ton/core/-/core-0.61.0.tgz",
+ "integrity": "sha512-0qyVfP2dDue2bq80ydXggo2MlufcmzuFk6G94qRrZxvyQ3NSe4UeBTeRf1gQmN7tywgTsX2gS61e4yvJrlUu4Q==",
"license": "MIT",
"dependencies": {
"symbol.inspect": "1.0.1"
@@ -2492,27 +2492,30 @@
}
},
"node_modules/@ton/crypto": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/@ton/crypto/-/crypto-3.2.0.tgz",
- "integrity": "sha512-50RkwReEuV2FkxSZ8ht/x9+n0ZGtwRKGsJ0ay4I/HFhkYVG/awIIBQeH0W4j8d5lADdO5h01UtX8PJ8AjiejjA==",
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/@ton/crypto/-/crypto-3.3.0.tgz",
+ "integrity": "sha512-/A6CYGgA/H36OZ9BbTaGerKtzWp50rg67ZCH2oIjV1NcrBaCK9Z343M+CxedvM7Haf3f/Ee9EhxyeTp0GKMUpA==",
+ "license": "MIT",
"dependencies": {
- "@ton/crypto-primitives": "2.0.0",
+ "@ton/crypto-primitives": "2.1.0",
"jssha": "3.2.0",
"tweetnacl": "1.0.3"
}
},
"node_modules/@ton/crypto-primitives": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/@ton/crypto-primitives/-/crypto-primitives-2.0.0.tgz",
- "integrity": "sha512-wttiNClmGbI6Dfy/8oyNnsIV0b/qYkCJz4Gn4eP62lJZzMtVQ94Ko7nikDX1EfYHkLI1xpOitWpW+8ZuG6XtDg==",
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@ton/crypto-primitives/-/crypto-primitives-2.1.0.tgz",
+ "integrity": "sha512-PQesoyPgqyI6vzYtCXw4/ZzevePc4VGcJtFwf08v10OevVJHVfW238KBdpj1kEDQkxWLeuNHEpTECNFKnP6tow==",
+ "license": "MIT",
"dependencies": {
"jssha": "3.2.0"
}
},
"node_modules/@ton/ton": {
- "version": "14.0.0",
- "resolved": "https://registry.npmjs.org/@ton/ton/-/ton-14.0.0.tgz",
- "integrity": "sha512-xb2CY6U0AlHUKc7DV7xK/K4Gqn6YoR253yUrM2E7L5WegVFsDF0CQRUIfpYACCuj1oUywQc5J2oMolYNu/uGkA==",
+ "version": "15.3.1",
+ "resolved": "https://registry.npmjs.org/@ton/ton/-/ton-15.3.1.tgz",
+ "integrity": "sha512-+UuvbE0o0VIU/0W90STO+emRIDr3Vs39LdbX5ySm/Ra+RQJSiH0KX6TDOFqWDmD2Wzk4/zw21KwSiZ6Xjk8zlw==",
+ "license": "MIT",
"dependencies": {
"axios": "^1.6.7",
"dataloader": "^2.0.0",
@@ -2521,7 +2524,7 @@
"zod": "^3.21.4"
},
"peerDependencies": {
- "@ton/core": ">=0.56.0",
+ "@ton/core": ">=0.60.0",
"@ton/crypto": ">=3.2.0"
}
},
@@ -3968,6 +3971,7 @@
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/jssha/-/jssha-3.2.0.tgz",
"integrity": "sha512-QuruyBENDWdN4tZwJbQq7/eAK85FqrI4oDbXjy5IBhYD+2pTJyBUWZe8ctWaCkrV0gy6AaelgOZZBMeswEa/6Q==",
+ "license": "BSD-3-Clause",
"engines": {
"node": "*"
}
diff --git a/package.json b/package.json
index 533879fe..4a4bc7d0 100644
--- a/package.json
+++ b/package.json
@@ -11,9 +11,9 @@
},
"dependencies": {
"@ton-community/assets-sdk": "0.0.5",
- "@ton/core": "^0.56.3",
- "@ton/crypto": "^3.2.0",
- "@ton/ton": "^14.0.0",
+ "@ton/core": "0.61.0",
+ "@ton/crypto": "3.3.0",
+ "@ton/ton": "15.3.1",
"@tonconnect/ui-react": "^2.2.0",
"buffer": "^6.0.3",
"crc-32": "^1.2.2",
diff --git a/src/App.scss b/src/App.scss
index d08e80de..2f869d9b 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -7,3 +7,17 @@
margin-bottom: 10px;
}
}
+
+.react-json-view {
+ background: rgba(24, 32, 48, 0.98) !important;
+ border-radius: 14px;
+ box-shadow: 0 2px 12px 0 rgba(16, 22, 31, 0.13);
+ padding: 18px 18px 14px 18px;
+ margin-top: 0;
+ margin-bottom: 0;
+ width: 100%;
+ overflow-x: auto;
+ font-size: 15px;
+ word-break: break-all;
+ white-space: pre-wrap;
+}
\ No newline at end of file
diff --git a/src/App.tsx b/src/App.tsx
index 6df2384a..f3064a97 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -8,6 +8,7 @@ import {CreateJettonDemo} from "./components/CreateJettonDemo/CreateJettonDemo";
import {WalletBatchLimitsTester} from "./components/WalletBatchLimitsTester/WalletBatchLimitsTester";
import {SignDataTester} from "./components/SignDataTester/SignDataTester";
import { MerkleExample } from "./components/MerkleExample/MerkleExample";
+import { FindTransactionDemo } from './components/FindTransactionDemo/FindTransactionDemo';
function App() {
return (
@@ -373,6 +374,7 @@ function App() {
+
diff --git a/src/TonProofDemoApi.ts b/src/TonProofDemoApi.ts
index 0425df69..dc803bc7 100644
--- a/src/TonProofDemoApi.ts
+++ b/src/TonProofDemoApi.ts
@@ -222,10 +222,33 @@ class TonProofDemoApiService {
}
}
+ async findTransactionByExternalMessage(boc: string, network: 'mainnet' | 'testnet') {
+ const response = await (
+ await fetch(`${this.host}/api/find_transaction_by_external_message`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ boc, network }),
+ })
+ ).json();
+ return response.transaction;
+ }
+
reset() {
this.accessToken = null;
localStorage.removeItem(this.accessTokenKey);
}
+
+ async waitForTransaction(inMessageBoc: string, network: 'mainnet' | 'testnet') {
+ const response = await (
+ await fetch(`${this.host}/api/wait_for_transaction`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ inMessageBoc, network }),
+ })
+ ).json();
+
+ return response.transaction;
+ }
}
export const TonProofDemoApi = new TonProofDemoApiService();
diff --git a/src/components/CreateJettonDemo/CreateJettonDemo.tsx b/src/components/CreateJettonDemo/CreateJettonDemo.tsx
index f6c57471..ca4bbf77 100644
--- a/src/components/CreateJettonDemo/CreateJettonDemo.tsx
+++ b/src/components/CreateJettonDemo/CreateJettonDemo.tsx
@@ -1,9 +1,9 @@
-import {useTonConnectUI, useTonWallet} from "@tonconnect/ui-react";
-import React, {useState} from 'react';
+import { useTonConnectUI, useTonWallet } from "@tonconnect/ui-react";
+import React, { useState } from 'react';
import ReactJson from 'react-json-view';
import './style.scss';
-import {CreateJettonRequestDto} from "../../server/dto/create-jetton-request-dto";
-import {TonProofDemoApi} from "../../TonProofDemoApi";
+import { CreateJettonRequestDto } from "../../server/dto/create-jetton-request-dto";
+import { TonProofDemoApi } from "../../TonProofDemoApi";
const jetton: CreateJettonRequestDto = {
name: 'Joint Photographic Experts Group',
@@ -39,7 +39,14 @@ export const CreateJettonDemo = () => {
) : (
Connect wallet to send transaction
)}
-
+ {data && Object.keys(data).length > 0 && (
+ <>
+ Response
+
+
+
+ >
+ )}
);
}
diff --git a/src/components/FindTransactionDemo/FindTransactionDemo.tsx b/src/components/FindTransactionDemo/FindTransactionDemo.tsx
new file mode 100644
index 00000000..d127c722
--- /dev/null
+++ b/src/components/FindTransactionDemo/FindTransactionDemo.tsx
@@ -0,0 +1,65 @@
+
+
+import React, { useState } from 'react';
+import ReactJson from 'react-json-view';
+
+import './style.scss';
+import { TonProofDemoApi } from '../../TonProofDemoApi';
+
+export const FindTransactionDemo = () => {
+ const [boc, setBoc] = useState('te6cckEBBQEA6wAB4YgB76ksIXpmobiUHDUtWosNdLgI+loKYwC+3DgXeRr2DJ4F4G+ja0rbyhi5yzD+xbfXI1owr5X3/uucREXZXZP4dqxPXukwqPGVrKzUL0g80tYaTgh95b0myTcmVFMS8cTIOU1NGLtDx7h4AAAQ8AAcAQJ7YgBFLU49uGmU3zOG8nNmcylqMjsoilVMAcYzYexnV5aM2BpiWgAAAAAAAAAAAAAAAAACMAAAAAEhlbGxvIYCBAEU/wD0pBP0vPLICwMASNMB0NMDAXGwkVvg+kAwcIAQyMsFWM8WIfoCy2oBzxbJgED7AAAAGE8sBQ==');
+ const [network, setNetwork] = useState<'mainnet' | 'testnet'>('mainnet');
+ const [txLoading, setTxLoading] = useState(false);
+ const [txError, setTxError] = useState(null);
+ const [txResult, setTxResult] = useState(null);
+
+ const handleFindTx = async () => {
+ setTxLoading(true);
+ setTxError(null);
+ setTxResult(null);
+ try {
+ const transaction = await TonProofDemoApi.findTransactionByExternalMessage(boc, network);
+ if (!transaction) {
+ setTxError('Transaction not found');
+ } else {
+ setTxResult(transaction);
+ }
+ } catch (err: any) {
+ setTxError(err?.message || 'Unknown error');
+ } finally {
+ setTxLoading(false);
+ }
+ };
+
+ return (
+
+
Find Transaction by External-in Message BOC
+
+ {txResult !== null && (
+ <>
+
Transaction
+
+ >
+ )}
+
+ );
+};
\ No newline at end of file
diff --git a/src/components/FindTransactionDemo/style.scss b/src/components/FindTransactionDemo/style.scss
new file mode 100644
index 00000000..c9cec568
--- /dev/null
+++ b/src/components/FindTransactionDemo/style.scss
@@ -0,0 +1,156 @@
+.find-transaction-demo {
+ display: flex;
+ width: 100%;
+ flex-direction: column;
+ gap: 20px;
+ align-items: center;
+ margin: 60px auto 0 auto;
+ padding: 28px 28px 24px 28px;
+
+ h3 {
+ color: white;
+ opacity: 0.8;
+ }
+
+ &__form {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ max-width: 600px;
+ width: 100%;
+ }
+
+ &__select-row {
+ display: flex;
+ gap: 12px;
+ width: 100%;
+ margin-bottom: 0;
+ }
+
+ &__json-label {
+ color: #b8d4f1;
+ font-size: 15px;
+ margin-bottom: 6px;
+ font-weight: 500;
+ margin-top: 18px;
+ margin-left: 2px;
+ letter-spacing: 0.01em;
+ }
+
+ >div:nth-child(3) {
+ width: 100%;
+
+ span {
+ word-break: break-word;
+ }
+ }
+
+ &__error {
+ color: rgba(102, 170, 238, 0.91);
+ font-size: 18px;
+ line-height: 20px;
+ }
+
+ button {
+ border: none;
+ padding: 7px 15px;
+ border-radius: 15px;
+ cursor: pointer;
+
+ background-color: rgba(102, 170, 238, 0.91);
+ color: white;
+ font-size: 16px;
+ line-height: 20px;
+
+ transition: transform 0.1s ease-in-out;
+
+ &:hover {
+ transform: scale(1.03);
+ }
+
+ &:active {
+ transform: scale(0.97);
+ }
+ }
+
+ input[type="text"],
+ input[type="number"],
+ input[type="search"] {
+ width: 100%;
+ padding: 10px 14px;
+ border-radius: 12px;
+ border: 1px solid rgba(102, 170, 238, 0.25);
+ background: rgba(30, 40, 60, 0.7);
+ color: #fff;
+ font-size: 16px;
+ margin-bottom: 10px;
+ outline: none;
+ transition: border-color 0.2s, box-shadow 0.2s;
+ box-shadow: 0 1px 4px 0 rgba(16, 22, 31, 0.08);
+ }
+
+ textarea {
+ width: 100%;
+ min-width: 100%;
+ max-width: 100%;
+ min-height: 100px;
+ padding: 14px 18px;
+ border-radius: 14px;
+ border: 1.5px solid rgba(102, 170, 238, 0.25);
+ background: rgba(30, 40, 60, 0.8);
+ color: #fff;
+ font-size: 16px;
+ margin-bottom: 12px;
+ outline: none;
+ transition: border-color 0.2s, box-shadow 0.2s;
+ box-shadow: 0 2px 8px 0 rgba(16, 22, 31, 0.10);
+ resize: vertical;
+ }
+
+ input[type="text"]:focus,
+ input[type="number"]:focus,
+ input[type="search"]:focus,
+ textarea:focus {
+ border-color: #66aaee;
+ box-shadow: 0 0 0 2px rgba(102, 170, 238, 0.15);
+ background: rgba(30, 40, 60, 0.92);
+ }
+
+ label {
+ color: #b8d4f1;
+ font-size: 15px;
+ margin-bottom: 6px;
+ font-weight: 500;
+ letter-spacing: 0.01em;
+ display: block;
+ margin-top: 10px;
+ margin-left: 2px;
+ }
+
+ select {
+ width: 100%;
+ padding: 10px 14px;
+ border-radius: 12px;
+ border: 1px solid rgba(102, 170, 238, 0.25);
+ background: rgba(30, 40, 60, 0.7);
+ color: #fff;
+ font-size: 16px;
+ margin-bottom: 10px;
+ outline: none;
+ transition: border-color 0.2s, box-shadow 0.2s;
+ box-shadow: 0 1px 4px 0 rgba(16, 22, 31, 0.08);
+ appearance: none;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' fill='white' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4 6l4 4 4-4' stroke='%2366aaee' stroke-width='2' fill='none' fill-rule='evenodd'/%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: right 12px center;
+ background-size: 18px 18px;
+ }
+
+ select:focus {
+ border-color: #66aaee;
+ box-shadow: 0 0 0 2px rgba(102, 170, 238, 0.15);
+ background: rgba(30, 40, 60, 0.92);
+ }
+}
\ No newline at end of file
diff --git a/src/components/SignDataTester/SignDataTester.tsx b/src/components/SignDataTester/SignDataTester.tsx
index 80b69eca..0a4296b7 100644
--- a/src/components/SignDataTester/SignDataTester.tsx
+++ b/src/components/SignDataTester/SignDataTester.tsx
@@ -19,21 +19,21 @@ export function SignDataTester() {
setSignDataRequest(null);
setSignDataResponse(null);
setVerificationResult(null);
-
+
try {
const requestPayload = {
type: 'text' as const,
text: 'I confirm this test signature request.',
};
-
+
setSignDataRequest(requestPayload);
console.log('📤 Sign Data Request (Text):', requestPayload);
-
+
const result = await tonConnectUi.signData(requestPayload);
-
+
setSignDataResponse(result);
console.log('📥 Sign Data Response (Text):', result);
-
+
// Verify the signature
if (wallet) {
const verification = await TonProofDemoApi.checkSignData(result, wallet.account);
@@ -56,7 +56,7 @@ export function SignDataTester() {
setSignDataRequest(null);
setSignDataResponse(null);
setVerificationResult(null);
-
+
try {
// Example binary data (random bytes)
const binaryData = Buffer.from('I confirm this test signature request.', 'ascii');
@@ -67,12 +67,12 @@ export function SignDataTester() {
setSignDataRequest(requestPayload);
console.log('📤 Sign Data Request (Binary):', requestPayload);
-
+
const result = await tonConnectUi.signData(requestPayload);
-
+
setSignDataResponse(result);
console.log('📥 Sign Data Response (Binary):', result);
-
+
// Verify the signature
if (wallet) {
const verification = await TonProofDemoApi.checkSignData(result, wallet.account);
@@ -95,7 +95,7 @@ export function SignDataTester() {
setSignDataRequest(null);
setSignDataResponse(null);
setVerificationResult(null);
-
+
try {
// Create a simple cell with a message
const text = "Test message in cell";
@@ -109,15 +109,15 @@ export function SignDataTester() {
schema: 'message#_ len:uint7 {len <= 127} text:(bits len * 8) = Message;',
cell: cell.toBoc().toString('base64'),
};
-
+
setSignDataRequest(requestPayload);
console.log('📤 Sign Data Request (Cell):', requestPayload);
-
+
const result = await tonConnectUi.signData(requestPayload);
-
+
setSignDataResponse(result);
console.log('📥 Sign Data Response (Cell):', result);
-
+
// Verify the signature
if (wallet) {
const verification = await TonProofDemoApi.checkSignData(result, wallet.account);
@@ -137,11 +137,11 @@ export function SignDataTester() {
return (
Sign Data Test & Verification
-
+
Test different types of data signing: text, binary, and cell formats with signature verification
-
+
{wallet ? (
@@ -162,22 +162,28 @@ export function SignDataTester() {
{signDataRequest && (
-
📤 Sign Data Request
-
+
📤 Sign Data Request
+
+
+
)}
{signDataResponse && (
-
📥 Sign Data Response
-
+
📥 Sign Data Response
+
+
+
)}
{verificationResult && (
-
✅ Verification Result
-
+
✅ Verification Result
+
+
+
)}
diff --git a/src/components/TonProofDemo/TonProofDemo.tsx b/src/components/TonProofDemo/TonProofDemo.tsx
index 4d59a871..69a44075 100644
--- a/src/components/TonProofDemo/TonProofDemo.tsx
+++ b/src/components/TonProofDemo/TonProofDemo.tsx
@@ -1,8 +1,8 @@
-import React, {useCallback, useEffect, useRef, useState} from 'react';
+import React, { useCallback, useEffect, useRef, useState } from 'react';
import ReactJson from 'react-json-view';
import './style.scss';
-import {TonProofDemoApi} from "../../TonProofDemoApi";
-import {useTonConnectUI, useTonWallet} from "@tonconnect/ui-react";
+import { TonProofDemoApi } from "../../TonProofDemoApi";
+import { useTonConnectUI, useTonWallet } from "@tonconnect/ui-react";
import useInterval from "../../hooks/useInterval";
@@ -80,7 +80,14 @@ export const TonProofDemo = () => {
) : (
Connect wallet to call API
)}
-
+ {data && Object.keys(data).length > 0 && (
+ <>
+
Response
+
+
+
+ >
+ )}
);
}
diff --git a/src/components/TxForm/TxForm.tsx b/src/components/TxForm/TxForm.tsx
index 6b943fa2..b92f490b 100644
--- a/src/components/TxForm/TxForm.tsx
+++ b/src/components/TxForm/TxForm.tsx
@@ -1,69 +1,120 @@
-import React, {useCallback, useState} from 'react';
+import React, { useCallback, useState } from 'react';
import { beginCell } from '@ton/ton';
-import ReactJson, {InteractionProps} from 'react-json-view';
+import ReactJson, { InteractionProps } from 'react-json-view';
import './style.scss';
-import {SendTransactionRequest, useTonConnectUI, useTonWallet} from "@tonconnect/ui-react";
+import { SendTransactionRequest, useTonConnectUI, useTonWallet } from "@tonconnect/ui-react";
+import { TonProofDemoApi } from '../../TonProofDemoApi';
+import { CHAIN } from '@tonconnect/ui-react';
const defaultBody = beginCell().storeUint(0, 32).storeStringTail("Hello!").endCell();
// In this example, we are using a predefined smart contract state initialization (`stateInit`)
// to interact with an "EchoContract". This contract is designed to send the value back to the sender,
// serving as a testing tool to prevent users from accidentally spending money.
+
+const defaultBody = beginCell().storeUint(0, 32).storeStringTail("Hello!").endCell();
+
const defaultTx: SendTransactionRequest = {
- // The transaction is valid for 10 minutes from now, in unix epoch seconds.
validUntil: Math.floor(Date.now() / 1000) + 600,
messages: [
-
{
- // The receiver's address.
address: 'EQCKWpx7cNMpvmcN5ObM5lLUZHZRFKqYA4xmw9jOry0ZsF9M',
- // Amount to send in nanoTON. For example, 0.005 TON is 5000000 nanoTON.
amount: '5000000',
- // (optional) State initialization in boc base64 format.
- stateInit: 'te6cckEBBAEAOgACATQCAQAAART/APSkE/S88sgLAwBI0wHQ0wMBcbCRW+D6QDBwgBDIywVYzxYh+gLLagHPFsmAQPsAlxCarA==',
- // (optional) Payload in boc base64 format.
+ // (optional) Body in boc base64 format.
payload: defaultBody.toBoc().toString('base64'),
+ // (optional) State init in boc base64 format.
+ stateInit: 'te6cckEBBAEAOgACATQCAQAAART/APSkE/S88sgLAwBI0wHQ0wMBcbCRW+D6QDBwgBDIywVYzxYh+gLLagHPFsmAQPsAlxCarA==',
},
-
- // Uncomment the following message to send two messages in one transaction.
- /*
- {
- // Note: Funds sent to this address will not be returned back to the sender.
- address: 'UQAuz15H1ZHrZ_psVrAra7HealMIVeFq0wguqlmFno1f3B-m',
- amount: toNano('0.01').toString(),
- }
- */
-
],
};
export function TxForm() {
-
const [tx, setTx] = useState(defaultTx);
+ const [waitForTx, setWaitForTx] = useState(false);
+ const [txResult, setTxResult] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [waitingTx, setWaitingTx] = useState(false);
const wallet = useTonWallet();
-
const [tonConnectUi] = useTonConnectUI();
const onChange = useCallback((value: InteractionProps) => {
setTx(value.updated_src as SendTransactionRequest)
}, []);
+ const handleSendTx = async () => {
+ setTxResult(null);
+ setLoading(true);
+ setWaitingTx(false);
+ try {
+ const transaction = await tonConnectUi.sendTransaction(tx);
+ if (waitForTx && wallet && wallet.account && transaction) {
+ setWaitingTx(true);
+ const network = wallet.account.chain === CHAIN.TESTNET ? 'testnet' : 'mainnet';
+ const txBoc = transaction.boc;
+ const result = await TonProofDemoApi.waitForTransaction(txBoc, network);
+ setTxResult(result);
+ setWaitingTx(false);
+ }
+ } finally {
+ setLoading(false);
+ }
+ }
+
return (
Configure and send transaction
-
+
+
+
+ setWaitForTx(e.target.checked)}
+ style={{ marginRight: 8 }}
+ />
+ Wait for transaction confirmation
+
+
+ {waitForTx && (
+
+ {waitingTx ? (
+ <>
+ Waiting for transaction confirmation...
+
+ >
+ ) : (
+ The transaction will be automatically found and shown below
+ )}
+
+ )}
{wallet ? (
- tonConnectUi.sendTransaction(tx)}>
- Send transaction
+
+ {loading ? 'Sending...' : 'Send transaction'}
) : (
tonConnectUi.openModal()}>
Connect wallet to send the transaction
)}
+
+ {txResult && (
+ <>
+ Transaction
+
+
+
+ >
+ )}
+
+
);
}
diff --git a/src/components/TxForm/style.scss b/src/components/TxForm/style.scss
index 95e1b42b..0463721f 100644
--- a/src/components/TxForm/style.scss
+++ b/src/components/TxForm/style.scss
@@ -42,4 +42,18 @@
transform: scale(0.97);
}
}
+
+ &__json-view {
+ background: rgba(24, 32, 48, 0.98);
+ border-radius: 14px;
+ box-shadow: 0 2px 12px 0 rgba(16, 22, 31, 0.13);
+ padding: 18px 18px 14px 18px;
+ margin-top: 0;
+ margin-bottom: 0;
+ width: 100%;
+ overflow-x: auto;
+ font-size: 15px;
+ word-break: break-all;
+ white-space: pre-wrap;
+ }
}
diff --git a/src/server/api/find-transaction-by-external-message.ts b/src/server/api/find-transaction-by-external-message.ts
new file mode 100644
index 00000000..31980bca
--- /dev/null
+++ b/src/server/api/find-transaction-by-external-message.ts
@@ -0,0 +1,92 @@
+import { HttpResponseResolver } from 'msw';
+import { TonClient } from '@ton/ton';
+import { Cell, loadMessage, Transaction } from '@ton/core';
+
+import { badRequest, notFound, ok } from '../utils/http-utils';
+import { getNormalizedExtMessageHash, retry } from '../utils/transactions-utils';
+
+
+
+async function getTransactionByInMessage(
+ inMessageBoc: string,
+ client: TonClient,
+): Promise {
+ // Step 1. Convert Base64 boc to Message if input is a string
+ const inMessage = loadMessage(Cell.fromBase64(inMessageBoc).beginParse());
+
+ // Step 2. Ensure the message is an external-in message
+ if (inMessage.info.type !== 'external-in') {
+ throw new Error(`Message must be "external-in", got ${inMessage.info.type}`);
+ }
+ const account = inMessage.info.dest;
+
+ // Step 3. Compute the normalized hash of the input message
+ const targetInMessageHash = getNormalizedExtMessageHash(inMessage);
+
+ let lt: string | undefined = undefined;
+ let hash: string | undefined = undefined;
+
+ // Step 4. Paginate through transaction history of account
+ while (true) {
+ const transactions = await retry(
+ () =>
+ client.getTransactions(account, {
+ hash,
+ lt,
+ limit: 10,
+ archival: true,
+ }),
+ { delay: 1000, retries: 3 },
+ );
+
+ if (transactions.length === 0) {
+ // No more transactions found - message may not be processed yet
+ return undefined;
+ }
+
+ // Step 5. Search for a transaction whose input message matches the normalized hash
+ for (const transaction of transactions) {
+ if (transaction.inMessage?.info.type !== 'external-in') {
+ continue;
+ }
+
+ const inMessageHash = getNormalizedExtMessageHash(transaction.inMessage);
+ if (inMessageHash.equals(targetInMessageHash)) {
+ return transaction;
+ }
+ }
+
+ const last = transactions.at(-1)!;
+ lt = last.lt.toString();
+ hash = last.hash().toString('base64');
+ }
+}
+
+/**
+ * POST /api/find_transaction_by_external_message
+ * Body: { boc: string, network: 'mainnet' | 'testnet' }
+ * Returns: { transaction: any | undefined }
+ */
+export const findTransactionByExternalMessage: HttpResponseResolver = async ({ request }) => {
+ try {
+ const body = (await request.json()) as any;
+ const boc = body.boc;
+ const network = body.network;
+ if (typeof boc !== 'string' || (network !== 'mainnet' && network !== 'testnet')) {
+ return badRequest({ error: 'Invalid request body' });
+ }
+
+ const client = new TonClient({
+ endpoint: `https://${network === 'testnet' ? 'tesnet.' : ''}toncenter.com/api/v2/jsonRPC`,
+ });
+
+ const transaction = await getTransactionByInMessage(boc, client);
+ if (!transaction) {
+ return notFound({ error: 'Transaction not found' });
+ }
+
+ return ok({ transaction: { ...transaction, hash: transaction.hash().toString('base64') } });
+ } catch (e) {
+ return badRequest({ error: 'Invalid request', trace: e instanceof Error ? e.message : e });
+ }
+};
\ No newline at end of file
diff --git a/src/server/api/merkle_proof.ts b/src/server/api/merkle_proof.ts
index f03a33db..3c93e4b6 100644
--- a/src/server/api/merkle_proof.ts
+++ b/src/server/api/merkle_proof.ts
@@ -2,7 +2,6 @@ import { beginCell, Cell, storeStateInit, toNano } from "@ton/core";
import {Address} from "@ton/ton";
import {CHAIN} from "@tonconnect/sdk";
import {HttpResponseResolver} from "msw";
-import {CreateJettonRequest} from "../dto/create-jetton-request-dto";
import {badRequest, ok, unauthorized} from "../utils/http-utils";
import {decodeAuthToken, verifyToken} from "../utils/jwt";
import { buildSuccessMerkleProof, buildVerifyMerkleProof, Exotic } from "../utils/exotic";
diff --git a/src/server/api/wait-for-transaction.ts b/src/server/api/wait-for-transaction.ts
new file mode 100644
index 00000000..b9c96728
--- /dev/null
+++ b/src/server/api/wait-for-transaction.ts
@@ -0,0 +1,68 @@
+import { Cell, loadMessage, TonClient, Transaction } from "@ton/ton";
+import { getNormalizedExtMessageHash, retry } from "../utils/transactions-utils";
+import { HttpResponseResolver } from "msw";
+import { badRequest, notFound, ok } from "../utils/http-utils";
+
+async function waitForTransaction(
+ inMessageBoc: string,
+ client: TonClient,
+ retries: number = 10,
+ timeout: number = 1000,
+): Promise {
+ const inMessage = loadMessage(Cell.fromBase64(inMessageBoc).beginParse());
+
+ if (inMessage.info.type !== 'external-in') {
+ throw new Error(`Message must be "external-in", got ${inMessage.info.type}`);
+ }
+ const account = inMessage.info.dest;
+
+ const targetInMessageHash = getNormalizedExtMessageHash(inMessage);
+
+ let attempt = 0;
+ while (attempt < retries) {
+ console.log(`Waiting for transaction to appear in network. Attempt: ${attempt}`);
+
+ const transactions = await retry(
+ () =>
+ client.getTransactions(account, {
+ limit: 10,
+ archival: true,
+ }),
+ { delay: 1000, retries: 3 },
+ );
+
+ for (const transaction of transactions) {
+ if (transaction.inMessage?.info.type !== 'external-in') {
+ continue;
+ }
+
+ const inMessageHash = getNormalizedExtMessageHash(transaction.inMessage);
+ if (inMessageHash.equals(targetInMessageHash)) {
+ return transaction;
+ }
+ }
+
+ await new Promise((resolve) => setTimeout(resolve, timeout));
+ }
+
+ return undefined;
+}
+
+export const waitForTransactionResolver: HttpResponseResolver = async ({ request }) => {
+ try {
+ const body = (await request.json()) as any;
+ const network = body.network;
+ const inMessageBoc = body.inMessageBoc;
+ const client = new TonClient({
+ endpoint: `https://${network === 'testnet' ? 'tesnet.' : ''}toncenter.com/api/v2/jsonRPC`,
+ });
+ const transaction = await waitForTransaction(inMessageBoc, client);
+ if (!transaction) {
+ return notFound({ error: 'Transaction not found' });
+ }
+
+ return ok({ transaction: { ...transaction, hash: transaction.hash().toString('base64') } });
+ } catch (e) {
+ return badRequest({ error: 'Invalid request', trace: e instanceof Error ? e.message : e });
+ }
+};
\ No newline at end of file
diff --git a/src/server/utils/exotic.ts b/src/server/utils/exotic.ts
index 3dbe9977..0928f5df 100644
--- a/src/server/utils/exotic.ts
+++ b/src/server/utils/exotic.ts
@@ -75,7 +75,7 @@ export function merkleFixture() {
export function buildSuccessMerkleProof() {
const { dict, merkleRoot, address } = merkleFixture();
- const merkleProof = dict.generateMerkleProof(address);
+ const merkleProof = dict.generateMerkleProof([address]);
return { merkleRoot, merkleProof };
}
diff --git a/src/server/utils/http-utils.ts b/src/server/utils/http-utils.ts
index 95b8b5ab..cd874d6a 100644
--- a/src/server/utils/http-utils.ts
+++ b/src/server/utils/http-utils.ts
@@ -1,10 +1,12 @@
-import {HttpResponse, JsonBodyType, StrictResponse} from "msw";
+import { HttpResponse, JsonBodyType, StrictResponse } from "msw";
+import { Address, Cell } from '@ton/core';
+
/**
* Receives a body and returns an HTTP response with the given body and status code 200.
*/
export function ok(body: T): StrictResponse {
- return HttpResponse.json(body, {status: 200, statusText: 'OK'});
+ return HttpResponse.text(JSON.stringify(body, jsonReplacer, 2), { status: 200, statusText: 'OK', headers: { 'Content-Type': 'application/json' } });
}
/**
@@ -26,3 +28,34 @@ export function unauthorized(body: T): StrictResponse(body: T): StrictResponse {
+ return HttpResponse.json(body, {
+ status: 404,
+ statusText: 'Not Found'
+ });
+}
+
+
+export function jsonReplacer(_key: string, value: unknown): unknown {
+ if (typeof value === 'bigint') {
+ return value.toString();
+ } else if (value instanceof Address) {
+ return value.toString();
+ } else if (value instanceof Cell) {
+ return value.toBoc().toString('base64');
+ } else if (value instanceof Buffer) {
+ return value.toString('base64');
+ } else if (
+ value &&
+ typeof value === 'object' &&
+ (value as any).type === 'Buffer' &&
+ Array.isArray((value as any).data)
+ ) {
+ return Buffer.from((value as any).data).toString('base64');
+ }
+
+ return value;
+}
+
diff --git a/src/server/utils/transactions-utils.ts b/src/server/utils/transactions-utils.ts
new file mode 100644
index 00000000..9a0f670e
--- /dev/null
+++ b/src/server/utils/transactions-utils.ts
@@ -0,0 +1,35 @@
+import { beginCell, storeMessage } from "@ton/core";
+/**
+ * Generates a normalized hash of an "external-in" message for comparison.
+ * Follows TEP-467.
+ */
+export function getNormalizedExtMessageHash(message: any) {
+ if (message.info.type !== 'external-in') {
+ throw new Error(`Message must be "external-in", got ${message.info.type}`);
+ }
+ const info = { ...message.info, src: undefined, importFee: 0n };
+ const normalizedMessage = {
+ ...message,
+ init: null,
+ info: info,
+ };
+ return beginCell().store(storeMessage(normalizedMessage, { forceRef: true })).endCell().hash();
+}
+
+/**
+ * Retries async fn with delay and count.
+ */
+export async function retry(fn: () => Promise, options: { retries: number; delay: number }): Promise {
+ let lastError: Error | undefined;
+ for (let i = 0; i < options.retries; i++) {
+ try {
+ return await fn();
+ } catch (e) {
+ if (e instanceof Error) {
+ lastError = e;
+ }
+ await new Promise((resolve) => setTimeout(resolve, options.delay));
+ }
+ }
+ throw lastError;
+}
\ No newline at end of file
diff --git a/src/server/worker.ts b/src/server/worker.ts
index 2c9f9d22..078ac4ca 100644
--- a/src/server/worker.ts
+++ b/src/server/worker.ts
@@ -7,6 +7,8 @@ import { generatePayload } from "./api/generate-payload";
import { getAccountInfo } from "./api/get-account-info";
import { healthz } from "./api/healthz";
import { merkleProof } from "./api/merkle_proof";
+import { findTransactionByExternalMessage } from "./api/find-transaction-by-external-message";
+import { waitForTransactionResolver } from "./api/wait-for-transaction";
const baseUrl = document.baseURI.replace(/\/$/, "");
@@ -18,4 +20,6 @@ export const worker = setupWorker(
http.get(`${baseUrl}/api/get_account_info`, getAccountInfo),
http.post(`${baseUrl}/api/create_jetton`, createJetton),
http.post(`${baseUrl}/api/merkle_proof`, merkleProof),
+ http.post(`${baseUrl}/api/find_transaction_by_external_message`, findTransactionByExternalMessage),
+ http.post(`${baseUrl}/api/wait_for_transaction`, waitForTransactionResolver),
);