diff --git a/advanced/wallets/react-wallet-v2/package.json b/advanced/wallets/react-wallet-v2/package.json index 18e8eb705..64f156e48 100644 --- a/advanced/wallets/react-wallet-v2/package.json +++ b/advanced/wallets/react-wallet-v2/package.json @@ -69,6 +69,7 @@ "borsh": "^1.0.0", "bs58": "6.0.0", "cosmos-wallet": "1.2.0", + "crc-32": "^1.2.2", "ecpair": "^2.1.0", "ed25519-hd-key": "^1.3.0", "ethers": "5.7.2", diff --git a/advanced/wallets/react-wallet-v2/pnpm-lock.yaml b/advanced/wallets/react-wallet-v2/pnpm-lock.yaml index a44e0de0d..9c86081d8 100644 --- a/advanced/wallets/react-wallet-v2/pnpm-lock.yaml +++ b/advanced/wallets/react-wallet-v2/pnpm-lock.yaml @@ -185,6 +185,9 @@ importers: cosmos-wallet: specifier: 1.2.0 version: 1.2.0 + crc-32: + specifier: ^1.2.2 + version: 1.2.2 ecpair: specifier: ^2.1.0 version: 2.1.0 @@ -2705,6 +2708,11 @@ packages: cosmos-wallet@1.2.0: resolution: {integrity: sha512-lMEpNhjN6FHU6c8l/lYi1hWU/74bOlTmo3pz0mwVpCHjNSe5u7sZCO7j0dndd3oV0tM8tj/u3eJa4NgZxG9a0Q==} + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + create-hash@1.2.0: resolution: {integrity: sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==} @@ -9269,6 +9277,8 @@ snapshots: '@cosmjs/amino': 0.25.6 '@cosmjs/proto-signing': 0.25.6 + crc-32@1.2.2: {} + create-hash@1.2.0: dependencies: cipher-base: 1.0.7 diff --git a/advanced/wallets/react-wallet-v2/src/lib/TonLib.ts b/advanced/wallets/react-wallet-v2/src/lib/TonLib.ts index 5aee2d6b1..47df4d1bc 100644 --- a/advanced/wallets/react-wallet-v2/src/lib/TonLib.ts +++ b/advanced/wallets/react-wallet-v2/src/lib/TonLib.ts @@ -4,14 +4,35 @@ import { TonClient, internal, Address, - Transaction, Cell, Message, - address, beginCell, - storeMessage + storeMessage, + storeStateInit, + loadStateInit } from '@ton/ton' import { TON_MAINNET_CHAINS, TON_TEST_CHAINS } from '@/data/TonData' +import { sha256 } from '@noble/hashes/sha2' +import { Buffer } from 'buffer' +import crc32 from 'crc-32' + +export async function retry( + fn: () => Promise, + { retries = 3, delay = 1200 }: { retries?: number; delay?: number } = {} +): Promise { + let lastError: Error | undefined + for (let i = 0; i < retries; i++) { + try { + return await fn() + } catch (e) { + if (e instanceof Error) { + lastError = e + } + await new Promise(resolve => setTimeout(resolve, delay)) + } + } + throw lastError ?? new Error('Retry attempts exhausted') +} /** * Types @@ -21,6 +42,12 @@ interface IInitArguments { seed?: string } +export class TonValidationError extends Error { + constructor(message: string) { + super(`TonValidationError: ${message}`) + } +} + /** * Library */ @@ -49,47 +76,160 @@ export default class TonLib { return new TonLib(keypair) } + public async getAddressRaw() { + return this.wallet.address.toRawString() + } + public async getAddress() { return this.wallet.address.toString({ bounceable: false }) } + public getStateInit() { + return beginCell().store(storeStateInit(this.wallet.init)).endCell().toBoc().toString('base64') + } + + public getPublicKey() { + return this.keypair.publicKey.toString('hex') + } + public getSecretKey() { return this.keypair.secretKey.toString('hex') } - public async signMessage( - params: TonLib.SignMessage['params'] - ): Promise { - const signature = sign(Buffer.from(params.message), this.keypair.secretKey) - return { - signature: signature.toString('base64'), - publicKey: this.keypair.publicKey.toString('base64') + public validateSendMessage(params: unknown) { + if (typeof params !== 'object' || params === null) { + throw new TonValidationError('Invalid params') + } + + if ('from' in params) { + if (typeof params.from !== 'string') { + throw new TonValidationError('From must be a string.') + } + let from: Address + try { + from = Address.parse(params.from) + } catch (e) { + throw new TonValidationError('Invalid from address.') + } + if (!this.wallet.address.equals(from)) { + throw new TonValidationError('From address does not match.') + } + } + + if ('valid_until' in params) { + if (typeof params.valid_until !== 'number') { + throw new TonValidationError('Valid until must be a number.') + } + + if (params.valid_until < Date.now() / 1000) { + throw new TonValidationError('Message is expired.') + } + } + + if (!('messages' in params)) { + throw new TonValidationError('Messages are absent.') + } + + if (!Array.isArray(params.messages)) { + throw new TonValidationError('Messages must be an array.') + } + + if (params.messages.length === 0) { + throw new TonValidationError('Messages are empty.') + } + + for (const message of params.messages as unknown[]) { + if (typeof message !== 'object' || message === null) { + throw new TonValidationError('Messages must be an object.') + } + + if (!('address' in message)) { + throw new TonValidationError('Address is absent.') + } + if (typeof message.address !== 'string') { + throw new TonValidationError('Address must be a string.') + } + + if (Address.isRaw(message.address)) { + throw new TonValidationError('Address is in HEX format.') + } + if (!Address.isFriendly(message.address)) { + throw new TonValidationError('Address is invalid.') + } + if (!('amount' in message)) { + throw new TonValidationError('Amount is absent.') + } + if (typeof message.amount === 'number') { + throw new TonValidationError('Amount is a number.') + } + if (typeof message.amount !== 'string') { + throw new TonValidationError('Amount is invalid.') + } + + try { + BigInt(message.amount) + } catch (e) { + throw new TonValidationError('Amount is invalid.') + } + + if ('payload' in message) { + if (typeof message.payload !== 'string') { + throw new TonValidationError('Payload is invalid.') + } + try { + Cell.fromBase64(message.payload) + } catch (e) { + throw new TonValidationError('Payload is invalid.') + } + } + + if ('stateInit' in message) { + if (typeof message.stateInit !== 'string') { + throw new TonValidationError('StateInit is invalid.') + } + + try { + Cell.fromBase64(message.stateInit) + } catch (e) { + throw new TonValidationError('StateInit is invalid.') + } + } } } - public async sendMessage( - params: TonLib.SendMessage['params'], - chainId: string - ): Promise { - const client = this.getTonClient(chainId) - const walletContract = client.open(this.wallet) - const seqno = await walletContract.getSeqno() - const messages = (params.messages || []).map(m => { + private parseTonMessages(params: TonLib.SendMessage['params']) { + this.validateSendMessage(params) + return params.messages.map(m => { const amountBigInt = typeof m.amount === 'string' ? BigInt(m.amount) : BigInt(m.amount) return internal({ to: Address.parse(m.address), + bounce: Address.parseFriendly(m.address).isBounceable, value: amountBigInt, - body: m.payload ?? 'Test transfer from ton WalletConnect' + body: m.payload ? Cell.fromBase64(m.payload) : 'Test transfer from ton WalletConnect', + init: m.stateInit ? loadStateInit(Cell.fromBase64(m.stateInit).beginParse()) : undefined }) }) + } + + public async sendMessage( + params: TonLib.SendMessage['params'], + chainId: string + ): Promise { + const client = this.getTonClient(chainId) + const walletContract = client.open(this.wallet) + const seqno = await retry(() => walletContract.getSeqno()) + + + const messages = this.parseTonMessages(params) const transfer = walletContract.createTransfer({ seqno, secretKey: this.keypair.secretKey, - messages + messages, + timeout: params.valid_until }) - await walletContract.send(transfer) + await retry(() => walletContract.send(transfer)) // Build external-in message for the result const message: Message = { @@ -110,23 +250,199 @@ export default class TonLib { return externalMessageCell.toBoc().toString('base64') } - public async signData(params: TonLib.SignData['params']): Promise { + private readonly tonProofPrefix = 'ton-proof-item-v2/' + private readonly tonConnectPrefix = 'ton-connect' + + private createTonProofMessageBytes(message: { + address: Address + timestamp: number + domain: { + lengthBytes: number + value: string + } + payload: string + }): Buffer { + const innerMessage = { + workchain: message.address.workChain, + address: message.address.hash, + domain: message.domain, + payload: message.payload, + timestamp: message.timestamp + } + + const wc = Buffer.alloc(4) + wc.writeUInt32BE(message.address.workChain) + + const ts = Buffer.alloc(8) + ts.writeBigUInt64LE(BigInt(message.timestamp)) + + const dl = Buffer.alloc(4) + dl.writeUInt32LE(message.domain.lengthBytes) + + const m = Buffer.concat([ + Buffer.from(this.tonProofPrefix), + wc, + message.address.hash, + dl, + Buffer.from(message.domain.value), + ts, + Buffer.from(message.payload) + ]) + + const messageHash = sha256(m) + + const fullMes = Buffer.concat([ + Buffer.from([0xff, 0xff]), + Buffer.from(this.tonConnectPrefix), + messageHash + ]) + const res = sha256(fullMes) + + return Buffer.from(res) + } + + public async generateTonProof( + params: TonLib.TonProof['params'] + ): Promise { + const domain = params.domain + + const dataToSign = this.createTonProofMessageBytes({ + address: this.wallet.address, + domain: { + lengthBytes: domain.length, + value: params.domain + }, + payload: params.payload, + timestamp: Math.floor(new Date(params.iat).getTime() / 1000) + }) + + const signature = sign(dataToSign, this.keypair.secretKey) + + return { + signature: signature.toString('base64'), + publicKey: this.getPublicKey() + } + } + + /** + * Creates hash for Cell payload according to TON Connect specification. + */ + /** + * Creates hash for text or binary payload. + * Message format: + * message = 0xffff || "ton-connect/sign-data/" || workchain || address_hash || domain_len || domain || timestamp || payload + */ + private createTextBinaryHash( + payload: TonLib.SignData['params'] & { type: 'text' | 'binary' }, + parsedAddr: Address, + domain: string, + timestamp: number + ): Buffer { + // Create workchain buffer + const wcBuffer = Buffer.alloc(4) + wcBuffer.writeInt32BE(parsedAddr.workChain) + + // Create domain buffer + const domainBuffer = Buffer.from(domain, 'utf8') + const domainLenBuffer = Buffer.alloc(4) + domainLenBuffer.writeUInt32BE(domainBuffer.length) + + // Create timestamp buffer + const tsBuffer = Buffer.alloc(8) + tsBuffer.writeBigUInt64BE(BigInt(timestamp)) + + // Create payload buffer + const typePrefix = payload.type === 'text' ? 'txt' : 'bin' + const content = payload.type === 'text' ? payload.text : payload.bytes + const encoding = payload.type === 'text' ? 'utf8' : 'base64' + + const payloadPrefix = Buffer.from(typePrefix) + const payloadBuffer = Buffer.from(content, encoding) + const payloadLenBuffer = Buffer.alloc(4) + payloadLenBuffer.writeUInt32BE(payloadBuffer.length) + + // Build message + const message = Buffer.concat([ + Buffer.from([0xff, 0xff]), + Buffer.from('ton-connect/sign-data/'), + wcBuffer, + parsedAddr.hash, + domainLenBuffer, + domainBuffer, + tsBuffer, + payloadPrefix, + payloadLenBuffer, + payloadBuffer + ]) + + // Hash message with sha256 + const hash = sha256(message) + return Buffer.from(hash) + } + + /** + * Creates hash for Cell payload according to TON Connect specification. + */ + private createCellHash( + payload: TonLib.SignData['params'] & { type: 'cell' }, + parsedAddr: Address, + domain: string, + timestamp: number + ): Buffer { + const cell = Cell.fromBase64(payload.cell) + const schemaHash = crc32.buf(Buffer.from(payload.schema, 'utf8')) >>> 0 // unsigned crc32 hash + + // Encode domain in DNS-like format (e.g. "example.com" -> "com\0example\0") + const encodedDomain = this.encodeDomainDnsLike(domain) + + const message = beginCell() + .storeUint(0x75569022, 32) // prefix + .storeUint(schemaHash, 32) // schema hash + .storeUint(timestamp, 64) // timestamp + .storeAddress(parsedAddr) // user wallet address + .storeStringRefTail(encodedDomain.toString('utf8')) // app domain (DNS-like encoded, snake stored) + .storeRef(cell) // payload cell + .endCell() + + return Buffer.from(message.hash()) + } + + private encodeDomainDnsLike(domain: string): Buffer { + const parts = domain.split('.').reverse() // reverse for DNS-like encoding + const encoded: number[] = [] + + for (const part of parts) { + // Add the part characters + for (let i = 0; i < part.length; i++) { + encoded.push(part.charCodeAt(i)) + } + encoded.push(0) // null byte after each part + } + + return Buffer.from(encoded) + } + + public async signData( + params: TonLib.SignData['params'], + domain: string, + chainId: string + ): Promise { const payload: TonLib.SignData['params'] = params - const dataToSign = this.getToSign(params) - const signature = sign(dataToSign, this.keypair.secretKey as unknown as Buffer) - const addressStr = await this.getAddress() + const timestamp = Math.floor(Date.now() / 1000) + + const dataToSign = this.getToSign(params, this.wallet.address, domain, timestamp) + + const signature = sign(dataToSign, this.keypair.secretKey) + const addressStr = await this.getAddressRaw() const result = { signature: signature.toString('base64'), address: addressStr, - publicKey: this.keypair.publicKey.toString('base64'), - timestamp: Math.floor(Date.now() / 1000), - domain: - typeof window !== 'undefined' && window.location && window.location.hostname - ? window.location.hostname - : 'unknown', - payload + publicKey: this.getPublicKey(), + timestamp, + domain, + payload: { ...payload, network: chainId.split(":")[1] } } try { @@ -156,13 +472,16 @@ export default class TonLib { }) } - private getToSign(params: TonLib.SignData['params']): Buffer { - if (params.type === 'text') { - return Buffer.from(params.text) - } else if (params.type === 'binary') { - return Buffer.from(params.bytes) + private getToSign( + params: TonLib.SignData['params'], + address: Address, + domain: string, + timestamp: number + ): Buffer { + if (params.type === 'text' || params.type === 'binary') { + return this.createTextBinaryHash(params, address, domain, timestamp) } else if (params.type === 'cell') { - return Buffer.from(params.cell) + return this.createCellHash(params, address, domain, timestamp) } else { throw new Error('Unsupported sign data type') } @@ -175,9 +494,12 @@ export namespace TonLib { result: Result } - export type SignMessage = RPCRequest< - { message: string }, - { signature: string; publicKey: string } + export type TonProof = RPCRequest< + { iat: string; domain: string; payload: string }, + { + signature: string + publicKey: string + } > export type SendMessage = RPCRequest< @@ -186,7 +508,7 @@ export namespace TonLib { from?: string messages: Array<{ address: string - amount: number | string + amount: string payload?: string stateInit?: string extra_currency?: Record diff --git a/advanced/wallets/react-wallet-v2/src/utils/AuthUtil.ts b/advanced/wallets/react-wallet-v2/src/utils/AuthUtil.ts index 35c820e6d..cd30c841f 100644 --- a/advanced/wallets/react-wallet-v2/src/utils/AuthUtil.ts +++ b/advanced/wallets/react-wallet-v2/src/utils/AuthUtil.ts @@ -26,6 +26,9 @@ import bs58 from 'bs58' const didPrefix = 'did:pkh:' type AuthMessage = { + iat: string; + statement?: string; + domain: string; message: string chainId: string address: string @@ -97,6 +100,9 @@ export async function signAuthenticationMessages( const signedAuths = [] for (const toSign of authenticationMessagesToSign) { const result = await signMessage({ + iat: toSign.iat, + statement: toSign.statement, + domain: toSign.domain, chainId: `${getDidAddressNamespace(toSign.iss)!}:${getDidChainId(toSign.iss)!}`, address: getDidAddress(toSign.iss)!, message: toSign.message @@ -107,7 +113,7 @@ export async function signAuthenticationMessages( { t: result.type as any, s: result.signature, - m: result?.publicKey + m: result.publicKey }, toSign.iss ) @@ -125,11 +131,14 @@ export async function signMessage(AuthMessage: AuthMessage) { const eip155Result = await eip155Wallets[AuthMessage.address].signMessage(AuthMessage.message) return { signature: eip155Result, type: getSignatureType(parsed.namespace) } case 'ton': - const tonResult = await tonWallets[AuthMessage.address].signData({ - text: AuthMessage.message, - type: 'text' - }) - return { signature: tonResult.signature, publicKey: tonResult.publicKey, type: 'ton' } + if (AuthMessage.statement) { + const tonResult = await tonWallets[AuthMessage.address].generateTonProof({ + iat: AuthMessage.iat, + domain: AuthMessage.domain, + payload: AuthMessage.statement, + }) + return { signature: tonResult.signature, publicKey: tonResult.publicKey, type: 'ton' } + } case 'solana': const solanaResult = await solanaWallets[AuthMessage.address].signMessage({ message: bs58.encode(new Uint8Array(Buffer.from(AuthMessage.message))) diff --git a/advanced/wallets/react-wallet-v2/src/utils/TonRequestHandlerUtil.ts b/advanced/wallets/react-wallet-v2/src/utils/TonRequestHandlerUtil.ts index 8301b6065..2f8b172a4 100644 --- a/advanced/wallets/react-wallet-v2/src/utils/TonRequestHandlerUtil.ts +++ b/advanced/wallets/react-wallet-v2/src/utils/TonRequestHandlerUtil.ts @@ -1,14 +1,37 @@ import { getWallet } from '@/utils/TonWalletUtil' -import { getSignParamsMessage } from '@/utils/HelperUtil' import { formatJsonRpcError, formatJsonRpcResult } from '@json-rpc-tools/utils' -import { SignClientTypes } from '@walletconnect/types' +import { SessionTypes, SignClientTypes } from '@walletconnect/types' import { getSdkError } from '@walletconnect/utils' import SettingsStore from '@/store/SettingsStore' import { TON_SIGNING_METHODS } from '@/data/TonData' type RequestEventArgs = Omit -export async function approveTonRequest(requestEvent: RequestEventArgs) { +export async function validateTonRequest(requestEvent: RequestEventArgs) { + const { params, id } = requestEvent + const { request } = params + + const payload = Array.isArray(request.params) ? request.params[0] : request.params || {} + + const wallet = await getWallet() + + try { + switch (request.method) { + case TON_SIGNING_METHODS.SIGN_DATA: + break + case TON_SIGNING_METHODS.SEND_MESSAGE: + wallet.validateSendMessage(payload) + } + } catch (error: any) { + console.error(error) + return formatJsonRpcError(id, error.message) + } +} + +export async function approveTonRequest( + requestEvent: RequestEventArgs, + session: SessionTypes.Struct +) { const { params, id } = requestEvent const { chainId, request } = params @@ -20,7 +43,8 @@ export async function approveTonRequest(requestEvent: RequestEventArgs) { case TON_SIGNING_METHODS.SIGN_DATA: try { const payload = Array.isArray(request.params) ? request.params[0] : request.params - const result = await wallet.signData(payload) + const domain = new URL(session.peer.metadata.url).hostname + const result = await wallet.signData(payload, domain, chainId) return formatJsonRpcResult(id, result) } catch (error: any) { console.error(error) diff --git a/advanced/wallets/react-wallet-v2/src/views/SessionProposalModal.tsx b/advanced/wallets/react-wallet-v2/src/views/SessionProposalModal.tsx index 8d76b04d2..b5355086f 100644 --- a/advanced/wallets/react-wallet-v2/src/views/SessionProposalModal.tsx +++ b/advanced/wallets/react-wallet-v2/src/views/SessionProposalModal.tsx @@ -61,7 +61,7 @@ import { stacksAddresses, stacksWallet } from '@/utils/StacksWalletUtil' import { getWallet as getSuiWallet } from '@/utils/SuiWalletUtil' import StacksLib from '@/lib/StacksLib' import { TON_CHAINS, TON_SIGNING_METHODS } from '@/data/TonData' -import { tonAddresses } from '@/utils/TonWalletUtil' +import { getWallet, tonAddresses, tonWallets } from '@/utils/TonWalletUtil' import { prepareAuthenticationMessages, signAuthenticationMessages } from '@/utils/AuthUtil' import { AuthenticationMessage } from '@/types/auth' @@ -434,6 +434,12 @@ export default function SessionProposalModal() { ]) } + if (namespaces.ton) { + const tonWallet = await getWallet(); + sessionProperties.ton_getPublicKey = tonWallet.getPublicKey(); + sessionProperties.ton_getStateInit = tonWallet.getStateInit(); + } + console.log('sessionProperties', sessionProperties) const signedAuths = await signAuthenticationMessages(authenticationMessagesToSign) diff --git a/advanced/wallets/react-wallet-v2/src/views/SessionSignTonPersonalMessageModal.tsx b/advanced/wallets/react-wallet-v2/src/views/SessionSignTonPersonalMessageModal.tsx index d93816f03..169e2ef4f 100644 --- a/advanced/wallets/react-wallet-v2/src/views/SessionSignTonPersonalMessageModal.tsx +++ b/advanced/wallets/react-wallet-v2/src/views/SessionSignTonPersonalMessageModal.tsx @@ -1,6 +1,6 @@ /* eslint-disable react-hooks/rules-of-hooks */ import { Col, Divider, Row, Text } from '@nextui-org/react' -import { useCallback, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import RequesDetailsCard from '@/components/RequestDetalilsCard' import ModalStore from '@/store/ModalStore' @@ -8,7 +8,11 @@ import { styledToast } from '@/utils/HelperUtil' import { walletkit } from '@/utils/WalletConnectUtil' import RequestModal from '../components/RequestModal' import { tonAddresses } from '@/utils/TonWalletUtil' -import { approveTonRequest, rejectTonRequest } from '@/utils/TonRequestHandlerUtil' +import { + approveTonRequest, + rejectTonRequest, + validateTonRequest +} from '@/utils/TonRequestHandlerUtil' export default function SessionTonSignDataModal() { // Get request and wallet data from store @@ -28,12 +32,30 @@ export default function SessionTonSignDataModal() { const payload = Array.isArray(request.params) ? request.params[0] : request.params || {} + useEffect(() => { + if (!requestEvent) { + return + } + const effect = async () => { + const validationResult = await validateTonRequest(requestEvent) + if (validationResult) { + styledToast(validationResult.error.message, 'error') + await walletkit.respondSessionRequest({ + topic, + response: validationResult + }) + ModalStore.close() + } + } + void effect() + }, [requestEvent, topic]) + // Handle approve action (logic varies based on request method) const onApprove = useCallback(async () => { try { if (requestEvent) { setIsLoadingApprove(true) - const response = await approveTonRequest(requestEvent) + const response = await approveTonRequest(requestEvent, requestSession) await walletkit.respondSessionRequest({ topic, response diff --git a/advanced/wallets/react-wallet-v2/src/views/SessionSignTonTransactionModal.tsx b/advanced/wallets/react-wallet-v2/src/views/SessionSignTonTransactionModal.tsx index c2f8376d9..1c5bc0296 100644 --- a/advanced/wallets/react-wallet-v2/src/views/SessionSignTonTransactionModal.tsx +++ b/advanced/wallets/react-wallet-v2/src/views/SessionSignTonTransactionModal.tsx @@ -1,6 +1,6 @@ /* eslint-disable react-hooks/rules-of-hooks */ import { Col, Divider, Row, Text } from '@nextui-org/react' -import { useCallback, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import RequesDetailsCard from '@/components/RequestDetalilsCard' import ModalStore from '@/store/ModalStore' @@ -8,7 +8,11 @@ import { styledToast } from '@/utils/HelperUtil' import { walletkit } from '@/utils/WalletConnectUtil' import RequestModal from '../components/RequestModal' import { tonAddresses } from '@/utils/TonWalletUtil' -import { approveTonRequest, rejectTonRequest } from '@/utils/TonRequestHandlerUtil' +import { + approveTonRequest, + rejectTonRequest, + validateTonRequest +} from '@/utils/TonRequestHandlerUtil' export default function SessionTonSendMessageModal() { // Get request and wallet data from store @@ -30,12 +34,30 @@ export default function SessionTonSendMessageModal() { const tx = Array.isArray(request.params) ? request.params[0] : request.params || {} const messages = Array.isArray(tx.messages) ? tx.messages : [] + useEffect(() => { + if (!request.params) { + return + } + const effect = async () => { + const validationResult = await validateTonRequest(requestEvent) + if (validationResult) { + styledToast(validationResult.error.message, 'error') + await walletkit.respondSessionRequest({ + topic, + response: validationResult + }) + ModalStore.close() + } + } + void effect() + }, [requestEvent, topic]) + // Handle approve action (logic varies based on request method) const onApprove = useCallback(async () => { try { if (requestEvent) { setIsLoadingApprove(true) - const response = await approveTonRequest(requestEvent) + const response = await approveTonRequest(requestEvent, requestSession) await walletkit.respondSessionRequest({ topic, response